Browse Source

feat: 添加管理员侧边导航组件并更新相关页面逻辑

shadcn-as
npmrun 3 weeks ago
parent
commit
b9b8d80cfa
  1. 158
      app/components/admin/AdminSidebarNav.vue
  2. 99
      app/pages/admin.vue
  3. 30
      app/pages/admin/profile/index.vue
  4. 19
      bun.lock
  5. 10
      nuxt.config.ts
  6. 5
      package.json
  7. BIN
      packages/drizzle-pkg/db.sqlite
  8. 24
      server/api/auth/profile.put.ts

158
app/components/admin/AdminSidebarNav.vue

@ -0,0 +1,158 @@
<script setup lang="ts">
export interface NavItem {
label: string
to?: string
icon?: string
children?: NavItem[]
}
defineProps<{
nav: NavItem[]
}>()
const expandedMenus = ref<Set<string>>(new Set())
const toggleMenu = (label: string) => {
if (expandedMenus.value.has(label)) {
expandedMenus.value.delete(label)
} else {
expandedMenus.value.add(label)
}
}
const isExpanded = (label: string) => expandedMenus.value.has(label)
</script>
<template>
<nav class="sidebar-nav">
<template v-for="item in nav" :key="item.label">
<div v-if="item.children" class="nav-group">
<button
class="sidebar-link nav-group-header"
:class="{ expanded: isExpanded(item.label) }"
@click="toggleMenu(item.label)"
>
<Icon v-if="item.icon" :name="item.icon" class="sidebar-icon" />
<span class="sidebar-link-label">{{ item.label }}</span>
<Icon class="nav-chevron" name="lucide:chevron-down" />
</button>
<div v-show="isExpanded(item.label)" class="nav-children">
<NuxtLink
v-for="child in item.children"
:key="child.to"
:to="child.to"
class="sidebar-link sidebar-link-child"
active-class="active"
>
<span class="sidebar-link-label">{{ child.label }}</span>
</NuxtLink>
</div>
</div>
<NuxtLink
v-else
:to="item.to"
class="sidebar-link"
active-class="active"
>
<Icon v-if="item.icon" :name="item.icon" class="sidebar-icon" />
<span class="sidebar-link-label">{{ item.label }}</span>
</NuxtLink>
</template>
</nav>
</template>
<style scoped>
.sidebar-nav {
display: flex;
flex-direction: column;
gap: 2px;
padding: 16px 12px;
flex: 1;
}
.sidebar-link {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
border-radius: 8px;
text-decoration: none;
color: var(--color-on-dark-soft);
font-size: 14px;
font-weight: 500;
transition: all 0.15s ease;
}
.sidebar-link:hover {
background: rgba(255, 255, 255, 0.06);
color: var(--color-on-dark);
}
.sidebar-link.active {
background: var(--color-primary);
color: var(--color-on-primary);
}
.sidebar-icon {
width: 18px;
height: 18px;
flex-shrink: 0;
opacity: 0.7;
}
.sidebar-link:hover .sidebar-icon,
.sidebar-link.active .sidebar-icon {
opacity: 1;
}
.nav-group {
display: flex;
flex-direction: column;
}
.nav-group-header {
width: 100%;
justify-content: flex-start;
text-align: left;
}
.nav-chevron {
width: 14px;
height: 14px;
margin-left: auto;
flex-shrink: 0;
opacity: 0.6;
transition: transform 0.2s ease;
}
.nav-group-header.expanded .nav-chevron {
transform: rotate(180deg);
}
.nav-children {
display: flex;
flex-direction: column;
gap: 2px;
padding-left: 38px;
margin-top: 2px;
}
.sidebar-link-child {
padding: 8px 12px;
font-size: 13px;
}
.sidebar-link-child::before {
content: '';
width: 4px;
height: 4px;
border-radius: 50%;
background: var(--color-on-dark-soft);
margin-right: 8px;
flex-shrink: 0;
}
.sidebar-link-child.active::before {
background: var(--color-on-primary);
}
</style>

99
app/pages/admin.vue

