diff --git a/src/api/app.ts b/src/api/app.ts index 2138f17..4131536 100644 --- a/src/api/app.ts +++ b/src/api/app.ts @@ -146,6 +146,13 @@ export const appApi = { AppUUID ? { AppUUID } : {}, ), + /** + * 指定公司已启用的应用列表(公司详情页用); + * 权限:管理员 + 该公司在可管理范围内(super=全部 / tenant=本租户 / company=被授权公司) + */ + companyApps: (CompanyUUID: string): Promise<{ list: AppItem[]; total: number }> => + get<{ list: AppItem[]; total: number }>('/API/App/CompanyApps', { CompanyUUID }), + /** 管理员:安装企业功能(不传 CompanyUUIDs=全租户;传 CompanyUUIDs=指定公司); * GrantRoles=RAM 角色粒度授权(可选):指定角色可见该应用(安装范围内) */ install: (AppUUID: string, CompanyUUIDs?: string[], GrantRoles?: string[]): Promise => diff --git a/src/components/CrudFormPanel.vue b/src/components/CrudFormPanel.vue index ab2c2f8..de0105c 100644 --- a/src/components/CrudFormPanel.vue +++ b/src/components/CrudFormPanel.vue @@ -1,5 +1,6 @@ - + diff --git a/src/components/CrudTable.vue b/src/components/CrudTable.vue index e5bd32a..cab98da 100644 --- a/src/components/CrudTable.vue +++ b/src/components/CrudTable.vue @@ -86,6 +86,12 @@ const props = withDefaults(defineProps<{ * (用于 role_scope 非管理员场景,如 Products/Orders 仅管理员可改)。 */ editable?: boolean + /** + * 新增/编辑表单容器形态(默认 'dialog' 居中弹窗): + * 传 'drawer' 时改为右侧抽屉(列表页编辑建议用抽屉)。 + * 注意:表格自身嵌在弹窗里时不要用抽屉(弹窗内再叠抽屉会错位)。 + */ + formMode?: 'dialog' | 'drawer' }>(), { // editable 必须显式给默认值 true:Vue 会把「未传的 Boolean prop」强制转成 false, // 不给默认值时 props.editable 恒为 false,canEdit 判定失败 → 所有未传 editable 的 diff --git a/src/views/app/AppConfigPage.vue b/src/views/app/AppConfigPage.vue index 0e8f5b2..829776b 100644 --- a/src/views/app/AppConfigPage.vue +++ b/src/views/app/AppConfigPage.vue @@ -19,6 +19,8 @@ import { setPageTitle } from '@/utils/serverScope' const route = useRoute() const AppUUID = computed(() => (route.query.AppUUID as string) || '') const AppName = computed(() => (route.query.AppName as string) || '') +/** URL 指定公司(公司详情页「已启用应用 → 配置」入口):优先定位到该公司范围 */ +const QueryCompanyUUID = computed(() => (route.query.CompanyUUID as string) || '') setPageTitle('应用配置') @@ -61,6 +63,12 @@ async function initScope(): Promise { const r = await appApi.manageCompanies() companies.value = r.Companies || [] scope.value = r.Scope || '' + // URL 指定公司(公司详情页进入):在可管理范围内则直接定位到该公司 + const target = QueryCompanyUUID.value + if (target && companies.value.some((c) => c.CompanyUUID === target)) { + currentCompany.value = target + return + } if (scope.value === 'company') { // 公司管理员:默认本公司(第一个可管理公司) currentCompany.value = companies.value[0]?.CompanyUUID || '' diff --git a/src/views/company/Company.vue b/src/views/company/Company.vue index c71c28a..ecdea4c 100644 --- a/src/views/company/Company.vue +++ b/src/views/company/Company.vue @@ -99,6 +99,7 @@ async function handleIconChange(event: Event): Promise { searchable stretch selectable + form-mode="drawer" card-mode card-title-field="CompanyName" card-icon-field="CompanyIcon" diff --git a/src/views/company/CompanyDetail.vue b/src/views/company/CompanyDetail.vue index 9f7d2ba..086e5be 100644 --- a/src/views/company/CompanyDetail.vue +++ b/src/views/company/CompanyDetail.vue @@ -24,12 +24,15 @@ */ import { ref, reactive, computed, onMounted, watch } from 'vue' import { useRoute, useRouter } from 'vue-router' -import { ElMessage } from 'element-plus' +import { ElMessage, ElMessageBox } from 'element-plus' +import { Grid } from '@element-plus/icons-vue' import DetailPage from '@/components/DetailPage.vue' import CrudTable from '@/components/CrudTable.vue' import EntityPicker from '@/components/EntityPicker.vue' import BatchPositionDialog from '@/components/BatchPositionDialog.vue' import { formApi } from '@/api/form' +import { appApi, type AppItem } from '@/api/app' +import { useUserStore } from '@/stores/user' import { upload } from '@/api/request' import { useResolveUUIDs } from '@/composables/useResolveUUIDs' import { setPageTitle } from '@/utils/serverScope' @@ -53,6 +56,7 @@ type RowData = Record const route = useRoute() const router = useRouter() +const userStore = useUserStore() const { resolveUUIDs } = useResolveUUIDs() /** 加载态 */ @@ -250,6 +254,59 @@ async function saveEdit(): Promise { } } +// ===== 已启用应用(公司维度:安装记录 InstallStatus=1)===== +/** + * 仅管理员可见(后端 /API/App/CompanyApps 按可管理范围校验: + * super=全部 / tenant=本租户 / company=被授权公司),非管理员不发请求。 + */ +const companyApps = ref([]) +const appsLoading = ref(false) + +async function loadCompanyApps(): Promise { + if (!userStore.isAdmin || !companyUUID.value) { + companyApps.value = [] + return + } + appsLoading.value = true + try { + const r = await appApi.companyApps(companyUUID.value) + companyApps.value = r.list || [] + } catch { + // 错误提示已由响应拦截器统一弹出(无权/网络异常时按空列表展示) + companyApps.value = [] + } finally { + appsLoading.value = false + } +} + +/** 打开应用配置页:带上 CompanyUUID 让配置页直接定位到本公司范围 */ +function openAppConfig(a: AppItem): void { + router.push({ + path: '/app/config', + query: { AppUUID: a.AppUUID, AppName: a.AppName, CompanyUUID: companyUUID.value }, + }) +} + +/** 卸载:停用本公司的安装记录(租户级安装与其它公司安装不受影响) */ +async function uninstallApp(a: AppItem): Promise { + try { + await ElMessageBox.confirm( + `确认在本公司停用「${a.AppName}」?停用后本公司用户将不可使用该应用(租户级安装与其它公司不受影响)。`, + '确认停用', + { type: 'warning', confirmButtonText: '停用', cancelButtonText: '取消' }, + ) + } catch { + return // 用户取消 + } + try { + await appApi.uninstall(a.AppUUID, [companyUUID.value]) + ElMessage.success('已停用') + await loadCompanyApps() + } catch { + // 错误提示已由响应拦截器统一弹出 + } +} + onMounted(loadCompany) // 同一 tab 复用(v-show 常驻)时 query 变化:重新加载(防展示旧公司数据 / 缺参报错) @@ -257,6 +314,7 @@ onMounted(loadCompany) // 空值场景静默显示空态(loadCompany 内处理),不再弹警告 watch(companyUUID, () => { void loadCompany() + void loadCompanyApps() }, { immediate: true }) @@ -326,6 +384,45 @@ watch(companyUUID, () => { + + + +
+ + +
+
+