feat(gerp): 配置页注入「应用订阅方」卡片,订阅应用新增软件管理菜单

- ConfigPanel.vue 重写为可编辑卡片:按租户分组多选提供者/消费者公司(互斥),保存应用级订阅方(初始化引用)
- apps/index.ts:subscription 入口改 /subscription/software,注入卡片改「应用订阅方」
- router:新增 /subscription/software(软件管理,与订阅组管理同页不同入口)
- Group.vue:按路由切换页面标题(软件管理 / 订阅组管理)
This commit is contained in:
TermiNexus committed 2026-10-02 23:12:32 +08:00
1 parent 3010a116c2
commit 30bd077e93
5 files changed
+159 -100

No files matched your search

+22
View File
@@ -45,6 +45,20 @@ export interface SubscriptionGroupPayload {
Consumers: string[]
}
/** 应用级订阅方(应用本身的两方公司,不分具体产品) */
export interface SubscriptionAppParties {
AppUUID: string
Providers: SubscriptionCompanyItem[]
Consumers: SubscriptionCompanyItem[]
}
/** 应用级订阅方保存入参(整体重写) */
export interface SubscriptionAppPartyPayload {
AppUUID: string
Providers: string[]
Consumers: string[]
}
/** 订阅式软件提供 API 客户端 */
export const subscriptionApi = {
/** 订阅组(产品)列表(含提供者/消费者公司) */
@@ -66,4 +80,12 @@ export const subscriptionApi = {
/** 可选公司列表(当前管理员全部可管理公司,含所属租户名) */
companies: (): Promise<{ list: SubscriptionCompanyItem[]; total: number; Scope: string }> =>
get<{ list: SubscriptionCompanyItem[]; total: number; Scope: string }>('/API/Subscription/Companies'),
/** 应用级订阅方(应用配置页注入卡片:提供者/消费者公司,不分产品) */
appParties: (AppUUID: string): Promise<SubscriptionAppParties> =>
get<SubscriptionAppParties>('/API/Subscription/AppParties', { AppUUID }),
/** 保存应用级订阅方(整体重写) */
saveAppParties: (data: SubscriptionAppPartyPayload): Promise<SubscriptionAppParties> =>
post<SubscriptionAppParties>('/API/Subscription/AppParty/Save', data),
}
+5 -6
View File
@@ -165,16 +165,15 @@ const builtin: Record<string, AppFrontendModule> = {
AppCode: 'subscription',
Title: '订阅式软件提供',
Icon: 'Connection',
FrontendRoute: '/subscription/group',
FrontendRoute: '/subscription/software',
FrontendType: 'vue',
LegacySrc: '',
// 注入到「应用配置页」的自定义区块:展示当前范围作为提供者/消费者参与的产品
// 注入到「应用配置页」的自定义卡片:确定本应用自身的提供者/消费者公司(初始化引用)
ConfigPanels: [
{
Key: 'subscription-relation-overview',
Title: '订阅关系概览',
Sub: '当前范围作为提供者 / 消费者参与的产品(按公司隔离)',
CompanyOnly: true,
Key: 'subscription-app-party',
Title: '应用订阅方',
Sub: '确定本应用自身的提供者公司与消费者公司(不分具体产品),作为订阅关系的初始化引用',
Component: () => import('@/views/subscription/ConfigPanel.vue'),
},
],
+6
View File
@@ -451,6 +451,12 @@ export const staticRoutes: RouteRecordRaw[] = [
meta: { title: '绩效管理', icon: 'TrendCharts', requiresAuth: true, menuGuarded: true },
},
// === 订阅式软件提供(订阅组=产品:提供者/消费者公司) ===
{
path: 'subscription/software',
name: 'SubscriptionSoftware',
component: () => import('@/views/subscription/Group.vue'),
meta: { title: '软件管理', icon: 'Management', requiresAuth: true, menuGuarded: true },
},
{
path: 'subscription/group',
name: 'SubscriptionGroup',
+122 -93
View File
@@ -1,20 +1,21 @@
<script setup lang="ts">
/**
* 订阅式软件提供 · 注入到「应用配置页」的自定义区块
* 订阅式软件提供 · 注入到「应用配置页」的自定义卡片
*
* 由前端应用注册表(src/apps/index.ts 的 subscription.ConfigPanels)挂载,
* 配置页把当前范围上下文以 props 传入,本组件只读展示当前公司的订阅关系。
* 由前端应用注册表(src/apps/index.ts 的 subscription.ConfigPanels)挂载。
* 用途:确定**本应用自身**的提供者公司与消费者公司(不分具体产品),
* 作为订阅关系的初始化引用(与「订阅组(产品)」的产品级名单相互独立)。
*/
import { computed, ref, watch } from 'vue'
import { useRouter } from 'vue-router'
import { subscriptionApi, type SubscriptionRelationItem } from '@/api/subscription'
import { computed, onMounted, ref } from 'vue'
import { ElMessage } from 'element-plus'
import { subscriptionApi, type SubscriptionCompanyItem } from '@/api/subscription'
const props = defineProps<{
/** 应用 UUID */
AppUUID: string
/** 应用名 */
AppName: string
/** '' = 租户默认;否则为公司 UUID */
/** '' = 租户默认;否则为公司 UUID(本卡片与范围无关,仅用于文案) */
CompanyUUID: string
/** 配置管理范围 */
Scope: string
@@ -22,23 +23,38 @@ const props = defineProps<{
ScopeLabel: string
}>()
const router = useRouter()
const loading = ref(false)
const relation = ref<SubscriptionRelationItem | null>(null)
const saving = ref(false)
const companies = ref<SubscriptionCompanyItem[]>([])
/** 提供者公司 UUID 列表 */
const providers = ref<string[]>([])
/** 消费者公司 UUID 列表 */
const consumers = ref<string[]>([])
const hasRelation = computed(
() => !!relation.value && (relation.value.ProviderGroups.length > 0 || relation.value.ConsumerGroups.length > 0),
)
/** 公司按所属租户分组(下拉用 el-option-group 展示) */
const companyGroups = computed(() => {
const map = new Map<string, { TenantUUID: string; TenantName: string; Companies: SubscriptionCompanyItem[] }>()
for (const c of companies.value) {
const key = c.TenantUUID || ''
if (!map.has(key)) {
map.set(key, { TenantUUID: key, TenantName: c.TenantName || '未知租户', Companies: [] })
}
map.get(key)!.Companies.push(c)
}
return [...map.values()]
})
async function load(): Promise<void> {
if (!props.CompanyUUID) {
relation.value = null
return
}
if (!props.AppUUID) return
loading.value = true
try {
const r = await subscriptionApi.relations()
relation.value = (r.list || []).find((x) => x.CompanyUUID === props.CompanyUUID) || null
const [c, p] = await Promise.all([
subscriptionApi.companies(),
subscriptionApi.appParties(props.AppUUID),
])
companies.value = c.list || []
providers.value = (p.Providers || []).map((x) => x.CompanyUUID)
consumers.value = (p.Consumers || []).map((x) => x.CompanyUUID)
} catch {
/* 拦截器已提示 */
} finally {
@@ -46,96 +62,109 @@ async function load(): Promise<void> {
}
}
watch(() => props.CompanyUUID, load, { immediate: true })
async function onSave(): Promise<void> {
saving.value = true
try {
await subscriptionApi.saveAppParties({
AppUUID: props.AppUUID,
Providers: providers.value,
Consumers: consumers.value,
})
ElMessage.success('应用订阅方已保存')
await load()
} catch {
/* 拦截器已提示 */
} finally {
saving.value = false
}
}
onMounted(load)
</script>
<template>
<div v-loading="loading" class="sub-cfg-panel">
<div class="sub-cfg-tip">
当前范围:<b>{{ ScopeLabel }}</b>。订阅关系按<b>产品(订阅组)</b>登记,下述为该范围作为「提供者 / 消费者」参与的产品。
<div v-loading="loading" class="sub-party-panel">
<div class="sub-party-tip">
确定<b>本应用自身</b>的提供者公司与消费者公司(不分具体产品),作为订阅关系的<b>初始化引用</b>;
可选范围 = 您有管理权的全部公司。同一家公司不能同时列为两方。
</div>
<el-empty
v-if="!loading && !hasRelation"
description="该范围尚未参与任何订阅组(可在「订阅组管理」里新建产品并指定提供者/消费者)"
:image-size="60"
/>
<div v-else-if="relation" class="sub-cfg-body">
<div class="sub-cfg-row">
<span class="sub-cfg-label">作为提供者</span>
<div class="sub-cfg-tags">
<el-tag
v-for="g in relation.ProviderGroups"
:key="g.SubscriptionGroupUUID"
size="small"
effect="plain"
:type="g.GroupStatus ? 'primary' : 'info'"
>{{ g.GroupName }}</el-tag>
<span v-if="!relation.ProviderGroups.length" class="sub-cfg-empty">—</span>
</div>
</div>
<div class="sub-cfg-row">
<span class="sub-cfg-label">作为消费者</span>
<div class="sub-cfg-tags">
<el-tag
v-for="g in relation.ConsumerGroups"
:key="g.SubscriptionGroupUUID"
size="small"
type="success"
effect="plain"
>{{ g.GroupName }}</el-tag>
<span v-if="!relation.ConsumerGroups.length" class="sub-cfg-empty">—</span>
</div>
</div>
</div>
<div class="sub-cfg-actions">
<el-button size="small" @click="router.push('/subscription/group')">订阅组管理</el-button>
<el-button size="small" type="primary" plain @click="router.push('/subscription/relation')">订阅关系</el-button>
<el-form label-width="96px" label-position="right" class="sub-party-form">
<el-form-item label="提供者公司">
<el-select
v-model="providers"
multiple
filterable
collapse-tags
collapse-tags-tooltip
placeholder="选择一家或多家提供者公司"
class="sub-party-select"
>
<el-option-group
v-for="gp in companyGroups"
:key="gp.TenantUUID"
:label="gp.TenantName || '未知租户'"
>
<el-option
v-for="c in gp.Companies"
:key="c.CompanyUUID"
:label="c.CompanyName"
:value="c.CompanyUUID"
:disabled="consumers.includes(c.CompanyUUID)"
/>
</el-option-group>
</el-select>
</el-form-item>
<el-form-item label="消费者公司">
<el-select
v-model="consumers"
multiple
filterable
collapse-tags
collapse-tags-tooltip
placeholder="选择一家或多家消费者公司"
class="sub-party-select"
>
<el-option-group
v-for="gp in companyGroups"
:key="gp.TenantUUID"
:label="gp.TenantName || '未知租户'"
>
<el-option
v-for="c in gp.Companies"
:key="c.CompanyUUID"
:label="c.CompanyName"
:value="c.CompanyUUID"
:disabled="providers.includes(c.CompanyUUID)"
/>
</el-option-group>
</el-select>
</el-form-item>
</el-form>
<div class="sub-party-actions">
<el-button type="primary" :loading="saving" @click="onSave">保存应用订阅方</el-button>
</div>
</div>
</template>
<style scoped lang="scss">
.sub-cfg-panel {
.sub-party-panel {
min-height: 60px;
}
.sub-cfg-tip {
.sub-party-tip {
font-size: 12px;
color: var(--el-text-color-secondary);
margin-bottom: 10px;
margin-bottom: 12px;
line-height: 1.6;
}
.sub-cfg-body {
.sub-party-form {
max-width: 640px;
}
.sub-party-select {
width: 100%;
}
.sub-party-actions {
display: flex;
flex-direction: column;
gap: 8px;
}
.sub-cfg-row {
display: flex;
align-items: flex-start;
gap: 10px;
}
.sub-cfg-label {
flex-shrink: 0;
width: 76px;
font-size: 13px;
color: var(--el-text-color-regular);
line-height: 24px;
}
.sub-cfg-tags {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.sub-cfg-empty {
font-size: 12px;
color: var(--el-text-color-placeholder);
line-height: 24px;
}
.sub-cfg-actions {
margin-top: 14px;
padding-top: 12px;
border-top: 1px solid var(--el-border-color-lighter);
display: flex;
gap: 8px;
justify-content: flex-end;
max-width: 640px;
}
</style>
+4 -1
View File
@@ -7,6 +7,7 @@
* (跨租户管理员可选任意租户的公司)。
*/
import { computed, onMounted, reactive, ref } from 'vue'
import { useRoute } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { setPageTitle } from '@/utils/serverScope'
import {
@@ -16,7 +17,9 @@ import {
type SubscriptionGroupPayload,
} from '@/api/subscription'
setPageTitle('订阅组管理')
// 同一页面挂两个菜单:软件管理(/subscription/software)与 订阅组管理(/subscription/group)
const route = useRoute()
setPageTitle(route.path.includes('/software') ? '软件管理' : '订阅组管理')
const loading = ref(false)
const saving = ref(false)