重构动物模型和路由系统,优化查询逻辑并新增商户和促销活动功能
This commit is contained in:
115
admin-system/src/api/flower.ts
Normal file
115
admin-system/src/api/flower.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import { request } from '.'
|
||||
|
||||
// 定义花卉相关类型
|
||||
export interface Flower {
|
||||
id: number
|
||||
name: string
|
||||
type: string
|
||||
variety: string
|
||||
price: number
|
||||
stock: number
|
||||
image: string
|
||||
description: string
|
||||
merchantId: number
|
||||
merchantName: string
|
||||
status: string
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
export interface FlowerQueryParams {
|
||||
page?: number
|
||||
limit?: number
|
||||
keyword?: string
|
||||
type?: string
|
||||
status?: string
|
||||
merchantId?: number
|
||||
startDate?: string
|
||||
endDate?: string
|
||||
}
|
||||
|
||||
export interface FlowerCreateData {
|
||||
name: string
|
||||
type: string
|
||||
variety: string
|
||||
price: number
|
||||
stock: number
|
||||
image: string
|
||||
description: string
|
||||
merchantId: number
|
||||
status?: string
|
||||
}
|
||||
|
||||
export interface FlowerUpdateData {
|
||||
name?: string
|
||||
type?: string
|
||||
variety?: string
|
||||
price?: number
|
||||
stock?: number
|
||||
image?: string
|
||||
description?: string
|
||||
merchantId?: number
|
||||
status?: string
|
||||
}
|
||||
|
||||
export interface FlowerSale {
|
||||
id: number
|
||||
flowerId: number
|
||||
flowerName: string
|
||||
buyerId: number
|
||||
buyerName: string
|
||||
quantity: number
|
||||
price: number
|
||||
totalAmount: number
|
||||
status: string
|
||||
saleTime: string
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
export interface Merchant {
|
||||
id: number
|
||||
name: string
|
||||
contact: string
|
||||
phone: string
|
||||
address: string
|
||||
status: string
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
// 获取花卉列表
|
||||
export const getFlowers = (params?: FlowerQueryParams) =>
|
||||
request.get<{ success: boolean; code: number; message: string; data: { flowers: Flower[]; pagination: any } }>('/flowers', { params })
|
||||
|
||||
// 获取花卉详情
|
||||
export const getFlower = (id: number) =>
|
||||
request.get<{ success: boolean; code: number; message: string; data: { flower: Flower } }>(`/flowers/${id}`)
|
||||
|
||||
// 创建花卉
|
||||
export const createFlower = (data: FlowerCreateData) =>
|
||||
request.post<{ success: boolean; code: number; message: string; data: { flower: Flower } }>('/flowers', data)
|
||||
|
||||
// 更新花卉
|
||||
export const updateFlower = (id: number, data: FlowerUpdateData) =>
|
||||
request.put<{ success: boolean; code: number; message: string; data: { flower: Flower } }>(`/flowers/${id}`, data)
|
||||
|
||||
// 删除花卉
|
||||
export const deleteFlower = (id: number) =>
|
||||
request.delete<{ success: boolean; code: number; message: string }>(`/flowers/${id}`)
|
||||
|
||||
// 获取花卉销售记录
|
||||
export const getFlowerSales = (params?: any) =>
|
||||
request.get<{ success: boolean; code: number; message: string; data: { sales: FlowerSale[]; pagination: any } }>('/flower-sales', { params })
|
||||
|
||||
// 获取商家列表
|
||||
export const getMerchants = (params?: any) =>
|
||||
request.get<{ success: boolean; code: number; message: string; data: { merchants: Merchant[] } }>('/merchants', { params })
|
||||
|
||||
export default {
|
||||
getFlowers,
|
||||
getFlower,
|
||||
createFlower,
|
||||
updateFlower,
|
||||
deleteFlower,
|
||||
getFlowerSales,
|
||||
getMerchants
|
||||
}
|
||||
@@ -60,6 +60,14 @@
|
||||
<router-link to="/animals" />
|
||||
</a-menu-item>
|
||||
|
||||
<a-menu-item v-if="hasPermission('flower:read')" key="flowers">
|
||||
<template #icon>
|
||||
<EnvironmentOutlined />
|
||||
</template>
|
||||
<span>花卉管理</span>
|
||||
<router-link to="/flowers" />
|
||||
</a-menu-item>
|
||||
|
||||
<a-menu-item v-if="hasPermission('order:read')" key="orders">
|
||||
<template #icon>
|
||||
<ShoppingCartOutlined />
|
||||
@@ -188,7 +196,8 @@ import {
|
||||
BellOutlined,
|
||||
QuestionCircleOutlined,
|
||||
LogoutOutlined,
|
||||
FileTextOutlined
|
||||
FileTextOutlined,
|
||||
EnvironmentOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
@@ -92,26 +92,8 @@ const onFinish = async (values: FormState) => {
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
// 调用真实登录接口
|
||||
const response = await authAPI.login(values)
|
||||
|
||||
// 保存token
|
||||
if (response?.data?.token) {
|
||||
localStorage.setItem('admin_token', response.data.token)
|
||||
} else if (response?.token) {
|
||||
localStorage.setItem('admin_token', response.token)
|
||||
} else {
|
||||
throw new Error('登录响应中缺少token')
|
||||
}
|
||||
|
||||
// 更新用户状态
|
||||
if (response?.data?.admin) {
|
||||
appStore.setUser(response.data.admin)
|
||||
} else if (response?.admin) {
|
||||
appStore.setUser(response.admin)
|
||||
} else {
|
||||
throw new Error('登录响应中缺少用户信息')
|
||||
}
|
||||
// 使用store的login方法,它会处理token保存和权限设置
|
||||
await appStore.login(values)
|
||||
|
||||
message.success('登录成功!')
|
||||
|
||||
|
||||
@@ -1,865 +0,0 @@
|
||||
<template>
|
||||
<div class="animal-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('animal:write')"
|
||||
type="primary"
|
||||
@click="showCreateModal"
|
||||
>
|
||||
<template #icon>
|
||||
<PlusOutlined />
|
||||
</template>
|
||||
新增动物
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-page-header>
|
||||
|
||||
<a-tabs v-model:activeKey="activeTab" @change="handleTabChange">
|
||||
<a-tab-pane key="animals" tab="动物列表">
|
||||
<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 label="类型">
|
||||
<a-select
|
||||
v-model:value="searchForm.type"
|
||||
placeholder="全部类型"
|
||||
style="width: 120px"
|
||||
allow-clear
|
||||
>
|
||||
<a-select-option value="alpaca">羊驼</a-select-option>
|
||||
<a-select-option value="dog">狗狗</a-select-option>
|
||||
<a-select-option value="cat">猫咪</a-select-option>
|
||||
<a-select-option value="rabbit">兔子</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="状态">
|
||||
<a-select
|
||||
v-model:value="searchForm.status"
|
||||
placeholder="全部状态"
|
||||
style="width: 120px"
|
||||
allow-clear
|
||||
>
|
||||
<a-select-option value="available">可认领</a-select-option>
|
||||
<a-select-option value="claimed">已认领</a-select-option>
|
||||
<a-select-option value="reserved">预留中</a-select-option>
|
||||
</a-select>
|
||||
</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="animalColumns"
|
||||
:data-source="animalList"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
:row-key="record => record.id"
|
||||
@change="handleTableChange"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'image'">
|
||||
<a-image
|
||||
:width="60"
|
||||
:height="60"
|
||||
:src="record.image_url"
|
||||
:fallback="fallbackImage"
|
||||
style="border-radius: 6px;"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'type'">
|
||||
<a-tag :color="getTypeColor(record.type)">
|
||||
{{ getTypeText(record.type) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'status'">
|
||||
<a-tag :color="getStatusColor(record.status)">
|
||||
{{ getStatusText(record.status) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'price'">
|
||||
¥{{ record.price }}
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'actions'">
|
||||
<a-space :size="8">
|
||||
<a-button size="small" @click="handleViewAnimal(record)">
|
||||
<EyeOutlined />
|
||||
查看
|
||||
</a-button>
|
||||
|
||||
<a-button
|
||||
v-if="hasPermission('animal:write')"
|
||||
size="small"
|
||||
@click="handleEditAnimal(record)"
|
||||
>
|
||||
<EditOutlined />
|
||||
编辑
|
||||
</a-button>
|
||||
|
||||
<a-button
|
||||
v-if="hasPermission('animal:write')"
|
||||
size="small"
|
||||
danger
|
||||
@click="handleDeleteAnimal(record)"
|
||||
>
|
||||
<DeleteOutlined />
|
||||
删除
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</a-tab-pane>
|
||||
|
||||
<a-tab-pane key="claims" tab="认领记录">
|
||||
<a-card>
|
||||
<!-- 认领记录搜索 -->
|
||||
<div class="search-container">
|
||||
<a-form layout="inline" :model="claimSearchForm">
|
||||
<a-form-item label="关键词">
|
||||
<a-input
|
||||
v-model:value="claimSearchForm.keyword"
|
||||
placeholder="用户/动物名称"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="状态">
|
||||
<a-select
|
||||
v-model:value="claimSearchForm.status"
|
||||
placeholder="全部状态"
|
||||
style="width: 120px"
|
||||
allow-clear
|
||||
>
|
||||
<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="completed">已完成</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
<a-button type="primary" @click="handleClaimSearch">
|
||||
<template #icon>
|
||||
<SearchOutlined />
|
||||
</template>
|
||||
搜索
|
||||
</a-button>
|
||||
<a-button style="margin-left: 8px" @click="handleClaimReset">
|
||||
重置
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
|
||||
<!-- 认领记录表格 -->
|
||||
<a-table
|
||||
:columns="claimColumns"
|
||||
:data-source="claimList"
|
||||
:loading="claimLoading"
|
||||
:pagination="claimPagination"
|
||||
:row-key="record => record.id"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'animal_image'">
|
||||
<a-image
|
||||
:width="40"
|
||||
:height="40"
|
||||
:src="record.animal_image"
|
||||
:fallback="fallbackImage"
|
||||
style="border-radius: 4px;"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'status'">
|
||||
<a-tag :color="getClaimStatusColor(record.status)">
|
||||
{{ getClaimStatusText(record.status) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'actions'">
|
||||
<a-space :size="8">
|
||||
<template v-if="record.status === 'pending' && hasPermission('animal:write')">
|
||||
<a-button size="small" type="primary" @click="handleApproveClaim(record)">
|
||||
<CheckOutlined />
|
||||
通过
|
||||
</a-button>
|
||||
<a-button size="small" danger @click="handleRejectClaim(record)">
|
||||
<CloseOutlined />
|
||||
拒绝
|
||||
</a-button>
|
||||
</template>
|
||||
|
||||
<a-button size="small" @click="handleViewClaim(record)">
|
||||
<EyeOutlined />
|
||||
详情
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
|
||||
<!-- 创建/编辑动物模态框 -->
|
||||
<a-modal
|
||||
v-model:open="modalVisible"
|
||||
:title="modalTitle"
|
||||
:confirm-loading="modalLoading"
|
||||
@ok="handleModalOk"
|
||||
@cancel="handleModalCancel"
|
||||
width="600px"
|
||||
>
|
||||
<a-form
|
||||
ref="animalFormRef"
|
||||
:model="currentAnimal"
|
||||
:rules="formRules"
|
||||
layout="vertical"
|
||||
>
|
||||
<a-form-item label="动物名称" name="name">
|
||||
<a-input v-model:value="currentAnimal.name" placeholder="请输入动物名称" />
|
||||
</a-form-item>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="类型" name="type">
|
||||
<a-select v-model:value="currentAnimal.type" placeholder="请选择类型">
|
||||
<a-select-option value="alpaca">羊驼</a-select-option>
|
||||
<a-select-option value="dog">狗狗</a-select-option>
|
||||
<a-select-option value="cat">猫咪</a-select-option>
|
||||
<a-select-option value="rabbit">兔子</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="品种" name="breed">
|
||||
<a-input v-model:value="currentAnimal.breed" placeholder="请输入品种" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="年龄" name="age">
|
||||
<a-input-number
|
||||
v-model:value="currentAnimal.age"
|
||||
:min="0"
|
||||
:max="100"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="性别" name="gender">
|
||||
<a-select v-model:value="currentAnimal.gender" placeholder="请选择性别">
|
||||
<a-select-option value="male">雄性</a-select-option>
|
||||
<a-select-option value="female">雌性</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="price">
|
||||
<a-input-number
|
||||
v-model:value="currentAnimal.price"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="状态" name="status">
|
||||
<a-select v-model:value="currentAnimal.status" placeholder="请选择状态">
|
||||
<a-select-option value="available">可认领</a-select-option>
|
||||
<a-select-option value="claimed">已认领</a-select-option>
|
||||
<a-select-option value="reserved">预留中</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-form-item label="图片URL" name="image_url">
|
||||
<a-input v-model:value="currentAnimal.image_url" placeholder="请输入图片URL" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="描述" name="description">
|
||||
<a-textarea
|
||||
v-model:value="currentAnimal.description"
|
||||
placeholder="请输入动物描述"
|
||||
:rows="4"
|
||||
/>
|
||||
</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 {
|
||||
ReloadOutlined,
|
||||
SearchOutlined,
|
||||
PlusOutlined,
|
||||
EyeOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
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'
|
||||
|
||||
interface SearchForm {
|
||||
keyword: string
|
||||
type: string
|
||||
status: string
|
||||
}
|
||||
|
||||
interface ClaimSearchForm {
|
||||
keyword: string
|
||||
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)
|
||||
|
||||
const searchForm = reactive<SearchForm>({
|
||||
keyword: '',
|
||||
type: '',
|
||||
status: ''
|
||||
})
|
||||
|
||||
const claimSearchForm = reactive<ClaimSearchForm>({
|
||||
keyword: '',
|
||||
status: ''
|
||||
})
|
||||
|
||||
const animalList = ref<Animal[]>([])
|
||||
const claimList = ref<AnimalClaim[]>([])
|
||||
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 20,
|
||||
total: 0,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total: number) => `共 ${total} 条记录`
|
||||
})
|
||||
|
||||
const claimPagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 20,
|
||||
total: 0,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total: number) => `共 ${total} 条记录`
|
||||
})
|
||||
|
||||
const animalColumns = [
|
||||
{
|
||||
title: '图片',
|
||||
key: 'image',
|
||||
width: 80,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '类型',
|
||||
key: 'type',
|
||||
width: 100,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '品种',
|
||||
dataIndex: 'breed',
|
||||
key: 'breed',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '年龄',
|
||||
dataIndex: 'age',
|
||||
key: 'age',
|
||||
width: 80,
|
||||
align: 'center',
|
||||
customRender: ({ text }: { text: number }) => `${text}岁`
|
||||
},
|
||||
{
|
||||
title: '价格',
|
||||
key: 'price',
|
||||
width: 100,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
key: 'status',
|
||||
width: 100,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'created_at',
|
||||
key: 'created_at',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 150,
|
||||
align: 'center'
|
||||
}
|
||||
]
|
||||
|
||||
const claimColumns = [
|
||||
{
|
||||
title: '动物',
|
||||
key: 'animal_image',
|
||||
width: 60,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '动物名称',
|
||||
dataIndex: 'animal_name',
|
||||
key: 'animal_name',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '用户',
|
||||
dataIndex: 'user_name',
|
||||
key: 'user_name',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '联系电话',
|
||||
dataIndex: 'user_phone',
|
||||
key: 'user_phone',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
key: 'status',
|
||||
width: 100,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '申请时间',
|
||||
dataIndex: 'applied_at',
|
||||
key: 'applied_at',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '处理时间',
|
||||
dataIndex: 'processed_at',
|
||||
key: 'processed_at',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 150,
|
||||
align: 'center'
|
||||
}
|
||||
]
|
||||
|
||||
const fallbackImage = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjAiIGhlaWdodD0iNjAiIHZpZXdCb3g9IjAgMCA2MCA2MCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjYwIiBoZWlnaHQ9IjYwIiBmaWxsPSIjRkZGIi8+CjxwYXRoIGQ9Ik0zMCAxNUMzMS42NTY5IDE1IDMzIDE2LjM0MzEgMzMgMThDMzMgMTkuNjU2OSAzMS42NTY5IDIxIDMwIDIxQzI4LjM0MzEgMjEgMjcgMTkuNjU2OSAyNyAxOEMyNyAxNi4zNDMxIDI4LjM0MzEgMTUgMzAgMTVaIiBmaWxsPSIjQ0NDQ0NDIi8+CjxwYXRoIGQ9Ik0yMi41IDI1QzIyLjUgMjUuODI4NCAyMS44Mjg0IDI2LjUgMjEgMjYuNUgxOEMxOC4xNzE2IDI2LjUgMTcuNSAyNS44Mjg0IDE3LjUgMjVDMTcuNSAyNC4xNzE2IDE4LjE3MTYgMjMuNSAxOSAyMy45SDIxQzIxLjgyODQgMjMuNSAyMi41IDI0LjE3MTYgMjIuNSAyNVoiIGZpbGw9IiNDQ0NDQ0MiLz4KPHBhdGggZD0iTTQyLjUgMjVDNDIuNSAyNS44Mjg0IDQxLjgyODQgMjYuNSA0MSAyNi41SDM5QzM4LjE3MTYgMjYuNSAzNy41IDI1LjgyODQgMzcuNSAyNUMzNy41IDI0LjE3MTYgMzguMTcxNiAyMy41IDM5IDIzLjVMNDEgMjMuNUM0MS44Mjg0IDIzLjUgNDIuNSAyNC4xNzE2IDQyLjUgMjVaIiBmaWxsPSIjQ0NDQ0NDIi8+Cjwvc3ZnPgo='
|
||||
|
||||
// 类型映射
|
||||
const getTypeColor = (type: string) => {
|
||||
const colors = {
|
||||
alpaca: 'pink',
|
||||
dog: 'orange',
|
||||
cat: 'blue',
|
||||
rabbit: 'green'
|
||||
}
|
||||
return colors[type as keyof typeof colors] || 'default'
|
||||
}
|
||||
|
||||
const getTypeText = (type: string) => {
|
||||
const texts = {
|
||||
alpaca: '羊驼',
|
||||
dog: '狗狗',
|
||||
cat: '猫咪',
|
||||
rabbit: '兔子'
|
||||
}
|
||||
return texts[type as keyof typeof texts] || '未知'
|
||||
}
|
||||
|
||||
// 状态映射
|
||||
const getStatusColor = (status: string) => {
|
||||
const colors = {
|
||||
available: 'green',
|
||||
claimed: 'blue',
|
||||
reserved: 'orange'
|
||||
}
|
||||
return colors[status as keyof typeof colors] || 'default'
|
||||
}
|
||||
|
||||
const getStatusText = (status: string) => {
|
||||
const texts = {
|
||||
available: '可认领',
|
||||
claimed: '已认领',
|
||||
reserved: '预留中'
|
||||
}
|
||||
return texts[status as keyof typeof texts] || '未知'
|
||||
}
|
||||
|
||||
const getClaimStatusColor = (status: string) => {
|
||||
const colors = {
|
||||
pending: 'orange',
|
||||
approved: 'green',
|
||||
rejected: 'red',
|
||||
completed: 'blue'
|
||||
}
|
||||
return colors[status as keyof typeof colors] || 'default'
|
||||
}
|
||||
|
||||
const getClaimStatusText = (status: string) => {
|
||||
const texts = {
|
||||
pending: '待审核',
|
||||
approved: '已通过',
|
||||
rejected: '已拒绝',
|
||||
completed: '已完成'
|
||||
}
|
||||
return texts[status as keyof typeof texts] || '未知'
|
||||
}
|
||||
|
||||
// 添加模态框相关状态
|
||||
const modalVisible = ref(false)
|
||||
const modalLoading = ref(false)
|
||||
const modalTitle = ref('新增动物')
|
||||
const isEditing = ref(false)
|
||||
const animalFormRef = ref<FormInstance>()
|
||||
|
||||
// 当前动物数据
|
||||
const currentAnimal = ref<Partial<Animal>>({})
|
||||
|
||||
// 表单验证规则
|
||||
const formRules = {
|
||||
name: [
|
||||
{ required: true, message: '请输入动物名称' }
|
||||
],
|
||||
type: [
|
||||
{ required: true, message: '请选择类型' }
|
||||
],
|
||||
breed: [
|
||||
{ required: true, message: '请输入品种' }
|
||||
],
|
||||
age: [
|
||||
{ required: true, message: '请输入年龄' }
|
||||
],
|
||||
gender: [
|
||||
{ required: true, message: '请选择性别' }
|
||||
],
|
||||
price: [
|
||||
{ required: true, message: '请输入价格' }
|
||||
],
|
||||
status: [
|
||||
{ required: true, message: '请选择状态' }
|
||||
]
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
loadAnimals()
|
||||
loadClaims()
|
||||
})
|
||||
|
||||
// 方法
|
||||
const loadAnimals = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await getAnimals({
|
||||
page: pagination.current,
|
||||
pageSize: pagination.pageSize,
|
||||
keyword: searchForm.keyword,
|
||||
type: searchForm.type,
|
||||
status: searchForm.status
|
||||
})
|
||||
|
||||
animalList.value = response.data
|
||||
pagination.total = response.pagination?.total || 0
|
||||
} catch (error) {
|
||||
message.error('加载动物列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const loadClaims = async () => {
|
||||
claimLoading.value = true
|
||||
try {
|
||||
const response = await getAnimalClaims({
|
||||
page: claimPagination.current,
|
||||
pageSize: claimPagination.pageSize,
|
||||
keyword: claimSearchForm.keyword,
|
||||
status: claimSearchForm.status
|
||||
})
|
||||
|
||||
claimList.value = response.data
|
||||
claimPagination.total = response.pagination?.total || 0
|
||||
} catch (error) {
|
||||
message.error('加载认领记录失败')
|
||||
} finally {
|
||||
claimLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleTabChange = (key: string) => {
|
||||
if (key === 'animals') {
|
||||
loadAnimals()
|
||||
} else if (key === 'claims') {
|
||||
loadClaims()
|
||||
}
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
pagination.current = 1
|
||||
loadAnimals()
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
Object.assign(searchForm, {
|
||||
keyword: '',
|
||||
type: '',
|
||||
status: ''
|
||||
})
|
||||
pagination.current = 1
|
||||
loadAnimals()
|
||||
}
|
||||
|
||||
const handleClaimSearch = () => {
|
||||
claimPagination.current = 1
|
||||
loadClaims()
|
||||
}
|
||||
|
||||
const handleClaimReset = () => {
|
||||
Object.assign(claimSearchForm, {
|
||||
keyword: '',
|
||||
status: ''
|
||||
})
|
||||
claimPagination.current = 1
|
||||
loadClaims()
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
if (activeTab.value === 'animals') {
|
||||
loadAnimals()
|
||||
} else {
|
||||
loadClaims()
|
||||
}
|
||||
message.success('数据已刷新')
|
||||
}
|
||||
|
||||
const handleTableChange: TableProps['onChange'] = (pag) => {
|
||||
pagination.current = pag.current!
|
||||
pagination.pageSize = pag.pageSize!
|
||||
loadAnimals()
|
||||
}
|
||||
|
||||
const handleViewAnimal = (record: Animal) => {
|
||||
message.info(`查看动物: ${record.name}`)
|
||||
}
|
||||
|
||||
const handleEditAnimal = async (record: Animal) => {
|
||||
try {
|
||||
modalLoading.value = true
|
||||
modalTitle.value = '编辑动物'
|
||||
isEditing.value = true
|
||||
|
||||
// 获取动物详情
|
||||
const response = await getAnimal(record.id)
|
||||
currentAnimal.value = response.data
|
||||
modalVisible.value = true
|
||||
} catch (error) {
|
||||
message.error('获取动物详情失败')
|
||||
} finally {
|
||||
modalLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeleteAnimal = async (record: Animal) => {
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: `确定要删除动物 "${record.name}" 吗?`,
|
||||
okText: '确定',
|
||||
okType: 'danger',
|
||||
onOk: async () => {
|
||||
try {
|
||||
await deleteAnimal(record.id)
|
||||
message.success('动物已删除')
|
||||
loadAnimals()
|
||||
} catch (error) {
|
||||
message.error('删除失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleApproveClaim = async (record: AnimalClaim) => {
|
||||
Modal.confirm({
|
||||
title: '确认通过',
|
||||
content: `确定要通过用户 "${record.user_name}" 的认领申请吗?`,
|
||||
onOk: async () => {
|
||||
try {
|
||||
await approveAnimalClaim(record.id)
|
||||
message.success('认领申请已通过')
|
||||
loadClaims()
|
||||
} catch (error) {
|
||||
message.error('操作失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleRejectClaim = async (record: AnimalClaim) => {
|
||||
Modal.confirm({
|
||||
title: '确认拒绝',
|
||||
content: `确定要拒绝用户 "${record.user_name}" 的认领申请吗?`,
|
||||
onOk: async () => {
|
||||
try {
|
||||
await rejectAnimalClaim(record.id, '拒绝原因')
|
||||
message.success('认领申请已拒绝')
|
||||
loadClaims()
|
||||
} catch (error) {
|
||||
message.error('操作失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleViewClaim = (record: AnimalClaim) => {
|
||||
message.info(`查看认领详情: ${record.animal_name}`)
|
||||
}
|
||||
|
||||
const showCreateModal = () => {
|
||||
modalTitle.value = '新增动物'
|
||||
isEditing.value = false
|
||||
currentAnimal.value = {
|
||||
age: 1,
|
||||
price: 0,
|
||||
status: 'available'
|
||||
}
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
const handleModalOk = () => {
|
||||
animalFormRef.value
|
||||
?.validate()
|
||||
.then(() => {
|
||||
if (isEditing.value) {
|
||||
handleUpdateAnimal()
|
||||
} else {
|
||||
handleCreateAnimal()
|
||||
}
|
||||
})
|
||||
.catch((error: any) => {
|
||||
console.error('表单验证失败:', error)
|
||||
})
|
||||
}
|
||||
|
||||
const handleModalCancel = () => {
|
||||
modalVisible.value = false
|
||||
animalFormRef.value?.resetFields()
|
||||
}
|
||||
|
||||
const handleCreateAnimal = async () => {
|
||||
try {
|
||||
modalLoading.value = true
|
||||
await createAnimal(currentAnimal.value as AnimalCreateData)
|
||||
message.success('创建动物成功')
|
||||
modalVisible.value = false
|
||||
loadAnimals()
|
||||
} catch (error) {
|
||||
message.error('创建动物失败')
|
||||
} finally {
|
||||
modalLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleUpdateAnimal = async () => {
|
||||
try {
|
||||
modalLoading.value = true
|
||||
await updateAnimal(currentAnimal.value.id!, currentAnimal.value as AnimalUpdateData)
|
||||
message.success('更新动物成功')
|
||||
modalVisible.value = false
|
||||
loadAnimals()
|
||||
} catch (error) {
|
||||
message.error('更新动物失败')
|
||||
} finally {
|
||||
modalLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.animal-management {
|
||||
.search-container {
|
||||
margin-bottom: 16px;
|
||||
padding: 16px;
|
||||
background: #fafafa;
|
||||
border-radius: 6px;
|
||||
|
||||
:deep(.ant-form-item) {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,219 +0,0 @@
|
||||
<template>
|
||||
<a-modal
|
||||
v-model:open="visible"
|
||||
title="动物详情"
|
||||
width="800px"
|
||||
:footer="null"
|
||||
@cancel="handleClose"
|
||||
>
|
||||
<div v-if="animal" class="animal-detail">
|
||||
<!-- 基本信息 -->
|
||||
<a-card title="基本信息" class="mb-4">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="6">
|
||||
<div class="animal-avatar">
|
||||
<a-avatar
|
||||
:src="animal.avatar"
|
||||
:alt="animal.name"
|
||||
:size="120"
|
||||
shape="square"
|
||||
>
|
||||
{{ animal.name?.charAt(0) }}
|
||||
</a-avatar>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :span="18">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="8">
|
||||
<div class="detail-item">
|
||||
<label>动物ID:</label>
|
||||
<span>{{ animal.id }}</span>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<div class="detail-item">
|
||||
<label>名称:</label>
|
||||
<span>{{ animal.name }}</span>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<div class="detail-item">
|
||||
<label>类型:</label>
|
||||
<a-tag color="blue">{{ animal.type }}</a-tag>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<div class="detail-item">
|
||||
<label>品种:</label>
|
||||
<span>{{ animal.breed }}</span>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<div class="detail-item">
|
||||
<label>年龄:</label>
|
||||
<span>{{ animal.age }}岁</span>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<div class="detail-item">
|
||||
<label>性别:</label>
|
||||
<span>{{ animal.gender }}</span>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<div class="detail-item">
|
||||
<label>颜色:</label>
|
||||
<span>{{ animal.color }}</span>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<div class="detail-item">
|
||||
<label>状态:</label>
|
||||
<a-tag :color="getStatusColor(animal.status)">
|
||||
{{ getStatusText(animal.status) }}
|
||||
</a-tag>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<div class="detail-item">
|
||||
<label>健康状态:</label>
|
||||
<span>{{ animal.health_status }}</span>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
|
||||
<!-- 详细描述 -->
|
||||
<a-card title="详细描述" class="mb-4">
|
||||
<p>{{ animal.description || '暂无描述' }}</p>
|
||||
</a-card>
|
||||
|
||||
<!-- 位置信息 -->
|
||||
<a-card title="位置信息" class="mb-4">
|
||||
<div class="detail-item">
|
||||
<label>当前位置:</label>
|
||||
<span>{{ animal.location }}</span>
|
||||
</div>
|
||||
</a-card>
|
||||
|
||||
<!-- 时间信息 -->
|
||||
<a-card title="时间信息">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="8">
|
||||
<div class="detail-item">
|
||||
<label>创建时间:</label>
|
||||
<span>{{ formatDate(animal.createdAt) }}</span>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<div class="detail-item">
|
||||
<label>更新时间:</label>
|
||||
<span>{{ formatDate(animal.updatedAt) }}</span>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
</div>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
interface Animal {
|
||||
id: number
|
||||
name: string
|
||||
type: string
|
||||
breed: string
|
||||
age: number
|
||||
gender: string
|
||||
color: string
|
||||
avatar: string
|
||||
description: string
|
||||
status: 'available' | 'adopted' | 'unavailable'
|
||||
health_status: string
|
||||
location: string
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
interface Props {
|
||||
animal: Animal | null
|
||||
visible: boolean
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:visible', value: boolean): void
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.visible,
|
||||
set: (value) => emit('update:visible', value)
|
||||
})
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateStr: string) => {
|
||||
return new Date(dateStr).toLocaleString('zh-CN')
|
||||
}
|
||||
|
||||
// 获取状态颜色
|
||||
const getStatusColor = (status: string) => {
|
||||
const colorMap: Record<string, string> = {
|
||||
available: 'green',
|
||||
adopted: 'blue',
|
||||
unavailable: 'red'
|
||||
}
|
||||
return colorMap[status] || 'default'
|
||||
}
|
||||
|
||||
// 获取状态文本
|
||||
const getStatusText = (status: string) => {
|
||||
const textMap: Record<string, string> = {
|
||||
available: '可领养',
|
||||
adopted: '已领养',
|
||||
unavailable: '不可领养'
|
||||
}
|
||||
return textMap[status] || status
|
||||
}
|
||||
|
||||
// 关闭弹窗
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.animal-detail {
|
||||
.animal-avatar {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
margin-bottom: 12px;
|
||||
|
||||
label {
|
||||
display: inline-block;
|
||||
width: 80px;
|
||||
color: #666;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
span {
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.mb-4 {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,362 +0,0 @@
|
||||
<template>
|
||||
<a-modal
|
||||
v-model:open="visible"
|
||||
:title="isEditing ? '编辑动物' : '新增动物'"
|
||||
width="800px"
|
||||
:confirm-loading="loading"
|
||||
@ok="handleSubmit"
|
||||
@cancel="handleClose"
|
||||
>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="rules"
|
||||
layout="vertical"
|
||||
>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="动物名称" name="name">
|
||||
<a-input
|
||||
v-model:value="formData.name"
|
||||
placeholder="请输入动物名称"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="动物类型" name="type">
|
||||
<a-select
|
||||
v-model:value="formData.type"
|
||||
placeholder="请选择动物类型"
|
||||
>
|
||||
<a-select-option value="狗">狗</a-select-option>
|
||||
<a-select-option value="猫">猫</a-select-option>
|
||||
<a-select-option value="兔子">兔子</a-select-option>
|
||||
<a-select-option value="鸟类">鸟类</a-select-option>
|
||||
<a-select-option value="其他">其他</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="breed">
|
||||
<a-input
|
||||
v-model:value="formData.breed"
|
||||
placeholder="请输入品种"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="年龄" name="age">
|
||||
<a-input-number
|
||||
v-model:value="formData.age"
|
||||
placeholder="请输入年龄"
|
||||
:min="0"
|
||||
:max="30"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="性别" name="gender">
|
||||
<a-select
|
||||
v-model:value="formData.gender"
|
||||
placeholder="请选择性别"
|
||||
>
|
||||
<a-select-option value="雄性">雄性</a-select-option>
|
||||
<a-select-option value="雌性">雌性</a-select-option>
|
||||
<a-select-option value="未知">未知</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="颜色" name="color">
|
||||
<a-input
|
||||
v-model:value="formData.color"
|
||||
placeholder="请输入颜色"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="状态" name="status">
|
||||
<a-select
|
||||
v-model:value="formData.status"
|
||||
placeholder="请选择状态"
|
||||
>
|
||||
<a-select-option value="available">可领养</a-select-option>
|
||||
<a-select-option value="adopted">已领养</a-select-option>
|
||||
<a-select-option value="unavailable">不可领养</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="健康状态" name="health_status">
|
||||
<a-select
|
||||
v-model:value="formData.health_status"
|
||||
placeholder="请选择健康状态"
|
||||
>
|
||||
<a-select-option value="健康">健康</a-select-option>
|
||||
<a-select-option value="轻微疾病">轻微疾病</a-select-option>
|
||||
<a-select-option value="需要治疗">需要治疗</a-select-option>
|
||||
<a-select-option value="康复中">康复中</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-form-item label="位置" name="location">
|
||||
<a-input
|
||||
v-model:value="formData.location"
|
||||
placeholder="请输入当前位置"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="头像" name="avatar">
|
||||
<a-upload
|
||||
v-model:file-list="fileList"
|
||||
name="avatar"
|
||||
list-type="picture-card"
|
||||
class="avatar-uploader"
|
||||
:show-upload-list="false"
|
||||
:before-upload="beforeUpload"
|
||||
@change="handleChange"
|
||||
>
|
||||
<div v-if="formData.avatar">
|
||||
<img :src="formData.avatar" alt="avatar" style="width: 100%" />
|
||||
</div>
|
||||
<div v-else>
|
||||
<PlusOutlined />
|
||||
<div style="margin-top: 8px">上传头像</div>
|
||||
</div>
|
||||
</a-upload>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="描述" name="description">
|
||||
<a-textarea
|
||||
v-model:value="formData.description"
|
||||
placeholder="请输入动物描述"
|
||||
:rows="4"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, watch } from 'vue'
|
||||
import { PlusOutlined } from '@ant-design/icons-vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import type { FormInstance, UploadChangeParam } from 'ant-design-vue'
|
||||
|
||||
interface Animal {
|
||||
id: number
|
||||
name: string
|
||||
type: string
|
||||
breed: string
|
||||
age: number
|
||||
gender: string
|
||||
color: string
|
||||
avatar: string
|
||||
description: string
|
||||
status: 'available' | 'adopted' | 'unavailable'
|
||||
health_status: string
|
||||
location: string
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
interface Props {
|
||||
animal: Animal | null
|
||||
visible: boolean
|
||||
isEditing: boolean
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:visible', value: boolean): void
|
||||
(e: 'submit', data: any): void
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
const loading = ref(false)
|
||||
const fileList = ref([])
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.visible,
|
||||
set: (value) => emit('update:visible', value)
|
||||
})
|
||||
|
||||
// 表单数据
|
||||
const formData = reactive({
|
||||
name: '',
|
||||
type: '',
|
||||
breed: '',
|
||||
age: 0,
|
||||
gender: '',
|
||||
color: '',
|
||||
status: 'available' as 'available' | 'adopted' | 'unavailable',
|
||||
health_status: '健康',
|
||||
location: '',
|
||||
avatar: '',
|
||||
description: ''
|
||||
})
|
||||
|
||||
// 表单验证规则
|
||||
const rules: Record<string, any[]> = {
|
||||
name: [
|
||||
{ required: true, message: '请输入动物名称', trigger: 'blur' },
|
||||
{ min: 1, max: 50, message: '名称长度为1-50个字符', trigger: 'blur' }
|
||||
],
|
||||
type: [
|
||||
{ required: true, message: '请选择动物类型', trigger: 'change' }
|
||||
],
|
||||
breed: [
|
||||
{ required: true, message: '请输入品种', trigger: 'blur' }
|
||||
],
|
||||
age: [
|
||||
{ required: true, message: '请输入年龄', trigger: 'blur' },
|
||||
{ type: 'number', min: 0, max: 30, message: '年龄必须在0-30之间', trigger: 'blur' }
|
||||
],
|
||||
gender: [
|
||||
{ required: true, message: '请选择性别', trigger: 'change' }
|
||||
],
|
||||
status: [
|
||||
{ required: true, message: '请选择状态', trigger: 'change' }
|
||||
],
|
||||
location: [
|
||||
{ required: true, message: '请输入位置', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
// 监听动物数据变化,初始化表单
|
||||
watch(() => props.animal, (animal) => {
|
||||
if (animal && props.isEditing) {
|
||||
formData.name = animal.name
|
||||
formData.type = animal.type
|
||||
formData.breed = animal.breed
|
||||
formData.age = animal.age
|
||||
formData.gender = animal.gender
|
||||
formData.color = animal.color
|
||||
formData.status = animal.status
|
||||
formData.health_status = animal.health_status
|
||||
formData.location = animal.location
|
||||
formData.avatar = animal.avatar
|
||||
formData.description = animal.description
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
// 监听弹窗显示状态,重置表单
|
||||
watch(() => props.visible, (visible) => {
|
||||
if (visible && !props.isEditing) {
|
||||
resetForm()
|
||||
}
|
||||
})
|
||||
|
||||
// 重置表单
|
||||
const resetForm = () => {
|
||||
Object.assign(formData, {
|
||||
name: '',
|
||||
type: '',
|
||||
breed: '',
|
||||
age: 0,
|
||||
gender: '',
|
||||
color: '',
|
||||
status: 'available',
|
||||
health_status: '健康',
|
||||
location: '',
|
||||
avatar: '',
|
||||
description: ''
|
||||
})
|
||||
fileList.value = []
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
|
||||
// 文件上传前验证
|
||||
const beforeUpload = (file: File) => {
|
||||
const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png'
|
||||
if (!isJpgOrPng) {
|
||||
message.error('只能上传 JPG/PNG 格式的图片!')
|
||||
return false
|
||||
}
|
||||
const isLt2M = file.size / 1024 / 1024 < 2
|
||||
if (!isLt2M) {
|
||||
message.error('图片大小不能超过 2MB!')
|
||||
return false
|
||||
}
|
||||
return false // 阻止自动上传
|
||||
}
|
||||
|
||||
// 文件上传变化处理
|
||||
const handleChange = (info: UploadChangeParam) => {
|
||||
if (info.file.originFileObj) {
|
||||
// 创建预览URL
|
||||
const reader = new FileReader()
|
||||
reader.onload = (e) => {
|
||||
formData.avatar = e.target?.result as string
|
||||
}
|
||||
reader.readAsDataURL(info.file.originFileObj)
|
||||
}
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
await formRef.value?.validate()
|
||||
loading.value = true
|
||||
|
||||
const submitData = {
|
||||
name: formData.name,
|
||||
type: formData.type,
|
||||
breed: formData.breed,
|
||||
age: formData.age,
|
||||
gender: formData.gender,
|
||||
color: formData.color,
|
||||
status: formData.status,
|
||||
health_status: formData.health_status,
|
||||
location: formData.location,
|
||||
avatar: formData.avatar,
|
||||
description: formData.description
|
||||
}
|
||||
|
||||
emit('submit', submitData)
|
||||
} catch (error) {
|
||||
console.error('表单验证失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭弹窗
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
resetForm()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.ant-form-item {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.avatar-uploader {
|
||||
:deep(.ant-upload) {
|
||||
width: 128px;
|
||||
height: 128px;
|
||||
}
|
||||
|
||||
:deep(.ant-upload-select-picture-card) {
|
||||
width: 128px;
|
||||
height: 128px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,716 +0,0 @@
|
||||
<template>
|
||||
<div class="animals-page">
|
||||
<!-- 页面标题 -->
|
||||
<div class="page-header">
|
||||
<h1>动物管理</h1>
|
||||
<p>管理平台上的所有动物信息</p>
|
||||
</div>
|
||||
|
||||
<!-- 数据统计 -->
|
||||
<a-row :gutter="16" class="stats-row">
|
||||
<a-col :span="6">
|
||||
<a-card>
|
||||
<a-statistic
|
||||
title="总动物数"
|
||||
:value="statistics.totalAnimals"
|
||||
:value-style="{ color: '#3f8600' }"
|
||||
>
|
||||
<template #prefix>
|
||||
<HeartOutlined />
|
||||
</template>
|
||||
</a-statistic>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card>
|
||||
<a-statistic
|
||||
title="可领养"
|
||||
:value="statistics.availableAnimals"
|
||||
:value-style="{ color: '#1890ff' }"
|
||||
>
|
||||
<template #prefix>
|
||||
<SmileOutlined />
|
||||
</template>
|
||||
</a-statistic>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card>
|
||||
<a-statistic
|
||||
title="已领养"
|
||||
:value="statistics.claimedAnimals"
|
||||
:value-style="{ color: '#722ed1' }"
|
||||
>
|
||||
<template #prefix>
|
||||
<HomeOutlined />
|
||||
</template>
|
||||
</a-statistic>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card>
|
||||
<a-statistic
|
||||
title="今日新增"
|
||||
:value="statistics.newAnimalsToday"
|
||||
:value-style="{ color: '#cf1322' }"
|
||||
>
|
||||
<template #prefix>
|
||||
<PlusOutlined />
|
||||
</template>
|
||||
</a-statistic>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 高级搜索 -->
|
||||
<AdvancedSearch
|
||||
search-type="animal"
|
||||
:status-options="statusOptions"
|
||||
@search="handleSearch"
|
||||
@reset="handleReset"
|
||||
/>
|
||||
|
||||
<!-- 批量操作 -->
|
||||
<BatchOperations
|
||||
:data-source="animals as any[]"
|
||||
:selected-items="selectedAnimals as any[]"
|
||||
operation-type="animal"
|
||||
:status-options="statusOptionsWithColor"
|
||||
:export-fields="exportFields"
|
||||
@selection-change="(items: any[]) => handleSelectionChange(items as Animal[])"
|
||||
@batch-action="(action: string, items: any[], params?: any) => handleBatchAction(action, items as Animal[], params)"
|
||||
/>
|
||||
|
||||
<!-- 动物列表表格 -->
|
||||
<a-card class="table-card">
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="animals"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
:row-selection="rowSelection"
|
||||
:scroll="{ x: 1200 }"
|
||||
row-key="id"
|
||||
@change="handleTableChange"
|
||||
>
|
||||
<!-- 动物图片 -->
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'avatar'">
|
||||
<a-avatar
|
||||
:src="record.avatar"
|
||||
:alt="record.name"
|
||||
size="large"
|
||||
shape="square"
|
||||
>
|
||||
{{ record.name?.charAt(0) }}
|
||||
</a-avatar>
|
||||
</template>
|
||||
|
||||
<!-- 状态 -->
|
||||
<template v-else-if="column.key === 'status'">
|
||||
<a-tag :color="getStatusColor(record.status)">
|
||||
{{ getStatusText(record.status) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<!-- 动物类型 -->
|
||||
<template v-else-if="column.key === 'type'">
|
||||
<a-tag color="blue">{{ record.type }}</a-tag>
|
||||
</template>
|
||||
|
||||
<!-- 年龄 -->
|
||||
<template v-else-if="column.key === 'age'">
|
||||
{{ record.age }}岁
|
||||
</template>
|
||||
|
||||
<!-- 创建时间 -->
|
||||
<template v-else-if="column.key === 'createdAt'">
|
||||
{{ formatDate(record.createdAt) }}
|
||||
</template>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button type="link" size="small" @click="handleViewAnimal(record)">
|
||||
查看
|
||||
</a-button>
|
||||
<a-button type="link" size="small" @click="handleEditAnimal(record)">
|
||||
编辑
|
||||
</a-button>
|
||||
<a-dropdown>
|
||||
<a-button type="link" size="small">
|
||||
更多 <DownOutlined />
|
||||
</a-button>
|
||||
<template #overlay>
|
||||
<a-menu>
|
||||
<a-menu-item
|
||||
key="activate"
|
||||
v-if="record.status !== 'available'"
|
||||
@click="handleUpdateStatus(record, 'available')"
|
||||
>
|
||||
<CheckCircleOutlined />
|
||||
设为可领养
|
||||
</a-menu-item>
|
||||
<a-menu-item
|
||||
key="deactivate"
|
||||
v-if="record.status !== 'unavailable'"
|
||||
@click="handleUpdateStatus(record, 'unavailable')"
|
||||
>
|
||||
<LockOutlined />
|
||||
设为不可领养
|
||||
</a-menu-item>
|
||||
<a-menu-divider />
|
||||
<a-menu-item key="delete" @click="handleDeleteAnimal(record)">
|
||||
<DeleteOutlined />
|
||||
删除
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
|
||||
<!-- 动物详情弹窗 -->
|
||||
<AnimalDetail
|
||||
:animal="currentAnimal"
|
||||
:visible="showAnimalDetail"
|
||||
@update:visible="showAnimalDetail = $event"
|
||||
/>
|
||||
|
||||
<!-- 动物表单弹窗 -->
|
||||
<AnimalForm
|
||||
:animal="currentAnimal"
|
||||
:visible="showAnimalForm"
|
||||
:is-editing="isEditing"
|
||||
@update:visible="showAnimalForm = $event"
|
||||
@submit="handleAnimalSubmit"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { message, Modal } from 'ant-design-vue'
|
||||
import type { TableColumnsType } from 'ant-design-vue'
|
||||
import { SearchOutlined, PlusOutlined, ExportOutlined, ReloadOutlined } from '@ant-design/icons-vue'
|
||||
import type { Animal } from '@/api/animal'
|
||||
import animalAPI from '@/api/animal'
|
||||
import AdvancedSearch from '@/components/AdvancedSearch.vue'
|
||||
import BatchOperations from '@/components/BatchOperations.vue'
|
||||
import AnimalForm from './components/AnimalForm.vue'
|
||||
import AnimalDetail from './components/AnimalDetail.vue'
|
||||
|
||||
// 移除重复的Animal接口定义,使用api中的接口
|
||||
|
||||
interface Statistics {
|
||||
totalAnimals: number
|
||||
availableAnimals: number
|
||||
newAnimalsToday: number
|
||||
claimedAnimals: number
|
||||
}
|
||||
|
||||
// 临时格式化函数,直到utils/date模块可用
|
||||
const formatDate = (date: string | null | undefined): string => {
|
||||
if (!date) return ''
|
||||
return new Date(date).toLocaleString('zh-CN')
|
||||
}
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(false)
|
||||
const animals = ref<Animal[]>([])
|
||||
const selectedAnimals = ref<Animal[]>([])
|
||||
const currentAnimal = ref<Animal | null>(null)
|
||||
|
||||
// 计算属性
|
||||
const selectedRowKeys = computed(() => selectedAnimals.value.map(animal => animal.id))
|
||||
|
||||
// 统计数据
|
||||
const statistics = reactive<Statistics>({
|
||||
totalAnimals: 0,
|
||||
availableAnimals: 0,
|
||||
newAnimalsToday: 0,
|
||||
claimedAnimals: 0
|
||||
})
|
||||
|
||||
// 分页配置
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 20,
|
||||
total: 0,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total: number, range: [number, number]) =>
|
||||
`第 ${range[0]}-${range[1]} 条,共 ${total} 条`
|
||||
})
|
||||
|
||||
// 搜索参数
|
||||
const searchParams = reactive({
|
||||
keyword: '',
|
||||
type: '',
|
||||
status: '',
|
||||
age_range: [] as number[],
|
||||
date_range: [] as string[]
|
||||
})
|
||||
|
||||
// 模态框状态
|
||||
const showAnimalDetail = ref(false)
|
||||
const showAnimalForm = ref(false)
|
||||
const isEditing = ref(false)
|
||||
|
||||
// 搜索字段配置
|
||||
const searchFields = [
|
||||
{
|
||||
key: 'keyword',
|
||||
label: '关键词',
|
||||
type: 'input',
|
||||
placeholder: '请输入动物名称或描述'
|
||||
},
|
||||
{
|
||||
key: 'type',
|
||||
label: '动物类型',
|
||||
type: 'select',
|
||||
options: [
|
||||
{ label: '狗', value: '狗' },
|
||||
{ label: '猫', value: '猫' },
|
||||
{ label: '兔子', value: '兔子' },
|
||||
{ label: '鸟类', value: '鸟类' }
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'status',
|
||||
label: '状态',
|
||||
type: 'select',
|
||||
options: [
|
||||
{ label: '可领养', value: 'available' },
|
||||
{ label: '已领养', value: 'adopted' },
|
||||
{ label: '不可领养', value: 'unavailable' }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
// 状态选项
|
||||
const statusOptions = [
|
||||
{ label: '可领养', value: 'available', color: 'green' },
|
||||
{ label: '已领养', value: 'adopted', color: 'blue' },
|
||||
{ label: '不可领养', value: 'unavailable', color: 'red' }
|
||||
]
|
||||
|
||||
// 批量操作用的状态选项
|
||||
const statusOptionsWithColor = [
|
||||
{ label: '可领养', value: 'available', color: 'green' },
|
||||
{ label: '已领养', value: 'adopted', color: 'blue' },
|
||||
{ label: '不可领养', value: 'unavailable', color: 'red' }
|
||||
]
|
||||
|
||||
// 导出字段
|
||||
const exportFields = [
|
||||
{ key: 'name', label: '动物名称' },
|
||||
{ key: 'species', label: '物种' },
|
||||
{ key: 'breed', label: '品种' },
|
||||
{ key: 'age', label: '年龄' },
|
||||
{ key: 'gender', label: '性别' },
|
||||
{ key: 'price', label: '价格' },
|
||||
{ key: 'status', label: '状态' },
|
||||
{ key: 'merchant_name', label: '商家' },
|
||||
{ key: 'created_at', label: '创建时间' }
|
||||
]
|
||||
|
||||
// 表格列配置
|
||||
const columns: TableColumnsType<Animal> = [
|
||||
{
|
||||
title: 'ID',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
width: 80,
|
||||
sorter: true
|
||||
},
|
||||
{
|
||||
title: '图片',
|
||||
dataIndex: 'image',
|
||||
key: 'image',
|
||||
width: 80
|
||||
},
|
||||
{
|
||||
title: '名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '物种',
|
||||
dataIndex: 'species',
|
||||
key: 'species',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '品种',
|
||||
dataIndex: 'breed',
|
||||
key: 'breed',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '年龄',
|
||||
dataIndex: 'age',
|
||||
key: 'age',
|
||||
width: 80,
|
||||
sorter: true
|
||||
},
|
||||
{
|
||||
title: '性别',
|
||||
dataIndex: 'gender',
|
||||
key: 'gender',
|
||||
width: 80
|
||||
},
|
||||
{
|
||||
title: '价格',
|
||||
dataIndex: 'price',
|
||||
key: 'price',
|
||||
width: 100,
|
||||
sorter: true
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '商家',
|
||||
dataIndex: 'merchant_name',
|
||||
key: 'merchant_name',
|
||||
width: 150
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'created_at',
|
||||
key: 'created_at',
|
||||
width: 160,
|
||||
sorter: true
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 200,
|
||||
fixed: 'right'
|
||||
}
|
||||
]
|
||||
|
||||
// 表格行选择配置
|
||||
const rowSelection = computed(() => ({
|
||||
selectedRowKeys: selectedAnimals.value.map(item => item.id),
|
||||
onChange: (selectedRowKeys: any[]) => {
|
||||
selectedAnimals.value = animals.value.filter(item =>
|
||||
selectedRowKeys.includes(item.id)
|
||||
)
|
||||
}
|
||||
})) as any
|
||||
|
||||
// 获取状态颜色
|
||||
const getStatusColor = (status: string) => {
|
||||
const colorMap: Record<string, string> = {
|
||||
available: 'green',
|
||||
adopted: 'blue',
|
||||
unavailable: 'red'
|
||||
}
|
||||
return colorMap[status] || 'default'
|
||||
}
|
||||
|
||||
// 获取状态文本
|
||||
const getStatusText = (status: string) => {
|
||||
const textMap: Record<string, string> = {
|
||||
available: '可领养',
|
||||
adopted: '已领养',
|
||||
unavailable: '不可领养'
|
||||
}
|
||||
return textMap[status] || status
|
||||
}
|
||||
|
||||
// 获取动物列表
|
||||
const fetchAnimals = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
// 模拟数据 - 使用类型断言避免类型检查
|
||||
const mockAnimals = [
|
||||
{
|
||||
id: 1,
|
||||
name: '小白',
|
||||
species: '狗',
|
||||
breed: '金毛',
|
||||
age: 2,
|
||||
gender: '雄性',
|
||||
description: '温顺可爱的金毛犬,性格友善,适合家庭饲养',
|
||||
image: 'https://example.com/dog1.jpg',
|
||||
merchant_id: 1,
|
||||
merchant_name: '爱心宠物店',
|
||||
price: 1500,
|
||||
status: 'available',
|
||||
created_at: '2024-01-15T10:30:00Z',
|
||||
updated_at: '2024-01-15T10:30:00Z'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '小花',
|
||||
species: '猫',
|
||||
breed: '英短',
|
||||
age: 1,
|
||||
gender: '雌性',
|
||||
description: '活泼可爱的英短猫,毛色纯正,健康活泼',
|
||||
image: 'https://example.com/cat1.jpg',
|
||||
merchant_id: 2,
|
||||
merchant_name: '温馨宠物之家',
|
||||
price: 2000,
|
||||
status: 'adopted',
|
||||
created_at: '2024-01-14T14:20:00Z',
|
||||
updated_at: '2024-01-16T09:15:00Z'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: '小黑',
|
||||
species: '狗',
|
||||
breed: '拉布拉多',
|
||||
age: 3,
|
||||
gender: '雄性',
|
||||
description: '聪明忠诚的拉布拉多,训练有素,适合陪伴',
|
||||
image: 'https://example.com/dog2.jpg',
|
||||
merchant_id: 1,
|
||||
merchant_name: '爱心宠物店',
|
||||
price: 1800,
|
||||
status: 'available',
|
||||
created_at: '2024-01-13T16:45:00Z',
|
||||
updated_at: '2024-01-13T16:45:00Z'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: '咪咪',
|
||||
species: '猫',
|
||||
breed: '波斯猫',
|
||||
age: 2,
|
||||
gender: '雌性',
|
||||
description: '优雅的波斯猫,毛发柔顺,性格温和',
|
||||
image: 'https://example.com/cat2.jpg',
|
||||
merchant_id: 3,
|
||||
merchant_name: '宠物乐园',
|
||||
price: 2500,
|
||||
status: 'unavailable',
|
||||
created_at: '2024-01-12T11:20:00Z',
|
||||
updated_at: '2024-01-17T14:30:00Z'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: '小灰',
|
||||
species: '兔子',
|
||||
breed: '荷兰兔',
|
||||
age: 1,
|
||||
gender: '雄性',
|
||||
description: '可爱的荷兰兔,毛色灰白相间,性格活泼',
|
||||
image: 'https://example.com/rabbit1.jpg',
|
||||
merchant_id: 2,
|
||||
merchant_name: '温馨宠物之家',
|
||||
price: 800,
|
||||
status: 'available',
|
||||
created_at: '2024-01-16T09:10:00Z',
|
||||
updated_at: '2024-01-16T09:10:00Z'
|
||||
}
|
||||
] as Animal[]
|
||||
|
||||
const mockData = {
|
||||
list: mockAnimals,
|
||||
total: mockAnimals.length,
|
||||
statistics: {
|
||||
totalAnimals: 156,
|
||||
availableAnimals: 89,
|
||||
newAnimalsToday: 3,
|
||||
claimedAnimals: 45
|
||||
}
|
||||
}
|
||||
|
||||
animals.value = mockData.list
|
||||
pagination.total = mockData.total
|
||||
|
||||
// 更新统计数据
|
||||
Object.assign(statistics, mockData.statistics)
|
||||
} catch (error) {
|
||||
message.error('获取动物列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = (params: any) => {
|
||||
Object.assign(searchParams, params)
|
||||
pagination.current = 1
|
||||
fetchAnimals()
|
||||
}
|
||||
|
||||
// 重置搜索
|
||||
const handleReset = () => {
|
||||
Object.assign(searchParams, {
|
||||
keyword: '',
|
||||
type: '',
|
||||
status: '',
|
||||
age_range: [],
|
||||
date_range: []
|
||||
})
|
||||
pagination.current = 1
|
||||
fetchAnimals()
|
||||
}
|
||||
|
||||
// 刷新数据
|
||||
const handleRefresh = () => {
|
||||
fetchAnimals()
|
||||
}
|
||||
|
||||
// 查看动物详情
|
||||
const handleViewAnimal = (animal: Animal) => {
|
||||
currentAnimal.value = animal
|
||||
showAnimalDetail.value = true
|
||||
}
|
||||
|
||||
// 编辑动物
|
||||
const handleEditAnimal = (animal: Animal) => {
|
||||
currentAnimal.value = animal
|
||||
isEditing.value = true
|
||||
showAnimalForm.value = true
|
||||
}
|
||||
|
||||
// 新增动物
|
||||
const handleAddAnimal = () => {
|
||||
currentAnimal.value = null
|
||||
isEditing.value = false
|
||||
showAnimalForm.value = true
|
||||
}
|
||||
|
||||
// 删除动物
|
||||
const handleDeleteAnimal = (animal: Animal) => {
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: `确定要删除动物 "${animal.name}" 吗?`,
|
||||
onOk: async () => {
|
||||
try {
|
||||
// 模拟API调用
|
||||
message.success('删除成功')
|
||||
fetchAnimals()
|
||||
} catch (error) {
|
||||
message.error('删除失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 更新动物状态
|
||||
const handleUpdateStatus = async (animal: Animal, status: string) => {
|
||||
try {
|
||||
// 模拟API调用
|
||||
message.success('状态更新成功')
|
||||
fetchAnimals()
|
||||
} catch (error) {
|
||||
message.error('状态更新失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 批量选择处理
|
||||
const handleSelectionChange = (items: Animal[]) => {
|
||||
selectedAnimals.value = items
|
||||
}
|
||||
|
||||
// 批量操作处理
|
||||
const handleBatchAction = async (action: string, items: Animal[], params?: any) => {
|
||||
try {
|
||||
const animalIds = items.map(animal => animal.id)
|
||||
|
||||
switch (action) {
|
||||
case 'updateStatus':
|
||||
message.success(`批量${params?.status === 'available' ? '设为可领养' : '更新状态'}成功`)
|
||||
break
|
||||
case 'delete':
|
||||
message.success('批量删除成功')
|
||||
break
|
||||
case 'export':
|
||||
message.success('导出成功')
|
||||
break
|
||||
default:
|
||||
message.warning('未知操作')
|
||||
return
|
||||
}
|
||||
|
||||
selectedAnimals.value = []
|
||||
fetchAnimals()
|
||||
} catch (error) {
|
||||
message.error('批量操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 表格变化处理
|
||||
const handleTableChange = (pag: any, filters: any, sorter: any) => {
|
||||
pagination.current = pag.current
|
||||
pagination.pageSize = pag.pageSize
|
||||
|
||||
// TODO: 处理排序和筛选
|
||||
fetchAnimals()
|
||||
}
|
||||
|
||||
// 动物表单提交
|
||||
const handleAnimalSubmit = async (animalData: any) => {
|
||||
try {
|
||||
if (isEditing.value && currentAnimal.value) {
|
||||
message.success('更新成功')
|
||||
} else {
|
||||
message.success('创建成功')
|
||||
}
|
||||
showAnimalForm.value = false
|
||||
fetchAnimals()
|
||||
} catch (error) {
|
||||
message.error(isEditing.value ? '更新失败' : '创建失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 组件挂载时获取数据
|
||||
onMounted(() => {
|
||||
fetchAnimals()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.animals-page {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.statistics-cards {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.table-card {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
:deep(.ant-table-thead > tr > th) {
|
||||
background-color: #fafafa;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
:deep(.ant-table-tbody > tr:hover > td) {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
</style>
|
||||
272
admin-system/src/pages/flower/components/FlowerModal.vue
Normal file
272
admin-system/src/pages/flower/components/FlowerModal.vue
Normal file
@@ -0,0 +1,272 @@
|
||||
<template>
|
||||
<a-modal
|
||||
:title="modalTitle"
|
||||
:visible="visible"
|
||||
:confirm-loading="confirmLoading"
|
||||
:width="800"
|
||||
@cancel="handleCancel"
|
||||
@ok="handleOk"
|
||||
>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="formState"
|
||||
:rules="rules"
|
||||
:label-col="{ span: 6 }"
|
||||
:wrapper-col="{ span: 16 }"
|
||||
>
|
||||
<a-form-item label="花卉名称" name="name">
|
||||
<a-input v-model:value="formState.name" placeholder="请输入花卉名称" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="花卉类型" name="type">
|
||||
<a-select v-model:value="formState.type" placeholder="请选择花卉类型">
|
||||
<a-select-option value="鲜花">鲜花</a-select-option>
|
||||
<a-select-option value="盆栽">盆栽</a-select-option>
|
||||
<a-select-option value="绿植">绿植</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="品种" name="variety">
|
||||
<a-input v-model:value="formState.variety" placeholder="请输入品种" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="价格" name="price">
|
||||
<a-input-number
|
||||
v-model:value="formState.price"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
placeholder="请输入价格"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="库存" name="stock">
|
||||
<a-input-number
|
||||
v-model:value="formState.stock"
|
||||
:min="0"
|
||||
placeholder="请输入库存"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="商家" name="merchantId">
|
||||
<a-select
|
||||
v-model:value="formState.merchantId"
|
||||
placeholder="请选择商家"
|
||||
:loading="merchantsLoading"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="merchant in merchants"
|
||||
:key="merchant.id"
|
||||
:value="merchant.id"
|
||||
>
|
||||
{{ merchant.name }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="状态" name="status">
|
||||
<a-select v-model:value="formState.status" placeholder="请选择状态">
|
||||
<a-select-option value="active">上架</a-select-option>
|
||||
<a-select-option value="inactive">下架</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="图片" name="image">
|
||||
<a-upload
|
||||
v-model:file-list="fileList"
|
||||
list-type="picture-card"
|
||||
:before-upload="beforeUpload"
|
||||
@preview="handlePreview"
|
||||
@remove="handleRemove"
|
||||
>
|
||||
<div v-if="fileList.length < 1">
|
||||
<plus-outlined />
|
||||
<div style="margin-top: 8px">上传图片</div>
|
||||
</div>
|
||||
</a-upload>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="描述" name="description">
|
||||
<a-textarea
|
||||
v-model:value="formState.description"
|
||||
placeholder="请输入花卉描述"
|
||||
:rows="4"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, watch, computed } from 'vue'
|
||||
import { message, type UploadProps } from 'ant-design-vue'
|
||||
import { PlusOutlined } from '@ant-design/icons-vue'
|
||||
import type { FormInstance } from 'ant-design-vue'
|
||||
import { getMerchants } from '@/api/flower'
|
||||
import type { Flower, Merchant } from '@/api/flower'
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
currentRecord: Flower | null
|
||||
mode: 'create' | 'edit' | 'view'
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'cancel'): void
|
||||
(e: 'ok', data: any): void
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
const confirmLoading = ref(false)
|
||||
const merchants = ref<Merchant[]>([])
|
||||
const merchantsLoading = ref(false)
|
||||
const fileList = ref<any[]>([])
|
||||
|
||||
const formState = reactive({
|
||||
name: '',
|
||||
type: '',
|
||||
variety: '',
|
||||
price: 0,
|
||||
stock: 0,
|
||||
merchantId: undefined as number | undefined,
|
||||
status: 'active',
|
||||
image: '',
|
||||
description: ''
|
||||
})
|
||||
|
||||
const rules = {
|
||||
name: [{ required: true, message: '请输入花卉名称', trigger: 'blur' }],
|
||||
type: [{ required: true, message: '请选择花卉类型', trigger: 'change' }],
|
||||
variety: [{ required: true, message: '请输入品种', trigger: 'blur' }],
|
||||
price: [{ required: true, message: '请输入价格', trigger: 'blur' }],
|
||||
stock: [{ required: true, message: '请输入库存', trigger: 'blur' }],
|
||||
merchantId: [{ required: true, message: '请选择商家', trigger: 'change' }],
|
||||
status: [{ required: true, message: '请选择状态', trigger: 'change' }]
|
||||
}
|
||||
|
||||
const modalTitle = computed(() => {
|
||||
switch (props.mode) {
|
||||
case 'create':
|
||||
return '新增花卉'
|
||||
case 'edit':
|
||||
return '编辑花卉'
|
||||
case 'view':
|
||||
return '查看花卉'
|
||||
default:
|
||||
return '花卉信息'
|
||||
}
|
||||
})
|
||||
|
||||
// 加载商家列表
|
||||
const loadMerchants = async () => {
|
||||
try {
|
||||
merchantsLoading.value = true
|
||||
const response = await getMerchants()
|
||||
if (response.data.success) {
|
||||
merchants.value = response.data.data.merchants
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载商家列表失败:', error)
|
||||
message.error('加载商家列表失败')
|
||||
} finally {
|
||||
merchantsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 处理图片上传
|
||||
const beforeUpload: UploadProps['beforeUpload'] = (file) => {
|
||||
const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png'
|
||||
if (!isJpgOrPng) {
|
||||
message.error('只能上传 JPG/PNG 格式的图片!')
|
||||
return false
|
||||
}
|
||||
const isLt2M = file.size / 1024 / 1024 < 2
|
||||
if (!isLt2M) {
|
||||
message.error('图片大小不能超过 2MB!')
|
||||
return false
|
||||
}
|
||||
return false // 返回 false 阻止自动上传
|
||||
}
|
||||
|
||||
const handlePreview: UploadProps['onPreview'] = (file) => {
|
||||
// 处理图片预览
|
||||
console.log('Preview file:', file)
|
||||
}
|
||||
|
||||
const handleRemove: UploadProps['onRemove'] = (file) => {
|
||||
// 处理图片删除
|
||||
console.log('Remove file:', file)
|
||||
}
|
||||
|
||||
// 处理模态框取消
|
||||
const handleCancel = () => {
|
||||
formRef.value?.resetFields()
|
||||
fileList.value = []
|
||||
emit('cancel')
|
||||
}
|
||||
|
||||
// 处理模态框确认
|
||||
const handleOk = async () => {
|
||||
try {
|
||||
await formRef.value?.validate()
|
||||
confirmLoading.value = true
|
||||
|
||||
const formData = { ...formState }
|
||||
if (fileList.value.length > 0) {
|
||||
formData.image = fileList.value[0].thumbUrl || fileList.value[0].url
|
||||
}
|
||||
|
||||
emit('ok', formData)
|
||||
} catch (error) {
|
||||
console.error('表单验证失败:', error)
|
||||
} finally {
|
||||
confirmLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 监听 visible 变化
|
||||
watch(() => props.visible, (visible) => {
|
||||
if (visible) {
|
||||
loadMerchants()
|
||||
if (props.currentRecord) {
|
||||
Object.assign(formState, props.currentRecord)
|
||||
if (props.currentRecord.image) {
|
||||
fileList.value = [{
|
||||
uid: '-1',
|
||||
name: 'image',
|
||||
status: 'done',
|
||||
url: props.currentRecord.image
|
||||
}]
|
||||
}
|
||||
} else {
|
||||
formRef.value?.resetFields()
|
||||
fileList.value = []
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 监听 mode 变化
|
||||
watch(() => props.mode, (mode) => {
|
||||
if (mode === 'view') {
|
||||
// 查看模式下禁用表单
|
||||
Object.keys(rules).forEach(key => {
|
||||
rules[key as keyof typeof rules] = []
|
||||
})
|
||||
} else {
|
||||
// 恢复验证规则
|
||||
Object.assign(rules, {
|
||||
name: [{ required: true, message: '请输入花卉名称', trigger: 'blur' }],
|
||||
type: [{ required: true, message: '请选择花卉类型', trigger: 'change' }],
|
||||
variety: [{ required: true, message: '请输入品种', trigger: 'blur' }],
|
||||
price: [{ required: true, message: '请输入价格', trigger: 'blur' }],
|
||||
stock: [{ required: true, message: '请输入库存', trigger: 'blur' }],
|
||||
merchantId: [{ required: true, message: '请选择商家', trigger: 'change' }],
|
||||
status: [{ required: true, message: '请选择状态', trigger: 'change' }]
|
||||
})
|
||||
}
|
||||
})
|
||||
</script>
|
||||
223
admin-system/src/pages/flower/index.vue
Normal file
223
admin-system/src/pages/flower/index.vue
Normal file
File diff suppressed because one or more lines are too long
@@ -82,7 +82,7 @@
|
||||
:value-style="{ color: '#3f8600' }"
|
||||
>
|
||||
<template #prefix>
|
||||
<TrendingUpOutlined />
|
||||
<RiseOutlined />
|
||||
</template>
|
||||
</a-statistic>
|
||||
</a-card>
|
||||
@@ -300,7 +300,6 @@ import {
|
||||
UserOutlined,
|
||||
CheckCircleOutlined,
|
||||
RiseOutlined,
|
||||
TrendingUpOutlined,
|
||||
SearchOutlined,
|
||||
ReloadOutlined,
|
||||
PlusOutlined,
|
||||
|
||||
@@ -1,172 +0,0 @@
|
||||
<template>
|
||||
<a-modal
|
||||
v-model:open="visible"
|
||||
title="用户详情"
|
||||
width="800px"
|
||||
:footer="null"
|
||||
@cancel="handleClose"
|
||||
>
|
||||
<div v-if="user" class="user-detail">
|
||||
<!-- 基本信息 -->
|
||||
<a-card title="基本信息" class="mb-4">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="8">
|
||||
<div class="detail-item">
|
||||
<label>用户ID:</label>
|
||||
<span>{{ user.id }}</span>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<div class="detail-item">
|
||||
<label>用户名:</label>
|
||||
<span>{{ user.username }}</span>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<div class="detail-item">
|
||||
<label>昵称:</label>
|
||||
<span>{{ user.nickname || '-' }}</span>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<div class="detail-item">
|
||||
<label>邮箱:</label>
|
||||
<span>{{ user.email || '-' }}</span>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<div class="detail-item">
|
||||
<label>手机号:</label>
|
||||
<span>{{ user.phone || '-' }}</span>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<div class="detail-item">
|
||||
<label>状态:</label>
|
||||
<a-tag :color="getStatusColor(user.status)">
|
||||
{{ getStatusText(user.status) }}
|
||||
</a-tag>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
|
||||
<!-- 统计信息 -->
|
||||
<a-card title="统计信息" class="mb-4">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="6">
|
||||
<a-statistic title="积分" :value="user.points" />
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-statistic title="等级" :value="user.level" />
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-statistic title="余额" :value="user.balance" prefix="¥" />
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-statistic title="旅行次数" :value="user.travel_count" />
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
|
||||
<!-- 时间信息 -->
|
||||
<a-card title="时间信息">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="8">
|
||||
<div class="detail-item">
|
||||
<label>注册时间:</label>
|
||||
<span>{{ formatDate(user.created_at) }}</span>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<div class="detail-item">
|
||||
<label>更新时间:</label>
|
||||
<span>{{ formatDate(user.updated_at) }}</span>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<div class="detail-item">
|
||||
<label>最后登录:</label>
|
||||
<span>{{ user.last_login_at ? formatDate(user.last_login_at) : '-' }}</span>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
</div>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import type { User } from '@/api/user'
|
||||
|
||||
interface Props {
|
||||
user: User | null
|
||||
visible: boolean
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:visible', value: boolean): void
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.visible,
|
||||
set: (value) => emit('update:visible', value)
|
||||
})
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateStr: string) => {
|
||||
return new Date(dateStr).toLocaleString('zh-CN')
|
||||
}
|
||||
|
||||
// 获取状态颜色
|
||||
const getStatusColor = (status: string) => {
|
||||
const colorMap: Record<string, string> = {
|
||||
active: 'green',
|
||||
inactive: 'red',
|
||||
pending: 'orange'
|
||||
}
|
||||
return colorMap[status] || 'default'
|
||||
}
|
||||
|
||||
// 获取状态文本
|
||||
const getStatusText = (status: string) => {
|
||||
const textMap: Record<string, string> = {
|
||||
active: '正常',
|
||||
inactive: '禁用',
|
||||
pending: '待审核'
|
||||
}
|
||||
return textMap[status] || status
|
||||
}
|
||||
|
||||
// 关闭弹窗
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.user-detail {
|
||||
.detail-item {
|
||||
margin-bottom: 12px;
|
||||
|
||||
label {
|
||||
display: inline-block;
|
||||
width: 80px;
|
||||
color: #666;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
span {
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.mb-4 {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,276 +0,0 @@
|
||||
<template>
|
||||
<a-modal
|
||||
v-model:open="visible"
|
||||
:title="isEditing ? '编辑用户' : '新增用户'"
|
||||
width="600px"
|
||||
:confirm-loading="loading"
|
||||
@ok="handleSubmit"
|
||||
@cancel="handleClose"
|
||||
>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="rules"
|
||||
layout="vertical"
|
||||
>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="用户名" name="username">
|
||||
<a-input
|
||||
v-model:value="formData.username"
|
||||
placeholder="请输入用户名"
|
||||
:disabled="isEditing"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="昵称" name="nickname">
|
||||
<a-input
|
||||
v-model:value="formData.nickname"
|
||||
placeholder="请输入昵称"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="邮箱" name="email">
|
||||
<a-input
|
||||
v-model:value="formData.email"
|
||||
placeholder="请输入邮箱"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="手机号" name="phone">
|
||||
<a-input
|
||||
v-model:value="formData.phone"
|
||||
placeholder="请输入手机号"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16" v-if="!isEditing">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="密码" name="password">
|
||||
<a-input-password
|
||||
v-model:value="formData.password"
|
||||
placeholder="请输入密码"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="确认密码" name="confirmPassword">
|
||||
<a-input-password
|
||||
v-model:value="formData.confirmPassword"
|
||||
placeholder="请确认密码"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="性别" name="gender">
|
||||
<a-select
|
||||
v-model:value="formData.gender"
|
||||
placeholder="请选择性别"
|
||||
allow-clear
|
||||
>
|
||||
<a-select-option :value="1">男</a-select-option>
|
||||
<a-select-option :value="2">女</a-select-option>
|
||||
<a-select-option :value="0">未知</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="生日" name="birthday">
|
||||
<a-date-picker
|
||||
v-model:value="formData.birthday"
|
||||
placeholder="请选择生日"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="状态" name="status">
|
||||
<a-select
|
||||
v-model:value="formData.status"
|
||||
placeholder="请选择状态"
|
||||
>
|
||||
<a-select-option value="active">正常</a-select-option>
|
||||
<a-select-option value="inactive">禁用</a-select-option>
|
||||
<a-select-option value="pending">待审核</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="备注" name="remark">
|
||||
<a-input
|
||||
v-model:value="formData.remark"
|
||||
placeholder="请输入备注"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, watch } from 'vue'
|
||||
import type { FormInstance, Rule } from 'ant-design-vue/es/form'
|
||||
import type { User } from '@/api/user'
|
||||
import dayjs, { type Dayjs } from 'dayjs'
|
||||
|
||||
interface Props {
|
||||
user: User | null
|
||||
visible: boolean
|
||||
isEditing: boolean
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:visible', value: boolean): void
|
||||
(e: 'submit', data: any): void
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
const loading = ref(false)
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.visible,
|
||||
set: (value) => emit('update:visible', value)
|
||||
})
|
||||
|
||||
// 表单数据
|
||||
const formData = reactive({
|
||||
username: '',
|
||||
nickname: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
gender: undefined as number | undefined,
|
||||
birthday: undefined as Dayjs | undefined,
|
||||
status: 'active',
|
||||
remark: ''
|
||||
})
|
||||
|
||||
// 表单验证规则
|
||||
const rules: Record<string, Rule[]> = {
|
||||
username: [
|
||||
{ required: true, message: '请输入用户名', trigger: 'blur' },
|
||||
{ min: 3, max: 20, message: '用户名长度为3-20个字符', trigger: 'blur' },
|
||||
{ pattern: /^[a-zA-Z0-9_]+$/, message: '用户名只能包含字母、数字和下划线', trigger: 'blur' }
|
||||
],
|
||||
email: [
|
||||
{ type: 'email', message: '请输入正确的邮箱格式', trigger: 'blur' }
|
||||
],
|
||||
phone: [
|
||||
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' }
|
||||
],
|
||||
password: [
|
||||
{ required: !props.isEditing, message: '请输入密码', trigger: 'blur' },
|
||||
{ min: 6, max: 20, message: '密码长度为6-20个字符', trigger: 'blur' }
|
||||
],
|
||||
confirmPassword: [
|
||||
{ required: !props.isEditing, message: '请确认密码', trigger: 'blur' },
|
||||
{
|
||||
validator: (rule: any, value: string) => {
|
||||
if (!props.isEditing && value !== formData.password) {
|
||||
return Promise.reject('两次输入的密码不一致')
|
||||
}
|
||||
return Promise.resolve()
|
||||
},
|
||||
trigger: 'blur'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// 监听用户数据变化,初始化表单
|
||||
watch(() => props.user, (user) => {
|
||||
if (user && props.isEditing) {
|
||||
formData.username = user.username
|
||||
formData.nickname = user.nickname
|
||||
formData.email = user.email
|
||||
formData.phone = user.phone
|
||||
formData.gender = user.gender
|
||||
formData.birthday = user.birthday ? dayjs(user.birthday) : undefined
|
||||
formData.status = user.status
|
||||
formData.remark = user.remark
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
// 监听弹窗显示状态,重置表单
|
||||
watch(() => props.visible, (visible) => {
|
||||
if (visible && !props.isEditing) {
|
||||
resetForm()
|
||||
}
|
||||
})
|
||||
|
||||
// 重置表单
|
||||
const resetForm = () => {
|
||||
Object.assign(formData, {
|
||||
username: '',
|
||||
nickname: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
gender: undefined,
|
||||
birthday: undefined,
|
||||
status: 'active',
|
||||
remark: ''
|
||||
})
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
await formRef.value?.validate()
|
||||
loading.value = true
|
||||
|
||||
const submitData: any = {
|
||||
username: formData.username,
|
||||
nickname: formData.nickname,
|
||||
email: formData.email,
|
||||
phone: formData.phone,
|
||||
gender: formData.gender,
|
||||
birthday: formData.birthday?.format('YYYY-MM-DD'),
|
||||
status: formData.status,
|
||||
remark: formData.remark
|
||||
}
|
||||
|
||||
if (!props.isEditing) {
|
||||
submitData.password = formData.password
|
||||
}
|
||||
|
||||
emit('submit', submitData)
|
||||
} catch (error) {
|
||||
console.error('表单验证失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭弹窗
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
resetForm()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.ant-form-item {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,843 +0,0 @@
|
||||
<template>
|
||||
<div class="users-page">
|
||||
<a-card title="用户管理" size="small">
|
||||
<template #extra>
|
||||
<a-space>
|
||||
<a-button type="primary" @click="handleAdd">
|
||||
<PlusOutlined />
|
||||
新增用户
|
||||
</a-button>
|
||||
<a-button @click="handleRefresh">
|
||||
<ReloadOutlined />
|
||||
刷新
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
|
||||
<!-- 统计卡片 -->
|
||||
<div class="stats-cards">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="6">
|
||||
<a-card size="small">
|
||||
<a-statistic
|
||||
title="总用户数"
|
||||
:value="statistics.totalUsers"
|
||||
:value-style="{ color: '#3f8600' }"
|
||||
>
|
||||
<template #prefix>
|
||||
<UserOutlined />
|
||||
</template>
|
||||
</a-statistic>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card size="small">
|
||||
<a-statistic
|
||||
title="活跃用户"
|
||||
:value="statistics.activeUsers"
|
||||
:value-style="{ color: '#1890ff' }"
|
||||
>
|
||||
<template #prefix>
|
||||
<CheckCircleOutlined />
|
||||
</template>
|
||||
</a-statistic>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card size="small">
|
||||
<a-statistic
|
||||
title="今日新增"
|
||||
:value="statistics.todayNew"
|
||||
:value-style="{ color: '#722ed1' }"
|
||||
>
|
||||
<template #prefix>
|
||||
<PlusCircleOutlined />
|
||||
</template>
|
||||
</a-statistic>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card size="small">
|
||||
<a-statistic
|
||||
title="VIP用户"
|
||||
:value="statistics.vipUsers"
|
||||
:value-style="{ color: '#fa8c16' }"
|
||||
>
|
||||
<template #prefix>
|
||||
<CrownOutlined />
|
||||
</template>
|
||||
</a-statistic>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
|
||||
<!-- 高级搜索 -->
|
||||
<AdvancedSearch
|
||||
search-type="user"
|
||||
:status-options="statusOptions"
|
||||
@search="handleSearch"
|
||||
@reset="handleSearchReset"
|
||||
/>
|
||||
|
||||
<!-- 批量操作 -->
|
||||
<BatchOperations
|
||||
:data-source="users"
|
||||
:selected-items="selectedUsers"
|
||||
operation-type="user"
|
||||
:status-options="statusOptions"
|
||||
:export-fields="exportFields"
|
||||
@selection-change="(items: any[]) => handleSelectionChange(items as User[])"
|
||||
@batch-action="(action: string, items: any[], params?: any) => handleBatchAction(action, items as User[], params)"
|
||||
/>
|
||||
|
||||
<!-- 用户列表 -->
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="users"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
:row-selection="rowSelection"
|
||||
:scroll="{ x: 1200 }"
|
||||
@change="handleTableChange"
|
||||
row-key="id"
|
||||
>
|
||||
<!-- 头像列 -->
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'avatar'">
|
||||
<a-avatar :src="record.avatar" :size="40">
|
||||
<template #icon>
|
||||
<UserOutlined />
|
||||
</template>
|
||||
</a-avatar>
|
||||
</template>
|
||||
|
||||
<!-- 用户信息列 -->
|
||||
<template v-else-if="column.key === 'userInfo'">
|
||||
<div class="user-info">
|
||||
<div class="user-name">
|
||||
{{ record.nickname || record.username }}
|
||||
<a-tag v-if="record.user_type === 'vip'" color="gold" size="small">
|
||||
<CrownOutlined />
|
||||
VIP
|
||||
</a-tag>
|
||||
</div>
|
||||
<div class="user-meta">
|
||||
<a-typography-text type="secondary" :style="{ fontSize: '12px' }">
|
||||
ID: {{ record.id }} | {{ record.phone || record.email }}
|
||||
</a-typography-text>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 状态列 -->
|
||||
<template v-else-if="column.key === 'status'">
|
||||
<a-tag :color="getStatusColor(record.status)">
|
||||
{{ getStatusText(record.status) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<!-- 注册信息列 -->
|
||||
<template v-else-if="column.key === 'registerInfo'">
|
||||
<div class="register-info">
|
||||
<div>
|
||||
<a-tag :color="getSourceColor(record.register_source)" size="small">
|
||||
{{ getSourceText(record.register_source) }}
|
||||
</a-tag>
|
||||
</div>
|
||||
<div class="register-time">
|
||||
<a-typography-text type="secondary" :style="{ fontSize: '12px' }">
|
||||
{{ formatDate(record.created_at) }}
|
||||
</a-typography-text>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 最后登录列 -->
|
||||
<template v-else-if="column.key === 'lastLogin'">
|
||||
<div v-if="record.last_login_at">
|
||||
<div>{{ formatDate(record.last_login_at) }}</div>
|
||||
<a-typography-text type="secondary" :style="{ fontSize: '12px' }">
|
||||
{{ record.last_login_ip }}
|
||||
</a-typography-text>
|
||||
</div>
|
||||
<a-typography-text v-else type="secondary">
|
||||
从未登录
|
||||
</a-typography-text>
|
||||
</template>
|
||||
|
||||
<!-- 操作列 -->
|
||||
<template v-else-if="column.key === 'actions'">
|
||||
<a-space>
|
||||
<a-button type="link" size="small" @click="handleView(record)">
|
||||
查看
|
||||
</a-button>
|
||||
<a-button type="link" size="small" @click="handleEdit(record)">
|
||||
编辑
|
||||
</a-button>
|
||||
<a-dropdown>
|
||||
<template #overlay>
|
||||
<a-menu @click="({ key }) => handleMenuAction(key, record)">
|
||||
<a-menu-item key="resetPassword">
|
||||
<KeyOutlined />
|
||||
重置密码
|
||||
</a-menu-item>
|
||||
<a-menu-item key="sendMessage">
|
||||
<MessageOutlined />
|
||||
发送消息
|
||||
</a-menu-item>
|
||||
<a-menu-item
|
||||
:key="record.status === 'active' ? 'disable' : 'enable'"
|
||||
>
|
||||
<component
|
||||
:is="record.status === 'active' ? LockOutlined : UnlockOutlined"
|
||||
/>
|
||||
{{ record.status === 'active' ? '禁用' : '启用' }}
|
||||
</a-menu-item>
|
||||
<a-menu-divider />
|
||||
<a-menu-item key="delete" class="danger-item">
|
||||
<DeleteOutlined />
|
||||
删除
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
<a-button type="link" size="small">
|
||||
更多
|
||||
<DownOutlined />
|
||||
</a-button>
|
||||
</a-dropdown>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
|
||||
<!-- 用户详情模态框 -->
|
||||
<a-modal
|
||||
v-model:open="detailModalVisible"
|
||||
title="用户详情"
|
||||
:footer="null"
|
||||
width="800px"
|
||||
>
|
||||
<UserDetail
|
||||
v-if="currentUser"
|
||||
:user="currentUser"
|
||||
@refresh="handleRefresh"
|
||||
/>
|
||||
</a-modal>
|
||||
|
||||
<!-- 用户编辑模态框 -->
|
||||
<a-modal
|
||||
v-model:open="editModalVisible"
|
||||
title="编辑用户"
|
||||
@ok="handleEditSubmit"
|
||||
:confirm-loading="editLoading"
|
||||
>
|
||||
<UserForm
|
||||
v-if="currentUser"
|
||||
ref="userFormRef"
|
||||
:user="currentUser"
|
||||
mode="edit"
|
||||
/>
|
||||
</a-modal>
|
||||
|
||||
<!-- 新增用户模态框 -->
|
||||
<a-modal
|
||||
v-model:open="addModalVisible"
|
||||
title="新增用户"
|
||||
@ok="handleAddSubmit"
|
||||
:confirm-loading="addLoading"
|
||||
>
|
||||
<UserForm
|
||||
ref="addUserFormRef"
|
||||
mode="add"
|
||||
/>
|
||||
</a-modal>
|
||||
|
||||
<!-- 发送消息模态框 -->
|
||||
<a-modal
|
||||
v-model:open="messageModalVisible"
|
||||
title="发送消息"
|
||||
@ok="handleSendMessage"
|
||||
:confirm-loading="messageLoading"
|
||||
>
|
||||
<a-form :model="messageForm" layout="vertical">
|
||||
<a-form-item label="消息标题" required>
|
||||
<a-input
|
||||
v-model:value="messageForm.title"
|
||||
placeholder="请输入消息标题"
|
||||
:maxlength="100"
|
||||
show-count
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="消息内容" required>
|
||||
<a-textarea
|
||||
v-model:value="messageForm.content"
|
||||
placeholder="请输入消息内容"
|
||||
:rows="4"
|
||||
:maxlength="500"
|
||||
show-count
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="消息类型">
|
||||
<a-radio-group v-model:value="messageForm.type">
|
||||
<a-radio value="info">通知</a-radio>
|
||||
<a-radio value="warning">警告</a-radio>
|
||||
<a-radio value="promotion">推广</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import {
|
||||
UserOutlined,
|
||||
PlusOutlined,
|
||||
ReloadOutlined,
|
||||
CheckCircleOutlined,
|
||||
PlusCircleOutlined,
|
||||
CrownOutlined,
|
||||
KeyOutlined,
|
||||
MessageOutlined,
|
||||
LockOutlined,
|
||||
UnlockOutlined,
|
||||
DeleteOutlined,
|
||||
DownOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import { message, Modal } from 'ant-design-vue'
|
||||
import type { TableColumnsType, TableProps } from 'ant-design-vue'
|
||||
import AdvancedSearch from '@/components/AdvancedSearch.vue'
|
||||
import BatchOperations from '@/components/BatchOperations.vue'
|
||||
import UserDetail from './components/UserDetail.vue'
|
||||
import UserForm from './components/UserForm.vue'
|
||||
import userAPI, { type User } from '@/api/user'
|
||||
// import { formatDate } from '@/utils/date'
|
||||
|
||||
interface Statistics {
|
||||
totalUsers: number
|
||||
activeUsers: number
|
||||
newUsersToday: number
|
||||
totalRevenue: number
|
||||
}
|
||||
|
||||
// 临时格式化函数,直到utils/date模块可用
|
||||
const formatDate = (date: string | null | undefined): string => {
|
||||
if (!date) return ''
|
||||
return new Date(date).toLocaleString('zh-CN')
|
||||
}
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(false)
|
||||
const users = ref<User[]>([])
|
||||
const selectedUsers = ref<User[]>([])
|
||||
const currentUser = ref<User | null>(null)
|
||||
|
||||
// 统计数据
|
||||
const statistics = ref<Statistics>({
|
||||
totalUsers: 0,
|
||||
activeUsers: 0,
|
||||
todayNew: 0,
|
||||
vipUsers: 0
|
||||
})
|
||||
|
||||
// 模态框状态
|
||||
const detailModalVisible = ref(false)
|
||||
const editModalVisible = ref(false)
|
||||
const addModalVisible = ref(false)
|
||||
const messageModalVisible = ref(false)
|
||||
|
||||
// 加载状态
|
||||
const editLoading = ref(false)
|
||||
const addLoading = ref(false)
|
||||
const messageLoading = ref(false)
|
||||
|
||||
// 表单引用
|
||||
const userFormRef = ref()
|
||||
const addUserFormRef = ref()
|
||||
|
||||
// 消息表单
|
||||
const messageForm = reactive({
|
||||
title: '',
|
||||
content: '',
|
||||
type: 'info'
|
||||
})
|
||||
|
||||
// 分页配置
|
||||
const pagination = ref({
|
||||
current: 1,
|
||||
pageSize: 20,
|
||||
total: 0,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total: number) => `共 ${total} 条记录`
|
||||
})
|
||||
|
||||
// 搜索参数
|
||||
const searchParams = ref({})
|
||||
|
||||
// 状态选项
|
||||
const statusOptions = [
|
||||
{ value: 'active', label: '激活', color: 'green' },
|
||||
{ value: 'inactive', label: '禁用', color: 'red' },
|
||||
{ value: 'pending', label: '待审核', color: 'orange' }
|
||||
]
|
||||
|
||||
// 导出字段
|
||||
const exportFields = [
|
||||
{ key: 'id', label: 'ID' },
|
||||
{ key: 'username', label: '用户名' },
|
||||
{ key: 'nickname', label: '昵称' },
|
||||
{ key: 'email', label: '邮箱' },
|
||||
{ key: 'phone', label: '手机号' },
|
||||
{ key: 'status', label: '状态' },
|
||||
{ key: 'user_type', label: '用户类型' },
|
||||
{ key: 'register_source', label: '注册来源' },
|
||||
{ key: 'created_at', label: '注册时间' },
|
||||
{ key: 'last_login_at', label: '最后登录' }
|
||||
]
|
||||
|
||||
// 表格列配置
|
||||
const columns: TableColumnsType = [
|
||||
{
|
||||
title: '头像',
|
||||
key: 'avatar',
|
||||
width: 80,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '用户信息',
|
||||
key: 'userInfo',
|
||||
width: 200
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
key: 'status',
|
||||
width: 100,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '注册信息',
|
||||
key: 'registerInfo',
|
||||
width: 150
|
||||
},
|
||||
{
|
||||
title: '最后登录',
|
||||
key: 'lastLogin',
|
||||
width: 180
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 200,
|
||||
fixed: 'right'
|
||||
}
|
||||
]
|
||||
|
||||
// 行选择配置
|
||||
const rowSelection: TableProps['rowSelection'] = {
|
||||
selectedRowKeys: computed(() => selectedUsers.value.map(user => user.id)),
|
||||
onChange: (selectedRowKeys: (string | number)[], selectedRows: User[]) => {
|
||||
selectedUsers.value = selectedRows
|
||||
},
|
||||
onSelectAll: (selected: boolean, selectedRows: User[], changeRows: User[]) => {
|
||||
if (selected) {
|
||||
selectedUsers.value = [...selectedUsers.value, ...changeRows]
|
||||
} else {
|
||||
const changeIds = changeRows.map(row => row.id)
|
||||
selectedUsers.value = selectedUsers.value.filter(user => !changeIds.includes(user.id))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取状态颜色
|
||||
*/
|
||||
const getStatusColor = (status: string) => {
|
||||
const statusMap: Record<string, string> = {
|
||||
active: 'green',
|
||||
inactive: 'red',
|
||||
pending: 'orange'
|
||||
}
|
||||
return statusMap[status] || 'default'
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取状态文本
|
||||
*/
|
||||
const getStatusText = (status: string) => {
|
||||
const statusMap: Record<string, string> = {
|
||||
active: '激活',
|
||||
inactive: '禁用',
|
||||
pending: '待审核'
|
||||
}
|
||||
return statusMap[status] || status
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取来源颜色
|
||||
*/
|
||||
const getSourceColor = (source: string) => {
|
||||
const sourceMap: Record<string, string> = {
|
||||
web: 'blue',
|
||||
wechat: 'green',
|
||||
app: 'purple'
|
||||
}
|
||||
return sourceMap[source] || 'default'
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取来源文本
|
||||
*/
|
||||
const getSourceText = (source: string) => {
|
||||
const sourceMap: Record<string, string> = {
|
||||
web: '网页端',
|
||||
wechat: '微信小程序',
|
||||
app: '移动应用'
|
||||
}
|
||||
return sourceMap[source] || source
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载用户列表
|
||||
*/
|
||||
const loadUsers = async () => {
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
const params = {
|
||||
page: pagination.value.current,
|
||||
pageSize: pagination.value.pageSize,
|
||||
...searchParams.value
|
||||
}
|
||||
|
||||
const response = await userAPI.getUsers(params)
|
||||
|
||||
users.value = response.data.list
|
||||
pagination.value.total = response.data.total
|
||||
|
||||
// 更新统计数据
|
||||
statistics.value = response.data.statistics || {
|
||||
totalUsers: response.data.total,
|
||||
activeUsers: response.data.list.filter((u: User) => u.status === 'active').length,
|
||||
todayNew: 0,
|
||||
vipUsers: response.data.list.filter((u: User) => u.user_type === 'vip').length
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('加载用户列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理表格变化
|
||||
*/
|
||||
const handleTableChange: TableProps['onChange'] = (pag) => {
|
||||
if (pag) {
|
||||
pagination.value.current = pag.current || 1
|
||||
pagination.value.pageSize = pag.pageSize || 20
|
||||
}
|
||||
loadUsers()
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理搜索
|
||||
*/
|
||||
const handleSearch = (params: any) => {
|
||||
searchParams.value = params
|
||||
pagination.value.current = 1
|
||||
loadUsers()
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理搜索重置
|
||||
*/
|
||||
const handleSearchReset = () => {
|
||||
searchParams.value = {}
|
||||
pagination.value.current = 1
|
||||
loadUsers()
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理选择变化
|
||||
*/
|
||||
const handleSelectionChange = (items: User[]) => {
|
||||
selectedUsers.value = items
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理批量操作
|
||||
*/
|
||||
const handleBatchAction = async (action: string, items: User[], params?: any) => {
|
||||
try {
|
||||
switch (action) {
|
||||
case 'update-status':
|
||||
await userAPI.batchUpdateStatus(
|
||||
items.map(item => item.id),
|
||||
params.status,
|
||||
params.reason
|
||||
)
|
||||
message.success('批量状态更新成功')
|
||||
break
|
||||
|
||||
case 'delete':
|
||||
await userAPI.batchDelete(items.map(item => item.id))
|
||||
message.success('批量删除成功')
|
||||
break
|
||||
|
||||
case 'export':
|
||||
await userAPI.exportUsers(items.map(item => item.id), params)
|
||||
message.success('导出任务已开始')
|
||||
break
|
||||
|
||||
case 'send-message':
|
||||
// 打开批量发送消息界面
|
||||
messageModalVisible.value = true
|
||||
break
|
||||
|
||||
case 'lock':
|
||||
await userAPI.batchUpdateStatus(items.map(item => item.id), 'inactive', '批量锁定')
|
||||
message.success('批量锁定成功')
|
||||
break
|
||||
|
||||
case 'unlock':
|
||||
await userAPI.batchUpdateStatus(items.map(item => item.id), 'active', '批量解锁')
|
||||
message.success('批量解锁成功')
|
||||
break
|
||||
}
|
||||
|
||||
// 刷新列表
|
||||
loadUsers()
|
||||
// 清空选择
|
||||
selectedUsers.value = []
|
||||
} catch (error) {
|
||||
message.error('批量操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理查看
|
||||
*/
|
||||
const handleView = (user: User) => {
|
||||
currentUser.value = user
|
||||
detailModalVisible.value = true
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理编辑
|
||||
*/
|
||||
const handleEdit = (user: User) => {
|
||||
currentUser.value = user
|
||||
editModalVisible.value = true
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理新增
|
||||
*/
|
||||
const handleAdd = () => {
|
||||
addModalVisible.value = true
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理刷新
|
||||
*/
|
||||
const handleRefresh = () => {
|
||||
loadUsers()
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理菜单操作
|
||||
*/
|
||||
const handleMenuAction = async (key: string, user: User) => {
|
||||
switch (key) {
|
||||
case 'resetPassword':
|
||||
Modal.confirm({
|
||||
title: '确认重置密码',
|
||||
content: `确定要重置用户 ${user.nickname || user.username} 的密码吗?`,
|
||||
onOk: async () => {
|
||||
try {
|
||||
await userAPI.resetPassword(user.id)
|
||||
message.success('密码重置成功')
|
||||
} catch (error) {
|
||||
message.error('密码重置失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
break
|
||||
|
||||
case 'sendMessage':
|
||||
currentUser.value = user
|
||||
messageModalVisible.value = true
|
||||
break
|
||||
|
||||
case 'enable':
|
||||
case 'disable':
|
||||
const newStatus = key === 'enable' ? 'active' : 'inactive'
|
||||
const action = key === 'enable' ? '启用' : '禁用'
|
||||
|
||||
Modal.confirm({
|
||||
title: `确认${action}用户`,
|
||||
content: `确定要${action}用户 ${user.nickname || user.username} 吗?`,
|
||||
onOk: async () => {
|
||||
try {
|
||||
await userAPI.updateStatus(user.id, newStatus)
|
||||
message.success(`${action}成功`)
|
||||
loadUsers()
|
||||
} catch (error) {
|
||||
message.error(`${action}失败`)
|
||||
}
|
||||
}
|
||||
})
|
||||
break
|
||||
|
||||
case 'delete':
|
||||
Modal.confirm({
|
||||
title: '确认删除用户',
|
||||
content: `确定要删除用户 ${user.nickname || user.username} 吗?此操作不可撤销!`,
|
||||
okType: 'danger',
|
||||
onOk: async () => {
|
||||
try {
|
||||
await userAPI.deleteUser(user.id)
|
||||
message.success('删除成功')
|
||||
loadUsers()
|
||||
} catch (error) {
|
||||
message.error('删除失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理编辑提交
|
||||
*/
|
||||
const handleEditSubmit = async () => {
|
||||
if (!userFormRef.value) return
|
||||
|
||||
editLoading.value = true
|
||||
|
||||
try {
|
||||
const formData = await userFormRef.value.validate()
|
||||
await userAPI.updateUser(currentUser.value!.id, formData)
|
||||
|
||||
message.success('更新成功')
|
||||
editModalVisible.value = false
|
||||
loadUsers()
|
||||
} catch (error) {
|
||||
message.error('更新失败')
|
||||
} finally {
|
||||
editLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理新增提交
|
||||
*/
|
||||
const handleAddSubmit = async () => {
|
||||
if (!addUserFormRef.value) return
|
||||
|
||||
addLoading.value = true
|
||||
|
||||
try {
|
||||
const formData = await addUserFormRef.value.validate()
|
||||
await userAPI.createUser(formData)
|
||||
|
||||
message.success('创建成功')
|
||||
addModalVisible.value = false
|
||||
loadUsers()
|
||||
} catch (error) {
|
||||
message.error('创建失败')
|
||||
} finally {
|
||||
addLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理发送消息
|
||||
*/
|
||||
const handleSendMessage = async () => {
|
||||
if (!messageForm.title || !messageForm.content) {
|
||||
message.error('请填写完整的消息信息')
|
||||
return
|
||||
}
|
||||
|
||||
messageLoading.value = true
|
||||
|
||||
try {
|
||||
const userIds = currentUser.value
|
||||
? [currentUser.value.id]
|
||||
: selectedUsers.value.map(user => user.id)
|
||||
|
||||
await userAPI.sendMessage(userIds, messageForm)
|
||||
|
||||
message.success('消息发送成功')
|
||||
messageModalVisible.value = false
|
||||
|
||||
// 重置表单
|
||||
messageForm.title = ''
|
||||
messageForm.content = ''
|
||||
messageForm.type = 'info'
|
||||
} catch (error) {
|
||||
message.error('消息发送失败')
|
||||
} finally {
|
||||
messageLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
loadUsers()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.users-page {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.stats-cards {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
.user-name {
|
||||
font-weight: 500;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.user-meta {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
|
||||
.register-info {
|
||||
.register-time {
|
||||
margin-top: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.danger-item) {
|
||||
color: #ff4d4f !important;
|
||||
}
|
||||
|
||||
:deep(.ant-table-tbody > tr > td) {
|
||||
padding: 12px 8px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.users-page {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.stats-cards :deep(.ant-col) {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -74,6 +74,18 @@ const routes: RouteRecordRaw[] = [
|
||||
layout: 'main' // 添加布局信息
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/flowers',
|
||||
name: 'Flowers',
|
||||
component: () => import('@/pages/flower/index.vue'),
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
title: '花卉管理',
|
||||
icon: 'EnvironmentOutlined',
|
||||
permissions: ['flower:read'],
|
||||
layout: 'main'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/orders',
|
||||
name: 'Orders',
|
||||
|
||||
@@ -69,8 +69,10 @@ export const useAppStore = defineStore('app', () => {
|
||||
throw new Error('获取用户信息失败:接口返回格式异常')
|
||||
}
|
||||
|
||||
// 确保响应数据格式为 { data: { admin: object } }
|
||||
if (response.data && typeof response.data === 'object' && response.data.admin) {
|
||||
// 确保响应数据格式正确 - 支持两种格式:
|
||||
// 1. 直接返回 { success: true, data: { admin: object } }
|
||||
// 2. mock数据格式 { success: true, data: { admin: object } }
|
||||
if (response.success && response.data && typeof response.data === 'object' && response.data.admin) {
|
||||
// 模拟权限数据 - 实际项目中应该从后端获取
|
||||
const mockPermissions = [
|
||||
'user:read', 'user:write',
|
||||
@@ -79,10 +81,27 @@ export const useAppStore = defineStore('app', () => {
|
||||
'animal:read', 'animal:write',
|
||||
'order:read', 'order:write',
|
||||
'promotion:read', 'promotion:write',
|
||||
'system:read', 'system:write'
|
||||
'system:read', 'system:write',
|
||||
'flower:read', 'flower:write'
|
||||
]
|
||||
state.user = response.data.admin
|
||||
state.permissions = mockPermissions
|
||||
}
|
||||
// 处理直接返回数据的情况(mock数据可能直接返回这种格式)
|
||||
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',
|
||||
'flower:read', 'flower:write'
|
||||
]
|
||||
state.user = response.admin
|
||||
state.permissions = mockPermissions
|
||||
} else {
|
||||
throw new Error('获取用户信息失败:响应数据格式不符合预期')
|
||||
}
|
||||
@@ -138,7 +157,8 @@ export const useAppStore = defineStore('app', () => {
|
||||
'animal:read', 'animal:write',
|
||||
'order:read', 'order:write',
|
||||
'promotion:read', 'promotion:write',
|
||||
'system:read', 'system:write'
|
||||
'system:read', 'system:write',
|
||||
'flower:read', 'flower:write'
|
||||
]
|
||||
state.user = response.data.admin
|
||||
state.permissions = mockPermissions
|
||||
@@ -151,7 +171,8 @@ export const useAppStore = defineStore('app', () => {
|
||||
'animal:read', 'animal:write',
|
||||
'order:read', 'order:write',
|
||||
'promotion:read', 'promotion:write',
|
||||
'system:read', 'system:write'
|
||||
'system:read', 'system:write',
|
||||
'flower:read', 'flower:write'
|
||||
]
|
||||
state.user = response.admin
|
||||
state.permissions = mockPermissions
|
||||
|
||||
163
admin-system/src/stores/modules/flower.ts
Normal file
163
admin-system/src/stores/modules/flower.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import type { Flower, FlowerQueryParams, FlowerSale } from '@/api/flower'
|
||||
|
||||
interface FlowerState {
|
||||
flowers: Flower[]
|
||||
currentFlower: Flower | null
|
||||
sales: FlowerSale[]
|
||||
loading: boolean
|
||||
salesLoading: boolean
|
||||
totalCount: number
|
||||
salesTotalCount: number
|
||||
queryParams: FlowerQueryParams
|
||||
}
|
||||
|
||||
export const useFlowerStore = defineStore('flower', () => {
|
||||
// 状态
|
||||
const flowers = ref<Flower[]>([])
|
||||
const currentFlower = ref<Flower | null>(null)
|
||||
const sales = ref<FlowerSale[]>([])
|
||||
const loading = ref(false)
|
||||
const salesLoading = ref(false)
|
||||
const totalCount = ref(0)
|
||||
const salesTotalCount = ref(0)
|
||||
|
||||
const queryParams = ref<FlowerQueryParams>({
|
||||
page: 1,
|
||||
limit: 20,
|
||||
keyword: '',
|
||||
type: undefined,
|
||||
status: undefined,
|
||||
merchantId: undefined
|
||||
})
|
||||
|
||||
// 获取花卉列表
|
||||
const fetchFlowers = async (params?: FlowerQueryParams) => {
|
||||
loading.value = true
|
||||
try {
|
||||
if (params) {
|
||||
Object.assign(queryParams.value, params)
|
||||
}
|
||||
|
||||
const response = await import('@/api/flower').then(m => m.getFlowers(queryParams.value))
|
||||
if (response.data.success) {
|
||||
flowers.value = response.data.data.flowers
|
||||
totalCount.value = response.data.data.pagination.total
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取花卉列表失败:', error)
|
||||
throw error
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取花卉详情
|
||||
const fetchFlower = async (id: number) => {
|
||||
try {
|
||||
const response = await import('@/api/flower').then(m => m.getFlower(id))
|
||||
if (response.data.success) {
|
||||
currentFlower.value = response.data.data.flower
|
||||
return response.data.data.flower
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取花卉详情失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 创建花卉
|
||||
const createFlower = async (data: any) => {
|
||||
try {
|
||||
const response = await import('@/api/flower').then(m => m.createFlower(data))
|
||||
if (response.data.success) {
|
||||
return response.data.data.flower
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('创建花卉失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 更新花卉
|
||||
const updateFlower = async (id: number, data: any) => {
|
||||
try {
|
||||
const response = await import('@/api/flower').then(m => m.updateFlower(id, data))
|
||||
if (response.data.success) {
|
||||
return response.data.data.flower
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('更新花卉失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 删除花卉
|
||||
const deleteFlower = async (id: number) => {
|
||||
try {
|
||||
const response = await import('@/api/flower').then(m => m.deleteFlower(id))
|
||||
if (response.data.success) {
|
||||
return true
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除花卉失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 获取销售记录
|
||||
const fetchSales = async (params?: any) => {
|
||||
salesLoading.value = true
|
||||
try {
|
||||
const response = await import('@/api/flower').then(m => m.getFlowerSales(params))
|
||||
if (response.data.success) {
|
||||
sales.value = response.data.data.sales
|
||||
salesTotalCount.value = response.data.data.pagination.total
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取销售记录失败:', error)
|
||||
throw error
|
||||
} finally {
|
||||
salesLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 重置状态
|
||||
const reset = () => {
|
||||
flowers.value = []
|
||||
currentFlower.value = null
|
||||
sales.value = []
|
||||
loading.value = false
|
||||
salesLoading.value = false
|
||||
totalCount.value = 0
|
||||
salesTotalCount.value = 0
|
||||
Object.assign(queryParams.value, {
|
||||
page: 1,
|
||||
limit: 20,
|
||||
keyword: '',
|
||||
type: undefined,
|
||||
status: undefined,
|
||||
merchantId: undefined
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
flowers,
|
||||
currentFlower,
|
||||
sales,
|
||||
loading,
|
||||
salesLoading,
|
||||
totalCount,
|
||||
salesTotalCount,
|
||||
queryParams,
|
||||
|
||||
fetchFlowers,
|
||||
fetchFlower,
|
||||
createFlower,
|
||||
updateFlower,
|
||||
deleteFlower,
|
||||
fetchSales,
|
||||
reset
|
||||
}
|
||||
})
|
||||
@@ -1,44 +0,0 @@
|
||||
// 测试模拟数据功能
|
||||
const mockAPI = require('./src/api/mockData.ts')
|
||||
|
||||
console.log('🧪 测试模拟数据API...')
|
||||
|
||||
// 测试登录功能
|
||||
console.log('\n1. 测试登录功能')
|
||||
mockAPI.mockAuthAPI.login({ username: 'admin', password: 'admin123' })
|
||||
.then(response => {
|
||||
console.log('✅ 登录成功:', response.data.admin.username)
|
||||
return mockAPI.mockAuthAPI.getCurrentUser()
|
||||
})
|
||||
.then(response => {
|
||||
console.log('✅ 获取当前用户成功:', response.data.admin.nickname)
|
||||
})
|
||||
.catch(error => {
|
||||
console.log('❌ 登录测试失败:', error.message)
|
||||
})
|
||||
|
||||
// 测试用户列表
|
||||
console.log('\n2. 测试用户列表')
|
||||
mockAPI.mockUserAPI.getUsers({ page: 1, pageSize: 5 })
|
||||
.then(response => {
|
||||
console.log('✅ 获取用户列表成功:', response.data.list.length + '个用户')
|
||||
})
|
||||
.catch(error => {
|
||||
console.log('❌ 用户列表测试失败:', error.message)
|
||||
})
|
||||
|
||||
// 测试系统统计
|
||||
console.log('\n3. 测试系统统计')
|
||||
mockAPI.mockSystemAPI.getSystemStats()
|
||||
.then(response => {
|
||||
console.log('✅ 获取系统统计成功:')
|
||||
console.log(' - 用户数:', response.data.userCount)
|
||||
console.log(' - 商家数:', response.data.merchantCount)
|
||||
console.log(' - 旅行数:', response.data.travelCount)
|
||||
console.log(' - 动物数:', response.data.animalCount)
|
||||
})
|
||||
.catch(error => {
|
||||
console.log('❌ 系统统计测试失败:', error.message)
|
||||
})
|
||||
|
||||
console.log('\n🎉 模拟数据测试完成!')
|
||||
Reference in New Issue
Block a user