第 5 阶段:纠纷、通知与后台-2
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
import { apiClient } from './client'
|
||||
|
||||
export interface AdminUser {
|
||||
id: number
|
||||
username: string
|
||||
nickname: string
|
||||
status: string
|
||||
last_login_at?: string
|
||||
}
|
||||
|
||||
export interface AdminTokenPair {
|
||||
access_token: string
|
||||
refresh_token: string
|
||||
token_type: string
|
||||
expires_in: number
|
||||
}
|
||||
|
||||
export interface AdminLoginData {
|
||||
admin: AdminUser
|
||||
tokens: AdminTokenPair
|
||||
}
|
||||
|
||||
interface ApiResponse<T> {
|
||||
code: string
|
||||
message: string
|
||||
data: T
|
||||
}
|
||||
|
||||
export async function loginAdmin(username: string, password: string) {
|
||||
const { data } = await apiClient.post<ApiResponse<AdminLoginData>>('/admin/auth/login', { username, password })
|
||||
return data.data
|
||||
}
|
||||
|
||||
export async function fetchAdminMe() {
|
||||
const { data } = await apiClient.get<ApiResponse<AdminUser>>('/admin/me')
|
||||
return data.data
|
||||
}
|
||||
|
||||
export async function logoutAdmin() {
|
||||
const { data } = await apiClient.post<ApiResponse<{ logged_out: boolean }>>('/admin/auth/logout')
|
||||
return data.data
|
||||
}
|
||||
@@ -6,7 +6,9 @@ export const apiClient = axios.create({
|
||||
})
|
||||
|
||||
apiClient.interceptors.request.use((config) => {
|
||||
const token = localStorage.getItem('access_token')
|
||||
const url = config.url || ''
|
||||
const tokenKey = url.startsWith('/admin') ? 'admin_access_token' : 'access_token'
|
||||
const token = localStorage.getItem(tokenKey)
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { Bell, House, Operation, Phone, ScaleToOriginal, Shop, Tickets, UserFilled, Wallet } from '@element-plus/icons-vue'
|
||||
import { Bell, House, Key, Operation, Phone, ScaleToOriginal, Shop, Tickets, UserFilled, Wallet } from '@element-plus/icons-vue'
|
||||
|
||||
const navItems = [
|
||||
{ label: '首页', to: '/', icon: House },
|
||||
@@ -8,6 +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/disputes', icon: ScaleToOriginal },
|
||||
{ label: '配置', to: '/admin/system-configs', icon: Operation },
|
||||
{ label: '登录', to: '/login', icon: Phone },
|
||||
|
||||
@@ -17,10 +17,21 @@ 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/disputes', name: 'admin-disputes', component: () => import('@/views/admin/AdminDisputesView.vue') },
|
||||
{ path: '/admin/system-configs', name: 'admin-system-configs', component: () => import('@/views/admin/AdminSystemConfigsView.vue') },
|
||||
],
|
||||
})
|
||||
|
||||
router.beforeEach((to) => {
|
||||
if (to.path.startsWith('/admin') && to.path !== '/admin/login') {
|
||||
const token = localStorage.getItem('admin_access_token')
|
||||
if (!token) {
|
||||
return '/admin/login'
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
export default router
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
import { fetchAdminMe, loginAdmin, type AdminUser } from '@/api/adminAuth'
|
||||
|
||||
export const useAdminSessionStore = defineStore('adminSession', {
|
||||
state: () => ({
|
||||
token: localStorage.getItem('admin_access_token') || '',
|
||||
refreshToken: localStorage.getItem('admin_refresh_token') || '',
|
||||
adminId: Number(localStorage.getItem('admin_id') || 0),
|
||||
username: localStorage.getItem('admin_username') || '',
|
||||
nickname: '',
|
||||
}),
|
||||
actions: {
|
||||
async login(username: string, password: string) {
|
||||
const result = await loginAdmin(username, password)
|
||||
this.applySession(result.admin, result.tokens.access_token, result.tokens.refresh_token)
|
||||
return result
|
||||
},
|
||||
async loadMe() {
|
||||
const admin = await fetchAdminMe()
|
||||
this.applyAdmin(admin)
|
||||
return admin
|
||||
},
|
||||
logout() {
|
||||
this.token = ''
|
||||
this.refreshToken = ''
|
||||
this.adminId = 0
|
||||
this.username = ''
|
||||
this.nickname = ''
|
||||
localStorage.removeItem('admin_access_token')
|
||||
localStorage.removeItem('admin_refresh_token')
|
||||
localStorage.removeItem('admin_id')
|
||||
localStorage.removeItem('admin_username')
|
||||
},
|
||||
applySession(admin: AdminUser, accessToken: string, refreshToken: string) {
|
||||
this.token = accessToken
|
||||
this.refreshToken = refreshToken
|
||||
localStorage.setItem('admin_access_token', accessToken)
|
||||
localStorage.setItem('admin_refresh_token', refreshToken)
|
||||
this.applyAdmin(admin)
|
||||
},
|
||||
applyAdmin(admin: AdminUser) {
|
||||
this.adminId = admin.id
|
||||
this.username = admin.username
|
||||
this.nickname = admin.nickname
|
||||
localStorage.setItem('admin_id', String(admin.id))
|
||||
localStorage.setItem('admin_username', admin.username)
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,56 @@
|
||||
<script setup lang="ts">
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { reactive, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import { useAdminSessionStore } from '@/stores/adminSession'
|
||||
|
||||
const router = useRouter()
|
||||
const adminSession = useAdminSessionStore()
|
||||
const loading = ref(false)
|
||||
const form = reactive({
|
||||
username: 'admin',
|
||||
password: 'admin123456',
|
||||
})
|
||||
|
||||
async function handleLogin() {
|
||||
loading.value = true
|
||||
try {
|
||||
await adminSession.login(form.username, form.password)
|
||||
ElMessage.success('后台登录成功')
|
||||
await router.push('/admin/dashboard')
|
||||
} catch (error) {
|
||||
ElMessage.error(readError(error, '后台登录失败'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function readError(error: unknown, fallback: string) {
|
||||
if (typeof error === 'object' && error && 'response' in error) {
|
||||
const response = (error as { response?: { data?: { message?: string } } }).response
|
||||
return response?.data?.message || fallback
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="page">
|
||||
<div class="page-header">
|
||||
<p class="eyebrow">Admin Login</p>
|
||||
<h1>后台登录</h1>
|
||||
<p>开发态首次登录会初始化默认管理员,后续可接入角色权限和密码修改。</p>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</section>
|
||||
</template>
|
||||
Reference in New Issue
Block a user