feat: 补齐素材文件夹、挂号改期、系统配置分组与工作台简报开关

素材库按 object_key 目录同步文件夹侧栏,选图和筛选可按目录收窄。
挂号列表支持管理端代改期,号源日历与患者端同源。
系统配置按端拆模块(患者首页展示、医生默认常用入口、工作台简报总开关、处方签名),旧菜单 key 做迁移以免刷新丢选中。
AI 生成记录补每日简报场景和缓存命中筛选;个人中心改走后端菜单 Profile。
This commit is contained in:
李琦
2026-08-18 09:12:07 +08:00
parent c6d2b0f917
commit d159e514b2
33 changed files with 4342 additions and 1133 deletions

View File

@@ -1,9 +1,13 @@
<script lang="ts" setup>
import type { Component } from 'vue';
import type { ModuleTabItem } from './components/module-tabs-panel.vue';
/**
* 系统配置页壳:左侧一级菜单 + 右侧动态面板
* 各模块自管 load/save;AI 在右侧保留二级 Tab
* 系统配置页壳:左侧一级模块 + 右侧二级 Tab(与 AI 模块同一交互)
* 各子面板自管 load/save;旧菜单 key 会映射到新模块/Tab
*/
import { computed, onMounted, ref, type Component } from 'vue';
import { computed, onMounted, ref } from 'vue';
import { Page } from '@vben/common-ui';
@@ -11,15 +15,20 @@ 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 HomeDisplayConfigPanel from './components/home-display-config-panel.vue';
import InputAuditConfigPanel from './components/input-audit-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 SalespersonConfigPanel from './components/salesperson-config-panel.vue';
import WorkbenchConfigPanel from './components/workbench-config-panel.vue';
import WxWorkbenchDefaultFavPanel from './components/wx-workbench-default-fav-panel.vue';
defineOptions({ name: 'SystemConfig' });
@@ -28,35 +37,146 @@ const MENU_STORAGE_KEY = 'system_config_active_tab';
interface MenuItem {
key: string;
title: string;
component: Component;
/** 已自带二级 Tab 的整块面板(AI) */
component?: Component;
tabs?: ModuleTabItem[];
}
const menuItems: MenuItem[] = [
{ key: 'price_adjust', title: '订单调价', component: PriceAdjustConfigPanel },
{ key: 'input_audit', title: '录入审核', component: InputAuditConfigPanel },
{ key: 'miniprogram', title: '小程序', component: MiniprogramConfigPanel },
{ key: 'logistics', title: '物流展示', component: LogisticsConfigPanel },
{ key: 'salesperson', title: '业务员权限', component: SalespersonConfigPanel },
{ key: 'doctor_login', title: '登录安全', component: DoctorLoginConfigPanel },
{ key: 'dict_search', title: '字典搜索', component: DictSearchConfigPanel },
{ key: 'medical_record', title: '词条', component: MedicalRecordConfigPanel },
{ key: 'invoice_notice', title: '开票通知', component: InvoiceNoticeConfigPanel },
{ key: 'oa_notify', title: 'OA通知', component: OaNotifyConfigPanel },
{ key: 'ai_model', title: 'AI模型', component: AiModelConfigPanel },
{
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: 'logistics', title: '物流展示', component: LogisticsConfigPanel },
],
},
{
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: 'ai_model',
title: 'AI模型',
component: AiModelConfigPanel,
},
];
const activeKey = ref('price_adjust');
/** 旧左侧菜单 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' },
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 currentPanel = computed(() => {
const hit = menuItems.find((m) => m.key === activeKey.value);
return hit?.component || PriceAdjustConfigPanel;
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 && menuItems.some((m) => m.key === stored)) {
activeKey.value = stored;
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) {
@@ -84,7 +204,17 @@ onMounted(readStoredMenu);
</button>
</aside>
<section class="sys-cfg-main">
<component :is="currentPanel" :key="activeKey" />
<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>
@@ -93,58 +223,65 @@ onMounted(readStoredMenu);
<style scoped>
.sys-cfg-card {
height: 100%;
min-height: 0;
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 {
width: 188px;
flex-shrink: 0;
overflow-y: auto;
border-right: 1px solid hsl(var(--border));
background: hsl(var(--muted) / 0.25);
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%;
text-align: left;
border: 1px solid transparent;
background: transparent;
color: hsl(var(--foreground));
border-radius: 8px;
padding: 10px 12px;
margin-bottom: 4px;
cursor: pointer;
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 {
border-color: hsl(var(--primary) / 35%);
background: hsl(var(--primary) / 0.1);
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;
display: flex;
flex-direction: column;
}
/* 子面板统一:内容滚动 + 底栏不滚走 */
.sys-cfg-main :deep(.cfg-panel) {
display: flex;
@@ -152,17 +289,24 @@ onMounted(readStoredMenu);
height: 100%;
min-height: 0;
}
.sys-cfg-main :deep(.cfg-panel-body) {
flex: 1;
min-height: 0;
overflow: auto;
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) {
flex-shrink: 0;
border-top: 1px solid hsl(var(--border));
background: hsl(var(--card, var(--background)));
padding: 12px 20px;
z-index: 2;
flex-shrink: 0;
padding: 12px 20px;
background: hsl(var(--card, var(--background)));
border-top: 1px solid hsl(var(--border));
}
</style>