diff --git a/apps/web-antd/.github/agents/game-admin-frontend.agent.md b/apps/web-antd/.github/agents/game-admin-frontend.agent.md new file mode 100644 index 0000000..0fc012b --- /dev/null +++ b/apps/web-antd/.github/agents/game-admin-frontend.agent.md @@ -0,0 +1,163 @@ +--- +description: "游戏中台管理后台前端开发。Use when: 编写 Vue 页面、创建 vxe-table 表格、编写 API 接口、创建 Pinia store、配置路由、使用 Vben Admin 组件、处理表单逻辑、编写 TypeScript 类型定义、游戏后台业务开发。" +tools: [read, edit, search, execute, agent, todo] +--- + +# 游戏中台管理后台前端专家 + +你是一个资深的游戏中台前端开发工程师,专注于 Vue 3 + TypeScript + Ant Design Vue + VxeTable + Vben Admin 技术栈的管理后台开发。 + +## 技术栈 + +- **框架**: Vue 3 (Composition API, ` + + +``` + +### 新增 Store + +```typescript +// src/store/[feature].ts +import { defineStore } from 'pinia'; +import { ref } from 'vue'; + +export const useFeatureStore = defineStore('feature', () => { + const data = ref([]); + const loading = ref(false); + + async function fetchData() { + loading.value = true; + try { + data.value = await someApi(); + } finally { + loading.value = false; + } + } + + return { data, loading, fetchData }; +}); +``` + +### 新增路由模块 + +```typescript +// src/router/routes/modules/[module].ts +import type { RouteRecordRaw } from 'vue-router'; + +const routes: RouteRecordRaw[] = [ + { + component: () => import('#/layouts/basic.vue'), + meta: { icon: 'some-icon', order: 10, title: '模块名' }, + name: 'ModuleName', + path: '/module', + children: [ + { + component: () => import('#/views/module/index.vue'), + meta: { title: '页面名' }, + name: 'PageName', + path: 'page', + }, + ], + }, +]; + +export default routes; +``` + +## 工作流程 + +1. 理解需求后,先检查现有代码结构和相关文件 +2. 按项目约定创建/修改文件(API → Model → Store → Route → View) +3. 复用已有的 adapter 配置和自定义渲染器(如 `CellImage`, `CellLink`) +4. 编写完成后检查 TypeScript 类型是否正确 diff --git a/apps/web-antd/.github/copilot-instructions.md b/apps/web-antd/.github/copilot-instructions.md new file mode 100644 index 0000000..26fe443 --- /dev/null +++ b/apps/web-antd/.github/copilot-instructions.md @@ -0,0 +1,42 @@ +# Project Guidelines + +## 技术栈 + +- Vue 3 + TypeScript + Ant Design Vue + vxe-table 4.x + Vben Admin 5.x +- Pinia (setup store) + Vue Router 4 + Vite + pnpm monorepo +- 路径别名 `#/*` → `./src/*`,不使用 `@/` + +## 代码风格 + +- 所有组件使用 ` + + + + diff --git a/apps/web-antd/src/component/index.ts b/apps/web-antd/src/component/index.ts index 439293b..fdab00f 100644 --- a/apps/web-antd/src/component/index.ts +++ b/apps/web-antd/src/component/index.ts @@ -1,10 +1,11 @@ import eventTable from "./calendar/event-table.vue"; import calendar from "./calendar/index.vue"; +import heatmap from "./calendar/heatmap.vue"; import eventModal from "./modal/event.vue"; import assetModal from "./modal/asset.vue"; import orderComponent from "./modal/orderComponent.vue"; import chessComponent from "./modal/chessComponent.vue"; import type {dataType} from "./calendar/index.vue"; import friendComponent from "./user/friend/index.vue"; -export { eventTable, calendar, eventModal, assetModal, orderComponent, chessComponent, friendComponent }; +export { eventTable, calendar, heatmap, eventModal, assetModal, orderComponent, chessComponent, friendComponent }; export type { dataType }; diff --git a/apps/web-antd/src/component/modal/chessComponent.vue b/apps/web-antd/src/component/modal/chessComponent.vue index 2cd5ede..07f37cc 100644 --- a/apps/web-antd/src/component/modal/chessComponent.vue +++ b/apps/web-antd/src/component/modal/chessComponent.vue @@ -5,8 +5,8 @@ import { CardContent, CardHeader, CardTitle, - VbenIcon, } from '../../../../../packages/@core/ui-kit/shadcn-ui'; +import { VbenIcon } from '@vben/common-ui'; import type { Chess } from '#/model/type'; interface Props { items: Chess[]; diff --git a/apps/web-antd/src/component/modal/orderComponent.vue b/apps/web-antd/src/component/modal/orderComponent.vue index 5fdf564..f31f9ca 100644 --- a/apps/web-antd/src/component/modal/orderComponent.vue +++ b/apps/web-antd/src/component/modal/orderComponent.vue @@ -5,9 +5,9 @@ import { CardContent, CardHeader, CardTitle, - VbenIcon, VbenPopover } from '../../../../../packages/@core/ui-kit/shadcn-ui'; +import { VbenIcon } from '@vben/common-ui'; import type { Order } from '#/model/type'; import { computed, toRefs } from 'vue'; interface Props { 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 ea8bfdd..6fc9ca5 100644 --- a/apps/web-antd/src/locales/langs/zh-CN/page.json +++ b/apps/web-antd/src/locales/langs/zh-CN/page.json @@ -13,6 +13,10 @@ "endTime": "结束时间", "action": "操作" }, + "experiment": { + "title": "A/B测试", + "index": "A/B测试首页" + }, "auth": { "login": "登录", "register": "注册", diff --git a/apps/web-antd/src/router/routes/modules/experiment.ts b/apps/web-antd/src/router/routes/modules/experiment.ts new file mode 100644 index 0000000..a20aeaf --- /dev/null +++ b/apps/web-antd/src/router/routes/modules/experiment.ts @@ -0,0 +1,31 @@ +import type { RouteRecordRaw } from 'vue-router'; + +import { BasicLayout } from '#/layouts'; +import { $t } from '#/locales'; + +const routes: RouteRecordRaw[] = [ + { + component: BasicLayout, + meta: { + icon: 'lucide:layout-dashboard', + order: -1, + title: $t('page.experiment.title'), + }, + name: 'Experiment', + path: '/experiment', + children: [ + { + name: 'Index', + path: '/index', + component: () => import('#/views/experiment/index/index.vue'), + meta: { + affixTab: false, + icon: 'lucide:area-chart', + title: $t('page.experiment.index'), + }, + }, + ], + }, +]; + +export default routes; diff --git a/apps/web-antd/src/store/util.ts b/apps/web-antd/src/store/util.ts index 153afb8..87cb64d 100644 --- a/apps/web-antd/src/store/util.ts +++ b/apps/web-antd/src/store/util.ts @@ -1,5 +1,16 @@ import MergeData from './MergeData.json'; import dayjs from 'dayjs'; +import utc from 'dayjs/plugin/utc'; + +dayjs.extend(utc); + +/** + * 将 Unix 时间戳(秒)格式化为 UTC+8 时区的时间字符串 + */ +export function formatUTC8Time(timestamp: number): string { + if (!timestamp) return ''; + return dayjs.unix(timestamp).utcOffset(8).format('YYYY-MM-DD HH:mm:ss'); +} export function getImageUrl(key: string): string { if (!key) return ''; 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 789a65d..0511892 100644 --- a/apps/web-antd/src/views/admin/config/config-table.vue +++ b/apps/web-antd/src/views/admin/config/config-table.vue @@ -83,6 +83,6 @@ const deleteConfig = (row: AdminConfig) => { - > + diff --git a/apps/web-antd/src/views/admin/log/log-table.vue b/apps/web-antd/src/views/admin/log/log-table.vue index b3a7199..2a21b08 100644 --- a/apps/web-antd/src/views/admin/log/log-table.vue +++ b/apps/web-antd/src/views/admin/log/log-table.vue @@ -5,6 +5,7 @@ import type { VxeGridProps } from '#/adapter/vxe-table'; import type { AdminLog } from '#/model/admin.user'; import { getAdminLogListApi } from '#/api/core/admin.user'; import type { AdminLogParam } from '#/api/core/admin.user'; +import { formatUTC8Time } from '#/store/util'; import type { VbenFormProps } from '#/adapter/form'; const formOptions: VbenFormProps = { // 所有表单项共用,可单独在表单内覆盖 @@ -40,7 +41,7 @@ const gridOptions: VxeGridProps = { { field: 'params', title: '参数' }, { field: 'ip', title: 'IP' }, { - field: 'createTime', title: '时间', formatter: ({ cellValue }) => new Date(cellValue * 1000).toLocaleString() + field: 'createTime', title: '时间', formatter: ({ cellValue }) => formatUTC8Time(cellValue), slots: { header: 'time_header' } }, ], height: 'auto', @@ -75,6 +76,10 @@ const [Grid] = useVbenVxeGrid({ formOptions, gridOptions }); diff --git a/apps/web-antd/src/views/experiment/index/add-config.vue b/apps/web-antd/src/views/experiment/index/add-config.vue new file mode 100644 index 0000000..301935a --- /dev/null +++ b/apps/web-antd/src/views/experiment/index/add-config.vue @@ -0,0 +1,91 @@ + + + diff --git a/apps/web-antd/src/views/experiment/index/edit-config.vue b/apps/web-antd/src/views/experiment/index/edit-config.vue new file mode 100644 index 0000000..a8a7cf0 --- /dev/null +++ b/apps/web-antd/src/views/experiment/index/edit-config.vue @@ -0,0 +1,104 @@ + + + diff --git a/apps/web-antd/src/views/experiment/index/experiment-table.vue b/apps/web-antd/src/views/experiment/index/experiment-table.vue new file mode 100644 index 0000000..98be5f2 --- /dev/null +++ b/apps/web-antd/src/views/experiment/index/experiment-table.vue @@ -0,0 +1,90 @@ + + + diff --git a/apps/web-antd/src/views/experiment/index/index.vue b/apps/web-antd/src/views/experiment/index/index.vue new file mode 100644 index 0000000..d6004c4 --- /dev/null +++ b/apps/web-antd/src/views/experiment/index/index.vue @@ -0,0 +1,6 @@ + + diff --git a/apps/web-antd/src/views/operation/activity/activity-table.vue b/apps/web-antd/src/views/operation/activity/activity-table.vue index f7c19bc..e2be208 100644 --- a/apps/web-antd/src/views/operation/activity/activity-table.vue +++ b/apps/web-antd/src/views/operation/activity/activity-table.vue @@ -13,6 +13,7 @@ import { onMounted, ref } from 'vue'; import AddActivityModal from './activity-add.vue'; import DetailActivityModal from './activity-detail.vue'; import SyncActivityModal from './activity-sync.vue'; +import { formatUTC8Time } from '#/store/util'; import { activityTypeData } from '#/store/order'; import { parseNumber } from '#/store/util'; import { $t } from '#/locales' @@ -66,8 +67,8 @@ const gridOptions: VxeGridProps = { { field: 'id', title: 'id' }, { field: 'type', title: '活动类型', formatter: ({ cellValue }) => activityTypeData[cellValue] || cellValue }, { field: 'level', title: '开启等级', }, - { field: 'now_start_time', title: '开启时间', formatter: ({ cellValue }) => new Date(cellValue * 1000).toLocaleString(), }, - { field: 'now_end_time', title: '结束时间', formatter: ({ cellValue }) => new Date(cellValue * 1000).toLocaleString(), }, + { field: 'now_start_time', title: '开启时间', formatter: ({ cellValue }) => formatUTC8Time(cellValue), slots: { header: 'start_time_header' } }, + { field: 'now_end_time', title: '结束时间', formatter: ({ cellValue }) => formatUTC8Time(cellValue), slots: { header: 'end_time_header' } }, { field: 'interval', title: '活动循环间隔(秒)从上次开始时间开始计算,0表示不循环', }, { field: 'tag', title: '状态', slots: { default: 'tag' } }, ], @@ -263,6 +264,12 @@ async function syncCfg(){ + + diff --git a/apps/web-antd/src/views/operation/copyUser/copy.vue b/apps/web-antd/src/views/operation/copyUser/copy.vue index 8d6a58b..e922310 100644 --- a/apps/web-antd/src/views/operation/copyUser/copy.vue +++ b/apps/web-antd/src/views/operation/copyUser/copy.vue @@ -2,8 +2,8 @@ import { useVbenForm } from '#/adapter/form'; import { message } from 'ant-design-vue' import { copyUser } from '#/api/core/operation'; -import { Page } from '@vben/common-ui'; -import { VbenPopover, VbenIcon } from '../../../../../../packages/@core/ui-kit/shadcn-ui'; +import { Page, VbenIcon } from '@vben/common-ui'; +import { VbenPopover } from '../../../../../../packages/@core/ui-kit/shadcn-ui'; import type { copyUserParam } from '#/model/type'; const [Form] = useVbenForm({ // 所有表单项共用,可单独在表单内覆盖 diff --git a/apps/web-antd/src/views/operation/mail/mail-detail.vue b/apps/web-antd/src/views/operation/mail/mail-detail.vue index 5b8f7b4..202dd22 100644 --- a/apps/web-antd/src/views/operation/mail/mail-detail.vue +++ b/apps/web-antd/src/views/operation/mail/mail-detail.vue @@ -124,6 +124,11 @@ const [Form, FormApi] = useVbenForm({ fieldName: 'register_time', label: '注册时间', }, + { + component: 'Input', + fieldName: 'min_level', + label: '最低等级', + }, { component: 'Select', fieldName: 'mail_type', @@ -206,6 +211,7 @@ const [Modal, modalApi] = useVbenModal({ items: modalData.items, start_time: 0, register_time: modalData.register_time, + min_level: modalData.min_level ?? 0, mail_type: modalData.mail_type === 1 ? '普通邮件' : '节日邮件', send_type: modalData.send_type === 1 ? '全服邮件' : '个人邮件', ToUids: modalData.to_uids, @@ -277,6 +283,11 @@ const [Modal, modalApi] = useVbenModal({ disabled: true, fieldName: 'register_time', }, + { + component: 'Input', + disabled: true, + fieldName: 'min_level', + }, { component: 'Input', disabled: true, diff --git a/apps/web-antd/src/views/operation/mail/mail-info.vue b/apps/web-antd/src/views/operation/mail/mail-info.vue index 5004f9c..7654447 100644 --- a/apps/web-antd/src/views/operation/mail/mail-info.vue +++ b/apps/web-antd/src/views/operation/mail/mail-info.vue @@ -1,12 +1,10 @@