chore: initial baseline with P0-safety .gitignore

This commit is contained in:
Simon
2026-06-14 16:49:18 +08:00
commit 63262292d7
510 changed files with 146008 additions and 0 deletions
@@ -0,0 +1,127 @@
<!-- =============================================================================
// 企微IT智能服务台 — H5用户端 AI 助手面板容器
// =============================================================================
// 说明:AI 助手面板的容器组件,使用 Vant4 Tab 组件
// 4个Tab:相似问题 | 审批流程 | 软件下载 | 搜索
// 右上角展开/收起按钮(移动端)
// ============================================================================= -->
<template>
<div class="ai-helper-panel">
<!-- 面板头部标题 + 收起按钮 -->
<div class="ai-helper-panel__header">
<span class="ai-helper-panel__title">AI 助手</span>
<!-- 移动端收起按钮 -->
<van-icon
name="cross"
size="18"
color="var(--text-tertiary)"
class="ai-helper-panel__close"
@click="handleClose"
/>
</div>
<!-- Tab 切换区域 -->
<van-tabs v-model:active="activeTab" sticky animated swipeable>
<!-- Tab1: 相似问题暂未实现显示占位符 -->
<van-tab title="相似问题">
<ComingSoon title="相似问题与做法" />
</van-tab>
<!-- Tab2: 审批流程 -->
<van-tab title="审批流程">
<ApprovalLinks />
</van-tab>
<!-- Tab3: 软件下载 -->
<van-tab title="软件下载">
<SoftwareDownloads />
</van-tab>
<!-- Tab4: 搜索暂未实现显示占位符 -->
<van-tab title="搜索">
<ComingSoon title="知识库搜索" />
</van-tab>
</van-tabs>
</div>
</template>
<script setup lang="ts">
/**
* AiHelperPanel AI 助手面板容器
* 4个Tab模块:
* - 相似问题:占位符"即将上线"
* - 审批流程:真实功能(从 API 获取数据)
* - 软件下载:真实功能(从 API 获取数据)
* - 搜索:占位符"即将上线"
* 移动端可通过右上角按钮收起面板
*/
import { ref } from 'vue'
import { useConversationStore } from '@/stores/conversation'
import ComingSoon from './ComingSoon.vue'
import ApprovalLinks from './ApprovalLinks.vue'
import SoftwareDownloads from './SoftwareDownloads.vue'
const store = useConversationStore()
/** 当前激活的 Tab 索引(0=相似问题, 1=审批流程, 2=软件下载, 3=搜索) */
const activeTab = ref<number>(1) // 默认显示审批流程
/**
* 收起面板(移动端使用)
* 通知 store 切换面板可见性
*/
function handleClose(): void {
store.toggleAssistantPanel()
}
</script>
<style scoped>
/* AI 助手面板容器 */
.ai-helper-panel {
height: 100%;
display: flex;
flex-direction: column;
background-color: var(--bg-secondary);
}
/* 面板头部 */
.ai-helper-panel__header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
border-bottom: 1px solid var(--border-color);
flex-shrink: 0;
}
/* 面板标题 */
.ai-helper-panel__title {
font-size: 16px;
font-weight: 600;
color: var(--text-primary);
}
/* 收起按钮 */
.ai-helper-panel__close {
cursor: pointer;
padding: 4px;
}
/* Tab 区域占满剩余空间 */
.ai-helper-panel :deep(.van-tabs) {
flex: 1;
display: flex;
flex-direction: column;
}
.ai-helper-panel :deep(.van-tabs__content) {
flex: 1;
overflow-y: auto;
}
.ai-helper-panel :deep(.van-tab__panel) {
min-height: 100%;
}
</style>
@@ -0,0 +1,111 @@
<!-- =============================================================================
// 企微IT智能服务台 — H5用户端审批流程链接组件
// =============================================================================
// 说明:展示所有审批流程链接,按分类分组
// 使用 Vant4 CellGroup + Cell 组件
// 点击链接在企微内置浏览器中打开
// ============================================================================= -->
<template>
<div class="approval-links">
<!-- 加载中提示 -->
<div v-if="loading" class="approval-links__loading">
<van-loading size="24px" vertical>加载中...</van-loading>
</div>
<!-- 无数据提示 -->
<div v-else-if="Object.keys(groupedLinks).length === 0" class="approval-links__empty">
<van-empty description="暂无审批流程" image="search" />
</div>
<!-- 按分类展示审批链接 -->
<template v-else>
<div
v-for="(links, category) in groupedLinks"
:key="category"
class="approval-links__group"
>
<!-- 分类标题 -->
<div class="approval-links__category">{{ category }}</div>
<!-- 该分类下的链接列表 -->
<van-cell-group inset>
<van-cell
v-for="link in links"
:key="link.id"
:title="link.title"
:icon="link.icon || 'link-o'"
is-link
@click="openLink(link.url)"
>
<!-- 右侧箭头 -->
<template #right-icon>
<van-icon name="arrow" />
</template>
</van-cell>
</van-cell-group>
</div>
</template>
</div>
</template>
<script setup lang="ts">
/**
* ApprovalLinks 审批流程链接组件
* 从 API 获取审批流程数据,按分类分组展示
* 点击链接在企微内置浏览器中打开
*/
import { computed } from 'vue'
import { useConversationStore } from '@/stores/conversation'
const store = useConversationStore()
/** 加载状态:当审批链接列表为空且未初始化时视为加载中 */
const loading = computed(() => {
return store.approvalLinks.length === 0 && !store.initialized
})
/** 审批链接按分类分组(从 store 计算属性获取) */
const groupedLinks = computed(() => store.approvalLinksByCategory)
/**
* 打开审批流程链接
* 在企微内置浏览器中打开目标链接
* @param url 审批流程链接地址
*/
function openLink(url: string): void {
// 在企微 WebView 中直接使用 window.open 即可在内置浏览器中打开
window.open(url, '_blank')
}
</script>
<style scoped>
/* 审批链接容器 */
.approval-links {
padding: 8px 0;
}
/* 加载中状态 */
.approval-links__loading {
display: flex;
justify-content: center;
padding: 40px 0;
}
/* 空数据状态 */
.approval-links__empty {
padding: 20px 0;
}
/* 分类分组容器 */
.approval-links__group {
margin-bottom: 12px;
}
/* 分类标题 */
.approval-links__category {
font-size: 13px;
color: var(--text-tertiary);
padding: 8px 16px 4px;
font-weight: 500;
}
</style>
@@ -0,0 +1,63 @@
<!-- =============================================================================
// 企微IT智能服务台 — H5用户端"即将上线"占位组件
// =============================================================================
// 说明:通用占位组件,用于尚未实现的功能模块
// 接收 title prop,显示灰色图标 + "{title} · 即将上线"
// 用于"相似问题与做法"和"知识库搜索"两个模块
// ============================================================================= -->
<template>
<div class="coming-soon">
<!-- 灰色占位图标 -->
<div class="coming-soon__icon">
<van-icon name="clock-o" size="48" color="var(--text-placeholder)" />
</div>
<!-- 模块名称 + 即将上线提示 -->
<p class="coming-soon__text">{{ title }} · 即将上线</p>
<p class="coming-soon__subtext">功能开发中敬请期待</p>
</div>
</template>
<script setup lang="ts">
/**
* ComingSoon 占位组件
* @prop title - 模块名称(如"相似问题与做法"、"知识库搜索"
*/
defineProps<{
/** 模块名称,显示在"即将上线"前 */
title: string
}>()
</script>
<style scoped>
/* 占位容器:居中布局 */
.coming-soon {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 16px;
min-height: 200px;
}
/* 灰色图标 */
.coming-soon__icon {
margin-bottom: 16px;
}
/* 主提示文字:模块名 + 即将上线 */
.coming-soon__text {
font-size: 15px;
color: var(--text-tertiary);
text-align: center;
}
/* 副提示文字 */
.coming-soon__subtext {
font-size: 12px;
color: var(--text-placeholder);
margin-top: 8px;
text-align: center;
}
</style>
@@ -0,0 +1,629 @@
<!-- =============================================================================
// 企微IT智能服务台 — H5用户端右侧面板
// =============================================================================
// 说明:桌面端右侧面板,三段式布局:
// 1. 上方:AI推送区(根据排查步骤和会话内容动态推送)
// 2. 中部:固定常用资源标签页(资源申请流程入口、常用必装软件)
// 3. 下方:趣味问答(答对可提高用户积分和等级)
// 注意:此面板仅在桌面端(≥500px)显示,手机端隐藏
// ============================================================================= -->
<template>
<div class="right-panel">
<!-- ====== 上方AI推送区 ====== -->
<div class="right-panel__section right-panel__ai-push">
<div class="right-panel__section-header">
<span class="right-panel__section-icon">🤖</span>
<span class="right-panel__section-title">AI 推荐</span>
</div>
<div class="right-panel__section-body">
<!-- 推荐卡片列表根据排查步骤和会话内容动态推送 -->
<div
v-for="item in aiPushItems"
:key="item.id"
class="ai-push-card"
:class="`ai-push-card--${item.type}`"
@click="handlePushClick(item)"
>
<div class="ai-push-card__header">
<span class="ai-push-card__icon">{{ item.icon }}</span>
<span class="ai-push-card__type-label">{{ item.typeLabel }}</span>
</div>
<div class="ai-push-card__title">{{ item.title }}</div>
<div v-if="item.subtitle" class="ai-push-card__subtitle">{{ item.subtitle }}</div>
</div>
<!-- 暂无推荐 -->
<div v-if="aiPushItems.length === 0" class="right-panel__empty">
<span>💡 对话过程中会自动推送相关资源</span>
</div>
</div>
</div>
<!-- ====== 中部常用资源标签页 ====== -->
<div class="right-panel__section right-panel__resources">
<div class="right-panel__section-header">
<span class="right-panel__section-icon">📚</span>
<span class="right-panel__section-title">常用资源</span>
</div>
<!-- 标签切换 -->
<div class="right-panel__tabs">
<button
class="right-panel__tab"
:class="{ 'right-panel__tab--active': activeResourceTab === 'process' }"
@click="activeResourceTab = 'process'"
>申请流程</button>
<button
class="right-panel__tab"
:class="{ 'right-panel__tab--active': activeResourceTab === 'software' }"
@click="activeResourceTab = 'software'"
>必装软件</button>
</div>
<!-- 申请流程标签页内容 -->
<div v-if="activeResourceTab === 'process'" class="right-panel__tab-content">
<div
v-for="item in processItems"
:key="item.id"
class="resource-item"
@click="handleProcessClick(item)"
>
<span class="resource-item__icon">{{ item.icon }}</span>
<div class="resource-item__info">
<span class="resource-item__title">{{ item.title }}</span>
<span v-if="item.desc" class="resource-item__desc">{{ item.desc }}</span>
</div>
<span class="resource-item__arrow"></span>
</div>
</div>
<!-- 必装软件标签页内容 -->
<div v-if="activeResourceTab === 'software'" class="right-panel__tab-content">
<div
v-for="item in softwareItems"
:key="item.id"
class="resource-item"
@click="handleSoftwareClick(item)"
>
<span class="resource-item__icon">{{ item.icon }}</span>
<div class="resource-item__info">
<span class="resource-item__title">{{ item.title }}</span>
<span v-if="item.desc" class="resource-item__desc">{{ item.desc }}</span>
</div>
<span class="resource-item__arrow"></span>
</div>
</div>
</div>
<!-- ====== 下方趣味问答 ====== -->
<div class="right-panel__section right-panel__quiz">
<div class="right-panel__section-header">
<span class="right-panel__section-icon">🎯</span>
<span class="right-panel__section-title">趣味问答</span>
<span class="right-panel__quiz-score">🏆 {{ userScore }}</span>
</div>
<div class="right-panel__section-body">
<template v-if="currentQuiz">
<div class="quiz-question">{{ currentQuiz.question }}</div>
<div class="quiz-options">
<button
v-for="(option, idx) in currentQuiz.options"
:key="idx"
class="quiz-option"
:class="{
'quiz-option--correct': quizAnswered && idx === currentQuiz.correctIndex,
'quiz-option--wrong': quizAnswered && quizSelectedIndex === idx && idx !== currentQuiz.correctIndex
}"
:disabled="quizAnswered"
@click="handleQuizAnswer(idx)"
>
<span class="quiz-option__label">{{ optionLabels[idx] }}</span>
<span class="quiz-option__text">{{ option }}</span>
</button>
</div>
<!-- 答题结果 -->
<div v-if="quizAnswered" class="quiz-result">
<span v-if="quizSelectedIndex === currentQuiz.correctIndex" class="quiz-result--correct">
答对啦+10积分
</span>
<span v-else class="quiz-result--wrong">
答错了正确答案是 {{ optionLabels[currentQuiz.correctIndex] }}
</span>
</div>
</template>
<div v-else class="right-panel__empty">
<span>暂无问答题目</span>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
/**
* RightPanel 右侧面板组件
* 三段式布局:AI推送区 / 常用资源标签页 / 趣味问答
* 仅在桌面端(≥500px)显示
*/
import { ref, computed } from 'vue'
// 阶段二接入 Dify 动态推送时启用:
// import { useConversationStore } from '@/stores/conversation'
// const store = useConversationStore() // 阶段二接入 Dify 动态推送时启用
// ── AI推送区 ──
/** AI推送条目类型 */
interface AiPushItem {
id: string
type: 'guide' | 'process' | 'download' // 处理指南/申请流程/软件下载
icon: string
typeLabel: string
title: string
subtitle?: string
}
/** AI推送数据(阶段一使用静态数据,阶段二接入Dify动态推送) */
const aiPushItems = computed<AiPushItem[]>(() => {
// TODO: 阶段二根据排查步骤和会话内容动态生成推送
// 当前使用示例数据
return [
{
id: 'push-1',
type: 'guide',
icon: '📖',
typeLabel: '处理指南',
title: 'WiFi连接问题处理指南',
subtitle: '已解决28次类似问题',
},
{
id: 'push-2',
type: 'process',
icon: '📋',
typeLabel: '申请流程',
title: '网络连接申请流程',
subtitle: '在线申请,1-3个工作日',
},
{
id: 'push-3',
type: 'download',
icon: '💾',
typeLabel: '软件下载',
title: '无线网卡驱动下载',
subtitle: '适用于 Windows 10/11',
},
]
})
/**
* 点击AI推送卡片
*/
function handlePushClick(item: AiPushItem): void {
// TODO: 阶段二实现推送跳转
console.log('[RightPanel] AI推送点击:', item.title)
}
// ── 常用资源标签页 ──
/** 当前激活的资源标签页 */
const activeResourceTab = ref<'process' | 'software'>('process')
/** 申请流程列表 */
const processItems = ref([
{ id: 'p-1', icon: '💻', title: 'IT设备申请', desc: '电脑/显示器/外设' },
{ id: 'p-2', icon: '🔐', title: '权限申请', desc: '系统/文件夹/VPN' },
{ id: 'p-3', icon: '🌐', title: 'VPN申请', desc: '远程办公网络' },
{ id: 'p-4', icon: '📧', title: '邮箱别名申请', desc: '别名/分发组' },
])
/** 必装软件列表 */
const softwareItems = ref([
{ id: 's-1', icon: '📝', title: 'Office 365', desc: 'Word/Excel/PPT' },
{ id: 's-2', icon: '📄', title: 'Adobe Acrobat', desc: 'PDF阅读/编辑' },
{ id: 's-3', icon: '💬', title: '企业微信', desc: '即时通讯/协作' },
{ id: 's-4', icon: '🛡️', title: '火绒安全', desc: '杀毒/终端防护' },
])
/**
* 点击申请流程项
*/
function handleProcessClick(item: { id: string; title: string }): void {
// TODO: 阶段二实现流程跳转
console.log('[RightPanel] 申请流程点击:', item.title)
}
/**
* 点击软件下载项
*/
function handleSoftwareClick(item: { id: string; title: string }): void {
// TODO: 阶段二实现软件下载
console.log('[RightPanel] 软件下载点击:', item.title)
}
// ── 趣味问答 ──
/** 问答题目类型 */
interface QuizQuestion {
id: string
question: string
options: string[]
correctIndex: number // 正确答案的索引
}
/** 选项标签 */
const optionLabels = ['A', 'B', 'C', 'D']
/** 用户积分 */
const userScore = ref(0)
/** 是否已答题 */
const quizAnswered = ref(false)
/** 用户选择的答案索引 */
const quizSelectedIndex = ref(-1)
/** 问答题目列表(阶段一使用静态数据) */
const quizQuestions = ref<QuizQuestion[]>([
{
id: 'q-1',
question: 'IT服务台电话分机号是?',
options: ['8001', '8002', '8003', '8004'],
correctIndex: 0,
},
{
id: 'q-2',
question: '电脑无法连接WiFi时,首先应该检查什么?',
options: ['重启路由器', 'WiFi适配器是否禁用', '联系网络管理员', '重新安装系统'],
correctIndex: 1,
},
{
id: 'q-3',
question: 'VPN申请一般需要几个工作日?',
options: ['1个工作日', '1-3个工作日', '3-5个工作日', '5个工作日以上'],
correctIndex: 1,
},
])
/** 当前问答题目 */
const currentQuiz = computed<QuizQuestion | null>(() => {
return quizQuestions.value[0] || null
})
/**
* 用户回答问答
* @param index - 用户选择的选项索引
*/
function handleQuizAnswer(index: number): void {
if (quizAnswered.value) return
quizAnswered.value = true
quizSelectedIndex.value = index
// 答对加10分
if (currentQuiz.value && index === currentQuiz.value.correctIndex) {
userScore.value += 10
}
}
</script>
<style scoped>
/* ====== 右侧面板容器 ====== */
.right-panel {
display: flex;
flex-direction: column;
height: 100%;
background-color: var(--bg-secondary);
border-left: 1px solid var(--border-color);
overflow: hidden;
}
/* ====== 面板区域通用样式 ====== */
.right-panel__section {
border-bottom: 1px solid var(--border-color);
}
.right-panel__section-header {
display: flex;
align-items: center;
gap: 6px;
padding: 10px 14px;
background-color: var(--bg-tertiary);
border-bottom: 1px solid var(--border-color);
}
.right-panel__section-icon {
font-size: 16px;
}
.right-panel__section-title {
font-size: 13px;
font-weight: 600;
color: var(--text-primary);
}
.right-panel__section-body {
padding: 10px 14px;
}
.right-panel__empty {
text-align: center;
font-size: 12px;
color: var(--text-tertiary);
padding: 16px 0;
}
/* ====== AI推送区 ====== */
.right-panel__ai-push {
flex: 1;
overflow-y: auto;
}
.ai-push-card {
padding: 10px 12px;
border-radius: 8px;
border: 1px solid var(--border-color);
background: var(--bg-primary);
margin-bottom: 8px;
cursor: pointer;
transition: all 0.2s;
}
.ai-push-card:last-child {
margin-bottom: 0;
}
.ai-push-card:hover {
border-color: var(--accent);
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.1);
}
.ai-push-card:active {
transform: scale(0.98);
}
.ai-push-card__header {
display: flex;
align-items: center;
gap: 4px;
margin-bottom: 4px;
}
.ai-push-card__icon {
font-size: 14px;
}
.ai-push-card__type-label {
font-size: 11px;
padding: 1px 6px;
border-radius: 4px;
font-weight: 500;
}
/* 处理指南类型 */
.ai-push-card--guide .ai-push-card__type-label {
background: rgba(34, 197, 94, 0.1);
color: #22c55e;
}
/* 申请流程类型 */
.ai-push-card--process .ai-push-card__type-label {
background: rgba(59, 130, 246, 0.1);
color: #3b82f6;
}
/* 软件下载类型 */
.ai-push-card--download .ai-push-card__type-label {
background: rgba(168, 85, 247, 0.1);
color: #a855f7;
}
.ai-push-card__title {
font-size: 13px;
font-weight: 500;
color: var(--text-primary);
line-height: 1.4;
}
.ai-push-card__subtitle {
font-size: 11px;
color: var(--text-tertiary);
margin-top: 2px;
}
/* ====== 常用资源标签页 ====== */
.right-panel__resources {
flex: 1;
overflow-y: auto;
}
.right-panel__tabs {
display: flex;
border-bottom: 1px solid var(--border-color);
}
.right-panel__tab {
flex: 1;
padding: 8px 12px;
border: none;
background: transparent;
font-size: 13px;
color: var(--text-tertiary);
cursor: pointer;
transition: all 0.2s;
border-bottom: 2px solid transparent;
font-family: inherit;
}
.right-panel__tab--active {
color: var(--accent);
border-bottom-color: var(--accent);
font-weight: 600;
}
.right-panel__tab:hover:not(.right-panel__tab--active) {
color: var(--text-secondary);
background: var(--bg-tertiary);
}
.right-panel__tab-content {
padding: 8px 14px;
}
.resource-item {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
border-radius: 8px;
cursor: pointer;
transition: background-color 0.2s;
margin-bottom: 4px;
}
.resource-item:last-child {
margin-bottom: 0;
}
.resource-item:hover {
background: var(--bg-tertiary);
}
.resource-item:active {
background: var(--border-color);
}
.resource-item__icon {
font-size: 20px;
flex-shrink: 0;
}
.resource-item__info {
flex: 1;
min-width: 0;
}
.resource-item__title {
font-size: 13px;
font-weight: 500;
color: var(--text-primary);
display: block;
}
.resource-item__desc {
font-size: 11px;
color: var(--text-tertiary);
display: block;
margin-top: 2px;
}
.resource-item__arrow {
font-size: 14px;
color: var(--text-tertiary);
flex-shrink: 0;
}
/* ====== 趣味问答 ====== */
.right-panel__quiz {
flex-shrink: 0;
}
.right-panel__quiz-score {
margin-left: auto;
font-size: 12px;
color: var(--color-warning);
font-weight: 600;
}
.quiz-question {
font-size: 14px;
font-weight: 600;
color: var(--text-primary);
line-height: 1.5;
margin-bottom: 10px;
}
.quiz-options {
display: flex;
flex-direction: column;
gap: 6px;
}
.quiz-option {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
border-radius: 8px;
border: 1px solid var(--border-color);
background: var(--bg-primary);
cursor: pointer;
transition: all 0.2s;
font-family: inherit;
text-align: left;
}
.quiz-option:hover:not(:disabled) {
border-color: var(--accent);
background: var(--accent-soft);
}
.quiz-option:active:not(:disabled) {
transform: scale(0.98);
}
.quiz-option:disabled {
cursor: default;
}
/* 正确选项 */
.quiz-option--correct {
border-color: #22c55e;
background: rgba(34, 197, 94, 0.1);
}
/* 错误选项 */
.quiz-option--wrong {
border-color: #ef4444;
background: rgba(239, 68, 68, 0.1);
}
.quiz-option__label {
width: 22px;
height: 22px;
border-radius: 50%;
background: var(--bg-tertiary);
color: var(--text-secondary);
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: 700;
flex-shrink: 0;
}
.quiz-option--correct .quiz-option__label {
background: #22c55e;
color: #fff;
}
.quiz-option--wrong .quiz-option__label {
background: #ef4444;
color: #fff;
}
.quiz-option__text {
font-size: 13px;
color: var(--text-primary);
}
.quiz-result {
margin-top: 8px;
font-size: 13px;
font-weight: 600;
text-align: center;
}
.quiz-result--correct {
color: #22c55e;
}
.quiz-result--wrong {
color: #ef4444;
}
</style>
@@ -0,0 +1,148 @@
<!-- =============================================================================
// 企微IT智能服务台 — H5用户端软件下载入口组件
// =============================================================================
// 说明:展示所有可下载的软件,按分类分组
// 显示名称 + 版本号 + 平台标签
// 点击下载链接直接下载
// ============================================================================= -->
<template>
<div class="software-downloads">
<!-- 加载中提示 -->
<div v-if="loading" class="software-downloads__loading">
<van-loading size="24px" vertical>加载中...</van-loading>
</div>
<!-- 无数据提示 -->
<div v-else-if="Object.keys(groupedDownloads).length === 0" class="software-downloads__empty">
<van-empty description="暂无软件下载" image="search" />
</div>
<!-- 按分类展示软件下载列表 -->
<template v-else>
<div
v-for="(items, category) in groupedDownloads"
:key="category"
class="software-downloads__group"
>
<!-- 分类标题 -->
<div class="software-downloads__category">{{ category }}</div>
<!-- 该分类下的软件列表 -->
<van-cell-group inset>
<van-cell
v-for="item in items"
:key="item.id"
:title="item.name"
:label="versionLabel(item)"
:icon="item.icon || 'down'"
is-link
@click="handleDownload(item)"
>
<!-- 右侧平台标签 -->
<template #right-icon>
<div class="software-downloads__platforms">
<span
v-for="platform in item.platforms"
:key="platform"
class="software-downloads__tag"
>
{{ platform }}
</span>
</div>
</template>
</van-cell>
</van-cell-group>
</div>
</template>
</div>
</template>
<script setup lang="ts">
/**
* SoftwareDownloads 软件下载入口组件
* 从 API 获取软件下载数据,按分类分组展示
* 显示名称、版本号、平台标签,点击下载
*/
import { computed } from 'vue'
import { useConversationStore } from '@/stores/conversation'
import type { SoftwareDownload } from '@/api/conversation'
const store = useConversationStore()
/** 加载状态:当软件下载列表为空且未初始化时视为加载中 */
const loading = computed(() => {
return store.softwareDownloads.length === 0 && !store.initialized
})
/** 软件下载按分类分组(从 store 计算属性获取) */
const groupedDownloads = computed(() => store.softwareDownloadsByCategory)
/**
* 生成版本号标签文字
* @param item 软件下载项
* @returns 版本号文字(如 "v2.1.0"
*/
function versionLabel(item: SoftwareDownload): string {
return item.version ? `版本: ${item.version}` : ''
}
/**
* 处理下载点击
* 在企微内置浏览器中打开下载链接
* @param item 软件下载项
*/
function handleDownload(item: SoftwareDownload): void {
window.open(item.download_url, '_blank')
}
</script>
<style scoped>
/* 软件下载容器 */
.software-downloads {
padding: 8px 0;
}
/* 加载中状态 */
.software-downloads__loading {
display: flex;
justify-content: center;
padding: 40px 0;
}
/* 空数据状态 */
.software-downloads__empty {
padding: 20px 0;
}
/* 分类分组容器 */
.software-downloads__group {
margin-bottom: 12px;
}
/* 分类标题 */
.software-downloads__category {
font-size: 13px;
color: var(--text-tertiary);
padding: 8px 16px 4px;
font-weight: 500;
}
/* 平台标签容器 */
.software-downloads__platforms {
display: flex;
gap: 4px;
align-items: center;
}
/* 单个平台标签 */
.software-downloads__tag {
display: inline-block;
font-size: 10px;
padding: 1px 6px;
border-radius: 3px;
background-color: var(--accent-soft);
color: var(--accent);
white-space: nowrap;
}
</style>