This commit is contained in:
hy 2026-04-22 21:26:44 +08:00
parent 22bf3427fe
commit 6f71528fcb
4 changed files with 704 additions and 196 deletions

View File

@ -6,10 +6,13 @@ import { useAccessStore } from '@vben/stores';
import { import {
deleteBaishunGame, deleteBaishunGame,
fetchBaishunCatalog,
getBaishunProviderConfig,
importBaishunCatalog, importBaishunCatalog,
pageBaishunCatalog,
pageBaishunGames, pageBaishunGames,
saveBaishunGame, saveBaishunGame,
syncBaishunGames, saveBaishunProviderConfig,
} from '#/api/legacy/baishun-game'; } from '#/api/legacy/baishun-game';
import { getAllowedSysOrigins } from '#/views/system/shared'; import { getAllowedSysOrigins } from '#/views/system/shared';
@ -26,6 +29,8 @@ import {
Select, Select,
Space, Space,
Table, Table,
TabPane,
Tabs,
Tag, Tag,
message, message,
} from 'antdv-next'; } from 'antdv-next';
@ -37,7 +42,7 @@ import {
defineOptions({ name: 'OperateBaishunGameManage' }); defineOptions({ name: 'OperateBaishunGameManage' });
function createForm(sysOrigin = '') { function createGameForm(sysOrigin = '') {
return { return {
amounts: '', amounts: '',
category: 'OTHER', category: 'OTHER',
@ -58,7 +63,7 @@ function createForm(sysOrigin = '') {
previewUrl: '', previewUrl: '',
regions: '', regions: '',
safeHeight: 0, safeHeight: 0,
showcase: false as any, showcase: true as any,
sort: 0, sort: 0,
sysOrigin, sysOrigin,
vendorGameId: undefined as any, vendorGameId: undefined as any,
@ -67,6 +72,20 @@ function createForm(sysOrigin = '') {
}; };
} }
function createProviderForm(sysOrigin = '') {
return {
sysOrigin,
platformBaseUrl: '',
appId: undefined as any,
appName: '',
appChannel: '',
appKey: '',
gsp: 101 as any,
launchCodeTtlSeconds: 300 as any,
ssTokenTtlSeconds: 86400 as any,
};
}
const accessStore = useAccessStore(); const accessStore = useAccessStore();
const sysOriginOptions = computed(() => { const sysOriginOptions = computed(() => {
const options = getAllowedSysOrigins(accessStore.accessCodes || []); const options = getAllowedSysOrigins(accessStore.accessCodes || []);
@ -105,24 +124,37 @@ const orientationOptions = [
{ label: '2', value: 2 as any }, { label: '2', value: 2 as any },
]; ];
const query = reactive<Record<string, any>>({ const activeTab = ref('configured');
const contextQuery = reactive({ sysOrigin: '' });
const gameQuery = reactive<Record<string, any>>({
cursor: 1, cursor: 1,
keyword: '', keyword: '',
limit: 20, limit: 20,
showcase: '', showcase: '',
sysOrigin: '', });
const catalogQuery = reactive<Record<string, any>>({
cursor: 1,
keyword: '',
limit: 20,
}); });
const loading = ref(false); const gameLoading = ref(false);
const saving = ref(false); const gameSaving = ref(false);
const importing = ref(false); const providerLoading = ref(false);
const syncing = ref(false); const providerSaving = ref(false);
const total = ref(0); const catalogLoading = ref(false);
const catalogFetching = ref(false);
const catalogImporting = ref(false);
const totalGames = ref(0);
const totalCatalog = ref(0);
const modalOpen = ref(false); const modalOpen = ref(false);
const list = ref<Array<Record<string, any>>>([]); const gameList = ref<Array<Record<string, any>>>([]);
const form = reactive<Record<string, any>>(createForm()); const catalogList = ref<Array<Record<string, any>>>([]);
const selectedCatalogKeys = ref<Array<number>>([]);
const gameForm = reactive<Record<string, any>>(createGameForm());
const providerForm = reactive<Record<string, any>>(createProviderForm());
const columns = [ const gameColumns = [
{ dataIndex: 'cover', key: 'cover', title: '封面', width: 90 }, { dataIndex: 'cover', key: 'cover', title: '封面', width: 90 },
{ dataIndex: 'name', key: 'name', title: '名称', width: 180 }, { dataIndex: 'name', key: 'name', title: '名称', width: 180 },
{ dataIndex: 'gameId', key: 'gameId', title: '内部ID', width: 150 }, { dataIndex: 'gameId', key: 'gameId', title: '内部ID', width: 150 },
@ -137,37 +169,124 @@ const columns = [
{ dataIndex: 'actions', key: 'actions', title: '操作', width: 160 }, { dataIndex: 'actions', key: 'actions', title: '操作', width: 160 },
]; ];
const catalogColumns = [
{ dataIndex: 'cover', key: 'cover', title: '封面', width: 90 },
{ dataIndex: 'name', key: 'name', title: '名称', width: 180 },
{ dataIndex: 'gameId', key: 'gameId', title: '内部ID', width: 150 },
{ dataIndex: 'vendorGameId', key: 'vendorGameId', title: '百顺ID', width: 120 },
{ dataIndex: 'status', key: 'status', title: '目录状态', width: 110 },
{ dataIndex: 'packageVersion', key: 'packageVersion', title: '包版本', width: 110 },
{ dataIndex: 'gsp', key: 'gsp', title: 'GSP', width: 90 },
{ dataIndex: 'added', key: 'added', title: '后台列表', width: 110 },
{ dataIndex: 'downloadUrl', key: 'downloadUrl', title: '入口地址', width: 360 },
{ dataIndex: 'updateTime', key: 'updateTime', title: '更新时间', width: 180 },
];
const catalogRowSelection = computed(() => ({
selectedRowKeys: selectedCatalogKeys.value,
onChange: (keys: Array<number | string>) => {
selectedCatalogKeys.value = keys.map((item) => Number(item));
},
getCheckboxProps: (record: Record<string, any>) => ({
disabled: Boolean(record.added),
}),
}));
watch( watch(
sysOriginOptions, sysOriginOptions,
(options) => { (options) => {
if (!query.sysOrigin) { if (!contextQuery.sysOrigin) {
query.sysOrigin = String(options[0]?.value || ''); contextQuery.sysOrigin = String(options[0]?.value || '');
} }
}, },
{ immediate: true }, { immediate: true },
); );
watch( watch(
() => query.sysOrigin, () => contextQuery.sysOrigin,
(value) => { (value) => {
if (value) { if (!value) {
void loadData(true); return;
} }
Object.assign(providerForm, createProviderForm(value));
selectedCatalogKeys.value = [];
void Promise.all([loadProviderConfig(), loadGames(true), loadCatalog(true)]);
}, },
{ immediate: true }, { immediate: true },
); );
function resetForm() { function resetGameForm() {
Object.assign(form, createForm(query.sysOrigin)); Object.assign(gameForm, createGameForm(contextQuery.sysOrigin));
}
function applyProviderForm(record: Record<string, any>) {
Object.assign(providerForm, createProviderForm(contextQuery.sysOrigin), {
...record,
sysOrigin: contextQuery.sysOrigin,
appId: Number(record.appId || 0) || undefined,
gsp: Number(record.gsp || 101) || 101,
launchCodeTtlSeconds:
Number(record.launchCodeTtlSeconds || 300) || 300,
ssTokenTtlSeconds:
Number(record.ssTokenTtlSeconds || 86400) || 86400,
});
}
async function loadProviderConfig() {
if (!contextQuery.sysOrigin) {
return;
}
providerLoading.value = true;
try {
const result = await getBaishunProviderConfig({
sysOrigin: contextQuery.sysOrigin,
});
applyProviderForm(result || {});
} finally {
providerLoading.value = false;
}
}
async function persistProviderConfig(showSuccess = true) {
if (
!contextQuery.sysOrigin ||
!providerForm.platformBaseUrl ||
!providerForm.appId ||
!providerForm.appChannel ||
!providerForm.appKey
) {
message.warning('请补全百顺平台地址、AppId、AppChannel、AppKey');
return false;
}
providerSaving.value = true;
try {
const result = await saveBaishunProviderConfig({
...providerForm,
sysOrigin: contextQuery.sysOrigin,
appId: Number(providerForm.appId || 0),
gsp: Number(providerForm.gsp || 101),
launchCodeTtlSeconds: Number(providerForm.launchCodeTtlSeconds || 300),
ssTokenTtlSeconds: Number(providerForm.ssTokenTtlSeconds || 86400),
});
applyProviderForm(result || providerForm);
if (showSuccess) {
message.success('百顺配置已保存');
}
return true;
} catch {
return false;
} finally {
providerSaving.value = false;
}
} }
function openCreate() { function openCreate() {
resetForm(); resetGameForm();
modalOpen.value = true; modalOpen.value = true;
} }
function openEdit(record: Record<string, any>) { function openEdit(record: Record<string, any>) {
Object.assign(form, createForm(query.sysOrigin), { Object.assign(gameForm, createGameForm(contextQuery.sysOrigin), {
...record, ...record,
id: String(record.id || ''), id: String(record.id || ''),
vendorGameId: Number(record.vendorGameId || 0) || undefined, vendorGameId: Number(record.vendorGameId || 0) || undefined,
@ -175,50 +294,88 @@ function openEdit(record: Record<string, any>) {
modalOpen.value = true; modalOpen.value = true;
} }
async function loadData(reset = false) { async function loadGames(reset = false) {
if (!query.sysOrigin) { if (!contextQuery.sysOrigin) {
return; return;
} }
if (reset) { if (reset) {
query.cursor = 1; gameQuery.cursor = 1;
} }
loading.value = true; gameLoading.value = true;
try { try {
const result = await pageBaishunGames({ ...query }); const result = await pageBaishunGames({
list.value = Array.isArray(result.records) ? result.records : []; ...gameQuery,
total.value = Number(result.total || 0); sysOrigin: contextQuery.sysOrigin,
});
gameList.value = Array.isArray(result.records) ? result.records : [];
totalGames.value = Number(result.total || 0);
} finally { } finally {
loading.value = false; gameLoading.value = false;
} }
} }
function handleSearch() { async function loadCatalog(reset = false) {
void loadData(true); if (!contextQuery.sysOrigin) {
return;
}
if (reset) {
catalogQuery.cursor = 1;
}
catalogLoading.value = true;
try {
const result = await pageBaishunCatalog({
...catalogQuery,
sysOrigin: contextQuery.sysOrigin,
});
catalogList.value = Array.isArray(result.records) ? result.records : [];
totalCatalog.value = Number(result.total || 0);
} finally {
catalogLoading.value = false;
}
} }
function handlePageChange(page: number, pageSize: number) { function handleGameSearch() {
query.cursor = page; void loadGames(true);
query.limit = pageSize;
void loadData();
} }
async function submitForm() { function handleCatalogSearch() {
if (!form.sysOrigin || !form.name || !form.gameId || !form.vendorGameId) { void loadCatalog(true);
}
function handleGamePageChange(page: number, pageSize: number) {
gameQuery.cursor = page;
gameQuery.limit = pageSize;
void loadGames();
}
function handleCatalogPageChange(page: number, pageSize: number) {
catalogQuery.cursor = page;
catalogQuery.limit = pageSize;
void loadCatalog();
}
async function submitGameForm() {
if (
!gameForm.sysOrigin ||
!gameForm.name ||
!gameForm.gameId ||
!gameForm.vendorGameId
) {
message.warning('请补全系统、名称、内部ID和百顺ID'); message.warning('请补全系统、名称、内部ID和百顺ID');
return; return;
} }
saving.value = true; gameSaving.value = true;
try { try {
await saveBaishunGame({ await saveBaishunGame({
...form, ...gameForm,
id: Number(form.id || 0), id: Number(gameForm.id || 0),
vendorGameId: Number(form.vendorGameId || 0), vendorGameId: Number(gameForm.vendorGameId || 0),
}); });
message.success('保存成功'); message.success('保存成功');
modalOpen.value = false; modalOpen.value = false;
await loadData(true); await Promise.all([loadGames(true), loadCatalog(true)]);
} finally { } finally {
saving.value = false; gameSaving.value = false;
} }
} }
@ -228,207 +385,391 @@ function handleDelete(record: Record<string, any>) {
async onOk() { async onOk() {
await deleteBaishunGame(record.id, record.sysOrigin); await deleteBaishunGame(record.id, record.sysOrigin);
message.success('删除成功'); message.success('删除成功');
await loadData(true); await Promise.all([loadGames(true), loadCatalog(true)]);
}, },
}); });
} }
async function handleImportCatalog() { async function handleSaveProviderConfig() {
if (!query.sysOrigin) { await persistProviderConfig(true);
}
async function handleFetchCatalog() {
if (!(await persistProviderConfig(false))) {
return; return;
} }
importing.value = true; catalogFetching.value = true;
try { try {
const result = await importBaishunCatalog({ sysOrigin: query.sysOrigin }); const result = await fetchBaishunCatalog({
message.success(`导入完成:新增 ${Number(result.imported || 0)},已存在 ${Number(result.existing || 0)}`); sysOrigin: contextQuery.sysOrigin,
await loadData(true); });
message.success(
`获取完成:新增 ${Number(result.inserted || 0)},更新 ${Number(result.updated || 0)},跳过 ${Number(result.skipped || 0)}`,
);
activeTab.value = 'catalog';
await loadCatalog(true);
} finally { } finally {
importing.value = false; catalogFetching.value = false;
} }
} }
async function handleSync() { async function handleImportSelected() {
if (!query.sysOrigin) { if (!contextQuery.sysOrigin) {
return; return;
} }
syncing.value = true; if (selectedCatalogKeys.value.length === 0) {
message.warning('请选择要添加到后台列表的百顺游戏');
return;
}
catalogImporting.value = true;
try { try {
const result = await syncBaishunGames({ sysOrigin: query.sysOrigin }); const result = await importBaishunCatalog({
sysOrigin: contextQuery.sysOrigin,
vendorGameIds: selectedCatalogKeys.value,
});
selectedCatalogKeys.value = [];
message.success( message.success(
`同步完成:目录新增 ${Number(result.inserted || 0)},更新 ${Number(result.updated || 0)},导入 ${Number(result.imported || 0)}`, `添加完成:新增 ${Number(result.imported || 0)},已存在 ${Number(result.existing || 0)}`,
); );
await loadData(true); await Promise.all([loadGames(true), loadCatalog(true)]);
} finally { } finally {
syncing.value = false; catalogImporting.value = false;
} }
} }
</script> </script>
<template> <template>
<Page title="百顺游戏管理"> <Page title="三方-百顺">
<Card> <Card :loading="providerLoading" title="百顺接入配置">
<div class="toolbar"> <Form layout="vertical">
<SysOriginSelect <div class="form-grid provider-grid">
v-model:value="query.sysOrigin" <FormItem label="系统">
style="width: 140px" <SysOriginSelect
:options="sysOriginOptions" v-model:value="contextQuery.sysOrigin"
/> :options="sysOriginOptions"
<Select />
v-model:value="query.showcase" </FormItem>
style="width: 120px" <FormItem
:options="showcaseOptions" label="平台地址"
@change="handleSearch" extra="填写百顺平台接口地址,例如 https://game-cn-test.jieyou.shop不要填 Lucky Gift 域名。"
/> >
<Input <Input
v-model:value="query.keyword" v-model:value="providerForm.platformBaseUrl"
placeholder="搜索名称 / 内部ID / 百顺ID" placeholder="https://game-cn-test.jieyou.shop"
style="width: 240px" />
@pressEnter="handleSearch" </FormItem>
/> <FormItem label="AppId">
<Button type="primary" :loading="loading" @click="handleSearch"> <InputNumber
搜索 v-model:value="providerForm.appId"
</Button> :min="1"
<Button :loading="importing" @click="handleImportCatalog"> style="width: 100%"
导入本地目录 />
</Button> </FormItem>
<Button :loading="syncing" @click="handleSync"> <FormItem label="AppName">
同步百顺目录 <Input v-model:value="providerForm.appName" placeholder="可选" />
</Button> </FormItem>
<Button type="primary" @click="openCreate"> <FormItem label="AppChannel">
新增 <Input
</Button> v-model:value="providerForm.appChannel"
</div> placeholder="yumiparty"
/>
</FormItem>
<FormItem label="AppKey">
<Input
v-model:value="providerForm.appKey"
placeholder="请输入百顺密钥"
type="password"
/>
</FormItem>
<FormItem label="默认 GSP">
<InputNumber
v-model:value="providerForm.gsp"
:min="1"
style="width: 100%"
/>
</FormItem>
<FormItem label="LaunchCode TTL(秒)">
<InputNumber
v-model:value="providerForm.launchCodeTtlSeconds"
:min="1"
style="width: 100%"
/>
</FormItem>
<FormItem label="SsToken TTL(秒)">
<InputNumber
v-model:value="providerForm.ssTokenTtlSeconds"
:min="1"
style="width: 100%"
/>
</FormItem>
</div>
<div class="toolbar">
<Button
type="primary"
:loading="providerSaving"
@click="handleSaveProviderConfig"
>
保存配置
</Button>
<Button :loading="catalogFetching" @click="handleFetchCatalog">
获取百顺游戏列表
</Button>
<span class="hint">
房间页只读取已添加游戏目录页只负责预览和选择导入
</span>
</div>
</Form>
</Card>
<Table <Card class="content-card">
:columns="columns" <Tabs v-model:activeKey="activeTab">
:data-source="list" <TabPane key="configured" tab="已添加游戏">
:loading="loading" <div class="toolbar">
:pagination="false" <Select
row-key="id" v-model:value="gameQuery.showcase"
:scroll="{ x: 1600 }" style="width: 120px"
> :options="showcaseOptions"
<template #bodyCell="{ column, record }"> @change="handleGameSearch"
<template v-if="column.key === 'cover'"> />
<Image v-if="record.cover" :src="record.cover" class="cover" /> <Input
<span v-else>-</span> v-model:value="gameQuery.keyword"
</template> placeholder="搜索名称 / 内部ID / 百顺ID"
<template v-else-if="column.key === 'showcase'"> style="width: 240px"
<Tag :color="record.showcase ? 'success' : 'default'"> @pressEnter="handleGameSearch"
{{ record.showcase ? '上架' : '下架' }} />
</Tag> <Button type="primary" :loading="gameLoading" @click="handleGameSearch">
</template> 搜索
<template v-else-if="column.key === 'actions'"> </Button>
<Space> <Button type="primary" @click="openCreate">
<Button size="small" type="link" @click="openEdit(record)"> 新增
编辑 </Button>
</Button> </div>
<Button danger size="small" type="link" @click="handleDelete(record)">
删除
</Button>
</Space>
</template>
</template>
</Table>
<div v-if="total > 0" class="pager"> <Table
<Pagination :columns="gameColumns"
:current="query.cursor" :data-source="gameList"
:page-size="query.limit" :loading="gameLoading"
:total="total" :pagination="false"
show-size-changer row-key="id"
@change="handlePageChange" :scroll="{ x: 1600 }"
@showSizeChange="handlePageChange" >
/> <template #bodyCell="{ column, record }">
</div> <template v-if="column.key === 'cover'">
<Image v-if="record.cover" :src="record.cover" class="cover" />
<span v-else>-</span>
</template>
<template v-else-if="column.key === 'showcase'">
<Tag :color="record.showcase ? 'success' : 'default'">
{{ record.showcase ? '上架' : '下架' }}
</Tag>
</template>
<template v-else-if="column.key === 'actions'">
<Space>
<Button size="small" type="link" @click="openEdit(record)">
编辑
</Button>
<Button
danger
size="small"
type="link"
@click="handleDelete(record)"
>
删除
</Button>
</Space>
</template>
</template>
</Table>
<div v-if="totalGames > 0" class="pager">
<Pagination
:current="gameQuery.cursor"
:page-size="gameQuery.limit"
:total="totalGames"
show-size-changer
@change="handleGamePageChange"
@showSizeChange="handleGamePageChange"
/>
</div>
</TabPane>
<TabPane key="catalog" tab="百顺目录">
<div class="toolbar">
<Input
v-model:value="catalogQuery.keyword"
placeholder="搜索目录名称 / 内部ID / 百顺ID"
style="width: 260px"
@pressEnter="handleCatalogSearch"
/>
<Button :loading="catalogLoading" type="primary" @click="handleCatalogSearch">
搜索
</Button>
<Button :loading="catalogFetching" @click="handleFetchCatalog">
重新获取目录
</Button>
<Button
type="primary"
:loading="catalogImporting"
@click="handleImportSelected"
>
添加已选游戏
</Button>
</div>
<Table
:columns="catalogColumns"
:data-source="catalogList"
:loading="catalogLoading"
:pagination="false"
row-key="vendorGameId"
:row-selection="catalogRowSelection"
:scroll="{ x: 1700 }"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'cover'">
<Image v-if="record.cover" :src="record.cover" class="cover" />
<span v-else>-</span>
</template>
<template v-else-if="column.key === 'status'">
<Tag :color="record.status === 'ENABLED' ? 'success' : 'default'">
{{ record.status || '-' }}
</Tag>
</template>
<template v-else-if="column.key === 'added'">
<Tag :color="record.added ? 'success' : 'default'">
{{ record.added ? (record.showcase ? '已添加-上架' : '已添加-下架') : '未添加' }}
</Tag>
</template>
<template v-else-if="column.key === 'downloadUrl'">
<a
v-if="record.downloadUrl"
:href="record.downloadUrl"
class="link-cell"
rel="noreferrer"
target="_blank"
>
{{ record.downloadUrl }}
</a>
<span v-else>-</span>
</template>
</template>
</Table>
<div v-if="totalCatalog > 0" class="pager">
<Pagination
:current="catalogQuery.cursor"
:page-size="catalogQuery.limit"
:total="totalCatalog"
show-size-changer
@change="handleCatalogPageChange"
@showSizeChange="handleCatalogPageChange"
/>
</div>
</TabPane>
</Tabs>
</Card> </Card>
<Modal <Modal
:confirm-loading="saving" :confirm-loading="gameSaving"
:open="modalOpen" :open="modalOpen"
destroy-on-close destroy-on-close
:title="form.id ? '编辑百顺游戏' : '新增百顺游戏'" :title="gameForm.id ? '编辑百顺游戏' : '新增百顺游戏'"
width="860px" width="860px"
@cancel="modalOpen = false" @cancel="modalOpen = false"
@ok="submitForm" @ok="submitGameForm"
> >
<Form layout="vertical"> <Form layout="vertical">
<div class="form-grid"> <div class="form-grid">
<FormItem label="系统"> <FormItem label="系统">
<SysOriginSelect v-model:value="form.sysOrigin" :options="sysOriginOptions" /> <SysOriginSelect
v-model:value="gameForm.sysOrigin"
:options="sysOriginOptions"
/>
</FormItem> </FormItem>
<FormItem label="名称"> <FormItem label="名称">
<Input v-model:value="form.name" placeholder="请输入游戏名称" /> <Input v-model:value="gameForm.name" placeholder="请输入游戏名称" />
</FormItem> </FormItem>
<FormItem label="内部ID"> <FormItem label="内部ID">
<Input v-model:value="form.gameId" placeholder="例如 bs_1100" /> <Input v-model:value="gameForm.gameId" placeholder="例如 bs_1100" />
</FormItem> </FormItem>
<FormItem label="百顺ID"> <FormItem label="百顺ID">
<InputNumber v-model:value="form.vendorGameId" :min="1" style="width: 100%" /> <InputNumber
v-model:value="gameForm.vendorGameId"
:min="1"
style="width: 100%"
/>
</FormItem> </FormItem>
<FormItem label="编号"> <FormItem label="编号">
<Input v-model:value="form.gameCode" placeholder="默认取百顺ID" /> <Input v-model:value="gameForm.gameCode" placeholder="默认取百顺ID" />
</FormItem> </FormItem>
<FormItem label="分类"> <FormItem label="分类">
<Select v-model:value="form.category" :options="categoryOptions" /> <Select v-model:value="gameForm.category" :options="categoryOptions" />
</FormItem> </FormItem>
<FormItem label="客户端"> <FormItem label="客户端">
<Select v-model:value="form.clientOrigin" :options="clientOriginOptions" /> <Select
v-model:value="gameForm.clientOrigin"
:options="clientOriginOptions"
/>
</FormItem> </FormItem>
<FormItem label="状态"> <FormItem label="状态">
<Select v-model:value="form.showcase" :options="booleanOptions" /> <Select v-model:value="gameForm.showcase" :options="booleanOptions" />
</FormItem> </FormItem>
<FormItem label="是否全屏"> <FormItem label="是否全屏">
<Select v-model:value="form.fullScreen" :options="booleanOptions" /> <Select v-model:value="gameForm.fullScreen" :options="booleanOptions" />
</FormItem> </FormItem>
<FormItem label="Game Mode"> <FormItem label="Game Mode">
<Select v-model:value="form.gameMode" :options="gameModeOptions" /> <Select v-model:value="gameForm.gameMode" :options="gameModeOptions" />
</FormItem> </FormItem>
<FormItem label="启动模式"> <FormItem label="启动模式">
<Select v-model:value="form.launchMode" :options="launchModeOptions" /> <Select v-model:value="gameForm.launchMode" :options="launchModeOptions" />
</FormItem> </FormItem>
<FormItem label="排序"> <FormItem label="排序">
<InputNumber v-model:value="form.sort" :min="0" style="width: 100%" /> <InputNumber v-model:value="gameForm.sort" :min="0" style="width: 100%" />
</FormItem> </FormItem>
<FormItem label="安全区高度"> <FormItem label="安全区高度">
<InputNumber v-model:value="form.safeHeight" :min="0" style="width: 100%" /> <InputNumber
v-model:value="gameForm.safeHeight"
:min="0"
style="width: 100%"
/>
</FormItem> </FormItem>
<FormItem label="Orientation"> <FormItem label="Orientation">
<Select <Select
v-model:value="form.orientation" v-model:value="gameForm.orientation"
:options="orientationOptions" :options="orientationOptions"
allow-clear allow-clear
/> />
</FormItem> </FormItem>
<FormItem label="宽"> <FormItem label="宽">
<InputNumber v-model:value="form.width" :min="0" style="width: 100%" /> <InputNumber v-model:value="gameForm.width" :min="0" style="width: 100%" />
</FormItem> </FormItem>
<FormItem label="高"> <FormItem label="高">
<InputNumber v-model:value="form.height" :min="0" style="width: 100%" /> <InputNumber v-model:value="gameForm.height" :min="0" style="width: 100%" />
</FormItem> </FormItem>
<FormItem label="GSP"> <FormItem label="GSP">
<Input v-model:value="form.gsp" placeholder="默认取目录或系统配置" /> <Input v-model:value="gameForm.gsp" placeholder="默认取系统百顺配置" />
</FormItem> </FormItem>
<FormItem label="金额"> <FormItem label="金额">
<Input v-model:value="form.amounts" placeholder="可选" /> <Input v-model:value="gameForm.amounts" placeholder="可选" />
</FormItem> </FormItem>
<FormItem label="区域"> <FormItem label="区域">
<Input v-model:value="form.regions" placeholder="多个区域用逗号分隔" /> <Input v-model:value="gameForm.regions" placeholder="多个区域用逗号分隔" />
</FormItem> </FormItem>
<FormItem label="货币图标"> <FormItem label="货币图标">
<Input v-model:value="form.currencyIcon" placeholder="可选" /> <Input v-model:value="gameForm.currencyIcon" placeholder="可选" />
</FormItem> </FormItem>
</div> </div>
<FormItem label="封面"> <FormItem label="封面">
<Input v-model:value="form.cover" placeholder="请输入封面 URL" /> <Input v-model:value="gameForm.cover" placeholder="请输入封面 URL" />
</FormItem> </FormItem>
<FormItem label="预览地址"> <FormItem label="预览地址">
<Input v-model:value="form.previewUrl" placeholder="请输入预览地址" /> <Input v-model:value="gameForm.previewUrl" placeholder="请输入预览地址" />
</FormItem> </FormItem>
<FormItem label="包地址"> <FormItem label="包地址">
<Input v-model:value="form.packageUrl" placeholder="请输入包地址" /> <Input v-model:value="gameForm.packageUrl" placeholder="请输入包地址" />
</FormItem> </FormItem>
<FormItem label="包版本"> <FormItem label="包版本">
<Input v-model:value="form.packageVersion" placeholder="请输入包版本" /> <Input v-model:value="gameForm.packageVersion" placeholder="请输入包版本" />
</FormItem> </FormItem>
</Form> </Form>
</Modal> </Modal>
@ -436,6 +777,10 @@ async function handleSync() {
</template> </template>
<style scoped> <style scoped>
.content-card {
margin-top: 16px;
}
.toolbar { .toolbar {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
@ -462,8 +807,33 @@ async function handleSync() {
gap: 0 16px; gap: 0 16px;
} }
.provider-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.hint {
color: rgb(100 116 139);
line-height: 32px;
}
.link-cell {
display: inline-block;
max-width: 340px;
overflow: hidden;
color: rgb(37 99 235);
text-overflow: ellipsis;
white-space: nowrap;
}
@media (max-width: 1200px) {
.provider-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 900px) { @media (max-width: 900px) {
.form-grid { .form-grid,
.provider-grid {
grid-template-columns: minmax(0, 1fr); grid-template-columns: minmax(0, 1fr);
} }
} }

View File

@ -1,4 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import dayjs from 'dayjs';
import { import {
computed, computed,
reactive, reactive,
@ -12,7 +14,6 @@ import { useAccessStore } from '@vben/stores';
import { getCountryAlls } from '#/api/legacy/system'; import { getCountryAlls } from '#/api/legacy/system';
import { import {
getUserBaseInfoByAccount,
pageUserBaseInfo, pageUserBaseInfo,
} from '#/api/legacy/user'; } from '#/api/legacy/user';
import { getAllowedSysOrigins } from '#/views/system/shared'; import { getAllowedSysOrigins } from '#/views/system/shared';
@ -44,10 +45,14 @@ import {
REGISTER_ORIGIN_OPTIONS, REGISTER_ORIGIN_OPTIONS,
USER_TYPE_OPTIONS, USER_TYPE_OPTIONS,
} from './constants'; } from './constants';
import UserProfileLink from './components/user-profile-link.vue';
import UserAuthInfoDrawer from './components/user-auth-info-drawer.vue'; import UserAuthInfoDrawer from './components/user-auth-info-drawer.vue';
import UserBalanceHandleModal from './components/user-balance-handle-modal.vue'; import UserBalanceHandleModal from './components/user-balance-handle-modal.vue';
import UserResetPasswordModal from './components/user-reset-password-modal.vue'; import UserResetPasswordModal from './components/user-reset-password-modal.vue';
import {
getUserProfileAvatar,
getUserProfileId,
getUserProfileName,
} from './shared';
defineOptions({ name: 'OperateUserManage' }); defineOptions({ name: 'OperateUserManage' });
@ -114,7 +119,6 @@ const columns = [
}, },
{ dataIndex: 'accountStatus', key: 'accountStatus', title: '账户', width: 120 }, { dataIndex: 'accountStatus', key: 'accountStatus', title: '账户', width: 120 },
{ dataIndex: 'registerSource', key: 'registerSource', title: '注册来源', width: 180 }, { dataIndex: 'registerSource', key: 'registerSource', title: '注册来源', width: 180 },
{ dataIndex: 'ids', key: 'ids', title: '编号', width: 180 },
{ dataIndex: 'time', key: 'time', title: '时间', width: 220 }, { dataIndex: 'time', key: 'time', title: '时间', width: 220 },
{ dataIndex: 'actions', key: 'actions', title: '操作', width: 320 }, { dataIndex: 'actions', key: 'actions', title: '操作', width: 320 },
]; ];
@ -245,6 +249,21 @@ function openUserDetails(userId?: number | string) {
router.push(`/common/user/deatils/${userId}`); router.push(`/common/user/deatils/${userId}`);
} }
function getUserShortId(profile?: Record<string, any> | null) {
return profile?.actualAccount || profile?.account || '';
}
function formatListDate(value?: number | string) {
if (!value) {
return '-';
}
const date = dayjs(value);
if (!date.isValid()) {
return String(value);
}
return date.format('YYYY MM DD');
}
function openEdit(record: Record<string, any>) { function openEdit(record: Record<string, any>) {
activeUserId.value = record.userProfile?.id || ''; activeUserId.value = record.userProfile?.id || '';
editOpen.value = true; editOpen.value = true;
@ -330,19 +349,6 @@ async function copyUserId(value?: number | string) {
message.success('复制成功'); message.success('复制成功');
} }
async function openByShortAccount(account?: number | string) {
if (!account) {
return;
}
const result = await getUserBaseInfoByAccount(account);
const userId = result?.id || result?.userId;
if (!userId) {
message.warning('未找到用户');
return;
}
openUserDetails(userId);
}
void loadCountries(); void loadCountries();
</script> </script>
@ -424,11 +430,48 @@ void loadCountries();
:loading="loading" :loading="loading"
:pagination="false" :pagination="false"
row-key="id" row-key="id"
:scroll="{ x: 1760 }" :scroll="{ x: 1580 }"
> >
<template #bodyCell="{ column, record }"> <template #bodyCell="{ column, record }">
<template v-if="column.key === 'userProfile'"> <template v-if="column.key === 'userProfile'">
<UserProfileLink :profile="record.userProfile" /> <div class="user-cell">
<button
v-if="getUserProfileId(record.userProfile)"
type="button"
class="user-cell__avatar-button"
@click="openUserDetails(getUserProfileId(record.userProfile))"
>
<img
:src="getUserProfileAvatar(record.userProfile) || 'https://dummyimage.com/56x56/e2e8f0/64748b&text=U'"
alt=""
class="user-cell__avatar"
>
</button>
<div v-else class="user-cell__avatar-shell">
<img
:src="getUserProfileAvatar(record.userProfile) || 'https://dummyimage.com/56x56/e2e8f0/64748b&text=U'"
alt=""
class="user-cell__avatar"
>
</div>
<div class="user-cell__info">
<div class="user-cell__name">{{ getUserProfileName(record.userProfile) }}</div>
<button
type="button"
class="user-cell__copy"
@click="copyUserId(getUserProfileId(record.userProfile))"
>
ID{{ getUserProfileId(record.userProfile) || '-' }}
</button>
<button
type="button"
class="user-cell__copy"
@click="copyUserId(getUserShortId(record.userProfile))"
>
短ID{{ getUserShortId(record.userProfile) || '-' }}
</button>
</div>
</div>
</template> </template>
<template v-else-if="column.key === 'goldBalance'"> <template v-else-if="column.key === 'goldBalance'">
<Button size="small" type="link" @click="openRunningWater(record, 'gold')"> <Button size="small" type="link" @click="openRunningWater(record, 'gold')">
@ -451,19 +494,9 @@ void loadCountries();
<template v-else-if="column.key === 'registerSource'"> <template v-else-if="column.key === 'registerSource'">
{{ resolveRegisterSource(record) }} {{ resolveRegisterSource(record) }}
</template> </template>
<template v-else-if="column.key === 'ids'">
<div class="id-cell">
<button type="button" @click="copyUserId(record.userProfile?.id)">
用户ID{{ record.userProfile?.id || '-' }}
</button>
<button type="button" @click="openByShortAccount(record.userProfile?.actualAccount)">
短账号{{ record.userProfile?.actualAccount || '-' }}
</button>
</div>
</template>
<template v-else-if="column.key === 'time'"> <template v-else-if="column.key === 'time'">
<div>创建时间{{ record.userProfile?.createTime || '-' }}</div> <div>创建时间{{ formatListDate(record.userProfile?.createTime) }}</div>
<div>最近活跃{{ record.lastActiveTime || '-' }}</div> <div>最近活跃{{ formatListDate(record.lastActiveTime) }}</div>
</template> </template>
<template v-else-if="column.key === 'actions'"> <template v-else-if="column.key === 'actions'">
<Space wrap> <Space wrap>
@ -582,12 +615,48 @@ void loadCountries();
margin-bottom: 16px; margin-bottom: 16px;
} }
.id-cell { .user-cell {
display: grid; align-items: center;
gap: 4px; display: flex;
gap: 12px;
} }
.id-cell button { .user-cell__avatar-button,
.user-cell__avatar-shell {
align-items: center;
background: transparent;
border: 0;
display: inline-flex;
flex-shrink: 0;
justify-content: center;
padding: 0;
}
.user-cell__avatar-button {
cursor: pointer;
}
.user-cell__avatar {
background: #e2e8f0;
border-radius: 999px;
height: 40px;
object-fit: cover;
width: 40px;
}
.user-cell__info {
display: grid;
gap: 4px;
min-width: 0;
}
.user-cell__name {
color: #0f172a;
font-weight: 600;
word-break: break-all;
}
.user-cell__copy {
background: transparent; background: transparent;
border: 0; border: 0;
color: #2563eb; color: #2563eb;

View File

@ -179,9 +179,14 @@ loadData(true);
class="user-avatar" class="user-avatar"
> >
<div class="user-info"> <div class="user-info">
<div>{{ record.userProfile?.userNickname || '-' }}</div> <div class="user-name-row">
<span>{{ record.userProfile?.userNickname || '-' }}</span>
<span class="user-short-id">
短ID{{ record.userProfile?.actualAccount || '-' }}
</span>
</div>
<div class="user-sub"> <div class="user-sub">
{{ record.userProfile?.actualAccount || record.userProfile?.id || '-' }} ID{{ record.userProfile?.id || '-' }}
</div> </div>
</div> </div>
</div> </div>
@ -271,6 +276,18 @@ loadData(true);
width: 44px; width: 44px;
} }
.user-name-row {
align-items: center;
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.user-short-id {
color: #64748b;
font-size: 12px;
}
.user-sub { .user-sub {
color: #64748b; color: #64748b;
margin-top: 4px; margin-top: 4px;

View File

@ -0,0 +1,52 @@
INSERT INTO sys_menu (
id,
parent_id,
menu_name,
path,
menu_type,
icon,
create_user,
update_user,
sort,
status,
router,
alias
)
SELECT
1700000088,
(SELECT id FROM sys_menu WHERE alias = 'TeamManager' LIMIT 1),
'BD列表',
'team/business-development/index',
2,
'form',
'0',
'0',
241,
0,
'room/business-development',
'BusinessDevelopment'
WHERE NOT EXISTS (
SELECT 1 FROM sys_menu WHERE alias = 'BusinessDevelopment'
);
UPDATE sys_menu
SET
parent_id = (
SELECT team_menu.id
FROM (
SELECT id
FROM sys_menu
WHERE alias = 'TeamManager'
LIMIT 1
) AS team_menu
),
menu_name = 'BD列表',
path = 'team/business-development/index',
menu_type = 2,
icon = 'form',
create_user = '0',
update_user = '0',
sort = 241,
status = 0,
router = 'room/business-development'
WHERE alias = 'BusinessDevelopment';