607 lines
16 KiB
Vue
607 lines
16 KiB
Vue
<script lang="ts" setup>
|
|
import { computed, reactive, ref, watch } from 'vue';
|
|
|
|
import { Page } from '@vben/common-ui';
|
|
|
|
import {
|
|
Button,
|
|
Card,
|
|
DatePicker,
|
|
Form,
|
|
FormItem,
|
|
InputNumber,
|
|
message,
|
|
Modal,
|
|
Pagination,
|
|
Select,
|
|
Space,
|
|
Table,
|
|
Tag,
|
|
TextArea,
|
|
} from 'antdv-next';
|
|
import dayjs from 'dayjs';
|
|
|
|
import { submitGmGoldOperation } from '#/api/legacy/gm';
|
|
import { getClsGoldRunningWater } from '#/api/legacy/operate';
|
|
import { listMembers } from '#/api/legacy/team';
|
|
import { getUserBaseInfoBySysOriginAccount } from '#/api/legacy/user';
|
|
import AccountInput from '#/components/account-input.vue';
|
|
import UserProfileLink from '#/views/operate/components/user-profile-link.vue';
|
|
import { formatDate } from '#/views/system/shared';
|
|
|
|
defineOptions({ name: 'GmGoldOperation' });
|
|
|
|
const INCOME_REASON_OPTIONS = [
|
|
{ label: '奖励', value: 1 },
|
|
{ label: '内部', value: 2 },
|
|
{ label: '工资', value: 3 },
|
|
{ label: '充值', value: 4 },
|
|
{ label: '其他', value: 5 },
|
|
];
|
|
|
|
const DEDUCT_REASON_OPTIONS = [
|
|
{ label: '违规', value: 1 },
|
|
{ label: '多发', value: 2 },
|
|
{ label: '操作错误', value: 3 },
|
|
{ label: '其他', value: 4 },
|
|
];
|
|
|
|
const TYPE_OPTIONS = [
|
|
{ label: '增加', value: 0 },
|
|
{ label: '减少', value: 1 },
|
|
];
|
|
|
|
const DEFAULT_PAGE_SIZE = 100;
|
|
|
|
const open = ref(false);
|
|
const loading = ref(false);
|
|
const loadMoreLoading = ref(false);
|
|
const submitting = ref(false);
|
|
const list = ref<Array<Record<string, any>>>([]);
|
|
const notMore = ref(false);
|
|
const currentPage = ref(1);
|
|
const rangeDate = ref<[string, string] | null>(null);
|
|
const operators = ref<Array<Record<string, any>>>([]);
|
|
|
|
const query = reactive<Record<string, any>>({
|
|
backOperationUser: undefined,
|
|
context: '',
|
|
endTime: '',
|
|
lastId: '',
|
|
limit: DEFAULT_PAGE_SIZE,
|
|
startTime: '',
|
|
type: undefined,
|
|
userId: '',
|
|
});
|
|
|
|
const form = reactive({
|
|
operationType: 0,
|
|
quantity: undefined as number | undefined,
|
|
reasonType: 1,
|
|
remarks: '',
|
|
usdQuantity: undefined as number | undefined,
|
|
userSearch: '',
|
|
});
|
|
|
|
const columns = [
|
|
{ dataIndex: 'userProfile', key: 'userProfile', title: '用户', width: 240 },
|
|
{ dataIndex: 'type', key: 'type', title: '类型', width: 100 },
|
|
{ dataIndex: 'quantity', key: 'quantity', title: '金额', width: 130 },
|
|
{ dataIndex: 'balance', key: 'balance', title: '余额', width: 130 },
|
|
{ dataIndex: 'originName', key: 'originName', title: '原因', width: 220 },
|
|
{ dataIndex: 'remark', key: 'remark', title: '备注', width: 260 },
|
|
{ dataIndex: 'backOperationName', key: 'backOperationName', title: '操作人', width: 160 },
|
|
{ dataIndex: 'createTime', key: 'createTime', title: '操作时间', width: 180 },
|
|
];
|
|
|
|
const reasonOptions = computed(() =>
|
|
form.operationType === 0 ? INCOME_REASON_OPTIONS : DEDUCT_REASON_OPTIONS,
|
|
);
|
|
|
|
const showUsdQuantity = computed(
|
|
() => form.operationType === 0 && [3, 4].includes(Number(form.reasonType)),
|
|
);
|
|
|
|
const operatorOptions = computed(() =>
|
|
operators.value.map((item) => ({
|
|
label: `${item.nickname || item.loginName || item.id} / ${item.loginName || item.id}`,
|
|
value: item.id,
|
|
})),
|
|
);
|
|
|
|
const currentPageList = computed(() => {
|
|
const pageSize = Number(query.limit || DEFAULT_PAGE_SIZE);
|
|
const startIndex = (currentPage.value - 1) * pageSize;
|
|
return list.value.slice(startIndex, startIndex + pageSize);
|
|
});
|
|
|
|
const paginationTotal = computed(() => {
|
|
const pageSize = Number(query.limit || DEFAULT_PAGE_SIZE);
|
|
if (notMore.value) {
|
|
return list.value.length;
|
|
}
|
|
return Math.max(list.value.length + pageSize, (currentPage.value + 1) * pageSize);
|
|
});
|
|
|
|
watch(
|
|
rangeDate,
|
|
(value) => {
|
|
query.startTime = value?.[0] || '';
|
|
query.endTime = value?.[1] || '';
|
|
},
|
|
{ immediate: true },
|
|
);
|
|
|
|
watch(
|
|
() => form.operationType,
|
|
() => {
|
|
form.reasonType = reasonOptions.value[0]?.value || 1;
|
|
form.usdQuantity = undefined;
|
|
},
|
|
);
|
|
|
|
watch(showUsdQuantity, (value) => {
|
|
if (!value) {
|
|
form.usdQuantity = undefined;
|
|
}
|
|
});
|
|
|
|
function createDefaultRange() {
|
|
const end = dayjs().endOf('day');
|
|
const start = dayjs().subtract(1, 'day').startOf('day');
|
|
return [String(start.valueOf()), String(end.valueOf())] as [string, string];
|
|
}
|
|
|
|
function applyRangeDate(value: [string, string] | null) {
|
|
rangeDate.value = value;
|
|
query.startTime = value?.[0] || '';
|
|
query.endTime = value?.[1] || '';
|
|
}
|
|
|
|
function resetForm() {
|
|
form.operationType = 0;
|
|
form.quantity = undefined;
|
|
form.reasonType = 1;
|
|
form.remarks = '';
|
|
form.usdQuantity = undefined;
|
|
form.userSearch = '';
|
|
}
|
|
|
|
function parseEncodedValue(value: unknown) {
|
|
const raw = String(value ?? '');
|
|
const match = raw.match(/^\*(.*)\*$/);
|
|
if (!match) {
|
|
return {
|
|
accountType: '',
|
|
content: raw,
|
|
sysOrigin: '',
|
|
};
|
|
}
|
|
const body = match[1] || '';
|
|
return {
|
|
accountType: body.match(/accountType:([^,]*)/)?.[1] || '',
|
|
content: body.match(/content:(.*)$/)?.[1] || '',
|
|
sysOrigin: body.match(/sysOrigin:([^,]*)/)?.[1] || '',
|
|
};
|
|
}
|
|
|
|
async function resolveUserId() {
|
|
const parsed = parseEncodedValue(form.userSearch);
|
|
const content = String(parsed.content || '').trim();
|
|
if (!content) {
|
|
message.warning('请输入用户ID');
|
|
return '';
|
|
}
|
|
if (!parsed.accountType || parsed.accountType === 'LONG') {
|
|
return content.replace(/[^\d]/g, '');
|
|
}
|
|
if (!parsed.sysOrigin) {
|
|
message.warning('请选择系统');
|
|
return '';
|
|
}
|
|
const result = await getUserBaseInfoBySysOriginAccount(parsed.sysOrigin, content);
|
|
const userId = result?.id || result?.userId;
|
|
if (!userId) {
|
|
message.warning('未找到用户');
|
|
return '';
|
|
}
|
|
return String(userId);
|
|
}
|
|
|
|
function validateForm() {
|
|
if (!String(form.userSearch || '').trim()) {
|
|
message.warning('请输入用户ID');
|
|
return false;
|
|
}
|
|
if (!form.quantity || Number(form.quantity) <= 0) {
|
|
message.warning('请输入大于0的金币数量');
|
|
return false;
|
|
}
|
|
if (!String(form.remarks || '').trim()) {
|
|
message.warning('请输入备注');
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function formatRunningWaterTime(value: any) {
|
|
if (typeof value === 'string') {
|
|
const trimmed = value.trim();
|
|
if (/^\d+$/.test(trimmed)) {
|
|
return formatDate(Number(trimmed));
|
|
}
|
|
}
|
|
return formatDate(value);
|
|
}
|
|
|
|
function getRunningWater(record: Record<string, any>) {
|
|
return record.runningWater || record;
|
|
}
|
|
|
|
function getRowKey(record: Record<string, any>) {
|
|
return getRunningWater(record).id || record.id;
|
|
}
|
|
|
|
function getRecordType(record: Record<string, any>) {
|
|
return Number(getRunningWater(record).type);
|
|
}
|
|
|
|
function getAmountPrefix(record: Record<string, any>) {
|
|
return getRecordType(record) === 0 ? '+' : '-';
|
|
}
|
|
|
|
function getAmountClass(record: Record<string, any>) {
|
|
return getRecordType(record) === 0 ? 'amount--income' : 'amount--expense';
|
|
}
|
|
|
|
function filterOperatorOption(input: string, option: any) {
|
|
return String(option?.label || '').toLowerCase().includes(input.toLowerCase());
|
|
}
|
|
|
|
async function loadOperators() {
|
|
operators.value = (await listMembers()) || [];
|
|
}
|
|
|
|
async function loadData(reset = false) {
|
|
if (loading.value || loadMoreLoading.value) {
|
|
return;
|
|
}
|
|
if (!query.startTime || !query.endTime) {
|
|
message.warning('请选择时间范围');
|
|
return;
|
|
}
|
|
|
|
const startYear = new Date(Number(query.startTime)).getFullYear();
|
|
const endYear = new Date(Number(query.endTime)).getFullYear();
|
|
if (startYear !== endYear) {
|
|
message.warning('不允许跨年查询');
|
|
return;
|
|
}
|
|
|
|
if (reset) {
|
|
list.value = [];
|
|
notMore.value = false;
|
|
query.context = '';
|
|
query.lastId = '';
|
|
currentPage.value = 1;
|
|
}
|
|
|
|
if (reset) {
|
|
loading.value = true;
|
|
} else {
|
|
loadMoreLoading.value = true;
|
|
}
|
|
|
|
try {
|
|
const result = await getClsGoldRunningWater({
|
|
backOperationUser: query.backOperationUser || undefined,
|
|
context: query.context || undefined,
|
|
endTime: query.endTime,
|
|
lastId: query.lastId || undefined,
|
|
limit: query.limit,
|
|
queryBackOperationUser: true,
|
|
startTime: query.startTime,
|
|
type: query.type,
|
|
userId: query.userId || undefined,
|
|
});
|
|
const waters = result?.waters || [];
|
|
const nextList = reset ? waters : [...list.value, ...waters];
|
|
list.value = nextList;
|
|
query.context = result?.context || '';
|
|
query.lastId = String(getRunningWater(nextList[nextList.length - 1] || {}).id || '');
|
|
notMore.value =
|
|
Boolean(result?.listOver) ||
|
|
(!query.context && waters.length < Number(query.limit || DEFAULT_PAGE_SIZE));
|
|
} finally {
|
|
loading.value = false;
|
|
loadMoreLoading.value = false;
|
|
}
|
|
}
|
|
|
|
async function ensurePageLoaded(page: number) {
|
|
const pageSize = Number(query.limit || DEFAULT_PAGE_SIZE);
|
|
let guard = 0;
|
|
while (!notMore.value && list.value.length < page * pageSize && guard < page) {
|
|
const beforeLength = list.value.length;
|
|
const beforeContext = query.context;
|
|
await loadData(false);
|
|
guard += 1;
|
|
if (list.value.length === beforeLength && query.context === beforeContext) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
async function handlePageChange(page: number, pageSize?: number) {
|
|
if (pageSize && pageSize !== Number(query.limit || DEFAULT_PAGE_SIZE)) {
|
|
query.limit = pageSize;
|
|
await loadData(true);
|
|
return;
|
|
}
|
|
|
|
await ensurePageLoaded(page);
|
|
if (notMore.value) {
|
|
const maxPage = Math.max(1, Math.ceil(list.value.length / Number(query.limit || DEFAULT_PAGE_SIZE)));
|
|
currentPage.value = Math.min(page, maxPage);
|
|
return;
|
|
}
|
|
currentPage.value = page;
|
|
}
|
|
|
|
async function handleSearch() {
|
|
await loadData(true);
|
|
}
|
|
|
|
async function handleSubmit() {
|
|
if (!validateForm()) {
|
|
return;
|
|
}
|
|
const userId = await resolveUserId();
|
|
if (!userId) {
|
|
return;
|
|
}
|
|
submitting.value = true;
|
|
try {
|
|
const result = await submitGmGoldOperation({
|
|
operationType: form.operationType,
|
|
quantity: form.quantity,
|
|
reasonType: form.reasonType,
|
|
remarks: form.remarks,
|
|
usdQuantity: showUsdQuantity.value ? form.usdQuantity : undefined,
|
|
userId,
|
|
});
|
|
if (result?.status === 'PENDING') {
|
|
message.success('已提交 yumiadmin 审核');
|
|
} else {
|
|
message.success('金币操作已执行');
|
|
}
|
|
await loadData(true);
|
|
open.value = false;
|
|
resetForm();
|
|
} finally {
|
|
submitting.value = false;
|
|
}
|
|
}
|
|
|
|
applyRangeDate(createDefaultRange());
|
|
void loadOperators();
|
|
void loadData(true);
|
|
</script>
|
|
|
|
<template>
|
|
<Page title="金币操作">
|
|
<Card>
|
|
<div class="page-head">
|
|
<div>
|
|
<div class="page-head__title">后台金币增减记录</div>
|
|
<div class="page-head__desc">查询所有管理员给用户发放或扣除金币的操作流水</div>
|
|
</div>
|
|
<Button type="primary" @click="open = true">金币操作</Button>
|
|
</div>
|
|
|
|
<div class="toolbar">
|
|
<DatePicker.RangePicker
|
|
v-model:value="rangeDate"
|
|
show-time
|
|
style="width: 360px"
|
|
value-format="x"
|
|
/>
|
|
<Select
|
|
v-model:value="query.type"
|
|
allow-clear
|
|
placeholder="类型"
|
|
style="width: 140px"
|
|
:options="TYPE_OPTIONS"
|
|
/>
|
|
<AccountInput
|
|
v-model:value="query.userId"
|
|
default-select-type="LONG"
|
|
placeholder="用户ID"
|
|
style="width: 240px"
|
|
/>
|
|
<Select
|
|
v-model:value="query.backOperationUser"
|
|
allow-clear
|
|
show-search
|
|
placeholder="操作人"
|
|
style="width: 220px"
|
|
:filter-option="filterOperatorOption"
|
|
:options="operatorOptions"
|
|
/>
|
|
<Button :loading="loading" type="primary" @click="handleSearch">查询</Button>
|
|
</div>
|
|
|
|
<Table
|
|
:columns="columns"
|
|
:data-source="currentPageList"
|
|
:loading="loading"
|
|
:pagination="false"
|
|
:row-key="getRowKey"
|
|
:scroll="{ x: 1420 }"
|
|
>
|
|
<template #bodyCell="{ column, record }">
|
|
<template v-if="column.key === 'userProfile'">
|
|
<UserProfileLink :profile="record.userProfile" />
|
|
</template>
|
|
<template v-else-if="column.key === 'type'">
|
|
<Tag :color="getRecordType(record) === 0 ? 'green' : 'red'">
|
|
{{ getRecordType(record) === 0 ? '增加' : '减少' }}
|
|
</Tag>
|
|
</template>
|
|
<template v-else-if="column.key === 'quantity'">
|
|
<span :class="['amount', getAmountClass(record)]">
|
|
{{ getAmountPrefix(record) }}{{ getRunningWater(record).quantity ?? 0 }}
|
|
</span>
|
|
</template>
|
|
<template v-else-if="column.key === 'balance'">
|
|
{{ getRunningWater(record).balance ?? '-' }}
|
|
</template>
|
|
<template v-else-if="column.key === 'originName'">
|
|
{{ getRunningWater(record).originName || '-' }}
|
|
</template>
|
|
<template v-else-if="column.key === 'remark'">
|
|
{{ getRunningWater(record).remark || '-' }}
|
|
</template>
|
|
<template v-else-if="column.key === 'backOperationName'">
|
|
{{ record.backOperationName || '-' }}
|
|
</template>
|
|
<template v-else-if="column.key === 'createTime'">
|
|
{{ formatRunningWaterTime(getRunningWater(record).createTime) }}
|
|
</template>
|
|
</template>
|
|
</Table>
|
|
|
|
<div v-if="list.length > 0" class="pagination">
|
|
<Pagination
|
|
:current="currentPage"
|
|
:disabled="loading || loadMoreLoading"
|
|
:page-size="query.limit"
|
|
:page-size-options="['20', '50', '100', '200']"
|
|
:show-size-changer="true"
|
|
:total="paginationTotal"
|
|
show-less-items
|
|
@change="handlePageChange"
|
|
@showSizeChange="handlePageChange"
|
|
/>
|
|
</div>
|
|
</Card>
|
|
|
|
<Modal
|
|
v-model:open="open"
|
|
title="金币操作"
|
|
:confirm-loading="submitting"
|
|
width="560px"
|
|
@cancel="resetForm"
|
|
@ok="handleSubmit"
|
|
>
|
|
<Form layout="vertical">
|
|
<FormItem label="用户ID">
|
|
<AccountInput
|
|
v-model:value="form.userSearch"
|
|
default-select-type="LONG"
|
|
placeholder="请输入长ID或短ID"
|
|
/>
|
|
</FormItem>
|
|
<FormItem label="操作类型">
|
|
<Select
|
|
v-model:value="form.operationType"
|
|
:options="[
|
|
{ label: '增加金币', value: 0 },
|
|
{ label: '扣除金币', value: 1 },
|
|
]"
|
|
/>
|
|
</FormItem>
|
|
<Space class="form-row" :size="12">
|
|
<FormItem class="form-row__item" label="金币数量">
|
|
<InputNumber
|
|
v-model:value="form.quantity"
|
|
:min="0.01"
|
|
:precision="2"
|
|
class="full-input"
|
|
/>
|
|
</FormItem>
|
|
<FormItem class="form-row__item" label="原因">
|
|
<Select v-model:value="form.reasonType" :options="reasonOptions" />
|
|
</FormItem>
|
|
</Space>
|
|
<FormItem v-if="showUsdQuantity" label="折算USD金额">
|
|
<InputNumber
|
|
v-model:value="form.usdQuantity"
|
|
:min="0"
|
|
:precision="2"
|
|
class="full-input"
|
|
/>
|
|
</FormItem>
|
|
<FormItem label="备注">
|
|
<TextArea
|
|
v-model:value="form.remarks"
|
|
:maxlength="500"
|
|
:rows="4"
|
|
show-count
|
|
placeholder="请输入操作原因和备注"
|
|
/>
|
|
</FormItem>
|
|
</Form>
|
|
</Modal>
|
|
</Page>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.page-head {
|
|
align-items: center;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.page-head__title {
|
|
color: #111827;
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.page-head__desc {
|
|
color: #6b7280;
|
|
font-size: 13px;
|
|
margin-top: 4px;
|
|
}
|
|
|
|
.toolbar {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 12px;
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.amount {
|
|
font-weight: 600;
|
|
}
|
|
|
|
.amount--income {
|
|
color: #16a34a;
|
|
}
|
|
|
|
.amount--expense {
|
|
color: #dc2626;
|
|
}
|
|
|
|
.pagination {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
margin-top: 16px;
|
|
}
|
|
|
|
.form-row {
|
|
display: flex;
|
|
width: 100%;
|
|
}
|
|
|
|
.form-row__item {
|
|
flex: 1;
|
|
}
|
|
|
|
.full-input {
|
|
width: 100%;
|
|
}
|
|
</style>
|