增加喜喜 概念框架

This commit is contained in:
zhonghua.li
2026-04-05 22:23:19 +08:00
parent b53195de68
commit b768d52f4b
11 changed files with 1363 additions and 1 deletions

View File

@@ -200,6 +200,7 @@
'pages/furniture_reception/furniture_reception': '/reception',
'pages/ai_qa/ai_qa': '/ai-qa',
'pages/ai_analysis/ai_analysis': '/ai-analysis',
'pages/xixi/xixi': '/xixi',
'pages/ucenter/ucenter': '/profile'
};

View File

@@ -0,0 +1,235 @@
<template>
<scroll-view class="panel-scroll" scroll-y>
<view class="head-card">
<text class="head-title">今日约会建议</text>
<text class="head-date">{{ todayLabel }}</text>
<text class="head-note">以下内容根据你在我是谁约会准备中的填写自动生成仅供当日参考</text>
</view>
<view v-if="emptyHint" class="empty-wrap">
<text class="empty-title">还没有足够的信息</text>
<text class="empty-desc">请先在我是谁约会准备里填写内容保存后再来查看约会建议</text>
</view>
<view v-else class="list">
<view v-for="(line, idx) in lines" :key="idx" class="advice-item">
<text class="advice-index">{{ idx + 1 }}</text>
<text class="advice-text">{{ line }}</text>
</view>
</view>
</scroll-view>
</template>
<script>
const KEY_WHO = 'xixi_who_am_i';
const KEY_MEET = 'xixi_first_meeting';
function formatToday() {
const d = new Date();
const wk = ['日', '一', '二', '三', '四', '五', '六'];
return `${d.getFullYear()}${d.getMonth() + 1}${d.getDate()}日 星期${wk[d.getDay()]}`;
}
function trimSnippet(text, max) {
if (text == null || text === '') return '';
const s = String(text).trim().replace(/\s+/g, ' ');
if (!s) return '';
return s.length > max ? s.slice(0, max) + '…' : s;
}
function buildLines(who, meet) {
const w = who && typeof who === 'object' ? who : {};
const m = meet && typeof meet === 'object' ? meet : {};
const hasWho = !!(w.selfIntro && String(w.selfIntro).trim()) || !!(w.partnerExpect && String(w.partnerExpect).trim());
const hasMeet =
!!(m.relationshipStatus && String(m.relationshipStatus).trim()) || !!(m.meetingGoal && String(m.meetingGoal).trim());
if (!hasWho && !hasMeet) {
return { empty: true, lines: [] };
}
const lines = [];
lines.push('下面几条结合你在「我是谁」「约会准备」中的填写整理,方便你出门前快速过一遍思路。');
if (m.relationshipStatus) {
const sn = trimSnippet(m.relationshipStatus, 56);
lines.push(
`关系阶段你描述为「${sn}」:聊天节奏与对方投入程度尽量匹配这一阶段,不必急于定义关系,也避免过度冷淡让对方不安。`,
);
}
if (m.meetingGoal) {
const sn = trimSnippet(m.meetingGoal, 72);
lines.push(
`本次见面你希望「${sn}」:可以准备 12 个轻松的小话题或问题,自然带入即可,不必像完成任务一样逐项核对。`,
);
}
if (w.selfIntro) {
lines.push(
'「我是谁」里你写到的特点:今天选 12 点最想被对方感受到的,用小事或简短例子带过即可,留一点空间让对方慢慢了解你。',
);
}
if (w.partnerExpect) {
lines.push(
'关于对另一半的期待:更适合在互动中观察对方的言行是否合拍,用好奇和分享代替盘问,减少「考察感」。',
);
}
lines.push('见面时多倾听、适当复述对方话里的关键词,让对方感到被听懂;适当留白比一直找话题更重要。');
lines.push('若短暂冷场,可以从环境、共同经历或轻松八卦切入,不必为「必须有趣」而焦虑。');
lines.push('结束前可以自然聊聊今天的感受、是否愉快,再顺势看下次是否方便再约,不给对方过大压力。');
return { empty: false, lines };
}
export default {
name: 'XixiDatingAdvice',
props: {
refreshTick: {
type: Number,
default: 0,
},
},
data() {
return {
todayLabel: formatToday(),
emptyHint: true,
lines: [],
};
},
watch: {
refreshTick() {
this.regenerate();
},
},
mounted() {
this.regenerate();
},
methods: {
regenerate() {
this.todayLabel = formatToday();
let who = {};
let meet = {};
try {
const rawW = uni.getStorageSync(KEY_WHO);
if (rawW && typeof rawW === 'object') who = rawW;
} catch (e) {
/* ignore */
}
try {
const rawM = uni.getStorageSync(KEY_MEET);
if (rawM && typeof rawM === 'object') meet = rawM;
} catch (e) {
/* ignore */
}
const { empty, lines } = buildLines(who, meet);
this.emptyHint = empty;
this.lines = lines;
},
},
};
</script>
<style scoped>
.panel-scroll {
height: 100%;
box-sizing: border-box;
padding: 24rpx 28rpx 40rpx;
}
.head-card {
background: #fff;
border-radius: 20rpx;
padding: 28rpx;
margin-bottom: 24rpx;
box-shadow: 0 4rpx 24rpx rgba(233, 30, 140, 0.06);
}
.head-title {
display: block;
font-size: 34rpx;
font-weight: 600;
color: #333;
margin-bottom: 12rpx;
}
.head-date {
display: block;
font-size: 26rpx;
color: #e91e8c;
margin-bottom: 16rpx;
}
.head-note {
display: block;
font-size: 24rpx;
color: #888;
line-height: 1.55;
}
.empty-wrap {
background: #fff;
border-radius: 20rpx;
padding: 48rpx 32rpx;
text-align: center;
box-shadow: 0 4rpx 24rpx rgba(233, 30, 140, 0.04);
}
.empty-title {
display: block;
font-size: 30rpx;
font-weight: 600;
color: #333;
margin-bottom: 16rpx;
}
.empty-desc {
font-size: 26rpx;
color: #888;
line-height: 1.6;
}
.list {
display: flex;
flex-direction: column;
}
.advice-item {
display: flex;
flex-direction: row;
align-items: flex-start;
background: #fff;
border-radius: 20rpx;
padding: 24rpx 24rpx 24rpx 20rpx;
margin-bottom: 20rpx;
box-shadow: 0 4rpx 20rpx rgba(233, 30, 140, 0.06);
}
.advice-item:last-child {
margin-bottom: 0;
}
.advice-index {
flex-shrink: 0;
width: 44rpx;
height: 44rpx;
line-height: 44rpx;
text-align: center;
font-size: 24rpx;
font-weight: 600;
color: #fff;
background: linear-gradient(135deg, #ff6b9d, #e91e8c);
border-radius: 12rpx;
margin-right: 16rpx;
margin-top: 4rpx;
}
.advice-text {
flex: 1;
font-size: 28rpx;
color: #444;
line-height: 1.65;
}
</style>

View File

@@ -0,0 +1,111 @@
<template>
<scroll-view class="panel-scroll" scroll-y>
<view v-if="!items.length" class="empty-wrap">
<text class="empty-title">暂无诊断结果</text>
<text class="empty-desc">请在幸福相遇中完成录音并点击分析录音</text>
</view>
<view v-for="(row, idx) in items" :key="row.id" class="diag-card">
<view class="diag-head">
<text class="diag-label">诊断 {{ items.length - idx }}</text>
<text class="diag-time">{{ formatDate(row.createdAt) }}</text>
</view>
<text class="diag-body">{{ row.analysisText }}</text>
</view>
</scroll-view>
</template>
<script>
const KEY = 'xixi_recordings';
export default {
name: 'XixiDiagnosis',
props: {
refreshTick: {
type: Number,
default: 0,
},
},
data() {
return {
items: [],
};
},
watch: {
refreshTick() {
this.load();
},
},
mounted() {
this.load();
},
methods: {
load() {
try {
const list = uni.getStorageSync(KEY);
const arr = Array.isArray(list) ? list : [];
this.items = arr.filter((r) => r && r.analysisText);
} catch (e) {
this.items = [];
}
},
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())}`;
},
},
};
</script>
<style scoped>
.panel-scroll {
height: 100%;
box-sizing: border-box;
padding: 24rpx 28rpx 32rpx;
}
.empty-wrap {
padding: 80rpx 32rpx;
text-align: center;
}
.empty-title {
display: block;
font-size: 30rpx;
font-weight: 600;
color: #333;
margin-bottom: 16rpx;
}
.empty-desc {
font-size: 26rpx;
color: #888;
line-height: 1.6;
}
.diag-card {
background: #fff;
border-radius: 20rpx;
padding: 28rpx;
margin-bottom: 24rpx;
box-shadow: 0 4rpx 24rpx rgba(233, 30, 140, 0.08);
border-left: 8rpx solid #ff6b9d;
}
.diag-head {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
}
.diag-label {
font-size: 28rpx;
font-weight: 600;
color: #e91e8c;
}
.diag-time {
font-size: 24rpx;
color: #999;
}
.diag-body {
font-size: 28rpx;
color: #444;
line-height: 1.65;
white-space: pre-wrap;
}
</style>

View File

@@ -0,0 +1,108 @@
<template>
<scroll-view class="panel-scroll" scroll-y>
<view class="card">
<text class="label">现在两个人的关系</text>
<textarea
class="area area-sm"
v-model="local.relationshipStatus"
placeholder="例如:刚认识 / 暧昧期 / 已确定关系 / 异地恋…"
maxlength="800"
:show-confirm-bar="false"
/>
</view>
<view class="card">
<text class="label">本次见面要达到的结果</text>
<textarea
class="area"
v-model="local.meetingGoal"
placeholder="例如:更了解对方家庭观、约定下次约会、化解上次误会…"
maxlength="2000"
:show-confirm-bar="false"
/>
</view>
<button class="btn-save" type="primary" @click="save">保存</button>
</scroll-view>
</template>
<script>
const KEY = 'xixi_first_meeting';
export default {
name: 'XixiFirstMeeting',
data() {
return {
local: {
relationshipStatus: '',
meetingGoal: '',
},
};
},
mounted() {
this.load();
},
methods: {
load() {
try {
const raw = uni.getStorageSync(KEY);
if (raw && typeof raw === 'object') {
this.local.relationshipStatus = raw.relationshipStatus || '';
this.local.meetingGoal = raw.meetingGoal || '';
}
} catch (e) {
/* ignore */
}
},
save() {
try {
uni.setStorageSync(KEY, { ...this.local });
uni.showToast({ title: '已保存', icon: 'success' });
this.$emit('saved');
} catch (e) {
uni.showToast({ title: '保存失败', icon: 'none' });
}
},
},
};
</script>
<style scoped>
.panel-scroll {
height: 100%;
box-sizing: border-box;
padding: 24rpx 28rpx 32rpx;
}
.card {
background: #fff;
border-radius: 20rpx;
padding: 28rpx;
margin-bottom: 24rpx;
box-shadow: 0 4rpx 24rpx rgba(233, 30, 140, 0.06);
}
.label {
display: block;
font-size: 28rpx;
font-weight: 600;
color: #333;
margin-bottom: 16rpx;
}
.area {
width: 100%;
min-height: 200rpx;
font-size: 28rpx;
line-height: 1.6;
color: #333;
box-sizing: border-box;
padding: 16rpx;
background: #fff8fa;
border-radius: 12rpx;
}
.area-sm {
min-height: 120rpx;
}
.btn-save {
margin-top: 12rpx;
border-radius: 999rpx;
background: linear-gradient(135deg, #ff6b9d, #e91e8c);
border: none;
}
</style>

View 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 ? '你已填写「我是谁」,建议在真实对话中自然带出其中 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>

View File

@@ -0,0 +1,105 @@
<template>
<scroll-view class="panel-scroll" scroll-y>
<view class="card">
<text class="label">我是谁恋爱相关的自我描述</text>
<textarea
class="area"
v-model="local.selfIntro"
placeholder="例如:我的性格、价值观、在关系里看重什么…"
maxlength="2000"
:show-confirm-bar="false"
/>
</view>
<view class="card">
<text class="label">对另一半的要求</text>
<textarea
class="area"
v-model="local.partnerExpect"
placeholder="例如:希望对方具备的特质、相处方式、底线与期待…"
maxlength="2000"
:show-confirm-bar="false"
/>
</view>
<button class="btn-save" type="primary" @click="save">保存</button>
</scroll-view>
</template>
<script>
const KEY = 'xixi_who_am_i';
export default {
name: 'XixiWhoAmI',
data() {
return {
local: {
selfIntro: '',
partnerExpect: '',
},
};
},
mounted() {
this.load();
},
methods: {
load() {
try {
const raw = uni.getStorageSync(KEY);
if (raw && typeof raw === 'object') {
this.local.selfIntro = raw.selfIntro || '';
this.local.partnerExpect = raw.partnerExpect || '';
}
} catch (e) {
/* ignore */
}
},
save() {
try {
uni.setStorageSync(KEY, { ...this.local });
uni.showToast({ title: '已保存', icon: 'success' });
this.$emit('saved');
} catch (e) {
uni.showToast({ title: '保存失败', icon: 'none' });
}
},
},
};
</script>
<style scoped>
.panel-scroll {
height: 100%;
box-sizing: border-box;
padding: 24rpx 28rpx 32rpx;
}
.card {
background: #fff;
border-radius: 20rpx;
padding: 28rpx;
margin-bottom: 24rpx;
box-shadow: 0 4rpx 24rpx rgba(233, 30, 140, 0.06);
}
.label {
display: block;
font-size: 28rpx;
font-weight: 600;
color: #333;
margin-bottom: 16rpx;
}
.area {
width: 100%;
min-height: 220rpx;
font-size: 28rpx;
line-height: 1.6;
color: #333;
box-sizing: border-box;
padding: 16rpx;
background: #fff8fa;
border-radius: 12rpx;
}
.btn-save {
margin-top: 12rpx;
border-radius: 999rpx;
background: linear-gradient(135deg, #ff6b9d, #e91e8c);
border: none;
}
</style>

View File

@@ -45,6 +45,19 @@
"navigationStyle": "custom"
}
},
{
"path": "pages/xixi/xixi",
"style": {
"navigationStyle": "custom"
},
"componentPlaceholder": {
"XixiWhoAmI": "pages-subpackage/xixi/components/xixi-who-am-i",
"XixiFirstMeeting": "pages-subpackage/xixi/components/xixi-first-meeting",
"XixiRecord": "pages-subpackage/xixi/components/xixi-record",
"XixiDiagnosis": "pages-subpackage/xixi/components/xixi-diagnosis",
"XixiDatingAdvice": "pages-subpackage/xixi/components/xixi-dating-advice"
}
},
// #ifdef APP
{
"path": "uni_modules/uni-upgrade-center-app/pages/upgrade-popup",
@@ -439,6 +452,12 @@
"selectedIconPath": "static/tabbar/ai_analysis_active.png",
"text": "AI分析"
},
{
"pagePath": "pages/xixi/xixi",
"iconPath": "static/tabbar/insight.png",
"selectedIconPath": "static/tabbar/insight_active.png",
"text": "喜喜"
},
{
"pagePath": "pages/ucenter/ucenter",
"iconPath": "static/tabbar/me.png",

View File

@@ -2500,6 +2500,7 @@
'pages/furniture_reception/furniture_reception': '/reception',
'pages/ai_qa/ai_qa': '/ai-qa',
'pages/ai_analysis/ai_analysis': '/ai-analysis',
'pages/xixi/xixi': '/xixi',
'pages/ucenter/ucenter': '/profile'
};

View File

@@ -987,6 +987,7 @@ import ServiceListFurniture from "./serviceListFurniture.vue";
'pages/furniture_reception/furniture_reception': '/reception',
'pages/ai_qa/ai_qa': '/ai-qa',
'pages/ai_analysis/ai_analysis': '/ai-analysis',
'pages/xixi/xixi': '/xixi',
'pages/ucenter/ucenter': '/profile'
};

View File

@@ -375,7 +375,7 @@
this[item.event]();
} else if (item.to) {
// 如果是 tabBar 页面,使用 switchTab
const tabBarPages = ['/pages/furniture_reception/furniture_reception', '/pages/furniture_customer/furniture_customer', '/pages/ai_analysis/ai_analysis', '/pages/ucenter/ucenter']
const tabBarPages = ['/pages/furniture_reception/furniture_reception', '/pages/furniture_customer/furniture_customer', '/pages/ai_analysis/ai_analysis', '/pages/xixi/xixi', '/pages/ucenter/ucenter']
if (tabBarPages.includes(item.to)) {
uni.switchTab({
url: item.to
@@ -702,6 +702,7 @@
'pages/furniture_reception/furniture_reception': '/reception',
'pages/ai_qa/ai_qa': '/ai-qa',
'pages/ai_analysis/ai_analysis': '/ai-analysis',
'pages/xixi/xixi': '/xixi',
'pages/ucenter/ucenter': '/profile'
};

502
pages/xixi/xixi.vue Normal file
View File

@@ -0,0 +1,502 @@
<template>
<view class="page">
<!-- #ifdef APP -->
<statusBar></statusBar>
<!-- #endif -->
<uni-nav-bar
:fixed="true"
:statusBar="true"
:border="false"
:title="navTitle"
:leftIcon="viewMode === 'detail' ? 'left' : ''"
@clickLeft="onNavLeftClick"
color="#333"
backgroundColor="#FFFFFF"
/>
<view class="content">
<!-- 首页 AI 分析一致的功能分区 + 卡片网格 -->
<scroll-view
v-show="viewMode === 'home'"
class="content-scroll"
scroll-y="true"
:style="{ top: computedContentTop || contentTop, bottom: contentBottom || '96rpx' }"
>
<view class="category-section two-column">
<view class="category-header">
<text class="category-icon">💕</text>
<text class="category-title">恋爱自画像</text>
</view>
<view class="function-grid">
<view class="function-item" @click="openDetail('who')">
<view class="function-icon">🪞</view>
<text class="function-name">我是谁</text>
<text class="function-desc">自我描述与对另一半的期待</text>
</view>
<view class="function-item" @click="openDetail('meet')">
<view class="function-icon">🌷</view>
<text class="function-name">约会准备</text>
<text class="function-desc">关系阶段与本次见面目标</text>
</view>
</view>
<view class="advice-full" @click="openDetail('advice')">
<view class="advice-full-inner">
<view class="function-icon advice-full-icon">💡</view>
<view class="advice-full-texts">
<text class="function-name">约会建议</text>
<text class="function-desc">根据我是谁约会准备生成今日参考</text>
</view>
</view>
</view>
</view>
<view class="category-section two-column">
<view class="category-header">
<text class="category-icon">💬</text>
<text class="category-title">相处与复盘</text>
</view>
<view class="function-grid">
<view class="function-item" @click="openDetail('record')">
<view class="function-icon">🎉</view>
<text class="function-name">幸福相遇</text>
<text class="function-desc">录音记录与智能分析</text>
</view>
<view class="function-item" @click="openDetail('diag')">
<view class="function-icon">📋</view>
<text class="function-name">诊断结果</text>
<text class="function-desc">查看历史诊断与建议</text>
</view>
</view>
</view>
</scroll-view>
<!-- 详情固定区域 + 子组件自带 scroll-view避免双层 scroll 嵌套 -->
<view
v-show="viewMode === 'detail'"
class="detail-wrap"
:style="{ top: computedContentTop || contentTop, bottom: contentBottom || '96rpx' }"
>
<view v-show="detailKey === 'who'" class="detail-panel">
<XixiWhoAmI @saved="onFormSaved" />
</view>
<view v-show="detailKey === 'meet'" class="detail-panel">
<XixiFirstMeeting @saved="onFormSaved" />
</view>
<view v-show="detailKey === 'record'" class="detail-panel">
<XixiRecord @recordings-changed="onRecordingsChanged" />
</view>
<view v-show="detailKey === 'diag'" class="detail-panel">
<XixiDiagnosis :refresh-tick="diagTick" />
</view>
<view v-show="detailKey === 'advice'" class="detail-panel">
<XixiDatingAdvice :refresh-tick="adviceRefreshTick" />
</view>
</view>
</view>
</view>
</template>
<script>
// #ifdef APP
import statusBar from '@/uni_modules/uni-nav-bar/components/uni-nav-bar/uni-status-bar';
// #endif
// 微信小程序:由 pages.json 的 componentPlaceholder 注册分包组件(分包异步化)
// H5 / App 等:须显式 import
// #ifndef MP-WEIXIN
import XixiWhoAmI from '@/pages-subpackage/xixi/components/xixi-who-am-i.vue';
import XixiFirstMeeting from '@/pages-subpackage/xixi/components/xixi-first-meeting.vue';
import XixiRecord from '@/pages-subpackage/xixi/components/xixi-record.vue';
import XixiDiagnosis from '@/pages-subpackage/xixi/components/xixi-diagnosis.vue';
import XixiDatingAdvice from '@/pages-subpackage/xixi/components/xixi-dating-advice.vue';
// #endif
function getSystemInfo() {
// #ifdef MP-WEIXIN
if (typeof wx !== 'undefined' && wx.getWindowInfo) {
try {
const windowInfo = wx.getWindowInfo();
return {
statusBarHeight: windowInfo.statusBarHeight || 20,
windowWidth: windowInfo.windowWidth,
windowHeight: windowInfo.windowHeight,
};
} catch (e) {
console.warn('[Xixi] wx.getWindowInfo failed:', e);
}
}
// #endif
try {
const systemInfo = uni.getSystemInfoSync();
return {
statusBarHeight: systemInfo.statusBarHeight || 20,
windowWidth: systemInfo.windowWidth,
windowHeight: systemInfo.windowHeight,
};
} catch (e) {
return {
statusBarHeight: 20,
windowWidth: 375,
windowHeight: 667,
};
}
}
const DETAIL_LABELS = {
who: '我是谁',
meet: '约会准备',
record: '幸福相遇',
diag: '诊断结果',
advice: '约会建议',
};
export default {
components: {
// #ifdef APP
statusBar,
// #endif
// #ifndef MP-WEIXIN
XixiWhoAmI,
XixiFirstMeeting,
XixiRecord,
XixiDiagnosis,
XixiDatingAdvice,
// #endif
},
data() {
const systemInfo = getSystemInfo();
const statusBarHeightRpx = systemInfo.statusBarHeight * 2;
const navbarHeightRpx = 44 * 2;
const totalNavbarHeight = statusBarHeightRpx + navbarHeightRpx;
return {
viewMode: 'home',
detailKey: null,
contentTop: totalNavbarHeight + 'rpx',
contentBottom: '96rpx',
diagTick: 0,
adviceRefreshTick: 0,
};
},
computed: {
computedContentTop() {
const systemInfo = getSystemInfo();
const statusBarHeightRpx = systemInfo.statusBarHeight * 2;
const navbarHeightRpx = 44 * 2;
return statusBarHeightRpx + navbarHeightRpx + 'rpx';
},
navTitle() {
if (this.viewMode === 'detail' && this.detailKey && DETAIL_LABELS[this.detailKey]) {
return DETAIL_LABELS[this.detailKey];
}
return '喜喜';
},
},
onLoad() {
this.updateNavbarPosition();
// #ifdef MP-WEIXIN
setTimeout(() => this.preloadSubpackage(), 100);
// #endif
},
onShow() {
this.diagTick += 1;
this.adviceRefreshTick += 1;
this.$nextTick(() => {
this.updateNavbarPosition();
setTimeout(() => this.updateNavbarPosition(), 50);
});
},
onReady() {
this.$nextTick(() => {
const query = uni.createSelectorQuery().in(this);
query
.select('.uni-navbar__content')
.boundingClientRect((data) => {
if (data && data.height) {
const adjustedHeight = Math.max(data.height * 2 - 4, 0);
this.$set(this, 'contentTop', adjustedHeight + 'rpx');
} else {
const systemInfo = getSystemInfo();
const total =
systemInfo.statusBarHeight * 2 + 44 * 2;
this.$set(this, 'contentTop', Math.max(total - 4, 0) + 'rpx');
}
})
.exec();
const queryTabbar = uni.createSelectorQuery().in(this);
queryTabbar
.select('.tabbar')
.boundingClientRect((tabbarData) => {
if (tabbarData && tabbarData.height) {
const adjustedBottom = Math.max(tabbarData.height * 2 - 4, 0);
this.$set(this, 'contentBottom', adjustedBottom + 'rpx');
} else {
this.$set(this, 'contentBottom', '96rpx');
}
})
.exec();
setTimeout(() => {
const q2 = uni.createSelectorQuery().in(this);
q2
.select('.uni-navbar__content')
.boundingClientRect((data) => {
if (data && data.height) {
const adjustedHeight = Math.max(data.height * 2 - 4, 0);
this.$set(this, 'contentTop', adjustedHeight + 'rpx');
}
})
.exec();
const qTab2 = uni.createSelectorQuery().in(this);
qTab2
.select('.tabbar')
.boundingClientRect((tabbarData) => {
if (tabbarData && tabbarData.height) {
const adjustedBottom = Math.max(tabbarData.height * 2 - 4, 0);
this.$set(this, 'contentBottom', adjustedBottom + 'rpx');
}
})
.exec();
}, 200);
});
},
methods: {
// #ifdef MP-WEIXIN
preloadSubpackage() {
if (typeof wx !== 'undefined' && wx.loadSubpackage) {
wx.loadSubpackage({
name: 'pages-subpackage',
success: () => console.log('[Xixi] subpackage loaded'),
fail: (err) => console.warn('[Xixi] subpackage preload:', err),
});
}
},
// #endif
updateNavbarPosition() {
const systemInfo = getSystemInfo();
const totalNavbarHeight = systemInfo.statusBarHeight * 2 + 44 * 2;
this.$set(this, 'contentTop', totalNavbarHeight + 'rpx');
// #ifdef MP-WEIXIN
this.$forceUpdate();
// #endif
},
onNavLeftClick() {
if (this.viewMode === 'detail') {
this.backHome();
return;
}
const pages = getCurrentPages();
if (pages.length <= 1) {
return;
}
uni.navigateBack({
fail: (err) => console.error('[Xixi] navigateBack:', err),
});
},
openDetail(key) {
this.detailKey = key;
this.viewMode = 'detail';
if (key === 'diag' || key === 'record') {
this.diagTick += 1;
}
if (key === 'advice') {
this.adviceRefreshTick += 1;
}
},
backHome() {
this.viewMode = 'home';
this.detailKey = null;
},
onFormSaved() {},
onRecordingsChanged() {
this.diagTick += 1;
},
},
};
</script>
<style scoped>
page {
background-color: #f5f5f5;
}
.page {
min-height: 100vh;
background-color: #f5f5f5;
}
.content {
padding-top: 0 !important;
margin-top: 0 !important;
position: relative;
min-height: 100vh;
top: 0;
}
::v-deep .uni-navbar__placeholder {
display: none !important;
height: 0 !important;
}
::v-deep .uni-navbar__placeholder-view {
display: none !important;
height: 0 !important;
}
::v-deep [class*='tabbar'][class*='placeholder'] {
display: none !important;
height: 0 !important;
}
::v-deep [class*='safe-area'][class*='bottom'],
::v-deep [class*='bottom'][class*='safe'] {
display: none !important;
height: 0 !important;
}
.content-scroll {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
padding: 0 !important;
padding-bottom: 0 !important;
margin: 0 !important;
box-sizing: border-box;
}
.detail-wrap {
position: absolute;
left: 0;
right: 0;
overflow: hidden;
box-sizing: border-box;
background-color: #f5f5f5;
}
.detail-panel {
height: 100%;
width: 100%;
box-sizing: border-box;
}
.category-section {
margin: 20rpx 30rpx;
background: #ffffff;
border-radius: 16rpx;
overflow: hidden;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
}
.category-section:first-child {
margin-top: 0;
}
.category-section:last-child {
margin-bottom: 0;
}
.category-header {
display: flex;
align-items: center;
padding: 30rpx;
background: #ffffff;
border-bottom: 1rpx solid #f0f0f0;
}
.category-icon {
font-size: 32rpx;
margin-right: 20rpx;
}
.category-title {
font-size: 32rpx;
font-weight: 500;
color: #333;
}
.function-grid {
display: flex;
flex-wrap: wrap;
padding: 10rpx;
}
.two-column .function-item {
flex: 1;
background: #ffffff;
border-radius: 12rpx;
padding: 24rpx 16rpx;
margin: 0 5rpx;
text-align: center;
box-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.04);
transition: all 0.3s ease;
border: 1rpx solid #f0f0f0;
}
.function-item:active {
transform: scale(0.95);
box-shadow: 0 1rpx 2rpx rgba(0, 0, 0, 0.1);
}
.function-icon {
font-size: 48rpx;
margin-bottom: 12rpx;
}
.function-name {
display: block;
font-size: 28rpx;
font-weight: 500;
color: #333;
margin-bottom: 8rpx;
}
.function-desc {
display: block;
font-size: 24rpx;
color: #666;
line-height: 1.4;
}
.advice-full {
padding: 0 15rpx 18rpx;
box-sizing: border-box;
}
.advice-full-inner {
display: flex;
flex-direction: row;
align-items: center;
background: linear-gradient(135deg, #fff8fa 0%, #ffffff 100%);
border-radius: 12rpx;
padding: 24rpx 20rpx;
margin: 0 5rpx;
border: 1rpx solid #f5d0e0;
box-shadow: 0 2rpx 8rpx rgba(233, 30, 140, 0.08);
}
.advice-full:active .advice-full-inner {
transform: scale(0.98);
opacity: 0.96;
}
.advice-full-icon {
margin-bottom: 0;
margin-right: 20rpx;
flex-shrink: 0;
}
.advice-full-texts {
flex: 1;
min-width: 0;
text-align: left;
}
.advice-full-texts .function-name,
.advice-full-texts .function-desc {
text-align: left;
}
</style>