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,204 @@
<!--
=============================================================================
企微IT智能服务台 坐席列表表格组件
=============================================================================
说明管理后台坐席列表表格使用 Element Plus el-table
功能显示坐席信息状态筛选角色/技能标签编辑操作
-->
<template>
<div class="table-wrapper">
<el-table
:data="agents"
style="width: 100%"
:header-cell-style="{ background: 'var(--bg-tertiary)', color: 'var(--text-secondary)', fontSize: '12px', fontWeight: '500' }"
:cell-style="{ color: 'var(--text-primary)', fontSize: '13px' }"
row-class-name="agent-table-row"
v-loading="loading"
element-loading-text="正在加载坐席数据..."
>
<!-- 坐席列 -->
<el-table-column label="坐席" min-width="180">
<template #default="{ row }">
<div class="agent-info">
<div class="agent-avatar" :style="{ background: getAvatarBg(row) }">
{{ row.name.charAt(0) }}
</div>
<div>
<div class="agent-name">{{ row.name }}</div>
<div class="agent-id">{{ row.user_id }}</div>
</div>
</div>
</template>
</el-table-column>
<!-- 状态列 -->
<el-table-column label="状态" width="90">
<template #default="{ row }">
<el-tag :type="getStatusType(row.status)" size="small" effect="dark">
{{ getStatusText(row.status) }}
</el-tag>
</template>
</el-table-column>
<!-- 技能标签列 -->
<el-table-column label="技能标签" min-width="180">
<template #default="{ row }">
<div class="tag-group">
<el-tag
v-for="tag in row.skill_tags"
:key="tag"
size="small"
effect="plain"
>
{{ tag }}
</el-tag>
<span v-if="!row.skill_tags || row.skill_tags.length === 0" class="text-muted">未设置</span>
</div>
</template>
</el-table-column>
<!-- 角色列 -->
<el-table-column label="角色" width="80">
<template #default="{ row }">
<el-tag :type="row.role === 'admin' ? 'warning' : 'info'" size="small">
{{ row.role === 'admin' ? '组长' : '坐席' }}
</el-tag>
</template>
</el-table-column>
<!-- OTP列 -->
<el-table-column label="OTP" width="80" align="center">
<template #default="{ row }">
<el-tag v-if="row.otp_enabled === 1" type="success" size="small">
已启用
</el-tag>
<el-tag v-else-if="row.otp_secret" type="warning" size="small">
未验证
</el-tag>
<span v-else class="text-muted">未绑定</span>
</template>
</el-table-column>
<!-- 负载列 -->
<el-table-column label="当前/最大负载" width="110" align="center">
<template #default="{ row }">
<span>{{ row.current_load }} / {{ row.max_load }}</span>
</template>
</el-table-column>
<!-- 今日结单列 -->
<el-table-column label="今日结单" width="90" align="center">
<template #default="{ row }">
{{ row.today_resolved ?? 0 }}
</template>
</el-table-column>
<!-- 操作列 -->
<el-table-column label="操作" width="120" align="center" fixed="right">
<template #default="{ row }">
<el-button size="small" text type="primary" @click="$emit('edit', row)">
编辑
</el-button>
<el-button size="small" text type="danger" @click="$emit('delete', row)">
移除
</el-button>
</template>
</el-table-column>
</el-table>
</div>
</template>
<script setup lang="ts">
// ==========================================================================
// 依赖导入
// ==========================================================================
import type { Agent } from '@/types'
// ==========================================================================
// Props / Emits
// ==========================================================================
defineProps<{
/** 坐席数据列表 */
agents: Agent[]
/** 是否正在加载 */
loading?: boolean
}>()
defineEmits<{
/** 编辑坐席 */
edit: [agent: Agent]
/** 移除坐席 */
delete: [agent: Agent]
}>()
// ==========================================================================
// 工具方法
// ==========================================================================
/** 获取状态标签类型 */
function getStatusType(status: string): 'success' | 'warning' | 'danger' | 'info' {
switch (status) {
case 'online': return 'success'
case 'busy': return 'warning'
case 'offline': return 'info'
default: return 'info'
}
}
/** 获取状态文本 */
function getStatusText(status: string): string {
switch (status) {
case 'online': return '在线'
case 'busy': return '忙碌'
case 'offline': return '离线'
default: return status
}
}
/** 获取头像背景色 */
function getAvatarBg(row: Agent): string {
if (row.role === 'admin') return 'var(--accent-light)'
return 'rgba(16, 185, 129, 0.12)'
}
</script>
<style scoped>
/* 坐席信息 */
.agent-info {
display: flex;
align-items: center;
gap: 10px;
}
.agent-avatar {
width: 32px;
height: 32px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: var(--accent);
font-size: 12px;
font-weight: 600;
flex-shrink: 0;
}
.agent-name {
font-weight: 500;
color: var(--text-primary);
}
.agent-id {
font-size: 11px;
color: var(--text-muted);
}
.text-muted {
color: var(--text-muted);
font-size: 12px;
}
</style>
<style>
/* 全局表行悬停样式 */
.agent-table-row:hover td {
background-color: var(--bg-tertiary) !important;
}
</style>
@@ -0,0 +1,27 @@
<!--
=============================================================================
企微IT智能服务台 面包屑导航组件
=============================================================================
说明根据当前路由 meta.title 动态生成面包屑导航
格式管理后台 / 当前页面标题
-->
<template>
<el-breadcrumb separator="/">
<el-breadcrumb-item :to="{ path: '/dashboard' }">管理后台</el-breadcrumb-item>
<el-breadcrumb-item v-if="currentTitle">{{ currentTitle }}</el-breadcrumb-item>
</el-breadcrumb>
</template>
<script setup lang="ts">
// ==========================================================================
// 依赖导入
// ==========================================================================
import { computed } from 'vue'
import { useRoute } from 'vue-router'
// ==========================================================================
// 当前页面标题
// ==========================================================================
const route = useRoute()
const currentTitle = computed(() => (route.meta.title as string) || '')
</script>
@@ -0,0 +1,195 @@
<!--
=============================================================================
企微IT智能服务台 配置分组卡片组件
=============================================================================
说明配置分组卡片显示组名描述每项配置支持
- 布尔值使用 el-switch 开关
- 数值使用 el-input-number
- JSON 数组/字符串显示值 + 编辑按钮
-->
<template>
<el-card class="config-group-card" shadow="never">
<!-- 卡片头部 -->
<template #header>
<div class="config-group-header">
<div class="config-group-title">
<el-icon v-if="icon" :size="16" :style="{ color: iconColor || 'var(--accent)', marginRight: '6px' }">
<component :is="icon" />
</el-icon>
{{ group.name }}
</div>
<el-tag v-if="statusTag" :type="statusTag.type" size="small">
{{ statusTag.text }}
</el-tag>
</div>
<div v-if="groupDescription" class="config-group-desc">{{ groupDescription }}</div>
</template>
<!-- 配置项列表 -->
<div class="config-items">
<div
v-for="item in group.items"
:key="item.key"
class="config-item"
>
<div class="config-item-info">
<span class="config-item-name">{{ item.description }}</span>
<span v-if="itemStage" class="config-item-stage">{{ itemStage }}</span>
</div>
<div class="config-item-control">
<!-- 布尔值开关 -->
<el-switch
v-if="item.value_type === 'boolean'"
:model-value="item.value === 'true'"
@change="(val: boolean) => $emit('update', item.key, val ? 'true' : 'false')"
/>
<!-- 数值输入框 -->
<el-input-number
v-else-if="item.value_type === 'number'"
:model-value="Number(item.value)"
:min="0"
:max="999"
size="small"
controls-position="right"
@change="(val: number | undefined) => $emit('update', item.key, String(val ?? 0))"
style="width: 120px"
/>
<!-- JSON 数组/字符串代码显示 + 编辑按钮 -->
<template v-else>
<code class="config-json-preview">{{ truncateValue(item.value) }}</code>
<el-button size="small" text type="primary" @click="$emit('edit', item)">
编辑
</el-button>
</template>
</div>
</div>
</div>
</el-card>
</template>
<script setup lang="ts">
// ==========================================================================
// 依赖导入
// ==========================================================================
import type { ConfigGroup } from '@/types'
// ==========================================================================
// Props / Emits
// ==========================================================================
defineProps<{
/** 配置分组数据 */
group: ConfigGroup
/** 分组图标名称 */
icon?: string
/** 图标颜色 */
iconColor?: string
/** 分组描述 */
groupDescription?: string
/** 状态标签 */
statusTag?: {
type: 'success' | 'warning' | 'danger' | 'info'
text: string
}
/** 配置项阶段标签 */
itemStage?: string
}>()
defineEmits<{
/** 配置值更新 */
update: [key: string, value: string]
/** 编辑 JSON 值 */
edit: [item: ConfigGroup['items'][0]]
}>()
// ==========================================================================
// 工具方法
// ==========================================================================
/** 截断过长的配置值显示 */
function truncateValue(value: string): string {
if (value.length > 40) {
return value.substring(0, 40) + '...'
}
return value
}
</script>
<style scoped>
/* 配置分组卡片 */
.config-group-card {
background-color: var(--bg-secondary);
border-color: var(--border);
}
/* 卡片头部 */
.config-group-header {
display: flex;
align-items: center;
justify-content: space-between;
}
.config-group-title {
display: flex;
align-items: center;
font-size: 14px;
font-weight: 600;
color: var(--text-primary);
}
.config-group-desc {
font-size: 12px;
color: var(--text-secondary);
margin-top: 6px;
}
/* 配置项列表 */
.config-items {
display: flex;
flex-direction: column;
}
/* 单个配置项 */
.config-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 0;
border-top: 1px solid var(--border);
}
.config-item:first-child {
border-top: none;
padding-top: 0;
}
/* 配置项信息 */
.config-item-info {
display: flex;
flex-direction: column;
gap: 2px;
}
.config-item-name {
font-size: 13px;
color: var(--text-primary);
}
.config-item-stage {
font-size: 11px;
color: var(--text-muted);
}
/* 配置项控件 */
.config-item-control {
display: flex;
align-items: center;
gap: 8px;
flex-shrink: 0;
}
/* JSON 预览 */
.config-json-preview {
font-size: 11px;
max-width: 180px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
</style>
@@ -0,0 +1,199 @@
<!--
=============================================================================
企微IT智能服务台 集成系统卡片组件
=============================================================================
说明外部系统集成卡片显示系统名称图标状态指示灯配置按钮
可配置的系统Dify/RAGFlow显示"配置"按钮
不可配置的系统仅显示状态和描述
-->
<template>
<div class="integration-card" :class="{ 'is-configurable': integration.configurable }">
<!-- 系统图标 -->
<div
class="integration-icon"
:style="{ background: iconBg, color: iconColor }"
>
<el-icon :size="22">
<component :is="icon" />
</el-icon>
</div>
<!-- 系统名称 -->
<div class="integration-name">{{ integration.name }}</div>
<!-- 状态指示灯 -->
<div class="integration-status">
<el-tag :type="statusTagType" size="small" effect="dark">
{{ statusText }}
</el-tag>
</div>
<!-- 描述信息 -->
<div class="integration-detail">
<slot name="detail">
<template v-if="integration.configurable && integration.config">
<!-- url_key 模式Dify / RAGFlow -->
<template v-if="integration.config_type === 'url_key' || !integration.config_type">
<div v-if="integration.config.api_url" class="config-info">API: {{ integration.config.api_url }}</div>
<div class="config-info">{{ integration.config.api_key_set ? '密钥已配置' : '密钥未配置' }}</div>
</template>
<!-- access_key 模式火绒安全 -->
<template v-else-if="integration.config_type === 'access_key'">
<div v-if="integration.config.base_url" class="config-info">URL: {{ integration.config.base_url }}</div>
<div class="config-info">{{ integration.config.access_key_id_set ? 'AccessKey 已配置' : 'AccessKey 未配置' }}</div>
</template>
<!-- account_password 模式联软LV7000 -->
<template v-else-if="integration.config_type === 'account_password'">
<div v-if="integration.config.base_url" class="config-info">URL: {{ integration.config.base_url }}</div>
<div class="config-info">{{ integration.config.api_account_set ? '账号已配置' : '账号未配置' }}</div>
</template>
</template>
</slot>
</div>
<!-- 操作按钮 -->
<div class="integration-actions">
<el-button
v-if="integration.configurable"
size="small"
type="primary"
@click="$emit('configure', integration)"
>
配置
</el-button>
<el-button
v-if="integration.configurable"
size="small"
@click="$emit('test', integration)"
>
测试
</el-button>
<el-button
v-else
size="small"
@click="$emit('view', integration)"
>
查看
</el-button>
</div>
</div>
</template>
<script setup lang="ts">
// ==========================================================================
// 依赖导入
// ==========================================================================
import { computed } from 'vue'
import type { Integration } from '@/types'
// ==========================================================================
// Props / Emits
// ==========================================================================
const props = defineProps<{
/** 集成系统数据 */
integration: Integration
/** 图标组件名 */
icon: string
/** 图标背景色 */
iconBg: string
/** 图标颜色 */
iconColor: string
}>()
defineEmits<{
/** 配置按钮点击 */
configure: [integration: Integration]
/** 测试按钮点击 */
test: [integration: Integration]
/** 查看按钮点击 */
view: [integration: Integration]
}>()
// ==========================================================================
// 计算属性
// ==========================================================================
/** 状态标签类型 */
const statusTagType = computed(() => {
switch (props.integration.status) {
case 'connected': return 'success'
case 'partial': return 'warning'
case 'disconnected': return 'info'
case 'pending': return 'info'
default: return 'info'
}
})
/** 状态文本 */
const statusText = computed(() => {
switch (props.integration.status) {
case 'connected': return '已连接'
case 'partial': return '部分集成'
case 'disconnected': return '未连接'
case 'pending': return '待确认'
default: return props.integration.status
}
})
</script>
<style scoped>
/* 集成卡片 */
.integration-card {
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
padding: 20px;
text-align: center;
transition: border-color 0.2s;
}
.integration-card:hover {
border-color: var(--border-hover);
}
.integration-card.is-configurable {
border-color: var(--accent);
border-opacity: 0.3;
}
/* 图标 */
.integration-icon {
width: 48px;
height: 48px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 12px;
}
/* 名称 */
.integration-name {
font-size: 14px;
font-weight: 600;
margin-bottom: 4px;
color: var(--text-primary);
}
/* 状态 */
.integration-status {
margin-bottom: 12px;
}
/* 详情 */
.integration-detail {
font-size: 11px;
color: var(--text-muted);
margin-bottom: 12px;
min-height: 32px;
}
.config-info {
margin-bottom: 2px;
}
/* 操作按钮 */
.integration-actions {
display: flex;
justify-content: center;
gap: 8px;
}
</style>
@@ -0,0 +1,170 @@
<!--
=============================================================================
企微IT智能服务台 快速回复卡片组件
=============================================================================
说明快速回复模板卡片显示分类标题内容状态
对于待审核模板显示通过/驳回操作按钮
-->
<template>
<div class="reply-card" :class="{ 'is-pending': reply.status === 'pending_review' }">
<!-- 卡片头部 -->
<div class="reply-card-header">
<div class="reply-card-title">
<el-tag size="small" effect="plain" style="margin-right: 8px">
{{ reply.category }}
</el-tag>
{{ reply.title }}
</div>
<div class="reply-card-status">
<!-- 已审核 -->
<el-tag v-if="reply.status === 'approved'" type="success" size="small">
已审核
</el-tag>
<!-- 待审核 -->
<template v-else-if="reply.status === 'pending_review'">
<div class="review-actions">
<el-button size="small" type="primary" @click="$emit('approve', reply)">
通过
</el-button>
<el-button size="small" @click="$emit('reject', reply)">
驳回
</el-button>
</div>
</template>
<!-- 已驳回 -->
<el-tag v-else-if="reply.status === 'rejected'" type="danger" size="small">
已驳回
</el-tag>
<!-- 草稿 -->
<el-tag v-else type="info" size="small">
草稿
</el-tag>
</div>
</div>
<!-- 内容预览 -->
<div class="reply-card-content">
{{ reply.content }}
</div>
<!-- 底部信息 -->
<div class="reply-card-footer">
<template v-if="reply.variables && reply.variables.length > 0">
<span class="footer-label">变量:</span>
<span class="footer-value">{{ reply.variables.join(', ') }}</span>
<span class="footer-sep">·</span>
</template>
<template v-if="reply.submitted_by_name">
<span class="footer-label">提交人:</span>
<span class="footer-value">{{ reply.submitted_by_name }}</span>
<span class="footer-sep">·</span>
</template>
<span class="footer-label">版本:</span>
<span class="footer-value">v{{ reply.version }}</span>
<span class="footer-sep">·</span>
<span class="footer-label">更新:</span>
<span class="footer-value">{{ formatDate(reply.updated_at) }}</span>
</div>
</div>
</template>
<script setup lang="ts">
// ==========================================================================
// 依赖导入
// ==========================================================================
import type { QuickReplyTemplate } from '@/types'
// ==========================================================================
// Props / Emits
// ==========================================================================
defineProps<{
/** 快速回复模板数据 */
reply: QuickReplyTemplate
}>()
defineEmits<{
/** 通过审核 */
approve: [reply: QuickReplyTemplate]
/** 驳回 */
reject: [reply: QuickReplyTemplate]
}>()
// ==========================================================================
// 工具方法
// ==========================================================================
/** 格式化日期 */
function formatDate(dateStr: string): string {
if (!dateStr) return '-'
try {
const d = new Date(dateStr)
return d.toLocaleDateString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit' })
} catch {
return dateStr
}
}
</script>
<style scoped>
/* 卡片 */
.reply-card {
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 14px 16px;
margin-bottom: 10px;
transition: border-color 0.2s;
}
.reply-card:hover {
border-color: var(--border-hover);
}
.reply-card.is-pending {
border-color: var(--warning);
}
/* 卡片头部 */
.reply-card-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
}
.reply-card-title {
font-size: 13px;
font-weight: 600;
color: var(--text-primary);
display: flex;
align-items: center;
}
/* 内容预览 */
.reply-card-content {
font-size: 12px;
color: var(--text-secondary);
line-height: 1.6;
background: var(--bg-primary);
padding: 10px;
border-radius: 6px;
margin-bottom: 8px;
}
/* 底部信息 */
.reply-card-footer {
font-size: 11px;
color: var(--text-muted);
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 2px;
}
.footer-label {
color: var(--text-muted);
}
.footer-value {
color: var(--text-secondary);
}
.footer-sep {
margin: 0 4px;
color: var(--text-muted);
}
</style>
+243
View File
@@ -0,0 +1,243 @@
<!--
=============================================================================
企微IT智能服务台 全局搜索组件
=============================================================================
说明全局搜索组件支持搜索配置项坐席快速回复
回车或点搜索图标触发搜索结果以下拉面板展示
-->
<template>
<div class="search-box-wrapper">
<el-popover
:visible="showResults"
placement="bottom-end"
:width="320"
trigger="manual"
:popper-style="{ background: 'var(--bg-secondary)', border: '1px solid var(--border)' }"
:show-arrow="false"
>
<template #reference>
<div class="search-box" @click="showResults = searchText.length > 0">
<el-icon :size="14" style="color: var(--text-muted)"><Search /></el-icon>
<input
v-model="searchText"
type="text"
placeholder="搜索功能或配置..."
class="search-input"
@keydown.enter="handleSearch"
@input="handleInput"
/>
<el-icon
v-if="searchText"
:size="14"
style="color: var(--text-muted); cursor: pointer"
@click="clearSearch"
>
<Close />
</el-icon>
</div>
</template>
<!-- 搜索结果 -->
<div class="search-results">
<div v-if="loading" class="search-loading">搜索中...</div>
<template v-else-if="results.length > 0">
<div class="search-result-count">找到 {{ results.length }} 条结果</div>
<div
v-for="item in results"
:key="item.id"
class="search-result-item"
@click="navigateTo(item.route)"
>
<el-tag :type="getResultTagType(item.type)" size="small" effect="plain">
{{ getResultTypeText(item.type) }}
</el-tag>
<span class="result-name">{{ item.name }}</span>
</div>
</template>
<div v-else-if="searched" class="search-empty">暂无匹配结果</div>
</div>
</el-popover>
</div>
</template>
<script setup lang="ts">
// ==========================================================================
// 依赖导入
// ==========================================================================
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import type { SearchResultItem } from '@/types'
import { globalSearch } from '@/api/admin'
// ==========================================================================
// 路由
// ==========================================================================
const router = useRouter()
// ==========================================================================
// 状态
// ==========================================================================
/** 搜索文本 */
const searchText = ref<string>('')
/** 是否显示搜索结果 */
const showResults = ref<boolean>(false)
/** 搜索结果 */
const results = ref<SearchResultItem[]>([])
/** 是否已搜索过 */
const searched = ref<boolean>(false)
/** 是否正在加载 */
const loading = ref<boolean>(false)
// ==========================================================================
// 方法
// ==========================================================================
/** 执行搜索 */
async function handleSearch(): Promise<void> {
const query = searchText.value.trim()
if (!query) {
clearSearch()
return
}
loading.value = true
searched.value = true
showResults.value = true
try {
const response = await globalSearch(query)
results.value = response.data.data.items
} catch {
results.value = []
} finally {
loading.value = false
}
}
/** 输入处理 */
function handleInput(): void {
if (searchText.value.length > 0) {
showResults.value = searched.value
} else {
showResults.value = false
results.value = []
searched.value = false
}
}
/** 清除搜索 */
function clearSearch(): void {
searchText.value = ''
showResults.value = false
results.value = []
searched.value = false
}
/** 导航到搜索结果 */
function navigateTo(routePath: string): void {
showResults.value = false
router.push(routePath)
}
/** 获取结果标签类型 */
function getResultTagType(type: string): string {
switch (type) {
case 'config': return 'primary'
case 'agent': return 'success'
case 'quick_reply': return 'warning'
default: return 'info'
}
}
/** 获取结果类型文本 */
function getResultTypeText(type: string): string {
switch (type) {
case 'config': return '配置'
case 'agent': return '坐席'
case 'quick_reply': return '回复'
default: return type
}
}
</script>
<style scoped>
/* 搜索框容器 */
.search-box-wrapper {
position: relative;
}
/* 搜索框 */
.search-box {
display: flex;
align-items: center;
background: var(--bg-primary);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 0 10px;
gap: 6px;
}
.search-box:focus-within {
border-color: var(--accent);
}
/* 输入框 */
.search-input {
background: transparent;
border: none;
color: var(--text-primary);
padding: 6px 0;
font-size: 13px;
outline: none;
width: 180px;
}
.search-input::placeholder {
color: var(--text-muted);
}
/* 搜索结果 */
.search-results {
max-height: 300px;
overflow-y: auto;
}
.search-loading,
.search-empty {
padding: 16px;
text-align: center;
color: var(--text-muted);
font-size: 13px;
}
.search-result-count {
padding: 8px 12px;
font-size: 11px;
color: var(--text-muted);
border-bottom: 1px solid var(--border);
}
.search-result-item {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
cursor: pointer;
transition: background 0.2s;
border-bottom: 1px solid var(--border);
}
.search-result-item:last-child {
border-bottom: none;
}
.search-result-item:hover {
background: var(--bg-tertiary);
}
.result-name {
font-size: 13px;
color: var(--text-primary);
}
</style>
+216
View File
@@ -0,0 +1,216 @@
<!--
=============================================================================
企微IT智能服务台 侧边栏导航组件
=============================================================================
说明管理后台侧边栏导航深色背景菜单分组展示
- 📋 运营管理运营总览功能开关坐席管理
- 🔗 系统集成系统集成
- 运营配置快速回复分配模式排查流程图
- 📊 监控与数据会话监控
- 🔒 开发中P2占位灰化+锁图标主题模板数据看板知识库管理
-->
<template>
<div class="sidebar">
<!-- Logo 区域 -->
<div class="sidebar-logo">
<div class="sidebar-logo-icon">
<el-icon :size="16"><Headset /></el-icon>
</div>
<div>
<div class="sidebar-logo-text">IT智能服务台</div>
<div class="sidebar-logo-sub">管理后台 v1.0</div>
</div>
</div>
<!-- 导航菜单 -->
<el-menu
:default-active="activeMenu"
:router="true"
:collapse="false"
class="sidebar-menu"
background-color="var(--bg-secondary)"
text-color="var(--text-secondary)"
active-text-color="var(--accent)"
>
<!-- 📋 运营管理 -->
<div class="menu-section-title">📋 运营管理</div>
<el-menu-item index="/dashboard">
<el-icon><PieChart /></el-icon>
<span>运营总览</span>
</el-menu-item>
<el-menu-item index="/configs">
<el-icon><Switch /></el-icon>
<span>功能开关</span>
</el-menu-item>
<el-menu-item index="/agents">
<el-icon><UserFilled /></el-icon>
<span>坐席管理</span>
</el-menu-item>
<el-menu-item index="/roles">
<el-icon><Key /></el-icon>
<span>角色管理</span>
</el-menu-item>
<!-- 🔗 系统集成 -->
<div class="menu-section-title">🔗 系统集成</div>
<el-menu-item index="/integrations">
<el-icon><Connection /></el-icon>
<span>系统集成</span>
</el-menu-item>
<el-menu-item index="/terminal-security">
<el-icon><Warning /></el-icon>
<span>终端安全</span>
</el-menu-item>
<!-- 运营配置 -->
<div class="menu-section-title"> 运营配置</div>
<el-menu-item index="/quick-replies">
<el-icon><ChatLineSquare /></el-icon>
<span>快速回复</span>
</el-menu-item>
<el-menu-item index="/assignment-mode">
<el-icon><Sort /></el-icon>
<span>分配模式</span>
</el-menu-item>
<el-menu-item index="/flowcharts">
<el-icon><Share /></el-icon>
<span>排查流程图</span>
</el-menu-item>
<!-- 📊 监控与数据 -->
<div class="menu-section-title">📊 监控与数据</div>
<el-menu-item index="/monitor">
<el-icon><Monitor /></el-icon>
<span>会话监控</span>
</el-menu-item>
<el-menu-item index="/session-audit">
<el-icon><Document /></el-icon>
<span>会话审计</span>
</el-menu-item>
<el-menu-item index="/agent-performance">
<el-icon><TrendCharts /></el-icon>
<span>坐席绩效</span>
</el-menu-item>
<el-menu-item index="/system-logs">
<el-icon><Notebook /></el-icon>
<span>系统日志</span>
</el-menu-item>
<!-- 🔒 开发中P2占位 -->
<div class="menu-section-title">🔒 开发中</div>
<el-menu-item index="/themes" class="locked-menu-item">
<el-icon><Brush /></el-icon>
<span>主题模板</span>
<el-icon class="lock-icon"><Lock /></el-icon>
</el-menu-item>
<el-menu-item index="/reports" class="locked-menu-item">
<el-icon><DataAnalysis /></el-icon>
<span>数据看板</span>
<el-icon class="lock-icon"><Lock /></el-icon>
</el-menu-item>
<el-menu-item index="/knowledge" class="locked-menu-item">
<el-icon><Reading /></el-icon>
<span>知识库管理</span>
<el-icon class="lock-icon"><Lock /></el-icon>
</el-menu-item>
</el-menu>
</div>
</template>
<script setup lang="ts">
// ==========================================================================
// 依赖导入
// ==========================================================================
import { computed } from 'vue'
import { useRoute } from 'vue-router'
import { Headset, PieChart, Switch, UserFilled, Connection, Warning, ChatLineSquare, Sort, Share, Monitor, Brush, DataAnalysis, Reading, Lock, Key, Document, TrendCharts, Notebook } from '@element-plus/icons-vue'
// ==========================================================================
// 当前激活菜单
// ==========================================================================
const route = useRoute()
const activeMenu = computed(() => route.path)
</script>
<style scoped>
/* 侧边栏容器 */
.sidebar {
width: 220px;
background: var(--bg-secondary);
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
flex-shrink: 0;
overflow: hidden;
}
/* Logo 区域 */
.sidebar-logo {
padding: 16px 20px;
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
gap: 10px;
}
.sidebar-logo-icon {
width: 32px;
height: 32px;
background: linear-gradient(135deg, #3b82f6, #8b5cf6);
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
color: white;
}
.sidebar-logo-text {
font-size: 15px;
font-weight: 600;
color: var(--text-primary);
}
.sidebar-logo-sub {
font-size: 10px;
color: var(--text-muted);
}
/* 菜单容器 */
.sidebar-menu {
flex: 1;
overflow-y: auto;
border-right: none;
}
/* 菜单分组标题 */
.menu-section-title {
padding: 12px 20px 6px;
font-size: 11px;
color: var(--text-muted);
letter-spacing: 0.5px;
user-select: none;
}
/* 菜单项 */
.sidebar-menu .el-menu-item {
height: 40px;
line-height: 40px;
font-size: 13px;
border-left: 3px solid transparent;
margin: 0;
}
.sidebar-menu .el-menu-item.is-active {
background-color: var(--accent-light);
border-left-color: var(--accent);
}
/* 锁定的菜单项(灰化) */
.locked-menu-item {
opacity: 0.5;
pointer-events: auto !important;
}
.lock-icon {
margin-left: auto;
font-size: 12px;
color: var(--text-muted);
}
</style>
+120
View File
@@ -0,0 +1,120 @@
<!--
=============================================================================
企微IT智能服务台 统计卡片组件
=============================================================================
说明深色背景统计卡片显示图标数值标签趋势
用于仪表盘会话监控等页面的统计数据展示
-->
<template>
<div class="stat-card">
<div class="stat-card-header">
<span class="stat-label">{{ label }}</span>
<el-icon v-if="icon" :size="20" :style="{ color: iconColor || 'var(--text-secondary)' }">
<component :is="icon" />
</el-icon>
</div>
<div class="stat-value" :style="{ color: valueColor || 'var(--text-primary)' }">
<slot name="value">
{{ formattedValue }}
</slot>
</div>
<div v-if="trend || subtitle" class="stat-footer" :class="trendClass">
<slot name="footer">
<span v-if="trend">{{ trend }}</span>
<span v-if="subtitle" class="stat-subtitle">{{ subtitle }}</span>
</slot>
</div>
</div>
</template>
<script setup lang="ts">
// ==========================================================================
// Props 定义
// ==========================================================================
import { computed } from 'vue'
const props = defineProps<{
/** 统计卡片标签 */
label: string
/** 统计数值 */
value?: string | number
/** 数值颜色 */
valueColor?: string
/** 图标组件名 */
icon?: string
/** 图标颜色 */
iconColor?: string
/** 趋势文本 */
trend?: string
/** 趋势方向:up/down */
trendDirection?: 'up' | 'down' | 'none'
/** 副标题 */
subtitle?: string
}>()
// ==========================================================================
// 计算属性
// ==========================================================================
/** 格式化后的数值 */
const formattedValue = computed(() => props.value ?? '-')
/** 趋势样式类 */
const trendClass = computed(() => {
if (props.trendDirection === 'up') return 'trend-up'
if (props.trendDirection === 'down') return 'trend-down'
return ''
})
</script>
<style scoped>
/* 统计卡片 */
.stat-card {
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
padding: 20px;
transition: border-color 0.2s;
}
.stat-card:hover {
border-color: var(--border-hover);
}
/* 卡片头部(标签 + 图标) */
.stat-card-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
}
/* 标签 */
.stat-label {
font-size: 12px;
color: var(--text-secondary);
}
/* 数值 */
.stat-value {
font-size: 28px;
font-weight: 700;
margin-bottom: 4px;
}
/* 尾部 */
.stat-footer {
font-size: 11px;
}
/* 趋势 */
.trend-up {
color: var(--success);
}
.trend-down {
color: var(--danger);
}
.stat-subtitle {
color: var(--text-muted);
}
</style>