From 3c746fcea980579f092a251e67beb886c0a39060 Mon Sep 17 00:00:00 2001 From: hahwu <31872165+hahwu@users.noreply.github.com> Date: Fri, 24 Apr 2026 17:23:21 +0800 Subject: [PATCH] ab test config and notification config --- apps/web-antd/src/api/core/experiment.ts | 227 +++++++++ apps/web-antd/src/api/core/index.ts | 2 + apps/web-antd/src/api/core/notification.ts | 92 ++++ apps/web-antd/src/api/core/scripts.ts | 3 + .../src/locales/langs/en-US/page.json | 13 +- .../src/locales/langs/zh-CN/page.json | 11 +- .../src/router/routes/modules/experiment.ts | 21 +- .../src/router/routes/modules/notification.ts | 33 ++ .../src/router/routes/modules/userlog.ts | 35 +- .../src/views/admin/config/config-table.vue | 7 +- .../src/views/admin/user/user-table.vue | 2 +- .../abtest/experiment-form-modal.vue | 438 ++++++++++++++++ .../experiment/abtest/experiment-table.vue | 283 ++++++++++ .../src/views/experiment/abtest/index.vue | 7 + .../views/experiment/group-query/index.vue | 81 +++ .../experiment/index/experiment-table.vue | 2 +- .../web-antd/src/views/notification/index.vue | 482 ++++++++++++++++++ .../operation/activity/activity-table.vue | 2 +- .../src/views/operation/copyUser/copy.vue | 2 +- .../src/views/operation/mail/mail-table.vue | 2 +- .../src/views/operation/order/table.vue | 2 +- .../src/views/operation/scripts/scripts.vue | 41 ++ .../views/userlog/assetlog/asset-table.vue | 2 +- .../views/userlog/eventlog/event-table.vue | 2 +- .../views/userlog/orderlog/event-table.vue | 2 +- .../views/userlog/userlist/asset-table.vue | 2 +- .../views/userlog/userlist/event-table.vue | 2 +- .../views/userlog/userlist/order-table.vue | 2 +- 28 files changed, 1743 insertions(+), 57 deletions(-) create mode 100644 apps/web-antd/src/api/core/experiment.ts create mode 100644 apps/web-antd/src/api/core/notification.ts create mode 100644 apps/web-antd/src/router/routes/modules/notification.ts create mode 100644 apps/web-antd/src/views/experiment/abtest/experiment-form-modal.vue create mode 100644 apps/web-antd/src/views/experiment/abtest/experiment-table.vue create mode 100644 apps/web-antd/src/views/experiment/abtest/index.vue create mode 100644 apps/web-antd/src/views/experiment/group-query/index.vue create mode 100644 apps/web-antd/src/views/notification/index.vue diff --git a/apps/web-antd/src/api/core/experiment.ts b/apps/web-antd/src/api/core/experiment.ts new file mode 100644 index 0000000..28c849a --- /dev/null +++ b/apps/web-antd/src/api/core/experiment.ts @@ -0,0 +1,227 @@ +import { requestClient } from '#/api/request'; + +export namespace ExperimentApi { + export interface ExperimentItem { + id: number; + name: string; + description: string; + status: number; + start_time: null | string; + end_time: null | string; + created_at: string; + updated_at: string; + } + + export interface ListParams { + page?: number; + page_size?: number; + status?: number; + } + + export interface ListResponse { + list: ExperimentItem[]; + page: number; + page_size: number; + total: number; + } + + export interface CreateParams { + description: string; + end_time?: null | string; + name: string; + start_time?: null | string; + status?: number; + variants?: CreateVariantParams[]; + whitelist?: CreateExperimentWhitelistParams[]; + } + + export interface VariantItem { + created_at: string; + description: string; + experiment_id: number; + id: number; + name: string; + params: null | Record; + updated_at: string; + weight: number; + } + + export interface CreateVariantParams { + description?: string; + name: string; + params?: any; + weight?: number; + } + + export interface UpdateVariantParams { + description?: string; + name?: string; + params?: any; + weight?: number; + } + + export interface UpdateExperimentVariantParams { + description?: string; + id?: number; + name: string; + params?: any; + weight?: number; + } + + export interface WhitelistItem { + created_at: string; + experiment_id: number; + id: number; + user_id: string; + variant_id: number; + } + + export interface CreateWhitelistParams { + user_id: string; + variant_id: number; + } + + export interface CreateExperimentWhitelistParams { + user_id: string; + variant_name: string; + } + + export interface BatchCreateWhitelistParams { + items: CreateWhitelistParams[]; + } + + export interface VariantStats { + convert_count: number; + convert_rate: number; + exposure_count: number; + total_value: number; + user_count: number; + variant_id: number; + variant_name: string; + } + + export interface ExperimentResult { + experiment_id: number; + experiment_name: string; + status: number; + variants: VariantStats[]; + } + + export interface UserExperimentGroupItem { + experiment_id: number; + experiment_name: string; + params: null | Record; + variant_id: number; + variant_name: string; + } + + export interface UpdateParams { + description?: string; + end_time?: null | string; + name?: string; + start_time?: null | string; + status?: number; + variants?: UpdateExperimentVariantParams[]; + whitelist?: CreateExperimentWhitelistParams[]; + } +} + +export async function getExperimentListApi(params: ExperimentApi.ListParams) { + return requestClient.get('/v1/experiments', { + params, + }); +} + +export async function createExperimentApi(params: ExperimentApi.CreateParams) { + return requestClient.post('/v1/experiments', params); +} + +export async function updateExperimentApi( + id: number, + params: ExperimentApi.UpdateParams, +) { + return requestClient.put(`/v1/experiments/${id}`, params); +} + +export async function deleteExperimentApi(id: number) { + return requestClient.delete(`/v1/experiments/${id}`); +} + +export async function getExperimentVariantsApi(experimentId: number) { + return requestClient.get( + `/v1/experiments/${experimentId}/variants`, + ); +} + +export async function createExperimentVariantApi( + experimentId: number, + params: ExperimentApi.CreateVariantParams, +) { + return requestClient.post( + `/v1/experiments/${experimentId}/variants`, + params, + ); +} + +export async function updateExperimentVariantApi( + experimentId: number, + variantId: number, + params: ExperimentApi.UpdateVariantParams, +) { + return requestClient.put( + `/v1/experiments/${experimentId}/variants/${variantId}`, + params, + ); +} + +export async function deleteExperimentVariantApi( + experimentId: number, + variantId: number, +) { + return requestClient.delete(`/v1/experiments/${experimentId}/variants/${variantId}`); +} + +export async function getExperimentWhitelistApi(experimentId: number) { + return requestClient.get( + `/v1/experiments/${experimentId}/whitelist`, + ); +} + +export async function createExperimentWhitelistApi( + experimentId: number, + params: ExperimentApi.CreateWhitelistParams, +) { + return requestClient.post( + `/v1/experiments/${experimentId}/whitelist`, + params, + ); +} + +export async function batchCreateExperimentWhitelistApi( + experimentId: number, + params: ExperimentApi.BatchCreateWhitelistParams, +) { + return requestClient.post( + `/v1/experiments/${experimentId}/whitelist/batch`, + params, + ); +} + +export async function removeExperimentWhitelistApi( + experimentId: number, + userId: string, +) { + return requestClient.delete(`/v1/experiments/${experimentId}/whitelist/${userId}`); +} + +export async function getExperimentResultApi(experimentId: number) { + return requestClient.get( + `/v1/experiments/${experimentId}/results`, + ); +} + +export async function getUserExperimentGroupsApi(userId: string) { + return requestClient.get( + `/v1/users/${encodeURIComponent(userId)}/groups`, + ); +} \ No newline at end of file diff --git a/apps/web-antd/src/api/core/index.ts b/apps/web-antd/src/api/core/index.ts index 28a5aef..9563bc7 100644 --- a/apps/web-antd/src/api/core/index.ts +++ b/apps/web-antd/src/api/core/index.ts @@ -1,3 +1,5 @@ export * from './auth'; +export * from './experiment'; export * from './menu'; +export * from './notification'; export * from './user'; diff --git a/apps/web-antd/src/api/core/notification.ts b/apps/web-antd/src/api/core/notification.ts new file mode 100644 index 0000000..6498219 --- /dev/null +++ b/apps/web-antd/src/api/core/notification.ts @@ -0,0 +1,92 @@ +import { requestClient } from '#/api/request'; + +export namespace NotificationApi { + export type ConfigSectionKey = 'activeNotis' | 'inactiveNotis' | 'presentations' | 'schedules'; + + export interface TickValue { + ticks: number | string; + } + + export interface ScheduleItem { + cancelType: number; + cancelValue: number; + deltaTime: TickValue; + fireTime: TickValue; + id: number; + repeatInterval: number; + repeats: boolean; + scheduleType: number; + } + + export interface LocalizedText { + language: number; + locText: string; + param: number; + } + + export interface PresentationItem { + id: number; + infos: LocalizedText[]; + titles: LocalizedText[]; + } + + export interface NoticeItem { + conditionId: number; + id: number; + presentationId: number; + scheduleId: number; + } + + export interface NotificationConfig { + activeNotis: NoticeItem[]; + inactiveNotis: NoticeItem[]; + presentations: PresentationItem[]; + schedules: ScheduleItem[]; + } + + export interface ConfigState { + config: NotificationConfig; + updated_at: string; + } +} + +const NOTIFICATION_CONFIG_BASE_URL = '/config/notification'; +function normalizeForCSharp(value: any): any { + if (Array.isArray(value)) { + return value.map((item) => normalizeForCSharp(item)); + } + if (value && typeof value === 'object') { + return Object.fromEntries( + Object.entries(value).map(([key, currentValue]) => { + if (key === 'ticks' && currentValue != null) { + return [key, String(currentValue)]; + } + return [key, normalizeForCSharp(currentValue)]; + }), + ); + } + return value; +} + +function normalizeConfigState(parsed: NotificationApi.ConfigState) { + return { + config: normalizeForCSharp(parsed.config), + updated_at: parsed.updated_at, + } satisfies NotificationApi.ConfigState; +} + +export async function getNotificationConfigApi() { + const state = await requestClient.get( + NOTIFICATION_CONFIG_BASE_URL, + ); + return normalizeConfigState(state); +} + + +export async function updateNotificationConfigApi(params: NotificationApi.NotificationConfig) { + const state = await requestClient.put( + `${NOTIFICATION_CONFIG_BASE_URL}/update`, + { config: JSON.stringify(normalizeForCSharp(params)) }, + ); + return normalizeConfigState(state); +} \ No newline at end of file diff --git a/apps/web-antd/src/api/core/scripts.ts b/apps/web-antd/src/api/core/scripts.ts index 2c36fde..c134ef8 100644 --- a/apps/web-antd/src/api/core/scripts.ts +++ b/apps/web-antd/src/api/core/scripts.ts @@ -8,4 +8,7 @@ export async function copywritingscript(params :scripts_params) { } export async function copyonlinescript(params :scripts_params) { return requestClient.post('/scripts/copyonline', params, {timeout: 1800000}); +} +export async function clientImageGitPull(params :scripts_params) { + return requestClient.post('/scripts/clientImageGitPull', params, {timeout: 1800000}); } \ No newline at end of file diff --git a/apps/web-antd/src/locales/langs/en-US/page.json b/apps/web-antd/src/locales/langs/en-US/page.json index 0522097..6282038 100644 --- a/apps/web-antd/src/locales/langs/en-US/page.json +++ b/apps/web-antd/src/locales/langs/en-US/page.json @@ -13,6 +13,12 @@ "endTime": "End Time", "action": "Action" }, + "experiment": { + "title": "A/B Test", + "index": "A/B Test Home", + "abtest": "Experiment Management", + "groupQuery": "Player Experiment Groups" + }, "auth": { "login": "Login", "register": "Register", @@ -55,7 +61,12 @@ "level": "Level", "mail": "Mail", "order": "Order", - "language": "Language" + "language": "Language", + "notification": "Client Notification" + }, + "notification": { + "title": "Client Notification", + "config": "Notification Config" }, "log": { "event": { diff --git a/apps/web-antd/src/locales/langs/zh-CN/page.json b/apps/web-antd/src/locales/langs/zh-CN/page.json index 6fc9ca5..a49fcd9 100644 --- a/apps/web-antd/src/locales/langs/zh-CN/page.json +++ b/apps/web-antd/src/locales/langs/zh-CN/page.json @@ -15,7 +15,9 @@ }, "experiment": { "title": "A/B测试", - "index": "A/B测试首页" + "index": "A/B测试首页", + "abtest": "实验管理", + "groupQuery": "玩家实验分组查询" }, "auth": { "login": "登录", @@ -62,7 +64,12 @@ "order": "订单管理", "language": "翻译管理", "copyUser": "用户数据复制", - "activity": "活动管理" + "activity": "活动管理", + "notification": "客户端通知配置" + }, + "notification": { + "title": "客户端通知", + "config": "通知配置" }, "server":{ "merge_pet_test":"测试服", diff --git a/apps/web-antd/src/router/routes/modules/experiment.ts b/apps/web-antd/src/router/routes/modules/experiment.ts index a20aeaf..f09fb1a 100644 --- a/apps/web-antd/src/router/routes/modules/experiment.ts +++ b/apps/web-antd/src/router/routes/modules/experiment.ts @@ -10,18 +10,29 @@ const routes: RouteRecordRaw[] = [ icon: 'lucide:layout-dashboard', order: -1, title: $t('page.experiment.title'), + authority: ['super', 'admin'], }, name: 'Experiment', path: '/experiment', children: [ { - name: 'Index', - path: '/index', - component: () => import('#/views/experiment/index/index.vue'), + name: 'Abtest', + path: '/abtest', + component: () => import('#/views/experiment/abtest/index.vue'), meta: { affixTab: false, - icon: 'lucide:area-chart', - title: $t('page.experiment.index'), + icon: 'lucide:flask-conical', + title: $t('page.experiment.abtest'), + }, + }, + { + name: 'ExperimentGroupQuery', + path: '/group-query', + component: () => import('#/views/experiment/group-query/index.vue'), + meta: { + affixTab: false, + icon: 'lucide:users', + title: $t('page.experiment.groupQuery'), }, }, ], diff --git a/apps/web-antd/src/router/routes/modules/notification.ts b/apps/web-antd/src/router/routes/modules/notification.ts new file mode 100644 index 0000000..a97c7e0 --- /dev/null +++ b/apps/web-antd/src/router/routes/modules/notification.ts @@ -0,0 +1,33 @@ +import type { RouteRecordRaw } from 'vue-router'; + +import { BasicLayout } from '#/layouts'; +import { $t } from '#/locales'; + +const routes: RouteRecordRaw[] = [ + { + component: BasicLayout, + meta: { + icon: 'lucide:bell-ring', + order: 1002, + title: $t('page.notification.title'), + authority: ['super', 'admin'], + }, + name: 'Notification', + path: '/notification', + children: [ + { + name: 'NotificationConfig', + path: '/index', + component: () => import('#/views/notification/index.vue'), + meta: { + affixTab: true, + icon: 'lucide:bell-plus', + title: $t('page.notification.config'), + authority: ['super', 'admin'], + }, + }, + ], + }, +]; + +export default routes; \ No newline at end of file diff --git a/apps/web-antd/src/router/routes/modules/userlog.ts b/apps/web-antd/src/router/routes/modules/userlog.ts index 87a1324..301a336 100644 --- a/apps/web-antd/src/router/routes/modules/userlog.ts +++ b/apps/web-antd/src/router/routes/modules/userlog.ts @@ -24,40 +24,7 @@ const routes: RouteRecordRaw[] = [ icon: 'lucide:list', title: $t('page.userlog.userlist'), }, - }, - { - name: 'Assetlog', - path: '/assetlog', - component: () => import('#/views/userlog/assetlog/index.vue'), - meta: { - affixTab: true, - icon: 'solar:stars-bold', - title: $t('page.userlog.assetlog'), - authority: ['super'], - }, - }, - { - name: 'EventLog', - path: '/eventlog', - component: () => import('#/views/userlog/eventlog/index.vue'), - meta: { - affixTab: true, - icon: 'lucide:apple', - title: $t('page.userlog.eventlog'), - authority: ['super'], - }, - }, - { - name: 'OrderLog', - path: '/orderlog', - component: () => import('#/views/userlog/orderlog/index.vue'), - meta: { - affixTab: true, - icon: 'solar:chat-round-money-bold', - title: $t('page.userlog.orderlog'), - authority: ['super'], - }, - }, + } ], }, ]; diff --git a/apps/web-antd/src/views/admin/config/config-table.vue b/apps/web-antd/src/views/admin/config/config-table.vue index 0511892..6f2a71e 100644 --- a/apps/web-antd/src/views/admin/config/config-table.vue +++ b/apps/web-antd/src/views/admin/config/config-table.vue @@ -23,8 +23,9 @@ const gridOptions: VxeGridProps = { result: 'data', }, ajax: { - query: async ( { page }: { page: { pageSize: number; currentPage: number } }, - formValues: Record,) => { + query: async ( + { page }: { page: { pageSize: number; currentPage: number } }, + ) => { return await getAdminConfigList({ page: page.currentPage, pageSize: page.pageSize, @@ -67,7 +68,7 @@ const deleteConfig = (row: AdminConfig) => {