增加自动化脚本
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-01-06 16:22:40 +08:00
parent 1dc256452e
commit abab4f3637
21 changed files with 294 additions and 227 deletions

1
.gitignore vendored
View File

@ -49,3 +49,4 @@ vite.config.ts.*
*.sln
*.sw?
.history
node-compile-cache/*

View File

@ -23,7 +23,12 @@ export interface UserLogOrder {
Time: number;
Type: number;
Diff: number;
ChessId: string;
ChessId: ChessType[];
}
export interface ChessType {
Id: string;
Icon: string;
}
export interface heatType {
date: string;

View File

@ -0,0 +1,8 @@
import { requestClient } from '#/api/request';
export interface scripts_params{
step :number;
}
export async function copywritingscript(params :scripts_params) {
return requestClient.post('/scripts/copywriting', params, {timeout: 180000});
}

View File

@ -18,8 +18,8 @@ export interface ServerData {
AppId : number;
ServerId: number;
ServerName: string;
Status: number;
OpenServerTime: number;
Status?: number;
OpenServerTime?: number;
StartTime?: number;
PlayerNum?: number;
Loading?:boolean;
@ -30,6 +30,14 @@ export interface ServerData {
Tags?: string[];
}
export interface editServerParam {
AppId : number;
ServerId: number;
ServerName: string;
Tags?: string;
ClientVersion?: string;
}
export interface NodeData{
NodeId?: number;
NodeName: string;
@ -85,3 +93,7 @@ export async function addServer(AppId:number, ServerId:number, ServerName: strin
return requestClient.post(`/server/addServer`, {AppId:AppId, ServerId: ServerId, ServerName: ServerName, Status: Status, OpenServerTime: OpenServerTime});
}
export async function editServer(editParam: editServerParam){
return requestClient.post(`/server/editServer`, editParam);
}

View File

@ -118,8 +118,8 @@ defineEmits(['click']);
<div class="text-foreground/80 mt-4 h-12 ">
<div v-if="item.merge && item.merge.length > 0" class="flex flex-wrap gap-1">
<img v-for="mergeItem in item.merge" :key="mergeItem.Icon"
:src="`../../../merge/${mergeItem.Icon}.png`" class="inline-block h-6 w-6 rounded"
:alt="mergeItem.Title" :title="mergeItem.Title" />
:src="`${mergeItem.Icon}`" class="inline-block h-6 w-6 rounded"
:alt="mergeItem.Icon" :title="mergeItem.Id" />
</div>
<span v-else>{{ item.content }}</span>
</div>

View File

@ -27,7 +27,7 @@
"log": "操作日志"
},
"dashboard": {
"title": "服务器管理",
"title": "运维管理",
"analytics": "分析台",
"server-list": "区服列表",
"node-list": "节点列表",
@ -52,12 +52,18 @@
},
"operation": {
"title": "运营管理",
"level": "等级分布",
"scripts": "自动化脚本",
"mail": "邮件管理",
"order": "订单管理",
"language": "翻译管理",
"copyUser": "用户数据复制"
},
"server":{
"merge_pet_test":"测试服",
"merge_pet_sdk":"QA服",
"merge_pet_online":"正式服",
"merge_pet_audit":"审核服"
},
"log": {
"event": {
"order_finish": "完成订单",

View File

@ -21,12 +21,15 @@ export interface Merge {
export interface MergeRecord{
[key: string]: Merge;
}
export interface ChessType {
Id: string;
Icon: string;
}
export interface Order {
id: number;
color?: string;
icon?:string;
merge?: Merge[];
merge?: ChessType[];
type?: number;
typeName?: string;
diff: number;
@ -72,3 +75,11 @@ export interface copyUserParam{
dst_app: number;
dst_uid: number;
}
export interface scriptsRecord{
step: number;
label: string;
tips :string[];
code: number;
color?: string;
}

View File

@ -17,7 +17,7 @@ const routes: RouteRecordRaw[] = [
{
name: 'Language',
path: '/language',
component: () => import('#/views/language/language/index.vue'),
component: () => import('#/views/language/index.vue'),
meta: {
affixTab: true,
icon: 'lets-icons:order',

View File

@ -16,13 +16,13 @@ const routes: RouteRecordRaw[] = [
path: '/operation',
children: [
{
name: 'Level',
path: '/level',
component: () => import('#/views/operation/level/index.vue'),
name: 'Scripts',
path: '/scripts',
component: () => import('#/views/operation/scripts/index.vue'),
meta: {
affixTab: true,
icon: 'lucide:chart-no-axes-column-increasing',
title: $t('page.operation.level'),
title: $t('page.operation.scripts'),
authority: ['super', 'admin'],
},
},

View File

@ -157,12 +157,7 @@ function confirmUpdate(Server: ServerData) {
</div>
<div class="flex flex-col justify-center md:mt-0 lg:w-1/12">
<h1 class="text-md font-semibold md:text-ml text-center">
<span>32ms</span>
</h1>
</div>
<div class="flex flex-col justify-center md:mt-0 lg:w-1/12">
<h1 class="text-md font-semibold md:text-ml text-center">
<span>1.0.0</span>
<span>{{ item.ClientVersion }}</span>
</h1>
</div>
<div class="flex flex-col justify-center md:mt-0 lg:w-1/12">

View File

@ -1,6 +1,6 @@
<script lang="ts" setup>
import { addServer } from '#/api/core/server';
import { editServer } from '#/api/core/server';
import { useVbenModal } from '@vben/common-ui';
import { useVbenForm } from '#/adapter/form';
import dayjs from 'dayjs';
@ -79,18 +79,6 @@ const [Form, FormApi] = useVbenForm({
label: 'version',
formItemClass: 'col-span-2',
},
{
component: 'NumberPicker',
defaultValue: '',
componentProps: {
placeholder: 'node1',
},
rules: "required",
fieldName: 'max_online',
label: 'max_online',
formItemClass: 'col-span-2',
},
],
showDefaultActions: false,
// 321
@ -163,12 +151,16 @@ const [Modal, modalApi] = useVbenModal({
onConfirm: async () => {
//
const values = await FormApi.getValues();
const date = dayjs(values.datePicker).format('YYYY-MM-DD');
const time = dayjs(values.timePicker).format('HH:mm:ss');
const dateTime = dayjs(`${date} ${time}`).valueOf() / 1000;
const modalData = modalApi.getData();
const ServerId = parseInt(values.ServerId, 10);
await addServer(modalData.AppId, ServerId, values.ServerName, values.Status, dateTime);
const editParam = {
AppId: modalData.AppId,
ServerId: ServerId,
ServerName: values.ServerName,
Tags: values.Tags,
ClientVersion: values.version,
}
await editServer(editParam);
modalApi.close();
},
});

View File

@ -9,6 +9,7 @@ import { useVbenModal } from '@vben/common-ui'
import { ref,onMounted } from 'vue';
import addServerModal from './addServer.vue';
import dayjs from 'dayjs';
import { $t } from '#/locales'
const [BaseForm, BaseFormApi] = useVbenForm({
//
commonConfig: {
@ -111,7 +112,7 @@ onMounted(async() => {
component: 'Select',
componentProps: {
options: appList.value.map((item) => ({
label: item.AppName,
label: $t('page.server.' + item.AppName),
value: item.AppId,
})),
},
@ -207,11 +208,6 @@ async function update() {
<span>MEMORY</span>
</h1>
</div>
<div class="flex flex-col justify-center md:mt-0 lg:w-1/12">
<h1 class="text-md font-semibold md:text-ml text-center">
<span>LATENCY</span>
</h1>
</div>
<div class="flex flex-col justify-center md:mt-0 lg:w-1/12">
<h1 class="text-md font-semibold md:text-ml text-center">
<span>CLIENT VERSION</span>

View File

@ -100,7 +100,7 @@ const [Modal, modalApi] = useVbenModal({
});
</script>
<template>
<Modal width="100%">
<Modal width="80%">
<Form />
<Grid />
</Modal>

View File

@ -103,6 +103,9 @@ const formOptions: VbenFormProps = {
}
const gridOptions: VxeGridProps<languageType> = {
border: true,
cellConfig:{
height:60,
},
columns: [
{ title: 'Id', field: 'Id', width: 100 },
{ editRender: { name: 'input' }, field: 'key', title: 'key', filters: [{ data: "" }], filterRender: { name: "input" }, visible: keyVisible },
@ -110,7 +113,7 @@ const gridOptions: VxeGridProps<languageType> = {
field: 'imageUrl',
slots: { default: 'image-url' },
title: 'Image',
width: 100,
width: 60,
},
{ editRender: { name: 'input' }, field: 'en_US', title: 'en_US', filters: [{ data: "" }], filterRender: { name: "input" }, visible: en_USVisible },
{
@ -253,7 +256,7 @@ const gridOptions: VxeGridProps<languageType> = {
field: 'imageUrl',
slots: { default: 'image-url' },
title: 'Image',
width: 50,
width: 60,
},
{ editRender: { name: 'input' }, field: 'en_US', title: 'en_US', filters: [{ data: "" }], filterRender: { name: "input" }, visible: en_USVisible },
{
@ -420,7 +423,7 @@ function deleteRow(row: languageType) {
<template>
<div class="min-h-screen flex flex-col">
<Page auto-content-height>
<AddLanguageModal width="1200px" height="1200px"></AddLanguageModal />
<AddLanguageModal width="800px" height="800px"></AddLanguageModal />
<Grid>
<template #toolbar-tools>
<span class="mr-4 font-bold">{{ $t('page.language.lastUpdate') }}{{ lastUpdate }}</span>
@ -428,11 +431,11 @@ function deleteRow(row: languageType) {
{{ $t('page.common.addLine') }}
</Button>
<Button type="primary" @click="saveAll" class="mr-2"> {{ $t('page.common.save') }} </Button>
<Button type="primary" @click="exportLang"> {{ $t('page.common.gitCommit') }} </Button>
<!-- <Button type="primary" @click="exportLang"> {{ $t('page.common.gitCommit') }} </Button> -->
</template>
<template #image-url="{ row }">
<!-- <Image src="../../../../public/merge/Launcher_A_LV1.png" height="50" width="50" /> -->
<Image v-if="row.url" :src="row.url" height="50" width="50" />
<!-- <Image src="./public/merge/Launcher_A_LV1.png" height="50" width="50" /> -->
<Image v-if="row.url" :src="row.url" height="40" width="40" />
</template>
<template #action="{ row }">
<AccessControl :codes="['super', 'admin']" type="role">

View File

@ -2,6 +2,7 @@
import { useVbenForm } from '#/adapter/form';
import { message } from 'ant-design-vue'
import { copyUser } from '#/api/core/operation';
import { VbenPopover, VbenIcon } from '../../../../../../packages/@core/ui-kit/shadcn-ui';
import type { copyUserParam } from '#/model/type';
const [Form] = useVbenForm({
//
@ -69,16 +70,6 @@ const [Form] = useVbenForm({
],
},
},
{
component: 'Input',
fieldName: 'dst_uid',
label: '目标用户ID',
defaultValue: 100100042,
rules: 'required',
componentProps: {
placeholder: '请输入目标用户ID',
},
},
],
handleSubmit: onSubmit,
@ -102,7 +93,22 @@ async function onSubmit(values: Record<string, any>) {
</script>
<template>
<VbenPopover class="ml-5" :content-props="{ side: 'top' }">
<template #trigger>
<VbenIcon icon="solar:question-circle-bold" class="ml-5" />
</template>
<div class="max-w-xs text-sm">
<h3 class="font-semibold mt-4 mb-2">账号复制说明</h3>
<ul>
<li><b>源应用ID</b>需要复制的账号所在的APP</li>
<li><b>源用户ID</b>需要复制的账号所生成的后端唯一ID</li>
<li><b>目标应用ID</b>需要复制到的APP</li>
<li><b>目标应用账号</b>源用户ID</li>
</ul>
</div>
</VbenPopover>
<Form class="mt-20" >
<Form class="mt-20" />
</Form>
</template>

View File

@ -1,152 +0,0 @@
<script setup lang="ts">
import { Page } from '@vben/common-ui';
import { getStatisticsLevel } from '#/api/core/statistics';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import type { VxeGridProps } from '#/adapter/vxe-table';
import type { VbenFormProps } from '#/adapter/form';
import { getServerListApi, getAppListApi } from '#/api/core/server';
import type { AppData, ServerData } from '#/api/core/server';
import { onMounted, ref } from 'vue';
const appList = ref<AppData[]>([]);
const ServerList = ref<ServerData[]>([]);
interface RowType {
Uid: number;
change_type: string;
change_num: string;
change_after: string;
item_id: string;
timestamp: string;
}
const formOptions: VbenFormProps = {
//
collapsed: false,
schema: [
{
component: 'Select',
defaultValue: 1,
componentProps: {
onChange: async (value: number) => {
const serverResponse = await getServerListApi({ AppId: value, Type: 1 });
ServerList.value = Array.isArray(serverResponse) ? serverResponse : [];
GridApi.formApi.updateSchema([
{
component: 'CheckboxGroup',
componentProps: {
options: ServerList.value.map((item) => ({
label: item.ServerName,
value: item.ServerId,
})),
},
fieldName: 'ServerList',
},
]);
},
filterOption: true,
options: [
],
placeholder: '请选择',
showSearch: true,
},
fieldName: 'AppId',
label: 'APP',
},
{
component: 'CheckboxGroup',
componentProps: {
name: 'cname',
options: [],
},
fieldName: 'ServerList',
label: '区服',
},
],
//
showCollapseButton: true,
submitButtonOptions: {
content: '查询',
},
//
submitOnChange: false,
//
submitOnEnter: false,
}
const gridOptions: VxeGridProps<RowType> = {
columns: [
{ field: 'Level', title: '等级' },
{ field: 'Sum', title: '人数' },
],
height: 'auto',
pagerConfig: {},
proxyConfig: {
response: {
total: "total",
result: "data"
},
ajax: {
query: async ({ }, formValues) => {
if (formValues.ServerList.length == 0) {
return []
}
let AppId = parseInt(formValues.AppId, 10);
return await getStatisticsLevel({
AppId: AppId,
ServerList: formValues.ServerList
});
},
},
},
rowConfig: {
isHover: true,
},
};
const [Grid, GridApi] = useVbenVxeGrid({ formOptions, gridOptions });
onMounted(async () => {
try {
const response = await getAppListApi();
appList.value = Array.isArray(response) ? response : [];
const app = appList.value[0];
if (!app) return;
GridApi.formApi.updateSchema([
{
component: 'Select',
componentProps: {
options: appList.value.map((item) => ({
label: item.AppName,
value: item.AppId,
})),
},
fieldName: 'AppId',
},
]);
const serverResponse = await getServerListApi({ AppId: app.AppId, Type: 1 });
ServerList.value = Array.isArray(serverResponse) ? serverResponse : [];
GridApi.formApi.updateSchema([
{
component: 'CheckboxGroup',
componentProps: {
options: ServerList.value.map((item) => ({
label: item.ServerName,
value: item.ServerId,
})),
},
fieldName: 'ServerList',
},
]);
} catch (e) {
appList.value = [];
//console.log(e);
}
});
</script>
<template>
<Page auto-content-height class="h-[800px]">
<Grid />
</Page>
</template>

View File

@ -0,0 +1,112 @@
<script lang="ts" setup>
import { ref } from 'vue';
import { useVbenDrawer } from '@vben/common-ui';
import { VbenIcon } from '../../../../../../packages/@core/ui-kit/shadcn-ui';
import { Timeline } from 'ant-design-vue';
import { copywritingscript } from '#/api/core/scripts';
import type { scriptsRecord } from '#/model/type';
const data = ref();
const end = ref(false);
const pendinglabel = ref('脚本运行中...');
const stepList = ref<scriptsRecord[]>([]);
const [Drawer, drawerApi] = useVbenDrawer({
onCancel() {
drawerApi.close();
},
onConfirm() {
console.log('onConfirm');
},
onOpenChange(open: boolean) {
if (open) {
data.value = drawerApi.getData<Record<string, any>>();
stepList.value = [];
end.value = false;
pendinglabel.value = '脚本运行中...';
// 使 nextTick
setTimeout(() => {
switch (data.value.scriptInstanceId) {
case 1:
console.log('running script copywriting');
runningStep(copywritingscript, stepList.value.length);
break;
}
}, 0);
}
// switch (data.value.scriptInstanceId) {
// case 1:
// // load data or do something
// console.log('running script copywriting');
// runningStep(copywritingscript, stepList.value.length);
// break;
// }
},
});
function runningStep(func: Function, step: number) {
func({ step: step }).then((res: scriptsRecord) => {
if (res.step != 1) {
stepList.value?.push({
step: res.step - 1,
label: pendinglabel.value,
tips: res.tips || [],
code: res.code,
});
if (res.code !== 0) {
//
end.value = true;
pendinglabel.value = '';
stepList.value = stepList.value.map((item, index) => ({
...item,
color: index === stepList.value.length - 1 ? 'red' : 'blue'
}));
return;
}
}
if (res.step !== -1) {
// tipsrecord
stepList.value = stepList.value.map((item, index) => ({
...item,
color: index === stepList.value.length ? 'gray' : 'blue'
}));
pendinglabel.value = res.label;
runningStep(func, res.step);
} else {
stepList.value = stepList.value.map((item, index) => ({
...item,
color: 'blue'
}));
end.value = true;
pendinglabel.value = '';
}
});
}
</script>
<template>
<Drawer>
<Timeline :pending="pendinglabel">
<Timeline.Item v-for="item in stepList" :key="item.step" :pending="false" :color="item.color || 'blue'">
<template #dot>
<VbenIcon :icon="item.code == 0?'system-uicons:check-circle':'system-uicons:cross-circle'" />
</template>
<span>{{ item.label }}</span>
<div>
<ul class="list-disc ml-5">
<li v-for="tip in item.tips">{{ tip }}</li>
</ul>
</div>
</Timeline.Item>
<Timeline.Item v-if="end" color="blue">
<template #dot>
<VbenIcon icon="system-uicons:check-circle" />
</template>
completed
</Timeline.Item>
<template #pendingDot>
<VbenIcon icon="svg-spinners:12-dots-scale-rotate" />
</template>
</Timeline>
</Drawer>
</template>

View File

@ -1,7 +1,7 @@
<script lang="ts" setup>
import AnalyticsVisitsTable from './level-table.vue';
import AnalyticsVisitsTable from './scripts.vue';

View File

@ -0,0 +1,83 @@
<script lang="ts" setup>
import { Page } from '@vben/common-ui';
import { Button, Card, Space, notification, Tag } from 'ant-design-vue';
import { useVbenDrawer, VbenButton } from '@vben/common-ui';
import ExtraDrawer from './drawer.vue';
const [Drawer, drawerApi] = useVbenDrawer({
connectedComponent: ExtraDrawer,
});
function startScript(id:number) {
notification.success({
message: '脚本已启动',
description: '文案自动化脚本已成功启动,正在运行中。',
});
drawerApi.setState({
title: '文案自动化脚本',
});
drawerApi.setData({
scriptLaber: '文案自动化脚本',
scriptInstanceId: id,
});
drawerApi.open();
}
</script>
<template>
<Page>
<Drawer />
<div class="p-5">
<div class="card-box p-4 py-6 flex justify-between h-12">
<div class="flex flex-col justify-center md:mt-0 lg:w-1/12">
<h1 class="text-md font-semibold md:text-ml text-center">
<span>Script Name</span>
</h1>
</div>
<div class="flex flex-col justify-center md:mt-0 lg:w-1/12">
<h1 class="text-md font-semibold md:text-ml text-center">
<span>TAGS</span>
</h1>
</div>
<div class="flex flex-col justify-center md:mt-0 lg:w-1/12">
<h1 class="text-md font-semibold md:text-ml text-center">
<span>Last Running</span>
</h1>
</div>
<div class="flex flex-col justify-center md:mt-0 lg:w-1/12">
<h1 class="text-md font-semibold md:text-ml text-center">
<span>Start</span>
</h1>
</div>
</div>
</div>
<div class="p-5">
<div class="card-box p-4 py-6 flex justify-between h-12">
<div class="flex flex-col justify-center md:mt-0 lg:w-1/12">
<h1 class="text-md font-semibold md:text-ml text-center">
<span>文案自动化脚本</span>
</h1>
</div>
<div class="flex flex-col justify-center md:mt-0 lg:w-1/12">
<h1 class="text-md font-semibold md:text-ml text-center">
<Tag>xlsx</Tag>
</h1>
</div>
<div class="flex flex-col justify-center md:mt-0 lg:w-1/12">
<h1 class="text-md font-semibold md:text-ml text-center">
<span>2026-01-01 12:00:00</span>
</h1>
</div>
<div class="flex flex-col justify-center md:mt-0 lg:w-1/12">
<h1 class="text-md font-semibold md:text-ml text-center">
<Button type="primary" @click="startScript(1)">Start</Button>
</h1>
</div>
</div>
</div>
</Page>
</template>

View File

@ -331,16 +331,6 @@ const [Modal, modalApi] = useVbenModal({
}
for (const i in r.Order) {
var chessArr = r.Order[i]?.ChessId
? r.Order[i]?.ChessId.split(' ')
: [];
var ChessData: Merge[] = [];
for (const chessId of chessArr) {
const key = String(parseInt(chessId));
const chessInfo = (MergeData as Record<string, Merge>)[key];
if (chessInfo) {
ChessData.push(chessInfo);
}
}
var orderTypeName = Object.entries(orderTypeData).find(
([key]) => Number(key) === r.Order[i]?.Type
)?.[1];
@ -349,9 +339,8 @@ const [Modal, modalApi] = useVbenModal({
)?.[1];
info.value.Order.push({
id: r.Order[i]?.Id || 0,
merge: ChessData,
merge: chessArr,
color: '#3fb27f',
content: r.Order[i]?.ChessId || '',
typeName: orderTypeName || '未知类型',
diff: r.Order[i]?.Diff || 0,
diffName: diffName || '未知',