This commit is contained in:
2026-05-31 12:09:14 +08:00
commit e4f9eedb80
1121 changed files with 209380 additions and 0 deletions
+744
View File
@@ -0,0 +1,744 @@
<template>
<a-page-header :title="getPageTitle()" @back="() => $router.go(-1)">
<template #extra>
<a-space>
<a-button @click="openBatchModal()">
<template #icon>
<ThunderboltOutlined />
</template>
批量生成
</a-button>
<a-button type="primary" @click="openGenerateModal()">
<template #icon>
<PlusOutlined />
</template>
生成兑换码
</a-button>
<a-button @click="reload()">
<template #icon>
<ReloadOutlined />
</template>
刷新
</a-button>
</a-space>
</template>
<a-card :bordered="false" :body-style="{ padding: '16px' }">
<!-- Tab 切换 -->
<a-tabs v-model:activeKey="activeTab" @change="handleTabChange">
<a-tab-pane key="codes" tab="兑换码管理">
<!-- 搜索区域 -->
<div class="search-container">
<a-form layout="inline" :model="searchForm" class="search-form">
<a-form-item label="兑换码">
<a-input
v-model:value="searchForm.code"
placeholder="请输入兑换码"
allow-clear
style="width: 200px"
/>
</a-form-item>
<a-form-item label="状态">
<a-select
v-model:value="searchForm.status"
placeholder="请选择状态"
allow-clear
style="width: 120px"
>
<a-select-option :value="0">未使用</a-select-option>
<a-select-option :value="1">已使用</a-select-option>
<a-select-option :value="2">已过期</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="批次号">
<a-input
v-model:value="searchForm.batchNo"
placeholder="请输入批次号"
allow-clear
style="width: 200px"
/>
</a-form-item>
<a-form-item>
<a-space>
<a-button type="primary" @click="handleSearch">
<template #icon>
<SearchOutlined />
</template>
搜索
</a-button>
<a-button @click="handleReset">
<template #icon>
<ClearOutlined />
</template>
重置
</a-button>
</a-space>
</a-form-item>
</a-form>
</div>
<!-- 统计卡片 -->
<a-row :gutter="16" style="margin-bottom: 16px">
<a-col :span="6">
<a-statistic
title="总兑换码数量"
:value="statistics.total"
:value-style="{ color: '#1890ff' }"
/>
</a-col>
<a-col :span="6">
<a-statistic
title="未使用"
:value="statistics.unused"
:value-style="{ color: '#52c41a' }"
/>
</a-col>
<a-col :span="6">
<a-statistic
title="已使用"
:value="statistics.used"
:value-style="{ color: '#faad14' }"
/>
</a-col>
<a-col :span="6">
<a-statistic
title="已过期"
:value="statistics.expired"
:value-style="{ color: '#ff4d4f' }"
/>
</a-col>
</a-row>
<!-- 表格 -->
<ele-pro-table
ref="tableRef"
row-key="id"
:columns="columns"
:datasource="datasource"
:scroll="{ x: 1400 }"
tool-class="ele-toolbar-form"
>
<template #bodyCell="{ column, record }">
<!-- 兑换码信息 -->
<template v-if="column.key === 'code'">
<div class="code-info">
<a-typography-text strong copyable>{{ record.code }}</a-typography-text>
<div class="batch-no" v-if="record.batchNo">
批次: {{ record.batchNo }}
</div>
</div>
</template>
<!-- 金额 -->
<template v-if="column.key === 'amount'">
<span class="amount-value">¥{{ formatAmount(record.amount) }}</span>
</template>
<!-- 状态 -->
<template v-if="column.key === 'status'">
<a-tag :color="getStatusColor(record.status)">
{{ getStatusText(record.status) }}
</a-tag>
</template>
<!-- 生成方式 -->
<template v-if="column.key === 'generateType'">
<a-tag :color="record.generateType === 1 ? 'blue' : 'purple'">
{{ record.generateType === 1 ? '管理员生成' : '批量生成' }}
</a-tag>
</template>
<!-- 过期时间 -->
<template v-if="column.key === 'expireTime'">
<span v-if="record.expireTime">{{ formatDate(record.expireTime) }}</span>
<span v-else>-</span>
</template>
<!-- 使用信息 -->
<template v-if="column.key === 'usedInfo'">
<template v-if="record.status === 1 && record.usedAt">
<div>使用时间: {{ formatDate(record.usedAt) }}</div>
<div class="balance-change">
<span class="balance-before">{{ formatAmount(record.balanceBefore) }}</span>
<ArrowRightOutlined />
<span class="balance-after">{{ formatAmount(record.balanceAfter) }}</span>
</div>
</template>
<span v-else>-</span>
</template>
<!-- 创建时间 -->
<template v-if="column.key === 'createTime'">
{{ formatDate(record.createTime) }}
</template>
<!-- 操作列 -->
<template v-if="column.key === 'action'">
<a-space>
<a-tooltip title="查看详情">
<a-button size="small" type="link" @click="viewDetail(record)">
<template #icon><EyeOutlined /></template>
</a-button>
</a-tooltip>
<a-tooltip title="复制兑换码">
<a-button size="small" type="link" @click="copyCode(record.code)">
<template #icon><CopyOutlined /></template>
</a-button>
</a-tooltip>
<a-popconfirm
title="确定要删除此兑换码吗?"
@confirm="handleDelete(record.id)"
>
<a-tooltip title="删除">
<a-button size="small" type="link" danger>
<template #icon><DeleteOutlined /></template>
</a-button>
</a-tooltip>
</a-popconfirm>
</a-space>
</template>
</template>
</ele-pro-table>
</a-tab-pane>
<a-tab-pane key="records" tab="兑换记录">
<RechargeRecord />
</a-tab-pane>
</a-tabs>
</a-card>
</a-page-header>
<!-- 生成兑换码弹窗 -->
<a-modal
v-model:open="generateModalVisible"
title="生成兑换码"
:width="500"
@ok="handleGenerate"
@cancel="generateModalVisible = false"
:confirm-loading="generateLoading"
>
<a-form
ref="generateFormRef"
:model="generateForm"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 16 }"
>
<a-form-item
label="充值金额"
name="amount"
:rules="[{ required: true, message: '请输入充值金额' }, { type: 'number', min: 0.01, message: '金额必须大于0' }]"
>
<a-input-number
v-model:value="generateForm.amount"
:min="0.01"
:precision="2"
style="width: 100%"
placeholder="请输入充值金额"
/>
</a-form-item>
<a-form-item
label="生成数量"
name="quantity"
:rules="[{ required: true, message: '请输入生成数量' }]"
>
<a-input-number
v-model:value="generateForm.quantity"
:min="1"
:max="100"
style="width: 100%"
placeholder="请输入生成数量"
/>
</a-form-item>
<a-form-item
label="有效期"
name="expireDays"
:rules="[{ required: true, message: '请输入有效期天数' }]"
>
<a-input-number
v-model:value="generateForm.expireDays"
:min="1"
:max="365"
style="width: 100%"
addon-after="天"
placeholder="请输入有效期天数"
/>
</a-form-item>
</a-form>
</a-modal>
<!-- 批量生成弹窗 -->
<a-modal
v-model:open="batchModalVisible"
title="批量生成兑换码"
:width="600"
@ok="handleBatchGenerate"
@cancel="batchModalVisible = false"
:confirm-loading="batchLoading"
>
<a-form
ref="batchFormRef"
:model="batchForm"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 16 }"
>
<a-form-item label="预设金额">
<a-select
v-model:value="batchForm.presetAmounts"
mode="tags"
placeholder="输入预设金额后按回车"
style="width: 100%"
:token-separators="[',']"
>
</a-select>
</a-form-item>
<a-form-item
label="每个金额数量"
name="quantityPerAmount"
:rules="[{ required: true, message: '请输入数量' }]"
>
<a-input-number
v-model:value="batchForm.quantityPerAmount"
:min="1"
:max="50"
style="width: 100%"
placeholder="每个预设金额生成的数量"
/>
</a-form-item>
<a-form-item
label="有效期"
name="expireDays"
:rules="[{ required: true, message: '请输入有效期天数' }]"
>
<a-input-number
v-model:value="batchForm.expireDays"
:min="1"
:max="365"
style="width: 100%"
addon-after="天"
placeholder="请输入有效期天数"
/>
</a-form-item>
</a-form>
</a-modal>
<!-- 生成的兑换码列表弹窗 -->
<a-modal
v-model:open="codesModalVisible"
title="生成的兑换码"
:width="700"
:footer="null"
>
<a-alert
:message="`共生成 ${generatedCodes.length} 个兑换码`"
type="success"
style="margin-bottom: 16px"
/>
<a-list
:data-source="generatedCodes"
size="small"
bordered
:max-height="400"
style="overflow-y: auto"
>
<template #renderItem="{ item }">
<a-list-item>
<a-list-item-meta>
<template #title>
<a-typography-text strong copyable>{{ item.code }}</a-typography-text>
</template>
<template #description>
金额: ¥{{ formatAmount(item.amount) }} |
有效期至: {{ formatDate(item.expireTime) }}
</template>
</a-list-item-meta>
</a-list-item>
</template>
</a-list>
<div style="margin-top: 16px; text-align: center">
<a-button type="primary" @click="copyAllCodes">复制全部兑换码</a-button>
</div>
</a-modal>
<!-- 兑换码详情弹窗 -->
<a-modal
v-model:open="detailModalVisible"
title="兑换码详情"
:width="500"
:footer="null"
>
<a-descriptions :column="1" bordered v-if="currentRecord">
<a-descriptions-item label="兑换码">
<a-typography-text copyable strong>{{ currentRecord.code }}</a-typography-text>
</a-descriptions-item>
<a-descriptions-item label="充值金额">
<span style="color: #f5222d; font-weight: bold">¥{{ formatAmount(currentRecord.amount) }}</span>
</a-descriptions-item>
<a-descriptions-item label="状态">
<a-tag :color="getStatusColor(currentRecord.status)">
{{ getStatusText(currentRecord.status) }}
</a-tag>
</a-descriptions-item>
<a-descriptions-item label="生成方式">
{{ currentRecord.generateType === 1 ? '管理员生成' : '批量生成' }}
</a-descriptions-item>
<a-descriptions-item label="批次号">{{ currentRecord.batchNo || '-' }}</a-descriptions-item>
<a-descriptions-item label="过期时间">{{ formatDate(currentRecord.expireTime) || '-' }}</a-descriptions-item>
<a-descriptions-item label="使用时间">{{ formatDate(currentRecord.usedAt) || '-' }}</a-descriptions-item>
<a-descriptions-item label="使用前余额">
¥{{ formatAmount(currentRecord.balanceBefore) }}
</a-descriptions-item>
<a-descriptions-item label="使用后余额">
¥{{ formatAmount(currentRecord.balanceAfter) }}
</a-descriptions-item>
<a-descriptions-item label="创建时间">{{ formatDate(currentRecord.createTime) }}</a-descriptions-item>
</a-descriptions>
</a-modal>
</template>
<script lang="ts" setup>
import { ref, reactive, computed, onMounted } from 'vue';
import { message } from 'ant-design-vue';
import {
PlusOutlined,
ReloadOutlined,
SearchOutlined,
ClearOutlined,
EyeOutlined,
CopyOutlined,
DeleteOutlined,
ArrowRightOutlined,
ThunderboltOutlined
} from '@ant-design/icons-vue';
import { toDateString } from 'ele-admin-pro';
import { pageRechargeCode, generateRechargeCode, batchGenerateRechargeCode, removeRechargeCode } from '@/api/shop/shopRechargeCode';
import type { ShopRechargeCode } from '@/api/shop/shopRechargeCode/model';
import RechargeRecord from '@/views/shop/shopRechargeRecord/index.vue';
const TABLE_COLUMN_COLORS = {
unused: '#52c41a',
used: '#faad14',
expired: '#ff4d4f'
};
// Tab切换
const activeTab = ref('codes');
const loadedTabs = ref<Set<string>>(new Set(['codes']));
function handleTabChange(tab: string) {
if (!loadedTabs.value.has(tab)) {
loadedTabs.value.add(tab);
}
}
// 搜索表单
const searchForm = reactive({
code: '',
status: undefined as number | undefined,
batchNo: ''
});
// 表格引用
const tableRef = ref(null);
// 生成弹窗相关
const generateModalVisible = ref(false);
const generateFormRef = ref(null);
const generateForm = reactive({
amount: 10,
quantity: 1,
expireDays: 30
});
const generateLoading = ref(false);
// 批量生成弹窗相关
const batchModalVisible = ref(false);
const batchFormRef = ref(null);
const batchForm = reactive({
presetAmounts: [] as number[],
quantityPerAmount: 1,
expireDays: 30
});
const batchLoading = ref(false);
// 生成的兑换码列表弹窗
const codesModalVisible = ref(false);
const generatedCodes = ref<ShopRechargeCode[]>([]);
// 详情弹窗
const detailModalVisible = ref(false);
const currentRecord = ref<ShopRechargeCode | null>(null);
// 统计数据
const statistics = reactive({
total: 0,
unused: 0,
used: 0,
expired: 0
});
// 表格列配置
const columns = ref([
{
title: '兑换码信息',
key: 'code',
width: 250,
fixed: 'left'
},
{
title: '充值金额',
key: 'amount',
width: 120,
align: 'center'
},
{
title: '状态',
key: 'status',
width: 100,
align: 'center'
},
{
title: '生成方式',
key: 'generateType',
width: 120,
align: 'center'
},
{
title: '过期时间',
key: 'expireTime',
width: 160,
align: 'center'
},
{
title: '使用信息',
key: 'usedInfo',
width: 200,
align: 'center'
},
{
title: '创建时间',
key: 'createTime',
width: 160,
align: 'center',
sorter: true
},
{
title: '操作',
key: 'action',
width: 150,
fixed: 'right',
align: 'center'
}
]);
// 数据源
const datasource = ({ page, limit, where, orders }) => {
const params = {
...where,
...searchForm,
...orders,
page,
limit
};
return pageRechargeCode(params).then((res) => {
// 更新统计数据
if (res.list && res.list.length > 0) {
updateStatistics(res.list);
}
return res;
});
};
// 更新统计数据
const updateStatistics = (list: ShopRechargeCode[]) => {
statistics.total = list.length;
statistics.unused = list.filter((item) => item.status === 0).length;
statistics.used = list.filter((item) => item.status === 1).length;
statistics.expired = list.filter((item) => item.status === 2).length;
};
// 格式化金额
const formatAmount = (amount: string | number | undefined) => {
if (amount === null || amount === undefined) return '0.00';
return Number(amount).toFixed(2);
};
// 格式化日期
const formatDate = (dateStr: string | undefined) => {
if (!dateStr) return '-';
return toDateString(dateStr, 'yyyy-MM-dd HH:mm');
};
// 获取状态文本
const getStatusText = (status: number) => {
const map: Record<number, string> = { 0: '未使用', 1: '已使用', 2: '已过期' };
return map[status] || '未知';
};
// 获取状态颜色
const getStatusColor = (status: number) => {
const map: Record<number, string> = { 0: 'green', 1: 'orange', 2: 'red' };
return map[status] || 'default';
};
// 搜索
const handleSearch = () => {
tableRef.value?.reload();
};
// 重置
const handleReset = () => {
searchForm.code = '';
searchForm.status = undefined;
searchForm.batchNo = '';
tableRef.value?.reload();
};
// 刷新
const reload = () => {
tableRef.value?.reload();
};
// 打开生成弹窗
const openGenerateModal = () => {
generateForm.amount = 10;
generateForm.quantity = 1;
generateForm.expireDays = 30;
generateModalVisible.value = true;
};
// 打开批量生成弹窗
const openBatchModal = () => {
batchForm.presetAmounts = [];
batchForm.quantityPerAmount = 1;
batchForm.expireDays = 30;
batchModalVisible.value = true;
};
// 生成兑换码
const handleGenerate = async () => {
try {
await generateFormRef.value.validate();
generateLoading.value = true;
const res = await generateRechargeCode(
generateForm.amount,
generateForm.quantity,
generateForm.expireDays
);
generatedCodes.value = res || [];
generateModalVisible.value = false;
codesModalVisible.value = true;
message.success(`成功生成 ${generatedCodes.value.length} 个兑换码`);
tableRef.value?.reload();
} catch (error) {
console.error(error);
} finally {
generateLoading.value = false;
}
};
// 批量生成兑换码
const handleBatchGenerate = async () => {
try {
if (!batchForm.presetAmounts || batchForm.presetAmounts.length === 0) {
message.warning('请输入至少一个预设金额');
return;
}
batchLoading.value = true;
const res = await batchGenerateRechargeCode({
presetAmounts: batchForm.presetAmounts.map(Number),
quantityPerAmount: batchForm.quantityPerAmount,
expireDays: batchForm.expireDays
});
generatedCodes.value = res?.codes || [];
batchModalVisible.value = false;
codesModalVisible.value = true;
message.success(`成功生成 ${generatedCodes.value.length} 个兑换码`);
tableRef.value?.reload();
} catch (error) {
console.error(error);
} finally {
batchLoading.value = false;
}
};
// 复制兑换码
const copyCode = (code: string) => {
navigator.clipboard.writeText(code).then(() => {
message.success('兑换码已复制');
});
};
// 复制所有兑换码
const copyAllCodes = () => {
const codes = generatedCodes.value.map((item) => item.code).join('\n');
navigator.clipboard.writeText(codes).then(() => {
message.success('所有兑换码已复制');
});
};
// 查看详情
const viewDetail = (record: ShopRechargeCode) => {
currentRecord.value = record;
detailModalVisible.value = true;
};
// 删除
const handleDelete = async (id: number) => {
try {
await removeRechargeCode(id);
message.success('删除成功');
tableRef.value?.reload();
} catch (error) {
console.error(error);
}
};
// 获取页面标题
const getPageTitle = () => {
return '充值兑换码管理';
};
onMounted(() => {
// 初始加载
});
</script>
<style scoped>
.search-container {
margin-bottom: 16px;
}
.code-info {
display: flex;
flex-direction: column;
}
.batch-no {
font-size: 12px;
color: #999;
margin-top: 4px;
}
.amount-value {
font-weight: bold;
color: #f5222d;
font-size: 16px;
}
.balance-change {
display: flex;
align-items: center;
gap: 8px;
margin-top: 4px;
}
.balance-before {
color: #999;
text-decoration: line-through;
}
.balance-after {
color: #52c41a;
font-weight: bold;
}
</style>