yumi-admin/apps/src/views/operate/hotgame-config.vue
2026-06-10 15:50:41 +08:00

469 lines
13 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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