refactor(admin): 移动端应用配置页从 payment 迁至 system

- API 改调 /platform/config/mobile-app
- 页面与 locale 迁到 system/config/mobileApp、system.mobileApp
This commit is contained in:
DaxPay Dev
2026-07-10 17:58:35 +08:00
parent 1b00bd124c
commit 59a71ba7a7
5 changed files with 377 additions and 46 deletions

View File

@@ -10,7 +10,7 @@ export const MobileAppApi = {
* ()
*/
list(): Promise<Result<MobileAppResult[]>> {
return defHttp.get({ url: '/admin/mobile-app/list' });
return defHttp.get({ url: '/platform/config/mobile-app/list' });
},
/**
@@ -18,7 +18,7 @@ export const MobileAppApi = {
*/
listByAppType(appType: string): Promise<Result<MobileAppResult[]>> {
return defHttp.get({
url: '/admin/mobile-app/list-by-app-type',
url: '/platform/config/mobile-app/list-by-app-type',
params: { appType },
});
},
@@ -27,14 +27,20 @@ export const MobileAppApi = {
*
*/
findById(id: string): Promise<Result<MobileAppResult>> {
return defHttp.get({ url: '/admin/mobile-app/get', params: { id } });
return defHttp.get({
url: '/platform/config/mobile-app/get',
params: { id },
});
},
/**
* (+ upsert)
*/
save(param: MobileAppParam): Promise<Result<MobileAppResult>> {
return defHttp.post({ url: '/admin/mobile-app/save', data: param });
return defHttp.post({
url: '/platform/config/mobile-app/save',
data: param,
});
},
/**
@@ -42,7 +48,7 @@ export const MobileAppApi = {
*/
updateEnabled(id: string, enable: boolean): Promise<Result<void>> {
return defHttp.post({
url: '/admin/mobile-app/update-enabled',
url: '/platform/config/mobile-app/update-enabled',
params: { id, enable },
});
},

View File

@@ -39,12 +39,25 @@
"originalIdPlaceholder": "Enter original ID (gh_..., optional)",
"alipayAppId": "Mini Program AppId",
"alipayAppIdPlaceholder": "Enter Alipay Mini Program AppId",
"authType": "Auth Type",
"authTypeKey": "Public Key",
"authTypeCert": "Certificate",
"authTypeRequired": "Please select auth type",
"privateKey": "App Private Key",
"privateKeyPlaceholder": "Enter app private key",
"privateKeyPlaceholder": "Enter app private key (PEM, required for both modes)",
"privateKeyRequired": "Please enter app private key",
"alipayPublicKey": "Alipay Public Key",
"alipayPublicKeyPlaceholder": "Enter Alipay public key",
"alipayPublicKeyPlaceholder": "Enter Alipay public key (public key mode)",
"alipayPublicKeyRequired": "Please enter Alipay public key",
"appCert": "App Public Key Certificate",
"appCertRequired": "Please upload app public key certificate",
"uploadAppCert": "Upload app public key certificate",
"alipayCert": "Alipay Public Key Certificate",
"alipayCertRequired": "Please upload Alipay public key certificate",
"uploadAlipayCert": "Upload Alipay public key certificate",
"alipayRootCert": "Alipay Root Certificate",
"alipayRootCertRequired": "Please upload Alipay root certificate",
"uploadRootCert": "Upload Alipay root certificate",
"dyAppId": "Mini Program AppId",
"dyAppIdPlaceholder": "Enter Douyin Mini Program AppId",
"dyAppSecret": "Mini Program AppSecret",

View File

@@ -39,12 +39,25 @@
"originalIdPlaceholder": "请输入小程序原始 ID(gh_ 开头, 可选)",
"alipayAppId": "小程序 AppId",
"alipayAppIdPlaceholder": "请输入支付宝小程序 AppId",
"authType": "鉴权方式",
"authTypeKey": "公钥模式",
"authTypeCert": "证书模式",
"authTypeRequired": "请选择鉴权方式",
"privateKey": "应用私钥",
"privateKeyPlaceholder": "请输入应用私钥",
"privateKeyPlaceholder": "请输入应用私钥(PEM 格式, 公钥与证书模式都需要)",
"privateKeyRequired": "请输入应用私钥",
"alipayPublicKey": "支付宝公钥",
"alipayPublicKeyPlaceholder": "请输入支付宝公钥",
"alipayPublicKeyPlaceholder": "请输入支付宝公钥(公钥模式使用)",
"alipayPublicKeyRequired": "请输入支付宝公钥",
"appCert": "应用公钥证书",
"appCertRequired": "请上传应用公钥证书",
"uploadAppCert": "上传应用公钥证书",
"alipayCert": "支付宝公钥证书",
"alipayCertRequired": "请上传支付宝公钥证书",
"uploadAlipayCert": "上传支付宝公钥证书",
"alipayRootCert": "支付宝根证书",
"alipayRootCertRequired": "请上传支付宝根证书",
"uploadRootCert": "上传支付宝根证书",
"dyAppId": "小程序 AppId",
"dyAppIdPlaceholder": "请输入抖音小程序 AppId",
"dyAppSecret": "小程序 AppSecret",

View File

@@ -37,7 +37,7 @@
*/
function handleEnter(appType: string, disabled: boolean) {
if (disabled) return;
router.push({ path: `/payment/config/mobile-app/detail/${appType}` });
router.push({ path: `/system/config/mobile-app/detail/${appType}` });
}
/**
@@ -60,7 +60,7 @@
<a-card variant="borderless" class="rounded-xl shadow-sm">
<template #title>
<span class="text-lg font-bold text-foreground">{{
$t('payment.config.mobileApp.title')
$t('system.mobileApp.title')
}}</span>
</template>
@@ -77,7 +77,7 @@
>
<!-- 收银台灰显角标 -->
<div v-if="card.disabled" class="coming-soon-badge">
{{ $t('payment.config.mobileApp.card.comingSoon') }}
{{ $t('system.mobileApp.card.comingSoon') }}
</div>
<div class="card-icon">
@@ -90,15 +90,15 @@
</div>
<div class="card-name">
{{ $t(`payment.config.mobileApp.card.${card.appType}.name`) }}
{{ $t(`system.mobileApp.card.${card.appType}.name`) }}
</div>
<div class="card-desc">
{{ $t(`payment.config.mobileApp.card.${card.appType}.desc`) }}
{{ $t(`system.mobileApp.card.${card.appType}.desc`) }}
</div>
<div v-if="!card.disabled" class="card-enter">
<IconifyIcon icon="ant-design:setting-outlined" class="text-sm" />
<span>{{ $t('payment.config.mobileApp.card.enter') }}</span>
<span>{{ $t('system.mobileApp.card.enter') }}</span>
</div>
</div>
</div>

View File

@@ -9,19 +9,30 @@
import {
type MobileAppParam,
MobileAppApi,
} from '#/api/payment/config/mobile-app.api';
} from '#/api/system/mobile-app.api';
import { useFormEdit } from '#/hooks/useFormEdit';
import { useMessage } from '#/hooks/useMessage';
import { readFileAsText } from '#/utils/file';
defineOptions({ name: 'MobileAppDetail' });
// : public_key() / cert(), AlipayAuthTypeEnum
const AUTH_TYPE_KEY = 'public_key';
const AUTH_TYPE_CERT = 'cert';
/** 结构化配置字段(各平台共用, 按平台取用) */
interface StructuredConfig {
appId: string;
appSecret: string;
originalId: string;
// :
authType: string;
privateKey: string;
alipayPublicKey: string;
//
appCert: string;
alipayCert: string;
alipayRootCert: string;
}
/** 已支持结构化表单的平台 */
@@ -83,9 +94,9 @@
//
const appTitle = computed(() => {
if (appType.value) {
return $t(`payment.config.mobileApp.card.${appType.value}.name`);
return $t(`system.mobileApp.card.${appType.value}.name`);
}
return $t('payment.config.mobileApp.detail.notSupportedAppType');
return $t('system.mobileApp.detail.notSupportedAppType');
});
/**
@@ -127,8 +138,13 @@
appId: '',
appSecret: '',
originalId: '',
//
authType: AUTH_TYPE_KEY,
privateKey: '',
alipayPublicKey: '',
appCert: '',
alipayCert: '',
alipayRootCert: '',
};
}
@@ -143,8 +159,14 @@
result.appId = cfg.appId || '';
result.appSecret = cfg.appSecret || '';
result.originalId = cfg.originalId || '';
// : public_key / cert
result.authType =
cfg.authType === AUTH_TYPE_CERT ? AUTH_TYPE_CERT : AUTH_TYPE_KEY;
result.privateKey = cfg.privateKey || '';
result.alipayPublicKey = cfg.alipayPublicKey || '';
result.appCert = cfg.appCert || '';
result.alipayCert = cfg.alipayCert || '';
result.alipayRootCert = cfg.alipayRootCert || '';
}
} catch {
// JSON
@@ -152,6 +174,13 @@
return result;
}
/**
* 是否证书模式
*/
function isCertMode(structured: StructuredConfig): boolean {
return structured.authType === AUTH_TYPE_CERT;
}
/**
* 按平台序列化 appConfig, 敏感字段仅在 diffForm 有新值时写入
*/
@@ -169,12 +198,23 @@
obj.appSecret = sensitive.appSecret;
}
} else if (platform === 'alipay_mini') {
//
obj.authType = structured.authType || AUTH_TYPE_KEY;
if (sensitive.privateKey !== undefined) {
obj.privateKey = sensitive.privateKey;
}
if (sensitive.alipayPublicKey !== undefined) {
obj.alipayPublicKey = sensitive.alipayPublicKey;
}
if (sensitive.appCert !== undefined) {
obj.appCert = sensitive.appCert;
}
if (sensitive.alipayCert !== undefined) {
obj.alipayCert = sensitive.alipayCert;
}
if (sensitive.alipayRootCert !== undefined) {
obj.alipayRootCert = sensitive.alipayRootCert;
}
} else if (platform === 'dy_mini') {
if (sensitive.appSecret !== undefined) {
obj.appSecret = sensitive.appSecret;
@@ -192,27 +232,52 @@
original: StructuredConfig,
): boolean {
if (!structured.appId?.trim()) {
message.warning($t('payment.config.mobileApp.fields.appIdRequired'));
message.warning($t('system.mobileApp.fields.appIdRequired'));
return false;
}
if (platform === 'wx_mini' || platform === 'dy_mini') {
if (!original.appSecret && !structured.appSecret?.trim()) {
message.warning(
$t('payment.config.mobileApp.fields.appSecretRequired'),
$t('system.mobileApp.fields.appSecretRequired'),
);
return false;
}
}
if (platform === 'alipay_mini') {
if (!structured.authType?.trim()) {
message.warning($t('system.mobileApp.fields.authTypeRequired'));
return false;
}
// ;
if (!original.privateKey && !structured.privateKey?.trim()) {
message.warning(
$t('payment.config.mobileApp.fields.privateKeyRequired'),
$t('system.mobileApp.fields.privateKeyRequired'),
);
return false;
}
if (!original.alipayPublicKey && !structured.alipayPublicKey?.trim()) {
if (isCertMode(structured)) {
// :
if (!original.appCert && !structured.appCert?.trim()) {
message.warning($t('system.mobileApp.fields.appCertRequired'));
return false;
}
if (!original.alipayCert && !structured.alipayCert?.trim()) {
message.warning($t('system.mobileApp.fields.alipayCertRequired'));
return false;
}
if (!original.alipayRootCert && !structured.alipayRootCert?.trim()) {
message.warning(
$t('system.mobileApp.fields.alipayRootCertRequired'),
);
return false;
}
} else if (
!original.alipayPublicKey &&
!structured.alipayPublicKey?.trim()
) {
// :
message.warning(
$t('payment.config.mobileApp.fields.alipayPublicKeyRequired'),
$t('system.mobileApp.fields.alipayPublicKeyRequired'),
);
return false;
}
@@ -220,6 +285,43 @@
return true;
}
/**
* 上传证书文件, 读取文本内容写入对应字段
*/
function handleCertUpload(
platform: string,
fieldName: 'appCert' | 'alipayCert' | 'alipayRootCert',
info: { file: File },
) {
const file = info.file;
if (!file) {
return;
}
const structured = structuredMap[platform];
if (!structured) {
return;
}
readFileAsText(file).then((content) => {
structured[fieldName] = content;
message.success(
$t('components.upload.uploadSuccess', { name: file.name }),
);
});
}
/**
* 截断证书内容用于 tooltip 预览
*/
function truncateContent(content: string, maxLength = 500): string {
if (!content) {
return '';
}
if (content.length <= maxLength) {
return content;
}
return `${content.slice(0, Math.max(0, maxLength))}...`;
}
/**
* 加载该端所有平台配置
*/
@@ -300,7 +402,13 @@
// diffForm
const sensitiveKeys =
platform === 'alipay_mini'
? (['privateKey', 'alipayPublicKey'] as const)
? ([
'privateKey',
'alipayPublicKey',
'appCert',
'alipayCert',
'alipayRootCert',
] as const)
: (['appSecret'] as const);
const sensitive = diffForm(
originalStructured,
@@ -322,7 +430,7 @@
saving.value = true;
try {
await MobileAppApi.save(submit);
message.success($t('payment.config.mobileApp.detail.saveSuccess'));
message.success($t('system.mobileApp.detail.saveSuccess'));
isEditing.value = false;
await loadData(true);
} finally {
@@ -336,7 +444,7 @@
* 返回卡片页
*/
function handleBack() {
router.push({ path: '/payment/config/mobile-app' });
router.push({ path: '/system/config/mobile-app' });
}
// Tab 退,
@@ -407,7 +515,7 @@
<template #tab>
<span :class="{ 'opacity-50': item.disabled }">
{{
$t(`payment.config.mobileApp.platformNames.${item.platform}`)
$t(`system.mobileApp.platformNames.${item.platform}`)
}}
<a-tag
v-if="item.disabled"
@@ -415,7 +523,7 @@
class="ml-1"
style="font-size: 11px; line-height: 18px"
>
{{ $t('payment.config.mobileApp.card.comingSoon') }}
{{ $t('system.mobileApp.card.comingSoon') }}
</a-tag>
</span>
</template>
@@ -424,7 +532,7 @@
<a-empty
v-if="item.disabled"
:description="
$t('payment.config.mobileApp.detail.platformComingSoon')
$t('system.mobileApp.detail.platformComingSoon')
"
/>
@@ -442,19 +550,19 @@
<!-- 微信小程序 -->
<template v-if="item.platform === 'wx_mini'">
<a-form-item
:label="$t('payment.config.mobileApp.fields.wxAppId')"
:label="$t('system.mobileApp.fields.wxAppId')"
required
>
<a-input
v-model:value="structuredMap[item.platform]!.appId"
:disabled="!isEditing"
:placeholder="
$t('payment.config.mobileApp.fields.wxAppIdPlaceholder')
$t('system.mobileApp.fields.wxAppIdPlaceholder')
"
/>
</a-form-item>
<a-form-item
:label="$t('payment.config.mobileApp.fields.wxAppSecret')"
:label="$t('system.mobileApp.fields.wxAppSecret')"
required
>
<a-input
@@ -463,30 +571,30 @@
allow-clear
:placeholder="
$t(
'payment.config.mobileApp.fields.wxAppSecretPlaceholder',
'system.mobileApp.fields.wxAppSecretPlaceholder',
)
"
/>
</a-form-item>
<a-form-item
:label="$t('payment.config.mobileApp.fields.originalId')"
:label="$t('system.mobileApp.fields.originalId')"
>
<a-input
v-model:value="structuredMap[item.platform]!.originalId"
:disabled="!isEditing"
:placeholder="
$t(
'payment.config.mobileApp.fields.originalIdPlaceholder',
'system.mobileApp.fields.originalIdPlaceholder',
)
"
/>
</a-form-item>
</template>
<!-- 支付宝小程序 -->
<!-- 支付宝小程序: 公钥 / 证书双模式 -->
<template v-else-if="item.platform === 'alipay_mini'">
<a-form-item
:label="$t('payment.config.mobileApp.fields.alipayAppId')"
:label="$t('system.mobileApp.fields.alipayAppId')"
required
>
<a-input
@@ -494,28 +602,48 @@
:disabled="!isEditing"
:placeholder="
$t(
'payment.config.mobileApp.fields.alipayAppIdPlaceholder',
'system.mobileApp.fields.alipayAppIdPlaceholder',
)
"
/>
</a-form-item>
<a-form-item
:label="$t('payment.config.mobileApp.fields.privateKey')"
:label="$t('system.mobileApp.fields.authType')"
required
>
<a-radio-group
v-model:value="structuredMap[item.platform]!.authType"
button-style="solid"
:disabled="!isEditing"
>
<a-radio-button :value="AUTH_TYPE_KEY">
{{ $t('system.mobileApp.fields.authTypeKey') }}
</a-radio-button>
<a-radio-button :value="AUTH_TYPE_CERT">
{{ $t('system.mobileApp.fields.authTypeCert') }}
</a-radio-button>
</a-radio-group>
</a-form-item>
<a-form-item
:label="$t('system.mobileApp.fields.privateKey')"
required
>
<a-textarea
v-model:value="structuredMap[item.platform]!.privateKey"
:disabled="!isEditing"
:rows="4"
allow-clear
:placeholder="
$t(
'payment.config.mobileApp.fields.privateKeyPlaceholder',
'system.mobileApp.fields.privateKeyPlaceholder',
)
"
/>
</a-form-item>
<!-- 公钥模式: 支付宝公钥 -->
<a-form-item
:label="$t('payment.config.mobileApp.fields.alipayPublicKey')"
v-if="!isCertMode(structuredMap[item.platform]!)"
:label="$t('system.mobileApp.fields.alipayPublicKey')"
required
>
<a-textarea
@@ -524,31 +652,202 @@
"
:disabled="!isEditing"
:rows="4"
allow-clear
:placeholder="
$t(
'payment.config.mobileApp.fields.alipayPublicKeyPlaceholder',
'system.mobileApp.fields.alipayPublicKeyPlaceholder',
)
"
/>
</a-form-item>
<!-- 证书模式: 三本 .crt 上传 -->
<template v-else>
<a-form-item
:label="$t('system.mobileApp.fields.appCert')"
required
>
<a-upload
v-if="!structuredMap[item.platform]!.appCert"
:disabled="!isEditing"
:multiple="false"
:show-upload-list="false"
accept=".crt"
:before-upload="() => false"
@change="
(info: any) =>
handleCertUpload(item.platform, 'appCert', info)
"
>
<a-button :disabled="!isEditing">
<template #icon>
<IconifyIcon
icon="ant-design:upload-outlined"
class="text-lg"
/>
</template>
{{ $t('system.mobileApp.fields.uploadAppCert') }}
</a-button>
</a-upload>
<a-tooltip
v-else
:title="
truncateContent(
structuredMap[item.platform]!.appCert || '',
)
"
placement="top"
:mouse-enter-delay="0.3"
>
<a-input value="appCert.crt" disabled>
<template #suffix>
<span
v-if="isEditing"
class="cursor-pointer text-gray-400"
@click="
structuredMap[item.platform]!.appCert = ''
"
>
<IconifyIcon
icon="ant-design:close-circle-outlined"
class="text-lg"
/>
</span>
</template>
</a-input>
</a-tooltip>
</a-form-item>
<a-form-item
:label="$t('system.mobileApp.fields.alipayCert')"
required
>
<a-upload
v-if="!structuredMap[item.platform]!.alipayCert"
:disabled="!isEditing"
:multiple="false"
:show-upload-list="false"
accept=".crt"
:before-upload="() => false"
@change="
(info: any) =>
handleCertUpload(item.platform, 'alipayCert', info)
"
>
<a-button :disabled="!isEditing">
<template #icon>
<IconifyIcon
icon="ant-design:upload-outlined"
class="text-lg"
/>
</template>
{{ $t('system.mobileApp.fields.uploadAlipayCert') }}
</a-button>
</a-upload>
<a-tooltip
v-else
:title="
truncateContent(
structuredMap[item.platform]!.alipayCert || '',
)
"
placement="top"
:mouse-enter-delay="0.3"
>
<a-input value="alipayCert.crt" disabled>
<template #suffix>
<span
v-if="isEditing"
class="cursor-pointer text-gray-400"
@click="
structuredMap[item.platform]!.alipayCert = ''
"
>
<IconifyIcon
icon="ant-design:close-circle-outlined"
class="text-lg"
/>
</span>
</template>
</a-input>
</a-tooltip>
</a-form-item>
<a-form-item
:label="$t('system.mobileApp.fields.alipayRootCert')"
required
>
<a-upload
v-if="!structuredMap[item.platform]!.alipayRootCert"
:disabled="!isEditing"
:multiple="false"
:show-upload-list="false"
accept=".crt"
:before-upload="() => false"
@change="
(info: any) =>
handleCertUpload(
item.platform,
'alipayRootCert',
info,
)
"
>
<a-button :disabled="!isEditing">
<template #icon>
<IconifyIcon
icon="ant-design:upload-outlined"
class="text-lg"
/>
</template>
{{ $t('system.mobileApp.fields.uploadRootCert') }}
</a-button>
</a-upload>
<a-tooltip
v-else
:title="
truncateContent(
structuredMap[item.platform]!.alipayRootCert || '',
)
"
placement="top"
:mouse-enter-delay="0.3"
>
<a-input value="alipayRootCert.crt" disabled>
<template #suffix>
<span
v-if="isEditing"
class="cursor-pointer text-gray-400"
@click="
structuredMap[item.platform]!.alipayRootCert =
''
"
>
<IconifyIcon
icon="ant-design:close-circle-outlined"
class="text-lg"
/>
</span>
</template>
</a-input>
</a-tooltip>
</a-form-item>
</template>
</template>
<!-- 抖音小程序 -->
<template v-else-if="item.platform === 'dy_mini'">
<a-form-item
:label="$t('payment.config.mobileApp.fields.dyAppId')"
:label="$t('system.mobileApp.fields.dyAppId')"
required
>
<a-input
v-model:value="structuredMap[item.platform]!.appId"
:disabled="!isEditing"
:placeholder="
$t('payment.config.mobileApp.fields.dyAppIdPlaceholder')
$t('system.mobileApp.fields.dyAppIdPlaceholder')
"
/>
</a-form-item>
<a-form-item
:label="$t('payment.config.mobileApp.fields.dyAppSecret')"
:label="$t('system.mobileApp.fields.dyAppSecret')"
required
>
<a-input
@@ -557,7 +856,7 @@
allow-clear
:placeholder="
$t(
'payment.config.mobileApp.fields.dyAppSecretPlaceholder',
'system.mobileApp.fields.dyAppSecretPlaceholder',
)
"
/>