feat(app): 应用中心支持公司维度安装——安装弹窗可选全租户/指定公司,停用双停(租户+当前公司)

This commit is contained in:
TermiNexus committed 2026-08-13 06:28:27 +08:00
1 parent 1894786327
commit 91e2c186c7
2 files changed
+172 -21

No files matched your search

+20 -5
View File
@@ -14,10 +14,19 @@ export interface AppItem {
/** 市场页附加状态 */
TenantInstalled?: number
TenantAuth?: number
/** 指定公司安装状态(当前公司维度) */
CompanyInstalled?: number
UserEnabled?: number
ApplyStatus?: number
}
/** 管理员可安装应用的公司范围项 */
export interface CompanyScopeItem {
CompanyUUID: string
CompanyName: string
CompanyShort: string
}
/** 个人授权申请项(租户管理员审批台) */
export interface PendingItem {
UserAppUUID: string
@@ -31,17 +40,23 @@ export interface PendingItem {
/** 应用/功能 API 客户端 */
export const appApi = {
/** 应用市场列表(附租户安装/个人启用/申请状态) */
/** 应用市场列表(附租户/公司安装/个人启用/申请状态) */
market: (): Promise<{ list: AppItem[]; total: number }> => get<{ list: AppItem[]; total: number }>('/API/App/Market'),
/** 我的应用(当前用户可用) */
myApps: (): Promise<{ list: AppItem[]; total: number }> => get<{ list: AppItem[]; total: number }>('/API/App/MyApps'),
/** 租户管理员:安装企业功能 */
install: (AppUUID: string): Promise<AppItem> => post<AppItem>('/API/App/Install', { AppUUID }),
/** 管理员可安装应用的公司列表(按权限范围) */
manageCompanies: (): Promise<{ Scope: string; Companies: CompanyScopeItem[] }> =>
get<{ Scope: string; Companies: CompanyScopeItem[] }>('/API/App/ManageCompanies'),
/** 租户管理员:停用企业功能 */
uninstall: (AppUUID: string): Promise<void> => post<void>('/API/App/Uninstall', { AppUUID }),
/** 管理员:安装企业功能(不传 CompanyUUIDs=全租户;传 CompanyUUIDs=指定公司) */
install: (AppUUID: string, CompanyUUIDs?: string[]): Promise<AppItem> =>
post<AppItem>('/API/App/Install', { AppUUID, CompanyUUIDs: CompanyUUIDs || [] }),
/** 管理员:停用企业功能(不传 CompanyUUIDs=停用全租户;传 CompanyUUIDs=停用指定公司) */
uninstall: (AppUUID: string, CompanyUUIDs?: string[]): Promise<void> =>
post<void>('/API/App/Uninstall', { AppUUID, CompanyUUIDs: CompanyUUIDs || [] }),
/** 个人:启用应用 */
enable: (AppUUID: string): Promise<AppItem> => post<AppItem>('/API/App/Enable', { AppUUID }),
+152 -16
View File
@@ -7,9 +7,9 @@
* 接口:GET/POST /API/App/*
*/
import { ref, computed, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Grid } from '@element-plus/icons-vue'
import { appApi, type AppItem } from '@/api/app'
import { appApi, type AppItem, type CompanyScopeItem } from '@/api/app'
import { useUserStore } from '@/stores/user'
import { setPageTitle } from '@/utils/serverScope'
@@ -21,6 +21,11 @@ const isAdmin = computed(() => userStore.isAdmin)
const market = ref<AppItem[]>([])
const loading = ref(false)
/** 可管理公司列表(仅管理员) */
const manageableCompanies = ref<CompanyScopeItem[]>([])
/** 权限范围:all=跨租户 / tenant=租户内 / company=指定公司 */
const manageScope = ref('')
function typeLabel(t: string): { text: string; type: 'success' | 'info' | 'warning' } {
if (t === 'enterprise') return { text: '企业功能', type: 'success' }
if (t === 'personal') return { text: '个人应用', type: 'info' }
@@ -44,18 +49,69 @@ async function loadMarket(): Promise<void> {
}
}
// ==================== 安装弹窗(全租户 / 指定公司) ====================
const installDialogVisible = ref(false)
const installDialogLoading = ref(false)
const installTargetApp = ref<AppItem | null>(null)
/** 安装粒度:tenant=全租户 / company=指定公司 */
const installScope = ref<'tenant' | 'company'>('tenant')
/** 选中的公司 UUID 列表 */
const selectedCompanies = ref<string[]>([])
/** 打开安装弹窗:拉取可管理公司 */
async function openInstallDialog(a: AppItem): Promise<void> {
installTargetApp.value = a
installScope.value = 'tenant'
selectedCompanies.value = []
installDialogVisible.value = true
if (isAdmin.value && !manageableCompanies.value.length) {
installDialogLoading.value = true
try {
const r = await appApi.manageCompanies()
manageableCompanies.value = r.Companies || []
manageScope.value = r.Scope || ''
} catch { /* 拦截器已提示 */ } finally {
installDialogLoading.value = false
}
}
}
/** 确认安装 */
async function onConfirmInstall(): Promise<void> {
const a = installTargetApp.value
if (!a) return
installDialogLoading.value = true
try {
const companyUUIDs = installScope.value === 'company' ? selectedCompanies.value : []
await appApi.install(a.AppUUID, companyUUIDs)
const scopeText = installScope.value === 'company'
? `已为 ${selectedCompanies.value.length} 家公司安装`
: '已全租户安装(全员可用)'
ElMessage.success(`「${a.AppName}」${scopeText}`)
installDialogVisible.value = false
await loadMarket()
} catch { /* 拦截器已提示 */ } finally {
installDialogLoading.value = false
}
}
/** 租户管理员:安装/停用企业功能 */
async function toggleInstall(a: AppItem): Promise<void> {
try {
if (a.TenantInstalled) {
await appApi.uninstall(a.AppUUID)
// 已安装(租户或当前公司)→ 直接停用;未安装 → 打开安装弹窗
if (a.TenantInstalled || a.CompanyInstalled) {
await ElMessageBox.confirm(
`确定停用「${a.AppName}」吗?\n${a.TenantInstalled ? '将停用全租户安装' : '将停用当前公司安装'}`,
'停用功能',
{ type: 'warning', confirmButtonText: '停用', cancelButtonText: '取消' },
)
await appApi.uninstall(a.AppUUID, a.TenantInstalled ? undefined : [])
ElMessage.success(`已停用「${a.AppName}」`)
} else {
await appApi.install(a.AppUUID)
ElMessage.success(`已安装「${a.AppName}」,全员可用`)
await loadMarket()
return
}
await loadMarket()
} catch { /* 拦截器已提示 */ }
await openInstallDialog(a)
} catch { /* 拦截器已提示或取消 */ }
}
/** 个人:启用/停用应用 */
@@ -84,7 +140,8 @@ async function applyAuth(a: AppItem): Promise<void> {
/** 市场卡片操作按钮(按类型 + 管理员/个人视角返回描述文案) */
function marketActionDesc(a: AppItem): string {
if (a.AuthRequired && a.ApplyStatus === 1) return '授权申请中'
if (a.TenantInstalled) return '企业已安装'
if (a.TenantInstalled) return '企业已安装(全租户)'
if (a.CompanyInstalled) return '企业已安装(本公司)'
if (a.UserEnabled) return '个人已启用'
return ''
}
@@ -123,9 +180,9 @@ onMounted(loadMarket)
<div class="spacer" />
<!-- 企业功能:管理员安装/停用;普通用户提示 -->
<template v-if="a.AppType === 'enterprise'">
<el-button v-if="isAdmin" size="small" :type="a.TenantInstalled ? 'danger' : 'primary'"
:plain="!!a.TenantInstalled" @click="toggleInstall(a)">
{{ a.TenantInstalled ? '停用' : '安装' }}
<el-button v-if="isAdmin" size="small" :type="a.TenantInstalled || a.CompanyInstalled ? 'danger' : 'primary'"
:plain="!!(a.TenantInstalled || a.CompanyInstalled)" @click="toggleInstall(a)">
{{ a.TenantInstalled || a.CompanyInstalled ? '停用' : '安装' }}
</el-button>
<el-tag v-else size="small" type="info" effect="plain">由企业管理员开通</el-tag>
</template>
@@ -143,9 +200,9 @@ onMounted(loadMarket)
<!-- 混合:管理员可安装,个人可启用 -->
<template v-else>
<template v-if="isAdmin">
<el-button size="small" :type="a.TenantInstalled ? 'danger' : 'primary'"
:plain="!!a.TenantInstalled" @click="toggleInstall(a)">
{{ a.TenantInstalled ? '停用' : '安装' }}
<el-button size="small" :type="a.TenantInstalled || a.CompanyInstalled ? 'danger' : 'primary'"
:plain="!!(a.TenantInstalled || a.CompanyInstalled)" @click="toggleInstall(a)">
{{ a.TenantInstalled || a.CompanyInstalled ? '停用' : '安装' }}
</el-button>
</template>
<el-button v-else size="small" :type="a.UserEnabled ? 'danger' : 'primary'"
@@ -158,6 +215,56 @@ onMounted(loadMarket)
<el-empty v-if="!loading && !market.length" description="暂无可用的应用" :image-size="80" />
</div>
</el-card>
<!-- 安装弹窗:全租户 / 指定公司 -->
<el-dialog
v-model="installDialogVisible"
:title="`安装「${installTargetApp?.AppName || ''}」`"
width="520px"
:close-on-click-modal="false"
>
<div v-loading="installDialogLoading">
<el-radio-group v-model="installScope" class="gs-scope-group">
<el-radio value="tenant">全租户安装(租户全员可用)</el-radio>
<el-radio value="company">指定公司安装</el-radio>
</el-radio-group>
<template v-if="installScope === 'company'">
<el-empty
v-if="!manageableCompanies.length && !installDialogLoading"
description="您没有可管理的公司(或当前无公司权限)"
:image-size="60"
/>
<div v-else class="gs-company-list">
<el-checkbox-group v-model="selectedCompanies">
<el-checkbox
v-for="c in manageableCompanies"
:key="c.CompanyUUID"
:value="c.CompanyUUID"
class="gs-company-item"
>
{{ c.CompanyName }}{{ c.CompanyShort ? `(${c.CompanyShort})` : '' }}
</el-checkbox>
</el-checkbox-group>
</div>
<p v-if="manageScope === 'company'" class="gs-scope-tip">您仅有指定公司的管理权限,只能为授权公司安装。</p>
<p v-else-if="manageScope === 'tenant'" class="gs-scope-tip">您可管理本租户下全部公司。</p>
<p v-else-if="manageScope === 'all'" class="gs-scope-tip">您是跨租户管理员,可管理全部公司。</p>
</template>
</div>
<template #footer>
<el-button @click="installDialogVisible = false">取消</el-button>
<el-button
type="primary"
:loading="installDialogLoading"
:disabled="installScope === 'company' && !selectedCompanies.length"
@click="onConfirmInstall"
>
确认安装
</el-button>
</template>
</el-dialog>
</div>
</template>
@@ -259,4 +366,33 @@ onMounted(loadMarket)
color: var(--el-color-danger);
}
}
/* 安装弹窗 */
.gs-scope-group {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 10px;
margin-bottom: 14px;
}
.gs-company-list {
max-height: 260px;
overflow-y: auto;
border: 1px solid var(--el-border-color-lighter);
border-radius: 8px;
padding: 10px 14px;
}
.gs-company-item {
display: flex;
width: 100%;
margin-right: 0;
margin-bottom: 8px;
&:last-child {
margin-bottom: 0;
}
}
.gs-scope-tip {
font-size: 12px;
color: var(--el-text-color-secondary);
margin: 10px 0 0;
}
</style>