版本更新
Some checks failed
CI / Test (ubuntu-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Lint (ubuntu-latest) (push) Has been cancelled
CI / Lint (windows-latest) (push) Has been cancelled
CI / Check (ubuntu-latest) (push) Has been cancelled
CI / Check (windows-latest) (push) Has been cancelled
CodeQL / Analyze (${{ matrix.language }}) (none, javascript-typescript) (push) Has been cancelled
Deploy Website on push / Deploy Push Playground Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Docs Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Antd Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Element Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Naive Ftp (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
CI / CI OK (push) Has been cancelled

This commit is contained in:
hahwu 2026-04-21 16:03:45 +08:00
parent 717f5a3c34
commit dbfb3c4bd9
32 changed files with 1116 additions and 52 deletions

View 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` hookschema 驱动
- 弹窗使用 `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 类型是否正确

View 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` 自动加载

View 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` hookschema 驱动
- 验证规则使用 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" />
```

View 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/`

View File

@ -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,
);
}

View File

@ -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 {

View 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>

View File

@ -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 };

View File

@ -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[];

View File

@ -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 {

View File

@ -13,6 +13,10 @@
"endTime": "结束时间",
"action": "操作"
},
"experiment": {
"title": "A/B测试",
"index": "A/B测试首页"
},
"auth": {
"login": "登录",
"register": "注册",

View 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;

View File

@ -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 '';

View File

@ -83,6 +83,6 @@ const deleteConfig = (row: AdminConfig) => {
<Button type="dashed" @click="deleteConfig(row)">删除</Button>
</Space>
</template>
</Grid>>
</Grid>
</Page>
</template>

View File

@ -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>

View File

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

View File

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

View File

@ -0,0 +1,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>

View File

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

View File

@ -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>

View File

@ -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({
//

View File

@ -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,

View File

@ -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 }[]>([]);
@ -108,7 +106,7 @@ const [Form, FormApi] = useVbenForm({
rows: 8,
},
rules: 'required',
},
},
{
component: 'Textarea',
fieldName: 'Items',
@ -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',
});

View File

@ -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>

View File

@ -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';

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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="订单" />

View File

@ -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>

View File

@ -11,6 +11,7 @@ export * from '@vben-core/popup-ui';
export {
VbenButton,
VbenCountToAnimator,
VbenIcon,
VbenInputPassword,
VbenLoading,
VbenPinInput,