版本更新
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-28 14:18:55 +08:00
parent 6e85e2a0c8
commit a3a280787e
21 changed files with 664 additions and 50 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

@ -48,6 +48,13 @@ export interface UserLogInfo {
Order: UserLogOrder[];
ChessMap?:string;
Heatmap?: heatType[];
ActLog?:actlog[];
}
export interface actlog {
Time: number;
Type: number;
Param: string;
}
export interface UserOrder {

View File

@ -1,12 +1,22 @@
import { requestClient } from '#/api/request';
import type { languageRecord, languageType } from '#/model/type';
export interface OperationParam{
AppId: number;
ServerList?: number[];
Emit?:string[];
}
export interface languageParam{
PageSize: number;
CurrentPage: number;
Key?: string;
StartTime?: string;
EndTime?: string;
SearchField?: string;
SearchValue?: string;
}
export async function getStatisticsOrder(data : OperationParam) {
return requestClient.post('/statistics/order', data);
}
@ -21,4 +31,20 @@ export async function getstatisticsInfo(data : OperationParam) {
export async function getstatisticsHeat(data : OperationParam) {
return requestClient.post('/statistics/heat', data, {timeout: 120000});
}
export async function getLanguageList(data: languageParam) {
return requestClient.post('/language/list', data,{});
}
export async function saveLanguageList(data: languageRecord[]) {
return requestClient.post('/language/save', {data:data},{});
}
export async function addLanguageList(data: languageType[]) {
return requestClient.post('/language/add', {data:data},{});
}
export async function exportLanguageFile() {
return requestClient.post('/language/export', {});
}

View File

@ -12,7 +12,7 @@ interface Props {
items: Chess[];
title: string;
}
import { Tag } from 'ant-design-vue';
defineOptions({
name: 'WorkbenchProject',
});
@ -20,18 +20,7 @@ defineOptions({
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>
@ -43,10 +32,12 @@ defineEmits(['click']);
<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()">
:style="{ width: '35px', height: '35px', backgroundColor: index % 2 === 1 ? '#ced1ca' : '#dde0d7' }"
style="position: relative;" :id="index.toString()">
<img v-if="item.Icon" :src="`../../../merge/${item.Icon}.png`"
style="width:100%;height:100%;object-fit:contain;" />
style="width:100%;height:100%;object-fit:contain;position: absolute;" />
<img v-if="item.Lock > 0" :src="`../../../merge/merge_bag.png`"
style="width:100%;height:100%;object-fit:contain;opacity: 0.7;" />
</div>
<div v-if="(index + 1) % 7 === 0" class="w-full h-0" :id="(index + 1).toString()"></div>
</template>

View File

@ -44,7 +44,7 @@ defineEmits(['click']);
<template v-for="(item, index) in items" :key="item.title">
<div :class="{
'border-r-0': index % 3 === 2,
'border-b-0': index < 3,
'border-b-0': !((Math.floor(index / 3) === Math.floor(items.length / 3 - 1)) && (index + 3 >= items.length)),
'pb-4': index > 2,
}"
class="border-border group w-full cursor-pointer border-b border-r border-t p-4 transition-all hover:shadow-xl md:w-1/2 lg:w-1/3">
@ -52,8 +52,8 @@ defineEmits(['click']);
<VbenIcon :color="item.color" :icon="item.icon"
class="size-8 transition-all duration-300 group-hover:scale-110"
@click="$emit('click', item)" />
<span class="ml-4 text-lg font-medium">{{ item.id }}</span>
<tag class="ml-8 text-sm" :color="getTagColor(item.diff)">{{ item.diffName }}</tag>
<span class="ml-2 text-lg font-medium">{{ item.id }}</span>
<tag class="ml-2 text-sm" :color="getTagColor(item.diff)">{{ item.diffName }}</tag>
</div>
<div class="text-foreground/80 mt-4 h-12 ">
<div v-if="item.merge && item.merge.length > 0" class="flex flex-wrap gap-1">

View File

@ -27,11 +27,17 @@
"eventlog": "事件日志",
"orderlog": "订单日志"
},
"language": {
"title": "翻译管理",
"languageList": "语言列表",
"translationList": "翻译列表"
},
"operation": {
"title": "运营管理",
"level": "等级分布",
"mail": "邮件管理",
"order": "订单管理"
"order": "订单管理",
"language": "翻译管理"
},
"log": {
"event": {

View File

@ -46,3 +46,20 @@ export interface Chess{
}
export interface languageType {
Id: number;
key: string;
English: string;
ChineseSimplified: string;
}
export interface languageRecord{
OldValue: string;
NewValue: string;
Id? :string;
LanguageId: number;
Type : string;
Field: string;
Update?: number;
}

View File

@ -10,7 +10,6 @@ const routes: RouteRecordRaw[] = [
icon: 'lucide:layout-dashboard',
order: -1,
title: $t('page.dashboard.title'),
authority: ['super', 'admin'],
},
name: 'Dashboard',
path: '/dashboard',
@ -33,6 +32,7 @@ const routes: RouteRecordRaw[] = [
affixTab: false,
icon: 'lucide:gamepad',
title: $t('page.dashboard.server-list'),
authority: ['super', 'admin'],
},
},
{
@ -43,6 +43,7 @@ const routes: RouteRecordRaw[] = [
affixTab: false,
icon: 'lucide:app-window',
title: $t('page.dashboard.app-list'),
authority: ['super', 'admin'],
},
},
{
@ -53,6 +54,7 @@ const routes: RouteRecordRaw[] = [
affixTab: false,
icon: 'lucide:server',
title: $t('page.dashboard.node-list'),
authority: ['super', 'admin'],
},
},
{
@ -63,6 +65,7 @@ const routes: RouteRecordRaw[] = [
affixTab: false,
icon: 'lucide:database',
title: $t('page.dashboard.mysql-list'),
authority: ['super', 'admin'],
},
},
],

View File

@ -0,0 +1,32 @@
import type { RouteRecordRaw } from 'vue-router';
import { BasicLayout } from '#/layouts';
import { $t } from '#/locales';
const routes: RouteRecordRaw[] = [
{
component: BasicLayout,
meta: {
icon: 'lucide:file-clock',
order: 1001,
title: $t('page.language.title'),
},
name: 'Translate',
path: '/translate',
children: [
{
name: 'Language',
path: '/language',
component: () => import('#/views/language/language/index.vue'),
meta: {
affixTab: true,
icon: 'lets-icons:order',
title: $t('page.language.translationList'),
},
}
],
},
];
export default routes;

View File

@ -10,6 +10,7 @@ const routes: RouteRecordRaw[] = [
icon: 'lucide:file-clock',
order: 1001,
title: $t('page.operation.title'),
authority: ['super', 'admin'],
},
name: 'Operation',
path: '/operation',
@ -22,6 +23,7 @@ const routes: RouteRecordRaw[] = [
affixTab: true,
icon: 'lucide:chart-no-axes-column-increasing',
title: $t('page.operation.level'),
authority: ['super', 'admin'],
},
},
{
@ -44,7 +46,6 @@ const routes: RouteRecordRaw[] = [
title: $t('page.operation.order'),
},
}
],
},
];

