mirror of
https://gitee.com/bootx/dax-pay-ui
synced 2026-08-11 07:05:33 +08:00
feat(payment): 移动端应用管理前端: 卡片页+详情页+API+locale
- API: src/api/payment/config/mobile-app.api.ts (list/list-by-app-type/get/save/update-enabled) - 卡片页 MobileAppConfig.vue: 三张卡片(商户端/管理端可点击, 收银台灰显敬请期待) - 详情页 detail/MobileAppDetail.vue: 按移动平台分Tab配置(密钥/通知/绑定/启用), 路由 /payment/config/mobile-app/detail/:appType - locale: payment/config/mobileApp.json (zh-CN/en-US, 含 platformNames)
This commit is contained in:
91
apps/daxpay-admin/src/api/payment/config/mobile-app.api.ts
Normal file
91
apps/daxpay-admin/src/api/payment/config/mobile-app.api.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import type { BaseEntity, Result } from '#/types/web';
|
||||
|
||||
import { defHttp } from '#/api/request';
|
||||
|
||||
/**
|
||||
* 移动端应用配置 API(平台级, 按端类型+移动平台维度)
|
||||
*/
|
||||
export const MobileAppApi = {
|
||||
/**
|
||||
* 查询全部(按端类型分组)
|
||||
*/
|
||||
list(): Promise<Result<MobileAppResult[]>> {
|
||||
return defHttp.get({ url: '/admin/mobile-app/list' });
|
||||
},
|
||||
|
||||
/**
|
||||
* 按端类型查询所有平台配置
|
||||
*/
|
||||
listByAppType(appType: string): Promise<Result<MobileAppResult[]>> {
|
||||
return defHttp.get({
|
||||
url: '/admin/mobile-app/list-by-app-type',
|
||||
params: { appType },
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 查询单条详情
|
||||
*/
|
||||
findById(id: number): Promise<Result<MobileAppResult>> {
|
||||
return defHttp.get({ url: '/admin/mobile-app/get', params: { id } });
|
||||
},
|
||||
|
||||
/**
|
||||
* 保存(按端类型+平台 upsert)
|
||||
*/
|
||||
save(param: MobileAppParam): Promise<Result<MobileAppResult>> {
|
||||
return defHttp.post({ url: '/admin/mobile-app/save', data: param });
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新启用状态
|
||||
*/
|
||||
updateEnabled(id: number, enable: boolean): Promise<Result<void>> {
|
||||
return defHttp.post({
|
||||
url: '/admin/mobile-app/update-enabled',
|
||||
params: { id, enable },
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
/** 移动端应用配置结果 */
|
||||
export interface MobileAppResult extends BaseEntity {
|
||||
/** 端类型: merchant/admin/cashier */
|
||||
appType?: string;
|
||||
/** 移动平台: wx_h5/wx_mini/alipay_mini/dy_mini/android/ios */
|
||||
platform?: string;
|
||||
/** 应用名称 */
|
||||
appName?: string;
|
||||
/** 平台特有密钥配置(JSON文本) */
|
||||
appConfig?: string;
|
||||
/** 消息通知配置(JSON文本) */
|
||||
notifyConfig?: string;
|
||||
/** 是否启用第三方账号用户绑定 */
|
||||
bindingEnabled?: boolean;
|
||||
/** 是否启用 */
|
||||
enabled?: boolean;
|
||||
/** 备注 */
|
||||
remark?: string;
|
||||
}
|
||||
|
||||
/** 移动端应用配置保存参数 */
|
||||
export interface MobileAppParam {
|
||||
/** 主键(更新时传) */
|
||||
id?: number;
|
||||
/** 端类型 */
|
||||
appType?: string;
|
||||
/** 移动平台 */
|
||||
platform?: string;
|
||||
/** 应用名称 */
|
||||
appName?: string;
|
||||
/** 平台特有密钥配置(JSON文本) */
|
||||
appConfig?: string;
|
||||
/** 消息通知配置(JSON文本) */
|
||||
notifyConfig?: string;
|
||||
/** 是否启用第三方账号用户绑定 */
|
||||
bindingEnabled?: boolean;
|
||||
/** 是否启用 */
|
||||
enabled?: boolean;
|
||||
/** 备注 */
|
||||
remark?: string;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"title": "Mobile App Management",
|
||||
"card": {
|
||||
"merchant": {
|
||||
"name": "Merchant App",
|
||||
"desc": "Mobile app for merchant staff"
|
||||
},
|
||||
"admin": {
|
||||
"name": "Admin App",
|
||||
"desc": "Mobile app for platform operators"
|
||||
},
|
||||
"cashier": {
|
||||
"name": "Cashier Mini Program",
|
||||
"desc": "Mini program for consumer payment"
|
||||
},
|
||||
"comingSoon": "Coming Soon",
|
||||
"enter": "Configure"
|
||||
},
|
||||
"platformNames": {
|
||||
"wx_h5": "WeChat Official Account",
|
||||
"wx_mini": "WeChat Mini Program",
|
||||
"alipay_mini": "Alipay Mini Program",
|
||||
"dy_mini": "Douyin Mini Program",
|
||||
"android": "Android App",
|
||||
"ios": "iOS App"
|
||||
},
|
||||
"detail": {
|
||||
"back": "Back",
|
||||
"appName": "App Name",
|
||||
"appNamePlaceholder": "Enter app name",
|
||||
"appConfig": "Platform Key Config",
|
||||
"appConfigPlaceholder": "Enter platform key config in JSON format",
|
||||
"notifyConfig": "Notification Config",
|
||||
"notifyConfigPlaceholder": "Enter notification config in JSON format",
|
||||
"bindingEnabled": "Enable third-party account binding",
|
||||
"enabled": "Enabled",
|
||||
"remark": "Remark",
|
||||
"remarkPlaceholder": "Enter remark",
|
||||
"save": "Save",
|
||||
"saveSuccess": "Saved successfully",
|
||||
"notSupportedAppType": "Unsupported app type",
|
||||
"emptyPlatformConfig": "No platform config yet, fill in and save to create"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"title": "移动端应用管理",
|
||||
"card": {
|
||||
"merchant": {
|
||||
"name": "商户端",
|
||||
"desc": "商户员工使用的移动应用"
|
||||
},
|
||||
"admin": {
|
||||
"name": "管理端",
|
||||
"desc": "平台运营使用的移动应用"
|
||||
},
|
||||
"cashier": {
|
||||
"name": "收银台小程序",
|
||||
"desc": "消费者扫码支付的小程序"
|
||||
},
|
||||
"comingSoon": "敬请期待",
|
||||
"enter": "进入配置"
|
||||
},
|
||||
"platformNames": {
|
||||
"wx_h5": "微信公众号",
|
||||
"wx_mini": "微信小程序",
|
||||
"alipay_mini": "支付宝小程序",
|
||||
"dy_mini": "抖音小程序",
|
||||
"android": "安卓APP",
|
||||
"ios": "iOS APP"
|
||||
},
|
||||
"detail": {
|
||||
"back": "返回",
|
||||
"appName": "应用名称",
|
||||
"appNamePlaceholder": "请输入应用名称",
|
||||
"appConfig": "平台密钥配置",
|
||||
"appConfigPlaceholder": "请输入 JSON 格式的平台密钥配置",
|
||||
"notifyConfig": "消息通知配置",
|
||||
"notifyConfigPlaceholder": "请输入 JSON 格式的消息通知配置",
|
||||
"bindingEnabled": "启用第三方账号用户绑定",
|
||||
"enabled": "是否启用",
|
||||
"remark": "备注",
|
||||
"remarkPlaceholder": "请输入备注",
|
||||
"save": "保存",
|
||||
"saveSuccess": "保存成功",
|
||||
"notSupportedAppType": "不支持的端类型",
|
||||
"emptyPlatformConfig": "暂无平台配置,填写后保存即可创建"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
<script lang="ts" setup>
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { IconifyIcon } from '@vben-core/icons';
|
||||
|
||||
defineOptions({ name: 'MobileAppConfig' });
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
// 三种端类型卡片配置(收银台本期灰显)
|
||||
const cards: {
|
||||
appType: string;
|
||||
disabled: boolean;
|
||||
icon: string;
|
||||
theme: string;
|
||||
}[] = [
|
||||
{
|
||||
appType: 'merchant',
|
||||
disabled: false,
|
||||
icon: 'ant-design:shop-filled',
|
||||
theme: 'blue',
|
||||
},
|
||||
{
|
||||
appType: 'admin',
|
||||
disabled: false,
|
||||
icon: 'ant-design:control-filled',
|
||||
theme: 'green',
|
||||
},
|
||||
{
|
||||
appType: 'cashier',
|
||||
disabled: true,
|
||||
icon: 'ant-design:qrcode-outlined',
|
||||
theme: 'amber',
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* 进入端详情页
|
||||
*/
|
||||
function handleEnter(appType: string, disabled: boolean) {
|
||||
if (disabled) return;
|
||||
router.push({ path: `/payment/config/mobile-app/detail/${appType}` });
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="m-4">
|
||||
<a-card variant="borderless" class="rounded-xl shadow-sm">
|
||||
<template #title>
|
||||
<span class="text-lg font-bold text-foreground">{{
|
||||
$t('payment.config.mobileApp.title')
|
||||
}}</span>
|
||||
</template>
|
||||
|
||||
<div class="mobile-app-grid">
|
||||
<div
|
||||
v-for="card in cards"
|
||||
:key="card.appType"
|
||||
class="mobile-app-card"
|
||||
:class="[
|
||||
{ 'mobile-app-card-disabled': card.disabled },
|
||||
`theme-${card.theme}`,
|
||||
]"
|
||||
@click="handleEnter(card.appType, card.disabled)"
|
||||
>
|
||||
<!-- 收银台灰显角标 -->
|
||||
<div v-if="card.disabled" class="coming-soon-badge">
|
||||
{{ $t('payment.config.mobileApp.card.comingSoon') }}
|
||||
</div>
|
||||
|
||||
<div class="card-icon">
|
||||
<IconifyIcon :icon="card.icon" class="text-5xl" />
|
||||
</div>
|
||||
|
||||
<div class="card-name">
|
||||
{{ $t(`payment.config.mobileApp.card.${card.appType}.name`) }}
|
||||
</div>
|
||||
<div class="card-desc">
|
||||
{{ $t(`payment.config.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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.mobile-app-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 20px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.mobile-app-card {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40px 24px;
|
||||
border-radius: 16px;
|
||||
border: 1px solid hsl(var(--border));
|
||||
background: hsl(var(--card));
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.mobile-app-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 12px 24px hsl(var(--muted) / 0.5);
|
||||
}
|
||||
|
||||
.mobile-app-card.theme-blue:hover {
|
||||
border-color: hsl(var(--primary));
|
||||
}
|
||||
|
||||
.mobile-app-card.theme-green:hover {
|
||||
border-color: hsl(142 71% 45%);
|
||||
}
|
||||
|
||||
.mobile-app-card-disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.mobile-app-card-disabled:hover {
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.theme-blue .card-icon {
|
||||
color: hsl(var(--primary));
|
||||
}
|
||||
|
||||
.theme-green .card-icon {
|
||||
color: hsl(142 71% 45%);
|
||||
}
|
||||
|
||||
.theme-amber .card-icon {
|
||||
color: hsl(38 92% 50%);
|
||||
}
|
||||
|
||||
.card-name {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: hsl(var(--foreground));
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.card-desc {
|
||||
font-size: 13px;
|
||||
color: hsl(var(--muted-foreground));
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.card-enter {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 16px;
|
||||
border-radius: 9999px;
|
||||
background: hsl(var(--muted));
|
||||
color: hsl(var(--muted-foreground));
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.mobile-app-card:hover .card-enter {
|
||||
background: hsl(var(--primary));
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.coming-soon-badge {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
padding: 2px 10px;
|
||||
border-radius: 9999px;
|
||||
background: hsl(38 92% 50%);
|
||||
color: #fff;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,220 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, reactive, ref } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { IconifyIcon } from '@vben-core/icons';
|
||||
|
||||
import {
|
||||
type MobileAppParam,
|
||||
MobileAppApi,
|
||||
} from '#/api/payment/config/mobile-app.api';
|
||||
import { useMessage } from '#/hooks/useMessage';
|
||||
|
||||
defineOptions({ name: 'MobileAppDetail' });
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const { message } = useMessage();
|
||||
|
||||
const appType = ref((route.params.appType as string) || '');
|
||||
const loading = ref(false);
|
||||
const activePlatform = ref('');
|
||||
const saving = ref(false);
|
||||
|
||||
// 各端支持的平台(收银台无H5和APP)
|
||||
const PLATFORMS_BY_APP_TYPE: Record<string, string[]> = {
|
||||
admin: ['wx_h5', 'wx_mini', 'alipay_mini', 'dy_mini', 'android', 'ios'],
|
||||
cashier: ['wx_mini', 'alipay_mini', 'dy_mini'],
|
||||
merchant: ['wx_h5', 'wx_mini', 'alipay_mini', 'dy_mini', 'android', 'ios'],
|
||||
};
|
||||
|
||||
const platforms = computed(() => PLATFORMS_BY_APP_TYPE[appType.value] || []);
|
||||
|
||||
// 各平台表单数据(platform -> formData)
|
||||
const formDataMap = reactive<Record<string, MobileAppParam>>({});
|
||||
|
||||
// 端标题
|
||||
const appTitle = computed(() => {
|
||||
if (appType.value) {
|
||||
return $t(`payment.config.mobileApp.card.${appType.value}.name`);
|
||||
}
|
||||
return $t('payment.config.mobileApp.detail.notSupportedAppType');
|
||||
});
|
||||
|
||||
/**
|
||||
* 初始化某平台的空表单
|
||||
*/
|
||||
function initForm(platform: string): MobileAppParam {
|
||||
return {
|
||||
appType: appType.value,
|
||||
platform,
|
||||
appName: '',
|
||||
appConfig: '',
|
||||
bindingEnabled: false,
|
||||
enabled: true,
|
||||
notifyConfig: '',
|
||||
remark: '',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载该端所有平台配置
|
||||
*/
|
||||
async function loadData() {
|
||||
if (!appType.value || !platforms.value.length) return;
|
||||
loading.value = true;
|
||||
try {
|
||||
// 初始化所有平台为空表单
|
||||
for (const p of platforms.value) {
|
||||
formDataMap[p] = initForm(p);
|
||||
}
|
||||
// 用后端数据填充已存在的配置
|
||||
const { data } = await MobileAppApi.listByAppType(appType.value);
|
||||
for (const item of data || []) {
|
||||
if (item.platform && formDataMap[item.platform]) {
|
||||
Object.assign(formDataMap[item.platform]!, item);
|
||||
}
|
||||
}
|
||||
activePlatform.value = platforms.value[0]!;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存当前平台配置
|
||||
*/
|
||||
async function handleSave() {
|
||||
const form = formDataMap[activePlatform.value];
|
||||
if (!form) return;
|
||||
saving.value = true;
|
||||
try {
|
||||
await MobileAppApi.save(form);
|
||||
message.success($t('payment.config.mobileApp.detail.saveSuccess'));
|
||||
await loadData();
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回卡片页
|
||||
*/
|
||||
function handleBack() {
|
||||
router.push({ path: '/payment/config/mobile-app' });
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="m-4">
|
||||
<a-card variant="borderless" class="rounded-xl shadow-sm">
|
||||
<template #title>
|
||||
<div class="flex items-center gap-2">
|
||||
<a-button
|
||||
type="text"
|
||||
class="flex items-center justify-center rounded-full hover:bg-accent"
|
||||
@click="handleBack"
|
||||
>
|
||||
<template #icon>
|
||||
<IconifyIcon
|
||||
icon="ant-design:arrow-left-outlined"
|
||||
class="text-lg"
|
||||
/>
|
||||
</template>
|
||||
</a-button>
|
||||
<span class="text-lg font-bold text-foreground">{{ appTitle }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<a-spin :spinning="loading">
|
||||
<a-tabs v-model:active-key="activePlatform">
|
||||
<a-tab-pane
|
||||
v-for="p in platforms"
|
||||
:key="p"
|
||||
:tab="$t(`payment.config.mobileApp.platformNames.${p}`)"
|
||||
>
|
||||
<a-form
|
||||
v-if="formDataMap[p]"
|
||||
:model="formDataMap[p]"
|
||||
layout="vertical"
|
||||
class="max-w-2xl"
|
||||
>
|
||||
<a-form-item :label="$t('payment.config.mobileApp.detail.appName')">
|
||||
<a-input
|
||||
v-model:value="formDataMap[p]!.appName"
|
||||
:placeholder="
|
||||
$t('payment.config.mobileApp.detail.appNamePlaceholder')
|
||||
"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item
|
||||
:label="$t('payment.config.mobileApp.detail.appConfig')"
|
||||
>
|
||||
<a-textarea
|
||||
v-model:value="formDataMap[p]!.appConfig"
|
||||
:rows="6"
|
||||
:placeholder="
|
||||
$t('payment.config.mobileApp.detail.appConfigPlaceholder')
|
||||
"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item
|
||||
:label="$t('payment.config.mobileApp.detail.notifyConfig')"
|
||||
>
|
||||
<a-textarea
|
||||
v-model:value="formDataMap[p]!.notifyConfig"
|
||||
:rows="4"
|
||||
:placeholder="
|
||||
$t('payment.config.mobileApp.detail.notifyConfigPlaceholder')
|
||||
"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
<template #label>
|
||||
{{ $t('payment.config.mobileApp.detail.bindingEnabled') }}
|
||||
</template>
|
||||
<a-switch v-model:checked="formDataMap[p]!.bindingEnabled" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
<template #label>
|
||||
{{ $t('payment.config.mobileApp.detail.enabled') }}
|
||||
</template>
|
||||
<a-switch v-model:checked="formDataMap[p]!.enabled" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item :label="$t('payment.config.mobileApp.detail.remark')">
|
||||
<a-textarea
|
||||
v-model:value="formDataMap[p]!.remark"
|
||||
:rows="2"
|
||||
:placeholder="
|
||||
$t('payment.config.mobileApp.detail.remarkPlaceholder')
|
||||
"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
<a-button
|
||||
type="primary"
|
||||
:loading="saving"
|
||||
@click="handleSave"
|
||||
>
|
||||
{{ $t('payment.config.mobileApp.detail.save') }}
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</a-spin>
|
||||
</a-card>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user