增加热游

This commit is contained in:
zhx 2026-06-10 15:50:41 +08:00
parent c22cff97c3
commit 915c8bf0be
4 changed files with 585 additions and 1 deletions

View File

@ -108,3 +108,51 @@ export async function syncLingxianGames(data: Record<string, any>) {
data,
);
}
export async function getHotgameProviderConfig(params: Record<string, any>) {
return requestClient.get<Record<string, any>>('/go/operate/hotgame-game/config', {
params,
});
}
export async function saveHotgameProviderConfig(data: Record<string, any>) {
return requestClient.post<Record<string, any>>(
'/go/operate/hotgame-game/config',
data,
);
}
export async function activateHotgameProviderProfile(data: Record<string, any>) {
return requestClient.post<Record<string, any>>(
'/go/operate/hotgame-game/config/activate',
data,
);
}
export async function pageHotgameCatalog(params: Record<string, any>) {
return requestClient.get<Record<string, any>>(
'/go/operate/hotgame-game/catalog/page',
{ params },
);
}
export async function fetchHotgameCatalog(data: Record<string, any>) {
return requestClient.post<Record<string, any>>(
'/go/operate/hotgame-game/catalog/fetch',
data,
);
}
export async function importHotgameCatalog(data: Record<string, any>) {
return requestClient.post<Record<string, any>>(
'/go/operate/hotgame-game/import-catalog',
data,
);
}
export async function syncHotgameGames(data: Record<string, any>) {
return requestClient.post<Record<string, any>>(
'/go/operate/hotgame-game/sync',
data,
);
}

View File

@ -29,6 +29,12 @@ const routes: RouteRecordRaw[] = [
component: () => import('#/views/operate/lingxian-config.vue'),
meta: { title: '灵仙配置' },
},
{
name: 'OperateHotgameConfig',
path: 'hotgame-config',
component: () => import('#/views/operate/hotgame-config.vue'),
meta: { title: '热游配置' },
},
],
},
];

View File

