feat(app): 应用市场卡片支持右侧详情抽屉

- 卡片主体点击弹出 el-drawer,展示图标/名称/简介/标签与安装管理

- 配置入口仍跳转原独立页面 /app/config

- 操作区阻止冒泡,避免误触抽屉
This commit is contained in:
TermiNexus committed 2026-09-28 17:42:55 +08:00
1 parent 2b13f72045
commit d00451c924
1 file changed
+223 -2
+223 -2
View File
@@ -275,6 +275,31 @@ function openConfig(a: AppItem): void {
router.push({ path: '/app/config', query: { AppUUID: a.AppUUID, AppName: a.AppName } })
}
// ==================== 应用详情抽屉(仿 VSCode 扩展详情:右侧展开) ====================
const detailVisible = ref(false)
/** 当前在抽屉中查看的应用 */
const detailApp = ref<AppItem | null>(null)
/** 点击卡片主体:右侧抽屉展示 icon / 名称 / 简介 / 安装管理 */
function openDetail(a: AppItem): void {
detailApp.value = a
detailVisible.value = true
}
/** 抽屉内「打开应用」:先收抽屉再跳转(页面被 tab 缓存,避免返回时抽屉仍开着) */
function OpenAppFromDetail(): void {
const a = detailApp.value
detailVisible.value = false
if (a) openApp(a)
}
/** 抽屉内「配置」:配置仍走原本的独立页面(/app/config) */
function OpenConfigFromDetail(): void {
const a = detailApp.value
detailVisible.value = false
if (a) openConfig(a)
}
// ==================== 应用-职务绑定(管理员) ====================
const bindDialogVisible = ref(false)
/** 绑定弹窗 Tab:direct=直接职务 / virtual=虚拟职务 */
@@ -326,7 +351,7 @@ onMounted(loadMarket)
</template>
<div v-loading="loading" class="app-grid">
<div v-for="a in market" :key="a.AppUUID" class="app-card">
<div v-for="a in market" :key="a.AppUUID" class="app-card" @click="openDetail(a)">
<div class="app-card-head">
<div class="app-icon">
<el-icon :size="24"><component :is="a.AppIcon || 'Grid'" /></el-icon>
@@ -360,7 +385,8 @@ onMounted(loadMarket)
:class="{ ok: a.ApplyStatus === 2, err: a.ApplyStatus === 3 }"
>{{ statusRowText(a) }}</span>
</div>
<div class="app-foot">
<!-- 操作区:点击按钮不触发卡片详情(@click.stop) -->
<div class="app-foot" @click.stop>
<div class="spacer" />
<!-- 严格职务可见开关(管理员):1=无绑定时全员不可见,必须绑定职务。
强制职务(RequirePositionBinding=1)为应用清单声明,禁用开关 -->
@@ -425,6 +451,133 @@ onMounted(loadMarket)
</div>
</el-card>
<!-- 应用详情抽屉(仿 VSCode 扩展详情:右侧展开,展示 icon / 名称 / 简介 / 安装管理) -->
<el-drawer
v-model="detailVisible"
:title="detailApp?.AppName || '应用详情'"
direction="rtl"
:size="420"
:modal="false"
class="app-detail-drawer"
>
<template v-if="detailApp">
<div class="detail-head">
<div class="app-icon detail-icon">
<el-icon :size="30"><component :is="detailApp.AppIcon || 'Grid'" /></el-icon>
</div>
<div class="detail-titles">
<div class="detail-name">{{ detailApp.AppName }}</div>
<div class="app-code">{{ detailApp.AppCode }}</div>
<div class="detail-tags">
<el-tag :type="typeLabel(detailApp.AppType).type" size="small">
{{ typeLabel(detailApp.AppType).text }}
</el-tag>
<el-tag v-if="detailApp.AuthRequired" size="small" effect="plain">需授权</el-tag>
<el-tag v-if="detailApp.RequirePositionBinding === 1" type="danger" size="small" effect="dark">强制职务</el-tag>
<el-tag v-else-if="detailApp.StrictPositionVisible === 1" type="warning" size="small" effect="dark">严格可见</el-tag>
</div>
</div>
</div>
<div class="detail-section">
<div class="detail-section-title">简介</div>
<p class="detail-desc">{{ detailApp.AppDesc || '暂无描述' }}</p>
</div>
<div v-if="statusRowText(detailApp)" class="detail-section">
<div class="detail-section-title">状态</div>
<span
class="app-status"
:class="{ ok: detailApp.ApplyStatus === 2, err: detailApp.ApplyStatus === 3 }"
>{{ 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">
<el-button v-if="isAppUsable(detailApp)" type="primary" plain @click="OpenAppFromDetail">打开</el-button>
<!-- 企业功能:管理员安装/管理(公司级);普通用户提示 -->
<template v-if="detailApp.AppType === 'enterprise'">
<el-button
v-if="isAdmin && (isAppUsable(detailApp) || hasManageableInstall(detailApp))"
type="info"
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>
</template>
<!-- 个人应用:启用/停用/申请 -->
<template v-else-if="detailApp.AppType === 'personal'">
<el-button
v-if="detailApp.AuthRequired && !detailApp.UserEnabled && !detailApp.ApplyStatus"
type="primary"
@click="applyAuth(detailApp)"
>申请授权</el-button>
<el-button
v-else-if="!detailApp.AuthRequired || detailApp.ApplyStatus === 2"
:type="detailApp.UserEnabled ? 'danger' : 'primary'"
:plain="!!detailApp.UserEnabled"
@click="toggleEnable(detailApp)"
>{{ detailApp.UserEnabled ? '停用' : '启用' }}</el-button>
<el-tag v-else type="info" effect="plain">{{ applyStatusLabel(detailApp.ApplyStatus) }}</el-tag>
</template>
<!-- 混合:管理员可安装/管理,个人可启用 -->
<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>
</template>
<el-button
v-else
:type="detailApp.UserEnabled ? 'danger' : 'primary'"
:plain="!!detailApp.UserEnabled"
@click="toggleEnable(detailApp)"
>{{ detailApp.UserEnabled ? '停用' : '启用' }}</el-button>
</template>
<!-- 严格职务可见(管理员) -->
<el-tooltip
v-if="isAdmin"
:content="detailApp.RequirePositionBinding === 1
? '该应用由清单声明强制要求绑定职务,不可关闭'
: detailApp.StrictPositionVisible === 1
? '关闭严格后,未绑定职务的应用恢复全员可见'
: '开启严格后,未绑定职务的应用全员不可见'"
placement="top"
>
<el-button
:type="detailApp.StrictPositionVisible === 1 ? 'warning' : 'default'"
:plain="detailApp.StrictPositionVisible === 1"
:disabled="detailApp.RequirePositionBinding === 1"
@click="toggleStrictVisible(detailApp)"
>
{{ detailApp.RequirePositionBinding === 1 ? '强制职务' : (detailApp.StrictPositionVisible === 1 ? '关闭严格' : '开启严格') }}
</el-button>
</el-tooltip>
</div>
<p class="detail-tip">应用配置项仍在独立页面中打开(与卡片上的「配置」一致)。</p>
</div>
</template>
</el-drawer>
<!-- 安装弹窗:全租户 / 指定公司 -->
<el-dialog
v-model="installDialogVisible"
@@ -638,6 +791,8 @@ onMounted(loadMarket)
flex-direction: column;
gap: 10px;
transition: box-shadow 0.15s;
// 整卡可点击 → 右侧展开应用详情抽屉
cursor: pointer;
&:hover {
box-shadow: var(--el-box-shadow-light);
}
@@ -822,4 +977,70 @@ onMounted(loadMarket)
.gm-installed-tag {
margin-left: 6px;
}
/* ==================== 应用详情抽屉(仿 VSCode 扩展详情) ==================== */
.app-detail-drawer {
.detail-head {
display: flex;
align-items: flex-start;
gap: 12px;
padding-bottom: 14px;
border-bottom: 1px solid var(--el-border-color-lighter);
}
.detail-icon {
width: 56px;
height: 56px;
}
.detail-titles {
min-width: 0;
flex: 1;
}
.detail-name {
font-size: 16px;
font-weight: 600;
color: var(--el-text-color-primary);
}
.detail-tags {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 8px;
}
.detail-section {
padding: 14px 0;
border-bottom: 1px solid var(--el-border-color-lighter);
&:last-child {
border-bottom: none;
}
}
.detail-section-title {
margin-bottom: 8px;
font-size: 13px;
font-weight: 600;
color: var(--el-text-color-primary);
}
.detail-desc {
margin: 0;
font-size: 13px;
line-height: 1.7;
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-tip {
margin: 10px 0 0;
font-size: 12px;
color: var(--el-text-color-secondary);
}
}
</style>