后台界面独立

This commit is contained in:
yml
2026-05-22 17:09:31 +08:00
parent 3f5f18b43e
commit 6edee641fb
8 changed files with 274 additions and 28 deletions
+11 -1
View File
@@ -1,11 +1,21 @@
<script setup lang="ts">
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'
const route = useRoute()
const layout = computed(() => route.meta.layout || 'app')
</script>
<template>
<AppLayout>
<AdminLayout v-if="layout === 'admin'">
<RouterView />
</AdminLayout>
<RouterView v-else-if="layout === 'blank'" />
<AppLayout v-else>
<RouterView />
</AppLayout>
</template>
+63
View File
@@ -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>
+2 -5
View File
@@ -1,5 +1,5 @@
<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 = [
{ label: '首页', to: '/', icon: House },
@@ -8,10 +8,7 @@ const navItems = [
{ label: '钱包', to: '/wallet', icon: Wallet },
{ label: '通知', to: '/notifications', icon: Bell },
{ label: '实名', to: '/realname', icon: UserFilled },
{ 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: '/admin/login', icon: Key },
{ label: '登录', to: '/login', icon: Phone },
]
</script>
+30 -6
View File
@@ -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'
+144
View File
@@ -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;
}
+22 -15
View File
@@ -36,21 +36,28 @@ function readError(error: unknown, fallback: string) {
</script>
<template>
<section class="page">
<div class="page-header">
<p class="eyebrow">Admin Login</p>
<h1>后台登录</h1>
<p>开发态首次登录会初始化默认管理员后续可接入角色权限和密码修改</p>
</div>
<section class="admin-login-shell">
<div class="admin-login-panel">
<RouterLink class="admin-login-brand" to="/">
<span class="brand-mark">H</span>
<span>哈夫币后台</span>
</RouterLink>
<el-form class="login-form" label-position="top">
<el-form-item label="用户名">
<el-input v-model="form.username" placeholder="请输入管理员用户名" />
</el-form-item>
<el-form-item label="密码">
<el-input v-model="form.password" type="password" show-password placeholder="请输入管理员密码" />
</el-form-item>
<el-button type="primary" :loading="loading" @click="handleLogin">登录后台</el-button>
</el-form>
<div class="admin-login-header">
<p class="eyebrow">Admin Login</p>
<h1>后台登录</h1>
<p>管理员登录后进入后台管理系统</p>
</div>
<el-form label-position="top">
<el-form-item label="用户名">
<el-input v-model="form.username" placeholder="请输入管理员用户名" />
</el-form-item>
<el-form-item label="密码">
<el-input v-model="form.password" type="password" show-password placeholder="请输入管理员密码" @keyup.enter="handleLogin" />
</el-form-item>
<el-button class="full-control" type="primary" :loading="loading" @click="handleLogin">登录后台</el-button>
</el-form>
</div>
</section>
</template>