版本更新
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-04-14 16:16:52 +08:00
parent 337f15b252
commit 717f5a3c34
24 changed files with 742 additions and 305 deletions

View File

@ -11,6 +11,15 @@
"url": "http://localhost:5666", "url": "http://localhost:5666",
"webRoot": "${workspaceFolder}", "webRoot": "${workspaceFolder}",
"runtimeArgs": ["--do-not-de-elevate"] "runtimeArgs": ["--do-not-de-elevate"]
} },
{
"type": "chrome",
"name": "vben admin antd dev",
"request": "launch",
"url": "http://localhost:5666",
"env": { "NODE_ENV": "development" },
"sourceMaps": true,
"webRoot": "${workspaceFolder}"
},
] ]
} }

View File

@ -12,6 +12,9 @@ export interface ActivityData {
cfg?: string; cfg?: string;
extra?: string; extra?: string;
tag?: string; tag?: string;
interval?: number;
now_start_time?: number;
now_end_time?: number;
} }
export interface ActivityListParam { export interface ActivityListParam {
@ -32,6 +35,13 @@ export interface ResgetActivityListApi{
data: ActivityData[]; data: ActivityData[];
} }
export interface SyncActivityParam {
SrcAppId : number;
DstAppId : number;
}
export async function syncActivityApi(p: SyncActivityParam) {
return requestClient.post('/activity/sync', p);
}
export async function getActivityListApi(p:ActivityListParam) { export async function getActivityListApi(p:ActivityListParam) {
return requestClient.post<ResgetActivityListApi>('/activity/list', p); return requestClient.post<ResgetActivityListApi>('/activity/list', p);
@ -41,7 +51,6 @@ export async function editActivityApi(p: EditActivityParam) {
return requestClient.post('/activity/edit', p); return requestClient.post('/activity/edit', p);
} }
export async function addActivityApi(p: EditActivityParam) { export async function addActivityApi(p: EditActivityParam) {
return requestClient.post('/activity/add', p); return requestClient.post('/activity/add', p);
} }

View File

@ -10,6 +10,19 @@ export interface AdminLogParam {
pageSize?: number; pageSize?: number;
opration?: string; opration?: string;
} }
export interface AdminConfig {
id ?: number;
key: string;
value: string;
remark?: string;
}
export interface AdminConfigListParam{
page?: number;
pageSize?: number;
}
export async function getAdminInfoApi(param: UserParam) { export async function getAdminInfoApi(param: UserParam) {
return requestClient.post<UserInfo>('/admin/info', param); return requestClient.post<UserInfo>('/admin/info', param);
} }
@ -26,3 +39,15 @@ export async function getAdminLogListApi(param:AdminLogParam) {
export async function addAdminApi(param: UserInfo) { export async function addAdminApi(param: UserInfo) {
return requestClient.post<UserInfo>('/admin/add', param); return requestClient.post<UserInfo>('/admin/add', param);
} }
export async function getAdminConfigList(param:AdminConfigListParam) {
return requestClient.post<AdminConfig>('/admin/config/list', param);
}
export async function addAdminConfig(param:AdminConfig) {
return requestClient.post('/admin/config/add', param);
}
export async function editAdminConfig(param:AdminConfig) {
return requestClient.post('/admin/config/edit', param);
}

View File

@ -18,6 +18,7 @@ export interface languageParam{
len_limit?: string; len_limit?: string;
} }
export async function getStatisticsOrder(data : OperationParam) { export async function getStatisticsOrder(data : OperationParam) {
return requestClient.post('/statistics/order', data); return requestClient.post('/statistics/order', data);
} }

View File

@ -13,11 +13,11 @@ defineProps({
<template> <template>
<div class="flex items-center p-4 border-b border-black pl-12 ml-4 mr-4 mt-2 w-[60%]" style="background: rgb(255, 254, 231);border-radius: 1cm; padding: 1px 10px 1px 10px;justify-content: space-between;"> <div class="flex items-center p-4 border-b border-black pl-12 ml-4 mr-4 mt-2 w-[60%]" style="background: rgb(255, 254, 231);border-radius: 1cm; padding: 1px 10px 1px 10px;justify-content: space-between;">
<VbenAvatar :src="'./' + friend.avatarUrl" class="w-12 h-12 rounded-full " /> <VbenAvatar :src="'./' + friend.AvatarUrl" class="w-12 h-12 rounded-full " />
<div class="ml-5 w-full"> <div class="ml-5 w-full">
<div class="flex flex-row items-center w-full"> <div class="flex flex-row items-center w-full">
<h2 class="text-sm font-semibold text-black">{{ friend.NickName }}</h2> <h2 class="text-sm font-semibold text-black">{{ friend.NickName }}</h2>
<Tag class="text-xs ml-2" color="green" v-if="friend.onlineStatus">在线</Tag> <Tag class="text-xs ml-2" color="green" v-if="friend.OnlineStatus">在线</Tag>
<Tag class="text-xs ml-2" color="red" v-else>离线</Tag> <Tag class="text-xs ml-2" color="red" v-else>离线</Tag>
</div> </div>
<p class="text-xs text-gray-500">uid:{{ friend.Uid }}</p> <p class="text-xs text-gray-500">uid:{{ friend.Uid }}</p>

View File

@ -24,7 +24,8 @@
"title": "管理中心", "title": "管理中心",
"user": "用户管理", "user": "用户管理",
"setting": "系统设置", "setting": "系统设置",
"log": "操作日志" "log": "操作日志",
"config": "配置管理"
}, },
"dashboard": { "dashboard": {
"title": "运维管理", "title": "运维管理",

View File

@ -57,6 +57,8 @@ export interface languageType {
pt_BR: string; pt_BR: string;
es_LATAM?: string; es_LATAM?: string;
url?: string; url?: string;
len_limit?: number;
character_limit?: string;
} }
@ -88,9 +90,9 @@ export interface scriptsRecord{
export interface friendRecord{ export interface friendRecord{
Uid: number; Uid: number;
NickName: string; NickName: string;
avatarUrl: string; AvatarUrl: string;
Level: number; Level: number;
LogoutTime?: string; LogoutTime?: string;
LoginTime: string; LoginTime: string;
onlineStatus?: boolean; OnlineStatus?: boolean;
} }

View File

@ -37,6 +37,17 @@ const routes: RouteRecordRaw[] = [
title: $t('page.admin.log'), title: $t('page.admin.log'),
}, },
}, },
{
name: 'UserManagementConfig',
path: '/user-management-config',
component: () => import('#/views/admin/config/index.vue'),
meta: {
authority: ['super', 'admin'],
affixTab: false,
icon: 'material-symbols:assignment-rounded',
title: $t('page.admin.config'),
},
},
], ],
}, },
]; ];

