feat(tongji): 血糖页 AI 咨询默认按住说话,松手发送、上滑取消
- 默认整条“按住 说话”,松手直接发给 AI,不用再点发送;按住时手指上滑进入取消状态(按钮和浮层变红) - 说话时浮层实时显示识别出的文字和声波动画 - 右侧键盘图标切到文字输入(自动聚焦),有内容才出现发送按钮;麦克风图标切回语音;不支持语音的环境直接用文字 - 回答卡片移到输入栏上方,先显示“你问”方便核对语音识别结果 - 按住说话时暂时隐藏 2d 画布趋势图(层级高于普通元素会盖住浮层),松手后重绘
This commit is contained in:
@@ -59,6 +59,7 @@ const ICON_PATHS = {
|
||||
moon: '<path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"/>',
|
||||
sunset: '<path d="M12 10V2M18.364 5.636l-2.12 2.12M5.636 18.364l2.12-2.12M22 18h-3M5 18H2M18.364 18.364l-2.12-2.12M5.636 5.636l2.12 2.12M16 12a4 4 0 1 1-8 0 4 4 0 0 1 8 0Z"/>',
|
||||
mic: '<path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" x2="12" y1="19" y2="22"/>',
|
||||
keyboard: '<rect width="20" height="16" x="2" y="4" rx="2"/><path d="M6 8h.01M10 8h.01M14 8h.01M18 8h.01M8 12h.01M12 12h.01M16 12h.01M7 16h10"/>',
|
||||
camera: '<path d="M14.5 4h-5L7 7H4a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2h-3l-2.5-3z"/><circle cx="12" cy="13" r="3"/>',
|
||||
bulb: '<path d="M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1.3.5 2.6 1.5 3.5.8.8 1.3 1.5 1.5 2.5"/><path d="M9 18h6"/><path d="M10 22h4"/>',
|
||||
leaf: '<path d="M11 20A7 7 0 0 1 9.8 6.1C15.5 5 17 4.48 19 2c1 2 2 4.18 2 8 0 5.5-4.78 10-10 10Z"/><path d="M2 21c0-3 1.85-5.36 5.08-6"/>',
|
||||
|
||||
@@ -82,16 +82,16 @@
|
||||
</view>
|
||||
<view class="st-chart-area" @click="onChartTap">
|
||||
<canvas
|
||||
v-if="!recordOverlayOpen"
|
||||
v-if="!chartHidden"
|
||||
id="bloodWaveChart"
|
||||
type="2d"
|
||||
class="st-chart-canvas"
|
||||
@click.stop="onChartTap"
|
||||
/>
|
||||
<view v-if="recordOverlayOpen" class="st-chart-placeholder">
|
||||
<view v-if="chartHidden" class="st-chart-placeholder">
|
||||
<text>图表已暂停显示</text>
|
||||
</view>
|
||||
<view v-if="!chartHasDisplayData && !loading && !recordOverlayOpen" class="st-chart-empty">
|
||||
<view v-if="!chartHasDisplayData && !loading && !chartHidden" class="st-chart-empty">
|
||||
<text>当前时间范围暂无血糖数据</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -190,53 +190,13 @@
|
||||
<TongjiIcon name="sparkles" size="sm" color="#ffffff" />
|
||||
</view>
|
||||
|
||||
<!-- 底部浮动 AI 输入栏 -->
|
||||
<!-- 底部浮动 AI 输入栏:默认按住说话(松手发送、上滑取消),右侧图标切换文字输入 -->
|
||||
<view v-if="diagnosisId && !floatAskClosed" class="st-float-ask">
|
||||
<view class="st-float-close" @click="floatAskClosed = true">
|
||||
<text class="st-float-close-icon">×</text>
|
||||
</view>
|
||||
<view class="st-float-inner" :class="{ 'is-expanded': dietAiAskExpanded }">
|
||||
<view class="st-float-icon">
|
||||
<TongjiIcon name="sparkles" size="sm" color="#006c49" />
|
||||
<view v-if="lastAskQuestion && (dietAiAsking || dietAiAskResult.advice)" class="st-float-result">
|
||||
<view class="st-float-question">
|
||||
<text class="st-float-question-label">你问</text>
|
||||
<text class="st-float-question-text">{{ lastAskQuestion }}</text>
|
||||
</view>
|
||||
<textarea
|
||||
class="st-float-input"
|
||||
:value="dietAiAskText"
|
||||
placeholder="输入或语音咨询 AI…"
|
||||
placeholder-class="st-float-placeholder"
|
||||
:maxlength="-1"
|
||||
auto-height
|
||||
:show-confirm-bar="false"
|
||||
:disable-default-padding="true"
|
||||
:cursor-spacing="24"
|
||||
confirm-type="send"
|
||||
:confirm-hold="false"
|
||||
@input="onDietAiAskInput"
|
||||
@confirm="askDietAiFood"
|
||||
@focus="onDietAiAskFocus"
|
||||
@blur="onDietAiAskBlur"
|
||||
/>
|
||||
<view
|
||||
class="st-float-mic"
|
||||
:class="{ listening: sttListening || sttHolding, disabled: dietAiAsking || !sttSupported }"
|
||||
@touchstart.stop.prevent="onDietAiSpeechStart"
|
||||
@touchend.stop.prevent="onDietAiSpeechEnd"
|
||||
@touchcancel.stop.prevent="onDietAiSpeechEnd"
|
||||
>
|
||||
<TongjiIcon name="mic" size="sm" :color="(sttListening || sttHolding) ? '#ffffff' : '#006c49'" />
|
||||
</view>
|
||||
<view
|
||||
class="st-float-send"
|
||||
:class="{ disabled: dietAiAsking }"
|
||||
@click="askDietAiFood"
|
||||
>
|
||||
<TongjiIcon name="send" size="sm" color="#ffffff" />
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="sttHolding && !dietAiAsking" class="st-float-hold-hint">
|
||||
<text>{{ sttListening ? '松手结束' : '准备录音…' }}</text>
|
||||
</view>
|
||||
<view v-if="dietAiAsking || dietAiAskResult.advice" class="st-float-result">
|
||||
<view v-if="dietAiAskResult.level_label" class="st-float-badge">
|
||||
<text>{{ dietAiAskResult.level_label }}</text>
|
||||
</view>
|
||||
@@ -244,16 +204,68 @@
|
||||
{{ dietAiAskResult.advice || (dietAiAsking ? '正在想…' : '') }}<text v-if="dietAiAsking" class="ai-stream-cursor">▍</text>
|
||||
</text>
|
||||
</view>
|
||||
<view class="st-float-bar">
|
||||
<view class="st-float-close" @click="floatAskClosed = true">
|
||||
<text class="st-float-close-icon">×</text>
|
||||
</view>
|
||||
<view class="st-float-inner" :class="{ 'is-expanded': !voiceMode && dietAiAskExpanded }">
|
||||
<view class="st-float-icon">
|
||||
<TongjiIcon name="sparkles" size="sm" color="#006c49" />
|
||||
</view>
|
||||
<view
|
||||
v-if="voiceMode"
|
||||
class="st-hold-btn"
|
||||
:class="{ holding: sttHolding, cancel: sttHolding && voiceCancel, disabled: dietAiAsking }"
|
||||
@touchstart.stop.prevent="onVoiceTouchStart"
|
||||
@touchmove.stop.prevent="onVoiceTouchMove"
|
||||
@touchend.stop.prevent="onVoiceTouchEnd"
|
||||
@touchcancel.stop.prevent="onVoiceTouchEnd"
|
||||
>
|
||||
<TongjiIcon name="mic" size="sm" :color="sttHolding ? '#ffffff' : '#006c49'" />
|
||||
<text class="st-hold-text">{{ holdBtnText }}</text>
|
||||
</view>
|
||||
<textarea
|
||||
v-else
|
||||
class="st-float-input"
|
||||
:value="dietAiAskText"
|
||||
:focus="askInputFocus"
|
||||
placeholder="问问 AI:能吃什么、吃多少…"
|
||||
placeholder-class="st-float-placeholder"
|
||||
:maxlength="-1"
|
||||
auto-height
|
||||
:show-confirm-bar="false"
|
||||
:disable-default-padding="true"
|
||||
:cursor-spacing="24"
|
||||
confirm-type="send"
|
||||
:confirm-hold="false"
|
||||
@input="onDietAiAskInput"
|
||||
@confirm="sendTextAsk"
|
||||
@focus="onDietAiAskFocus"
|
||||
@blur="onDietAiAskBlur"
|
||||
/>
|
||||
<view
|
||||
v-if="!voiceMode && hasAskText"
|
||||
class="st-float-send"
|
||||
:class="{ disabled: dietAiAsking }"
|
||||
@click="sendTextAsk"
|
||||
>
|
||||
<TongjiIcon name="send" size="sm" color="#ffffff" />
|
||||
</view>
|
||||
<view v-if="sttSupported" class="st-mode-toggle" @click="toggleAskMode">
|
||||
<TongjiIcon :name="voiceMode ? 'keyboard' : 'mic'" size="sm" color="#3c4a42" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 语音长按状态浮层:居中显示,避免被键盘/底栏遮挡 -->
|
||||
<!-- 按住说话浮层:实时显示识别出的文字;手指上滑进入取消状态 -->
|
||||
<view v-if="sttHolding && !dietAiAsking" class="st-voice-overlay">
|
||||
<view class="st-voice-card">
|
||||
<view class="st-voice-icon" :class="{ active: sttListening }">
|
||||
<TongjiIcon name="mic" size="lg" color="#ffffff" />
|
||||
<view class="st-voice-card" :class="{ cancel: voiceCancel }">
|
||||
<view class="st-voice-wave" :class="{ active: sttListening && !voiceCancel }">
|
||||
<view v-for="i in 5" :key="i" class="st-voice-bar" :style="{ animationDelay: i * 0.12 + 's' }" />
|
||||
</view>
|
||||
<text class="st-voice-title">{{ sttListening ? '正在聆听…' : '准备录音…' }}</text>
|
||||
<text class="st-voice-sub">松开手指结束并识别</text>
|
||||
<text class="st-voice-live">{{ voicePartial || (sttListening ? '请说,比如“晚饭能吃西瓜吗”' : '准备录音…') }}</text>
|
||||
<text class="st-voice-sub">{{ voiceCancel ? '松开手指,取消发送' : '松开发送 · 上滑取消' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user