feat(app): 应用市场详情抽屉加遮罩并内联安装管理

- 抽屉改为带遮罩阴影,点击遮罩即关闭;宽度 420 -> 560

- 将「管理」弹窗内容(全租户状态/公司级勾选/保存)内联进抽屉

- 卡片「管理」按钮改为打开抽屉,移除独立管理弹窗

- 打开抽屉预取安装数据,保存后自动刷新勾选与「已装」标记

- 修复 loadMarket 重建列表后 detailApp 引用失效
This commit is contained in:
TermiNexus committed 2026-09-28 17:53:57 +08:00
1 parent efd7f98522
commit cdfd6f827d
1 file changed
+95 -124
+95 -124
View File
@@ -25,6 +25,11 @@ const isAdmin = computed(() => userStore.isAdmin)
const market = ref<AppItem[]>([])
const loading = ref(false)
/** 应用详情抽屉:是否展开 */
const detailVisible = ref(false)
/** 当前在抽屉中查看的应用 */
const detailApp = ref<AppItem | null>(null)
/** 可管理公司列表(仅管理员) */
const manageableCompanies = ref<CompanyScopeItem[]>([])
/** 权限范围:all=跨租户 / tenant=租户内 / company=指定公司 */
@@ -50,6 +55,10 @@ async function loadMarket(): Promise<void> {
try {
const r = await appApi.market()
market.value = r.list
// 抽屉打开时同步指向最新数据(loadMarket 会重建列表对象,旧引用会失效)
if (detailApp.value) {
detailApp.value = r.list.find((x) => x.AppUUID === detailApp.value!.AppUUID) || detailApp.value
}
} catch { /* 拦截器已提示 */ } finally {
loading.value = false
}
@@ -108,34 +117,20 @@ async function onConfirmInstall(): Promise<void> {
}
}
/** 租户管理员:安装/停用企业功能。
* 管理范围内已装(含非本公司)→ 打开管理弹窗(公司级开关);未装 → 打开安装弹窗 */
async function toggleInstall(a: AppItem): Promise<void> {
try {
if (hasManageableInstall(a)) {
await openManageDialog(a)
return
}
await openInstallDialog(a)
} catch { /* 拦截器已提示或取消 */ }
}
// ==================== 管理弹窗(公司级安装/停用) ====================
const manageDialogVisible = ref(false)
const manageDialogLoading = ref(false)
// ==================== 安装管理(公司级安装/停用,内联于应用详情抽屉) ====================
const manageLoading = ref(false)
const manageTargetApp = ref<AppItem | null>(null)
/** 可管理公司列表(含各公司安装状态 Installed) */
const manageCompaniesList = ref<CompanyScopeItem[]>([])
/** 当前勾选(已装)的公司 UUID 列表 */
const manageSelected = ref<string[]>([])
/** 打开管理弹窗:拉取该应用在各可管理公司的安装状态 */
async function openManageDialog(a: AppItem): Promise<void> {
/** 拉取该应用在各可管理公司的安装状态(抽屉内联「安装管理」) */
async function LoadManageData(a: AppItem): Promise<void> {
manageTargetApp.value = a
manageSelected.value = []
manageCompaniesList.value = []
manageDialogVisible.value = true
manageDialogLoading.value = true
manageLoading.value = true
try {
const r = await appApi.manageCompanies(a.AppUUID)
manageCompaniesList.value = r.Companies || []
@@ -144,7 +139,7 @@ async function openManageDialog(a: AppItem): Promise<void> {
.map((c) => c.CompanyUUID)
manageScope.value = r.Scope || ''
} catch { /* 拦截器已提示 */ } finally {
manageDialogLoading.value = false
manageLoading.value = false
}
}
@@ -161,14 +156,15 @@ async function stopTenantInstall(): Promise<void> {
} catch {
return
}
manageDialogLoading.value = true
manageLoading.value = true
try {
await appApi.uninstall(a.AppUUID, [], true) // TenantOnly=仅停用全租户(公司级记录不动)
ElMessage.success('已停用全租户安装')
manageDialogVisible.value = false
await loadMarket()
// 抽屉内联管理:刷新公司级安装状态,保持勾选与「已装」标记一致
if (detailVisible.value && detailApp.value) await LoadManageData(detailApp.value)
} catch { /* 拦截器已提示 */ } finally {
manageDialogLoading.value = false
manageLoading.value = false
}
}
@@ -188,15 +184,16 @@ async function saveCompanyManage(): Promise<void> {
ElMessage.info('公司安装状态未变化')
return
}
manageDialogLoading.value = true
manageLoading.value = true
try {
if (toInstall.length) await appApi.install(a.AppUUID, toInstall)
if (toUninstall.length) await appApi.uninstall(a.AppUUID, toUninstall)
ElMessage.success(`已更新 ${toInstall.length + toUninstall.length} 家公司的安装状态`)
manageDialogVisible.value = false
await loadMarket()
// 抽屉内联管理:刷新公司级安装状态,保持勾选与「已装」标记一致
if (detailVisible.value && detailApp.value) await LoadManageData(detailApp.value)
} catch { /* 拦截器已提示 */ } finally {
manageDialogLoading.value = false
manageLoading.value = false
}
}
@@ -276,14 +273,13 @@ function openConfig(a: AppItem): void {
}
// ==================== 应用详情抽屉(仿 VSCode 扩展详情:右侧展开) ====================
const detailVisible = ref(false)
/** 当前在抽屉中查看的应用 */
const detailApp = ref<AppItem | null>(null)
/** 点击卡片主体:右侧抽屉展示 icon / 名称 / 简介 / 安装管理 */
/** 点击卡片主体:右侧抽屉展示 icon / 名称 / 简介 / 安装管理;管理员打开时预取安装管理数据 */
function openDetail(a: AppItem): void {
detailApp.value = a
detailVisible.value = true
// 管理员且已有安装 → 抽屉内直接内联「安装管理」,打开即预取公司级安装数据
if (isAdmin.value && hasManageableInstall(a)) void LoadManageData(a)
}
/** 抽屉内「打开应用」:先收抽屉再跳转(页面被 tab 缓存,避免返回时抽屉仍开着) */
@@ -414,10 +410,9 @@ onMounted(loadMarket)
<!-- 企业功能:管理员安装/管理(公司级);普通用户提示 -->
<template v-if="a.AppType === 'enterprise'">
<el-button v-if="isAdmin && (isAppUsable(a) || hasManageableInstall(a))" size="small" type="info" plain @click="openConfig(a)">配置</el-button>
<el-button v-if="isAdmin" size="small" type="primary"
:plain="hasManageableInstall(a)" @click="toggleInstall(a)">
{{ hasManageableInstall(a) ? '管理' : '安装' }}
</el-button>
<!-- 已装:打开详情抽屉(安装管理内联于抽屉);未装:打开安装弹窗 -->
<el-button v-if="isAdmin && hasManageableInstall(a)" size="small" type="primary" plain @click="openDetail(a)">管理</el-button>
<el-button v-else-if="isAdmin" size="small" type="primary" @click="openInstallDialog(a)">安装</el-button>
<el-tag v-else size="small" type="info" effect="plain">由企业管理员开通</el-tag>
</template>
<!-- 个人应用:启用/停用/申请 -->
@@ -435,10 +430,9 @@ onMounted(loadMarket)
<template v-else>
<template v-if="isAdmin">
<el-button v-if="isAppUsable(a) || hasManageableInstall(a)" size="small" type="info" plain @click="openConfig(a)">配置</el-button>
<el-button size="small" type="primary"
:plain="hasManageableInstall(a)" @click="toggleInstall(a)">
{{ hasManageableInstall(a) ? '管理' : '安装' }}
</el-button>
<!-- 已装:打开详情抽屉(安装管理内联于抽屉);未装:打开安装弹窗 -->
<el-button v-if="hasManageableInstall(a)" size="small" type="primary" plain @click="openDetail(a)">管理</el-button>
<el-button v-else size="small" type="primary" @click="openInstallDialog(a)">安装</el-button>
</template>
<el-button v-else size="small" :type="a.UserEnabled ? 'danger' : 'primary'"
:plain="!!a.UserEnabled" @click="toggleEnable(a)">
@@ -456,8 +450,8 @@ onMounted(loadMarket)
v-model="detailVisible"
:title="detailApp?.AppName || '应用详情'"
direction="rtl"
:size="420"
:modal="false"
:size="560"
:modal="true"
class="app-detail-drawer"
>
<template v-if="detailApp">
@@ -492,20 +486,6 @@ onMounted(loadMarket)
>{{ statusRowText(detailApp) }}</span>
</div>
<div v-if="isAdmin && detailApp.InstalledCompanies?.length" class="detail-section">
<div class="detail-section-title">管理范围内已装公司</div>
<div class="detail-companies">
<el-tag
v-for="n in detailApp.InstalledCompanies"
:key="n"
size="small"
type="success"
effect="plain"
class="app-installed-tag"
>{{ n }}</el-tag>
</div>
</div>
<div class="detail-section">
<div class="detail-section-title">安装与使用</div>
<div class="detail-actions">
@@ -518,10 +498,13 @@ onMounted(loadMarket)
plain
@click="OpenConfigFromDetail"
>配置</el-button>
<el-button v-if="isAdmin" type="primary" :plain="hasManageableInstall(detailApp)" @click="toggleInstall(detailApp)">
{{ hasManageableInstall(detailApp) ? '管理' : '安装' }}
</el-button>
<el-tag v-else type="info" effect="plain">由企业管理员开通</el-tag>
<!-- 已装:安装管理已内联在下方「安装管理」,无需再点按钮开弹窗 -->
<el-button
v-if="isAdmin && !hasManageableInstall(detailApp)"
type="primary"
@click="openInstallDialog(detailApp)"
>安装</el-button>
<el-tag v-if="!isAdmin" type="info" effect="plain">由企业管理员开通</el-tag>
</template>
<!-- 个人应用:启用/停用/申请 -->
<template v-else-if="detailApp.AppType === 'personal'">
@@ -542,9 +525,8 @@ onMounted(loadMarket)
<template v-else>
<template v-if="isAdmin">
<el-button v-if="isAppUsable(detailApp) || hasManageableInstall(detailApp)" type="info" plain @click="OpenConfigFromDetail">配置</el-button>
<el-button type="primary" :plain="hasManageableInstall(detailApp)" @click="toggleInstall(detailApp)">
{{ hasManageableInstall(detailApp) ? '管理' : '安装' }}
</el-button>
<!-- 已装:安装管理已内联在下方「安装管理」 -->
<el-button v-if="!hasManageableInstall(detailApp)" type="primary" @click="openInstallDialog(detailApp)">安装</el-button>
</template>
<el-button
v-else
@@ -575,6 +557,54 @@ onMounted(loadMarket)
</div>
<p class="detail-tip">应用配置项仍在独立页面中打开(与卡片上的「配置」一致)。</p>
</div>
<!-- 安装管理(管理员 · 已装):直接内联在抽屉中,免去再开弹窗,便于随手管理 -->
<div v-if="isAdmin && hasManageableInstall(detailApp)" class="detail-section">
<div class="detail-section-title">安装管理</div>
<div v-loading="manageLoading">
<!-- 全租户安装状态 -->
<div class="gm-tenant-row">
<el-tag :type="detailApp.TenantInstalled ? 'success' : 'info'" size="small">
{{ detailApp.TenantInstalled ? '全租户已安装(租户全员可用)' : '未全租户安装' }}
</el-tag>
<el-button
v-if="detailApp.TenantInstalled"
size="small"
type="danger"
plain
:loading="manageLoading"
@click="stopTenantInstall"
>停用全租户</el-button>
</div>
<!-- 公司级安装状态 -->
<div class="gm-company-title">公司级安装(勾选=该公司已安装)</div>
<el-empty
v-if="!manageCompaniesList.length && !manageLoading"
description="您没有可管理的公司(或当前无公司权限)"
:image-size="60"
/>
<div v-else class="gm-company-list">
<el-checkbox-group v-model="manageSelected">
<el-checkbox
v-for="c in manageCompaniesList"
:key="c.CompanyUUID"
:value="c.CompanyUUID"
class="gm-company-item"
>
{{ c.CompanyName }}{{ c.CompanyShort ? `(${c.CompanyShort})` : '' }}
<el-tag v-if="c.Installed === 1" size="small" type="success" effect="plain" class="gm-installed-tag">已装</el-tag>
</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>
</div>
<div class="detail-actions detail-manage-actions">
<el-button type="primary" :loading="manageLoading" @click="saveCompanyManage">保存公司状态</el-button>
</div>
</div>
</template>
</el-drawer>
@@ -651,64 +681,6 @@ onMounted(loadMarket)
</template>
</el-dialog>
<!-- 管理弹窗:公司级安装/停用 -->
<el-dialog
v-model="manageDialogVisible"
:title="`管理「${manageTargetApp?.AppName || ''}」`"
width="560px"
:close-on-click-modal="false"
>
<div v-loading="manageDialogLoading">
<!-- 全租户安装状态 -->
<div class="gm-tenant-row">
<div class="gm-tenant-info">
<el-tag :type="manageTargetApp?.TenantInstalled ? 'success' : 'info'" size="small">
{{ manageTargetApp?.TenantInstalled ? '全租户已安装(租户全员可用)' : '未全租户安装' }}
</el-tag>
</div>
<el-button
v-if="manageTargetApp?.TenantInstalled"
size="small"
type="danger"
plain
:loading="manageDialogLoading"
@click="stopTenantInstall"
>
停用全租户
</el-button>
</div>
<!-- 公司级安装状态 -->
<div class="gm-company-title">公司级安装(勾选=该公司已安装)</div>
<el-empty
v-if="!manageCompaniesList.length && !manageDialogLoading"
description="您没有可管理的公司(或当前无公司权限)"
:image-size="60"
/>
<div v-else class="gm-company-list">
<el-checkbox-group v-model="manageSelected">
<el-checkbox
v-for="c in manageCompaniesList"
:key="c.CompanyUUID"
:value="c.CompanyUUID"
class="gm-company-item"
>
{{ c.CompanyName }}{{ c.CompanyShort ? `(${c.CompanyShort})` : '' }}
<el-tag v-if="c.Installed === 1" size="small" type="success" effect="plain" class="gm-installed-tag">已装</el-tag>
</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>
</div>
<template #footer>
<el-button @click="manageDialogVisible = false">关闭</el-button>
<el-button type="primary" :loading="manageDialogLoading" @click="saveCompanyManage">保存公司状态</el-button>
</template>
</el-dialog>
<!-- 应用-职务绑定弹窗:直接职务绑定 / 虚拟职务绑定 -->
<el-dialog v-model="bindDialogVisible" title="应用-职务绑定" width="900px" destroy-on-close append-to-body>
<p class="gs-bind-tip">
@@ -1027,16 +999,15 @@ onMounted(loadMarket)
color: var(--el-text-color-regular);
white-space: pre-wrap;
}
.detail-companies {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.detail-actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
/* 抽屉内联「安装管理」的保存按钮行 */
.detail-manage-actions {
margin-top: 14px;
}
.detail-tip {
margin: 10px 0 0;
font-size: 12px;