View File

@ -0,0 +1,26 @@
export const languageLimitData: Record<string, languageLimit> = {
'^UI_MergeData_(\\d+)$': { zh_CN: 20, latin:20 },
'UI_MainLevelPanel_title': { zh_CN:16, latin: 16 },
}
export interface languageLimit{
key?: string;
zh_CN?: number;
latin?: number;
}
/**
* 2 1
*/
export function getWeightedTextLength(text: string): number {
if (!text) {
return 0;
}
let length = 0;
for (const ch of text) {
length += /[\u3400-\u4DBF\u4E00-\u9FFF\uF900-\uFAFF]/.test(ch) ? 2 : 1;
}
return length;
}

View File

@ -37,4 +37,7 @@ export const activityTypeData: Record<number, string> = {
5: '买一赠一礼包', 5: '买一赠一礼包',
6: '超值加购礼包', 6: '超值加购礼包',
7: '好友合作活动', 7: '好友合作活动',
8: '通行证',
9: '锦标赛',
10: '猫猫回礼',
}; };

View File

@ -118,3 +118,14 @@ export const parseNumber = (param: any) => {
return 0; return 0;
} }
} }
export const formatJsonStr = (jsonStr: string): string => {
try {
return JSON.stringify(JSON.parse(jsonStr))
.replace(/\\r?\\n/g, '')
.replace(/\r?\n/g, '');
} catch (e) {
console.error('Failed to format JSON string:', e);
return jsonStr;
}
};

View File

@ -0,0 +1,91 @@
<script lang="ts" setup>
import { addAdminConfig } from '#/api/core/admin.user';
import { useVbenModal } from '@vben/common-ui';
import { useVbenForm } from '#/adapter/form';
import type { AdminConfig } from '#/api/core/admin.user';
import JsonEitorVue from 'json-editor-vue';
import {ref} from 'vue';
const config_value = ref();
const [Form, FormApi] = useVbenForm({
// s
commonConfig: {
//
componentProps: {
class: 'w-full',
},
},
// 使 tailwindcss grid
//
// labelinputvertical
layout: 'horizontal',
// labelinput
schema: [
{
component: 'Input',
componentProps: {
placeholder: 'key',
},
formItemClass: 'col-span-2',
fieldName: 'key',
rules: 'required',
label: 'Key',
},
{
component: 'Input',
componentProps: {},
rules: 'required',
fieldName: 'value',
label: '值',
formItemClass: 'col-span-2',
},
{
component: 'Textarea',
componentProps: {},
rules: 'required',
fieldName: 'remark',
label: '备注',
formItemClass: 'col-span-2',
},
],
showDefaultActions: false,
// 321
wrapperClass: 'grid-cols-2',
});
const [Modal, modalApi] = useVbenModal({
confirmText: '提交',
onOpenChange: () => { },
onConfirm: async () => {
//
const values = await FormApi.getValues();
const cfgjson =
typeof config_value.value === 'string'
? JSON.parse(config_value.value)
: config_value.value;
const jsonString = JSON.stringify(cfgjson);
const Parms: AdminConfig = {
key: values.key,
value: jsonString,
remark: values.remark,
};
await addAdminConfig(Parms);
modalApi.close();
},
});
defineOptions({
name: 'AddServerModal',
});
defineExpose({
FormApi,
});
</script>
<template>
<Modal title="添加配置" :width="800">
<Form >
<template #value>
<JsonEitorVue v-model="config_value" v-bind="{/* local props & attrs */}" class="w-[100%] h-[500px]"/>
</template>
</Form>>
</Modal>
</template>

View File

@ -0,0 +1,88 @@
<script setup lang="ts">
import { Page } from '@vben/common-ui';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { Button, Card, Space } from 'ant-design-vue';
import type { VxeGridProps } from '#/adapter/vxe-table';
import type { AdminConfig } from '#/api/core/admin.user';
import { getAdminConfigList } from '#/api/core/admin.user';
import { useVbenModal } from '@vben/common-ui';
import addConfigModal from './add-config.vue';
import editConfigModal from './edit-config.vue';
const gridOptions: VxeGridProps<AdminConfig> = {
columns: [
{ field: 'key', title: 'Key', },
{ field: 'value', title: '值' },
{ field: 'remark', title: '备注', },
{title: '操作',width: 200,fixed: 'right',slots: {default:"operation"},},
],
height: 'auto',
pagerConfig: {},
proxyConfig: {
response: {
total: 'total',
result: 'data',
},
ajax: {
query: async ( { page }: { page: { pageSize: number; currentPage: number } },
formValues: Record<string, any>,) => {
return await getAdminConfigList({
page: page.currentPage,
pageSize: page.pageSize,
});
},
},
},
rowConfig: {
isHover: true,
},
};
const [Grid, GridApi] = useVbenVxeGrid({ gridOptions });
const [addConfigM, addConfigApi] = useVbenModal({
connectedComponent: addConfigModal,
onClosed: async () => {
addConfigApi.close();
GridApi.reload();
},
});
const addConfig = () => {
addConfigApi.open();
};
const [editConfigM, editConfigApi] = useVbenModal({
connectedComponent: editConfigModal,
onClosed: async () => {
editConfigApi.close();
GridApi.reload();
},
});
const editConfig = (row: AdminConfig) => {
editConfigApi.setData(row);
editConfigApi.open();
};
const deleteConfig = (row: AdminConfig) => {
//
console.log('删除配置', row);
};
</script>
<template>
<Page auto-content-height class="h-[1200px]">
<addConfigM class="w-[50%]" />
<editConfigM class="w-[50%]" />
<Card class="mb-5" title="配置操作">
<Space>
<Button @click="addConfig">新增</Button>
<Button> 删除 </Button>
</Space>
</Card>
<Grid>
<template #operation="{ row }">
<Space>
<Button type="primary" @click="editConfig(row)">编辑</Button>
<Button type="dashed" @click="deleteConfig(row)">删除</Button>
</Space>
</template>
</Grid>>
</Page>
</template>

View File

