refactor(backend): 重构动物相关 API 接口

- 更新了动物数据结构和相关类型定义
- 优化了动物列表、详情、创建、更新和删除接口
- 新增了更新动物状态接口
- 移除了与认领记录相关的接口
-调整了 API 响应结构
This commit is contained in:
ylweng
2025-08-31 00:45:46 +08:00
parent 0cad74b06f
commit 8e5295b572
111 changed files with 15290 additions and 1972 deletions

View File

@@ -69,6 +69,7 @@ import { useRouter } from 'vue-router'
import { UserOutlined, LockOutlined } from '@ant-design/icons-vue'
import { message } from 'ant-design-vue'
import { useAppStore } from '@/stores/app'
import { authAPI } from '@/api'
interface FormState {
username: string
@@ -91,25 +92,22 @@ const onFinish = async (values: FormState) => {
loading.value = true
try {
// 模拟登录过程
console.log('登录信息:', values)
// TODO: 调用真实登录接口
// const response = await authAPI.login(values)
// 模拟登录成功
await new Promise(resolve => setTimeout(resolve, 1000))
// 调用真实登录接口
const response = await authAPI.login(values)
// 保存token
localStorage.setItem('admin_token', 'mock_token_123456')
if (response?.data?.token) {
localStorage.setItem('admin_token', response.data.token)
} else {
throw new Error('登录响应中缺少token')
}
// 更新用户状态
appStore.setUser({
id: 1,
username: values.username,
nickname: '管理员',
role: 'admin'
})
if (response?.data?.admin) {
appStore.setUser(response.data.admin)
} else {
throw new Error('登录响应中缺少用户信息')
}
message.success('登录成功!')
@@ -117,9 +115,10 @@ const onFinish = async (values: FormState) => {
const redirect = router.currentRoute.value.query.redirect as string
router.push(redirect || '/dashboard')
} catch (error) {
} catch (error: any) {
console.error('登录失败:', error)
message.error('登录失败,请检查用户名和密码')
const errorMessage = error.message || '登录失败,请检查用户名和密码'
message.error(errorMessage)
} finally {
loading.value = false
}
@@ -174,10 +173,11 @@ const onFinishFailed = (errorInfo: any) => {
border-top: 1px solid #f0f0f0;
}
.login-footer p {
.login-footer {
text-align: center;
margin-top: 30px;
color: #999;
font-size: 12px;
margin: 0;
}
:deep(.ant-input-affix-wrapper) {

View File

@@ -137,7 +137,7 @@
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { ref, reactive, onMounted, h } from 'vue'
import { message, Modal } from 'ant-design-vue'
import type { TableProps } from 'ant-design-vue'
import {
@@ -150,7 +150,7 @@ import {
StopOutlined,
PlayCircleOutlined
} from '@ant-design/icons-vue'
import { getMerchants, approveMerchant, rejectMerchant, disableMerchant, enableMerchant } from '@/api/merchant'
import { getMerchants, getMerchant, approveMerchant, rejectMerchant, disableMerchant, enableMerchant } from '@/api/merchant'
import type { Merchant } from '@/api/merchant'
interface SearchForm {
@@ -314,8 +314,33 @@ const handleTableChange: TableProps['onChange'] = (pag) => {
loadMerchants()
}
const handleView = (record: Merchant) => {
message.info(`查看商家: ${record.business_name}`)
const handleView = async (record: Merchant) => {
try {
const response = await getMerchant(record.id)
Modal.info({
title: '商家详情',
width: 600,
content: h('div', { class: 'merchant-detail-modal' }, [
h('a-descriptions', {
column: 1,
bordered: true
}, [
h('a-descriptions-item', { label: '商家ID' }, response.data.id),
h('a-descriptions-item', { label: '商家名称' }, response.data.business_name),
h('a-descriptions-item', { label: '商家类型' }, getTypeText(response.data.merchant_type)),
h('a-descriptions-item', { label: '联系人' }, response.data.contact_person),
h('a-descriptions-item', { label: '联系电话' }, response.data.contact_phone),
h('a-descriptions-item', { label: '状态' }, [
h('a-tag', { color: getStatusColor(response.data.status) }, getStatusText(response.data.status))
]),
h('a-descriptions-item', { label: '入驻时间' }, response.data.created_at),
h('a-descriptions-item', { label: '更新时间' }, response.data.updated_at)
])
])
})
} catch (error) {
message.error('获取商家详情失败')
}
}
const handleApprove = async (record: Merchant) => {

View File

@@ -1,162 +1,241 @@
<template>
<div class="order-management">
<a-page-header title="订单管理" sub-title="管理花束订单和交易记录">
<a-page-header
title="订单管理"
sub-title="管理系统订单信息"
>
<template #extra>
<a-space>
<a-button @click="handleRefresh">
<template #icon><ReloadOutlined /></template>
<template #icon>
<ReloadOutlined />
</template>
刷新
</a-button>
<a-button type="primary" @click="showStats">
<template #icon><BarChartOutlined /></template>
销售统计
</a-button>
</a-space>
</template>
</a-page-header>
<a-tabs v-model:activeKey="activeTab" @change="handleTabChange">
<a-tab-pane key="orders" tab="订单列表">
<!-- 统计卡片 -->
<a-row :gutter="16" class="stats-row">
<a-col :span="6">
<a-card>
<div class="search-container">
<a-form layout="inline" :model="searchForm">
<a-form-item label="订单号">
<a-input v-model:value="searchForm.order_no" placeholder="输入订单号" allow-clear />
</a-form-item>
<a-form-item label="状态">
<a-select v-model:value="searchForm.status" placeholder="全部状态" style="width: 120px" allow-clear>
<a-select-option value="pending">待支付</a-select-option>
<a-select-option value="paid">已支付</a-select-option>
<a-select-option value="shipped">已发货</a-select-option>
<a-select-option value="completed">已完成</a-select-option>
<a-select-option value="cancelled">已取消</a-select-option>
<a-select-option value="refunded">已退款</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="下单时间">
<a-range-picker v-model:value="searchForm.orderTime" :placeholder="['开始时间', '结束时间']" />
</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="orderColumns"
:data-source="orderList"
:loading="loading"
:pagination="pagination"
:row-key="record => record.id"
@change="handleTableChange"
<a-statistic
title="今日订单"
:value="statistics.today_orders"
:precision="0"
suffix="单"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'order_no'">
<a-typography-text copyable>{{ record.order_no }}</a-typography-text>
</template>
<template v-else-if="column.key === 'amount'">¥{{ record.amount }}</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 === 'payment_method'">
<span>{{ getPaymentMethodText(record.payment_method) }}</span>
</template>
<template v-else-if="column.key === 'actions'">
<a-space :size="8">
<a-button size="small" @click="handleViewOrder(record)">
<EyeOutlined />详情
</a-button>
<template v-if="record.status === 'paid'">
<a-button size="small" type="primary" @click="handleShip(record)">
<CarOutlined />发货
</a-button>
</template>
<template v-if="record.status === 'shipped'">
<a-button size="small" type="primary" @click="handleComplete(record)">
<CheckCircleOutlined />完成
</a-button>
</template>
<template v-if="['pending', 'paid'].includes(record.status)">
<a-button size="small" danger @click="handleCancel(record)">
<CloseOutlined />取消
</a-button>
</template>
<template v-if="record.status === 'paid'">
<a-button size="small" danger @click="handleRefund(record)">
<RollbackOutlined />退款
</a-button>
</template>
</a-space>
</template>
<template #prefix>
<ShoppingCartOutlined />
</template>
</a-table>
</a-statistic>
</a-card>
</a-tab-pane>
</a-col>
<a-col :span="6">
<a-card>
<a-statistic
title="今日销售额"
:value="statistics.today_sales"
:precision="2"
suffix="元"
:value-style="{ color: '#cf1322' }"
>
<template #prefix>
<MoneyCollectOutlined />
</template>
</a-statistic>
</a-card>
</a-col>
<a-col :span="6">
<a-card>
<a-statistic
title="本月订单"
:value="statistics.month_orders"
:precision="0"
suffix="单"
>
<template #prefix>
<CalendarOutlined />
</template>
</a-statistic>
</a-card>
</a-col>
<a-col :span="6">
<a-card>
<a-statistic
title="本月销售额"
:value="statistics.month_sales"
:precision="2"
suffix="元"
:value-style="{ color: '#cf1322' }"
>
<template #prefix>
<DollarOutlined />
</template>
</a-statistic>
</a-card>
</a-col>
</a-row>
<a-tab-pane key="statistics" tab="销售统计">
<a-card title="销售数据概览">
<a-row :gutter="16">
<a-col :span="6">
<a-statistic title="今日订单" :value="statistics.today_orders" :precision="0" :value-style="{ color: '#3f8600' }">
<template #prefix><ShoppingOutlined /></template>
</a-statistic>
</a-col>
<a-col :span="6">
<a-statistic title="今日销售额" :value="statistics.today_sales" :precision="2" prefix="¥" :value-style="{ color: '#cf1322' }" />
</a-col>
<a-col :span="6">
<a-statistic title="本月订单" :value="statistics.month_orders" :precision="0" :value-style="{ color: '#1890ff' }" />
</a-col>
<a-col :span="6">
<a-statistic title="本月销售额" :value="statistics.month_sales" :precision="2" prefix="¥" :value-style="{ color: '#722ed1' }" />
</a-col>
</a-row>
</a-card>
<a-card>
<!-- 搜索区域 -->
<div class="search-container">
<a-form layout="inline" :model="searchForm">
<a-form-item label="订单号">
<a-input
v-model:value="searchForm.order_no"
placeholder="请输入订单号"
allow-clear
/>
</a-form-item>
<a-form-item label="状态">
<a-select
v-model:value="searchForm.status"
placeholder="全部状态"
style="width: 120px"
allow-clear
>
<a-select-option value="pending">待支付</a-select-option>
<a-select-option value="paid">已支付</a-select-option>
<a-select-option value="shipped">已发货</a-select-option>
<a-select-option value="completed">已完成</a-select-option>
<a-select-option value="cancelled">已取消</a-select-option>
<a-select-option value="refunded">已退款</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="下单时间">
<a-range-picker
v-model:value="searchForm.orderTime"
:placeholder="['开始时间', '结束时间']"
/>
</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-card title="销售趋势" style="margin-top: 16px;">
<div style="height: 300px;">
<div style="display: flex; align-items: center; justify-content: center; height: 100%; color: #999;">
<BarChartOutlined style="font-size: 48px; margin-right: 12px;" />
<span>销售趋势图表开发中</span>
</div>
</div>
</a-card>
</a-tab-pane>
</a-tabs>
<!-- 订单表格 -->
<a-table
:columns="columns"
:data-source="orderList"
:loading="loading"
:pagination="pagination"
:row-key="record => record.id"
@change="handleTableChange"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'amount'">
<span class="amount-text">¥{{ record.amount }}</span>
</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 === 'actions'">
<a-space :size="8">
<a-button size="small" @click="handleView(record)">
<EyeOutlined />
查看
</a-button>
<template v-if="record.status === 'pending'">
<a-button size="small" type="primary" @click="handlePay(record)">
<PayCircleOutlined />
支付
</a-button>
</template>
<template v-else-if="record.status === 'paid'">
<a-button size="small" type="primary" @click="handleShip(record)">
<SendOutlined />
发货
</a-button>
</template>
<template v-else-if="record.status === 'shipped'">
<a-button size="small" type="primary" @click="handleComplete(record)">
<CheckCircleOutlined />
完成
</a-button>
</template>
<a-dropdown>
<a-button size="small">
更多
<DownOutlined />
</a-button>
<template #overlay>
<a-menu>
<a-menu-item
v-if="['pending', 'paid'].includes(record.status)"
@click="handleCancel(record)"
>
<CloseCircleOutlined />
取消
</a-menu-item>
<a-menu-item
v-if="['paid', 'shipped', 'completed'].includes(record.status)"
@click="handleRefund(record)"
>
<RedoOutlined />
退款
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</a-space>
</template>
</template>
</a-table>
</a-card>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { ref, reactive, onMounted, h } from 'vue'
import { message, Modal } from 'ant-design-vue'
import type { TableProps } from 'ant-design-vue'
import {
ReloadOutlined,
ShoppingCartOutlined,
MoneyCollectOutlined,
CalendarOutlined,
DollarOutlined,
SearchOutlined,
BarChartOutlined,
ReloadOutlined,
EyeOutlined,
CarOutlined,
PayCircleOutlined,
SendOutlined,
CheckCircleOutlined,
CloseOutlined,
RollbackOutlined,
ShoppingOutlined
CloseCircleOutlined,
RedoOutlined,
DownOutlined
} from '@ant-design/icons-vue'
import { getOrders, updateOrderStatus, getOrderStatistics } from '@/api/order'
import {
getOrders,
getOrder,
getOrderStatistics,
updateOrderStatus,
shipOrder,
completeOrder,
cancelOrder,
refundOrder
} from '@/api/order'
import type { Order, OrderStatistics } from '@/api/order'
interface SearchForm {
@@ -165,8 +244,13 @@ interface SearchForm {
orderTime: any[]
}
const activeTab = ref('orders')
const loading = ref(false)
const statistics = ref<OrderStatistics>({
today_orders: 0,
today_sales: 0,
month_orders: 0,
month_sales: 0
})
const searchForm = reactive<SearchForm>({
order_no: '',
@@ -174,13 +258,6 @@ const searchForm = reactive<SearchForm>({
orderTime: []
})
const statistics = reactive<OrderStatistics>({
today_orders: 0,
today_sales: 0,
month_orders: 0,
month_sales: 0
})
const orderList = ref<Order[]>([])
const pagination = reactive({
current: 1,
@@ -191,25 +268,66 @@ const pagination = reactive({
showTotal: (total: number) => `共 ${total} 条记录`
})
const orderColumns = [
{ title: '订单号', key: 'order_no', width: 160 },
{ title: '用户', dataIndex: 'user_name', key: 'user_name', width: 100 },
{ title: '联系电话', dataIndex: 'user_phone', key: 'user_phone', width: 120 },
{ title: '金额', key: 'amount', width: 100, align: 'center' },
{ title: '状态', key: 'status', width: 100, align: 'center' },
{ title: '支付方式', key: 'payment_method', width: 100, align: 'center' },
{ title: '下单时间', dataIndex: 'created_at', key: 'created_at', width: 150 },
{ title: '操作', key: 'actions', width: 200, align: 'center' }
const columns = [
{
title: '订单号',
dataIndex: 'order_no',
key: 'order_no',
width: 150
},
{
title: '用户',
dataIndex: 'user_name',
key: 'user_name',
width: 100
},
{
title: '联系电话',
dataIndex: 'user_phone',
key: 'user_phone',
width: 120
},
{
title: '金额',
key: 'amount',
width: 100,
align: 'right'
},
{
title: '状态',
key: 'status',
width: 100,
align: 'center'
},
{
title: '支付方式',
dataIndex: 'payment_method',
key: 'payment_method',
width: 100
},
{
title: '下单时间',
dataIndex: 'created_at',
key: 'created_at',
width: 120
},
{
title: '操作',
key: 'actions',
width: 200,
align: 'center'
}
]
// 状态映射
const getStatusColor = (status: string) => {
const colors = {
pending: 'orange',
paid: 'blue',
shipped: 'green',
completed: 'purple',
shipped: 'purple',
completed: 'green',
cancelled: 'red',
refunded: 'default'
refunded: 'red'
}
return colors[status as keyof typeof colors] || 'default'
}
@@ -226,21 +344,24 @@ const getStatusText = (status: string) => {
return texts[status as keyof typeof texts] || '未知'
}
// 支付方式映射
const getPaymentMethodText = (method: string) => {
const texts = {
wechat: '微信支付',
alipay: '支付宝',
bank: '银行',
bank: '银行转账',
balance: '余额支付'
}
return texts[method as keyof typeof texts] || '未知'
}
// 生命周期
onMounted(() => {
loadOrders()
loadStatistics()
})
// 方法
const loadOrders = async () => {
loading.value = true
try {
@@ -263,20 +384,12 @@ const loadOrders = async () => {
const loadStatistics = async () => {
try {
const response = await getOrderStatistics()
Object.assign(statistics, response.data)
statistics.value = response.data
} catch (error) {
message.error('加载统计数据失败')
}
}
const handleTabChange = (key: string) => {
if (key === 'orders') {
loadOrders()
} else if (key === 'statistics') {
loadStatistics()
}
}
const handleSearch = () => {
pagination.current = 1
loadOrders()
@@ -293,11 +406,8 @@ const handleReset = () => {
}
const handleRefresh = () => {
if (activeTab.value === 'orders') {
loadOrders()
} else {
loadStatistics()
}
loadOrders()
loadStatistics()
message.success('数据已刷新')
}
@@ -307,17 +417,60 @@ const handleTableChange: TableProps['onChange'] = (pag) => {
loadOrders()
}
const handleViewOrder = (record: Order) => {
message.info(`查看订单: ${record.order_no}`)
const handleView = async (record: Order) => {
try {
const response = await getOrder(record.id)
Modal.info({
title: '订单详情',
width: 600,
content: h('div', { class: 'order-detail-modal' }, [
h('a-descriptions', {
column: 1,
bordered: true
}, [
h('a-descriptions-item', { label: '订单号' }, response.data.order_no),
h('a-descriptions-item', { label: '用户' }, response.data.user_name),
h('a-descriptions-item', { label: '联系电话' }, response.data.user_phone),
h('a-descriptions-item', { label: '订单金额' }, `¥${response.data.amount}`),
h('a-descriptions-item', { label: '状态' }, [
h('a-tag', { color: getStatusColor(response.data.status) }, getStatusText(response.data.status))
]),
h('a-descriptions-item', { label: '支付方式' }, getPaymentMethodText(response.data.payment_method)),
h('a-descriptions-item', { label: '下单时间' }, response.data.created_at),
h('a-descriptions-item', { label: '支付时间' }, response.data.paid_at || '-'),
h('a-descriptions-item', { label: '发货时间' }, response.data.shipped_at || '-'),
h('a-descriptions-item', { label: '完成时间' }, response.data.completed_at || '-')
])
])
})
} catch (error) {
message.error('获取订单详情失败')
}
}
const handlePay = async (record: Order) => {
Modal.confirm({
title: '确认支付',
content: `确定要标记订单 "${record.order_no}" 为已支付状态吗?`,
onOk: async () => {
try {
await updateOrderStatus(record.id, 'paid')
message.success('订单状态已更新')
loadOrders()
} catch (error) {
message.error('操作失败')
}
}
})
}
const handleShip = async (record: Order) => {
Modal.confirm({
title: '确认发货',
content: `确定要发货订单 "${record.order_no}" 吗?`,
content: `确定要标记订单 "${record.order_no}" 为已发货状态吗?`,
onOk: async () => {
try {
await updateOrderStatus(record.id, 'shipped')
await shipOrder(record.id)
message.success('订单已发货')
loadOrders()
} catch (error) {
@@ -330,10 +483,10 @@ const handleShip = async (record: Order) => {
const handleComplete = async (record: Order) => {
Modal.confirm({
title: '确认完成',
content: `确定要完成订单 "${record.order_no}" 吗?`,
content: `确定要标记订单 "${record.order_no}" 为已完成状态吗?`,
onOk: async () => {
try {
await updateOrderStatus(record.id, 'completed')
await completeOrder(record.id)
message.success('订单已完成')
loadOrders()
} catch (error) {
@@ -349,7 +502,7 @@ const handleCancel = async (record: Order) => {
content: `确定要取消订单 "${record.order_no}" 吗?`,
onOk: async () => {
try {
await updateOrderStatus(record.id, 'cancelled')
await cancelOrder(record.id)
message.success('订单已取消')
loadOrders()
} catch (error) {
@@ -362,11 +515,11 @@ const handleCancel = async (record: Order) => {
const handleRefund = async (record: Order) => {
Modal.confirm({
title: '确认退款',
content: `确定要退款订单 "${record.order_no}" 吗?退款金额: ¥${record.amount}`,
content: `确定要订单 "${record.order_no}" 办理退款吗?`,
onOk: async () => {
try {
await updateOrderStatus(record.id, 'refunded')
message.success('退款申请已提交')
await refundOrder(record.id)
message.success('订单已退款')
loadOrders()
} catch (error) {
message.error('操作失败')
@@ -374,15 +527,14 @@ const handleRefund = async (record: Order) => {
}
})
}
const showStats = () => {
activeTab.value = 'statistics'
loadStatistics()
}
</script>
<style scoped lang="less">
.order-management {
.stats-row {
margin-bottom: 24px;
}
.search-container {
margin-bottom: 16px;
padding: 16px;
@@ -393,5 +545,15 @@ const showStats = () => {
margin-bottom: 16px;
}
}
.amount-text {
font-weight: 500;
color: #cf1322;
}
}
:deep(.ant-table-thead > tr > th) {
background: #fafafa;
font-weight: 600;
}
</style>

View File

@@ -236,8 +236,8 @@
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue'
import { message, Modal, type FormInstance } from 'ant-design-vue'
import { ref, reactive, computed, onMounted, h } from 'vue'
import { message, Modal, type FormInstance, TableProps } from 'ant-design-vue'
import {
UserAddOutlined,
SearchOutlined,
@@ -248,10 +248,10 @@ import {
StopOutlined,
PlayCircleOutlined,
WarningOutlined,
DownOutlined
DownOutlined,
type TableProps
} from '@ant-design/icons-vue'
import type { TableProps } from 'ant-design-vue'
import { getUsers, updateUser, createUser, deleteUser } from '@/api/user'
import { getUsers, getUser, updateUser, createUser, deleteUser } from '@/api/user'
import type { User } from '@/api/user'
interface SearchForm {
@@ -473,9 +473,44 @@ const showCreateModal = () => {
modalVisible.value = true
}
const handleView = (record: User) => {
// TODO: 跳转到用户详情页
message.info(`查看用户: ${record.nickname}`)
const handleView = async (record: User) => {
try {
const response = await getUser(record.id)
Modal.info({
title: '用户详情',
width: 600,
content: h('div', { class: 'user-detail-modal' }, [
h('a-descriptions', {
column: 1,
bordered: true
}, [
h('a-descriptions-item', { label: '用户ID' }, response.data.id),
h('a-descriptions-item', { label: '用户名' }, response.data.username),
h('a-descriptions-item', { label: '昵称' }, response.data.nickname),
h('a-descriptions-item', { label: '头像' }, [
h('a-avatar', {
src: response.data.avatar,
size: 64,
shape: 'square'
})
]),
h('a-descriptions-item', { label: '性别' }, response.data.gender),
h('a-descriptions-item', { label: '生日' }, response.data.birthday),
h('a-descriptions-item', { label: '手机号' }, response.data.phone),
h('a-descriptions-item', { label: '邮箱' }, response.data.email),
h('a-descriptions-item', { label: '状态' }, [
h('a-tag', { color: getStatusColor(response.data.status) }, getStatusText(response.data.status))
]),
h('a-descriptions-item', { label: '等级' }, `Lv.${response.data.level}`),
h('a-descriptions-item', { label: '积分' }, response.data.points),
h('a-descriptions-item', { label: '注册时间' }, response.data.created_at),
h('a-descriptions-item', { label: '更新时间' }, response.data.updated_at)
])
])
})
} catch (error) {
message.error('获取用户详情失败')
}
}
const handleEdit = (record: User) => {