yumi-admin/apps/src/views/resident-activity/voice-room-red-packet.vue
2026-05-08 18:56:43 +08:00

495 lines
14 KiB
Vue

<script lang="ts" setup>
import { computed, reactive, ref, watch } from 'vue';
import { Page } from '@vben/common-ui';
import { useAccessStore } from '@vben/stores';
import {
Button,
Card,
Input,
InputNumber,
Modal,
Select,
Space,
Spin,
Switch,
Table,
Tabs,
TabPane,
Tag,
message,
} from 'antdv-next';
import {
getVoiceRoomRedPacketConfig,
pageVoiceRoomRedPacketRecords,
retryVoiceRoomRedPacketRefund,
saveVoiceRoomRedPacketConfig,
} from '#/api/legacy/resident-activity';
import { getAllowedSysOrigins } from '#/views/system/shared';
defineOptions({ name: 'ResidentVoiceRoomRedPacketPage' });
const accessStore = useAccessStore();
const sysOriginOptions = computed(() => {
const options = getAllowedSysOrigins(accessStore.accessCodes || []);
return options.length > 0 ? options : getAllowedSysOrigins([]);
});
const activeTab = ref('config');
const loading = ref(false);
const recordLoading = ref(false);
const saving = ref(false);
const selectedSysOrigin = ref('');
const form = reactive<Record<string, any>>({
amountOptionsText: '15000,30000,70000,100000',
configured: false,
countOptionsText: '5,10,20,50',
dailySendLimit: 10,
delaySeconds: 300,
enabled: false,
refundAfterSeconds: 7200,
sysOrigin: '',
timezone: 'Asia/Riyadh',
updateTime: '',
});
const recordQuery = reactive<Record<string, any>>({
cursor: 1,
limit: 20,
status: '',
userId: '',
});
const recordPage = reactive<Record<string, any>>({
records: [],
total: 0,
});
const statusOptions = [
{ label: '全部', value: '' },
{ label: '初始化', value: 'INIT' },
{ label: '生效中', value: 'ACTIVE' },
{ label: '已抢完', value: 'FINISHED' },
{ label: '已过期', value: 'EXPIRED' },
{ label: '已退款', value: 'REFUNDED' },
{ label: '退款失败', value: 'REFUND_FAILED' },
{ label: '失败', value: 'FAILED' },
];
const recordColumns = [
{ dataIndex: 'packetNo', key: 'packetNo', title: '红包号', width: 190 },
{ dataIndex: 'status', key: 'status', title: '状态', width: 110 },
{ dataIndex: 'packetMode', key: 'packetMode', title: '形式', width: 100 },
{ dataIndex: 'senderUserId', key: 'senderUserId', title: '发送人', width: 140 },
{ dataIndex: 'roomId', key: 'roomId', title: '房间ID', width: 160 },
{ dataIndex: 'amount', key: 'amount', title: '金额 / 剩余', width: 180 },
{ dataIndex: 'count', key: 'count', title: '数量 / 剩余', width: 150 },
{ dataIndex: 'refundStatus', key: 'refundStatus', title: '退款', width: 120 },
{ dataIndex: 'createTime', key: 'createTime', title: '发送时间', width: 180 },
{ dataIndex: 'actions', key: 'actions', title: '操作', width: 120 },
];
const statusMeta = computed(() => {
if (!form.configured) {
return { color: 'processing', text: '未配置' };
}
if (form.enabled) {
return { color: 'success', text: '已启用' };
}
return { color: 'default', text: '已关闭' };
});
function parseNumberList(value: string, fieldName: string) {
const values = String(value || '')
.split(',')
.map((item) => Number(String(item).trim()))
.filter((item) => Number.isFinite(item));
if (values.length === 0) {
message.warning(`请配置${fieldName}`);
return [];
}
if (values.some((item) => item <= 0 || !Number.isInteger(item))) {
message.warning(`${fieldName}必须是正整数,多个值用英文逗号分隔`);
return [];
}
return Array.from(new Set(values));
}
function listText(values: unknown, fallback: string) {
if (Array.isArray(values) && values.length > 0) {
return values.map((item) => String(item)).join(',');
}
return fallback;
}
function normalizeConfig(result: null | Record<string, any> | undefined) {
const value = result || {};
form.amountOptionsText = listText(value.amountOptions, '15000,30000,70000,100000');
form.configured = Boolean(value.configured);
form.countOptionsText = listText(value.countOptions, '5,10,20,50');
form.dailySendLimit = Number(value.dailySendLimit || 10);
form.delaySeconds = Number(value.delaySeconds || 300);
form.enabled = Boolean(value.enabled);
form.refundAfterSeconds = Number(value.refundAfterSeconds || 7200);
form.sysOrigin = String(value.sysOrigin || selectedSysOrigin.value || '');
form.timezone = String(value.timezone || 'Asia/Riyadh');
form.updateTime = String(value.updateTime || '');
}
async function loadConfig() {
if (!selectedSysOrigin.value) {
return;
}
loading.value = true;
try {
const result = await getVoiceRoomRedPacketConfig(selectedSysOrigin.value);
normalizeConfig(result);
} finally {
loading.value = false;
}
}
async function submitForm() {
const amountOptions = parseNumberList(form.amountOptionsText, '红包金额');
const countOptions = parseNumberList(form.countOptionsText, '红包数量');
if (amountOptions.length === 0 || countOptions.length === 0) {
return;
}
if (Number(form.refundAfterSeconds || 0) <= Number(form.delaySeconds || 0)) {
message.warning('退款时间必须大于延时领取时间');
return;
}
saving.value = true;
try {
await saveVoiceRoomRedPacketConfig({
amountOptions,
countOptions,
dailySendLimit: Number(form.dailySendLimit || 10),
delaySeconds: Number(form.delaySeconds || 300),
enabled: Boolean(form.enabled),
refundAfterSeconds: Number(form.refundAfterSeconds || 7200),
sysOrigin: selectedSysOrigin.value,
timezone: String(form.timezone || '').trim() || 'Asia/Riyadh',
});
message.success('保存成功');
await loadConfig();
} finally {
saving.value = false;
}
}
function confirmSave() {
Modal.confirm({
async onOk() {
await submitForm();
},
title: '确认保存当前系统的语音房红包配置?',
});
}
async function loadRecords(reset = false) {
if (!selectedSysOrigin.value) {
return;
}
if (reset) {
recordQuery.cursor = 1;
}
recordLoading.value = true;
try {
const result = await pageVoiceRoomRedPacketRecords({
...recordQuery,
sysOrigin: selectedSysOrigin.value,
});
recordPage.records = Array.isArray(result.records) ? result.records : [];
recordPage.total = Number(result.total || 0);
} finally {
recordLoading.value = false;
}
}
function searchRecords() {
void loadRecords(true);
}
function handleRecordTableChange(pagination: Record<string, any>) {
recordQuery.cursor = Number(pagination.current || 1);
recordQuery.limit = Number(pagination.pageSize || 20);
void loadRecords();
}
async function handleRetryRefund(record: Record<string, any>) {
const packetNo = String(record.packetNo || '');
if (!packetNo) {
return;
}
await retryVoiceRoomRedPacketRefund(packetNo);
message.success('已提交重试');
await loadRecords();
}
function statusColor(status: string) {
switch (String(status || '').toUpperCase()) {
case 'ACTIVE':
return 'success';
case 'FINISHED':
case 'REFUNDED':
return 'blue';
case 'EXPIRED':
return 'warning';
case 'FAILED':
case 'REFUND_FAILED':
return 'error';
default:
return 'default';
}
}
function refundColor(status: string) {
switch (String(status || '').toUpperCase()) {
case 'SUCCESS':
return 'success';
case 'FAILED':
return 'error';
case 'PENDING':
return 'processing';
default:
return 'default';
}
}
watch(
sysOriginOptions,
(options) => {
if (!selectedSysOrigin.value) {
selectedSysOrigin.value = String(options[0]?.value || '');
}
},
{ immediate: true },
);
watch(
selectedSysOrigin,
(value) => {
if (value) {
void loadConfig();
if (activeTab.value === 'records') {
void loadRecords(true);
}
}
},
{ immediate: true },
);
watch(activeTab, (value) => {
if (value === 'records') {
void loadRecords(true);
}
});
</script>
<template>
<Page title="红包">
<div class="voice-room-red-packet-page">
<Card>
<Space wrap>
<span>系统</span>
<Select
v-model:value="selectedSysOrigin"
:options="sysOriginOptions"
style="width: 180px"
/>
<Tag :color="statusMeta.color">{{ statusMeta.text }}</Tag>
<span v-if="form.updateTime" class="muted-text">
更新时间:{{ form.updateTime }}
</span>
</Space>
</Card>
<Tabs v-model:active-key="activeTab">
<TabPane key="config" tab="配置">
<Spin :spinning="loading">
<Card>
<div class="config-grid">
<label class="field-row">
<span>启用红包</span>
<Switch v-model:checked="form.enabled" />
</label>
<label class="field-row">
<span>每日发送次数</span>
<InputNumber
v-model:value="form.dailySendLimit"
:min="1"
:precision="0"
class="field-control"
/>
</label>
<label class="field-row">
<span>延时领取秒数</span>
<InputNumber
v-model:value="form.delaySeconds"
:min="0"
:precision="0"
class="field-control"
/>
</label>
<label class="field-row">
<span>退款等待秒数</span>
<InputNumber
v-model:value="form.refundAfterSeconds"
:min="1"
:precision="0"
class="field-control"
/>
</label>
<label class="field-row">
<span>活动时区</span>
<Input v-model:value="form.timezone" class="field-control" />
</label>
<label class="field-row field-wide">
<span>红包金额</span>
<Input
v-model:value="form.amountOptionsText"
class="field-control"
placeholder="15000,30000,70000,100000"
/>
</label>
<label class="field-row field-wide">
<span>红包数量</span>
<Input
v-model:value="form.countOptionsText"
class="field-control"
placeholder="5,10,20,50"
/>
</label>
</div>
<div class="actions">
<Space>
<Button @click="loadConfig">刷新</Button>
<Button type="primary" :loading="saving" @click="confirmSave">
保存配置
</Button>
</Space>
</div>
</Card>
</Spin>
</TabPane>
<TabPane key="records" tab="发包记录">
<Card>
<Space wrap class="record-toolbar">
<Select
v-model:value="recordQuery.status"
:options="statusOptions"
style="width: 140px"
/>
<Input
v-model:value="recordQuery.userId"
allow-clear
placeholder="发送人ID"
style="width: 180px"
/>
<Button type="primary" @click="searchRecords">查询</Button>
<Button @click="loadRecords()">刷新</Button>
</Space>
<Table
:columns="recordColumns"
:data-source="recordPage.records"
:loading="recordLoading"
:pagination="{
current: recordQuery.cursor,
pageSize: recordQuery.limit,
showSizeChanger: true,
total: recordPage.total,
}"
:scroll="{ x: 1360 }"
row-key="packetNo"
@change="handleRecordTableChange"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'status'">
<Tag :color="statusColor(record.status)">
{{ record.status || '-' }}
</Tag>
</template>
<template v-else-if="column.key === 'amount'">
{{ record.totalAmount || 0 }} / {{ record.remainAmount || 0 }}
</template>
<template v-else-if="column.key === 'count'">
{{ record.totalCount || 0 }} / {{ record.remainCount || 0 }}
</template>
<template v-else-if="column.key === 'refundStatus'">
<Tag :color="refundColor(record.refundStatus)">
{{ record.refundStatus || 'NONE' }}
</Tag>
</template>
<template v-else-if="column.key === 'actions'">
<Button
v-if="record.refundStatus === 'FAILED'"
size="small"
@click="handleRetryRefund(record)"
>
重试退款
</Button>
</template>
</template>
</Table>
</Card>
</TabPane>
</Tabs>
</div>
</Page>
</template>
<style scoped>
.voice-room-red-packet-page {
display: flex;
flex-direction: column;
gap: 16px;
}
.muted-text {
color: rgb(0 0 0 / 45%);
font-size: 13px;
}
.config-grid {
display: grid;
grid-template-columns: repeat(2, minmax(260px, 1fr));
gap: 18px 24px;
max-width: 920px;
}
.field-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
}
.field-row > span {
flex: 0 0 120px;
color: rgb(0 0 0 / 65%);
}
.field-control {
flex: 1;
}
.field-wide {
grid-column: span 2;
}
.actions {
margin-top: 24px;
}
.record-toolbar {
margin-bottom: 16px;
}
</style>