后台界面独立
This commit is contained in:
@@ -37,7 +37,7 @@ npm run dev
|
|||||||
- 站内信已支持订单关键节点自动写入,可通过 `GET /api/notifications` 查看。
|
- 站内信已支持订单关键节点自动写入,可通过 `GET /api/notifications` 查看。
|
||||||
- 申诉仲裁已支持订单双方发起申诉、开发态后台处理,后台页面为 `http://localhost:5173/admin/disputes`。
|
- 申诉仲裁已支持订单双方发起申诉、开发态后台处理,后台页面为 `http://localhost:5173/admin/disputes`。
|
||||||
- 系统配置已支持默认配置初始化和后台编辑,页面为 `http://localhost:5173/admin/system-configs`,更新会写入审计日志。
|
- 系统配置已支持默认配置初始化和后台编辑,页面为 `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/dashboard`。
|
||||||
- 商品审核后台已接入,页面为 `http://localhost:5173/admin/listings/review`。
|
- 商品审核后台已接入,页面为 `http://localhost:5173/admin/listings/review`。
|
||||||
|
|
||||||
|
|||||||
@@ -98,6 +98,7 @@
|
|||||||
## 开发态后台登录
|
## 开发态后台登录
|
||||||
|
|
||||||
- 后台登录接口为 `/api/admin/auth/login`,前端页面为 `/admin/login`。
|
- 后台登录接口为 `/api/admin/auth/login`,前端页面为 `/admin/login`。
|
||||||
|
- 后台页面使用独立管理布局,不再混用用户端侧边栏。
|
||||||
- 后台 JWT 与普通用户 JWT 区分 `admin` 和 `user`,普通用户 Token 不能访问 `/api/admin/*`。
|
- 后台 JWT 与普通用户 JWT 区分 `admin` 和 `user`,普通用户 Token 不能访问 `/api/admin/*`。
|
||||||
- 开发态首次后台登录会自动初始化默认管理员:用户名 `admin`,密码 `admin123456`。
|
- 开发态首次后台登录会自动初始化默认管理员:用户名 `admin`,密码 `admin123456`。
|
||||||
- 默认管理员只用于本地开发;正式部署前必须改为初始化脚本、强密码和管理员密码修改流程。
|
- 默认管理员只用于本地开发;正式部署前必须改为初始化脚本、强密码和管理员密码修改流程。
|
||||||
|
|||||||
+11
-1
@@ -1,11 +1,21 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { RouterView } from 'vue-router'
|
import { RouterView } from 'vue-router'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
|
||||||
|
import AdminLayout from './layouts/AdminLayout.vue'
|
||||||
import AppLayout from './layouts/AppLayout.vue'
|
import AppLayout from './layouts/AppLayout.vue'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const layout = computed(() => route.meta.layout || 'app')
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<AppLayout>
|
<AdminLayout v-if="layout === 'admin'">
|
||||||
|
<RouterView />
|
||||||
|
</AdminLayout>
|
||||||
|
<RouterView v-else-if="layout === 'blank'" />
|
||||||
|
<AppLayout v-else>
|
||||||
<RouterView />
|
<RouterView />
|
||||||
</AppLayout>
|
</AppLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { DataLine, DocumentChecked, Operation, ScaleToOriginal, SwitchButton } from '@element-plus/icons-vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
import { logoutAdmin } from '@/api/adminAuth'
|
||||||
|
import { useAdminSessionStore } from '@/stores/adminSession'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const adminSession = useAdminSessionStore()
|
||||||
|
|
||||||
|
const navItems = [
|
||||||
|
{ label: '仪表盘', to: '/admin/dashboard', icon: DataLine },
|
||||||
|
{ label: '商品审核', to: '/admin/listings/review', icon: DocumentChecked },
|
||||||
|
{ label: '仲裁中心', to: '/admin/disputes', icon: ScaleToOriginal },
|
||||||
|
{ label: '系统配置', to: '/admin/system-configs', icon: Operation },
|
||||||
|
]
|
||||||
|
|
||||||
|
const adminName = computed(() => adminSession.nickname || adminSession.username || '管理员')
|
||||||
|
|
||||||
|
async function handleLogout() {
|
||||||
|
try {
|
||||||
|
await logoutAdmin()
|
||||||
|
} catch {
|
||||||
|
// Local logout should still happen if the development API is unavailable.
|
||||||
|
}
|
||||||
|
adminSession.logout()
|
||||||
|
ElMessage.success('已退出后台')
|
||||||
|
await router.push('/admin/login')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="admin-shell">
|
||||||
|
<aside class="admin-sidebar">
|
||||||
|
<RouterLink class="admin-brand" to="/admin/dashboard">
|
||||||
|
<span class="brand-mark">H</span>
|
||||||
|
<span>哈夫币后台</span>
|
||||||
|
</RouterLink>
|
||||||
|
|
||||||
|
<nav class="admin-nav">
|
||||||
|
<RouterLink v-for="item in navItems" :key="item.to" class="admin-nav-link" :to="item.to">
|
||||||
|
<el-icon><component :is="item.icon" /></el-icon>
|
||||||
|
<span>{{ item.label }}</span>
|
||||||
|
</RouterLink>
|
||||||
|
</nav>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<section class="admin-workspace">
|
||||||
|
<header class="admin-topbar">
|
||||||
|
<div>
|
||||||
|
<span>管理系统</span>
|
||||||
|
<strong>{{ adminName }}</strong>
|
||||||
|
</div>
|
||||||
|
<el-button :icon="SwitchButton" @click="handleLogout">退出</el-button>
|
||||||
|
</header>
|
||||||
|
<main class="admin-main">
|
||||||
|
<slot />
|
||||||
|
</main>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Bell, House, Key, Operation, Phone, ScaleToOriginal, Shop, Tickets, UserFilled, Wallet, View } from '@element-plus/icons-vue'
|
import { Bell, House, Key, Phone, Shop, Tickets, UserFilled, Wallet } from '@element-plus/icons-vue'
|
||||||
|
|
||||||
const navItems = [
|
const navItems = [
|
||||||
{ label: '首页', to: '/', icon: House },
|
{ label: '首页', to: '/', icon: House },
|
||||||
@@ -8,10 +8,7 @@ const navItems = [
|
|||||||
{ label: '钱包', to: '/wallet', icon: Wallet },
|
{ label: '钱包', to: '/wallet', icon: Wallet },
|
||||||
{ label: '通知', to: '/notifications', icon: Bell },
|
{ label: '通知', to: '/notifications', icon: Bell },
|
||||||
{ label: '实名', to: '/realname', icon: UserFilled },
|
{ label: '实名', to: '/realname', icon: UserFilled },
|
||||||
{ label: '后台登录', to: '/admin/login', icon: Key },
|
{ label: '后台', to: '/admin/login', icon: Key },
|
||||||
{ label: '审核', to: '/admin/listings/review', icon: View },
|
|
||||||
{ label: '仲裁', to: '/admin/disputes', icon: ScaleToOriginal },
|
|
||||||
{ label: '配置', to: '/admin/system-configs', icon: Operation },
|
|
||||||
{ label: '登录', to: '/login', icon: Phone },
|
{ label: '登录', to: '/login', icon: Phone },
|
||||||
]
|
]
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { createRouter, createWebHistory } from 'vue-router'
|
|||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory(),
|
history: createWebHistory(),
|
||||||
routes: [
|
routes: [
|
||||||
|
{ path: '/admin', redirect: '/admin/dashboard' },
|
||||||
{ path: '/', name: 'home', component: () => import('@/views/public/HomeView.vue') },
|
{ path: '/', name: 'home', component: () => import('@/views/public/HomeView.vue') },
|
||||||
{ path: '/login', name: 'login', component: () => import('@/views/account/LoginView.vue') },
|
{ path: '/login', name: 'login', component: () => import('@/views/account/LoginView.vue') },
|
||||||
{ path: '/listings', name: 'listings', component: () => import('@/views/public/ListingsView.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/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/handoffs', name: 'seller-handoffs', component: () => import('@/views/seller/SellerHandoffsView.vue') },
|
||||||
{ path: '/seller/earnings', name: 'seller-earnings', component: () => import('@/views/seller/SellerEarningsView.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/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') },
|
{
|
||||||
{ path: '/admin/listings/review', name: 'admin-listing-review', component: () => import('@/views/admin/AdminListingReviewView.vue') },
|
path: '/admin/dashboard',
|
||||||
{ path: '/admin/disputes', name: 'admin-disputes', component: () => import('@/views/admin/AdminDisputesView.vue') },
|
name: 'admin-dashboard',
|
||||||
{ path: '/admin/system-configs', name: 'admin-system-configs', component: () => import('@/views/admin/AdminSystemConfigsView.vue') },
|
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) => {
|
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')
|
const token = localStorage.getItem('admin_access_token')
|
||||||
if (!token) {
|
if (!token) {
|
||||||
return '/admin/login'
|
return '/admin/login'
|
||||||
|
|||||||
@@ -81,6 +81,124 @@ a {
|
|||||||
padding: 32px;
|
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 {
|
.page {
|
||||||
max-width: 1120px;
|
max-width: 1120px;
|
||||||
}
|
}
|
||||||
@@ -407,6 +525,10 @@ h1 {
|
|||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.admin-shell {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
.sidebar {
|
.sidebar {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
@@ -421,10 +543,32 @@ h1 {
|
|||||||
overflow-x: auto;
|
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 {
|
.main {
|
||||||
padding: 24px 16px;
|
padding: 24px 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.admin-main {
|
||||||
|
padding: 24px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
.metric-grid {
|
.metric-grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,21 +36,28 @@ function readError(error: unknown, fallback: string) {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="page">
|
<section class="admin-login-shell">
|
||||||
<div class="page-header">
|
<div class="admin-login-panel">
|
||||||
|
<RouterLink class="admin-login-brand" to="/">
|
||||||
|
<span class="brand-mark">H</span>
|
||||||
|
<span>哈夫币后台</span>
|
||||||
|
</RouterLink>
|
||||||
|
|
||||||
|
<div class="admin-login-header">
|
||||||
<p class="eyebrow">Admin Login</p>
|
<p class="eyebrow">Admin Login</p>
|
||||||
<h1>后台登录</h1>
|
<h1>后台登录</h1>
|
||||||
<p>开发态首次登录会初始化默认管理员,后续可接入角色权限和密码修改。</p>
|
<p>管理员登录后进入后台管理系统。</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-form class="login-form" label-position="top">
|
<el-form label-position="top">
|
||||||
<el-form-item label="用户名">
|
<el-form-item label="用户名">
|
||||||
<el-input v-model="form.username" placeholder="请输入管理员用户名" />
|
<el-input v-model="form.username" placeholder="请输入管理员用户名" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="密码">
|
<el-form-item label="密码">
|
||||||
<el-input v-model="form.password" type="password" show-password placeholder="请输入管理员密码" />
|
<el-input v-model="form.password" type="password" show-password placeholder="请输入管理员密码" @keyup.enter="handleLogin" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-button type="primary" :loading="loading" @click="handleLogin">登录后台</el-button>
|
<el-button class="full-control" type="primary" :loading="loading" @click="handleLogin">登录后台</el-button>
|
||||||
</el-form>
|
</el-form>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Reference in New Issue
Block a user