金币权限
This commit is contained in:
parent
a38e98e29e
commit
9dbb25287d
23
apps/src/api/legacy/gm.ts
Normal file
23
apps/src/api/legacy/gm.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import type { LegacyPageResult } from '#/api/legacy/system';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export async function submitGmGoldOperation(data: Record<string, any>) {
|
||||
return requestClient.post<Record<string, any>>('/gm/gold-operation', data);
|
||||
}
|
||||
|
||||
export async function pageGmGoldOperation(params: Record<string, any>) {
|
||||
return requestClient.get<LegacyPageResult<Record<string, any>>>(
|
||||
'/gm/gold-operation/page',
|
||||
{
|
||||
params,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export async function reviewGmGoldOperation(data: Record<string, any>) {
|
||||
return requestClient.post<Record<string, any>>(
|
||||
'/gm/gold-operation/review',
|
||||
data,
|
||||
);
|
||||
}
|
||||
@ -106,6 +106,16 @@ export async function updatePayFactory(data: Record<string, any>) {
|
||||
return requestClient.post('/sys-pay-factory/update', data);
|
||||
}
|
||||
|
||||
export async function getPayFactoryConfig(params: Record<string, any>) {
|
||||
return requestClient.get<Record<string, any>>('/sys-pay-factory/config', {
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
export async function savePayFactoryConfig(data: Record<string, any>) {
|
||||
return requestClient.post('/sys-pay-factory/config/save', data);
|
||||
}
|
||||
|
||||
export async function addPayFactoryAssociatedChannels(
|
||||
data: Array<Record<string, any>>,
|
||||
) {
|
||||
|
||||
30
apps/src/router/routes/modules/gm.ts
Normal file
30
apps/src/router/routes/modules/gm.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import type { RouteRecordRaw } from 'vue-router';
|
||||
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
meta: {
|
||||
icon: 'lucide:shield',
|
||||
order: 49.4,
|
||||
title: 'GM操作',
|
||||
},
|
||||
name: 'GmOperation',
|
||||
path: '/gm/operation',
|
||||
redirect: '/gm/operation/gold-operation',
|
||||
children: [
|
||||
{
|
||||
name: 'GmGoldOperation',
|
||||
path: 'gold-operation',
|
||||
component: () => import('#/views/gm/gold-operation.vue'),
|
||||
meta: { title: '金币操作' },
|
||||
},
|
||||
{
|
||||
name: 'GmGoldOperationReview',
|
||||
path: 'gold-operation-review',
|
||||
component: () => import('#/views/gm/gold-operation-review.vue'),
|
||||
meta: { title: '金币操作审核' },
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default routes;
|
||||
305
apps/src/views/gm/gold-operation-review.vue
Normal file
305
apps/src/views/gm/gold-operation-review.vue
Normal file
@ -0,0 +1,305 @@
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref, watch } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
DatePicker,
|
||||
Input,
|
||||
message,
|
||||
Modal,
|
||||
Pagination,
|
||||
Select,
|
||||
Space,
|
||||
Table,
|
||||
Tag,
|
||||
} from 'antdv-next';
|
||||
|
||||
import {
|
||||
pageGmGoldOperation,
|
||||
reviewGmGoldOperation,
|
||||
} from '#/api/legacy/gm';
|
||||
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,
|
||||
getDisplaySysOriginText,
|
||||
} from '#/views/system/shared';
|
||||
|
||||
defineOptions({ name: 'GmGoldOperationReview' });
|
||||
|
||||
const loading = ref(false);
|
||||
const reviewing = ref(false);
|
||||
const list = ref<Array<Record<string, any>>>([]);
|
||||
const total = ref(0);
|
||||
const rangeDate = ref<[string, string] | null>(null);
|
||||
const userSearch = ref('');
|
||||
|
||||
const query = reactive({
|
||||
applicantId: '',
|
||||
endTime: '',
|
||||
operationType: undefined as number | undefined,
|
||||
pageIndex: 1,
|
||||
pageSize: 20,
|
||||
startTime: '',
|
||||
status: 'PENDING',
|
||||
userId: '',
|
||||
});
|
||||
|
||||
const columns = [
|
||||
{ dataIndex: 'userProfile', key: 'userProfile', title: '用户', width: 240 },
|
||||
{ dataIndex: 'sysOrigin', key: 'sysOrigin', title: '系统', width: 100 },
|
||||
{ dataIndex: 'operationType', key: 'operationType', title: '类型', width: 100 },
|
||||
{ dataIndex: 'quantity', key: 'quantity', title: '金币数量', width: 120 },
|
||||
{ dataIndex: 'reasonName', key: 'reasonName', title: '原因', width: 120 },
|
||||
{ dataIndex: 'remarks', key: 'remarks', title: '备注', width: 220 },
|
||||
{ dataIndex: 'applicant', key: 'applicant', title: '申请人', width: 160 },
|
||||
{ dataIndex: 'status', key: 'status', title: '状态', width: 110 },
|
||||
{ dataIndex: 'reviewer', key: 'reviewer', title: '审核人', width: 160 },
|
||||
{ dataIndex: 'time', key: 'time', title: '时间', width: 240 },
|
||||
{ dataIndex: 'actions', key: 'actions', title: '操作', width: 160 },
|
||||
];
|
||||
|
||||
watch(rangeDate, (value) => {
|
||||
query.startTime = value?.[0] || '';
|
||||
query.endTime = value?.[1] || '';
|
||||
});
|
||||
|
||||
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(userSearch.value);
|
||||
const content = String(parsed.content || '').trim();
|
||||
if (!content) {
|
||||
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);
|
||||
}
|
||||
|
||||
async function loadData(reset = false) {
|
||||
if (reset) {
|
||||
query.pageIndex = 1;
|
||||
}
|
||||
loading.value = true;
|
||||
try {
|
||||
query.userId = await resolveUserId();
|
||||
const result = await pageGmGoldOperation({
|
||||
...query,
|
||||
applicantId: query.applicantId || undefined,
|
||||
operationType: query.operationType,
|
||||
userId: query.userId || undefined,
|
||||
});
|
||||
list.value = result.records || [];
|
||||
total.value = result.total || 0;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handlePageChange(page: number, pageSize: number) {
|
||||
query.pageIndex = page;
|
||||
query.pageSize = pageSize;
|
||||
void loadData();
|
||||
}
|
||||
|
||||
function statusColor(status?: string) {
|
||||
if (status === 'APPROVED') {
|
||||
return 'success';
|
||||
}
|
||||
if (status === 'REJECTED') {
|
||||
return 'error';
|
||||
}
|
||||
return 'warning';
|
||||
}
|
||||
|
||||
function handleReview(record: Record<string, any>, approved: boolean) {
|
||||
Modal.confirm({
|
||||
title: approved ? '确认通过该金币操作?' : '确认拒绝该金币操作?',
|
||||
okText: approved ? '通过' : '拒绝',
|
||||
okType: approved ? 'primary' : 'danger',
|
||||
async onOk() {
|
||||
reviewing.value = true;
|
||||
try {
|
||||
await reviewGmGoldOperation({
|
||||
approved,
|
||||
id: record.id,
|
||||
reviewRemark: approved ? '同意' : '拒绝',
|
||||
});
|
||||
message.success(approved ? '已通过并执行金币操作' : '已拒绝');
|
||||
await loadData();
|
||||
} finally {
|
||||
reviewing.value = false;
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
void loadData(true);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page title="金币操作审核">
|
||||
<Card>
|
||||
<div class="toolbar">
|
||||
<Select
|
||||
v-model:value="query.status"
|
||||
allow-clear
|
||||
placeholder="状态"
|
||||
style="width: 140px"
|
||||
:options="[
|
||||
{ label: '待审核', value: 'PENDING' },
|
||||
{ label: '已通过', value: 'APPROVED' },
|
||||
{ label: '已拒绝', value: 'REJECTED' },
|
||||
]"
|
||||
/>
|
||||
<Select
|
||||
v-model:value="query.operationType"
|
||||
allow-clear
|
||||
placeholder="操作类型"
|
||||
style="width: 140px"
|
||||
:options="[
|
||||
{ label: '增加金币', value: 0 },
|
||||
{ label: '扣除金币', value: 1 },
|
||||
]"
|
||||
/>
|
||||
<AccountInput
|
||||
v-model:value="userSearch"
|
||||
default-select-type="LONG"
|
||||
placeholder="用户ID"
|
||||
style="width: 260px"
|
||||
/>
|
||||
<Input
|
||||
v-model:value="query.applicantId"
|
||||
allow-clear
|
||||
placeholder="申请人后台ID"
|
||||
style="width: 160px"
|
||||
/>
|
||||
<DatePicker.RangePicker
|
||||
v-model:value="rangeDate"
|
||||
show-time
|
||||
style="width: 360px"
|
||||
value-format="x"
|
||||
/>
|
||||
<Button :loading="loading" type="primary" @click="loadData(true)">
|
||||
搜索
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Table
|
||||
:columns="columns"
|
||||
:data-source="list"
|
||||
:loading="loading || reviewing"
|
||||
:pagination="false"
|
||||
row-key="id"
|
||||
:scroll="{ x: 1650 }"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'userProfile'">
|
||||
<UserProfileLink :profile="record.userProfile" />
|
||||
</template>
|
||||
<template v-else-if="column.key === 'sysOrigin'">
|
||||
{{ getDisplaySysOriginText(record.sysOrigin) }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'operationType'">
|
||||
<Tag :color="record.operationType === 0 ? 'red' : 'default'">
|
||||
{{ record.operationName }}
|
||||
</Tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'status'">
|
||||
<Tag :color="statusColor(record.status)">
|
||||
{{ record.statusName }}
|
||||
</Tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'applicant'">
|
||||
<div>{{ record.applicantName || '-' }}</div>
|
||||
<div class="muted">{{ record.applicantId || '-' }}</div>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'reviewer'">
|
||||
<div>{{ record.reviewerName || '-' }}</div>
|
||||
<div class="muted">{{ record.reviewerId || '-' }}</div>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'time'">
|
||||
<div>申请:{{ formatDate(record.createTime) }}</div>
|
||||
<div>审核:{{ formatDate(record.reviewTime) }}</div>
|
||||
<div>执行:{{ formatDate(record.executedTime) }}</div>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'actions'">
|
||||
<Space v-if="record.status === 'PENDING'">
|
||||
<Button size="small" type="link" @click="handleReview(record, true)">
|
||||
通过
|
||||
</Button>
|
||||
<Button danger size="small" type="link" @click="handleReview(record, false)">
|
||||
拒绝
|
||||
</Button>
|
||||
</Space>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</template>
|
||||
</Table>
|
||||
|
||||
<div class="pager">
|
||||
<Pagination
|
||||
:current="query.pageIndex"
|
||||
:page-size="query.pageSize"
|
||||
:total="total"
|
||||
show-size-changer
|
||||
@change="handlePageChange"
|
||||
@showSizeChange="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.toolbar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.pager {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.muted {
|
||||
color: #8c8c8c;
|
||||
font-size: 12px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
</style>
|
||||
590
apps/src/views/gm/gold-operation.vue
Normal file
590
apps/src/views/gm/gold-operation.vue
Normal file
@ -0,0 +1,590 @@
|
||||
<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: '',
|
||||
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 = '';
|
||||
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,
|
||||
limit: query.limit,
|
||||
queryBackOperationUser: true,
|
||||
startTime: query.startTime,
|
||||
type: query.type,
|
||||
userId: query.userId || undefined,
|
||||
});
|
||||
const waters = result?.waters || [];
|
||||
list.value = reset ? waters : [...list.value, ...waters];
|
||||
query.context = result?.context || '';
|
||||
notMore.value = Boolean(result?.listOver) || !query.context;
|
||||
} 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) {
|
||||
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"
|
||||
:total="paginationTotal"
|
||||
show-less-items
|
||||
@change="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>
|
||||
231
apps/src/views/operate/components/pay-factory-config-modal.vue
Normal file
231
apps/src/views/operate/components/pay-factory-config-modal.vue
Normal file
@ -0,0 +1,231 @@
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref, watch } from 'vue';
|
||||
|
||||
import SysOriginSelect from '#/components/sys-origin-select.vue';
|
||||
import {
|
||||
getPayFactoryConfig,
|
||||
savePayFactoryConfig,
|
||||
} from '#/api/legacy/pay';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Form,
|
||||
FormItem,
|
||||
Input,
|
||||
Modal,
|
||||
Select,
|
||||
Switch,
|
||||
TextArea,
|
||||
message,
|
||||
} from 'antdv-next';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
open: boolean;
|
||||
row?: Record<string, any>;
|
||||
}>(),
|
||||
{
|
||||
row: () => ({}),
|
||||
},
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
close: [];
|
||||
success: [];
|
||||
}>();
|
||||
|
||||
const loading = ref(false);
|
||||
const saving = ref(false);
|
||||
const form = reactive<Record<string, any>>({
|
||||
enabled: true,
|
||||
env: 'PROD',
|
||||
factoryCode: '',
|
||||
gatewayBaseUrl: '',
|
||||
id: '',
|
||||
merAccount: '',
|
||||
merNo: '',
|
||||
notifyUrl: '',
|
||||
platformRsaPublicKey: '',
|
||||
rsaPrivateKey: '',
|
||||
sysOrigin: '',
|
||||
});
|
||||
|
||||
const envOptions = [
|
||||
{ label: 'PROD', value: 'PROD' },
|
||||
{ label: 'TEST', value: 'TEST' },
|
||||
{ label: '通用', value: '' },
|
||||
];
|
||||
|
||||
watch(
|
||||
() => props.open,
|
||||
(open) => {
|
||||
if (!open) {
|
||||
return;
|
||||
}
|
||||
const row = props.row || {};
|
||||
Object.assign(form, {
|
||||
enabled: true,
|
||||
env: 'PROD',
|
||||
factoryCode: row.factoryCode || '',
|
||||
gatewayBaseUrl: '',
|
||||
id: '',
|
||||
merAccount: '',
|
||||
merNo: '',
|
||||
notifyUrl: '',
|
||||
platformRsaPublicKey: '',
|
||||
rsaPrivateKey: '',
|
||||
sysOrigin: '',
|
||||
});
|
||||
if (form.factoryCode) {
|
||||
void loadConfig();
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
function resetConfigFields() {
|
||||
Object.assign(form, {
|
||||
enabled: true,
|
||||
gatewayBaseUrl: '',
|
||||
id: '',
|
||||
merAccount: '',
|
||||
merNo: '',
|
||||
notifyUrl: '',
|
||||
platformRsaPublicKey: '',
|
||||
rsaPrivateKey: '',
|
||||
});
|
||||
}
|
||||
|
||||
async function loadConfig() {
|
||||
if (!form.factoryCode) {
|
||||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
try {
|
||||
const scope = {
|
||||
env: form.env || '',
|
||||
factoryCode: form.factoryCode,
|
||||
sysOrigin: form.sysOrigin || '',
|
||||
};
|
||||
const result = await getPayFactoryConfig(scope);
|
||||
resetConfigFields();
|
||||
Object.assign(form, scope, result || {});
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function validateForm() {
|
||||
const requiredFields: Array<[string, string]> = [
|
||||
['merAccount', '请输入商户标识'],
|
||||
['merNo', '请输入商户编号'],
|
||||
['gatewayBaseUrl', '请输入网关地址'],
|
||||
['rsaPrivateKey', '请输入商户私钥'],
|
||||
['platformRsaPublicKey', '请输入平台公钥'],
|
||||
['notifyUrl', '请输入回调地址'],
|
||||
];
|
||||
for (const [key, text] of requiredFields) {
|
||||
if (!String(form[key] || '').trim()) {
|
||||
message.warning(text);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!validateForm()) {
|
||||
return;
|
||||
}
|
||||
saving.value = true;
|
||||
try {
|
||||
await savePayFactoryConfig({ ...form });
|
||||
message.success('保存成功');
|
||||
emit('success');
|
||||
emit('close');
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal
|
||||
:confirm-loading="saving"
|
||||
:open="open"
|
||||
destroy-on-close
|
||||
title="MiFaPay 配置"
|
||||
width="760px"
|
||||
@cancel="emit('close')"
|
||||
@ok="handleSubmit"
|
||||
>
|
||||
<Form layout="vertical">
|
||||
<div class="scope-row">
|
||||
<FormItem label="厂商Code">
|
||||
<Input v-model:value="form.factoryCode" disabled />
|
||||
</FormItem>
|
||||
<FormItem label="环境">
|
||||
<Select
|
||||
v-model:value="form.env"
|
||||
:options="envOptions"
|
||||
@change="loadConfig"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="来源系统">
|
||||
<SysOriginSelect
|
||||
v-model:value="form.sysOrigin"
|
||||
allow-clear
|
||||
placeholder="通用"
|
||||
@change="loadConfig"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label=" ">
|
||||
<Button :loading="loading" @click="loadConfig">读取配置</Button>
|
||||
</FormItem>
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
<FormItem label="商户标识 merAccount">
|
||||
<Input v-model:value="form.merAccount" />
|
||||
</FormItem>
|
||||
<FormItem label="商户编号 merNo">
|
||||
<Input v-model:value="form.merNo" />
|
||||
</FormItem>
|
||||
</div>
|
||||
|
||||
<FormItem label="网关地址 gatewayBaseUrl">
|
||||
<Input v-model:value="form.gatewayBaseUrl" />
|
||||
</FormItem>
|
||||
|
||||
<FormItem label="商户RSA私钥">
|
||||
<TextArea v-model:value="form.rsaPrivateKey" :rows="5" />
|
||||
</FormItem>
|
||||
|
||||
<FormItem label="平台RSA公钥">
|
||||
<TextArea v-model:value="form.platformRsaPublicKey" :rows="5" />
|
||||
</FormItem>
|
||||
|
||||
<FormItem label="回调地址 notifyUrl">
|
||||
<Input v-model:value="form.notifyUrl" />
|
||||
</FormItem>
|
||||
|
||||
<FormItem label="启用">
|
||||
<Switch v-model:checked="form.enabled" />
|
||||
</FormItem>
|
||||
</Form>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.grid {
|
||||
column-gap: 12px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.scope-row {
|
||||
column-gap: 12px;
|
||||
display: grid;
|
||||
grid-template-columns: 180px 130px minmax(180px, 1fr) 96px;
|
||||
}
|
||||
</style>
|
||||
@ -16,6 +16,7 @@ import {
|
||||
} from 'antdv-next';
|
||||
|
||||
import PayFactoryAssociatedModal from './components/pay-factory-associated-modal.vue';
|
||||
import PayFactoryConfigModal from './components/pay-factory-config-modal.vue';
|
||||
import PayFactoryEditModal from './components/pay-factory-edit-modal.vue';
|
||||
|
||||
defineOptions({ name: 'OperatePayFactory' });
|
||||
@ -25,6 +26,7 @@ const total = ref(0);
|
||||
const list = ref<Array<Record<string, any>>>([]);
|
||||
const formOpen = ref(false);
|
||||
const associatedOpen = ref(false);
|
||||
const configOpen = ref(false);
|
||||
const activeRow = ref<Record<string, any>>({});
|
||||
|
||||
const query = reactive<Record<string, any>>({
|
||||
@ -39,7 +41,7 @@ const columns: any[] = [
|
||||
{ dataIndex: 'factoryName', key: 'factoryName', title: '名称', width: 220 },
|
||||
{ dataIndex: 'factoryIcon', key: 'factoryIcon', title: 'Icon', width: 120 },
|
||||
{ dataIndex: 'createTime', key: 'createTime', title: '创建时间', width: 180 },
|
||||
{ dataIndex: 'actions', key: 'actions', title: '操作', width: 140 },
|
||||
{ dataIndex: 'actions', key: 'actions', title: '操作', width: 220 },
|
||||
];
|
||||
|
||||
async function loadData(reset = false) {
|
||||
@ -81,6 +83,11 @@ function handleAssociated(record: Record<string, any>) {
|
||||
associatedOpen.value = true;
|
||||
}
|
||||
|
||||
function handleConfig(record: Record<string, any>) {
|
||||
activeRow.value = { ...record };
|
||||
configOpen.value = true;
|
||||
}
|
||||
|
||||
void loadData(true);
|
||||
</script>
|
||||
|
||||
@ -126,6 +133,7 @@ void loadData(true);
|
||||
<Button type="link" @click="handleAssociated(record)">
|
||||
关联渠道
|
||||
</Button>
|
||||
<Button type="link" @click="handleConfig(record)">配置</Button>
|
||||
<Button type="link" @click="handleEdit(record)">编辑</Button>
|
||||
</template>
|
||||
</template>
|
||||
@ -155,6 +163,12 @@ void loadData(true);
|
||||
:open="associatedOpen"
|
||||
@close="associatedOpen = false"
|
||||
/>
|
||||
|
||||
<PayFactoryConfigModal
|
||||
:open="configOpen"
|
||||
:row="activeRow"
|
||||
@close="configOpen = false"
|
||||
/>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
|
||||
@ -46,7 +46,6 @@ import {
|
||||
USER_TYPE_OPTIONS,
|
||||
} from './constants';
|
||||
import UserAuthInfoDrawer from './components/user-auth-info-drawer.vue';
|
||||
import UserBalanceHandleModal from './components/user-balance-handle-modal.vue';
|
||||
import UserResetPasswordModal from './components/user-reset-password-modal.vue';
|
||||
import {
|
||||
getUserProfileAvatar,
|
||||
@ -98,12 +97,9 @@ const accountLogOpen = ref(false);
|
||||
const violationOpen = ref(false);
|
||||
const authInfoOpen = ref(false);
|
||||
const resetPasswordOpen = ref(false);
|
||||
const balanceOpen = ref(false);
|
||||
|
||||
const activeUserId = ref<number | string>('');
|
||||
const activeUserRecord = ref<Record<string, any> | null>(null);
|
||||
const balanceAction = ref<'deduct' | 'reward'>('reward');
|
||||
const balanceType = ref<'diamond' | 'gameCoupon' | 'gold'>('gold');
|
||||
|
||||
const query = reactive(createQuery());
|
||||
|
||||
@ -120,7 +116,7 @@ const columns = [
|
||||
{ dataIndex: 'accountStatus', key: 'accountStatus', title: '账户', width: 120 },
|
||||
{ dataIndex: 'registerSource', key: 'registerSource', title: '注册来源', width: 180 },
|
||||
{ dataIndex: 'time', key: 'time', title: '时间', width: 220 },
|
||||
{ dataIndex: 'actions', key: 'actions', title: '操作', width: 320 },
|
||||
{ dataIndex: 'actions', key: 'actions', title: '操作', width: 260 },
|
||||
];
|
||||
|
||||
const filteredCountries = computed(() => {
|
||||
@ -295,22 +291,11 @@ function openResetPassword(record: Record<string, any>) {
|
||||
resetPasswordOpen.value = true;
|
||||
}
|
||||
|
||||
function openBalanceModal(
|
||||
record: Record<string, any>,
|
||||
type: 'diamond' | 'gameCoupon' | 'gold',
|
||||
action: 'deduct' | 'reward',
|
||||
) {
|
||||
activeUserId.value = record.userProfile?.id || '';
|
||||
balanceType.value = type;
|
||||
balanceAction.value = action;
|
||||
balanceOpen.value = true;
|
||||
}
|
||||
|
||||
function buildEncodedAccount(sysOrigin: string, userId: number | string) {
|
||||
return `*type:USER,accountType:LONG,sysOrigin:${sysOrigin},content:${userId}*`;
|
||||
}
|
||||
|
||||
function openRunningWater(record: Record<string, any>, kind: 'diamond' | 'gameCoupon' | 'gold') {
|
||||
function openRunningWater(record: Record<string, any>, kind: 'diamond' | 'gold') {
|
||||
const userId = record.userProfile?.id;
|
||||
if (!userId) {
|
||||
return;
|
||||
@ -333,12 +318,7 @@ function openRunningWater(record: Record<string, any>, kind: 'diamond' | 'gameCo
|
||||
path: '/user/diamond-run-water',
|
||||
query: { sysOrigin, userId: encoded },
|
||||
});
|
||||
return;
|
||||
}
|
||||
router.push({
|
||||
path: '/game/coupon/running/water',
|
||||
query: { userId: encoded },
|
||||
});
|
||||
}
|
||||
|
||||
async function copyUserId(value?: number | string) {
|
||||
@ -515,18 +495,6 @@ void loadCountries();
|
||||
<Button size="small" type="link" @click="openAccountLog(record)">
|
||||
处理记录
|
||||
</Button>
|
||||
<Button size="small" type="link" @click="openBalanceModal(record, 'gold', 'reward')">
|
||||
金币奖励
|
||||
</Button>
|
||||
<Button size="small" type="link" @click="openBalanceModal(record, 'gold', 'deduct')">
|
||||
金币扣除
|
||||
</Button>
|
||||
<Button size="small" type="link" @click="openBalanceModal(record, 'diamond', 'reward')">
|
||||
钻石奖励
|
||||
</Button>
|
||||
<Button size="small" type="link" @click="openBalanceModal(record, 'diamond', 'deduct')">
|
||||
钻石扣除
|
||||
</Button>
|
||||
<Button size="small" type="link" @click="openAuthInfo(record)">
|
||||
认证信息
|
||||
</Button>
|
||||
@ -538,14 +506,6 @@ void loadCountries();
|
||||
>
|
||||
重置密码
|
||||
</Button>
|
||||
<Button
|
||||
v-if="activeTab === 'table'"
|
||||
size="small"
|
||||
type="link"
|
||||
@click="openRunningWater(record, 'gameCoupon')"
|
||||
>
|
||||
游戏券收支
|
||||
</Button>
|
||||
</Space>
|
||||
</template>
|
||||
</template>
|
||||
@ -599,14 +559,6 @@ void loadCountries();
|
||||
@close="resetPasswordOpen = false"
|
||||
@success="resetPasswordOpen = false"
|
||||
/>
|
||||
<UserBalanceHandleModal
|
||||
:action="balanceAction"
|
||||
:open="balanceOpen"
|
||||
:type="balanceType"
|
||||
:user-id="activeUserId"
|
||||
@close="balanceOpen = false"
|
||||
@success="balanceOpen = false; loadData()"
|
||||
/>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
|
||||
@ -48,7 +48,7 @@ const fullscreen = ref(false);
|
||||
const visualMode = ref(false);
|
||||
const loading = ref(false);
|
||||
const dashboard = ref<CountryDashboardResult | null>(null);
|
||||
const upDashboards = ref<{
|
||||
const arpuDashboards = ref<{
|
||||
day: CountryDashboardResult | null;
|
||||
threeDay: CountryDashboardResult | null;
|
||||
week: CountryDashboardResult | null;
|
||||
@ -282,12 +282,12 @@ const appDataCards = computed(() => {
|
||||
{ label: '30 日留存率', value: formatPercent(summary.d30RetentionRate) },
|
||||
];
|
||||
});
|
||||
const upValueCards = computed(() => {
|
||||
const arpuCards = computed(() => {
|
||||
return [
|
||||
{ label: '日 UP 值', value: formatUpValue(upDashboards.value.day) },
|
||||
{ label: '3 日 UP 值', value: formatUpValue(upDashboards.value.threeDay) },
|
||||
{ label: '周 UP 值', value: formatUpValue(upDashboards.value.week) },
|
||||
{ label: '筛选 UP 值', value: formatAmount(calcUpValue(visibleSummary.value)) },
|
||||
{ label: '日 ARPU', value: formatArpu(arpuDashboards.value.day) },
|
||||
{ label: '3 日 ARPU', value: formatArpu(arpuDashboards.value.threeDay) },
|
||||
{ label: '周 ARPU', value: formatArpu(arpuDashboards.value.week) },
|
||||
{ label: '筛选 ARPU', value: formatAmount(calcArpu(visibleSummary.value)) },
|
||||
];
|
||||
});
|
||||
|
||||
@ -372,7 +372,7 @@ async function loadDashboard() {
|
||||
}
|
||||
loading.value = true;
|
||||
try {
|
||||
const [mainDashboard, dayUpDashboard, threeDayUpDashboard, weekUpDashboard] =
|
||||
const [mainDashboard, dayArpuDashboard, threeDayArpuDashboard, weekArpuDashboard] =
|
||||
await Promise.all([
|
||||
countryDashboard(buildParams()),
|
||||
countryDashboard(buildRecentDayParams(1)),
|
||||
@ -380,14 +380,14 @@ async function loadDashboard() {
|
||||
countryDashboard(buildRecentDayParams(7)),
|
||||
]);
|
||||
dashboard.value = mainDashboard;
|
||||
upDashboards.value = {
|
||||
day: dayUpDashboard,
|
||||
threeDay: threeDayUpDashboard,
|
||||
week: weekUpDashboard,
|
||||
arpuDashboards.value = {
|
||||
day: dayArpuDashboard,
|
||||
threeDay: threeDayArpuDashboard,
|
||||
week: weekArpuDashboard,
|
||||
};
|
||||
} catch (error) {
|
||||
dashboard.value = null;
|
||||
upDashboards.value = {
|
||||
arpuDashboards.value = {
|
||||
day: null,
|
||||
threeDay: null,
|
||||
week: null,
|
||||
@ -495,7 +495,7 @@ function resolveMetricRate(record: Record<string, any>, key: string) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
function calcUpValue(summary: Record<string, any>) {
|
||||
function calcArpu(summary: Record<string, any>) {
|
||||
const activeUser = toAmount(summary.dailyActiveUser);
|
||||
if (activeUser === 0) {
|
||||
return 0;
|
||||
@ -503,9 +503,9 @@ function calcUpValue(summary: Record<string, any>) {
|
||||
return toAmount(summary.totalRecharge) / activeUser;
|
||||
}
|
||||
|
||||
function formatUpValue(result?: CountryDashboardResult | null) {
|
||||
function formatArpu(result?: CountryDashboardResult | null) {
|
||||
const summary = buildVisibleSummary(filterRecordsBySelectedCountries(result?.records || []));
|
||||
return formatAmount(calcUpValue(summary));
|
||||
return formatAmount(calcArpu(summary));
|
||||
}
|
||||
|
||||
function emptySummary(): SummaryMetric {
|
||||
@ -887,10 +887,10 @@ onBeforeUnmount(() => {
|
||||
</div>
|
||||
</section>
|
||||
<section class="panel metric-section">
|
||||
<div class="metric-section__title">UP 值</div>
|
||||
<div class="metric-card-grid metric-card-grid--up">
|
||||
<div class="metric-section__title">ARPU</div>
|
||||
<div class="metric-card-grid metric-card-grid--arpu">
|
||||
<div
|
||||
v-for="item in upValueCards"
|
||||
v-for="item in arpuCards"
|
||||
:key="item.label"
|
||||
class="metric-card"
|
||||
>
|
||||
@ -1074,7 +1074,7 @@ onBeforeUnmount(() => {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.metric-card-grid--up {
|
||||
.metric-card-grid--arpu {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
@ -1195,7 +1195,7 @@ onBeforeUnmount(() => {
|
||||
.metric-section-grid,
|
||||
.metric-card-grid,
|
||||
.metric-card-grid--app,
|
||||
.metric-card-grid--up {
|
||||
.metric-card-grid--arpu {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
214
scripts/sql/20260430_gm_gold_operation.sql
Normal file
214
scripts/sql/20260430_gm_gold_operation.sql
Normal file
@ -0,0 +1,214 @@
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `gm_gold_operation_record` (
|
||||
`id` bigint NOT NULL COMMENT '记录ID',
|
||||
`user_id` bigint NOT NULL COMMENT 'APP用户ID',
|
||||
`sys_origin` varchar(32) DEFAULT NULL COMMENT '系统来源',
|
||||
`operation_type` tinyint NOT NULL COMMENT '操作类型:0增加,1扣除',
|
||||
`quantity` decimal(20,2) NOT NULL COMMENT '金币数量',
|
||||
`reason_type` tinyint NOT NULL COMMENT '原因类型',
|
||||
`usd_quantity` decimal(20,2) DEFAULT NULL COMMENT '工资/充值折算USD金额',
|
||||
`remarks` varchar(500) NOT NULL COMMENT '备注',
|
||||
`status` varchar(20) NOT NULL DEFAULT 'PENDING' COMMENT '状态:PENDING待审核,APPROVED已通过,REJECTED已拒绝',
|
||||
`applicant_id` bigint NOT NULL COMMENT '申请后台用户ID',
|
||||
`applicant_name` varchar(100) DEFAULT NULL COMMENT '申请后台用户名称',
|
||||
`reviewer_id` bigint DEFAULT NULL COMMENT '审核后台用户ID',
|
||||
`reviewer_name` varchar(100) DEFAULT NULL COMMENT '审核后台用户名称',
|
||||
`review_remark` varchar(500) DEFAULT NULL COMMENT '审核备注',
|
||||
`track_id` varchar(64) NOT NULL COMMENT '钱包流水跟踪ID',
|
||||
`review_time` datetime DEFAULT NULL COMMENT '审核时间',
|
||||
`executed_time` datetime DEFAULT NULL COMMENT '执行时间',
|
||||
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
`create_user` bigint DEFAULT NULL COMMENT '创建用户',
|
||||
`update_user` bigint DEFAULT NULL COMMENT '更新用户',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_gm_gold_track_id` (`track_id`),
|
||||
KEY `idx_gm_gold_status_time` (`status`, `create_time`),
|
||||
KEY `idx_gm_gold_user` (`user_id`),
|
||||
KEY `idx_gm_gold_applicant` (`applicant_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='GM金币操作申请记录';
|
||||
|
||||
INSERT INTO sys_menu (
|
||||
id,
|
||||
parent_id,
|
||||
menu_name,
|
||||
path,
|
||||
menu_type,
|
||||
icon,
|
||||
create_user,
|
||||
update_user,
|
||||
sort,
|
||||
status,
|
||||
router,
|
||||
alias
|
||||
)
|
||||
SELECT
|
||||
1700000400,
|
||||
0,
|
||||
'GM操作',
|
||||
'/gm/operation',
|
||||
1,
|
||||
'lucide:shield',
|
||||
'0',
|
||||
'0',
|
||||
48,
|
||||
0,
|
||||
'/gm/operation',
|
||||
'GmOperation'
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM sys_menu WHERE alias = 'GmOperation'
|
||||
);
|
||||
|
||||
UPDATE sys_menu
|
||||
SET
|
||||
parent_id = 0,
|
||||
menu_name = 'GM操作',
|
||||
path = '/gm/operation',
|
||||
menu_type = 1,
|
||||
icon = 'lucide:shield',
|
||||
create_user = '0',
|
||||
update_user = '0',
|
||||
sort = 48,
|
||||
status = 0,
|
||||
router = '/gm/operation'
|
||||
WHERE alias = 'GmOperation';
|
||||
|
||||
INSERT INTO sys_menu (
|
||||
id,
|
||||
parent_id,
|
||||
menu_name,
|
||||
path,
|
||||
menu_type,
|
||||
icon,
|
||||
create_user,
|
||||
update_user,
|
||||
sort,
|
||||
status,
|
||||
router,
|
||||
alias
|
||||
)
|
||||
SELECT
|
||||
1700000401,
|
||||
(SELECT id FROM (SELECT id FROM sys_menu WHERE alias = 'GmOperation' LIMIT 1) AS parent_menu),
|
||||
'金币操作',
|
||||
'gm/gold-operation',
|
||||
2,
|
||||
'lucide:coins',
|
||||
'0',
|
||||
'0',
|
||||
1,
|
||||
0,
|
||||
'gold-operation',
|
||||
'GmGoldOperation'
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM sys_menu WHERE alias = 'GmGoldOperation'
|
||||
);
|
||||
|
||||
UPDATE sys_menu
|
||||
SET
|
||||
parent_id = (SELECT id FROM (SELECT id FROM sys_menu WHERE alias = 'GmOperation' LIMIT 1) AS parent_menu),
|
||||
menu_name = '金币操作',
|
||||
path = 'gm/gold-operation',
|
||||
menu_type = 2,
|
||||
icon = 'lucide:coins',
|
||||
create_user = '0',
|
||||
update_user = '0',
|
||||
sort = 1,
|
||||
status = 0,
|
||||
router = 'gold-operation'
|
||||
WHERE alias = 'GmGoldOperation';
|
||||
|
||||
INSERT INTO sys_menu (
|
||||
id,
|
||||
parent_id,
|
||||
menu_name,
|
||||
path,
|
||||
menu_type,
|
||||
icon,
|
||||
create_user,
|
||||
update_user,
|
||||
sort,
|
||||
status,
|
||||
router,
|
||||
alias
|
||||
)
|
||||
SELECT
|
||||
1700000402,
|
||||
(SELECT id FROM (SELECT id FROM sys_menu WHERE alias = 'GmOperation' LIMIT 1) AS parent_menu),
|
||||
'金币操作审核',
|
||||
'gm/gold-operation-review',
|
||||
2,
|
||||
'lucide:badge-check',
|
||||
'0',
|
||||
'0',
|
||||
2,
|
||||
0,
|
||||
'gold-operation-review',
|
||||
'GmGoldOperationReview'
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM sys_menu WHERE alias = 'GmGoldOperationReview'
|
||||
);
|
||||
|
||||
UPDATE sys_menu
|
||||
SET
|
||||
parent_id = (SELECT id FROM (SELECT id FROM sys_menu WHERE alias = 'GmOperation' LIMIT 1) AS parent_menu),
|
||||
menu_name = '金币操作审核',
|
||||
path = 'gm/gold-operation-review',
|
||||
menu_type = 2,
|
||||
icon = 'lucide:badge-check',
|
||||
create_user = '0',
|
||||
update_user = '0',
|
||||
sort = 2,
|
||||
status = 0,
|
||||
router = 'gold-operation-review'
|
||||
WHERE alias = 'GmGoldOperationReview';
|
||||
|
||||
DELETE role_menu
|
||||
FROM sys_role_menu AS role_menu
|
||||
JOIN sys_menu AS gm_menu
|
||||
ON gm_menu.id = role_menu.menu_id
|
||||
LEFT JOIN sys_role AS valid_role
|
||||
ON valid_role.id = role_menu.role_id
|
||||
WHERE gm_menu.alias IN ('GmOperation', 'GmGoldOperation', 'GmGoldOperationReview')
|
||||
AND valid_role.id IS NULL;
|
||||
|
||||
INSERT INTO sys_role_menu (role_id, menu_id)
|
||||
SELECT DISTINCT
|
||||
valid_source_role.id,
|
||||
target_menu.id
|
||||
FROM sys_role_menu AS source_role
|
||||
JOIN sys_role AS valid_source_role
|
||||
ON valid_source_role.id = source_role.role_id
|
||||
JOIN sys_menu AS source_menu
|
||||
ON source_menu.id = source_role.menu_id
|
||||
JOIN sys_menu AS target_menu
|
||||
ON target_menu.alias IN ('GmOperation', 'GmGoldOperation')
|
||||
LEFT JOIN sys_role_menu AS existing_role_menu
|
||||
ON existing_role_menu.role_id = valid_source_role.id
|
||||
AND existing_role_menu.menu_id = target_menu.id
|
||||
WHERE source_menu.alias IN ('OperateInsideOperationGold', 'OperateUserManage')
|
||||
AND existing_role_menu.id IS NULL;
|
||||
|
||||
DELETE role_menu
|
||||
FROM sys_role_menu AS role_menu
|
||||
JOIN sys_menu AS review_menu
|
||||
ON review_menu.id = role_menu.menu_id
|
||||
WHERE review_menu.alias = 'GmGoldOperationReview';
|
||||
|
||||
INSERT INTO sys_role_menu (role_id, menu_id)
|
||||
SELECT DISTINCT
|
||||
yumi_role.id,
|
||||
target_menu.id
|
||||
FROM sys_user AS yumi_user
|
||||
JOIN sys_user_role AS user_role
|
||||
ON user_role.uid = yumi_user.uid
|
||||
JOIN sys_role AS yumi_role
|
||||
ON yumi_role.id = user_role.role_id
|
||||
JOIN sys_menu AS target_menu
|
||||
ON target_menu.alias IN ('GmOperation', 'GmGoldOperation', 'GmGoldOperationReview')
|
||||
LEFT JOIN sys_role_menu AS existing_role_menu
|
||||
ON existing_role_menu.role_id = yumi_role.id
|
||||
AND existing_role_menu.menu_id = target_menu.id
|
||||
WHERE LOWER(yumi_user.login_name) = 'yumiadmin'
|
||||
AND existing_role_menu.id IS NULL;
|
||||
Loading…
x
Reference in New Issue
Block a user