Files
DJKB/my-vue-app/src/views/secondTop/components/GoodMusic.vue
lbw_9527443 0e0d297da7 feat(GoodMusic): 添加外部录音数据支持并优化显示
- 新增recordData prop接收外部录音数据
- 实现processedRecordings计算属性处理外部数据
- 添加销售员姓名和评分显示
- 优化转录文本和录音分析功能
2025-08-22 20:56:04 +08:00

1206 lines
29 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<!-- 录音管理区域 -->
<div class="charts-section">
<div class="chart-container">
<div class="chart-header">
<h3>优秀录音</h3>
<button class="upload-icon-btn" @click="triggerFileUpload" title="上传录音">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14 2H6C4.9 2 4 2.9 4 4V20C4 21.1 4.89 22 5.99 22H18C19.1 22 20 21.1 20 20V8L14 2Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<polyline points="14,2 14,8 20,8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<line x1="12" y1="18" x2="12" y2="12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<polyline points="9,15 12,12 15,15" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
</div>
<div class="chart-content">
<div class="recording-section">
<!-- 录音文件列表 -->
<div v-if="!showTranscriptView">
<div class="recording-list" v-if="processedRecordings.length > 0">
<div
v-for="(recording, index) in processedRecordings"
:key="index"
class="recording-item"
:class="{ active: selectedRecording === index }"
@click="selectRecording(index)"
>
<div class="recording-info">
<div class="recording-name" :title="recording.name">{{ recording.name.length > 10 ? recording.name.substring(0, 10) + '...' : recording.name }}</div>
<div class="recording-meta">
<span v-if="recording.sale_name" class="sale-name">{{ recording.sale_name }}</span>
<span v-if="recording.score" class="score">评分: {{ recording.score }}</span>
<span class="file-size">{{ formatFileSize(recording.size) }}</span>
<span class="upload-time">{{ recording.uploadTime || recording.date }}</span>
</div>
</div>
<div class="recording-actions">
<button
class="action-btn download-btn"
@click.stop="downloadRecording(index)"
:disabled="!recording.url"
>
下载录音
</button>
<button
class="action-btn convert-btn"
@click.stop="convertToText(index)"
:disabled="recording.isConverting"
>
<span v-if="!recording.isConverting">转文字</span>
<span v-else class="converting-text">
<span class="dot-animation">转换中</span>
</span>
</button>
</div>
</div>
</div>
<!-- 空状态 -->
<div class="empty-state" v-if="recordings.length === 0">
<i class="el-icon-microphone"></i>
<p>暂无录音文件请上传录音文件</p>
</div>
</div>
<!-- 转换结果 -->
<div class="conversion-result full-page" v-if="showTranscriptView">
<div class="result-header">
<button class="back-btn" @click="backToRecordings">
<i class="el-icon-arrow-left"></i>
返回录音列表
</button>
<h4>{{ isConverting ? '正在转换...' : (currentViewType === 'transcript' ? '转换文本' : '录音分析') }}</h4>
<div class="header-actions">
<!-- 视图切换按钮 -->
<div class="view-toggle" v-if="currentTranscript && !isConverting">
<button
class="toggle-btn"
:class="{ active: currentViewType === 'transcript' }"
@click="switchViewType('transcript')"
>
查看原文
</button>
<button
class="toggle-btn"
:class="{ active: currentViewType === 'analysis' }"
@click="switchViewType('analysis')"
>
录音分析
</button>
</div>
<button class="expand-btn" @click="showExpandDialog" v-if="(currentTranscript && currentViewType === 'transcript') || (analysisResult && currentViewType === 'analysis')">
<i class="el-icon-full-screen"></i>
展开查看
</button>
<button class="copy-btn" @click="copyText" v-if="currentTranscript && currentViewType === 'transcript'">
<i class="el-icon-document-copy"></i>
复制
</button>
<button class="copy-btn" @click="copyAnalysisText" v-if="analysisResult && currentViewType === 'analysis'">
<i class="el-icon-document-copy"></i>
复制
</button>
</div>
</div>
<!-- 原文显示 -->
<div class="transcript-text" v-if="currentTranscript && currentViewType === 'transcript'">
{{ currentTranscript }}
</div>
<!-- 分析结果显示 -->
<div class="analysis-result" v-if="currentViewType === 'analysis'">
<div v-if="isAnalyzing" class="analysis-loading">
<div class="loading-animation">
<span></span>
<span></span>
<span></span>
</div>
<p>正在进行录音分析请稍候...</p>
</div>
<div v-else-if="analysisResult" class="analysis-content" v-html="formattedAnalysisResult"></div>
<div v-else class="no-analysis">
<p>暂无分析结果</p>
</div>
</div>
<!-- 转换加载状态 -->
<div class="transcript-loading" v-if="isConverting">
<div class="loading-animation">
<span></span>
<span></span>
<span></span>
</div>
<p>正在将录音转换为文字请稍候...</p>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 展开查看弹框 -->
<div class="dialog-overlay" v-if="showDialog" @click="closeDialog">
<div class="dialog-content" @click.stop>
<div class="dialog-header">
<h3>{{ currentViewType === 'transcript' ? '转换文本内容' : '录音分析结果' }}</h3>
<button class="close-btn" @click="closeDialog">
<i class="el-icon-close"></i>
</button>
</div>
<div class="dialog-body">
<!-- 转换文本内容 -->
<div class="expanded-transcript" v-if="currentViewType === 'transcript'">
{{ currentTranscript }}
</div>
<!-- 分析结果内容 -->
<div class="expanded-analysis" v-if="currentViewType === 'analysis'">
<div v-if="analysisResult" class="analysis-content" v-html="formattedAnalysisResult"></div>
<div v-else class="no-analysis">
<p>暂无分析结果</p>
</div>
</div>
</div>
<div class="dialog-footer">
<button class="dialog-copy-btn" @click="currentViewType === 'transcript' ? copyText() : copyAnalysisText()">
<i class="el-icon-document-copy"></i>
复制全部内容
</button>
<button class="dialog-close-btn" @click="closeDialog">
关闭
</button>
</div>
</div>
</div>
</template>
<script>
import { SimpleChatService } from '@/utils/ChatService.js'
import MarkdownIt from 'markdown-it'
import axios from 'axios'
export default {
name: 'GoodMusic',
props: {
recordData: {
type: Object,
default: () => ({})
}
},
data() {
return {
recordings: [ ],
selectedRecording: null,
currentAudio: null,
showTranscriptView: false,
isConverting: false,
currentTranscript: null,
showDialog: false,
// 录音分析相关
showAnalysisView: false,
isAnalyzing: false,
analysisResult: '',
currentViewType: 'transcript', // 'transcript' 或 'analysis'
// Dify API配置
DIFY_API_KEY_02: 'app-h4uBo5kOGoiYhjuBF1AHZi8b', // 通话录音分析
chatService_02: null,
md: null
}
},
created() {
// 初始化服务
this.chatService_02 = new SimpleChatService(this.DIFY_API_KEY_02)
this.md = new MarkdownIt({
html: true,
linkify: true,
typographer: true
})
},
computed: {
// 格式化分析结果
formattedAnalysisResult() {
if (!this.analysisResult) return ''
return this.md.render(this.analysisResult)
},
// 处理外部传入的录音数据
processedRecordings() {
let externalRecordings = []
// 处理外部传入的数据
if (this.recordData && this.recordData.excellent_record_list) {
const recordList = this.recordData.excellent_record_list
// 遍历每个销售人员的录音
Object.keys(recordList).forEach(saleName => {
recordList[saleName].forEach((record, index) => {
externalRecordings.push({
id: `${saleName}_${index}`,
name: record.obj_file_name ? record.obj_file_name.split('/').pop() : `${record.sale_name}的录音`,
size: 2048576, // 默认大小
duration: '未知',
date: new Date().toLocaleDateString(),
url: record.obj_file_name,
transcription: record.context || null,
score: record.score,
sale_name: record.sale_name,
sop: record.sop,
isPlaying: false,
isConverting: false
})
})
})
}
// 合并外部数据和本地数据
return [...externalRecordings, ...this.recordings]
}
},
beforeUnmount() {
if (this.currentAudio) {
this.currentAudio.pause()
this.currentAudio = null
}
},
methods: {
// 录音文件选择
handleFileSelect(event) {
const file = event.target.files[0]
if (file) {
const recording = {
name: file.name,
size: file.size,
uploadTime: new Date().toLocaleString(),
url: URL.createObjectURL(file),
isPlaying: false,
isConverting: false,
transcript: null
}
// 如果没有外部数据则添加到本地recordings数组
if (!this.recordData || !this.recordData.excellent_record_list) {
this.recordings.push(recording)
} else {
// 如果有外部数据,可以触发事件通知父组件或其他处理逻辑
this.$emit('file-uploaded', recording)
}
// 清空input以便重复选择同一文件
event.target.value = ''
}
},
// 选择录音
selectRecording(index) {
this.selectedRecording = index
},
// 播放/暂停录音
togglePlay(index) {
const recording = this.processedRecordings[index]
// 停止当前播放的音频
if (this.currentAudio) {
this.currentAudio.pause()
this.processedRecordings.forEach(r => r.isPlaying = false)
}
if (!recording.isPlaying) {
this.currentAudio = new Audio(recording.url)
this.currentAudio.play()
recording.isPlaying = true
this.currentAudio.onended = () => {
recording.isPlaying = false
this.currentAudio = null
}
}
},
// 开始通话录音分析
async startRecordingAnalysis(recording) {
this.isAnalyzing = true
this.analysisResult = ''
// 构建通话录音分析查询
const recordingQuery = `请对录音文件 ${recording.name} 进行通话录音分析,包括:
1. 通话质量评估
2. 客户情绪分析
3. 沟通效果评价
4. 关键信息提取
5. 改进建议
录音信息:
文件名:${recording.name}
文件大小:${this.formatFileSize(recording.size)}
转换文本:${recording.transcription}`
try {
await this.chatService_02.sendMessage(
recordingQuery,
(update) => {
// 实时更新通话录音分析结果
this.analysisResult = update.content
},
() => {
// 流结束回调
console.log('通话录音分析完成')
this.isAnalyzing = false
}
)
} catch (error) {
console.error('通话录音分析失败:', error)
this.analysisResult = '通话录音分析失败,请重试。'
this.isAnalyzing = false
}
},
// 切换视图类型
switchViewType(type) {
this.currentViewType = type
},
// 返回录音列表
backToRecordings() {
this.showTranscriptView = false
this.currentTranscript = null
this.analysisResult = ''
this.currentViewType = 'transcript'
this.isAnalyzing = false
},
// 复制文本
copyText() {
if (this.currentTranscript) {
navigator.clipboard.writeText(this.currentTranscript)
alert('文本已复制到剪贴板')
}
},
// 复制分析结果
copyAnalysisText() {
if (this.analysisResult) {
navigator.clipboard.writeText(this.analysisResult)
alert('分析结果已复制到剪贴板')
}
},
// 显示展开弹框
showExpandDialog() {
this.showDialog = true
},
// 关闭弹框
closeDialog() {
this.showDialog = false
},
// 格式化文件大小
formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes'
const k = 1024
const sizes = ['Bytes', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
},
triggerFileUpload() {
const fileInput = document.createElement('input')
fileInput.type = 'file'
fileInput.accept = 'audio/*'
fileInput.style.display = 'none'
fileInput.addEventListener('change', this.handleFileSelect)
document.body.appendChild(fileInput)
fileInput.click()
document.body.removeChild(fileInput)
},
// 转换为文字
convertToText(index) {
const recording = this.processedRecordings[index]
if (recording.isConverting) return
// 如果有外部传入的transcription直接显示
if (recording.transcription) {
this.currentTranscript = recording.transcription
this.showTranscriptView = true
this.currentViewType = 'transcript'
// 开始录音分析
this.switchViewType('analysis')
this.startRecordingAnalysis(recording)
return
}
recording.isConverting = true
this.isConverting = true
this.showTranscriptView = true
this.currentViewType = 'transcript'
// 模拟转换过程(用于本地上传的文件)
setTimeout(() => {
const transcriptText = `这是录音 "${recording.name}" 的转录文本内容。这里会显示实际的语音转文字结果。`
recording.transcription = transcriptText
this.currentTranscript = transcriptText
recording.isConverting = false
this.isConverting = false
// 开始录音分析
this.startRecordingAnalysis(recording)
}, 2000)
},
downloadRecording(index) {
const recording = this.processedRecordings[index]
// 使用obj_file_name字段作为下载链接
const downloadUrl = recording.url
if (!downloadUrl) {
alert('录音文件不可用')
return
}
const link = document.createElement('a')
link.href = downloadUrl
link.download = recording.name
link.style.display = 'none'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
}
}
}
</script>
<style scoped>
.dashboard-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.dashboard-header h2 {
margin: 0;
color: #303133;
font-size: 24px;
font-weight: 600;
}
.charts-section {
display: grid;
grid-template-columns: 1fr;
gap: 20px;
}
.chart-container {
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
min-height: 420px;
}
.chart-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 20px 0;
border-bottom: 1px solid #ebeef5;
}
.chart-header h3 {
margin: 0 0 16px 0;
color: #303133;
font-size: 18px;
font-weight: 600;
}
.upload-icon-btn {
background: #409eff;
border: none;
border-radius: 6px;
padding: 8px;
cursor: pointer;
color: white;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
}
.upload-icon-btn:hover {
background: #337ecc;
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(64, 158, 255, 0.3);
}
.upload-icon-btn:active {
transform: translateY(0);
}
.chart-content {
padding: 20px;
}
.recording-section {
width: 100%;
min-height: 300px;
max-height: 500px;
overflow-y: auto;
}
.recording-list {
margin-bottom: 20px;
max-height: 400px;
overflow-y: auto;
}
.recording-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
border: 1px solid #ebeef5;
border-radius: 6px;
margin-bottom: 8px;
cursor: pointer;
transition: all 0.2s ease;
}
.recording-item:hover {
border-color: #c6e2ff;
background: #f5f7fa;
}
.recording-item.active {
border-color: #409eff;
background: #ecf5ff;
}
.recording-info {
flex: 1;
}
.recording-name {
font-weight: 500;
color: #303133;
margin-bottom: 4px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
display: inline-block;
}
.recording-meta {
display: flex;
gap: 12px;
font-size: 12px;
color: #909399;
}
.sale-name {
color: #409eff;
font-weight: 500;
}
.score {
color: #67c23a;
font-weight: 500;
}
.recording-actions {
display: flex;
gap: 12px;
align-items: center;
}
.action-btn {
padding: 8px 16px;
height: 36px;
border: none;
border-radius: 6px;
background: #f8f9fa;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
font-size: 13px;
font-weight: 500;
color: #495057;
min-width: 80px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.action-btn:hover:not(:disabled) {
transform: translateY(-1px);
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
}
.action-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.download-btn {
background: linear-gradient(135deg, #28a745, #20c997);
color: white;
}
.download-btn:hover:not(:disabled) {
background: linear-gradient(135deg, #218838, #1ea085);
transform: translateY(-1px);
box-shadow: 0 3px 8px rgba(40, 167, 69, 0.3);
}
.convert-btn {
background: linear-gradient(135deg, #007bff, #6610f2);
color: white;
}
.convert-btn:hover:not(:disabled) {
background: linear-gradient(135deg, #0056b3, #520dc2);
transform: translateY(-1px);
box-shadow: 0 3px 8px rgba(0, 123, 255, 0.3);
}
.converting-text {
display: inline-flex;
align-items: center;
justify-content: center;
}
.dot-animation {
position: relative;
}
.dot-animation::after {
content: "...";
position: absolute;
overflow: hidden;
display: inline-block;
vertical-align: bottom;
animation: dots 2s infinite;
width: 0;
margin-left: 2px;
}
@keyframes dots {
0% { width: 0; }
33% { width: 0.5em; }
66% { width: 1em; }
100% { width: 1.5em; }
}
.conversion-result {
border: 1px solid #ebeef5;
border-radius: 6px;
overflow: hidden;
transition: all 0.5s ease;
}
.conversion-result.full-page {
height: 100%;
display: flex;
flex-direction: column;
animation: fadeIn 0.3s ease;
}
.show-result {
animation: pulse 1s ease;
}
@keyframes pulse {
0% { transform: scale(1); box-shadow: 0 0 0 rgba(0, 123, 255, 0); }
50% { transform: scale(1.03); box-shadow: 0 0 10px rgba(0, 123, 255, 0.3); }
100% { transform: scale(1); box-shadow: 0 0 0 rgba(0, 123, 255, 0); }
}
.result-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background: #f5f7fa;
border-bottom: 1px solid #ebeef5;
}
.header-actions {
display: flex;
gap: 8px;
align-items: center;
}
.view-toggle {
display: flex;
background: #f5f7fa;
border-radius: 6px;
padding: 2px;
margin-right: 12px;
}
.toggle-btn {
padding: 6px 12px;
border: none;
background: transparent;
color: #606266;
cursor: pointer;
border-radius: 4px;
font-size: 12px;
transition: all 0.2s ease;
white-space: nowrap;
}
.toggle-btn.active {
background: #409eff;
color: white;
box-shadow: 0 1px 3px rgba(64, 158, 255, 0.3);
}
.toggle-btn:not(.active):hover {
background: #ecf5ff;
color: #409eff;
}
.back-btn {
display: flex;
align-items: center;
gap: 4px;
padding: 6px 12px;
border: 1px solid #dcdfe6;
border-radius: 4px;
background: white;
cursor: pointer;
font-size: 13px;
color: #606266;
transition: all 0.2s ease;
}
.back-btn:hover {
border-color: #409eff;
color: #409eff;
}
.result-header h4 {
margin: 0;
font-size: 14px;
color: #303133;
}
.copy-btn, .expand-btn {
display: flex;
align-items: center;
gap: 4px;
padding: 4px 8px;
border: 1px solid #dcdfe6;
border-radius: 4px;
background: white;
cursor: pointer;
font-size: 12px;
color: #606266;
transition: all 0.2s ease;
}
.copy-btn:hover, .expand-btn:hover {
border-color: #409eff;
color: #409eff;
}
.expand-btn {
background: #f0f9ff;
border-color: #409eff;
color: #409eff;
}
.expand-btn:hover {
background: #409eff;
color: white;
}
.transcript-text {
padding: 8px;
line-height: 1.6;
color: #303133;
background: white;
flex: 1;
max-height: 200px;
overflow-y: auto;
animation: fadeIn 0.5s ease;
}
.analysis-result {
/* padding: 20px; */
/* margin: 0 20px 20px; */
max-height: 270px;
overflow-y: auto;
}
.analysis-content {
line-height: 1.8;
color: #303133;
font-size: 14px;
background: #fafafa;
border-radius: 4px;
padding: 8px;
max-height: 200px;
}
.analysis-content h1,
.analysis-content h2,
.analysis-content h3,
.analysis-content h4,
.analysis-content h5,
.analysis-content h6 {
color: #409eff;
margin-top: 20px;
margin-bottom: 10px;
}
.analysis-content p {
margin-bottom: 12px;
}
.analysis-content ul,
.analysis-content ol {
margin-bottom: 12px;
padding-left: 20px;
}
.analysis-content li {
margin-bottom: 6px;
}
.analysis-loading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px;
color: #909399;
text-align: center;
}
.analysis-loading p {
margin: 16px 0 0 0;
font-size: 14px;
}
.no-analysis {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px;
color: #909399;
text-align: center;
}
.no-analysis p {
margin: 0;
font-size: 14px;
}
.transcript-loading {
padding: 30px 16px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: white;
}
.transcript-loading p {
margin-top: 16px;
color: #606266;
font-size: 14px;
}
.loading-animation {
display: flex;
justify-content: center;
align-items: center;
gap: 6px;
}
.loading-animation span {
display: inline-block;
width: 10px;
height: 10px;
background-color: #409eff;
border-radius: 50%;
animation: bounce 1.4s infinite ease-in-out both;
}
.loading-animation span:nth-child(1) {
animation-delay: -0.32s;
}
.loading-animation span:nth-child(2) {
animation-delay: -0.16s;
}
@keyframes bounce {
0%, 80%, 100% {
transform: scale(0);
} 40% {
transform: scale(1.0);
}
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px;
color: #909399;
text-align: center;
}
.empty-state i {
font-size: 48px;
margin-bottom: 16px;
opacity: 0.5;
}
.empty-state p {
margin: 0;
font-size: 14px;
}
/* 响应式设计 */
/* 弹框样式 */
.dialog-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
animation: fadeIn 0.3s ease;
}
.dialog-content {
background: white;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
width: 90%;
max-width: 800px;
max-height: 80vh;
display: flex;
flex-direction: column;
animation: slideIn 0.3s ease;
}
@keyframes slideIn {
from {
transform: translateY(-50px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.dialog-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
border-bottom: 1px solid #ebeef5;
background: #f8f9fa;
border-radius: 8px 8px 0 0;
}
.dialog-header h3 {
margin: 0;
color: #303133;
font-size: 18px;
font-weight: 600;
}
.close-btn {
background: none;
border: none;
font-size: 18px;
color: #909399;
cursor: pointer;
padding: 4px;
border-radius: 4px;
transition: all 0.2s ease;
}
.close-btn:hover {
background: #f5f7fa;
color: #606266;
}
.dialog-body {
flex: 1;
padding: 20px;
overflow-y: auto;
max-height: 60vh;
}
.expanded-transcript {
line-height: 1.8;
color: #303133;
font-size: 14px;
white-space: pre-wrap;
word-wrap: break-word;
}
.expanded-analysis {
line-height: 1.8;
color: #303133;
font-size: 14px;
}
.expanded-analysis .analysis-content {
background: #fafafa;
border-radius: 4px;
padding: 20px;
margin: 0;
}
.expanded-analysis .analysis-content h1,
.expanded-analysis .analysis-content h2,
.expanded-analysis .analysis-content h3,
.expanded-analysis .analysis-content h4,
.expanded-analysis .analysis-content h5,
.expanded-analysis .analysis-content h6 {
color: #409eff;
margin-top: 20px;
margin-bottom: 10px;
}
.expanded-analysis .analysis-content p {
margin-bottom: 12px;
}
.expanded-analysis .analysis-content ul,
.expanded-analysis .analysis-content ol {
margin-bottom: 12px;
padding-left: 20px;
}
.expanded-analysis .analysis-content li {
margin-bottom: 6px;
}
.expanded-analysis .no-analysis {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px;
color: #909399;
text-align: center;
}
.expanded-analysis .no-analysis p {
margin: 0;
font-size: 14px;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
padding: 20px;
border-top: 1px solid #ebeef5;
background: #f8f9fa;
border-radius: 0 0 8px 8px;
}
.dialog-copy-btn, .dialog-close-btn {
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: all 0.2s ease;
display: flex;
align-items: center;
gap: 4px;
}
.dialog-copy-btn {
background: #409eff;
color: white;
}
.dialog-copy-btn:hover {
background: #337ecc;
}
.dialog-close-btn {
background: #f5f7fa;
color: #606266;
border: 1px solid #dcdfe6;
}
.dialog-close-btn:hover {
background: #ecf5ff;
border-color: #409eff;
color: #409eff;
}
@media (max-width: 768px) {
.group-rank {
padding: 15px 10px;
}
.dashboard-header {
padding: 15px;
}
.dashboard-header h2 {
font-size: 20px;
}
.chart-content {
padding: 15px;
}
.recording-item {
flex-direction: column;
align-items: flex-start;
gap: 12px;
}
.recording-actions {
width: 100%;
justify-content: flex-end;
}
.dialog-content {
width: 95%;
max-height: 90vh;
}
.dialog-header, .dialog-body, .dialog-footer {
padding: 15px;
}
.dialog-footer {
flex-direction: column;
}
.dialog-copy-btn, .dialog-close-btn {
width: 100%;
justify-content: center;
}
}
</style>