由于本次代码变更内容为空,无法生成有效的提交信息。请提供具体的代码变更内容以便生成合适的提交信息。

This commit is contained in:
2025-09-10 20:51:49 +08:00
parent d875bb49af
commit 72f254e6ba
15 changed files with 2073 additions and 1112 deletions

View 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

View File

@@ -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

View 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

View File

@@ -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]

View 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>

View File

@@ -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) => {

View File

@@ -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>

View File

@@ -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('获取订单详情失败')

View 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>

View File

@@ -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

View File

@@ -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',

View File

@@ -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

View File

@@ -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()
})

View File

@@ -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
}
})