diff --git a/README.md b/README.md index b5c3d38..2cf139c 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ npm run dev - 站内信已支持订单关键节点自动写入,可通过 `GET /api/notifications` 查看。 - 申诉仲裁已支持订单双方发起申诉、开发态后台处理,后台页面为 `http://localhost:5173/admin/disputes`。 - 系统配置已支持默认配置初始化和后台编辑,页面为 `http://localhost:5173/admin/system-configs`,更新会写入审计日志。 -- 后台已使用独立登录,页面为 `http://localhost:5173/admin/login`;开发态默认管理员为 `admin / admin123456`。 +- 后台已使用独立登录和独立管理 UI,页面为 `http://localhost:5173/admin/login`;开发态默认管理员为 `admin / admin123456`。 - 后台仪表盘已接入真实统计数据,页面为 `http://localhost:5173/admin/dashboard`。 - 商品审核后台已接入,页面为 `http://localhost:5173/admin/listings/review`。 diff --git a/docs/business-rules.md b/docs/business-rules.md index d353188..a25a4b9 100644 --- a/docs/business-rules.md +++ b/docs/business-rules.md @@ -98,6 +98,7 @@ ## 开发态后台登录 - 后台登录接口为 `/api/admin/auth/login`,前端页面为 `/admin/login`。 +- 后台页面使用独立管理布局,不再混用用户端侧边栏。 - 后台 JWT 与普通用户 JWT 区分 `admin` 和 `user`,普通用户 Token 不能访问 `/api/admin/*`。 - 开发态首次后台登录会自动初始化默认管理员:用户名 `admin`,密码 `admin123456`。 - 默认管理员只用于本地开发;正式部署前必须改为初始化脚本、强密码和管理员密码修改流程。 diff --git a/frontend/src/App.vue b/frontend/src/App.vue index c51a1ce..098c538 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -1,11 +1,21 @@ diff --git a/frontend/src/layouts/AdminLayout.vue b/frontend/src/layouts/AdminLayout.vue new file mode 100644 index 0000000..aa7d1fa --- /dev/null +++ b/frontend/src/layouts/AdminLayout.vue @@ -0,0 +1,63 @@ + + + diff --git a/frontend/src/layouts/AppLayout.vue b/frontend/src/layouts/AppLayout.vue index fe40ecc..6e416a2 100644 --- a/frontend/src/layouts/AppLayout.vue +++ b/frontend/src/layouts/AppLayout.vue @@ -1,5 +1,5 @@ diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index a56d313..dc5690b 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -3,6 +3,7 @@ import { createRouter, createWebHistory } from 'vue-router' const router = createRouter({ history: createWebHistory(), routes: [ + { path: '/admin', redirect: '/admin/dashboard' }, { path: '/', name: 'home', component: () => import('@/views/public/HomeView.vue') }, { path: '/login', name: 'login', component: () => import('@/views/account/LoginView.vue') }, { path: '/listings', name: 'listings', component: () => import('@/views/public/ListingsView.vue') }, @@ -17,16 +18,39 @@ const router = createRouter({ { path: '/seller/listings/create', name: 'seller-listing-create', component: () => import('@/views/seller/SellerListingCreateView.vue') }, { path: '/seller/handoffs', name: 'seller-handoffs', component: () => import('@/views/seller/SellerHandoffsView.vue') }, { path: '/seller/earnings', name: 'seller-earnings', component: () => import('@/views/seller/SellerEarningsView.vue') }, - { path: '/admin/login', name: 'admin-login', component: () => import('@/views/admin/AdminLoginView.vue') }, - { path: '/admin/dashboard', name: 'admin-dashboard', component: () => import('@/views/admin/AdminDashboardView.vue') }, - { path: '/admin/listings/review', name: 'admin-listing-review', component: () => import('@/views/admin/AdminListingReviewView.vue') }, - { path: '/admin/disputes', name: 'admin-disputes', component: () => import('@/views/admin/AdminDisputesView.vue') }, - { path: '/admin/system-configs', name: 'admin-system-configs', component: () => import('@/views/admin/AdminSystemConfigsView.vue') }, + { path: '/admin/login', name: 'admin-login', component: () => import('@/views/admin/AdminLoginView.vue'), meta: { layout: 'blank' } }, + { + path: '/admin/dashboard', + name: 'admin-dashboard', + component: () => import('@/views/admin/AdminDashboardView.vue'), + meta: { layout: 'admin', requiresAdmin: true }, + }, + { + path: '/admin/listings/review', + name: 'admin-listing-review', + component: () => import('@/views/admin/AdminListingReviewView.vue'), + meta: { layout: 'admin', requiresAdmin: true }, + }, + { + path: '/admin/disputes', + name: 'admin-disputes', + component: () => import('@/views/admin/AdminDisputesView.vue'), + meta: { layout: 'admin', requiresAdmin: true }, + }, + { + path: '/admin/system-configs', + name: 'admin-system-configs', + component: () => import('@/views/admin/AdminSystemConfigsView.vue'), + meta: { layout: 'admin', requiresAdmin: true }, + }, ], }) router.beforeEach((to) => { - if (to.path.startsWith('/admin') && to.path !== '/admin/login') { + if (to.path === '/admin/login' && localStorage.getItem('admin_access_token')) { + return '/admin/dashboard' + } + if (to.meta.requiresAdmin) { const token = localStorage.getItem('admin_access_token') if (!token) { return '/admin/login' diff --git a/frontend/src/styles/base.css b/frontend/src/styles/base.css index ad6a1b9..a888fbc 100644 --- a/frontend/src/styles/base.css +++ b/frontend/src/styles/base.css @@ -81,6 +81,124 @@ a { padding: 32px; } +.admin-shell { + display: grid; + min-height: 100vh; + grid-template-columns: 260px 1fr; + background: #f5f7fb; +} + +.admin-sidebar { + display: flex; + flex-direction: column; + border-right: 1px solid #dfe5ec; + background: #111827; + padding: 20px 16px; + color: #dbe4ee; +} + +.admin-brand { + display: flex; + align-items: center; + gap: 10px; + min-height: 44px; + color: #ffffff; + font-weight: 700; +} + +.admin-nav { + display: grid; + gap: 6px; + margin-top: 28px; +} + +.admin-nav-link { + display: flex; + align-items: center; + gap: 10px; + min-height: 42px; + border-radius: 8px; + padding: 0 12px; + color: #b7c3d2; +} + +.admin-nav-link.router-link-active { + background: #0f766e; + color: #ffffff; + font-weight: 600; +} + +.admin-workspace { + min-width: 0; +} + +.admin-topbar { + display: flex; + align-items: center; + justify-content: space-between; + min-height: 64px; + border-bottom: 1px solid #e4e7ed; + background: #ffffff; + padding: 0 28px; +} + +.admin-topbar span { + display: block; + color: #6b7785; + font-size: 12px; +} + +.admin-topbar strong { + display: block; + margin-top: 3px; + color: #111827; +} + +.admin-main { + min-width: 0; + padding: 28px; +} + +.admin-login-shell { + display: grid; + min-height: 100vh; + place-items: center; + background: + linear-gradient(135deg, rgba(15, 118, 110, 0.12), rgba(17, 24, 39, 0.08)), + #f5f7fb; + padding: 24px; +} + +.admin-login-panel { + width: min(420px, 100%); + border: 1px solid #e4e7ed; + border-radius: 8px; + background: #ffffff; + padding: 28px; + box-shadow: 0 20px 60px rgba(17, 24, 39, 0.10); +} + +.admin-login-brand { + display: inline-flex; + align-items: center; + gap: 10px; + color: #111827; + font-weight: 700; +} + +.admin-login-header { + margin: 28px 0 22px; +} + +.admin-login-header h1 { + font-size: 30px; +} + +.admin-login-header p:last-child { + margin: 10px 0 0; + color: #52616f; +} + .page { max-width: 1120px; } @@ -407,6 +525,10 @@ h1 { grid-template-columns: 1fr; } + .admin-shell { + grid-template-columns: 1fr; + } + .sidebar { position: sticky; top: 0; @@ -421,10 +543,32 @@ h1 { overflow-x: auto; } + .admin-sidebar { + position: sticky; + top: 0; + z-index: 10; + border-right: 0; + border-bottom: 1px solid #1f2937; + } + + .admin-nav { + grid-auto-flow: column; + grid-auto-columns: max-content; + overflow-x: auto; + } + + .admin-topbar { + padding: 0 16px; + } + .main { padding: 24px 16px; } + .admin-main { + padding: 24px 16px; + } + .metric-grid { grid-template-columns: 1fr; } diff --git a/frontend/src/views/admin/AdminLoginView.vue b/frontend/src/views/admin/AdminLoginView.vue index bd2a036..8fdf41c 100644 --- a/frontend/src/views/admin/AdminLoginView.vue +++ b/frontend/src/views/admin/AdminLoginView.vue @@ -36,21 +36,28 @@ function readError(error: unknown, fallback: string) {