|
|
@@ -0,0 +1,1611 @@
|
|
|
+<template>
|
|
|
+ <div class="ai-statistics-container">
|
|
|
+
|
|
|
+ <!-- 统计卡片区域 -->
|
|
|
+ <div class="stats-dashboard">
|
|
|
+ <el-row :gutter="24">
|
|
|
+ <el-col :xs="24" :sm="12" :md="6">
|
|
|
+ <div class="stat-card stat-session-card">
|
|
|
+ <div class="card-content">
|
|
|
+ <div class="stat-value">{{ formatNumber(totalSession) }}</div>
|
|
|
+ <div class="stat-label">总会话数</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-col>
|
|
|
+ <el-col :xs="24" :sm="12" :md="6">
|
|
|
+ <div class="stat-card stat-token-card">
|
|
|
+ <div class="card-content">
|
|
|
+ <div class="stat-value">{{ formatNumber(totalToken) }}</div>
|
|
|
+ <div class="stat-label">总消耗Token</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-col>
|
|
|
+ <el-col :xs="24" :sm="12" :md="6">
|
|
|
+ <div class="stat-card stat-input-card">
|
|
|
+ <div class="card-content">
|
|
|
+ <div class="stat-value">{{ formatNumber(chatPromptToken) }}</div>
|
|
|
+ <div class="stat-label">入参消耗Token</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-col>
|
|
|
+ <el-col :xs="24" :sm="12" :md="6">
|
|
|
+ <div class="stat-card stat-output-card">
|
|
|
+ <div class="card-content">
|
|
|
+ <div class="stat-value">{{ formatNumber(chatCompletionToken) }}</div>
|
|
|
+ <div class="stat-label">出参消耗Token</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 筛选区域 -->
|
|
|
+ <el-card shadow="hover" class="filter-card">
|
|
|
+ <div class="filter-header">
|
|
|
+ <span class="filter-title"><i class="el-icon-search"></i> 筛选条件</span>
|
|
|
+ <el-button-group>
|
|
|
+ <el-button v-for="preset in presets" :key="preset.value" :type="activePreset === preset.value ? 'primary' : 'default'" size="small" round @click="selectPreset(preset.value)">{{ preset.label }}</el-button>
|
|
|
+ </el-button-group>
|
|
|
+ </div>
|
|
|
+ <el-form :inline="true" :model="queryParams" class="filter-form">
|
|
|
+ <el-form-item>
|
|
|
+ <el-date-picker v-model="queryParams.dateRange" type="daterange" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" value-format="yyyy-MM-dd" size="small" style="width: 260px" @change="handleDateChange" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item>
|
|
|
+ <el-select v-model="queryParams.aiSopCallRoleId" placeholder="选择外呼角色" clearable filterable size="small" style="width: 160px" @change="handleQuery">
|
|
|
+ <el-option v-for="item in aiRoleOptions" :key="item.id" :label="item.callRoleName" :value="item.id" />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item>
|
|
|
+ <el-button type="primary" size="small" icon="el-icon-search" @click="handleQuery" v-hasPermi="['qw:groupVoiceMsgUser:details:statistics']">搜索</el-button>
|
|
|
+ <el-button size="small" icon="el-icon-refresh" @click="resetQuery">重置</el-button>
|
|
|
+ <el-button type="success" size="small" icon="el-icon-download" @click="handleExport" v-hasPermi="['qw:groupVoiceMsgUser:details:exportStatistics']">导出</el-button>
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+ </el-card>
|
|
|
+
|
|
|
+ <!-- 数据展示区域 -->
|
|
|
+ <el-card shadow="never" class="data-table-card">
|
|
|
+ <div slot="header" class="table-header">
|
|
|
+ <div class="header-left">
|
|
|
+ <i class="el-icon-data-analysis"></i>
|
|
|
+ <span class="header-title">智能数据分析</span>
|
|
|
+ <el-tag size="mini" type="info">共 {{ total }} 条记录</el-tag>
|
|
|
+ </div>
|
|
|
+ <div class="header-actions">
|
|
|
+ <el-button
|
|
|
+ type="text"
|
|
|
+ icon="el-icon-refresh"
|
|
|
+ :loading="loading"
|
|
|
+ @click="getList"
|
|
|
+ class="refresh-btn"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <el-table
|
|
|
+ v-loading="loading"
|
|
|
+ :data="detailsList"
|
|
|
+ stripe
|
|
|
+ border
|
|
|
+ class="modern-table"
|
|
|
+ :header-cell-style="headerStyle"
|
|
|
+ @sort-change="handleSortChange"
|
|
|
+ >
|
|
|
+ <el-table-column type="index" label="序号" width="100" align="center" />
|
|
|
+ <el-table-column prop="aiSopCallRoleName" label="外呼角色" width="200" sortable>
|
|
|
+ <template slot-scope="{row}">
|
|
|
+ <div class="role-cell">
|
|
|
+ <div class="role-avatar">
|
|
|
+ <i class="el-icon-service"></i>
|
|
|
+ <div class="avatar-glow"></div>
|
|
|
+ </div>
|
|
|
+ <span class="role-name">{{ row.aiSopCallRoleName || '-' }}</span>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="durationMsStr" label="总时长" width="200" align="center" sortable />
|
|
|
+ <el-table-column prop="totalSession" label="总会话数" align="center" sortable>
|
|
|
+ <template slot-scope="{row}">
|
|
|
+ <span class="metric-value primary">{{ formatNumber(row.totalSession) }}</span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="totalToken" label="总Token" align="center" sortable>
|
|
|
+ <template slot-scope="{row}">
|
|
|
+ <span class="metric-value success">{{ formatNumber(row.totalToken) }}</span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="chatPromptToken" label="输入Token" align="center" sortable>
|
|
|
+ <template slot-scope="{row}">
|
|
|
+ <span class="metric-value warning">{{ formatNumber(row.chatPromptToken) }}</span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="chatCompletionToken" label="输出Token" align="center" sortable>
|
|
|
+ <template slot-scope="{row}">
|
|
|
+ <span class="metric-value info">{{ formatNumber(row.chatCompletionToken) }}</span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="createTime" label="创建时间" min-width="160" align="center" sortable />
|
|
|
+ <el-table-column label="操作" width="120" align="center" fixed="right">
|
|
|
+ <template slot-scope="{row}">
|
|
|
+ <div class="action-cell">
|
|
|
+ <el-tooltip content="查看详细数据" placement="top">
|
|
|
+ <el-button
|
|
|
+ type="primary"
|
|
|
+ size="mini"
|
|
|
+ icon="el-icon-view"
|
|
|
+ circle
|
|
|
+ @click="handleView(row)"
|
|
|
+ v-hasPermi="['qw:groupVoiceMsgUser:details:list']"
|
|
|
+ class="view-btn"
|
|
|
+ />
|
|
|
+ </el-tooltip>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+ <div class="table-footer">
|
|
|
+ <div class="pagination-info">
|
|
|
+ <span>显示第 {{ (queryParams.pageNum - 1) * queryParams.pageSize + 1 }} 到 {{ Math.min(queryParams.pageNum * queryParams.pageSize, total) }} 条,共 {{ total }} 条记录</span>
|
|
|
+ </div>
|
|
|
+ <pagination
|
|
|
+ v-show="total>0"
|
|
|
+ :total="total"
|
|
|
+ :page.sync="queryParams.pageNum"
|
|
|
+ :limit.sync="queryParams.pageSize"
|
|
|
+ @pagination="getList"
|
|
|
+ class="modern-pagination"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </el-card>
|
|
|
+
|
|
|
+
|
|
|
+ <!-- 数据展示页面 -->
|
|
|
+ <el-dialog
|
|
|
+ :title="`会话详情 - ${currentRow.aiSopCallRoleName || '未知客服'}`"
|
|
|
+ :visible.sync="detailDialogVisible"
|
|
|
+ width="85%"
|
|
|
+ top="3vh"
|
|
|
+ append-to-body
|
|
|
+ class="detail-dialog-enhanced"
|
|
|
+ :close-on-click-modal="false"
|
|
|
+ :modal-append-to-body="true"
|
|
|
+ >
|
|
|
+ <div v-loading="detailLoading" class="dialog-content-wrapper">
|
|
|
+ <!-- 数据表格 -->
|
|
|
+ <el-card shadow="never" class="detail-table-card">
|
|
|
+ <div slot="header" class="table-header-enhanced">
|
|
|
+ <span><i class="el-icon-chat-line-round"></i> 会话记录详情</span>
|
|
|
+ <el-tag type="info" size="medium">共 {{ detailTotal }} 条记录</el-tag>
|
|
|
+ </div>
|
|
|
+ <el-table
|
|
|
+ :data="detailList"
|
|
|
+ stripe
|
|
|
+ border
|
|
|
+ v-loading="detailLoading"
|
|
|
+ style="width: 100%"
|
|
|
+ class="clean-table"
|
|
|
+ >
|
|
|
+ <el-table-column prop="sessionId" label="会话ID" align="center" min-width="120" />
|
|
|
+ <el-table-column prop="externalUserName" label="联系人" align="center" min-width="100" />
|
|
|
+ <el-table-column prop="satisfaction" label="满意度" align="center" min-width="100">
|
|
|
+ <template slot-scope="{row}">
|
|
|
+ <el-tag
|
|
|
+ :type="row.satisfaction === 3 ? 'success' : row.satisfaction === 2 ? 'warning' : 'danger'"
|
|
|
+ size="small"
|
|
|
+ effect="dark"
|
|
|
+ >
|
|
|
+ <i :class="row.satisfaction === 3 ? 'el-icon-success' : row.satisfaction === 2 ? 'el-icon-warning' : 'el-icon-error'"></i>
|
|
|
+ {{ formatSatisfaction(row.satisfaction) }}
|
|
|
+ </el-tag>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="totalToken" label="总Token" align="center" min-width="100" />
|
|
|
+ <el-table-column prop="chatPromptToken" label="输入Token" align="center" min-width="100" />
|
|
|
+ <el-table-column prop="chatCompletionToken" label="输出Token" align="center" min-width="100" />
|
|
|
+ <el-table-column prop="aiChatDetails" label="内容" align="center" min-width="300">
|
|
|
+ <template slot-scope="{row}">
|
|
|
+ <div
|
|
|
+ class="content-text"
|
|
|
+ :title="formatChatContent(row.aiChatDetails, row.aiSopCallRoleName, row.externalUserName)"
|
|
|
+ >
|
|
|
+ {{ truncateText(formatChatContent(row.aiChatDetails, row.aiSopCallRoleName, row.externalUserName), 3) }}
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="语音" align="center" min-width="140">
|
|
|
+ <template slot-scope="{row}">
|
|
|
+ <div v-if="isValidVoiceData(row.voiceText)" class="audio-container">
|
|
|
+ <audio
|
|
|
+ :src="getAudioSource(row.voiceText)"
|
|
|
+ controls
|
|
|
+ style="width: 120px; height: 32px"
|
|
|
+ @error="handleAudioError"
|
|
|
+ preload="none"
|
|
|
+ :title="isAudioFailed(getAudioSource(row.voiceText)) ? '音频加载失败' : '语音播放'"
|
|
|
+ ></audio>
|
|
|
+ <el-tag
|
|
|
+ v-if="isAudioFailed(getAudioSource(row.voiceText))"
|
|
|
+ size="mini"
|
|
|
+ type="danger"
|
|
|
+ class="audio-error"
|
|
|
+ >
|
|
|
+ 加载失败
|
|
|
+ </el-tag>
|
|
|
+ <el-tooltip
|
|
|
+ v-else-if="Array.isArray(row.voiceText) && row.voiceText.length > 1"
|
|
|
+ content="点击查看所有语音">
|
|
|
+ <el-tag size="mini" type="info" class="audio-count">
|
|
|
+ +{{ row.voiceText.length - 1 }}
|
|
|
+ </el-tag>
|
|
|
+ </el-tooltip>
|
|
|
+ </div>
|
|
|
+ <span v-else class="no-data">-</span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+
|
|
|
+ <el-table-column prop="createTime" label="创建时间" align="center" min-width="150" />
|
|
|
+ </el-table>
|
|
|
+
|
|
|
+ <!-- 分页 -->
|
|
|
+ <pagination
|
|
|
+ v-show="detailTotal>0"
|
|
|
+ :total="detailTotal"
|
|
|
+ :page.sync="detailQuery.pageNum"
|
|
|
+ :limit.sync="detailQuery.pageSize"
|
|
|
+ @pagination="handleDetailPagination"
|
|
|
+ class="detail-pagination"
|
|
|
+ />
|
|
|
+ </el-card>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div slot="footer" class="dialog-footer-enhanced">
|
|
|
+ <el-button @click="detailDialogVisible = false" class="close-btn">
|
|
|
+ <i class="el-icon-close"></i> 关闭
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ </el-dialog>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+import { listDetails,statisticsAiCallDetails,exportStatistics } from "@/api/qw/groupVoiceMsgUserDetails";
|
|
|
+import { listRole } from "@/api/aiSopCall/aiSopCallRole";
|
|
|
+import Pagination from "@/components/Pagination";
|
|
|
+
|
|
|
+export default {
|
|
|
+ name: "detailsStatistics",
|
|
|
+ components: { Pagination },
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ // AI智能分析相关
|
|
|
+ aiAnalysisDialogVisible: false,
|
|
|
+ aiAnalysisLoading: false,
|
|
|
+
|
|
|
+ // 查看详情相关数据
|
|
|
+ detailDialogVisible: false,
|
|
|
+ detailLoading: false,
|
|
|
+ currentRow: {},
|
|
|
+ detailData: {},
|
|
|
+ detailList: [],
|
|
|
+ detailTotal: 0,
|
|
|
+ detailQuery: {
|
|
|
+ pageNum: 1,
|
|
|
+ pageSize: 10,
|
|
|
+ sessionId: null
|
|
|
+ },
|
|
|
+
|
|
|
+ // 时间快捷选项
|
|
|
+ presets: [
|
|
|
+ { value: 'today', label: '今天' },
|
|
|
+ { value: 'week', label: '本周' },
|
|
|
+ { value: 'month', label: '本月' },
|
|
|
+ { value: 'quarter', label: '本季度' },
|
|
|
+ { value: 'year', label: '本年' }
|
|
|
+ ],
|
|
|
+ activePreset: 'month',
|
|
|
+
|
|
|
+ // 统计卡片数据
|
|
|
+ totalSession: 0,
|
|
|
+ totalToken: 0,
|
|
|
+ chatPromptToken: 0,
|
|
|
+ chatCompletionToken: 0,
|
|
|
+ failedAudioSources: new Set(), // 跟踪加载失败的音频
|
|
|
+ // 外呼角色选项
|
|
|
+ aiRoleOptions: [],
|
|
|
+ // 遮罩层
|
|
|
+ loading: true,
|
|
|
+ // 导出遮罩层
|
|
|
+ exportLoading: false,
|
|
|
+ // 选中数组
|
|
|
+ ids: [],
|
|
|
+ // 总条数
|
|
|
+ total: 0,
|
|
|
+ // AI外呼客服呼叫详情表格数据
|
|
|
+ detailsList: [],
|
|
|
+ // 查询参数
|
|
|
+ queryParams: {
|
|
|
+ pageNum: 1,
|
|
|
+ pageSize: 10,
|
|
|
+ aiSopCallRoleId: null,
|
|
|
+ aiSopCallRoleName: null,
|
|
|
+ externalUserId: null,
|
|
|
+ externalUserName: null,
|
|
|
+ dateRange: [],
|
|
|
+ startTime: null,
|
|
|
+ endTime: null
|
|
|
+ },
|
|
|
+ // 查询参数
|
|
|
+ queryDetailsParams: {
|
|
|
+ pageNum: 1,
|
|
|
+ pageSize: 10,
|
|
|
+ aiSopCallRoleId: null,
|
|
|
+ startTime: null,
|
|
|
+ endTime: null
|
|
|
+ },
|
|
|
+ // 表头样式
|
|
|
+ headerStyle: {
|
|
|
+ background: '#ffffff',
|
|
|
+ color: '#4c5161',
|
|
|
+ fontWeight: '600',
|
|
|
+ fontSize: '14px',
|
|
|
+ textAlign: 'center'
|
|
|
+ }
|
|
|
+ };
|
|
|
+ },
|
|
|
+ created() {
|
|
|
+ // 设置默认时间范围:本月第一天到当前时间
|
|
|
+ this.setDefaultDateRange();
|
|
|
+ this.loadAiRoles();
|
|
|
+ this.getList();
|
|
|
+ },
|
|
|
+
|
|
|
+ methods: {
|
|
|
+
|
|
|
+ // 设置默认时间范围
|
|
|
+ setDefaultDateRange() {
|
|
|
+ const now = new Date();
|
|
|
+ const firstDay = new Date(now.getFullYear(), now.getMonth(), 1);
|
|
|
+
|
|
|
+ this.queryParams.dateRange = [
|
|
|
+ this.formatDate(firstDay),
|
|
|
+ this.formatDate(now)
|
|
|
+ ];
|
|
|
+ this.queryParams.startTime = this.formatDate(firstDay);
|
|
|
+ this.queryParams.endTime = this.formatDate(now);
|
|
|
+ },
|
|
|
+
|
|
|
+
|
|
|
+ // 格式化日期
|
|
|
+ formatDate(date) {
|
|
|
+ const year = date.getFullYear();
|
|
|
+ const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
|
+ const day = String(date.getDate()).padStart(2, '0');
|
|
|
+ return `${year}-${month}-${day}`;
|
|
|
+ },
|
|
|
+
|
|
|
+ /** 日期变化处理 */
|
|
|
+ handleDateChange(val) {
|
|
|
+ if (val && val.length >= 2) {
|
|
|
+ this.queryParams.startTime = val[0];
|
|
|
+ this.queryParams.endTime = val[1];
|
|
|
+ } else {
|
|
|
+ this.queryParams.startTime = null;
|
|
|
+ this.queryParams.endTime = null;
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ /** 加载外呼角色列表 */
|
|
|
+ loadAiRoles() {
|
|
|
+ listRole({ pageSize: 100, pageNum: 1 }).then(response => {
|
|
|
+ if (response.code === 200) {
|
|
|
+ this.aiRoleOptions = response.rows || [];
|
|
|
+ }
|
|
|
+ }).catch(error => {
|
|
|
+ console.error('加载外呼角色失败:', error);
|
|
|
+ this.$message.error('加载外呼角色失败');
|
|
|
+ });
|
|
|
+ },
|
|
|
+
|
|
|
+ /** 查询AI外呼客服呼叫详情列表 */
|
|
|
+ getList() {
|
|
|
+ this.loading = true;
|
|
|
+ statisticsAiCallDetails(this.queryParams).then(response => {
|
|
|
+ this.detailsList = response.rows || [];
|
|
|
+ this.total = response.total || 0;
|
|
|
+ this.calculateStatistics();
|
|
|
+ this.loading = false;
|
|
|
+ });
|
|
|
+ },
|
|
|
+
|
|
|
+ /** 计算统计数据 */
|
|
|
+ calculateStatistics() {
|
|
|
+ const stats = { totalSession: 0, totalToken: 0, chatPromptToken: 0, chatCompletionToken: 0 };
|
|
|
+ this.detailsList.forEach(item => {
|
|
|
+ stats.totalSession += (item.totalSession || 0);
|
|
|
+ stats.totalToken += (item.totalToken || 0);
|
|
|
+ stats.chatPromptToken += (item.chatPromptToken || 0);
|
|
|
+ stats.chatCompletionToken += (item.chatCompletionToken || 0);
|
|
|
+ });
|
|
|
+ this.totalSession = stats.totalSession;
|
|
|
+ this.totalToken = stats.totalToken;
|
|
|
+ this.chatPromptToken = stats.chatPromptToken;
|
|
|
+ this.chatCompletionToken = stats.chatCompletionToken;
|
|
|
+ },
|
|
|
+
|
|
|
+ /** 格式化数字 */
|
|
|
+ formatNumber(num) {
|
|
|
+ if (num === null || num === undefined) return '0';
|
|
|
+ return num.toLocaleString();
|
|
|
+ },
|
|
|
+
|
|
|
+ /** 格式化聊天内容 */
|
|
|
+ formatChatContent(content, roleName, externalName) {
|
|
|
+ if (!content) return '';
|
|
|
+ return content
|
|
|
+ .replace(/\[客服\]/g, roleName || '客服')
|
|
|
+ .replace(/\[客户\]/g, externalName || '客户');
|
|
|
+ },
|
|
|
+
|
|
|
+ /** 格式化满意度 */
|
|
|
+ formatSatisfaction(value) {
|
|
|
+ if (value === 1) return '低';
|
|
|
+ if (value === 2) return '中';
|
|
|
+ if (value === 3) return '高';
|
|
|
+ return '-';
|
|
|
+ },
|
|
|
+
|
|
|
+ /** 截断文本显示 */
|
|
|
+ truncateText(text, maxLines = 2) {
|
|
|
+ if (!text) return '-';
|
|
|
+ const lines = text.split('\n');
|
|
|
+ if (lines.length <= maxLines) return text;
|
|
|
+ return lines.slice(0, maxLines).join('\n') + '...';
|
|
|
+ },
|
|
|
+
|
|
|
+ /** 格式化语音文本 */
|
|
|
+ formatVoiceText(voiceText) {
|
|
|
+ if (!voiceText) return '';
|
|
|
+ if (Array.isArray(voiceText)) {
|
|
|
+ return voiceText.join(', ');
|
|
|
+ }
|
|
|
+ if (typeof voiceText === 'string') {
|
|
|
+ return voiceText;
|
|
|
+ }
|
|
|
+ return String(voiceText);
|
|
|
+ },
|
|
|
+
|
|
|
+ /** 获取音频源 */
|
|
|
+ getAudioSource(voiceText) {
|
|
|
+ if (!this.isValidVoiceData(voiceText)) {
|
|
|
+ return '';
|
|
|
+ }
|
|
|
+
|
|
|
+ // 处理数组形式的语音
|
|
|
+ if (Array.isArray(voiceText)) {
|
|
|
+ // 找到第一个有效的音频URL
|
|
|
+ const validUrl = voiceText.find(url => this.isValidAudioUrl(url));
|
|
|
+ return validUrl || '';
|
|
|
+ }
|
|
|
+
|
|
|
+ // 处理字符串形式的语音
|
|
|
+ if (typeof voiceText === 'string') {
|
|
|
+ if (this.isValidAudioUrl(voiceText)) {
|
|
|
+ let url = voiceText.trim();
|
|
|
+
|
|
|
+ // 验证URL格式
|
|
|
+ try {
|
|
|
+ // 如果已经是完整URL,则直接返回
|
|
|
+ if (url.startsWith('http://') || url.startsWith('https://')) {
|
|
|
+ return url;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果是相对路径,添加基础URL
|
|
|
+ if (url.startsWith('/')) {
|
|
|
+ return url;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果是有效的URL片段,构建完整URL
|
|
|
+ if (url && !url.startsWith('http') && !url.startsWith('/') && !url.startsWith('blob:')) {
|
|
|
+ return process.env.VUE_APP_BASE_API + '/' + url;
|
|
|
+ }
|
|
|
+
|
|
|
+ return url;
|
|
|
+ } catch (e) {
|
|
|
+ console.error('URL格式错误:', e);
|
|
|
+ return '';
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return '';
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 验证语音URL格式是否有效
|
|
|
+ * @param {string} url - 待验证的URL
|
|
|
+ * @returns {boolean} - 是否为有效的语音URL格式
|
|
|
+ */
|
|
|
+ isValidAudioUrl(url) {
|
|
|
+ if (!url || typeof url !== 'string') {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 去除首尾空白字符
|
|
|
+ url = url.trim();
|
|
|
+
|
|
|
+ // 检查是否为空
|
|
|
+ if (!url) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 定义有效的音频文件扩展名
|
|
|
+ const validExtensions = ['.mp3', '.wav', '.ogg', '.m4a', '.aac', '.flac'];
|
|
|
+ const lowerCaseUrl = url.toLowerCase();
|
|
|
+
|
|
|
+ // 检查URL是否包含有效的音频扩展名
|
|
|
+ const hasValidExtension = validExtensions.some(ext => lowerCaseUrl.includes(ext));
|
|
|
+
|
|
|
+ // 如果包含有效扩展名,直接返回true
|
|
|
+ if (hasValidExtension) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查是否为有效的HTTP/HTTPS URL
|
|
|
+ try {
|
|
|
+ const parsedUrl = new URL(url);
|
|
|
+ return parsedUrl.protocol === 'http:' || parsedUrl.protocol === 'https:';
|
|
|
+ } catch (e) {
|
|
|
+ // 如果URL解析失败,检查是否为相对路径且包含音频相关信息
|
|
|
+ return /^\/.*\.(mp3|wav|ogg|m4a|aac|flac)/i.test(url) ||
|
|
|
+ /audio/i.test(url) ||
|
|
|
+ /voice/i.test(url) ||
|
|
|
+ /record/i.test(url);
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 检查语音数据是否有效
|
|
|
+ * @param {any} voiceText - 语音数据
|
|
|
+ * @returns {boolean} - 语音数据是否有效
|
|
|
+ */
|
|
|
+ isValidVoiceData(voiceText) {
|
|
|
+ if (!voiceText) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (Array.isArray(voiceText)) {
|
|
|
+ // 数组情况下,检查是否有至少一个有效的音频URL
|
|
|
+ return voiceText.some(url => this.isValidAudioUrl(url));
|
|
|
+ }
|
|
|
+
|
|
|
+ // 字符串情况下,检查是否为有效的音频URL
|
|
|
+ return this.isValidAudioUrl(voiceText);
|
|
|
+ },
|
|
|
+
|
|
|
+ /** 处理音频错误 */
|
|
|
+ handleAudioError(event) {
|
|
|
+ console.error('音频播放失败:', event);
|
|
|
+ const audioElement = event.target;
|
|
|
+ const src = audioElement.getAttribute('src');
|
|
|
+
|
|
|
+ // 将失败的源添加到集合中
|
|
|
+ if (src) {
|
|
|
+ this.failedAudioSources.add(src);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 清除当前音频源
|
|
|
+ audioElement.src = '';
|
|
|
+ audioElement.removeAttribute('src');
|
|
|
+
|
|
|
+ // 停止加载
|
|
|
+ audioElement.load();
|
|
|
+ },
|
|
|
+ /** 检查音频是否加载失败 */
|
|
|
+ isAudioFailed(src) {
|
|
|
+ return this.failedAudioSources.has(src);
|
|
|
+ },
|
|
|
+
|
|
|
+ /** 重置音频失败状态 */
|
|
|
+ resetAudioFailures() {
|
|
|
+ this.failedAudioSources.clear();
|
|
|
+ },
|
|
|
+
|
|
|
+ /** 截断语音文本显示 */
|
|
|
+ truncateVoiceText(voiceText) {
|
|
|
+ const formatted = this.formatVoiceText(voiceText);
|
|
|
+ if (!formatted) return '-';
|
|
|
+
|
|
|
+ const lines = formatted.split(',');
|
|
|
+ if (lines.length <= 2) return formatted;
|
|
|
+ return lines.slice(0, 2).join(',') + '...';
|
|
|
+ },
|
|
|
+
|
|
|
+ /** 选择快捷时间 */
|
|
|
+ selectPreset(value) {
|
|
|
+ this.activePreset = value;
|
|
|
+ const now = new Date();
|
|
|
+ let start = now;
|
|
|
+
|
|
|
+ switch (value) {
|
|
|
+ case 'today':
|
|
|
+ start = new Date(now.setHours(0, 0, 0, 0));
|
|
|
+ break;
|
|
|
+ case 'week':
|
|
|
+ start = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
|
|
+ start = new Date(start.setHours(0, 0, 0, 0));
|
|
|
+ break;
|
|
|
+ case 'month':
|
|
|
+ start = new Date(now.getFullYear(), now.getMonth(), 1);
|
|
|
+ break;
|
|
|
+ case 'quarter':
|
|
|
+ start = new Date(now.getFullYear(), Math.floor(now.getMonth() / 3) * 3, 1);
|
|
|
+ break;
|
|
|
+ case 'year':
|
|
|
+ start = new Date(now.getFullYear(), 0, 1);
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ this.queryParams.dateRange = [this.formatDate(start), this.formatDate(now)];
|
|
|
+ this.queryParams.startTime = this.formatDate(start);
|
|
|
+ this.queryParams.endTime = this.formatDate(now);
|
|
|
+ this.handleQuery();
|
|
|
+ },
|
|
|
+ /** 详情分页处理 */
|
|
|
+ handleDetailPagination() {
|
|
|
+ this.getDetailList();
|
|
|
+ },
|
|
|
+ /** 查看详情 */
|
|
|
+ handleView(row) {
|
|
|
+ this.currentRow = row;
|
|
|
+ // 重置分页参数
|
|
|
+ this.detailQuery.pageNum = 1;
|
|
|
+ this.detailQuery.pageSize = 10;
|
|
|
+ this.getDetailList();
|
|
|
+ },
|
|
|
+ getDetailList(){
|
|
|
+ this.detailDialogVisible = true;
|
|
|
+ this.queryDetailsParams = {
|
|
|
+ pageNum: this.detailQuery.pageNum,
|
|
|
+ pageSize: this.detailQuery.pageSize,
|
|
|
+ aiSopCallRoleId: this.currentRow.aiSopCallRoleId,
|
|
|
+ startTime: this.currentRow.startTime,
|
|
|
+ endTime: this.currentRow.endTime
|
|
|
+ };
|
|
|
+ listDetails(this.queryDetailsParams).then(response => {
|
|
|
+ this.detailList = response.rows || [];
|
|
|
+ this.detailTotal = response.total || 0;
|
|
|
+ this.detailLoading = false;
|
|
|
+ }).catch(() => {
|
|
|
+ this.detailLoading = false;
|
|
|
+ this.$message.error('获取详情列表失败');
|
|
|
+ });
|
|
|
+ },
|
|
|
+
|
|
|
+ /** 搜索按钮操作 */
|
|
|
+ handleQuery() {
|
|
|
+ this.queryParams.pageNum = 1;
|
|
|
+ this.getList();
|
|
|
+ },
|
|
|
+ // 分页大小变化
|
|
|
+ handleSizeChange(val) {
|
|
|
+ this.queryParams.pageSize = val;
|
|
|
+ this.getList();
|
|
|
+ },
|
|
|
+ // 当前页码变化
|
|
|
+ handleCurrentChange(val) {
|
|
|
+ this.queryParams.pageNum = val;
|
|
|
+ this.getList();
|
|
|
+ },
|
|
|
+ /** 重置按钮操作 */
|
|
|
+ resetQuery() {
|
|
|
+ this.resetForm("queryForm");
|
|
|
+ // 重置时间范围为默认值(本月第一天到当前时间)
|
|
|
+ this.setDefaultDateRange();
|
|
|
+ // 重置预设时间为本月
|
|
|
+ this.activePreset = 'month';
|
|
|
+ this.handleQuery();
|
|
|
+ },
|
|
|
+
|
|
|
+
|
|
|
+ /** 导出按钮操作 */
|
|
|
+ handleExport() {
|
|
|
+ const queryParams = this.queryParams;
|
|
|
+ this.$confirm('是否确认导出所有AI外呼客服统计数据项?', "警告", {
|
|
|
+ confirmButtonText: "确定",
|
|
|
+ cancelButtonText: "取消",
|
|
|
+ type: "warning"
|
|
|
+ }).then(() => {
|
|
|
+ this.exportLoading = true;
|
|
|
+ return exportStatistics(queryParams);
|
|
|
+ }).then(response => {
|
|
|
+ this.download(response.msg);
|
|
|
+ this.exportLoading = false;
|
|
|
+ }).catch(() => {});
|
|
|
+ },
|
|
|
+
|
|
|
+ // 排序处理
|
|
|
+ handleSortChange({ column, prop, order }) {
|
|
|
+ console.log('排序字段:', prop, '排序方式:', order);
|
|
|
+ // 可以在这里添加自定义排序逻辑
|
|
|
+ },
|
|
|
+
|
|
|
+ // 计算效率
|
|
|
+ calculateEfficiency() {
|
|
|
+ if (!this.currentRow.totalSession || !this.currentRow.totalToken) return '0';
|
|
|
+ const efficiency = (this.currentRow.totalSession / this.currentRow.totalToken * 10000).toFixed(1);
|
|
|
+ return efficiency;
|
|
|
+ },
|
|
|
+
|
|
|
+ // 预测下月数据
|
|
|
+ predictNextMonth() {
|
|
|
+ if (!this.currentRow.totalSession) return '0';
|
|
|
+ // 简单的线性预测
|
|
|
+ const predicted = Math.round(this.currentRow.totalSession * 1.15);
|
|
|
+ return this.formatNumber(predicted);
|
|
|
+ },
|
|
|
+
|
|
|
+ // 获取优化建议
|
|
|
+ getRecommendations() {
|
|
|
+ const tips = [
|
|
|
+ '建议优化提示词以提高响应准确性',
|
|
|
+ '可以考虑调整模型温度参数',
|
|
|
+ '增加训练数据可提升模型表现',
|
|
|
+ '定期更新知识库保持时效性'
|
|
|
+ ];
|
|
|
+ return tips;
|
|
|
+ },
|
|
|
+
|
|
|
+ // 导出AI报告
|
|
|
+ exportAIReport() {
|
|
|
+ this.$message.success('AI分析报告已导出');
|
|
|
+ this.aiAnalysisDialogVisible = false;
|
|
|
+ },
|
|
|
+
|
|
|
+ // 格式化时长
|
|
|
+ formatDuration(durationStr) {
|
|
|
+ if (!durationStr) return '00:00:00';
|
|
|
+ return durationStr;
|
|
|
+ }
|
|
|
+ }
|
|
|
+};
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.ai-statistics-container {
|
|
|
+ padding: 20px;
|
|
|
+ background: #ffffff;
|
|
|
+ min-height: 100vh;
|
|
|
+}
|
|
|
+
|
|
|
+.search-card {
|
|
|
+ margin-bottom: 20px;
|
|
|
+ border-radius: 12px;
|
|
|
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
|
|
+ background: #ffffff;
|
|
|
+}
|
|
|
+
|
|
|
+.card-title {
|
|
|
+ font-weight: 600;
|
|
|
+ color: #303133;
|
|
|
+}
|
|
|
+
|
|
|
+.table-card {
|
|
|
+ border-radius: 12px;
|
|
|
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
|
|
+ background: #ffffff;
|
|
|
+}
|
|
|
+
|
|
|
+.total-text {
|
|
|
+ float: right;
|
|
|
+ color: #909399;
|
|
|
+ font-size: 14px;
|
|
|
+}
|
|
|
+
|
|
|
+.compact-form {
|
|
|
+ margin-bottom: -18px;
|
|
|
+}
|
|
|
+
|
|
|
+.form-actions {
|
|
|
+ margin-left: auto;
|
|
|
+}
|
|
|
+
|
|
|
+.operation-row {
|
|
|
+ margin-bottom: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.custom-table {
|
|
|
+ border-radius: 8px;
|
|
|
+ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
|
|
|
+}
|
|
|
+
|
|
|
+.custom-table ::v-deep .el-table__header th {
|
|
|
+ background: #ffffff;
|
|
|
+ color: #4c5161;
|
|
|
+ font-weight: 600;
|
|
|
+ border-color: #e2e8f0;
|
|
|
+ text-align: center;
|
|
|
+}
|
|
|
+
|
|
|
+.custom-table ::v-deep .el-table__row td {
|
|
|
+ text-align: center;
|
|
|
+ vertical-align: middle;
|
|
|
+ border-color: #e2e8f0;
|
|
|
+}
|
|
|
+
|
|
|
+.pagination-container {
|
|
|
+ margin-top: 20px;
|
|
|
+ text-align: right;
|
|
|
+ padding: 16px;
|
|
|
+ background: #ffffff;
|
|
|
+ border-radius: 8px;
|
|
|
+ box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
|
|
|
+}
|
|
|
+
|
|
|
+/* 统计仪表盘 */
|
|
|
+.stats-dashboard {
|
|
|
+ margin-bottom: 24px;
|
|
|
+ padding: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.stat-card {
|
|
|
+ position: relative;
|
|
|
+ border-radius: 12px;
|
|
|
+ padding: 20px;
|
|
|
+ margin-bottom: 20px;
|
|
|
+ transition: all 0.3s ease;
|
|
|
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
|
|
+ background: #ffffff;
|
|
|
+ border: 1px solid #e2e8f0;
|
|
|
+ height: 140px;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+}
|
|
|
+
|
|
|
+.stat-card:hover {
|
|
|
+ transform: translateY(-2px);
|
|
|
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
|
|
+}
|
|
|
+
|
|
|
+.stat-session-card {
|
|
|
+ border-top: 4px solid #667eea;
|
|
|
+}
|
|
|
+
|
|
|
+.stat-token-card {
|
|
|
+ border-top: 4px solid #11998e;
|
|
|
+}
|
|
|
+
|
|
|
+.stat-input-card {
|
|
|
+ border-top: 4px solid #f2994a;
|
|
|
+}
|
|
|
+
|
|
|
+.stat-output-card {
|
|
|
+ border-top: 4px solid #2193b0;
|
|
|
+}
|
|
|
+
|
|
|
+.card-icon-wrapper {
|
|
|
+ width: 40px;
|
|
|
+ height: 40px;
|
|
|
+ border-radius: 10px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ font-size: 18px;
|
|
|
+ margin-bottom: 12px;
|
|
|
+ color: white;
|
|
|
+}
|
|
|
+
|
|
|
+.session-icon {
|
|
|
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
|
+}
|
|
|
+
|
|
|
+.token-icon {
|
|
|
+ background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
|
|
|
+}
|
|
|
+
|
|
|
+.input-icon {
|
|
|
+ background: linear-gradient(135deg, #f2994a 0%, #f2c94c 100%);
|
|
|
+}
|
|
|
+
|
|
|
+.output-icon {
|
|
|
+ background: linear-gradient(135deg, #2193b0 0%, #6dd5ed 100%);
|
|
|
+}
|
|
|
+
|
|
|
+.card-content {
|
|
|
+ flex: 1;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+}
|
|
|
+
|
|
|
+.stat-value {
|
|
|
+ font-size: 28px;
|
|
|
+ font-weight: 700;
|
|
|
+ line-height: 1.2;
|
|
|
+ margin-bottom: 6px;
|
|
|
+}
|
|
|
+
|
|
|
+.stat-label {
|
|
|
+ font-size: 13px;
|
|
|
+ opacity: 0.8;
|
|
|
+ margin-bottom: 0;
|
|
|
+ font-weight: 500;
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+/* 智能筛选卡片 */
|
|
|
+.smart-filter-card {
|
|
|
+ border-radius: 16px;
|
|
|
+ border: none;
|
|
|
+ margin-bottom: 32px;
|
|
|
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.08);
|
|
|
+ background: #ffffff;
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+
|
|
|
+.smart-filter-card ::v-deep .el-card__header {
|
|
|
+ background: #ffffff;
|
|
|
+ border-bottom: 1px solid #e2e8f0;
|
|
|
+ padding: 20px 24px;
|
|
|
+}
|
|
|
+
|
|
|
+.filter-header {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ gap: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.filter-title-section {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 12px;
|
|
|
+ font-size: 18px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #2d3748;
|
|
|
+}
|
|
|
+
|
|
|
+.filter-title-section i {
|
|
|
+ color: #667eea;
|
|
|
+ font-size: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.preset-buttons {
|
|
|
+ display: flex;
|
|
|
+ gap: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.preset-btn {
|
|
|
+ transition: all 0.3s ease;
|
|
|
+ border: 1px solid #e2e8f0;
|
|
|
+}
|
|
|
+
|
|
|
+.preset-btn:hover {
|
|
|
+ transform: translateY(-2px);
|
|
|
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
|
|
+}
|
|
|
+
|
|
|
+.filter-form {
|
|
|
+ padding: 24px;
|
|
|
+}
|
|
|
+
|
|
|
+.filter-row {
|
|
|
+ display: flex;
|
|
|
+ align-items: end;
|
|
|
+ gap: 20px;
|
|
|
+ flex-wrap: wrap;
|
|
|
+}
|
|
|
+
|
|
|
+.filter-row ::v-deep .el-form-item {
|
|
|
+ margin-bottom: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.filter-row ::v-deep .el-form-item__label {
|
|
|
+ font-weight: 500;
|
|
|
+ color: #4a5568;
|
|
|
+}
|
|
|
+
|
|
|
+.date-picker {
|
|
|
+ width: 280px;
|
|
|
+}
|
|
|
+
|
|
|
+.role-select {
|
|
|
+ width: 200px;
|
|
|
+}
|
|
|
+
|
|
|
+.action-buttons {
|
|
|
+ display: flex;
|
|
|
+ gap: 12px;
|
|
|
+ margin-left: auto;
|
|
|
+}
|
|
|
+
|
|
|
+.search-btn {
|
|
|
+ background: #667eea;
|
|
|
+ border: none;
|
|
|
+ transition: all 0.3s ease;
|
|
|
+}
|
|
|
+
|
|
|
+.search-btn:hover {
|
|
|
+ transform: translateY(-2px);
|
|
|
+ box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4);
|
|
|
+}
|
|
|
+
|
|
|
+.reset-btn {
|
|
|
+ border: 1px solid #cbd5e0;
|
|
|
+ color: #4a5568;
|
|
|
+ transition: all 0.3s ease;
|
|
|
+}
|
|
|
+
|
|
|
+.reset-btn:hover {
|
|
|
+ background: #f7fafc;
|
|
|
+ transform: translateY(-1px);
|
|
|
+}
|
|
|
+
|
|
|
+.export-btn {
|
|
|
+ background: #11998e;
|
|
|
+ border: none;
|
|
|
+ transition: all 0.3s ease;
|
|
|
+}
|
|
|
+
|
|
|
+.export-btn:hover {
|
|
|
+ transform: translateY(-2px);
|
|
|
+ box-shadow: 0 6px 20px rgba(17, 153, 142, 0.4);
|
|
|
+}
|
|
|
+
|
|
|
+/* 数据表格卡片 */
|
|
|
+.data-table-card {
|
|
|
+ border-radius: 16px;
|
|
|
+ border: none;
|
|
|
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.08);
|
|
|
+ background: #ffffff;
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+
|
|
|
+.data-table-card ::v-deep .el-card__header {
|
|
|
+ background: #ffffff;
|
|
|
+ padding: 20px 24px;
|
|
|
+ border-bottom: 1px solid #e2e8f0;
|
|
|
+}
|
|
|
+
|
|
|
+.table-header {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ color: #2d3748;
|
|
|
+}
|
|
|
+
|
|
|
+.header-left {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.header-title {
|
|
|
+ font-size: 18px;
|
|
|
+ font-weight: 600;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.header-title i {
|
|
|
+ font-size: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+.header-actions {
|
|
|
+ display: flex;
|
|
|
+ gap: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.refresh-btn {
|
|
|
+ color: #667eea;
|
|
|
+ font-size: 18px;
|
|
|
+ transition: all 0.3s ease;
|
|
|
+}
|
|
|
+
|
|
|
+.refresh-btn:hover {
|
|
|
+ transform: rotate(180deg);
|
|
|
+}
|
|
|
+
|
|
|
+.modern-table {
|
|
|
+ border-radius: 8px;
|
|
|
+ overflow: hidden;
|
|
|
+ border: 1px solid #e2e8f0;
|
|
|
+}
|
|
|
+
|
|
|
+.modern-table ::v-deep .el-table__header th {
|
|
|
+ background: #ffffff;
|
|
|
+ color: #4c5161;
|
|
|
+ font-weight: 600;
|
|
|
+ text-align: center;
|
|
|
+ height: 56px;
|
|
|
+ padding: 8px 4px;
|
|
|
+ border: 1px solid #e2e8f0;
|
|
|
+}
|
|
|
+
|
|
|
+.modern-table ::v-deep .el-table__body td {
|
|
|
+ padding: 16px 4px;
|
|
|
+ text-align: center;
|
|
|
+ vertical-align: middle;
|
|
|
+ border-color: #edf2f7;
|
|
|
+ transition: background-color 0.2s ease;
|
|
|
+ color: #000000;
|
|
|
+}
|
|
|
+
|
|
|
+.modern-table ::v-deep .el-table__row:hover td {
|
|
|
+ background-color: #f8f9fa !important;
|
|
|
+}
|
|
|
+
|
|
|
+.modern-table ::v-deep .el-table__row:nth-child(even) td {
|
|
|
+ background-color: #fdfdfd;
|
|
|
+}
|
|
|
+
|
|
|
+.role-cell {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 12px;
|
|
|
+ justify-content: center;
|
|
|
+}
|
|
|
+
|
|
|
+.role-avatar {
|
|
|
+ position: relative;
|
|
|
+ width: 36px;
|
|
|
+ height: 36px;
|
|
|
+ border-radius: 50%;
|
|
|
+ background: #667eea;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ color: white;
|
|
|
+ font-size: 16px;
|
|
|
+ box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+.role-name {
|
|
|
+ font-weight: 500;
|
|
|
+ color: #2d3748;
|
|
|
+}
|
|
|
+
|
|
|
+.action-cell {
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ gap: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.view-btn {
|
|
|
+ background: #ffffff;
|
|
|
+ border: 1px solid #000000;
|
|
|
+ transition: all 0.3s ease;
|
|
|
+ color: #000000;
|
|
|
+}
|
|
|
+
|
|
|
+.view-btn:hover {
|
|
|
+ transform: translateY(-2px) scale(1.1);
|
|
|
+ box-shadow: 0 6px 20px rgba(0, 0, 0, 0.2);
|
|
|
+ background: #f0f0f0;
|
|
|
+}
|
|
|
+
|
|
|
+.ai-btn {
|
|
|
+ background: #ffffff;
|
|
|
+ border: 1px solid #000000;
|
|
|
+ transition: all 0.3s ease;
|
|
|
+ color: #000000;
|
|
|
+}
|
|
|
+
|
|
|
+.ai-btn:hover {
|
|
|
+ transform: translateY(-2px) scale(1.1);
|
|
|
+ box-shadow: 0 6px 20px rgba(0, 0, 0, 0.2);
|
|
|
+ background: #f0f0f0;
|
|
|
+}
|
|
|
+
|
|
|
+.table-footer {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ padding: 20px 24px;
|
|
|
+ background: #ffffff;
|
|
|
+ border-top: 1px solid #e2e8f0;
|
|
|
+}
|
|
|
+
|
|
|
+.pagination-info {
|
|
|
+ color: #718096;
|
|
|
+ font-size: 14px;
|
|
|
+}
|
|
|
+
|
|
|
+.modern-pagination ::v-deep .el-pagination {
|
|
|
+ display: flex;
|
|
|
+ justify-content: flex-end;
|
|
|
+}
|
|
|
+
|
|
|
+.modern-pagination ::v-deep .el-pager li {
|
|
|
+ margin: 0 4px;
|
|
|
+ border-radius: 8px;
|
|
|
+ transition: all 0.3s ease;
|
|
|
+}
|
|
|
+
|
|
|
+.modern-pagination ::v-deep .el-pager li:hover {
|
|
|
+ color: #667eea;
|
|
|
+ transform: translateY(-2px);
|
|
|
+}
|
|
|
+
|
|
|
+/* AI分析对话框 */
|
|
|
+.ai-analysis-dialog ::v-deep .el-dialog {
|
|
|
+ border-radius: 16px;
|
|
|
+ overflow: hidden;
|
|
|
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.2);
|
|
|
+}
|
|
|
+
|
|
|
+.ai-analysis-dialog ::v-deep .el-dialog__header {
|
|
|
+ background: #ffffff;
|
|
|
+ padding: 24px;
|
|
|
+ color: #2d3748;
|
|
|
+ border-bottom: 1px solid #e2e8f0;
|
|
|
+}
|
|
|
+
|
|
|
+.ai-analysis-dialog ::v-deep .el-dialog__title {
|
|
|
+ color: #2d3748;
|
|
|
+ font-weight: 600;
|
|
|
+ font-size: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.ai-analysis-content {
|
|
|
+ padding: 24px;
|
|
|
+}
|
|
|
+
|
|
|
+.analysis-header {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 20px;
|
|
|
+ margin-bottom: 32px;
|
|
|
+ padding: 20px;
|
|
|
+ background: #ffffff;
|
|
|
+ border-radius: 12px;
|
|
|
+ border: 1px solid #f0f0f0;
|
|
|
+}
|
|
|
+
|
|
|
+.ai-avatar {
|
|
|
+ position: relative;
|
|
|
+ width: 60px;
|
|
|
+ height: 60px;
|
|
|
+ border-radius: 50%;
|
|
|
+ background: #667eea;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ color: white;
|
|
|
+ font-size: 24px;
|
|
|
+}
|
|
|
+
|
|
|
+.ai-pulse {
|
|
|
+ position: absolute;
|
|
|
+ top: -4px;
|
|
|
+ left: -4px;
|
|
|
+ right: -4px;
|
|
|
+ bottom: -4px;
|
|
|
+ border-radius: 50%;
|
|
|
+ border: 2px solid rgba(102, 126, 234, 0.4);
|
|
|
+ animation: aiPulse 2s infinite;
|
|
|
+}
|
|
|
+
|
|
|
+@keyframes aiPulse {
|
|
|
+ 0% { transform: scale(0.8); opacity: 1; }
|
|
|
+ 100% { transform: scale(1.5); opacity: 0; }
|
|
|
+}
|
|
|
+
|
|
|
+.analysis-info h3 {
|
|
|
+ margin: 0 0 8px 0;
|
|
|
+ color: #2d3748;
|
|
|
+ font-size: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.analysis-info p {
|
|
|
+ margin: 0;
|
|
|
+ color: #718096;
|
|
|
+ font-size: 14px;
|
|
|
+}
|
|
|
+
|
|
|
+.analysis-grid {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: 1fr 1fr;
|
|
|
+ gap: 24px;
|
|
|
+ margin-bottom: 32px;
|
|
|
+}
|
|
|
+
|
|
|
+.analysis-card {
|
|
|
+ background: #ffffff;
|
|
|
+ border-radius: 12px;
|
|
|
+ padding: 20px;
|
|
|
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
|
|
|
+ border: 1px solid #e2e8f0;
|
|
|
+ transition: all 0.3s ease;
|
|
|
+}
|
|
|
+
|
|
|
+.analysis-card:hover {
|
|
|
+ transform: translateY(-4px);
|
|
|
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
|
|
+}
|
|
|
+
|
|
|
+.card-header {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 12px;
|
|
|
+ margin-bottom: 16px;
|
|
|
+ color: #2d3748;
|
|
|
+ font-weight: 600;
|
|
|
+ font-size: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.card-header i {
|
|
|
+ color: #667eea;
|
|
|
+ font-size: 18px;
|
|
|
+}
|
|
|
+
|
|
|
+.metric, .prediction {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ padding: 12px 0;
|
|
|
+ border-bottom: 1px solid #edf2f7;
|
|
|
+}
|
|
|
+
|
|
|
+.metric:last-child, .prediction:last-child {
|
|
|
+ border-bottom: none;
|
|
|
+}
|
|
|
+
|
|
|
+.metric-label, .prediction-label {
|
|
|
+ color: #718096;
|
|
|
+ font-size: 14px;
|
|
|
+}
|
|
|
+
|
|
|
+.metric-value, .prediction-value {
|
|
|
+ font-weight: 700;
|
|
|
+ font-size: 16px;
|
|
|
+ /* 移除了固定颜色设置,使用默认文本颜色 */
|
|
|
+}
|
|
|
+
|
|
|
+/* 已移除 .metric-value.success 的颜色样式 */
|
|
|
+
|
|
|
+.prediction-trend {
|
|
|
+ padding: 4px 12px;
|
|
|
+ border-radius: 16px;
|
|
|
+ font-size: 12px;
|
|
|
+ font-weight: 600;
|
|
|
+}
|
|
|
+
|
|
|
+.prediction-trend.up {
|
|
|
+ background: rgba(56, 239, 125, 0.2);
|
|
|
+ color: #11998e;
|
|
|
+}
|
|
|
+
|
|
|
+.recommendations {
|
|
|
+ background: #ffffff;
|
|
|
+ border-radius: 12px;
|
|
|
+ padding: 24px;
|
|
|
+ border: 1px solid #f0f0f0;
|
|
|
+}
|
|
|
+
|
|
|
+.section-title {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 12px;
|
|
|
+ margin-bottom: 20px;
|
|
|
+ color: #2d3748;
|
|
|
+ font-weight: 600;
|
|
|
+ font-size: 18px;
|
|
|
+}
|
|
|
+
|
|
|
+.section-title i {
|
|
|
+ color: #f6e05e;
|
|
|
+ font-size: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.recommendation-list {
|
|
|
+ list-style: none;
|
|
|
+ padding: 0;
|
|
|
+ margin: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.recommendation-item {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 12px;
|
|
|
+ padding: 12px 0;
|
|
|
+ border-bottom: 1px solid #edf2f7;
|
|
|
+ color: #4a5568;
|
|
|
+}
|
|
|
+
|
|
|
+.recommendation-item:last-child {
|
|
|
+ border-bottom: none;
|
|
|
+}
|
|
|
+
|
|
|
+.recommendation-item i {
|
|
|
+ color: #38ef7d;
|
|
|
+ font-size: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.dialog-footer {
|
|
|
+ display: flex;
|
|
|
+ justify-content: flex-end;
|
|
|
+ gap: 16px;
|
|
|
+ padding: 20px 24px;
|
|
|
+ background: #ffffff;
|
|
|
+ border-top: 1px solid #e2e8f0;
|
|
|
+}
|
|
|
+
|
|
|
+.close-btn {
|
|
|
+ background: #e2e8f0;
|
|
|
+ border: 1px solid #cbd5e0;
|
|
|
+ color: #4a5568;
|
|
|
+ padding: 10px 24px;
|
|
|
+ transition: all 0.3s ease;
|
|
|
+}
|
|
|
+
|
|
|
+.close-btn:hover {
|
|
|
+ background: #cbd5e0;
|
|
|
+ transform: translateY(-2px);
|
|
|
+}
|
|
|
+
|
|
|
+.export-btn {
|
|
|
+ background: #667eea;
|
|
|
+ border: none;
|
|
|
+ color: white;
|
|
|
+ padding: 10px 24px;
|
|
|
+ transition: all 0.3s ease;
|
|
|
+}
|
|
|
+
|
|
|
+.export-btn:hover {
|
|
|
+ transform: translateY(-2px);
|
|
|
+ box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4);
|
|
|
+}
|
|
|
+
|
|
|
+/* 响应式设计 */
|
|
|
+@media (max-width: 1200px) {
|
|
|
+ .page-header {
|
|
|
+ flex-direction: column;
|
|
|
+ text-align: center;
|
|
|
+ gap: 20px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .analysis-grid {
|
|
|
+ grid-template-columns: 1fr;
|
|
|
+ }
|
|
|
+
|
|
|
+ .filter-row {
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: stretch;
|
|
|
+ }
|
|
|
+
|
|
|
+ .action-buttons {
|
|
|
+ margin-left: 0;
|
|
|
+ justify-content: center;
|
|
|
+ }
|
|
|
+
|
|
|
+ .ai-statistics-container {
|
|
|
+ background: #ffffff;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+@media (max-width: 768px) {
|
|
|
+ .ai-statistics-container {
|
|
|
+ padding: 16px;
|
|
|
+ background: #ffffff;
|
|
|
+ }
|
|
|
+
|
|
|
+ .page-header {
|
|
|
+ padding: 20px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .page-title {
|
|
|
+ font-size: 24px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .stat-card {
|
|
|
+ padding: 20px;
|
|
|
+ height: 140px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .stat-value {
|
|
|
+ font-size: 28px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .filter-form {
|
|
|
+ padding: 20px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .date-picker, .role-select {
|
|
|
+ width: 100%;
|
|
|
+ }
|
|
|
+
|
|
|
+ .table-footer {
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 16px;
|
|
|
+ align-items: stretch;
|
|
|
+ }
|
|
|
+
|
|
|
+ .pagination-info {
|
|
|
+ text-align: center;
|
|
|
+ }
|
|
|
+
|
|
|
+ .ai-powered-badge {
|
|
|
+ position: static;
|
|
|
+ margin-bottom: 16px;
|
|
|
+ justify-content: center;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+@media (max-width: 480px) {
|
|
|
+ .stat-card {
|
|
|
+ padding: 16px;
|
|
|
+ height: 120px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .stat-value {
|
|
|
+ font-size: 24px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .stat-label {
|
|
|
+ font-size: 13px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .card-icon-wrapper {
|
|
|
+ width: 48px;
|
|
|
+ height: 48px;
|
|
|
+ font-size: 20px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .page-title {
|
|
|
+ font-size: 20px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .analysis-header {
|
|
|
+ flex-direction: column;
|
|
|
+ text-align: center;
|
|
|
+ gap: 16px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .dialog-footer {
|
|
|
+ flex-direction: column;
|
|
|
+ }
|
|
|
+
|
|
|
+ .export-btn, .close-btn {
|
|
|
+ width: 100%;
|
|
|
+ }
|
|
|
+
|
|
|
+ .ai-statistics-container {
|
|
|
+ background: #ffffff;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/* 性能优化 */
|
|
|
+.stat-card, .analysis-card, .smart-filter-card, .data-table-card {
|
|
|
+ will-change: transform, box-shadow;
|
|
|
+ backface-visibility: hidden;
|
|
|
+ transform: translateZ(0);
|
|
|
+}
|
|
|
+
|
|
|
+/* 滚动条美化 */
|
|
|
+::-webkit-scrollbar {
|
|
|
+ width: 6px;
|
|
|
+ height: 6px;
|
|
|
+}
|
|
|
+
|
|
|
+::-webkit-scrollbar-track {
|
|
|
+ background: #f8f9fa;
|
|
|
+ border-radius: 3px;
|
|
|
+}
|
|
|
+
|
|
|
+::-webkit-scrollbar-thumb {
|
|
|
+ background: #667eea;
|
|
|
+ border-radius: 3px;
|
|
|
+}
|
|
|
+
|
|
|
+::-webkit-scrollbar-thumb:hover {
|
|
|
+ background: #5a6fd8;
|
|
|
+}
|
|
|
+</style>
|