|
|
|
@@ -0,0 +1,50 @@
|
|
|
|
|
<template>
|
|
|
|
|
<section class="platform-apps-page">
|
|
|
|
|
<el-alert v-if="lastError" :title="lastError" type="error" show-icon closable @close="lastError = ''" />
|
|
|
|
|
<el-alert title="AppSecret、微信支付密钥和证书只允许配置在服务端环境变量中,本页面不会读取、保存或回显。" type="info" show-icon :closable="false" />
|
|
|
|
|
<section class="platform-app-metrics"><span><small>已绑定应用</small><strong>{{ apps.length }}</strong></span><span><small>启用绑定</small><strong>{{ activeCount }}</strong></span><span><small>默认应用</small><strong>{{ defaultCount }}</strong></span><span><small>品牌配置完整</small><strong>{{ configuredCount }}</strong></span></section>
|
|
|
|
|
<section class="panel">
|
|
|
|
|
<header class="panel-toolbar platform-app-toolbar"><div><p class="section-kicker">M08-D · 多应用租户</p><h3>多小程序与租户品牌配置</h3></div><div class="toolbar-actions"><el-button :icon="RefreshCw" :loading="loading" @click="loadData">刷新</el-button><el-button type="primary" :icon="Plus" @click="openBind">绑定应用</el-button></div></header>
|
|
|
|
|
<el-table v-loading="loading" :data="apps" class="data-table" row-key="platformAppId">
|
|
|
|
|
<el-table-column label="逻辑应用" min-width="210"><template #default="{ row }"><div class="stack"><strong>{{ row.appName }}</strong><span>{{ row.appId }} · #{{ row.platformAppId }}</span></div></template></el-table-column>
|
|
|
|
|
<el-table-column label="绑定" width="130"><template #default="{ row }"><div class="stack"><el-tag :type="row.bindingStatus === 'ACTIVE' ? 'success' : 'info'">{{ row.bindingStatus }}</el-tag><span>{{ row.isDefault ? '租户默认' : '普通绑定' }}</span></div></template></el-table-column>
|
|
|
|
|
<el-table-column label="品牌" min-width="190"><template #default="{ row }"><div class="stack"><strong>{{ row.brandName }}</strong><span><i class="theme-dot" :style="{ background: row.themeColor }" />{{ row.themeColor }} · {{ row.servicePhone || '未留客服电话' }}</span></div></template></el-table-column>
|
|
|
|
|
<el-table-column label="默认门店" min-width="160"><template #default="{ row }">{{ row.defaultStoreId ? storeName(row.defaultStoreId) : '未配置' }}</template></el-table-column>
|
|
|
|
|
<el-table-column label="分享" min-width="200"><template #default="{ row }"><div class="stack"><strong>{{ row.shareTitle || '-' }}</strong><span>{{ row.shareImageUrl ? '已配置分享图' : '未配置分享图' }}</span></div></template></el-table-column>
|
|
|
|
|
<el-table-column label="更新时间" width="150"><template #default="{ row }">{{ shortDate(row.updatedAt) }}</template></el-table-column>
|
|
|
|
|
<el-table-column label="操作" width="100" fixed="right"><template #default="{ row }"><el-button size="small" :icon="Settings2" @click="openEdit(row)">配置</el-button></template></el-table-column>
|
|
|
|
|
</el-table>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<el-dialog v-model="dialog" :title="form.platformAppId ? '编辑租户应用配置' : '绑定逻辑应用'" width="min(760px, 94vw)">
|
|
|
|
|
<el-form label-position="top" class="asset-form">
|
|
|
|
|
<template v-if="!form.platformAppId"><el-form-item label="微信 AppID"><el-input v-model="form.appId" autocomplete="off" /></el-form-item><el-form-item label="应用名称"><el-input v-model="form.appName" /></el-form-item><el-form-item label="全局应用状态"><el-select v-model="form.appStatus"><el-option label="启用" value="ACTIVE" /><el-option label="停用" value="DISABLED" /></el-select></el-form-item></template>
|
|
|
|
|
<el-form-item label="品牌名称"><el-input v-model="form.brandName" /></el-form-item><el-form-item label="主题色"><el-color-picker v-model="form.themeColor" /><el-input v-model="form.themeColor" class="color-input" /></el-form-item><el-form-item label="客服电话"><el-input v-model="form.servicePhone" /></el-form-item><el-form-item label="加盟电话"><el-input v-model="form.franchisePhone" /></el-form-item><el-form-item label="默认门店"><el-select v-model="form.defaultStoreId" clearable><el-option label="不指定" value="" /><el-option v-for="store in stores" :key="store.id" :label="store.name" :value="store.id" /></el-select></el-form-item><el-form-item label="绑定状态"><el-select v-model="form.bindingStatus"><el-option label="启用" value="ACTIVE" /><el-option label="停用" value="DISABLED" /></el-select></el-form-item><el-form-item label="租户默认应用"><el-switch v-model="form.isDefault" /></el-form-item><el-form-item label="Logo URL" class="form-span-2"><el-input v-model="form.logoUrl" /></el-form-item><el-form-item label="分享标题" class="form-span-2"><el-input v-model="form.shareTitle" /></el-form-item><el-form-item label="分享图片 URL" class="form-span-2"><el-input v-model="form.shareImageUrl" /></el-form-item><el-form-item label="非敏感扩展配置 JSON" class="form-span-2"><el-input v-model="form.extraConfigText" type="textarea" :rows="5" placeholder='{"bookingMode":"STANDARD"}' /></el-form-item>
|
|
|
|
|
</el-form>
|
|
|
|
|
<template #footer><el-button @click="dialog = false">取消</el-button><el-button type="primary" :loading="saving" @click="save">保存</el-button></template>
|
|
|
|
|
</el-dialog>
|
|
|
|
|
</section>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
import { computed, reactive, ref, watch } from 'vue';
|
|
|
|
|
import { ElMessage } from 'element-plus';
|
|
|
|
|
import { Plus, RefreshCw, Settings2 } from '@lucide/vue';
|
|
|
|
|
import { ApiError, bindPlatformApplication, listManagedStores, listPlatformApplications, updatePlatformApplicationConfig, type ApiSession } from '../api';
|
|
|
|
|
import { shortDate } from '../format';
|
|
|
|
|
import type { ManagedStore, PlatformApplication, TenantApplicationConfig } from '../types';
|
|
|
|
|
|
|
|
|
|
const props = defineProps<{ session: ApiSession }>();
|
|
|
|
|
const apps = ref<PlatformApplication[]>([]); const stores = ref<ManagedStore[]>([]); const loading = ref(false); const saving = ref(false); const lastError = ref(''); const dialog = ref(false);
|
|
|
|
|
const form = reactive({ platformAppId: '', appId: '', appName: '', appStatus: 'ACTIVE' as 'ACTIVE' | 'DISABLED', brandName: '', logoUrl: '', themeColor: '#1677ff', servicePhone: '', franchisePhone: '', shareTitle: '', shareImageUrl: '', defaultStoreId: '', bindingStatus: 'ACTIVE' as 'ACTIVE' | 'DISABLED', isDefault: false, extraConfigText: '{}' });
|
|
|
|
|
const activeCount = computed(() => apps.value.filter((item) => item.bindingStatus === 'ACTIVE').length); const defaultCount = computed(() => apps.value.filter((item) => item.isDefault).length); const configuredCount = computed(() => apps.value.filter((item) => item.brandName && item.themeColor && item.shareTitle).length);
|
|
|
|
|
async function capture<T>(work: () => Promise<T>) { lastError.value = ''; try { return await work(); } catch (error) { lastError.value = error instanceof ApiError ? `${error.code}${error.traceId ? ` · ${error.traceId}` : ''}` : error instanceof Error ? error.message : '操作失败'; throw error; } }
|
|
|
|
|
async function loadData() { if (!props.session.token) return; loading.value = true; try { [apps.value, stores.value] = await Promise.all([capture(() => listPlatformApplications(props.session)), capture(() => listManagedStores(props.session))]); } catch { /* displayed */ } finally { loading.value = false; } }
|
|
|
|
|
function defaults() { return { platformAppId: '', appId: '', appName: '', appStatus: 'ACTIVE', brandName: '', logoUrl: '', themeColor: '#1677ff', servicePhone: '', franchisePhone: '', shareTitle: '', shareImageUrl: '', defaultStoreId: '', bindingStatus: 'ACTIVE', isDefault: apps.value.length === 0, extraConfigText: '{}' }; }
|
|
|
|
|
function openBind() { Object.assign(form, defaults()); dialog.value = true; }
|
|
|
|
|
function openEdit(row: PlatformApplication) { Object.assign(form, { platformAppId: row.platformAppId, appId: row.appId, appName: row.appName, appStatus: row.appStatus, brandName: row.brandName, logoUrl: row.logoUrl, themeColor: row.themeColor, servicePhone: row.servicePhone, franchisePhone: row.franchisePhone, shareTitle: row.shareTitle, shareImageUrl: row.shareImageUrl, defaultStoreId: row.defaultStoreId || '', bindingStatus: row.bindingStatus, isDefault: row.isDefault, extraConfigText: JSON.stringify(row.extraConfig, null, 2) }); dialog.value = true; }
|
|
|
|
|
function configInput(): TenantApplicationConfig | null { let extraConfig: unknown; try { extraConfig = JSON.parse(form.extraConfigText || '{}'); } catch { ElMessage.warning('扩展配置必须是 JSON 对象'); return null; } if (!extraConfig || typeof extraConfig !== 'object' || Array.isArray(extraConfig)) { ElMessage.warning('扩展配置必须是 JSON 对象'); return null; } return { brandName: form.brandName.trim(), logoUrl: form.logoUrl.trim(), themeColor: form.themeColor, servicePhone: form.servicePhone.trim(), franchisePhone: form.franchisePhone.trim(), shareTitle: form.shareTitle.trim(), shareImageUrl: form.shareImageUrl.trim(), defaultStoreId: form.defaultStoreId || null, extraConfig: extraConfig as Record<string, unknown>, bindingStatus: form.bindingStatus, isDefault: form.isDefault }; }
|
|
|
|
|
async function save() { const config = configInput(); if (!config || !config.brandName || !/^#[0-9A-Fa-f]{6}$/.test(config.themeColor) || (!form.platformAppId && (!form.appId || !form.appName))) return ElMessage.warning('请填写 AppID、应用名称、品牌名称和有效主题色'); saving.value = true; try { if (form.platformAppId) await capture(() => updatePlatformApplicationConfig(props.session, form.platformAppId, config)); else await capture(() => bindPlatformApplication(props.session, { appId: form.appId.trim(), appName: form.appName.trim(), appStatus: form.appStatus, config })); dialog.value = false; ElMessage.success('应用配置已保存'); await loadData(); } catch { /* displayed */ } finally { saving.value = false; } }
|
|
|
|
|
function storeName(id: string) { return stores.value.find((item) => item.id === id)?.name || `门店 #${id}`; }
|
|
|
|
|
watch(() => props.session.token, (token) => { if (token) void loadData(); }, { immediate: true });
|
|
|
|
|
</script>
|