玩家列表UI调整
Some checks are pending
CI / Test (ubuntu-latest) (push) Waiting to run
CI / Test (windows-latest) (push) Waiting to run
CI / Lint (ubuntu-latest) (push) Waiting to run
CI / Lint (windows-latest) (push) Waiting to run
CI / Check (ubuntu-latest) (push) Waiting to run
CI / Check (windows-latest) (push) Waiting to run
CI / CI OK (push) Blocked by required conditions
CodeQL / Analyze (${{ matrix.language }}) (none, javascript-typescript) (push) Waiting to run
Deploy Website on push / Deploy Push Playground Ftp (push) Waiting to run
Deploy Website on push / Deploy Push Docs Ftp (push) Waiting to run
Deploy Website on push / Deploy Push Antd Ftp (push) Waiting to run
Deploy Website on push / Deploy Push Element Ftp (push) Waiting to run
Deploy Website on push / Deploy Push Naive Ftp (push) Waiting to run
Release Drafter / update_release_draft (push) Waiting to run

This commit is contained in:
hahwu 2026-05-08 10:06:11 +08:00
parent 8b5a7d28b8
commit f412003d8b
4 changed files with 513 additions and 110 deletions

View File

@ -1,5 +1,5 @@
<script setup lang="ts">
import { Page, useVbenModal } from '@vben/common-ui';
import { Page, useVbenModal, VbenIcon } from '@vben/common-ui';
import type { VxeGridListeners } from '#/adapter/vxe-table';
import { getUserLogAssetApi } from '#/api/core/log';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
@ -10,6 +10,7 @@ import type { VbenFormProps } from '#/adapter/form';
import { ItemData } from '#/store/item';
import { eventModal } from '#/component';
import { getUnixTime, formatUTC8Time } from "#/store/util";
import { Tag } from 'ant-design-vue';
import dayjs from 'dayjs';
// props
@ -148,11 +149,11 @@ const gridEvents: VxeGridListeners<RowType> = {
const gridOptions: VxeGridProps<RowType> = {
columns: [
// { field: 'Uid', title: 'Uid', align: 'center' },
{ field: 'change_type', title: '变化类型', formatter: ({ cellValue }) => formatType(cellValue), align: 'center' },
{ field: 'change_type', title: '变化类型', align: 'center', slots: { default: 'change_type' } },
{ field: 'change_num', title: '变化数值', align: 'center', width: 120 },
{ field: 'change_after', title: '变化后数值', align: 'center' },
{ field: 'change_reason', title: '原因', align: 'center' },
{ field: 'item_name', title: '道具名称', align: 'center' },
{ field: 'change_reason', title: '原因', align: 'center', slots: { default: 'change_reason' } },
{ field: 'item_name', title: '道具名称', align: 'center', slots: { default: 'item_name' } },
{ field: 'timestamp', title: '时间', formatter: ({ cellValue }) => formatUTC8Time(cellValue), align: 'center', slots: { header: 'time_header' } },
],
stripe: true,
@ -235,29 +236,124 @@ const gridOptions: VxeGridProps<RowType> = {
const [Grid] = useVbenVxeGrid({ formOptions, gridOptions, gridEvents });
function getChangeTypeColor(type: string) {
return type === 'consume' ? 'error' : 'success';
}
function getChangeTypeIcon(type: string) {
return type === 'consume' ? 'solar:arrow-to-down-left-bold' : 'solar:arrow-to-top-left-bold';
}
</script>
<template>
<div>
<div class="asset-log-page">
<Page auto-content-height>
<div class="asset-log-panel">
<Grid>
<template #empty>
<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;">
<div class="asset-log-empty">
<img src="https://n.sinaimg.cn/sinacn17/w120h120/20180314/89fc-fyscsmv5911424.gif" alt="no-data"
style="max-width:200px;display:block;">
<p style="margin:8px 0 0;">没有更多数据了</p>
class="asset-log-empty__image">
<p class="asset-log-empty__text">没有更多数据了</p>
</div>
</template>
<template #time_header>
时间 <span style="color: red">(UTC+8)</span>
</template>
<template #change_type="{ row }">
<Tag :color="getChangeTypeColor(row.change_type)" class="asset-log-change-tag">
<VbenIcon :icon="getChangeTypeIcon(row.change_type)" class="mr-1" />
{{ formatType(row.change_type) }}
</Tag>
</template>
<template #change_reason="{ row }">
<span class="asset-log-reason">{{ row.change_reason || '-' }}</span>
</template>
<template #item_name="{ row }">
<span class="asset-log-item-chip">{{ row.item_name || '-' }}</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>
负数和: <span style="color: red;margin-left: 5px;">{{ d.nsum }} </span>
<div class="asset-log-toolbar">
<Tag color="processing" class="asset-log-toolbar__tag">
<VbenIcon icon="solar:user-id-bold" class="mr-1" />
UID {{ props.uid || '-' }}
</Tag>
<Tag color="blue" class="asset-log-toolbar__tag">
总数 {{ d.sum }}
</Tag>
<Tag color="success" class="asset-log-toolbar__tag">
正数和 {{ d.psum }}
</Tag>
<Tag color="error" class="asset-log-toolbar__tag">
负数和 {{ d.nsum }}
</Tag>
</div>
</template>
</Grid>
</div>
</Page>
<Modal class="w-[1200px]"> </Modal>
</div>
</template>
<style lang="css">
.asset-log-page {
min-height: 100%;
}
.asset-log-panel {
overflow: hidden;
border-radius: 22px;
box-shadow: 0 16px 30px rgb(15 23 42 / 8%);
}
.asset-log-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.asset-log-empty__image {
display: block;
max-width: 200px;
}
.asset-log-empty__text {
margin-top: 8px;
color: #475569;
font-weight: 600;
}
.asset-log-toolbar {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.asset-log-toolbar__tag {
display: inline-flex;
align-items: center;
border-radius: 999px;
padding-inline: 10px;
font-weight: 700;
}
.asset-log-change-tag {
display: inline-flex;
align-items: center;
border-radius: 999px;
font-weight: 700;
}
.asset-log-item-chip {
display: inline-flex;
align-items: center;
padding: 4px 10px;
border-radius: 999px;
background: linear-gradient(135deg, #e0ecff 0%, #dbeafe 100%);
color: #1d4ed8;
font-weight: 700;
}
</style>

View File

@ -7,7 +7,7 @@ import type { VxeGridListeners } from 'vxe-table';
import { globalState } from '#/store/globalState';
import type { VxeGridProps } from '#/adapter/vxe-table';
import type { VbenFormProps } from '#/adapter/form';
import { Page, useVbenModal } from '@vben/common-ui';
import { Page, useVbenModal, VbenIcon } from '@vben/common-ui';
import { assetModal } from '#/component';
import { Tag } from 'ant-design-vue';
import dayjs from 'dayjs';
@ -100,7 +100,7 @@ const gridEvents: VxeGridListeners<RowType> = {
const gridOptions: VxeGridProps<RowType> = {
columns: [
// { field: 'Uid', title: 'Uid' },
{ field: 'Label', title: '事件类型',width:120 },
{ field: 'Label', title: '事件名称', width: 160 },
{ field: 'Event', title: '事件类型',width:120, slots: { default: 'event' } },
{ field: 'Param', title: '参数' },
{ field: 'Timestamp', title: '时间',width:180, formatter: ({ cellValue }) => formatUTC8Time(cellValue), slots: { header: 'time_header' } },
@ -184,21 +184,86 @@ function getTagColor(tag:string){
return "green";
}
}
function getTagIcon(tag: string) {
switch (tag) {
case 'error':
return 'solar:danger-circle-bold';
default:
return 'solar:check-circle-bold';
}
}
</script>
<template>
<div>
<div class="event-log-page">
<Page auto-content-height>
<div class="event-log-panel">
<Grid>
<template #toolbar-tools>
<div class="event-log-toolbar">
<Tag color="processing" class="event-log-toolbar__tag">
<VbenIcon icon="solar:user-id-bold" class="mr-1" />
UID {{ props.uid || '-' }}
</Tag>
<Tag color="blue" class="event-log-toolbar__tag">
<VbenIcon icon="solar:cursor-square-bold" class="mr-1" />
双击日志可查看资产快照
</Tag>
</div>
</template>
<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>
<Tag
v-for="(label, index) in getTag(row.Event)"
:key="index"
:color="getTagColor(label)"
class="event-log-status-tag"
>
<VbenIcon :icon="getTagIcon(label)" class="mr-1" />
{{ label }}
</Tag>
</template>
</Grid>
</div>
</Page>
<Modal class="w-[800px]"> </Modal>
</div>
</template>
<style lang="css">
.event-log-page {
min-height: 100%;
}
.event-log-panel {
overflow: hidden;
border-radius: 22px;
background: linear-gradient(180deg, rgb(255 255 255 / 98%) 0%, rgb(247 250 255 / 98%) 100%);
box-shadow: 0 16px 30px rgb(15 23 42 / 8%);
}
.event-log-toolbar {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.event-log-toolbar__tag {
display: inline-flex;
align-items: center;
padding-inline: 10px;
border-radius: 999px;
font-weight: 600;
}
.event-log-status-tag {
display: inline-flex;
align-items: center;
border-radius: 999px;
font-weight: 700;
}
</style>

View File

@ -3,8 +3,8 @@ import { ref, computed } from 'vue';
import MergeData from '#/store/MergeData.json';
import { orderTypeData, orderDiffData } from '#/store/order';
import { faceTypeData } from '#/store/face';
import { useVbenModal, useVbenForm, WorkbenchTrends } from '@vben/common-ui';
import { message, Card, Tabs } from 'ant-design-vue';
import { useVbenModal, useVbenForm, WorkbenchTrends, VbenIcon } from '@vben/common-ui';
import { message, Card, Collapse, Tabs, Tag } from 'ant-design-vue';
import { getUserlogInfoApi } from '#/api/core/log';
import { userGmApi, userBanApi } from '#/api/core/user';
import { heatmap as LoginHeatmap } from '#/component/index';
@ -26,16 +26,23 @@ import orderTable from './order-table.vue';
// url navTo
// url: /dashboard/workspace
const projectItems: WorkbenchProjectItem[] = [];
const { hasAccessByCodes, hasAccessByRoles } = useAccess();
const canUseGm = computed(() =>
hasAccessByRoles(['super']) || hasAccessByCodes(['AC3003']),
);
const canBanUser = computed(() =>
hasAccessByRoles(['super']) || hasAccessByCodes(['AC3004']),
);
const gmPermissions:boolean = useAccess().hasAccessByRoles(['super']) || useAccess().hasAccessByRoles(['AC9301']);
const chargeDisplay = computed(() => (Number(info.value?.Charge ?? 0)).toFixed(2));
const banStatusText = computed(() => {
if (info.value?.Ban === -1) {
return '永久封禁';
}
if (Number(info.value?.Ban ?? 0) > 0) {
return `剩余 ${info.value?.Ban}`;
}
return '正常';
});
const banStatusColor = computed(() => {
if (info.value?.Ban === -1 || Number(info.value?.Ban ?? 0) > 0) {
return 'error';
}
return 'success';
});
const [BaseForm] = useVbenForm({
//
@ -276,8 +283,6 @@ const [Modal, modalApi] = useVbenModal({
async onOpenChange(isOpen: boolean) {
if (isOpen) {
data.value = modalApi.getData<Record<string, any>>();
const b = hasAccessByCodes(['AC3003']);
console.log('canUseGm', canUseGm.value, 'hasAccessByCodes AC3003', b);
try {
const r = await getUserlogInfoApi({
Id: data.value.uid,
@ -473,10 +478,11 @@ function formatActLog(type: number, content = ''): [string, string] {
</script>
<template>
<Modal title="玩家详情" class="h-[100%]">
<div class="p-5">
<Tabs>
<Modal title="玩家详情" class="player-detail-modal h-[100%]">
<div class="player-detail-page p-5">
<Tabs class="player-detail-tabs">
<Tabs.TabPane key="1" tab="玩家信息">
<div class="player-detail-hero">
<UserHeader :avatar="info.Face" :ban="info.Ban">
<template #nick_name> nick_name: {{ info.Name || 'N/A' }} </template>
<template #user_name> user_name: {{ info.Mac }} </template>
@ -486,26 +492,72 @@ function formatActLog(type: number, content = ''): [string, string] {
<template #energy>{{ info.Energy }} </template>
<template #diamond>{{ info.Diamond }}</template>
</UserHeader>
<div class="mt-5 flex">
<AccessControl :codes="['AC3003']" type="code">
<Card class="card-box flex flex-col p-5 w-[50%]">
<div class="player-detail-metrics">
<div class="player-detail-metric-card">
<div class="player-detail-metric-card__label">累计充值</div>
<div class="player-detail-metric-card__value">${{ chargeDisplay }}</div>
</div>
<div class="player-detail-metric-card">
<div class="player-detail-metric-card__label">最高充值</div>
<div class="player-detail-metric-card__value">${{ info.MaxCharge ?? 0 }}</div>
</div>
<div class="player-detail-metric-card">
<div class="player-detail-metric-card__label">今日在线</div>
<div class="player-detail-metric-card__value">{{ info.TodayCumulative }}</div>
</div>
<div class="player-detail-metric-card">
<div class="player-detail-metric-card__label">封禁状态</div>
<div class="player-detail-metric-card__value">
<Tag :color="banStatusColor">{{ banStatusText }}</Tag>
</div>
</div>
</div>
</div>
<div class="player-detail-panel-grid mt-5">
<div if="gmPermissions">
<Card class="player-detail-action-card" :bordered="false">
<template #title>
<div class="player-detail-card-title">
<VbenIcon icon="solar:shield-keyhole-bold" />
<span>GM 指令</span>
</div>
</template>
<div class="player-detail-card-desc">输入 GM 指令后直接对当前玩家执行调试操作</div>
<BaseForm />
</Card>
</AccessControl>
</div>
<AccessControl :codes="['AC3004']" type="code">
<Card class="card-box flex flex-col p-5 w-[45%] ml-5">
<Card class="player-detail-action-card" :bordered="false">
<template #title>
<div class="player-detail-card-title">
<VbenIcon icon="solar:forbidden-circle-bold" />
<span>封禁管理</span>
</div>
</template>
<div class="player-detail-card-desc">支持快捷封禁解封和填写操作原因</div>
<BanForm />
</Card>
</AccessControl>
</div>
<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">
<LoginHeatmap :app-id="data?.AppId" :uid="data?.uid" title="登录热力图" class="player-detail-section mt-5" />
<div class="player-detail-content mt-5">
<div class="player-detail-content__main">
<div class="player-detail-section">
<orderComponent :items="info.Order" title="订单" />
<WorkbenchTrends :items="trendItems" class="mt-5" title="最新动态" />
</div>
<div class="w-full lg:w-2/5">
<WorkbenchDetail :items="projectItems" class="mt-5 lg:mt-0" title="玩家详情">
<div class="player-detail-section player-detail-section--transparent mt-5">
<Collapse class="player-detail-collapse" :bordered="false">
<Collapse.Panel key="latest-trends" header="最新动态">
<WorkbenchTrends :items="trendItems" title="最新动态" />
</Collapse.Panel>
</Collapse>
</div>
</div>
<div class="player-detail-content__side">
<div class="player-detail-section">
<WorkbenchDetail :items="projectItems" title="玩家详情">
<template #areaid> {{ info.AreaId }}</template>
<template #charge> <b>$</b>{{ chargeDisplay }}</template>
<template #maxCharge> <b>$</b>{{ info.MaxCharge }}</template>
@ -517,9 +569,13 @@ function formatActLog(type: number, content = ''): [string, string] {
<template #TodayCumulative>{{ info.TodayCumulative }}</template>
<template #ad_watch>{{ info.AdWatch ? '是' : '否' }}</template>
</WorkbenchDetail>
<chessComponent :items="info.Chess" title="棋盘" class="mt-6" />
<friendComponent :Items="info.Friend" title="好友" class="mt-6" />
<!-- <WorkbenchTodo :items="todoItems" class="mt-5" title="待办事项" /> -->
</div>
<div class="player-detail-section mt-6">
<chessComponent :items="info.Chess" title="棋盘" />
</div>
<div class="player-detail-section mt-6">
<friendComponent :Items="info.Friend" title="好友" />
</div>
</div>
</div>
@ -538,3 +594,133 @@ function formatActLog(type: number, content = ''): [string, string] {
</div>
</Modal>
</template>
<style lang="css">
.player-detail-page {
min-height: 100%;
}
.player-detail-tabs .ant-tabs-nav {
margin-bottom: 20px;
}
.player-detail-tabs .ant-tabs-tab {
border-radius: 999px;
padding-inline: 16px;
font-weight: 600;
}
.player-detail-tabs .ant-tabs-tab-active {
background: rgb(29 78 216 / 8%);
}
.player-detail-hero {
display: flex;
flex-direction: column;
gap: 16px;
}
.player-detail-metrics {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 12px;
}
.player-detail-metric-card {
padding: 16px 18px;
border: 1px solid rgb(191 219 254 / 80%);
border-radius: 20px;
background: linear-gradient(180deg, rgb(255 255 255 / 96%) 0%, rgb(239 246 255 / 96%) 100%);
box-shadow: 0 12px 24px rgb(59 130 246 / 10%);
}
.player-detail-metric-card__label {
font-size: 12px;
font-weight: 700;
color: #64748b;
}
.player-detail-metric-card__value {
margin-top: 8px;
font-size: 22px;
font-weight: 800;
color: #0f172a;
}
.player-detail-panel-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 20px;
}
.player-detail-action-card,
.player-detail-section {
border-radius: 24px;
box-shadow: 0 18px 32px rgb(15 23 42 / 8%);
}
.player-detail-section--transparent {
background: transparent;
box-shadow: none;
}
.player-detail-card-title {
display: inline-flex;
align-items: center;
gap: 8px;
font-weight: 700;
}
.player-detail-card-desc {
margin-bottom: 16px;
color: #64748b;
font-size: 13px;
}
.player-detail-content {
display: grid;
grid-template-columns: minmax(0, 1.6fr) minmax(320px, 1fr);
gap: 20px;
}
.player-detail-content__main,
.player-detail-content__side {
min-width: 0;
}
.player-detail-collapse {
background: transparent;
}
.player-detail-collapse :where(.ant-collapse-header) {
align-items: center !important;
padding: 18px 22px 12px !important;
font-size: 16px;
font-weight: 700;
}
.player-detail-collapse :where(.ant-collapse-content) {
background: transparent;
}
.player-detail-collapse :where(.ant-collapse-content-box) {
padding: 0 22px 22px !important;
}
@media (max-width: 1200px) {
.player-detail-metrics {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.player-detail-content {
grid-template-columns: 1fr;
}
}
@media (max-width: 768px) {
.player-detail-panel-grid,
.player-detail-metrics {
grid-template-columns: 1fr;
}
}
</style>

View File

@ -1,22 +1,22 @@
<script setup lang="ts">
import { getUserListApi } from '#/api';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { VbenIcon, Page } from '@vben/common-ui';
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';
import type { VxeGridListeners } from 'vxe-table';
import { useVbenModal } from '@vben/common-ui'
import { onMounted, ref, inject } from 'vue';
import { computed, inject, onMounted, ref } from 'vue';
import { globalState } from '#/store/globalState';
import { getServerListApi, getAppListApi } from '#/api/core/server';
import type { AppData, ServerData } from '#/api/core/server';
import { getAppListApi } from '#/api/core/server';
import type { AppData } from '#/api/core/server';
import { $t } from '#/locales'
const state = inject('globalState', globalState);
const appList = ref<AppData[]>([]);
const ServerList = ref<ServerData[]>([]);
const selectedAppId = ref<number>(1);
import userModalDemo from './user.vue';
// import { PlayerInfo } from '#/model/player';
const [userModal, userModalApi] = useVbenModal({
@ -37,6 +37,11 @@ interface RowType {
}
const startDate = dayjs().subtract(7, 'day').startOf('day');
const endDate = dayjs().endOf('day');
const currentAppLabel = computed(() => {
const current = appList.value.find((item) => item.AppId === selectedAppId.value);
return current ? $t(`page.server.${current.AppName}`) : '未选择 APP';
});
const formOptions: VbenFormProps = {
//
collapsed: false,
@ -45,22 +50,8 @@ const formOptions: VbenFormProps = {
component: 'Select',
defaultValue: 1,
componentProps: {
onChange: async (value: number) => {
const serverResponse = await getServerListApi({ AppId: value, Type: 1 });
ServerList.value = Array.isArray(serverResponse) ? serverResponse : [];
GridApi.formApi.updateSchema([
{
component: 'Select',
componentProps: {
options: ServerList.value.map((item) => ({
label: item.ServerId,
value: item.ServerId,
})),
},
fieldName: 'ServerId',
},
]);
GridApi.formApi.setValues({ ServerId: 1 });
onChange: (value: number) => {
selectedAppId.value = value;
},
filterOption: true,
options: [
@ -170,7 +161,7 @@ const gridEvents: VxeGridListeners<RowType> = {
const gridOptions: VxeGridProps<RowType> = {
columns: [
{ field: 'Uid', title: 'id', sortable: true, sortBy: 'Uid' },
{ field: 'Uid', title: 'id', sortable: true, sortBy: 'Uid', slots: { default: 'uid' } },
{ field: 'UserName', title: '登录名' },
{ field: 'Level', title: '等级', formatter: ({ cellValue }: { cellValue: string | number }) => `${cellValue}`, sortable: true, sortBy: 'Level' },
{ field: 'Node', title: '节点', },
@ -203,12 +194,17 @@ const gridOptions: VxeGridProps<RowType> = {
formValues: Record<string, any>,
) => {
let Id = parseInt(formValues.AppId, 10);
let ServerId = 1;
let Uid = parseInt(formValues.Uid, 10);
let Nickname = formValues.Nickname ? String(formValues.Nickname) : '';
let Username = formValues.Username ? String(formValues.Username) : '';
let DeviceId = formValues.DeviceId ? String(formValues.DeviceId) : '';
let r = await getUserListApi({ Id: Id, ServerId: ServerId, pageSize: page.pageSize, currentPage: page.currentPage, Uid: Uid, Nickname: Nickname, Username: Username, DeviceId: DeviceId, StartTime: formValues.StartTime ? Math.floor(new Date(formValues.StartTime).getTime() / 1000) : undefined, EndTime: formValues.EndTime ? Math.floor(new Date(formValues.EndTime).getTime() / 1000) : undefined });
if (Number.isNaN(Id)) {
Id = selectedAppId.value;
}
if (Number.isNaN(Uid)) {
Uid = 0;
}
let r = await getUserListApi({ Id: Id, ServerId: 1, pageSize: page.pageSize, currentPage: page.currentPage, Uid: Uid, Nickname: Nickname, Username: Username, DeviceId: DeviceId, StartTime: formValues.StartTime ? Math.floor(new Date(formValues.StartTime).getTime() / 1000) : undefined, EndTime: formValues.EndTime ? Math.floor(new Date(formValues.EndTime).getTime() / 1000) : undefined });
return r;
},
},
@ -228,6 +224,7 @@ onMounted(async () => {
appList.value = Array.isArray(response) ? response : [];
const app = appList.value[0];
if (!app) return;
selectedAppId.value = app.AppId;
GridApi.formApi.updateSchema([
{
component: 'Select',
@ -240,20 +237,7 @@ onMounted(async () => {
fieldName: 'AppId',
},
]);
const serverResponse = await getServerListApi({ AppId: app.AppId, Type: 1 });
ServerList.value = Array.isArray(serverResponse) ? serverResponse : [];
GridApi.formApi.updateSchema([
{
component: 'Select',
componentProps: {
options: ServerList.value.map((item) => ({
label: item.ServerId,
value: item.ServerId,
})),
},
fieldName: 'ServerId',
},
]);
GridApi.formApi.setValues({ AppId: app.AppId });
} catch (e) {
appList.value = [];
console.log(e);
@ -266,21 +250,93 @@ function getTagColor(online: string): string {
</script>
<template>
<div class="userlist-page">
<Page auto-content-height>
<div class="userlist-grid-panel">
<Grid>
<template #toolbar-tools>
<div class="userlist-toolbar-meta">
<Tag color="processing" class="userlist-toolbar-tag">
<VbenIcon icon="solar:gamepad-bold" class="mr-1" />
{{ currentAppLabel }}
</Tag>
</div>
</template>
<template #login_time_header>
登录时间 <span style="color: red">(UTC+8)</span>
</template>
<template #uid="{ row }">
<span class="user-id-chip">{{ row.Uid }}</span>
</template>
<template #online="{ row }">
<Tag :color="getTagColor(row.Online)">{{ row.Online }}</Tag>
<Tag :color="getTagColor(row.Online)" :class="['user-status-tag', row.Online === '在线' ? 'user-status-tag--online' : 'user-status-tag--offline']">
<VbenIcon :icon="row.Online === '在线' ? 'solar:check-circle-bold' : 'solar:close-circle-bold'" class="mr-1" />
{{ row.Online }}
</Tag>
</template>
</Grid>
</div>
<userModal class="w-[100%]" />
</Page>
</div>
</template>
<style lang="css">
.userlist-page {
min-height: 100%;
}
.userlist-grid-panel {
border-radius: 24px;
background: rgb(255 255 255 / 94%);
box-shadow: 0 16px 34px rgb(15 23 42 / 8%);
overflow: hidden;
}
.userlist-toolbar-meta {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.userlist-toolbar-tag {
display: inline-flex;
align-items: center;
padding: 4px 10px;
border-radius: 999px;
font-weight: 600;
}
.user-id-chip {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 68px;
padding: 4px 12px;
border-radius: 999px;
background: linear-gradient(135deg, #e0ecff 0%, #c7ddff 100%);
color: #1d4ed8;
font-weight: 700;
box-shadow: inset 0 0 0 1px rgb(59 130 246 / 14%);
}
.user-status-tag {
display: inline-flex;
align-items: center;
border-radius: 999px;
padding-inline: 10px;
font-weight: 700;
}
.user-status-tag--online {
box-shadow: 0 8px 16px rgb(34 197 94 / 14%);
}
.user-status-tag--offline {
box-shadow: 0 8px 16px rgb(239 68 68 / 14%);
}
.broder-bottom-1 {
border-bottom: 0.5px solid rgb(136, 134, 134);
}