版本更新
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
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:
parent
6e85e2a0c8
commit
a3a280787e
BIN
apps/web-antd/public/item/LimitedtimeCWJ_pic_daibi.png
Normal file
BIN
apps/web-antd/public/item/LimitedtimeCWJ_pic_daibi.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
BIN
apps/web-antd/public/item/mini-gamesWK_icon_yanzhao.png
Normal file
BIN
apps/web-antd/public/item/mini-gamesWK_icon_yanzhao.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
BIN
apps/web-antd/public/merge/merge_bag.png
Normal file
BIN
apps/web-antd/public/merge/merge_bag.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
@ -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 {
|
||||
|
||||
@ -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', {});
|
||||
}
|
||||
@ -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>
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -27,11 +27,17 @@
|
||||
"eventlog": "事件日志",
|
||||
"orderlog": "订单日志"
|
||||
},
|
||||
"language": {
|
||||
"title": "翻译管理",
|
||||
"languageList": "语言列表",
|
||||
"translationList": "翻译列表"
|
||||
},
|
||||
"operation": {
|
||||
"title": "运营管理",
|
||||
"level": "等级分布",
|
||||
"mail": "邮件管理",
|
||||
"order": "订单管理"
|
||||
"order": "订单管理",
|
||||
"language": "翻译管理"
|
||||
},
|
||||
"log": {
|
||||
"event": {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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'],
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
32
apps/web-antd/src/router/routes/modules/language.ts
Normal file
32
apps/web-antd/src/router/routes/modules/language.ts
Normal 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;
|
||||
@ -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'),
|
||||
},
|
||||
}
|
||||
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@ -10,6 +10,7 @@ const routes: RouteRecordRaw[] = [
|
||||
icon: 'lucide:laugh',
|
||||
order: 1000,
|
||||
title: $t('page.userlog.title'),
|
||||
authority: ['super', 'admin'],
|
||||
},
|
||||
name: 'Userlog',
|
||||
path: '/userlog',
|
||||
|
||||
@ -45,7 +45,6 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
]);
|
||||
|
||||
userInfo = fetchUserInfoResult;
|
||||
|
||||
userStore.setUserInfo(userInfo);
|
||||
accessStore.setAccessCodes(accessCodes);
|
||||
|
||||
|
||||
@ -17,6 +17,7 @@ export const orderTypeData: Record<number, string> = {
|
||||
};
|
||||
|
||||
export const orderDiffData: Record<number, string> = {
|
||||
0:'预设',
|
||||
1: '低难度',
|
||||
2: '中难度',
|
||||
3: '高难度',
|
||||
|
||||
@ -54,6 +54,7 @@ const [Form, FormApi] = useVbenForm({
|
||||
{ label: '超级管理员', value: 0 },
|
||||
{ label: '管理员', value: 1 },
|
||||
{ label: '普通用户', value: 2 },
|
||||
{ label: '外包翻译', value: 99 },
|
||||
],
|
||||
},
|
||||
rules: 'required',
|
||||
|
||||
@ -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>
|
||||
|
||||
101
apps/web-antd/src/views/language/language/addLanguage.vue
Normal file
101
apps/web-antd/src/views/language/language/addLanguage.vue
Normal 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);
|
||||
}
|
||||
},
|
||||
// 水平布局,label和input在同一行
|
||||
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>
|
||||
11
apps/web-antd/src/views/language/language/index.vue
Normal file
11
apps/web-antd/src/views/language/language/index.vue
Normal file
@ -0,0 +1,11 @@
|
||||
<script lang="ts" setup>
|
||||
|
||||
|
||||
import AnalyticsVisitsTable from './language.vue';
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
<template>
|
||||
<AnalyticsVisitsTable />
|
||||
</template>
|
||||
299
apps/web-antd/src/views/language/language/language.vue
Normal file
299
apps/web-antd/src/views/language/language/language.vue
Normal 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>
|
||||
@ -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="
|
||||
|
||||
Loading…
Reference in New Issue
Block a user