yumi-admin/apps/src/views/operate/baishun-config.vue

536 lines
15 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 {
activateBaishunProviderProfile,
fetchBaishunCatalog,
getBaishunProviderConfig,
importBaishunCatalog,
pageBaishunCatalog,
saveBaishunProviderConfig,
} from '#/api/legacy/baishun-game';
import { getAllowedSysOrigins } from '#/views/system/shared';
import {
Button,
Card,
Form,
FormItem,
Image,
Input,
InputNumber,
Pagination,
Select,
Table,
Tag,
message,
} from 'antdv-next';
defineOptions({ name: 'OperateBaishunConfig' });
function createProviderForm(sysOrigin = '') {
return {
sysOrigin,
profile: 'PROD',
platformBaseUrl: '',
appId: undefined as any,
appName: '',
appChannel: '',
appKey: '',
gsp: 101 as any,
launchCodeTtlSeconds: 300 as any,
ssTokenTtlSeconds: 86400 as any,
};
}
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 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<number>>([]);
const activeProfile = ref('PROD');
const isCurrentProfileActive = computed(
() => activeProfile.value === contextQuery.profile,
);
const activeProfileLabel = computed(
() =>
profileOptions.find((item) => item.value === activeProfile.value)?.label ||
activeProfile.value,
);
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: 120 },
{ 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(
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,
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({
profile: contextQuery.profile,
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,
profile: contextQuery.profile,
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;
}
}
async function loadCatalog(reset = false) {
if (!contextQuery.sysOrigin) {
return;
}
if (reset) {
catalogQuery.cursor = 1;
}
catalogLoading.value = true;
try {
const result = await pageBaishunCatalog({
...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 handleCatalogSearch() {
void loadCatalog(true);
}
function handleCatalogPageChange(page: number, pageSize: number) {
catalogQuery.cursor = page;
catalogQuery.limit = pageSize;
void loadCatalog();
}
async function handleSaveProviderConfig() {
await persistProviderConfig(true);
}
async function handleActivateProfile() {
if (!(await persistProviderConfig(false))) {
return;
}
profileActivating.value = true;
try {
const result = await activateBaishunProviderProfile({
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 fetchBaishunCatalog({
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 (!contextQuery.sysOrigin) {
return;
}
if (selectedCatalogKeys.value.length === 0) {
message.warning('请选择要添加到统一游戏列表的百顺游戏');
return;
}
catalogImporting.value = true;
try {
const result = await importBaishunCatalog({
profile: contextQuery.profile,
sysOrigin: contextQuery.sysOrigin,
vendorGameIds: selectedCatalogKeys.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="平台地址"
extra="填写百顺平台接口地址,例如 https://game-cn-test.jieyou.shop不要填 Lucky Gift 域名。"
>
<Input
v-model:value="providerForm.platformBaseUrl"
placeholder="https://game-cn-test.jieyou.shop"
/>
</FormItem>
<FormItem label="AppId">
<InputNumber
v-model:value="providerForm.appId"
:min="1"
style="width: 100%"
/>
</FormItem>
<FormItem label="AppName">
<Input v-model:value="providerForm.appName" placeholder="可选" />
</FormItem>
<FormItem label="AppChannel">
<Input
v-model:value="providerForm.appChannel"
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
:disabled="isCurrentProfileActive"
:loading="profileActivating"
@click="handleActivateProfile"
>
启用此环境
</Button>
<Button :loading="catalogFetching" @click="handleFetchCatalog">
获取百顺游戏列表
</Button>
<Tag :color="isCurrentProfileActive ? 'success' : 'warning'">
当前启用{{ activeProfileLabel }}
</Tag>
<span class="hint">
正式和测试环境各自保存配置目录和已添加游戏切换启用环境后Go app 游戏列表同步切换
</span>
</div>
</Form>
</Card>
<Card class="content-card" title="百顺目录">
<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 ? '已进入统一游戏列表' : '未添加' }}
</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;
}
.cover {
width: 50px;
height: 50px;
object-fit: cover;
border-radius: 6px;
}
.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));
}
.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) {
.form-grid,
.provider-grid {
grid-template-columns: minmax(0, 1fr);
}
}
</style>