Files
smartDriveEEUniApp/pages-subpackage/xixi/components/xixi-record.vue
2026-04-05 22:23:19 +08:00

279 lines
8.1 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>
<scroll-view class="panel-scroll" scroll-y>
<view class="toolbar">
<button
v-if="!recording"
class="btn-round"
type="primary"
:disabled="!recorderSupported"
@click="startRecord"
>
开始录音
</button>
<button v-else class="btn-round btn-stop" type="warn" @click="stopRecord">结束录音</button>
<text v-if="!recorderSupported" class="hint">当前环境不支持录音请使用微信小程序或 App</text>
<text v-else-if="recording" class="hint recording">正在录音</text>
</view>
<view class="section-title">录音列表</view>
<view v-if="!recordings.length" class="empty">暂无录音点击开始录音记录相处时刻</view>
<view v-for="item in recordings" :key="item.id" class="rec-card">
<view class="rec-head">
<text class="rec-time">{{ formatDate(item.createdAt) }}</text>
<text class="rec-dur">{{ formatDuration(item.duration) }}</text>
</view>
<view class="rec-actions">
<button class="btn-mini" size="mini" @click="play(item)" :disabled="!item.tempFilePath">试听</button>
<button
class="btn-mini primary"
size="mini"
:disabled="item.analyzing"
@click="analyze(item)"
>
{{ item.analyzing ? '分析中…' : item.analysisText ? '重新分析' : '分析录音' }}
</button>
</view>
<text v-if="item.analysisText" class="rec-preview">{{ truncate(item.analysisText, 80) }}</text>
</view>
</scroll-view>
</template>
<script>
const KEY = 'xixi_recordings';
export default {
name: 'XixiRecord',
data() {
return {
recordings: [],
recording: false,
recorderSupported: false,
recorderManager: null,
innerAudio: null,
};
},
mounted() {
this.recorderSupported = typeof uni.getRecorderManager === 'function';
this.loadList();
if (this.recorderSupported) {
this.recorderManager = uni.getRecorderManager();
this.recorderManager.onStop((res) => {
this.recording = false;
const duration = res.duration || 0;
const tempFilePath = res.tempFilePath || '';
if (!tempFilePath) {
uni.showToast({ title: '未获取到录音文件', icon: 'none' });
return;
}
const item = {
id: `r_${Date.now()}`,
tempFilePath,
duration,
createdAt: Date.now(),
analysisText: '',
analyzing: false,
};
this.recordings.unshift(item);
this.persist();
uni.showToast({ title: '已保存录音', icon: 'success' });
this.$emit('recordings-changed');
});
this.recorderManager.onError(() => {
this.recording = false;
uni.showToast({ title: '录音出错', icon: 'none' });
});
}
},
beforeUnmount() {
this.stopAudio();
},
methods: {
loadList() {
try {
const list = uni.getStorageSync(KEY);
this.recordings = Array.isArray(list) ? list : [];
} catch (e) {
this.recordings = [];
}
},
persist() {
try {
uni.setStorageSync(KEY, this.recordings);
} catch (e) {
/* ignore */
}
},
startRecord() {
if (!this.recorderSupported || !this.recorderManager) return;
this.recording = true;
try {
this.recorderManager.start({
duration: 600000,
sampleRate: 44100,
numberOfChannels: 1,
encodeBitRate: 96000,
format: 'mp3',
});
} catch (e) {
this.recording = false;
uni.showToast({ title: '无法开始录音', icon: 'none' });
}
},
stopRecord() {
if (this.recorderManager && this.recording) {
this.recorderManager.stop();
}
},
play(item) {
if (!item.tempFilePath) return;
this.stopAudio();
const ctx = uni.createInnerAudioContext();
this.innerAudio = ctx;
ctx.src = item.tempFilePath;
ctx.onEnded(() => this.stopAudio());
ctx.onError(() => {
uni.showToast({ title: '播放失败', icon: 'none' });
this.stopAudio();
});
ctx.play();
},
stopAudio() {
if (this.innerAudio) {
try {
this.innerAudio.stop();
this.innerAudio.destroy();
} catch (e) {
/* ignore */
}
this.innerAudio = null;
}
},
analyze(item) {
const idx = this.recordings.findIndex((r) => r.id === item.id);
if (idx < 0) return;
const cur = this.recordings[idx];
this.recordings.splice(idx, 1, { ...cur, analyzing: true });
const who = uni.getStorageSync('xixi_who_am_i') || {};
const meet = uni.getStorageSync('xixi_first_meeting') || {};
const id = item.id;
setTimeout(() => {
const i = this.recordings.findIndex((r) => r.id === id);
if (i < 0) return;
const row = this.recordings[i];
const sec = Math.max(1, Math.round((row.duration || 0) / 1000));
const text = this.buildMockAnalysis(sec, who, meet);
this.recordings.splice(i, 1, { ...row, analysisText: text, analyzing: false });
this.persist();
uni.showToast({ title: '分析完成', icon: 'success' });
this.$emit('recordings-changed');
}, 900);
},
buildMockAnalysis(seconds, who, meet) {
const intro = who.selfIntro ? '你已填写「我是谁」,建议在真实对话中自然带出其中 12 点,避免像背稿。' : '尚未填写「我是谁」,补全后诊断可结合你的自我描述更贴切。';
const goal = meet.meetingGoal ? `结合你写的见面目标「${meet.meetingGoal.slice(0, 40)}${meet.meetingGoal.length > 40 ? '…' : ''}」,可回顾本次对话是否朝该方向推进。` : '可补充「本次见面要达到的结果」,便于对照复盘。';
return (
`【喜喜 · AI 相处诊断】(示例)\n\n` +
`· 录音时长约 ${seconds} 秒。\n` +
`· ${intro}\n` +
`· ${goal}\n\n` +
`沟通氛围:语气平稳、留白适中更易建立信任;若语速偏快,可适当停顿给对方回应空间。\n` +
`下一步:在「诊断结果」页查看完整记录;接入语音转写与大模型后,将替换为基于真实内容的分析。`
);
},
formatDate(ts) {
const d = new Date(ts);
const p = (n) => (n < 10 ? '0' + n : '' + n);
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`;
},
formatDuration(ms) {
const s = Math.round((ms || 0) / 1000);
const m = Math.floor(s / 60);
const r = s % 60;
return m > 0 ? `${m}${r}` : `${r}`;
},
truncate(s, n) {
if (!s) return '';
return s.length <= n ? s : s.slice(0, n) + '…';
},
},
};
</script>
<style scoped>
.panel-scroll {
height: 100%;
box-sizing: border-box;
padding: 24rpx 28rpx 32rpx;
}
.toolbar {
margin-bottom: 24rpx;
}
.btn-round {
border-radius: 999rpx;
background: linear-gradient(135deg, #ff6b9d, #e91e8c);
border: none;
margin-bottom: 12rpx;
}
.btn-stop {
background: linear-gradient(135deg, #ff8a65, #f4511e);
}
.hint {
display: block;
font-size: 24rpx;
color: #888;
margin-top: 8rpx;
}
.hint.recording {
color: #e91e8c;
font-weight: 600;
}
.section-title {
font-size: 30rpx;
font-weight: 600;
color: #333;
margin-bottom: 16rpx;
}
.empty {
font-size: 26rpx;
color: #999;
padding: 40rpx 0;
text-align: center;
}
.rec-card {
background: #fff;
border-radius: 20rpx;
padding: 24rpx;
margin-bottom: 20rpx;
box-shadow: 0 4rpx 24rpx rgba(233, 30, 140, 0.06);
}
.rec-head {
display: flex;
justify-content: space-between;
margin-bottom: 16rpx;
}
.rec-time {
font-size: 26rpx;
color: #333;
}
.rec-dur {
font-size: 24rpx;
color: #e91e8c;
}
.rec-actions {
display: flex;
gap: 20rpx;
}
.btn-mini.primary {
background: #fff0f5;
color: #e91e8c;
border: 1rpx solid #ffb6c1;
}
.rec-preview {
display: block;
margin-top: 16rpx;
font-size: 24rpx;
color: #666;
line-height: 1.5;
}
</style>