Files
urbanLifeline/江西城市生命线-可交互原型/frontend/src/views/AdminView.vue

4052 lines
142 KiB
Vue
Raw Normal View History

2025-12-12 18:32:14 +08:00
<template>
<div class="admin-system">
<!-- 内容区域 -->
<div class="admin-container">
<!-- 左侧主导航 -->
<aside class="admin-sidebar-main">
<div class="sidebar-header">
<div class="logo" @click="backToMain">
<img src="/logo.jpg" alt="Logo" class="logo-img" />
<span class="logo-text">管理后台</span>
</div>
</div>
<nav class="main-nav">
<div class="section-title">系统管理</div>
<div
v-for="item in navItems"
:key="item.key"
class="main-nav-item"
:class="{ active: activeNav === item.key }"
@click="activeNav = item.key"
>
<el-icon>
<Setting v-if="item.key === 'platform'" />
<Document v-else-if="item.key === 'bidding'" />
<Service v-else-if="item.key === 'service'" />
</el-icon>
<span>{{ item.label }}</span>
</div>
<div class="nav-divider"></div>
<div class="section-title">个人设置</div>
<div
class="main-nav-item"
:class="{ active: activeNav === 'profile' }"
@click="activeNav = 'profile'"
>
<el-icon><User /></el-icon>
<span>个人中心</span>
</div>
</nav>
<!-- 用户信息和返回按钮 -->
<div class="sidebar-footer">
<div class="user-section">
<div class="user-avatar" @click="goToProfile">
<img src="/avatar.svg" alt="User" />
</div>
<span class="user-name">李志鹏</span>
<el-tooltip content="返回主系统" placement="top">
<div class="back-icon" @click="backToMain">
<el-icon><Back /></el-icon>
</div>
</el-tooltip>
</div>
</div>
</aside>
<!-- 左侧子级导航根据不同系统显示不同内容 -->
<aside class="admin-sidebar-sub">
<!-- 平台管理后台的子导航 -->
<nav v-if="activeNav === 'platform'" class="sub-nav">
<div class="sub-nav-title">平台管理</div>
<div
v-for="item in platformSubNav"
:key="item.key"
class="sub-nav-item"
:class="{ active: activeSubNav === item.key }"
@click="activeSubNav = item.key"
>
<el-icon>
<DataLine v-if="item.key === 'dashboard'" />
<User v-else-if="item.key === 'users'" />
<Document v-else-if="item.key === 'knowledge'" />
<Setting v-else-if="item.key === 'settings'" />
</el-icon>
<span>{{ item.label }}</span>
</div>
</nav>
<!-- 智能标书管理后台的子导航 -->
<nav v-if="activeNav === 'bidding'" class="sub-nav">
<div class="sub-nav-title">标书管理</div>
<div
v-for="item in biddingSubNav"
:key="item.key"
class="sub-nav-item"
:class="{ active: activeSubNav === item.key }"
@click="activeSubNav = item.key"
>
<el-icon>
<DataLine v-if="item.key === 'dashboard'" />
<Document v-else-if="item.key === 'projects'" />
<Files v-else-if="item.key === 'templates'" />
<Setting v-else-if="item.key === 'settings'" />
</el-icon>
<span>{{ item.label }}</span>
</div>
</nav>
<!-- 泰豪小电管理后台的子导航 -->
<nav v-if="activeNav === 'service'" class="sub-nav">
<div class="sub-nav-title">客服管理</div>
<template v-for="item in serviceSubNav" :key="item.key">
<!-- 普通导航项 -->
<div
v-if="!item.children"
class="sub-nav-item"
:class="{ active: activeSubNav === item.key }"
@click="activeSubNav = item.key"
>
<el-icon>
<DataLine v-if="item.icon === 'DataLine'" />
<Document v-else-if="item.icon === 'Document'" />
<Tickets v-else-if="item.icon === 'Tickets'" />
<ChatDotRound v-else-if="item.icon === 'ChatDotRound'" />
<Service v-else-if="item.icon === 'Service'" />
<List v-else-if="item.icon === 'List'" />
<User v-else-if="item.icon === 'User'" />
</el-icon>
<span>{{ item.label }}</span>
</div>
<!-- 带子级的导航项 -->
<div v-else class="sub-nav-group">
<div
class="sub-nav-item has-children"
:class="{ expanded: logNavExpanded }"
@click="logNavExpanded = !logNavExpanded"
>
<el-icon><List /></el-icon>
<span>{{ item.label }}</span>
<el-icon class="arrow-icon"><ArrowDown /></el-icon>
</div>
<div v-show="logNavExpanded" class="sub-nav-children">
<div
v-for="child in item.children"
:key="child.key"
class="sub-nav-child-item"
:class="{ active: activeSubNav === child.key }"
@click="activeSubNav = child.key"
>
<span>{{ child.label }}</span>
</div>
</div>
</div>
</template>
</nav>
</aside>
<!-- 主内容区 -->
<div class="admin-main">
<!-- 平台管理后台 -->
<div v-if="activeNav === 'platform'" class="admin-content">
<div class="content-header">
<h1>平台管理后台</h1>
<p class="subtitle">AI数智化平台系统管理</p>
</div>
<div class="dashboard-cards">
<div class="stat-card">
<div class="stat-icon" style="background: #409eff;">
<el-icon><User /></el-icon>
</div>
<div class="stat-info">
<div class="stat-label">用户总数</div>
<div class="stat-value">1,248</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon" style="background: #67c23a;">
<el-icon><Document /></el-icon>
</div>
<div class="stat-info">
<div class="stat-label">知识库文档</div>
<div class="stat-value">856</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon" style="background: #e6a23c;">
<el-icon><ChatDotRound /></el-icon>
</div>
<div class="stat-info">
<div class="stat-label">今日对话</div>
<div class="stat-value">342</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon" style="background: #f56c6c;">
<el-icon><Warning /></el-icon>
</div>
<div class="stat-info">
<div class="stat-label">待处理告警</div>
<div class="stat-value">12</div>
</div>
</div>
</div>
<el-card style="margin-top: 24px;">
<template #header>
<div class="card-header">
<span>系统配置</span>
<el-button type="primary" size="small">保存配置</el-button>
</div>
</template>
<el-form label-width="150px">
<el-form-item label="系统名称">
<el-input v-model="platformConfig.systemName" />
</el-form-item>
<el-form-item label="AI模型">
<el-select v-model="platformConfig.aiModel" style="width: 100%;">
<el-option label="GPT-4" value="gpt-4" />
<el-option label="Claude-3" value="claude-3" />
<el-option label="文心一言" value="ernie" />
</el-select>
</el-form-item>
<el-form-item label="最大并发数">
<el-input-number v-model="platformConfig.maxConcurrency" :min="1" :max="1000" />
</el-form-item>
<el-form-item label="会话超时(分钟)">
<el-input-number v-model="platformConfig.sessionTimeout" :min="5" :max="120" />
</el-form-item>
</el-form>
</el-card>
</div>
<!-- 智能标书管理后台 -->
<div v-if="activeNav === 'bidding'" class="admin-content">
<div class="content-header">
<h1>智能标书管理后台</h1>
<p class="subtitle">招标助手系统管理</p>
</div>
<div class="dashboard-cards">
<div class="stat-card">
<div class="stat-icon" style="background: #409eff;">
<el-icon><Document /></el-icon>
</div>
<div class="stat-info">
<div class="stat-label">标书总数</div>
<div class="stat-value">486</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon" style="background: #67c23a;">
<el-icon><Select /></el-icon>
</div>
<div class="stat-info">
<div class="stat-label">中标率</div>
<div class="stat-value">78.5%</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon" style="background: #e6a23c;">
<el-icon><Clock /></el-icon>
</div>
<div class="stat-info">
<div class="stat-label">进行中项目</div>
<div class="stat-value">23</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon" style="background: #909399;">
<el-icon><Files /></el-icon>
</div>
<div class="stat-info">
<div class="stat-label">模板库</div>
<div class="stat-value">156</div>
</div>
</div>
</div>
<el-card style="margin-top: 24px;">
<template #header>
<div class="card-header">
<span>标书模板管理</span>
<el-button type="primary" size="small">新增模板</el-button>
</div>
</template>
<el-table :data="biddingTemplates" style="width: 100%">
<el-table-column prop="name" label="模板名称" />
<el-table-column prop="category" label="类别" width="120" />
<el-table-column prop="usage" label="使用次数" width="100" align="center" />
<el-table-column prop="updateTime" label="更新时间" width="180" />
<el-table-column label="操作" width="150">
<template>
<el-button type="primary" link size="small">编辑</el-button>
<el-button type="danger" link size="small">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
</div>
<!-- 泰豪小电管理后台 -->
<div v-if="activeNav === 'service'" class="admin-content">
<!-- 运营看板 -->
<template v-if="activeSubNav === 'dashboard'">
<div class="content-header">
<h1>数据概览</h1>
<p class="subtitle">泰豪小电智能客服运营数据</p>
</div>
<!-- 核心指标卡片 -->
<div class="dashboard-stats">
<div class="stat-card clickable" @click="activeSubNav = 'conversation'">
<div class="stat-icon" style="background: linear-gradient(135deg, #409eff, #66b1ff);">
<el-icon><ChatDotRound /></el-icon>
</div>
<div class="stat-info">
<div class="stat-value">1,258</div>
<div class="stat-label">咨询次数</div>
<div class="stat-trend up">
<el-icon><Top /></el-icon>
<span>较昨日 +12.5%</span>
</div>
</div>
</div>
<div class="stat-card clickable" @click="goToTickets('pending')">
<div class="stat-icon" style="background: linear-gradient(135deg, #e6a23c, #f5c06a);">
<el-icon><Clock /></el-icon>
</div>
<div class="stat-info">
<div class="stat-value">{{ dashboardData.pendingTickets }}</div>
<div class="stat-label">待处理工单</div>
<div class="stat-trend">
<span>点击查看详情</span>
<el-icon><Right /></el-icon>
</div>
</div>
</div>
<div class="stat-card clickable" @click="goToTickets('completed')">
<div class="stat-icon" style="background: linear-gradient(135deg, #67c23a, #95d475);">
<el-icon><Select /></el-icon>
</div>
<div class="stat-info">
<div class="stat-value">{{ dashboardData.completedTickets }}</div>
<div class="stat-label">已处理工单</div>
<div class="stat-trend">
<span>点击查看详情</span>
<el-icon><Right /></el-icon>
</div>
</div>
</div>
</div>
<!-- 问题分类统计和产品词云 -->
<div class="dashboard-charts">
<el-card class="chart-card">
<template #header>
<div class="card-header">
<span>问题分类统计</span>
<el-radio-group v-model="questionStatPeriod" size="small">
<el-radio-button label="today">今日</el-radio-button>
<el-radio-button label="week">本周</el-radio-button>
<el-radio-button label="month">本月</el-radio-button>
</el-radio-group>
</div>
</template>
<div class="question-stats">
<div
v-for="item in questionCategories"
:key="item.name"
class="question-stat-item clickable"
@click="goToConversationByCategory(item.name)"
>
<div class="stat-bar-header">
<span class="stat-name">{{ item.name }}</span>
<span class="stat-count">{{ item.count }} </span>
</div>
<div class="stat-bar">
<div
class="stat-bar-fill"
:style="{ width: item.percent + '%', background: item.color }"
></div>
</div>
</div>
</div>
</el-card>
<el-card class="chart-card">
<template #header>
<div class="card-header">
<span>涉及产品词云</span>
<el-button type="primary" link size="small" @click="activeSubNav = 'conversation'">
查看详情
<el-icon><Right /></el-icon>
</el-button>
</div>
</template>
<div class="product-cloud">
<span
v-for="(product, index) in productCloudData"
:key="index"
class="cloud-tag"
:style="{
fontSize: product.size + 'px',
color: product.color,
opacity: 0.7 + product.weight * 0.3
}"
@click="goToConversationByProduct(product.name)"
>
{{ product.name }}
</span>
</div>
</el-card>
</div>
<!-- 快捷入口 -->
<el-card class="quick-entry-card">
<template #header>
<span>快捷入口</span>
</template>
<div class="quick-entries">
<div class="quick-entry-item" @click="activeSubNav = 'knowledge'">
<el-icon class="entry-icon" style="background: #409eff;"><Document /></el-icon>
<span>知识库管理</span>
</div>
<div class="quick-entry-item" @click="activeSubNav = 'tickets'">
<el-icon class="entry-icon" style="background: #e6a23c;"><Tickets /></el-icon>
<span>工单管理</span>
</div>
<div class="quick-entry-item" @click="activeSubNav = 'conversation'">
<el-icon class="entry-icon" style="background: #67c23a;"><ChatDotRound /></el-icon>
<span>对话数据</span>
</div>
<div class="quick-entry-item" @click="activeSubNav = 'agent'">
<el-icon class="entry-icon" style="background: #909399;"><Service /></el-icon>
<span>智能体管理</span>
</div>
<div class="quick-entry-item" @click="activeSubNav = 'crm'">
<el-icon class="entry-icon" style="background: #f56c6c;"><User /></el-icon>
<span>CRM管理</span>
</div>
</div>
</el-card>
</template>
<!-- 知识库管理 -->
<template v-if="activeSubNav === 'knowledge'">
<div class="content-header">
<div class="header-title">
<h1>知识库管理</h1>
<p class="subtitle">管理外部和内部知识库文档</p>
</div>
<el-button type="primary" @click="showKbUploadDialog = true">
<el-icon><Upload /></el-icon>
上传文档
</el-button>
</div>
<el-card>
<el-tabs v-model="activeKbType">
<el-tab-pane label="外部知识库" name="external">
<p class="tab-desc">面向客户的知识库内容包含设备操作指南故障解决方案等</p>
<div class="kb-categories">
<div
v-for="cat in externalKbCategories"
:key="cat.key"
class="kb-category-card"
:class="{ active: activeExternalKbCat === cat.key }"
@click="activeExternalKbCat = cat.key"
>
<el-icon :style="{ color: cat.color }"><component :is="cat.icon" /></el-icon>
<span class="cat-name">{{ cat.name }}</span>
<span class="cat-count">{{ cat.count }} 个文件</span>
</div>
</div>
<div class="kb-files-section">
<div class="section-toolbar">
<h3>{{ currentExternalCatName }}</h3>
<el-input
v-model="externalKbSearch"
placeholder="搜索文件名"
style="width: 240px;"
:prefix-icon="Search"
clearable
/>
</div>
<el-table :data="filteredExternalKbFiles" style="width: 100%">
<el-table-column prop="name" label="文件名" min-width="280">
<template #default="{ row }">
<div class="file-name-cell">
<el-icon class="file-icon"><Document /></el-icon>
<span>{{ row.name }}</span>
</div>
</template>
</el-table-column>
<el-table-column prop="uploader" label="上传人员" width="120" />
<el-table-column prop="uploadTime" label="上传时间" width="180" />
<el-table-column label="操作" width="180" align="center">
<template #default="{ row }">
<el-button type="primary" link size="small" @click="previewKbFile(row)">
<el-icon><View /></el-icon>预览
</el-button>
<el-button type="success" link size="small" @click="downloadKbFile(row)">
<el-icon><Download /></el-icon>下载
</el-button>
<el-button type="danger" link size="small" @click="deleteKbFile(row, 'external')">
<el-icon><Delete /></el-icon>删除
</el-button>
</template>
</el-table-column>
</el-table>
</div>
</el-tab-pane>
<el-tab-pane label="内部知识库" name="internal">
<p class="tab-desc">内部技术资料与服务规范仅供内部员工使用</p>
<div class="kb-categories">
<div
v-for="cat in internalKbCategories"
:key="cat.key"
class="kb-category-card"
:class="{ active: activeInternalKbCat === cat.key }"
@click="activeInternalKbCat = cat.key"
>
<el-icon :style="{ color: cat.color }"><component :is="cat.icon" /></el-icon>
<span class="cat-name">{{ cat.name }}</span>
<span class="cat-count">{{ cat.count }} 个文件</span>
</div>
</div>
<div class="kb-files-section">
<div class="section-toolbar">
<h3>{{ currentInternalCatName }}</h3>
<el-input
v-model="internalKbSearch"
placeholder="搜索文件名"
style="width: 240px;"
:prefix-icon="Search"
clearable
/>
</div>
<el-table :data="filteredInternalKbFiles" style="width: 100%">
<el-table-column prop="name" label="文件名" min-width="280">
<template #default="{ row }">
<div class="file-name-cell">
<el-icon class="file-icon"><Document /></el-icon>
<span>{{ row.name }}</span>
</div>
</template>
</el-table-column>
<el-table-column prop="uploader" label="上传人员" width="120" />
<el-table-column prop="uploadTime" label="上传时间" width="180" />
<el-table-column label="操作" width="180" align="center">
<template #default="{ row }">
<el-button type="primary" link size="small" @click="previewKbFile(row)">
<el-icon><View /></el-icon>预览
</el-button>
<el-button type="success" link size="small" @click="downloadKbFile(row)">
<el-icon><Download /></el-icon>下载
</el-button>
<el-button type="danger" link size="small" @click="deleteKbFile(row, 'internal')">
<el-icon><Delete /></el-icon>删除
</el-button>
</template>
</el-table-column>
</el-table>
</div>
</el-tab-pane>
</el-tabs>
</el-card>
</template>
<!-- 工单管理 -->
<template v-if="activeSubNav === 'tickets'">
<div class="content-header">
<div class="header-title">
<h1>工单管理</h1>
<p class="subtitle">查看和处理客户服务工单</p>
</div>
<el-button type="primary" @click="showCreateTicketDialog = true">
<el-icon><Plus /></el-icon>
创建工单
</el-button>
</div>
<!-- 筛选区 -->
<el-card class="filter-card">
<div class="ticket-filters">
<el-radio-group v-model="ticketStatusFilter" @change="filterTickets">
<el-radio-button label="all">全部</el-radio-button>
<el-radio-button label="pending">
待处理 <el-badge :value="ticketCounts.pending" :max="99" />
</el-radio-button>
<el-radio-button label="processing">处理中</el-radio-button>
<el-radio-button label="completed">已完成</el-radio-button>
<el-radio-button label="closed">已关闭</el-radio-button>
</el-radio-group>
<div class="filter-right">
<el-select v-model="ticketTypeFilter" placeholder="故障类型" clearable style="width: 140px;">
<el-option label="电气系统故障" value="electrical" />
<el-option label="机械故障" value="mechanical" />
<el-option label="控制系统故障" value="control" />
<el-option label="配件更换" value="parts" />
<el-option label="安装调试" value="install" />
<el-option label="其他" value="other" />
</el-select>
<el-select v-model="ticketUrgencyFilter" placeholder="紧急程度" clearable style="width: 120px;">
<el-option label="紧急" value="urgent" />
<el-option label="普通" value="normal" />
<el-option label="低" value="low" />
</el-select>
<el-input
v-model="ticketSearchKeyword"
placeholder="搜索工单号/客户/设备"
style="width: 200px;"
:prefix-icon="Search"
clearable
/>
</div>
</div>
</el-card>
<!-- 工单列表 -->
<el-card>
<el-table :data="filteredTicketList" style="width: 100%">
<el-table-column prop="ticketNo" label="工单号" width="140">
<template #default="{ row }">
<span class="ticket-no">{{ row.ticketNo }}</span>
</template>
</el-table-column>
<el-table-column prop="customerName" label="客户信息" width="150">
<template #default="{ row }">
<div class="customer-info">
<span class="name">{{ row.customerName }}</span>
<span class="phone">{{ row.customerPhone }}</span>
</div>
</template>
</el-table-column>
<el-table-column prop="faultType" label="故障类型" width="120">
<template #default="{ row }">
<el-tag size="small">{{ row.faultTypeName }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="deviceModel" label="设备型号" width="120" />
<el-table-column prop="urgency" label="紧急程度" width="100">
<template #default="{ row }">
<el-tag
:type="row.urgency === 'urgent' ? 'danger' : row.urgency === 'normal' ? 'warning' : 'info'"
size="small"
>
{{ row.urgencyName }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag
:type="getTicketStatusType(row.status)"
size="small"
>
{{ row.statusName }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="assignee" label="处理人" width="100">
<template #default="{ row }">
<span v-if="row.assignee">{{ row.assignee }}</span>
<span v-else class="unassigned">未指派</span>
</template>
</el-table-column>
<el-table-column prop="source" label="来源" width="100">
<template #default="{ row }">
<el-tag :type="row.source === 'chat' ? 'success' : ''" size="small" effect="plain">
{{ row.source === 'chat' ? '小电对话' : '手动创建' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="160" />
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<el-button type="primary" link size="small" @click="viewTicketDetail(row)">
详情
</el-button>
<el-button
v-if="row.source === 'chat'"
type="success"
link
size="small"
@click="viewTicketConversation(row)"
>
对话
</el-button>
<el-button
v-if="row.status === 'pending'"
type="warning"
link
size="small"
@click="showAssignDialog(row)"
>
指派
</el-button>
<el-button
v-if="row.status === 'processing'"
type="success"
link
size="small"
@click="completeTicket(row)"
>
完成
</el-button>
</template>
</el-table-column>
</el-table>
<div class="table-pagination">
<el-pagination
v-model:current-page="ticketCurrentPage"
:page-size="10"
:total="ticketTotal"
layout="total, prev, pager, next"
/>
</div>
</el-card>
</template>
<!-- 创建工单弹窗 -->
<el-dialog v-model="showCreateTicketDialog" title="创建工单" width="650px">
<el-form :model="ticketForm" label-width="90px">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="客户姓名" required>
<el-input v-model="ticketForm.customerName" placeholder="请输入客户姓名" :prefix-icon="User" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="联系电话" required>
<el-input v-model="ticketForm.customerPhone" placeholder="请输入联系电话" :prefix-icon="Phone" />
</el-form-item>
</el-col>
</el-row>
<el-form-item label="设备型号" required>
<el-select v-model="ticketForm.deviceModel" placeholder="请选择设备型号" style="width: 100%;">
<el-option label="TH-500GF" value="TH-500GF" />
<el-option label="TH-300D" value="TH-300D" />
<el-option label="TH-800GF" value="TH-800GF" />
<el-option label="S-200X" value="S-200X" />
<el-option label="S-150X" value="S-150X" />
<el-option label="G-100S" value="G-100S" />
<el-option label="G-200S" value="G-200S" />
</el-select>
</el-form-item>
<el-form-item label="故障类型" required>
<el-select v-model="ticketForm.faultType" placeholder="请选择故障类型" style="width: 100%;">
<el-option label="电气系统故障" value="electrical" />
<el-option label="机械故障" value="mechanical" />
<el-option label="控制系统故障" value="control" />
<el-option label="配件更换" value="parts" />
<el-option label="安装调试" value="install" />
<el-option label="其他" value="other" />
</el-select>
</el-form-item>
<el-form-item label="故障描述" required>
<el-input
v-model="ticketForm.faultDesc"
type="textarea"
:rows="4"
placeholder="请详细描述故障现象..."
/>
</el-form-item>
<el-form-item label="紧急程度" required>
<el-select v-model="ticketForm.urgency" placeholder="请选择紧急程度" style="width: 100%;">
<el-option label="紧急" value="urgent" />
<el-option label="普通" value="normal" />
<el-option label="低" value="low" />
</el-select>
</el-form-item>
<el-form-item label="现场地址">
<el-input v-model="ticketForm.address" placeholder="请输入现场详细地址" />
</el-form-item>
<el-form-item label="故障照片">
<el-upload
action="#"
list-type="picture-card"
:auto-upload="false"
:limit="9"
>
<el-icon><Plus /></el-icon>
<template #tip>
<div class="el-upload__tip">支持jpgpng格式最多上传9张</div>
</template>
</el-upload>
</el-form-item>
<el-form-item label="设备铭牌">
<el-input v-model="ticketForm.nameplate" placeholder="请输入设备铭牌信息" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="showCreateTicketDialog = false">取消</el-button>
<el-button type="primary" @click="submitCreateTicket">提交工单</el-button>
</template>
</el-dialog>
<!-- 工单详情弹窗 -->
<el-dialog v-model="showTicketDetailDialog" title="工单详情" width="750px">
<div class="ticket-detail" v-if="currentTicket">
<div class="detail-header">
<div class="ticket-info">
<span class="ticket-no">{{ currentTicket.ticketNo }}</span>
<el-tag :type="getTicketStatusType(currentTicket.status)">{{ currentTicket.statusName }}</el-tag>
<el-tag :type="currentTicket.urgency === 'urgent' ? 'danger' : currentTicket.urgency === 'normal' ? 'warning' : 'info'">
{{ currentTicket.urgencyName }}
</el-tag>
</div>
<div class="detail-actions">
<el-button
v-if="currentTicket.source === 'chat'"
type="primary"
size="small"
@click="viewTicketConversation(currentTicket)"
>
<el-icon><ChatDotRound /></el-icon>
查看对话
</el-button>
</div>
</div>
<el-descriptions :column="2" border>
<el-descriptions-item label="客户姓名">{{ currentTicket.customerName }}</el-descriptions-item>
<el-descriptions-item label="联系电话">{{ currentTicket.customerPhone }}</el-descriptions-item>
<el-descriptions-item label="设备型号">{{ currentTicket.deviceModel }}</el-descriptions-item>
<el-descriptions-item label="故障类型">{{ currentTicket.faultTypeName }}</el-descriptions-item>
<el-descriptions-item label="现场地址" :span="2">{{ currentTicket.address || '-' }}</el-descriptions-item>
<el-descriptions-item label="故障描述" :span="2">{{ currentTicket.faultDesc }}</el-descriptions-item>
<el-descriptions-item label="设备铭牌" :span="2">{{ currentTicket.nameplate || '-' }}</el-descriptions-item>
<el-descriptions-item label="工单来源">
<el-tag :type="currentTicket.source === 'chat' ? 'success' : ''" size="small">
{{ currentTicket.source === 'chat' ? '小电对话' : '手动创建' }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="处理人">{{ currentTicket.assignee || '未指派' }}</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ currentTicket.createTime }}</el-descriptions-item>
<el-descriptions-item label="更新时间">{{ currentTicket.updateTime }}</el-descriptions-item>
</el-descriptions>
<!-- 故障照片 -->
<div class="detail-section" v-if="currentTicket.images && currentTicket.images.length > 0">
<h4>
<el-icon><Picture /></el-icon>
故障照片
</h4>
<div class="ticket-images">
<el-image
v-for="(img, index) in currentTicket.images"
:key="index"
:src="img"
:preview-src-list="currentTicket.images"
:initial-index="index"
fit="cover"
class="ticket-image-item"
>
<template #error>
<div class="image-error">
<el-icon><Picture /></el-icon>
</div>
</template>
</el-image>
</div>
</div>
<div class="detail-section">
<h4>处理记录</h4>
<el-timeline>
<el-timeline-item
v-for="(log, index) in currentTicket.logs"
:key="index"
:timestamp="log.time"
:type="log.type"
>
<div class="log-content">
<span class="log-operator">{{ log.operator }}</span>
<span class="log-action">{{ log.action }}</span>
</div>
<div v-if="log.remark" class="log-remark">{{ log.remark }}</div>
</el-timeline-item>
</el-timeline>
</div>
</div>
<template #footer>
<el-button @click="showTicketDetailDialog = false">关闭</el-button>
<el-button v-if="currentTicket?.status === 'pending'" type="warning" @click="showAssignDialogFromDetail">
指派工程师
</el-button>
<el-button v-if="currentTicket?.status === 'processing'" type="success" @click="completeTicket(currentTicket)">
完成工单
</el-button>
</template>
</el-dialog>
<!-- 指派工程师弹窗 -->
<el-dialog v-model="showAssignTicketDialog" title="指派工程师" width="600px">
<div class="assign-dialog">
<div class="ai-recommend" v-if="aiRecommendEngineer">
<div class="recommend-header">
<el-icon><MagicStick /></el-icon>
<span>智能推荐</span>
</div>
<div class="recommend-content">
<div class="engineer-card recommended" @click="selectedEngineer = aiRecommendEngineer.id">
<el-radio :model-value="selectedEngineer" :value="aiRecommendEngineer.id">
<div class="engineer-info">
<el-avatar :size="40">{{ aiRecommendEngineer.name.charAt(0) }}</el-avatar>
<div class="info-text">
<div class="name">{{ aiRecommendEngineer.name }}</div>
<div class="title">{{ aiRecommendEngineer.title }}</div>
</div>
<div class="match-score">
<span class="score">{{ aiRecommendEngineer.matchScore }}%</span>
<span class="label">匹配度</span>
</div>
</div>
</el-radio>
</div>
<div class="recommend-reason">
<el-icon><InfoFilled /></el-icon>
<span>{{ aiRecommendEngineer.reason }}</span>
</div>
</div>
</div>
<div class="engineer-list">
<div class="list-header">
<span>全部工程师</span>
<el-input
v-model="engineerSearch"
placeholder="搜索工程师"
style="width: 180px;"
size="small"
clearable
/>
</div>
<div class="list-content">
<div
v-for="eng in filteredEngineers"
:key="eng.id"
class="engineer-card"
:class="{ selected: selectedEngineer === eng.id }"
@click="selectedEngineer = eng.id"
>
<el-radio :model-value="selectedEngineer" :value="eng.id">
<div class="engineer-info">
<el-avatar :size="36">{{ eng.name.charAt(0) }}</el-avatar>
<div class="info-text">
<div class="name">{{ eng.name }}</div>
<div class="title">{{ eng.title }} | 当前工单: {{ eng.currentTickets }}</div>
</div>
</div>
</el-radio>
</div>
</div>
</div>
</div>
<template #footer>
<el-button @click="showAssignTicketDialog = false">取消</el-button>
<el-button type="primary" @click="confirmAssignTicket" :disabled="!selectedEngineer">确认指派</el-button>
</template>
</el-dialog>
<!-- 对话记录弹窗 -->
<el-dialog v-model="showConversationDialog" title="对话记录" width="700px">
<el-tabs v-model="conversationTab">
<el-tab-pane label="对话记录" name="record">
<div class="conversation-record">
<div
v-for="(msg, index) in currentConversation"
:key="index"
class="message-item"
:class="msg.role"
>
<div class="message-avatar">
<el-avatar v-if="msg.role === 'user'" :size="36"></el-avatar>
<el-avatar v-else :size="36" style="background: #7c3aed;"></el-avatar>
</div>
<div class="message-content">
<div class="message-header">
<span class="sender">{{ msg.role === 'user' ? '客户' : '小电' }}</span>
<span class="time">{{ msg.time }}</span>
</div>
<div class="message-text">{{ msg.content }}</div>
</div>
</div>
</div>
</el-tab-pane>
<el-tab-pane label="对话纪要" name="summary">
<div class="conversation-summary">
<div class="summary-section">
<h4><el-icon><Document /></el-icon> 问题概述</h4>
<p>{{ conversationSummary.overview }}</p>
</div>
<div class="summary-section">
<h4><el-icon><Warning /></el-icon> 客户诉求</h4>
<ul>
<li v-for="(item, index) in conversationSummary.demands" :key="index">{{ item }}</li>
</ul>
</div>
<div class="summary-section">
<h4><el-icon><InfoFilled /></el-icon> 关键信息</h4>
<el-descriptions :column="1" border size="small">
<el-descriptions-item label="设备型号">{{ conversationSummary.deviceModel }}</el-descriptions-item>
<el-descriptions-item label="故障现象">{{ conversationSummary.faultSymptom }}</el-descriptions-item>
<el-descriptions-item label="发生时间">{{ conversationSummary.occurTime }}</el-descriptions-item>
</el-descriptions>
</div>
</div>
</el-tab-pane>
</el-tabs>
</el-dialog>
<!-- 对话数据管理 -->
<template v-if="activeSubNav === 'conversation'">
<div class="content-header">
<div class="header-title">
<h1>对话数据管理</h1>
<p class="subtitle">管理和分析客服对话数据</p>
</div>
<el-button type="primary" @click="exportConversations">
<el-icon><Download /></el-icon>
导出数据
</el-button>
</div>
<!-- 筛选区 -->
<el-card class="filter-card">
<div class="conversation-filters">
<el-date-picker
v-model="conversationDateRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
style="width: 260px;"
value-format="YYYY-MM-DD"
/>
<el-select v-model="conversationCategoryFilter" placeholder="问题分类" clearable style="width: 140px;">
<el-option label="设备操作咨询" value="device" />
<el-option label="故障报修" value="fault" />
<el-option label="配件购买" value="parts" />
<el-option label="保修政策" value="warranty" />
<el-option label="安装指导" value="install" />
<el-option label="其他问题" value="other" />
</el-select>
<el-select v-model="conversationStatusFilter" placeholder="对话状态" clearable style="width: 120px;">
<el-option label="已完成" value="completed" />
<el-option label="已转工单" value="ticket" />
<el-option label="未解决" value="unresolved" />
</el-select>
<el-input
v-model="conversationSearchKeyword"
placeholder="搜索客户/设备/内容"
style="width: 200px;"
:prefix-icon="Search"
clearable
/>
</div>
</el-card>
<!-- 对话列表 -->
<el-card>
<el-table :data="filteredConversationList" style="width: 100%">
<el-table-column prop="sessionId" label="会话ID" width="140">
<template #default="{ row }">
<span class="session-id">{{ row.sessionId }}</span>
</template>
</el-table-column>
<el-table-column prop="customerName" label="客户信息" width="140">
<template #default="{ row }">
<div class="customer-info">
<span class="name">{{ row.customerName }}</span>
<span class="phone">{{ row.customerPhone }}</span>
</div>
</template>
</el-table-column>
<el-table-column prop="category" label="问题分类" width="120">
<template #default="{ row }">
<el-tag size="small">{{ row.categoryName }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="deviceModel" label="涉及产品" width="110" />
<el-table-column prop="summary" label="对话摘要" min-width="200">
<template #default="{ row }">
<span class="conversation-summary-text">{{ row.summary }}</span>
</template>
</el-table-column>
<el-table-column prop="messageCount" label="消息数" width="80" align="center" />
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag
:type="row.status === 'completed' ? 'success' : row.status === 'ticket' ? 'warning' : 'danger'"
size="small"
>
{{ row.statusName }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="satisfaction" label="满意度" width="100">
<template #default="{ row }">
<div class="satisfaction-stars" v-if="row.satisfaction">
<el-icon v-for="n in row.satisfaction" :key="n" class="star-filled"><StarFilled /></el-icon>
<el-icon v-for="n in (5 - row.satisfaction)" :key="'empty-' + n" class="star-empty"><Star /></el-icon>
</div>
<span v-else class="no-rating">未评价</span>
</template>
</el-table-column>
<el-table-column prop="startTime" label="对话时间" width="160" />
<el-table-column label="操作" width="120" fixed="right">
<template #default="{ row }">
<el-button type="primary" link size="small" @click="viewConversationDetail(row)">
<el-icon><View /></el-icon>
查看
</el-button>
</template>
</el-table-column>
</el-table>
<div class="table-pagination">
<el-pagination
v-model:current-page="conversationCurrentPage"
:page-size="10"
:total="conversationTotal"
layout="total, prev, pager, next"
/>
</div>
</el-card>
</template>
<!-- 对话详情弹窗 -->
<el-dialog v-model="showConversationDetailDialog" title="对话详情" width="800px">
<div class="conversation-detail-header" v-if="currentConversationDetail">
<div class="detail-info">
<span class="session-id">{{ currentConversationDetail.sessionId }}</span>
<el-tag :type="currentConversationDetail.status === 'completed' ? 'success' : currentConversationDetail.status === 'ticket' ? 'warning' : 'danger'" size="small">
{{ currentConversationDetail.statusName }}
</el-tag>
<el-tag size="small">{{ currentConversationDetail.categoryName }}</el-tag>
</div>
<div class="detail-meta">
<span>客户{{ currentConversationDetail.customerName }}</span>
<span>设备{{ currentConversationDetail.deviceModel }}</span>
<span>时间{{ currentConversationDetail.startTime }}</span>
</div>
</div>
<el-tabs v-model="conversationDetailTab">
<el-tab-pane label="对话记录" name="record">
<div class="conversation-record">
<div
v-for="(msg, index) in currentConversationMessages"
:key="index"
class="message-item"
:class="msg.role"
>
<div class="message-avatar">
<el-avatar v-if="msg.role === 'user'" :size="36"></el-avatar>
<el-avatar v-else :size="36" style="background: #7c3aed;"></el-avatar>
</div>
<div class="message-content">
<div class="message-header">
<span class="sender">{{ msg.role === 'user' ? '客户' : '小电' }}</span>
<span class="time">{{ msg.time }}</span>
</div>
<div class="message-text">{{ msg.content }}</div>
</div>
</div>
</div>
</el-tab-pane>
<el-tab-pane label="对话概要" name="summary">
<div class="conversation-summary" v-if="currentConversationDetail">
<div class="summary-section">
<h4><el-icon><Document /></el-icon> 问题概述</h4>
<p>{{ currentConversationDetail.summaryDetail?.overview }}</p>
</div>
<div class="summary-section">
<h4><el-icon><Warning /></el-icon> 客户诉求</h4>
<ul>
<li v-for="(item, index) in currentConversationDetail.summaryDetail?.demands" :key="index">{{ item }}</li>
</ul>
</div>
<div class="summary-section">
<h4><el-icon><Select /></el-icon> 解决方案</h4>
<p>{{ currentConversationDetail.summaryDetail?.solution }}</p>
</div>
<div class="summary-section">
<h4><el-icon><InfoFilled /></el-icon> 关键信息</h4>
<el-descriptions :column="2" border size="small">
<el-descriptions-item label="涉及设备">{{ currentConversationDetail.deviceModel }}</el-descriptions-item>
<el-descriptions-item label="问题分类">{{ currentConversationDetail.categoryName }}</el-descriptions-item>
<el-descriptions-item label="对话时长">{{ currentConversationDetail.duration }}</el-descriptions-item>
<el-descriptions-item label="消息数量">{{ currentConversationDetail.messageCount }} </el-descriptions-item>
<el-descriptions-item label="是否转工单">
<el-tag :type="currentConversationDetail.status === 'ticket' ? 'warning' : 'info'" size="small">
{{ currentConversationDetail.status === 'ticket' ? '是' : '否' }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="客户满意度">
<div class="satisfaction-stars" v-if="currentConversationDetail.satisfaction">
<el-icon v-for="n in currentConversationDetail.satisfaction" :key="n" class="star-filled"><StarFilled /></el-icon>
</div>
<span v-else>未评价</span>
</el-descriptions-item>
</el-descriptions>
</div>
</div>
</el-tab-pane>
</el-tabs>
</el-dialog>
<!-- 智能体管理 -->
<template v-if="activeSubNav === 'agent'">
<div class="content-header">
<div class="header-title">
<h1>智能体管理</h1>
<p class="subtitle">配置和管理AI智能客服</p>
</div>
</div>
<el-card class="agent-config-card">
<!-- 智能体头部信息 -->
<div class="agent-header">
<div class="agent-avatar">
<el-avatar :size="56" style="background: linear-gradient(135deg, #7c3aed, #a855f7);">
<el-icon :size="28"><Service /></el-icon>
</el-avatar>
</div>
<div class="agent-info">
<h2>泰豪小电</h2>
<span class="agent-type">智能问答助手</span>
</div>
<el-tag type="success" class="agent-status">已发布</el-tag>
</div>
<el-divider />
<!-- 配置表单 -->
<el-form :model="agentConfig" label-position="top" class="agent-form">
<el-form-item label="助手名称">
<el-input v-model="agentConfig.name" placeholder="请输入助手名称" />
</el-form-item>
<el-form-item label="对话风格">
<el-radio-group v-model="agentConfig.style">
<el-radio value="professional">简洁专业</el-radio>
<el-radio value="friendly">友好亲切</el-radio>
<el-radio value="detailed">详细解答</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="助手描述" class="full-width">
<el-input
v-model="agentConfig.description"
type="textarea"
:rows="2"
placeholder="请输入助手描述,将作为开场白展示"
/>
</el-form-item>
<el-form-item label="功能开关" class="full-width">
<div class="feature-switches">
<el-checkbox v-model="agentConfig.features.autoGreeting">自动问候</el-checkbox>
<el-checkbox v-model="agentConfig.features.smartRecommend">智能推荐</el-checkbox>
<el-checkbox v-model="agentConfig.features.conversationRecord">对话记录</el-checkbox>
<el-checkbox v-model="agentConfig.features.voiceChat">语音对话</el-checkbox>
<el-checkbox v-model="agentConfig.features.ticketGuide">工单引导</el-checkbox>
<el-checkbox v-model="agentConfig.features.satisfaction">满意度评价</el-checkbox>
</div>
</el-form-item>
<el-form-item label="响应延迟(毫秒)">
<el-input-number
v-model="agentConfig.responseDelay"
:min="0"
:max="5000"
:step="100"
style="width: 100%;"
/>
<div class="form-tip">模拟AI思考时间建议设置为800-1500ms</div>
</el-form-item>
<el-form-item label="最大回复长度(字符)">
<el-input-number
v-model="agentConfig.maxReplyLength"
:min="100"
:max="2000"
:step="100"
style="width: 100%;"
/>
</el-form-item>
<el-form-item class="form-actions">
<el-button type="primary" @click="saveAgentConfig">保存配置</el-button>
<el-button @click="resetAgentConfig">重置</el-button>
</el-form-item>
</el-form>
</el-card>
</template>
<!-- 知识库日志 -->
<template v-if="activeSubNav === 'log-knowledge'">
<div class="content-header">
<div class="header-title">
<h1>知识库日志</h1>
<p class="subtitle">查看知识库操作记录</p>
</div>
<el-button @click="exportKnowledgeLogs">
<el-icon><Download /></el-icon>
导出日志
</el-button>
</div>
<el-card class="filter-card">
<div class="log-filters">
<el-date-picker
v-model="knowledgeLogDateRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
style="width: 260px;"
value-format="YYYY-MM-DD"
/>
<el-select v-model="knowledgeLogTypeFilter" placeholder="操作类型" clearable style="width: 120px;">
<el-option label="上传" value="upload" />
<el-option label="更新" value="update" />
<el-option label="删除" value="delete" />
<el-option label="重新索引" value="reindex" />
</el-select>
<el-select v-model="knowledgeLogOperatorFilter" placeholder="操作人" clearable style="width: 120px;">
<el-option label="李志鹏" value="李志鹏" />
<el-option label="张伟" value="张伟" />
<el-option label="系统" value="系统" />
</el-select>
<el-input
v-model="knowledgeLogSearch"
placeholder="搜索文档名称"
style="width: 180px;"
:prefix-icon="Search"
clearable
/>
</div>
</el-card>
<el-card>
<el-table :data="filteredKnowledgeLogs" style="width: 100%">
<el-table-column prop="time" label="操作时间" width="170" />
<el-table-column prop="type" label="操作类型" width="100">
<template #default="{ row }">
<el-tag
:type="row.type === 'upload' ? 'success' : row.type === 'delete' ? 'danger' : row.type === 'update' ? 'warning' : 'info'"
size="small"
>
{{ row.typeName }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="docName" label="文档名称" min-width="200" />
<el-table-column prop="category" label="文档分类" width="120" />
<el-table-column prop="operator" label="操作人" width="100" />
<el-table-column prop="result" label="操作结果" width="100">
<template #default="{ row }">
<el-tag :type="row.result === 'success' ? 'success' : 'danger'" size="small">
{{ row.result === 'success' ? '成功' : '失败' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="remark" label="备注" min-width="150" />
</el-table>
<div class="table-pagination">
<el-pagination
v-model:current-page="knowledgeLogPage"
:page-size="10"
:total="knowledgeLogTotal"
layout="total, prev, pager, next"
/>
</div>
</el-card>
</template>
<!-- 工单日志 -->
<template v-if="activeSubNav === 'log-ticket'">
<div class="content-header">
<div class="header-title">
<h1>工单日志</h1>
<p class="subtitle">查看工单操作记录</p>
</div>
<el-button @click="exportTicketLogs">
<el-icon><Download /></el-icon>
导出日志
</el-button>
</div>
<el-card class="filter-card">
<div class="log-filters">
<el-date-picker
v-model="ticketLogDateRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
style="width: 260px;"
value-format="YYYY-MM-DD"
/>
<el-input
v-model="ticketLogNoFilter"
placeholder="工单号"
style="width: 160px;"
clearable
/>
<el-select v-model="ticketLogTypeFilter" placeholder="操作类型" clearable style="width: 120px;">
<el-option label="创建" value="create" />
<el-option label="指派" value="assign" />
<el-option label="转派" value="transfer" />
<el-option label="完成" value="complete" />
<el-option label="关闭" value="close" />
</el-select>
<el-select v-model="ticketLogOperatorFilter" placeholder="操作人" clearable style="width: 120px;">
<el-option label="系统" value="系统" />
<el-option label="李志鹏" value="李志鹏" />
<el-option label="小李" value="小李" />
<el-option label="小王" value="小王" />
</el-select>
</div>
</el-card>
<el-card>
<el-table :data="filteredTicketLogs" style="width: 100%">
<el-table-column prop="time" label="操作时间" width="170" />
<el-table-column prop="ticketNo" label="工单号" width="160">
<template #default="{ row }">
<span class="ticket-no">{{ row.ticketNo }}</span>
</template>
</el-table-column>
<el-table-column prop="type" label="操作类型" width="100">
<template #default="{ row }">
<el-tag
:type="row.type === 'create' ? 'primary' : row.type === 'assign' ? 'warning' : row.type === 'complete' ? 'success' : row.type === 'close' ? 'info' : ''"
size="small"
>
{{ row.typeName }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="operator" label="操作人" width="100" />
<el-table-column prop="content" label="操作内容" min-width="250" />
<el-table-column prop="ip" label="IP地址" width="140" />
</el-table>
<div class="table-pagination">
<el-pagination
v-model:current-page="ticketLogPage"
:page-size="10"
:total="ticketLogTotal"
layout="total, prev, pager, next"
/>
</div>
</el-card>
</template>
<!-- 系统日志 -->
<template v-if="activeSubNav === 'log-system'">
<div class="content-header">
<div class="header-title">
<h1>系统日志</h1>
<p class="subtitle">查看系统配置变更与异常信息</p>
</div>
<el-button @click="exportSystemLogs">
<el-icon><Download /></el-icon>
导出日志
</el-button>
</div>
<el-card class="filter-card">
<div class="log-filters">
<el-date-picker
v-model="systemLogDateRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
style="width: 260px;"
value-format="YYYY-MM-DD"
/>
<el-select v-model="systemLogTypeFilter" placeholder="日志类型" clearable style="width: 120px;">
<el-option label="配置变更" value="config" />
<el-option label="系统异常" value="error" />
</el-select>
<el-select v-model="systemLogLevelFilter" placeholder="日志级别" clearable style="width: 120px;">
<el-option label="INFO" value="info" />
<el-option label="WARN" value="warn" />
<el-option label="ERROR" value="error" />
</el-select>
<el-select v-model="systemLogModuleFilter" placeholder="模块" clearable style="width: 140px;">
<el-option label="智能体配置" value="agent" />
<el-option label="知识库索引" value="knowledge" />
<el-option label="对话服务" value="chat" />
<el-option label="API调用" value="api" />
</el-select>
</div>
</el-card>
<el-card>
<el-table :data="filteredSystemLogs" style="width: 100%">
<el-table-column prop="time" label="时间" width="170" />
<el-table-column prop="logType" label="类型" width="100">
<template #default="{ row }">
<el-tag :type="row.logType === 'config' ? 'primary' : 'danger'" size="small">
{{ row.logType === 'config' ? '配置变更' : '系统异常' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="level" label="级别" width="80">
<template #default="{ row }">
<span
class="log-level"
:class="row.level"
>
{{ row.level.toUpperCase() }}
</span>
</template>
</el-table-column>
<el-table-column prop="module" label="模块" width="120">
<template #default="{ row }">
<el-tag size="small" type="info">{{ row.moduleName }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="operator" label="操作人" width="100" />
<el-table-column prop="content" label="内容" min-width="300" />
</el-table>
<div class="table-pagination">
<el-pagination
v-model:current-page="systemLogPage"
:page-size="10"
:total="systemLogTotal"
layout="total, prev, pager, next"
/>
</div>
</el-card>
</template>
<!-- CRM管理 -->
<template v-if="activeSubNav === 'crm'">
<div class="content-header">
<div class="header-title">
<h1>CRM管理</h1>
<p class="subtitle">客户关系管理系统</p>
</div>
<el-button type="primary">
<el-icon><User /></el-icon>
新建客户
</el-button>
</div>
<el-card>
<div class="placeholder-content">
<el-icon class="placeholder-icon"><User /></el-icon>
<h3>CRM管理功能开发中</h3>
<p>此功能正在开发中敬请期待...</p>
</div>
</el-card>
</template>
</div>
<!-- 知识库上传对话框 -->
<el-dialog v-model="showKbUploadDialog" title="上传知识库文件" width="600px">
<el-form :model="kbUploadForm" label-width="100px">
<el-form-item label="选择文件" required>
<el-upload
:auto-upload="false"
:on-change="handleKbFileChange"
:limit="1"
accept=".pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx"
>
<template #trigger>
<el-button type="primary">
<el-icon><Upload /></el-icon>
选择文件
</el-button>
</template>
<template #tip>
<div class="el-upload__tip">支持 PDFWordExcelPPT 格式</div>
</template>
</el-upload>
</el-form-item>
<el-form-item label="文件名称" required>
<el-input v-model="kbUploadForm.name" placeholder="上传时自动识别" />
</el-form-item>
<el-form-item label="知识库类型" required>
<el-radio-group v-model="kbUploadForm.type" @change="kbUploadForm.category = ''">
<el-radio label="external">外部知识库</el-radio>
<el-radio label="internal">内部知识库</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="文件分类" required>
<el-select v-model="kbUploadForm.category" placeholder="请选择分类" style="width: 100%;">
<template v-if="kbUploadForm.type === 'external'">
<el-option label="设备操作指南" value="device-guide" />
<el-option label="常见故障解决方案" value="fault-solution" />
<el-option label="三包外服务政策" value="service-policy" />
<el-option label="配件咨询话术" value="parts-consult" />
</template>
<template v-else>
<el-option label="技术维修手册" value="tech-manual" />
<el-option label="产品参数明细" value="product-params" />
<el-option label="内部服务流程规范" value="service-process" />
<el-option label="客户服务话术模板" value="service-script" />
</template>
</el-select>
</el-form-item>
<el-form-item label="上传人员" required>
<el-input v-model="kbUploadForm.uploader" placeholder="自动匹配当前用户" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="aiRecognizeKbFile" :loading="kbAiRecognizing">
<el-icon><MagicStick /></el-icon>
AI智能识别
</el-button>
<span style="margin-left: 12px; color: #909399; font-size: 13px;">自动识别并填充分类信息</span>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="showKbUploadDialog = false">取消</el-button>
<el-button type="primary" @click="confirmKbUpload">确认上传</el-button>
</template>
</el-dialog>
<!-- 个人中心 -->
<div v-if="activeNav === 'profile'" class="admin-content profile-content">
<div class="content-header">
<h1>个人中心</h1>
<p class="subtitle">管理您的个人信息和账户设置</p>
</div>
<div class="profile-main">
<el-tabs v-model="activeProfileTab">
<el-tab-pane label="基本信息" name="basic">
<el-card>
<div class="profile-section">
<div class="avatar-section">
<el-avatar :size="100" src="/avatar.svg">李志鹏</el-avatar>
<el-button type="primary" size="small" style="margin-top: 16px;">
<el-icon><Upload /></el-icon>
更换头像
</el-button>
</div>
<el-form :model="profileForm" label-width="100px" style="max-width: 600px;">
<el-form-item label="姓名">
<el-input v-model="profileForm.name" />
</el-form-item>
<el-form-item label="工号">
<el-input v-model="profileForm.employeeId" disabled />
</el-form-item>
<el-form-item label="部门">
<el-input v-model="profileForm.department" />
</el-form-item>
<el-form-item label="职位">
<el-input v-model="profileForm.position" />
</el-form-item>
<el-form-item label="手机号">
<el-input v-model="profileForm.phone" />
</el-form-item>
<el-form-item label="邮箱">
<el-input v-model="profileForm.email" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="saveProfile">保存修改</el-button>
<el-button @click="resetProfile">重置</el-button>
</el-form-item>
</el-form>
</div>
</el-card>
</el-tab-pane>
<el-tab-pane label="账户安全" name="security">
<el-card>
<div class="security-section">
<div class="security-item">
<div class="security-info">
<h3>登录密码</h3>
<p>定期更换密码可以提高账户安全性</p>
</div>
<el-button type="primary" link @click="showPasswordDialog = true">修改密码</el-button>
</div>
<el-divider />
<div class="security-item">
<div class="security-info">
<h3>双因素认证</h3>
<p>开启后登录需要验证码更加安全</p>
</div>
<el-switch v-model="twoFactorEnabled" />
</div>
</div>
</el-card>
</el-tab-pane>
<el-tab-pane label="操作日志" name="logs">
<el-card>
<el-table :data="operationLogs" style="width: 100%">
<el-table-column prop="time" label="时间" width="180" />
<el-table-column prop="action" label="操作" width="200" />
<el-table-column prop="ip" label="IP地址" width="150" />
<el-table-column prop="device" label="设备" />
</el-table>
</el-card>
</el-tab-pane>
</el-tabs>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import { useRouter } from 'vue-router'
import {
Setting,
Document,
Service,
Back,
User,
Warning,
Select,
Clock,
Files,
DArrowRight,
DataLine,
Tickets,
Upload,
View,
Download,
Delete,
Search,
MagicStick,
Tools,
QuestionFilled,
ChatDotRound,
List,
ArrowDown,
Top,
Right,
Plus,
Phone,
InfoFilled,
Picture,
StarFilled,
Star
} from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus'
const router = useRouter()
const activeNav = ref('platform')
const activeSubNav = ref('dashboard')
const navItems = [
{ key: 'platform', label: '平台管理后台', icon: 'Setting' },
{ key: 'bidding', label: '智能标书管理后台', icon: 'Document' },
{ key: 'service', label: '泰豪小电管理后台', icon: 'Service' }
]
// 平台管理后台的子导航
const platformSubNav = [
{ key: 'dashboard', label: '数据概览', icon: 'DataLine' },
{ key: 'users', label: '用户管理', icon: 'User' },
{ key: 'knowledge', label: '知识库', icon: 'Document' },
{ key: 'settings', label: '系统配置', icon: 'Setting' }
]
// 智能标书管理后台的子导航
const biddingSubNav = [
{ key: 'dashboard', label: '数据概览', icon: 'DataLine' },
{ key: 'projects', label: '项目管理', icon: 'Document' },
{ key: 'templates', label: '模板库', icon: 'Files' },
{ key: 'settings', label: '系统配置', icon: 'Setting' }
]
// 泰豪小电管理后台的子导航
const serviceSubNav = [
{ key: 'dashboard', label: '数据概览', icon: 'DataLine' },
{ key: 'knowledge', label: '知识库', icon: 'Document' },
{ key: 'tickets', label: '工单管理', icon: 'Tickets' },
{ key: 'conversation', label: '对话数据管理', icon: 'ChatDotRound' },
{ key: 'agent', label: '智能体管理', icon: 'Service' },
{
key: 'logs',
label: '日志管理',
icon: 'List',
children: [
{ key: 'log-knowledge', label: '知识库日志' },
{ key: 'log-ticket', label: '工单日志' },
{ key: 'log-system', label: '系统日志' }
]
},
{ key: 'crm', label: 'CRM管理', icon: 'User' }
]
// 日志导航展开状态
const logNavExpanded = ref(false)
// 数据概览
const dashboardData = ref({
consultations: 1258,
pendingTickets: 23,
completedTickets: 156
})
const questionStatPeriod = ref('today')
const questionCategories = ref([
{ name: '设备操作咨询', count: 386, percent: 85, color: '#409eff' },
{ name: '故障报修', count: 298, percent: 65, color: '#e6a23c' },
{ name: '配件购买', count: 215, percent: 47, color: '#67c23a' },
{ name: '保修政策', count: 189, percent: 42, color: '#909399' },
{ name: '安装指导', count: 112, percent: 25, color: '#f56c6c' },
{ name: '其他问题', count: 58, percent: 13, color: '#b3d8ff' }
])
const productCloudData = ref([
{ name: 'TH-500GF', size: 24, weight: 1, color: '#409eff' },
{ name: 'S-200X', size: 20, weight: 0.8, color: '#67c23a' },
{ name: 'TH-300D', size: 18, weight: 0.7, color: '#e6a23c' },
{ name: 'G-100S', size: 22, weight: 0.9, color: '#f56c6c' },
{ name: 'TH-800GF', size: 16, weight: 0.6, color: '#909399' },
{ name: 'S-150X', size: 19, weight: 0.75, color: '#7c3aed' },
{ name: 'TH-1000D', size: 15, weight: 0.5, color: '#00d4ff' },
{ name: 'G-200S', size: 17, weight: 0.65, color: '#ff6b6b' },
{ name: 'TH-600GF', size: 21, weight: 0.85, color: '#ffa940' },
{ name: 'S-300X', size: 14, weight: 0.45, color: '#36cfc9' },
{ name: 'TH-400D', size: 16, weight: 0.55, color: '#b37feb' },
{ name: 'G-150S', size: 18, weight: 0.7, color: '#ff85c0' }
])
// 跳转方法
const goToTickets = (status) => {
activeSubNav.value = 'tickets'
// 可以存储筛选状态供工单管理页面使用
ElMessage.info(`跳转至工单管理 - ${status === 'pending' ? '待处理' : '已处理'}工单`)
}
const goToConversationByCategory = (category) => {
activeSubNav.value = 'conversation'
ElMessage.info(`跳转至对话数据 - 分类:${category}`)
}
const goToConversationByProduct = (product) => {
activeSubNav.value = 'conversation'
ElMessage.info(`跳转至对话数据 - 产品:${product}`)
}
// 工单管理
const ticketStatusFilter = ref('all')
const ticketTypeFilter = ref('')
const ticketUrgencyFilter = ref('')
const ticketSearchKeyword = ref('')
const ticketCurrentPage = ref(1)
const ticketTotal = ref(45)
const ticketCounts = ref({
pending: 23,
processing: 8,
completed: 12,
closed: 2
})
const showCreateTicketDialog = ref(false)
const showTicketDetailDialog = ref(false)
const showAssignTicketDialog = ref(false)
const showConversationDialog = ref(false)
const currentTicket = ref(null)
const selectedEngineer = ref('')
const engineerSearch = ref('')
const conversationTab = ref('record')
const ticketForm = ref({
customerName: '',
customerPhone: '',
deviceModel: '',
faultType: '',
faultDesc: '',
urgency: 'normal',
address: '',
nameplate: ''
})
const ticketList = ref([
{
id: 1,
ticketNo: 'WO202501130001',
customerName: '张伟',
customerPhone: '138****5678',
deviceModel: 'TH-500GF',
faultType: 'electrical',
faultTypeName: '电气系统故障',
faultDesc: '发电机启动后电压不稳定,波动范围较大,影响正常使用',
urgency: 'urgent',
urgencyName: '紧急',
status: 'pending',
statusName: '待处理',
source: 'chat',
assignee: null,
address: '江西省南昌市红谷滩区xxx路xxx号',
nameplate: 'TH-500GF-2023-001',
images: [
'https://picsum.photos/400/300?random=1',
'https://picsum.photos/400/300?random=2',
'https://picsum.photos/400/300?random=3'
],
createTime: '2025-01-13 09:30:00',
updateTime: '2025-01-13 09:30:00',
logs: [
{ time: '2025-01-13 09:30:00', operator: '系统', action: '工单创建', type: 'primary', remark: '客户通过小电对话提交' }
]
},
{
id: 2,
ticketNo: 'WO202501130002',
customerName: '李明',
customerPhone: '139****1234',
deviceModel: 'S-200X',
faultType: 'mechanical',
faultTypeName: '机械故障',
faultDesc: '静音发电机运行时有异常噪音,疑似轴承磨损',
urgency: 'normal',
urgencyName: '普通',
status: 'processing',
statusName: '处理中',
source: 'chat',
assignee: '小李',
address: '江西省九江市浔阳区xxx街xxx号',
nameplate: 'S-200X-2022-156',
images: [
'https://picsum.photos/400/300?random=4',
'https://picsum.photos/400/300?random=5'
],
createTime: '2025-01-12 14:20:00',
updateTime: '2025-01-13 08:15:00',
logs: [
{ time: '2025-01-12 14:20:00', operator: '系统', action: '工单创建', type: 'primary', remark: '客户通过小电对话提交' },
{ time: '2025-01-12 15:00:00', operator: '管理员', action: '指派工程师', type: 'warning', remark: '指派给小李工程师处理' },
{ time: '2025-01-13 08:15:00', operator: '小李', action: '开始处理', type: 'success', remark: '已联系客户,计划今日上门检修' }
]
},
{
id: 3,
ticketNo: 'WO202501120003',
customerName: '王芳',
customerPhone: '137****9876',
deviceModel: 'TH-300D',
faultType: 'control',
faultTypeName: '控制系统故障',
faultDesc: '控制面板显示屏不亮,无法查看运行状态',
urgency: 'urgent',
urgencyName: '紧急',
status: 'pending',
statusName: '待处理',
source: 'manual',
assignee: null,
address: '江西省赣州市章贡区xxx路xxx号',
images: [
'https://picsum.photos/400/300?random=6'
],
nameplate: 'TH-300D-2023-089',
createTime: '2025-01-12 10:45:00',
updateTime: '2025-01-12 10:45:00',
logs: [
{ time: '2025-01-12 10:45:00', operator: '客服小张', action: '工单创建', type: 'primary', remark: '电话接报后手动创建' }
]
},
{
id: 4,
ticketNo: 'WO202501110004',
customerName: '赵强',
customerPhone: '136****5432',
deviceModel: 'G-100S',
faultType: 'parts',
faultTypeName: '配件更换',
faultDesc: '需要更换空气滤芯和机油滤芯',
urgency: 'low',
urgencyName: '低',
status: 'completed',
statusName: '已完成',
source: 'chat',
assignee: '小王',
address: '江西省上饶市信州区xxx街xxx号',
nameplate: 'G-100S-2021-234',
createTime: '2025-01-11 16:30:00',
updateTime: '2025-01-12 17:00:00',
logs: [
{ time: '2025-01-11 16:30:00', operator: '系统', action: '工单创建', type: 'primary', remark: '客户通过小电对话提交' },
{ time: '2025-01-11 17:00:00', operator: '管理员', action: '指派工程师', type: 'warning', remark: '指派给小王工程师处理' },
{ time: '2025-01-12 09:00:00', operator: '小王', action: '开始处理', type: 'success', remark: '已准备配件,前往客户现场' },
{ time: '2025-01-12 17:00:00', operator: '小王', action: '完成工单', type: 'success', remark: '已完成滤芯更换,设备运行正常' }
]
},
{
id: 5,
ticketNo: 'WO202501100005',
customerName: '孙丽',
customerPhone: '135****8765',
deviceModel: 'TH-800GF',
faultType: 'install',
faultTypeName: '安装调试',
faultDesc: '新购发电机组需要安装调试',
urgency: 'normal',
urgencyName: '普通',
status: 'processing',
statusName: '处理中',
source: 'manual',
assignee: '小张',
address: '江西省宜春市袁州区xxx路xxx号',
nameplate: 'TH-800GF-2025-003',
createTime: '2025-01-10 11:00:00',
updateTime: '2025-01-13 10:00:00',
logs: [
{ time: '2025-01-10 11:00:00', operator: '销售小刘', action: '工单创建', type: 'primary', remark: '新机销售后创建安装工单' },
{ time: '2025-01-10 14:00:00', operator: '管理员', action: '指派工程师', type: 'warning', remark: '指派给小张工程师处理' },
{ time: '2025-01-13 10:00:00', operator: '小张', action: '进行中', type: 'success', remark: '安装基本完成,正在进行调试' }
]
}
])
const engineerList = ref([
{ id: 1, name: '小李', title: '高级工程师', currentTickets: 2, skills: ['电气系统', '控制系统'] },
{ id: 2, name: '小王', title: '工程师', currentTickets: 1, skills: ['机械维修', '配件更换'] },
{ id: 3, name: '小张', title: '高级工程师', currentTickets: 1, skills: ['安装调试', '电气系统'] },
{ id: 4, name: '小陈', title: '工程师', currentTickets: 0, skills: ['机械维修', '控制系统'] },
{ id: 5, name: '小刘', title: '技术专家', currentTickets: 3, skills: ['电气系统', '机械维修', '控制系统'] }
])
const aiRecommendEngineer = ref(null)
const currentConversation = ref([])
const conversationSummary = ref({
overview: '',
demands: [],
deviceModel: '',
faultSymptom: '',
occurTime: ''
})
// 工单筛选
const filteredTicketList = computed(() => {
return ticketList.value.filter(ticket => {
if (ticketStatusFilter.value !== 'all' && ticket.status !== ticketStatusFilter.value) return false
if (ticketTypeFilter.value && ticket.faultType !== ticketTypeFilter.value) return false
if (ticketUrgencyFilter.value && ticket.urgency !== ticketUrgencyFilter.value) return false
if (ticketSearchKeyword.value) {
const keyword = ticketSearchKeyword.value.toLowerCase()
if (!ticket.ticketNo.toLowerCase().includes(keyword) &&
!ticket.customerName.toLowerCase().includes(keyword) &&
!ticket.deviceModel.toLowerCase().includes(keyword)) {
return false
}
}
return true
})
})
const filteredEngineers = computed(() => {
if (!engineerSearch.value) return engineerList.value
return engineerList.value.filter(eng =>
eng.name.includes(engineerSearch.value) || eng.title.includes(engineerSearch.value)
)
})
// 工单状态类型
const getTicketStatusType = (status) => {
const typeMap = {
pending: 'warning',
processing: 'primary',
completed: 'success',
closed: 'info'
}
return typeMap[status] || 'info'
}
const filterTickets = () => {
ticketCurrentPage.value = 1
}
// 查看工单详情
const viewTicketDetail = (ticket) => {
currentTicket.value = ticket
showTicketDetailDialog.value = true
}
// 查看对话记录
const viewTicketConversation = (ticket) => {
currentConversation.value = [
{ role: 'user', content: '你好,我的发电机出问题了', time: '09:25:30' },
{ role: 'assistant', content: '您好!我是泰豪小电,很高兴为您服务。请问您的发电机是什么型号?出现了什么问题?', time: '09:25:32' },
{ role: 'user', content: '型号是TH-500GF启动后电压一直跳不稳定', time: '09:26:15' },
{ role: 'assistant', content: '了解了。TH-500GF电压不稳定可能有以下几种原因\n1. AVR自动电压调节器故障\n2. 发电机转速不稳\n3. 负载过大或不平衡\n请问这个问题是一直存在还是最近才出现的', time: '09:26:18' },
{ role: 'user', content: '最近两天才开始的,之前用着挺好的', time: '09:27:05' },
{ role: 'assistant', content: '明白了。建议您先检查一下负载情况如果负载正常可能需要专业工程师上门检查AVR和调速系统。您需要我帮您创建一个维修工单吗', time: '09:27:08' },
{ role: 'user', content: '好的,帮我创建工单吧,比较着急', time: '09:28:00' },
{ role: 'assistant', content: '好的,我已为您创建紧急维修工单。请问您方便提供一下现场地址和联系电话吗?我们会尽快安排工程师上门服务。', time: '09:28:03' }
]
conversationSummary.value = {
overview: '客户反映TH-500GF发电机组启动后电压不稳定波动范围较大问题最近两天才出现之前运行正常。可能原因包括AVR故障、转速不稳或负载问题。客户希望尽快安排上门维修。',
demands: [
'尽快安排工程师上门检修',
'解决电压不稳定问题',
'确保设备正常运行'
],
deviceModel: 'TH-500GF',
faultSymptom: '启动后电压不稳定,波动范围较大',
occurTime: '最近两天'
}
conversationTab.value = 'record'
showConversationDialog.value = true
}
// 显示指派弹窗
const showAssignDialog = (ticket) => {
currentTicket.value = ticket
selectedEngineer.value = ''
// 生成AI推荐
generateAIRecommend(ticket)
showAssignTicketDialog.value = true
}
const showAssignDialogFromDetail = () => {
selectedEngineer.value = ''
generateAIRecommend(currentTicket.value)
showAssignTicketDialog.value = true
}
// AI智能推荐工程师
const generateAIRecommend = (ticket) => {
// 模拟AI推荐逻辑
const faultTypeMap = {
electrical: { engId: 1, skill: '电气系统' },
mechanical: { engId: 2, skill: '机械维修' },
control: { engId: 1, skill: '控制系统' },
parts: { engId: 2, skill: '配件更换' },
install: { engId: 3, skill: '安装调试' },
other: { engId: 4, skill: '综合维修' }
}
const recommend = faultTypeMap[ticket.faultType] || faultTypeMap.other
const engineer = engineerList.value.find(e => e.id === recommend.engId)
if (engineer) {
aiRecommendEngineer.value = {
...engineer,
matchScore: 85 + Math.floor(Math.random() * 10),
reason: `客户问题涉及${ticket.deviceModel}${ticket.faultTypeName}${engineer.name}工程师在${recommend.skill}方面有丰富项目经验历史工单解决率98%平均响应时间2小时。`
}
}
}
// 确认指派
const confirmAssignTicket = () => {
const engineer = engineerList.value.find(e => e.id === selectedEngineer.value)
if (engineer && currentTicket.value) {
currentTicket.value.assignee = engineer.name
currentTicket.value.status = 'processing'
currentTicket.value.statusName = '处理中'
currentTicket.value.logs.push({
time: new Date().toLocaleString('zh-CN', { hour12: false }).replace(/\//g, '-'),
operator: '管理员',
action: '指派工程师',
type: 'warning',
remark: `指派给${engineer.name}工程师处理`
})
engineer.currentTickets++
ticketCounts.value.pending--
showAssignTicketDialog.value = false
ElMessage.success(`已成功指派给${engineer.name}工程师`)
}
}
// 完成工单
const completeTicket = (ticket) => {
ElMessageBox.confirm('确认完成该工单?', '完成确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'success'
}).then(() => {
ticket.status = 'completed'
ticket.statusName = '已完成'
ticket.logs.push({
time: new Date().toLocaleString('zh-CN', { hour12: false }).replace(/\//g, '-'),
operator: ticket.assignee,
action: '完成工单',
type: 'success',
remark: '工单已处理完成'
})
ticketCounts.value.completed++
showTicketDetailDialog.value = false
ElMessage.success('工单已完成')
}).catch(() => {})
}
// 创建工单
const submitCreateTicket = () => {
if (!ticketForm.value.customerName || !ticketForm.value.customerPhone ||
!ticketForm.value.deviceModel || !ticketForm.value.faultType || !ticketForm.value.faultDesc) {
ElMessage.warning('请填写必填信息')
return
}
const faultTypeNames = {
electrical: '电气系统故障',
mechanical: '机械故障',
control: '控制系统故障',
parts: '配件更换',
install: '安装调试',
other: '其他'
}
const urgencyNames = { urgent: '紧急', normal: '普通', low: '低' }
const newTicket = {
id: Date.now(),
ticketNo: 'WO' + new Date().toISOString().slice(0, 10).replace(/-/g, '') + String(ticketList.value.length + 1).padStart(4, '0'),
customerName: ticketForm.value.customerName,
customerPhone: ticketForm.value.customerPhone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2'),
deviceModel: ticketForm.value.deviceModel,
faultType: ticketForm.value.faultType,
faultTypeName: faultTypeNames[ticketForm.value.faultType],
faultDesc: ticketForm.value.faultDesc,
urgency: ticketForm.value.urgency,
urgencyName: urgencyNames[ticketForm.value.urgency],
status: 'pending',
statusName: '待处理',
source: 'manual',
assignee: null,
address: ticketForm.value.address,
nameplate: ticketForm.value.nameplate,
createTime: new Date().toLocaleString('zh-CN', { hour12: false }).replace(/\//g, '-'),
updateTime: new Date().toLocaleString('zh-CN', { hour12: false }).replace(/\//g, '-'),
logs: [
{
time: new Date().toLocaleString('zh-CN', { hour12: false }).replace(/\//g, '-'),
operator: '李志鹏',
action: '工单创建',
type: 'primary',
remark: '手动创建工单'
}
]
}
ticketList.value.unshift(newTicket)
ticketCounts.value.pending++
ticketTotal.value++
showCreateTicketDialog.value = false
ticketForm.value = {
customerName: '',
customerPhone: '',
deviceModel: '',
faultType: '',
faultDesc: '',
urgency: 'normal',
address: '',
nameplate: ''
}
ElMessage.success('工单创建成功')
}
// 对话数据管理
const conversationDateRange = ref(null)
const conversationCategoryFilter = ref('')
const conversationStatusFilter = ref('')
const conversationSearchKeyword = ref('')
const conversationCurrentPage = ref(1)
const conversationTotal = ref(86)
const showConversationDetailDialog = ref(false)
const conversationDetailTab = ref('record')
const currentConversationDetail = ref(null)
const currentConversationMessages = ref([])
const conversationList = ref([
{
id: 1,
sessionId: 'CONV20250113001',
customerName: '张伟',
customerPhone: '138****5678',
category: 'fault',
categoryName: '故障报修',
deviceModel: 'TH-500GF',
summary: '发电机启动后电压不稳定,需要工程师上门检修',
messageCount: 12,
status: 'ticket',
statusName: '已转工单',
satisfaction: 5,
startTime: '2025-01-13 09:25:00',
duration: '8分32秒',
summaryDetail: {
overview: '客户反映TH-500GF发电机组启动后电压不稳定波动范围较大问题最近两天才出现之前运行正常。经过诊断可能原因包括AVR故障、转速不稳或负载问题。',
demands: ['尽快安排工程师上门检修', '解决电压不稳定问题', '确保设备正常运行'],
solution: '已为客户创建紧急维修工单安排工程师上门检查AVR和调速系统。'
},
messages: [
{ role: 'user', content: '你好,我的发电机出问题了', time: '09:25:30' },
{ role: 'assistant', content: '您好!我是泰豪小电,很高兴为您服务。请问您的发电机是什么型号?出现了什么问题?', time: '09:25:32' },
{ role: 'user', content: '型号是TH-500GF启动后电压一直跳不稳定', time: '09:26:15' },
{ role: 'assistant', content: '了解了。TH-500GF电压不稳定可能有以下几种原因\n1. AVR自动电压调节器故障\n2. 发电机转速不稳\n3. 负载过大或不平衡\n请问这个问题是一直存在还是最近才出现的', time: '09:26:18' },
{ role: 'user', content: '最近两天才开始的,之前用着挺好的', time: '09:27:05' },
{ role: 'assistant', content: '明白了。建议您先检查一下负载情况如果负载正常可能需要专业工程师上门检查AVR和调速系统。您需要我帮您创建一个维修工单吗', time: '09:27:08' },
{ role: 'user', content: '好的,帮我创建工单吧,比较着急', time: '09:28:00' },
{ role: 'assistant', content: '好的,我已为您创建紧急维修工单。请问您方便提供一下现场地址和联系电话吗?我们会尽快安排工程师上门服务。', time: '09:28:03' }
]
},
{
id: 2,
sessionId: 'CONV20250113002',
customerName: '李娜',
customerPhone: '139****2345',
category: 'device',
categoryName: '设备操作咨询',
deviceModel: 'S-200X',
summary: '咨询静音发电机的操作方法和保养周期',
messageCount: 8,
status: 'completed',
statusName: '已完成',
satisfaction: 5,
startTime: '2025-01-13 10:15:00',
duration: '5分18秒',
summaryDetail: {
overview: '客户咨询S-200X静音发电机的日常操作方法和保养周期包括启动流程、停机注意事项和定期保养项目。',
demands: ['了解设备操作方法', '了解保养周期和保养项目'],
solution: '详细介绍了设备的启动/停机流程以及建议每200小时或3个月进行一次常规保养的计划。'
},
messages: [
{ role: 'user', content: '请问S-200X发电机怎么操作', time: '10:15:20' },
{ role: 'assistant', content: '您好S-200X静音发电机的操作非常简单我来为您介绍一下\n\n【启动流程】\n1. 检查燃油和机油液位\n2. 打开燃油开关\n3. 将启动开关转到"ON"位置\n4. 按下启动按钮\n\n请问您还有其他问题吗', time: '10:15:25' },
{ role: 'user', content: '保养周期是多久?需要保养哪些项目?', time: '10:17:00' },
{ role: 'assistant', content: 'S-200X建议每运行200小时或每3个月进行一次常规保养保养项目包括\n1. 更换机油和机油滤芯\n2. 检查/更换空气滤芯\n3. 检查燃油滤芯\n4. 检查火花塞(汽油机)\n5. 检查蓄电池电量\n\n如需专业保养服务可以预约我们的工程师上门。', time: '10:17:05' }
]
},
{
id: 3,
sessionId: 'CONV20250112003',
customerName: '王强',
customerPhone: '137****8765',
category: 'parts',
categoryName: '配件购买',
deviceModel: 'G-100S',
summary: '咨询空气滤芯和机油滤芯的价格和购买方式',
messageCount: 6,
status: 'completed',
statusName: '已完成',
satisfaction: 4,
startTime: '2025-01-12 14:30:00',
duration: '3分45秒',
summaryDetail: {
overview: '客户需要购买G-100S发电机的空气滤芯和机油滤芯咨询配件价格和购买渠道。',
demands: ['了解配件价格', '了解购买方式'],
solution: '提供了配件价格信息,并引导客户通过官方渠道或授权经销商购买。'
},
messages: [
{ role: 'user', content: 'G-100S的滤芯多少钱', time: '14:30:15' },
{ role: 'assistant', content: '您好G-100S发电机的滤芯价格如下\n- 空气滤芯¥85/个\n- 机油滤芯¥45/个\n\n您可以通过以下方式购买\n1. 拨打售后热线订购\n2. 联系当地授权经销商\n3. 官方商城在线购买\n\n请问您需要哪种滤芯', time: '14:30:20' }
]
},
{
id: 4,
sessionId: 'CONV20250112004',
customerName: '赵敏',
customerPhone: '136****4321',
category: 'warranty',
categoryName: '保修政策',
deviceModel: 'TH-300D',
summary: '咨询设备保修期限和保修范围',
messageCount: 5,
status: 'completed',
statusName: '已完成',
satisfaction: null,
startTime: '2025-01-12 16:45:00',
duration: '2分58秒',
summaryDetail: {
overview: '客户咨询TH-300D柴油发电机的保修政策包括保修期限和保修范围。',
demands: ['了解保修期限', '了解保修范围'],
solution: '告知客户整机保修2年或2000小时核心部件保修3年人为损坏和易损件不在保修范围内。'
},
messages: [
{ role: 'user', content: '我买的TH-300D保修多久', time: '16:45:10' },
{ role: 'assistant', content: '您好TH-300D柴油发电机的保修政策如下\n\n【保修期限】\n- 整机保修2年或累计运行2000小时以先到者为准\n- 核心部件发动机、发电机3年\n\n【保修范围】\n- 因产品质量问题导致的故障\n- 不包含人为损坏、使用不当\n- 不包含易损件(滤芯、皮带等)\n\n请问您的设备遇到什么问题了吗', time: '16:45:15' }
]
},
{
id: 5,
sessionId: 'CONV20250111005',
customerName: '孙丽',
customerPhone: '135****9876',
category: 'install',
categoryName: '安装指导',
deviceModel: 'TH-800GF',
summary: '咨询新购发电机的安装要求和注意事项',
messageCount: 10,
status: 'ticket',
statusName: '已转工单',
satisfaction: 5,
startTime: '2025-01-11 11:00:00',
duration: '12分15秒',
summaryDetail: {
overview: '客户新购TH-800GF大型发电机组咨询安装场地要求、电气接入规范和通风散热要求。由于安装较为复杂已创建安装调试工单。',
demands: ['了解安装场地要求', '了解电气接入规范', '安排专业安装服务'],
solution: '详细介绍了安装要求后,已创建安装调试工单,安排工程师上门进行专业安装。'
},
messages: [
{ role: 'user', content: '我刚买了TH-800GF安装有什么要求', time: '11:00:30' },
{ role: 'assistant', content: '恭喜您购买TH-800GF发电机组这款设备功率较大安装需要注意以下几点\n\n【场地要求】\n1. 安装场地需平整坚固承重不小于2吨/平米\n2. 预留设备四周各1米以上的维护空间\n3. 地面需做防震处理\n\n【通风要求】\n1. 进风口面积不小于2平方米\n2. 排风口面积不小于1.5平方米\n\n您的安装场地是否具备这些条件', time: '11:00:35' }
]
}
])
// 对话数据筛选
const filteredConversationList = computed(() => {
return conversationList.value.filter(conv => {
if (conversationCategoryFilter.value && conv.category !== conversationCategoryFilter.value) return false
if (conversationStatusFilter.value && conv.status !== conversationStatusFilter.value) return false
if (conversationSearchKeyword.value) {
const keyword = conversationSearchKeyword.value.toLowerCase()
if (!conv.customerName.toLowerCase().includes(keyword) &&
!conv.deviceModel.toLowerCase().includes(keyword) &&
!conv.summary.toLowerCase().includes(keyword)) {
return false
}
}
return true
})
})
// 查看对话详情
const viewConversationDetail = (conv) => {
currentConversationDetail.value = conv
currentConversationMessages.value = conv.messages || []
conversationDetailTab.value = 'record'
showConversationDetailDialog.value = true
}
// 导出对话数据
const exportConversations = () => {
ElMessage.success('对话数据导出中,请稍候...')
}
// 智能体配置
const agentConfig = ref({
name: '泰豪小电',
description: '您好!我是泰豪小电,有什么可以帮助您的吗?',
style: 'friendly',
features: {
autoGreeting: true,
smartRecommend: true,
conversationRecord: true,
voiceChat: false,
ticketGuide: true,
satisfaction: true
},
responseDelay: 1000,
maxReplyLength: 500
})
const defaultAgentConfig = {
name: '泰豪小电',
description: '您好!我是泰豪小电,有什么可以帮助您的吗?',
style: 'friendly',
features: {
autoGreeting: true,
smartRecommend: true,
conversationRecord: true,
voiceChat: false,
ticketGuide: true,
satisfaction: true
},
responseDelay: 1000,
maxReplyLength: 500
}
const saveAgentConfig = () => {
ElMessage.success('智能体配置保存成功')
}
const resetAgentConfig = () => {
agentConfig.value = { ...defaultAgentConfig, features: { ...defaultAgentConfig.features } }
ElMessage.info('配置已重置')
}
// 知识库日志
const knowledgeLogDateRange = ref(null)
const knowledgeLogTypeFilter = ref('')
const knowledgeLogOperatorFilter = ref('')
const knowledgeLogSearch = ref('')
const knowledgeLogPage = ref(1)
const knowledgeLogTotal = ref(45)
const knowledgeLogs = ref([
{ id: 1, time: '2025-01-13 14:30:00', type: 'upload', typeName: '上传', docName: 'TH-800GF安装指南.pdf', category: '产品文档', operator: '李志鹏', result: 'success', remark: '文件大小3.2MB' },
{ id: 2, time: '2025-01-13 11:20:00', type: 'update', typeName: '更新', docName: 'S-200X操作手册.pdf', category: '产品文档', operator: '李志鹏', result: 'success', remark: '更新第3章内容' },
{ id: 3, time: '2025-01-13 10:15:00', type: 'reindex', typeName: '重新索引', docName: '常见故障处理指南.docx', category: '维修知识', operator: '系统', result: 'success', remark: '自动触发重新索引' },
{ id: 4, time: '2025-01-12 16:45:00', type: 'delete', typeName: '删除', docName: '旧版产品目录.pdf', category: '产品文档', operator: '张伟', result: 'success', remark: '文档已过期' },
{ id: 5, time: '2025-01-12 14:30:00', type: 'upload', typeName: '上传', docName: '2025年配件价格表.xlsx', category: '配件信息', operator: '李志鹏', result: 'success', remark: '文件大小1.5MB' },
{ id: 6, time: '2025-01-12 10:00:00', type: 'upload', typeName: '上传', docName: '发电机保养规范.pdf', category: '维修知识', operator: '李志鹏', result: 'fail', remark: '文件格式不支持' },
{ id: 7, time: '2025-01-11 15:20:00', type: 'update', typeName: '更新', docName: 'TH-500GF技术参数.pdf', category: '产品文档', operator: '张伟', result: 'success', remark: '更新技术参数表' }
])
const filteredKnowledgeLogs = computed(() => {
return knowledgeLogs.value.filter(log => {
if (knowledgeLogTypeFilter.value && log.type !== knowledgeLogTypeFilter.value) return false
if (knowledgeLogOperatorFilter.value && log.operator !== knowledgeLogOperatorFilter.value) return false
if (knowledgeLogSearch.value && !log.docName.toLowerCase().includes(knowledgeLogSearch.value.toLowerCase())) return false
return true
})
})
const exportKnowledgeLogs = () => {
ElMessage.success('知识库日志导出中...')
}
// 工单日志
const ticketLogDateRange = ref(null)
const ticketLogNoFilter = ref('')
const ticketLogTypeFilter = ref('')
const ticketLogOperatorFilter = ref('')
const ticketLogPage = ref(1)
const ticketLogTotal = ref(128)
const ticketLogs = ref([
{ id: 1, time: '2025-01-13 15:00:00', ticketNo: 'WO202501130001', type: 'assign', typeName: '指派', operator: '李志鹏', content: '将工单指派给小李工程师', ip: '192.168.1.100' },
{ id: 2, time: '2025-01-13 09:30:00', ticketNo: 'WO202501130001', type: 'create', typeName: '创建', operator: '系统', content: '客户通过小电对话创建工单', ip: '-' },
{ id: 3, time: '2025-01-13 08:15:00', ticketNo: 'WO202501130002', type: 'complete', typeName: '完成', operator: '小李', content: '工单处理完成,故障已修复', ip: '192.168.1.105' },
{ id: 4, time: '2025-01-12 15:00:00', ticketNo: 'WO202501130002', type: 'assign', typeName: '指派', operator: '李志鹏', content: '将工单指派给小李工程师', ip: '192.168.1.100' },
{ id: 5, time: '2025-01-12 14:20:00', ticketNo: 'WO202501130002', type: 'create', typeName: '创建', operator: '系统', content: '客户通过小电对话创建工单', ip: '-' },
{ id: 6, time: '2025-01-12 10:45:00', ticketNo: 'WO202501120003', type: 'create', typeName: '创建', operator: '李志鹏', content: '手动创建工单', ip: '192.168.1.100' },
{ id: 7, time: '2025-01-11 16:30:00', ticketNo: 'WO202501110004', type: 'close', typeName: '关闭', operator: '李志鹏', content: '客户确认问题已解决,关闭工单', ip: '192.168.1.100' }
])
const filteredTicketLogs = computed(() => {
return ticketLogs.value.filter(log => {
if (ticketLogNoFilter.value && !log.ticketNo.includes(ticketLogNoFilter.value)) return false
if (ticketLogTypeFilter.value && log.type !== ticketLogTypeFilter.value) return false
if (ticketLogOperatorFilter.value && log.operator !== ticketLogOperatorFilter.value) return false
return true
})
})
const exportTicketLogs = () => {
ElMessage.success('工单日志导出中...')
}
// 系统日志
const systemLogDateRange = ref(null)
const systemLogTypeFilter = ref('')
const systemLogLevelFilter = ref('')
const systemLogModuleFilter = ref('')
const systemLogPage = ref(1)
const systemLogTotal = ref(89)
const systemLogs = ref([
{ id: 1, time: '2025-01-13 14:00:00', logType: 'config', level: 'info', module: 'agent', moduleName: '智能体配置', operator: '李志鹏', content: '修改响应延迟1000ms -> 1200ms' },
{ id: 2, time: '2025-01-13 11:30:00', logType: 'error', level: 'warn', module: 'api', moduleName: 'API调用', operator: '系统', content: 'Dify API响应超时已自动重试耗时3.2s' },
{ id: 3, time: '2025-01-13 10:00:00', logType: 'config', level: 'info', module: 'agent', moduleName: '智能体配置', operator: '李志鹏', content: '开启功能:工单引导' },
{ id: 4, time: '2025-01-12 18:45:00', logType: 'error', level: 'error', module: 'knowledge', moduleName: '知识库索引', operator: '系统', content: '文档索引失败:发电机保养规范.pdf原因文件格式不支持' },
{ id: 5, time: '2025-01-12 15:30:00', logType: 'config', level: 'info', module: 'agent', moduleName: '智能体配置', operator: '李志鹏', content: '修改对话风格:简洁专业 -> 友好亲切' },
{ id: 6, time: '2025-01-12 09:00:00', logType: 'error', level: 'warn', module: 'chat', moduleName: '对话服务', operator: '系统', content: '对话会话超时自动关闭会话IDCONV20250111008' },
{ id: 7, time: '2025-01-11 16:20:00', logType: 'config', level: 'info', module: 'agent', moduleName: '智能体配置', operator: '张伟', content: '修改助手描述' },
{ id: 8, time: '2025-01-11 11:00:00', logType: 'error', level: 'error', module: 'api', moduleName: 'API调用', operator: '系统', content: 'Dify API连接失败错误码503已切换备用节点' }
])
const filteredSystemLogs = computed(() => {
return systemLogs.value.filter(log => {
if (systemLogTypeFilter.value && log.logType !== systemLogTypeFilter.value) return false
if (systemLogLevelFilter.value && log.level !== systemLogLevelFilter.value) return false
if (systemLogModuleFilter.value && log.module !== systemLogModuleFilter.value) return false
return true
})
})
const exportSystemLogs = () => {
ElMessage.success('系统日志导出中...')
}
const platformConfig = ref({
systemName: 'AI数智化平台',
aiModel: 'gpt-4',
maxConcurrency: 100,
sessionTimeout: 30
})
const biddingTemplates = ref([
{ name: '工程类标书模板', category: '工程建设', usage: 45, updateTime: '2024-12-01 10:30' },
{ name: '服务类标书模板', category: '服务采购', usage: 32, updateTime: '2024-11-28 14:20' },
{ name: '货物采购模板', category: '货物采购', usage: 28, updateTime: '2024-11-25 09:15' }
])
const knowledgeBase = ref([
{ title: '设备操作手册_TH-500GF', category: '产品文档', size: '2.3MB', status: '已索引', uploadTime: '2024-11-15 10:20' },
{ title: '常见故障排查指南', category: '技术文档', size: '856KB', status: '已索引', uploadTime: '2024-11-14 14:30' },
{ title: '维修案例汇总2023Q3', category: '案例库', size: '1.2MB', status: '处理中', uploadTime: '2024-11-10 09:15' }
])
// 知识库管理
const activeKbType = ref('external')
const activeExternalKbCat = ref('device-guide')
const activeInternalKbCat = ref('tech-manual')
const externalKbSearch = ref('')
const internalKbSearch = ref('')
const showKbUploadDialog = ref(false)
const kbAiRecognizing = ref(false)
const kbUploadForm = ref({
name: '',
type: 'external',
category: '',
uploader: '李志鹏'
})
// 外部知识库分类
const externalKbCategories = ref([
{ key: 'device-guide', name: '设备操作指南', count: 24, icon: 'Tools', color: '#409eff' },
{ key: 'fault-solution', name: '常见故障解决方案', count: 36, icon: 'QuestionFilled', color: '#e6a23c' },
{ key: 'service-policy', name: '三包外服务政策', count: 12, icon: 'Tickets', color: '#67c23a' },
{ key: 'parts-consult', name: '配件咨询话术', count: 18, icon: 'ChatDotRound', color: '#909399' }
])
// 外部知识库文件
const externalKbFiles = ref([
{ id: 1, name: 'TH-500GF发电机组操作手册.pdf', category: 'device-guide', uploader: '张三', uploadTime: '2025-01-10 10:30' },
{ id: 2, name: 'S-200X静音发电机使用指南.pdf', category: 'device-guide', uploader: '李四', uploadTime: '2025-01-08 14:20' },
{ id: 3, name: '发电机无法启动故障排查指南.pdf', category: 'fault-solution', uploader: '王五', uploadTime: '2025-01-05 09:15' },
{ id: 4, name: '电压不稳定常见原因及解决方案.docx', category: 'fault-solution', uploader: '张三', uploadTime: '2025-01-03 16:45' },
{ id: 5, name: '三包服务政策说明书(2025版).pdf', category: 'service-policy', uploader: '李四', uploadTime: '2024-12-20 11:00' },
{ id: 6, name: '延保服务套餐介绍.pdf', category: 'service-policy', uploader: '张三', uploadTime: '2024-12-15 15:30' },
{ id: 7, name: '配件价格查询话术模板.docx', category: 'parts-consult', uploader: '王五', uploadTime: '2024-12-10 09:00' },
{ id: 8, name: '滤芯更换周期咨询话术.docx', category: 'parts-consult', uploader: '李四', uploadTime: '2024-12-08 14:00' }
])
// 内部知识库分类
const internalKbCategories = ref([
{ key: 'tech-manual', name: '技术维修手册', count: 45, icon: 'Tools', color: '#409eff' },
{ key: 'product-params', name: '产品参数明细', count: 28, icon: 'Document', color: '#67c23a' },
{ key: 'service-process', name: '内部服务流程规范', count: 15, icon: 'Tickets', color: '#e6a23c' },
{ key: 'service-script', name: '客户服务话术模板', count: 22, icon: 'ChatDotRound', color: '#909399' }
])
// 内部知识库文件
const internalKbFiles = ref([
{ id: 1, name: 'TH系列发电机维修手册(内部版).pdf', category: 'tech-manual', uploader: '技术部', uploadTime: '2025-01-12 09:00' },
{ id: 2, name: 'S系列静音发电机拆装指南.pdf', category: 'tech-manual', uploader: '技术部', uploadTime: '2025-01-10 11:30' },
{ id: 3, name: '控制系统故障诊断流程.pdf', category: 'tech-manual', uploader: '技术部', uploadTime: '2025-01-08 14:00' },
{ id: 4, name: 'TH-500GF产品参数表.xlsx', category: 'product-params', uploader: '产品部', uploadTime: '2025-01-05 10:00' },
{ id: 5, name: 'S-200X技术规格书.pdf', category: 'product-params', uploader: '产品部', uploadTime: '2025-01-03 15:30' },
{ id: 6, name: '售后服务标准流程(内部).pdf', category: 'service-process', uploader: '服务部', uploadTime: '2024-12-28 09:00' },
{ id: 7, name: '工单处理规范V2.0.docx', category: 'service-process', uploader: '服务部', uploadTime: '2024-12-25 11:00' },
{ id: 8, name: '电话接待话术模板(内部).docx', category: 'service-script', uploader: '培训部', uploadTime: '2024-12-20 14:00' },
{ id: 9, name: '投诉处理话术指南.docx', category: 'service-script', uploader: '培训部', uploadTime: '2024-12-18 16:30' }
])
// 当前分类名称
const currentExternalCatName = computed(() => {
const cat = externalKbCategories.value.find(c => c.key === activeExternalKbCat.value)
return cat ? cat.name : ''
})
const currentInternalCatName = computed(() => {
const cat = internalKbCategories.value.find(c => c.key === activeInternalKbCat.value)
return cat ? cat.name : ''
})
// 过滤后的文件列表
const filteredExternalKbFiles = computed(() => {
return externalKbFiles.value.filter(f => {
if (f.category !== activeExternalKbCat.value) return false
if (externalKbSearch.value && !f.name.includes(externalKbSearch.value)) return false
return true
})
})
const filteredInternalKbFiles = computed(() => {
return internalKbFiles.value.filter(f => {
if (f.category !== activeInternalKbCat.value) return false
if (internalKbSearch.value && !f.name.includes(internalKbSearch.value)) return false
return true
})
})
// 知识库操作方法
const handleKbFileChange = (file) => {
kbUploadForm.value.name = file.name.replace(/\.[^.]+$/, '')
}
const aiRecognizeKbFile = () => {
kbAiRecognizing.value = true
setTimeout(() => {
const externalMap = { '操作': 'device-guide', '指南': 'device-guide', '故障': 'fault-solution', '解决': 'fault-solution', '三包': 'service-policy', '服务': 'service-policy', '配件': 'parts-consult', '话术': 'parts-consult' }
const internalMap = { '维修': 'tech-manual', '手册': 'tech-manual', '参数': 'product-params', '规格': 'product-params', '流程': 'service-process', '规范': 'service-process', '话术': 'service-script', '模板': 'service-script' }
const categoryMap = kbUploadForm.value.type === 'external' ? externalMap : internalMap
for (const [keyword, category] of Object.entries(categoryMap)) {
if (kbUploadForm.value.name.includes(keyword)) {
kbUploadForm.value.category = category
break
}
}
if (!kbUploadForm.value.category) {
kbUploadForm.value.category = kbUploadForm.value.type === 'external' ? 'device-guide' : 'tech-manual'
}
kbAiRecognizing.value = false
ElMessage.success('AI识别完成已自动填充分类信息')
}, 1500)
}
const confirmKbUpload = () => {
if (!kbUploadForm.value.name || !kbUploadForm.value.category) {
ElMessage.warning('请填写完整信息')
return
}
const newFile = {
id: Date.now(),
name: kbUploadForm.value.name + '.pdf',
category: kbUploadForm.value.category,
uploader: kbUploadForm.value.uploader,
uploadTime: new Date().toLocaleString('zh-CN', { hour12: false }).replace(/\//g, '-')
}
if (kbUploadForm.value.type === 'external') {
externalKbFiles.value.unshift(newFile)
const cat = externalKbCategories.value.find(c => c.key === kbUploadForm.value.category)
if (cat) cat.count++
} else {
internalKbFiles.value.unshift(newFile)
const cat = internalKbCategories.value.find(c => c.key === kbUploadForm.value.category)
if (cat) cat.count++
}
showKbUploadDialog.value = false
kbUploadForm.value = { name: '', type: 'external', category: '', uploader: '李志鹏' }
ElMessage.success('文件上传成功')
}
const previewKbFile = (file) => {
ElMessage.info(`正在预览:${file.name}`)
}
const downloadKbFile = (file) => {
ElMessage.success(`开始下载:${file.name}`)
}
const deleteKbFile = (file, type) => {
ElMessageBox.confirm(`确定要删除文件"${file.name}"吗?`, '删除确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
if (type === 'external') {
const idx = externalKbFiles.value.findIndex(f => f.id === file.id)
if (idx > -1) {
externalKbFiles.value.splice(idx, 1)
const cat = externalKbCategories.value.find(c => c.key === file.category)
if (cat) cat.count--
}
} else {
const idx = internalKbFiles.value.findIndex(f => f.id === file.id)
if (idx > -1) {
internalKbFiles.value.splice(idx, 1)
const cat = internalKbCategories.value.find(c => c.key === file.category)
if (cat) cat.count--
}
}
ElMessage.success('删除成功')
}).catch(() => {})
}
const backToMain = () => {
router.push('/')
}
const goToProfile = () => {
activeNav.value = 'profile'
}
// 个人中心数据
const activeProfileTab = ref('basic')
const showPasswordDialog = ref(false)
const twoFactorEnabled = ref(false)
const profileForm = ref({
name: '李志鹏',
employeeId: 'TH20230001',
department: '技术研发部',
position: '高级工程师',
phone: '13800138000',
email: 'lizhipeng@taihao.com'
})
const operationLogs = ref([
{ time: '2024-12-06 18:30:00', action: '登录系统', ip: '192.168.1.100', device: 'Windows 11 / Chrome' },
{ time: '2024-12-06 14:20:00', action: '修改个人信息', ip: '192.168.1.100', device: 'Windows 11 / Chrome' },
{ time: '2024-12-05 09:15:00', action: '登录系统', ip: '192.168.1.100', device: 'Windows 11 / Chrome' },
{ time: '2024-12-04 16:45:00', action: '创建工单', ip: '192.168.1.100', device: 'Windows 11 / Chrome' }
])
const saveProfile = () => {
ElMessage.success('个人信息保存成功!')
}
const resetProfile = () => {
ElMessage.info('已重置为原始信息')
}
</script>
<style lang="scss" scoped>
.admin-system {
display: flex;
height: 100vh;
background: #f5f7fa;
}
// 内容容器
.admin-container {
flex: 1;
display: flex;
overflow: hidden;
}
// 左侧主导航
.admin-sidebar-main {
width: 200px;
background: #F0EAF4;
border-right: 1px solid #e4e7ed;
display: flex;
flex-direction: column;
.sidebar-header {
padding: 20px;
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
.logo {
display: flex;
align-items: center;
gap: 10px;
cursor: pointer;
transition: opacity 0.2s;
&:hover {
opacity: 0.8;
}
.logo-img {
width: 36px;
height: 36px;
border-radius: 6px;
object-fit: contain;
}
.logo-text {
font-size: 16px;
font-weight: 600;
color: #303133;
}
}
}
.main-nav {
flex: 1;
padding: 16px 0;
overflow-y: auto;
.section-title {
padding: 8px 20px;
font-size: 12px;
color: #909399;
margin-bottom: 8px;
}
.main-nav-item {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 20px;
cursor: pointer;
color: #606266;
font-size: 14px;
transition: all 0.2s;
&:hover {
background: rgba(124, 58, 237, 0.1);
color: #7c3aed;
}
&.active {
background: rgba(124, 58, 237, 0.15);
color: #7c3aed;
font-weight: 600;
}
.el-icon {
font-size: 18px;
}
}
.nav-divider {
height: 1px;
background: rgba(0, 0, 0, 0.08);
margin: 12px 20px;
}
}
.sidebar-footer {
margin-top: auto;
padding: 16px;
border-top: 1px solid rgba(0, 0, 0, 0.08);
.user-section {
display: flex;
align-items: center;
gap: 12px;
.user-avatar {
width: 36px;
height: 36px;
cursor: pointer;
transition: transform 0.2s;
&:hover {
transform: scale(1.1);
}
img {
width: 100%;
height: 100%;
border-radius: 50%;
object-fit: cover;
}
}
.user-name {
flex: 1;
font-size: 14px;
font-weight: 500;
color: #303133;
}
.back-icon {
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 6px;
cursor: pointer;
color: #606266;
transition: all 0.2s;
&:hover {
color: #7c3aed;
background: rgba(124, 58, 237, 0.1);
}
.el-icon {
font-size: 18px;
}
}
}
}
}
// 左侧子导航
.admin-sidebar-sub {
width: 180px;
background: #fff;
border-right: 1px solid #e4e7ed;
display: flex;
flex-direction: column;
overflow-y: auto;
.sub-nav {
padding: 16px 0;
.sub-nav-title {
padding: 8px 20px;
font-size: 12px;
color: #909399;
margin-bottom: 8px;
}
.sub-nav-item {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 20px;
cursor: pointer;
color: #606266;
font-size: 14px;
transition: all 0.2s;
&:hover {
background: rgba(124, 58, 237, 0.1);
color: #7c3aed;
}
&.active {
background: rgba(124, 58, 237, 0.15);
color: #7c3aed;
border-right: 3px solid #7c3aed;
}
.el-icon {
font-size: 16px;
}
}
}
}
.admin-main {
flex: 1;
overflow-y: auto;
}
.admin-content {
.content-header {
padding: 32px 40px;
background: #fff;
border-bottom: 1px solid #e4e7ed;
h1 {
font-size: 28px;
margin: 0 0 8px 0;
color: #303133;
}
.subtitle {
font-size: 14px;
color: #909399;
margin: 0;
}
}
.dashboard-cards {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20px;
padding: 24px 40px;
}
.stat-card {
background: #fff;
padding: 24px;
border-radius: 8px;
display: flex;
gap: 16px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
.stat-icon {
width: 56px;
height: 56px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-size: 28px;
color: #fff;
}
.stat-info {
flex: 1;
}
.stat-label {
font-size: 14px;
color: #909399;
margin-bottom: 8px;
}
.stat-value {
font-size: 28px;
font-weight: 600;
color: #303133;
}
}
:deep(.el-card) {
margin: 0 40px 24px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
}
// 个人中心样式
.profile-content {
.profile-main {
padding: 24px 40px;
}
.profile-section {
.avatar-section {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 32px;
padding: 24px;
background: #f5f7fa;
border-radius: 8px;
}
}
.security-section {
.security-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 0;
.security-info {
h3 {
font-size: 16px;
margin: 0 0 8px 0;
color: #303133;
}
p {
font-size: 14px;
color: #909399;
margin: 0;
}
}
}
}
}
// 知识库管理样式
.content-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 24px;
.header-title {
h1 {
font-size: 24px;
margin: 0 0 8px 0;
color: #303133;
}
.subtitle {
font-size: 14px;
color: #909399;
margin: 0;
}
}
}
.tab-desc {
font-size: 14px;
color: #909399;
margin: 0 0 20px 0;
}
.kb-categories {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
margin-bottom: 24px;
}
.kb-category-card {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
padding: 20px 16px;
background: #f5f7fa;
border: 2px solid transparent;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
&:hover {
border-color: #409eff;
background: #ecf5ff;
}
&.active {
border-color: #409eff;
background: #ecf5ff;
}
.el-icon {
font-size: 28px;
}
.cat-name {
font-size: 14px;
font-weight: 500;
color: #303133;
text-align: center;
}
.cat-count {
font-size: 12px;
color: #909399;
}
}
.kb-files-section {
background: #fafafa;
padding: 20px;
border-radius: 8px;
}
.section-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
h3 {
font-size: 16px;
margin: 0;
color: #303133;
}
}
.file-name-cell {
display: flex;
align-items: center;
gap: 8px;
.file-icon {
color: #409eff;
font-size: 18px;
}
}
// 日志导航样式
.sub-nav-group {
.sub-nav-item.has-children {
justify-content: flex-start;
.arrow-icon {
margin-left: auto;
transition: transform 0.2s;
}
&.expanded .arrow-icon {
transform: rotate(180deg);
}
}
}
.sub-nav-children {
background: rgba(124, 58, 237, 0.05);
.sub-nav-child-item {
padding: 8px 20px 8px 44px;
cursor: pointer;
color: #606266;
font-size: 13px;
transition: all 0.2s;
&:hover {
background: rgba(124, 58, 237, 0.1);
color: #7c3aed;
}
&.active {
background: rgba(124, 58, 237, 0.15);
color: #7c3aed;
}
}
}
// 数据概览样式
.dashboard-stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
padding: 0 40px;
margin-bottom: 24px;
.stat-card {
background: #fff;
border-radius: 12px;
padding: 24px;
display: flex;
align-items: flex-start;
gap: 20px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
transition: all 0.3s;
&.clickable {
cursor: pointer;
&:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
}
}
.stat-icon {
width: 60px;
height: 60px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 28px;
color: #fff;
flex-shrink: 0;
}
.stat-info {
flex: 1;
.stat-value {
font-size: 32px;
font-weight: 700;
color: #303133;
line-height: 1.2;
}
.stat-label {
font-size: 14px;
color: #909399;
margin-top: 4px;
}
.stat-trend {
display: flex;
align-items: center;
gap: 4px;
font-size: 12px;
color: #909399;
margin-top: 8px;
&.up {
color: #67c23a;
}
&.down {
color: #f56c6c;
}
}
}
}
}
.dashboard-charts {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
padding: 0 40px;
margin-bottom: 24px;
.chart-card {
margin: 0 !important;
}
}
.question-stats {
.question-stat-item {
padding: 12px 0;
border-bottom: 1px solid #f0f0f0;
&:last-child {
border-bottom: none;
}
&.clickable {
cursor: pointer;
padding: 12px;
margin: 0 -12px;
border-radius: 8px;
&:hover {
background: #f5f7fa;
}
}
.stat-bar-header {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
.stat-name {
font-size: 14px;
color: #303133;
}
.stat-count {
font-size: 14px;
color: #606266;
font-weight: 500;
}
}
.stat-bar {
height: 8px;
background: #f0f0f0;
border-radius: 4px;
overflow: hidden;
.stat-bar-fill {
height: 100%;
border-radius: 4px;
transition: width 0.6s ease;
}
}
}
}
.product-cloud {
display: flex;
flex-wrap: wrap;
gap: 16px 24px;
padding: 20px 0;
justify-content: center;
align-items: center;
min-height: 180px;
.cloud-tag {
cursor: pointer;
transition: all 0.2s;
font-weight: 500;
&:hover {
transform: scale(1.15);
opacity: 1 !important;
}
}
}
.quick-entry-card {
margin: 0 40px 24px !important;
}
.quick-entries {
display: flex;
gap: 24px;
justify-content: center;
.quick-entry-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
padding: 20px 32px;
cursor: pointer;
border-radius: 12px;
transition: all 0.2s;
&:hover {
background: #f5f7fa;
.entry-icon {
transform: scale(1.1);
}
}
.entry-icon {
width: 48px;
height: 48px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
color: #fff;
transition: transform 0.2s;
}
span {
font-size: 14px;
color: #606266;
}
}
}
// 工单管理样式
.filter-card {
margin: 0 40px 16px !important;
}
.ticket-filters {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 12px;
.el-radio-group {
.el-badge {
margin-left: 4px;
}
}
.filter-right {
display: flex;
gap: 12px;
}
}
.ticket-no {
font-family: monospace;
color: #409eff;
font-weight: 500;
}
.customer-info {
display: flex;
flex-direction: column;
gap: 2px;
.name {
font-weight: 500;
color: #303133;
}
.phone {
font-size: 12px;
color: #909399;
}
}
.unassigned {
color: #c0c4cc;
font-style: italic;
}
.table-pagination {
display: flex;
justify-content: flex-end;
margin-top: 16px;
}
// 工单详情样式
.ticket-detail {
.detail-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
.ticket-info {
display: flex;
align-items: center;
gap: 12px;
.ticket-no {
font-size: 18px;
font-weight: 600;
}
}
}
.detail-section {
margin-top: 24px;
h4 {
font-size: 15px;
margin: 0 0 16px 0;
color: #303133;
}
}
.log-content {
.log-operator {
font-weight: 500;
margin-right: 8px;
}
.log-action {
color: #606266;
}
}
.log-remark {
margin-top: 4px;
font-size: 13px;
color: #909399;
}
}
.ticket-images {
display: flex;
flex-wrap: wrap;
gap: 12px;
.ticket-image-item {
width: 120px;
height: 90px;
border-radius: 8px;
overflow: hidden;
cursor: pointer;
border: 1px solid #e4e7ed;
transition: all 0.2s;
&:hover {
border-color: #409eff;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
}
.image-error {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background: #f5f7fa;
color: #c0c4cc;
font-size: 24px;
}
}
.detail-section h4 {
display: flex;
align-items: center;
gap: 8px;
.el-icon {
color: #7c3aed;
}
}
// 对话数据管理样式
.conversation-filters {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.session-id {
font-family: monospace;
color: #7c3aed;
font-weight: 500;
}
.conversation-summary-text {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
color: #606266;
font-size: 13px;
}
.satisfaction-stars {
display: flex;
gap: 2px;
.star-filled {
color: #f7ba2a;
}
.star-empty {
color: #e4e7ed;
}
}
.no-rating {
color: #c0c4cc;
font-size: 12px;
}
.conversation-detail-header {
margin-bottom: 20px;
padding-bottom: 16px;
border-bottom: 1px solid #e4e7ed;
.detail-info {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 12px;
.session-id {
font-size: 18px;
font-weight: 600;
}
}
.detail-meta {
display: flex;
gap: 24px;
font-size: 13px;
color: #909399;
}
}
// 智能体管理样式
.agent-config-card {
margin: 0 40px !important;
}
.agent-header {
display: flex;
align-items: center;
gap: 16px;
.agent-info {
flex: 1;
h2 {
margin: 0 0 4px 0;
font-size: 20px;
font-weight: 600;
color: #303133;
}
.agent-type {
font-size: 14px;
color: #909399;
}
}
}
.agent-form {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0 40px;
.el-form-item {
margin-bottom: 24px;
&.full-width {
grid-column: 1 / -1;
}
}
.feature-switches {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px 24px;
.el-checkbox {
margin-right: 0;
}
}
.form-tip {
margin-top: 8px;
font-size: 12px;
color: #909399;
}
.form-actions {
grid-column: 1 / -1;
margin-top: 16px;
}
}
// 日志管理样式
.log-filters {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.log-level {
display: inline-block;
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 600;
&.info {
background: #e6f7ff;
color: #1890ff;
}
&.warn {
background: #fff7e6;
color: #fa8c16;
}
&.error {
background: #fff2f0;
color: #ff4d4f;
}
}
// 指派工程师弹窗样式
.assign-dialog {
.ai-recommend {
background: linear-gradient(135deg, #f0e6ff, #e6f0ff);
border-radius: 12px;
padding: 16px;
margin-bottom: 20px;
.recommend-header {
display: flex;
align-items: center;
gap: 8px;
color: #7c3aed;
font-weight: 600;
margin-bottom: 12px;
.el-icon {
font-size: 18px;
}
}
.engineer-card.recommended {
background: #fff;
border: 2px solid #7c3aed;
border-radius: 8px;
padding: 12px;
.match-score {
text-align: center;
.score {
display: block;
font-size: 24px;
font-weight: 700;
color: #7c3aed;
}
.label {
font-size: 12px;
color: #909399;
}
}
}
.recommend-reason {
display: flex;
align-items: flex-start;
gap: 8px;
margin-top: 12px;
padding: 12px;
background: rgba(255, 255, 255, 0.6);
border-radius: 8px;
font-size: 13px;
color: #606266;
line-height: 1.6;
.el-icon {
color: #7c3aed;
margin-top: 2px;
flex-shrink: 0;
}
}
}
.engineer-list {
.list-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
span {
font-weight: 500;
color: #303133;
}
}
.list-content {
max-height: 240px;
overflow-y: auto;
.engineer-card {
padding: 12px;
border: 1px solid #e4e7ed;
border-radius: 8px;
margin-bottom: 8px;
cursor: pointer;
transition: all 0.2s;
&:hover {
border-color: #409eff;
background: #f5f7fa;
}
&.selected {
border-color: #409eff;
background: #ecf5ff;
}
}
}
}
.engineer-info {
display: flex;
align-items: center;
gap: 12px;
.info-text {
flex: 1;
.name {
font-weight: 500;
color: #303133;
}
.title {
font-size: 12px;
color: #909399;
margin-top: 2px;
}
}
}
}
// 对话记录弹窗样式
.conversation-record {
max-height: 400px;
overflow-y: auto;
padding: 16px;
background: #f5f7fa;
border-radius: 8px;
.message-item {
display: flex;
gap: 12px;
margin-bottom: 16px;
&.user {
flex-direction: row-reverse;
.message-content {
align-items: flex-end;
.message-text {
background: #409eff;
color: #fff;
}
}
}
&.assistant {
.message-content {
.message-text {
background: #fff;
}
}
}
.message-content {
display: flex;
flex-direction: column;
max-width: 70%;
.message-header {
display: flex;
gap: 8px;
margin-bottom: 4px;
font-size: 12px;
.sender {
color: #606266;
font-weight: 500;
}
.time {
color: #909399;
}
}
.message-text {
padding: 12px 16px;
border-radius: 12px;
line-height: 1.6;
white-space: pre-wrap;
}
}
}
}
.conversation-summary {
.summary-section {
margin-bottom: 24px;
&:last-child {
margin-bottom: 0;
}
h4 {
display: flex;
align-items: center;
gap: 8px;
font-size: 15px;
margin: 0 0 12px 0;
color: #303133;
.el-icon {
color: #7c3aed;
}
}
p {
margin: 0;
line-height: 1.8;
color: #606266;
}
ul {
margin: 0;
padding-left: 20px;
li {
line-height: 1.8;
color: #606266;
}
}
}
}
// 占位页面样式
.placeholder-content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80px 20px;
text-align: center;
.placeholder-icon {
font-size: 64px;
color: #dcdfe6;
margin-bottom: 24px;
}
h3 {
font-size: 18px;
color: #606266;
margin: 0 0 12px 0;
}
p {
font-size: 14px;
color: #909399;
margin: 0;
}
}
</style>