glossary配置;language增加文件导入功能;user展示邮件列表
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
1f5f0c1632
commit
084420bcc6
@ -25,6 +25,32 @@ export interface UserLogOrder {
|
||||
Type: number;
|
||||
Diff: number;
|
||||
ChessId: ChessType[];
|
||||
VirtualEnergy?: number;
|
||||
}
|
||||
|
||||
export interface MailItemInfo {
|
||||
Id: number;
|
||||
Num: number;
|
||||
}
|
||||
|
||||
export interface MailInfo {
|
||||
Id: number;
|
||||
Title: string;
|
||||
Content: string;
|
||||
Time: number;
|
||||
Status: number;
|
||||
Items: MailItemInfo[];
|
||||
Type: number;
|
||||
TitleEn: string;
|
||||
ContentEn: string;
|
||||
SubTitle: string;
|
||||
SubTitleEn: string;
|
||||
TitlePtBr: string;
|
||||
ContentPtBr: string;
|
||||
SubTitlePtBr: string;
|
||||
TitleEsLa: string;
|
||||
SubTitleEsLa: string;
|
||||
ContentEsLa: string;
|
||||
}
|
||||
|
||||
export interface ChessType {
|
||||
@ -60,6 +86,7 @@ export interface UserLogInfo {
|
||||
MaxCharge?: number;
|
||||
FriendList?: friendRecord[];
|
||||
AdWatch?: number;
|
||||
MailList?: Record<string, MailInfo>;
|
||||
}
|
||||
|
||||
export interface actlog {
|
||||
|
||||
@ -53,4 +53,13 @@ export async function exportLanguageFile() {
|
||||
|
||||
export async function deleteLanguageItem(data: {key: string}) {
|
||||
return requestClient.post('/language/delete', data);
|
||||
}
|
||||
|
||||
export async function importLanguageApi(file: File) {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
return requestClient.post('/language/import', formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
timeout: 120000,
|
||||
});
|
||||
}
|
||||
@ -92,7 +92,7 @@ defineEmits(['click']);
|
||||
<ul>
|
||||
<li><b>简单:</b>订单总消耗体力在15-150之间</li>
|
||||
<li><b>中等:</b>订单总消耗体力在100-600之间</li>
|
||||
<li><b>困难:</b>订单总消耗体力在500-1200之间</li>
|
||||
<li><b>困难:</b>订单总消耗体力在500-2500之间</li>
|
||||
<li><b>零件订单:</b>订单总消耗体力在40-400之间</li>
|
||||
<li><b>消耗品订单:</b>订单总消耗体力在40-200之间</li>
|
||||
</ul>
|
||||
@ -113,7 +113,7 @@ defineEmits(['click']);
|
||||
<VbenIcon :color="item.color" :icon="item.icon"
|
||||
class="size-8 transition-all duration-300 group-hover:scale-110" />
|
||||
<span class="ml-2 text-lg font-medium">{{ item.id }}</span>
|
||||
<tag class="ml-2 text-sm" :color="getTagColor(item.diff)">{{ item.diffName }}</tag>
|
||||
<tag v-if="item.group === '1'" class="ml-2 text-sm" :color="getTagColor(item.diff)">{{ item.diffName }}</tag>
|
||||
</div>
|
||||
<div class="text-foreground/80 mt-4 h-12 ">
|
||||
<div v-if="item.merge && item.merge.length > 0" class="flex flex-wrap gap-1">
|
||||
@ -125,6 +125,9 @@ defineEmits(['click']);
|
||||
</div>
|
||||
<div class="text-foreground/80 flex justify-between">
|
||||
<span class="text-sm font-semibold">{{ item.typeName }}</span>
|
||||
<span v-if="item.virtualEnergy" class="text-sm text-orange-500" title="虚拟体力">
|
||||
⚡ {{ item.virtualEnergy }}
|
||||
</span>
|
||||
<span class="text-sm">{{ item.date }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -39,6 +39,7 @@ export interface Order {
|
||||
group?: string;
|
||||
title?: string;
|
||||
url?: string;
|
||||
virtualEnergy?: number;
|
||||
}
|
||||
|
||||
export interface Chess{
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VxeGridProps, VxeGridListeners } from '#/adapter/vxe-table';
|
||||
import { Button, notification, Image } from 'ant-design-vue';
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { getLanguageList, saveLanguageList, deleteLanguageItem } from '#/api/core/statistics';
|
||||
import { Button, notification, Image, Drawer, Table } from 'ant-design-vue';
|
||||
import { Page, VbenIcon } from '@vben/common-ui';
|
||||
import { getLanguageList, saveLanguageList, deleteLanguageItem, importLanguageApi } from '#/api/core/statistics';
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import addLanguage from './addLanguage.vue';
|
||||
import type { VbenFormProps } from '#/adapter/form';
|
||||
@ -14,7 +14,7 @@ import dayjs from 'dayjs';
|
||||
import { AccessControl } from '@vben/access';
|
||||
import { useAccess } from '@vben/access';
|
||||
import GlossaryData from '#/store/glossary.json';
|
||||
import { ref, onMounted, onUnmounted } from 'vue';
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue';
|
||||
import { parseNumber } from '#/store/util';
|
||||
import {getWeightedTextLength} from '#/store/language';
|
||||
import type { languageLimit } from '#/store/language';
|
||||
@ -330,7 +330,7 @@ const gridOptions: VxeGridProps<languageType> = {
|
||||
},
|
||||
{ editRender: { name: 'input' }, field: 'pt_BR', title: 'pt_BR', filters: [{ data: "" }], filterRender: { name: "input" }, visible: pt_BRVisible, className: 'en-us-column' },
|
||||
{ editRender: { name: 'input' }, field: 'es_LATAM', title: 'es_LATAM', filters: [{ data: "" }], filterRender: { name: "input" }, visible: es_LATAMVisible, className: 'en-us-column' },
|
||||
{ slots: { default: 'glossary' }, title: 'Glossary', width: 150 },
|
||||
{ slots: { default: 'glossary' }, title: 'Glossary', width: 60, align: 'center' },
|
||||
{ slots: { default: 'character_limit' }, title: 'character_limit', width: 150 },
|
||||
{ slots: { default: 'action' }, title: $t('page.common.action'), width: 150, visible: actionVisible },
|
||||
],
|
||||
@ -424,6 +424,94 @@ const gridEvents: VxeGridListeners<languageType> = {
|
||||
//console.log('editClosed:', row, column);
|
||||
},
|
||||
};
|
||||
// Glossary Drawer 状态
|
||||
const glossaryDrawerVisible = ref(false);
|
||||
const glossaryDrawerRow = ref<languageType | null>(null);
|
||||
const glossaryDrawerItems = ref<any[]>([]);
|
||||
|
||||
function hasGlossaryMatch(row: languageType): boolean {
|
||||
if (!row.en_US) return false;
|
||||
return Object.values(GlossaryData).some((value: any) => {
|
||||
const inGameGlossary = value.term || '';
|
||||
if (!inGameGlossary) return false;
|
||||
return new RegExp(`\\b${inGameGlossary.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'i').test(row.en_US);
|
||||
});
|
||||
}
|
||||
|
||||
function openGlossaryDrawer(row: languageType) {
|
||||
glossaryDrawerRow.value = row;
|
||||
glossaryDrawerItems.value = [];
|
||||
|
||||
Object.entries(GlossaryData).forEach(([_key, value]: [string, any]) => {
|
||||
const inGameGlossary = value.term || '';
|
||||
if (row.en_US && new RegExp(`\\b${inGameGlossary.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'i').test(row.en_US)) {
|
||||
glossaryDrawerItems.value.push(value);
|
||||
}
|
||||
});
|
||||
|
||||
glossaryDrawerVisible.value = true;
|
||||
}
|
||||
|
||||
// Glossary Drawer 表格列定义
|
||||
const glossaryDrawerColumns = computed(() => {
|
||||
const cols: any[] = [
|
||||
{ title: 'No.', dataIndex: 'no', width: 60, align: 'center' },
|
||||
{ title: 'Term', dataIndex: 'term', width: 180 },
|
||||
{ title: 'en', dataIndex: 'term', width: 120 },
|
||||
];
|
||||
if (chineseVisible) {
|
||||
cols.push({ title: 'zh_CN', dataIndex: 'zh_CN', width: 120 });
|
||||
}
|
||||
if (pt_BRVisible) {
|
||||
cols.push({ title: 'pt_BR', dataIndex: 'pt_BR', width: 120 });
|
||||
}
|
||||
if (es_LATAMVisible) {
|
||||
cols.push({ title: 'es_LATAM', dataIndex: 'es_LATAM', width: 120 });
|
||||
}
|
||||
cols.push({ title: '说明', dataIndex: 'introduction', width: 300 });
|
||||
return cols;
|
||||
});
|
||||
|
||||
// 文件导入
|
||||
const fileInputRef = ref<HTMLInputElement | null>(null);
|
||||
|
||||
function handleImportClick() {
|
||||
fileInputRef.value?.click();
|
||||
}
|
||||
|
||||
async function handleFileChange(event: Event) {
|
||||
const target = event.target as HTMLInputElement;
|
||||
const file = target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
// 验证文件扩展名
|
||||
if (!file.name.toLowerCase().endsWith('.xlsx')) {
|
||||
notification.error({
|
||||
duration: 10,
|
||||
message: '请选择 .xlsx 格式的文件',
|
||||
});
|
||||
target.value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result: any = await importLanguageApi(file);
|
||||
const stats = result as { updated?: number; added?: number };
|
||||
notification.success({
|
||||
duration: 10,
|
||||
message: `导入成功!更新 ${stats.updated || 0} 条,新增 ${stats.added || 0} 条`,
|
||||
});
|
||||
GridApi.reload();
|
||||
} catch (error: any) {
|
||||
notification.error({
|
||||
duration: 10,
|
||||
message: '导入失败:' + (error?.message || String(error)),
|
||||
});
|
||||
} finally {
|
||||
target.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
const [Grid, GridApi] = useVbenVxeGrid({ formOptions, gridOptions, gridEvents });
|
||||
|
||||
function handleDocumentClick(e: MouseEvent) {
|
||||
@ -481,45 +569,6 @@ function deleteRow(row: languageType) {
|
||||
});
|
||||
}
|
||||
|
||||
function getGlossary(row: languageType) {
|
||||
const glossaryItems: string[] = [];
|
||||
let head = 'en';
|
||||
if (chineseVisible) {
|
||||
head += ' | zh';
|
||||
}
|
||||
if (pt_BRVisible) {
|
||||
head += ' | pt';
|
||||
}
|
||||
if (es_LATAMVisible) {
|
||||
head += ' | es';
|
||||
}
|
||||
|
||||
Object.entries(GlossaryData).forEach(([_key, value]: [string, any]) => {
|
||||
const inGameGlossary = value.term || '';
|
||||
if (row.en_US && new RegExp(`\\b${inGameGlossary.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'i').test(row.en_US)) {
|
||||
let content = `${inGameGlossary}`;
|
||||
if (chineseVisible) {
|
||||
const zhCN = value.zh_CN || '';
|
||||
content += ` | ${zhCN}`;
|
||||
}
|
||||
if (pt_BRVisible) {
|
||||
const ptBR = value.pt_BR || '';
|
||||
content += ` | ${ptBR}`;
|
||||
}
|
||||
if (es_LATAMVisible) {
|
||||
const esLATAM = value.es_LATAM || '';
|
||||
content += ` | ${esLATAM}`;
|
||||
}
|
||||
glossaryItems.push(content);
|
||||
}
|
||||
});
|
||||
if (glossaryItems.length > 0) {
|
||||
glossaryItems.unshift(head);
|
||||
return glossaryItems.join('\n');
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function characterLimit(row: languageType, columnName : string) {
|
||||
for (const key in language_len_limit.value) {
|
||||
if (language_len_limit.value[key] && new RegExp(language_len_limit.value[key].key || '').test(row.key)) {
|
||||
@ -623,6 +672,16 @@ function characterLimitColor(row: languageType, columnName : string) {
|
||||
{{ $t('page.common.addLine') }}
|
||||
</Button>
|
||||
<Button type="primary" @click="saveAll" class="mr-2"> {{ $t('page.common.save') }} </Button>
|
||||
<Button class="mr-2" type="primary" ghost @click="handleImportClick">
|
||||
导入
|
||||
</Button>
|
||||
<input
|
||||
ref="fileInputRef"
|
||||
type="file"
|
||||
accept=".xlsx"
|
||||
style="display: none"
|
||||
@change="handleFileChange"
|
||||
/>
|
||||
<!-- <Button type="primary" @click="exportLang"> {{ $t('page.common.gitCommit') }} </Button> -->
|
||||
</template>
|
||||
<template #image-url="{ row }">
|
||||
@ -630,7 +689,14 @@ function characterLimitColor(row: languageType, columnName : string) {
|
||||
<Image v-if="row.url" :src="row.url" height="40" width="40" />
|
||||
</template>
|
||||
<template #glossary="{ row }">
|
||||
<span style="font-size: 11px;">{{ getGlossary(row) }} </span>
|
||||
<div v-if="hasGlossaryMatch(row)" class="flex justify-center items-center h-full">
|
||||
<VbenIcon
|
||||
icon="mdi:book-open-variant"
|
||||
class="cursor-pointer text-blue-500 hover:text-blue-700 transition-colors"
|
||||
style="font-size: 20px;"
|
||||
@click="openGlossaryDrawer(row)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template #character_limit="{ row }">
|
||||
<span :style="{ fontSize: '11px', color: characterLimitColor(row, 'zh_CN') }">{{ characterLimit(row, 'zh_CN') }}</span><br>
|
||||
@ -645,5 +711,38 @@ function characterLimitColor(row: languageType, columnName : string) {
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
|
||||
<!-- Glossary 详情抽屉 -->
|
||||
<Drawer
|
||||
:open="glossaryDrawerVisible"
|
||||
title="Glossary 详情"
|
||||
placement="right"
|
||||
width="900px"
|
||||
@close="glossaryDrawerVisible = false"
|
||||
>
|
||||
<template v-if="glossaryDrawerRow">
|
||||
<div class="mb-4 p-3 bg-gray-50 rounded">
|
||||
<div class="font-bold text-base mb-1">当前 key:</div>
|
||||
<div class="text-sm text-gray-600">{{ glossaryDrawerRow.key }}</div>
|
||||
<div class="font-bold text-base mt-2 mb-1">当前 en_US:</div>
|
||||
<div class="text-sm text-gray-600">{{ glossaryDrawerRow.en_US }}</div>
|
||||
</div>
|
||||
|
||||
<div v-if="glossaryDrawerItems.length > 0">
|
||||
<div class="font-bold text-base mb-2">匹配到的 Glossary 词条({{ glossaryDrawerItems.length }}):</div>
|
||||
<Table
|
||||
:dataSource="glossaryDrawerItems"
|
||||
:columns="glossaryDrawerColumns"
|
||||
:pagination="false"
|
||||
size="small"
|
||||
:rowKey="(record: any) => record.no"
|
||||
bordered
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="text-gray-400 text-center py-8">
|
||||
未匹配到 Glossary 词条
|
||||
</div>
|
||||
</template>
|
||||
</Drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -1,11 +1,13 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import MergeData from '#/store/MergeData.json';
|
||||
import ItemDataMap from '#/store/Item.json';
|
||||
import { orderTypeData, orderDiffData } from '#/store/order';
|
||||
import { faceTypeData } from '#/store/face';
|
||||
import { useVbenModal, useVbenForm, WorkbenchTrends, VbenIcon } from '@vben/common-ui';
|
||||
import { message, Card, Collapse, Tabs, Tag } from 'ant-design-vue';
|
||||
import { message, Card, Collapse, Tabs, Tag, Button, Pagination } from 'ant-design-vue';
|
||||
import { getUserlogInfoApi } from '#/api/core/log';
|
||||
import type { MailInfo } from '#/api/core/log';
|
||||
import { userGmApi, userBanApi } from '#/api/core/user';
|
||||
import { heatmap as LoginHeatmap } from '#/component/index';
|
||||
// 引入 cal-heatmap 样式
|
||||
@ -250,6 +252,7 @@ const info = ref<{
|
||||
Friend: friendRecord[];
|
||||
MaxCharge?: number;
|
||||
AdWatch?: number;
|
||||
MailList: MailInfo[];
|
||||
}>({
|
||||
Level: 0,
|
||||
Star: 0,
|
||||
@ -270,7 +273,67 @@ const info = ref<{
|
||||
Friend: [],
|
||||
MaxCharge: 0,
|
||||
AdWatch: 0,
|
||||
MailList: [],
|
||||
});
|
||||
// 邮件语言切换 & 分页
|
||||
const mailLang = ref<'zh' | 'en' | 'pt' | 'es'>('zh');
|
||||
const mailPage = ref(1);
|
||||
const mailPageSize = ref(5);
|
||||
const mailLangOptions = [
|
||||
{ key: 'zh' as const, label: '中文' },
|
||||
{ key: 'en' as const, label: 'English' },
|
||||
{ key: 'pt' as const, label: 'Português' },
|
||||
{ key: 'es' as const, label: 'Español' },
|
||||
];
|
||||
const mailStatusMap: Record<number, string> = {
|
||||
0: '未读',
|
||||
1: '已读',
|
||||
2: '已领取',
|
||||
3: '已删除',
|
||||
};
|
||||
|
||||
function getMailStatus(status: number | undefined): string {
|
||||
return mailStatusMap[status ?? 0] || '未读';
|
||||
}
|
||||
|
||||
function getMailStatusColor(status: number | undefined): string {
|
||||
return (status ?? 0) === 0 ? 'blue' : status === 1 ? 'green' : status === 2 ? 'orange' : 'default';
|
||||
}
|
||||
|
||||
type ItemInfoMap = Record<string, { Name?: string; IType?: number; Effect?: string | number; FullResourcePath?: string }>;
|
||||
const ItemMap: ItemInfoMap = ItemDataMap;
|
||||
|
||||
function getItemDisplay(item: { Id?: number; Num?: number }): { name: string; id: number; count: number } {
|
||||
const itemId = item?.Id ?? 0;
|
||||
const itemCount = item?.Num ?? 0;
|
||||
const itemInfo = ItemMap[String(itemId)];
|
||||
return {
|
||||
name: itemInfo?.Name || `道具#${itemId}`,
|
||||
id: itemId,
|
||||
count: itemCount,
|
||||
};
|
||||
}
|
||||
|
||||
function getMailDisplay(mail: MailInfo, lang: string) {
|
||||
switch (lang) {
|
||||
case 'en':
|
||||
return { title: mail.TitleEn || mail.Title, subTitle: mail.SubTitleEn || mail.SubTitle, content: mail.ContentEn || mail.Content };
|
||||
case 'pt':
|
||||
return { title: mail.TitlePtBr || mail.Title, subTitle: mail.SubTitlePtBr || mail.SubTitle, content: mail.ContentPtBr || mail.Content };
|
||||
case 'es':
|
||||
return { title: mail.TitleEsLa || mail.Title, subTitle: mail.SubTitleEsLa || mail.SubTitle, content: mail.ContentEsLa || mail.Content };
|
||||
default:
|
||||
return { title: mail.Title, subTitle: mail.SubTitle, content: mail.Content };
|
||||
}
|
||||
}
|
||||
|
||||
const paginatedMailList = computed(() => {
|
||||
const start = (mailPage.value - 1) * mailPageSize.value;
|
||||
return info.value.MailList.slice(start, start + mailPageSize.value);
|
||||
});
|
||||
|
||||
const mailTotal = computed(() => info.value.MailList.length);
|
||||
|
||||
let trendItems: WorkbenchTrendItem[] = [
|
||||
|
||||
];
|
||||
@ -395,8 +458,19 @@ const [Modal, modalApi] = useVbenModal({
|
||||
icon: 'lets-icons:order',
|
||||
title: String(r.Order[i]?.Id) || '',
|
||||
url: 'https://merge-pet-web.oss-cn-heyuan.aliyuncs.com/favicon.ico',
|
||||
virtualEnergy: r.Order[i]?.VirtualEnergy || 0,
|
||||
});
|
||||
}
|
||||
// 邮件列表解析
|
||||
info.value.MailList = [];
|
||||
mailPage.value = 1;
|
||||
if (r.MailList) {
|
||||
for (const [, mail] of Object.entries(r.MailList)) {
|
||||
if (mail) {
|
||||
info.value.MailList.push(mail);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
//console.error('Error fetching user info:', error);
|
||||
}
|
||||
@ -567,6 +641,67 @@ function formatActLog(type: number, content = ''): [string, string] {
|
||||
</div>
|
||||
<div class="player-detail-section player-detail-section--transparent mt-5">
|
||||
<Collapse class="player-detail-collapse" :bordered="false">
|
||||
<Collapse.Panel key="player-mail" :header="`邮件 (${mailTotal})`">
|
||||
<!-- 语言切换 -->
|
||||
<div class="flex gap-2 mb-4">
|
||||
<Button
|
||||
v-for="lang in mailLangOptions"
|
||||
:key="lang.key"
|
||||
size="small"
|
||||
:type="mailLang === lang.key ? 'primary' : 'default'"
|
||||
@click="mailLang = lang.key"
|
||||
>
|
||||
{{ lang.label }}
|
||||
</Button>
|
||||
</div>
|
||||
<!-- 邮件列表 -->
|
||||
<div v-if="mailTotal > 0" class="space-y-3">
|
||||
<Card
|
||||
v-for="mail in paginatedMailList"
|
||||
:key="mail.Id"
|
||||
size="small"
|
||||
class="mail-card"
|
||||
:bordered="true"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<Tag :color="getMailStatusColor(mail.Status)">
|
||||
{{ getMailStatus(mail.Status) }}
|
||||
</Tag>
|
||||
<span class="font-semibold text-base">{{ getMailDisplay(mail, mailLang).title }}</span>
|
||||
</div>
|
||||
<span class="text-xs text-gray-400">
|
||||
{{ dayjs(mail.Time * 1000).format('YYYY-MM-DD HH:mm:ss') }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="getMailDisplay(mail, mailLang).subTitle" class="text-sm text-gray-500 mt-1">
|
||||
{{ getMailDisplay(mail, mailLang).subTitle }}
|
||||
</div>
|
||||
<div class="text-sm mt-2 whitespace-pre-wrap">
|
||||
{{ getMailDisplay(mail, mailLang).content }}
|
||||
</div>
|
||||
<div v-if="mail.Items && mail.Items.length > 0" class="flex items-center gap-2 mt-2">
|
||||
<span class="text-xs text-gray-400">奖励:</span>
|
||||
<Tag v-for="(item, idx) in mail.Items" :key="idx" color="gold">
|
||||
{{ getItemDisplay(item).name }} × {{ item.Num }}
|
||||
</Tag>
|
||||
</div>
|
||||
</Card>
|
||||
<!-- 分页 -->
|
||||
<div v-if="mailTotal > mailPageSize" class="flex justify-center">
|
||||
<Pagination
|
||||
v-model:current="mailPage"
|
||||
:page-size="mailPageSize"
|
||||
:total="mailTotal"
|
||||
size="small"
|
||||
show-less-items
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="text-center text-gray-400 py-4">
|
||||
暂无邮件
|
||||
</div>
|
||||
</Collapse.Panel>
|
||||
<Collapse.Panel key="latest-trends" header="最新动态">
|
||||
<WorkbenchTrends :items="trendItems" title="最新动态" />
|
||||
</Collapse.Panel>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user