mirror of https://github.com/veypi/OneAuth.git
update ui
parent
a74ccb104f
commit
271131a5e1
@ -1,10 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<body>
|
||||
</body>
|
||||
<script setup>
|
||||
id = $router.params.id
|
||||
</script>
|
||||
|
||||
</html>
|
||||
@ -1,237 +1,113 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>404 Galaxy Lost</title>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="description" content="404 Page Not Found" details="页面未找到">
|
||||
<title>404 - Galaxy Lost</title>
|
||||
</head>
|
||||
<style>
|
||||
body {
|
||||
--primary-color: #ff6f61;
|
||||
--nebula-color: rgba(195, 207, 226, 0.1);
|
||||
body {
|
||||
--primary-color: var(--color-primary);
|
||||
--bg-gradient-start: color-mix(in srgb, var(--bg-color), #000 80%);
|
||||
--bg-gradient-end: color-mix(in srgb, var(--bg-color), #000 60%);
|
||||
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
background: linear-gradient(135deg, #0b0b2a, #1a1a4a);
|
||||
font-family: 'Arial', sans-serif;
|
||||
background: linear-gradient(135deg, var(--bg-gradient-start), var(--bg-gradient-end));
|
||||
font-family: var(--font-family);
|
||||
overflow: hidden;
|
||||
cursor: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><circle cx="12" cy="12" r="8" fill="%23fff" opacity="0.5"/></svg>') 12 12, auto;
|
||||
}
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.container {
|
||||
.container {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
text-align: center;
|
||||
color: white;
|
||||
perspective: 1000px;
|
||||
}
|
||||
}
|
||||
|
||||
.error-code {
|
||||
.error-code {
|
||||
font-size: 20rem;
|
||||
font-weight: 900;
|
||||
margin: 0;
|
||||
text-shadow: 0 0 30px var(--primary-color);
|
||||
color: #fff;
|
||||
transform-style: preserve-3d;
|
||||
animation: float 4s ease-in-out infinite;
|
||||
}
|
||||
}
|
||||
|
||||
.message {
|
||||
.message {
|
||||
font-size: 2rem;
|
||||
margin: 2rem 0;
|
||||
opacity: 0.8;
|
||||
transform: translateZ(50px);
|
||||
animation: textGlow 2s alternate infinite;
|
||||
}
|
||||
|
||||
.button {
|
||||
position: relative;
|
||||
padding: 1rem 3rem;
|
||||
font-size: 1.2rem;
|
||||
background: transparent;
|
||||
border: 2px solid var(--primary-color);
|
||||
border-radius: 50px;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
transition: 0.3s all ease;
|
||||
}
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
background: var(--primary-color);
|
||||
box-shadow: 0 0 30px var(--primary-color);
|
||||
transform: translateY(-3px);
|
||||
}
|
||||
|
||||
/* 新增太空元素样式 */
|
||||
.planet {
|
||||
.planet {
|
||||
position: absolute;
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(45deg, #3a1c71, #d76d77, #ffaf7b);
|
||||
background: linear-gradient(45deg, color-mix(in srgb, var(--primary-color), transparent 20%), color-mix(in srgb, var(--primary-color), #000 40%), #ffaf7b);
|
||||
filter: drop-shadow(0 0 50px rgba(255, 175, 123, 0.5));
|
||||
animation: rotate 30s linear infinite;
|
||||
left: 20%;
|
||||
top: 30%;
|
||||
}
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.meteor {
|
||||
.meteor {
|
||||
position: absolute;
|
||||
width: 2px;
|
||||
height: 30px;
|
||||
background: linear-gradient(to bottom, white, var(--primary-color));
|
||||
animation: meteorFall 3s linear infinite;
|
||||
}
|
||||
|
||||
/* 关键帧动画 */
|
||||
@keyframes float {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
transform: translateY(0) rotateX(10deg) rotateY(10deg);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translateY(-20px) rotateX(-10deg) rotateY(-10deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes meteorFall {
|
||||
from {
|
||||
transform: translate(-100vw, -100vh) rotate(-45deg);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translate(100vw, 100vh) rotate(-45deg);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes textGlow {
|
||||
from {
|
||||
text-shadow: 0 0 10px white;
|
||||
}
|
||||
|
||||
to {
|
||||
text-shadow: 0 0 30px var(--primary-color);
|
||||
}
|
||||
}
|
||||
|
||||
.particle {
|
||||
position: absolute;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
background: white;
|
||||
animation: particleMove 2s linear;
|
||||
}
|
||||
|
||||
@keyframes particleMove {
|
||||
from {
|
||||
transform: translate(-50%, -50%) scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translate(-50%, -50%) scale(0);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% { transform: translateY(0) rotateX(10deg) rotateY(10deg); }
|
||||
50% { transform: translateY(-20px) rotateX(-10deg) rotateY(-10deg); }
|
||||
}
|
||||
|
||||
@keyframes textGlow {
|
||||
from { text-shadow: 0 0 10px rgba(255, 255, 255, 0.5); }
|
||||
to { text-shadow: 0 0 20px rgba(255, 255, 255, 0.8), 0 0 30px var(--primary-color); }
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
@keyframes meteorFall {
|
||||
0% { transform: translateY(-100vh) translateX(100vw) rotate(45deg); opacity: 1; }
|
||||
100% { transform: translateY(100vh) translateX(-100vw) rotate(45deg); opacity: 0; }
|
||||
}
|
||||
</style>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1 class="error-code">404</h1>
|
||||
<p class="message">Warp Drive Malfunction!</p>
|
||||
<button class="button" @click="goHome" @mousemove="createRipple">
|
||||
Beam Me Home
|
||||
</button>
|
||||
|
||||
<!-- 新增太空元素 -->
|
||||
<div class="planet"></div>
|
||||
<div v-for="(meteor, index) in meteors" :key="'meteor-'+index" class="meteor" :style="meteor.style"></div>
|
||||
</div>
|
||||
|
||||
<!-- 新增交互粒子 -->
|
||||
<div class="particles">
|
||||
<!-- <div v-for="(particle, index) in particles" :key="'particle-'+index" class="particle" :style="particle.style"></div> -->
|
||||
</div>
|
||||
<!-- Generate some meteors -->
|
||||
<div class="meteor" style="left: 10%; animation-delay: 0s;"></div>
|
||||
<div class="meteor" style="left: 30%; animation-delay: 2s;"></div>
|
||||
<div class="meteor" style="left: 60%; animation-delay: 1s;"></div>
|
||||
<div class="meteor" style="left: 80%; animation-delay: 3s;"></div>
|
||||
|
||||
<div class="container">
|
||||
<h1 class="error-code">404</h1>
|
||||
<div class="message">Oops! You seem to be lost in space.</div>
|
||||
<v-btn :click="goHome" size="lg" variant="primary" style="font-size: 1.2rem; padding: 0.8rem 3rem; border-radius: 50px;">Go Home</v-btn>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
<script setup>
|
||||
// 响应式数据
|
||||
meteors = Array.from({length: 5}).map(() => ({
|
||||
style: {
|
||||
top: `${Math.random() * 100}vh`,
|
||||
left: `${Math.random() * 100}vw`,
|
||||
animationDelay: `${Math.random() * 3}s`
|
||||
}
|
||||
}));
|
||||
|
||||
particles = [];
|
||||
maxParticles = 150;
|
||||
|
||||
// 返回首页方法
|
||||
goHome = () => {
|
||||
$data.particles = [];
|
||||
window.location.href = '/';
|
||||
};
|
||||
|
||||
// 创建粒子效果
|
||||
createRipple = (e) => {
|
||||
if ($data.particles.length < $data.maxParticles) {
|
||||
$data.particles = [...$data.particles, {
|
||||
style: {
|
||||
left: `${e.clientX}px`,
|
||||
top: `${e.clientY}px`,
|
||||
background: `hsl(${Math.random() * 360}, 70%, 50%)`
|
||||
}
|
||||
}];
|
||||
}
|
||||
};
|
||||
goHome = () => {
|
||||
$router.push('/');
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
// 鼠标跟随效果
|
||||
let mouseX = 0;
|
||||
let mouseY = 0;
|
||||
|
||||
document.addEventListener('mousemove', (e) => {
|
||||
mouseX = e.clientX;
|
||||
mouseY = e.clientY;
|
||||
|
||||
// 创建轨迹粒子
|
||||
if ($data.particles.length < $data.maxParticles) {
|
||||
$data.particles.push({
|
||||
style: {
|
||||
left: `${mouseX}px`,
|
||||
top: `${mouseY}px`,
|
||||
background: `rgba(255,255,255,1)`,
|
||||
// transform: `scale(${Math.random()})`
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 自动清理旧粒子
|
||||
setInterval(() => {
|
||||
let l = $data.particles.length;
|
||||
if (l > 30) {
|
||||
l = 30
|
||||
}
|
||||
if (l > 0) {
|
||||
$data.particles = $data.particles.slice(l);
|
||||
}
|
||||
}, 300);
|
||||
</script>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
@ -1,393 +1,191 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>应用</title>
|
||||
|
||||
<style>
|
||||
body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
header {
|
||||
display: flex;
|
||||
justify-content: start;
|
||||
align-items: center;
|
||||
margin-bottom: 30px;
|
||||
flex-wrap: wrap;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-primary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
position: relative;
|
||||
margin-left: auto;
|
||||
max-width: 400px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.search-bar input {
|
||||
width: 100%;
|
||||
padding: 12px 20px;
|
||||
padding-right: 40px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 30px;
|
||||
font-size: 16px;
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.search-bar input:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: 0 0 0 3px rgba(74, 108, 247, 0.2);
|
||||
}
|
||||
|
||||
.search-bar i {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 15px;
|
||||
transform: translateY(-50%);
|
||||
color: var(--secondary-color);
|
||||
}
|
||||
|
||||
.filters {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 25px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.filter-btn {
|
||||
padding: 8px 16px;
|
||||
background-color: var(--card-background);
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 20px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.filter-btn:hover {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
.filter-btn.active {
|
||||
background-color: var(--color-primary);
|
||||
color: white;
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.app-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 25px;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.app-card {
|
||||
background-color: var(--card-background);
|
||||
border-radius: var(--border-radius);
|
||||
overflow: hidden;
|
||||
box-shadow: var(--box-shadow);
|
||||
transition: var(--transition);
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.app-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 6px 25px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.app-card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.app-icon {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
background-color: #f0f0f0;
|
||||
border-radius: 12px;
|
||||
margin-right: 15px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.app-icon img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.app-title h2 {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.app-status {
|
||||
font-size: 12px;
|
||||
border-radius: 20px;
|
||||
padding: 3px 10px;
|
||||
display: inline-block;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.status-active {
|
||||
background-color: rgba(40, 167, 69, 0.1);
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
.status-pending {
|
||||
background-color: rgba(255, 193, 7, 0.1);
|
||||
color: var(--warning-color);
|
||||
}
|
||||
|
||||
.status-inactive {
|
||||
background-color: rgba(220, 53, 69, 0.1);
|
||||
color: var(--danger-color);
|
||||
}
|
||||
|
||||
.app-card-body {
|
||||
padding: 20px;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.app-description {
|
||||
color: var(--secondary-color);
|
||||
margin-bottom: 15px;
|
||||
font-size: 14px;
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
.app-meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 13px;
|
||||
color: var(--secondary-color);
|
||||
}
|
||||
|
||||
.app-card-footer {
|
||||
padding: 15px 20px;
|
||||
background-color: #f9f9f9;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
|
||||
.app-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 8px 16px;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: var(--transition);
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: var(--color-primary);
|
||||
color: var(--color-text);
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: #3955d1;
|
||||
}
|
||||
|
||||
.btn-outline {
|
||||
background-color: transparent;
|
||||
color: var(--color-primary);
|
||||
border: 1px solid var(--color-primary);
|
||||
}
|
||||
|
||||
.btn-outline:hover {
|
||||
background-color: rgba(74, 108, 247, 0.1);
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 50px;
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 4px solid rgba(74, 108, 247, 0.1);
|
||||
border-left-color: var(--color-primary);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.no-apps {
|
||||
grid-column: 1 / -1;
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
color: var(--secondary-color);
|
||||
font-size: 18px;
|
||||
}
|
||||
</style>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="description" content="App List Page">
|
||||
<title>应用列表</title>
|
||||
</head>
|
||||
<style>
|
||||
body {
|
||||
padding: var(--spacing-lg);
|
||||
background-color: var(--bg-color-primary);
|
||||
color: var(--text-color-primary);
|
||||
}
|
||||
header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--spacing-xl);
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
h1 {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
.filters {
|
||||
display: flex;
|
||||
gap: var(--spacing-sm);
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
.app-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: var(--spacing-lg);
|
||||
}
|
||||
.app-card {
|
||||
background-color: var(--bg-color-secondary);
|
||||
border-radius: var(--radius-md);
|
||||
box-shadow: var(--shadow-sm);
|
||||
transition: var(--transition-base);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
.app-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
.app-card-header {
|
||||
padding: var(--spacing-md);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
.app-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: var(--radius-sm);
|
||||
margin-right: var(--spacing-md);
|
||||
object-fit: cover;
|
||||
background-color: var(--bg-color-tertiary);
|
||||
}
|
||||
.app-info {
|
||||
flex: 1;
|
||||
}
|
||||
.app-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: var(--spacing-xs);
|
||||
color: var(--text-color-primary);
|
||||
}
|
||||
.app-status {
|
||||
font-size: 0.8rem;
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
.status-active { background-color: color-mix(in srgb, var(--color-success), transparent 90%); color: var(--color-success); }
|
||||
.status-pending { background-color: color-mix(in srgb, var(--color-warning), transparent 90%); color: var(--color-warning); }
|
||||
.status-inactive { background-color: color-mix(in srgb, var(--color-danger), transparent 90%); color: var(--color-danger); }
|
||||
|
||||
.app-card-body {
|
||||
padding: var(--spacing-md);
|
||||
flex: 1;
|
||||
color: var(--text-color-secondary);
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.app-card-footer {
|
||||
padding: var(--spacing-md);
|
||||
border-top: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: var(--spacing-sm);
|
||||
background-color: color-mix(in srgb, var(--bg-color-secondary), black 2%);
|
||||
}
|
||||
</style>
|
||||
<body layout="app">
|
||||
<header>
|
||||
<h1>应用中心</h1>
|
||||
<div style="width: 300px;">
|
||||
<v-input v:value="searchQuery" placeholder="搜索应用..." prefix="search"></v-input>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
<body>
|
||||
<header>
|
||||
<h1>权限应用管理</h1>
|
||||
<div class="search-bar">
|
||||
<input type="text" !value="searchTerm" @input="searchTerm=$event.target.value" placeholder="搜索应用..." />
|
||||
<i class="search-icon">🔍</i>
|
||||
</div>
|
||||
<div>
|
||||
<button class="btn btn-primary" @click="show_create_app = true">创建应用</button>
|
||||
</div>
|
||||
</header>
|
||||
<v-dialog v:show='show_create_app'>
|
||||
<c-app-create :onsuccess='sync' style="width: 40vw;"></c-app-create>
|
||||
</v-dialog>
|
||||
|
||||
<main>
|
||||
<div class="filters">
|
||||
<button v-for="filter in filters" :class="{ 'filter-btn': true, active: selectedFilter === filter.value }"
|
||||
@click="selectFilter(filter.value)">
|
||||
{{ filter.label }}
|
||||
</button>
|
||||
<v-btn v-for="filter in filters" :key="filter.value"
|
||||
:variant="currentFilter === filter.value ? 'primary' : 'outline'"
|
||||
@click="currentFilter = filter.value"
|
||||
size="sm">
|
||||
{{ filter.label }}
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<div class="app-grid">
|
||||
<div v-if="loading" class="loading">
|
||||
<div class="spinner"></div>
|
||||
<p>加载中...</p>
|
||||
</div>
|
||||
<div v-else-if="filteredApps().length === 0" class="no-apps">
|
||||
没有找到任何应用
|
||||
</div>
|
||||
<div v-else v-for="app in filteredApps()" class="app-card">
|
||||
<div class="app-card-header">
|
||||
<div class="app-icon">
|
||||
<img :src="app.icon || '/default-icon.png'" :alt="app.name" />
|
||||
</div>
|
||||
<div class="app-title">
|
||||
<h2>{{ app.name }}{{app.cid}}</h2>
|
||||
<span :class="['app-status', getStatusClass(app.status)]">
|
||||
{{ app.status }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="app-card-body">
|
||||
<p class="app-description">{{ app.des || '暂无描述信息' }}</p>
|
||||
<div class="app-meta">
|
||||
<span>类型: {{ app.typ || 'N/A' }}</span>
|
||||
<span>用户数量: {{ app.user_count }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="app-card-footer">
|
||||
<div class="app-actions">
|
||||
<a :href="app.init_url || '#'" class="btn btn-primary">打开应用</a>
|
||||
<a :href="`/app/${app.id}`">
|
||||
<button class="btn btn-outline">详情</button>
|
||||
</a>
|
||||
</div>
|
||||
<div class="app-card" v-for="app in filteredApps" :key="app.id">
|
||||
<div class="app-card-header">
|
||||
<img :src="app.icon" class="app-icon" alt="icon">
|
||||
<div class="app-info">
|
||||
<div class="app-title">{{ app.name }}</div>
|
||||
<span class="app-status" :class="getStatusClass(app.status)">
|
||||
{{ getStatusLabel(app.status) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="app-card-body">
|
||||
{{ app.des || '暂无描述' }}
|
||||
</div>
|
||||
<div class="app-card-footer">
|
||||
<v-btn size="sm" variant="text" @click="openApp(app)">访问</v-btn>
|
||||
<v-btn size="sm" variant="outline" @click="manageApp(app)">管理</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
|
||||
<script setup>
|
||||
// 响应式数据
|
||||
show_create_app = false
|
||||
loading = true;
|
||||
apps = [];
|
||||
searchTerm = '';
|
||||
selectedFilter = 'all';
|
||||
|
||||
filters = [
|
||||
{label: '所有应用', value: 'all'},
|
||||
{label: '最近更新', value: 'recent'},
|
||||
{label: '最多用户', value: 'users'},
|
||||
];
|
||||
|
||||
// 获取状态类名
|
||||
getStatusClass = (status) => {
|
||||
if (status === 'ok') return 'status-active';
|
||||
if (status === 'pending') return 'status-pending';
|
||||
return 'status-inactive';
|
||||
};
|
||||
|
||||
// 过滤后的应用列表
|
||||
filteredApps = () => {
|
||||
let result = apps;
|
||||
|
||||
// 搜索过滤
|
||||
if (searchTerm) {
|
||||
const term = searchTerm.toLowerCase();
|
||||
result = result.filter(
|
||||
(app) =>
|
||||
app.name.toLowerCase().includes(term) ||
|
||||
(app.des && app.des.toLowerCase().includes(term)) ||
|
||||
(app.typ && app.typ.toLowerCase().includes(term))
|
||||
);
|
||||
}
|
||||
|
||||
// 排序过滤
|
||||
if (selectedFilter === 'recent') {
|
||||
result.sort((a, b) => {
|
||||
if (a.updated_at === '0001-01-01T00:00:00Z') return 1;
|
||||
if (b.updated_at === '0001-01-01T00:00:00Z') return -1;
|
||||
return new Date(b.updated_at) - new Date(a.updated_at);
|
||||
});
|
||||
} else if (selectedFilter === 'users') {
|
||||
result.sort((a, b) => b.user_count - a.user_count);
|
||||
}
|
||||
result.forEach((app, id) => {
|
||||
app.cid = id
|
||||
})
|
||||
return result;
|
||||
};
|
||||
|
||||
// 选择过滤器
|
||||
selectFilter = (filter) => {
|
||||
selectedFilter = filter;
|
||||
};
|
||||
sync = () => {
|
||||
show_create_app = false
|
||||
$axios.get('/api/app').then((res) => {
|
||||
apps = res;
|
||||
loading = false;
|
||||
});
|
||||
}
|
||||
sync()
|
||||
searchQuery = ''
|
||||
currentFilter = 'all'
|
||||
filters = [
|
||||
{ label: '全部', value: 'all' },
|
||||
{ label: '我的应用', value: 'my' },
|
||||
{ label: '最近使用', value: 'recent' }
|
||||
]
|
||||
apps = []
|
||||
filteredApps = []
|
||||
|
||||
getStatusClass = (status) => {
|
||||
if (status === 'ok') return 'status-active'
|
||||
if (status === 'pending') return 'status-pending'
|
||||
return 'status-inactive'
|
||||
}
|
||||
|
||||
getStatusLabel = (status) => {
|
||||
if (status === 'ok') return '运行中'
|
||||
if (status === 'pending') return '审核中'
|
||||
return '已停止'
|
||||
}
|
||||
|
||||
openApp = (app) => {
|
||||
if (app.init_url) {
|
||||
window.open(app.init_url, '_blank')
|
||||
} else {
|
||||
$message.info('该应用暂无入口')
|
||||
}
|
||||
}
|
||||
|
||||
manageApp = (app) => {
|
||||
$router.push('/app/settings?id=' + app.id)
|
||||
}
|
||||
|
||||
// Fetch apps
|
||||
$axios.get('/api/app').then(res => {
|
||||
apps = res || []
|
||||
$data.filteredApps = apps
|
||||
})
|
||||
</script>
|
||||
<script>
|
||||
$watch(() => [searchQuery, currentFilter, apps], () => {
|
||||
let result = $data.apps
|
||||
if ($data.searchQuery) {
|
||||
const q = $data.searchQuery.toLowerCase()
|
||||
result = result.filter(app => app.name.toLowerCase().includes(q) || (app.des && app.des.toLowerCase().includes(q)))
|
||||
}
|
||||
// Filter logic for 'my' and 'recent' would go here, simplified for now
|
||||
if ($data.currentFilter === 'my') {
|
||||
// Mock logic
|
||||
}
|
||||
$data.filteredApps = result
|
||||
})
|
||||
</script>
|
||||
|
||||
</html>
|
||||
|
||||
@ -0,0 +1,104 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="description" content="应用设置" details="配置应用的基本信息">
|
||||
<title>应用设置</title>
|
||||
</head>
|
||||
<style>
|
||||
body {
|
||||
padding: var(--spacing-lg);
|
||||
background-color: var(--bg-color-secondary);
|
||||
color: var(--text-color);
|
||||
}
|
||||
.section-title {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: var(--spacing-lg);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
color: var(--text-color);
|
||||
}
|
||||
.settings-section {
|
||||
background-color: var(--bg-color);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--spacing-xl);
|
||||
box-shadow: var(--shadow-md);
|
||||
margin-bottom: var(--spacing-xl);
|
||||
}
|
||||
.form-group {
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
.form-label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
color: var(--text-color-secondary);
|
||||
}
|
||||
.form-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: var(--spacing-md);
|
||||
margin-top: var(--spacing-xl);
|
||||
}
|
||||
</style>
|
||||
<body>
|
||||
<div class="settings-section">
|
||||
<div class="section-title">
|
||||
<i class="fa-solid fa-gear"></i> 基本设置
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">应用名称</label>
|
||||
<v-input v:value="form.name" placeholder="请输入应用名称"></v-input>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">应用图标 (URL)</label>
|
||||
<v-input v:value="form.icon" placeholder="请输入图标链接"></v-input>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">应用描述</label>
|
||||
<v-input type="textarea" v:value="form.des" placeholder="请输入应用描述"></v-input>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<v-btn :click="saveSettings" :loading="loading" variant="primary">保存更改</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
<script setup>
|
||||
form = {
|
||||
name: $G.app?.name || '',
|
||||
icon: $G.app?.icon || '',
|
||||
des: $G.app?.des || ''
|
||||
}
|
||||
loading = false
|
||||
|
||||
saveSettings = () => {
|
||||
loading = true
|
||||
$axios.patch(`/api/app/${$G.app.id}`, form).then(res => {
|
||||
$message.success('保存成功')
|
||||
Object.assign($G.app, form)
|
||||
}).catch(e => {
|
||||
$message.error(e.message || '保存失败')
|
||||
}).finally(() => {
|
||||
loading = false
|
||||
})
|
||||
}
|
||||
</script>
|
||||
<script>
|
||||
$watch(() => $G.app, () => {
|
||||
if ($G.app && $G.app.id && (!$data.form.name || $data.form.name !== $G.app.name)) {
|
||||
// Only update if form is empty or mismatch, but usually we just want init
|
||||
// For simplicity, just update if we have data now
|
||||
$data.form.name = $G.app.name
|
||||
$data.form.icon = $G.app.icon
|
||||
$data.form.des = $G.app.des
|
||||
}
|
||||
})
|
||||
</script>
|
||||
</html>
|
||||
@ -1,100 +1,137 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>用户管理</title>
|
||||
<title>用户管理</title>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="description" content="用户管理" details="管理应用下的用户及其角色">
|
||||
</head>
|
||||
<style>
|
||||
body {
|
||||
padding: 20px;
|
||||
}
|
||||
body {
|
||||
padding: var(--spacing-lg);
|
||||
background-color: var(--bg-color-secondary);
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.header {
|
||||
font-size: 24px;
|
||||
.header {
|
||||
font-size: 1.5rem;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 20px;
|
||||
margin-bottom: var(--spacing-lg);
|
||||
font-weight: 600;
|
||||
color: var(--text-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.dialog {
|
||||
.dialog {
|
||||
height: 60vh;
|
||||
width: 60vw;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||
padding: 2rem;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
background-color: var(--bg-color);
|
||||
box-shadow: var(--shadow-lg);
|
||||
padding: var(--spacing-xl);
|
||||
border-radius: var(--radius-lg);
|
||||
border: 1px solid var(--border-color);
|
||||
color: var(--text-color);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.dialog-title {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: var(--spacing-lg);
|
||||
font-weight: 600;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Responsive dialog */
|
||||
@media (max-width: 768px) {
|
||||
.dialog {
|
||||
width: 90vw;
|
||||
height: 80vh;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<body>
|
||||
<div class="header">应用用户管理</div>
|
||||
<v-table :axios='$axios' :keys="keys" api='/api/user'>
|
||||
<v-btn vslot='_addon' size='sm' @click='show_user(row)' color='#2c9'>权限表</v-btn>
|
||||
</v-table>
|
||||
<v-dialog v:show='show'>
|
||||
<div class="dialog">
|
||||
<div class="text-2xl">{{selected.username}} 角色表</div>
|
||||
<v-table :axios='$axios' :keys="au_keys" :api='`/api/user/${$G.app.id}/user_role`'></v-table>
|
||||
<div class="header">
|
||||
<i class="fa-solid fa-users-gear"></i> 应用用户管理
|
||||
</div>
|
||||
</v-dialog>
|
||||
<v-table :axios='$axios' :keys="keys" api='/api/user'>
|
||||
<v-btn vslot='_addon' size='sm' @click='show_user(row)' variant="primary">
|
||||
<i class="fa-solid fa-id-card"></i> 权限表
|
||||
</v-btn>
|
||||
</v-table>
|
||||
|
||||
<v-dialog v:show='show'>
|
||||
<div class="dialog">
|
||||
<div class="dialog-title">
|
||||
<i class="fa-solid fa-user-tag"></i> {{selected.username}} 角色分配
|
||||
</div>
|
||||
<div style="flex: 1; overflow: hidden;">
|
||||
<!--
|
||||
Assumption: The API to get roles for a specific user in a specific app
|
||||
might need to be clarified.
|
||||
If the original code had /api/user/${$G.app.id}/user_role, maybe it meant /api/app/${appId}/user_role?
|
||||
Or /api/user_role?query={user_id: ...}
|
||||
|
||||
Looking at line 101 in original: user_role_url = `/api/user/${row.id}/user_role`
|
||||
But line 51 used: :api='`/api/user/${$G.app.id}/user_role`'
|
||||
|
||||
I will use the one from line 101 as it seems more specific to the user,
|
||||
but I need to make sure it filters by the current app if necessary.
|
||||
However, based on standard REST, /api/user/{uid}/user_role likely returns roles for that user.
|
||||
But we are in an "App" context.
|
||||
|
||||
Let's stick to a dynamic URL variable `current_user_role_url`
|
||||
-->
|
||||
<v-table v-if="current_user_role_url" :axios='$axios' :keys="au_keys" :api='current_user_role_url' height="100%"></v-table>
|
||||
</div>
|
||||
<div style="margin-top: var(--spacing-md); text-align: right;">
|
||||
<v-btn @click="show = false">关闭</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</v-dialog>
|
||||
</body>
|
||||
<script setup>
|
||||
show = false
|
||||
console.log($env)
|
||||
auOpts = {
|
||||
show = false
|
||||
selected = {}
|
||||
current_user_role_url = ''
|
||||
|
||||
auOpts = {
|
||||
0: ['正常', 'positive'],
|
||||
1: ['拒绝', 'warning'],
|
||||
2: ['申请中', 'primary'],
|
||||
3: ['禁用', 'warning'],
|
||||
}
|
||||
keys = [
|
||||
{
|
||||
name: 'id',
|
||||
label: 'ID',
|
||||
style: '',
|
||||
},
|
||||
{
|
||||
name: 'username',
|
||||
label: '用户名',
|
||||
style: 'text-align: left',
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
name: 'nickname',
|
||||
label: '昵称',
|
||||
style: 'text-align: left',
|
||||
editable: true,
|
||||
sortable: true
|
||||
},
|
||||
{name: 'created_at', label: '创建时间', field: (r) => new Date(r.created_at).toLocaleString()},
|
||||
{name: 'updated_at', label: '更新时间', field: (r) => new Date(r.updated_at).toLocaleString()},
|
||||
{name: 'status', label: '账号状态', sortable: true},
|
||||
]
|
||||
au_keys = [
|
||||
{name: 'id', label: 'ID', no_create: true},
|
||||
{name: 'role_name', label: 'role_name'},
|
||||
{name: 'created_at', label: '创建时间', no_create: true, field: (r) => new Date(r.created_at).toLocaleString()},
|
||||
{name: 'updated_at', label: '更新时间', no_create: true, field: (r) => new Date(r.updated_at).toLocaleString()},
|
||||
{name: 'status', label: '账号状态', sortable: true},
|
||||
]
|
||||
}
|
||||
|
||||
keys = [
|
||||
{ name: 'id', label: 'ID', style: 'width: 4rem' },
|
||||
{ name: 'username', label: '用户名', style: 'text-align: left', sortable: true },
|
||||
{ name: 'nickname', label: '昵称', style: 'text-align: left', editable: true, sortable: true },
|
||||
{ name: 'created_at', label: '创建时间', field: (r) => new Date(r.created_at).toLocaleString() },
|
||||
{ name: 'updated_at', label: '更新时间', field: (r) => new Date(r.updated_at).toLocaleString() },
|
||||
{ name: 'status', label: '账号状态', sortable: true },
|
||||
]
|
||||
|
||||
au_keys = [
|
||||
{ name: 'id', label: 'ID', no_create: true, style: 'width: 4rem' },
|
||||
{ name: 'role_name', label: '角色名称' },
|
||||
{ name: 'created_at', label: '创建时间', no_create: true, field: (r) => new Date(r.created_at).toLocaleString() },
|
||||
{ name: 'updated_at', label: '更新时间', no_create: true, field: (r) => new Date(r.updated_at).toLocaleString() },
|
||||
{ name: 'status', label: '状态', sortable: true },
|
||||
]
|
||||
|
||||
user_role_data = []
|
||||
selected = {}
|
||||
show_user = async (row) => {
|
||||
show_user = (row) => {
|
||||
selected = row
|
||||
console.log(row)
|
||||
user_role_url = `/api/user/${row.id}/user_role`
|
||||
// accessData = await access_api.next(0, 10)
|
||||
// user_role_data = await user_role_api.next(0, 10)
|
||||
// Construct URL to fetch roles for this user.
|
||||
// Assuming the intent is to see roles of this user within the context of the app or globally?
|
||||
// If it's "App User Management", we probably want to see roles assigned to this user for THIS app.
|
||||
// The original code had ambiguity.
|
||||
// Let's assume /api/user/{uid}/user_role gets the user's roles.
|
||||
// We might need to filter by app_id if the backend supports it.
|
||||
// For now, I'll use the pattern from line 101 of original file.
|
||||
current_user_role_url = `/api/user/${row.id}/user_role`
|
||||
show = true
|
||||
}
|
||||
user_role_api = {
|
||||
next: async (page, size) => {
|
||||
return await $axios.get(user_role_url, {params: {page, size}})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
@ -1,9 +1,95 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>OneAuth</title>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="description" content="OneAuth Landing Page">
|
||||
</head>
|
||||
<style>
|
||||
body {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: linear-gradient(135deg, var(--bg-color), var(--bg-color-secondary));
|
||||
color: var(--text-color);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 4rem;
|
||||
color: var(--color-primary);
|
||||
margin-bottom: var(--spacing-md);
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 1.5rem;
|
||||
color: var(--text-color-secondary);
|
||||
margin-bottom: var(--spacing-xl);
|
||||
max-width: 600px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.features {
|
||||
display: flex;
|
||||
gap: var(--spacing-xl);
|
||||
margin-bottom: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.feature-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
font-size: 2rem;
|
||||
color: var(--color-primary);
|
||||
background: color-mix(in srgb, var(--color-primary), transparent 90%);
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.feature-text {
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
|
||||
<body>
|
||||
hello
|
||||
<h1>OneAuth</h1>
|
||||
<p>Secure, Simple, Scalable Authentication Service for your applications.</p>
|
||||
|
||||
<div class="features">
|
||||
<div class="feature-item">
|
||||
<div class="feature-icon"><i class="fa-solid fa-shield-halved"></i></div>
|
||||
<div class="feature-text">Secure</div>
|
||||
</div>
|
||||
<div class="feature-item">
|
||||
<div class="feature-icon"><i class="fa-solid fa-bolt"></i></div>
|
||||
<div class="feature-text">Fast</div>
|
||||
</div>
|
||||
<div class="feature-item">
|
||||
<div class="feature-icon"><i class="fa-solid fa-code"></i></div>
|
||||
<div class="feature-text">Developer Friendly</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<v-btn round size="large" :click="goLogin">Get Started <i class="fa-solid fa-arrow-right" style="margin-left: 8px;"></i></v-btn>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
<script setup>
|
||||
goLogin = () => {
|
||||
$router.push('/login');
|
||||
}
|
||||
</script>
|
||||
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,14 +1,265 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Settings</title>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="description" content="系统设置" details="管理系统配置、安全设置和个人偏好">
|
||||
<title>系统设置</title>
|
||||
</head>
|
||||
<style>
|
||||
body {
|
||||
background-color: var(--bg-color-secondary);
|
||||
color: var(--text-color);
|
||||
padding: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.settings-container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-bottom: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 24px;
|
||||
color: var(--text-color);
|
||||
margin: 0;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.settings-section {
|
||||
background: var(--bg-color);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--spacing-lg);
|
||||
box-shadow: var(--shadow-sm);
|
||||
margin-bottom: var(--spacing-lg);
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin-bottom: var(--spacing-lg);
|
||||
padding-bottom: var(--spacing-sm);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.section-title i {
|
||||
margin-right: 10px;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.theme-options {
|
||||
display: flex;
|
||||
gap: var(--spacing-md);
|
||||
margin-top: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.theme-option {
|
||||
flex: 1;
|
||||
padding: var(--spacing-md);
|
||||
border: 2px solid var(--border-color);
|
||||
border-radius: var(--radius-md);
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.theme-option.active {
|
||||
border-color: var(--color-primary);
|
||||
background-color: color-mix(in srgb, var(--color-primary), transparent 95%);
|
||||
color: var(--color-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.theme-option:hover {
|
||||
border-color: color-mix(in srgb, var(--color-primary), transparent 50%);
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<body>
|
||||
<div class="header">系统设置</div>
|
||||
<div class="settings-container">
|
||||
<div class="header">
|
||||
<h1>系统设置</h1>
|
||||
</div>
|
||||
|
||||
<div class="settings-section">
|
||||
<div class="section-title">
|
||||
<i class="fa-solid fa-user-shield"></i> 安全设置
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">修改密码</label>
|
||||
<div style="margin-bottom: 10px;">
|
||||
<v-input type="password" placeholder="当前密码" v:value="security.currentPassword"></v-input>
|
||||
</div>
|
||||
<div style="margin-bottom: 10px;">
|
||||
<v-input type="password" placeholder="新密码" v:value="security.newPassword"></v-input>
|
||||
</div>
|
||||
<div>
|
||||
<v-input type="password" placeholder="确认新密码" v:value="security.confirmPassword"></v-input>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<v-btn :click="updatePassword" :loading="loading.password">更新密码</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-section">
|
||||
<div class="section-title">
|
||||
<i class="fa-solid fa-palette"></i> 外观设置
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">主题颜色</label>
|
||||
<div class="theme-options">
|
||||
<div class="theme-option" :class="{active: preferences.theme === 'light'}" @click="setTheme('light')">
|
||||
<i class="fa-solid fa-sun"></i> 浅色
|
||||
</div>
|
||||
<div class="theme-option" :class="{active: preferences.theme === 'dark'}" @click="setTheme('dark')">
|
||||
<i class="fa-solid fa-moon"></i> 深色
|
||||
</div>
|
||||
<div class="theme-option" :class="{active: preferences.theme === 'auto'}" @click="setTheme('auto')">
|
||||
<i class="fa-solid fa-desktop"></i> 跟随系统
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" style="margin-top: 20px;">
|
||||
<label class="form-label">紧凑模式</label>
|
||||
<!-- toggle switch implementation using standard checkbox for now, could use v-switch if available -->
|
||||
<label style="display: flex; align-items: center; cursor: pointer;">
|
||||
<input type="checkbox" v:checked="preferences.compactMode" style="margin-right: 10px;">
|
||||
<span>启用紧凑视图</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<v-btn variant="outline" :click="savePreferences" :loading="loading.preferences">保存偏好</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-section">
|
||||
<div class="section-title">
|
||||
<i class="fa-solid fa-bell"></i> 通知设置
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label style="display: flex; align-items: center; cursor: pointer; margin-bottom: 10px;">
|
||||
<input type="checkbox" v:checked="notifications.email" style="margin-right: 10px;">
|
||||
<span>接收邮件通知</span>
|
||||
</label>
|
||||
|
||||
<label style="display: flex; align-items: center; cursor: pointer; margin-bottom: 10px;">
|
||||
<input type="checkbox" v:checked="notifications.browser" style="margin-right: 10px;">
|
||||
<span>接收浏览器推送</span>
|
||||
</label>
|
||||
|
||||
<label style="display: flex; align-items: center; cursor: pointer;">
|
||||
<input type="checkbox" v:checked="notifications.marketing" style="margin-right: 10px;">
|
||||
<span>接收产品更新和营销信息</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<v-btn variant="outline" :click="saveNotifications" :loading="loading.notifications">保存设置</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
<script setup>
|
||||
security = {
|
||||
currentPassword: '',
|
||||
newPassword: '',
|
||||
confirmPassword: ''
|
||||
}
|
||||
|
||||
preferences = {
|
||||
theme: 'light',
|
||||
compactMode: false
|
||||
}
|
||||
|
||||
notifications = {
|
||||
email: true,
|
||||
browser: true,
|
||||
marketing: false
|
||||
}
|
||||
|
||||
loading = {
|
||||
password: false,
|
||||
preferences: false,
|
||||
notifications: false
|
||||
}
|
||||
|
||||
updatePassword = () => {
|
||||
if (!security.currentPassword || !security.newPassword) {
|
||||
$message.error('请填写完整密码信息');
|
||||
return;
|
||||
}
|
||||
|
||||
if (security.newPassword !== security.confirmPassword) {
|
||||
$message.error('两次输入的新密码不一致');
|
||||
return;
|
||||
}
|
||||
|
||||
loading.password = true;
|
||||
// 模拟API调用
|
||||
setTimeout(() => {
|
||||
loading.password = false;
|
||||
$message.success('密码更新成功');
|
||||
security.currentPassword = '';
|
||||
security.newPassword = '';
|
||||
security.confirmPassword = '';
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
setTheme = (theme) => {
|
||||
preferences.theme = theme;
|
||||
// 这里可以添加实际切换主题的逻辑,比如 document.documentElement.setAttribute('data-theme', theme)
|
||||
$message.info(`已切换到 ${theme === 'light' ? '浅色' : theme === 'dark' ? '深色' : '自动'} 主题`);
|
||||
}
|
||||
|
||||
savePreferences = () => {
|
||||
loading.preferences = true;
|
||||
setTimeout(() => {
|
||||
loading.preferences = false;
|
||||
$message.success('偏好设置已保存');
|
||||
}, 800);
|
||||
}
|
||||
|
||||
saveNotifications = () => {
|
||||
loading.notifications = true;
|
||||
setTimeout(() => {
|
||||
loading.notifications = false;
|
||||
$message.success('通知设置已保存');
|
||||
}, 800);
|
||||
}
|
||||
</script>
|
||||
|
||||
</html>
|
||||
Loading…
Reference in New Issue