@ -0,0 +1,468 @@
<script lang="ts" setup>
import { computed, reactive, ref, watch } from 'vue';
import { Page } from '@vben/common-ui';
import { useAccessStore } from '@vben/stores';
import {
activateHotgameProviderProfile,
fetchHotgameCatalog,
getHotgameProviderConfig,
importHotgameCatalog,
pageHotgameCatalog,
saveHotgameProviderConfig,
} from '#/api/legacy/baishun-game';
import { getAllowedSysOrigins } from '#/views/system/shared';
import {
Button,
Card,
Collapse,
CollapsePanel,
Form,
FormItem,
Input,
Pagination,
Select,
Table,
Tag,
message,
} from 'antdv-next';
defineOptions({ name: 'OperateHotgameConfig' });
function createProviderForm(sysOrigin = '') {
return {
appKey: '',
callbackBaseUrl: '',
profile: 'PROD',
sysOrigin,
testToken: '',
testUid: '',
};
}
const accessStore = useAccessStore();
const sysOriginOptions = computed(() => {
const options = getAllowedSysOrigins(accessStore.accessCodes || []);
return options.length > 0 ? options : getAllowedSysOrigins([]);
});
const profileOptions = [
{ label: '正式环境', value: 'PROD' },
{ label: '测试环境', value: 'TEST' },
];
const importShowcaseOptions = [
{ label: '默认上架', value: true as any },
{ label: '默认下架', value: false as any },
];
const contextQuery = reactive({ profile: 'PROD', sysOrigin: '' });
const providerForm = reactive(createProviderForm());
const catalogQuery = reactive<Record<string, any>>({
cursor: 1,
keyword: '',
limit: 20,
});
const providerLoading = ref(false);
const providerSaving = ref(false);
const catalogLoading = ref(false);
const catalogFetching = ref(false);
const catalogImporting = ref(false);
const profileActivating = ref(false);
const totalCatalog = ref(0);
const catalogList = ref<Array<Record<string, any>>>([]);
const selectedCatalogKeys = ref<Array<string>>([]);
const activeProfile = ref('PROD');
const importDefaultShowcase = ref(true as any);
const isCurrentProfileActive = computed(
() => activeProfile.value === contextQuery.profile,
);
const activeProfileLabel = computed(
() =>
profileOptions.find((item) => item.value === activeProfile.value)?.label ||
activeProfile.value,
);
const catalogColumns = [
{ 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: 90 },
{ dataIndex: 'added', key: 'added', title: '统一游戏列表', width: 140 },
{ dataIndex: 'downloadUrl', key: 'downloadUrl', title: '启动地址', width: 520 },
{ dataIndex: 'updateTime', key: 'updateTime', title: '更新时间', width: 180 },
];
const catalogRowSelection = computed(() => ({
selectedRowKeys: selectedCatalogKeys.value,
onChange: (keys: Array<number | string>) => {
selectedCatalogKeys.value = keys.map((item) => String(item));
},
getCheckboxProps: (record: Record<string, any>) => ({
disabled: Boolean(record.added),
}),
}));
watch(
sysOriginOptions,
(options) => {
if (!contextQuery.sysOrigin) {
contextQuery.sysOrigin = String(options[0]?.value || '');
}
},
{ immediate: true },
);
watch(
() => [contextQuery.sysOrigin, contextQuery.profile],
([sysOrigin, profile]) => {
if (!sysOrigin || !profile) {
return;
}
Object.assign(providerForm, createProviderForm(sysOrigin), {
profile,
sysOrigin,
});
selectedCatalogKeys.value = [];
void Promise.all([loadProviderConfig(), loadCatalog(true)]);
},
{ immediate: true },
);
function applyProviderForm(record: Record<string, any>) {
activeProfile.value = String(record.activeProfile || activeProfile.value || 'PROD');
Object.assign(providerForm, createProviderForm(contextQuery.sysOrigin), {
...record,
profile: contextQuery.profile,
sysOrigin: contextQuery.sysOrigin,
});
}
async function loadProviderConfig() {
if (!contextQuery.sysOrigin) {
return;
}
providerLoading.value = true;
try {
const result = await getHotgameProviderConfig({
profile: contextQuery.profile,
sysOrigin: contextQuery.sysOrigin,
});
applyProviderForm(result || {});
} finally {
providerLoading.value = false;
}
}
async function persistProviderConfig(showSuccess = true) {
if (!contextQuery.sysOrigin || !providerForm.appKey) {
message.warning('请补全热游签名 Key');
return false;
}
providerSaving.value = true;
try {
const result = await saveHotgameProviderConfig({
...providerForm,
profile: contextQuery.profile,
sysOrigin: contextQuery.sysOrigin,
});
applyProviderForm(result || providerForm);
if (showSuccess) {
message.success('热游配置已保存');
}
return true;
} catch {
return false;
} finally {
providerSaving.value = false;
}
}
async function loadCatalog(reset = false) {
if (!contextQuery.sysOrigin) {
return;
}
if (reset) {
catalogQuery.cursor = 1;
}
catalogLoading.value = true;
try {
const result = await pageHotgameCatalog({
...catalogQuery,
profile: contextQuery.profile,
sysOrigin: contextQuery.sysOrigin,
});
catalogList.value = Array.isArray(result.records) ? result.records : [];
totalCatalog.value = Number(result.total || 0);
} finally {
catalogLoading.value = false;
}
}
function handleCatalogPageChange(page: number, pageSize: number) {
catalogQuery.cursor = page;
catalogQuery.limit = pageSize;
void loadCatalog();
}
async function handleActivateProfile() {
if (!(await persistProviderConfig(false))) {
return;
}
profileActivating.value = true;
try {
const result = await activateHotgameProviderProfile({
profile: contextQuery.profile,
sysOrigin: contextQuery.sysOrigin,
});
applyProviderForm(result || providerForm);
message.success(`已切换到${activeProfileLabel.value}`);
await loadCatalog(true);
} finally {
profileActivating.value = false;
}
}
async function handleFetchCatalog() {
if (!(await persistProviderConfig(false))) {
return;
}
catalogFetching.value = true;
try {
const result = await fetchHotgameCatalog({
profile: contextQuery.profile,
sysOrigin: contextQuery.sysOrigin,
});
message.success(
`同步完成:新增 ${Number(result.inserted || 0)},更新 ${Number(result.updated || 0)},跳过 ${Number(result.skipped || 0)}`,
);
await loadCatalog(true);
} finally {
catalogFetching.value = false;
}
}
async function handleImportSelected() {
if (selectedCatalogKeys.value.length === 0) {
message.warning('请选择要添加到统一游戏列表的热游游戏');
return;
}
catalogImporting.value = true;
try {
const result = await importHotgameCatalog({
profile: contextQuery.profile,
sysOrigin: contextQuery.sysOrigin,
vendorGameIds: selectedCatalogKeys.value,
defaultShowcase: importDefaultShowcase.value,
});
selectedCatalogKeys.value = [];
message.success(
`添加完成:新增 ${Number(result.imported || 0)},已存在 ${Number(result.existing || 0)}`,
);
await loadCatalog(true);
} finally {
catalogImporting.value = false;
}
}
</script>
<template>
<Page title="热游配置">
<Card :loading="providerLoading" title="热游接入配置">
<Form layout="vertical">
<div class="form-grid provider-grid">
<FormItem label="系统">
<SysOriginSelect
v-model:value="contextQuery.sysOrigin"
:options="sysOriginOptions"
/>
</FormItem>
<FormItem label="配置环境" extra="app 侧只读取当前启用环境。">
<Select
v-model:value="contextQuery.profile"
:options="profileOptions"
option-label-prop="label"
/>
</FormItem>
<FormItem label="回调域名">
<Input
v-model:value="providerForm.callbackBaseUrl"
placeholder="https://api.global-interaction.com/"
/>
</FormItem>
<FormItem label="Key">
<Input
v-model:value="providerForm.appKey"
placeholder="请输入热游签名 Key"
type="password"
/>
</FormItem>
</div>
<Collapse class="test-user-collapse" ghost>
<CollapsePanel key="test-user" header="测试用户">
<div class="form-grid">
<FormItem label="UID">
<Input v-model:value="providerForm.testUid" placeholder="可选" />
</FormItem>
<FormItem label="Token">
<Input
v-model:value="providerForm.testToken"
placeholder="可选"
type="password"
/>
</FormItem>
</div>
</CollapsePanel>
</Collapse>
<div class="toolbar">
<Button
type="primary"
:loading="providerSaving"
@click="persistProviderConfig(true)"
>
保存配置
</Button>
<Button
:disabled="isCurrentProfileActive"
:loading="profileActivating"
@click="handleActivateProfile"
>
启用此环境
</Button>
<Button :loading="catalogFetching" @click="handleFetchCatalog">
同步热游目录
</Button>
<Tag :color="isCurrentProfileActive ? 'success' : 'warning'">
当前启用{{ activeProfileLabel }}
</Tag>
</div>
</Form>
</Card>
<Card class="content-card" title="热游目录">
<div class="toolbar">
<Input
v-model:value="catalogQuery.keyword"
placeholder="搜索目录名称 / 内部ID / 热游ID"
style="width: 280px"
@pressEnter="loadCatalog(true)"
/>
<Button :loading="catalogLoading" type="primary" @click="loadCatalog(true)">
搜索
</Button>
<Button :loading="catalogFetching" @click="handleFetchCatalog">
重新同步目录
</Button>
<Select
v-model:value="importDefaultShowcase"
:options="importShowcaseOptions"
style="width: 120px"
/>
<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: 1500 }"
>
<template #bodyCell="{ column, record }">
<template v-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 ? '已进入统一游戏列表' : '未添加' }}
</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>
</Card>
</Page>
</template>
<style scoped>
.content-card {
margin-top: 16px;
}
.toolbar {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-bottom: 16px;
}
.pager {
display: flex;
justify-content: flex-end;
margin-top: 16px;
}
.form-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0 16px;
}
.provider-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.test-user-collapse {
margin-bottom: 12px;
}
.link-cell {
display: inline-block;
max-width: 500px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
@media (max-width: 960px) {
.form-grid,
.provider-grid {
grid-template-columns: 1fr;
}
}
</style>

View File

@ -650,6 +650,67 @@ SET
router = 'baishun-config'
WHERE alias = 'OperateBaishunGameManage';
INSERT INTO sys_menu (
id,
parent_id,
menu_name,
path,
menu_type,
icon,
create_user,
update_user,
sort,
status,
router,
alias
)
SELECT
1700000258,
(
SELECT game_manager.id
FROM (
SELECT id
FROM sys_menu
WHERE alias = 'OperateGameManager'
LIMIT 1
) AS game_manager
),
'热游配置',
'sys/game-manager/hotgame-config/index',
2,
'form',
'0',
'0',
4,
0,
'hotgame-config',
'OperateHotgameConfig'
WHERE NOT EXISTS (
SELECT 1 FROM sys_menu WHERE alias = 'OperateHotgameConfig'
);
UPDATE sys_menu
SET
parent_id = (
SELECT game_manager.id
FROM (
SELECT id
FROM sys_menu
WHERE alias = 'OperateGameManager'
LIMIT 1
) AS game_manager
),
menu_name = '热游配置',
path = 'sys/game-manager/hotgame-config/index',
menu_type = 2,
icon = 'form',
create_user = '0',
update_user = '0',
sort = 4,
status = 0,
router = 'hotgame-config'
WHERE alias = 'OperateHotgameConfig';
INSERT INTO sys_role_menu (role_id, menu_id)
SELECT DISTINCT
parent_role.role_id,
@ -669,7 +730,8 @@ JOIN sys_menu AS target_menu
'ResidentDailyTaskConfig',
'OperateGameManager',
'GameConfig',
'OperateBaishunGameManage'
'OperateBaishunGameManage',
'OperateHotgameConfig'
)
LEFT JOIN sys_role_menu AS existing_role_menu
ON existing_role_menu.role_id = parent_role.role_id