diff --git a/TUICallKit-Vue3/tongji/components/TongjiIcon.vue b/TUICallKit-Vue3/tongji/components/TongjiIcon.vue
index 056aae927..53576727c 100644
--- a/TUICallKit-Vue3/tongji/components/TongjiIcon.vue
+++ b/TUICallKit-Vue3/tongji/components/TongjiIcon.vue
@@ -59,6 +59,7 @@ const ICON_PATHS = {
moon: '',
sunset: '',
mic: '',
+ keyboard: '',
camera: '',
bulb: '',
leaf: '',
diff --git a/TUICallKit-Vue3/tongji/pages/weekly.vue b/TUICallKit-Vue3/tongji/pages/weekly.vue
index 809932519..434ca2a18 100644
--- a/TUICallKit-Vue3/tongji/pages/weekly.vue
+++ b/TUICallKit-Vue3/tongji/pages/weekly.vue
@@ -82,16 +82,16 @@
-
+
图表已暂停显示
-
+
当前时间范围暂无血糖数据
@@ -190,53 +190,13 @@
-
+
-
- ×
-
-
-
-
+
+
+ 你问
+ {{ lastAskQuestion }}
-
-
-
-
-
-
-
-
-
- {{ sttListening ? '松手结束' : '准备录音…' }}
-
-
{{ dietAiAskResult.level_label }}
@@ -244,16 +204,68 @@
{{ dietAiAskResult.advice || (dietAiAsking ? '正在想…' : '') }}▍
+
+
+ ×
+
+
+
+
+
+
+
+ {{ holdBtnText }}
+
+
+
+
+
+
+
+
+
+
-
+
-
-
-
+
+
+
- {{ sttListening ? '正在聆听…' : '准备录音…' }}
- 松开手指结束并识别
+ {{ voicePartial || (sttListening ? '请说,比如“晚饭能吃西瓜吗”' : '准备录音…') }}
+ {{ voiceCancel ? '松开手指,取消发送' : '松开发送 · 上滑取消' }}
@@ -535,6 +547,7 @@ function onDietAiAskFocus() {
}
function onDietAiAskBlur() {
dietAiAskFocused.value = false
+ askInputFocus.value = false
}
/** 聚焦或已有内容时展开为多行输入(内容多则自动增高) */
const dietAiAskExpanded = computed(
@@ -547,6 +560,20 @@ function onKeyboardHeightChange(res) {
keyboardHeight.value = res && res.height ? Math.round(res.height) : 0
}
+/** 按住说话时手指上滑超过这个距离(px)进入取消状态 */
+const VOICE_CANCEL_DY = 60
+
+/** 输入方式:默认按住说话,右侧图标切到文字 */
+const askMode = ref('voice')
+const askInputFocus = ref(false)
+/** 上滑取消:松手后丢弃这次识别结果 */
+const voiceCancel = ref(false)
+/** 说话过程中的实时识别文字 */
+const voicePartial = ref('')
+let voiceStartY = 0
+/** 最近一次提问,显示在回答卡片上,方便核对语音有没有听错 */
+const lastAskQuestion = ref('')
+
const {
sttListening,
sttHolding,
@@ -555,19 +582,73 @@ const {
endHoldSpeech,
stopSpeechToText
} = useSpeechToText({
- onResult(text) {
- const prev = String(dietAiAskText.value || '').trim()
- dietAiAskText.value = prev ? `${prev} ${text}` : text
+ onPartial(text) {
+ voicePartial.value = text
+ },
+ onSettle(text) {
+ if (voiceCancel.value) {
+ voiceCancel.value = false
+ return true
+ }
+ // 没识别到内容:交给默认流程(按得太短不打扰,否则提示没听清)
+ if (!text) return false
+ submitAsk(text)
+ return true
},
showToast: (msg) => showUserToast(msg)
})
-function onDietAiSpeechStart() {
+/** 不支持语音的环境(如开发者工具)直接用文字输入 */
+const voiceMode = computed(() => sttSupported.value && askMode.value === 'voice')
+const hasAskText = computed(() => String(dietAiAskText.value || '').trim().length > 0)
+const holdBtnText = computed(() => {
+ if (dietAiAsking.value) return 'AI 正在回答…'
+ if (!sttHolding.value) return '按住 说话'
+ return voiceCancel.value ? '松开 取消' : '松开 发送'
+})
+
+function submitAsk(text) {
+ const q = String(text || '').trim()
+ if (!q || dietAiAsking.value) return
+ lastAskQuestion.value = q
+ dietAiAskText.value = q
+ askDietAiFood()
+}
+
+function sendTextAsk() {
+ submitAsk(dietAiAskText.value)
+}
+
+function toggleAskMode() {
+ if (askMode.value === 'voice') {
+ askMode.value = 'text'
+ askInputFocus.value = true
+ return
+ }
+ askMode.value = 'voice'
+ askInputFocus.value = false
+ uni.hideKeyboard()
+}
+
+function touchY(e) {
+ const t = (e && e.touches && e.touches[0]) || (e && e.changedTouches && e.changedTouches[0])
+ return t ? Number(t.clientY ?? t.pageY ?? 0) : 0
+}
+
+function onVoiceTouchStart(e) {
if (dietAiAsking.value) return
+ voiceStartY = touchY(e)
+ voiceCancel.value = false
+ voicePartial.value = ''
beginHoldSpeech()
}
-function onDietAiSpeechEnd() {
+function onVoiceTouchMove(e) {
+ if (!sttHolding.value) return
+ voiceCancel.value = voiceStartY - touchY(e) > VOICE_CANCEL_DY
+}
+
+function onVoiceTouchEnd() {
endHoldSpeech()
}
@@ -1812,9 +1893,11 @@ function closeInputForm() {
/** 任一录入弹层打开时需卸载 canvas(小程序原生组件会盖住普通 view)*/
const recordOverlayOpen = computed(() => inputForm.value.visible || phoneGateVisible.value)
+/** 2d 画布层级高于普通元素,会盖住弹层:录入弹窗、授权弹窗、按住说话浮层出现时先隐藏趋势图 */
+const chartHidden = computed(() => recordOverlayOpen.value || sttHolding.value)
-watch(recordOverlayOpen, (open) => {
- if (!open) {
+watch(chartHidden, (hidden) => {
+ if (!hidden) {
resetCanvasCache()
nextTick(() => scheduleChartRedraw())
}
diff --git a/TUICallKit-Vue3/tongji/styles/weekly-stitch.scss b/TUICallKit-Vue3/tongji/styles/weekly-stitch.scss
index 5ed4d09c8..01d612020 100644
--- a/TUICallKit-Vue3/tongji/styles/weekly-stitch.scss
+++ b/TUICallKit-Vue3/tongji/styles/weekly-stitch.scss
@@ -704,7 +704,7 @@
text-align: center;
}
-/* ===== 底部浮动输入 ===== */
+/* ===== 底部浮动 AI 输入:默认按住说话,右侧切换文字 ===== */
.weekly-page .st-float-ask {
position: fixed;
left: var(--st-container-margin);
@@ -713,14 +713,18 @@
z-index: 210;
}
+.weekly-page .st-float-bar {
+ position: relative;
+}
+
.weekly-page .st-float-close {
position: absolute;
- top: -18rpx;
+ top: -14rpx;
right: -6rpx;
- width: 44rpx;
- height: 44rpx;
+ width: 40rpx;
+ height: 40rpx;
border-radius: 50%;
- background: rgba(0, 0, 0, 0.45);
+ background: rgba(22, 29, 25, 0.55);
display: flex;
align-items: center;
justify-content: center;
@@ -729,7 +733,7 @@
.weekly-page .st-float-close-icon {
color: #ffffff;
- font-size: 30rpx;
+ font-size: 28rpx;
line-height: 1;
}
@@ -751,21 +755,21 @@
.weekly-page .st-float-inner {
display: flex;
align-items: center;
- gap: 16rpx;
- padding: 16rpx 16rpx 16rpx 28rpx;
+ gap: 14rpx;
+ padding: 12rpx;
border-radius: 999rpx;
- background: rgba(255, 255, 255, 0.9);
+ background: rgba(255, 255, 255, 0.92);
backdrop-filter: blur(20rpx);
border: 1rpx solid rgba(0, 108, 73, 0.1);
box-shadow: var(--st-shadow-float);
transition: border-radius 0.22s ease, padding 0.22s ease, border-color 0.22s ease;
}
-/* 聚焦/有内容时展开:底部对齐让输入框向上增高,圆角收敛为圆角矩形 */
+/* 文字输入聚焦 / 有内容时展开:底部对齐让输入框向上增高,圆角收敛为圆角矩形 */
.weekly-page .st-float-inner.is-expanded {
align-items: flex-end;
border-radius: 32rpx;
- padding: 18rpx 16rpx 18rpx 32rpx;
+ padding: 16rpx 12rpx 16rpx 16rpx;
border-color: rgba(0, 108, 73, 0.28);
}
@@ -780,6 +784,67 @@
flex-shrink: 0;
}
+/* 按住说话:占满整条,按下变实心主色,上滑取消变红 */
+.weekly-page .st-hold-btn {
+ flex: 1;
+ min-width: 0;
+ height: 80rpx;
+ border-radius: 999rpx;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 12rpx;
+ background: linear-gradient(180deg, #f3faf6 0%, #e2f2e9 100%);
+ border: 1rpx solid rgba(0, 108, 73, 0.16);
+ transition: background 0.18s ease, transform 0.15s ease, box-shadow 0.18s ease;
+}
+
+.weekly-page .st-hold-text {
+ font-size: 30rpx;
+ font-weight: 700;
+ letter-spacing: 4rpx;
+ color: var(--primary);
+ line-height: 1;
+}
+
+.weekly-page .st-hold-btn.holding {
+ background: linear-gradient(180deg, #1f8a5c 0%, #006c49 100%);
+ border-color: transparent;
+ box-shadow: 0 8rpx 24rpx rgba(0, 108, 73, 0.3);
+ transform: scale(0.98);
+}
+
+.weekly-page .st-hold-btn.cancel {
+ background: linear-gradient(180deg, #e5484d 0%, #ba1a1a 100%);
+ box-shadow: 0 8rpx 24rpx rgba(186, 26, 26, 0.3);
+}
+
+.weekly-page .st-hold-btn.holding .st-hold-text {
+ color: #ffffff;
+}
+
+.weekly-page .st-hold-btn.disabled {
+ opacity: 0.55;
+ pointer-events: none;
+}
+
+.weekly-page .st-mode-toggle {
+ width: 72rpx;
+ height: 72rpx;
+ border-radius: 50%;
+ background: var(--surface-container-low);
+ border: 1rpx solid rgba(0, 108, 73, 0.1);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ transition: transform 0.15s ease;
+}
+
+.weekly-page .st-mode-toggle:active {
+ transform: scale(0.92);
+}
+
.weekly-page .st-float-input {
flex: 1;
min-width: 0;
@@ -801,7 +866,7 @@
}
.weekly-page .st-float-placeholder {
- color: rgba(60, 74, 66, 0.6);
+ color: rgba(60, 74, 66, 0.55);
font-size: 26rpx;
line-height: 40rpx;
white-space: nowrap;
@@ -809,106 +874,10 @@
text-overflow: ellipsis;
}
-.weekly-page .st-float-mic {
+.weekly-page .st-float-send {
width: 72rpx;
height: 72rpx;
border-radius: 50%;
- background: rgba(0, 108, 73, 0.1);
- display: flex;
- align-items: center;
- justify-content: center;
- flex-shrink: 0;
- transition: background 0.2s ease, transform 0.15s ease;
-}
-
-.weekly-page .st-float-mic.listening {
- background: var(--primary);
- animation: st-float-mic-pulse 1.2s ease-in-out infinite;
-}
-
-.weekly-page .st-float-mic.disabled {
- opacity: 0.45;
- pointer-events: none;
-}
-
-.weekly-page .st-float-mic:active {
- transform: scale(0.94);
-}
-
-.weekly-page .st-float-hold-hint {
- margin-top: 12rpx;
- text-align: center;
- font-size: 24rpx;
- color: var(--primary);
- line-height: 1.4;
-}
-
-.weekly-page .st-voice-overlay {
- position: fixed;
- left: 0;
- right: 0;
- top: 0;
- bottom: 0;
- z-index: 3000;
- display: flex;
- align-items: center;
- justify-content: center;
- background: rgba(0, 0, 0, 0.32);
- pointer-events: none;
-}
-
-.weekly-page .st-voice-card {
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: 20rpx;
- padding: 56rpx 64rpx;
- border-radius: 32rpx;
- background: rgba(20, 24, 22, 0.86);
- box-shadow: 0 16rpx 48rpx rgba(0, 0, 0, 0.35);
-}
-
-.weekly-page .st-voice-icon {
- width: 120rpx;
- height: 120rpx;
- border-radius: 50%;
- display: flex;
- align-items: center;
- justify-content: center;
- background: linear-gradient(180deg, #2d7340 0%, #006c49 100%);
-}
-
-.weekly-page .st-voice-icon.active {
- animation: st-voice-pulse 1.1s ease-in-out infinite;
-}
-
-@keyframes st-voice-pulse {
- 0%, 100% { box-shadow: 0 0 0 0 rgba(0, 108, 73, 0.45); }
- 50% { box-shadow: 0 0 0 20rpx rgba(0, 108, 73, 0); }
-}
-
-.weekly-page .st-voice-title {
- font-size: 32rpx;
- font-weight: 700;
- color: #ffffff;
- line-height: 1.3;
-}
-
-.weekly-page .st-voice-sub {
- font-size: 24rpx;
- color: rgba(255, 255, 255, 0.7);
- line-height: 1.3;
-}
-
-@keyframes st-float-mic-pulse {
- 0%, 100% { box-shadow: 0 0 0 0 rgba(0, 108, 73, 0.35); }
- 50% { box-shadow: 0 0 0 12rpx rgba(0, 108, 73, 0); }
-}
-
-.weekly-page .st-float-send {
- width: 80rpx;
- height: 80rpx;
- border-radius: 50%;
background: var(--primary);
display: flex;
align-items: center;
@@ -927,13 +896,121 @@
transform: scale(0.95);
}
+/* 按住说话浮层:居中显示,避免被键盘/底栏遮挡;不拦截触摸,手指始终在按钮上 */
+.weekly-page .st-voice-overlay {
+ position: fixed;
+ left: 0;
+ right: 0;
+ top: 0;
+ bottom: 0;
+ z-index: 3000;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: rgba(0, 0, 0, 0.36);
+ pointer-events: none;
+}
+
+.weekly-page .st-voice-card {
+ width: 540rpx;
+ box-sizing: border-box;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 24rpx;
+ padding: 48rpx 40rpx 40rpx;
+ border-radius: 40rpx;
+ background: rgba(20, 24, 22, 0.9);
+ box-shadow: 0 16rpx 48rpx rgba(0, 0, 0, 0.35);
+ transition: background 0.18s ease;
+}
+
+.weekly-page .st-voice-card.cancel {
+ background: rgba(186, 26, 26, 0.92);
+}
+
+.weekly-page .st-voice-wave {
+ height: 72rpx;
+ display: flex;
+ align-items: center;
+ gap: 12rpx;
+}
+
+.weekly-page .st-voice-bar {
+ width: 10rpx;
+ height: 24rpx;
+ border-radius: 6rpx;
+ background: #4edea3;
+}
+
+.weekly-page .st-voice-card.cancel .st-voice-bar {
+ background: rgba(255, 255, 255, 0.85);
+}
+
+.weekly-page .st-voice-wave.active .st-voice-bar {
+ animation: st-voice-wave 0.9s ease-in-out infinite;
+}
+
+@keyframes st-voice-wave {
+ 0%, 100% { transform: scaleY(0.5); }
+ 50% { transform: scaleY(2.4); }
+}
+
+.weekly-page .st-voice-live {
+ min-height: 92rpx;
+ font-size: 32rpx;
+ font-weight: 600;
+ line-height: 1.45;
+ color: #ffffff;
+ text-align: center;
+ display: -webkit-box;
+ -webkit-line-clamp: 3;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+}
+
+.weekly-page .st-voice-sub {
+ font-size: 24rpx;
+ color: rgba(255, 255, 255, 0.72);
+ line-height: 1.3;
+}
+
+/* 回答卡片:在输入栏上方,先显示“你问”方便核对语音识别 */
.weekly-page .st-float-result {
- margin-top: 16rpx;
- padding: 24rpx 32rpx;
- border-radius: var(--st-radius-xl);
- background: rgba(255, 255, 255, 0.95);
+ margin-bottom: 16rpx;
+ max-height: 42vh;
+ overflow-y: auto;
+ padding: 24rpx 28rpx;
+ border-radius: 32rpx;
+ background: rgba(255, 255, 255, 0.97);
border: 1rpx solid var(--outline-variant);
- box-shadow: var(--st-shadow-ambient);
+ box-shadow: var(--st-shadow-float);
+}
+
+.weekly-page .st-float-question {
+ display: flex;
+ align-items: flex-start;
+ gap: 12rpx;
+ margin-bottom: 14rpx;
+}
+
+.weekly-page .st-float-question-label {
+ flex-shrink: 0;
+ padding: 2rpx 12rpx;
+ border-radius: 8rpx;
+ font-size: 22rpx;
+ font-weight: 700;
+ color: var(--on-surface-variant);
+ background: var(--surface-container);
+ line-height: 1.6;
+}
+
+.weekly-page .st-float-question-text {
+ flex: 1;
+ min-width: 0;
+ font-size: 26rpx;
+ line-height: 1.6;
+ color: var(--on-surface-variant);
}
.weekly-page .st-float-badge {
@@ -950,7 +1027,7 @@
.weekly-page .st-float-advice {
font-size: 28rpx;
line-height: 1.5;
- color: var(--on-surface-variant);
+ color: var(--on-surface);
}
.weekly-page .ai-stream-cursor {