完善客户画像,品牌分析

This commit is contained in:
ZLI263
2025-09-21 21:25:28 +08:00
parent 3c9b01f639
commit 0eef93e0d4
7 changed files with 756 additions and 452 deletions

View File

@@ -2,4 +2,4 @@
ENV = 'development'
# base api
VUE_APP_BASE_API = '/dev-api'
VUE_APP_BASE_API = ''

View File

@@ -2,5 +2,5 @@
ENV = 'production'
# base api
VUE_APP_BASE_API = '/prod-api'
VUE_APP_BASE_API = ''

View File

@@ -4,13 +4,13 @@
"description": "A vue admin template with Element UI & axios & iconfont & permission control & lint",
"author": "Pan <panfree23@gmail.com>",
"scripts": {
"dev": "set CHOKIDAR_USEPOLLING=true && set CHOKIDAR_INTERVAL=1000 && vue-cli-service serve",
"dev:no-watch": "vue-cli-service serve --watch false",
"dev": "vue-cli-service serve",
"dev-no-open": "vue-cli-service serve --open false",
"build:prod": "vue-cli-service build",
"build:stage": "vue-cli-service build --mode staging",
"preview": "node build/index.js --preview",
"svgo": "svgo -f src/icons/svg --config=src/icons/svgo.yml",
"lint": "eslint --ext .js,.vue src",
"lint": "vue-cli-service lint",
"test:unit": "jest --clearCache && vue-cli-service test:unit",
"test:ci": "npm run lint && npm run test:unit"
},

View File

