完善客户画像,品牌分析

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' ENV = 'development'
# base api # base api
VUE_APP_BASE_API = '/dev-api' VUE_APP_BASE_API = ''

View File

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

View File

@@ -120,12 +120,30 @@ export const constantRoutes = [
{ {
path: '/dealership', path: '/dealership',
component: Layout, component: Layout,
redirect: '/dealership/index',
name: 'Dealership',
meta: {
title: '门店管理',
icon: 'el-icon-s-shop'
},
children: [ children: [
{ {
path: 'index', path: 'index',
name: 'Dealership', name: 'DealershipIndex',
component: () => import('@/views/dealership/index'), component: () => import('@/views/dealership/index'),
meta: { title: '门店管理', icon: 'el-icon-s-shop' } 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 v-if="scope.row.audioFileUrl">
<div style="display: flex; align-items: center;"> <div style="display: flex; align-items: center;">
<el-tag type="success" size="mini" style="margin-right: 8px;"> <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-tag>
<el-link <el-link
@@ -203,15 +203,15 @@
target="_blank" target="_blank"
:underline="false" :underline="false"
> >
<i class="el-icon-document"></i> <i class="el-icon-document" />
{{ scope.row.audioFileOriginalName || getAudioFileName(scope.row.audioFileUrl) }} {{ scope.row.audioFileOriginalName || getAudioFileName(scope.row.audioFileUrl) }}
</el-link> </el-link>
<el-button <el-button
type="text" type="text"
size="mini" size="mini"
icon="el-icon-download" icon="el-icon-download"
@click="downloadAudioFile(scope.row)"
style="margin-left: 10px;" style="margin-left: 10px;"
@click="downloadAudioFile(scope.row)"
> >
下载 下载
</el-button> </el-button>
@@ -219,12 +219,32 @@
</div> </div>
<div v-else> <div v-else>
<el-tag type="info" size="mini"> <el-tag type="info" size="mini">
<i class="el-icon-warning"></i> <i class="el-icon-warning" />
暂无音频文件 暂无音频文件
</el-tag> </el-tag>
</div> </div>
</template> </template>
</el-table-column> </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"> <el-table-column label="操作" width="280" fixed="right">
<template slot-scope="scope"> <template slot-scope="scope">
<el-button <el-button
@@ -380,6 +400,7 @@
width="800px" width="800px"
:close-on-click-modal="false" :close-on-click-modal="false"
@close="handleEditDialogClose" @close="handleEditDialogClose"
@open="handleEditDialogOpen"
> >
<el-form ref="audioForm" :model="audioForm" :rules="audioRules" label-width="120px"> <el-form ref="audioForm" :model="audioForm" :rules="audioRules" label-width="120px">
<el-row :gutter="20"> <el-row :gutter="20">
@@ -495,127 +516,104 @@
<el-dialog <el-dialog
title="上传音频" title="上传音频"
:visible.sync="uploadDialogVisible" :visible.sync="uploadDialogVisible"
width="800px" width="500px"
:close-on-click-modal="false" :close-on-click-modal="false"
@close="handleUploadDialogClose" @close="handleUploadDialogClose"
@open="handleUploadDialogOpen"
> >
<el-form ref="uploadForm" :model="uploadForm" :rules="uploadRules" label-width="120px"> <div class="upload-container">
<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">
<el-upload <el-upload
ref="audioUpload" ref="audioUpload"
:action="uploadAction" :action="uploadAction"
:headers="uploadHeaders" :headers="uploadHeaders"
:data="uploadData" :data="uploadData"
name="file"
:before-upload="beforeAudioUpload" :before-upload="beforeAudioUpload"
:on-success="handleUploadSuccess" :on-success="handleUploadSuccess"
:on-error="handleUploadError" :on-error="handleUploadError"
:on-progress="handleUploadProgress" :on-progress="handleUploadProgress"
:on-change="handleFileChange"
:on-remove="handleFileRemove"
:file-list="uploadFileList" :file-list="uploadFileList"
:auto-upload="false" :auto-upload="true"
:limit="1" :limit="1"
accept=".mp3,.wav,.m4a,.aac,.flac" accept=".mp3,.wav,.m4a,.aac,.flac"
drag 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__text">将音频文件拖到此处<em>点击上传</em></div>
<div class="el-upload__tip" slot="tip"> <div slot="tip" class="el-upload__tip">
支持 mp3wavm4aaacflac 格式文件大小不超过 100MB 支持 mp3wavm4aaacflac 格式文件大小不超过 100MB
</div> </div>
</el-upload> </el-upload>
</el-form-item> </div>
<el-form-item label="录音描述" prop="description">
<el-input
v-model="uploadForm.description"
type="textarea"
:rows="3"
placeholder="请输入录音描述"
/>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer"> <div slot="footer" class="dialog-footer">
<el-button @click="uploadDialogVisible = false"> </el-button> <el-button @click="uploadDialogVisible = false"> </el-button>
<el-button type="primary" @click="handleUploadSubmit" :loading="uploadLoading"> </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> </div>
</el-dialog> </el-dialog>
@@ -638,7 +636,7 @@
<div class="audio-player"> <div class="audio-player">
<div v-if="audioLoading" class="audio-loading"> <div v-if="audioLoading" class="audio-loading">
<i class="el-icon-loading"></i> <i class="el-icon-loading" />
<span>音频加载中...</span> <span>音频加载中...</span>
</div> </div>
<audio <audio
@@ -646,13 +644,13 @@
:src="currentAudio.audioUrl" :src="currentAudio.audioUrl"
controls controls
preload="metadata" preload="metadata"
style="width: 100%;"
@loadstart="handleAudioLoadStart" @loadstart="handleAudioLoadStart"
@loadedmetadata="handleAudioLoaded" @loadedmetadata="handleAudioLoaded"
@timeupdate="handleTimeUpdate" @timeupdate="handleTimeUpdate"
@ended="handleAudioEnded" @ended="handleAudioEnded"
@error="handleAudioError" @error="handleAudioError"
@canplay="handleAudioCanPlay" @canplay="handleAudioCanPlay"
style="width: 100%;"
> >
您的浏览器不支持音频播放 您的浏览器不支持音频播放
</audio> </audio>
@@ -664,23 +662,23 @@
<el-button <el-button
type="primary" type="primary"
icon="el-icon-video-play" icon="el-icon-video-play"
@click="playAudio"
:disabled="!currentAudio.audioUrl" :disabled="!currentAudio.audioUrl"
@click="playAudio"
> >
播放 播放
</el-button> </el-button>
<el-button <el-button
type="warning" type="warning"
icon="el-icon-video-pause" icon="el-icon-video-pause"
@click="pauseAudio"
:disabled="!currentAudio.audioUrl" :disabled="!currentAudio.audioUrl"
@click="pauseAudio"
> >
暂停 暂停
</el-button> </el-button>
<el-button <el-button
icon="el-icon-refresh" icon="el-icon-refresh"
@click="restartAudio"
:disabled="!currentAudio.audioUrl" :disabled="!currentAudio.audioUrl"
@click="restartAudio"
> >
重新播放 重新播放
</el-button> </el-button>
@@ -701,7 +699,7 @@
</el-row> </el-row>
</div> </div>
<div class="audio-progress" v-if="audioDuration > 0"> <div v-if="audioDuration > 0" class="audio-progress">
<div class="progress-info"> <div class="progress-info">
<span>{{ formatTime(currentTime) }} / {{ formatTime(audioDuration) }}</span> <span>{{ formatTime(currentTime) }} / {{ formatTime(audioDuration) }}</span>
<span>{{ Math.round((currentTime / audioDuration) * 100) }}%</span> <span>{{ Math.round((currentTime / audioDuration) * 100) }}%</span>
@@ -713,7 +711,7 @@
/> />
</div> </div>
<div class="audio-notes" v-if="currentAudio.description"> <div v-if="currentAudio.description" class="audio-notes">
<h4>录音备注</h4> <h4>录音备注</h4>
<p>{{ currentAudio.description }}</p> <p>{{ currentAudio.description }}</p>
</div> </div>
@@ -721,7 +719,7 @@
<div slot="footer" class="dialog-footer"> <div slot="footer" class="dialog-footer">
<el-button @click="playDialogVisible = false"> </el-button> <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> </el-button>
</div> </div>
@@ -762,6 +760,9 @@ export default {
uploadDialogVisible: false, uploadDialogVisible: false,
uploadLoading: false, uploadLoading: false,
uploadFileList: [], uploadFileList: [],
// AI分析链接相关
aiAnalysisUrlVisible: false,
aiAnalysisUrl: '',
// 音频播放相关 // 音频播放相关
playDialogVisible: false, playDialogVisible: false,
currentAudio: { currentAudio: {
@@ -778,25 +779,13 @@ export default {
audioDuration: 0, audioDuration: 0,
playbackSpeed: '1.0', playbackSpeed: '1.0',
audioLoading: false, audioLoading: false,
uploadAction: process.env.VUE_APP_BASE_API + '/audio/upload', uploadAction: '/api/audio/upload',
uploadHeaders: { uploadHeaders: {
'Authorization': 'Bearer ' + this.$store.getters.token 'Authorization': 'Bearer ' + this.$store.getters.token
}, },
uploadData: {}, uploadData: {
uploadRules: { type: 'audio',
recordingName: [ audioId: ''
{ 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' }
]
}, },
// 查询参数 // 查询参数
queryParams: { queryParams: {
@@ -857,8 +846,8 @@ export default {
dealershipName: '', dealershipName: '',
projectName: '', projectName: '',
speechModel: '', speechModel: '',
uploadStatus: '', uploadStatus: '已上传',
syncStatus: '', syncStatus: '未同步',
isMerged: false, isMerged: false,
description: '', description: '',
dealershipId: '', dealershipId: '',
@@ -880,19 +869,13 @@ export default {
duration: [ duration: [
{ required: true, message: '请输入录音时长', trigger: 'blur' } { required: true, message: '请输入录音时长', trigger: 'blur' }
] ]
}
}
}, },
// 上传音频表单 watch: {
uploadForm: { playbackSpeed(newSpeed) {
recordingName: '', if (this.$refs.audioPlayer) {
dealershipId: '', this.$refs.audioPlayer.playbackRate = parseFloat(newSpeed)
projectId: '',
salesId: '',
customerName: '',
customerPhone: '',
intentionLevel: '',
recordingTime: '',
audioFile: null,
description: ''
} }
} }
}, },
@@ -902,13 +885,6 @@ export default {
this.getProjectOptions() this.getProjectOptions()
this.getSalesOptions() this.getSalesOptions()
}, },
watch: {
playbackSpeed(newSpeed) {
if (this.$refs.audioPlayer) {
this.$refs.audioPlayer.playbackRate = parseFloat(newSpeed)
}
}
},
methods: { methods: {
// 获取列表数据 // 获取列表数据
getList() { getList() {
@@ -1268,16 +1244,41 @@ export default {
// 获取录音详细信息 // 获取录音详细信息
getAudioById(row.id).then(response => { getAudioById(row.id).then(response => {
console.log('编辑录音 111111111111111111111', response)
if (response.code === 20000) { if (response.code === 20000) {
const audioData = response.data || row const audioData = response.data.audio || row
this.audioForm = { console.log('获取到的录音数据:', audioData)
...audioData, // 确保所有字段都正确映射
// 确保ID字段正确 const formData = {
dealershipId: audioData.dealershipId || audioData.dealership?.id, id: audioData.id,
projectId: audioData.projectId || audioData.project?.id, recordingName: audioData.recordingName || '',
salesId: audioData.salesId || audioData.sales?.id 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 this.editDialogVisible = true
})
} else { } else {
this.$message.error(response.message || '获取录音信息失败') this.$message.error(response.message || '获取录音信息失败')
} }
@@ -1285,17 +1286,64 @@ export default {
}).catch(error => { }).catch(error => {
console.error('获取录音信息失败:', error) console.error('获取录音信息失败:', error)
// 如果API调用失败使用行数据 // 如果API调用失败使用行数据
this.audioForm = { const formData = {
...row, id: row.id,
dealershipId: row.dealershipId || row.dealership?.id, recordingName: row.recordingName || '',
projectId: row.projectId || row.project?.id, recordingTime: row.recordingTime || '',
salesId: row.salesId || row.sales?.id 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.editDialogVisible = true
})
this.loading = false 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() { handleEditDialogClose() {
this.$refs.audioForm?.resetFields() this.$refs.audioForm?.resetFields()
@@ -1312,8 +1360,8 @@ export default {
dealershipName: '', dealershipName: '',
projectName: '', projectName: '',
speechModel: '', speechModel: '',
uploadStatus: '', uploadStatus: '已上传',
syncStatus: '', syncStatus: '未同步',
isMerged: false, isMerged: false,
description: '', description: '',
dealershipId: '', dealershipId: '',
@@ -1346,6 +1394,7 @@ export default {
projectId: '', projectId: '',
salesId: '' salesId: ''
} }
console.log('添加录音时的表单数据:', this.audioForm)
this.editDialogVisible = true this.editDialogVisible = true
}, },
@@ -1397,92 +1446,116 @@ export default {
// 上传音频 // 上传音频
handleUpload() { handleUpload() {
this.uploadForm = { // 检查是否有选中的录音
recordingName: '', if (this.selectedIds.length === 0) {
dealershipId: '', this.$message.warning('请先选择要上传音频的录音记录')
projectId: '', return
salesId: '',
customerName: '',
customerPhone: '',
intentionLevel: '',
recordingTime: '',
audioFile: null,
description: ''
} }
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 = [] this.uploadFileList = []
console.log('上传音频 - 重置后的文件列表:', this.uploadFileList)
console.log('上传音频 - 设置的上传数据:', this.uploadData)
this.uploadDialogVisible = true this.uploadDialogVisible = true
}, },
// 上传音频对话框关闭 // 上传音频对话框关闭
handleUploadDialogClose() { handleUploadDialogClose() {
this.$refs.uploadForm?.resetFields() console.log('上传音频对话框关闭 - 重置文件列表')
this.uploadFileList = [] this.uploadFileList = []
// 重置audioId
this.uploadData.audioId = ''
console.log('上传音频对话框关闭 - 重置后的文件列表:', this.uploadFileList)
console.log('上传音频对话框关闭 - 重置后的上传数据:', this.uploadData)
}, },
// 上传前检查文件大小和类型 // 上传前检查文件大小和类型
beforeAudioUpload(file) { 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 const isLt100M = file.size / 1024 / 1024 < 100
if (!isLt100M) { if (!isLt100M) {
this.$message.error('上传文件大小不能超过 100MB!') this.$message.error('上传文件大小不能超过 100MB!')
return false
} }
return isLt100M console.log('上传前检查 - 文件大小检查结果:', isLt100M)
console.log('上传前检查 - 最终上传数据:', this.uploadData)
return true
}, },
// 上传成功 // 上传成功
handleUploadSuccess(response, file, fileList) { 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.$message.success('音频上传成功')
this.uploadForm.audioFile = file
this.uploadLoading = false this.uploadLoading = false
// 刷新列表
this.getList()
// 关闭对话框
this.uploadDialogVisible = false
} else { } else {
this.$message.error(response.message || '音频上传失败') this.$message.error(response?.message || '音频上传失败')
this.uploadLoading = false this.uploadLoading = false
} }
}, },
// 上传失败 // 上传失败
handleUploadError(err, file, fileList) { handleUploadError(err, file, fileList) {
console.error('上传失败:', err) console.error('上传失败 - 错误:', err)
this.$message.error('音频上传失败') 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 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) { handleUploadProgress(event, file, fileList) {
console.log('上传进度 - 事件:', event)
console.log('上传进度 - 文件:', file)
this.uploadLoading = true 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) { handlePlay(row) {
this.currentAudio = { this.currentAudio = {
@@ -1521,7 +1594,9 @@ export default {
// 生成音频URL // 生成音频URL
generateAudioUrl(audioId) { 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 // 获取完整音频URL
@@ -1531,8 +1606,93 @@ export default {
if (audioFileUrl.startsWith('http')) { if (audioFileUrl.startsWith('http')) {
return audioFileUrl 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,6 +1835,57 @@ export default {
'D': 'danger' 'D': 'danger'
} }
return typeMap[level] || 'info' 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)
} }
} }
} }
@@ -1798,4 +2009,82 @@ export default {
color: #606266; color: #606266;
line-height: 1.6; 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> </style>

View File

@@ -68,13 +68,7 @@
<el-table-column label="设备信息(最近一次)" min-width="300"> <el-table-column label="设备信息(最近一次)" min-width="300">
<template slot-scope="scope"> <template slot-scope="scope">
<div class="device-info"> <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>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> <div>
充电状态: 充电状态:
<span <span

View File

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