Files
GERP/src/apps/index.ts
T
TermiNexus 3010a116c2 feat(gerp): 应用可注入自己的配置页/配置区块,配置范围只列已安装公司
- apps/index.ts:注册表新增 ConfigPage(替换通用「配置项」表单)与 ConfigPanels(注入自定义区块),新增 getAppConfigPage/getAppConfigPanels,并登记 subscription 应用
- AppConfigPage.vue:渲染应用注入的配置页/区块(下发 AppUUID/公司/范围上下文);右上角范围下拉只列「该应用已安装 且 有管理权」的公司,全租户安装视为全部公司已装
- subscription:注入「订阅关系概览」区块(按公司展示作为提供者/消费者参与的产品)
2026-10-02 22:35:04 +08:00

210 lines
6.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 前端应用注册表(AppFrontendRegistry)
*
* 与后端应用清单(gvsds/apps/gerp/app_manifests.py)一一对应的前端侧注册:
* 每个应用声明其前端入口(路由/类型/旧页面路径),供"应用中心 · 我的应用"
* 打开应用、及未来动态路由注入使用。
*
* 数据契约(对齐后端 App 表):
* AppCode 应用编码
* Title 应用名
* Icon 图标(Element Plus 图标名 / FontAwesome 类名)
* FrontendRoute 前端路由路径(打开应用跳转目标)
* FrontendType vue=前端组件 / legacy=旧页面 iframe 直载
* LegacySrc legacy 页面原始路径(如 pages/private/CreateE/shop.html)
*
* MSYNC 扩展:未来子节点上报应用清单时,通过 registerRemoteApp() 动态登记,
* 前端可据此动态 addRoute(路由由后端菜单驱动,此处登记即可)。
*/
import type { Component } from 'vue'
import type { RouteLocationRaw } from 'vue-router'
/** 注入到应用配置页的自定义区块(由应用自己声明与提供) */
export interface AppConfigPanel {
/** 区块唯一键(v-for 渲染用) */
Key: string
/** 区块标题 */
Title: string
/** 区块副标题(可选) */
Sub?: string
/** 是否仅在选中「具体公司」时展示(租户默认层无公司上下文时隐藏) */
CompanyOnly?: boolean
/** 区块组件(异步加载) */
Component: () => Promise<{ default: Component }>
}
/** 配置页注入上下文:传给应用自定义配置页 / 自定义区块 */
export interface AppConfigContext {
AppUUID: string
AppName: string
/** '' = 租户默认(全租户共享);否则为公司 UUID */
CompanyUUID: string
/** 配置管理范围:all(跨租户)/ tenant(租户内)/ company(指定公司) */
Scope: string
/** 当前范围展示名(用于区块内文案) */
ScopeLabel: string
}
export interface AppFrontendModule {
AppCode: string
Title: string
Icon: string
FrontendRoute: string
FrontendType: 'vue' | 'legacy'
LegacySrc: string
/** 远端注入(MSYNC):提供方服务名(打开远端 legacy 页面时签发用户令牌用) */
ServiceName?: string
/** 应用自定义配置页:存在时替换配置页的通用「配置项」表单 */
ConfigPage?: () => Promise<{ default: Component }>
/** 应用注入配置页的自定义区块:渲染在「配置项」区块之后 */
ConfigPanels?: AppConfigPanel[]
}
/** 打开应用的跳转目标(vue 走前端路由;legacy 走对应 LegacyPage 路由) */
export function appOpenTarget(mod: AppFrontendModule): RouteLocationRaw {
// 远端 legacy(绝对 URL,子服务器托管):走 /remote/:code,带 src+svc 供令牌透传
if (mod.FrontendType === 'legacy' && /^https?:\/\//.test(mod.LegacySrc)) {
return {
path: `/remote/${encodeURIComponent(mod.AppCode)}`,
query: { src: mod.LegacySrc, svc: mod.ServiceName || '' },
}
}
return mod.FrontendRoute || (mod.LegacySrc ? `/${mod.LegacySrc}` : '/app/mine')
}
const builtin: Record<string, AppFrontendModule> = {
mailbox: {
AppCode: 'mailbox',
Title: '企业邮箱',
Icon: 'Message',
FrontendRoute: '/mail/mailbox',
FrontendType: 'vue',
LegacySrc: '',
},
gitea: {
AppCode: 'gitea',
Title: '代码托管 Gitea',
Icon: 'Connection',
FrontendRoute: '/gitea/mine',
FrontendType: 'vue',
LegacySrc: '',
},
talk: {
AppCode: 'talk',
Title: 'Talk 企业通讯',
Icon: 'ChatDotRound',
FrontendRoute: '/talk',
FrontendType: 'vue',
LegacySrc: '',
},
shop: {
AppCode: 'shop',
Title: '枢机协律-商店',
Icon: 'ShoppingCart',
FrontendRoute: '/private/CreateE/shop',
FrontendType: 'legacy',
LegacySrc: 'pages/private/CreateE/shop.html',
},
'store-value': {
AppCode: 'store-value',
Title: '枢机协律-储值',
Icon: 'Wallet',
FrontendRoute: '/private/CreateE/StoreValue',
FrontendType: 'legacy',
LegacySrc: 'pages/private/CreateE/StoreValue.html',
},
wallet: {
AppCode: 'wallet',
Title: '枢机协律-支付系统',
Icon: 'Wallet',
FrontendRoute: '/private/CreateE/wallet',
FrontendType: 'legacy',
LegacySrc: 'pages/private/CreateE/wallet.html',
},
enterprise: {
AppCode: 'enterprise',
Title: '枢机协律-模拟企业',
Icon: 'OfficeBuilding',
FrontendRoute: '/private/CreateE/enterprise',
FrontendType: 'legacy',
LegacySrc: 'pages/private/CreateE/enterprise.html',
},
'dingcalc-finance': {
AppCode: 'dingcalc-finance',
Title: '鼎算财务',
Icon: 'Money',
FrontendRoute: '/finance/voucher',
FrontendType: 'vue',
LegacySrc: '',
},
'product-order': {
AppCode: 'product-order',
Title: '产品与订单',
Icon: 'Goods',
FrontendRoute: '/inventory/products',
FrontendType: 'vue',
LegacySrc: '',
},
ehrm: {
AppCode: 'ehrm',
Title: '人力资源管理扩展',
Icon: 'UserFilled',
FrontendRoute: '/ehrm/profile',
FrontendType: 'vue',
LegacySrc: '',
},
oa: {
AppCode: 'oa',
Title: '办公自动化流程',
Icon: 'Files',
FrontendRoute: '/oa/leave',
FrontendType: 'vue',
LegacySrc: '',
},
subscription: {
AppCode: 'subscription',
Title: '订阅式软件提供',
Icon: 'Connection',
FrontendRoute: '/subscription/group',
FrontendType: 'vue',
LegacySrc: '',
// 注入到「应用配置页」的自定义区块:展示当前范围作为提供者/消费者参与的产品
ConfigPanels: [
{
Key: 'subscription-relation-overview',
Title: '订阅关系概览',
Sub: '当前范围作为提供者 / 消费者参与的产品(按公司隔离)',
CompanyOnly: true,
Component: () => import('@/views/subscription/ConfigPanel.vue'),
},
],
},
}
/** 远端注册表(MSYNC 上报的应用,本地内置同名优先) */
const remote: Record<string, AppFrontendModule> = {}
/** 注册 MSYNC 远端应用模块(AppCode 冲突时远端生效,覆盖内置) */
export function registerRemoteApp(mod: AppFrontendModule): void {
remote[mod.AppCode] = mod
}
export function getAppModule(AppCode: string): AppFrontendModule | undefined {
return remote[AppCode] || builtin[AppCode]
}
export function listAppModules(): AppFrontendModule[] {
const merged = { ...remote, ...builtin }
return Object.values(merged)
}
/** 应用自定义配置页(未声明返回 undefined → 配置页保留通用「配置项」表单) */
export function getAppConfigPage(AppCode: string): (() => Promise<{ default: Component }>) | undefined {
return getAppModule(AppCode)?.ConfigPage
}
/** 应用注入到配置页的自定义区块(未声明返回空数组) */
export function getAppConfigPanels(AppCode: string): AppConfigPanel[] {
return getAppModule(AppCode)?.ConfigPanels || []
}