350 lines
9.3 KiB
Vue
350 lines
9.3 KiB
Vue
<script lang="ts" setup>
|
||
import type { Component } from 'vue';
|
||
|
||
import type { ModuleTabItem } from './components/module-tabs-panel.vue';
|
||
|
||
/**
|
||
* 系统配置页壳:左侧一级模块 + 右侧二级 Tab(与 AI 模块同一交互)
|
||
* 各子面板自管 load/save;旧菜单 key 会映射到新模块/Tab
|
||
*/
|
||
import { computed, onMounted, ref } from 'vue';
|
||
|
||
import { Page } from '@vben/common-ui';
|
||
|
||
import { Card } from 'ant-design-vue';
|
||
|
||
import AiModelConfigPanel from './components/ai-model-config-panel.vue';
|
||
import DictSearchConfigPanel from './components/dict-search-config-panel.vue';
|
||
import DispenseSignConfigPanel from './components/dispense-sign-config-panel.vue';
|
||
import DoctorLoginConfigPanel from './components/doctor-login-config-panel.vue';
|
||
import FreeShippingConfigPanel from './components/free-shipping-config-panel.vue';
|
||
import HomeDisplayConfigPanel from './components/home-display-config-panel.vue';
|
||
import InputAuditConfigPanel from './components/input-audit-config-panel.vue';
|
||
import ImageTransferConfigPanel from './components/image-transfer-config-panel.vue';
|
||
import InvoiceNoticeConfigPanel from './components/invoice-notice-config-panel.vue';
|
||
import LogisticsConfigPanel from './components/logistics-config-panel.vue';
|
||
import MedicalRecordConfigPanel from './components/medical-record-config-panel.vue';
|
||
import MiniprogramConfigPanel from './components/miniprogram-config-panel.vue';
|
||
import ModuleTabsPanel from './components/module-tabs-panel.vue';
|
||
import OaNotifyConfigPanel from './components/oa-notify-config-panel.vue';
|
||
import PriceAdjustConfigPanel from './components/price-adjust-config-panel.vue';
|
||
import ExcipientPriceConfigPanel from './components/excipient-price-config-panel.vue';
|
||
import PublicAccountPayConfigPanel from './components/public-account-pay-config-panel.vue';
|
||
import SalespersonConfigPanel from './components/salesperson-config-panel.vue';
|
||
import WorkbenchConfigPanel from './components/workbench-config-panel.vue';
|
||
import WithdrawalRulesConfigPanel from './components/withdrawal-rules-config-panel.vue';
|
||
import WxWorkbenchDefaultFavPanel from './components/wx-workbench-default-fav-panel.vue';
|
||
|
||
defineOptions({ name: 'SystemConfig' });
|
||
|
||
const MENU_STORAGE_KEY = 'system_config_active_tab';
|
||
|
||
interface MenuItem {
|
||
key: string;
|
||
title: string;
|
||
/** 已自带二级 Tab 的整块面板(AI) */
|
||
component?: Component;
|
||
tabs?: ModuleTabItem[];
|
||
}
|
||
|
||
const menuItems: MenuItem[] = [
|
||
{
|
||
key: 'mp_patient',
|
||
title: '患者端小程序',
|
||
tabs: [
|
||
{ key: 'empty', title: '空状态图', component: MiniprogramConfigPanel },
|
||
{ key: 'home', title: '首页展示', component: HomeDisplayConfigPanel },
|
||
],
|
||
},
|
||
{
|
||
key: 'mp_doctor',
|
||
title: '医生端小程序',
|
||
tabs: [
|
||
{
|
||
key: 'default_fav',
|
||
title: '默认常用入口',
|
||
component: WxWorkbenchDefaultFavPanel,
|
||
},
|
||
],
|
||
},
|
||
{
|
||
key: 'workbench',
|
||
title: '工作台',
|
||
component: WorkbenchConfigPanel,
|
||
},
|
||
{
|
||
key: 'dispense_sign',
|
||
title: '处方签名',
|
||
component: DispenseSignConfigPanel,
|
||
},
|
||
{
|
||
key: 'order',
|
||
title: '订单',
|
||
tabs: [
|
||
{
|
||
key: 'price_adjust',
|
||
title: '订单调价',
|
||
component: PriceAdjustConfigPanel,
|
||
},
|
||
{
|
||
key: 'excipient_price',
|
||
title: '辅料定价',
|
||
component: ExcipientPriceConfigPanel,
|
||
},
|
||
{ key: 'logistics', title: '物流展示', component: LogisticsConfigPanel },
|
||
{
|
||
key: 'free_shipping',
|
||
title: '满额包邮',
|
||
component: FreeShippingConfigPanel,
|
||
},
|
||
{
|
||
key: 'public_account_pay',
|
||
title: '公账支付',
|
||
component: PublicAccountPayConfigPanel,
|
||
},
|
||
],
|
||
},
|
||
{
|
||
key: 'security',
|
||
title: '权限安全',
|
||
tabs: [
|
||
{ key: 'login', title: '登录安全', component: DoctorLoginConfigPanel },
|
||
{
|
||
key: 'salesperson',
|
||
title: '业务员权限',
|
||
component: SalespersonConfigPanel,
|
||
},
|
||
],
|
||
},
|
||
{
|
||
key: 'medical',
|
||
title: '病历词条',
|
||
tabs: [
|
||
{ key: 'entry', title: '词条', component: MedicalRecordConfigPanel },
|
||
{
|
||
key: 'dict_search',
|
||
title: '字典搜索',
|
||
component: DictSearchConfigPanel,
|
||
},
|
||
],
|
||
},
|
||
{
|
||
key: 'notice',
|
||
title: '通知',
|
||
tabs: [
|
||
{
|
||
key: 'invoice',
|
||
title: '开票通知',
|
||
component: InvoiceNoticeConfigPanel,
|
||
},
|
||
{ key: 'oa', title: 'OA通知', component: OaNotifyConfigPanel },
|
||
],
|
||
},
|
||
{
|
||
key: 'audit',
|
||
title: '审核',
|
||
component: InputAuditConfigPanel,
|
||
},
|
||
{
|
||
key: 'image_transfer',
|
||
title: '传方',
|
||
component: ImageTransferConfigPanel,
|
||
},
|
||
{
|
||
key: 'finance',
|
||
title: '财务',
|
||
tabs: [
|
||
{
|
||
key: 'withdrawal_rules',
|
||
title: '提现规则',
|
||
component: WithdrawalRulesConfigPanel,
|
||
},
|
||
],
|
||
},
|
||
{
|
||
key: 'ai_model',
|
||
title: 'AI模型',
|
||
component: AiModelConfigPanel,
|
||
},
|
||
];
|
||
|
||
/** 旧左侧菜单 key → 新模块 + 二级 Tab,避免刷新后丢选中 */
|
||
const LEGACY_MENU_MAP: Record<string, { menu: string; tab?: string }> = {
|
||
price_adjust: { menu: 'order', tab: 'price_adjust' },
|
||
input_audit: { menu: 'audit', tab: 'input_audit' },
|
||
miniprogram: { menu: 'mp_patient' },
|
||
logistics: { menu: 'order', tab: 'logistics' },
|
||
public_account_pay: { menu: 'order', tab: 'public_account_pay' },
|
||
salesperson: { menu: 'security', tab: 'salesperson' },
|
||
doctor_login: { menu: 'security', tab: 'login' },
|
||
dict_search: { menu: 'medical', tab: 'dict_search' },
|
||
medical_record: { menu: 'medical', tab: 'entry' },
|
||
invoice_notice: { menu: 'notice', tab: 'invoice' },
|
||
oa_notify: { menu: 'notice', tab: 'oa' },
|
||
ai_model: { menu: 'ai_model' },
|
||
};
|
||
|
||
const activeKey = ref('mp_patient');
|
||
|
||
const currentMenu = computed(() => {
|
||
return menuItems.find((m) => m.key === activeKey.value) || menuItems[0];
|
||
});
|
||
|
||
function readStoredMenu() {
|
||
const stored = localStorage.getItem(MENU_STORAGE_KEY);
|
||
if (!stored) {
|
||
return;
|
||
}
|
||
if (menuItems.some((m) => m.key === stored)) {
|
||
activeKey.value = stored;
|
||
return;
|
||
}
|
||
const mapped = LEGACY_MENU_MAP[stored];
|
||
if (!mapped) {
|
||
return;
|
||
}
|
||
activeKey.value = mapped.menu;
|
||
if (mapped.tab) {
|
||
localStorage.setItem(`system_config_${mapped.menu}_sub_tab`, mapped.tab);
|
||
} else if (stored === 'miniprogram') {
|
||
// 旧「小程序」二级 Tab 迁到患者端,避免刷新后丢「首页展示」
|
||
const oldSub = localStorage.getItem('system_config_miniprogram_sub_tab');
|
||
if (oldSub) {
|
||
localStorage.setItem('system_config_mp_patient_sub_tab', oldSub);
|
||
}
|
||
}
|
||
localStorage.setItem(MENU_STORAGE_KEY, mapped.menu);
|
||
}
|
||
|
||
function selectMenu(key: string) {
|
||
activeKey.value = key;
|
||
localStorage.setItem(MENU_STORAGE_KEY, key);
|
||
}
|
||
|
||
onMounted(readStoredMenu);
|
||
</script>
|
||
|
||
<template>
|
||
<Page auto-content-height title="系统配置">
|
||
<Card class="sys-cfg-card" :body-style="{ padding: 0, height: '100%' }">
|
||
<div class="sys-cfg-layout">
|
||
<aside class="sys-cfg-nav">
|
||
<button
|
||
v-for="item in menuItems"
|
||
:key="item.key"
|
||
type="button"
|
||
class="sys-cfg-nav-item"
|
||
:class="{ active: activeKey === item.key }"
|
||
@click="selectMenu(item.key)"
|
||
>
|
||
{{ item.title }}
|
||
</button>
|
||
</aside>
|
||
<section class="sys-cfg-main">
|
||
<component
|
||
v-if="currentMenu.component"
|
||
:is="currentMenu.component"
|
||
:key="currentMenu.key"
|
||
/>
|
||
<ModuleTabsPanel
|
||
v-else-if="currentMenu.tabs"
|
||
:key="currentMenu.key"
|
||
:module-key="currentMenu.key"
|
||
:tabs="currentMenu.tabs"
|
||
/>
|
||
</section>
|
||
</div>
|
||
</Card>
|
||
</Page>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.sys-cfg-card {
|
||
display: flex;
|
||
flex-direction: column;
|
||
height: 100%;
|
||
min-height: 0;
|
||
}
|
||
|
||
.sys-cfg-layout {
|
||
display: flex;
|
||
height: 100%;
|
||
min-height: 480px;
|
||
max-height: calc(100vh - 160px);
|
||
}
|
||
|
||
.sys-cfg-nav {
|
||
flex-shrink: 0;
|
||
width: 188px;
|
||
padding: 8px;
|
||
overflow-y: auto;
|
||
background: hsl(var(--muted) / 25%);
|
||
border-right: 1px solid hsl(var(--border));
|
||
}
|
||
|
||
.sys-cfg-nav-item {
|
||
display: block;
|
||
width: 100%;
|
||
padding: 10px 12px;
|
||
margin-bottom: 4px;
|
||
font-size: 14px;
|
||
color: hsl(var(--foreground));
|
||
text-align: left;
|
||
cursor: pointer;
|
||
background: transparent;
|
||
border: 1px solid transparent;
|
||
border-radius: 8px;
|
||
transition:
|
||
background 0.15s ease,
|
||
border-color 0.15s ease,
|
||
color 0.15s ease;
|
||
}
|
||
|
||
.sys-cfg-nav-item:hover {
|
||
background: hsl(var(--accent));
|
||
}
|
||
|
||
.sys-cfg-nav-item.active {
|
||
color: hsl(var(--primary));
|
||
background: hsl(var(--primary) / 10%);
|
||
border-color: hsl(var(--primary) / 35%);
|
||
box-shadow: 0 0 6px hsl(var(--primary) / 18%);
|
||
}
|
||
|
||
.sys-cfg-main {
|
||
display: flex;
|
||
flex: 1;
|
||
flex-direction: column;
|
||
min-width: 0;
|
||
min-height: 0;
|
||
}
|
||
|
||
/* 子面板统一:内容滚动 + 底栏不滚走 */
|
||
.sys-cfg-main :deep(.cfg-panel) {
|
||
display: flex;
|
||
flex-direction: column;
|
||
height: 100%;
|
||
min-height: 0;
|
||
}
|
||
|
||
.sys-cfg-main :deep(.cfg-panel-body) {
|
||
flex: 1;
|
||
min-height: 0;
|
||
padding: 16px 20px;
|
||
overflow: auto;
|
||
}
|
||
|
||
.sys-cfg-main :deep(.module-tabs-body) {
|
||
padding: 8px 0 0;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.sys-cfg-main :deep(.cfg-panel-footer) {
|
||
z-index: 2;
|
||
flex-shrink: 0;
|
||
padding: 12px 20px;
|
||
background: hsl(var(--card, var(--background)));
|
||
border-top: 1px solid hsl(var(--border));
|
||
}
|
||
</style>
|