增加喜喜 概念框架
This commit is contained in:
1
App.vue
1
App.vue
@@ -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'
|
||||
};
|
||||
|
||||
|
||||
235
pages-subpackage/xixi/components/xixi-dating-advice.vue
Normal file
235
pages-subpackage/xixi/components/xixi-dating-advice.vue
Normal 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}」:可以准备 1~2 个轻松的小话题或问题,自然带入即可,不必像完成任务一样逐项核对。`,
|
||||
);
|
||||
}
|
||||
|
||||
if (w.selfIntro) {
|
||||
lines.push(
|
||||
'「我是谁」里你写到的特点:今天选 1~2 点最想被对方感受到的,用小事或简短例子带过即可,留一点空间让对方慢慢了解你。',
|
||||
);
|
||||
}
|
||||
|
||||
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>
|
||||
111
pages-subpackage/xixi/components/xixi-diagnosis.vue
Normal file
111
pages-subpackage/xixi/components/xixi-diagnosis.vue
Normal 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>
|
||||
108
pages-subpackage/xixi/components/xixi-first-meeting.vue
Normal file
108
pages-subpackage/xixi/components/xixi-first-meeting.vue
Normal 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>
|
||||
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>
|
||||
105
pages-subpackage/xixi/components/xixi-who-am-i.vue
Normal file
105
pages-subpackage/xixi/components/xixi-who-am-i.vue
Normal 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>
|
||||
19
pages.json
19
pages.json
@@ -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",
|
||||
|
||||
@@ -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'
|
||||
};
|
||||
|
||||
|
||||
@@ -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'
|
||||
};
|
||||
|
||||
|
||||
@@ -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
502
pages/xixi/xixi.vue
Normal 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>
|
||||
Reference in New Issue
Block a user