棋子暂存区、充值订单补单
Some checks failed
CI / Test (ubuntu-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Lint (ubuntu-latest) (push) Has been cancelled
CI / Lint (windows-latest) (push) Has been cancelled
CI / Check (ubuntu-latest) (push) Has been cancelled
CI / Check (windows-latest) (push) Has been cancelled
CodeQL / Analyze (${{ matrix.language }}) (none, javascript-typescript) (push) Has been cancelled
Deploy Website on push / Deploy Push Playground Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Docs Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Antd Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Element Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Naive Ftp (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
CI / CI OK (push) Has been cancelled

This commit is contained in:
hahwu 2026-05-14 14:35:45 +08:00
parent a1bd2b850e
commit fc9455cfce
5 changed files with 411 additions and 19 deletions

View File

@ -54,6 +54,7 @@ export interface UserLogInfo {
Face?:number; Face?:number;
Order: UserLogOrder[]; Order: UserLogOrder[];
ChessMap?:string; ChessMap?:string;
ChessBuffer?: Record<string, number>;
Heatmap?: heatType[]; Heatmap?: heatType[];
ActLog?:actlog[]; ActLog?:actlog[];
MaxCharge?: number; MaxCharge?: number;
@ -69,9 +70,11 @@ export interface actlog {
export interface UserOrder { export interface UserOrder {
Id: number; Id: number;
OrderId: number; AppId?: number;
ServerId?: number;
OrderId: string;
Price: number; Price: number;
PayChannelOrderId: number; PayChannelOrderId: string;
ProductId: number; ProductId: number;
CreateTime: number; CreateTime: number;
PayTime: number; PayTime: number;
@ -80,6 +83,30 @@ export interface UserOrder {
CreateTimeStr?: string; CreateTimeStr?: string;
PayTimeStr?: string; PayTimeStr?: string;
PayStatus?:number; PayStatus?:number;
ReissueAuditId?: number;
ReissueStatus?: number;
ReissueReason?: string;
ReissueApplicant?: string;
ReissueReviewer?: string;
ReissueReviewTime?: number;
ReissueRemark?: string;
ReissueThirdOrder?: string;
}
export namespace OrderReissueApi {
export interface ApplyParams {
AppId: number;
OrderId: string;
Reason: string;
ServerId: number;
ThirdPartyOrderId: string;
Uid: number;
}
export interface ReviewParams {
audit_id: number;
review_remark?: string;
}
} }
export async function getUserLogAssetApi(data: UserLogAssetParam) { export async function getUserLogAssetApi(data: UserLogAssetParam) {
@ -98,6 +125,18 @@ export async function getUserlogOrderApi(data: UserLogAssetParam) {
return requestClient.post<UserOrder>('/log/order', data); return requestClient.post<UserOrder>('/log/order', data);
} }
export async function addOrderReissueApi(data: OrderReissueApi.ApplyParams) {
return requestClient.post('/log/order/reissue/apply', data);
}
export async function approveOrderReissueApi(data: OrderReissueApi.ReviewParams) {
return requestClient.post('/log/order/reissue/approve', data);
}
export async function rejectOrderReissueApi(data: OrderReissueApi.ReviewParams) {
return requestClient.post('/log/order/reissue/reject', data);
}
export namespace LoginCountApi { export namespace LoginCountApi {
export interface Params { export interface Params {
Appid: number; Appid: number;

View File

@ -0,0 +1,85 @@
<script setup lang="ts">
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { addOrderReissueApi } from '#/api/core/log';
interface ModalData {
Uid: number;
AppId: number;
ServerId: number;
OrderId: string;
PayChannelOrderId?: string;
}
const [Form, FormApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
},
layout: 'horizontal',
schema: [
{
component: 'Input',
componentProps: {
placeholder: '请输入第三方订单号',
},
fieldName: 'ThirdPartyOrderId',
formItemClass: 'col-span-2',
label: '第三方订单号',
rules: 'required',
},
{
component: 'Textarea',
componentProps: {
placeholder: '请填写补单原因',
rows: 4,
},
fieldName: 'Reason',
formItemClass: 'col-span-2',
label: '补单理由',
rules: 'required',
},
],
showDefaultActions: false,
wrapperClass: 'grid-cols-2',
});
const [Modal, modalApi] = useVbenModal({
confirmText: '提交审核',
onOpened: async () => {
const modalData = modalApi.getData<ModalData>();
await FormApi.resetForm();
await FormApi.setValues({
Reason: '',
ThirdPartyOrderId: modalData?.PayChannelOrderId || '',
});
},
onConfirm: async () => {
const values = await FormApi.getValues();
const modalData = modalApi.getData<ModalData>();
await addOrderReissueApi({
AppId: modalData.AppId,
OrderId: modalData.OrderId,
Reason: values.Reason,
ServerId: modalData.ServerId,
ThirdPartyOrderId: values.ThirdPartyOrderId,
Uid: modalData.Uid,
});
message.success('补单申请已提交审核');
modalApi.close();
},
});
defineOptions({
name: 'OrderReissueModal',
});
</script>
<template>
<Modal title="申请补单" :width="640">
<Form />
</Modal>
</template>

View File

@ -1,28 +1,52 @@
<script setup lang="ts"> <script setup lang="ts">
import type { VbenFormProps } from '#/adapter/form'; import type { VbenFormProps } from '#/adapter/form';
import type { VxeGridProps } from '#/adapter/vxe-table'; import type { VxeGridProps } from '#/adapter/vxe-table';
import type { UserOrder } from '#/api/core/log';
import type { AppData } from '#/api/core/server'; import type { AppData } from '#/api/core/server';
import { inject, onMounted, ref } from 'vue'; import { inject, onMounted, ref } from 'vue';
import { Page } from '@vben/common-ui'; import { Page, useVbenModal } from '@vben/common-ui';
import { useAccess } from '@vben/access';
import { useVbenVxeGrid } from '#/adapter/vxe-table'; import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getUserlogOrderApi } from '#/api/core/log'; import {
approveOrderReissueApi,
getUserlogOrderApi,
rejectOrderReissueApi,
} from '#/api/core/log';
import { getAppListApi } from '#/api/core/server'; import { getAppListApi } from '#/api/core/server';
import { globalState } from '#/store/globalState'; import { globalState } from '#/store/globalState';
import { rechargeData } from '#/store/recharge'; import { rechargeData } from '#/store/recharge';
import OrderReissueModal from '#/views/userlog/components/order-reissue-modal.vue';
import { Button, message, Modal, Space, Tag } from 'ant-design-vue';
const state = inject('globalState', globalState); const state = inject('globalState', globalState);
const { hasAccessByRoles } = useAccess();
interface RowType { interface RowType extends UserOrder {
Uid: string; Uid: string;
Event: string;
Param: string;
timestamp: string;
} }
const appList = ref<AppData[]>([]); const appList = ref<AppData[]>([]);
function getReissueStatusText(status?: number) {
if (status === 1) return '待审核';
if (status === 2) return '已通过';
if (status === 3) return '已驳回';
return '-';
}
function getReissueStatusColor(status?: number) {
if (status === 1) return 'processing';
if (status === 2) return 'success';
if (status === 3) return 'error';
return 'default';
}
function canApplyReissue(row: RowType) {
return !row.ReissueStatus && [0, 3].includes(row.PayStatus ?? -1);
}
const formOptions: VbenFormProps = { const formOptions: VbenFormProps = {
// //
collapsed: false, collapsed: false,
@ -73,6 +97,14 @@ const gridOptions: VxeGridProps<RowType> = {
}, },
}, },
{ field: 'CreateTimeStr', title: '创建时间' }, { field: 'CreateTimeStr', title: '创建时间' },
{
field: 'ReissueStatus',
title: '补单状态',
width: 100,
slots: { default: 'reissue_status' },
},
{ field: 'ReissueThirdOrder', title: '补单三方单号', minWidth: 160 },
{ field: 'ReissueReason', title: '补单理由', minWidth: 180 },
{ field: 'PayTimeStr', title: '支付时间' }, { field: 'PayTimeStr', title: '支付时间' },
{ {
field: 'PayType', field: 'PayType',
@ -86,6 +118,7 @@ const gridOptions: VxeGridProps<RowType> = {
}, },
}, },
{ field: 'Param', title: '参数' }, { field: 'Param', title: '参数' },
{ title: '操作', width: 220, fixed: 'right', slots: { default: 'action' } },
], ],
exportConfig: { exportConfig: {
filename: '用户订单日志', filename: '用户订单日志',
@ -126,9 +159,65 @@ const gridOptions: VxeGridProps<RowType> = {
rowConfig: { rowConfig: {
isHover: true, isHover: true,
}, },
showOverflow: true,
}; };
const [Grid, GridApi] = useVbenVxeGrid({ formOptions, gridOptions }); const [Grid, GridApi] = useVbenVxeGrid({ formOptions, gridOptions });
const [OrderReissueM, OrderReissueApi] = useVbenModal({
connectedComponent: OrderReissueModal,
onClosed: async () => {
OrderReissueApi.close();
await GridApi.query();
},
});
function openOrderReissue(row: RowType) {
OrderReissueApi.setData({
AppId: row.AppId || 1,
OrderId: row.OrderId,
PayChannelOrderId: row.PayChannelOrderId,
ServerId: row.ServerId || 1,
Uid: Number(row.Uid),
});
OrderReissueApi.open();
}
async function refreshGrid() {
await GridApi.query();
}
function approveReissue(row: RowType) {
const auditId = row.ReissueAuditId;
if (!auditId) {
message.error('缺少补单审核单ID');
return;
}
Modal.confirm({
title: '确认通过这条补单申请吗?',
async onOk() {
await approveOrderReissueApi({ audit_id: auditId });
message.success('补单审核通过');
await refreshGrid();
},
});
}
function rejectReissue(row: RowType) {
const auditId = row.ReissueAuditId;
if (!auditId) {
message.error('缺少补单审核单ID');
return;
}
Modal.confirm({
title: '确认驳回这条补单申请吗?',
async onOk() {
await rejectOrderReissueApi({ audit_id: auditId });
message.success('补单申请已驳回');
await refreshGrid();
},
});
}
onMounted(async () => { onMounted(async () => {
try { try {
const response = await getAppListApi(); const response = await getAppListApi();
@ -156,6 +245,39 @@ onMounted(async () => {
<template> <template>
<Page auto-content-height> <Page auto-content-height>
<Grid /> <OrderReissueM />
<Grid>
<template #reissue_status="{ row }">
<Tag :color="getReissueStatusColor(row.ReissueStatus)">
{{ getReissueStatusText(row.ReissueStatus) }}
</Tag>
</template>
<template #action="{ row }">
<Space>
<Button
v-if="canApplyReissue(row) && hasAccessByRoles(['super', 'AC2006'])"
type="primary"
@click="openOrderReissue(row)"
>
申请补单
</Button>
<Button
v-if="row.ReissueStatus === 1 && hasAccessByRoles(['super', 'AC2007'])"
type="primary"
@click="approveReissue(row)"
>
通过
</Button>
<Button
v-if="row.ReissueStatus === 1 && hasAccessByRoles(['super', 'AC2008'])"
danger
type="primary"
@click="rejectReissue(row)"
>
驳回
</Button>
</Space>
</template>
</Grid>
</Page> </Page>
</template> </template>

View File

@ -1,29 +1,52 @@
<script setup lang="ts"> <script setup lang="ts">
import type { VbenFormProps } from '#/adapter/form'; import type { VbenFormProps } from '#/adapter/form';
import type { VxeGridProps } from '#/adapter/vxe-table'; import type { VxeGridProps } from '#/adapter/vxe-table';
import type { UserOrder } from '#/api/core/log';
import type { AppData } from '#/api/core/server'; import type { AppData } from '#/api/core/server';
import { inject, onMounted, ref } from 'vue'; import { inject, onMounted, ref } from 'vue';
import { Page } from '@vben/common-ui'; import { Page, useVbenModal } from '@vben/common-ui';
import { useAccess } from '@vben/access';
import { useVbenVxeGrid } from '#/adapter/vxe-table'; import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getUserlogOrderApi } from '#/api/core/log'; import {
approveOrderReissueApi,
getUserlogOrderApi,
rejectOrderReissueApi,
} from '#/api/core/log';
import { getAppListApi } from '#/api/core/server'; import { getAppListApi } from '#/api/core/server';
import { globalState } from '#/store/globalState'; import { globalState } from '#/store/globalState';
import { rechargeData } from '#/store/recharge'; import { rechargeData } from '#/store/recharge';
import { Tag } from 'ant-design-vue'; import OrderReissueModal from '#/views/userlog/components/order-reissue-modal.vue';
import { Button, message, Modal, Space, Tag } from 'ant-design-vue';
const state = inject('globalState', globalState); const state = inject('globalState', globalState);
const { hasAccessByRoles } = useAccess();
interface RowType { interface RowType extends UserOrder {
Uid: string; Uid: string;
Event: string;
Param: string;
timestamp: string;
} }
const appList = ref<AppData[]>([]); const appList = ref<AppData[]>([]);
function getReissueStatusText(status?: number) {
if (status === 1) return '待审核';
if (status === 2) return '已通过';
if (status === 3) return '已驳回';
return '-';
}
function getReissueStatusColor(status?: number) {
if (status === 1) return 'processing';
if (status === 2) return 'success';
if (status === 3) return 'error';
return 'default';
}
function canApplyReissue(row: RowType) {
return !row.ReissueStatus && [0, 3].includes(row.PayStatus ?? -1);
}
const formOptions: VbenFormProps = { const formOptions: VbenFormProps = {
// //
collapsed: false, collapsed: false,
@ -51,7 +74,6 @@ const formOptions: VbenFormProps = {
}; };
const gridOptions: VxeGridProps<RowType> = { const gridOptions: VxeGridProps<RowType> = {
columns: [ columns: [
{ field: 'Uid', title: 'id' },
{ field: 'OrderId', title: '订单号' }, { field: 'OrderId', title: '订单号' },
{ field: 'PayChannelOrderId', title: '3th订单号' }, { field: 'PayChannelOrderId', title: '3th订单号' },
{ field: 'Price', title: '金额' }, { field: 'Price', title: '金额' },
@ -63,6 +85,14 @@ const gridOptions: VxeGridProps<RowType> = {
}, },
}, },
{ field: 'PayStatus', title: '支付状态', slots: { default: 'status' } }, { field: 'PayStatus', title: '支付状态', slots: { default: 'status' } },
{
field: 'ReissueStatus',
title: '补单状态',
width: 100,
slots: { default: 'reissue_status' },
},
{ field: 'ReissueThirdOrder', title: '补单三方单号', minWidth: 160 },
{ field: 'ReissueReason', title: '补单理由', minWidth: 180 },
{ field: 'PayTimeStr', title: '支付时间' }, { field: 'PayTimeStr', title: '支付时间' },
{ {
field: 'PayType', field: 'PayType',
@ -75,7 +105,7 @@ const gridOptions: VxeGridProps<RowType> = {
return cellValue; return cellValue;
}, },
}, },
{ field: 'Param', title: '参数' }, { title: '操作', width: 220, fixed: 'right', slots: { default: 'action' } },
], ],
exportConfig: { exportConfig: {
filename: '用户订单日志', filename: '用户订单日志',
@ -117,9 +147,65 @@ const gridOptions: VxeGridProps<RowType> = {
rowConfig: { rowConfig: {
isHover: true, isHover: true,
}, },
showOverflow: true,
}; };
const [Grid, GridApi] = useVbenVxeGrid({ formOptions, gridOptions }); const [Grid, GridApi] = useVbenVxeGrid({ formOptions, gridOptions });
const [OrderReissueM, OrderReissueApi] = useVbenModal({
connectedComponent: OrderReissueModal,
onClosed: async () => {
OrderReissueApi.close();
await GridApi.query();
},
});
function openOrderReissue(row: RowType) {
OrderReissueApi.setData({
AppId: row.AppId || Number(state.uid) / 100000000,
OrderId: row.OrderId,
PayChannelOrderId: row.PayChannelOrderId,
ServerId: row.ServerId || 1,
Uid: Number(row.Uid),
});
OrderReissueApi.open();
}
async function refreshGrid() {
await GridApi.query();
}
function approveReissue(row: RowType) {
const auditId = row.ReissueAuditId;
if (!auditId) {
message.error('缺少补单审核单ID');
return;
}
Modal.confirm({
title: '确认通过这条补单申请吗?',
async onOk() {
await approveOrderReissueApi({ audit_id: auditId });
message.success('补单审核通过');
await refreshGrid();
},
});
}
function rejectReissue(row: RowType) {
const auditId = row.ReissueAuditId;
if (!auditId) {
message.error('缺少补单审核单ID');
return;
}
Modal.confirm({
title: '确认驳回这条补单申请吗?',
async onOk() {
await rejectOrderReissueApi({ audit_id: auditId });
message.success('补单申请已驳回');
await refreshGrid();
},
});
}
onMounted(async () => { onMounted(async () => {
try { try {
const response = await getAppListApi(); const response = await getAppListApi();
@ -147,6 +233,7 @@ onMounted(async () => {
<template> <template>
<Page auto-content-height> <Page auto-content-height>
<OrderReissueM />
<Grid> <Grid>
<template #status="{ row }"> <template #status="{ row }">
<Tag color="gray" v-if="row.PayStatus === 0">未支付</Tag> <Tag color="gray" v-if="row.PayStatus === 0">未支付</Tag>
@ -155,6 +242,37 @@ onMounted(async () => {
<Tag color="green" v-else-if="row.PayStatus === 3">已发货</Tag> <Tag color="green" v-else-if="row.PayStatus === 3">已发货</Tag>
<Tag v-else>未知态{{row.ProductId}}</Tag> <Tag v-else>未知态{{row.ProductId}}</Tag>
</template> </template>
<template #reissue_status="{ row }">
<Tag :color="getReissueStatusColor(row.ReissueStatus)">
{{ getReissueStatusText(row.ReissueStatus) }}
</Tag>
</template>
<template #action="{ row }">
<Space>
<Button
v-if="canApplyReissue(row) && hasAccessByRoles(['super', 'AC2006'])"
type="primary"
@click="openOrderReissue(row)"
>
申请补单
</Button>
<Button
v-if="row.ReissueStatus === 1 && hasAccessByRoles(['super', 'AC2007'])"
type="primary"
@click="approveReissue(row)"
>
通过
</Button>
<Button
v-if="row.ReissueStatus === 1 && hasAccessByRoles(['super', 'AC2008'])"
danger
type="primary"
@click="rejectReissue(row)"
>
驳回
</Button>
</Space>
</template>
</Grid> </Grid>
</Page> </Page>
</template> </template>

View File

@ -28,6 +28,7 @@ import orderTable from './order-table.vue';
const projectItems: WorkbenchProjectItem[] = []; const projectItems: WorkbenchProjectItem[] = [];
const gmPermissions:boolean = useAccess().hasAccessByRoles(['super']) || useAccess().hasAccessByRoles(['AC9301']); const gmPermissions:boolean = useAccess().hasAccessByRoles(['super']) || useAccess().hasAccessByRoles(['AC9301']);
const chargeDisplay = computed(() => (Number(info.value?.Charge ?? 0)).toFixed(2)); const chargeDisplay = computed(() => (Number(info.value?.Charge ?? 0)).toFixed(2));
const chessBufferCount = computed(() => info.value.ChessBuffer.length);
const banStatusText = computed(() => { const banStatusText = computed(() => {
if (info.value?.Ban === -1) { if (info.value?.Ban === -1) {
return '永久封禁'; return '永久封禁';
@ -245,6 +246,7 @@ const info = ref<{
Face?: string; Face?: string;
Order: Order[]; Order: Order[];
Chess: Chess[]; Chess: Chess[];
ChessBuffer: Chess[];
Friend: friendRecord[]; Friend: friendRecord[];
MaxCharge?: number; MaxCharge?: number;
AdWatch?: number; AdWatch?: number;
@ -264,6 +266,7 @@ const info = ref<{
TodayCumulative: '0h', TodayCumulative: '0h',
Order: [], Order: [],
Chess: [], Chess: [],
ChessBuffer: [],
Friend: [], Friend: [],
MaxCharge: 0, MaxCharge: 0,
AdWatch: 0, AdWatch: 0,
@ -340,6 +343,7 @@ const [Modal, modalApi] = useVbenModal({
); // ); //
info.value.Face = faceTypeData[r.Face || 0] || '未知'; info.value.Face = faceTypeData[r.Face || 0] || '未知';
info.value.Chess = Array.from({ length: 63 }, () => ({} as Chess)); info.value.Chess = Array.from({ length: 63 }, () => ({} as Chess));
info.value.ChessBuffer = [];
if (r.ChessMap) { if (r.ChessMap) {
for (const [key, value] of Object.entries(r.ChessMap)) { for (const [key, value] of Object.entries(r.ChessMap)) {
const [colStr = '0', rowStr = '0', lock = '0'] = key.split('@'); const [colStr = '0', rowStr = '0', lock = '0'] = key.split('@');
@ -354,6 +358,20 @@ const [Modal, modalApi] = useVbenModal({
}); });
} }
} }
if (r.ChessBuffer) {
let pos = 0;
for (const [chessId, count] of Object.entries(r.ChessBuffer)) {
const total = Number(count) || 0;
for (let index = 0; index < total; index++) {
info.value.ChessBuffer.push({
Id: parseInt(chessId, 10),
Icon: ((MergeData as Record<string, Merge>)[chessId]?.Icon) ?? '',
Lock: 0,
Pos: pos++,
});
}
}
}
for (const i in r.Order) { for (const i in r.Order) {
var chessArr = r.Order[i]?.ChessId var chessArr = r.Order[i]?.ChessId
var orderTypeName = Object.entries(orderTypeData).find( var orderTypeName = Object.entries(orderTypeData).find(
@ -516,7 +534,7 @@ function formatActLog(type: number, content = ''): [string, string] {
</div> </div>
<div class="player-detail-panel-grid mt-5"> <div class="player-detail-panel-grid mt-5">
<div if="gmPermissions"> <div v-if="gmPermissions">
<Card class="player-detail-action-card" :bordered="false"> <Card class="player-detail-action-card" :bordered="false">
<template #title> <template #title>
<div class="player-detail-card-title"> <div class="player-detail-card-title">
@ -573,6 +591,16 @@ function formatActLog(type: number, content = ''): [string, string] {
<div class="player-detail-section mt-6"> <div class="player-detail-section mt-6">
<chessComponent :items="info.Chess" title="棋盘" /> <chessComponent :items="info.Chess" title="棋盘" />
</div> </div>
<div class="player-detail-section player-detail-section--transparent mt-6">
<Collapse class="player-detail-collapse" :bordered="false">
<Collapse.Panel
key="chess-buffer"
:header="`棋子缓冲区 (${chessBufferCount})`"
>
<chessComponent :items="info.ChessBuffer" title="棋子缓冲区" />
</Collapse.Panel>
</Collapse>
</div>
<div class="player-detail-section mt-6"> <div class="player-detail-section mt-6">
<friendComponent :Items="info.Friend" title="好友" /> <friendComponent :Items="info.Friend" title="好友" />
</div> </div>