第 5 阶段:纠纷、通知与后台-2

This commit is contained in:
yml
2026-05-22 16:50:33 +08:00
parent 99f9df7bcd
commit aee3455e9a
22 changed files with 519 additions and 36 deletions
+1
View File
@@ -37,6 +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`
## 文档
+1 -1
View File
@@ -7,6 +7,7 @@ require (
github.com/golang-jwt/jwt/v5 v5.3.0
github.com/redis/go-redis/v9 v9.17.0
go.uber.org/zap v1.27.0
golang.org/x/crypto v0.42.0
gorm.io/datatypes v1.2.7
gorm.io/driver/mysql v1.6.0
gorm.io/gorm v1.31.1
@@ -44,7 +45,6 @@ require (
go.uber.org/mock v0.5.0 // indirect
go.uber.org/multierr v1.10.0 // indirect
golang.org/x/arch v0.20.0 // indirect
golang.org/x/crypto v0.42.0 // indirect
golang.org/x/mod v0.27.0 // indirect
golang.org/x/net v0.43.0 // indirect
golang.org/x/sync v0.17.0 // indirect
+28 -3
View File
@@ -10,8 +10,10 @@ import (
)
const (
ContextUserID = "user_id"
ContextPhone = "phone"
ContextUserID = "user_id"
ContextPhone = "phone"
ContextAdminID = "admin_id"
ContextUsername = "username"
)
func Auth(jwtManager *auth.JWTManager) gin.HandlerFunc {
@@ -24,7 +26,7 @@ func Auth(jwtManager *auth.JWTManager) gin.HandlerFunc {
return
}
claims, err := jwtManager.Parse(tokenText, "access")
claims, err := jwtManager.ParseSubject(tokenText, "access", "user")
if err != nil {
response.Unauthorized(c, "访问令牌无效或已过期")
c.Abort()
@@ -36,3 +38,26 @@ func Auth(jwtManager *auth.JWTManager) gin.HandlerFunc {
c.Next()
}
}
func AdminAuth(jwtManager *auth.JWTManager) gin.HandlerFunc {
return func(c *gin.Context) {
header := c.GetHeader("Authorization")
tokenText := strings.TrimSpace(strings.TrimPrefix(header, "Bearer "))
if tokenText == "" || tokenText == header {
response.Unauthorized(c, "缺少后台访问令牌")
c.Abort()
return
}
claims, err := jwtManager.ParseSubject(tokenText, "access", "admin")
if err != nil {
response.Unauthorized(c, "后台访问令牌无效或已过期")
c.Abort()
return
}
c.Set(ContextAdminID, claims.UserID)
c.Set(ContextUsername, claims.Phone)
c.Next()
}
}
+18
View File
@@ -0,0 +1,18 @@
package model
import "time"
type AdminUser struct {
ID uint64 `gorm:"primaryKey" json:"id"`
Username string `gorm:"size:64;not null;uniqueIndex" json:"username"`
PasswordHash string `gorm:"size:255;not null" json:"-"`
Nickname string `gorm:"size:64;not null;default:''" json:"nickname"`
Status string `gorm:"size:32;not null;default:'active'" json:"status"`
LastLoginAt *time.Time `json:"last_login_at"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
func (AdminUser) TableName() string {
return "admin_users"
}
+25
View File
@@ -0,0 +1,25 @@
package adminauth
import (
"time"
"hfb_sys/backend/internal/modules/auth"
)
type AdminDTO struct {
ID uint64 `json:"id"`
Username string `json:"username"`
Nickname string `json:"nickname"`
Status string `json:"status"`
LastLoginAt *time.Time `json:"last_login_at"`
}
type LoginRequest struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}
type LoginResult struct {
Admin AdminDTO `json:"admin"`
Tokens auth.TokenPair `json:"tokens"`
}
@@ -0,0 +1,70 @@
package adminauth
import (
"errors"
"net/http"
"strings"
"hfb_sys/backend/internal/middleware"
"hfb_sys/backend/pkg/response"
"github.com/gin-gonic/gin"
)
type Handler struct {
service *Service
}
func NewHandler(service *Service) *Handler {
return &Handler{service: service}
}
func (h *Handler) Login(c *gin.Context) {
var req LoginRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "用户名和密码不能为空")
return
}
result, err := h.service.Login(strings.TrimSpace(req.Username), req.Password)
if err != nil {
writeAdminAuthError(c, err)
return
}
response.OK(c, result)
}
func (h *Handler) Me(c *gin.Context) {
value, ok := c.Get(middleware.ContextAdminID)
if !ok {
response.Unauthorized(c, "缺少管理员上下文")
return
}
adminID, ok := value.(uint64)
if !ok {
response.Unauthorized(c, "管理员上下文无效")
return
}
admin, err := h.service.Me(adminID)
if err != nil {
writeAdminAuthError(c, err)
return
}
response.OK(c, admin)
}
func (h *Handler) Logout(c *gin.Context) {
response.OK(c, gin.H{"logged_out": true})
}
func writeAdminAuthError(c *gin.Context, err error) {
switch {
case errors.Is(err, ErrDependencyUnavailable):
response.ServiceUnavailable(c, "数据库未连接")
case errors.Is(err, ErrInvalidCredential):
response.BadRequest(c, "用户名或密码错误")
case errors.Is(err, ErrAdminDisabled):
response.Error(c, http.StatusForbidden, "admin_disabled", "管理员已禁用")
default:
response.Error(c, http.StatusInternalServerError, "internal_error", "后台认证服务暂时不可用")
}
}
@@ -0,0 +1,98 @@
package adminauth
import (
"errors"
"time"
"hfb_sys/backend/internal/model"
"hfb_sys/backend/internal/modules/auth"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
)
const (
defaultAdminUsername = "admin"
defaultAdminPassword = "admin123456"
)
type Repository struct {
db *gorm.DB
jwt *auth.JWTManager
}
func NewRepository(db *gorm.DB, jwt *auth.JWTManager) *Repository {
return &Repository{db: db, jwt: jwt}
}
func (r *Repository) Login(username string, password string) (LoginResult, error) {
if err := r.ensureDefaultAdmin(); err != nil {
return LoginResult{}, err
}
var admin model.AdminUser
if err := r.db.Where("username = ?", username).First(&admin).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return LoginResult{}, ErrInvalidCredential
}
return LoginResult{}, err
}
if admin.Status != "active" {
return LoginResult{}, ErrAdminDisabled
}
if err := bcrypt.CompareHashAndPassword([]byte(admin.PasswordHash), []byte(password)); err != nil {
return LoginResult{}, ErrInvalidCredential
}
now := time.Now()
admin.LastLoginAt = &now
if err := r.db.Save(&admin).Error; err != nil {
return LoginResult{}, err
}
tokens, err := r.jwt.GenerateSubjectPair(admin.ID, admin.Username, "admin")
if err != nil {
return LoginResult{}, err
}
return LoginResult{Admin: toDTO(admin), Tokens: tokens}, nil
}
func (r *Repository) FindByID(id uint64) (*AdminDTO, error) {
var admin model.AdminUser
if err := r.db.First(&admin, id).Error; err != nil {
return nil, err
}
if admin.Status != "active" {
return nil, ErrAdminDisabled
}
dto := toDTO(admin)
return &dto, nil
}
func (r *Repository) ensureDefaultAdmin() error {
var count int64
if err := r.db.Model(&model.AdminUser{}).Count(&count).Error; err != nil {
return err
}
if count > 0 {
return nil
}
hash, err := bcrypt.GenerateFromPassword([]byte(defaultAdminPassword), bcrypt.DefaultCost)
if err != nil {
return err
}
admin := model.AdminUser{
Username: defaultAdminUsername,
PasswordHash: string(hash),
Nickname: "超级管理员",
Status: "active",
}
return r.db.Create(&admin).Error
}
func toDTO(admin model.AdminUser) AdminDTO {
return AdminDTO{
ID: admin.ID,
Username: admin.Username,
Nickname: admin.Nickname,
Status: admin.Status,
LastLoginAt: admin.LastLoginAt,
}
}
@@ -0,0 +1,34 @@
package adminauth
import "errors"
var (
ErrDependencyUnavailable = errors.New("dependency unavailable")
ErrInvalidCredential = errors.New("invalid credential")
ErrAdminDisabled = errors.New("admin disabled")
)
type Service struct {
repo *Repository
}
func NewService(repo *Repository) *Service {
return &Service{repo: repo}
}
func (s *Service) Login(username string, password string) (LoginResult, error) {
if s.repo == nil {
return LoginResult{}, ErrDependencyUnavailable
}
if username == "" || password == "" {
return LoginResult{}, ErrInvalidCredential
}
return s.repo.Login(username, password)
}
func (s *Service) Me(adminID uint64) (*AdminDTO, error) {
if s.repo == nil {
return nil, ErrDependencyUnavailable
}
return s.repo.FindByID(adminID)
}
+23 -10
View File
@@ -22,9 +22,10 @@ type JWTManager struct {
}
type Claims struct {
UserID uint64 `json:"uid"`
Phone string `json:"phone"`
TokenType string `json:"typ"`
UserID uint64 `json:"uid"`
Phone string `json:"phone"`
TokenType string `json:"typ"`
SubjectType string `json:"sub_type"`
jwt.RegisteredClaims
}
@@ -45,11 +46,15 @@ func NewJWTManager(secret string) *JWTManager {
}
func (m *JWTManager) GeneratePair(userID uint64, phone string) (TokenPair, error) {
accessToken, err := m.generate(userID, phone, tokenTypeAccess, m.accessTTL)
return m.GenerateSubjectPair(userID, phone, "user")
}
func (m *JWTManager) GenerateSubjectPair(userID uint64, subject string, subjectType string) (TokenPair, error) {
accessToken, err := m.generate(userID, subject, subjectType, tokenTypeAccess, m.accessTTL)
if err != nil {
return TokenPair{}, err
}
refreshToken, err := m.generate(userID, phone, tokenTypeRefresh, m.refreshTTL)
refreshToken, err := m.generate(userID, subject, subjectType, tokenTypeRefresh, m.refreshTTL)
if err != nil {
return TokenPair{}, err
}
@@ -62,6 +67,10 @@ func (m *JWTManager) GeneratePair(userID uint64, phone string) (TokenPair, error
}
func (m *JWTManager) Parse(tokenText, expectedType string) (*Claims, error) {
return m.ParseSubject(tokenText, expectedType, "")
}
func (m *JWTManager) ParseSubject(tokenText, expectedType string, expectedSubjectType string) (*Claims, error) {
claims := &Claims{}
token, err := jwt.ParseWithClaims(tokenText, claims, func(token *jwt.Token) (any, error) {
if token.Method != m.signingMethod {
@@ -75,17 +84,21 @@ func (m *JWTManager) Parse(tokenText, expectedType string) (*Claims, error) {
if claims.TokenType != expectedType {
return nil, ErrInvalidToken
}
if expectedSubjectType != "" && claims.SubjectType != expectedSubjectType {
return nil, ErrInvalidToken
}
return claims, nil
}
func (m *JWTManager) generate(userID uint64, phone string, tokenType string, ttl time.Duration) (string, error) {
func (m *JWTManager) generate(userID uint64, subject string, subjectType string, tokenType string, ttl time.Duration) (string, error) {
now := time.Now()
claims := Claims{
UserID: userID,
Phone: phone,
TokenType: tokenType,
UserID: userID,
Phone: subject,
TokenType: tokenType,
SubjectType: subjectType,
RegisteredClaims: jwt.RegisteredClaims{
Subject: phone,
Subject: subject,
IssuedAt: jwt.NewNumericDate(now),
ExpiresAt: jwt.NewNumericDate(now.Add(ttl)),
},
+1 -1
View File
@@ -109,7 +109,7 @@ func (s *Service) LoginWithSMS(ctx context.Context, phone string, code string) (
}
func (s *Service) RefreshToken(refreshToken string) (TokenPair, error) {
claims, err := s.jwt.Parse(refreshToken, tokenTypeRefresh)
claims, err := s.jwt.ParseSubject(refreshToken, tokenTypeRefresh, "user")
if err != nil {
return TokenPair{}, err
}
+11 -2
View File
@@ -84,9 +84,9 @@ func (h *Handler) AdminList(c *gin.Context) {
}
func (h *Handler) AdminArbitrate(c *gin.Context) {
adminID, ok := currentUserID(c)
adminID, ok := currentAdminID(c)
if !ok {
response.Unauthorized(c, "缺少用户上下文")
response.Unauthorized(c, "缺少管理员上下文")
return
}
id, ok := parseID(c)
@@ -106,6 +106,15 @@ func (h *Handler) AdminArbitrate(c *gin.Context) {
response.OK(c, item)
}
func currentAdminID(c *gin.Context) (uint64, bool) {
value, ok := c.Get(middleware.ContextAdminID)
if !ok {
return 0, false
}
adminID, ok := value.(uint64)
return adminID, ok
}
func currentUserID(c *gin.Context) (uint64, bool) {
value, ok := c.Get(middleware.ContextUserID)
if !ok {
@@ -28,9 +28,9 @@ func (h *Handler) List(c *gin.Context) {
}
func (h *Handler) Update(c *gin.Context) {
userID, ok := currentUserID(c)
adminID, ok := currentAdminID(c)
if !ok {
response.Unauthorized(c, "缺少用户上下文")
response.Unauthorized(c, "缺少管理员上下文")
return
}
key := c.Param("key")
@@ -39,7 +39,7 @@ func (h *Handler) Update(c *gin.Context) {
response.BadRequest(c, "配置值不能为空")
return
}
item, err := h.service.Update(userID, key, req, AuditMeta{
item, err := h.service.Update(adminID, key, req, AuditMeta{
IP: c.ClientIP(),
UserAgent: c.GetHeader("User-Agent"),
})
@@ -50,13 +50,13 @@ func (h *Handler) Update(c *gin.Context) {
response.OK(c, item)
}
func currentUserID(c *gin.Context) (uint64, bool) {
value, ok := c.Get(middleware.ContextUserID)
func currentAdminID(c *gin.Context) (uint64, bool) {
value, ok := c.Get(middleware.ContextAdminID)
if !ok {
return 0, false
}
userID, ok := value.(uint64)
return userID, ok
adminID, ok := value.(uint64)
return adminID, ok
}
func writeConfigError(c *gin.Context, err error) {
+16 -1
View File
@@ -4,6 +4,7 @@ import (
"hfb_sys/backend/internal/config"
"hfb_sys/backend/internal/handler"
"hfb_sys/backend/internal/middleware"
"hfb_sys/backend/internal/modules/adminauth"
"hfb_sys/backend/internal/modules/auth"
"hfb_sys/backend/internal/modules/dispute"
"hfb_sys/backend/internal/modules/listing"
@@ -37,6 +38,12 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine {
}
authService := auth.NewService(userRepo, deps.Redis, jwtManager, logger)
authHandler := auth.NewHandler(authService)
var adminAuthRepo *adminauth.Repository
if deps.DB != nil {
adminAuthRepo = adminauth.NewRepository(deps.DB, jwtManager)
}
adminAuthService := adminauth.NewService(adminAuthRepo)
adminAuthHandler := adminauth.NewHandler(adminAuthService)
userHandler := user.NewHandler(userRepo)
var realnameRepo *realname.Repository
if deps.DB != nil {
@@ -81,6 +88,7 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine {
systemConfigService := systemconfig.NewService(systemConfigRepo)
systemConfigHandler := systemconfig.NewHandler(systemConfigService)
requireAuth := middleware.Auth(jwtManager)
requireAdmin := middleware.AdminAuth(jwtManager)
requireRealname := middleware.RequireRealname(userRepo)
api := engine.Group("/api")
@@ -151,8 +159,15 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine {
realnameRoutes.GET("/status", realnameHandler.Status)
}
adminRoutes := api.Group("/admin", requireAuth)
adminAuthRoutes := api.Group("/admin/auth")
{
adminAuthRoutes.POST("/login", adminAuthHandler.Login)
}
adminRoutes := api.Group("/admin", requireAdmin)
{
adminRoutes.GET("/me", adminAuthHandler.Me)
adminRoutes.POST("/auth/logout", adminAuthHandler.Logout)
adminRoutes.GET("/disputes", disputeHandler.AdminList)
adminRoutes.POST("/disputes/:id/arbitrate", disputeHandler.AdminArbitrate)
adminRoutes.GET("/system-configs", systemConfigHandler.List)
+3
View File
@@ -46,6 +46,9 @@ API 规划以 [项目计划](project-plan.md) 第 10 章为准。
- `GET /api/wallet/ledger`
- `GET /api/notifications`
- `POST /api/notifications/{id}/read`
- `POST /api/admin/auth/login`
- `POST /api/admin/auth/logout`
- `GET /api/admin/me`
- `GET /api/admin/disputes`
- `POST /api/admin/disputes/{id}/arbitrate`
- `GET /api/admin/system-configs`
+8 -1
View File
@@ -91,4 +91,11 @@
- 系统配置接口为 `/api/admin/system-configs`
- 首次查询会自动补齐一组默认配置,包括交接超时、归还确认超时、短信限流、最低押金、实名下单开关、平台抽成和提现门槛。
- 更新配置会写入 `audit_logs`,记录操作人、配置项、修改前值和修改后值。
- 当前后台接口仍是开发态 mock 权限,只要求登录;后续接入后台管理员、角色和权限后再限制可操作配置项。
- 当前后台接口已使用独立管理员登录和后台 JWT;后续接入角色和权限后再限制可操作配置项。
## 开发态后台登录
- 后台登录接口为 `/api/admin/auth/login`,前端页面为 `/admin/login`
- 后台 JWT 与普通用户 JWT 区分 `admin``user`,普通用户 Token 不能访问 `/api/admin/*`
- 开发态首次后台登录会自动初始化默认管理员:用户名 `admin`,密码 `admin123456`
- 默认管理员只用于本地开发;正式部署前必须改为初始化脚本、强密码和管理员密码修改流程。
+11 -8
View File
@@ -614,20 +614,23 @@
后台:
- `GET /admin/dashboard`:后台仪表盘
- `GET /admin/users`:用户列表
- `POST /admin/users/{id}/freeze`:冻结用户
- `POST /admin/users/{id}/unfreeze`:解冻用户
- `GET /admin/listings/pending`:待审核商品
- `POST /admin/listings/{id}/approve`:审核通过
- `POST /admin/listings/{id}/reject`:审核拒绝
- `POST /api/admin/auth/login`:后台登录
- `POST /api/admin/auth/logout`:后台退出
- `GET /api/admin/me`:当前管理员信息
- `GET /api/admin/dashboard`:后台仪表盘
- `GET /api/admin/users`:用户列表
- `POST /api/admin/users/{id}/freeze`:冻结用户
- `POST /api/admin/users/{id}/unfreeze`:解冻用户
- `GET /api/admin/listings/pending`:待审核商品。
- `POST /api/admin/listings/{id}/approve`:审核通过。
- `POST /api/admin/listings/{id}/reject`:审核拒绝。
- `GET /api/admin/orders`:订单列表。
- `GET /api/admin/disputes`:纠纷列表。
- `POST /api/admin/disputes/{id}/arbitrate`:纠纷仲裁。
- `GET /api/admin/wallet/ledger`:资金流水。
- `GET /api/admin/system-configs`:系统配置列表。
- `PUT /api/admin/system-configs/{key}`:更新系统配置。
- `GET /admin/audit-logs`:审计日志。
- `GET /api/admin/audit-logs`:审计日志。
## 11. 安全与风控
+42
View File
@@ -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
}
+3 -1
View File
@@ -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}`
}
+2 -1
View File
@@ -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 },
+11
View File
@@ -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
+50
View File
@@ -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>