feat(app): 应用中心支持公司维度安装——安装弹窗可选全租户/指定公司,停用双停(租户+当前公司)
This commit is contained in:
1 parent
1894786327
commit
91e2c186c7
2 files changed
+172
-21
No files matched your search
+20
-5
@@ -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
@@ -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>
|
||||
Reference in new issue
Block a user