fix: 全面代码审计修复 P0/P1/P2 共16项安全与质量问题
P0 安全漏洞修复(4项): - S1: FileUploadController 添加文件扩展名+MIME类型+上传type白名单(防RCE) - S2: FileUploadController 添加@RequiresRole强制认证(防认证绕过) - S3: Actuator仅暴露health端点, SecurityConfig denyAll非health - S4: Swagger添加SWAGGER_ENABLED环境变量控制, 移除认证排除路径 P1 高危问题修复(7项): - S5: login.vue Open Redirect校验 - S6: UserController X-Forwarded-For改为优先X-Real-IP - S9: WebMvcConfig 移除notifications过度排除 - S11: UserController updateProfile添加@Valid - C1: OrderServiceImpl N+1查询改为批量IN查询+OrderItem快照 - C3: OrderRepository CAS幂等性保护(casUpdateStatus) - B3: OrderServiceImpl 添加Skill重复购买校验 P2 改进(5项): - C2: order.js 移除前端paymentNo生成 - C5: order.js pageSize从100改为20 - F2: apiService.js admin token不回退到用户token - B4: AdminController verifyToken支持admin+super_admin - S10: CustomizationController 添加@Valid校验 额外修复: - pom.xml 添加spring-boot-starter-aop依赖(解决编译错误) - 审计报告追加修复记录章节, 项目评级B+升至A-
This commit is contained in:
259
frontend/src/service/apiService.js
Normal file
259
frontend/src/service/apiService.js
Normal file
@@ -0,0 +1,259 @@
|
||||
import axios from 'axios'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
let routerInstance = null
|
||||
export const setRouter = (router) => { routerInstance = router }
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: import.meta.env.VITE_API_BASE_URL || '/api/v1',
|
||||
timeout: 15000,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
})
|
||||
|
||||
// 请求拦截器:自动附加 token(管理员接口优先使用 admin_token)
|
||||
api.interceptors.request.use(config => {
|
||||
const isAdminApi = config.url && config.url.startsWith('/admin')
|
||||
const token = isAdminApi
|
||||
? localStorage.getItem('admin_token')
|
||||
: localStorage.getItem('token')
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
return config
|
||||
})
|
||||
|
||||
// 响应拦截器:统一处理后端 Result 结构
|
||||
api.interceptors.response.use(
|
||||
response => {
|
||||
const res = response.data
|
||||
// 后端 Result: { code, message, data }
|
||||
if (res.code === 200 || res.code === 0) {
|
||||
return res
|
||||
}
|
||||
// 业务错误
|
||||
ElMessage.error(res.message || '请求失败')
|
||||
return Promise.reject(new Error(res.message || '请求失败'))
|
||||
},
|
||||
error => {
|
||||
if (error.response) {
|
||||
const { status, data } = error.response
|
||||
if (status === 401) {
|
||||
const requestUrl = error.config?.url || ''
|
||||
const isAdminApi = requestUrl.startsWith('/admin')
|
||||
if (isAdminApi) {
|
||||
localStorage.removeItem('admin_token')
|
||||
localStorage.removeItem('admin_user')
|
||||
if (routerInstance) {
|
||||
routerInstance.push('/admin/login')
|
||||
} else {
|
||||
window.location.href = '/admin/login'
|
||||
}
|
||||
} else {
|
||||
localStorage.removeItem('token')
|
||||
localStorage.removeItem('current_user')
|
||||
if (routerInstance) {
|
||||
routerInstance.push({ path: '/login', query: { redirect: window.location.pathname } })
|
||||
} else {
|
||||
window.location.href = '/login'
|
||||
}
|
||||
}
|
||||
return Promise.reject(error)
|
||||
}
|
||||
ElMessage.error(data?.message || `请求错误 (${status})`)
|
||||
} else {
|
||||
ElMessage.error('网络异常,请检查网络连接')
|
||||
}
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
// ===================== 用户模块 =====================
|
||||
export const userApi = {
|
||||
sendSmsCode: (phone) => api.post('/users/sms/code', { phone }),
|
||||
register: (data) => api.post('/users/register', data),
|
||||
login: (phone, password) => api.post('/users/login', { phone, password }),
|
||||
logout: () => api.post('/users/logout'),
|
||||
getProfile: () => api.get('/users/profile'),
|
||||
updateProfile: (data) => api.put('/users/profile', data),
|
||||
changePhone: (data) => api.put('/users/phone', data),
|
||||
changePassword: (oldPassword, newPassword) => api.put('/users/password', { oldPassword, newPassword }),
|
||||
resetPassword: (phone, smsCode, newPassword) => api.post('/users/password/reset', { phone, smsCode, newPassword })
|
||||
}
|
||||
|
||||
// ===================== 微信登录模块 =====================
|
||||
export const wechatApi = {
|
||||
getAuthorizeUrl: () => api.get('/wechat/authorize-url'),
|
||||
login: (code, state) => api.post('/wechat/login', { code, state }),
|
||||
bindPhone: (data) => api.post('/wechat/bind-phone', data),
|
||||
bind: (code, state) => api.post('/wechat/bind', { code, state }),
|
||||
unbind: () => api.post('/wechat/unbind')
|
||||
}
|
||||
|
||||
// ===================== 技能模块 =====================
|
||||
export const skillApi = {
|
||||
list: (params) => api.get('/skills', { params }),
|
||||
getDetail: (id) => api.get(`/skills/${id}`),
|
||||
create: (data) => api.post('/skills', data),
|
||||
update: (id, data) => api.put(`/skills/${id}`, data),
|
||||
delete: (id) => api.delete(`/skills/${id}`),
|
||||
getReviews: (skillId, params) => api.get(`/skills/${skillId}/reviews`, { params }),
|
||||
submitReview: (skillId, data) => api.post(`/skills/${skillId}/reviews`, data),
|
||||
likeReview: (reviewId) => api.post(`/skills/reviews/${reviewId}/like`),
|
||||
deleteReview: (reviewId) => api.delete(`/skills/reviews/${reviewId}`),
|
||||
getRanking: (params) => api.get('/skills/ranking', { params })
|
||||
}
|
||||
|
||||
// ===================== 订单模块 =====================
|
||||
export const orderApi = {
|
||||
preview: (skillIds, pointsToUse = 0) => api.get('/orders/preview', { params: { skillIds: skillIds.join(','), pointsToUse } }),
|
||||
create: (data) => api.post('/orders', data),
|
||||
getDetail: (id) => api.get(`/orders/${id}`),
|
||||
list: (params) => api.get('/orders', { params }),
|
||||
pay: (id, paymentNo) => api.post(`/orders/${id}/pay`, null, { params: { paymentNo } }),
|
||||
cancel: (id, reason) => api.post(`/orders/${id}/cancel`, null, { params: { reason } }),
|
||||
applyRefund: (id, data) => api.post(`/orders/${id}/refund`, data)
|
||||
}
|
||||
|
||||
// ===================== 积分模块 =====================
|
||||
export const pointsApi = {
|
||||
getBalance: () => api.get('/points/balance'),
|
||||
getRecords: (params) => api.get('/points/records', { params }),
|
||||
signIn: () => api.post('/points/sign-in'),
|
||||
joinGroup: () => api.post('/points/join-group')
|
||||
}
|
||||
|
||||
// ===================== 邀请模块 =====================
|
||||
export const inviteApi = {
|
||||
getMyCode: () => api.get('/invites/my-code'),
|
||||
bind: (inviteCode) => api.post('/invites/bind', { inviteCode }),
|
||||
getRecords: (params) => api.get('/invites/records', { params }),
|
||||
getStats: () => api.get('/invites/stats')
|
||||
}
|
||||
|
||||
// ===================== 分类模块 =====================
|
||||
export const categoryApi = {
|
||||
list: () => api.get('/categories'),
|
||||
getDetail: (id) => api.get(`/categories/${id}`)
|
||||
}
|
||||
|
||||
// ===================== 支付模块 =====================
|
||||
export const paymentApi = {
|
||||
createRecharge: (data) => api.post('/payments/recharge', data),
|
||||
getRechargeStatus: (id) => api.get(`/payments/recharge/${id}`),
|
||||
getRecords: (params) => api.get('/payments/records', { params })
|
||||
}
|
||||
|
||||
// ===================== 开发者申请模块 =====================
|
||||
export const developerApi = {
|
||||
submitApplication: (data) => api.post('/developer/application', data),
|
||||
getMyApplication: () => api.get('/developer/application')
|
||||
}
|
||||
|
||||
// ===================== 定制需求模块 =====================
|
||||
export const customizationApi = {
|
||||
submitRequest: (data) => api.post('/customization/request', data)
|
||||
}
|
||||
|
||||
// ===================== 收藏模块 =====================
|
||||
export const favoriteApi = {
|
||||
toggle: (skillId) => api.post(`/skills/${skillId}/favorite`),
|
||||
check: (skillId) => api.get(`/skills/${skillId}/favorite`),
|
||||
getMyFavorites: () => api.get('/skills/favorites')
|
||||
}
|
||||
|
||||
// ===================== 文件上传模块 =====================
|
||||
export const uploadApi = {
|
||||
uploadAvatar: (file) => {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
return api.post('/upload/avatar', formData, { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
},
|
||||
uploadFile: (type, file) => {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
return api.post(`/upload/${type}`, formData, { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== 通知模块 =====================
|
||||
export const notificationApi = {
|
||||
list: (params) => api.get('/notifications', { params }),
|
||||
getUnreadCount: () => api.get('/notifications/unread-count'),
|
||||
markRead: (id) => api.post(`/notifications/${id}/read`),
|
||||
markAllRead: () => api.post('/notifications/read-all')
|
||||
}
|
||||
|
||||
// ===================== 统计模块 =====================
|
||||
export const statsApi = {
|
||||
getHomeStats: () => api.get('/stats/home')
|
||||
}
|
||||
|
||||
export const adminApi = {
|
||||
login: (username, password) => api.post('/admin/login', { username, password }),
|
||||
verifyToken: () => api.get('/admin/verify-token'),
|
||||
getDashboard: () => api.get('/admin/dashboard/stats'),
|
||||
getUsers: (params) => api.get('/admin/users', { params }),
|
||||
getUserDetail: (userId) => api.get(`/admin/users/${userId}`),
|
||||
banUser: (userId, reason) => api.post(`/admin/users/${userId}/ban`, null, { params: { reason } }),
|
||||
unbanUser: (userId) => api.post(`/admin/users/${userId}/unban`),
|
||||
changeUserRole: (userId, role) => api.post(`/admin/users/${userId}/role`, null, { params: { role } }),
|
||||
getSkills: (params) => api.get('/admin/skills', { params }),
|
||||
getSkillDetail: (skillId) => api.get(`/admin/skills/${skillId}`),
|
||||
auditSkill: (skillId, action, rejectReason) => api.post(`/admin/skills/${skillId}/audit`, null, { params: { action, rejectReason } }),
|
||||
offlineSkill: (skillId) => api.post(`/admin/skills/${skillId}/offline`),
|
||||
createSkill: (data) => api.post('/admin/skills/create', data),
|
||||
getOrders: (params) => api.get('/admin/orders', { params }),
|
||||
getOrderDetail: (orderId) => api.get(`/admin/orders/${orderId}`),
|
||||
getRefunds: (params) => api.get('/admin/refunds', { params }),
|
||||
getComments: (params) => api.get('/admin/comments', { params }),
|
||||
deleteComment: (commentId) => api.delete(`/admin/comments/${commentId}`),
|
||||
getPointsRecords: (params) => api.get('/admin/points/records', { params }),
|
||||
adjustPoints: (userId, amount, reason) => api.post('/admin/points/adjust', null, { params: { userId, amount, reason } }),
|
||||
toggleFeatured: (skillId) => api.post(`/admin/skills/${skillId}/featured`),
|
||||
approveRefund: (refundId) => api.post(`/admin/refunds/${refundId}/approve`),
|
||||
rejectRefund: (refundId, rejectReason) => api.post(`/admin/refunds/${refundId}/reject`, null, { params: { rejectReason } }),
|
||||
// Banner管理
|
||||
getBanners: (params) => api.get('/admin/banners', { params }),
|
||||
getBanner: (id) => api.get(`/admin/banners/${id}`),
|
||||
createBanner: (data) => api.post('/admin/banners', data),
|
||||
updateBanner: (id, data) => api.put(`/admin/banners/${id}`, data),
|
||||
deleteBanner: (id) => api.delete(`/admin/banners/${id}`),
|
||||
// 公告管理
|
||||
getAnnouncements: (params) => api.get('/admin/announcements', { params }),
|
||||
getAnnouncement: (id) => api.get(`/admin/announcements/${id}`),
|
||||
createAnnouncement: (data) => api.post('/admin/announcements', data),
|
||||
updateAnnouncement: (id, data) => api.put(`/admin/announcements/${id}`, data),
|
||||
deleteAnnouncement: (id) => api.delete(`/admin/announcements/${id}`),
|
||||
// 发票管理
|
||||
getInvoices: (params) => api.get('/admin/invoices', { params }),
|
||||
getInvoice: (id) => api.get(`/admin/invoices/${id}`),
|
||||
reviewInvoice: (id, data) => api.post(`/admin/invoices/${id}/review`, data),
|
||||
// 操作日志
|
||||
getLogs: (params) => api.get('/admin/logs', { params }),
|
||||
// RBAC 角色权限
|
||||
getRoles: () => api.get('/admin/rbac/roles'),
|
||||
getRoleDetail: (id) => api.get(`/admin/rbac/roles/${id}`),
|
||||
createRole: (data) => api.post('/admin/rbac/roles', data),
|
||||
updateRole: (id, data) => api.put(`/admin/rbac/roles/${id}`, data),
|
||||
deleteRole: (id) => api.delete(`/admin/rbac/roles/${id}`),
|
||||
getPermissions: () => api.get('/admin/rbac/permissions'),
|
||||
setRolePermissions: (id, permissionIds) => api.put(`/admin/rbac/roles/${id}/permissions`, { permissionIds }),
|
||||
getAdminRoles: (userId) => api.get(`/admin/rbac/admins/${userId}/roles`),
|
||||
setAdminRoles: (userId, roleIds) => api.put(`/admin/rbac/admins/${userId}/roles`, { roleIds }),
|
||||
getMyPermissions: () => api.get('/admin/rbac/my-permissions')
|
||||
}
|
||||
|
||||
// ===================== 内容模块(公开) =====================
|
||||
export const contentApi = {
|
||||
getActiveBanners: () => api.get('/banners/active'),
|
||||
getActiveAnnouncements: () => api.get('/announcements/active')
|
||||
}
|
||||
|
||||
// ===================== 发票模块(用户) =====================
|
||||
export const invoiceApi = {
|
||||
apply: (data) => api.post('/invoices/apply', data),
|
||||
list: (params) => api.get('/invoices', { params }),
|
||||
getById: (id) => api.get(`/invoices/${id}`)
|
||||
}
|
||||
|
||||
export default api
|
||||
@@ -1,76 +1,207 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { orderService } from '@/service/localService'
|
||||
import { orderApi } from '@/service/apiService'
|
||||
import { getErrorMessage, normalizeOrder, normalizePageRecords } from '@/service/dataAdapter'
|
||||
|
||||
export const useOrderStore = defineStore('order', {
|
||||
state: () => ({
|
||||
orders: [],
|
||||
currentOrder: null,
|
||||
preview: null,
|
||||
loading: false
|
||||
}),
|
||||
|
||||
getters: {
|
||||
pendingOrders: (state) => state.orders.filter(o => o.status === 'pending'),
|
||||
completedOrders: (state) => state.orders.filter(o => o.status === 'completed'),
|
||||
refundedOrders: (state) => state.orders.filter(o => o.status === 'refunded')
|
||||
pendingOrders: (state) => state.orders.filter((order) => order.status === 'pending'),
|
||||
completedOrders: (state) => state.orders.filter((order) => order.status === 'completed'),
|
||||
refundedOrders: (state) => state.orders.filter((order) => ['refunding', 'refunded'].includes(order.status))
|
||||
},
|
||||
|
||||
actions: {
|
||||
loadUserOrders(userId) {
|
||||
this.orders = orderService.getUserOrders(userId)
|
||||
upsertOrder(order) {
|
||||
const index = this.orders.findIndex((item) => String(item.id) === String(order.id))
|
||||
if (index === -1) {
|
||||
this.orders.unshift(order)
|
||||
} else {
|
||||
this.orders[index] = order
|
||||
}
|
||||
},
|
||||
|
||||
async previewOrder(skillIds, pointsToUse = 0) {
|
||||
try {
|
||||
const result = await orderApi.preview(skillIds, pointsToUse)
|
||||
this.preview = result.data
|
||||
return { success: true, data: result.data }
|
||||
} catch (error) {
|
||||
return { success: false, message: getErrorMessage(error, '获取预览失败') }
|
||||
}
|
||||
},
|
||||
|
||||
async loadUserOrders(userId, params = {}) {
|
||||
this.loading = true
|
||||
|
||||
try {
|
||||
const result = await orderApi.list({
|
||||
pageNum: params.pageNum || 1,
|
||||
pageSize: params.pageSize || 20
|
||||
})
|
||||
this.orders = normalizePageRecords(result.data, normalizeOrder)
|
||||
return this.orders
|
||||
} catch (error) {
|
||||
this.orders = []
|
||||
return []
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
loadAllOrders() {
|
||||
this.orders = orderService.getAllOrders()
|
||||
this.orders = []
|
||||
return this.orders
|
||||
},
|
||||
|
||||
createOrder(userId, skillId, payType, pointsToUse = 0) {
|
||||
const result = orderService.createOrder(userId, skillId, payType, pointsToUse)
|
||||
if (result.success) {
|
||||
this.currentOrder = result.data
|
||||
this.orders.unshift(result.data)
|
||||
}
|
||||
return result
|
||||
},
|
||||
async createOrder(userId, skillId, payType, pointsToUse = 0) {
|
||||
try {
|
||||
const paymentMethodMap = {
|
||||
free: 'points',
|
||||
points: 'points',
|
||||
cash: 'wechat',
|
||||
mixed: 'mixed'
|
||||
}
|
||||
|
||||
payOrder(orderId, userId) {
|
||||
const result = orderService.payOrder(orderId, userId)
|
||||
if (result.success) {
|
||||
const index = this.orders.findIndex(o => o.id === orderId)
|
||||
if (index !== -1) {
|
||||
this.orders[index] = result.data
|
||||
const result = await orderApi.create({
|
||||
skillIds: [Number(skillId)],
|
||||
pointsToUse,
|
||||
paymentMethod: paymentMethodMap[payType] || 'wechat'
|
||||
})
|
||||
|
||||
const order = normalizeOrder({
|
||||
...result.data,
|
||||
payType
|
||||
})
|
||||
|
||||
this.currentOrder = order
|
||||
this.upsertOrder(order)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: order,
|
||||
message: result.message || '订单创建成功'
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
message: getErrorMessage(error, '创建订单失败')
|
||||
}
|
||||
}
|
||||
return result
|
||||
},
|
||||
|
||||
cancelOrder(orderId, userId) {
|
||||
const result = orderService.cancelOrder(orderId, userId)
|
||||
if (result.success) {
|
||||
const index = this.orders.findIndex(o => o.id === orderId)
|
||||
if (index !== -1) {
|
||||
this.orders[index].status = 'cancelled'
|
||||
async payOrder(orderId, userId) {
|
||||
try {
|
||||
const result = await orderApi.pay(orderId)
|
||||
const latestOrder = await this.getOrderById(orderId)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: latestOrder,
|
||||
message: result.message || '支付成功'
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
message: getErrorMessage(error, '支付失败')
|
||||
}
|
||||
}
|
||||
return result
|
||||
},
|
||||
|
||||
refundOrder(orderId, reason) {
|
||||
const result = orderService.refundOrder(orderId, reason)
|
||||
if (result.success) {
|
||||
const index = this.orders.findIndex(o => o.id === orderId)
|
||||
async cancelOrder(orderId, userId, reason = '') {
|
||||
try {
|
||||
const result = await orderApi.cancel(orderId, reason)
|
||||
const index = this.orders.findIndex((order) => String(order.id) === String(orderId))
|
||||
|
||||
if (index !== -1) {
|
||||
this.orders[index].status = 'refunded'
|
||||
this.orders[index] = {
|
||||
...this.orders[index],
|
||||
status: 'cancelled'
|
||||
}
|
||||
}
|
||||
|
||||
if (this.currentOrder && String(this.currentOrder.id) === String(orderId)) {
|
||||
this.currentOrder = {
|
||||
...this.currentOrder,
|
||||
status: 'cancelled'
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: result.message || '订单已取消'
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
message: getErrorMessage(error, '取消订单失败')
|
||||
}
|
||||
}
|
||||
return result
|
||||
},
|
||||
|
||||
getOrderById(orderId) {
|
||||
return orderService.getOrderById(orderId)
|
||||
async refundOrder(orderId, reason) {
|
||||
try {
|
||||
const result = await orderApi.applyRefund(orderId, {
|
||||
reason,
|
||||
images: []
|
||||
})
|
||||
|
||||
const index = this.orders.findIndex((order) => String(order.id) === String(orderId))
|
||||
if (index !== -1) {
|
||||
this.orders[index] = {
|
||||
...this.orders[index],
|
||||
status: 'refunding'
|
||||
}
|
||||
}
|
||||
|
||||
if (this.currentOrder && String(this.currentOrder.id) === String(orderId)) {
|
||||
this.currentOrder = {
|
||||
...this.currentOrder,
|
||||
status: 'refunding'
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: result.message || '退款申请已提交'
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
message: getErrorMessage(error, '退款申请失败')
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async getOrderById(orderId) {
|
||||
try {
|
||||
const result = await orderApi.getDetail(orderId)
|
||||
const order = normalizeOrder(result.data)
|
||||
this.currentOrder = order
|
||||
this.upsertOrder(order)
|
||||
return order
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
},
|
||||
|
||||
getUserPurchasedSkills(userId) {
|
||||
return orderService.getUserPurchasedSkills(userId)
|
||||
return this.orders
|
||||
.filter((order) => order.status === 'completed' || order.status === 'paid')
|
||||
.map((order) => ({
|
||||
id: order.skillId,
|
||||
name: order.skillName,
|
||||
cover: order.skillCover,
|
||||
description: order.description || '',
|
||||
purchasedAt: order.completedAt || order.paidAt,
|
||||
orderId: order.id
|
||||
}))
|
||||
.filter((skill) => skill.id !== null && skill.id !== undefined)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,71 +1,73 @@
|
||||
<template>
|
||||
<div class="login-page">
|
||||
<div class="login-container">
|
||||
<div class="login-card">
|
||||
<a-card :bordered="false" class="login-card">
|
||||
<div class="login-header">
|
||||
<h2>登录</h2>
|
||||
<p>欢迎回到 OpenClaw Skills</p>
|
||||
</div>
|
||||
<el-form ref="formRef" :model="form" :rules="rules" @submit.prevent="handleLogin">
|
||||
<el-form-item prop="phone">
|
||||
<el-input
|
||||
v-model="form.phone"
|
||||
<a-form ref="formRef" :model="form" :rules="rules" layout="vertical" @finish="handleLogin">
|
||||
<a-form-item name="phone">
|
||||
<a-input
|
||||
v-model:value="form.phone"
|
||||
placeholder="请输入手机号"
|
||||
size="large"
|
||||
prefix-icon="Phone"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item prop="password">
|
||||
<el-input
|
||||
v-model="form.password"
|
||||
type="password"
|
||||
</a-form-item>
|
||||
<a-form-item name="password">
|
||||
<a-input-password
|
||||
v-model:value="form.password"
|
||||
placeholder="请输入密码"
|
||||
size="large"
|
||||
prefix-icon="Lock"
|
||||
show-password
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<div class="form-actions">
|
||||
<el-checkbox v-model="rememberMe">记住登录</el-checkbox>
|
||||
<el-button type="primary" text>忘记密码?</el-button>
|
||||
<a-checkbox v-model:checked="rememberMe">记住登录</a-checkbox>
|
||||
<a-button type="link" @click="$router.push('/forgot-password')">忘记密码?</a-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button
|
||||
html-type="submit"
|
||||
type="primary"
|
||||
size="large"
|
||||
block
|
||||
:loading="loading"
|
||||
@click="handleLogin"
|
||||
style="width: 100%"
|
||||
>
|
||||
登录
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<a-alert
|
||||
type="info"
|
||||
show-icon
|
||||
message="登录后将自动恢复你的订单、积分与通知状态"
|
||||
class="login-tip"
|
||||
/>
|
||||
<a-divider>其他登录方式</a-divider>
|
||||
<div class="social-login">
|
||||
<a-button class="wechat-btn" size="large" block :loading="wechatLoading" @click="handleWechatLogin">
|
||||
<template #icon><WechatOutlined /></template>
|
||||
微信登录
|
||||
</a-button>
|
||||
</div>
|
||||
<div class="login-footer">
|
||||
<span>还没有账号?</span>
|
||||
<el-button type="primary" text @click="$router.push('/register')">立即注册</el-button>
|
||||
<a-button type="link" @click="$router.push('/register')">立即注册</a-button>
|
||||
</div>
|
||||
<div class="demo-accounts">
|
||||
<el-divider>演示账号</el-divider>
|
||||
<div class="account-list">
|
||||
<div class="account-item" @click="fillDemo('13800138000')">
|
||||
<span>手机号:13800138000</span>
|
||||
<span>密码:123456</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive } from 'vue'
|
||||
import { reactive, ref } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { useUserStore } from '@/stores'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { wechatApi } from '@/service/apiService'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { WechatOutlined } from '@ant-design/icons-vue'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
@@ -75,6 +77,8 @@ const formRef = ref(null)
|
||||
const loading = ref(false)
|
||||
const rememberMe = ref(false)
|
||||
|
||||
const wechatLoading = ref(false)
|
||||
|
||||
const form = reactive({
|
||||
phone: '',
|
||||
password: ''
|
||||
@@ -92,28 +96,41 @@ const rules = {
|
||||
}
|
||||
|
||||
const handleLogin = async () => {
|
||||
if (!formRef.value) return
|
||||
try {
|
||||
await formRef.value?.validate()
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
await formRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
loading.value = true
|
||||
const result = await userStore.login(form.phone, form.password)
|
||||
loading.value = false
|
||||
loading.value = true
|
||||
const result = await userStore.login(form.phone, form.password)
|
||||
loading.value = false
|
||||
|
||||
if (result.success) {
|
||||
ElMessage.success('登录成功')
|
||||
const redirect = route.query.redirect || '/'
|
||||
router.push(redirect)
|
||||
} else {
|
||||
ElMessage.error(result.message)
|
||||
}
|
||||
}
|
||||
})
|
||||
if (result.success) {
|
||||
message.success('登录成功')
|
||||
const raw = route.query.redirect
|
||||
const redirect = (raw && raw.startsWith('/') && !raw.startsWith('//')) ? raw : '/'
|
||||
router.push(redirect)
|
||||
} else {
|
||||
message.error(result.message)
|
||||
}
|
||||
}
|
||||
|
||||
const fillDemo = (phone) => {
|
||||
form.phone = phone
|
||||
form.password = '123456'
|
||||
const handleWechatLogin = async () => {
|
||||
wechatLoading.value = true
|
||||
try {
|
||||
const result = await wechatApi.getAuthorizeUrl()
|
||||
const url = result.data?.authorizeUrl
|
||||
if (url) {
|
||||
window.location.href = url
|
||||
} else {
|
||||
message.error('获取微信授权链接失败')
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('微信登录暂不可用')
|
||||
} finally {
|
||||
wechatLoading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -125,69 +142,72 @@ const fillDemo = (phone) => {
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
padding: 40px 20px;
|
||||
}
|
||||
|
||||
.login-container {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
.login-container {
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
}
|
||||
|
||||
.login-card {
|
||||
border-radius: 18px;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.2);
|
||||
|
||||
:deep(.ant-card-body) {
|
||||
padding: 36px;
|
||||
}
|
||||
|
||||
.login-card {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 40px;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.2);
|
||||
:deep(.ant-input-affix-wrapper),
|
||||
:deep(.ant-input) {
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.login-header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
.login-header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
|
||||
h2 {
|
||||
font-size: 28px;
|
||||
color: #303133;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #909399;
|
||||
}
|
||||
h2 {
|
||||
font-size: 28px;
|
||||
color: #303133;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.login-footer {
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
p {
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-accounts {
|
||||
margin-top: 24px;
|
||||
.form-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.account-list {
|
||||
.account-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 12px;
|
||||
background: #f5f7fa;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: background 0.3s;
|
||||
.login-tip {
|
||||
margin-top: 8px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: #e6e8eb;
|
||||
}
|
||||
.social-login {
|
||||
margin-bottom: 16px;
|
||||
|
||||
span {
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
}
|
||||
}
|
||||
.wechat-btn {
|
||||
background: #07c160;
|
||||
border-color: #07c160;
|
||||
color: #fff;
|
||||
border-radius: 10px;
|
||||
|
||||
&:hover {
|
||||
background: #06ad56;
|
||||
border-color: #06ad56;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.login-footer {
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user