@ -0,0 +1,104 @@
<script lang="ts" setup>
import { editAdminConfig } from '#/api/core/admin.user';
import { useVbenModal } from '@vben/common-ui';
import { useVbenForm } from '#/adapter/form';
import type { AdminConfig } from '#/api/core/admin.user';
import JsonEitorVue from 'json-editor-vue';
import {ref} from 'vue';
const config_value = ref();
const config_id = ref<number>();
const [Form, FormApi] = useVbenForm({
// s
commonConfig: {
//
componentProps: {
class: 'w-full',
},
},
// 使 tailwindcss grid
//
// labelinputvertical
layout: 'horizontal',
// labelinput
schema: [
{
component: 'Input',
componentProps: {
placeholder: 'key',
},
formItemClass: 'col-span-2',
fieldName: 'key',
rules: 'required',
label: 'Key',
},
{
component: 'Input',
componentProps: {},
rules: 'required',
fieldName: 'value',
label: '值',
formItemClass: 'col-span-2',
},
{
component: 'Textarea',
componentProps: {
rows: 8,
},
rules: 'required',
fieldName: 'remark',
label: '备注',
formItemClass: 'col-span-2',
},
],
showDefaultActions: false,
// 321
wrapperClass: 'grid-cols-2',
});
const [Modal, modalApi] = useVbenModal({
confirmText: '提交',
onOpenChange: () => {
const data = modalApi.getData();
if (data) {
const jsonData = typeof data.value === 'string' ? JSON.parse(data.value) : data.value;
config_value.value = jsonData;
config_id.value = data.id;
FormApi.setValues({
key: data.key,
remark: data.remark,
});
}
},
onConfirm: async () => {
//
const values = await FormApi.getValues();
const cfgjson =
typeof config_value.value === 'string'
? JSON.parse(config_value.value)
: config_value.value;
const jsonString = JSON.stringify(cfgjson);
const Parms: AdminConfig = {
id: config_id.value,
key: values.key,
value: jsonString,
remark: values.remark,
};
await editAdminConfig(Parms);
modalApi.close();
},
});
defineOptions({
name: 'EditConfigModal',
});
</script>
<template>
<Modal title="编辑配置" :width="800">
<Form >
<template #value>
<JsonEitorVue v-model="config_value" v-bind="{/* local props & attrs */}" class="w-[100%] h-[500px]"/>
</template>
</Form>
</Modal>
</template>

View File

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

View File

