增加喜喜 概念框架
This commit is contained in:
278
pages-subpackage/xixi/components/xixi-record.vue
Normal file
278
pages-subpackage/xixi/components/xixi-record.vue
Normal file
@@ -0,0 +1,278 @@
|
||||
<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 ? '你已填写「我是谁」,建议在真实对话中自然带出其中 1~2 点,避免像背稿。' : '尚未填写「我是谁」,补全后诊断可结合你的自我描述更贴切。';
|
||||
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>
|
||||
Reference in New Issue
Block a user