重构动物模型和路由系统,优化查询逻辑并新增商户和促销活动功能

This commit is contained in:
ylweng
2025-09-22 02:04:07 +08:00
parent 5fc1a4fcb9
commit 47c816270d
54 changed files with 5384 additions and 4639 deletions

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

View File

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

View File

@@ -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('登录成功!')

View File

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

View File

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

View File

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

View File

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

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

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

@@ -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🎉 模拟数据测试完成!')