feat(app): 应用市场卡片支持右侧详情抽屉
- 卡片主体点击弹出 el-drawer,展示图标/名称/简介/标签与安装管理 - 配置入口仍跳转原独立页面 /app/config - 操作区阻止冒泡,避免误触抽屉
This commit is contained in:
1 parent
2b13f72045
commit
d00451c924
1 file changed
+223
-2
+223
-2
@@ -275,6 +275,31 @@ function openConfig(a: AppItem): void {
|
|||||||
router.push({ path: '/app/config', query: { AppUUID: a.AppUUID, AppName: a.AppName } })
|
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)
|
const bindDialogVisible = ref(false)
|
||||||
/** 绑定弹窗 Tab:direct=直接职务 / virtual=虚拟职务 */
|
/** 绑定弹窗 Tab:direct=直接职务 / virtual=虚拟职务 */
|
||||||
@@ -326,7 +351,7 @@ onMounted(loadMarket)
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div v-loading="loading" class="app-grid">
|
<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-card-head">
|
||||||
<div class="app-icon">
|
<div class="app-icon">
|
||||||
<el-icon :size="24"><component :is="a.AppIcon || 'Grid'" /></el-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 }"
|
:class="{ ok: a.ApplyStatus === 2, err: a.ApplyStatus === 3 }"
|
||||||
>{{ statusRowText(a) }}</span>
|
>{{ statusRowText(a) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="app-foot">
|
<!-- 操作区:点击按钮不触发卡片详情(@click.stop) -->
|
||||||
|
<div class="app-foot" @click.stop>
|
||||||
<div class="spacer" />
|
<div class="spacer" />
|
||||||
<!-- 严格职务可见开关(管理员):1=无绑定时全员不可见,必须绑定职务。
|
<!-- 严格职务可见开关(管理员):1=无绑定时全员不可见,必须绑定职务。
|
||||||
强制职务(RequirePositionBinding=1)为应用清单声明,禁用开关 -->
|
强制职务(RequirePositionBinding=1)为应用清单声明,禁用开关 -->
|
||||||
@@ -425,6 +451,133 @@ onMounted(loadMarket)
|
|||||||
</div>
|
</div>
|
||||||
</el-card>
|
</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
|
<el-dialog
|
||||||
v-model="installDialogVisible"
|
v-model="installDialogVisible"
|
||||||
@@ -638,6 +791,8 @@ onMounted(loadMarket)
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
transition: box-shadow 0.15s;
|
transition: box-shadow 0.15s;
|
||||||
|
// 整卡可点击 → 右侧展开应用详情抽屉
|
||||||
|
cursor: pointer;
|
||||||
&:hover {
|
&:hover {
|
||||||
box-shadow: var(--el-box-shadow-light);
|
box-shadow: var(--el-box-shadow-light);
|
||||||
}
|
}
|
||||||
@@ -822,4 +977,70 @@ onMounted(loadMarket)
|
|||||||
.gm-installed-tag {
|
.gm-installed-tag {
|
||||||
margin-left: 6px;
|
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>
|
</style>
|
||||||
Reference in new issue
Block a user