@ -3,7 +3,6 @@ import { addAdminApi } from '#/api/core/admin.user';
import { useVbenModal } from '@vben/common-ui'; import { useVbenModal } from '@vben/common-ui';
import { useVbenForm } from '#/adapter/form'; import { useVbenForm } from '#/adapter/form';
import type { UserInfo } from '#/model/admin.user'; import type { UserInfo } from '#/model/admin.user';
import dayjs from 'dayjs';
const [Form, FormApi] = useVbenForm({ const [Form, FormApi] = useVbenForm({
// s // s
@ -105,7 +104,7 @@ const [Modal, modalApi] = useVbenModal({
}, },
}); });
defineOptions({ defineOptions({
name: 'AddServerModal', name: 'AddUserModal',
}); });
defineExpose({ defineExpose({
FormApi, FormApi,
@ -113,7 +112,7 @@ defineExpose({
</script> </script>
<template> <template>
<Modal title="添加服务器" :width="800"> <Modal title="添加用户" :width="800">
<Form /> <Form />
</Modal> </Modal>
</template> </template>

View File

@ -14,7 +14,10 @@ import dayjs from 'dayjs';
import { AccessControl } from '@vben/access'; import { AccessControl } from '@vben/access';
import { useAccess } from '@vben/access'; import { useAccess } from '@vben/access';
import GlossaryData from '#/store/glossary.json'; import GlossaryData from '#/store/glossary.json';
import { ref } from 'vue'; import { ref, onMounted, onUnmounted } from 'vue';
import { parseNumber } from '#/store/util';
import {getWeightedTextLength} from '#/store/language';
import type { languageLimit } from '#/store/language';
const { hasAccessByRoles } = useAccess(); const { hasAccessByRoles } = useAccess();
const [AddLanguageModal, AddLanguageModalApi] = useVbenModal({ const [AddLanguageModal, AddLanguageModalApi] = useVbenModal({
connectedComponent: addLanguage, connectedComponent: addLanguage,
@ -40,6 +43,7 @@ let total: languageType = {
es_LATAM: '', es_LATAM: '',
}; };
const len_limit = ref<string>(''); const len_limit = ref<string>('');
const language_len_limit = ref<languageLimit[]>([]);
const startDate = dayjs().subtract(7, 'day').startOf('day'); const startDate = dayjs().subtract(7, 'day').startOf('day');
const endDate = dayjs().endOf('day'); const endDate = dayjs().endOf('day');
const formOptions: VbenFormProps = { const formOptions: VbenFormProps = {
@ -96,9 +100,10 @@ const formOptions: VbenFormProps = {
}, },
{ {
component: 'Select', component: 'Select',
defaultValue: false, defaultValue: '',
componentProps: { componentProps: {
options: [ options: [
{ label: 'no_limit', value: '' },
{ label: 'en_US', value: 'en_US' }, { label: 'en_US', value: 'en_US' },
{ label: 'zh_CN', value: 'zh_CN' }, { label: 'zh_CN', value: 'zh_CN' },
{ label: 'pt_BR', value: 'pt_BR' }, { label: 'pt_BR', value: 'pt_BR' },
@ -177,16 +182,16 @@ const gridOptions: VxeGridProps<languageType> = {
return 'row-green'; return 'row-green';
} }
if (len_limit.value === 'zh_CN' && column.field === 'zh_CN') { if (len_limit.value === 'zh_CN' && column.field === 'zh_CN') {
return 'row-yellow'; return 'row-blue';
} }
if (len_limit.value === 'en_US' && column.field === 'en_US') { if (len_limit.value === 'en_US' && column.field === 'en_US') {
return 'row-yellow'; return 'row-blue';
} }
if (len_limit.value === 'pt_BR' && column.field === 'pt_BR') { if (len_limit.value === 'pt_BR' && column.field === 'pt_BR') {
return 'row-yellow'; return 'row-blue';
} }
if (len_limit.value === 'es_LATAM' && column.field === 'es_LATAM') { if (len_limit.value === 'es_LATAM' && column.field === 'es_LATAM') {
return 'row-yellow'; return 'row-blue';
} }
} }
}, },
@ -209,13 +214,16 @@ const gridOptions: VxeGridProps<languageType> = {
len_limit.value = formValues.len_limit || ''; len_limit.value = formValues.len_limit || '';
newData = response.data || []; newData = response.data || [];
columnStr = response.column || ""; columnStr = response.column || "";
language_len_limit.value = response.language_len_limit || [];
if (columnStr && columnStr.length > 0) { if (columnStr && columnStr.length > 0) {
const cols = columnStr.split(','); const cols = columnStr.split(',');
keyVisible = cols.includes('key'); keyVisible = cols.includes('key');
en_USVisible = cols.includes('en_US'); en_USVisible = cols.includes('en_US');
chineseVisible = cols.includes('zh_CN'); chineseVisible = cols.includes('zh_CN');
pt_BRVisible = cols.includes('pt_BR'); pt_BRVisible = cols.includes('pt_BR');
es_LATAMVisible = cols.includes('es_LATAM');
} }
console.log('Column visibility:', { keyVisible, en_USVisible, chineseVisible, pt_BRVisible, es_LATAMVisible });
oldData = newData.map(item => ({ ...item })); oldData = newData.map(item => ({ ...item }));
console.log('API response:', response); console.log('API response:', response);
lastOp = response.op || []; lastOp = response.op || [];
@ -284,8 +292,27 @@ const gridOptions: VxeGridProps<languageType> = {
} else { } else {
actionVisible = false; actionVisible = false;
} }
let length = response.data ? response.data.length : 0; let length = response.data ? response.data.length : 0;
const list = (response.data || []) as languageType[];
list.forEach(item => {
if (item.len_limit) {
const limit = parseNumber(item.len_limit);
if (!isNaN(limit)) {
if (len_limit.value === 'zh_CN' && item.zh_CN && item.zh_CN.length > limit) {
item.character_limit = `${item.zh_CN.length}/${limit}`;
}
if (len_limit.value === 'en_US' && item.en_US && item.en_US.length > limit) {
item.character_limit = `${item.en_US.length}/${limit}`;
}
if (len_limit.value === 'pt_BR' && item.pt_BR && item.pt_BR.length > limit) {
item.character_limit = `${item.pt_BR.length}/${limit}`;
}
if (len_limit.value === 'es_LATAM' && item.es_LATAM && item.es_LATAM.length > limit) {
item.character_limit = `${item.es_LATAM.length}/${limit}`;
}
}
}
});
GridApi.setGridOptions({ GridApi.setGridOptions({
columns: [ columns: [
{ editRender: { name: 'input' }, field: 'key', title: 'key', filters: [{ data: "" }], filterRender: { name: "input" }, visible: keyVisible, sortBy: 'key', sortable: true, }, { editRender: { name: 'input' }, field: 'key', title: 'key', filters: [{ data: "" }], filterRender: { name: "input" }, visible: keyVisible, sortBy: 'key', sortable: true, },
@ -304,6 +331,7 @@ const gridOptions: VxeGridProps<languageType> = {
{ editRender: { name: 'input' }, field: 'pt_BR', title: 'pt_BR', filters: [{ data: "" }], filterRender: { name: "input" }, visible: pt_BRVisible, className: 'en-us-column' }, { editRender: { name: 'input' }, field: 'pt_BR', title: 'pt_BR', filters: [{ data: "" }], filterRender: { name: "input" }, visible: pt_BRVisible, className: 'en-us-column' },
{ editRender: { name: 'input' }, field: 'es_LATAM', title: 'es_LATAM', filters: [{ data: "" }], filterRender: { name: "input" }, visible: es_LATAMVisible, className: 'en-us-column' }, { editRender: { name: 'input' }, field: 'es_LATAM', title: 'es_LATAM', filters: [{ data: "" }], filterRender: { name: "input" }, visible: es_LATAMVisible, className: 'en-us-column' },
{ slots: { default: 'glossary' }, title: 'Glossary', width: 150 }, { slots: { default: 'glossary' }, title: 'Glossary', width: 150 },
{ slots: { default: 'character_limit' }, title: 'character_limit', width: 150 },
{ slots: { default: 'action' }, title: $t('page.common.action'), width: 150, visible: actionVisible }, { slots: { default: 'action' }, title: $t('page.common.action'), width: 150, visible: actionVisible },
], ],
pagerConfig: { pagerConfig: {
@ -330,6 +358,7 @@ const gridOptions: VxeGridProps<languageType> = {
keepSource: true, keepSource: true,
keyboardConfig: { keyboardConfig: {
isArrow: true, isArrow: true,
isEsc : true,
isTab: true, isTab: true,
isEnter: true, isEnter: true,
isEdit: true, isEdit: true,
@ -397,7 +426,21 @@ const gridEvents: VxeGridListeners<languageType> = {
}; };
const [Grid, GridApi] = useVbenVxeGrid({ formOptions, gridOptions, gridEvents }); const [Grid, GridApi] = useVbenVxeGrid({ formOptions, gridOptions, gridEvents });
function handleDocumentClick(e: MouseEvent) {
const target = e.target as HTMLElement;
if (!target.closest('.vxe-body--column')) {
GridApi.grid?.clearSelected();
}
}
onMounted(() => {
document.addEventListener('click', handleDocumentClick);
});
onUnmounted(() => {
document.removeEventListener('click', handleDocumentClick);
});
function addRow() { function addRow() {
GridApi.grid?.clearSelected();
AddLanguageModalApi.open(); AddLanguageModalApi.open();
AddLanguageModalApi.setState({ AddLanguageModalApi.setState({
title: $t('page.language.newLineContent'), title: $t('page.language.newLineContent'),
@ -451,7 +494,7 @@ function getGlossary(row: languageType) {
head += ' | es'; head += ' | es';
} }
Object.entries(GlossaryData).forEach(([key, value]: [string, any]) => { Object.entries(GlossaryData).forEach(([_key, value]: [string, any]) => {
const inGameGlossary = value.term || ''; const inGameGlossary = value.term || '';
if (row.en_US && new RegExp(`\\b${inGameGlossary.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'i').test(row.en_US)) { if (row.en_US && new RegExp(`\\b${inGameGlossary.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'i').test(row.en_US)) {
let content = `${inGameGlossary}`; let content = `${inGameGlossary}`;
@ -476,14 +519,75 @@ function getGlossary(row: languageType) {
} }
return ''; return '';
} }
function characterLimit(row: languageType, columnName : string) {
for (const key in language_len_limit.value) {
if (language_len_limit.value[key] && new RegExp(language_len_limit.value[key].key || '').test(row.key)) {
const limitInfo: languageLimit = language_len_limit.value[key] || {};
let limit = 0;
let prefix = '';
if (columnName === 'zh_CN') {
limit = limitInfo.zh_CN || 0;
} else {
limit = limitInfo.latin || 0;
}
switch (columnName) {
case 'zh_CN':
prefix = '中文';
break;
case 'en_US':
prefix = 'English';
break;
case 'pt_BR':
prefix = 'Português';
break;
case 'es_LATAM':
prefix = 'Español';
break;
}
if (limit) {
const value = row[columnName as keyof languageType] || '';
if (typeof value === 'string') {
if (columnName == 'zh_CN'){
return `${prefix} ${String(getWeightedTextLength(value)/2)}/${limit/2}`;
}
return `${prefix} ${String(getWeightedTextLength(value))}/${limit}`;
}
}
}
}
}
function characterLimitColor(row: languageType, columnName : string) {
for (const pattern in language_len_limit.value) {
if (language_len_limit.value[pattern] && new RegExp(language_len_limit.value[pattern].key || '').test(row.key)) {
const limitInfo: languageLimit = language_len_limit.value[pattern] || {};
let limit = 0;
if (columnName === 'zh_CN') {
limit = limitInfo.zh_CN || 0;
} else {
limit = limitInfo.latin || 0;
}
if (limit) {
const value = row[columnName as keyof languageType] || '';
if (typeof value === 'string') {
const length = getWeightedTextLength(value);
if (length > limit) {
return 'red';
}
}
}
}
}
}
</script> </script>
<style lang="css"> <style lang="css">
.row-green { .row-green {
background-color: #0ea800; background-color: #0ea800;
} }
.row-yellow { .row-blue {
background-color: #c9bc04; background-color: #6281af;
} }
.zh-cn-column { .zh-cn-column {
font-family: "猫啃什锦黑", sans-serif; font-family: "猫啃什锦黑", sans-serif;
@ -515,6 +619,12 @@ function getGlossary(row: languageType) {
<template #glossary="{ row }"> <template #glossary="{ row }">
<span style="font-size: 11px;">{{ getGlossary(row) }} </span> <span style="font-size: 11px;">{{ getGlossary(row) }} </span>
</template> </template>
<template #character_limit="{ row }">
<span :style="{ fontSize: '11px', color: characterLimitColor(row, 'zh_CN') }">{{ characterLimit(row, 'zh_CN') }}</span><br>
<span :style="{ fontSize: '11px', color: characterLimitColor(row, 'en_US') }">{{ characterLimit(row, 'en_US') }}</span><br>
<span :style="{ fontSize: '11px', color: characterLimitColor(row, 'pt_BR') }">{{ characterLimit(row, 'pt_BR') }}</span><br>
<span :style="{ fontSize: '11px', color: characterLimitColor(row, 'es_LATAM') }">{{ characterLimit(row, 'es_LATAM') }}</span>
</template>
<template #action="{ row }"> <template #action="{ row }">
<AccessControl :codes="['super', 'admin']" type="role"> <AccessControl :codes="['super', 'admin']" type="role">
<Button type="primary" @click="deleteRow(row)">删除</Button> <Button type="primary" @click="deleteRow(row)">删除</Button>

View File

@ -5,8 +5,9 @@ import type { EditActivityParam } from '#/api/core/activity';
import { getUnixTime } from '#/store/util'; import { getUnixTime } from '#/store/util';
import JsonEitorVue from 'json-editor-vue'; import JsonEitorVue from 'json-editor-vue';
import { activityTypeData } from '#/store/order'; import { activityTypeData } from '#/store/order';
import { parseNumber } from '#/store/util';
import {ref} from 'vue'; import {ref} from 'vue';
const value = ref(); const value = ref({});
defineOptions({ defineOptions({
name: 'DetailMailModal', name: 'DetailMailModal',
}); });
@ -63,6 +64,14 @@ const [Form, FormApi] = useVbenForm({
valueFormat: 'YYYY-MM-DD HH:mm:ss', valueFormat: 'YYYY-MM-DD HH:mm:ss',
}, },
}, },
{
component: 'Input',
fieldName: 'interval',
defaultValue: 0,
label: '活动循环间隔从上次开始时间开始计算0表示不循环',
componentProps: {
},
},
{ {
component: 'Input', component: 'Input',
fieldName: 'title', fieldName: 'title',
@ -80,16 +89,6 @@ const [Form, FormApi] = useVbenForm({
fieldName: 'mail_content', fieldName: 'mail_content',
label: '回收邮件内容', label: '回收邮件内容',
}, },
{
component: 'Textarea',
fieldName: 'cfg',
label: '配置',
defaultValue: '{}',
componentProps: {
type: 'textarea',
rows: 8,
},
},
{ {
component: 'Textarea', component: 'Textarea',
fieldName: 'jsonCfg', fieldName: 'jsonCfg',
@ -113,20 +112,24 @@ const [Modal, modalApi] = useVbenModal({
onConfirm: async () => { onConfirm: async () => {
const values = await FormApi.getValues(); const values = await FormApi.getValues();
const mMdata = modalApi.getData(); const mMdata = modalApi.getData();
const cfgjson = JSON.parse(values.cfg); const cfgjson =
typeof values.cfg === 'string'
? JSON.parse(values.cfg)
: values.cfg;
const params: EditActivityParam = { const params: EditActivityParam = {
AppId: mMdata.AppId, AppId: mMdata.AppId,
Cfg: { Cfg: {
id: 0, id: 0,
type: values.type, type: parseNumber(values.type),
start_time: values.start_time ? getUnixTime(values.start_time) : 0, start_time: values.start_time ? getUnixTime(values.start_time) : 0,
end_time: values.end_time ? getUnixTime(values.end_time) : 0, end_time: values.end_time ? getUnixTime(values.end_time) : 0,
title: values.title, title: values.title,
level: values.level, level: parseNumber(values.level),
mail_title: values.mail_title, mail_title: values.mail_title,
mail_content: values.mail_content, mail_content: values.mail_content,
cfg: JSON.stringify(cfgjson), cfg: JSON.stringify(cfgjson),
extra: values.extra, extra: values.extra,
interval: parseNumber(values.interval),
}, },
}; };
await addActivityApi(params); await addActivityApi(params);

View File

@ -8,7 +8,7 @@ import { editActivityApi } from '#/api/core/activity';
import type { EditActivityParam } from '#/api/core/activity'; import type { EditActivityParam } from '#/api/core/activity';
import { getUnixTime } from '#/store/util'; import { getUnixTime } from '#/store/util';
import JsonEitorVue from 'json-editor-vue'; import JsonEitorVue from 'json-editor-vue';
import { parseNumber, formatJsonStr } from '#/store/util';
const value = ref(); const value = ref();
defineOptions({ defineOptions({
name: 'DetailMailModal', name: 'DetailMailModal',
@ -70,6 +70,14 @@ const [Form, FormApi] = useVbenForm({
valueFormat: 'YYYY-MM-DD HH:mm:ss', valueFormat: 'YYYY-MM-DD HH:mm:ss',
}, },
}, },
{
component: 'Input',
fieldName: 'interval',
defaultValue: 0,
label: '活动循环间隔从上次开始时间开始计算0表示不循环',
componentProps: {
},
},
{ {
component: 'Input', component: 'Input',
fieldName: 'title', fieldName: 'title',
@ -93,7 +101,8 @@ const [Form, FormApi] = useVbenForm({
label: '配置', label: '配置',
componentProps: { componentProps: {
type: 'textarea', type: 'textarea',
rows: 8, rows: 12,
overflow: 'auto',
}, },
rules: 'required', rules: 'required',
}, },
@ -123,6 +132,7 @@ const [Modal, modalApi] = useVbenModal({
mail_content: modalData.mail_content, mail_content: modalData.mail_content,
cfg: modalData.cfg, cfg: modalData.cfg,
extra: modalData.extra, extra: modalData.extra,
interval: modalData.interval,
}); });
value.value = JSON.parse(modalData.cfg || '{}'); value.value = JSON.parse(modalData.cfg || '{}');
console.log('value', value.value); console.log('value', value.value);
@ -132,6 +142,18 @@ const [Modal, modalApi] = useVbenModal({
const mMdata = modalApi.getData(); const mMdata = modalApi.getData();
const modalData = mMdata.cfg || {}; const modalData = mMdata.cfg || {};
const level: number = Number(values.level) || 0; const level: number = Number(values.level) || 0;
const interval: number = Number(values.interval) || 0;
let cfgStr = formatJsonStr(value.value);
if (typeof value.value === 'string') {
try {
cfgStr = formatJsonStr(value.value);
} catch (e) {
message.error('配置必须是合法的JSON字符串');
return;
}
} else {
cfgStr = JSON.stringify(value.value);
}
const params: EditActivityParam = { const params: EditActivityParam = {
AppId: mMdata.AppId, AppId: mMdata.AppId,
Cfg: { Cfg: {
@ -143,8 +165,9 @@ const [Modal, modalApi] = useVbenModal({
level: level, level: level,
mail_title: values.mail_title, mail_title: values.mail_title,
mail_content: values.mail_content, mail_content: values.mail_content,
cfg: JSON.stringify(value.value || {}), cfg: cfgStr,
extra: values.extra, extra: values.extra,
interval: interval,
}, },
}; };
const response = await editActivityApi(params); const response = await editActivityApi(params);
@ -157,7 +180,7 @@ const [Modal, modalApi] = useVbenModal({
}); });
</script> </script>
<template> <template>
<Modal :width="800" title="活动详情"> <Modal :width="1200" title="活动详情">
<Form> <Form>
<template #cfg> <template #cfg>
<JsonEitorVue v-model="value" v-bind="{/* local props & attrs */ }" class="w-[100%] h-[500px]" /> <JsonEitorVue v-model="value" v-bind="{/* local props & attrs */ }" class="w-[100%] h-[500px]" />
@ -165,3 +188,4 @@ const [Modal, modalApi] = useVbenModal({
</Form> </Form>
</Modal> </Modal>
</template> </template>

View File

@ -0,0 +1,128 @@
<script lang="ts" setup>
import { useVbenForm, useVbenModal } from '@vben/common-ui';
import {$t} from '#/locales';
import { notification } from 'ant-design-vue';
import { syncActivityApi } from '#/api/core/activity';
import type { SyncActivityParam } from '#/api/core/activity';
defineOptions({
name: 'DetailMailModal',
});
const [Form, FormApi] = useVbenForm({
//
commonConfig: {
//
componentProps: {
class: 'w-full h-full',
},
},
// 使 tailwindcss grid
//
// labelinputvertical
layout: 'horizontal',
showDefaultActions: false,
// labelinput
schema: [
{
component: 'Select',
defaultValue: 1,
componentProps: {
filterOption: true,
options: [],
placeholder: '请选择',
showSearch: true,
},
fieldName: 'SrcAppId',
label: '源APP',
},
{
component: 'Select',
defaultValue: 1,
componentProps: {
filterOption: true,
options: [],
placeholder: '请选择',
showSearch: true,
},
fieldName: 'DstAppId',
label: '目标APP',
},
{
component: 'Input',
fieldName: 'label',
label: '**',
componentProps: {
},
},
{
component: 'Input',
fieldName: 'ensure',
label: '确认框',
componentProps: {
},
},
],
});
const [Modal, modalApi] = useVbenModal({
onOpened: async () => {
const Value = modalApi.getData();
console.log("open", Value)
FormApi.updateSchema(
[
{
component: 'Select',
defaultValue: Value.AppId,
componentProps: {
filterOption: true,
options: Value.AppList ? Value.AppList.map((app: any) => ({ label: $t('page.server.' + app.AppName), value: app.AppId })) : [],
placeholder: '请选择',
showSearch: true,
},
fieldName: 'SrcAppId',
label: '源APP',
},
{
component: 'Select',
defaultValue: 1,
componentProps: {
filterOption: true,
options: Value.AppList ? Value.AppList.map((app: any) => ({ label: $t('page.server.' + app.AppName), value: app.AppId })) : [],
placeholder: '请选择',
showSearch: true,
},
fieldName: 'DstAppId',
label: '目标APP',
},
],
)
},
confirmText: '提交',
onConfirm: async () => {
const values = await FormApi.getValues();
if (values.ensure !== 'meowment') {
// eslint-disable-next-line no-alert
notification.error({ message: '请输入meowment进行确认' });
return;
}
const params: SyncActivityParam = {
SrcAppId: values.SrcAppId,
DstAppId: values.DstAppId,
};
const res = await syncActivityApi(params);
if (res.code === 0) {
notification.success({ message: '同步成功' });
} else {
notification.error({ message: '同步失败:' + res.message });
}
modalApi.close();
},
});
</script>
<template>
<Modal :width="800" title="同步活动配置">
<Form>
<template #label>
<span>请输入<span style="color: red;">meowment</span>进行确认</span>
</template>
</Form>
</Modal>
</template>

View File

@ -12,6 +12,7 @@ import { useVbenModal } from '@vben/common-ui';
import { onMounted, ref } from 'vue'; import { onMounted, ref } from 'vue';
import AddActivityModal from './activity-add.vue'; import AddActivityModal from './activity-add.vue';
import DetailActivityModal from './activity-detail.vue'; import DetailActivityModal from './activity-detail.vue';
import SyncActivityModal from './activity-sync.vue';
import { activityTypeData } from '#/store/order'; import { activityTypeData } from '#/store/order';
import { parseNumber } from '#/store/util'; import { parseNumber } from '#/store/util';
import { $t } from '#/locales' import { $t } from '#/locales'
@ -65,8 +66,9 @@ const gridOptions: VxeGridProps<ActivityData> = {
{ field: 'id', title: 'id' }, { field: 'id', title: 'id' },
{ field: 'type', title: '活动类型', formatter: ({ cellValue }) => activityTypeData[cellValue] || cellValue }, { field: 'type', title: '活动类型', formatter: ({ cellValue }) => activityTypeData[cellValue] || cellValue },
{ field: 'level', title: '开启等级', }, { field: 'level', title: '开启等级', },
{ field: 'start_time', title: '开启时间', formatter: ({ cellValue }) => new Date(cellValue * 1000).toLocaleString(), }, { field: 'now_start_time', title: '开启时间', formatter: ({ cellValue }) => new Date(cellValue * 1000).toLocaleString(), },
{ field: 'end_time', title: '结束时间', formatter: ({ cellValue }) => new Date(cellValue * 1000).toLocaleString(), }, { field: 'now_end_time', title: '结束时间', formatter: ({ cellValue }) => new Date(cellValue * 1000).toLocaleString(), },
{ field: 'interval', title: '活动循环间隔从上次开始时间开始计算0表示不循环', },
{ field: 'tag', title: '状态', slots: { default: 'tag' } }, { field: 'tag', title: '状态', slots: { default: 'tag' } },
], ],
height: 'auto', height: 'auto',
@ -95,9 +97,27 @@ const gridOptions: VxeGridProps<ActivityData> = {
const now = Math.floor(Date.now() / 1000); const now = Math.floor(Date.now() / 1000);
activityList.value = []; activityList.value = [];
for (let item of response.data) { for (let item of response.data) {
if (item.end_time < now) { const interval = item.interval || 0;
if (interval == 0) {
item.now_start_time = item.start_time;
item.now_end_time = item.end_time;
} else {
if (now < item.start_time) {
item.now_start_time = item.start_time;
item.now_end_time = item.end_time;
} else {
const cycle = Math.floor((now - item.start_time) / interval);
item.now_start_time = item.start_time + cycle * interval;
item.now_end_time = item.end_time + cycle * interval;
if (item.now_end_time < now) {
item.now_start_time += interval;
item.now_end_time += interval;
}
}
}
if (item.now_end_time < now) {
item.tag = '已结束'; item.tag = '已结束';
} else if (item.start_time > now) { } else if (item.now_start_time > now) {
item.tag = '未开始'; item.tag = '未开始';
} else { } else {
activityList.value = Array.from(new Set([...activityList.value, activityTypeData[item.type]])).filter((v): v is string => v !== undefined); activityList.value = Array.from(new Set([...activityList.value, activityTypeData[item.type]])).filter((v): v is string => v !== undefined);
@ -137,6 +157,20 @@ const [AddActivityM, AddActivityApi] = useVbenModal({
const [DetailActivityM, DetailActivityApi] = useVbenModal({ const [DetailActivityM, DetailActivityApi] = useVbenModal({
connectedComponent: DetailActivityModal, connectedComponent: DetailActivityModal,
onClosed: async () => {
DetailActivityApi.close();
GridApi.query();
//console.log("close")
}
});
const [SyncActivityM, SyncActivityApi] = useVbenModal({
connectedComponent: SyncActivityModal,
onClosed: async () => {
SyncActivityApi.close();
GridApi.query();
//console.log("close")
}
}); });
onMounted(async () => { onMounted(async () => {
try { try {
@ -199,12 +233,19 @@ async function addActivity() {
AddActivityApi.open(); AddActivityApi.open();
} }
async function syncCfg(){
const Value = await GridApi.formApi.getValues();
SyncActivityApi.setData({ AppList: appList.value, AppId: Value.AppId, ServerId: Value.ServerId });
SyncActivityApi.open();
}
</script> </script>
<template> <template>
<Page auto-content-height class="h-[800px]"> <Page auto-content-height class="h-[800px]">
<AddActivityM class="w-[50%]" /> <AddActivityM class="w-[50%]" />
<DetailActivityM class="w-[50%]" /> <DetailActivityM class="w-[50%]" />
<SyncActivityM class="w-[50%]" />
<Card class="mb-5" title="活动操作"> <Card class="mb-5" title="活动操作">
<div class="mb-5"> <div class="mb-5">
<Space> <Space>
@ -215,9 +256,11 @@ async function addActivity() {
</div> </div>
<Space> <Space>
<Button @click="addActivity">新增活动</Button> <Button type="primary" @click="addActivity">新增活动</Button>
</Space>
<Space class="ml-5">
<Button type="primary" @click="syncCfg">同步活动</Button>
</Space> </Space>
</Card> </Card>
<Grid> <Grid>
<template #tag="{ row }"> <template #tag="{ row }">

View File

@ -1,258 +0,0 @@
<script setup lang="ts">
import { Page } from '@vben/common-ui';
import { Button, Card, Space, Image, Tag } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import type { VxeGridListeners, VxeGridProps } from '#/adapter/vxe-table';
import type { VbenFormProps } from '#/adapter/form';
import type { MailData } from '#/api/core/mail';
import { getServerListApi, getAppListApi } from '#/api/core/server';
import { getMailListApi, deleteMailApi } from '#/api/core/mail';
import type { AppData, ServerData } from '#/api/core/server';
import { useVbenModal } from '@vben/common-ui';
import { onMounted, ref } from 'vue';
import AddMailModal from './activity-add.vue';
import DetailMailModal from './activity-detail.vue';
import { getItemUrl } from '#/store/util';
const appList = ref<AppData[]>([]);
const ServerList = ref<ServerData[]>([]);
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: 'Select',
componentProps: {
options: ServerList.value.map((item) => ({
label: item.ServerName,
value: item.ServerId,
})),
},
fieldName: 'ServerId',
},
]);
},
filterOption: true,
options: [],
placeholder: '请选择',
showSearch: true,
},
fieldName: 'AppId',
label: 'APP',
},
{
component: 'Select',
defaultValue: 1,
componentProps: {
options: [],
},
fieldName: 'ServerId',
label: '区服',
},
],
//
showCollapseButton: true,
submitButtonOptions: {
content: '查询',
},
//
submitOnChange: false,
//
submitOnEnter: false,
};
const gridOptions: VxeGridProps<MailData> = {
columns: [
{ field: 'mail_id', title: 'id' },
{ field: 'title', title: '邮件标题' },
// { field: 'title_en', title: '' },
{ field: 'subtitle', title: '邮件副标题' },
// { field: 'subtitleEN', title: '' },
{ field: 'content', title: '邮件内容' },
// { field: 'content_en', title: '' },
{ field: 'items', title: '道具', slots: { default: 'items' } },
{
field: 'start_time',
title: '开始时间',
formatter: ({ cellValue }) =>
cellValue ? new Date(cellValue * 1000).toLocaleString() : '',
},
{
field: 'mail_type',
title: '邮件类型',
formatter: ({ cellValue }) => (cellValue == 1 ? '普通邮件' : '节日邮件'),
},
{
field: 'send_type',
title: '邮件发送类型',
formatter: ({ cellValue }) => (cellValue == 1 ? '全服邮件' : '个人邮件'),
},
{ field: 'to_uids', title: '接收者' },
{ field: 'create_time', title: '创建时间' },
{ slots: { default: 'action' }, title: '操作', width: 100 },
],
height: 'auto',
pagerConfig: {},
proxyConfig: {
response: {
total: 'total',
result: 'data',
},
ajax: {
query: async ({ page }, formValues) => {
let AppId = parseInt(formValues.AppId, 10);
return await getMailListApi({
AppId: AppId,
ServerId: formValues.ServerId,
PageSize: page.pageSize,
CurrentPage: page.currentPage,
});
},
},
},
showOverflow: false,
rowConfig: {
isHover: true,
},
};
const gridEvents: VxeGridListeners<MailData> = {
cellClick: ({ row }) => {
AddMailApi2.setData(row);
AddMailApi2.open();
// message.info(`cell-click: ${row.title}`);
},
};
const [Grid, GridApi] = useVbenVxeGrid({
gridEvents,
formOptions,
gridOptions,
});
const [AddMailM, AddMailApi] = useVbenModal({
connectedComponent: AddMailModal,
onClosed: async () => {
AddMailApi.close();
GridApi.query();
//console.log("close")
},
});
const [AddMailM2, AddMailApi2] = useVbenModal({
connectedComponent: DetailMailModal,
});
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 : [];
GridApi.formApi.updateSchema([
{
component: 'Select',
componentProps: {
options: ServerList.value.map((item) => ({
label: item.ServerName,
value: item.ServerId,
})),
},
fieldName: 'ServerId',
},
]);
} catch (e) {
appList.value = [];
//console.log(e);
}
});
async function addMail() {
//console.log('addMail');
const Value = await GridApi.formApi.getValues();
AddMailApi.setData({ AppId: Value.AppId, ServerId: Value.ServerId });
AddMailApi.open();
}
async function deleteRow(row: MailData) {
//console.log(row);
GridApi.setLoading(true);
if (typeof row.mail_id == 'number') {
await deleteMailApi(row.AppId, row.ServerId, row.mail_id);
} else {
//console.error('Invalid mail_id:', row.mail_id);
}
GridApi.setLoading(false);
GridApi.query();
}
function fromatItems(items: string) {
try {
const itemList = JSON.parse(items);
const r = itemList.map((item: { Id: number; Num: number }) => ({
...item,
url: getItemUrl(item.Id),
}));
return r;
} catch (e) {
//console.error('Failed to parse items:', e);
return [];
}
}
</script>
<template>
<Page auto-content-height class="h-[800px]">
<AddMailM class="w-[50%]" />
<AddMailM2 class="w-[50%]" />
<Card class="mb-5" title="邮件操作">
<Space>
<Button @click="addMail">新增邮件</Button>
</Space>
</Card>
<Grid>
<template #items="{row}">
<div class="flex flex-wrap items-center justify-center">
<div v-for="item in fromatItems(row.items)" :key="item.Id" class="flex items-center gap-1">
<template v-if="item.url">
<Image :src="item.url" width="30px" />
</template>
<template v-else>
<Tag class="w-[45px] h-[30px] flex items-center justify-center text-xs">
{{item.Id}}
</Tag>
</template>
<span>x{{ item.Num }}</span>
</div>
</div>
</template>
<template #action="{ row }">
<Button type="link" @click="deleteRow(row)" style="color: #cc0000">删除</Button>
</template>
</Grid>
</Page>
</template>

View File

@ -152,7 +152,7 @@ const gridOptions: VxeGridProps<RowType> = {
{ field: 'change_num', title: '变化数值', align: 'center', width: 120 }, { field: 'change_num', title: '变化数值', align: 'center', width: 120 },
{ field: 'change_after', title: '变化后数值', align: 'center' }, { field: 'change_after', title: '变化后数值', align: 'center' },
{ field: 'change_reason', title: '原因', align: 'center' }, { field: 'change_reason', title: '原因', align: 'center' },
{ field: 'item_id', title: '道具名称', formatter: ({ cellValue }) => formatItemName(cellValue), align: 'center' }, { field: 'item_name', title: '道具名称', align: 'center' },
{ field: 'timestamp', title: '时间', formatter: ({ cellValue }) => new Date(cellValue * 1000).toLocaleString(), align: 'center' }, { field: 'timestamp', title: '时间', formatter: ({ cellValue }) => new Date(cellValue * 1000).toLocaleString(), align: 'center' },
], ],
stripe: true, stripe: true,

View File

@ -316,6 +316,7 @@ const [Modal, modalApi] = useVbenModal({
info.value.Diamond = r.Diamond; info.value.Diamond = r.Diamond;
info.value.Energy = r.Energy; info.value.Energy = r.Energy;
info.value.Mac = r.Mac; info.value.Mac = r.Mac;
r.TodayCumulative = r.TodayCumulative || 0;
info.value.Cumulative = (r.Cumulative / 3600).toFixed(2) + 'h'; info.value.Cumulative = (r.Cumulative / 3600).toFixed(2) + 'h';
info.value.TodayCumulative = info.value.TodayCumulative =
(r.TodayCumulative / 3600).toFixed(2) + 'h'; (r.TodayCumulative / 3600).toFixed(2) + 'h';