|
@@ -3,25 +3,60 @@
|
|
|
<div class="header">
|
|
<div class="header">
|
|
|
<h2>快捷导航</h2>
|
|
<h2>快捷导航</h2>
|
|
|
<div class="actions">
|
|
<div class="actions">
|
|
|
|
|
+ <el-switch
|
|
|
|
|
+ v-model="showCategory"
|
|
|
|
|
+ active-text="显示分类"
|
|
|
|
|
+ inactive-text=""
|
|
|
|
|
+ @change="handleCategoryToggle"
|
|
|
|
|
+ style="margin-right: 10px;"
|
|
|
|
|
+ />
|
|
|
<el-button @click="fetchMappings" :icon="Refresh" circle title="刷新" />
|
|
<el-button @click="fetchMappings" :icon="Refresh" circle title="刷新" />
|
|
|
<el-button @click="showSettings = true" :icon="Setting" circle title="图标设置" />
|
|
<el-button @click="showSettings = true" :icon="Setting" circle title="图标设置" />
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
- <div v-loading="loading" class="apps-grid">
|
|
|
|
|
- <div
|
|
|
|
|
- v-for="app in activeApps"
|
|
|
|
|
- :key="app.app_id"
|
|
|
|
|
- class="app-item"
|
|
|
|
|
- :style="{ width: iconSize + 'px' }"
|
|
|
|
|
- >
|
|
|
|
|
- <PlatformIcon
|
|
|
|
|
- :text="app.app_name"
|
|
|
|
|
- :size="iconSize"
|
|
|
|
|
- @click="handleAppClick(app)"
|
|
|
|
|
- />
|
|
|
|
|
|
|
+ <div v-loading="loading">
|
|
|
|
|
+ <!-- 按分类显示 -->
|
|
|
|
|
+ <template v-if="showCategory">
|
|
|
|
|
+ <div
|
|
|
|
|
+ v-for="category in categorizedApps"
|
|
|
|
|
+ :key="category.categoryId || 'uncategorized'"
|
|
|
|
|
+ class="category-section"
|
|
|
|
|
+ >
|
|
|
|
|
+ <h3 class="category-title">{{ category.categoryName }}</h3>
|
|
|
|
|
+ <div class="apps-grid">
|
|
|
|
|
+ <div
|
|
|
|
|
+ v-for="app in category.apps"
|
|
|
|
|
+ :key="app.app_id"
|
|
|
|
|
+ class="app-item"
|
|
|
|
|
+ :style="{ width: iconSize + 'px' }"
|
|
|
|
|
+ >
|
|
|
|
|
+ <PlatformIcon
|
|
|
|
|
+ :text="app.app_name"
|
|
|
|
|
+ :size="iconSize"
|
|
|
|
|
+ @click="handleAppClick(app)"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </template>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 不按分类显示(原有方式) -->
|
|
|
|
|
+ <div v-else class="apps-grid">
|
|
|
|
|
+ <div
|
|
|
|
|
+ v-for="app in activeApps"
|
|
|
|
|
+ :key="app.app_id"
|
|
|
|
|
+ class="app-item"
|
|
|
|
|
+ :style="{ width: iconSize + 'px' }"
|
|
|
|
|
+ >
|
|
|
|
|
+ <PlatformIcon
|
|
|
|
|
+ :text="app.app_name"
|
|
|
|
|
+ :size="iconSize"
|
|
|
|
|
+ @click="handleAppClick(app)"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
<div v-if="activeApps.length === 0 && !loading" class="empty-state">
|
|
<div v-if="activeApps.length === 0 && !loading" class="empty-state">
|
|
|
<el-empty description="暂无已配置的平台账号" />
|
|
<el-empty description="暂无已配置的平台账号" />
|
|
|
<el-button type="primary" @click="$router.push('/dashboard/apps')">去配置</el-button>
|
|
<el-button type="primary" @click="$router.push('/dashboard/apps')">去配置</el-button>
|
|
@@ -57,20 +92,69 @@ interface Mapping {
|
|
|
app_id: string
|
|
app_id: string
|
|
|
protocol_type: string
|
|
protocol_type: string
|
|
|
mapped_key: string
|
|
mapped_key: string
|
|
|
|
|
+ mapped_email?: string
|
|
|
is_active: boolean
|
|
is_active: boolean
|
|
|
|
|
+ description?: string
|
|
|
|
|
+ category_id?: number
|
|
|
|
|
+ category_name?: string
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+interface CategoryGroup {
|
|
|
|
|
+ categoryId: number | null
|
|
|
|
|
+ categoryName: string
|
|
|
|
|
+ apps: Mapping[]
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// State
|
|
// State
|
|
|
const mappings = ref<Mapping[]>([])
|
|
const mappings = ref<Mapping[]>([])
|
|
|
const loading = ref(false)
|
|
const loading = ref(false)
|
|
|
const showSettings = ref(false)
|
|
const showSettings = ref(false)
|
|
|
|
|
+const showCategory = ref(false)
|
|
|
|
|
|
|
|
// Settings
|
|
// Settings
|
|
|
const iconSize = ref(100)
|
|
const iconSize = ref(100)
|
|
|
|
|
|
|
|
-// 仅根据激活状态和已支持的协议类型过滤,支持 SIMPLE_API 与 OIDC
|
|
|
|
|
|
|
+// 新接口已经过滤了激活状态和协议类型,直接使用返回的应用列表
|
|
|
const activeApps = computed(() => {
|
|
const activeApps = computed(() => {
|
|
|
- return mappings.value.filter(m => m.is_active && (m.protocol_type === 'SIMPLE_API' || m.protocol_type === 'OIDC'))
|
|
|
|
|
|
|
+ return mappings.value
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+// 按分类分组
|
|
|
|
|
+const categorizedApps = computed(() => {
|
|
|
|
|
+ const groups: Map<number | null, CategoryGroup> = new Map()
|
|
|
|
|
+
|
|
|
|
|
+ // 初始化"未分类"组
|
|
|
|
|
+ groups.set(null, {
|
|
|
|
|
+ categoryId: null,
|
|
|
|
|
+ categoryName: '未分类',
|
|
|
|
|
+ apps: []
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ // 按分类分组
|
|
|
|
|
+ activeApps.value.forEach(app => {
|
|
|
|
|
+ const categoryId = app.category_id ?? null
|
|
|
|
|
+ const categoryName = app.category_name || '未分类'
|
|
|
|
|
+
|
|
|
|
|
+ if (!groups.has(categoryId)) {
|
|
|
|
|
+ groups.set(categoryId, {
|
|
|
|
|
+ categoryId,
|
|
|
|
|
+ categoryName,
|
|
|
|
|
+ apps: []
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ groups.get(categoryId)!.apps.push(app)
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ // 转换为数组并排序:有分类的在前,未分类在最后
|
|
|
|
|
+ const result = Array.from(groups.values())
|
|
|
|
|
+ result.sort((a, b) => {
|
|
|
|
|
+ if (a.categoryId === null) return 1
|
|
|
|
|
+ if (b.categoryId === null) return -1
|
|
|
|
|
+ return a.categoryName.localeCompare(b.categoryName, 'zh-CN')
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ return result
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
// Load Settings
|
|
// Load Settings
|
|
@@ -80,6 +164,7 @@ const loadSettings = () => {
|
|
|
try {
|
|
try {
|
|
|
const parsed = JSON.parse(saved)
|
|
const parsed = JSON.parse(saved)
|
|
|
if (parsed.iconSize) iconSize.value = parsed.iconSize
|
|
if (parsed.iconSize) iconSize.value = parsed.iconSize
|
|
|
|
|
+ if (parsed.showCategory !== undefined) showCategory.value = parsed.showCategory
|
|
|
} catch (e) {
|
|
} catch (e) {
|
|
|
console.error('Failed to parse settings', e)
|
|
console.error('Failed to parse settings', e)
|
|
|
}
|
|
}
|
|
@@ -90,20 +175,22 @@ const loadSettings = () => {
|
|
|
const saveSettings = () => {
|
|
const saveSettings = () => {
|
|
|
localStorage.setItem('launchpad_settings', JSON.stringify({
|
|
localStorage.setItem('launchpad_settings', JSON.stringify({
|
|
|
iconSize: iconSize.value,
|
|
iconSize: iconSize.value,
|
|
|
|
|
+ showCategory: showCategory.value,
|
|
|
}))
|
|
}))
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// 处理分类开关切换
|
|
|
|
|
+const handleCategoryToggle = () => {
|
|
|
|
|
+ saveSettings()
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
// Fetch Data
|
|
// Fetch Data
|
|
|
const fetchMappings = async () => {
|
|
const fetchMappings = async () => {
|
|
|
- // Prevent parallel loading if already loading (unless it's background refresh which we want to ignore loading state for?
|
|
|
|
|
- // actually for UX, showing loading is fine or we can hide it for background refresh.
|
|
|
|
|
- // But here we'll just show loading to indicate activity if user clicks refresh)
|
|
|
|
|
loading.value = true
|
|
loading.value = true
|
|
|
try {
|
|
try {
|
|
|
- // Add timestamp to prevent caching
|
|
|
|
|
- const res = await api.get('/simple/me/mappings', {
|
|
|
|
|
|
|
+ // 使用快捷导航专用接口,包含分类和描述信息
|
|
|
|
|
+ const res = await api.get('/simple/me/launchpad-apps', {
|
|
|
params: {
|
|
params: {
|
|
|
- limit: 100,
|
|
|
|
|
_t: Date.now()
|
|
_t: Date.now()
|
|
|
}
|
|
}
|
|
|
})
|
|
})
|
|
@@ -196,6 +283,7 @@ onUnmounted(() => {
|
|
|
|
|
|
|
|
.actions {
|
|
.actions {
|
|
|
display: flex;
|
|
display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
gap: 10px;
|
|
gap: 10px;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -212,6 +300,19 @@ onUnmounted(() => {
|
|
|
gap: 10px;
|
|
gap: 10px;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+.category-section {
|
|
|
|
|
+ margin-bottom: 40px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.category-title {
|
|
|
|
|
+ font-size: 18px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #303133;
|
|
|
|
|
+ margin-bottom: 20px;
|
|
|
|
|
+ padding-bottom: 10px;
|
|
|
|
|
+ border-bottom: 2px solid #e4e7ed;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
.setting-item {
|
|
.setting-item {
|
|
|
margin-bottom: 20px;
|
|
margin-bottom: 20px;
|
|
|
display: flex;
|
|
display: flex;
|