@ -1,27 +1,32 @@
<script setup lang="ts"> <script setup lang="ts">
import type { NavItem } from '~/components/admin/AdminSidebarNav.vue'
definePageMeta({ definePageMeta({
layout: false layout: false
}) })
const { user, clear } = useAuthSession() const { user, clear } = useAuthSession()
const adminNav = [ const adminNav: NavItem[] = [
{ {
label: '仪表盘', label: '仪表盘',
to: '/admin/dashboard', to: '/admin/dashboard',
icon: 'dashboard' icon: 'lucide:layout-dashboard'
}, },
{ {
label: '定时任务', label: '定时任务',
to: '/admin/scheduler', to: '/admin/scheduler',
icon: 'schedule' icon: 'lucide:clock'
},
{
label: '系统管理',
icon: 'lucide:settings',
children: [
{ label: '用户管理', to: '/admin/users' },
{ label: '角色管理', to: '/admin/roles' },
{ label: '操作日志', to: '/admin/logs' },
]
}, },
] ]
const iconPaths: Record<string, string> = {
dashboard: 'M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6',
schedule: 'M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z',
user: 'M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z',
}
const logout = async () => { const logout = async () => {
await clear() await clear()
@ -36,34 +41,17 @@ const logout = async () => {
<div class="sidebar-header"> <div class="sidebar-header">
<NuxtLink to="/admin/dashboard" class="brand-link"> <NuxtLink to="/admin/dashboard" class="brand-link">
<span class="brand-icon"> <span class="brand-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"> <Icon name="lucide:line-chart" />
<path d="M13 10V3L4 14h7v7l9-11h-7z" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</span> </span>
<span class="brand-name">管理后台</span> <span class="brand-name">管理后台</span>
</NuxtLink> </NuxtLink>
</div> </div>
<nav class="sidebar-nav"> <AdminSidebarNav :nav="adminNav" />
<NuxtLink
v-for="item in adminNav"
:key="item.to"
:to="item.to"
class="sidebar-link"
active-class="active"
>
<svg class="sidebar-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<path :d="iconPaths[item.icon]" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<span class="sidebar-link-label">{{ item.label }}</span>
</NuxtLink>
</nav>
<div class="sidebar-footer"> <div class="sidebar-footer">
<NuxtLink to="/" class="home-link"> <NuxtLink to="/" class="home-link">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"> <Icon name="lucide:home" />
<path d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
返回首页 返回首页
</NuxtLink> </NuxtLink>
<NuxtLink is="div" class="user-section" to="/admin/profile" v-if="user"> <NuxtLink is="div" class="user-section" to="/admin/profile" v-if="user">
@ -75,9 +63,7 @@ const logout = async () => {
<span class="user-role">{{ user.role === "user" ? "普通用户" : "管理员" }}</span> <span class="user-role">{{ user.role === "user" ? "普通用户" : "管理员" }}</span>
</div> </div>
<button class="logout-btn" @click.stop.prevent="logout" title="退出登录"> <button class="logout-btn" @click.stop.prevent="logout" title="退出登录">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"> <Icon name="lucide:log-out" />
<path d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button> </button>
</NuxtLink> </NuxtLink>
</div> </div>
@ -135,7 +121,7 @@ const logout = async () => {
justify-content: center; justify-content: center;
} }
.brand-icon svg { .brand-icon :deep(svg) {
width: 18px; width: 18px;
height: 18px; height: 18px;
color: var(--color-on-primary); color: var(--color-on-primary);
@ -148,49 +134,6 @@ const logout = async () => {
letter-spacing: -0.01em; letter-spacing: -0.01em;
} }
.sidebar-nav {
display: flex;
flex-direction: column;
gap: 2px;
padding: 16px 12px;
flex: 1;
}
.sidebar-link {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
border-radius: 8px;
text-decoration: none;
color: var(--color-on-dark-soft);
font-size: 14px;
font-weight: 500;
transition: all 0.15s ease;
}
.sidebar-link:hover {
background: rgba(255, 255, 255, 0.06);
color: var(--color-on-dark);
}
.sidebar-link.active {
background: var(--color-primary);
color: var(--color-on-primary);
}
.sidebar-icon {
width: 18px;
height: 18px;
flex-shrink: 0;
opacity: 0.7;
}
.sidebar-link:hover .sidebar-icon,
.sidebar-link.active .sidebar-icon {
opacity: 1;
}
.sidebar-footer { .sidebar-footer {
padding: 16px 12px; padding: 16px 12px;
border-top: 1px solid rgba(255, 255, 255, 0.06); border-top: 1px solid rgba(255, 255, 255, 0.06);
@ -217,7 +160,7 @@ const logout = async () => {
color: var(--color-on-dark); color: var(--color-on-dark);
} }
.home-link svg { .home-link :deep(svg) {
width: 16px; width: 16px;
height: 16px; height: 16px;
} }
@ -288,7 +231,7 @@ const logout = async () => {
color: #c64545; color: #c64545;
} }
.logout-btn svg { .logout-btn :deep(svg) {
width: 16px; width: 16px;
height: 16px; height: 16px;
} }

30
app/pages/admin/profile/index.vue

@ -8,13 +8,9 @@ const form = reactive({
}) })
const saving = ref(false) const saving = ref(false)
const success = ref(false)
const error = ref('')
async function handleSave() { async function handleSave() {
saving.value = true saving.value = true
error.value = ''
success.value = false
try { try {
await updateProfile({ await updateProfile({
@ -22,10 +18,8 @@ async function handleSave() {
email: form.email, email: form.email,
nickname: form.nickname, nickname: form.nickname,
}) })
success.value = true
setTimeout(() => { success.value = false }, 3000)
} catch (err: any) { } catch (err: any) {
error.value = err?.data?.message || err?.message || '保存失败' // updateProfile toast toast API
} finally { } finally {
saving.value = false saving.value = false
} }
@ -51,14 +45,12 @@ async function handleSave() {
</div> </div>
<form class="profile-form" @submit.prevent="handleSave"> <form class="profile-form" @submit.prevent="handleSave">
<div v-if="error" class="alert alert-error">{{ error }}</div>
<div v-if="success" class="alert alert-success">保存成功</div>
<div class="form-row"> <div class="form-row">
<div class="form-group"> <div class="form-group">
<label class="form-label">用户名</label> <label class="form-label">用户名(不可修改)</label>
<input <input
v-model="form.username" v-model="form.username"
disabled
type="text" type="text"
class="form-input" class="form-input"
placeholder="输入用户名" placeholder="输入用户名"
@ -234,22 +226,6 @@ async function handleSave() {
gap: 16px; gap: 16px;
} }
.alert {
padding: 12px 16px;
border-radius: 8px;
font-size: 14px;
}
.alert-error {
background: rgba(198, 69, 69, 0.08);
color: var(--color-error);
}
.alert-success {
background: rgba(93, 184, 114, 0.12);
color: var(--color-success);
}
.form-group { .form-group {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

19
bun.lock

@ -6,6 +6,7 @@
"name": "person-panel", "name": "person-panel",
"dependencies": { "dependencies": {
"@libsql/client": "0.17.3", "@libsql/client": "0.17.3",
"@nuxt/icon": "2.2.2",
"bcryptjs": "3.0.3", "bcryptjs": "3.0.3",
"cache": "workspace:*", "cache": "workspace:*",
"croner": "10.0.1", "croner": "10.0.1",
@ -34,7 +35,7 @@
"tsconfig": "workspace:*", "tsconfig": "workspace:*",
"tsx": "4.21.0", "tsx": "4.21.0",
"typescript": "6.0.2", "typescript": "6.0.2",
"vue3-toastify": "^0.2.9", "vue3-toastify": "0.2.9",
}, },
}, },
"packages/cache": { "packages/cache": {
@ -58,6 +59,8 @@
}, },
}, },
"packages": { "packages": {
"@antfu/install-pkg": ["@antfu/install-pkg@1.1.0", "", { "dependencies": { "package-manager-detector": "^1.3.0", "tinyexec": "^1.0.1" } }, "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ=="],
"@babel/code-frame": ["@babel/code-frame@7.29.0", "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.29.0.tgz", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], "@babel/code-frame": ["@babel/code-frame@7.29.0", "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.29.0.tgz", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
"@babel/compat-data": ["@babel/compat-data@7.29.0", "https://registry.npmmirror.com/@babel/compat-data/-/compat-data-7.29.0.tgz", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="], "@babel/compat-data": ["@babel/compat-data@7.29.0", "https://registry.npmmirror.com/@babel/compat-data/-/compat-data-7.29.0.tgz", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="],
@ -188,6 +191,14 @@
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="],
"@iconify/collections": ["@iconify/collections@1.0.689", "", { "dependencies": { "@iconify/types": "*" } }, "sha512-/XdTbQnsxfvpNvZvPCI2TGZHxem40PjUZ6IXoGA4M5QugVNN+l311l4PWyxECXOt95iFoKA8ZcHzyZVWFW9OFw=="],
"@iconify/types": ["@iconify/types@2.0.0", "", {}, "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="],
"@iconify/utils": ["@iconify/utils@3.1.3", "", { "dependencies": { "@antfu/install-pkg": "^1.1.0", "@iconify/types": "^2.0.0", "import-meta-resolve": "^4.2.0" } }, "sha512-LPKOXPn/zV+zis1oOfGWogaXVpqUybF3ZS6SCZIsz8vg0ivVp9+fVqyYB7xq0aiST/VhUQYGO1qo6uoYSiEJqw=="],
"@iconify/vue": ["@iconify/vue@5.0.1", "", { "dependencies": { "@iconify/types": "^2.0.0" }, "peerDependencies": { "vue": ">=3.0.0" } }, "sha512-aumwwooJlFJ5H5qYWB6ZTAyM0C8hpfcSVLB9/a3qnH1GGvIJ+FEbpEs4s/HfErYe/M5qZeLjwmESR5fFm3lXEw=="],
"@ioredis/commands": ["@ioredis/commands@1.5.1", "https://registry.npmmirror.com/@ioredis/commands/-/commands-1.5.1.tgz", {}, "sha512-JH8ZL/ywcJyR9MmJ5BNqZllXNZQqQbnVZOqpPQqE1vHiFgAw4NHbvE0FOduNU8IX9babitBT46571OnPTT0Zcw=="], "@ioredis/commands": ["@ioredis/commands@1.5.1", "https://registry.npmmirror.com/@ioredis/commands/-/commands-1.5.1.tgz", {}, "sha512-JH8ZL/ywcJyR9MmJ5BNqZllXNZQqQbnVZOqpPQqE1vHiFgAw4NHbvE0FOduNU8IX9babitBT46571OnPTT0Zcw=="],
"@isaacs/cliui": ["@isaacs/cliui@8.0.2", "https://registry.npmmirror.com/@isaacs/cliui/-/cliui-8.0.2.tgz", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], "@isaacs/cliui": ["@isaacs/cliui@8.0.2", "https://registry.npmmirror.com/@isaacs/cliui/-/cliui-8.0.2.tgz", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="],
@ -258,6 +269,8 @@
"@nuxt/devtools-wizard": ["@nuxt/devtools-wizard@3.2.4", "https://registry.npmmirror.com/@nuxt/devtools-wizard/-/devtools-wizard-3.2.4.tgz", { "dependencies": { "@clack/prompts": "^1.1.0", "consola": "^3.4.2", "diff": "^8.0.3", "execa": "^8.0.1", "magicast": "^0.5.2", "pathe": "^2.0.3", "pkg-types": "^2.3.0", "semver": "^7.7.4" }, "bin": { "devtools-wizard": "cli.mjs" } }, "sha512-5tu2+Quu9XTxwtpzM8CUN0UKn/bzZIfJcoGd+at5Yy1RiUQJ4E52tRK0idW1rMSUDkbkvX3dSnu8Tpj7SAtWdQ=="], "@nuxt/devtools-wizard": ["@nuxt/devtools-wizard@3.2.4", "https://registry.npmmirror.com/@nuxt/devtools-wizard/-/devtools-wizard-3.2.4.tgz", { "dependencies": { "@clack/prompts": "^1.1.0", "consola": "^3.4.2", "diff": "^8.0.3", "execa": "^8.0.1", "magicast": "^0.5.2", "pathe": "^2.0.3", "pkg-types": "^2.3.0", "semver": "^7.7.4" }, "bin": { "devtools-wizard": "cli.mjs" } }, "sha512-5tu2+Quu9XTxwtpzM8CUN0UKn/bzZIfJcoGd+at5Yy1RiUQJ4E52tRK0idW1rMSUDkbkvX3dSnu8Tpj7SAtWdQ=="],
"@nuxt/icon": ["@nuxt/icon@2.2.2", "", { "dependencies": { "@iconify/collections": "^1.0.679", "@iconify/types": "^2.0.0", "@iconify/utils": "^3.1.1", "@iconify/vue": "^5.0.0", "@nuxt/devtools-kit": "^3.2.4", "@nuxt/kit": "^4.4.4", "consola": "^3.4.2", "local-pkg": "^1.1.2", "mlly": "^1.8.2", "ohash": "^2.0.11", "pathe": "^2.0.3", "picomatch": "^4.0.4", "std-env": "^4.1.0", "tinyglobby": "^0.2.16" } }, "sha512-K9wINW21M9x5GcKF5JEXzPKAT/Kfxl/vdnEyppw54hh5qoLcdi5HmsYoTfDP9gbJ6Z1T6IdH5JxBWk72HMe1Zg=="],
"@nuxt/kit": ["@nuxt/kit@4.4.5", "https://registry.npmmirror.com/@nuxt/kit/-/kit-4.4.5.tgz", { "dependencies": { "c12": "^3.3.4", "consola": "^3.4.2", "defu": "^6.1.7", "destr": "^2.0.5", "errx": "^0.1.0", "exsolve": "^1.0.8", "ignore": "^7.0.5", "jiti": "^2.6.1", "klona": "^2.0.6", "mlly": "^1.8.2", "ohash": "^2.0.11", "pathe": "^2.0.3", "pkg-types": "^2.3.1", "rc9": "^3.0.1", "scule": "^1.3.0", "semver": "^7.7.4", "tinyglobby": "^0.2.16", "ufo": "^1.6.4", "unctx": "^2.5.0", "untyped": "^2.0.0" } }, "sha512-J0BpoOomzd3iVZozYlZJ7AwAVliXRgeChZnAkQLfg8d0h/Q+aMK9kkHuhwFULASaRn5idiD4BIhOUz7/uoLbSw=="], "@nuxt/kit": ["@nuxt/kit@4.4.5", "https://registry.npmmirror.com/@nuxt/kit/-/kit-4.4.5.tgz", { "dependencies": { "c12": "^3.3.4", "consola": "^3.4.2", "defu": "^6.1.7", "destr": "^2.0.5", "errx": "^0.1.0", "exsolve": "^1.0.8", "ignore": "^7.0.5", "jiti": "^2.6.1", "klona": "^2.0.6", "mlly": "^1.8.2", "ohash": "^2.0.11", "pathe": "^2.0.3", "pkg-types": "^2.3.1", "rc9": "^3.0.1", "scule": "^1.3.0", "semver": "^7.7.4", "tinyglobby": "^0.2.16", "ufo": "^1.6.4", "unctx": "^2.5.0", "untyped": "^2.0.0" } }, "sha512-J0BpoOomzd3iVZozYlZJ7AwAVliXRgeChZnAkQLfg8d0h/Q+aMK9kkHuhwFULASaRn5idiD4BIhOUz7/uoLbSw=="],
"@nuxt/nitro-server": ["@nuxt/nitro-server@4.4.5", "https://registry.npmmirror.com/@nuxt/nitro-server/-/nitro-server-4.4.5.tgz", { "dependencies": { "@babel/plugin-syntax-typescript": "^7.28.6", "@nuxt/devalue": "^2.0.2", "@nuxt/kit": "4.4.5", "@unhead/vue": "^2.1.13", "@vue/shared": "^3.5.33", "consola": "^3.4.2", "defu": "^6.1.7", "destr": "^2.0.5", "devalue": "^5.8.0", "errx": "^0.1.0", "escape-string-regexp": "^5.0.0", "exsolve": "^1.0.8", "h3": "^1.15.11", "impound": "^1.1.5", "klona": "^2.0.6", "mocked-exports": "^0.1.1", "nitropack": "^2.13.4", "nypm": "^0.6.6", "ohash": "^2.0.11", "pathe": "^2.0.3", "rou3": "^0.8.1", "std-env": "^4.1.0", "ufo": "^1.6.4", "unctx": "^2.5.0", "unstorage": "^1.17.5", "vue": "^3.5.33", "vue-bundle-renderer": "^2.2.0", "vue-devtools-stub": "^0.1.0" }, "peerDependencies": { "@babel/plugin-proposal-decorators": "^7.25.0", "@rollup/plugin-babel": "^6.0.0 || ^7.0.0", "nuxt": "^4.4.5" }, "optionalPeers": ["@babel/plugin-proposal-decorators", "@rollup/plugin-babel"] }, "sha512-ZxmfxZbQ6Yr/DYkuGmPFtE/A1hDbbcOurlPeh/H4oHfAkv/N6W7OWg/3PGViKwckmF69jUMe/a89HAguaH+r5A=="], "@nuxt/nitro-server": ["@nuxt/nitro-server@4.4.5", "https://registry.npmmirror.com/@nuxt/nitro-server/-/nitro-server-4.4.5.tgz", { "dependencies": { "@babel/plugin-syntax-typescript": "^7.28.6", "@nuxt/devalue": "^2.0.2", "@nuxt/kit": "4.4.5", "@unhead/vue": "^2.1.13", "@vue/shared": "^3.5.33", "consola": "^3.4.2", "defu": "^6.1.7", "destr": "^2.0.5", "devalue": "^5.8.0", "errx": "^0.1.0", "escape-string-regexp": "^5.0.0", "exsolve": "^1.0.8", "h3": "^1.15.11", "impound": "^1.1.5", "klona": "^2.0.6", "mocked-exports": "^0.1.1", "nitropack": "^2.13.4", "nypm": "^0.6.6", "ohash": "^2.0.11", "pathe": "^2.0.3", "rou3": "^0.8.1", "std-env": "^4.1.0", "ufo": "^1.6.4", "unctx": "^2.5.0", "unstorage": "^1.17.5", "vue": "^3.5.33", "vue-bundle-renderer": "^2.2.0", "vue-devtools-stub": "^0.1.0" }, "peerDependencies": { "@babel/plugin-proposal-decorators": "^7.25.0", "@rollup/plugin-babel": "^6.0.0 || ^7.0.0", "nuxt": "^4.4.5" }, "optionalPeers": ["@babel/plugin-proposal-decorators", "@rollup/plugin-babel"] }, "sha512-ZxmfxZbQ6Yr/DYkuGmPFtE/A1hDbbcOurlPeh/H4oHfAkv/N6W7OWg/3PGViKwckmF69jUMe/a89HAguaH+r5A=="],
@ -970,6 +983,8 @@
"image-meta": ["image-meta@0.2.2", "https://registry.npmmirror.com/image-meta/-/image-meta-0.2.2.tgz", {}, "sha512-3MOLanc3sb3LNGWQl1RlQlNWURE5g32aUphrDyFeCsxBTk08iE3VNe4CwsUZ0Qs1X+EfX0+r29Sxdpza4B+yRA=="], "image-meta": ["image-meta@0.2.2", "https://registry.npmmirror.com/image-meta/-/image-meta-0.2.2.tgz", {}, "sha512-3MOLanc3sb3LNGWQl1RlQlNWURE5g32aUphrDyFeCsxBTk08iE3VNe4CwsUZ0Qs1X+EfX0+r29Sxdpza4B+yRA=="],
"import-meta-resolve": ["import-meta-resolve@4.2.0", "", {}, "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg=="],
"impound": ["impound@1.1.5", "https://registry.npmmirror.com/impound/-/impound-1.1.5.tgz", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.31", "es-module-lexer": "^2.0.0", "pathe": "^2.0.3", "unplugin": "^3.0.0", "unplugin-utils": "^0.3.1" } }, "sha512-5AUn+QE0UofqNHu5f2Skf6Svukdg4ehOIq8O0EtqIx4jta0CDZYBPqpIHt0zrlUTiFVYlLpeH39DoikXBjPKpA=="], "impound": ["impound@1.1.5", "https://registry.npmmirror.com/impound/-/impound-1.1.5.tgz", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.31", "es-module-lexer": "^2.0.0", "pathe": "^2.0.3", "unplugin": "^3.0.0", "unplugin-utils": "^0.3.1" } }, "sha512-5AUn+QE0UofqNHu5f2Skf6Svukdg4ehOIq8O0EtqIx4jta0CDZYBPqpIHt0zrlUTiFVYlLpeH39DoikXBjPKpA=="],
"inherits": ["inherits@2.0.4", "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], "inherits": ["inherits@2.0.4", "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
@ -1208,6 +1223,8 @@
"package-json-from-dist": ["package-json-from-dist@1.0.1", "https://registry.npmmirror.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="], "package-json-from-dist": ["package-json-from-dist@1.0.1", "https://registry.npmmirror.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="],
"package-manager-detector": ["package-manager-detector@1.6.0", "", {}, "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA=="],
"parseurl": ["parseurl@1.3.3", "https://registry.npmmirror.com/parseurl/-/parseurl-1.3.3.tgz", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], "parseurl": ["parseurl@1.3.3", "https://registry.npmmirror.com/parseurl/-/parseurl-1.3.3.tgz", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
"path-key": ["path-key@3.1.1", "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], "path-key": ["path-key@3.1.1", "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],

10
nuxt.config.ts

@ -3,6 +3,7 @@ import tailwindcss from '@tailwindcss/vite'
// https://nuxt.com/docs/api/configuration/nuxt-config // https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({ export default defineNuxtConfig({
compatibilityDate: '2025-07-15', compatibilityDate: '2025-07-15',
app: { app: {
head: { head: {
link: [ link: [
@ -12,13 +13,16 @@ export default defineNuxtConfig({
], ],
}, },
}, },
css: ['~/assets/css/main.css'], css: ['~/assets/css/main.css'],
devtools: { enabled: true }, devtools: { enabled: true },
vite: { vite: {
plugins: [ plugins: [
tailwindcss(), tailwindcss(),
] ]
}, },
nitro: { nitro: {
typescript: { typescript: {
tsConfig: { tsConfig: {
@ -34,4 +38,10 @@ export default defineNuxtConfig({
}, },
} }
}, },
modules: ['@nuxt/icon'],
icon: {
mode: 'css',
cssLayer: 'base'
}
}) })

5
package.json

@ -20,9 +20,10 @@
}, },
"dependencies": { "dependencies": {
"@libsql/client": "0.17.3", "@libsql/client": "0.17.3",
"@nuxt/icon": "2.2.2",
"bcryptjs": "3.0.3", "bcryptjs": "3.0.3",
"croner": "10.0.1",
"cache": "workspace:*", "cache": "workspace:*",
"croner": "10.0.1",
"dotenv": "17.4.1", "dotenv": "17.4.1",
"drizzle-orm": "0.45.2", "drizzle-orm": "0.45.2",
"drizzle-pkg": "workspace:*", "drizzle-pkg": "workspace:*",
@ -33,9 +34,9 @@
"mime": "4.1.0", "mime": "4.1.0",
"multer": "2.1.1", "multer": "2.1.1",
"nuxt": "4.4.5", "nuxt": "4.4.5",
"svg-captcha": "1.4.0",
"tailwindcss": "4.3.0", "tailwindcss": "4.3.0",
"ufo": "1.6.3", "ufo": "1.6.3",
"svg-captcha": "1.4.0",
"vue": "3.5.32", "vue": "3.5.32",
"vue-router": "5.0.4", "vue-router": "5.0.4",
"zod": "4.3.6" "zod": "4.3.6"

BIN
packages/drizzle-pkg/db.sqlite

Binary file not shown.

24
server/api/auth/profile.put.ts

@ -16,7 +16,7 @@ export default defineWrappedResponseHandler(async (event) => {
} }
const body = await readBody<{ const body = await readBody<{
username?: string; // username?: string;
email?: string; email?: string;
nickname?: string; nickname?: string;
}>(event); }>(event);
@ -29,20 +29,20 @@ export default defineWrappedResponseHandler(async (event) => {
} }
const updateData: Partial<{ const updateData: Partial<{
username: string; // username: string;
email: string | null; email: string | null;
nickname: string | null; nickname: string | null;
}> = {}; }> = {};
if (body.username !== undefined) { // if (body.username !== undefined) {
if (typeof body.username !== "string" || body.username.trim().length === 0) { // if (typeof body.username !== "string" || body.username.trim().length === 0) {
throw createError({ statusCode: 400, statusMessage: "用户名不能为空" }); // throw createError({ statusCode: 400, statusMessage: "用户名不能为空" });
} // }
if (body.username.length < 2 || body.username.length > 50) { // if (body.username.length < 2 || body.username.length > 50) {
throw createError({ statusCode: 400, statusMessage: "用户名长度需在2-50字符之间" }); // throw createError({ statusCode: 400, statusMessage: "用户名长度需在2-50字符之间" });
} // }
updateData.username = body.username.trim(); // updateData.username = body.username.trim();
} // }
if (body.email !== undefined) { if (body.email !== undefined) {
if (body.email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(body.email)) { if (body.email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(body.email)) {
@ -69,7 +69,7 @@ export default defineWrappedResponseHandler(async (event) => {
const [row] = await dbGlobal const [row] = await dbGlobal
.select({ .select({
id: users.id, id: users.id,
username: users.username, // username: users.username,
email: users.email, email: users.email,
role: users.role, role: users.role,
nickname: users.nickname, nickname: users.nickname,

Loading…
Cancel
Save