refactor(app): 应用中心改父级菜单——拆分 应用市场/我的应用/授权审批 三个子页面
This commit is contained in:
1 parent
ca618aa512
commit
b15d3fe89d
4 files changed
+603
-4
No files matched your search
+17
-4
@@ -229,11 +229,24 @@ export const staticRoutes: RouteRecordRaw[] = [
|
|||||||
component: () => import('@/views/company/EpCreate.vue'),
|
component: () => import('@/views/company/EpCreate.vue'),
|
||||||
meta: { title: '企业创建向导(调试)', requiresAuth: true },
|
meta: { title: '企业创建向导(调试)', requiresAuth: true },
|
||||||
},
|
},
|
||||||
|
// === 应用中心(父级菜单,子页面) ===
|
||||||
{
|
{
|
||||||
path: 'app/center',
|
path: 'app/market',
|
||||||
name: 'AppCenter',
|
name: 'AppMarket',
|
||||||
component: () => import('@/views/app/AppCenter.vue'),
|
component: () => import('@/views/app/AppMarket.vue'),
|
||||||
meta: { title: '应用中心', icon: 'Grid', requiresAuth: true, menuGuarded: true },
|
meta: { title: '应用市场', icon: 'Grid', requiresAuth: true, menuGuarded: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'app/mine',
|
||||||
|
name: 'AppMine',
|
||||||
|
component: () => import('@/views/app/AppMine.vue'),
|
||||||
|
meta: { title: '我的应用', icon: 'Files', requiresAuth: true, menuGuarded: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'app/pending',
|
||||||
|
name: 'AppPending',
|
||||||
|
component: () => import('@/views/app/AppPending.vue'),
|
||||||
|
meta: { title: '授权审批', icon: 'Stamp', requiresAuth: true, menuGuarded: true },
|
||||||
},
|
},
|
||||||
// === 阶段 6:独立工具 ===
|
// === 阶段 6:独立工具 ===
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,262 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* 应用中心 · 应用市场
|
||||||
|
*
|
||||||
|
* 全部应用,标注类型(企业功能/个人应用/混合)与安装/启用状态。
|
||||||
|
* 租户管理员可安装/停用企业功能;个人可启用个人应用、申请企业授权。
|
||||||
|
* 接口:GET/POST /API/App/*
|
||||||
|
*/
|
||||||
|
import { ref, computed, onMounted } from 'vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { Grid } from '@element-plus/icons-vue'
|
||||||
|
import { appApi, type AppItem } from '@/api/app'
|
||||||
|
import { useUserStore } from '@/stores/user'
|
||||||
|
import { setPageTitle } from '@/utils/serverScope'
|
||||||
|
|
||||||
|
setPageTitle('应用市场')
|
||||||
|
|
||||||
|
const userStore = useUserStore()
|
||||||
|
const isAdmin = computed(() => userStore.isAdmin)
|
||||||
|
|
||||||
|
const market = ref<AppItem[]>([])
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
function typeLabel(t: string): { text: string; type: 'success' | 'info' | 'warning' } {
|
||||||
|
if (t === 'enterprise') return { text: '企业功能', type: 'success' }
|
||||||
|
if (t === 'personal') return { text: '个人应用', type: 'info' }
|
||||||
|
return { text: '混合', type: 'warning' }
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyStatusLabel(s: number | undefined): string {
|
||||||
|
if (s === 1) return '授权申请中'
|
||||||
|
if (s === 2) return '已授权'
|
||||||
|
if (s === 3) return '已拒绝'
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadMarket(): Promise<void> {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const r = await appApi.market()
|
||||||
|
market.value = r.list
|
||||||
|
} catch { /* 拦截器已提示 */ } finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 租户管理员:安装/停用企业功能 */
|
||||||
|
async function toggleInstall(a: AppItem): Promise<void> {
|
||||||
|
try {
|
||||||
|
if (a.TenantInstalled) {
|
||||||
|
await appApi.uninstall(a.AppUUID)
|
||||||
|
ElMessage.success(`已停用「${a.AppName}」`)
|
||||||
|
} else {
|
||||||
|
await appApi.install(a.AppUUID)
|
||||||
|
ElMessage.success(`已安装「${a.AppName}」,全员可用`)
|
||||||
|
}
|
||||||
|
await loadMarket()
|
||||||
|
} catch { /* 拦截器已提示 */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 个人:启用/停用应用 */
|
||||||
|
async function toggleEnable(a: AppItem): Promise<void> {
|
||||||
|
try {
|
||||||
|
if (a.UserEnabled) {
|
||||||
|
await appApi.disable(a.AppUUID)
|
||||||
|
ElMessage.success('已停用')
|
||||||
|
} else {
|
||||||
|
await appApi.enable(a.AppUUID)
|
||||||
|
ElMessage.success('已启用')
|
||||||
|
}
|
||||||
|
await loadMarket()
|
||||||
|
} catch { /* 拦截器已提示 */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 个人:申请企业授权 */
|
||||||
|
async function applyAuth(a: AppItem): Promise<void> {
|
||||||
|
try {
|
||||||
|
await appApi.apply(a.AppUUID)
|
||||||
|
ElMessage.success(`已提交「${a.AppName}」授权申请,等待租户管理员审批`)
|
||||||
|
await loadMarket()
|
||||||
|
} catch { /* 拦截器已提示 */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 市场卡片操作按钮(按类型 + 管理员/个人视角返回描述文案) */
|
||||||
|
function marketActionDesc(a: AppItem): string {
|
||||||
|
if (a.AuthRequired && a.ApplyStatus === 1) return '授权申请中'
|
||||||
|
if (a.TenantInstalled) return '企业已安装'
|
||||||
|
if (a.UserEnabled) return '个人已启用'
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(loadMarket)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="app-market-page">
|
||||||
|
<el-card shadow="never" class="app-card-wrap">
|
||||||
|
<template #header>
|
||||||
|
<div class="app-header">
|
||||||
|
<el-icon class="header-icon"><Grid /></el-icon>
|
||||||
|
<span>应用市场</span>
|
||||||
|
<span class="header-sub">企业功能 · 个人应用 · 企业授权</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div v-loading="loading" class="app-grid">
|
||||||
|
<div v-for="a in market" :key="a.AppUUID" class="app-card">
|
||||||
|
<div class="app-card-head">
|
||||||
|
<div class="app-icon">
|
||||||
|
<el-icon :size="24"><component :is="a.AppIcon || 'Grid'" /></el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="app-titles">
|
||||||
|
<div class="app-name">{{ a.AppName }}</div>
|
||||||
|
<div class="app-code">{{ a.AppCode }}</div>
|
||||||
|
</div>
|
||||||
|
<el-tag :type="typeLabel(a.AppType).type" size="small">{{ typeLabel(a.AppType).text }}</el-tag>
|
||||||
|
</div>
|
||||||
|
<div class="app-desc">{{ a.AppDesc || '暂无描述' }}</div>
|
||||||
|
<div class="app-foot">
|
||||||
|
<span v-if="marketActionDesc(a)" class="app-status">{{ marketActionDesc(a) }}</span>
|
||||||
|
<span v-else-if="a.ApplyStatus === 2" class="app-status ok">已授权</span>
|
||||||
|
<span v-else-if="a.ApplyStatus === 3" class="app-status err">已拒绝</span>
|
||||||
|
<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>
|
||||||
|
<el-tag v-else size="small" type="info" effect="plain">由企业管理员开通</el-tag>
|
||||||
|
</template>
|
||||||
|
<!-- 个人应用:启用/停用/申请 -->
|
||||||
|
<template v-else-if="a.AppType === 'personal'">
|
||||||
|
<el-button v-if="a.AuthRequired && !a.UserEnabled && !a.ApplyStatus" size="small" type="primary"
|
||||||
|
@click="applyAuth(a)">申请授权</el-button>
|
||||||
|
<el-button v-else-if="!a.AuthRequired || a.ApplyStatus === 2" size="small"
|
||||||
|
:type="a.UserEnabled ? 'danger' : 'primary'" :plain="!!a.UserEnabled"
|
||||||
|
@click="toggleEnable(a)">
|
||||||
|
{{ a.UserEnabled ? '停用' : '启用' }}
|
||||||
|
</el-button>
|
||||||
|
<el-tag v-else size="small" type="info" effect="plain">{{ applyStatusLabel(a.ApplyStatus) }}</el-tag>
|
||||||
|
</template>
|
||||||
|
<!-- 混合:管理员可安装,个人可启用 -->
|
||||||
|
<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>
|
||||||
|
</template>
|
||||||
|
<el-button v-else size="small" :type="a.UserEnabled ? 'danger' : 'primary'"
|
||||||
|
:plain="!!a.UserEnabled" @click="toggleEnable(a)">
|
||||||
|
{{ a.UserEnabled ? '停用' : '启用' }}
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<el-empty v-if="!loading && !market.length" description="暂无可用的应用" :image-size="80" />
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.app-market-page {
|
||||||
|
padding: 16px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.app-card-wrap {
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
.app-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 15px;
|
||||||
|
.header-icon {
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
}
|
||||||
|
.header-sub {
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.app-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||||
|
gap: 14px;
|
||||||
|
min-height: 120px;
|
||||||
|
}
|
||||||
|
.app-card {
|
||||||
|
border: 1px solid var(--el-border-color-lighter);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 14px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
transition: box-shadow 0.15s;
|
||||||
|
&:hover {
|
||||||
|
box-shadow: var(--el-box-shadow-light);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.app-card-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.app-icon {
|
||||||
|
width: 42px;
|
||||||
|
height: 42px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
border-radius: 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: linear-gradient(135deg, var(--el-color-primary-light-8), var(--el-color-primary-light-7));
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
}
|
||||||
|
.app-titles {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.app-name {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.app-code {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--el-text-color-placeholder);
|
||||||
|
}
|
||||||
|
.app-desc {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
min-height: 36px;
|
||||||
|
}
|
||||||
|
.app-foot {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.spacer {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.app-status {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
&.ok {
|
||||||
|
color: var(--el-color-success);
|
||||||
|
}
|
||||||
|
&.err {
|
||||||
|
color: var(--el-color-danger);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* 应用中心 · 我的应用
|
||||||
|
*
|
||||||
|
* 当前用户可用应用(租户已装的企业功能 + 个人已启用的应用)。
|
||||||
|
* 接口:GET/POST /API/App/*
|
||||||
|
*/
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { Grid } from '@element-plus/icons-vue'
|
||||||
|
import { appApi, type AppItem } from '@/api/app'
|
||||||
|
import { setPageTitle } from '@/utils/serverScope'
|
||||||
|
|
||||||
|
setPageTitle('我的应用')
|
||||||
|
|
||||||
|
const myApps = ref<AppItem[]>([])
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
function typeLabel(t: string): { text: string; type: 'success' | 'info' | 'warning' } {
|
||||||
|
if (t === 'enterprise') return { text: '企业功能', type: 'success' }
|
||||||
|
if (t === 'personal') return { text: '个人应用', type: 'info' }
|
||||||
|
return { text: '混合', type: 'warning' }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadMyApps(): Promise<void> {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const r = await appApi.myApps()
|
||||||
|
myApps.value = r.list
|
||||||
|
} catch { /* 拦截器已提示 */ } finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 个人:停用应用(企业功能不可停用) */
|
||||||
|
async function toggleDisable(a: AppItem): Promise<void> {
|
||||||
|
try {
|
||||||
|
await appApi.disable(a.AppUUID)
|
||||||
|
ElMessage.success('已停用')
|
||||||
|
await loadMyApps()
|
||||||
|
} catch { /* 拦截器已提示 */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(loadMyApps)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="app-mine-page">
|
||||||
|
<el-card shadow="never" class="app-card-wrap">
|
||||||
|
<template #header>
|
||||||
|
<div class="app-header">
|
||||||
|
<el-icon class="header-icon"><Grid /></el-icon>
|
||||||
|
<span>我的应用</span>
|
||||||
|
<span class="header-sub">企业功能(租户开通)· 个人应用(个人启用)</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div v-loading="loading" class="app-grid">
|
||||||
|
<div v-for="a in myApps" :key="a.AppUUID" class="app-card">
|
||||||
|
<div class="app-card-head">
|
||||||
|
<div class="app-icon">
|
||||||
|
<el-icon :size="24"><component :is="a.AppIcon || 'Grid'" /></el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="app-titles">
|
||||||
|
<div class="app-name">{{ a.AppName }}</div>
|
||||||
|
<div class="app-code">{{ a.AppCode }}</div>
|
||||||
|
</div>
|
||||||
|
<el-tag :type="typeLabel(a.AppType).type" size="small">{{ typeLabel(a.AppType).text }}</el-tag>
|
||||||
|
</div>
|
||||||
|
<div class="app-desc">{{ a.AppDesc || '暂无描述' }}</div>
|
||||||
|
<div class="app-foot">
|
||||||
|
<span class="app-status ok">{{ a.TenantInstalled && a.AppType !== 'personal' ? '企业已开通' : '个人已启用' }}</span>
|
||||||
|
<div class="spacer" />
|
||||||
|
<el-button v-if="a.AppType !== 'enterprise'" size="small" type="danger" plain @click="toggleDisable(a)">停用</el-button>
|
||||||
|
<el-tag v-else size="small" type="success" effect="plain">企业功能</el-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<el-empty v-if="!loading && !myApps.length" description="暂无可用应用,请到应用市场启用" :image-size="80" />
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.app-mine-page {
|
||||||
|
padding: 16px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.app-card-wrap {
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
.app-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 15px;
|
||||||
|
.header-icon {
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
}
|
||||||
|
.header-sub {
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.app-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||||
|
gap: 14px;
|
||||||
|
min-height: 120px;
|
||||||
|
}
|
||||||
|
.app-card {
|
||||||
|
border: 1px solid var(--el-border-color-lighter);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 14px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
transition: box-shadow 0.15s;
|
||||||
|
&:hover {
|
||||||
|
box-shadow: var(--el-box-shadow-light);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.app-card-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.app-icon {
|
||||||
|
width: 42px;
|
||||||
|
height: 42px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
border-radius: 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: linear-gradient(135deg, var(--el-color-primary-light-8), var(--el-color-primary-light-7));
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
}
|
||||||
|
.app-titles {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.app-name {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.app-code {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--el-text-color-placeholder);
|
||||||
|
}
|
||||||
|
.app-desc {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
min-height: 36px;
|
||||||
|
}
|
||||||
|
.app-foot {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.spacer {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.app-status {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
&.ok {
|
||||||
|
color: var(--el-color-success);
|
||||||
|
}
|
||||||
|
&.err {
|
||||||
|
color: var(--el-color-danger);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* 应用中心 · 授权审批(仅租户管理员)
|
||||||
|
*
|
||||||
|
* 审批个人对企业授权应用的申请。
|
||||||
|
* 接口:GET/POST /API/App/*
|
||||||
|
*/
|
||||||
|
import { ref, computed, onMounted } from 'vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { Grid } from '@element-plus/icons-vue'
|
||||||
|
import { appApi, type PendingItem } from '@/api/app'
|
||||||
|
import { useUserStore } from '@/stores/user'
|
||||||
|
import { setPageTitle } from '@/utils/serverScope'
|
||||||
|
|
||||||
|
setPageTitle('授权审批')
|
||||||
|
|
||||||
|
const userStore = useUserStore()
|
||||||
|
const isAdmin = computed(() => userStore.isAdmin)
|
||||||
|
|
||||||
|
const pending = ref<PendingItem[]>([])
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
async function loadPending(): Promise<void> {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const r = await appApi.pending()
|
||||||
|
pending.value = r.list
|
||||||
|
} catch { /* 拦截器已提示 */ } finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 审批:通过 / 拒绝 */
|
||||||
|
async function approve(u: PendingItem, ok: boolean): Promise<void> {
|
||||||
|
try {
|
||||||
|
await appApi.approve(u.UserAppUUID, ok)
|
||||||
|
ElMessage.success(ok ? '已通过' : '已拒绝')
|
||||||
|
await loadPending()
|
||||||
|
} catch { /* 拦截器已提示 */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (isAdmin.value) void loadPending()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="app-pending-page">
|
||||||
|
<el-card shadow="never" class="app-card-wrap">
|
||||||
|
<template #header>
|
||||||
|
<div class="app-header">
|
||||||
|
<el-icon class="header-icon"><Grid /></el-icon>
|
||||||
|
<span>授权审批</span>
|
||||||
|
<span class="header-sub">个人对企业授权应用的申请</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-empty v-if="!isAdmin" description="仅租户管理员可访问授权审批" :image-size="80" />
|
||||||
|
|
||||||
|
<div v-else v-loading="loading" class="pending-list">
|
||||||
|
<div v-for="u in pending" :key="u.UserAppUUID" class="pending-row">
|
||||||
|
<div class="app-icon">
|
||||||
|
<el-icon :size="18"><component :is="u.AppIcon || 'Grid'" /></el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="pending-main">
|
||||||
|
<div class="pending-title">{{ u.AppName }}</div>
|
||||||
|
<div class="pending-sub">申请人 {{ u.UserUUID.slice(0, 8) }} · 申请于 {{ u.ApplyTime ? new Date(u.ApplyTime).toLocaleString('zh-CN', { hour12: false }) : '-' }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="spacer" />
|
||||||
|
<el-button size="small" type="danger" @click="approve(u, false)">拒绝</el-button>
|
||||||
|
<el-button size="small" type="primary" @click="approve(u, true)">通过</el-button>
|
||||||
|
</div>
|
||||||
|
<el-empty v-if="!loading && !pending.length" description="暂无待审批的授权申请" :image-size="80" />
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.app-pending-page {
|
||||||
|
padding: 16px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.app-card-wrap {
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
.app-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 15px;
|
||||||
|
.header-icon {
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
}
|
||||||
|
.header-sub {
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.pending-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.pending-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px 14px;
|
||||||
|
border: 1px solid var(--el-border-color-lighter);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
.app-icon {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
border-radius: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: linear-gradient(135deg, var(--el-color-primary-light-8), var(--el-color-primary-light-7));
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
}
|
||||||
|
.pending-main {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.pending-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.pending-sub {
|
||||||
|
margin-top: 2px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
}
|
||||||
|
.spacer {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in new issue
Block a user