版本更新
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 2025-10-13 16:25:59 +08:00
parent 3d7dad79a7
commit f5054e2c2e
13 changed files with 318 additions and 11 deletions

View File

@ -46,6 +46,7 @@ export interface UserLogInfo {
Ban?: number;
Face?:number;
Order: UserLogOrder[];
ChessMap?:string;
Heatmap?: heatType[];
}

View File

@ -4,6 +4,11 @@ import { requestClient } from '#/api/request';
export interface OperationParam{
AppId: number;
ServerList?: number[];
Emit?:string[];
}
export async function getStatisticsOrder(data : OperationParam) {
return requestClient.post('/statistics/order', data);
}
export async function getStatisticsLevel(data : OperationParam) {

View File

@ -13,6 +13,8 @@ export interface UserListParam {
ServerId: number;
pageSize: number;
currentPage: number;
StartTime?: number;
EndTime?: number;
}

View File

@ -3,6 +3,7 @@ import calendar from "./calendar/index.vue";
import eventModal from "./modal/event.vue";
import assetModal from "./modal/asset.vue";
import orderComponent from "./modal/orderComponent.vue";
import chessComponent from "./modal/chessComponent.vue";
import type {dataType} from "./calendar/index.vue";
export { eventTable, calendar, eventModal, assetModal, orderComponent };
export { eventTable, calendar, eventModal, assetModal, orderComponent, chessComponent };
export type { dataType };

View File

@ -0,0 +1,55 @@
<script setup lang="ts">
import {
Card,
CardContent,
CardHeader,
CardTitle,
VbenIcon,
} from '../../../../../packages/@core/ui-kit/shadcn-ui';
import type { Chess } from '#/model/type';
interface Props {
items: Chess[];
title: string;
}
import { Tag } from 'ant-design-vue';
defineOptions({
name: 'WorkbenchProject',
});
withDefaults(defineProps<Props>(), {
items: () => [],
});
function getTagColor(diff: number): string {
if (diff === 0) {
return 'green';
} else if (diff === 1) {
return 'blue';
} else if (diff === 2) {
return 'red';
} else if (diff === 3) {
return 'red';
}
return 'red';
}
defineEmits(['click']);
</script>
<template>
<Card>
<CardHeader class="py-4 border-border border-b">
<CardTitle class="text-lg">{{ title }}</CardTitle>
</CardHeader>
<CardContent class="flex flex-wrap p-0">
<template v-for="(item, index) in [...items].sort((a, b) => a.Pos - b.Pos)" :key="index">
<div class="border-border group cursor-pointer border-b border-r border-t p-0 transition-all hover:shadow-xl flex-none"
:style="{ width: '35px', height: '35px', backgroundColor: item.Lock > 0 ? 'gray' : '' }"
:id="index.toString()">
<img v-if="item.Icon" :src="`../../../public/merge/${item.Icon}.png`"
style="width:100%;height:100%;object-fit:contain;" />
</div>
<div v-if="(index + 1) % 7 === 0" class="w-full h-0" :id="(index + 1).toString()"></div>
</template>
</CardContent>
</Card>
</template>

View File

@ -30,7 +30,8 @@
"operation": {
"title": "运营管理",
"level": "等级分布",
"mail": "邮件管理"
"mail": "邮件管理",
"order": "订单管理"
},
"log": {
"event": {

View File

@ -38,3 +38,11 @@ export interface Order {
url?: string;
}
export interface Chess{
Id: number;
Icon: string;
Pos: number;
Lock: number;
}

View File

@ -33,6 +33,16 @@ const routes: RouteRecordRaw[] = [
icon: 'lucide:mail',
title: $t('page.operation.mail'),
},
},
{
name: 'Order',
path: '/order',
component: () => import('#/views/operation/order/index.vue'),
meta: {
affixTab: true,
icon: 'lets-icons:order',
title: $t('page.operation.order'),
},
}
],

View File

@ -0,0 +1,11 @@
<script lang="ts" setup>
import AnalyticsVisitsTable from './table.vue';
</script>
<template>
<AnalyticsVisitsTable />
</template>

View File

@ -0,0 +1,160 @@
<script setup lang="ts">
import { Page } from '@vben/common-ui';
import { getStatisticsOrder } from '#/api/core/statistics';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import type { VxeGridProps } from '#/adapter/vxe-table';
import type { VbenFormProps } from '#/adapter/form';
import { getServerListApi, getAppListApi } from '#/api/core/server';
import type { AppData, ServerData } from '#/api/core/server';
import { onMounted, ref } from 'vue';
const appList = ref<AppData[]>([]);
const ServerList = ref<ServerData[]>([]);
let Emit = ref<string[]>([]);
interface RowType {
Uid: number;
change_type: string;
change_num: string;
change_after: string;
item_id: string;
timestamp: string;
}
const formOptions: VbenFormProps = {
//
collapsed: false,
schema: [
{
component: 'Select',
defaultValue: 1,
componentProps: {
onChange: async (value: number) => {
const serverResponse = await getServerListApi({ AppId: value, Type: 1 });
ServerList.value = Array.isArray(serverResponse) ? serverResponse : [];
GridApi.formApi.updateSchema([
{
component: 'CheckboxGroup',
componentProps: {
options: ServerList.value.map((item) => ({
label: item.ServerName,
value: item.ServerId,
})),
},
fieldName: 'ServerList',
},
]);
},
filterOption: true,
options: [
],
placeholder: '请选择',
showSearch: true,
},
fieldName: 'AppId',
label: 'APP',
},
{
component: 'CheckboxGroup',
componentProps: {
name: 'cname',
options: [
{ label: 'A', value: 'A' },
{ label: 'B', value: 'B' },
{ label: 'C', value: 'C' },
{ label: 'D', value: 'D' },
{ label: 'E', value: 'E' },
{ label: 'F', value: 'F' },
{ label: 'G', value: 'G' },
{ label: 'H', value: 'H' },
{ label: 'I', value: 'I' }, { label: 'J', value: 'J' }, { label: 'K', value: 'K' }, { label: 'L', value: 'L' }, { label: 'M', value: 'M' }, { label: 'N', value: 'N' }, { label: 'O', value: 'O' }, { label: 'P', value: 'P' }, { label: 'Q', value: 'Q' }, { label: 'R', value: 'R' }, { label: 'S', value: 'S' }, { label: 'T', value: 'T' }, { label: 'U', value: 'U' }, { label: 'V', value: 'V' }, { label: 'W', value: 'W' }, { label: 'X', value: 'X' }, { label: 'Y', value: 'Y' }, { label: 'Z', value: 'Z' },
],
},
fieldName: 'Emit',
label: '发射器系列',
},
],
//
showCollapseButton: true,
submitButtonOptions: {
content: '查询',
},
//
submitOnChange: false,
//
submitOnEnter: false,
}
const gridOptions: VxeGridProps<RowType> = {
columns: [
{ field: 'Type', title: '订单类型' },
{ field: 'Num', title: '该类订单持有数量' },
{ field: 'Sum', title: '流失玩家数' },
{ field: 'Prop', title: '占比', formatter: ({ cellValue }) => `${cellValue}%` },
],
height: 'auto',
pagerConfig: {},
proxyConfig: {
response: {
total: "total",
result: "data"
},
ajax: {
query: async ({ }, formValues) => {
let AppId = parseInt(formValues.AppId, 10);
Emit = formValues.Emit;
return await getStatisticsOrder({
AppId: AppId,
Emit: formValues.Emit
});
},
},
},
rowConfig: {
isHover: true,
},
};
const [Grid, GridApi] = useVbenVxeGrid({ formOptions, gridOptions });
onMounted(async () => {
try {
const response = await getAppListApi();
appList.value = Array.isArray(response) ? response : [];
const app = appList.value[0];
if (!app) return;
GridApi.formApi.updateSchema([
{
component: 'Select',
componentProps: {
options: appList.value.map((item) => ({
label: item.AppName,
value: item.AppId,
})),
},
fieldName: 'AppId',
},
]);
const serverResponse = await getServerListApi({ AppId: app.AppId, Type: 1 });
ServerList.value = Array.isArray(serverResponse) ? serverResponse : [];
} catch (e) {
appList.value = [];
//console.log(e);
}
});
</script>
<template>
<div>
<Page auto-content-height>
<Grid>
<template #toolbar-tools>
<span class="mr-5 font-semibold">发射器系列:</span>
<div v-for="e in (Emit || [])" :key="e" style="margin-right: 8px;">
<img :src="`../../../../public/merge/Launcher_${e}_LV4.png`" style="height: 32px;" />
</div>
</template>
</Grid>
</Page>
</div>
</template>

View File

@ -52,7 +52,7 @@ const banStatus = computed(() => {
</script>
<template>
<div class="card-box p-4 py-6 lg:flex">
<VbenAvatar :src="avatar" src2="../../public/avatar/1.png" class="size-20" />
<VbenAvatar :src="avatar" class="size-20" />
<div v-if="$slots.nick_name || $slots.user_name" class="flex flex-col justify-center md:ml-6 md:mt-0">
<h1 v-if="$slots.nick_name" class="text-md font-semibold md:text-xl">
<slot name="nick_name"></slot>

View File

@ -12,16 +12,14 @@ import type { dataType } from '#/component/index';
// cal-heatmap
import 'cal-heatmap/cal-heatmap.css';
import type { WorkbenchProjectItem } from '@vben/common-ui';
import { orderComponent } from '#/component/index';
import type { Order, Merge } from '#/model/type';
import { orderComponent, chessComponent } from '#/component/index';
import type { Order, Merge, Chess } from '#/model/type';
import dayjs from 'dayjs';
import { WorkbenchDetail } from '@vben/common-ui';
import UserHeader from './user-header.vue';
import { preferences } from '@vben/preferences';
import { useUserStore } from '@vben/stores';
import { AccessControl } from '@vben/access';
const userStore = useUserStore();
//
// url navTo
@ -145,6 +143,7 @@ const info = ref<{
Face?: string;
Order: Order[];
Heatmap: dataType[];
Chess: Chess[];
}>({
Level: 0,
Star: 0,
@ -161,6 +160,7 @@ const info = ref<{
TodayCumulative: '0h',
Order: [],
Heatmap: [],
Chess: [],
});
const [Modal, modalApi] = useVbenModal({
onCancel() {
@ -214,6 +214,21 @@ const [Modal, modalApi] = useVbenModal({
); //
info.value.Heatmap = r.Heatmap || [];
info.value.Face = faceTypeData[r.Face || 0] || '未知';
info.value.Chess = Array.from({ length: 63 }, () => ({} as Chess));
if (r.ChessMap) {
for (const [key, value] of Object.entries(r.ChessMap)) {
const [colStr = '0', rowStr = '0', lock = '0'] = key.split('@');
const pos = parseInt(rowStr, 10) * 7 + parseInt(colStr, 10);
info.value.Chess.splice(pos, 1, {
Id: parseInt(value),
Icon: ((MergeData as Record<string, Merge>)[
value
]?.Icon) ?? '',
Pos: pos,
Lock: parseInt(lock, 10),
});
}
}
for (const i in r.Order) {
var chessArr = r.Order[i]?.ChessId
? r.Order[i]?.ChessId.split(' ')
@ -299,7 +314,7 @@ watch(
<div class="mt-5 flex flex-col lg:flex-row">
<div class="mr-4 w-full lg:w-3/5">
<orderComponent :items="info.Order" title="订单" />
<!-- <WorkbenchTrends :items="trendItems" class="mt-5" title="最新动态" /> -->
<chessComponent :items="info.Chess" title="棋盘" class="mt-6" />
</div>
<div class="w-full lg:w-2/5">
<WorkbenchDetail :items="projectItems" class="mt-5 lg:mt-0" title="玩家详情">

View File

@ -31,7 +31,8 @@ interface RowType {
LoginTime: number;
Online: string;
}
const startDate = dayjs().subtract(7, 'day').startOf('day');
const endDate = dayjs().endOf('day');
const formOptions: VbenFormProps = {
//
collapsed: false,
@ -93,6 +94,43 @@ const formOptions: VbenFormProps = {
},
fieldName: 'Uid',
label: 'Uid',
},
{
component: 'DatePicker',
defaultValue: startDate,
componentProps: {
format: 'YYYY-MM-DD',
},
fieldName: 'StartTime',
label: '开始时间',
formItemClass: 'col-start-1',
},
{
component: 'TimePicker',
componentProps: {
placeholder: '时间',
},
fieldName: 'StartTime',
label: '--',
},
{
component: 'DatePicker',
defaultValue: endDate,
componentProps: {
format: 'YYYY-MM-DD',
},
fieldName: 'EndTime',
label: '结束时间',
},
{
component: 'TimePicker',
componentProps: {
placeholder: '时间',
},
fieldName: 'EndTime',
label: '--',
}
],
//
@ -149,7 +187,7 @@ const gridOptions: VxeGridProps<RowType> = {
let Id = parseInt(formValues.AppId, 10);
let ServerId = parseInt(formValues.ServerId, 10);
let Uid = parseInt(formValues.Uid, 10);
let r = await getUserListApi({ Id: Id, ServerId: ServerId, pageSize: page.pageSize, currentPage: page.currentPage, Uid: Uid });
let r = await getUserListApi({ Id: Id, ServerId: ServerId, pageSize: page.pageSize, currentPage: page.currentPage, Uid: Uid, StartTime: formValues.StartTime ? Math.floor(new Date(formValues.StartTime).getTime() / 1000) : undefined, EndTime: formValues.EndTime ? Math.floor(new Date(formValues.EndTime).getTime() / 1000) : undefined });
return r;
},
},