View File

@ -10,6 +10,7 @@ const routes: RouteRecordRaw[] = [
icon: 'lucide:laugh',
order: 1000,
title: $t('page.userlog.title'),
authority: ['super', 'admin'],
},
name: 'Userlog',
path: '/userlog',

View File

@ -45,7 +45,6 @@ export const useAuthStore = defineStore('auth', () => {
]);
userInfo = fetchUserInfoResult;
userStore.setUserInfo(userInfo);
accessStore.setAccessCodes(accessCodes);

View File

@ -17,6 +17,7 @@ export const orderTypeData: Record<number, string> = {
};
export const orderDiffData: Record<number, string> = {
0:'预设',
1: '低难度',
2: '中难度',
3: '高难度',

View File

@ -54,6 +54,7 @@ const [Form, FormApi] = useVbenForm({
{ label: '超级管理员', value: 0 },
{ label: '管理员', value: 1 },
{ label: '普通用户', value: 2 },
{ label: '外包翻译', value: 99 },
],
},
rules: 'required',

View File

@ -3,7 +3,10 @@ import { onMounted, ref } from 'vue';
import type { AnalysisOverviewItem } from '@vben/common-ui';
import type { TabOption } from '@vben/types';
import { getstatisticsInfo } from '#/api/core/statistics';
import { AccessControl } from '@vben/access';
import { useAccess } from '@vben/access';
const { hasAccessByRoles } = useAccess();
import {
AnalysisChartCard,
AnalysisChartsTabs,
@ -51,7 +54,7 @@ const overviewItems = ref<AnalysisOverviewItem[]>([
onMounted(async () => {
const data = await getstatisticsInfo({AppId:0});
const data = await getstatisticsInfo({ AppId: 0 });
if (data) {
if (overviewItems.value[0]) {
overviewItems.value[0].value = data.register;
@ -70,7 +73,7 @@ onMounted(async () => {
overviewItems.value[3].totalValue = data.totalRecharge / data.totalRegister;
}
}
});
const chartTabs: TabOption[] = [
{
@ -86,26 +89,30 @@ const chartTabs: TabOption[] = [
<template>
<div class="p-5">
<AnalysisOverview :items="overviewItems" />
<AnalysisChartsTabs :tabs="chartTabs" class="mt-5">
<template #trends>
<AnalyticsTrends />
</template>
<template #visits>
<AnalyticsVisits />
</template>
<AccessControl :codes="['super', 'admin']" type="role">
<AnalysisOverview :items="overviewItems" />
</AnalysisChartsTabs>
<div class="mt-5 w-full md:flex">
<AnalysisChartCard class="mt-5 md:mr-4 md:mt-0 md:w-1/3" title="访问数量">
<AnalyticsVisitsData />
</AnalysisChartCard>
<AnalysisChartCard class="mt-5 md:mr-4 md:mt-0 md:w-1/3" title="访问来源">
<AnalyticsVisitsSource />
</AnalysisChartCard>
<AnalysisChartCard class="mt-5 md:mt-0 md:w-1/3" title="访问来源">
<AnalyticsVisitsSales />
</AnalysisChartCard>
</div>
<AnalysisChartsTabs :tabs="chartTabs" class="mt-5">
<template #trends>
<AnalyticsTrends />
</template>
<template #visits>
<AnalyticsVisits />
</template>
</AnalysisChartsTabs>
<div class="mt-5 w-full md:flex">
<AnalysisChartCard class="mt-5 md:mr-4 md:mt-0 md:w-1/3" title="访问数量">
<AnalyticsVisitsData />
</AnalysisChartCard>
<AnalysisChartCard class="mt-5 md:mr-4 md:mt-0 md:w-1/3" title="访问来源">
<AnalyticsVisitsSource />
</AnalysisChartCard>
<AnalysisChartCard class="mt-5 md:mt-0 md:w-1/3" title="访问来源">
<AnalyticsVisitsSales />
</AnalysisChartCard>
</div>
</AccessControl>
</div>
</template>

View File

@ -0,0 +1,101 @@
<script lang="ts" setup>
import { useVbenForm, useVbenModal } from '@vben/common-ui';
import type { languageType } from '#/model/type';
import type { VxeGridProps } from '#/adapter/vxe-table';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { addLanguageList } from '#/api/core/statistics';
defineOptions({
name: 'AddLanguageModal',
});
let ld = [] as languageType[];
const gridOptions: VxeGridProps<languageType> = {
border: true,
columns: [
{ title: '序号', field: 'Id', width: 50 },
{ editRender: { name: 'input' }, field: 'key', title: '键值' },
{ editRender: { name: 'input' }, field: 'English', title: '英文' },
{
editRender: { name: 'input' },
field: 'ChineseSimplified',
title: '简体中文',
},
{ editRender: { name: 'input' }, field: 'Portuguese', title: '葡萄牙语' },
],
data: ld,
pagerConfig: {
enabled: false,
}
};
const [Grid, GridApi] = useVbenVxeGrid({ gridOptions });
const [Form] = useVbenForm({
//
commonConfig: {
//
componentProps: {
class: 'w-full h-full',
},
},
layout: 'horizontal',
showDefaultActions: true,
resetButtonOptions: {
show: false,
},
handleSubmit: async (formValues) => {
console.log('Submitting form with values:', formValues);
console.log('Language data to add:', ld);
//
try {
await addLanguageList(ld);
modalApi.close();
} catch (error) {
console.error('Error adding languages:', error);
}
},
// labelinput
schema: [
{
component: 'Textarea',
fieldName: 'ToUids',
label: '新增翻译',
componentProps: {
disabled: false,
placeholder: 'key | English | 简体中文| 葡萄牙语,一行一条记录',
type: 'textarea',
rows: 8,
onChange: (e: Event) => {
ld = [] as languageType[];
const target = e.target as HTMLTextAreaElement;
const value = target.value;
const lines = value.split('\n');
const newData: languageType[] = lines.map((line, index) => {
const parts = line.split('|').map(part => part.trim());
return {
Id: (ld.length + index + 1),
key: parts[0] || '',
English: parts[1] || '',
ChineseSimplified: parts[2] || '',
Portuguese: parts[3] || '',
};
});
ld = ld.concat(newData);
GridApi.setGridOptions({
data: newData,
});
},
},
},
],
});
const [Modal, modalApi] = useVbenModal({
confirmText: '提交',
showConfirmButton: false,
fullscreen: true,
});
</script>
<template>
<Modal width="100%" title="添加翻译">
<Form />
<Grid />
</Modal>
</template>

View File

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

View File

@ -0,0 +1,299 @@
<script lang="ts" setup>
import type { VxeGridProps, VxeGridListeners } from '#/adapter/vxe-table';
import { Button } from 'ant-design-vue';
import { Page } from '@vben/common-ui';
import { getLanguageList, exportLanguageFile, saveLanguageList } from '#/api/core/statistics';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import addLanguage from './addLanguage.vue';
import type { VbenFormProps } from '#/adapter/form';
import type { languageType, languageRecord } from '#/model/type';
import type { languageParam } from '#/api/core/statistics';
import { useVbenModal } from '@vben/common-ui';
import dayjs from 'dayjs';
const [AddLanguageModal, AddLanguageModalApi] = useVbenModal({
connectedComponent: addLanguage,
});
let oldData: languageType[] = [];
let newData: languageType[] = [];
let op: languageRecord[] = [];
let lastOp: languageRecord[] = [];
let lastUpdate: string = '';
const startDate = dayjs().subtract(7, 'day').startOf('day');
const endDate = dayjs().endOf('day');
const formOptions: VbenFormProps = {
//
collapsed: false,
schema: [
{
component: 'Select',
componentProps: {
filterOption: true,
options: [
{ label: '键值', value: 'key' },
{ label: '英文', value: 'English' },
{ label: '简体中文', value: 'ChineseSimplified' },
{ label: '葡萄牙语', value: 'Portuguese' },
],
placeholder: 'key',
showSearch: true,
},
fieldName: 'SearchField',
label: '搜索列:',
},
{
component: 'Input',
componentProps: {
filterOption: true,
placeholder: 'Merge',
showSearch: true,
},
fieldName: 'SearchValue',
label: '值:',
},
{
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: '--',
}
],
//
showCollapseButton: true,
submitButtonOptions: {
content: '查询',
},
wrapperClass: 'grid-cols-1 md:grid-cols-5',
//
submitOnChange: true,
//
submitOnEnter: false,
}
const gridOptions: VxeGridProps<languageType> = {
border: true,
columns: [
{ title: '序号', field: 'Id', width: 50 },
{ editRender: { name: 'input' }, field: 'key', title: '键值' },
{ editRender: { name: 'input' }, field: 'English', title: '英文' },
{
editRender: { name: 'input' },
field: 'ChineseSimplified',
title: '简体中文',
},
{ editRender: { name: 'input' }, field: 'Portuguese', title: '葡萄牙语' },
],
toolbarConfig: {
custom: true,
refresh: true,
zoom: true,
},
rowClassName: ({ rowIndex }: { rowIndex: number }) => {
const i = oldData[rowIndex];
if (!i) {
return 'row-yellow';
}
const info = lastOp.find((item) => String(item.LanguageId) == String(i.Id) && item.Type === 'Add');
if (info) {
return 'row-yellow';
}
},
cellClassName: ({ row, column }) => {
const info = oldData.find((item) => item.Id === row.Id);
if (info) {
const prop = (column.field || column.field) as keyof languageType;
const oldValue = info[prop];
const newValue = row[prop];
if (oldValue != newValue) {
console.log('Cell changed2:', prop, 'from', oldValue, 'to', newValue);
return 'row-green';
}
if (lastOp.find(opItem => opItem.LanguageId === row.Id && opItem.Field === prop && opItem.Type === 'Edit')) {
return 'row-green';
}
}
},
proxyConfig: {
response: {
total: "total",
result: "data"
},
ajax: {
query: async ({ page }, formValues) => {
const response = await getLanguageList({
PageSize: page.pageSize,
CurrentPage: page.currentPage,
StartTime: formValues.StartTime ? Math.floor(new Date(formValues.StartTime).getTime() / 1000) : undefined,
EndTime: formValues.EndTime ? Math.floor(new Date(formValues.EndTime).getTime() / 1000) : undefined,
SearchField: formValues.SearchField,
SearchValue: formValues.SearchValue,
} as languageParam);
newData = response.data || [];
oldData = newData.map(item => ({ ...item }));
console.log('API response:', response);
lastOp = response.op || [];
// lastUpdate lastOp update YYYY - MM - DD HH: mm: ss
if (lastOp && lastOp.length > 0) {
const last = lastOp[lastOp.length - 1];
const raw = last?.Update;
if (raw != null) {
let ts: any = raw;
//
if (typeof ts === 'string' && /^\d+$/.test(ts)) ts = Number(ts);
// 10
if (typeof ts === 'number' && String(ts).length === 10) ts = ts * 1000;
lastUpdate = dayjs(ts).format('YYYY-MM-DD HH:mm:ss');
} else {
lastUpdate = '';
}
} else {
lastUpdate = '';
}
return {
data: response.data || [],
total: response.total || 0,
};
}
}
},
editConfig: {
mode: 'cell',
trigger: 'dblclick',
},
pagerConfig: {
enabled: true,
pageSize: 80,
},
height: 'auto',
showOverflow: true,
};
const gridEvents: VxeGridListeners<languageType> = {
editClosed: ({ row, column }) => {
const info = oldData.find((item) => item.Id === row.Id);
if (info) {
const prop = (column.field || column.field) as keyof languageType;
const oldValue = info[prop];
const newValue = row[prop];
if (oldValue !== newValue) {
console.log('Cell changed:', prop, 'from', oldValue, 'to', newValue);
// newData
const newItem = newData.find((item) => item.Id === row.Id);
if (newItem) {
op.push({
LanguageId: row.Id,
Field: String(prop),
OldValue: oldValue,
NewValue: newValue,
Type: 'Edit',
} as languageRecord);
Object.assign(newItem, row);
} else {
newData.push({ ...(row as languageType) });
}
} else {
console.log('Cell not changed:', prop);
}
}
//console.log('editClosed:', row, column);
},
};
const [Grid, GridApi] = useVbenVxeGrid({ formOptions, gridOptions, gridEvents });
function addRow() {
AddLanguageModalApi.open();
}
function saveAll() {
saveLanguageList(op).then((response) => {
console.log('Save response:', response);
op = [];
GridApi.reload();
});
}
function downloadCSV(data: languageType[]) {
const headers = ['Id', 'key', 'English', 'ChineseSimplified'];
const csvRows = [];
csvRows.push(headers.join(','));
for (const row of data) {
const values = headers.map(header => {
const escaped = (row as any)[header]?.toString().replace(/"/g, '""') || '';
return `"${escaped}"`;
});
csvRows.push(values.join(','));
}
const csvContent = csvRows.join('\n');
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
const url = URL.createObjectURL(blob);
link.setAttribute('href', url);
link.setAttribute('download', 'language_export.csv');
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
function exportLang() {
exportLanguageFile().then((response) => {
// downloadCSV(response.data);
});
}
</script>
<style lang="css">
.row-green {
background-color: #1ecf0d;
}
.row-yellow {
background-color: #f2ff00;
}
</style>
<template>
<Page auto-content-height>
<AddLanguageModal width="1200px" title="添加翻译"></AddLanguageModal />
<Grid>
<template #toolbar-tools>
<span class="mr-4 font-bold">最后修改日期{{ lastUpdate }}</span>
<Button class="mr-2" type="primary" @click="addRow">
新增行
</Button>
<Button type="primary" @click="saveAll" class="mr-2"> 保存 </Button>
<Button type="primary" @click="exportLang"> git提交 </Button>
</template>
</Grid>
</Page>
</template>

View File

@ -3,7 +3,7 @@ import { ref, watch } from 'vue';
import MergeData from '#/store/MergeData.json';
import { orderTypeData, orderDiffData } from '#/store/order';
import { faceTypeData } from '#/store/face';
import { useVbenModal, useVbenForm } from '@vben/common-ui';
import { useVbenModal, useVbenForm, WorkbenchTrends } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { getUserlogInfoApi } from '#/api/core/log';
import { userGmApi, userBanApi } from '#/api/core/user';
@ -11,7 +11,7 @@ import { calendar } from '#/component/index';
import type { dataType } from '#/component/index';
// cal-heatmap
import 'cal-heatmap/cal-heatmap.css';
import type { WorkbenchProjectItem } from '@vben/common-ui';
import type { WorkbenchProjectItem, WorkbenchTrendItem } from '@vben/common-ui';
import { orderComponent, chessComponent } from '#/component/index';
import type { Order, Merge, Chess } from '#/model/type';
import dayjs from 'dayjs';
@ -162,6 +162,14 @@ const info = ref<{
Heatmap: [],
Chess: [],
});
let trendItems: WorkbenchTrendItem[] = [
{
avatar: 'svg:avatar-1',
content: `在 <a>开源组</a> 创建了项目 <a>Vue</a>`,
date: '刚刚',
title: '威廉',
}
];
const [Modal, modalApi] = useVbenModal({
onCancel() {
modalApi.close();
@ -180,7 +188,18 @@ const [Modal, modalApi] = useVbenModal({
PageSize: 10, // replace 10 with the actual page size
CurrentPage: 1, // replace 1 with the actual current page
});
trendItems = [];
if (r.ActLog) {
for (const logEntry of r.ActLog.reverse()) {
let [title, content] = formatActLog(logEntry.Type, logEntry.Param)
trendItems.push({
avatar: 'svg:avatar-1',
content: content || '',
date: dayjs(logEntry.Time * 1000).format('YYYY-MM-DD HH:mm:ss'),
title: title || '未知玩家',
});
}
}
info.value.Uid = data.value.uid;
info.value.Level = r.Level;
info.value.Star = r.Star;
@ -272,6 +291,96 @@ const [Modal, modalApi] = useVbenModal({
},
});
function formatActLog(type: number, content = ''): [string, string] {
const splitPair = (s: string): [string, string] => {
if (!s) return ['', ''];
const parts = s.split('|');
return [parts[0] ?? '', parts[1] ?? ''];
};
const rankText = (s: string) => {
const n = Number(s);
return Number.isFinite(n) && n > 0 ? `在锦标赛中获得了第${n}名!` : `在锦标赛中获得了第${s || 'X'}名!`;
};
const activityRewardText = (s: string) => {
const [act, reward] = splitPair(s);
if (act && reward) return `${act}活动中获得了${reward}`;
if (act) return `${act}活动中获得了奖励!`;
return `在活动中获得了奖励!`;
};
switch (type) {
case 1:
return ['首次登入游戏', '加入了拯救小猫的行列!'];
case 2:
return ['完成休息室', '为小猫建造了一个温暖的家!'];
case 3:
return ['完成餐厅', '为小猫准备了丰盛的食物!'];
case 4:
return ['完成浴室', '把小猫整理得香喷喷的!'];
case 5:
return ['完成衣帽间', '把小猫打扮得漂漂亮亮的!'];
case 6:
return ['获得新头像', '收藏了一个新的头像!'];
case 7:
return ['获得新头像框', '收藏了一个新的头像框!'];
case 8:
return ['获得新表情', '收藏了一个新的表情!'];
case 9:
return ['获得新装饰品', '获得了新的房间装饰!'];
case 10:
return ['获得新服装', '获得了漂漂亮亮的新衣服!'];
case 11: {
const subject = content || '某个角色';
return ['完成卡册收集', `收集了${subject}的所有卡牌!`];
}
case 12: {
const subject = content || '某个系列';
return ['完成全卡牌收集', `收集了${subject}的所有卡牌!`];
}
case 13:
return ['获得锦标赛名次', rankText(content)];
case 14:
return ['获得锦标赛大奖', '完成了锦标赛!'];
case 15:
return ['获得限时活动大奖', activityRewardText(content)];
case 16:
return ['参加好友合作类活动', '参加了合作伙伴活动!'];
case 17:
return ['获得拜访小游戏大奖', '把邻居家的小猫关了起来!'];
case 18:
return ['获得拜访小游戏大奖', '薅走了邻居的宠物币!'];
case 19: {
const who = content || '好友们';
return ['打开宠物宝藏', `${who}一起找到了宝藏!`];
}
case 20:
return ['拜访时点赞', '给邻居点了个大大的赞!'];
case 21: {
const subject = content || '某个系列';
return ['完成图鉴收集成就', `收集了${subject}的所有物品!`];
}
case 22: {
const chap = content || 'X';
return [`完成第${chap}章所有场景`, `将章节${chap}的场景变得焕然一新!`];
}
case 23:
return ['流失用户回归', '回来看小猫了!'];
//
case 4_00:
return ['发表文章', `发表文章 <a>${content}</a>`];
case 5_00:
return ['回复问题', `回复了 <a>某人</a> 的问题 <a>${content}</a>`];
case 6_00:
return ['关闭问题', `关闭了问题 <a>${content}</a>`];
case 7_00:
return ['代码推送', `推送了代码到 <a>${content}</a>`];
default:
return ['未知行为', content || ''];
}
}
//
watch(
() => info.value.Heatmap,
@ -282,6 +391,7 @@ watch(
},
{ deep: true },
);
</script>
<template>
<Modal title="玩家详情">
@ -314,7 +424,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="订单" />
<chessComponent :items="info.Chess" title="棋盘" class="mt-6" />
<WorkbenchTrends :items="trendItems" class="mt-5" title="最新动态" />
</div>
<div class="w-full lg:w-2/5">
<WorkbenchDetail :items="projectItems" class="mt-5 lg:mt-0" title="玩家详情">
@ -327,6 +437,7 @@ watch(
<template #Code>{{ info.Code || 0 }}</template>
<template #TodayCumulative>{{ info.TodayCumulative }}</template>
</WorkbenchDetail>
<chessComponent :items="info.Chess" title="棋盘" class="mt-6" />
<calendar v-if="info.Heatmap.length > 0" style="margin-top: 15px" :dataList="info.Heatmap" title="热力图"
:key="`heatmap-${info.Uid}`" />
<div v-else style="