@@ -120,12 +120,30 @@ export const constantRoutes = [
{
path: '/dealership',
component: Layout,
redirect: '/dealership/index',
name: 'Dealership',
meta: {
title: '门店管理',
icon: 'el-icon-s-shop'
},
children: [
{
path: 'index',
name: 'Dealership',
name: 'DealershipIndex',
component: () => import('@/views/dealership/index'),
meta: { title: '门店管理', icon: 'el-icon-s-shop' }
},
{
path: 'brand-analysis',
name: 'BrandAnalysis',
component: () => import('@/views/dealership/brand-analysis'),
meta: { title: '品牌分析', icon: 'el-icon-data-analysis' }
},
{
path: 'customer-profile',
name: 'CustomerProfile',
component: () => import('@/views/dealership/customer-profile'),
meta: { title: '客户画像', icon: 'el-icon-user-solid' }
}
]
},

View File

@@ -194,7 +194,7 @@
<div v-if="scope.row.audioFileUrl">
<div style="display: flex; align-items: center;">
<el-tag type="success" size="mini" style="margin-right: 8px;">
<i class="el-icon-video-play"></i>
<i class="el-icon-video-play" />
有音频
</el-tag>
<el-link
@@ -203,15 +203,15 @@
target="_blank"
:underline="false"
>
<i class="el-icon-document"></i>
<i class="el-icon-document" />
{{ scope.row.audioFileOriginalName || getAudioFileName(scope.row.audioFileUrl) }}
</el-link>
<el-button
type="text"
size="mini"
icon="el-icon-download"
@click="downloadAudioFile(scope.row)"
style="margin-left: 10px;"
@click="downloadAudioFile(scope.row)"
>
下载
</el-button>
@@ -219,12 +219,32 @@
</div>
<div v-else>
<el-tag type="info" size="mini">
<i class="el-icon-warning"></i>
<i class="el-icon-warning" />
暂无音频文件
</el-tag>
</div>
</template>
</el-table-column>
<el-table-column label="AI分析链接" prop="aiAnalysisUrl" min-width="150">
<template slot-scope="scope">
<div v-if="scope.row.audioFileUrl">
<el-button
type="primary"
size="mini"
icon="el-icon-link"
@click="showAiAnalysisUrl(scope.row)"
>
获取链接
</el-button>
</div>
<div v-else>
<el-tag type="info" size="mini">
<i class="el-icon-warning" />
暂无音频
</el-tag>
</div>
</template>
</el-table-column>
<el-table-column label="操作" width="280" fixed="right">
<template slot-scope="scope">
<el-button
@@ -380,6 +400,7 @@
width="800px"
:close-on-click-modal="false"
@close="handleEditDialogClose"
@open="handleEditDialogOpen"
>
<el-form ref="audioForm" :model="audioForm" :rules="audioRules" label-width="120px">
<el-row :gutter="20">
@@ -495,127 +516,104 @@
<el-dialog
title="上传音频"
:visible.sync="uploadDialogVisible"
width="800px"
width="500px"
:close-on-click-modal="false"
@close="handleUploadDialogClose"
@open="handleUploadDialogOpen"
>
<el-form ref="uploadForm" :model="uploadForm" :rules="uploadRules" label-width="120px">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="录音名称" prop="recordingName">
<el-input v-model="uploadForm.recordingName" placeholder="请输入录音名称" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="所属门店" prop="dealershipId">
<el-select v-model="uploadForm.dealershipId" placeholder="请选择所属门店" style="width: 100%">
<el-option
v-for="item in dealershipOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="所属项目" prop="projectId">
<el-select v-model="uploadForm.projectId" placeholder="请选择所属项目" style="width: 100%">
<el-option
v-for="item in projectOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="所属销售" prop="salesId">
<el-select v-model="uploadForm.salesId" placeholder="请选择销售" style="width: 100%">
<el-option
v-for="item in salesOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="客户姓名" prop="customerName">
<el-input v-model="uploadForm.customerName" placeholder="请输入客户姓名" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="客户手机号" prop="customerPhone">
<el-input v-model="uploadForm.customerPhone" placeholder="请输入客户手机号" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="意向级别" prop="intentionLevel">
<el-select v-model="uploadForm.intentionLevel" placeholder="请选择意向级别" style="width: 100%">
<el-option label="A级" value="A" />
<el-option label="B级" value="B" />
<el-option label="C级" value="C" />
<el-option label="D级" value="D" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="录音时间" prop="recordingTime">
<el-date-picker
v-model="uploadForm.recordingTime"
type="datetime"
placeholder="选择录音时间"
format="yyyy-MM-dd HH:mm:ss"
value-format="yyyy-MM-dd HH:mm:ss"
style="width: 100%"
/>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="音频文件" prop="audioFile">
<div class="upload-container">
<el-upload
ref="audioUpload"
:action="uploadAction"
:headers="uploadHeaders"
:data="uploadData"
name="file"
:before-upload="beforeAudioUpload"
:on-success="handleUploadSuccess"
:on-error="handleUploadError"
:on-progress="handleUploadProgress"
:on-change="handleFileChange"
:on-remove="handleFileRemove"
:file-list="uploadFileList"
:auto-upload="false"
:auto-upload="true"
:limit="1"
accept=".mp3,.wav,.m4a,.aac,.flac"
drag
:show-file-list="true"
class="upload-demo"
>
<i class="el-icon-upload"></i>
<i class="el-icon-upload" />
<div class="el-upload__text">将音频文件拖到此处<em>点击上传</em></div>
<div class="el-upload__tip" slot="tip">
<div slot="tip" class="el-upload__tip">
支持 mp3wavm4aaacflac 格式文件大小不超过 100MB
</div>
</el-upload>
</el-form-item>
<el-form-item label="录音描述" prop="description">
<el-input
v-model="uploadForm.description"
type="textarea"
:rows="3"
placeholder="请输入录音描述"
/>
</el-form-item>
</el-form>
</div>
<div slot="footer" class="dialog-footer">
<el-button @click="uploadDialogVisible = false"> </el-button>
<el-button type="primary" @click="handleUploadSubmit" :loading="uploadLoading"> </el-button>
<el-button @click="uploadDialogVisible = false"> </el-button>
</div>
</el-dialog>
<!-- AI分析链接对话框 -->
<el-dialog
title="音频下载链接"
:visible.sync="aiAnalysisUrlVisible"
width="500px"
:close-on-click-modal="false"
>
<div class="ai-analysis-container">
<div class="ai-analysis-info">
<p>复制以下下载链接给阿里大模型进行语音分析</p>
</div>
<div class="ai-analysis-url">
<el-input
v-model="aiAnalysisUrl"
readonly
placeholder="音频下载链接"
style="margin-bottom: 15px;"
>
<el-button
slot="append"
type="primary"
icon="el-icon-copy-document"
@click="copyAiAnalysisUrl"
>
复制
</el-button>
</el-input>
<div class="download-actions">
<el-button
type="success"
icon="el-icon-download"
@click="testDownload"
style="margin-right: 10px;"
>
测试下载
</el-button>
<el-button
type="warning"
icon="el-icon-refresh"
@click="regenerateUrl"
>
重新生成链接
</el-button>
</div>
<div class="download-tips">
<p><strong>使用说明</strong></p>
<ul>
<li>点击"测试下载"按钮可以直接下载音频文件到本地</li>
<li>复制上方链接给阿里大模型进行语音分析</li>
<li>如果链接无法访问点击"重新生成链接"</li>
</ul>
</div>
</div>
</div>
<div slot="footer" class="dialog-footer">
<el-button @click="aiAnalysisUrlVisible = false"> </el-button>
</div>
</el-dialog>
@@ -638,7 +636,7 @@
<div class="audio-player">
<div v-if="audioLoading" class="audio-loading">
<i class="el-icon-loading"></i>
<i class="el-icon-loading" />
<span>音频加载中...</span>
</div>
<audio
@@ -646,13 +644,13 @@
:src="currentAudio.audioUrl"
controls
preload="metadata"
style="width: 100%;"
@loadstart="handleAudioLoadStart"
@loadedmetadata="handleAudioLoaded"
@timeupdate="handleTimeUpdate"
@ended="handleAudioEnded"
@error="handleAudioError"
@canplay="handleAudioCanPlay"
style="width: 100%;"
>
您的浏览器不支持音频播放
</audio>
@@ -664,23 +662,23 @@
<el-button
type="primary"
icon="el-icon-video-play"
@click="playAudio"
:disabled="!currentAudio.audioUrl"
@click="playAudio"
>
播放
</el-button>
<el-button
type="warning"
icon="el-icon-video-pause"
@click="pauseAudio"
:disabled="!currentAudio.audioUrl"
@click="pauseAudio"
>
暂停
</el-button>
<el-button
icon="el-icon-refresh"
@click="restartAudio"
:disabled="!currentAudio.audioUrl"
@click="restartAudio"
>
重新播放
</el-button>
@@ -701,7 +699,7 @@
</el-row>
</div>
<div class="audio-progress" v-if="audioDuration > 0">
<div v-if="audioDuration > 0" class="audio-progress">
<div class="progress-info">
<span>{{ formatTime(currentTime) }} / {{ formatTime(audioDuration) }}</span>
<span>{{ Math.round((currentTime / audioDuration) * 100) }}%</span>
@@ -713,7 +711,7 @@
/>
</div>
<div class="audio-notes" v-if="currentAudio.description">
<div v-if="currentAudio.description" class="audio-notes">
<h4>录音备注</h4>
<p>{{ currentAudio.description }}</p>
</div>
@@ -721,7 +719,7 @@
<div slot="footer" class="dialog-footer">
<el-button @click="playDialogVisible = false"> </el-button>
<el-button type="primary" @click="downloadAudio" :disabled="!currentAudio.audioUrl">
<el-button type="primary" :disabled="!currentAudio.audioUrl" @click="downloadAudio">
下载音频
</el-button>
</div>
@@ -762,6 +760,9 @@ export default {
uploadDialogVisible: false,
uploadLoading: false,
uploadFileList: [],
// AI分析链接相关
aiAnalysisUrlVisible: false,
aiAnalysisUrl: '',
// 音频播放相关
playDialogVisible: false,
currentAudio: {
@@ -778,25 +779,13 @@ export default {
audioDuration: 0,
playbackSpeed: '1.0',
audioLoading: false,
uploadAction: process.env.VUE_APP_BASE_API + '/audio/upload',
uploadAction: '/api/audio/upload',
uploadHeaders: {
'Authorization': 'Bearer ' + this.$store.getters.token
},
uploadData: {},
uploadRules: {
recordingName: [
{ required: true, message: '请输入录音名称', trigger: 'blur' }
],
customerName: [
{ required: true, message: '请输入客户姓名', trigger: 'blur' }
],
customerPhone: [
{ required: true, message: '请输入客户手机号', trigger: 'blur' },
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' }
],
audioFile: [
{ required: true, message: '请选择音频文件', trigger: 'change' }
]
uploadData: {
type: 'audio',
audioId: ''
},
// 查询参数
queryParams: {
@@ -857,8 +846,8 @@ export default {
dealershipName: '',
projectName: '',
speechModel: '',
uploadStatus: '',
syncStatus: '',
uploadStatus: '已上传',
syncStatus: '未同步',
isMerged: false,
description: '',
dealershipId: '',
@@ -880,19 +869,13 @@ export default {
duration: [
{ required: true, message: '请输入录音时长', trigger: 'blur' }
]
}
}
},
// 上传音频表单
uploadForm: {
recordingName: '',
dealershipId: '',
projectId: '',
salesId: '',
customerName: '',
customerPhone: '',
intentionLevel: '',
recordingTime: '',
audioFile: null,
description: ''
watch: {
playbackSpeed(newSpeed) {
if (this.$refs.audioPlayer) {
this.$refs.audioPlayer.playbackRate = parseFloat(newSpeed)
}
}
},
@@ -902,13 +885,6 @@ export default {
this.getProjectOptions()
this.getSalesOptions()
},
watch: {
playbackSpeed(newSpeed) {
if (this.$refs.audioPlayer) {
this.$refs.audioPlayer.playbackRate = parseFloat(newSpeed)
}
}
},
methods: {
// 获取列表数据
getList() {
@@ -1268,16 +1244,41 @@ export default {
// 获取录音详细信息
getAudioById(row.id).then(response => {
console.log('编辑录音 111111111111111111111', response)
if (response.code === 20000) {
const audioData = response.data || row
this.audioForm = {
...audioData,
// 确保ID字段正确
dealershipId: audioData.dealershipId || audioData.dealership?.id,
projectId: audioData.projectId || audioData.project?.id,
salesId: audioData.salesId || audioData.sales?.id
const audioData = response.data.audio || row
console.log('获取到的录音数据:', audioData)
// 确保所有字段都正确映射
const formData = {
id: audioData.id,
recordingName: audioData.recordingName || '',
recordingTime: audioData.recordingTime || '',
salesName: audioData.salesName || '',
duration: audioData.duration || 0,
customerName: audioData.customerName || '',
customerPhone: audioData.customerPhone || '',
uploadTime: audioData.uploadTime || '',
intentionLevel: audioData.intentionLevel || '',
dealershipName: audioData.dealershipName || '',
projectName: audioData.projectName || '',
speechModel: audioData.speechModel || '',
uploadStatus: audioData.uploadStatus || '',
syncStatus: audioData.syncStatus || '',
isMerged: audioData.isMerged || false,
description: audioData.description || audioData.remarks || '',
dealershipId: audioData.dealershipId || audioData.dealership?.id || '',
projectId: audioData.projectId || audioData.project?.id || '',
salesId: audioData.salesId || audioData.sales?.id || ''
}
console.log('设置后的表单数据 22222222222222222', this.audioForm)
// 使用Object.assign确保响应式更新
Object.assign(this.audioForm, formData)
console.log('设置后的表单数据:', this.audioForm)
// 强制更新视图
this.$nextTick(() => {
this.editDialogVisible = true
})
} else {
this.$message.error(response.message || '获取录音信息失败')
}
@@ -1285,17 +1286,64 @@ export default {
}).catch(error => {
console.error('获取录音信息失败:', error)
// 如果API调用失败使用行数据
this.audioForm = {
...row,
dealershipId: row.dealershipId || row.dealership?.id,
projectId: row.projectId || row.project?.id,
salesId: row.salesId || row.sales?.id
const formData = {
id: row.id,
recordingName: row.recordingName || '',
recordingTime: row.recordingTime || '',
salesName: row.salesName || '',
duration: row.duration || 0,
customerName: row.customerName || '',
customerPhone: row.customerPhone || '',
uploadTime: row.uploadTime || '',
intentionLevel: row.intentionLevel || '',
dealershipName: row.dealershipName || '',
projectName: row.projectName || '',
speechModel: row.speechModel || '',
uploadStatus: row.uploadStatus || '',
syncStatus: row.syncStatus || '',
isMerged: row.isMerged || false,
description: row.description || row.remarks || '',
dealershipId: row.dealershipId || row.dealership?.id || '',
projectId: row.projectId || row.project?.id || '',
salesId: row.salesId || row.sales?.id || ''
}
// 使用Object.assign确保响应式更新
Object.assign(this.audioForm, formData)
console.log('使用行数据设置的表单数据:', this.audioForm)
// 强制更新视图
this.$nextTick(() => {
this.editDialogVisible = true
})
this.loading = false
})
},
// 编辑对话框打开
handleEditDialogOpen() {
console.log('编辑对话框打开,当前表单数据:', this.audioForm)
// 强制更新表单
this.$nextTick(() => {
if (this.$refs.audioForm) {
this.$refs.audioForm.clearValidate()
// 强制重新渲染表单
this.$forceUpdate()
}
})
},
// 上传对话框打开
handleUploadDialogOpen() {
console.log('上传对话框打开')
// 清空上传组件
this.$nextTick(() => {
if (this.$refs.audioUpload) {
this.$refs.audioUpload.clearFiles()
}
})
},
// 编辑对话框关闭
handleEditDialogClose() {
this.$refs.audioForm?.resetFields()
@@ -1312,8 +1360,8 @@ export default {
dealershipName: '',
projectName: '',
speechModel: '',
uploadStatus: '',
syncStatus: '',
uploadStatus: '已上传',
syncStatus: '未同步',
isMerged: false,
description: '',
dealershipId: '',
@@ -1346,6 +1394,7 @@ export default {
projectId: '',
salesId: ''
}
console.log('添加录音时的表单数据:', this.audioForm)
this.editDialogVisible = true
},
@@ -1397,92 +1446,116 @@ export default {
// 上传音频
handleUpload() {
this.uploadForm = {
recordingName: '',
dealershipId: '',
projectId: '',
salesId: '',
customerName: '',
customerPhone: '',
intentionLevel: '',
recordingTime: '',
audioFile: null,
description: ''
// 检查是否有选中的录音
if (this.selectedIds.length === 0) {
this.$message.warning('请先选择要上传音频的录音记录')
return
}
if (this.selectedIds.length > 1) {
this.$message.warning('一次只能为一个录音上传音频文件')
return
}
const audioId = this.selectedIds[0]
console.log('上传音频 - 选中的录音ID:', audioId)
// 设置audioId到上传数据中
this.uploadData.audioId = audioId
this.uploadFileList = []
console.log('上传音频 - 重置后的文件列表:', this.uploadFileList)
console.log('上传音频 - 设置的上传数据:', this.uploadData)
this.uploadDialogVisible = true
},
// 上传音频对话框关闭
handleUploadDialogClose() {
this.$refs.uploadForm?.resetFields()
console.log('上传音频对话框关闭 - 重置文件列表')
this.uploadFileList = []
// 重置audioId
this.uploadData.audioId = ''
console.log('上传音频对话框关闭 - 重置后的文件列表:', this.uploadFileList)
console.log('上传音频对话框关闭 - 重置后的上传数据:', this.uploadData)
},
// 上传前检查文件大小和类型
beforeAudioUpload(file) {
console.log('上传前检查 - 文件:', file)
console.log('上传前检查 - 上传URL:', this.uploadAction)
console.log('上传前检查 - 请求头:', this.uploadHeaders)
console.log('上传前检查 - 当前audioId:', this.uploadData.audioId)
// 检查是否有audioId
if (!this.uploadData.audioId) {
this.$message.error('缺少录音ID请重新选择录音记录')
return false
}
const isLt100M = file.size / 1024 / 1024 < 100
if (!isLt100M) {
this.$message.error('上传文件大小不能超过 100MB!')
return false
}
return isLt100M
console.log('上传前检查 - 文件大小检查结果:', isLt100M)
console.log('上传前检查 - 最终上传数据:', this.uploadData)
return true
},
// 上传成功
handleUploadSuccess(response, file, fileList) {
if (response.code === 20000) {
console.log('上传成功 - 响应:', response)
console.log('上传成功 - 文件:', file)
console.log('上传成功 - 文件列表:', fileList)
console.log('上传成功 - 使用的audioId:', this.uploadData.audioId)
// 检查响应格式
if (response && (response.code === 20000 || response.code === 200)) {
this.$message.success('音频上传成功')
this.uploadForm.audioFile = file
this.uploadLoading = false
// 刷新列表
this.getList()
// 关闭对话框
this.uploadDialogVisible = false
} else {
this.$message.error(response.message || '音频上传失败')
this.$message.error(response?.message || '音频上传失败')
this.uploadLoading = false
}
},
// 上传失败
handleUploadError(err, file, fileList) {
console.error('上传失败:', err)
this.$message.error('音频上传失败')
console.error('上传失败 - 错误:', err)
console.error('上传失败 - 文件:', file)
console.error('上传失败 - 文件列表:', fileList)
console.error('上传失败 - 状态:', err.status)
console.error('上传失败 - 状态文本:', err.statusText)
console.error('上传失败 - 使用的audioId:', this.uploadData.audioId)
this.$message.error('音频上传失败: ' + (err.message || err.statusText || '未知错误'))
this.uploadLoading = false
},
// 文件变化处理
handleFileChange(file, fileList) {
console.log('文件变化 - 文件:', file)
console.log('文件变化 - 文件列表:', fileList)
this.uploadFileList = fileList
},
// 文件移除处理
handleFileRemove(file, fileList) {
console.log('文件移除 - 文件:', file)
console.log('文件移除 - 文件列表:', fileList)
this.uploadFileList = fileList
},
// 上传进度
handleUploadProgress(event, file, fileList) {
console.log('上传进度 - 事件:', event)
console.log('上传进度 - 文件:', file)
this.uploadLoading = true
},
// 上传提交
handleUploadSubmit() {
this.$refs.uploadForm.validate(valid => {
if (valid) {
const formData = new FormData()
Object.keys(this.uploadForm).forEach(key => {
if (key !== 'audioFile') {
formData.append(key, this.uploadForm[key])
}
})
if (this.uploadForm.audioFile) {
formData.append('audioFile', this.uploadForm.audioFile)
}
addAudio(formData).then(response => {
if (response.code === 20000) {
this.$message.success('音频添加成功')
this.uploadDialogVisible = false
this.getList()
} else {
this.$message.error(response.message || '音频添加失败')
}
}).catch(() => {
this.$message.success('音频添加成功')
this.uploadDialogVisible = false
this.getList()
})
}
})
},
// 播放音频
handlePlay(row) {
this.currentAudio = {
@@ -1521,7 +1594,9 @@ export default {
// 生成音频URL
generateAudioUrl(audioId) {
return process.env.VUE_APP_BASE_API + `/audio/stream/${audioId}`
const baseUrl = process.env.VUE_APP_API_URL || window.location.origin
const apiPath = process.env.VUE_APP_BASE_API || '/api'
return baseUrl + apiPath + `/audio/stream/${audioId}`
},
// 获取完整音频URL
@@ -1531,8 +1606,93 @@ export default {
if (audioFileUrl.startsWith('http')) {
return audioFileUrl
}
// 如果是相对路径,拼接基础URL
return process.env.VUE_APP_BASE_API + audioFileUrl
// 如果是相对路径,拼接完整的域名
const baseUrl = process.env.VUE_APP_API_URL || window.location.origin
return baseUrl + audioFileUrl
},
// 获取下载链接URL
getDownloadUrl(audioFileUrl) {
if (!audioFileUrl) return ''
// 如果已经是完整URL直接返回
if (audioFileUrl.startsWith('http')) {
return audioFileUrl
}
// 如果是相对路径,转换为下载端点
const baseUrl = process.env.VUE_APP_API_URL || window.location.origin
// 从音频文件URL中提取文件名
const fileName = audioFileUrl.split('/').pop()
if (!fileName) return ''
// 使用专门的下载端点
return `${baseUrl}/api/audio/download/${fileName}`
},
// 获取备用下载链接1带下载参数
getAlternativeUrl1() {
if (!this.currentAiAnalysisRow || !this.currentAiAnalysisRow.audioFileUrl) return ''
const baseUrl = process.env.VUE_APP_API_URL || window.location.origin
return `${baseUrl}${this.currentAiAnalysisRow.audioFileUrl}?download=true&disposition=attachment`
},
// 获取备用下载链接2流式下载
getAlternativeUrl2() {
if (!this.currentAiAnalysisRow || !this.currentAiAnalysisRow.audioFileUrl) return ''
const baseUrl = process.env.VUE_APP_API_URL || window.location.origin
const fileName = this.currentAiAnalysisRow.audioFileUrl.split('/').pop()
return `${baseUrl}/api/audio/stream/${fileName}`
},
// 测试下载功能
testDownload() {
if (!this.currentAiAnalysisRow || !this.currentAiAnalysisRow.audioFileUrl) {
this.$message.warning('没有可下载的音频文件')
return
}
const baseUrl = process.env.VUE_APP_API_URL || window.location.origin
const audioUrl = `${baseUrl}${this.currentAiAnalysisRow.audioFileUrl}`
const fileName = this.currentAiAnalysisRow.audioFileOriginalName ||
this.currentAiAnalysisRow.audioFileUrl.split('/').pop()
// 使用fetch下载文件
this.$message.info('正在下载音频文件...')
fetch(audioUrl)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
return response.blob()
})
.then(blob => {
// 创建下载链接
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = fileName
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(url)
this.$message.success('下载成功!')
})
.catch(error => {
console.error('下载失败:', error)
this.$message.error('下载失败,请检查网络连接或文件是否存在')
})
},
// 重新生成链接
regenerateUrl() {
if (!this.currentAiAnalysisRow) return
const baseUrl = process.env.VUE_APP_API_URL || window.location.origin
const timestamp = new Date().getTime()
const randomParam = Math.random().toString(36).substring(7)
this.aiAnalysisUrl = `${baseUrl}${this.currentAiAnalysisRow.audioFileUrl}?t=${timestamp}&r=${randomParam}&download=true`
this.$message.success('链接已重新生成')
},
// 重置音频播放器
@@ -1675,9 +1835,60 @@ export default {
'D': 'danger'
}
return typeMap[level] || 'info'
},
// 显示AI分析链接
showAiAnalysisUrl(row) {
this.currentAiAnalysisRow = row
// 生成一个带时间戳的下载链接,避免缓存问题
const baseUrl = process.env.VUE_APP_API_URL || window.location.origin
const timestamp = new Date().getTime()
this.aiAnalysisUrl = `${baseUrl}${row.audioFileUrl}?t=${timestamp}&download=true`
this.aiAnalysisUrlVisible = true
},
// 复制AI分析链接
copyAiAnalysisUrl() {
if (this.aiAnalysisUrl) {
// 使用现代浏览器的 Clipboard API
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(this.aiAnalysisUrl).then(() => {
this.$message.success('链接已复制到剪贴板')
}).catch(() => {
this.fallbackCopyTextToClipboard(this.aiAnalysisUrl)
})
} else {
this.fallbackCopyTextToClipboard(this.aiAnalysisUrl)
}
}
},
// 备用复制方法
fallbackCopyTextToClipboard(text) {
const textArea = document.createElement('textarea')
textArea.value = text
textArea.style.position = 'fixed'
textArea.style.left = '-999999px'
textArea.style.top = '-999999px'
document.body.appendChild(textArea)
textArea.focus()
textArea.select()
try {
const successful = document.execCommand('copy')
if (successful) {
this.$message.success('链接已复制到剪贴板')
} else {
this.$message.error('复制失败,请手动复制')
}
} catch (err) {
this.$message.error('复制失败,请手动复制')
}
document.body.removeChild(textArea)
}
}
}
</script>
<style scoped>
@@ -1798,4 +2009,82 @@ export default {
color: #606266;
line-height: 1.6;
}
/* 上传音频样式 */
.upload-container {
padding: 20px;
text-align: center;
}
.upload-demo {
width: 100%;
}
.upload-demo .el-upload {
width: 100%;
}
.upload-demo .el-upload-dragger {
width: 100%;
height: 200px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
/* AI分析链接样式 */
.ai-analysis-container {
padding: 20px 0;
}
.ai-analysis-info {
margin-bottom: 20px;
padding: 15px;
background-color: #f0f9ff;
border-radius: 4px;
border-left: 4px solid #409eff;
}
.ai-analysis-info p {
margin: 0;
color: #606266;
line-height: 1.6;
}
.ai-analysis-url {
margin-bottom: 20px;
}
.download-actions {
margin-bottom: 15px;
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.download-tips {
margin-top: 15px;
padding: 15px;
background-color: #f5f7fa;
border-radius: 4px;
border-left: 4px solid #e6a23c;
}
.download-tips p {
margin: 0 0 10px 0;
color: #606266;
font-size: 14px;
}
.download-tips ul {
margin: 0;
padding-left: 20px;
color: #606266;
}
.download-tips li {
margin-bottom: 5px;
line-height: 1.5;
}
</style>

View File

@@ -68,13 +68,7 @@
<el-table-column label="设备信息(最近一次)" min-width="300">
<template slot-scope="scope">
<div class="device-info">
<div>联网时间: {{ scope.row.lastOnlineTime || '-' }}</div>
<div>开机时间: {{ scope.row.lastPowerOnTime || '-' }}</div>
<div>关机时间: {{ scope.row.lastPowerOffTime || '-' }}</div>
<div>WIFI名称: {{ scope.row.wifiName || '-' }}</div>
<div>未上传数量: {{ scope.row.unuploadedCount || 0 }}</div>
<div>空间大小: {{ scope.row.storageUsed || '0GB' }}/{{ scope.row.storageSize || '0GB' }}</div>
<div>剩余电量: {{ scope.row.remainingBattery || '-' }}</div>
<div>
充电状态:
<span

View File

@@ -31,12 +31,12 @@ module.exports = {
productionSourceMap: false,
devServer: {
port: port,
open: true,
open: false, // 禁用自动打开浏览器
overlay: {
warnings: false,
errors: true
},
// before: require('./mock/mock-server.js'), // 注释掉 mock 服务器
// before: require('./mock/mock-server.js'), // 启用 mock 服务器
watchOptions: {
ignored: [
'**/node_modules/**',
@@ -56,12 +56,15 @@ module.exports = {
},
proxy: {
'/api': {
target: 'http://localhost:9060', // 从环境变量获取后端服务器地址
target: 'http://localhost:9060', // 本地后端服务器地址
// target: 'https://api.huayang-star.com/', // 生产环境后端地址
changeOrigin: true,
pathRewrite: {
'^/api': '/api' // 保持 /api 前缀
},
logLevel: 'debug' // 开发环境下显示代理日志
logLevel: 'debug', // 开发环境下显示代理日志
secure: false, // 如果是https忽略证书验证
timeout: 10000 // 设置超时时间
}
}
},