版本更新
Some checks failed
CI / Test (ubuntu-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Lint (ubuntu-latest) (push) Has been cancelled
CI / Lint (windows-latest) (push) Has been cancelled
CI / Check (ubuntu-latest) (push) Has been cancelled
CI / Check (windows-latest) (push) Has been cancelled
CodeQL / Analyze (${{ matrix.language }}) (none, javascript-typescript) (push) Has been cancelled
Deploy Website on push / Deploy Push Playground Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Docs Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Antd Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Element Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Naive Ftp (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
CI / CI OK (push) Has been cancelled
Some checks failed
CI / Test (ubuntu-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Lint (ubuntu-latest) (push) Has been cancelled
CI / Lint (windows-latest) (push) Has been cancelled
CI / Check (ubuntu-latest) (push) Has been cancelled
CI / Check (windows-latest) (push) Has been cancelled
CodeQL / Analyze (${{ matrix.language }}) (none, javascript-typescript) (push) Has been cancelled
Deploy Website on push / Deploy Push Playground Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Docs Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Antd Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Element Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Naive Ftp (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
CI / CI OK (push) Has been cancelled
This commit is contained in:
parent
717f5a3c34
commit
dbfb3c4bd9
163
apps/web-antd/.github/agents/game-admin-frontend.agent.md
vendored
Normal file
163
apps/web-antd/.github/agents/game-admin-frontend.agent.md
vendored
Normal file
@ -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, `<script setup lang="ts">`)
|
||||
- **UI 库**: Ant Design Vue, vxe-table 4.x, vxe-pc-ui
|
||||
- **状态管理**: Pinia (setup store 风格)
|
||||
- **路由**: Vue Router 4 (动态路由 + 权限守卫)
|
||||
- **构建**: Vite, pnpm monorepo
|
||||
- **框架**: Vben Admin 5.x (`@vben/*` 系列包)
|
||||
- **工具**: dayjs, @vueuse/core, zod
|
||||
- **语言**: TypeScript (非严格模式, 路径别名 `#/*` → `./src/*`)
|
||||
|
||||
## 约束
|
||||
|
||||
- 所有 Vue 组件必须使用 `<script setup lang="ts">` 语法
|
||||
- 绝对不要使用 Options API
|
||||
- API 函数使用 `Namespace + 泛型` 模式定义请求/响应类型
|
||||
- 表格优先使用 `useVbenVxeGrid` hook,配合 `VxeGridProps` 类型
|
||||
- 表单优先使用 `useVbenForm` hook,schema 驱动
|
||||
- 弹窗使用 `useVbenModal` hook
|
||||
- 图标使用 `VbenIcon` 组件(从 `@vben/common-ui` 导入),图标来源为 [Iconify](https://icon-sets.iconify.design/),常用图标集:`mdi:`, `solar:`, `material-symbols:`, `system-uicons:`
|
||||
- Store 使用 Pinia `defineStore` + setup 函数风格
|
||||
- 路由模块文件放在 `src/router/routes/modules/`,使用 `import.meta.glob` 动态加载
|
||||
- UI 文本使用中文
|
||||
- 路径别名使用 `#/` 而非 `@/`
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
src/
|
||||
├── api/core/ # API 接口 (按资源分文件)
|
||||
├── adapter/ # VxeTable / Form 适配器配置
|
||||
├── component/ # 可复用业务组件
|
||||
├── layouts/ # 布局组件
|
||||
├── locales/ # 国际化 (zh-CN, en-US)
|
||||
├── model/ # TypeScript 接口/类型定义
|
||||
├── router/routes/ # 路由配置 (core + modules)
|
||||
├── store/ # Pinia 状态管理
|
||||
└── views/ # 页面组件 (按功能模块分目录)
|
||||
```
|
||||
|
||||
## 编码模式
|
||||
|
||||
### 新增 API 接口
|
||||
|
||||
```typescript
|
||||
// src/api/core/[resource].ts
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace ResourceApi {
|
||||
export interface QueryParams {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
}
|
||||
export interface ResourceItem {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getResourceListApi(params: ResourceApi.QueryParams) {
|
||||
return requestClient.post<ResourceApi.ResourceItem[]>('/resource/list', params);
|
||||
}
|
||||
```
|
||||
|
||||
### 新增表格页面
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getResourceListApi } from '#/api/core/resource';
|
||||
|
||||
const gridOptions: VxeGridProps = {
|
||||
columns: [
|
||||
{ field: 'id', title: 'ID', width: 80 },
|
||||
{ field: 'name', title: '名称' },
|
||||
],
|
||||
pagerConfig: {},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }) => {
|
||||
return await getResourceListApi({
|
||||
page: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const [Grid] = useVbenVxeGrid({ gridOptions });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Grid />
|
||||
</template>
|
||||
```
|
||||
|
||||
### 新增 Store
|
||||
|
||||
```typescript
|
||||
// src/store/[feature].ts
|
||||
import { defineStore } from 'pinia';
|
||||
import { ref } from 'vue';
|
||||
|
||||
export const useFeatureStore = defineStore('feature', () => {
|
||||
const data = ref<SomeType[]>([]);
|
||||
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 类型是否正确
|
||||
42
apps/web-antd/.github/copilot-instructions.md
vendored
Normal file
42
apps/web-antd/.github/copilot-instructions.md
vendored
Normal file
@ -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/*`,不使用 `@/`
|
||||
|
||||
## 代码风格
|
||||
|
||||
- 所有组件使用 `<script setup lang="ts">`,禁止 Options API
|
||||
- 类型导入使用 `import type`
|
||||
- API 文件使用 `namespace` 模式定义请求/响应类型,函数名以 `Api` 结尾(如 `getListApi`)
|
||||
- Store 使用 `defineStore` + setup 函数风格,命名 `use[Feature]Store`
|
||||
- UI 文本使用中文
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
src/api/core/ → API 接口(按资源分文件)
|
||||
src/model/ → TypeScript 类型定义
|
||||
src/store/ → Pinia Store
|
||||
src/router/routes/ → 路由(core + modules/ 动态加载)
|
||||
src/views/ → 页面(按功能模块分目录)
|
||||
src/adapter/ → VxeTable / Form 适配器
|
||||
src/component/ → 可复用业务组件
|
||||
```
|
||||
|
||||
## 构建与运行
|
||||
|
||||
```bash
|
||||
pnpm dev # 启动开发服务器
|
||||
pnpm build:antd # 生产构建
|
||||
pnpm typecheck # TypeScript 类型检查
|
||||
```
|
||||
|
||||
## 约定
|
||||
|
||||
- 新增页面流程:API → Model → Store(可选)→ Route → View
|
||||
- 表格使用 `useVbenVxeGrid`,表单使用 `useVbenForm`,弹窗使用 `useVbenModal`
|
||||
- API 代理:`/api` → `http://localhost:5320/api`
|
||||
- 路由模块放在 `src/router/routes/modules/`,使用 `import.meta.glob` 自动加载
|
||||
47
apps/web-antd/.github/instructions/vue-component.instructions.md
vendored
Normal file
47
apps/web-antd/.github/instructions/vue-component.instructions.md
vendored
Normal file
@ -0,0 +1,47 @@
|
||||
---
|
||||
description: "Vue 组件编码规范。Use when: 编写 Vue 单文件组件、创建页面、修改模板、处理组件逻辑。"
|
||||
applyTo: "**/*.vue"
|
||||
---
|
||||
|
||||
# Vue 组件编码规范
|
||||
|
||||
## 脚本
|
||||
|
||||
- 使用 `<script setup lang="ts">`,禁止 Options API
|
||||
- 路径别名使用 `#/`(如 `import { xxx } from '#/api/core/xxx'`)
|
||||
- 类型导入使用 `import type`
|
||||
- Props 使用 `defineProps<T>()` 泛型语法
|
||||
- Emits 使用 `defineEmits<T>()`
|
||||
|
||||
## 表格
|
||||
|
||||
- 使用 `useVbenVxeGrid` hook 创建表格,类型为 `VxeGridProps`
|
||||
- 分页配置使用 `pagerConfig: {}`
|
||||
- 数据代理使用 `proxyConfig.ajax.query`,返回 `{ items, total }` 结构
|
||||
- 复用已有渲染器:`CellImage`(图片列)、`CellLink`(链接列)
|
||||
|
||||
## 表单
|
||||
|
||||
- 使用 `useVbenForm` hook,schema 驱动
|
||||
- 验证规则使用 zod
|
||||
|
||||
## 弹窗
|
||||
|
||||
- 使用 `useVbenModal` hook 管理弹窗状态
|
||||
|
||||
## 模板
|
||||
|
||||
- UI 文本使用中文
|
||||
- 合理使用 Ant Design Vue 组件(`a-button`, `a-tag`, `a-card` 等)
|
||||
|
||||
## 图标
|
||||
|
||||
- 使用 `VbenIcon` 组件,从 `@vben/common-ui` 导入
|
||||
- 图标来源为 [Iconify](https://icon-sets.iconify.design/),通过 `icon` prop 传入图标名称
|
||||
- 常用图标集:`mdi:`、`solar:`、`material-symbols:`、`system-uicons:`、`svg-spinners:`
|
||||
- 示例:
|
||||
```vue
|
||||
import { VbenIcon } from '@vben/common-ui';
|
||||
<VbenIcon icon="mdi:account" />
|
||||
<VbenIcon icon="solar:question-circle-bold" class="ml-2" />
|
||||
```
|
||||
30
apps/web-antd/.github/skills/build-antd/SKILL.md
vendored
Normal file
30
apps/web-antd/.github/skills/build-antd/SKILL.md
vendored
Normal file
@ -0,0 +1,30 @@
|
||||
---
|
||||
name: build-antd
|
||||
description: '执行 pnpm build:antd 生产构建任务并报告结果。Use when: 构建项目、打包、build、生产部署前验证。'
|
||||
argument-hint: '可选:传入额外说明,如"检查是否有 TS 报错"'
|
||||
---
|
||||
|
||||
# 执行生产构建
|
||||
|
||||
## When to Use
|
||||
|
||||
- 用户要求构建/打包/build 项目
|
||||
- 部署前验证代码是否能正常编译
|
||||
- 检查 TypeScript 类型错误或构建产物
|
||||
|
||||
## Procedure
|
||||
|
||||
1. 在 monorepo 根目录 `D:\Github\admin_web` 执行构建任务:
|
||||
```
|
||||
pnpm build:antd
|
||||
```
|
||||
2. 等待构建完成,观察输出
|
||||
3. 报告构建结果:
|
||||
- **成功**:告知用户构建成功,输出产物路径 `apps/web-antd/dist/`
|
||||
- **失败**:提取关键错误信息,汇总报告给用户
|
||||
|
||||
## Notes
|
||||
|
||||
- 构建命令定义在 monorepo 根 `package.json` 中
|
||||
- 也可通过 VS Code Task `shell: pnpm build:antd` 执行(工作目录为 `D:\Github\admin_web`)
|
||||
- 构建产物输出到 `apps/web-antd/dist/`
|
||||
@ -97,3 +97,27 @@ export async function getUserlogInfoApi(data: UserLogAssetParam) {
|
||||
export async function getUserlogOrderApi(data: UserLogAssetParam) {
|
||||
return requestClient.post<UserOrder>('/log/order', data);
|
||||
}
|
||||
|
||||
export namespace LoginCountApi {
|
||||
export interface Params {
|
||||
Appid: number;
|
||||
Id: number;
|
||||
month: string;
|
||||
}
|
||||
export interface LoginDailyCount {
|
||||
date: string;
|
||||
count: number;
|
||||
}
|
||||
export interface Result {
|
||||
month: string;
|
||||
total: number;
|
||||
data: LoginDailyCount[];
|
||||
}
|
||||
}
|
||||
|
||||
export async function getLoginCountByMonthApi(params: LoginCountApi.Params) {
|
||||
return requestClient.post<LoginCountApi.Result>(
|
||||
'/log/loginCountByMonth',
|
||||
params,
|
||||
);
|
||||
}
|
||||
|
||||
@ -26,6 +26,7 @@ export interface MailData {
|
||||
title_es_latam?: string;
|
||||
subTitle_es_latam?: string;
|
||||
content_es_latam?: string;
|
||||
min_level?: number;
|
||||
}
|
||||
|
||||
export interface MailListParam {
|
||||
|
||||
372
apps/web-antd/src/component/calendar/heatmap.vue
Normal file
372
apps/web-antd/src/component/calendar/heatmap.vue
Normal file
@ -0,0 +1,372 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { Tooltip, Button } from 'ant-design-vue';
|
||||
import { VbenIcon } from '@vben/common-ui';
|
||||
import dayjs from 'dayjs';
|
||||
import { getLoginCountByMonthApi } from '#/api/core/log';
|
||||
import type { LoginCountApi } from '#/api/core/log';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
title?: string;
|
||||
appId: number;
|
||||
uid: number;
|
||||
}>(),
|
||||
{
|
||||
title: '登录热力图',
|
||||
},
|
||||
);
|
||||
|
||||
const currentMonth = ref(dayjs().startOf('month'));
|
||||
const monthData = ref<LoginCountApi.LoginDailyCount[]>([]);
|
||||
const totalCount = ref(0);
|
||||
const loading = ref(false);
|
||||
const collapsed = ref(true);
|
||||
|
||||
async function fetchData() {
|
||||
if (!props.uid) return;
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await getLoginCountByMonthApi({
|
||||
Appid: props.appId,
|
||||
Id: props.uid,
|
||||
month: currentMonth.value.format('YYYY-MM'),
|
||||
});
|
||||
monthData.value = res.data || [];
|
||||
totalCount.value = res.total || 0;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 月份变化或 uid/appId 变化时重新请求
|
||||
watch(
|
||||
() => [currentMonth.value, props.appId, props.uid] as const,
|
||||
() => fetchData(),
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
function prevMonth() {
|
||||
currentMonth.value = currentMonth.value.subtract(1, 'month');
|
||||
}
|
||||
|
||||
function nextMonth() {
|
||||
currentMonth.value = currentMonth.value.add(1, 'month');
|
||||
}
|
||||
|
||||
function goToday() {
|
||||
currentMonth.value = dayjs().startOf('month');
|
||||
}
|
||||
|
||||
function toggleCollapsed() {
|
||||
collapsed.value = !collapsed.value;
|
||||
}
|
||||
|
||||
const monthLabel = computed(() => currentMonth.value.format('YYYY年M月'));
|
||||
|
||||
// 颜色等级(按登录次数)
|
||||
function getColor(count: number): string {
|
||||
if (count <= 0) return 'var(--heatmap-l0)';
|
||||
if (count === 1) return 'var(--heatmap-l1)';
|
||||
if (count <= 3) return 'var(--heatmap-l2)';
|
||||
if (count <= 5) return 'var(--heatmap-l3)';
|
||||
return 'var(--heatmap-l4)';
|
||||
}
|
||||
|
||||
function getLevelText(count: number): string {
|
||||
if (count <= 0) return '未登录';
|
||||
return `${count}次`;
|
||||
}
|
||||
|
||||
function getCellTextColor(count: number): string {
|
||||
// 浅色格子使用深色文字,深色格子使用浅色文字,提升深色主题下可读性
|
||||
if (count <= 0) return 'hsl(var(--foreground))';
|
||||
if (count <= 3) return 'rgba(0, 0, 0, 0.82)';
|
||||
return 'rgba(255, 255, 255, 0.92)';
|
||||
}
|
||||
|
||||
function getCellSubTextColor(count: number): string {
|
||||
if (count <= 0) return 'hsl(var(--muted-foreground))';
|
||||
return '#ff4d4f';
|
||||
}
|
||||
|
||||
const weekDayLabels = ['一', '二', '三', '四', '五', '六', '日'];
|
||||
|
||||
// 按月构建周行数据
|
||||
const weeks = computed(() => {
|
||||
const dataMap = new Map<string, number>();
|
||||
for (const item of monthData.value) {
|
||||
dataMap.set(item.date, item.count);
|
||||
}
|
||||
|
||||
const monthStart = currentMonth.value;
|
||||
const monthEnd = currentMonth.value.endOf('month');
|
||||
|
||||
// 找到本月第一天所在周的周一
|
||||
const startDay = monthStart.day(); // 0=Sun, 1=Mon ...
|
||||
let cursor = monthStart.subtract((startDay + 6) % 7, 'day');
|
||||
|
||||
const result: { date: string; day: number; value: number; inMonth: boolean }[][] = [];
|
||||
|
||||
while (cursor.isBefore(monthEnd) || cursor.isSame(monthEnd, 'day')) {
|
||||
const week: { date: string; day: number; value: number; inMonth: boolean }[] = [];
|
||||
for (let i = 0; i < 7; i++) {
|
||||
const dateStr = cursor.format('YYYY-MM-DD');
|
||||
const inMonth = cursor.month() === monthStart.month() && cursor.year() === monthStart.year();
|
||||
week.push({
|
||||
date: dateStr,
|
||||
day: cursor.date(),
|
||||
value: dataMap.get(dateStr) ?? 0,
|
||||
inMonth,
|
||||
});
|
||||
cursor = cursor.add(1, 'day');
|
||||
}
|
||||
result.push(week);
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
// 当月统计
|
||||
const stats = computed(() => {
|
||||
let totalDays = 0;
|
||||
let totalCount = 0;
|
||||
let maxCount = 0;
|
||||
for (const item of monthData.value) {
|
||||
if (item.count > 0) {
|
||||
totalDays++;
|
||||
totalCount += item.count;
|
||||
if (item.count > maxCount) maxCount = item.count;
|
||||
}
|
||||
}
|
||||
return { totalDays, totalCount, maxCount };
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="heatmap-container">
|
||||
<!-- 头部:标题 + 月份切换 -->
|
||||
<div class="heatmap-header">
|
||||
<button class="heatmap-title-wrap" type="button" @click="toggleCollapsed">
|
||||
<VbenIcon :icon="collapsed ? 'mdi:chevron-right' : 'mdi:chevron-down'" />
|
||||
<span class="heatmap-title">{{ title }}</span>
|
||||
</button>
|
||||
<div v-show="!collapsed" class="heatmap-nav">
|
||||
<Button size="small" @click="prevMonth"><VbenIcon icon="mdi:chevron-left" /></Button>
|
||||
<span class="heatmap-month-label">{{ monthLabel }}</span>
|
||||
<Button size="small" @click="nextMonth"><VbenIcon icon="mdi:chevron-right" /></Button>
|
||||
<Button size="small" type="link" @click="goToday">今天</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-show="!collapsed">
|
||||
<!-- 统计 -->
|
||||
<div class="heatmap-stats">
|
||||
本月登录 <b>{{ stats.totalDays }}</b> 天,
|
||||
累计 <b>{{ stats.totalCount }}</b> 次,
|
||||
单日最多 <b>{{ getLevelText(stats.maxCount) }}</b>
|
||||
</div>
|
||||
|
||||
<!-- 日历表格:一周一行 -->
|
||||
<table class="heatmap-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th v-for="label in weekDayLabels" :key="label" class="heatmap-th">{{ label }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(week, wi) in weeks" :key="wi">
|
||||
<td v-for="(day, di) in week" :key="di" class="heatmap-td">
|
||||
<Tooltip v-if="day.inMonth" :title="`${day.date}:${getLevelText(day.value)}`">
|
||||
<div
|
||||
class="heatmap-day"
|
||||
:style="{
|
||||
backgroundColor: getColor(day.value),
|
||||
'--heatmap-cell-text': getCellTextColor(day.value),
|
||||
'--heatmap-cell-subtext': getCellSubTextColor(day.value),
|
||||
}"
|
||||
>
|
||||
<span class="heatmap-day-num">{{ day.day }}</span>
|
||||
<span v-if="day.value > 0" class="heatmap-day-text">{{ getLevelText(day.value) }}</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<div v-else class="heatmap-day heatmap-day-outside">
|
||||
<span class="heatmap-day-num-outside">{{ day.day }}</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- 图例 -->
|
||||
<div class="heatmap-legend">
|
||||
<span class="heatmap-legend-text">少</span>
|
||||
<div class="heatmap-legend-cell" style="background-color: var(--heatmap-l0)" />
|
||||
<div class="heatmap-legend-cell" style="background-color: var(--heatmap-l1)" />
|
||||
<div class="heatmap-legend-cell" style="background-color: var(--heatmap-l2)" />
|
||||
<div class="heatmap-legend-cell" style="background-color: var(--heatmap-l3)" />
|
||||
<div class="heatmap-legend-cell" style="background-color: var(--heatmap-l4)" />
|
||||
<span class="heatmap-legend-text">多</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.heatmap-container {
|
||||
--heatmap-l0: hsl(var(--muted));
|
||||
--heatmap-l1: #9be9a8;
|
||||
--heatmap-l2: #40c463;
|
||||
--heatmap-l3: #30a14e;
|
||||
--heatmap-l4: #216e39;
|
||||
background: hsl(var(--card));
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
border: 1px solid hsl(var(--border));
|
||||
}
|
||||
|
||||
:global(.dark) .heatmap-container {
|
||||
--heatmap-l0: hsl(var(--muted));
|
||||
--heatmap-l1: #1f6f43;
|
||||
--heatmap-l2: #238d50;
|
||||
--heatmap-l3: #2db35f;
|
||||
--heatmap-l4: #46d877;
|
||||
}
|
||||
|
||||
.heatmap-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.heatmap-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
.heatmap-title-wrap {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
.heatmap-title-wrap:hover {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.heatmap-nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.heatmap-month-label {
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
color: hsl(var(--foreground));
|
||||
min-width: 90px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.heatmap-stats {
|
||||
font-size: 13px;
|
||||
color: hsl(var(--muted-foreground));
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.heatmap-stats b {
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
.heatmap-table {
|
||||
width: 100%;
|
||||
border-collapse: separate;
|
||||
border-spacing: 4px;
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
.heatmap-th {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: hsl(var(--muted-foreground));
|
||||
text-align: center;
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
|
||||
.heatmap-td {
|
||||
padding: 0;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.heatmap-day {
|
||||
border-radius: 6px;
|
||||
padding: 6px;
|
||||
min-height: 54px;
|
||||
cursor: pointer;
|
||||
transition: box-shadow 0.2s;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.heatmap-day:hover {
|
||||
box-shadow: 0 0 0 2px hsl(var(--ring) / 0.3);
|
||||
}
|
||||
|
||||
.heatmap-day-num {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--heatmap-cell-text, hsl(var(--foreground)));
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.heatmap-day-text {
|
||||
font-size: 11px;
|
||||
color: var(--heatmap-cell-subtext, hsl(var(--muted-foreground)));
|
||||
margin-top: auto;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.heatmap-day-outside {
|
||||
background-color: transparent !important;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.heatmap-day-outside:hover {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.heatmap-day-num-outside {
|
||||
font-size: 13px;
|
||||
color: hsl(var(--muted-foreground) / 0.45);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.heatmap-legend {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
justify-content: flex-end;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.heatmap-legend-text {
|
||||
font-size: 11px;
|
||||
color: hsl(var(--muted-foreground));
|
||||
margin: 0 2px;
|
||||
}
|
||||
|
||||
.heatmap-legend-cell {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
</style>
|
||||
@ -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 };
|
||||
|
||||
@ -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[];
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -13,6 +13,10 @@
|
||||
"endTime": "结束时间",
|
||||
"action": "操作"
|
||||
},
|
||||
"experiment": {
|
||||
"title": "A/B测试",
|
||||
"index": "A/B测试首页"
|
||||
},
|
||||
"auth": {
|
||||
"login": "登录",
|
||||
"register": "注册",
|
||||
|
||||
31
apps/web-antd/src/router/routes/modules/experiment.ts
Normal file
31
apps/web-antd/src/router/routes/modules/experiment.ts
Normal file
@ -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;
|
||||
@ -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 '';
|
||||
|
||||
@ -83,6 +83,6 @@ const deleteConfig = (row: AdminConfig) => {
|
||||
<Button type="dashed" @click="deleteConfig(row)">删除</Button>
|
||||
</Space>
|
||||
</template>
|
||||
</Grid>>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
@ -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<AdminLog> = {
|
||||
{ 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 });
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<Grid />
|
||||
<Grid>
|
||||
<template #time_header>
|
||||
时间 <span style="color: red">(UTC+8)</span>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
91
apps/web-antd/src/views/experiment/index/add-config.vue
Normal file
91
apps/web-antd/src/views/experiment/index/add-config.vue
Normal 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布局
|
||||
// 提交函数
|
||||
// 垂直布局,label和input在不同行,值为vertical
|
||||
layout: 'horizontal',
|
||||
// 水平布局,label和input在同一行
|
||||
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,
|
||||
// 大屏一行显示3个,中屏一行显示2个,小屏一行显示1个
|
||||
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>
|
||||
104
apps/web-antd/src/views/experiment/index/edit-config.vue
Normal file
104
apps/web-antd/src/views/experiment/index/edit-config.vue
Normal 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布局
|
||||
// 提交函数
|
||||
// 垂直布局,label和input在不同行,值为vertical
|
||||
layout: 'horizontal',
|
||||
// 水平布局,label和input在同一行
|
||||
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,
|
||||
// 大屏一行显示3个,中屏一行显示2个,小屏一行显示1个
|
||||
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>
|
||||
@ -0,0 +1,90 @@
|
||||
<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: 'name', title: '实验名称', },
|
||||
{ field: 'desc', title: '实验描述' },
|
||||
{ field: 'status', title: '实验状态', },
|
||||
{ field: 'starttime', title: '开始时间', },
|
||||
{ field: 'endtime', 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>
|
||||
6
apps/web-antd/src/views/experiment/index/index.vue
Normal file
6
apps/web-antd/src/views/experiment/index/index.vue
Normal file
@ -0,0 +1,6 @@
|
||||
<script lang="ts" setup>
|
||||
import ConfigTable from './experiment-table.vue';
|
||||
</script>
|
||||
<template>
|
||||
<ConfigTable />
|
||||
</template>
|
||||
@ -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<ActivityData> = {
|
||||
{ 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(){
|
||||
</Space>
|
||||
</Card>
|
||||
<Grid>
|
||||
<template #start_time_header>
|
||||
开启时间 <span style="color: red">(UTC+8)</span>
|
||||
</template>
|
||||
<template #end_time_header>
|
||||
结束时间 <span style="color: red">(UTC+8)</span>
|
||||
</template>
|
||||
<template #tag="{ row }">
|
||||
<Tag :color=getTagColor(row.tag)>{{ row.tag }}</Tag>
|
||||
</template>
|
||||
|
||||
@ -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({
|
||||
// 所有表单项共用,可单独在表单内覆盖
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -1,12 +1,10 @@
|
||||
<script lang="ts" setup>
|
||||
import { useVbenForm, useVbenModal } from '@vben/common-ui';
|
||||
import { Select, Input, SelectOption, Image, SelectOptGroup } from 'ant-design-vue';
|
||||
import { useVbenForm, useVbenModal, VbenIcon } from '@vben/common-ui';
|
||||
import { Select, Input, SelectOption, SelectOptGroup } from 'ant-design-vue';
|
||||
import dayjs from 'dayjs';
|
||||
import { ref } from 'vue';
|
||||
import { addMailApi } from '#/api/core/mail';
|
||||
import type { MailData } from '#/api/core/mail';
|
||||
import { VbenIcon } from '../../../../../../packages/@core/ui-kit/shadcn-ui/src/components';
|
||||
import { getItemUrl } from '#/store/util';
|
||||
const value1 = ref<string>();
|
||||
const value2 = ref<number>();
|
||||
const items = ref<{ Id: string | number; Num: number; url?: string }[]>([]);
|
||||
@ -139,6 +137,14 @@ const [Form, FormApi] = useVbenForm({
|
||||
fieldName: 'register_time',
|
||||
label: '注册时间',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'min_level',
|
||||
label: '最低等级',
|
||||
componentProps: {
|
||||
placeholder: '请输入最低等级,默认0',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
fieldName: 'mail_type',
|
||||
@ -222,6 +228,7 @@ const [Modal, modalApi] = useVbenModal({
|
||||
const register_time = values.register_time
|
||||
? dayjs(values.register_time).unix()
|
||||
: 0;
|
||||
const min_level = values.min_level ? Number(values.min_level) : 0;
|
||||
const mail_type = values.mail_type;
|
||||
const send_type = values.send_type;
|
||||
const Title = values.Title;
|
||||
@ -252,6 +259,7 @@ const [Modal, modalApi] = useVbenModal({
|
||||
start_time: start_time,
|
||||
end_time: end_time,
|
||||
register_time: register_time,
|
||||
min_level: min_level,
|
||||
mail_type: mail_type,
|
||||
send_type: send_type,
|
||||
};
|
||||
@ -274,9 +282,7 @@ function addItem() {
|
||||
FormApi.setFieldValue('ItemList', JSON.stringify(items.value));
|
||||
}
|
||||
|
||||
function removeItem(index: number) {
|
||||
items.value.splice(index, 1);
|
||||
}
|
||||
|
||||
defineOptions({
|
||||
name: 'AddMailModal',
|
||||
});
|
||||
|
||||
@ -12,7 +12,7 @@ import { useVbenModal } from '@vben/common-ui';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import AddMailModal from './mail-info.vue';
|
||||
import DetailMailModal from './mail-detail.vue';
|
||||
import { getItemUrl } from '#/store/util';
|
||||
import { getItemUrl, formatUTC8Time } from '#/store/util';
|
||||
import { $t } from '#/locales'
|
||||
|
||||
const appList = ref<AppData[]>([]);
|
||||
@ -79,7 +79,8 @@ const gridOptions: VxeGridProps<MailData> = {
|
||||
field: 'start_time',
|
||||
title: '开始时间',
|
||||
formatter: ({ cellValue }) =>
|
||||
cellValue ? new Date(cellValue * 1000).toLocaleString() : '',
|
||||
cellValue ? formatUTC8Time(cellValue) : '',
|
||||
slots: { header: 'time_header' },
|
||||
},
|
||||
{
|
||||
field: 'mail_type',
|
||||
@ -91,6 +92,11 @@ const gridOptions: VxeGridProps<MailData> = {
|
||||
title: '邮件发送类型',
|
||||
formatter: ({ cellValue }) => (cellValue == 1 ? '全服邮件' : '个人邮件'),
|
||||
},
|
||||
{
|
||||
field: 'min_level',
|
||||
title: '最低等级',
|
||||
formatter: ({ cellValue }) => (cellValue ? cellValue : 0),
|
||||
},
|
||||
{ field: 'to_uids', title: '接收者' },
|
||||
{ field: 'create_time', title: '创建时间' },
|
||||
{ slots: { default: 'action' }, title: '操作', width: 100 },
|
||||
@ -114,13 +120,16 @@ const gridOptions: VxeGridProps<MailData> = {
|
||||
},
|
||||
},
|
||||
},
|
||||
showOverflow: false,
|
||||
showOverflow: true,
|
||||
rowConfig: {
|
||||
isHover: true,
|
||||
},
|
||||
};
|
||||
const gridEvents: VxeGridListeners<MailData> = {
|
||||
cellClick: ({ row }) => {
|
||||
cellClick: ({ row, column }) => {
|
||||
if (column.field === 'action') {
|
||||
return;
|
||||
}
|
||||
AddMailApi2.setData(row);
|
||||
AddMailApi2.open();
|
||||
// message.info(`cell-click: ${row.title}`);
|
||||
@ -212,6 +221,9 @@ function fromatItems(items: string) {
|
||||
</Space>
|
||||
</Card>
|
||||
<Grid>
|
||||
<template #time_header>
|
||||
开始时间 <span style="color: red">(UTC+8)</span>
|
||||
</template>
|
||||
<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">
|
||||
@ -228,7 +240,7 @@ function fromatItems(items: string) {
|
||||
</div>
|
||||
</template>
|
||||
<template #action="{ row }">
|
||||
<Button type="link" @click="deleteRow(row)" style="color: #cc0000">删除</Button>
|
||||
<Button type="default" @click="deleteRow(row)">删除</Button>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { useVbenDrawer } from '@vben/common-ui';
|
||||
import { VbenIcon } from '../../../../../../packages/@core/ui-kit/shadcn-ui';
|
||||
import { useVbenDrawer, VbenIcon } from '@vben/common-ui';
|
||||
import { Timeline } from 'ant-design-vue';
|
||||
import { copywritingscript,copyonlinescript } from '#/api/core/scripts';
|
||||
import type { scriptsRecord } from '#/model/type';
|
||||
|
||||
@ -10,6 +10,7 @@ import type { VbenFormProps } from '#/adapter/form';
|
||||
import { ItemData } from '#/store/item';
|
||||
import { eventModal } from '#/component';
|
||||
import dayjs from 'dayjs';
|
||||
import { formatUTC8Time } from '#/store/util';
|
||||
const state = inject('globalState', globalState);
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
connectedComponent: eventModal,
|
||||
@ -143,7 +144,7 @@ const gridOptions: VxeGridProps<RowType> = {
|
||||
{ field: 'change_after', title: '变化后数值', align: 'center' },
|
||||
{ field: 'item_id', title: '道具名称', formatter: ({ cellValue }) => formatItemName(cellValue), align: 'center' },
|
||||
{ field: 'item_id', title: '道具id', align: 'center' },
|
||||
{ field: 'timestamp', title: '时间', formatter: ({ cellValue }) => new Date(cellValue * 1000).toLocaleString(), align: 'center' },
|
||||
{ field: 'timestamp', title: '时间', formatter: ({ cellValue }) => formatUTC8Time(cellValue), align: 'center', slots: { header: 'time_header' } },
|
||||
],
|
||||
stripe: true,
|
||||
round: true,
|
||||
@ -237,6 +238,9 @@ const [Grid] = useVbenVxeGrid({ formOptions, gridOptions, gridEvents });
|
||||
<p style="margin:8px 0 0;">没有更多数据了!</p>
|
||||
</div>
|
||||
</template>
|
||||
<template #time_header>
|
||||
时间 <span style="color: red">(UTC+8)</span>
|
||||
</template>
|
||||
<template #toolbar-tools>
|
||||
总数:<span style="margin-right: 10px;margin-left: 5px;"> {{ d.sum }} </span>
|
||||
正数和: <span style="margin-right: 10px;margin-left: 5px;color:green">{{ d.psum }} </span>
|
||||
|
||||
@ -11,6 +11,7 @@ import type { VbenFormProps } from '#/adapter/form';
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
import { assetModal } from '#/component';
|
||||
import dayjs from 'dayjs';
|
||||
import { formatUTC8Time } from '#/store/util';
|
||||
const state = inject('globalState', globalState);
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
connectedComponent: assetModal,
|
||||
@ -96,7 +97,7 @@ const gridOptions: VxeGridProps<RowType> = {
|
||||
{ field: 'Event', title: '事件类型', formatter: ({ cellValue }) => $t('page.log.event.' + `${cellValue}`) || cellValue },
|
||||
{ field: 'Label', title: 'Label' },
|
||||
{ field: 'Param', title: '参数' },
|
||||
{ field: 'Timestamp', title: '时间', formatter: ({ cellValue }) => new Date(cellValue * 1000).toLocaleString() },
|
||||
{ field: 'Timestamp', title: '时间', formatter: ({ cellValue }) => formatUTC8Time(cellValue), slots: { header: 'time_header' } },
|
||||
],
|
||||
stripe: true,
|
||||
height: 'auto',
|
||||
@ -173,7 +174,11 @@ const [Grid] = useVbenVxeGrid({ formOptions, gridOptions, gridEvents });
|
||||
<template>
|
||||
<div>
|
||||
<Page auto-content-height class="h-[800px]">
|
||||
<Grid />
|
||||
<Grid>
|
||||
<template #time_header>
|
||||
时间 <span style="color: red">(UTC+8)</span>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
<Modal class="w-[1200px]"> </Modal>
|
||||
</div>
|
||||
|
||||
@ -9,7 +9,7 @@ import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
import type { VbenFormProps } from '#/adapter/form';
|
||||
import { ItemData } from '#/store/item';
|
||||
import { eventModal } from '#/component';
|
||||
import { getUnixTime } from "#/store/util";
|
||||
import { getUnixTime, formatUTC8Time } from "#/store/util";
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
// 接收父组件传递的 props
|
||||
@ -153,7 +153,7 @@ const gridOptions: VxeGridProps<RowType> = {
|
||||
{ field: 'change_after', title: '变化后数值', align: 'center' },
|
||||
{ field: 'change_reason', title: '原因', 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 }) => formatUTC8Time(cellValue), align: 'center', slots: { header: 'time_header' } },
|
||||
],
|
||||
stripe: true,
|
||||
round: true,
|
||||
@ -248,6 +248,9 @@ const [Grid] = useVbenVxeGrid({ formOptions, gridOptions, gridEvents });
|
||||
<p style="margin:8px 0 0;">没有更多数据了!</p>
|
||||
</div>
|
||||
</template>
|
||||
<template #time_header>
|
||||
时间 <span style="color: red">(UTC+8)</span>
|
||||
</template>
|
||||
<template #toolbar-tools>
|
||||
总数:<span style="margin-right: 10px;margin-left: 5px;"> {{ d.sum }} </span>
|
||||
正数和: <span style="margin-right: 10px;margin-left: 5px;color:green">{{ d.psum }} </span>
|
||||
|
||||
@ -11,6 +11,7 @@ import { Page, useVbenModal } from '@vben/common-ui';
|
||||
import { assetModal } from '#/component';
|
||||
import { Tag } from 'ant-design-vue';
|
||||
import dayjs from 'dayjs';
|
||||
import { formatUTC8Time } from '#/store/util';
|
||||
const state = inject('globalState', globalState);
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
connectedComponent: assetModal,
|
||||
@ -102,7 +103,7 @@ const gridOptions: VxeGridProps<RowType> = {
|
||||
{ field: 'Label', title: '事件类型',width:120 },
|
||||
{ field: 'Event', title: '事件类型',width:120, slots: { default: 'event' } },
|
||||
{ field: 'Param', title: '参数' },
|
||||
{ field: 'Timestamp', title: '时间',width:180, formatter: ({ cellValue }) => new Date(cellValue * 1000).toLocaleString() },
|
||||
{ field: 'Timestamp', title: '时间',width:180, formatter: ({ cellValue }) => formatUTC8Time(cellValue), slots: { header: 'time_header' } },
|
||||
],
|
||||
stripe: true,
|
||||
height: '800px',
|
||||
@ -189,6 +190,9 @@ function getTagColor(tag:string){
|
||||
<div>
|
||||
<Page class="h-[800px]">
|
||||
<Grid>
|
||||
<template #time_header>
|
||||
时间 <span style="color: red">(UTC+8)</span>
|
||||
</template>
|
||||
<template #event="{ row }">
|
||||
<Tag v-for="(label, index) in getTag(row.Event)" :key="index" :color="getTagColor(label)">{{ label }}</Tag>
|
||||
</template>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch, computed } from 'vue';
|
||||
import { ref, computed } from 'vue';
|
||||
import MergeData from '#/store/MergeData.json';
|
||||
import { orderTypeData, orderDiffData } from '#/store/order';
|
||||
import { faceTypeData } from '#/store/face';
|
||||
@ -7,7 +7,7 @@ import { useVbenModal, useVbenForm, WorkbenchTrends } from '@vben/common-ui';
|
||||
import { message, Card, Tabs } from 'ant-design-vue';
|
||||
import { getUserlogInfoApi } from '#/api/core/log';
|
||||
import { userGmApi, userBanApi } from '#/api/core/user';
|
||||
import type { dataType } from '#/component/index';
|
||||
import { heatmap as LoginHeatmap } from '#/component/index';
|
||||
// 引入 cal-heatmap 样式
|
||||
import 'cal-heatmap/cal-heatmap.css';
|
||||
import type { WorkbenchProjectItem, WorkbenchTrendItem } from '@vben/common-ui';
|
||||
@ -229,7 +229,6 @@ const info = ref<{
|
||||
Ban?: number;
|
||||
Face?: string;
|
||||
Order: Order[];
|
||||
Heatmap: dataType[];
|
||||
Chess: Chess[];
|
||||
Friend: friendRecord[];
|
||||
MaxCharge?: number;
|
||||
@ -249,7 +248,6 @@ const info = ref<{
|
||||
Cumulative: '0h',
|
||||
TodayCumulative: '0h',
|
||||
Order: [],
|
||||
Heatmap: [],
|
||||
Chess: [],
|
||||
Friend: [],
|
||||
MaxCharge: 0,
|
||||
@ -325,7 +323,6 @@ const [Modal, modalApi] = useVbenModal({
|
||||
info.value.RegisterTime = dayjs(r.RegisterTime * 1000).format(
|
||||
'YYYY-MM-DD HH:mm:ss',
|
||||
); // 转换注册时间戳
|
||||
info.value.Heatmap = r.Heatmap || [];
|
||||
info.value.Face = faceTypeData[r.Face || 0] || '未知';
|
||||
info.value.Chess = Array.from({ length: 63 }, () => ({} as Chess));
|
||||
if (r.ChessMap) {
|
||||
@ -464,17 +461,6 @@ function formatActLog(type: number, content = ''): [string, string] {
|
||||
}
|
||||
}
|
||||
|
||||
// 添加对热力图数据的监听
|
||||
watch(
|
||||
() => info.value.Heatmap,
|
||||
(newHeatmap) => {
|
||||
if (newHeatmap && newHeatmap.length > 0) {
|
||||
// 可以在这里触发热力图重新渲染
|
||||
}
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
</script>
|
||||
<template>
|
||||
<Modal title="玩家详情" class="h-[100%]">
|
||||
@ -500,6 +486,7 @@ watch(
|
||||
</Card>
|
||||
</div>
|
||||
</AccessControl>
|
||||
<LoginHeatmap :app-id="data?.AppId" :uid="data?.uid" title="登录热力图" class="mt-5" />
|
||||
<div class="mt-5 flex flex-col lg:flex-row">
|
||||
<div class="mr-4 w-full lg:w-3/5">
|
||||
<orderComponent :items="info.Order" title="订单" />
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { getUserListApi } from '#/api';
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import dayjs from 'dayjs'; // 使用 dayjs 库来格式化时间戳
|
||||
import { Tag } from 'ant-design-vue';
|
||||
import dayjs from 'dayjs';
|
||||
import { formatUTC8Time } from '#/store/util';
|
||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
import type { VbenFormProps } from '#/adapter/form';
|
||||
import { Page } from '@vben/common-ui';
|
||||
@ -13,7 +14,6 @@ import { globalState } from '#/store/globalState';
|
||||
import { getServerListApi, getAppListApi } from '#/api/core/server';
|
||||
import type { AppData, ServerData } from '#/api/core/server';
|
||||
import { $t } from '#/locales'
|
||||
|
||||
const state = inject('globalState', globalState);
|
||||
const appList = ref<AppData[]>([]);
|
||||
const ServerList = ref<ServerData[]>([]);
|
||||
@ -178,10 +178,10 @@ const gridOptions: VxeGridProps<RowType> = {
|
||||
{ field: 'Diamond', title: '钻石', formatter: ({ cellValue }: { cellValue: string | number }) => `${cellValue} 💎`, sortable: true, sortBy: 'Diamond' },
|
||||
{ field: 'Star', title: '星星', formatter: ({ cellValue }: { cellValue: string | number }) => `${cellValue} ⭐`, sortable: true, sortBy: 'Star' },
|
||||
{ field: 'Energy', title: '能量', formatter: ({ cellValue }: { cellValue: string | number }) => `${cellValue} ⚡`, sortable: true, sortBy: 'Energy' },
|
||||
{ field: 'LoginTime', sortable: true, title: '登录时间', formatter: ({ cellValue }: { cellValue: number }) => dayjs(cellValue * 1000).format('YYYY-MM-DD HH:mm:ss'), sortBy: 'LoginTime' },
|
||||
{ field: 'LoginTime', sortable: true, title: '登录时间', formatter: ({ cellValue }: { cellValue: number }) => formatUTC8Time(cellValue), sortBy: 'LoginTime', slots: { header: 'login_time_header' } },
|
||||
{ field: 'Online', title: '在线状态', slots: { default: 'online' }, sortable: true, sortBy: 'Online' },
|
||||
],
|
||||
height: 'auto',
|
||||
height: 'calc(100vh - 200px)',
|
||||
pagerConfig: {},
|
||||
sortConfig: {
|
||||
multiple: true,
|
||||
@ -266,8 +266,11 @@ function getTagColor(online: string): string {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height class="h-[1200px]">
|
||||
<Page auto-content-height>
|
||||
<Grid>
|
||||
<template #login_time_header>
|
||||
登录时间 <span style="color: red">(UTC+8)</span>
|
||||
</template>
|
||||
<template #online="{ row }">
|
||||
<Tag :color="getTagColor(row.Online)">{{ row.Online }}</Tag>
|
||||
</template>
|
||||
|
||||
@ -11,6 +11,7 @@ export * from '@vben-core/popup-ui';
|
||||
export {
|
||||
VbenButton,
|
||||
VbenCountToAnimator,
|
||||
VbenIcon,
|
||||
VbenInputPassword,
|
||||
VbenLoading,
|
||||
VbenPinInput,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user