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

This commit is contained in:
2025-09-10 20:35:04 +08:00
parent d4ab40989b
commit d875bb49af
7 changed files with 1212 additions and 31 deletions

View File

@@ -226,12 +226,106 @@
</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 } from 'ant-design-vue'
import { message, Modal, type FormInstance } from 'ant-design-vue'
import type { TableProps } from 'ant-design-vue'
import {
ReloadOutlined,
@@ -243,8 +337,8 @@ import {
CheckOutlined,
CloseOutlined
} from '@ant-design/icons-vue'
import { getAnimals, deleteAnimal, getAnimalClaims, approveAnimalClaim, rejectAnimalClaim } from '@/api/animal'
import type { Animal, AnimalClaim } from '@/api/animal'
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
@@ -403,7 +497,7 @@ const claimColumns = [
}
]
const fallbackImage = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjAiIGhlaWdodD0iNjAiIHZpZXdCb3g9IjAgMCA2MCA2MCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjYwIiBoZWlnaHQ9IjYwIiBmaWxsPSIjRkZGIi8+CjxwYXRoIGQ9Ik0zMCAxNUMzMS42NTY5IDE1IDMzIDE2LjM0MzEgMzMgMThDMzMgMTkuNjU2OSAzMS42NTY5IDIxIDMwIDIxQzI4LjM0MzEgMjEgMjcgMTkuNjU2OSAyNyAxOEMyNyAxNi4zNDMxIDI4LjM0MzEgMTUgMzAgMTVaIiBmaWxsPSIjQ0NDQ0NDIi8+CjxwYXRoIGQ9Ik0yMi41IDI1QzIyLjUgMjUuODI4NCAyMS44Mjg0IDI2LjUgMjEgMjYuNUgxOUMxOC4xNzE2IDI2LjUgMTcuNSAyNS44Mjg0IDE3LjUgMjVDMTcuNSAyNC4xNzE2IDE4LjE3MTYgMjMuNSAxOSAyMy41SDIxQzIxLjgyODQgMjMuNSAyMi41IDI0LjE3MTYgMjIuNSAyNVoiIGZpbGw9IiNDQ0NDQ0MiLz4KPHBhdGggZD0iTTQyLjUgMjVDNDIuNSAyNS44Mjg0IDQxLjgyODQgMjYuNSA0MSAyNi41SDM5QzM4LjE3MTYgMjYuNSAzNy41IDI1LjgyODQgMzcuNSAyNUMzNy41IDI0LjE3MTYgMzguMTcxNiAyMy41IDM5IDIzLjVMNDEgMjMuNUM0MS44Mjg0IDIzLjUgNDIuNSAyNC4xNzE2IDQyLjUgMjVaIiBmaWxsPSIjQ0NDQ0NDIi8+Cjwvc3ZnPgo='
const fallbackImage = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjAiIGhlaWdodD0iNjAiIHZpZXdCb3g9IjAgMCA2MCA2MCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjYwIiBoZWlnaHQ9IjYwIiBmaWxsPSIjRkZGIi8+CjxwYXRoIGQ9Ik0zMCAxNUMzMS42NTY5IDE1IDMzIDE2LjM0MzEgMzMgMThDMzMgMTkuNjU2OSAzMS42NTY5IDIxIDMwIDIxQzI4LjM0MzEgMjEgMjcgMTkuNjU2OSAyNyAxOEMyNyAxNi4zNDMxIDI4LjM0MzEgMTUgMzAgMTVaIiBmaWxsPSIjQ0NDQ0NDIi8+CjxwYXRoIGQ9Ik0yMi41IDI1QzIyLjUgMjUuODI4NCAyMS44Mjg0IDI2LjUgMjEgMjYuNUgxOUMxOC4xNzE2IDI2LjUgMTcuNSAyNS44Mjg0IDE3LjUgMjVDMTcuNSAyNC4xNzE2IDE4LjE3MTYgMjMuNSAxOSAyMy55SDIxQzIxLjgyODQgMjMuNSAyMi41IDI0LjE3MTYgMjIuNSAyNVoiIGZpbGw9IiNDQ0NDQ0MiLz4KPHBhdGggZD0iTTQyLjUgMjVDNDIuNSAyNS44Mjg0IDQxLjgyODQgMjYuNSA0MSAyNi41SDM5QzM4LjE3MTYgMjYuNSAzNy41IDI1LjgyODQgMzcuNSAyNUMzNy41IDI0LjE3MTYgMzguMTcxNiAyMy41IDM5IDIzLjVMNDEgMjMuNUM0MS44Mjg0IDIzLjUgNDIuNSAyNC4xNzE2IDQyLjUgMjVaIiBmaWxsPSIjQ0NDQ0NDIi8+Cjwvc3ZnPgo='
// 类型映射
const getTypeColor = (type: string) => {
@@ -465,6 +559,41 @@ const getClaimStatusText = (status: string) => {
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()
@@ -567,8 +696,21 @@ const handleViewAnimal = (record: Animal) => {
message.info(`查看动物: ${record.name}`)
}
const handleEditAnimal = (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) => {
@@ -626,8 +768,64 @@ const handleViewClaim = (record: AnimalClaim) => {
}
const showCreateModal = () => {
message.info('新增动物功能开发中')
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">

View File

@@ -104,6 +104,11 @@
查看
</a-button>
<a-button size="small" @click="showEditModal(record)">
<EditOutlined />
编辑
</a-button>
<template v-if="record.status === 'pending'">
<a-button size="small" type="primary" @click="handleApprove(record)">
<CheckOutlined />
@@ -133,12 +138,85 @@
</template>
</a-table>
</a-card>
<!-- 创建/编辑商家模态框 -->
<a-modal
v-model:open="modalVisible"
:title="modalTitle"
:confirm-loading="modalLoading"
@ok="handleModalOk"
@cancel="handleModalCancel"
width="600px"
>
<a-form
ref="merchantFormRef"
:model="currentMerchant"
:rules="formRules"
layout="vertical"
>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="商家名称" name="business_name">
<a-input v-model:value="currentMerchant.business_name" placeholder="请输入商家名称" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="商家类型" name="merchant_type">
<a-select v-model:value="currentMerchant.merchant_type" placeholder="请选择商家类型">
<a-select-option value="flower_shop">花店</a-select-option>
<a-select-option value="activity_organizer">活动组织</a-select-option>
<a-select-option value="farm_owner">农场主</a-select-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="联系人" name="contact_person">
<a-input v-model:value="currentMerchant.contact_person" placeholder="请输入联系人" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="联系电话" name="contact_phone">
<a-input v-model:value="currentMerchant.contact_phone" placeholder="请输入联系电话" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="联系邮箱" name="contact_email">
<a-input v-model:value="currentMerchant.contact_email" placeholder="请输入联系邮箱" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="状态" name="status">
<a-select v-model:value="currentMerchant.status" placeholder="请选择状态">
<a-select-option value="pending">待审核</a-select-option>
<a-select-option value="approved">已通过</a-select-option>
<a-select-option value="rejected">已拒绝</a-select-option>
<a-select-option value="disabled">已禁用</a-select-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-form-item label="详细地址" name="address">
<a-input v-model:value="currentMerchant.address" placeholder="请输入详细地址" />
</a-form-item>
<a-form-item label="备注" name="remark">
<a-textarea v-model:value="currentMerchant.remark" placeholder="请输入备注" :rows="3" />
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, h } from 'vue'
import { message, Modal } from 'ant-design-vue'
import { message, Modal, type FormInstance } from 'ant-design-vue'
import type { TableProps } from 'ant-design-vue'
import {
ShopOutlined,
@@ -148,10 +226,11 @@ import {
CheckOutlined,
CloseOutlined,
StopOutlined,
PlayCircleOutlined
PlayCircleOutlined,
EditOutlined
} from '@ant-design/icons-vue'
import { getMerchants, getMerchant, approveMerchant, rejectMerchant, disableMerchant, enableMerchant } from '@/api/merchant'
import type { Merchant } from '@/api/merchant'
import { getMerchants, getMerchant, approveMerchant, rejectMerchant, disableMerchant, enableMerchant, createMerchant, updateMerchant } from '@/api/merchant'
import type { Merchant, MerchantCreateData, MerchantUpdateData } from '@/api/merchant'
interface SearchForm {
keyword: string
@@ -262,6 +341,44 @@ const getStatusText = (status: string) => {
return texts[status as keyof typeof texts] || '未知'
}
// 添加模态框相关状态
const modalVisible = ref(false)
const modalLoading = ref(false)
const modalTitle = ref('新增商家')
const isEditing = ref(false)
const merchantFormRef = ref<FormInstance>()
// 当前商家数据
const currentMerchant = ref<Partial<Merchant>>({})
// 表单验证规则
const formRules = {
business_name: [
{ required: true, message: '请输入商家名称' },
{ min: 2, max: 50, message: '商家名称长度为2-50个字符' }
],
merchant_type: [
{ required: true, message: '请选择商家类型' }
],
contact_person: [
{ required: true, message: '请输入联系人' },
{ min: 2, max: 20, message: '联系人长度为2-20个字符' }
],
contact_phone: [
{ required: true, message: '请输入联系电话' },
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号' }
],
contact_email: [
{ type: 'email', message: '请输入正确的邮箱地址' }
],
status: [
{ required: true, message: '请选择状态' }
],
address: [
{ min: 5, max: 100, message: '地址长度为5-100个字符' }
]
}
// 生命周期
onMounted(() => {
loadMerchants()
@@ -408,8 +525,145 @@ const handleEnable = async (record: Merchant) => {
}
const showCreateModal = () => {
message.info('新增商家功能开发中')
modalTitle.value = '新增商家'
isEditing.value = false
currentMerchant.value = {
status: 'pending'
}
modalVisible.value = true
}
const showEditModal = async (record: Merchant) => {
try {
modalLoading.value = true
modalTitle.value = '编辑商家'
isEditing.value = true
// 获取商家详情
const response = await getMerchant(record.id)
currentMerchant.value = response.data
modalVisible.value = true
} catch (error) {
message.error('获取商家详情失败')
} finally {
modalLoading.value = false
}
}
const handleModalOk = () => {
merchantFormRef.value
?.validate()
.then(() => {
if (isEditing.value) {
handleUpdateMerchant()
} else {
handleCreateMerchant()
}
})
.catch((error: any) => {
console.error('表单验证失败:', error)
})
}
const handleModalCancel = () => {
modalVisible.value = false
merchantFormRef.value?.resetFields()
}
const handleCreateMerchant = async () => {
try {
modalLoading.value = true
// 前端数据验证
if (!currentMerchant.value.business_name) {
message.error('商家名称不能为空')
return
}
if (!currentMerchant.value.merchant_type) {
message.error('请选择商家类型')
return
}
if (!currentMerchant.value.contact_person) {
message.error('联系人不能为空')
return
}
if (!currentMerchant.value.contact_phone) {
message.error('联系电话不能为空')
return
}
if (!/^1[3-9]\d{9}$/.test(currentMerchant.value.contact_phone)) {
message.error('请输入正确的手机号')
return
}
if (currentMerchant.value.contact_email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(currentMerchant.value.contact_email)) {
message.error('请输入正确的邮箱地址')
return
}
await createMerchant(currentMerchant.value as MerchantCreateData)
message.success('创建商家成功')
modalVisible.value = false
loadMerchants()
} catch (error) {
console.error('创建商家失败:', error)
message.error('创建商家失败: ' + (error as Error).message)
} finally {
modalLoading.value = false
}
}
const handleUpdateMerchant = async () => {
try {
modalLoading.value = true
// 前端数据验证
if (!currentMerchant.value.business_name) {
message.error('商家名称不能为空')
return
}
if (!currentMerchant.value.merchant_type) {
message.error('请选择商家类型')
return
}
if (!currentMerchant.value.contact_person) {
message.error('联系人不能为空')
return
}
if (!currentMerchant.value.contact_phone) {
message.error('联系电话不能为空')
return
}
if (!/^1[3-9]\d{9}$/.test(currentMerchant.value.contact_phone)) {
message.error('请输入正确的手机号')
return
}
if (currentMerchant.value.contact_email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(currentMerchant.value.contact_email)) {
message.error('请输入正确的邮箱地址')
return
}
await updateMerchant(currentMerchant.value.id!, currentMerchant.value as MerchantUpdateData)
message.success('更新商家成功')
modalVisible.value = false
loadMerchants()
} catch (error) {
console.error('更新商家失败:', error)
message.error('更新商家失败: ' + (error as Error).message)
} finally {
modalLoading.value = false
}
}
</script>
<style scoped lang="less">

View File

@@ -204,12 +204,40 @@
</template>
</a-table>
</a-card>
<!-- 编辑订单备注模态框 -->
<a-modal
v-model:open="remarkModalVisible"
title="编辑订单备注"
:confirm-loading="remarkModalLoading"
@ok="handleRemarkModalOk"
@cancel="handleRemarkModalCancel"
width="500px"
>
<a-form
ref="remarkFormRef"
:model="currentOrder"
layout="vertical"
>
<a-form-item label="订单号">
<a-input v-model:value="currentOrder.order_no" disabled />
</a-form-item>
<a-form-item label="备注" name="remark">
<a-textarea
v-model:value="currentOrder.remark"
placeholder="请输入订单备注"
:rows="4"
/>
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, h } from 'vue'
import { message, Modal } from 'ant-design-vue'
import { message, Modal, type FormInstance } from 'ant-design-vue'
import type { TableProps } from 'ant-design-vue'
import {
ShoppingCartOutlined,
@@ -224,7 +252,8 @@ import {
CheckCircleOutlined,
CloseCircleOutlined,
RedoOutlined,
DownOutlined
DownOutlined,
EditOutlined
} from '@ant-design/icons-vue'
import {
getOrders,
@@ -234,9 +263,10 @@ import {
shipOrder,
completeOrder,
cancelOrder,
refundOrder
refundOrder,
updateOrder
} from '@/api/order'
import type { Order, OrderStatistics } from '@/api/order'
import type { Order, OrderStatistics, OrderUpdateData } from '@/api/order'
interface SearchForm {
order_no: string
@@ -355,6 +385,14 @@ const getPaymentMethodText = (method: string) => {
return texts[method as keyof typeof texts] || '未知'
}
// 添加备注模态框相关状态
const remarkModalVisible = ref(false)
const remarkModalLoading = ref(false)
const remarkFormRef = ref<FormInstance>()
// 当前订单数据
const currentOrder = ref<Partial<Order>>({})
// 生命周期
onMounted(() => {
loadOrders()
@@ -439,8 +477,19 @@ const handleView = async (record: Order) => {
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 || '-')
h('a-descriptions-item', { label: '完成时间' }, response.data.completed_at || '-'),
h('a-descriptions-item', { label: '备注' }, response.data.remark || '-')
])
]),
okText: '关闭',
footer: (_, { OkBtn }) => h('div', { style: 'text-align: right;' }, [
h('a-button', {
onClick: () => {
Modal.destroyAll()
handleEditRemark(response.data)
}
}, '编辑备注'),
h(OkBtn)
])
})
} catch (error) {
@@ -448,6 +497,46 @@ const handleView = async (record: Order) => {
}
}
// 添加编辑备注的方法
const handleEditRemark = async (record: Order) => {
try {
const response = await getOrder(record.id)
currentOrder.value = response.data
remarkModalVisible.value = true
} catch (error) {
message.error('获取订单详情失败')
}
}
const handleRemarkModalOk = () => {
remarkFormRef.value
?.validate()
.then(async () => {
try {
remarkModalLoading.value = true
const updateData: OrderUpdateData = {
remark: currentOrder.value.remark
}
await updateOrder(currentOrder.value.id!, updateData)
message.success('更新订单备注成功')
remarkModalVisible.value = false
loadOrders()
} catch (error) {
message.error('更新订单备注失败')
} finally {
remarkModalLoading.value = false
}
})
.catch((error: any) => {
console.error('表单验证失败:', error)
})
}
const handleRemarkModalCancel = () => {
remarkModalVisible.value = false
remarkFormRef.value?.resetFields()
}
const handlePay = async (record: Order) => {
Modal.confirm({
title: '确认支付',

View File

@@ -187,12 +187,95 @@
</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="promotionFormRef"
:model="currentActivity"
:rules="formRules"
layout="vertical"
>
<a-form-item label="活动名称" name="name">
<a-input v-model:value="currentActivity.name" placeholder="请输入活动名称" />
</a-form-item>
<a-form-item label="活动描述" name="description">
<a-textarea
v-model:value="currentActivity.description"
placeholder="请输入活动描述"
:rows="3"
/>
</a-form-item>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="奖励类型" name="reward_type">
<a-select v-model:value="currentActivity.reward_type" placeholder="请选择奖励类型">
<a-select-option value="cash">现金</a-select-option>
<a-select-option value="points">积分</a-select-option>
<a-select-option value="coupon">优惠券</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="奖励金额" name="reward_amount">
<a-input-number
v-model:value="currentActivity.reward_amount"
:min="0"
:precision="2"
style="width: 100%"
/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="最大参与人数" name="max_participants">
<a-input-number
v-model:value="currentActivity.max_participants"
:min="1"
:max="10000"
style="width: 100%"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="状态" name="status">
<a-select v-model:value="currentActivity.status" placeholder="请选择状态">
<a-select-option value="active">进行中</a-select-option>
<a-select-option value="upcoming">未开始</a-select-option>
<a-select-option value="ended">已结束</a-select-option>
<a-select-option value="paused">已暂停</a-select-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-form-item label="活动时间" required>
<a-range-picker
v-model:value="activityTimeRange"
format="YYYY-MM-DD HH:mm"
show-time
style="width: 100%"
/>
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { message, Modal } from 'ant-design-vue'
import { message, Modal, type FormInstance } from 'ant-design-vue'
import type { TableProps } from 'ant-design-vue'
import {
ReloadOutlined,
@@ -212,7 +295,10 @@ import {
pausePromotionActivity,
resumePromotionActivity,
getRewardRecords,
issueReward
issueReward,
createPromotionActivity,
updatePromotionActivity,
getPromotionActivity
} from '@/api/promotion'
import type { PromotionActivity, RewardRecord } from '@/api/promotion'
@@ -439,8 +525,28 @@ const handleViewActivity = (record: PromotionActivity) => {
message.info(`查看活动: ${record.name}`)
}
const handleEditActivity = (record: PromotionActivity) => {
message.info(`编辑活动: ${record.name}`)
const handleEditActivity = async (record: PromotionActivity) => {
try {
modalLoading.value = true
modalTitle.value = '编辑活动'
isEditing.value = true
// 获取活动详情
const response = await getPromotionActivity(record.id)
currentActivity.value = response.data
// 设置时间范围
activityTimeRange.value = [
new Date(response.data.start_time),
new Date(response.data.end_time)
]
modalVisible.value = true
} catch (error) {
message.error('获取活动详情失败')
} finally {
modalLoading.value = false
}
}
const handlePauseActivity = async (record: PromotionActivity) => {
@@ -514,8 +620,75 @@ const handleViewReward = (record: RewardRecord) => {
}
const showCreateModal = () => {
message.info('新活动功能开发中')
modalTitle.value = '新活动'
isEditing.value = false
currentActivity.value = {
reward_amount: 0,
max_participants: 100,
status: 'upcoming'
}
activityTimeRange.value = []
modalVisible.value = true
}
const handleModalOk = () => {
promotionFormRef.value
?.validate()
.then(() => {
// 检查时间范围是否已设置
if (!activityTimeRange.value || activityTimeRange.value.length !== 2) {
message.error('请选择活动时间')
return
}
// 设置时间范围到当前活动数据
currentActivity.value.start_time = activityTimeRange.value[0].toISOString()
currentActivity.value.end_time = activityTimeRange.value[1].toISOString()
if (isEditing.value) {
handleUpdateActivity()
} else {
handleCreateActivity()
}
})
.catch((error: any) => {
console.error('表单验证失败:', error)
})
}
const handleModalCancel = () => {
modalVisible.value = false
promotionFormRef.value?.resetFields()
}
const handleCreateActivity = async () => {
try {
modalLoading.value = true
await createPromotionActivity(currentActivity.value)
message.success('创建活动成功')
modalVisible.value = false
loadActivities()
} catch (error) {
message.error('创建活动失败')
} finally {
modalLoading.value = false
}
}
const handleUpdateActivity = async () => {
try {
modalLoading.value = true
await updatePromotionActivity(currentActivity.value.id!, currentActivity.value)
message.success('更新活动成功')
modalVisible.value = false
loadActivities()
} catch (error) {
message.error('更新活动失败')
} finally {
modalLoading.value = false
}
}
</script>
<style scoped lang="less">

View File

@@ -3,6 +3,10 @@
<a-page-header title="系统管理" sub-title="管理系统设置和配置">
<template #extra>
<a-space>
<a-button @click="exportConfig">
<ExportOutlined />
导出配置
</a-button>
<a-button @click="handleRefresh">
<ReloadOutlined />
刷新
@@ -91,6 +95,41 @@
</a-card>
</a-col>
<a-col :span="12">
<a-card title="系统监控" size="small">
<div style="height: 200px;">
<div style="display: flex; flex-direction: column; height: 100%; justify-content: space-around;">
<div>
<div style="display: flex; justify-content: space-between;">
<span>CPU使用率</span>
<span>{{ mockChartData.cpu }}%</span>
</div>
<a-progress :percent="mockChartData.cpu" size="small" />
</div>
<div>
<div style="display: flex; justify-content: space-between;">
<span>内存使用率</span>
<span>{{ mockChartData.memory }}%</span>
</div>
<a-progress :percent="mockChartData.memory" size="small" status="active" />
</div>
<div>
<div style="display: flex; justify-content: space-between;">
<span>网络流量</span>
<span>{{ mockChartData.network }} KB/s</span>
</div>
<a-progress :percent="Math.min(mockChartData.network / 10, 100)" size="small" status="success" />
</div>
<div style="text-align: center; font-size: 12px; color: #666;">
更新时间: {{ mockChartData.time }}
</div>
</div>
</div>
</a-card>
</a-col>
</a-row>
<a-row :gutter="16" style="margin-top: 16px;">
<a-col :span="12">
<a-card title="系统日志" size="small">
<a-timeline>
@@ -166,13 +205,14 @@
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { ref, reactive, onMounted, onBeforeUnmount } from 'vue'
import { message, Modal } from 'ant-design-vue'
import {
ReloadOutlined,
DatabaseOutlined,
CloudServerOutlined,
MessageOutlined
MessageOutlined,
ExportOutlined
} from '@ant-design/icons-vue'
import {
getServices,
@@ -217,12 +257,60 @@ const systemSettings = reactive<SystemSettings>({
enableSwagger: true
})
// 添加监控数据状态
const mockChartData = reactive({
time: new Date().toLocaleTimeString(),
cpu: 0,
memory: 0,
network: 0
})
let chartInterval: number | null = null
// 模拟系统监控数据
const generateMockChartData = () => {
return {
time: new Date().toLocaleTimeString(),
cpu: Math.floor(Math.random() * 100),
memory: Math.floor(Math.random() * 100),
network: Math.floor(Math.random() * 1000)
}
}
// 更新监控图表
const updateChart = () => {
const data = generateMockChartData()
mockChartData.time = data.time
mockChartData.cpu = data.cpu
mockChartData.memory = data.memory
mockChartData.network = data.network
}
// 开始监控
const startMonitoring = () => {
updateChart()
chartInterval = window.setInterval(updateChart, 5000)
}
// 停止监控
const stopMonitoring = () => {
if (chartInterval) {
clearInterval(chartInterval)
chartInterval = null
}
}
onMounted(() => {
loadSystemInfo()
loadDatabaseStatus()
loadCacheStatus()
loadServices()
loadSystemSettings()
startMonitoring()
})
onBeforeUnmount(() => {
stopMonitoring()
})
const loadSystemInfo = async () => {
@@ -278,6 +366,39 @@ const handleRefresh = () => {
message.success('系统状态已刷新')
}
const exportConfig = () => {
try {
// 创建要导出的数据对象
const exportData = {
systemInfo: systemInfo.value,
databaseStatus: databaseStatus.value,
cacheStatus: cacheStatus.value,
systemSettings: systemSettings,
services: services.value,
exportTime: new Date().toISOString()
}
// 创建Blob对象
const blob = new Blob([JSON.stringify(exportData, null, 2)], {
type: 'application/json'
})
// 创建下载链接
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = `system-config-${new Date().toISOString().slice(0, 10)}.json`
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(url)
message.success('配置导出成功')
} catch (error) {
message.error('配置导出失败')
}
}
const handleStopService = (service: Service) => {
Modal.confirm({
title: '确认停止',

View File

@@ -12,6 +12,12 @@
</template>
刷新
</a-button>
<a-button type="primary" @click="showCreateModal">
<template #icon>
<PlusOutlined />
</template>
新增旅行
</a-button>
<a-button type="primary" @click="showStats">
<template #icon>
<BarChartOutlined />
@@ -114,6 +120,11 @@
详情
</a-button>
<a-button size="small" @click="showEditModal(record)">
<EditOutlined />
编辑
</a-button>
<a-button size="small" @click="handleMembers(record)">
<TeamOutlined />
成员
@@ -134,12 +145,100 @@
</template>
</a-table>
</a-card>
<!-- 创建/编辑旅行计划模态框 -->
<a-modal
v-model:open="modalVisible"
:title="modalTitle"
:confirm-loading="modalLoading"
@ok="handleModalOk"
@cancel="handleModalCancel"
width="600px"
>
<a-form
ref="travelFormRef"
:model="currentTravel"
:rules="formRules"
layout="vertical"
>
<a-form-item label="旅行标题" name="title">
<a-input v-model:value="currentTravel.title" placeholder="请输入旅行标题" />
</a-form-item>
<a-form-item label="目的地" name="destination">
<a-input v-model:value="currentTravel.destination" placeholder="请输入目的地" />
</a-form-item>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="开始日期" name="start_date">
<a-date-picker
v-model:value="currentTravel.start_date"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
style="width: 100%"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="结束日期" name="end_date">
<a-date-picker
v-model:value="currentTravel.end_date"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
style="width: 100%"
/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="最大参与人数" name="max_participants">
<a-input-number
v-model:value="currentTravel.max_participants"
:min="1"
:max="100"
style="width: 100%"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="预算()" name="budget">
<a-input-number
v-model:value="currentTravel.budget"
:min="0"
:precision="2"
style="width: 100%"
/>
</a-form-item>
</a-col>
</a-row>
<a-form-item label="状态" name="status">
<a-select v-model:value="currentTravel.status" placeholder="请选择状态">
<a-select-option value="recruiting">招募中</a-select-option>
<a-select-option value="full">已满员</a-select-option>
<a-select-option value="completed">已完成</a-select-option>
<a-select-option value="cancelled">已取消</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="描述" name="description">
<a-textarea
v-model:value="currentTravel.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 } from 'ant-design-vue'
import { message, Modal, type FormInstance } from 'ant-design-vue'
import type { TableProps } from 'ant-design-vue'
import {
ReloadOutlined,
@@ -148,10 +247,12 @@ import {
EyeOutlined,
TeamOutlined,
RocketOutlined,
CloseOutlined
CloseOutlined,
PlusOutlined,
EditOutlined
} from '@ant-design/icons-vue'
import { getTravelPlans, closeTravelPlan } from '@/api/travel'
import type { TravelPlan } from '@/api/travel'
import { getTravelPlans, closeTravelPlan, createTravel, updateTravel, getTravel } from '@/api/travel'
import type { TravelPlan, TravelCreateData, TravelUpdateData } from '@/api/travel'
interface SearchForm {
destination: string
@@ -331,6 +432,197 @@ const handleClose = async (record: TravelPlan) => {
})
}
// 添加模态框相关状态
const modalVisible = ref(false)
const modalLoading = ref(false)
const modalTitle = ref('新增旅行')
const isEditing = ref(false)
const travelFormRef = ref<FormInstance>()
// 当前旅行数据
const currentTravel = ref<Partial<TravelPlan>>({})
// 表单验证规则
const formRules = {
title: [
{ required: true, message: '请输入旅行标题' },
{ min: 5, max: 100, message: '旅行标题长度为5-100个字符' }
],
destination: [
{ required: true, message: '请输入目的地' },
{ min: 2, max: 50, message: '目的地长度为2-50个字符' }
],
start_date: [
{ required: true, message: '请选择开始日期' }
],
end_date: [
{ required: true, message: '请选择结束日期' }
],
max_participants: [
{ required: true, message: '请输入最大参与人数' },
{ type: 'number', min: 1, max: 100, message: '参与人数应在1-100之间' }
],
budget: [
{ required: true, message: '请输入预算' },
{ type: 'number', min: 0, message: '预算不能为负数' }
],
status: [
{ required: true, message: '请选择状态' }
]
}
const showCreateModal = () => {
modalTitle.value = '新增旅行'
isEditing.value = false
currentTravel.value = {
status: 'recruiting',
max_participants: 10,
budget: 0
}
modalVisible.value = true
}
const showEditModal = async (record: TravelPlan) => {
try {
modalLoading.value = true
modalTitle.value = '编辑旅行'
isEditing.value = true
// 获取旅行详情
const response = await getTravel(record.id)
currentTravel.value = response.data
modalVisible.value = true
} catch (error) {
message.error('获取旅行详情失败')
} finally {
modalLoading.value = false
}
}
const handleModalOk = () => {
travelFormRef.value
?.validate()
.then(() => {
if (isEditing.value) {
handleUpdateTravel()
} else {
handleCreateTravel()
}
})
.catch((error: any) => {
console.error('表单验证失败:', error)
})
}
const handleModalCancel = () => {
modalVisible.value = false
travelFormRef.value?.resetFields()
}
const handleCreateTravel = async () => {
try {
modalLoading.value = true
// 前端数据验证
if (!currentTravel.value.title) {
message.error('旅行标题不能为空')
return
}
if (!currentTravel.value.destination) {
message.error('目的地不能为空')
return
}
if (!currentTravel.value.start_date) {
message.error('请选择开始日期')
return
}
if (!currentTravel.value.end_date) {
message.error('请选择结束日期')
return
}
if (new Date(currentTravel.value.start_date) >= new Date(currentTravel.value.end_date)) {
message.error('开始日期必须早于结束日期')
return
}
if (!currentTravel.value.max_participants || currentTravel.value.max_participants < 1) {
message.error('最大参与人数必须大于0')
return
}
if (currentTravel.value.budget === undefined || currentTravel.value.budget < 0) {
message.error('预算不能为负数')
return
}
await createTravel(currentTravel.value as TravelCreateData)
message.success('创建旅行计划成功')
modalVisible.value = false
loadTravelPlans()
} catch (error) {
console.error('创建旅行计划失败:', error)
message.error('创建旅行计划失败: ' + (error as Error).message)
} finally {
modalLoading.value = false
}
}
const handleUpdateTravel = async () => {
try {
modalLoading.value = true
// 前端数据验证
if (!currentTravel.value.title) {
message.error('旅行标题不能为空')
return
}
if (!currentTravel.value.destination) {
message.error('目的地不能为空')
return
}
if (!currentTravel.value.start_date) {
message.error('请选择开始日期')
return
}
if (!currentTravel.value.end_date) {
message.error('请选择结束日期')
return
}
if (new Date(currentTravel.value.start_date) >= new Date(currentTravel.value.end_date)) {
message.error('开始日期必须早于结束日期')
return
}
if (!currentTravel.value.max_participants || currentTravel.value.max_participants < 1) {
message.error('最大参与人数必须大于0')
return
}
if (currentTravel.value.budget === undefined || currentTravel.value.budget < 0) {
message.error('预算不能为负数')
return
}
await updateTravel(currentTravel.value.id!, currentTravel.value as TravelUpdateData)
message.success('更新旅行计划成功')
modalVisible.value = false
loadTravelPlans()
} catch (error) {
console.error('更新旅行计划失败:', error)
message.error('更新旅行计划失败: ' + (error as Error).message)
} finally {
modalLoading.value = false
}
}
const showStats = () => {
message.info('数据统计功能开发中')
}

View File

@@ -342,7 +342,8 @@ const avatarFileList = ref<UploadProps['fileList']>([])
const formRules = {
username: [
{ required: true, message: '请输入用户名' },
{ min: 3, max: 20, message: '用户名长度为3-20个字符' }
{ min: 3, max: 20, message: '用户名长度为3-20个字符' },
{ pattern: /^[a-zA-Z0-9_]+$/, message: '用户名只能包含字母、数字和下划线' }
],
nickname: [
{ required: true, message: '请输入昵称' },
@@ -353,6 +354,15 @@ const formRules = {
],
email: [
{ type: 'email', message: '请输入正确的邮箱地址' }
],
gender: [
{ required: true, message: '请选择性别' }
],
status: [
{ required: true, message: '请选择状态' }
],
level: [
{ required: true, message: '请选择等级' }
]
}
@@ -551,13 +561,35 @@ const handleModalCancel = () => {
const handleCreateUser = async () => {
try {
modalLoading.value = true
// 前端数据验证
if (!currentUser.value.username) {
message.error('用户名不能为空')
return
}
if (!currentUser.value.nickname) {
message.error('昵称不能为空')
return
}
if (currentUser.value.phone && !/^1[3-9]\d{9}$/.test(currentUser.value.phone)) {
message.error('请输入正确的手机号')
return
}
if (currentUser.value.email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(currentUser.value.email)) {
message.error('请输入正确的邮箱地址')
return
}
await createUser(currentUser.value)
message.success('创建用户成功')
modalVisible.value = false
fetchUsers()
} catch (error) {
console.error('创建用户失败:', error)
message.error('创建用户失败')
message.error('创建用户失败: ' + (error as Error).message)
} finally {
modalLoading.value = false
}
@@ -567,13 +599,35 @@ const handleCreateUser = async () => {
const handleUpdateUser = async () => {
try {
modalLoading.value = true
// 前端数据验证
if (!currentUser.value.username) {
message.error('用户名不能为空')
return
}
if (!currentUser.value.nickname) {
message.error('昵称不能为空')
return
}
if (currentUser.value.phone && !/^1[3-9]\d{9}$/.test(currentUser.value.phone)) {
message.error('请输入正确的手机号')
return
}
if (currentUser.value.email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(currentUser.value.email)) {
message.error('请输入正确的邮箱地址')
return
}
await updateUser(currentUser.value.id!, currentUser.value)
message.success('更新用户成功')
modalVisible.value = false
fetchUsers()
} catch (error) {
console.error('更新用户失败:', error)
message.error('更新用户失败')
message.error('更新用户失败: ' + (error as Error).message)
} finally {
modalLoading.value = false
}