chore: initial baseline with P0-safety .gitignore
This commit is contained in:
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user