由于本次代码变更内容为空,无法生成有效的提交信息。请提供具体的代码变更内容以便生成合适的提交信息。
This commit is contained in:
92
admin-system/src/api/dashboard.ts
Normal file
92
admin-system/src/api/dashboard.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { request } from '.'
|
||||
import { mockAPI } from './mockData'
|
||||
import { createMockWrapper } from '@/config/mock'
|
||||
|
||||
// 定义仪表板数据类型
|
||||
export interface DashboardStats {
|
||||
// 用户统计
|
||||
userCount: number
|
||||
newUserCount: number
|
||||
activeUserCount: number
|
||||
|
||||
// 商家统计
|
||||
merchantCount: number
|
||||
newMerchantCount: number
|
||||
activeMerchantCount: number
|
||||
|
||||
// 旅行统计
|
||||
travelCount: number
|
||||
newTravelCount: number
|
||||
|
||||
// 动物统计
|
||||
animalCount: number
|
||||
newAnimalCount: number
|
||||
|
||||
// 订单统计
|
||||
orderCount: number
|
||||
newOrderCount: number
|
||||
orderAmount: number
|
||||
|
||||
// 系统统计
|
||||
onlineUserCount: number
|
||||
systemLoad: number
|
||||
}
|
||||
|
||||
export interface UserGrowthData {
|
||||
date: string
|
||||
count: number
|
||||
}
|
||||
|
||||
export interface OrderStatsData {
|
||||
date: string
|
||||
count: number
|
||||
amount: number
|
||||
}
|
||||
|
||||
export interface ActivityLog {
|
||||
id: number
|
||||
title: string
|
||||
description: string
|
||||
avatar: string
|
||||
time: string
|
||||
}
|
||||
|
||||
export interface DashboardData {
|
||||
stats: DashboardStats
|
||||
userGrowth: UserGrowthData[]
|
||||
orderStats: OrderStatsData[]
|
||||
activities: ActivityLog[]
|
||||
}
|
||||
|
||||
export interface ApiResponse<T = any> {
|
||||
success: boolean
|
||||
code: number
|
||||
message: string
|
||||
data: T
|
||||
}
|
||||
|
||||
// 获取仪表板数据
|
||||
export const getDashboardData = () =>
|
||||
request.get<ApiResponse<DashboardData>>('/admin/dashboard')
|
||||
|
||||
// 获取用户增长数据
|
||||
export const getUserGrowthData = (days: number = 7) =>
|
||||
request.get<ApiResponse<UserGrowthData[]>>(`/admin/dashboard/user-growth?days=${days}`)
|
||||
|
||||
// 获取订单统计数据
|
||||
export const getOrderStatsData = (days: number = 7) =>
|
||||
request.get<ApiResponse<OrderStatsData[]>>(`/admin/dashboard/order-stats?days=${days}`)
|
||||
|
||||
// 获取最近活动日志
|
||||
export const getRecentActivities = () =>
|
||||
request.get<ApiResponse<ActivityLog[]>>('/admin/dashboard/activities')
|
||||
|
||||
// 开发环境使用模拟数据
|
||||
const dashboardAPI = createMockWrapper({
|
||||
getDashboardData,
|
||||
getUserGrowthData,
|
||||
getOrderStatsData,
|
||||
getRecentActivities
|
||||
}, mockAPI.dashboard)
|
||||
|
||||
export default dashboardAPI
|
||||
@@ -35,6 +35,24 @@ const mockOrders = [
|
||||
{ id: 2, userId: 2, merchantId: 1, amount: 299, status: 'pending', createdAt: '2024-01-21' }
|
||||
]
|
||||
|
||||
// 模拟权限数据
|
||||
const mockPermissions = [
|
||||
{ id: 1, name: '用户读取', code: 'user:read', description: '查看用户信息', resource_type: 'user', created_at: '2024-01-01', updated_at: '2024-01-01' },
|
||||
{ id: 2, name: '用户写入', code: 'user:write', description: '创建/编辑用户信息', resource_type: 'user', created_at: '2024-01-01', updated_at: '2024-01-01' },
|
||||
{ id: 3, name: '商家读取', code: 'merchant:read', description: '查看商家信息', resource_type: 'merchant', created_at: '2024-01-01', updated_at: '2024-01-01' },
|
||||
{ id: 4, name: '商家写入', code: 'merchant:write', description: '创建/编辑商家信息', resource_type: 'merchant', created_at: '2024-01-01', updated_at: '2024-01-01' },
|
||||
{ id: 5, name: '旅行读取', code: 'travel:read', description: '查看旅行信息', resource_type: 'travel', created_at: '2024-01-01', updated_at: '2024-01-01' },
|
||||
{ id: 6, name: '旅行写入', code: 'travel:write', description: '创建/编辑旅行信息', resource_type: 'travel', created_at: '2024-01-01', updated_at: '2024-01-01' },
|
||||
{ id: 7, name: '动物读取', code: 'animal:read', description: '查看动物信息', resource_type: 'animal', created_at: '2024-01-01', updated_at: '2024-01-01' },
|
||||
{ id: 8, name: '动物写入', code: 'animal:write', description: '创建/编辑动物信息', resource_type: 'animal', created_at: '2024-01-01', updated_at: '2024-01-01' },
|
||||
{ id: 9, name: '订单读取', code: 'order:read', description: '查看订单信息', resource_type: 'order', created_at: '2024-01-01', updated_at: '2024-01-01' },
|
||||
{ id: 10, name: '订单写入', code: 'order:write', description: '创建/编辑订单信息', resource_type: 'order', created_at: '2024-01-01', updated_at: '2024-01-01' },
|
||||
{ id: 11, name: '推广读取', code: 'promotion:read', description: '查看推广信息', resource_type: 'promotion', created_at: '2024-01-01', updated_at: '2024-01-01' },
|
||||
{ id: 12, name: '推广写入', code: 'promotion:write', description: '创建/编辑推广信息', resource_type: 'promotion', created_at: '2024-01-01', updated_at: '2024-01-01' },
|
||||
{ id: 13, name: '系统读取', code: 'system:read', description: '查看系统信息', resource_type: 'system', created_at: '2024-01-01', updated_at: '2024-01-01' },
|
||||
{ id: 14, name: '系统写入', code: 'system:write', description: '创建/编辑系统信息', resource_type: 'system', created_at: '2024-01-01', updated_at: '2024-01-01' }
|
||||
]
|
||||
|
||||
// 模拟API响应格式
|
||||
const createSuccessResponse = (data: any) => ({
|
||||
success: true,
|
||||
@@ -173,6 +191,188 @@ export const mockSystemAPI = {
|
||||
}
|
||||
}
|
||||
|
||||
// 模拟权限API
|
||||
export const mockPermissionAPI = {
|
||||
getPermissions: async (params: any = {}) => {
|
||||
await delay(800)
|
||||
const { page = 1, pageSize = 10, keyword = '' } = params
|
||||
|
||||
// 根据关键词过滤权限
|
||||
let filteredPermissions = mockPermissions
|
||||
if (keyword) {
|
||||
filteredPermissions = mockPermissions.filter(p =>
|
||||
p.name.includes(keyword) ||
|
||||
p.code.includes(keyword) ||
|
||||
p.description.includes(keyword)
|
||||
)
|
||||
}
|
||||
|
||||
const start = (page - 1) * pageSize
|
||||
const end = start + pageSize
|
||||
const paginatedData = filteredPermissions.slice(start, end)
|
||||
|
||||
return createPaginatedResponse(paginatedData, page, pageSize, filteredPermissions.length)
|
||||
},
|
||||
|
||||
getPermission: async (id: number) => {
|
||||
await delay(500)
|
||||
const permission = mockPermissions.find(p => p.id === id)
|
||||
if (permission) {
|
||||
return createSuccessResponse(permission)
|
||||
}
|
||||
message.error('权限不存在')
|
||||
throw new Error('权限不存在')
|
||||
},
|
||||
|
||||
createPermission: async (data: any) => {
|
||||
await delay(500)
|
||||
const newPermission = {
|
||||
id: mockPermissions.length + 1,
|
||||
...data,
|
||||
created_at: new Date().toISOString().split('T')[0],
|
||||
updated_at: new Date().toISOString().split('T')[0]
|
||||
}
|
||||
mockPermissions.push(newPermission)
|
||||
return createSuccessResponse(newPermission)
|
||||
},
|
||||
|
||||
updatePermission: async (id: number, data: any) => {
|
||||
await delay(500)
|
||||
const index = mockPermissions.findIndex(p => p.id === id)
|
||||
if (index !== -1) {
|
||||
mockPermissions[index] = {
|
||||
...mockPermissions[index],
|
||||
...data,
|
||||
updated_at: new Date().toISOString().split('T')[0]
|
||||
}
|
||||
return createSuccessResponse(mockPermissions[index])
|
||||
}
|
||||
message.error('权限不存在')
|
||||
throw new Error('权限不存在')
|
||||
},
|
||||
|
||||
deletePermission: async (id: number) => {
|
||||
await delay(500)
|
||||
const index = mockPermissions.findIndex(p => p.id === id)
|
||||
if (index !== -1) {
|
||||
mockPermissions.splice(index, 1)
|
||||
return createSuccessResponse(null)
|
||||
}
|
||||
message.error('权限不存在')
|
||||
throw new Error('权限不存在')
|
||||
},
|
||||
|
||||
batchDeletePermissions: async (ids: number[]) => {
|
||||
await delay(500)
|
||||
const deletedCount = ids.filter(id => {
|
||||
const index = mockPermissions.findIndex(p => p.id === id)
|
||||
if (index !== -1) {
|
||||
mockPermissions.splice(index, 1)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}).length
|
||||
|
||||
return createSuccessResponse({
|
||||
message: `成功删除${deletedCount}个权限`,
|
||||
affectedRows: deletedCount
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 模拟仪表板数据
|
||||
const mockDashboardData = {
|
||||
stats: {
|
||||
userCount: 1280,
|
||||
newUserCount: 25,
|
||||
activeUserCount: 860,
|
||||
merchantCount: 42,
|
||||
newMerchantCount: 3,
|
||||
activeMerchantCount: 38,
|
||||
travelCount: 156,
|
||||
newTravelCount: 8,
|
||||
animalCount: 89,
|
||||
newAnimalCount: 5,
|
||||
orderCount: 342,
|
||||
newOrderCount: 12,
|
||||
orderAmount: 25680,
|
||||
onlineUserCount: 142,
|
||||
systemLoad: 45
|
||||
},
|
||||
userGrowth: [
|
||||
{ date: '2024-03-01', count: 1200 },
|
||||
{ date: '2024-03-02', count: 1210 },
|
||||
{ date: '2024-03-03', count: 1225 },
|
||||
{ date: '2024-03-04', count: 1235 },
|
||||
{ date: '2024-03-05', count: 1248 },
|
||||
{ date: '2024-03-06', count: 1262 },
|
||||
{ date: '2024-03-07', count: 1280 }
|
||||
],
|
||||
orderStats: [
|
||||
{ date: '2024-03-01', count: 28, amount: 2100 },
|
||||
{ date: '2024-03-02', count: 32, amount: 2450 },
|
||||
{ date: '2024-03-03', count: 25, amount: 1890 },
|
||||
{ date: '2024-03-04', count: 35, amount: 2680 },
|
||||
{ date: '2024-03-05', count: 42, amount: 3200 },
|
||||
{ date: '2024-03-06', count: 38, amount: 2890 },
|
||||
{ date: '2024-03-07', count: 45, amount: 3420 }
|
||||
],
|
||||
activities: [
|
||||
{
|
||||
id: 1,
|
||||
title: '新用户注册',
|
||||
description: '用户"旅行爱好者"完成了注册',
|
||||
avatar: 'https://api.dicebear.com/7.x/miniavs/svg?seed=1',
|
||||
time: '2分钟前'
|
||||
},
|
||||
{
|
||||
title: '旅行计划创建',
|
||||
description: '用户"探险家"发布了西藏旅行计划',
|
||||
avatar: 'https://api.dicebear.com/7.x/miniavs/svg?seed=2',
|
||||
time: '5分钟前'
|
||||
},
|
||||
{
|
||||
title: '动物认领',
|
||||
description: '用户"动物之友"认领了一只羊驼',
|
||||
avatar: 'https://api.dicebear.com/7.x/miniavs/svg?seed=3',
|
||||
time: '10分钟前'
|
||||
},
|
||||
{
|
||||
title: '订单完成',
|
||||
description: '花店"鲜花坊"完成了一笔鲜花订单',
|
||||
avatar: 'https://api.dicebear.com/7.x/miniavs/svg?seed=4',
|
||||
time: '15分钟前'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// 模拟仪表板API
|
||||
export const mockDashboardAPI = {
|
||||
getDashboardData: async () => {
|
||||
await delay(800)
|
||||
return createSuccessResponse(mockDashboardData)
|
||||
},
|
||||
|
||||
getUserGrowthData: async (days: number = 7) => {
|
||||
await delay(500)
|
||||
// 根据天数返回相应的数据
|
||||
const data = mockDashboardData.userGrowth.slice(-days)
|
||||
return createSuccessResponse(data)
|
||||
},
|
||||
|
||||
getOrderStatsData: async (days: number = 7) => {
|
||||
await delay(500)
|
||||
// 根据天数返回相应的数据
|
||||
const data = mockDashboardData.orderStats.slice(-days)
|
||||
return createSuccessResponse(data)
|
||||
},
|
||||
|
||||
getRecentActivities: async () => {
|
||||
await delay(500)
|
||||
return createSuccessResponse(mockDashboardData.activities)
|
||||
}
|
||||
}
|
||||
|
||||
// 导出所有模拟API
|
||||
export const mockAPI = {
|
||||
auth: mockAuthAPI,
|
||||
@@ -181,7 +381,9 @@ export const mockAPI = {
|
||||
travel: mockTravelAPI,
|
||||
animal: mockAnimalAPI,
|
||||
order: mockOrderAPI,
|
||||
system: mockSystemAPI
|
||||
system: mockSystemAPI,
|
||||
permission: mockPermissionAPI,
|
||||
dashboard: mockDashboardAPI // 添加仪表板API
|
||||
}
|
||||
|
||||
export default mockAPI
|
||||
87
admin-system/src/api/permission.ts
Normal file
87
admin-system/src/api/permission.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { request } from '.'
|
||||
import { mockAPI } from './mockData'
|
||||
import { createMockWrapper } from '@/config/mock'
|
||||
|
||||
// 定义权限相关类型
|
||||
export interface Permission {
|
||||
id: number
|
||||
name: string
|
||||
code: string
|
||||
description: string
|
||||
resource_type: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export interface PermissionQueryParams {
|
||||
page?: number
|
||||
pageSize?: number
|
||||
keyword?: string
|
||||
}
|
||||
|
||||
export interface PermissionCreateData {
|
||||
name: string
|
||||
code: string
|
||||
description: string
|
||||
resource_type: string
|
||||
}
|
||||
|
||||
export interface PermissionUpdateData {
|
||||
name?: string
|
||||
code?: string
|
||||
description?: string
|
||||
resource_type?: string
|
||||
}
|
||||
|
||||
export interface ApiResponse<T = any> {
|
||||
success: boolean
|
||||
code: number
|
||||
message: string
|
||||
data: T
|
||||
}
|
||||
|
||||
export interface PermissionListResponse {
|
||||
permissions: Permission[]
|
||||
pagination: {
|
||||
page: number
|
||||
pageSize: number
|
||||
total: number
|
||||
totalPages: number
|
||||
}
|
||||
}
|
||||
|
||||
// 获取权限列表
|
||||
export const getPermissions = (params?: PermissionQueryParams) =>
|
||||
request.get<ApiResponse<PermissionListResponse>>('/admin/permissions', { params })
|
||||
|
||||
// 获取权限详情
|
||||
export const getPermission = (id: number) =>
|
||||
request.get<ApiResponse<Permission>>(`/admin/permissions/${id}`)
|
||||
|
||||
// 创建权限
|
||||
export const createPermission = (data: PermissionCreateData) =>
|
||||
request.post<ApiResponse<Permission>>('/admin/permissions', data)
|
||||
|
||||
// 更新权限
|
||||
export const updatePermission = (id: number, data: PermissionUpdateData) =>
|
||||
request.put<ApiResponse<Permission>>(`/admin/permissions/${id}`, data)
|
||||
|
||||
// 删除权限
|
||||
export const deletePermission = (id: number) =>
|
||||
request.delete<ApiResponse<void>>(`/admin/permissions/${id}`)
|
||||
|
||||
// 批量删除权限
|
||||
export const batchDeletePermissions = (ids: number[]) =>
|
||||
request.post<ApiResponse<{ message: string; affectedRows: number }>>('/admin/permissions/batch-delete', { ids })
|
||||
|
||||
// 开发环境使用模拟数据
|
||||
const permissionAPI = createMockWrapper({
|
||||
getPermissions,
|
||||
getPermission,
|
||||
createPermission,
|
||||
updatePermission,
|
||||
deletePermission,
|
||||
batchDeletePermissions
|
||||
}, mockAPI.permission)
|
||||
|
||||
export default permissionAPI
|
||||
@@ -28,7 +28,7 @@
|
||||
<router-link to="/dashboard" />
|
||||
</a-menu-item>
|
||||
|
||||
<a-menu-item key="users">
|
||||
<a-menu-item v-if="hasPermission('user:read')" key="users">
|
||||
<template #icon>
|
||||
<UserOutlined />
|
||||
</template>
|
||||
@@ -36,7 +36,7 @@
|
||||
<router-link to="/users" />
|
||||
</a-menu-item>
|
||||
|
||||
<a-menu-item key="merchants">
|
||||
<a-menu-item v-if="hasPermission('merchant:read')" key="merchants">
|
||||
<template #icon>
|
||||
<ShopOutlined />
|
||||
</template>
|
||||
@@ -44,7 +44,7 @@
|
||||
<router-link to="/merchants" />
|
||||
</a-menu-item>
|
||||
|
||||
<a-menu-item key="travel">
|
||||
<a-menu-item v-if="hasPermission('travel:read')" key="travel">
|
||||
<template #icon>
|
||||
<CompassOutlined />
|
||||
</template>
|
||||
@@ -52,7 +52,7 @@
|
||||
<router-link to="/travel" />
|
||||
</a-menu-item>
|
||||
|
||||
<a-menu-item key="animals">
|
||||
<a-menu-item v-if="hasPermission('animal:read')" key="animals">
|
||||
<template #icon>
|
||||
<HeartOutlined />
|
||||
</template>
|
||||
@@ -60,7 +60,7 @@
|
||||
<router-link to="/animals" />
|
||||
</a-menu-item>
|
||||
|
||||
<a-menu-item key="orders">
|
||||
<a-menu-item v-if="hasPermission('order:read')" key="orders">
|
||||
<template #icon>
|
||||
<ShoppingCartOutlined />
|
||||
</template>
|
||||
@@ -68,7 +68,7 @@
|
||||
<router-link to="/orders" />
|
||||
</a-menu-item>
|
||||
|
||||
<a-menu-item key="promotion">
|
||||
<a-menu-item v-if="hasPermission('promotion:read')" key="promotion">
|
||||
<template #icon>
|
||||
<GiftOutlined />
|
||||
</template>
|
||||
@@ -76,7 +76,7 @@
|
||||
<router-link to="/promotion" />
|
||||
</a-menu-item>
|
||||
|
||||
<a-menu-item key="system">
|
||||
<a-menu-item v-if="hasPermission('system:read')" key="system">
|
||||
<template #icon>
|
||||
<SettingOutlined />
|
||||
</template>
|
||||
@@ -194,6 +194,11 @@ const currentRouteMeta = computed(() => route.meta || {})
|
||||
const userName = computed(() => appStore.state.user?.nickname || '管理员')
|
||||
const userAvatar = computed(() => appStore.state.user?.avatar || 'https://api.dicebear.com/7.x/miniavs/svg?seed=admin')
|
||||
|
||||
// 权限检查方法
|
||||
const hasPermission = (permission: string) => {
|
||||
return appStore.hasPermission(permission)
|
||||
}
|
||||
|
||||
// 监听路由变化
|
||||
router.afterEach((to) => {
|
||||
selectedKeys.value = [to.name as string]
|
||||
|
||||
33
admin-system/src/pages/NoPermission.vue
Normal file
33
admin-system/src/pages/NoPermission.vue
Normal file
@@ -0,0 +1,33 @@
|
||||
<template>
|
||||
<div class="no-permission">
|
||||
<a-result
|
||||
status="403"
|
||||
title="403"
|
||||
sub-title="抱歉,您没有权限访问此页面。"
|
||||
>
|
||||
<template #extra>
|
||||
<a-button type="primary" @click="goHome">返回首页</a-button>
|
||||
</template>
|
||||
</a-result>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const goHome = () => {
|
||||
router.push('/')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.no-permission {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
</style>
|
||||
@@ -12,7 +12,11 @@
|
||||
</template>
|
||||
刷新
|
||||
</a-button>
|
||||
<a-button type="primary" @click="showCreateModal">
|
||||
<a-button
|
||||
v-if="hasPermission('animal:write')"
|
||||
type="primary"
|
||||
@click="showCreateModal"
|
||||
>
|
||||
<template #icon>
|
||||
<PlusOutlined />
|
||||
</template>
|
||||
@@ -120,12 +124,21 @@
|
||||
查看
|
||||
</a-button>
|
||||
|
||||
<a-button size="small" @click="handleEditAnimal(record)">
|
||||
<a-button
|
||||
v-if="hasPermission('animal:write')"
|
||||
size="small"
|
||||
@click="handleEditAnimal(record)"
|
||||
>
|
||||
<EditOutlined />
|
||||
编辑
|
||||
</a-button>
|
||||
|
||||
<a-button size="small" danger @click="handleDeleteAnimal(record)">
|
||||
<a-button
|
||||
v-if="hasPermission('animal:write')"
|
||||
size="small"
|
||||
danger
|
||||
@click="handleDeleteAnimal(record)"
|
||||
>
|
||||
<DeleteOutlined />
|
||||
删除
|
||||
</a-button>
|
||||
@@ -204,7 +217,7 @@
|
||||
|
||||
<template v-else-if="column.key === 'actions'">
|
||||
<a-space :size="8">
|
||||
<template v-if="record.status === 'pending'">
|
||||
<template v-if="record.status === 'pending' && hasPermission('animal:write')">
|
||||
<a-button size="small" type="primary" @click="handleApproveClaim(record)">
|
||||
<CheckOutlined />
|
||||
通过
|
||||
@@ -337,6 +350,7 @@ import {
|
||||
CheckOutlined,
|
||||
CloseOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import { getAnimals, deleteAnimal, getAnimalClaims, approveAnimalClaim, rejectAnimalClaim, createAnimal, updateAnimal, getAnimal } from '@/api/animal'
|
||||
import type { Animal, AnimalClaim, AnimalCreateData, AnimalUpdateData } from '@/api/animal'
|
||||
|
||||
@@ -351,6 +365,13 @@ interface ClaimSearchForm {
|
||||
status: string
|
||||
}
|
||||
|
||||
const appStore = useAppStore()
|
||||
|
||||
// 权限检查方法
|
||||
const hasPermission = (permission: string) => {
|
||||
return appStore.hasPermission(permission)
|
||||
}
|
||||
|
||||
const activeTab = ref('animals')
|
||||
const loading = ref(false)
|
||||
const claimLoading = ref(false)
|
||||
@@ -497,7 +518,7 @@ const claimColumns = [
|
||||
}
|
||||
]
|
||||
|
||||
const fallbackImage = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjAiIGhlaWdodD0iNjAiIHZpZXdCb3g9IjAgMCA2MCA2MCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjYwIiBoZWlnaHQ9IjYwIiBmaWxsPSIjRkZGIi8+CjxwYXRoIGQ9Ik0zMCAxNUMzMS42NTY5IDE1IDMzIDE2LjM0MzEgMzMgMThDMzMgMTkuNjU2OSAzMS42NTY5IDIxIDMwIDIxQzI4LjM0MzEgMjEgMjcgMTkuNjU2OSAyNyAxOEMyNyAxNi4zNDMxIDI4LjM0MzEgMTUgMzAgMTVaIiBmaWxsPSIjQ0NDQ0NDIi8+CjxwYXRoIGQ9Ik0yMi41IDI1QzIyLjUgMjUuODI4NCAyMS44Mjg0IDI2LjUgMjEgMjYuNUgxOUMxOC4xNzE2IDI2LjUgMTcuNSAyNS44Mjg0IDE3LjUgMjVDMTcuNSAyNC4xNzE2IDE4LjE3MTYgMjMuNSAxOSAyMy55SDIxQzIxLjgyODQgMjMuNSAyMi41IDI0LjE3MTYgMjIuNSAyNVoiIGZpbGw9IiNDQ0NDQ0MiLz4KPHBhdGggZD0iTTQyLjUgMjVDNDIuNSAyNS44Mjg0IDQxLjgyODQgMjYuNSA0MSAyNi41SDM5QzM4LjE3MTYgMjYuNSAzNy41IDI1LjgyODQgMzcuNSAyNUMzNy41IDI0LjE3MTYgMzguMTcxNiAyMy41IDM5IDIzLjVMNDEgMjMuNUM0MS44Mjg0IDIzLjUgNDIuNSAyNC4xNzE2IDQyLjUgMjVaIiBmaWxsPSIjQ0NDQ0NDIi8+Cjwvc3ZnPgo='
|
||||
const fallbackImage = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjAiIGhlaWdodD0iNjAiIHZpZXdCb3g9IjAgMCA2MCA2MCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjYwIiBoZWlnaHQ9IjYwIiBmaWxsPSIjRkZGIi8+CjxwYXRoIGQ9Ik0zMCAxNUMzMS42NTY5IDE1IDMzIDE2LjM0MzEgMzMgMThDMzMgMTkuNjU2OSAzMS42NTY5IDIxIDMwIDIxQzI4LjM0MzEgMjEgMjcgMTkuNjU2OSAyNyAxOEMyNyAxNi4zNDMxIDI4LjM0MzEgMTUgMzAgMTVaIiBmaWxsPSIjQ0NDQ0NDIi8+CjxwYXRoIGQ9Ik0yMi41IDI1QzIyLjUgMjUuODI4NCAyMS44Mjg0IDI2LjUgMjEgMjYuNUgxOEMxOC4xNzE2IDI2LjUgMTcuNSAyNS44Mjg0IDE3LjUgMjVDMTcuNSAyNC4xNzE2IDE4LjE3MTYgMjMuNSAxOSAyMy45SDIxQzIxLjgyODQgMjMuNSAyMi41IDI0LjE3MTYgMjIuNSAyNVoiIGZpbGw9IiNDQ0NDQ0MiLz4KPHBhdGggZD0iTTQyLjUgMjVDNDIuNSAyNS44Mjg0IDQxLjgyODQgMjYuNSA0MSAyNi41SDM5QzM4LjE3MTYgMjYuNSAzNy41IDI1LjgyODQgMzcuNSAyNUMzNy41IDI0LjE3MTYgMzguMTcxNiAyMy41IDM5IDIzLjVMNDEgMjMuNUM0MS44Mjg0IDIzLjUgNDIuNSAyNC4xNzE2IDQyLjUgMjVaIiBmaWxsPSIjQ0NDQ0NDIi8+Cjwvc3ZnPgo='
|
||||
|
||||
// 类型映射
|
||||
const getTypeColor = (type: string) => {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="merchant-management">
|
||||
<a-page-header
|
||||
title="商家管理"
|
||||
sub-title="管理入驻商家信息"
|
||||
sub-title="管理系统商家信息"
|
||||
>
|
||||
<template #extra>
|
||||
<a-space>
|
||||
@@ -12,9 +12,13 @@
|
||||
</template>
|
||||
刷新
|
||||
</a-button>
|
||||
<a-button type="primary" @click="showCreateModal">
|
||||
<a-button
|
||||
v-if="hasPermission('merchant:write')"
|
||||
type="primary"
|
||||
@click="showCreateModal"
|
||||
>
|
||||
<template #icon>
|
||||
<ShopOutlined />
|
||||
<PlusOutlined />
|
||||
</template>
|
||||
新增商家
|
||||
</a-button>
|
||||
@@ -29,24 +33,11 @@
|
||||
<a-form-item label="关键词">
|
||||
<a-input
|
||||
v-model:value="searchForm.keyword"
|
||||
placeholder="商家名称/联系人/手机号"
|
||||
placeholder="商家名称/联系人"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="类型">
|
||||
<a-select
|
||||
v-model:value="searchForm.type"
|
||||
placeholder="全部类型"
|
||||
style="width: 120px"
|
||||
allow-clear
|
||||
>
|
||||
<a-select-option value="flower_shop">花店</a-select-option>
|
||||
<a-select-option value="activity_organizer">活动组织</a-select-option>
|
||||
<a-select-option value="farm_owner">农场主</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="状态">
|
||||
<a-select
|
||||
v-model:value="searchForm.status"
|
||||
@@ -57,7 +48,22 @@
|
||||
<a-select-option value="pending">待审核</a-select-option>
|
||||
<a-select-option value="approved">已通过</a-select-option>
|
||||
<a-select-option value="rejected">已拒绝</a-select-option>
|
||||
<a-select-option value="disabled">已禁用</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="类型">
|
||||
<a-select
|
||||
v-model:value="searchForm.type"
|
||||
placeholder="全部类型"
|
||||
style="width: 120px"
|
||||
allow-clear
|
||||
>
|
||||
<a-select-option value="farm">农场</a-select-option>
|
||||
<a-select-option value="hotel">酒店</a-select-option>
|
||||
<a-select-option value="restaurant">餐厅</a-select-option>
|
||||
<a-select-option value="transport">交通</a-select-option>
|
||||
<a-select-option value="shop">商店</a-select-option>
|
||||
<a-select-option value="other">其他</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
@@ -85,15 +91,15 @@
|
||||
@change="handleTableChange"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'type'">
|
||||
<a-tag :color="getTypeColor(record.type)">
|
||||
{{ getTypeText(record.type) }}
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-tag :color="getStatusColor(record.status)">
|
||||
{{ getStatusText(record.status) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'status'">
|
||||
<a-tag :color="getStatusColor(record.status)">
|
||||
{{ getStatusText(record.status) }}
|
||||
<template v-else-if="column.key === 'type'">
|
||||
<a-tag :color="getTypeColor(record.type)">
|
||||
{{ getTypeText(record.type) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
@@ -104,12 +110,16 @@
|
||||
查看
|
||||
</a-button>
|
||||
|
||||
<a-button size="small" @click="showEditModal(record)">
|
||||
<a-button
|
||||
v-if="hasPermission('merchant:write')"
|
||||
size="small"
|
||||
@click="handleEdit(record)"
|
||||
>
|
||||
<EditOutlined />
|
||||
编辑
|
||||
</a-button>
|
||||
|
||||
<template v-if="record.status === 'pending'">
|
||||
<template v-if="record.status === 'pending' && hasPermission('merchant:write')">
|
||||
<a-button size="small" type="primary" @click="handleApprove(record)">
|
||||
<CheckOutlined />
|
||||
通过
|
||||
@@ -120,19 +130,15 @@
|
||||
</a-button>
|
||||
</template>
|
||||
|
||||
<template v-else-if="record.status === 'approved'">
|
||||
<a-button size="small" danger @click="handleDisable(record)">
|
||||
<StopOutlined />
|
||||
禁用
|
||||
</a-button>
|
||||
</template>
|
||||
|
||||
<template v-else-if="record.status === 'disabled'">
|
||||
<a-button size="small" type="primary" @click="handleEnable(record)">
|
||||
<PlayCircleOutlined />
|
||||
启用
|
||||
</a-button>
|
||||
</template>
|
||||
<a-button
|
||||
v-if="record.status === 'approved' && hasPermission('merchant:write')"
|
||||
size="small"
|
||||
danger
|
||||
@click="handleDisable(record)"
|
||||
>
|
||||
<StopOutlined />
|
||||
禁用
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
@@ -154,60 +160,56 @@
|
||||
:rules="formRules"
|
||||
layout="vertical"
|
||||
>
|
||||
<a-form-item label="商家名称" name="name">
|
||||
<a-input v-model:value="currentMerchant.name" placeholder="请输入商家名称" />
|
||||
</a-form-item>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="商家名称" name="business_name">
|
||||
<a-input v-model:value="currentMerchant.business_name" placeholder="请输入商家名称" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="商家类型" name="merchant_type">
|
||||
<a-select v-model:value="currentMerchant.merchant_type" placeholder="请选择商家类型">
|
||||
<a-select-option value="flower_shop">花店</a-select-option>
|
||||
<a-select-option value="activity_organizer">活动组织</a-select-option>
|
||||
<a-select-option value="farm_owner">农场主</a-select-option>
|
||||
<a-form-item label="类型" name="type">
|
||||
<a-select v-model:value="currentMerchant.type" placeholder="请选择类型">
|
||||
<a-select-option value="farm">农场</a-select-option>
|
||||
<a-select-option value="hotel">酒店</a-select-option>
|
||||
<a-select-option value="restaurant">餐厅</a-select-option>
|
||||
<a-select-option value="transport">交通</a-select-option>
|
||||
<a-select-option value="shop">商店</a-select-option>
|
||||
<a-select-option value="other">其他</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="联系人" name="contact_person">
|
||||
<a-input v-model:value="currentMerchant.contact_person" placeholder="请输入联系人" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="联系电话" name="contact_phone">
|
||||
<a-input v-model:value="currentMerchant.contact_phone" placeholder="请输入联系电话" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="联系邮箱" name="contact_email">
|
||||
<a-input v-model:value="currentMerchant.contact_email" placeholder="请输入联系邮箱" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="状态" name="status">
|
||||
<a-select v-model:value="currentMerchant.status" placeholder="请选择状态">
|
||||
<a-select-option value="pending">待审核</a-select-option>
|
||||
<a-select-option value="approved">已通过</a-select-option>
|
||||
<a-select-option value="rejected">已拒绝</a-select-option>
|
||||
<a-select-option value="disabled">已禁用</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-form-item label="详细地址" name="address">
|
||||
<a-input v-model:value="currentMerchant.address" placeholder="请输入详细地址" />
|
||||
<a-form-item label="联系人" name="contact_person">
|
||||
<a-input v-model:value="currentMerchant.contact_person" placeholder="请输入联系人" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="备注" name="remark">
|
||||
<a-textarea v-model:value="currentMerchant.remark" placeholder="请输入备注" :rows="3" />
|
||||
<a-form-item label="联系电话" name="contact_phone">
|
||||
<a-input v-model:value="currentMerchant.contact_phone" placeholder="请输入联系电话" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="地址" name="address">
|
||||
<a-textarea
|
||||
v-model:value="currentMerchant.address"
|
||||
placeholder="请输入地址"
|
||||
:rows="2"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="描述" name="description">
|
||||
<a-textarea
|
||||
v-model:value="currentMerchant.description"
|
||||
placeholder="请输入商家描述"
|
||||
:rows="3"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
@@ -215,35 +217,49 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, h } from 'vue'
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { message, Modal, type FormInstance } from 'ant-design-vue'
|
||||
import type { TableProps } from 'ant-design-vue'
|
||||
import {
|
||||
ShopOutlined,
|
||||
SearchOutlined,
|
||||
ReloadOutlined,
|
||||
PlusOutlined,
|
||||
EyeOutlined,
|
||||
EditOutlined,
|
||||
CheckOutlined,
|
||||
CloseOutlined,
|
||||
StopOutlined,
|
||||
PlayCircleOutlined,
|
||||
EditOutlined
|
||||
StopOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import { getMerchants, getMerchant, approveMerchant, rejectMerchant, disableMerchant, enableMerchant, createMerchant, updateMerchant } from '@/api/merchant'
|
||||
import type { Merchant, MerchantCreateData, MerchantUpdateData } from '@/api/merchant'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import {
|
||||
getMerchants,
|
||||
getMerchant,
|
||||
createMerchant,
|
||||
updateMerchant,
|
||||
approveMerchant,
|
||||
rejectMerchant,
|
||||
disableMerchant
|
||||
} from '@/api/merchant'
|
||||
import type { Merchant, MerchantQueryParams } from '@/api/merchant'
|
||||
|
||||
interface SearchForm {
|
||||
keyword: string
|
||||
type: string
|
||||
status: string
|
||||
type: string
|
||||
}
|
||||
|
||||
const appStore = useAppStore()
|
||||
|
||||
// 权限检查方法
|
||||
const hasPermission = (permission: string) => {
|
||||
return appStore.hasPermission(permission)
|
||||
}
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
const searchForm = reactive<SearchForm>({
|
||||
keyword: '',
|
||||
type: '',
|
||||
status: ''
|
||||
status: '',
|
||||
type: ''
|
||||
})
|
||||
|
||||
const merchantList = ref<Merchant[]>([])
|
||||
@@ -259,8 +275,8 @@ const pagination = reactive({
|
||||
const columns = [
|
||||
{
|
||||
title: '商家名称',
|
||||
dataIndex: 'business_name',
|
||||
key: 'business_name',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
width: 150
|
||||
},
|
||||
{
|
||||
@@ -288,7 +304,7 @@ const columns = [
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '入驻时间',
|
||||
title: '创建时间',
|
||||
dataIndex: 'created_at',
|
||||
key: 'created_at',
|
||||
width: 120
|
||||
@@ -301,32 +317,12 @@ const columns = [
|
||||
}
|
||||
]
|
||||
|
||||
// 类型映射
|
||||
const getTypeColor = (type: string) => {
|
||||
const colors = {
|
||||
flower_shop: 'pink',
|
||||
activity_organizer: 'green',
|
||||
farm_owner: 'orange'
|
||||
}
|
||||
return colors[type as keyof typeof colors] || 'default'
|
||||
}
|
||||
|
||||
const getTypeText = (type: string) => {
|
||||
const texts = {
|
||||
flower_shop: '花店',
|
||||
activity_organizer: '活动组织',
|
||||
farm_owner: '农场'
|
||||
}
|
||||
return texts[type as keyof typeof texts] || '未知'
|
||||
}
|
||||
|
||||
// 状态映射
|
||||
const getStatusColor = (status: string) => {
|
||||
const colors = {
|
||||
pending: 'orange',
|
||||
approved: 'green',
|
||||
rejected: 'red',
|
||||
disabled: 'default'
|
||||
rejected: 'red'
|
||||
}
|
||||
return colors[status as keyof typeof colors] || 'default'
|
||||
}
|
||||
@@ -335,12 +331,36 @@ const getStatusText = (status: string) => {
|
||||
const texts = {
|
||||
pending: '待审核',
|
||||
approved: '已通过',
|
||||
rejected: '已拒绝',
|
||||
disabled: '已禁用'
|
||||
rejected: '已拒绝'
|
||||
}
|
||||
return texts[status as keyof typeof texts] || '未知'
|
||||
}
|
||||
|
||||
// 类型映射
|
||||
const getTypeColor = (type: string) => {
|
||||
const colors = {
|
||||
farm: 'green',
|
||||
hotel: 'blue',
|
||||
restaurant: 'orange',
|
||||
transport: 'purple',
|
||||
shop: 'pink',
|
||||
other: 'gray'
|
||||
}
|
||||
return colors[type as keyof typeof colors] || 'default'
|
||||
}
|
||||
|
||||
const getTypeText = (type: string) => {
|
||||
const texts = {
|
||||
farm: '农场',
|
||||
hotel: '酒店',
|
||||
restaurant: '餐厅',
|
||||
transport: '交通',
|
||||
shop: '商店',
|
||||
other: '其他'
|
||||
}
|
||||
return texts[type as keyof typeof texts] || '未知'
|
||||
}
|
||||
|
||||
// 添加模态框相关状态
|
||||
const modalVisible = ref(false)
|
||||
const modalLoading = ref(false)
|
||||
@@ -353,29 +373,21 @@ const currentMerchant = ref<Partial<Merchant>>({})
|
||||
|
||||
// 表单验证规则
|
||||
const formRules = {
|
||||
business_name: [
|
||||
{ required: true, message: '请输入商家名称' },
|
||||
{ min: 2, max: 50, message: '商家名称长度为2-50个字符' }
|
||||
name: [
|
||||
{ required: true, message: '请输入商家名称' }
|
||||
],
|
||||
merchant_type: [
|
||||
{ required: true, message: '请选择商家类型' }
|
||||
],
|
||||
contact_person: [
|
||||
{ required: true, message: '请输入联系人' },
|
||||
{ min: 2, max: 20, message: '联系人长度为2-20个字符' }
|
||||
],
|
||||
contact_phone: [
|
||||
{ required: true, message: '请输入联系电话' },
|
||||
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号' }
|
||||
],
|
||||
contact_email: [
|
||||
{ type: 'email', message: '请输入正确的邮箱地址' }
|
||||
type: [
|
||||
{ required: true, message: '请选择类型' }
|
||||
],
|
||||
status: [
|
||||
{ required: true, message: '请选择状态' }
|
||||
],
|
||||
address: [
|
||||
{ min: 5, max: 100, message: '地址长度为5-100个字符' }
|
||||
contact_person: [
|
||||
{ required: true, message: '请输入联系人' }
|
||||
],
|
||||
contact_phone: [
|
||||
{ required: true, message: '请输入联系电话' },
|
||||
{ pattern: /^1[3-9]\d{9}$/, message: '手机号格式不正确' }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -388,16 +400,17 @@ onMounted(() => {
|
||||
const loadMerchants = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await getMerchants({
|
||||
const params: MerchantQueryParams = {
|
||||
page: pagination.current,
|
||||
pageSize: pagination.pageSize,
|
||||
limit: pagination.pageSize,
|
||||
keyword: searchForm.keyword,
|
||||
type: searchForm.type,
|
||||
status: searchForm.status
|
||||
})
|
||||
status: searchForm.status,
|
||||
type: searchForm.type
|
||||
}
|
||||
|
||||
merchantList.value = response.data
|
||||
pagination.total = response.pagination?.total || 0
|
||||
const response = await getMerchants(params)
|
||||
merchantList.value = response.data.list
|
||||
pagination.total = response.data.pagination.total
|
||||
} catch (error) {
|
||||
message.error('加载商家列表失败')
|
||||
} finally {
|
||||
@@ -413,8 +426,8 @@ const handleSearch = () => {
|
||||
const handleReset = () => {
|
||||
Object.assign(searchForm, {
|
||||
keyword: '',
|
||||
type: '',
|
||||
status: ''
|
||||
status: '',
|
||||
type: ''
|
||||
})
|
||||
pagination.current = 1
|
||||
loadMerchants()
|
||||
@@ -442,105 +455,33 @@ const handleView = async (record: Merchant) => {
|
||||
column: 1,
|
||||
bordered: true
|
||||
}, [
|
||||
h('a-descriptions-item', { label: '商家ID' }, response.data.id),
|
||||
h('a-descriptions-item', { label: '商家名称' }, response.data.business_name),
|
||||
h('a-descriptions-item', { label: '商家类型' }, getTypeText(response.data.merchant_type)),
|
||||
h('a-descriptions-item', { label: '联系人' }, response.data.contact_person),
|
||||
h('a-descriptions-item', { label: '联系电话' }, response.data.contact_phone),
|
||||
h('a-descriptions-item', { label: '商家名称' }, response.data.name),
|
||||
h('a-descriptions-item', { label: '类型' }, [
|
||||
h('a-tag', { color: getTypeColor(response.data.type) }, getTypeText(response.data.type))
|
||||
]),
|
||||
h('a-descriptions-item', { label: '状态' }, [
|
||||
h('a-tag', { color: getStatusColor(response.data.status) }, getStatusText(response.data.status))
|
||||
]),
|
||||
h('a-descriptions-item', { label: '入驻时间' }, response.data.created_at),
|
||||
h('a-descriptions-item', { label: '更新时间' }, response.data.updated_at)
|
||||
h('a-descriptions-item', { label: '联系人' }, response.data.contact_person),
|
||||
h('a-descriptions-item', { label: '联系电话' }, response.data.contact_phone),
|
||||
h('a-descriptions-item', { label: '地址' }, response.data.address || '-'),
|
||||
h('a-descriptions-item', { label: '描述' }, response.data.description || '-'),
|
||||
h('a-descriptions-item', { label: '创建时间' }, response.data.created_at)
|
||||
])
|
||||
])
|
||||
]),
|
||||
okText: '关闭'
|
||||
})
|
||||
} catch (error) {
|
||||
message.error('获取商家详情失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleApprove = async (record: Merchant) => {
|
||||
Modal.confirm({
|
||||
title: '确认通过',
|
||||
content: `确定要通过商家 "${record.business_name}" 的入驻申请吗?`,
|
||||
onOk: async () => {
|
||||
try {
|
||||
await approveMerchant(record.id)
|
||||
message.success('商家入驻申请已通过')
|
||||
loadMerchants()
|
||||
} catch (error) {
|
||||
message.error('操作失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleReject = async (record: Merchant) => {
|
||||
Modal.confirm({
|
||||
title: '确认拒绝',
|
||||
content: `确定要拒绝商家 "${record.business_name}" 的入驻申请吗?`,
|
||||
onOk: async () => {
|
||||
try {
|
||||
await rejectMerchant(record.id, '拒绝原因')
|
||||
message.success('商家入驻申请已拒绝')
|
||||
loadMerchants()
|
||||
} catch (error) {
|
||||
message.error('操作失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleDisable = async (record: Merchant) => {
|
||||
Modal.confirm({
|
||||
title: '确认禁用',
|
||||
content: `确定要禁用商家 "${record.business_name}" 吗?`,
|
||||
onOk: async () => {
|
||||
try {
|
||||
await disableMerchant(record.id)
|
||||
message.success('商家已禁用')
|
||||
loadMerchants()
|
||||
} catch (error) {
|
||||
message.error('操作失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleEnable = async (record: Merchant) => {
|
||||
Modal.confirm({
|
||||
title: '确认启用',
|
||||
content: `确定要启用商家 "${record.business_name}" 吗?`,
|
||||
onOk: async () => {
|
||||
try {
|
||||
await enableMerchant(record.id)
|
||||
message.success('商家已启用')
|
||||
loadMerchants()
|
||||
} catch (error) {
|
||||
message.error('操作失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const showCreateModal = () => {
|
||||
modalTitle.value = '新增商家'
|
||||
isEditing.value = false
|
||||
currentMerchant.value = {
|
||||
status: 'pending'
|
||||
}
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
const showEditModal = async (record: Merchant) => {
|
||||
const handleEdit = async (record: Merchant) => {
|
||||
try {
|
||||
modalLoading.value = true
|
||||
const response = await getMerchant(record.id)
|
||||
modalTitle.value = '编辑商家'
|
||||
isEditing.value = true
|
||||
|
||||
// 获取商家详情
|
||||
const response = await getMerchant(record.id)
|
||||
currentMerchant.value = response.data
|
||||
modalVisible.value = true
|
||||
} catch (error) {
|
||||
@@ -550,6 +491,16 @@ const showEditModal = async (record: Merchant) => {
|
||||
}
|
||||
}
|
||||
|
||||
const showCreateModal = () => {
|
||||
modalTitle.value = '新增商家'
|
||||
isEditing.value = false
|
||||
currentMerchant.value = {
|
||||
status: 'pending',
|
||||
type: 'farm'
|
||||
}
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
const handleModalOk = () => {
|
||||
merchantFormRef.value
|
||||
?.validate()
|
||||
@@ -573,45 +524,12 @@ const handleModalCancel = () => {
|
||||
const handleCreateMerchant = async () => {
|
||||
try {
|
||||
modalLoading.value = true
|
||||
|
||||
// 前端数据验证
|
||||
if (!currentMerchant.value.business_name) {
|
||||
message.error('商家名称不能为空')
|
||||
return
|
||||
}
|
||||
|
||||
if (!currentMerchant.value.merchant_type) {
|
||||
message.error('请选择商家类型')
|
||||
return
|
||||
}
|
||||
|
||||
if (!currentMerchant.value.contact_person) {
|
||||
message.error('联系人不能为空')
|
||||
return
|
||||
}
|
||||
|
||||
if (!currentMerchant.value.contact_phone) {
|
||||
message.error('联系电话不能为空')
|
||||
return
|
||||
}
|
||||
|
||||
if (!/^1[3-9]\d{9}$/.test(currentMerchant.value.contact_phone)) {
|
||||
message.error('请输入正确的手机号')
|
||||
return
|
||||
}
|
||||
|
||||
if (currentMerchant.value.contact_email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(currentMerchant.value.contact_email)) {
|
||||
message.error('请输入正确的邮箱地址')
|
||||
return
|
||||
}
|
||||
|
||||
await createMerchant(currentMerchant.value as MerchantCreateData)
|
||||
await createMerchant(currentMerchant.value as any)
|
||||
message.success('创建商家成功')
|
||||
modalVisible.value = false
|
||||
loadMerchants()
|
||||
} catch (error) {
|
||||
console.error('创建商家失败:', error)
|
||||
message.error('创建商家失败: ' + (error as Error).message)
|
||||
message.error('创建商家失败')
|
||||
} finally {
|
||||
modalLoading.value = false
|
||||
}
|
||||
@@ -620,50 +538,64 @@ const handleCreateMerchant = async () => {
|
||||
const handleUpdateMerchant = async () => {
|
||||
try {
|
||||
modalLoading.value = true
|
||||
|
||||
// 前端数据验证
|
||||
if (!currentMerchant.value.business_name) {
|
||||
message.error('商家名称不能为空')
|
||||
return
|
||||
}
|
||||
|
||||
if (!currentMerchant.value.merchant_type) {
|
||||
message.error('请选择商家类型')
|
||||
return
|
||||
}
|
||||
|
||||
if (!currentMerchant.value.contact_person) {
|
||||
message.error('联系人不能为空')
|
||||
return
|
||||
}
|
||||
|
||||
if (!currentMerchant.value.contact_phone) {
|
||||
message.error('联系电话不能为空')
|
||||
return
|
||||
}
|
||||
|
||||
if (!/^1[3-9]\d{9}$/.test(currentMerchant.value.contact_phone)) {
|
||||
message.error('请输入正确的手机号')
|
||||
return
|
||||
}
|
||||
|
||||
if (currentMerchant.value.contact_email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(currentMerchant.value.contact_email)) {
|
||||
message.error('请输入正确的邮箱地址')
|
||||
return
|
||||
}
|
||||
|
||||
await updateMerchant(currentMerchant.value.id!, currentMerchant.value as MerchantUpdateData)
|
||||
await updateMerchant(currentMerchant.value.id!, currentMerchant.value as any)
|
||||
message.success('更新商家成功')
|
||||
modalVisible.value = false
|
||||
loadMerchants()
|
||||
} catch (error) {
|
||||
console.error('更新商家失败:', error)
|
||||
message.error('更新商家失败: ' + (error as Error).message)
|
||||
message.error('更新商家失败')
|
||||
} finally {
|
||||
modalLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleApprove = async (record: Merchant) => {
|
||||
Modal.confirm({
|
||||
title: '确认通过',
|
||||
content: `确定要通过商家 "${record.name}" 的审核吗?`,
|
||||
onOk: async () => {
|
||||
try {
|
||||
await approveMerchant(record.id)
|
||||
message.success('商家审核已通过')
|
||||
loadMerchants()
|
||||
} catch (error) {
|
||||
message.error('操作失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleReject = async (record: Merchant) => {
|
||||
Modal.confirm({
|
||||
title: '确认拒绝',
|
||||
content: `确定要拒绝商家 "${record.name}" 的审核吗?`,
|
||||
onOk: async () => {
|
||||
try {
|
||||
await rejectMerchant(record.id)
|
||||
message.success('商家审核已拒绝')
|
||||
loadMerchants()
|
||||
} catch (error) {
|
||||
message.error('操作失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleDisable = async (record: Merchant) => {
|
||||
Modal.confirm({
|
||||
title: '确认禁用',
|
||||
content: `确定要禁用商家 "${record.name}" 吗?`,
|
||||
onOk: async () => {
|
||||
try {
|
||||
await disableMerchant(record.id)
|
||||
message.success('商家已禁用')
|
||||
loadMerchants()
|
||||
} catch (error) {
|
||||
message.error('操作失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
@@ -679,4 +611,9 @@ const handleUpdateMerchant = async () => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-table-thead > tr > th) {
|
||||
background: #fafafa;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
@@ -154,28 +154,28 @@
|
||||
查看
|
||||
</a-button>
|
||||
|
||||
<template v-if="record.status === 'pending'">
|
||||
<template v-if="record.status === 'pending' && hasPermission('order:write')">
|
||||
<a-button size="small" type="primary" @click="handlePay(record)">
|
||||
<PayCircleOutlined />
|
||||
支付
|
||||
</a-button>
|
||||
</template>
|
||||
|
||||
<template v-else-if="record.status === 'paid'">
|
||||
<template v-else-if="record.status === 'paid' && hasPermission('order:write')">
|
||||
<a-button size="small" type="primary" @click="handleShip(record)">
|
||||
<SendOutlined />
|
||||
发货
|
||||
</a-button>
|
||||
</template>
|
||||
|
||||
<template v-else-if="record.status === 'shipped'">
|
||||
<template v-else-if="record.status === 'shipped' && hasPermission('order:write')">
|
||||
<a-button size="small" type="primary" @click="handleComplete(record)">
|
||||
<CheckCircleOutlined />
|
||||
完成
|
||||
</a-button>
|
||||
</template>
|
||||
|
||||
<a-dropdown>
|
||||
<a-dropdown v-if="hasPermission('order:write')">
|
||||
<a-button size="small">
|
||||
更多
|
||||
<DownOutlined />
|
||||
@@ -255,6 +255,7 @@ import {
|
||||
DownOutlined,
|
||||
EditOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import {
|
||||
getOrders,
|
||||
getOrder,
|
||||
@@ -274,6 +275,13 @@ interface SearchForm {
|
||||
orderTime: any[]
|
||||
}
|
||||
|
||||
const appStore = useAppStore()
|
||||
|
||||
// 权限检查方法
|
||||
const hasPermission = (permission: string) => {
|
||||
return appStore.hasPermission(permission)
|
||||
}
|
||||
|
||||
const loading = ref(false)
|
||||
const statistics = ref<OrderStatistics>({
|
||||
today_orders: 0,
|
||||
@@ -482,7 +490,7 @@ const handleView = async (record: Order) => {
|
||||
])
|
||||
]),
|
||||
okText: '关闭',
|
||||
footer: (_, { OkBtn }) => h('div', { style: 'text-align: right;' }, [
|
||||
footer: (_, { OkBtn }) => hasPermission('order:write') ? h('div', { style: 'text-align: right;' }, [
|
||||
h('a-button', {
|
||||
onClick: () => {
|
||||
Modal.destroyAll()
|
||||
@@ -490,7 +498,7 @@ const handleView = async (record: Order) => {
|
||||
}
|
||||
}, '编辑备注'),
|
||||
h(OkBtn)
|
||||
])
|
||||
]) : h(OkBtn)
|
||||
})
|
||||
} catch (error) {
|
||||
message.error('获取订单详情失败')
|
||||
|
||||
418
admin-system/src/pages/permission/index.vue
Normal file
418
admin-system/src/pages/permission/index.vue
Normal file
@@ -0,0 +1,418 @@
|
||||
<template>
|
||||
<div class="permission-management">
|
||||
<a-page-header
|
||||
title="权限管理"
|
||||
sub-title="管理系统用户权限"
|
||||
>
|
||||
<template #extra>
|
||||
<a-space>
|
||||
<a-button @click="handleRefresh">
|
||||
<template #icon>
|
||||
<ReloadOutlined />
|
||||
</template>
|
||||
刷新
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="hasPermission('system:write')"
|
||||
type="primary"
|
||||
@click="showCreateModal"
|
||||
>
|
||||
<template #icon>
|
||||
<PlusOutlined />
|
||||
</template>
|
||||
新增权限
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-page-header>
|
||||
|
||||
<a-card>
|
||||
<!-- 搜索区域 -->
|
||||
<div class="search-container">
|
||||
<a-form layout="inline" :model="searchForm">
|
||||
<a-form-item label="关键词">
|
||||
<a-input
|
||||
v-model:value="searchForm.keyword"
|
||||
placeholder="权限名称/描述"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
<a-button type="primary" @click="handleSearch">
|
||||
<template #icon>
|
||||
<SearchOutlined />
|
||||
</template>
|
||||
搜索
|
||||
</a-button>
|
||||
<a-button style="margin-left: 8px" @click="handleReset">
|
||||
重置
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
|
||||
<!-- 权限表格 -->
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="permissionList"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
:row-key="record => record.id"
|
||||
@change="handleTableChange"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'actions'">
|
||||
<a-space :size="8">
|
||||
<a-button
|
||||
v-if="hasPermission('system:write')"
|
||||
size="small"
|
||||
@click="handleEdit(record)"
|
||||
>
|
||||
<EditOutlined />
|
||||
编辑
|
||||
</a-button>
|
||||
|
||||
<a-button
|
||||
v-if="hasPermission('system:write')"
|
||||
size="small"
|
||||
danger
|
||||
@click="handleDelete(record)"
|
||||
>
|
||||
<DeleteOutlined />
|
||||
删除
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
|
||||
<!-- 创建/编辑权限模态框 -->
|
||||
<a-modal
|
||||
v-model:open="modalVisible"
|
||||
:title="modalTitle"
|
||||
:confirm-loading="modalLoading"
|
||||
@ok="handleModalOk"
|
||||
@cancel="handleModalCancel"
|
||||
width="600px"
|
||||
>
|
||||
<a-form
|
||||
ref="permissionFormRef"
|
||||
:model="currentPermission"
|
||||
:rules="formRules"
|
||||
layout="vertical"
|
||||
>
|
||||
<a-form-item label="权限名称" name="name">
|
||||
<a-input v-model:value="currentPermission.name" placeholder="请输入权限名称" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="权限标识" name="code">
|
||||
<a-input v-model:value="currentPermission.code" placeholder="请输入权限标识,如 user:read" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="描述" name="description">
|
||||
<a-textarea
|
||||
v-model:value="currentPermission.description"
|
||||
placeholder="请输入权限描述"
|
||||
:rows="3"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="资源类型" name="resourceType">
|
||||
<a-select v-model:value="currentPermission.resource_type" placeholder="请选择资源类型">
|
||||
<a-select-option value="user">用户</a-select-option>
|
||||
<a-select-option value="merchant">商家</a-select-option>
|
||||
<a-select-option value="travel">旅行</a-select-option>
|
||||
<a-select-option value="animal">动物</a-select-option>
|
||||
<a-select-option value="order">订单</a-select-option>
|
||||
<a-select-option value="promotion">推广</a-select-option>
|
||||
<a-select-option value="system">系统</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { message, Modal, type FormInstance } from 'ant-design-vue'
|
||||
import type { TableProps } from 'ant-design-vue'
|
||||
import {
|
||||
SearchOutlined,
|
||||
ReloadOutlined,
|
||||
PlusOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import {
|
||||
getPermissions,
|
||||
createPermission,
|
||||
updatePermission,
|
||||
deletePermission
|
||||
} from '@/api/permission'
|
||||
import type { Permission } from '@/api/permission'
|
||||
|
||||
interface SearchForm {
|
||||
keyword: string
|
||||
}
|
||||
|
||||
const appStore = useAppStore()
|
||||
|
||||
// 权限检查方法
|
||||
const hasPermission = (permission: string) => {
|
||||
return appStore.hasPermission(permission)
|
||||
}
|
||||
|
||||
const loading = ref(false)
|
||||
const searchForm = reactive<SearchForm>({
|
||||
keyword: ''
|
||||
})
|
||||
|
||||
const permissionList = ref<Permission[]>([])
|
||||
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 20,
|
||||
total: 0,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total: number) => `共 ${total} 条记录`
|
||||
})
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '权限名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
width: 150
|
||||
},
|
||||
{
|
||||
title: '权限标识',
|
||||
dataIndex: 'code',
|
||||
key: 'code',
|
||||
width: 150
|
||||
},
|
||||
{
|
||||
title: '描述',
|
||||
dataIndex: 'description',
|
||||
key: 'description',
|
||||
width: 200
|
||||
},
|
||||
{
|
||||
title: '资源类型',
|
||||
dataIndex: 'resource_type',
|
||||
key: 'resource_type',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'created_at',
|
||||
key: 'created_at',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 150,
|
||||
align: 'center'
|
||||
}
|
||||
]
|
||||
|
||||
// 添加模态框相关状态
|
||||
const modalVisible = ref(false)
|
||||
const modalLoading = ref(false)
|
||||
const modalTitle = ref('新增权限')
|
||||
const isEditing = ref(false)
|
||||
const permissionFormRef = ref<FormInstance>()
|
||||
|
||||
// 当前权限数据
|
||||
const currentPermission = ref<Partial<Permission>>({
|
||||
resource_type: 'user' // 设置默认资源类型
|
||||
})
|
||||
|
||||
// 表单验证规则
|
||||
const formRules = {
|
||||
name: [
|
||||
{ required: true, message: '请输入权限名称' }
|
||||
],
|
||||
code: [
|
||||
{ required: true, message: '请输入权限标识' }
|
||||
],
|
||||
resourceType: [
|
||||
{ required: true, message: '请选择资源类型' }
|
||||
]
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
loadPermissions()
|
||||
})
|
||||
|
||||
// 方法
|
||||
const loadPermissions = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await getPermissions({
|
||||
page: pagination.current,
|
||||
pageSize: pagination.pageSize,
|
||||
keyword: searchForm.keyword
|
||||
})
|
||||
|
||||
if (response.success) {
|
||||
permissionList.value = response.data.permissions
|
||||
pagination.total = response.data.pagination.total
|
||||
} else {
|
||||
message.error('加载权限列表失败')
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('加载权限列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
pagination.current = 1
|
||||
loadPermissions()
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
Object.assign(searchForm, {
|
||||
keyword: ''
|
||||
})
|
||||
pagination.current = 1
|
||||
loadPermissions()
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
loadPermissions()
|
||||
message.success('数据已刷新')
|
||||
}
|
||||
|
||||
const handleTableChange: TableProps['onChange'] = (pag) => {
|
||||
pagination.current = pag.current!
|
||||
pagination.pageSize = pag.pageSize!
|
||||
loadPermissions()
|
||||
}
|
||||
|
||||
const handleEdit = (record: Permission) => {
|
||||
modalTitle.value = '编辑权限'
|
||||
isEditing.value = true
|
||||
currentPermission.value = { ...record }
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
const showCreateModal = () => {
|
||||
modalTitle.value = '新增权限'
|
||||
isEditing.value = false
|
||||
currentPermission.value = {}
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
const handleModalOk = () => {
|
||||
permissionFormRef.value
|
||||
?.validate()
|
||||
.then(() => {
|
||||
if (isEditing.value) {
|
||||
handleUpdatePermission()
|
||||
} else {
|
||||
handleCreatePermission()
|
||||
}
|
||||
})
|
||||
.catch((error: any) => {
|
||||
console.error('表单验证失败:', error)
|
||||
})
|
||||
}
|
||||
|
||||
const handleModalCancel = () => {
|
||||
modalVisible.value = false
|
||||
permissionFormRef.value?.resetFields()
|
||||
}
|
||||
|
||||
const handleCreatePermission = async () => {
|
||||
try {
|
||||
modalLoading.value = true
|
||||
const response = await createPermission(currentPermission.value as any)
|
||||
|
||||
if (response.success) {
|
||||
message.success('创建权限成功')
|
||||
modalVisible.value = false
|
||||
loadPermissions()
|
||||
} else {
|
||||
message.error('创建权限失败')
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('创建权限失败')
|
||||
} finally {
|
||||
modalLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleUpdatePermission = async () => {
|
||||
try {
|
||||
modalLoading.value = true
|
||||
if (currentPermission.value.id) {
|
||||
const response = await updatePermission(currentPermission.value.id, currentPermission.value as any)
|
||||
|
||||
if (response.success) {
|
||||
message.success('更新权限成功')
|
||||
modalVisible.value = false
|
||||
loadPermissions()
|
||||
} else {
|
||||
message.error('更新权限失败')
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('更新权限失败')
|
||||
} finally {
|
||||
modalLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = (record: Permission) => {
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: `确定要删除权限 "${record.name}" 吗?`,
|
||||
okText: '确定',
|
||||
okType: 'danger',
|
||||
onOk: async () => {
|
||||
try {
|
||||
const response = await deletePermission(record.id)
|
||||
|
||||
if (response.success) {
|
||||
message.success('权限已删除')
|
||||
loadPermissions()
|
||||
} else {
|
||||
message.error('删除失败')
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('删除失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.permission-management {
|
||||
.search-container {
|
||||
margin-bottom: 16px;
|
||||
padding: 16px;
|
||||
background: #fafafa;
|
||||
border-radius: 6px;
|
||||
|
||||
:deep(.ant-form-item) {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-table-thead > tr > th) {
|
||||
background: #fafafa;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
@@ -7,7 +7,11 @@
|
||||
<template #icon><ReloadOutlined /></template>
|
||||
刷新
|
||||
</a-button>
|
||||
<a-button type="primary" @click="showCreateModal">
|
||||
<a-button
|
||||
v-if="hasPermission('promotion:write')"
|
||||
type="primary"
|
||||
@click="showCreateModal"
|
||||
>
|
||||
<template #icon><PlusOutlined /></template>
|
||||
新建活动
|
||||
</a-button>
|
||||
@@ -87,23 +91,32 @@
|
||||
<EyeOutlined />详情
|
||||
</a-button>
|
||||
|
||||
<a-button size="small" @click="handleEditActivity(record)">
|
||||
<a-button
|
||||
v-if="hasPermission('promotion:write')"
|
||||
size="small"
|
||||
@click="handleEditActivity(record)"
|
||||
>
|
||||
<EditOutlined />编辑
|
||||
</a-button>
|
||||
|
||||
<template v-if="record.status === 'active'">
|
||||
<template v-if="record.status === 'active' && hasPermission('promotion:write')">
|
||||
<a-button size="small" danger @click="handlePauseActivity(record)">
|
||||
<PauseOutlined />暂停
|
||||
</a-button>
|
||||
</template>
|
||||
|
||||
<template v-if="record.status === 'paused'">
|
||||
<template v-if="record.status === 'paused' && hasPermission('promotion:write')">
|
||||
<a-button size="small" type="primary" @click="handleResumeActivity(record)">
|
||||
<PlayCircleOutlined />继续
|
||||
</a-button>
|
||||
</template>
|
||||
|
||||
<a-button size="small" danger @click="handleDeleteActivity(record)">
|
||||
<a-button
|
||||
v-if="hasPermission('promotion:write')"
|
||||
size="small"
|
||||
danger
|
||||
@click="handleDeleteActivity(record)"
|
||||
>
|
||||
<DeleteOutlined />删除
|
||||
</a-button>
|
||||
</a-space>
|
||||
@@ -171,7 +184,7 @@
|
||||
|
||||
<template v-else-if="column.key === 'actions'">
|
||||
<a-space :size="8">
|
||||
<template v-if="record.status === 'pending'">
|
||||
<template v-if="record.status === 'pending' && hasPermission('promotion:write')">
|
||||
<a-button size="small" type="primary" @click="handleIssueReward(record)">
|
||||
<CheckOutlined />发放
|
||||
</a-button>
|
||||
@@ -288,6 +301,7 @@ import {
|
||||
PlayCircleOutlined,
|
||||
CheckOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
|
||||
import {
|
||||
getPromotionActivities,
|
||||
@@ -302,6 +316,13 @@ import {
|
||||
} from '@/api/promotion'
|
||||
import type { PromotionActivity, RewardRecord } from '@/api/promotion'
|
||||
|
||||
const appStore = useAppStore()
|
||||
|
||||
// 权限检查方法
|
||||
const hasPermission = (permission: string) => {
|
||||
return appStore.hasPermission(permission)
|
||||
}
|
||||
|
||||
interface SearchForm {
|
||||
name: string
|
||||
status: string
|
||||
|
||||
@@ -3,7 +3,10 @@
|
||||
<a-page-header title="系统管理" sub-title="管理系统设置和配置">
|
||||
<template #extra>
|
||||
<a-space>
|
||||
<a-button @click="exportConfig">
|
||||
<a-button
|
||||
v-if="hasPermission('system:write')"
|
||||
@click="exportConfig"
|
||||
>
|
||||
<ExportOutlined />
|
||||
导出配置
|
||||
</a-button>
|
||||
@@ -82,10 +85,20 @@
|
||||
</template>
|
||||
</a-list-item-meta>
|
||||
<template #actions>
|
||||
<a-button size="small" v-if="item.status === 'running'" danger @click="handleStopService(item)">
|
||||
<a-button
|
||||
size="small"
|
||||
v-if="item.status === 'running' && hasPermission('system:write')"
|
||||
danger
|
||||
@click="handleStopService(item)"
|
||||
>
|
||||
停止
|
||||
</a-button>
|
||||
<a-button size="small" v-if="item.status === 'stopped'" type="primary" @click="handleStartService(item)">
|
||||
<a-button
|
||||
size="small"
|
||||
v-if="item.status === 'stopped' && hasPermission('system:write')"
|
||||
type="primary"
|
||||
@click="handleStartService(item)"
|
||||
>
|
||||
启动
|
||||
</a-button>
|
||||
</template>
|
||||
@@ -160,17 +173,28 @@
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="8">
|
||||
<a-form-item label="系统名称">
|
||||
<a-input v-model:value="systemSettings.systemName" placeholder="请输入系统名称" />
|
||||
<a-input
|
||||
v-model:value="systemSettings.systemName"
|
||||
placeholder="请输入系统名称"
|
||||
:disabled="!hasPermission('system:write')"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item label="系统版本">
|
||||
<a-input v-model:value="systemSettings.systemVersion" placeholder="请输入系统版本" />
|
||||
<a-input
|
||||
v-model:value="systemSettings.systemVersion"
|
||||
placeholder="请输入系统版本"
|
||||
:disabled="!hasPermission('system:write')"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item label="维护模式">
|
||||
<a-switch v-model:checked="systemSettings.maintenanceMode" />
|
||||
<a-switch
|
||||
v-model:checked="systemSettings.maintenanceMode"
|
||||
:disabled="!hasPermission('system:write')"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
@@ -178,22 +202,37 @@
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="8">
|
||||
<a-form-item label="会话超时(分钟)">
|
||||
<a-input-number v-model:value="systemSettings.sessionTimeout" :min="5" :max="480" style="width: 100%" />
|
||||
<a-input-number
|
||||
v-model:value="systemSettings.sessionTimeout"
|
||||
:min="5"
|
||||
:max="480"
|
||||
style="width: 100%"
|
||||
:disabled="!hasPermission('system:write')"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item label="每页显示数量">
|
||||
<a-input-number v-model:value="systemSettings.pageSize" :min="10" :max="100" style="width: 100%" />
|
||||
<a-input-number
|
||||
v-model:value="systemSettings.pageSize"
|
||||
:min="10"
|
||||
:max="100"
|
||||
style="width: 100%"
|
||||
:disabled="!hasPermission('system:write')"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item label="启用API文档">
|
||||
<a-switch v-model:checked="systemSettings.enableSwagger" />
|
||||
<a-switch
|
||||
v-model:checked="systemSettings.enableSwagger"
|
||||
:disabled="!hasPermission('system:write')"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-form-item>
|
||||
<a-form-item v-if="hasPermission('system:write')">
|
||||
<a-button type="primary" @click="saveSettings">保存设置</a-button>
|
||||
<a-button style="margin-left: 8px" @click="resetSettings">重置</a-button>
|
||||
</a-form-item>
|
||||
@@ -214,6 +253,7 @@ import {
|
||||
MessageOutlined,
|
||||
ExportOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import {
|
||||
getServices,
|
||||
startService,
|
||||
@@ -226,6 +266,13 @@ import {
|
||||
} from '@/api/system'
|
||||
import type { Service, SystemInfo, DatabaseStatus, CacheStatus, SystemSettings } from '@/api/system'
|
||||
|
||||
const appStore = useAppStore()
|
||||
|
||||
// 权限检查方法
|
||||
const hasPermission = (permission: string) => {
|
||||
return appStore.hasPermission(permission)
|
||||
}
|
||||
|
||||
const services = ref<Service[]>([])
|
||||
const systemInfo = ref<SystemInfo>({
|
||||
version: 'v1.0.0',
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="travel-management">
|
||||
<a-page-header
|
||||
title="旅行管理"
|
||||
sub-title="管理旅行计划和匹配"
|
||||
sub-title="管理用户发布的旅行信息"
|
||||
>
|
||||
<template #extra>
|
||||
<a-space>
|
||||
@@ -12,17 +12,15 @@
|
||||
</template>
|
||||
刷新
|
||||
</a-button>
|
||||
<a-button type="primary" @click="showCreateModal">
|
||||
<a-button
|
||||
v-if="hasPermission('travel:write')"
|
||||
type="primary"
|
||||
@click="showCreateModal"
|
||||
>
|
||||
<template #icon>
|
||||
<PlusOutlined />
|
||||
</template>
|
||||
新增旅行
|
||||
</a-button>
|
||||
<a-button type="primary" @click="showStats">
|
||||
<template #icon>
|
||||
<BarChartOutlined />
|
||||
</template>
|
||||
数据统计
|
||||
发布旅行
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
@@ -32,10 +30,10 @@
|
||||
<!-- 搜索区域 -->
|
||||
<div class="search-container">
|
||||
<a-form layout="inline" :model="searchForm">
|
||||
<a-form-item label="目的地">
|
||||
<a-form-item label="关键词">
|
||||
<a-input
|
||||
v-model:value="searchForm.destination"
|
||||
placeholder="输入目的地"
|
||||
v-model:value="searchForm.keyword"
|
||||
placeholder="目的地/用户昵称"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
@@ -47,14 +45,13 @@
|
||||
style="width: 120px"
|
||||
allow-clear
|
||||
>
|
||||
<a-select-option value="recruiting">招募中</a-select-option>
|
||||
<a-select-option value="full">已满员</a-select-option>
|
||||
<a-select-option value="completed">已完成</a-select-option>
|
||||
<a-select-option value="cancelled">已取消</a-select-option>
|
||||
<a-select-option value="draft">草稿</a-select-option>
|
||||
<a-select-option value="published">已发布</a-select-option>
|
||||
<a-select-option value="archived">已归档</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="旅行时间">
|
||||
<a-form-item label="出行时间">
|
||||
<a-range-picker
|
||||
v-model:value="searchForm.travelTime"
|
||||
:placeholder="['开始时间', '结束时间']"
|
||||
@@ -75,7 +72,7 @@
|
||||
</a-form>
|
||||
</div>
|
||||
|
||||
<!-- 旅行计划表格 -->
|
||||
<!-- 旅行表格 -->
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="travelList"
|
||||
@@ -85,29 +82,7 @@
|
||||
@change="handleTableChange"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'destination'">
|
||||
<strong>{{ record.destination }}</strong>
|
||||
<div style="font-size: 12px; color: #666;">
|
||||
{{ record.start_date }} 至 {{ record.end_date }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'budget'">
|
||||
¥{{ record.budget }}
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'members'">
|
||||
<a-progress
|
||||
:percent="(record.current_members / record.max_members) * 100"
|
||||
size="small"
|
||||
:show-info="false"
|
||||
/>
|
||||
<div style="font-size: 12px; text-align: center;">
|
||||
{{ record.current_members }}/{{ record.max_members }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'status'">
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-tag :color="getStatusColor(record.status)">
|
||||
{{ getStatusText(record.status) }}
|
||||
</a-tag>
|
||||
@@ -117,36 +92,51 @@
|
||||
<a-space :size="8">
|
||||
<a-button size="small" @click="handleView(record)">
|
||||
<EyeOutlined />
|
||||
详情
|
||||
查看
|
||||
</a-button>
|
||||
|
||||
<a-button size="small" @click="showEditModal(record)">
|
||||
<a-button
|
||||
v-if="hasPermission('travel:write')"
|
||||
size="small"
|
||||
@click="handleEdit(record)"
|
||||
>
|
||||
<EditOutlined />
|
||||
编辑
|
||||
</a-button>
|
||||
|
||||
<a-button size="small" @click="handleMembers(record)">
|
||||
<TeamOutlined />
|
||||
成员
|
||||
</a-button>
|
||||
|
||||
<template v-if="record.status === 'recruiting'">
|
||||
<a-button size="small" type="primary" @click="handlePromote(record)">
|
||||
<RocketOutlined />
|
||||
推广
|
||||
</a-button>
|
||||
<a-button size="small" danger @click="handleClose(record)">
|
||||
<CloseOutlined />
|
||||
关闭
|
||||
<template v-if="record.status === 'published' && hasPermission('travel:write')">
|
||||
<a-button size="small" danger @click="handleArchive(record)">
|
||||
<FolderOpenOutlined />
|
||||
归档
|
||||
</a-button>
|
||||
</template>
|
||||
|
||||
<a-button
|
||||
v-if="record.status !== 'published' && hasPermission('travel:write')"
|
||||
size="small"
|
||||
type="primary"
|
||||
@click="handlePublish(record)"
|
||||
>
|
||||
<SendOutlined />
|
||||
发布
|
||||
</a-button>
|
||||
|
||||
<a-button
|
||||
v-if="hasPermission('travel:write')"
|
||||
size="small"
|
||||
danger
|
||||
@click="handleDelete(record)"
|
||||
>
|
||||
<DeleteOutlined />
|
||||
删除
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
|
||||
<!-- 创建/编辑旅行计划模态框 -->
|
||||
<!-- 创建/编辑旅行模态框 -->
|
||||
<a-modal
|
||||
v-model:open="modalVisible"
|
||||
:title="modalTitle"
|
||||
@@ -161,66 +151,54 @@
|
||||
:rules="formRules"
|
||||
layout="vertical"
|
||||
>
|
||||
<a-form-item label="旅行标题" name="title">
|
||||
<a-input v-model:value="currentTravel.title" placeholder="请输入旅行标题" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="目的地" name="destination">
|
||||
<a-input v-model:value="currentTravel.destination" placeholder="请输入目的地" />
|
||||
</a-form-item>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="开始日期" name="start_date">
|
||||
<a-form-item label="开始日期" name="startDate">
|
||||
<a-date-picker
|
||||
v-model:value="currentTravel.start_date"
|
||||
v-model:value="currentTravel.startDate"
|
||||
format="YYYY-MM-DD"
|
||||
value-format="YYYY-MM-DD"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="结束日期" name="end_date">
|
||||
<a-form-item label="结束日期" name="endDate">
|
||||
<a-date-picker
|
||||
v-model:value="currentTravel.end_date"
|
||||
v-model:value="currentTravel.endDate"
|
||||
format="YYYY-MM-DD"
|
||||
value-format="YYYY-MM-DD"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="最大参与人数" name="max_participants">
|
||||
<a-input-number
|
||||
v-model:value="currentTravel.max_participants"
|
||||
:min="1"
|
||||
:max="100"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="预算(元)" name="budget">
|
||||
<a-input-number
|
||||
v-model:value="currentTravel.budget"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-form-item label="预算(元)" name="budget">
|
||||
<a-input-number
|
||||
v-model:value="currentTravel.budget"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="人数" name="peopleCount">
|
||||
<a-input-number
|
||||
v-model:value="currentTravel.peopleCount"
|
||||
:min="1"
|
||||
:max="100"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="状态" name="status">
|
||||
<a-select v-model:value="currentTravel.status" placeholder="请选择状态">
|
||||
<a-select-option value="recruiting">招募中</a-select-option>
|
||||
<a-select-option value="full">已满员</a-select-option>
|
||||
<a-select-option value="completed">已完成</a-select-option>
|
||||
<a-select-option value="cancelled">已取消</a-select-option>
|
||||
<a-select-option value="draft">草稿</a-select-option>
|
||||
<a-select-option value="published">已发布</a-select-option>
|
||||
<a-select-option value="archived">已归档</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
@@ -241,34 +219,48 @@ import { ref, reactive, onMounted } from 'vue'
|
||||
import { message, Modal, type FormInstance } from 'ant-design-vue'
|
||||
import type { TableProps } from 'ant-design-vue'
|
||||
import {
|
||||
ReloadOutlined,
|
||||
SearchOutlined,
|
||||
BarChartOutlined,
|
||||
EyeOutlined,
|
||||
TeamOutlined,
|
||||
RocketOutlined,
|
||||
CloseOutlined,
|
||||
ReloadOutlined,
|
||||
PlusOutlined,
|
||||
EditOutlined
|
||||
EyeOutlined,
|
||||
EditOutlined,
|
||||
SendOutlined,
|
||||
FolderOpenOutlined,
|
||||
DeleteOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import { getTravelPlans, closeTravelPlan, createTravel, updateTravel, getTravel } from '@/api/travel'
|
||||
import type { TravelPlan, TravelCreateData, TravelUpdateData } from '@/api/travel'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import {
|
||||
getTravels,
|
||||
getTravel,
|
||||
createTravel,
|
||||
updateTravel,
|
||||
publishTravel,
|
||||
archiveTravel,
|
||||
deleteTravel
|
||||
} from '@/api/travel'
|
||||
import type { Travel, TravelQueryParams } from '@/api/travel'
|
||||
|
||||
interface SearchForm {
|
||||
destination: string
|
||||
keyword: string
|
||||
status: string
|
||||
travelTime: any[]
|
||||
}
|
||||
|
||||
const loading = ref(false)
|
||||
const appStore = useAppStore()
|
||||
|
||||
// 权限检查方法
|
||||
const hasPermission = (permission: string) => {
|
||||
return appStore.hasPermission(permission)
|
||||
}
|
||||
|
||||
const loading = ref(false)
|
||||
const searchForm = reactive<SearchForm>({
|
||||
destination: '',
|
||||
keyword: '',
|
||||
status: '',
|
||||
travelTime: []
|
||||
})
|
||||
|
||||
const travelList = ref<TravelPlan[]>([])
|
||||
const travelList = ref<Travel[]>([])
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 20,
|
||||
@@ -280,20 +272,37 @@ const pagination = reactive({
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '旅行信息',
|
||||
title: '目的地',
|
||||
dataIndex: 'destination',
|
||||
key: 'destination',
|
||||
width: 200
|
||||
width: 150
|
||||
},
|
||||
{
|
||||
title: '用户',
|
||||
dataIndex: 'userName',
|
||||
key: 'userName',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '出行时间',
|
||||
key: 'travelTime',
|
||||
width: 200,
|
||||
customRender: ({ record }: { record: Travel }) =>
|
||||
`${record.startDate} 至 ${record.endDate}`
|
||||
},
|
||||
{
|
||||
title: '预算',
|
||||
dataIndex: 'budget',
|
||||
key: 'budget',
|
||||
width: 100,
|
||||
align: 'center'
|
||||
align: 'right',
|
||||
customRender: ({ text }: { text: number }) => `¥${text}`
|
||||
},
|
||||
{
|
||||
title: '成员',
|
||||
key: 'members',
|
||||
width: 120,
|
||||
title: '人数',
|
||||
dataIndex: 'peopleCount',
|
||||
key: 'peopleCount',
|
||||
width: 80,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
@@ -303,21 +312,15 @@ const columns = [
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '创建者',
|
||||
dataIndex: 'creator',
|
||||
key: 'creator',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'created_at',
|
||||
key: 'created_at',
|
||||
title: '发布时间',
|
||||
dataIndex: 'publishedAt',
|
||||
key: 'publishedAt',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 200,
|
||||
width: 250,
|
||||
align: 'center'
|
||||
}
|
||||
]
|
||||
@@ -325,44 +328,75 @@ const columns = [
|
||||
// 状态映射
|
||||
const getStatusColor = (status: string) => {
|
||||
const colors = {
|
||||
recruiting: 'blue',
|
||||
full: 'green',
|
||||
completed: 'purple',
|
||||
cancelled: 'red'
|
||||
draft: 'orange',
|
||||
published: 'green',
|
||||
archived: 'blue'
|
||||
}
|
||||
return colors[status as keyof typeof colors] || 'default'
|
||||
}
|
||||
|
||||
const getStatusText = (status: string) => {
|
||||
const texts = {
|
||||
recruiting: '招募中',
|
||||
full: '已满员',
|
||||
completed: '已完成',
|
||||
cancelled: '已取消'
|
||||
draft: '草稿',
|
||||
published: '已发布',
|
||||
archived: '已归档'
|
||||
}
|
||||
return texts[status as keyof typeof texts] || '未知'
|
||||
}
|
||||
|
||||
// 添加模态框相关状态
|
||||
const modalVisible = ref(false)
|
||||
const modalLoading = ref(false)
|
||||
const modalTitle = ref('发布旅行')
|
||||
const isEditing = ref(false)
|
||||
const travelFormRef = ref<FormInstance>()
|
||||
|
||||
// 当前旅行数据
|
||||
const currentTravel = ref<Partial<Travel>>({})
|
||||
|
||||
// 表单验证规则
|
||||
const formRules = {
|
||||
destination: [
|
||||
{ required: true, message: '请输入目的地' }
|
||||
],
|
||||
startDate: [
|
||||
{ required: true, message: '请选择开始日期' }
|
||||
],
|
||||
endDate: [
|
||||
{ required: true, message: '请选择结束日期' }
|
||||
],
|
||||
budget: [
|
||||
{ required: true, message: '请输入预算' }
|
||||
],
|
||||
peopleCount: [
|
||||
{ required: true, message: '请输入人数' }
|
||||
],
|
||||
status: [
|
||||
{ required: true, message: '请选择状态' }
|
||||
]
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
loadTravelPlans()
|
||||
loadTravels()
|
||||
})
|
||||
|
||||
// 方法
|
||||
const loadTravelPlans = async () => {
|
||||
const loadTravels = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await getTravelPlans({
|
||||
const params: TravelQueryParams = {
|
||||
page: pagination.current,
|
||||
pageSize: pagination.pageSize,
|
||||
destination: searchForm.destination,
|
||||
limit: pagination.pageSize,
|
||||
keyword: searchForm.keyword,
|
||||
status: searchForm.status
|
||||
})
|
||||
}
|
||||
|
||||
travelList.value = response.data
|
||||
pagination.total = response.pagination?.total || 0
|
||||
const response = await getTravels(params)
|
||||
travelList.value = response.data.list
|
||||
pagination.total = response.data.pagination.total
|
||||
} catch (error) {
|
||||
message.error('加载旅行计划失败')
|
||||
message.error('加载旅行列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -370,126 +404,67 @@ const loadTravelPlans = async () => {
|
||||
|
||||
const handleSearch = () => {
|
||||
pagination.current = 1
|
||||
loadTravelPlans()
|
||||
loadTravels()
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
Object.assign(searchForm, {
|
||||
destination: '',
|
||||
keyword: '',
|
||||
status: '',
|
||||
travelTime: []
|
||||
})
|
||||
pagination.current = 1
|
||||
loadTravelPlans()
|
||||
loadTravels()
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
loadTravelPlans()
|
||||
loadTravels()
|
||||
message.success('数据已刷新')
|
||||
}
|
||||
|
||||
const handleTableChange: TableProps['onChange'] = (pag) => {
|
||||
pagination.current = pag.current!
|
||||
pagination.pageSize = pag.pageSize!
|
||||
loadTravelPlans()
|
||||
loadTravels()
|
||||
}
|
||||
|
||||
const handleView = (record: TravelPlan) => {
|
||||
message.info(`查看旅行计划: ${record.destination}`)
|
||||
}
|
||||
|
||||
const handleMembers = (record: TravelPlan) => {
|
||||
message.info(`查看成员: ${record.destination}`)
|
||||
}
|
||||
|
||||
const handlePromote = (record: TravelPlan) => {
|
||||
Modal.confirm({
|
||||
title: '确认推广',
|
||||
content: `确定要推广旅行计划 "${record.destination}" 吗?`,
|
||||
onOk: async () => {
|
||||
try {
|
||||
message.success('旅行计划已推广')
|
||||
} catch (error) {
|
||||
message.error('操作失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleClose = async (record: TravelPlan) => {
|
||||
Modal.confirm({
|
||||
title: '确认关闭',
|
||||
content: `确定要关闭旅行计划 "${record.destination}" 吗?`,
|
||||
onOk: async () => {
|
||||
try {
|
||||
await closeTravelPlan(record.id)
|
||||
message.success('旅行计划已关闭')
|
||||
loadTravelPlans()
|
||||
} catch (error) {
|
||||
message.error('操作失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 添加模态框相关状态
|
||||
const modalVisible = ref(false)
|
||||
const modalLoading = ref(false)
|
||||
const modalTitle = ref('新增旅行')
|
||||
const isEditing = ref(false)
|
||||
const travelFormRef = ref<FormInstance>()
|
||||
|
||||
// 当前旅行数据
|
||||
const currentTravel = ref<Partial<TravelPlan>>({})
|
||||
|
||||
// 表单验证规则
|
||||
const formRules = {
|
||||
title: [
|
||||
{ required: true, message: '请输入旅行标题' },
|
||||
{ min: 5, max: 100, message: '旅行标题长度为5-100个字符' }
|
||||
],
|
||||
destination: [
|
||||
{ required: true, message: '请输入目的地' },
|
||||
{ min: 2, max: 50, message: '目的地长度为2-50个字符' }
|
||||
],
|
||||
start_date: [
|
||||
{ required: true, message: '请选择开始日期' }
|
||||
],
|
||||
end_date: [
|
||||
{ required: true, message: '请选择结束日期' }
|
||||
],
|
||||
max_participants: [
|
||||
{ required: true, message: '请输入最大参与人数' },
|
||||
{ type: 'number', min: 1, max: 100, message: '参与人数应在1-100之间' }
|
||||
],
|
||||
budget: [
|
||||
{ required: true, message: '请输入预算' },
|
||||
{ type: 'number', min: 0, message: '预算不能为负数' }
|
||||
],
|
||||
status: [
|
||||
{ required: true, message: '请选择状态' }
|
||||
]
|
||||
}
|
||||
|
||||
const showCreateModal = () => {
|
||||
modalTitle.value = '新增旅行'
|
||||
isEditing.value = false
|
||||
currentTravel.value = {
|
||||
status: 'recruiting',
|
||||
max_participants: 10,
|
||||
budget: 0
|
||||
const handleView = async (record: Travel) => {
|
||||
try {
|
||||
const response = await getTravel(record.id)
|
||||
Modal.info({
|
||||
title: '旅行详情',
|
||||
width: 600,
|
||||
content: h('div', { class: 'travel-detail-modal' }, [
|
||||
h('a-descriptions', {
|
||||
column: 1,
|
||||
bordered: true
|
||||
}, [
|
||||
h('a-descriptions-item', { label: '目的地' }, response.data.destination),
|
||||
h('a-descriptions-item', { label: '用户' }, response.data.userName),
|
||||
h('a-descriptions-item', { label: '出行时间' },
|
||||
`${response.data.startDate} 至 ${response.data.endDate}`),
|
||||
h('a-descriptions-item', { label: '预算' }, `¥${response.data.budget}`),
|
||||
h('a-descriptions-item', { label: '人数' }, response.data.peopleCount),
|
||||
h('a-descriptions-item', { label: '状态' }, [
|
||||
h('a-tag', { color: getStatusColor(response.data.status) }, getStatusText(response.data.status))
|
||||
]),
|
||||
h('a-descriptions-item', { label: '发布时间' }, response.data.publishedAt || '-'),
|
||||
h('a-descriptions-item', { label: '描述' }, response.data.description || '-')
|
||||
])
|
||||
]),
|
||||
okText: '关闭'
|
||||
})
|
||||
} catch (error) {
|
||||
message.error('获取旅行详情失败')
|
||||
}
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
const showEditModal = async (record: TravelPlan) => {
|
||||
const handleEdit = async (record: Travel) => {
|
||||
try {
|
||||
modalLoading.value = true
|
||||
const response = await getTravel(record.id)
|
||||
modalTitle.value = '编辑旅行'
|
||||
isEditing.value = true
|
||||
|
||||
// 获取旅行详情
|
||||
const response = await getTravel(record.id)
|
||||
currentTravel.value = response.data
|
||||
modalVisible.value = true
|
||||
} catch (error) {
|
||||
@@ -499,6 +474,17 @@ const showEditModal = async (record: TravelPlan) => {
|
||||
}
|
||||
}
|
||||
|
||||
const showCreateModal = () => {
|
||||
modalTitle.value = '发布旅行'
|
||||
isEditing.value = false
|
||||
currentTravel.value = {
|
||||
budget: 0,
|
||||
peopleCount: 1,
|
||||
status: 'draft'
|
||||
}
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
const handleModalOk = () => {
|
||||
travelFormRef.value
|
||||
?.validate()
|
||||
@@ -522,50 +508,12 @@ const handleModalCancel = () => {
|
||||
const handleCreateTravel = async () => {
|
||||
try {
|
||||
modalLoading.value = true
|
||||
|
||||
// 前端数据验证
|
||||
if (!currentTravel.value.title) {
|
||||
message.error('旅行标题不能为空')
|
||||
return
|
||||
}
|
||||
|
||||
if (!currentTravel.value.destination) {
|
||||
message.error('目的地不能为空')
|
||||
return
|
||||
}
|
||||
|
||||
if (!currentTravel.value.start_date) {
|
||||
message.error('请选择开始日期')
|
||||
return
|
||||
}
|
||||
|
||||
if (!currentTravel.value.end_date) {
|
||||
message.error('请选择结束日期')
|
||||
return
|
||||
}
|
||||
|
||||
if (new Date(currentTravel.value.start_date) >= new Date(currentTravel.value.end_date)) {
|
||||
message.error('开始日期必须早于结束日期')
|
||||
return
|
||||
}
|
||||
|
||||
if (!currentTravel.value.max_participants || currentTravel.value.max_participants < 1) {
|
||||
message.error('最大参与人数必须大于0')
|
||||
return
|
||||
}
|
||||
|
||||
if (currentTravel.value.budget === undefined || currentTravel.value.budget < 0) {
|
||||
message.error('预算不能为负数')
|
||||
return
|
||||
}
|
||||
|
||||
await createTravel(currentTravel.value as TravelCreateData)
|
||||
message.success('创建旅行计划成功')
|
||||
await createTravel(currentTravel.value as any)
|
||||
message.success('创建旅行成功')
|
||||
modalVisible.value = false
|
||||
loadTravelPlans()
|
||||
loadTravels()
|
||||
} catch (error) {
|
||||
console.error('创建旅行计划失败:', error)
|
||||
message.error('创建旅行计划失败: ' + (error as Error).message)
|
||||
message.error('创建旅行失败')
|
||||
} finally {
|
||||
modalLoading.value = false
|
||||
}
|
||||
@@ -574,57 +522,65 @@ const handleCreateTravel = async () => {
|
||||
const handleUpdateTravel = async () => {
|
||||
try {
|
||||
modalLoading.value = true
|
||||
|
||||
// 前端数据验证
|
||||
if (!currentTravel.value.title) {
|
||||
message.error('旅行标题不能为空')
|
||||
return
|
||||
}
|
||||
|
||||
if (!currentTravel.value.destination) {
|
||||
message.error('目的地不能为空')
|
||||
return
|
||||
}
|
||||
|
||||
if (!currentTravel.value.start_date) {
|
||||
message.error('请选择开始日期')
|
||||
return
|
||||
}
|
||||
|
||||
if (!currentTravel.value.end_date) {
|
||||
message.error('请选择结束日期')
|
||||
return
|
||||
}
|
||||
|
||||
if (new Date(currentTravel.value.start_date) >= new Date(currentTravel.value.end_date)) {
|
||||
message.error('开始日期必须早于结束日期')
|
||||
return
|
||||
}
|
||||
|
||||
if (!currentTravel.value.max_participants || currentTravel.value.max_participants < 1) {
|
||||
message.error('最大参与人数必须大于0')
|
||||
return
|
||||
}
|
||||
|
||||
if (currentTravel.value.budget === undefined || currentTravel.value.budget < 0) {
|
||||
message.error('预算不能为负数')
|
||||
return
|
||||
}
|
||||
|
||||
await updateTravel(currentTravel.value.id!, currentTravel.value as TravelUpdateData)
|
||||
message.success('更新旅行计划成功')
|
||||
await updateTravel(currentTravel.value.id!, currentTravel.value as any)
|
||||
message.success('更新旅行成功')
|
||||
modalVisible.value = false
|
||||
loadTravelPlans()
|
||||
loadTravels()
|
||||
} catch (error) {
|
||||
console.error('更新旅行计划失败:', error)
|
||||
message.error('更新旅行计划失败: ' + (error as Error).message)
|
||||
message.error('更新旅行失败')
|
||||
} finally {
|
||||
modalLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const showStats = () => {
|
||||
message.info('数据统计功能开发中')
|
||||
const handlePublish = async (record: Travel) => {
|
||||
Modal.confirm({
|
||||
title: '确认发布',
|
||||
content: `确定要发布旅行 "${record.destination}" 吗?`,
|
||||
onOk: async () => {
|
||||
try {
|
||||
await publishTravel(record.id)
|
||||
message.success('旅行已发布')
|
||||
loadTravels()
|
||||
} catch (error) {
|
||||
message.error('操作失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleArchive = async (record: Travel) => {
|
||||
Modal.confirm({
|
||||
title: '确认归档',
|
||||
content: `确定要归档旅行 "${record.destination}" 吗?`,
|
||||
onOk: async () => {
|
||||
try {
|
||||
await archiveTravel(record.id)
|
||||
message.success('旅行已归档')
|
||||
loadTravels()
|
||||
} catch (error) {
|
||||
message.error('操作失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleDelete = async (record: Travel) => {
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: `确定要删除旅行 "${record.destination}" 吗?`,
|
||||
okText: '确定',
|
||||
okType: 'danger',
|
||||
onOk: async () => {
|
||||
try {
|
||||
await deleteTravel(record.id)
|
||||
message.success('旅行已删除')
|
||||
loadTravels()
|
||||
} catch (error) {
|
||||
message.error('删除失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -641,4 +597,9 @@ const showStats = () => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-table-thead > tr > th) {
|
||||
background: #fafafa;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -98,6 +98,18 @@ const routes: RouteRecordRaw[] = [
|
||||
layout: 'main' // 添加布局信息
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/permission',
|
||||
name: 'Permission',
|
||||
component: () => import('@/pages/permission/index.vue'),
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
title: '权限管理',
|
||||
icon: 'LockOutlined',
|
||||
permissions: ['system:read'],
|
||||
layout: 'main' // 添加布局信息
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/system',
|
||||
name: 'System',
|
||||
@@ -110,6 +122,12 @@ const routes: RouteRecordRaw[] = [
|
||||
layout: 'main' // 添加布局信息
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/no-permission',
|
||||
name: 'NoPermission',
|
||||
component: () => import('@/pages/NoPermission.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/:pathMatch(.*)*',
|
||||
name: 'NotFound',
|
||||
@@ -147,6 +165,18 @@ router.beforeEach(async (to, from, next) => {
|
||||
return
|
||||
}
|
||||
|
||||
// 检查权限
|
||||
if (meta.requiresAuth && isAuthenticated && meta.permissions) {
|
||||
// 检查用户是否拥有访问该路由所需的权限
|
||||
const requiredPermissions = Array.isArray(meta.permissions) ? meta.permissions : [meta.permissions]
|
||||
|
||||
// 如果用户没有所有必需的权限,则重定向到无权限页面
|
||||
if (!appStore.hasAllPermissions(requiredPermissions)) {
|
||||
next({ name: 'NoPermission' })
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
next()
|
||||
})
|
||||
|
||||
|
||||
@@ -3,22 +3,52 @@ import { reactive } from 'vue'
|
||||
import { authAPI } from '@/api'
|
||||
import type { Admin } from '@/types/user'
|
||||
|
||||
// 定义用户权限类型
|
||||
export interface UserPermission {
|
||||
resource: string
|
||||
action: string
|
||||
}
|
||||
|
||||
export const useAppStore = defineStore('app', () => {
|
||||
// 状态
|
||||
const state = reactive({
|
||||
user: null as Admin | null,
|
||||
permissions: [] as string[], // 添加权限列表
|
||||
loading: false,
|
||||
initialized: false
|
||||
})
|
||||
|
||||
// 设置用户信息
|
||||
const setUser = (user: Admin) => {
|
||||
// 设置用户信息和权限
|
||||
const setUser = (user: Admin, permissions: string[] = []) => {
|
||||
state.user = user
|
||||
state.permissions = permissions
|
||||
}
|
||||
|
||||
// 检查是否有特定权限
|
||||
const hasPermission = (permission: string): boolean => {
|
||||
// 如果是超级管理员,拥有所有权限
|
||||
if (state.user?.role === 'admin') {
|
||||
return true
|
||||
}
|
||||
|
||||
// 检查是否拥有该权限
|
||||
return state.permissions.includes(permission)
|
||||
}
|
||||
|
||||
// 检查是否拥有所有指定权限
|
||||
const hasAllPermissions = (permissions: string[]): boolean => {
|
||||
return permissions.every(permission => hasPermission(permission))
|
||||
}
|
||||
|
||||
// 检查是否拥有任意一个指定权限
|
||||
const hasAnyPermission = (permissions: string[]): boolean => {
|
||||
return permissions.some(permission => hasPermission(permission))
|
||||
}
|
||||
|
||||
// 清除用户信息
|
||||
const clearUser = () => {
|
||||
state.user = null
|
||||
state.permissions = []
|
||||
localStorage.removeItem('admin_token')
|
||||
}
|
||||
|
||||
@@ -40,7 +70,18 @@ export const useAppStore = defineStore('app', () => {
|
||||
|
||||
// 确保响应数据格式为 { data: { admin: object } }
|
||||
if (response.data && typeof response.data === 'object' && response.data.admin) {
|
||||
// 模拟权限数据 - 实际项目中应该从后端获取
|
||||
const mockPermissions = [
|
||||
'user:read', 'user:write',
|
||||
'merchant:read', 'merchant:write',
|
||||
'travel:read', 'travel:write',
|
||||
'animal:read', 'animal:write',
|
||||
'order:read', 'order:write',
|
||||
'promotion:read', 'promotion:write',
|
||||
'system:read', 'system:write'
|
||||
]
|
||||
state.user = response.data.admin
|
||||
state.permissions = mockPermissions
|
||||
} else {
|
||||
throw new Error('获取用户信息失败:响应数据格式不符合预期')
|
||||
}
|
||||
@@ -76,9 +117,31 @@ export const useAppStore = defineStore('app', () => {
|
||||
|
||||
// 设置用户信息 - 修复数据结构访问问题
|
||||
if (response?.data?.admin) {
|
||||
// 模拟权限数据 - 实际项目中应该从后端获取
|
||||
const mockPermissions = [
|
||||
'user:read', 'user:write',
|
||||
'merchant:read', 'merchant:write',
|
||||
'travel:read', 'travel:write',
|
||||
'animal:read', 'animal:write',
|
||||
'order:read', 'order:write',
|
||||
'promotion:read', 'promotion:write',
|
||||
'system:read', 'system:write'
|
||||
]
|
||||
state.user = response.data.admin
|
||||
state.permissions = mockPermissions
|
||||
} else if (response?.admin) {
|
||||
// 模拟权限数据 - 实际项目中应该从后端获取
|
||||
const mockPermissions = [
|
||||
'user:read', 'user:write',
|
||||
'merchant:read', 'merchant:write',
|
||||
'travel:read', 'travel:write',
|
||||
'animal:read', 'animal:write',
|
||||
'order:read', 'order:write',
|
||||
'promotion:read', 'promotion:write',
|
||||
'system:read', 'system:write'
|
||||
]
|
||||
state.user = response.admin
|
||||
state.permissions = mockPermissions
|
||||
} else {
|
||||
throw new Error('登录响应中缺少用户信息')
|
||||
}
|
||||
@@ -104,7 +167,10 @@ export const useAppStore = defineStore('app', () => {
|
||||
clearUser,
|
||||
initialize,
|
||||
login,
|
||||
logout
|
||||
logout,
|
||||
hasPermission,
|
||||
hasAllPermissions,
|
||||
hasAnyPermission
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user