Compare commits
197
Commits
master-6-1
...
app-8-12
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f48a66b611 | ||
|
|
28cd110dae | ||
|
|
6d9fe1bbf8 | ||
|
|
bc9ad1d3cd | ||
|
|
09d3fcbf82 | ||
|
|
5711ad946f | ||
|
|
d005a0bf3c | ||
|
|
03fe4ddf9d | ||
|
|
25467b9d91 | ||
|
|
cfe4c82c90 | ||
|
|
c3ceb0dd0f | ||
|
|
9add23e019 | ||
|
|
2199887c07 | ||
|
|
56dbd7f115 | ||
|
|
d10f213573 | ||
|
|
a968945057 | ||
|
|
971a627288 | ||
|
|
c37d5abac4 | ||
|
|
ebc463864b | ||
|
|
ab140ab05e | ||
|
|
894f52e875 | ||
|
|
a133d5d85d | ||
|
|
2c238d6599 | ||
|
|
6ce58bcd85 | ||
|
|
1ca87f8d72 | ||
|
|
5bf6ea01f6 | ||
|
|
58a7197d3e | ||
|
|
90be4bf60b | ||
|
|
1c7b22a72f | ||
|
|
16d301f302 | ||
|
|
00ce6d6875 | ||
|
|
bdd8e69ec8 | ||
|
|
8bbd6f7885 | ||
|
|
245c52075d | ||
|
|
c836084fd3 | ||
|
|
a010483bdc | ||
|
|
e20374e5d8 | ||
|
|
037e5ba450 | ||
|
|
d5b0ab4709 | ||
|
|
079e50006d | ||
|
|
2c0b9c5afa | ||
|
|
dd28bba354 | ||
|
|
a797743aa4 | ||
|
|
abb4aced1c | ||
|
|
4ee8a8e98b | ||
|
|
7ddb4882e3 | ||
|
|
c2b7018a22 | ||
|
|
57a7c415a1 | ||
|
|
582c7cf0c6 | ||
|
|
2425fb60d0 | ||
|
|
22a371a733 | ||
|
|
81d6a38e26 | ||
|
|
64d760af1a | ||
|
|
9ba53b6145 | ||
|
|
30cf33c546 | ||
|
|
e1fe818dce | ||
|
|
31312ae975 | ||
|
|
8a6890767e | ||
|
|
d57e60539e | ||
|
|
78fd00fa05 | ||
|
|
fd7e6071a8 | ||
|
|
7192c4db8c | ||
|
|
b02c340348 | ||
|
|
0f21dd4451 | ||
|
|
1ed3e06fdf | ||
|
|
c608145023 | ||
|
|
4f95d9182e | ||
|
|
f662723a25 | ||
|
|
32370df7fe | ||
|
|
62467dc01c | ||
|
|
97fefbae8f | ||
|
|
541fe59ad1 | ||
|
|
b6c5b422c2 | ||
|
|
500038601c | ||
|
|
f6688a740d | ||
|
|
a347e0642c | ||
|
|
13adbac3fd | ||
|
|
333a18c37e | ||
|
|
e3a91a91f7 | ||
|
|
5ca5f06255 | ||
|
|
e01f3e7072 | ||
|
|
ed78803015 | ||
|
|
546138d60e | ||
|
|
fdddfdb3dd | ||
|
|
f5a89b4310 | ||
|
|
a650747fd3 | ||
|
|
46b62229a6 | ||
|
|
0f33f2ad28 | ||
|
|
a677b69e97 | ||
|
|
6432e9990c | ||
|
|
4d6125268a | ||
|
|
7f93cf5480 | ||
|
|
f8f1953a2a | ||
|
|
99eca8b2e5 | ||
|
|
bde396189a | ||
|
|
55e3b5d9e0 | ||
|
|
eb0efba739 | ||
|
|
c7e7022181 | ||
|
|
26e6cc433b | ||
|
|
c22b84cff9 | ||
|
|
4f9486fb3d | ||
|
|
d24b04f9e9 | ||
|
|
465cba3788 | ||
|
|
dc590d37e9 | ||
|
|
d1f587fdd7 | ||
|
|
e8f68101c4 | ||
|
|
cea9392239 | ||
|
|
3ea3c97c42 | ||
|
|
efd058bce5 | ||
|
|
e538dd03a1 | ||
|
|
4a17fe8a0c | ||
|
|
e2a1e62867 | ||
|
|
b6cdd20c56 | ||
|
|
c1be0aa3e5 | ||
|
|
6c87b72ce4 | ||
|
|
e28480af32 | ||
|
|
37b2945c44 | ||
|
|
c9c2b4608f | ||
|
|
e61e7f97fa | ||
|
|
14e5bba9a2 | ||
|
|
6b713a2d6c | ||
|
|
8ae5b4eead | ||
|
|
e5959fd89a | ||
|
|
08249451a7 | ||
|
|
79ae055bb6 | ||
|
|
c0cc23393e | ||
|
|
470ba419e3 | ||
|
|
1c0cd84355 | ||
|
|
f2e2f84e18 | ||
|
|
f7e52bcafc | ||
|
|
2e59e883b8 | ||
|
|
9f1801771e | ||
|
|
1adf8ccf4b | ||
|
|
abc78f4291 | ||
|
|
072c0c3663 | ||
|
|
e9cf65a41c | ||
|
|
af9bd05bb4 | ||
|
|
18f7fb3772 | ||
|
|
29ab45033d | ||
|
|
8a1a9ea0ca | ||
|
|
f8ccd8b0af | ||
|
|
9e019708f2 | ||
|
|
2712c262bd | ||
|
|
bd7002e176 | ||
|
|
0be6d06544 | ||
|
|
7a4973470c | ||
|
|
e79f3190b8 | ||
|
|
1811abc794 | ||
|
|
07abf2abca | ||
|
|
c76bd5416a | ||
|
|
7bb8db4126 | ||
|
|
30b6037752 | ||
|
|
5a235280a1 | ||
|
|
b9d36d8d04 | ||
|
|
f37310ec50 | ||
|
|
a440702c07 | ||
|
|
b9486f32fe | ||
|
|
07018598ad | ||
|
|
c554e08f2e | ||
|
|
e6209c8912 | ||
|
|
e49f806c3a | ||
|
|
b566e60624 | ||
|
|
123e9075dd | ||
|
|
84eef45553 | ||
|
|
9873ac5e87 | ||
|
|
cdf8b1f76d | ||
|
|
b0b78c82ab | ||
|
|
ff61e815d8 | ||
|
|
dd93ffe683 | ||
|
|
ef014e0d9d | ||
|
|
32f8e19bf1 | ||
|
|
26b2cde699 | ||
|
|
5ab3be1fdd | ||
|
|
6b528c34c8 | ||
|
|
2138626519 | ||
|
|
37df208706 | ||
|
|
ec4e0b9f29 | ||
|
|
a3bae1555a | ||
|
|
15e9c486f9 | ||
|
|
eadc82535d | ||
|
|
62f74888f3 | ||
|
|
79719b801e | ||
|
|
07dacbcb28 | ||
|
|
d364fe506a | ||
|
|
860b550180 | ||
|
|
707e4888ad | ||
|
|
b0a8bd83bc | ||
|
|
574d37908a | ||
|
|
62512d30f9 | ||
|
|
1dd5133202 | ||
|
|
51b34a98b5 | ||
|
|
1755185f50 | ||
|
|
b3d3048100 | ||
|
|
a38af5bb13 | ||
|
|
450be5852f | ||
|
|
3c35ab67cd | ||
|
|
7cfa4d269a |
+1
-4
@@ -26,11 +26,8 @@ bin-release/
|
|||||||
/.cursor
|
/.cursor
|
||||||
/.codex
|
/.codex
|
||||||
/.agents
|
/.agents
|
||||||
/.claude
|
|
||||||
/.claude
|
|
||||||
/.claude
|
|
||||||
/.kbctx
|
|
||||||
/server/.spool
|
/server/.spool
|
||||||
/server/.claude
|
/server/.claude
|
||||||
/.spool
|
/.spool
|
||||||
TUICallKit-Vue3/.env
|
TUICallKit-Vue3/.env
|
||||||
|
/.codegraph
|
||||||
|
|||||||
@@ -1,120 +0,0 @@
|
|||||||
alias ll='ls -alhG'
|
|
||||||
|
|
||||||
# Starship prompt
|
|
||||||
eval "$(starship init zsh)"
|
|
||||||
|
|
||||||
# zsh-autosuggestions 自动补全
|
|
||||||
source /opt/homebrew/share/zsh-autosuggestions/zsh-autosuggestions.zsh
|
|
||||||
|
|
||||||
# NVM 懒加载 - 只在第一次使用相关命令时再初始化
|
|
||||||
export NVM_DIR="$HOME/.nvm"
|
|
||||||
_nvm_default_bin() {
|
|
||||||
local default_alias version_dir
|
|
||||||
[ -r "$NVM_DIR/alias/default" ] || return 1
|
|
||||||
|
|
||||||
default_alias="$(<"$NVM_DIR/alias/default")"
|
|
||||||
default_alias="${default_alias#"${default_alias%%[![:space:]]*}"}"
|
|
||||||
default_alias="${default_alias%"${default_alias##*[![:space:]]}"}"
|
|
||||||
|
|
||||||
case "$default_alias" in
|
|
||||||
v[0-9]*)
|
|
||||||
version_dir="$NVM_DIR/versions/node/$default_alias"
|
|
||||||
;;
|
|
||||||
[0-9]*)
|
|
||||||
version_dir="$(ls -d "$NVM_DIR"/versions/node/v"$default_alias".* 2>/dev/null | tail -n 1)"
|
|
||||||
;;
|
|
||||||
node|stable|lts/*)
|
|
||||||
version_dir="$(ls -d "$NVM_DIR"/versions/node/v* 2>/dev/null | sort -V | tail -n 1)"
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
return 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
[ -n "$version_dir" ] && [ -x "$version_dir/bin/node" ] || return 1
|
|
||||||
printf '%s\n' "$version_dir/bin"
|
|
||||||
}
|
|
||||||
if _nvm_default_node_bin="$(_nvm_default_bin)"; then
|
|
||||||
export PATH="$_nvm_default_node_bin:$PATH"
|
|
||||||
fi
|
|
||||||
unset _nvm_default_node_bin
|
|
||||||
_lazy_load_nvm() {
|
|
||||||
unset -f nvm node npm npx pnpm yarn 2>/dev/null
|
|
||||||
[ -s "$NVM_DIR/nvm.sh" ] || return 1
|
|
||||||
\. "$NVM_DIR/nvm.sh"
|
|
||||||
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"
|
|
||||||
hash -r 2>/dev/null
|
|
||||||
}
|
|
||||||
nvm() { _lazy_load_nvm && nvm "$@"; }
|
|
||||||
node() { _lazy_load_nvm && node "$@"; }
|
|
||||||
npm() { _lazy_load_nvm && npm "$@"; }
|
|
||||||
npx() { _lazy_load_nvm && npx "$@"; }
|
|
||||||
pnpm() { _lazy_load_nvm && pnpm "$@"; }
|
|
||||||
yarn() { _lazy_load_nvm && yarn "$@"; }
|
|
||||||
export PATH="/Applications/EServer/bin:$PATH"
|
|
||||||
function EC_start(){
|
|
||||||
/Applications/EasyConnect.app/Contents/Resources/bin/EasyMonitor > /dev/null 2>&1 &
|
|
||||||
/Applications/EasyConnect.app/Contents/MacOS/EasyConnect > /dev/null 2>&1 &
|
|
||||||
open /Applications/EasyConnect.app
|
|
||||||
}
|
|
||||||
|
|
||||||
function EC_kill(){
|
|
||||||
pkill EasyMonitor
|
|
||||||
pkill ECAgent
|
|
||||||
pkill ECAgentProxy
|
|
||||||
pkill EasyConnect
|
|
||||||
}
|
|
||||||
# The following lines have been added by Docker Desktop to enable Docker CLI completions.
|
|
||||||
fpath=(/Users/long/.docker/completions $fpath)
|
|
||||||
autoload -Uz compinit
|
|
||||||
compinit
|
|
||||||
# End of Docker CLI completions
|
|
||||||
|
|
||||||
|
|
||||||
export HOMEBREW_BOTTLE_DOMAIN=https://mirrors.ustc.edu.cn/homebrew-bottles/
|
|
||||||
|
|
||||||
# pyenv 懒加载 - 只在第一次使用 Python 相关命令时再初始化
|
|
||||||
export PYENV_ROOT="$HOME/.pyenv"
|
|
||||||
_lazy_load_pyenv() {
|
|
||||||
unset -f pyenv python python3 pip pip3 2>/dev/null
|
|
||||||
|
|
||||||
local pyenv_bin
|
|
||||||
pyenv_bin="$(command -v pyenv 2>/dev/null)"
|
|
||||||
if [ -z "$pyenv_bin" ] && [ -x /opt/homebrew/bin/pyenv ]; then
|
|
||||||
pyenv_bin=/opt/homebrew/bin/pyenv
|
|
||||||
fi
|
|
||||||
|
|
||||||
[ -n "$pyenv_bin" ] || return 1
|
|
||||||
|
|
||||||
eval "$("$pyenv_bin" init --path)"
|
|
||||||
eval "$("$pyenv_bin" init - zsh)"
|
|
||||||
hash -r 2>/dev/null
|
|
||||||
}
|
|
||||||
pyenv() { _lazy_load_pyenv && pyenv "$@"; }
|
|
||||||
python() { _lazy_load_pyenv && python "$@"; }
|
|
||||||
python3() { _lazy_load_pyenv && python3 "$@"; }
|
|
||||||
pip() { _lazy_load_pyenv && pip "$@"; }
|
|
||||||
pip3() { _lazy_load_pyenv && pip3 "$@"; }
|
|
||||||
|
|
||||||
# 引入 bash 配置
|
|
||||||
if [ -f ~/.bash_profile ]; then
|
|
||||||
source ~/.bash_profile
|
|
||||||
fi
|
|
||||||
if [ -f ~/.bashrc ]; then
|
|
||||||
source ~/.bashrc
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Added by Windsurf
|
|
||||||
export PATH="/Users/long/.codeium/windsurf/bin:$PATH"
|
|
||||||
|
|
||||||
# Added by Antigravity
|
|
||||||
export PATH="/Users/long/.antigravity/antigravity/bin:$PATH"
|
|
||||||
export PATH="$HOME/.local/bin:$PATH"
|
|
||||||
|
|
||||||
# MindOS Desktop — CLI (mindos)
|
|
||||||
export PATH="$HOME/.mindos/bin:$PATH"
|
|
||||||
|
|
||||||
source "$HOME/.cargo/env"
|
|
||||||
|
|
||||||
# Added by Antigravity
|
|
||||||
export PATH="/Users/long/.antigravity/antigravity/bin:$PATH"
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
alias ll='ls -alhG'
|
|
||||||
|
|
||||||
# Starship prompt
|
|
||||||
eval "$(starship init zsh)"
|
|
||||||
|
|
||||||
# zsh-autosuggestions 自动补全
|
|
||||||
source /opt/homebrew/share/zsh-autosuggestions/zsh-autosuggestions.zsh
|
|
||||||
|
|
||||||
# NVM
|
|
||||||
export NVM_DIR="$HOME/.nvm"
|
|
||||||
if [ -s "$NVM_DIR/nvm.sh" ]; then
|
|
||||||
\. "$NVM_DIR/nvm.sh"
|
|
||||||
fi
|
|
||||||
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"
|
|
||||||
export PATH="/Applications/EServer/bin:$PATH"
|
|
||||||
function EC_start(){
|
|
||||||
/Applications/EasyConnect.app/Contents/Resources/bin/EasyMonitor > /dev/null 2>&1 &
|
|
||||||
/Applications/EasyConnect.app/Contents/MacOS/EasyConnect > /dev/null 2>&1 &
|
|
||||||
open /Applications/EasyConnect.app
|
|
||||||
}
|
|
||||||
|
|
||||||
function EC_kill(){
|
|
||||||
pkill EasyMonitor
|
|
||||||
pkill ECAgent
|
|
||||||
pkill ECAgentProxy
|
|
||||||
pkill EasyConnect
|
|
||||||
}
|
|
||||||
# The following lines have been added by Docker Desktop to enable Docker CLI completions.
|
|
||||||
fpath=(/Users/long/.docker/completions $fpath)
|
|
||||||
autoload -Uz compinit
|
|
||||||
compinit
|
|
||||||
# End of Docker CLI completions
|
|
||||||
|
|
||||||
|
|
||||||
export HOMEBREW_BOTTLE_DOMAIN=https://mirrors.ustc.edu.cn/homebrew-bottles/
|
|
||||||
|
|
||||||
# pyenv
|
|
||||||
export PYENV_ROOT="$HOME/.pyenv"
|
|
||||||
export PATH="/opt/homebrew/bin:$PYENV_ROOT/bin:$PATH"
|
|
||||||
if command -v pyenv >/dev/null 2>&1; then
|
|
||||||
eval "$(pyenv init --path)"
|
|
||||||
eval "$(pyenv init - zsh)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 引入 bash 配置
|
|
||||||
if [ -f ~/.bash_profile ]; then
|
|
||||||
source ~/.bash_profile
|
|
||||||
fi
|
|
||||||
if [ -f ~/.bashrc ]; then
|
|
||||||
source ~/.bashrc
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Added by Windsurf
|
|
||||||
export PATH="/Users/long/.codeium/windsurf/bin:$PATH"
|
|
||||||
|
|
||||||
# Added by Antigravity
|
|
||||||
export PATH="/Users/long/.antigravity/antigravity/bin:$PATH"
|
|
||||||
|
|
||||||
if command -v nvm >/dev/null 2>&1; then
|
|
||||||
_nvm_default_version="$(nvm version default 2>/dev/null)"
|
|
||||||
if [ -n "$_nvm_default_version" ] && [ -d "$NVM_DIR/versions/node/$_nvm_default_version/bin" ]; then
|
|
||||||
export PATH="$NVM_DIR/versions/node/$_nvm_default_version/bin:$PATH"
|
|
||||||
fi
|
|
||||||
unset _nvm_default_version
|
|
||||||
fi
|
|
||||||
export PATH="$HOME/.local/bin:$PATH"
|
|
||||||
|
|
||||||
# MindOS Desktop — CLI (mindos)
|
|
||||||
export PATH="$HOME/.mindos/bin:$PATH"
|
|
||||||
|
|
||||||
source "$HOME/.cargo/env"
|
|
||||||
|
|
||||||
# Added by Antigravity
|
|
||||||
export PATH="/Users/long/.antigravity/antigravity/bin:$PATH"
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
// 2. 获取组件实例,通过 proxy 访问全局属性
|
// 2. 获取组件实例,通过 proxy 访问全局属性
|
||||||
import { ref} from "vue";
|
import { ref} from "vue";
|
||||||
import * as GenerateTestUserSig from "@/debug/GenerateTestUserSig-es.js";
|
|
||||||
import { CallManager } from "@/TUICallKit/src/TUICallService/serve/callManager";
|
import { CallManager } from "@/TUICallKit/src/TUICallService/serve/callManager";
|
||||||
import { onLaunch, onShow, onHide, onError } from '@dcloudio/uni-app'
|
import { onLaunch, onShow, onHide, onError } from '@dcloudio/uni-app'
|
||||||
import { getCurrentInstance } from 'vue'
|
import { getCurrentInstance } from 'vue'
|
||||||
@@ -97,11 +96,7 @@ const video =async (data)=>{
|
|||||||
},
|
},
|
||||||
}, false)
|
}, false)
|
||||||
|
|
||||||
const {userId } = res.data;
|
const { userId, userSig, sdkAppId: SDKAppID } = res.data;
|
||||||
|
|
||||||
const { userSig, SDKAppID } = GenerateTestUserSig.genTestUserSig({
|
|
||||||
userID:userId,
|
|
||||||
});
|
|
||||||
console.log('SDKAppID',SDKAppID)
|
console.log('SDKAppID',SDKAppID)
|
||||||
getApp().globalData.userID = userId;
|
getApp().globalData.userID = userId;
|
||||||
getApp().globalData.userSig = userSig;
|
getApp().globalData.userSig = userSig;
|
||||||
|
|||||||
@@ -239,7 +239,6 @@
|
|||||||
import messageEnhancerPlugin from '@tencentcloud/lite-chat/plugins/message-enhancer';
|
import messageEnhancerPlugin from '@tencentcloud/lite-chat/plugins/message-enhancer';
|
||||||
import richMediaMessagePlugin from '@tencentcloud/lite-chat/plugins/rich-media-message';
|
import richMediaMessagePlugin from '@tencentcloud/lite-chat/plugins/rich-media-message';
|
||||||
import groupPlugin from '@tencentcloud/lite-chat/plugins/group';
|
import groupPlugin from '@tencentcloud/lite-chat/plugins/group';
|
||||||
import * as GenerateTestUserSig from "@/debug/GenerateTestUserSig-es.js";
|
|
||||||
import { TUICallKitAPI } from '@/TUICallKit/src/TUICallService/index';
|
import { TUICallKitAPI } from '@/TUICallKit/src/TUICallService/index';
|
||||||
import { TUIStore } from '@/TUICallKit/src/TUICallService/CallService/index';
|
import { TUIStore } from '@/TUICallKit/src/TUICallService/CallService/index';
|
||||||
import { StoreName, NAME, CallStatus } from '@/TUICallKit/src/TUICallService/const/index';
|
import { StoreName, NAME, CallStatus } from '@/TUICallKit/src/TUICallService/const/index';
|
||||||
@@ -561,17 +560,12 @@ const { proxy } = getCurrentInstance()
|
|||||||
},
|
},
|
||||||
}, false)
|
}, false)
|
||||||
|
|
||||||
const {userId } = res.data;
|
const { userId, userSig, sdkAppId: SDKAppID } = res.data;
|
||||||
|
|
||||||
patient_data.value=res.data.data
|
patient_data.value=res.data.data
|
||||||
const { userSig, SDKAppID,secretKey } = GenerateTestUserSig.genTestUserSig({
|
|
||||||
userID:userId,
|
|
||||||
});
|
|
||||||
|
|
||||||
getApp().globalData.userID = userId;
|
getApp().globalData.userID = userId;
|
||||||
getApp().globalData.userSig = userSig;
|
getApp().globalData.userSig = userSig;
|
||||||
getApp().globalData.SDKAppID = SDKAppID;
|
getApp().globalData.SDKAppID = SDKAppID;
|
||||||
getApp().globalData.secretKey = secretKey;
|
|
||||||
uni.setStorageSync('CallManager', userId)
|
uni.setStorageSync('CallManager', userId)
|
||||||
// TUICallKit 必须在 TIM chat 登录成功后再 init,且必须传入 tim,否则无法接收通话信令(聊天正常、视频接不到)
|
// TUICallKit 必须在 TIM chat 登录成功后再 init,且必须传入 tim,否则无法接收通话信令(聊天正常、视频接不到)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
<template>
|
||||||
|
<view class="tab-dock">
|
||||||
|
<view class="tab-dock__inner">
|
||||||
|
<view class="tab-dock__item" @click="goHome">
|
||||||
|
<image
|
||||||
|
class="tab-dock__icon"
|
||||||
|
:src="active === 0 ? '/static/user/home_no.png' : '/static/user/home.png'"
|
||||||
|
mode="aspectFit"
|
||||||
|
/>
|
||||||
|
<text class="tab-dock__label" :style="{ color: active === 0 ? activeColor : color }">首页</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="tab-dock__item tab-dock__item--center" @click="goWeekly">
|
||||||
|
<view class="tab-dock__fab" :class="{ 'tab-dock__fab--active': active === 1 }">
|
||||||
|
<text class="tab-dock__fab-text">糖</text>
|
||||||
|
</view>
|
||||||
|
<text class="tab-dock__label tab-dock__label--center" :style="{ color: active === 1 ? activeColor : color }">血糖</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="tab-dock__item" @click="goUser">
|
||||||
|
<image
|
||||||
|
class="tab-dock__icon"
|
||||||
|
:src="active === 2 ? '/static/user/user_no.png' : '/static/user/user.png'"
|
||||||
|
mode="aspectFit"
|
||||||
|
/>
|
||||||
|
<text class="tab-dock__label" :style="{ color: active === 2 ? activeColor : color }">我的</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="tab-dock__safe" />
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
const props = defineProps({
|
||||||
|
/** 0 首页 1 血糖 2 我的 */
|
||||||
|
active: {
|
||||||
|
type: Number,
|
||||||
|
default: 0
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const color = '#8a8a8a'
|
||||||
|
const activeColor = '#204e2b'
|
||||||
|
|
||||||
|
function goHome() {
|
||||||
|
if (props.active === 0) return
|
||||||
|
uni.redirectTo({ url: '/pages/index/index' })
|
||||||
|
}
|
||||||
|
|
||||||
|
function goUser() {
|
||||||
|
if (props.active === 2) return
|
||||||
|
uni.redirectTo({ url: '/pages/user/user' })
|
||||||
|
}
|
||||||
|
|
||||||
|
function goWeekly() {
|
||||||
|
if (props.active === 1) return
|
||||||
|
uni.redirectTo({ url: '/tongji/pages/weekly' })
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.tab-dock {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 999;
|
||||||
|
background: #ffffff;
|
||||||
|
border-top: 1rpx solid rgba(0, 0, 0, 0.06);
|
||||||
|
box-shadow: 0 -6rpx 20rpx rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-dock__inner {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
height: 100rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-dock__item {
|
||||||
|
flex: 1;
|
||||||
|
height: 100rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-dock__item--center {
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding-bottom: 6rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-dock__icon {
|
||||||
|
width: 46rpx;
|
||||||
|
height: 46rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-dock__label {
|
||||||
|
margin-top: 6rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-dock__label--center {
|
||||||
|
margin-top: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-dock__fab {
|
||||||
|
width: 96rpx;
|
||||||
|
height: 96rpx;
|
||||||
|
margin-top: -48rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: linear-gradient(180deg, #2d7340 0%, #204e2b 100%);
|
||||||
|
border: 6rpx solid #ffffff;
|
||||||
|
box-shadow: 0 10rpx 24rpx rgba(32, 78, 43, 0.35);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-dock__fab--active {
|
||||||
|
transform: scale(1.04);
|
||||||
|
box-shadow: 0 12rpx 28rpx rgba(32, 78, 43, 0.42);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-dock__fab-text {
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 38rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-dock__safe {
|
||||||
|
height: env(safe-area-inset-bottom);
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,5 +1,68 @@
|
|||||||
<template>
|
<template>
|
||||||
<view v-if="visible" class="dev-entry-wrap" :style="{ bottom: props.bottom + 'rpx' }">
|
<!-- 页面内嵌模块(如 weekly 统计页主内容区) -->
|
||||||
|
<view
|
||||||
|
v-if="visible && props.inline"
|
||||||
|
class="dev-entry-inline"
|
||||||
|
:class="{ 'dev-entry-inline--weekly': props.tone === 'weekly' }"
|
||||||
|
>
|
||||||
|
<view class="inline-hero">
|
||||||
|
<view class="inline-hero-icon" aria-hidden="true">
|
||||||
|
<view class="inline-hero-wave" />
|
||||||
|
</view>
|
||||||
|
<view class="inline-hero-text">
|
||||||
|
<view class="inline-hero-title-row">
|
||||||
|
<text class="inline-title">居家耗糖训练</text>
|
||||||
|
<text v-if="props.tone === 'weekly'" class="inline-pill">体验</text>
|
||||||
|
</view>
|
||||||
|
<text class="inline-sub">低强度动起来,帮助身体消耗糖分</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<scroll-view
|
||||||
|
scroll-x
|
||||||
|
class="inline-scroll"
|
||||||
|
:show-scrollbar="false"
|
||||||
|
:enable-flex="true"
|
||||||
|
>
|
||||||
|
<view class="inline-scroll-track">
|
||||||
|
<view
|
||||||
|
v-for="item in inlineItems"
|
||||||
|
:key="item.url"
|
||||||
|
class="inline-card"
|
||||||
|
@click="goto(item.url)"
|
||||||
|
>
|
||||||
|
<view v-if="item.featured" class="inline-card-badge">
|
||||||
|
<text>推荐</text>
|
||||||
|
</view>
|
||||||
|
<view class="menu-icon" :class="item.iconClass">
|
||||||
|
<view v-if="item.icon === 'dumbbell'" class="dumbbell-shape">
|
||||||
|
<view class="dumbbell-plate dumbbell-plate-left" />
|
||||||
|
<view class="dumbbell-bar" />
|
||||||
|
<view class="dumbbell-plate dumbbell-plate-right" />
|
||||||
|
</view>
|
||||||
|
<view v-else-if="item.icon === 'pedal'" class="foot-pedal-shape">
|
||||||
|
<view class="pedal-bar pedal-bar-left" />
|
||||||
|
<view class="pedal-bar pedal-bar-right" />
|
||||||
|
</view>
|
||||||
|
<view v-else-if="item.icon === 'pilates'" class="pilates-ring-shape" />
|
||||||
|
<view v-else-if="item.icon === 'grip'" class="grip-ring-shape" />
|
||||||
|
<view v-else-if="item.icon === 'metro'" class="menu-icon--metro-inner">
|
||||||
|
<view class="metro-bar metro-bar-1" />
|
||||||
|
<view class="metro-bar metro-bar-2" />
|
||||||
|
<view class="metro-bar metro-bar-3" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<text class="inline-card-title">{{ item.title }}</text>
|
||||||
|
<text class="inline-card-sub">{{ item.sub }}</text>
|
||||||
|
<text class="inline-card-go" aria-hidden="true">进入</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
|
||||||
|
<text v-if="props.tone === 'weekly'" class="inline-scroll-hint">左右滑动,选择适合您的训练</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-else-if="visible" class="dev-entry-wrap" :style="{ bottom: props.bottom + 'rpx' }">
|
||||||
<!-- 展开后的菜单卡片(从下往上动画) -->
|
<!-- 展开后的菜单卡片(从下往上动画) -->
|
||||||
<view v-if="expanded" class="menu-list" @click.stop>
|
<view v-if="expanded" class="menu-list" @click.stop>
|
||||||
<view class="menu-item" @click="goto('/training/pages/dumbbell')">
|
<view class="menu-item" @click="goto('/training/pages/dumbbell')">
|
||||||
@@ -84,25 +147,47 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
|
|
||||||
|
type TrainingItem = {
|
||||||
|
url: string
|
||||||
|
title: string
|
||||||
|
sub: string
|
||||||
|
icon: 'dumbbell' | 'pedal' | 'pilates' | 'grip' | 'metro'
|
||||||
|
iconClass: string
|
||||||
|
featured?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const trainingItems: TrainingItem[] = [
|
||||||
|
{ url: '/training/pages/dumbbell', title: '哑铃', sub: '力量塑形', icon: 'dumbbell', iconClass: 'menu-icon--dumbbell' },
|
||||||
|
{ url: '/training/pages/foot-pedal', title: '脚蹬器', sub: '下肢有氧', icon: 'pedal', iconClass: 'menu-icon--pedal' },
|
||||||
|
{ url: '/training/pages/pilates-ring', title: '瑜伽环', sub: '核心塑形', icon: 'pilates', iconClass: 'menu-icon--pilates' },
|
||||||
|
{ url: '/training/pages/grip-ring', title: '握力环', sub: '握力训练', icon: 'grip', iconClass: 'menu-icon--grip' },
|
||||||
|
{ url: '/training/pages/metronome', title: '耗糖节拍器', sub: '健走配速', icon: 'metro', iconClass: 'menu-icon--metro', featured: true },
|
||||||
|
]
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
|
/** 悬浮按钮距底部 rpx(仅非 inline) */
|
||||||
bottom?: number
|
bottom?: number
|
||||||
|
/** 内嵌到页面主内容区,非 fixed 悬浮 */
|
||||||
|
inline?: boolean
|
||||||
|
/** weekly 页:与统计页 Vital Mint 视觉一致 */
|
||||||
|
tone?: 'default' | 'weekly'
|
||||||
}>(),
|
}>(),
|
||||||
{ bottom: 200 },
|
{ bottom: 200, inline: false, tone: 'default' },
|
||||||
)
|
)
|
||||||
|
|
||||||
const isDevMode = (): boolean => {
|
/** weekly 将健走节拍器置前并标推荐 */
|
||||||
/* HBuilderX 工程:发行(release)模式下 NODE_ENV === 'production' */
|
const inlineItems = computed(() => {
|
||||||
try {
|
if (props.tone !== 'weekly') return trainingItems
|
||||||
return process.env.NODE_ENV !== 'production'
|
const metro = trainingItems.find((i) => i.icon === 'metro')
|
||||||
} catch (_) {
|
const rest = trainingItems.filter((i) => i.icon !== 'metro')
|
||||||
return false
|
return metro ? [metro, ...rest] : trainingItems
|
||||||
}
|
})
|
||||||
}
|
|
||||||
|
|
||||||
const visible = ref(isDevMode())
|
// 居家耗糖训练是正式功能,开发版和发行版都需要展示。
|
||||||
|
const visible = ref(true)
|
||||||
const expanded = ref(false)
|
const expanded = ref(false)
|
||||||
|
|
||||||
const toggle = () => {
|
const toggle = () => {
|
||||||
@@ -449,4 +534,196 @@ $brand-light: #34d399;
|
|||||||
line-height: 1;
|
line-height: 1;
|
||||||
margin-left: 4rpx;
|
margin-left: 4rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.menu-icon--metro-inner {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 4rpx;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
* 内嵌模块(weekly 等)
|
||||||
|
* ============================================================ */
|
||||||
|
.dev-entry-inline {
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dev-entry-inline--weekly {
|
||||||
|
padding: 32rpx 28rpx 24rpx;
|
||||||
|
border-radius: 48rpx;
|
||||||
|
background: var(--surface-container-lowest, #fff);
|
||||||
|
border: 1rpx solid rgba(0, 108, 73, 0.12);
|
||||||
|
box-shadow:
|
||||||
|
0 24rpx 60rpx -20rpx rgba(0, 108, 73, 0.08),
|
||||||
|
0 8rpx 20rpx -8rpx rgba(0, 0, 0, 0.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-hero {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20rpx;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-hero-icon {
|
||||||
|
width: 88rpx;
|
||||||
|
height: 88rpx;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
background: rgba(0, 108, 73, 0.1);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-hero-wave {
|
||||||
|
width: 52rpx;
|
||||||
|
height: 52rpx;
|
||||||
|
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23006c49' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'><polyline points='2,12 7,12 9.5,7 12,17 14.5,9 16.5,12 22,12'/></svg>");
|
||||||
|
background-size: contain;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-hero-text {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-hero-title-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-title {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #0f172a;
|
||||||
|
line-height: 1.25;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-pill {
|
||||||
|
font-size: 20rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #006c49;
|
||||||
|
padding: 4rpx 12rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
background: rgba(0, 108, 73, 0.1);
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-sub {
|
||||||
|
font-size: 26rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #475569;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-scroll {
|
||||||
|
width: 100%;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-scroll-track {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 16rpx;
|
||||||
|
padding: 4rpx 2rpx 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-card {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
width: 212rpx;
|
||||||
|
min-height: 220rpx;
|
||||||
|
padding: 20rpx 18rpx 18rpx;
|
||||||
|
border-radius: 28rpx;
|
||||||
|
background: #f4f7f5;
|
||||||
|
border: 1rpx solid rgba(0, 108, 73, 0.1);
|
||||||
|
box-sizing: border-box;
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: transform 0.2s ease, background 0.2s ease;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: scale(0.98);
|
||||||
|
background: #e8f3ee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-icon {
|
||||||
|
width: 72rpx;
|
||||||
|
height: 72rpx;
|
||||||
|
margin-bottom: 14rpx;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-card-badge {
|
||||||
|
position: absolute;
|
||||||
|
top: 12rpx;
|
||||||
|
right: 12rpx;
|
||||||
|
padding: 4rpx 10rpx;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
background: #006c49;
|
||||||
|
|
||||||
|
text {
|
||||||
|
font-size: 18rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #fff;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-card-title {
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #0f172a;
|
||||||
|
line-height: 1.25;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-card-sub {
|
||||||
|
margin-top: 6rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #64748b;
|
||||||
|
line-height: 1.35;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-card-go {
|
||||||
|
margin-top: auto;
|
||||||
|
padding-top: 12rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #006c49;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-scroll-hint {
|
||||||
|
display: block;
|
||||||
|
margin-top: 14rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #94a3b8;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.inline-card:active {
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -178,7 +178,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, computed } from 'vue'
|
import { ref, onMounted, computed } from 'vue'
|
||||||
import { getCurrentInstance } from 'vue'
|
import { getCurrentInstance } from 'vue'
|
||||||
import * as GenerateTestUserSig from "@/debug/GenerateTestUserSig-es.js";
|
|
||||||
import { CallManager } from "@/TUICallKit/src/TUICallService/serve/callManager";
|
import { CallManager } from "@/TUICallKit/src/TUICallService/serve/callManager";
|
||||||
const { proxy } = getCurrentInstance()
|
const { proxy } = getCurrentInstance()
|
||||||
let userId = ref("");
|
let userId = ref("");
|
||||||
@@ -379,10 +378,11 @@ const loginHandler = async (patient_id) => {
|
|||||||
data: { patient_id: patient_id },
|
data: { patient_id: patient_id },
|
||||||
}, false)
|
}, false)
|
||||||
|
|
||||||
const { userId: patientUserId } = res.data
|
const {
|
||||||
const { userSig: patientUserSig, SDKAppID: patientSDKAppID } = GenerateTestUserSig.genTestUserSig({
|
userId: patientUserId,
|
||||||
userID: patientUserId,
|
userSig: patientUserSig,
|
||||||
})
|
sdkAppId: patientSDKAppID,
|
||||||
|
} = res.data
|
||||||
console.log('获取签名成功:', patientUserSig)
|
console.log('获取签名成功:', patientUserSig)
|
||||||
|
|
||||||
// 更新ref值
|
// 更新ref值
|
||||||
|
|||||||
@@ -51,15 +51,16 @@
|
|||||||
/* 小程序特有相关 */
|
/* 小程序特有相关 */
|
||||||
"mp-weixin" : {
|
"mp-weixin" : {
|
||||||
"appid" : "wx79b9a0bfbfe7cbcd",
|
"appid" : "wx79b9a0bfbfe7cbcd",
|
||||||
|
"lazyCodeLoading" : "requiredComponents",
|
||||||
"setting" : {
|
"setting" : {
|
||||||
"urlCheck" : false
|
"urlCheck" : false,
|
||||||
|
"minified" : true
|
||||||
},
|
},
|
||||||
"optimization" : {
|
"optimization" : {
|
||||||
"subPackages" : true
|
"subPackages" : true
|
||||||
},
|
},
|
||||||
"usingComponents" : true,
|
"usingComponents" : true,
|
||||||
"requiredBackgroundModes" : ["audio"],
|
"requiredBackgroundModes" : [ "audio" ],
|
||||||
|
|
||||||
"plugins" : {
|
"plugins" : {
|
||||||
"WechatSI" : {
|
"WechatSI" : {
|
||||||
"version" : "0.3.5",
|
"version" : "0.3.5",
|
||||||
|
|||||||
Generated
-69
@@ -4142,9 +4142,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -4161,9 +4158,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -4180,9 +4174,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -4199,9 +4190,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -4433,9 +4421,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -4456,9 +4441,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -4479,9 +4461,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -4502,9 +4481,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -4525,9 +4501,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -4548,9 +4521,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -4729,9 +4699,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -4745,9 +4712,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -4761,9 +4725,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -4777,9 +4738,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -4793,9 +4751,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"loong64"
|
"loong64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -4809,9 +4764,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"loong64"
|
"loong64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -4825,9 +4777,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -4841,9 +4790,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -4857,9 +4803,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -4873,9 +4816,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -4889,9 +4829,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -4905,9 +4842,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -4921,9 +4855,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
|
|||||||
+47
-37
@@ -3,7 +3,9 @@
|
|||||||
{
|
{
|
||||||
"path": "pages/index/index",
|
"path": "pages/index/index",
|
||||||
"style": {
|
"style": {
|
||||||
"navigationBarTitleText": "甄养堂"
|
"navigationBarTitleText": "甄养堂",
|
||||||
|
"backgroundColor": "#F8F8F8",
|
||||||
|
"backgroundColorTop": "#F8F8F8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -24,14 +26,6 @@
|
|||||||
"navigationBarTitleText": "甄养堂"
|
"navigationBarTitleText": "甄养堂"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"path": "TUICallKit/src/Components/TUICallKit",
|
|
||||||
"style": {
|
|
||||||
"navigationBarTitleText": "视频问诊",
|
|
||||||
"navigationBarHidden": true,
|
|
||||||
"navigationStyle": "custom"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"path": "pages/order/monad/monad",
|
"path": "pages/order/monad/monad",
|
||||||
"style": {
|
"style": {
|
||||||
@@ -43,7 +37,9 @@
|
|||||||
{
|
{
|
||||||
"path": "pages/user/user",
|
"path": "pages/user/user",
|
||||||
"style": {
|
"style": {
|
||||||
"navigationBarTitleText": "我的"
|
"navigationBarTitleText": "我的",
|
||||||
|
"backgroundColor": "#e8eaed",
|
||||||
|
"backgroundColorTop": "#e8eaed"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -72,6 +68,19 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"subPackages": [
|
"subPackages": [
|
||||||
|
{
|
||||||
|
"root": "TUICallKit/src/Components",
|
||||||
|
"pages": [
|
||||||
|
{
|
||||||
|
"path": "TUICallKit",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "视频问诊",
|
||||||
|
"navigationBarHidden": true,
|
||||||
|
"navigationStyle": "custom"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"root": "TUIKit",
|
"root": "TUIKit",
|
||||||
"pages": [
|
"pages": [
|
||||||
@@ -177,16 +186,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"path": "pages/more",
|
|
||||||
"style": {
|
|
||||||
"navigationBarTitleText": "日常护理",
|
|
||||||
"navigationBarBackgroundColor": "#204e2b",
|
|
||||||
"navigationBarTextStyle": "white",
|
|
||||||
"backgroundColor": "#faf9f5",
|
|
||||||
"enablePullDownRefresh": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"path": "pages/weekly",
|
"path": "pages/weekly",
|
||||||
"style": {
|
"style": {
|
||||||
@@ -203,27 +202,38 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/more",
|
||||||
|
"style": {
|
||||||
|
"navigationStyle": "custom",
|
||||||
|
"navigationBarTitleText": "日常护理",
|
||||||
|
"backgroundColor": "#f4fbf4",
|
||||||
|
"enablePullDownRefresh": true,
|
||||||
|
"mp-weixin": {
|
||||||
|
"usingPlugins": {
|
||||||
|
"WechatSI": {
|
||||||
|
"version": "0.3.5",
|
||||||
|
"provider": "wx069ba97219f66d99"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "endless-game/index",
|
||||||
|
"style": {
|
||||||
|
"navigationStyle": "custom",
|
||||||
|
"navigationBarTitleText": "识糖小课堂",
|
||||||
|
"backgroundColor": "#eefbf4",
|
||||||
|
"disableScroll": false,
|
||||||
|
"enableShareAppMessage": true,
|
||||||
|
"enableShareTimeline": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"tabBar": {
|
|
||||||
"color": "#8a8a8a",
|
|
||||||
"selectedColor": "#204e2b",
|
|
||||||
"borderStyle": "black",
|
|
||||||
"backgroundColor": "#ffffff",
|
|
||||||
"list": [{
|
|
||||||
"pagePath": "pages/index/index",
|
|
||||||
"iconPath": "/static/user/home.png",
|
|
||||||
"selectedIconPath": "/static/user/home_no.png",
|
|
||||||
"text": "首页"
|
|
||||||
},{
|
|
||||||
"pagePath": "pages/user/user",
|
|
||||||
"iconPath": "/static/user/user.png",
|
|
||||||
"selectedIconPath": "/static/user/user_no.png",
|
|
||||||
"text": "我的"
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
"globalStyle": {
|
"globalStyle": {
|
||||||
"navigationBarTextStyle": "black",
|
"navigationBarTextStyle": "black",
|
||||||
"navigationBarTitleText": "甄养堂互联网医院",
|
"navigationBarTitleText": "甄养堂互联网医院",
|
||||||
|
|||||||
@@ -118,13 +118,15 @@
|
|||||||
</view>
|
</view>
|
||||||
</scroll-view>
|
</scroll-view>
|
||||||
</view>
|
</view>
|
||||||
|
<TabBarDock :active="0" />
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { getCurrentInstance } from 'vue'
|
import { getCurrentInstance } from 'vue'
|
||||||
import { onShow, onShareAppMessage } from '@dcloudio/uni-app'
|
import { onShareAppMessage } from '@dcloudio/uni-app'
|
||||||
|
import TabBarDock from '@/components/app-tab-bar/tab-bar-dock.vue'
|
||||||
const { proxy } = getCurrentInstance()
|
const { proxy } = getCurrentInstance()
|
||||||
|
|
||||||
const doctors = ref([])
|
const doctors = ref([])
|
||||||
@@ -263,7 +265,8 @@ $outline: #727970;
|
|||||||
.container {
|
.container {
|
||||||
background: $background;
|
background: $background;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
padding: 24rpx 32rpx 120rpx;
|
padding: 24rpx 32rpx;
|
||||||
|
padding-bottom: calc(160rpx + env(safe-area-inset-bottom));
|
||||||
font-family: 'Noto Sans SC', 'PingFang SC', 'Microsoft YaHei', sans-serif;
|
font-family: 'Noto Sans SC', 'PingFang SC', 'Microsoft YaHei', sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -62,7 +62,6 @@
|
|||||||
import { ref,onMounted,getCurrentInstance} from "vue";
|
import { ref,onMounted,getCurrentInstance} from "vue";
|
||||||
// 2. 获取组件实例,通过 proxy 访问全局属性
|
// 2. 获取组件实例,通过 proxy 访问全局属性
|
||||||
const { proxy } = getCurrentInstance()
|
const { proxy } = getCurrentInstance()
|
||||||
import * as GenerateTestUserSig from "@/debug/GenerateTestUserSig-es.js";
|
|
||||||
import { CallManager } from "@/TUICallKit/src/TUICallService/serve/callManager";
|
import { CallManager } from "@/TUICallKit/src/TUICallService/serve/callManager";
|
||||||
import { onLaunch, onShow, onHide, onError ,onShareAppMessage} from '@dcloudio/uni-app'
|
import { onLaunch, onShow, onHide, onError ,onShareAppMessage} from '@dcloudio/uni-app'
|
||||||
let userID = ref("4");
|
let userID = ref("4");
|
||||||
@@ -140,13 +139,9 @@ const loginHandler = async (patient_id) => {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
const {userId } = res.data;
|
const { userId, userSig, sdkAppId: SDKAppID } = res.data;
|
||||||
|
|
||||||
const { userSig, SDKAppID } = GenerateTestUserSig.genTestUserSig({
|
|
||||||
userID:userId,
|
|
||||||
});
|
|
||||||
console.log('获取签名成功:',userSig);
|
console.log('获取签名成功:',userSig);
|
||||||
getApp().globalData.userID = userId.value;
|
getApp().globalData.userID = userId;
|
||||||
getApp().globalData.userSig = userSig;
|
getApp().globalData.userSig = userSig;
|
||||||
getApp().globalData.SDKAppID = SDKAppID;
|
getApp().globalData.SDKAppID = SDKAppID;
|
||||||
|
|
||||||
|
|||||||
@@ -52,7 +52,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, getCurrentInstance,onMounted } from "vue";
|
import { ref, getCurrentInstance,onMounted } from "vue";
|
||||||
import { onShow, onShareAppMessage } from '@dcloudio/uni-app'
|
import { onShow, onShareAppMessage } from '@dcloudio/uni-app'
|
||||||
import * as GenerateTestUserSig from "@/debug/GenerateTestUserSig-es.js";
|
|
||||||
import { CallManager } from "@/TUICallKit/src/TUICallService/serve/callManager";
|
import { CallManager } from "@/TUICallKit/src/TUICallService/serve/callManager";
|
||||||
const { proxy } = getCurrentInstance()
|
const { proxy } = getCurrentInstance()
|
||||||
let userId = ref("");
|
let userId = ref("");
|
||||||
@@ -126,10 +125,7 @@ const loginHandler = async (patient_id) => {
|
|||||||
data: { patient_id: patient_id },
|
data: { patient_id: patient_id },
|
||||||
}, false)
|
}, false)
|
||||||
|
|
||||||
const {userId } = res.data;
|
const { userId, userSig, sdkAppId: SDKAppID } = res.data;
|
||||||
const { userSig, SDKAppID } = GenerateTestUserSig.genTestUserSig({
|
|
||||||
userID:userId,
|
|
||||||
});
|
|
||||||
console.log('获取签名成功:',userSig);
|
console.log('获取签名成功:',userSig);
|
||||||
|
|
||||||
getApp().globalData.userID = userId;
|
getApp().globalData.userID = userId;
|
||||||
|
|||||||
@@ -370,7 +370,6 @@
|
|||||||
import { ref,onMounted,getCurrentInstance } from "vue";
|
import { ref,onMounted,getCurrentInstance } from "vue";
|
||||||
import { onShow, onShareAppMessage,onLaunch } from '@dcloudio/uni-app'
|
import { onShow, onShareAppMessage,onLaunch } from '@dcloudio/uni-app'
|
||||||
// 2. 获取组件实例,通过 proxy 访问全局属性
|
// 2. 获取组件实例,通过 proxy 访问全局属性
|
||||||
import * as GenerateTestUserSig from "@/debug/GenerateTestUserSig-es.js";
|
|
||||||
import { CallManager } from "@/TUICallKit/src/TUICallService/serve/callManager";
|
import { CallManager } from "@/TUICallKit/src/TUICallService/serve/callManager";
|
||||||
const { proxy } = getCurrentInstance()
|
const { proxy } = getCurrentInstance()
|
||||||
|
|
||||||
@@ -667,25 +666,25 @@ const closePatientModal = async () => {
|
|||||||
const granted = await requestAVPermissionsInModal();
|
const granted = await requestAVPermissionsInModal();
|
||||||
if (!granted) return; // 未授权,停止跳转
|
if (!granted) return; // 未授权,停止跳转
|
||||||
const app = getApp();
|
const app = getApp();
|
||||||
// const userID = app.globalData.userID;
|
|
||||||
// const userSig = app.globalData.userSig;
|
|
||||||
// const SDKAppID = app.globalData.SDKAppID;
|
|
||||||
const doctor = 'doctor_' + doctorId.value;
|
const doctor = 'doctor_' + doctorId.value;
|
||||||
await confirmDiagnosis(diagnosisId);
|
await confirmDiagnosis(diagnosisId);
|
||||||
if(!diagnosisId.includes('patient')){
|
const signatureRes = await proxy.apiUrl({
|
||||||
diagnosisId='patient_'+diagnosisId
|
url: '/api/tcm/getPatientSignature',
|
||||||
}
|
method: 'GET',
|
||||||
const { userSig, SDKAppID } = GenerateTestUserSig.genTestUserSig({
|
data: { patient_id: diagnosisId },
|
||||||
userID:diagnosisId,
|
}, false);
|
||||||
});
|
const { userId, userSig, sdkAppId: SDKAppID } = signatureRes.data;
|
||||||
if (!doctorId.value) {
|
if (!doctorId.value) {
|
||||||
uni.showToast({ title: '诊室号不存在,联系助理处理!', icon: 'none' });
|
uni.showToast({ title: '诊室号不存在,联系助理处理!', icon: 'none' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(app.globalData, diagnosisId);
|
app.globalData.userID = userId;
|
||||||
|
app.globalData.userSig = userSig;
|
||||||
|
app.globalData.SDKAppID = SDKAppID;
|
||||||
|
console.log(app.globalData, userId);
|
||||||
|
|
||||||
const url=`/TUIKit/pages/chat/chat?userID=${encodeURIComponent(diagnosisId)}&userSig=${encodeURIComponent(userSig)}&SDKAppID=${SDKAppID}&targetUserID=${doctor}&msg=1`
|
const url=`/TUIKit/pages/chat/chat?userID=${encodeURIComponent(userId)}&userSig=${encodeURIComponent(userSig)}&SDKAppID=${SDKAppID}&targetUserID=${doctor}&msg=1`
|
||||||
|
|
||||||
showPatientModal.value = false;
|
showPatientModal.value = false;
|
||||||
uni.redirectTo({
|
uni.redirectTo({
|
||||||
@@ -707,11 +706,7 @@ const video =async (data)=>{
|
|||||||
},
|
},
|
||||||
}, false)
|
}, false)
|
||||||
|
|
||||||
const {userId } = res.data;
|
const { userId, userSig, sdkAppId: SDKAppID } = res.data;
|
||||||
|
|
||||||
const { userSig, SDKAppID } = GenerateTestUserSig.genTestUserSig({
|
|
||||||
userID:userId,
|
|
||||||
});
|
|
||||||
|
|
||||||
getApp().globalData.userID = userId;
|
getApp().globalData.userID = userId;
|
||||||
getApp().globalData.userSig = userSig;
|
getApp().globalData.userSig = userSig;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="mine-page">
|
<view class="mine-page">
|
||||||
<!-- 顶部用户信息卡片 -->
|
<!-- 顶部用户信息卡片 -->
|
||||||
<div class="user-card" @click="navigateTo('/pages/user/userinfo')">
|
<div class="user-card" @click="navigateTo('/pages/user/userinfo')">
|
||||||
<div class="user-header">
|
<div class="user-header">
|
||||||
@@ -90,16 +90,18 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- DEV 开发悬浮入口(生产环境自动隐藏) -->
|
<!-- DEV 开发悬浮入口(生产环境自动隐藏) -->
|
||||||
<dev-training-entry />
|
<dev-training-entry :bottom="360" />
|
||||||
</div>
|
<TabBarDock :active="2" />
|
||||||
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { onShow, onShareAppMessage } from '@dcloudio/uni-app'
|
import { onShow, onShareAppMessage } from '@dcloudio/uni-app'
|
||||||
import DevTrainingEntry from '@/components/dev-training-entry/index.vue'
|
import DevTrainingEntry from '@/components/dev-training-entry/index.vue'
|
||||||
|
import TabBarDock from '@/components/app-tab-bar/tab-bar-dock.vue'
|
||||||
export default {
|
export default {
|
||||||
name: 'profileB',
|
name: 'profileB',
|
||||||
components: { DevTrainingEntry },
|
components: { DevTrainingEntry, TabBarDock },
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
userInfo: {
|
userInfo: {
|
||||||
@@ -170,6 +172,7 @@ export default {
|
|||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background: #e8eaed;
|
background: #e8eaed;
|
||||||
padding: 40rpx;
|
padding: 40rpx;
|
||||||
|
padding-bottom: calc(40rpx + 200rpx + env(safe-area-inset-bottom));
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-card {
|
.user-card {
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 45 KiB |
@@ -8,12 +8,12 @@
|
|||||||
mode="aspectFit"
|
mode="aspectFit"
|
||||||
@error="onImageError"
|
@error="onImageError"
|
||||||
/>
|
/>
|
||||||
<text
|
<!-- <text
|
||||||
v-else
|
v-else
|
||||||
class="tj-icon-fallback"
|
class="tj-icon-fallback"
|
||||||
:class="[`tj-icon-fallback--${size}`]"
|
:class="[`tj-icon-fallback--${size}`]"
|
||||||
:style="{ color }"
|
:style="{ color }"
|
||||||
>{{ fallbackGlyph }}</text>
|
>{{ fallbackGlyph }}</text> -->
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -66,7 +66,12 @@ const ICON_PATHS = {
|
|||||||
egg: '<path d="M12 22c4.97 0 8-3.27 8-7.31C20 9.65 16.42 2 12 2S4 9.65 4 14.69C4 18.73 7.03 22 12 22Z"/>',
|
egg: '<path d="M12 22c4.97 0 8-3.27 8-7.31C20 9.65 16.42 2 12 2S4 9.65 4 14.69C4 18.73 7.03 22 12 22Z"/>',
|
||||||
home: '<path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/>',
|
home: '<path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/>',
|
||||||
'plus-circle': '<circle cx="12" cy="12" r="10"/><path d="M8 12h8M12 8v8"/>',
|
'plus-circle': '<circle cx="12" cy="12" r="10"/><path d="M8 12h8M12 8v8"/>',
|
||||||
'chevron-right': '<path d="m9 18 6-6-6-6"/>'
|
'chevron-right': '<path d="m9 18 6-6-6-6"/>',
|
||||||
|
'chevron-left': '<path d="m15 18-6-6 6-6"/>',
|
||||||
|
check: '<path d="M20 6 9 17l-5-5"/>',
|
||||||
|
settings: '<path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/>',
|
||||||
|
syringe: '<path d="m18 2 4 4"/><path d="m17 7 3-3"/><path d="M19 9 8.7 19.3c-1 1-2.5 1-3.4 0l-.6-.6c-1-1-1-2.5 0-3.4L15 5"/><path d="m9 11 4 4"/><path d="m5 19-3 3"/><path d="m14 4 6 6"/>',
|
||||||
|
zap: '<path d="M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z"/>'
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 图片加载失败时的 emoji 回退 */
|
/** 图片加载失败时的 emoji 回退 */
|
||||||
@@ -106,7 +111,12 @@ const ICON_FALLBACK = {
|
|||||||
egg: '🍳',
|
egg: '🍳',
|
||||||
home: '🏠',
|
home: '🏠',
|
||||||
'plus-circle': '⊕',
|
'plus-circle': '⊕',
|
||||||
'chevron-right': '›'
|
'chevron-right': '›',
|
||||||
|
'chevron-left': '‹',
|
||||||
|
check: '✓',
|
||||||
|
settings: '⚙',
|
||||||
|
syringe: '💉',
|
||||||
|
zap: '⚡'
|
||||||
}
|
}
|
||||||
|
|
||||||
const strokeColor = computed(() => {
|
const strokeColor = computed(() => {
|
||||||
@@ -155,6 +165,10 @@ function onImageError() {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
/* Lucide send 路径重心偏右上,微调使圆形按钮内视觉居中 */
|
||||||
|
.tj-icon--send {
|
||||||
|
transform: translate(-10%, 10%);
|
||||||
|
}
|
||||||
.tj-icon-fallback {
|
.tj-icon-fallback {
|
||||||
display: block;
|
display: block;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
|
|||||||
@@ -0,0 +1,189 @@
|
|||||||
|
<template>
|
||||||
|
<view class="vsm" :class="[`vsm--${size}`, { 'vsm--active': active, 'vsm--on-dark': onDark }]">
|
||||||
|
<view class="vsm-body">
|
||||||
|
<view class="vsm-leaf" aria-hidden="true" />
|
||||||
|
<view class="vsm-face">
|
||||||
|
<view class="vsm-eye vsm-eye--l" />
|
||||||
|
<view class="vsm-eye vsm-eye--r" />
|
||||||
|
<view class="vsm-blush vsm-blush--l" />
|
||||||
|
<view class="vsm-blush vsm-blush--r" />
|
||||||
|
<view class="vsm-mouth" />
|
||||||
|
</view>
|
||||||
|
<view v-if="active" class="vsm-waves" aria-hidden="true">
|
||||||
|
<view class="vsm-wave vsm-wave--1" />
|
||||||
|
<view class="vsm-wave vsm-wave--2" />
|
||||||
|
<view class="vsm-wave vsm-wave--3" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
defineProps({
|
||||||
|
/** 正在按住说话 / 录音中 */
|
||||||
|
active: { type: Boolean, default: false },
|
||||||
|
/** 深色条背景上使用浅色卡通 */
|
||||||
|
onDark: { type: Boolean, default: false },
|
||||||
|
size: { type: String, default: 'md' }
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.vsm {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.vsm--md { width: 72rpx; height: 72rpx; }
|
||||||
|
.vsm--sm { width: 64rpx; height: 64rpx; }
|
||||||
|
|
||||||
|
.vsm-body {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vsm-leaf {
|
||||||
|
position: absolute;
|
||||||
|
top: 2rpx;
|
||||||
|
left: 50%;
|
||||||
|
width: 16rpx;
|
||||||
|
height: 16rpx;
|
||||||
|
margin-left: -8rpx;
|
||||||
|
border-radius: 0 100% 0 100%;
|
||||||
|
background: #34d399;
|
||||||
|
transform: rotate(-18deg);
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
.vsm--on-dark .vsm-leaf {
|
||||||
|
background: #a7f3d0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vsm-face {
|
||||||
|
position: relative;
|
||||||
|
width: 52rpx;
|
||||||
|
height: 52rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #fef9c3;
|
||||||
|
border: 3rpx solid #047857;
|
||||||
|
box-shadow: 0 4rpx 10rpx rgba(4, 120, 87, 0.18);
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
.vsm--sm .vsm-face {
|
||||||
|
width: 46rpx;
|
||||||
|
height: 46rpx;
|
||||||
|
}
|
||||||
|
.vsm--on-dark .vsm-face {
|
||||||
|
background: #fffbeb;
|
||||||
|
border-color: rgba(255, 255, 255, 0.85);
|
||||||
|
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vsm-eye {
|
||||||
|
position: absolute;
|
||||||
|
top: 16rpx;
|
||||||
|
width: 6rpx;
|
||||||
|
height: 8rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #064e3b;
|
||||||
|
}
|
||||||
|
.vsm--sm .vsm-eye { top: 14rpx; width: 5rpx; height: 7rpx; }
|
||||||
|
.vsm--on-dark .vsm-eye { background: #134e4a; }
|
||||||
|
.vsm-eye--l { left: 12rpx; }
|
||||||
|
.vsm-eye--r { right: 12rpx; }
|
||||||
|
.vsm--sm .vsm-eye--l { left: 10rpx; }
|
||||||
|
.vsm--sm .vsm-eye--r { right: 10rpx; }
|
||||||
|
|
||||||
|
.vsm-blush {
|
||||||
|
position: absolute;
|
||||||
|
top: 24rpx;
|
||||||
|
width: 10rpx;
|
||||||
|
height: 6rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(251, 113, 133, 0.45);
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
.vsm--sm .vsm-blush { top: 21rpx; width: 8rpx; height: 5rpx; }
|
||||||
|
.vsm-blush--l { left: 6rpx; }
|
||||||
|
.vsm-blush--r { right: 6rpx; }
|
||||||
|
.vsm--on-dark .vsm-blush { background: rgba(255, 255, 255, 0.35); }
|
||||||
|
|
||||||
|
.vsm-mouth {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
bottom: 10rpx;
|
||||||
|
width: 18rpx;
|
||||||
|
height: 8rpx;
|
||||||
|
margin-left: -9rpx;
|
||||||
|
border-radius: 0 0 18rpx 18rpx;
|
||||||
|
background: #047857;
|
||||||
|
transform-origin: center top;
|
||||||
|
}
|
||||||
|
.vsm--sm .vsm-mouth {
|
||||||
|
bottom: 9rpx;
|
||||||
|
width: 16rpx;
|
||||||
|
height: 7rpx;
|
||||||
|
margin-left: -8rpx;
|
||||||
|
}
|
||||||
|
.vsm--on-dark .vsm-mouth { background: #ecfdf5; }
|
||||||
|
|
||||||
|
.vsm-waves {
|
||||||
|
position: absolute;
|
||||||
|
right: -2rpx;
|
||||||
|
top: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 4rpx;
|
||||||
|
height: 28rpx;
|
||||||
|
margin-top: -14rpx;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
.vsm-wave {
|
||||||
|
width: 5rpx;
|
||||||
|
border-radius: 4rpx;
|
||||||
|
background: #047857;
|
||||||
|
animation: vsm-wave-jump 0.72s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
.vsm--on-dark .vsm-wave { background: rgba(255, 255, 255, 0.9); }
|
||||||
|
.vsm-wave--1 { height: 10rpx; animation-delay: 0s; }
|
||||||
|
.vsm-wave--2 { height: 18rpx; animation-delay: 0.12s; }
|
||||||
|
.vsm-wave--3 { height: 12rpx; animation-delay: 0.24s; }
|
||||||
|
|
||||||
|
.vsm--active .vsm-body {
|
||||||
|
animation: vsm-bob 0.9s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
.vsm--active .vsm-mouth {
|
||||||
|
animation: vsm-talk 0.32s ease-in-out infinite alternate;
|
||||||
|
}
|
||||||
|
.vsm--active .vsm-eye {
|
||||||
|
animation: vsm-blink 2.4s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes vsm-bob {
|
||||||
|
0%, 100% { transform: translateY(0); }
|
||||||
|
50% { transform: translateY(-4rpx); }
|
||||||
|
}
|
||||||
|
@keyframes vsm-talk {
|
||||||
|
0% {
|
||||||
|
transform: scaleY(0.45);
|
||||||
|
border-radius: 0 0 18rpx 18rpx;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scaleY(1.15);
|
||||||
|
height: 12rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes vsm-blink {
|
||||||
|
0%, 42%, 46%, 100% { transform: scaleY(1); }
|
||||||
|
44% { transform: scaleY(0.15); }
|
||||||
|
}
|
||||||
|
@keyframes vsm-wave-jump {
|
||||||
|
0%, 100% { transform: scaleY(0.45); opacity: 0.55; }
|
||||||
|
50% { transform: scaleY(1); opacity: 1; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,203 @@
|
|||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { requestAiStream, parsePartialBloodCarePlainText } from '../utils/aiStreamRequest.js'
|
||||||
|
|
||||||
|
const emptyBloodCarePlan = () => ({
|
||||||
|
summary: '',
|
||||||
|
blood_advice: '',
|
||||||
|
tcm_plan: '',
|
||||||
|
watch_points: [],
|
||||||
|
next_steps: '',
|
||||||
|
control_level: '',
|
||||||
|
control_label: '',
|
||||||
|
disclaimer: '',
|
||||||
|
analysis: [],
|
||||||
|
source: '',
|
||||||
|
date: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
/** AI 血糖照护建议(累计 7 天有记录后) */
|
||||||
|
export function useBloodCareAi(proxy, { diagnosisId, chartUseMockData, showUserToast, formatUserMessage }) {
|
||||||
|
const bloodCarePlan = ref(emptyBloodCarePlan())
|
||||||
|
const bloodCareLoading = ref(false)
|
||||||
|
const bloodCareStreaming = ref(false)
|
||||||
|
const bloodCareStreamField = ref('')
|
||||||
|
let careStreamBuffer = ''
|
||||||
|
/** 用户点击后才展开照护建议卡片 */
|
||||||
|
const bloodCarePanelOpen = ref(false)
|
||||||
|
|
||||||
|
const bloodCareEligible = computed(() => {
|
||||||
|
return !!diagnosisId.value && !chartUseMockData.value
|
||||||
|
})
|
||||||
|
|
||||||
|
const hasBloodCareContent = computed(() => {
|
||||||
|
const p = bloodCarePlan.value
|
||||||
|
return !!(p.summary || p.blood_advice || p.tcm_plan)
|
||||||
|
})
|
||||||
|
|
||||||
|
const bloodCareAnalysis = computed(() => {
|
||||||
|
const list = bloodCarePlan.value?.analysis
|
||||||
|
return Array.isArray(list) ? list : []
|
||||||
|
})
|
||||||
|
|
||||||
|
const bloodCareControlLabel = computed(() => {
|
||||||
|
const label = String(bloodCarePlan.value?.control_label || '').trim()
|
||||||
|
return label
|
||||||
|
})
|
||||||
|
|
||||||
|
const bloodCareDisclaimer = computed(() => {
|
||||||
|
return String(bloodCarePlan.value?.disclaimer || '').trim()
|
||||||
|
})
|
||||||
|
|
||||||
|
function detectCareStreamField(text) {
|
||||||
|
const tail = String(text || '').match(/(?:^|\n)(总评|血糖|中医|留意|复诊)[::]\s*([^\n]*)$/)
|
||||||
|
if (!tail) return 'summary'
|
||||||
|
const map = { '总评': 'summary', '血糖': 'blood_advice', '中医': 'tcm_plan', '留意': 'watch_points', '复诊': 'next_steps' }
|
||||||
|
return map[tail[1]] || 'summary'
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergePartialCareFromStream() {
|
||||||
|
const partial = parsePartialBloodCarePlainText(careStreamBuffer)
|
||||||
|
const next = { ...bloodCarePlan.value }
|
||||||
|
for (const key of ['summary', 'blood_advice', 'tcm_plan', 'next_steps']) {
|
||||||
|
if (partial[key] != null) next[key] = partial[key]
|
||||||
|
}
|
||||||
|
if (Array.isArray(partial.watch_points)) next.watch_points = partial.watch_points
|
||||||
|
bloodCarePlan.value = next
|
||||||
|
bloodCareStreamField.value = detectCareStreamField(careStreamBuffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyBloodCarePayload(payload) {
|
||||||
|
if (!payload) return
|
||||||
|
const prevAnalysis = bloodCarePlan.value.analysis
|
||||||
|
bloodCarePlan.value = {
|
||||||
|
...emptyBloodCarePlan(),
|
||||||
|
...payload,
|
||||||
|
watch_points: Array.isArray(payload.watch_points) ? payload.watch_points : [],
|
||||||
|
analysis: Array.isArray(payload.analysis) && payload.analysis.length
|
||||||
|
? payload.analysis
|
||||||
|
: (Array.isArray(prevAnalysis) ? prevAnalysis : [])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onBloodCareStreamEvent(event, payload) {
|
||||||
|
if (event === 'analysis') {
|
||||||
|
if (Array.isArray(payload?.analysis)) {
|
||||||
|
bloodCarePlan.value = { ...bloodCarePlan.value, analysis: payload.analysis }
|
||||||
|
}
|
||||||
|
} else if (event === 'delta') {
|
||||||
|
bloodCareStreaming.value = true
|
||||||
|
if (payload.buffer != null) {
|
||||||
|
careStreamBuffer = payload.buffer
|
||||||
|
} else {
|
||||||
|
careStreamBuffer += payload.text || ''
|
||||||
|
}
|
||||||
|
mergePartialCareFromStream()
|
||||||
|
} else if (event === 'done') {
|
||||||
|
applyBloodCarePayload(payload)
|
||||||
|
careStreamBuffer = ''
|
||||||
|
bloodCareStreamField.value = ''
|
||||||
|
} else if (event === 'error') {
|
||||||
|
showUserToast(formatUserMessage(payload?.message, '照护建议加载失败'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchBloodCareAiFallback(refresh) {
|
||||||
|
const res = await proxy.apiUrl({
|
||||||
|
url: '/api/tcm/dailyBloodCareAiRecommend',
|
||||||
|
method: 'GET',
|
||||||
|
timeout: 35000,
|
||||||
|
data: {
|
||||||
|
diagnosis_id: diagnosisId.value,
|
||||||
|
refresh: refresh ? 1 : 0,
|
||||||
|
...(refresh ? { _t: Date.now() } : {})
|
||||||
|
}
|
||||||
|
}, false)
|
||||||
|
if (res?.code === 1 && res.data) {
|
||||||
|
applyBloodCarePayload(res.data)
|
||||||
|
} else if (res?.msg) {
|
||||||
|
showUserToast(res.msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeBloodCarePanel() {
|
||||||
|
bloodCarePanelOpen.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 趋势图 AI 按钮:未展开则打开并生成;已展开则收起 */
|
||||||
|
async function toggleBloodCareFromChart() {
|
||||||
|
if (!bloodCareEligible.value || bloodCareLoading.value) return
|
||||||
|
if (bloodCarePanelOpen.value) {
|
||||||
|
closeBloodCarePanel()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
bloodCarePanelOpen.value = true
|
||||||
|
if (!hasBloodCareContent.value) {
|
||||||
|
await fetchBloodCareAi(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchBloodCareAi(refresh = false) {
|
||||||
|
if (!bloodCareEligible.value || bloodCareLoading.value) return
|
||||||
|
bloodCareLoading.value = true
|
||||||
|
bloodCareStreaming.value = false
|
||||||
|
careStreamBuffer = ''
|
||||||
|
bloodCareStreamField.value = ''
|
||||||
|
if (refresh) {
|
||||||
|
applyBloodCarePayload(emptyBloodCarePlan())
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await requestAiStream({
|
||||||
|
baseUrl: proxy.$url,
|
||||||
|
url: '/api/tcm/dailyBloodCareAiRecommendStream',
|
||||||
|
method: 'GET',
|
||||||
|
data: {
|
||||||
|
diagnosis_id: diagnosisId.value,
|
||||||
|
refresh: refresh ? 1 : 0,
|
||||||
|
...(refresh ? { _t: Date.now() } : {})
|
||||||
|
},
|
||||||
|
onEvent: onBloodCareStreamEvent,
|
||||||
|
fallback: () => fetchBloodCareAiFallback(refresh)
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
try {
|
||||||
|
await fetchBloodCareAiFallback(refresh)
|
||||||
|
} catch (e2) {
|
||||||
|
showUserToast('照护建议加载失败')
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
bloodCareLoading.value = false
|
||||||
|
bloodCareStreaming.value = false
|
||||||
|
bloodCareStreamField.value = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshBloodCareAi() {
|
||||||
|
if (bloodCareLoading.value) return
|
||||||
|
fetchBloodCareAi(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetBloodCarePlan() {
|
||||||
|
applyBloodCarePayload(emptyBloodCarePlan())
|
||||||
|
careStreamBuffer = ''
|
||||||
|
bloodCareStreamField.value = ''
|
||||||
|
bloodCarePanelOpen.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
bloodCarePlan,
|
||||||
|
bloodCareLoading,
|
||||||
|
bloodCareStreaming,
|
||||||
|
bloodCareStreamField,
|
||||||
|
bloodCareEligible,
|
||||||
|
bloodCarePanelOpen,
|
||||||
|
hasBloodCareContent,
|
||||||
|
bloodCareAnalysis,
|
||||||
|
bloodCareControlLabel,
|
||||||
|
bloodCareDisclaimer,
|
||||||
|
fetchBloodCareAi,
|
||||||
|
toggleBloodCareFromChart,
|
||||||
|
closeBloodCarePanel,
|
||||||
|
refreshBloodCareAi,
|
||||||
|
resetBloodCarePlan
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ const DIET_AI_PREFILL_KEY = 'tongji_diet_ai_prefill'
|
|||||||
|
|
||||||
const emptyDietAiRecommend = () => ({
|
const emptyDietAiRecommend = () => ({
|
||||||
breakfast: '',
|
breakfast: '',
|
||||||
|
drinks: '',
|
||||||
lunch: '',
|
lunch: '',
|
||||||
dinner: '',
|
dinner: '',
|
||||||
tips: '',
|
tips: '',
|
||||||
@@ -31,20 +32,20 @@ export function useDietAi(proxy, { diagnosisId, showUserToast, formatUserMessage
|
|||||||
|
|
||||||
const hasDietAiMealContent = computed(() => {
|
const hasDietAiMealContent = computed(() => {
|
||||||
const r = dietAiRecommend.value
|
const r = dietAiRecommend.value
|
||||||
return !!(r.breakfast || r.lunch || r.dinner || r.tips)
|
return !!(r.breakfast || r.drinks || r.lunch || r.dinner || r.tips)
|
||||||
})
|
})
|
||||||
|
|
||||||
function detectDietStreamField(text) {
|
function detectDietStreamField(text) {
|
||||||
const tail = String(text || '').match(/(?:^|\n)(早餐|午餐|晚餐|提示|少碰)[::]\s*([^\n]*)$/)
|
const tail = String(text || '').match(/(?:^|\n)(早餐|喝的|午餐|晚餐|提示|少碰)[::]\s*([^\n]*)$/)
|
||||||
if (!tail) return 'breakfast'
|
if (!tail) return 'breakfast'
|
||||||
const map = { '早餐': 'breakfast', '午餐': 'lunch', '晚餐': 'dinner', '提示': 'tips', '少碰': 'tips' }
|
const map = { '早餐': 'breakfast', '喝的': 'drinks', '午餐': 'lunch', '晚餐': 'dinner', '提示': 'tips', '少碰': 'tips' }
|
||||||
return map[tail[1]] || 'breakfast'
|
return map[tail[1]] || 'breakfast'
|
||||||
}
|
}
|
||||||
|
|
||||||
function mergePartialDietFromStream() {
|
function mergePartialDietFromStream() {
|
||||||
const partial = parsePartialDietPlainText(dietStreamBuffer)
|
const partial = parsePartialDietPlainText(dietStreamBuffer)
|
||||||
const next = { ...dietAiRecommend.value }
|
const next = { ...dietAiRecommend.value }
|
||||||
for (const key of ['breakfast', 'lunch', 'dinner', 'tips']) {
|
for (const key of ['breakfast', 'drinks', 'lunch', 'dinner', 'tips']) {
|
||||||
if (partial[key] != null) next[key] = partial[key]
|
if (partial[key] != null) next[key] = partial[key]
|
||||||
}
|
}
|
||||||
if (Array.isArray(partial.avoid)) next.avoid = partial.avoid
|
if (Array.isArray(partial.avoid)) next.avoid = partial.avoid
|
||||||
@@ -80,6 +81,7 @@ export function useDietAi(proxy, { diagnosisId, showUserToast, formatUserMessage
|
|||||||
...emptyDietAiRecommend(),
|
...emptyDietAiRecommend(),
|
||||||
...payload,
|
...payload,
|
||||||
breakfast: streamed.breakfast,
|
breakfast: streamed.breakfast,
|
||||||
|
drinks: streamed.drinks || payload.drinks || '',
|
||||||
lunch: streamed.lunch || payload.lunch || '',
|
lunch: streamed.lunch || payload.lunch || '',
|
||||||
dinner: streamed.dinner || payload.dinner || '',
|
dinner: streamed.dinner || payload.dinner || '',
|
||||||
tips: streamed.tips || payload.tips || '',
|
tips: streamed.tips || payload.tips || '',
|
||||||
@@ -205,6 +207,8 @@ export function useDietAi(proxy, { diagnosisId, showUserToast, formatUserMessage
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!diagnosisId.value) return
|
if (!diagnosisId.value) return
|
||||||
|
// 发送后立即清空输入框
|
||||||
|
dietAiAskText.value = ''
|
||||||
dietAiAsking.value = true
|
dietAiAsking.value = true
|
||||||
dietAiAskResult.value = { advice: '', level: '', level_label: '', portion: '', food: '' }
|
dietAiAskResult.value = { advice: '', level: '', level_label: '', portion: '', food: '' }
|
||||||
try {
|
try {
|
||||||
@@ -259,6 +263,7 @@ export function useDietAi(proxy, { diagnosisId, showUserToast, formatUserMessage
|
|||||||
try {
|
try {
|
||||||
uni.setStorageSync(DIET_AI_PREFILL_KEY, {
|
uni.setStorageSync(DIET_AI_PREFILL_KEY, {
|
||||||
breakfast: r.breakfast,
|
breakfast: r.breakfast,
|
||||||
|
drinks: r.drinks || '',
|
||||||
lunch: r.lunch,
|
lunch: r.lunch,
|
||||||
dinner: r.dinner,
|
dinner: r.dinner,
|
||||||
tips: r.tips || ''
|
tips: r.tips || ''
|
||||||
|
|||||||
@@ -0,0 +1,450 @@
|
|||||||
|
import { ref, onUnmounted } from 'vue'
|
||||||
|
|
||||||
|
const MIN_HOLD_MS = 280
|
||||||
|
|
||||||
|
/** 短按/过早松手时插件常见错误,不应打扰用户 */
|
||||||
|
function isBenignSttError(msg, retcode) {
|
||||||
|
const m = String(msg || '').toLowerCase()
|
||||||
|
if (/internal voice data failed/i.test(m)) return true
|
||||||
|
if (/voice data failed/i.test(m)) return true
|
||||||
|
if (/recordfailed|record failed|record manager/i.test(m)) return true
|
||||||
|
if (/please stop after start/i.test(m)) return true
|
||||||
|
if (/no recognition|not start|未开始|无识别/i.test(m)) return true
|
||||||
|
const code = Number(retcode)
|
||||||
|
if (code === -30002 || code === -30003 || code === -30012) return true
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
function friendlySttError(msg) {
|
||||||
|
const m = String(msg || '').trim()
|
||||||
|
if (!m || isBenignSttError(m)) return ''
|
||||||
|
if (/not allowed|auth|permission|权限|麦克风/i.test(m)) return '请允许使用麦克风'
|
||||||
|
if (/network|网络|timeout/i.test(m)) return '网络异常,请稍后再试'
|
||||||
|
return '语音识别失败,请重试'
|
||||||
|
}
|
||||||
|
|
||||||
|
// #ifdef MP-WEIXIN
|
||||||
|
// WechatSI 录音管理器是全局单例:回调只在模块级绑定一次,
|
||||||
|
// 再分发给「当前活跃实例」。否则跨页面(如 more → weekly)后,
|
||||||
|
// 回调仍指向已销毁页面的闭包,新页面收不到 onStart/onStop,
|
||||||
|
// 其本地状态卡在 recording,表现为一直录制/无法再次语音。
|
||||||
|
let sharedManager = null
|
||||||
|
let sharedManagerInited = false
|
||||||
|
let sharedRecording = false
|
||||||
|
let activeCtl = null
|
||||||
|
|
||||||
|
function getSharedRecordManager() {
|
||||||
|
if (sharedManagerInited) return sharedManager
|
||||||
|
sharedManagerInited = true
|
||||||
|
try {
|
||||||
|
// eslint-disable-next-line no-undef
|
||||||
|
const plugin = requirePlugin('WechatSI')
|
||||||
|
sharedManager = plugin.getRecordRecognitionManager()
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('WechatSI record recognition not available', e)
|
||||||
|
sharedManager = null
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
if (!sharedManager) return null
|
||||||
|
|
||||||
|
sharedManager.onStart = () => {
|
||||||
|
sharedRecording = true
|
||||||
|
if (activeCtl) activeCtl.handleStart()
|
||||||
|
}
|
||||||
|
sharedManager.onRecognize = (res) => {
|
||||||
|
if (activeCtl) activeCtl.handleRecognize(res)
|
||||||
|
}
|
||||||
|
sharedManager.onStop = (res) => {
|
||||||
|
sharedRecording = false
|
||||||
|
if (activeCtl) activeCtl.handleStop(res)
|
||||||
|
}
|
||||||
|
sharedManager.onError = (res) => {
|
||||||
|
sharedRecording = false
|
||||||
|
if (activeCtl) activeCtl.handleError(res)
|
||||||
|
}
|
||||||
|
return sharedManager
|
||||||
|
}
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 语音转文字(长按说话):微信小程序 WechatSI;H5 Web Speech API。
|
||||||
|
*
|
||||||
|
* @param {{
|
||||||
|
* onResult?: (text: string) => void,
|
||||||
|
* onSettle?: (text: string, info: { heldMs: number }) => boolean | void,
|
||||||
|
* onPartial?: (text: string) => void,
|
||||||
|
* onError?: (msg: string) => boolean | void,
|
||||||
|
* showToast?: (msg: string) => void
|
||||||
|
* }} options
|
||||||
|
*
|
||||||
|
* onSettle 在每次录音结束时都会触发(含空结果),用于语音一问一答等需要
|
||||||
|
* 完全接管识别结果的场景。若 onSettle 返回 true,则跳过默认的 onResult 回调
|
||||||
|
* 与「没听清」提示,避免与上层流程重复处理。
|
||||||
|
*
|
||||||
|
* onPartial 在录音过程中实时返回中间识别结果(微信 onRecognize / H5 interim),
|
||||||
|
* 用于「抢答打断」等场景:检测到用户已开口作答即可提前结束播报/录音。
|
||||||
|
*
|
||||||
|
* onError 在录音/识别出错时触发(如 "record manager recordfailed")。
|
||||||
|
* 若返回 true,则跳过默认的错误 Toast,由上层自行处理(如自动重试)。
|
||||||
|
*/
|
||||||
|
export function useSpeechToText({ onResult, onSettle, onPartial, onError, showToast } = {}) {
|
||||||
|
const sttListening = ref(false)
|
||||||
|
const sttHolding = ref(false)
|
||||||
|
const sttSupported = ref(false)
|
||||||
|
|
||||||
|
let wxRecordManager = null
|
||||||
|
let h5Recognition = null
|
||||||
|
let h5GotResult = false
|
||||||
|
let holdStartTs = 0
|
||||||
|
let holdSessionId = 0
|
||||||
|
let startRequested = false
|
||||||
|
let recordAuthorized = null
|
||||||
|
// 快速点按可能残留会话:用排队标记把 start/stop 串行化,
|
||||||
|
// 避免 "please stop after start"
|
||||||
|
let pendingStartSession = 0
|
||||||
|
|
||||||
|
function notify(msg) {
|
||||||
|
if (!msg) return
|
||||||
|
if (typeof showToast === 'function') {
|
||||||
|
showToast(msg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uni.showToast({ title: msg, icon: 'none', duration: 2000 })
|
||||||
|
}
|
||||||
|
|
||||||
|
function emitResult(text) {
|
||||||
|
const t = String(text || '').trim()
|
||||||
|
const heldMs = Date.now() - holdStartTs
|
||||||
|
// onSettle 总会收到结果(含空),返回 true 表示已完全接管
|
||||||
|
if (typeof onSettle === 'function') {
|
||||||
|
const handled = onSettle(t, { heldMs })
|
||||||
|
if (handled === true) return
|
||||||
|
}
|
||||||
|
if (!t) {
|
||||||
|
if (heldMs < MIN_HOLD_MS) return
|
||||||
|
notify('没听清,请再试一次')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (typeof onResult === 'function') {
|
||||||
|
onResult(t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopListening() {
|
||||||
|
// #ifdef MP-WEIXIN
|
||||||
|
const ownsRecording = sharedRecording && activeCtl === controller
|
||||||
|
// 仅在识别已真正开始后再 stop,避免 -30012 / internal voice data failed
|
||||||
|
if (wxRecordManager && (sttListening.value || ownsRecording)) {
|
||||||
|
try {
|
||||||
|
wxRecordManager.stop()
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
// #ifdef H5
|
||||||
|
if (h5Recognition && sttListening.value) {
|
||||||
|
try {
|
||||||
|
h5Recognition.stop()
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
startRequested = false
|
||||||
|
sttHolding.value = false
|
||||||
|
sttListening.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureRecordAuth(onGranted) {
|
||||||
|
// #ifdef MP-WEIXIN
|
||||||
|
if (recordAuthorized === true) {
|
||||||
|
onGranted()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uni.authorize({
|
||||||
|
scope: 'scope.record',
|
||||||
|
success() {
|
||||||
|
recordAuthorized = true
|
||||||
|
onGranted()
|
||||||
|
},
|
||||||
|
fail() {
|
||||||
|
recordAuthorized = false
|
||||||
|
sttHolding.value = false
|
||||||
|
uni.showModal({
|
||||||
|
title: '需要麦克风权限',
|
||||||
|
content: '请在设置中允许使用麦克风,以便长按说话',
|
||||||
|
confirmText: '去设置',
|
||||||
|
success(res) {
|
||||||
|
if (res.confirm) {
|
||||||
|
uni.openSetting({})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// #endif
|
||||||
|
// #ifndef MP-WEIXIN
|
||||||
|
onGranted()
|
||||||
|
// #endif
|
||||||
|
}
|
||||||
|
|
||||||
|
// #ifdef MP-WEIXIN
|
||||||
|
function actuallyStartWechat(session) {
|
||||||
|
if (!wxRecordManager || session !== holdSessionId || !sttHolding.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
startRequested = true
|
||||||
|
wxRecordManager.start({
|
||||||
|
duration: 60000,
|
||||||
|
lang: 'zh_CN'
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
startRequested = false
|
||||||
|
sharedRecording = false
|
||||||
|
sttHolding.value = false
|
||||||
|
notify('无法开始录音')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
function startWechatRecord(session) {
|
||||||
|
// #ifdef MP-WEIXIN
|
||||||
|
if (!wxRecordManager || session !== holdSessionId || !sttHolding.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 上一段会话还没结束(含全局单例残留):先停止并排队,
|
||||||
|
// 待 onStop 回调后再真正开始,避免 "please stop after start"
|
||||||
|
if (sharedRecording || startRequested) {
|
||||||
|
pendingStartSession = session
|
||||||
|
try {
|
||||||
|
wxRecordManager.stop()
|
||||||
|
} catch (e) {}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
actuallyStartWechat(session)
|
||||||
|
// #endif
|
||||||
|
}
|
||||||
|
|
||||||
|
function startH5Record() {
|
||||||
|
// #ifdef H5
|
||||||
|
if (!h5Recognition) {
|
||||||
|
notify('当前浏览器不支持语音输入')
|
||||||
|
sttHolding.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
startRequested = true
|
||||||
|
h5GotResult = false
|
||||||
|
h5Recognition.start()
|
||||||
|
sttListening.value = true
|
||||||
|
} catch (e) {
|
||||||
|
startRequested = false
|
||||||
|
sttHolding.value = false
|
||||||
|
notify('无法开始语音识别,请检查麦克风权限')
|
||||||
|
}
|
||||||
|
// #endif
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 长按开始:手指按下 */
|
||||||
|
function beginHoldSpeech() {
|
||||||
|
if (!sttSupported.value) {
|
||||||
|
notify('当前环境不支持语音输入')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (sttHolding.value) return
|
||||||
|
|
||||||
|
const session = ++holdSessionId
|
||||||
|
holdStartTs = Date.now()
|
||||||
|
sttHolding.value = true
|
||||||
|
|
||||||
|
// #ifdef MP-WEIXIN
|
||||||
|
// 把全局录音回调切到当前实例
|
||||||
|
activeCtl = controller
|
||||||
|
ensureRecordAuth(() => startWechatRecord(session))
|
||||||
|
// #endif
|
||||||
|
// #ifdef H5
|
||||||
|
startH5Record()
|
||||||
|
// #endif
|
||||||
|
// #ifndef MP-WEIXIN
|
||||||
|
// #ifndef H5
|
||||||
|
sttHolding.value = false
|
||||||
|
notify('当前端暂不支持语音输入')
|
||||||
|
// #endif
|
||||||
|
// #endif
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 松手结束:手指抬起;force=true 时强制取消尚未真正开始的按住会话 */
|
||||||
|
function endHoldSpeech(force = false) {
|
||||||
|
let recording = false
|
||||||
|
// #ifdef MP-WEIXIN
|
||||||
|
recording = sharedRecording && activeCtl === controller
|
||||||
|
// #endif
|
||||||
|
if (!force && !sttHolding.value && !sttListening.value && !startRequested && !recording) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sttHolding.value = false
|
||||||
|
// 松手后不再补开排队的录音
|
||||||
|
pendingStartSession = 0
|
||||||
|
if (sttListening.value || recording) {
|
||||||
|
stopListening()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (startRequested) {
|
||||||
|
// start 已发出但 onStart 未到:取消会话,勿调 stop()
|
||||||
|
startRequested = false
|
||||||
|
holdSessionId += 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
holdSessionId += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// #ifdef MP-WEIXIN
|
||||||
|
// 当前实例的回调控制器:全局单例 manager 的事件由模块级分发器
|
||||||
|
// 转发给 activeCtl(最后一次 beginHoldSpeech 的实例)
|
||||||
|
const controller = {
|
||||||
|
handleStart() {
|
||||||
|
if (!sttHolding.value) {
|
||||||
|
// 用户已松手,忽略迟到的 onStart,不再 stop() 以免触发插件报错
|
||||||
|
startRequested = false
|
||||||
|
sttListening.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
startRequested = false
|
||||||
|
sttListening.value = true
|
||||||
|
},
|
||||||
|
handleRecognize(res) {
|
||||||
|
if (typeof onPartial !== 'function') return
|
||||||
|
const t = String(res?.result || '').trim()
|
||||||
|
if (t) onPartial(t)
|
||||||
|
},
|
||||||
|
handleStop(res) {
|
||||||
|
sttListening.value = false
|
||||||
|
startRequested = false
|
||||||
|
// 有排队的开始请求(残留会话已停止 / 用户仍按住):现在再真正开始
|
||||||
|
if (pendingStartSession && pendingStartSession === holdSessionId && sttHolding.value) {
|
||||||
|
const s = pendingStartSession
|
||||||
|
pendingStartSession = 0
|
||||||
|
actuallyStartWechat(s)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pendingStartSession = 0
|
||||||
|
emitResult(res?.result)
|
||||||
|
},
|
||||||
|
handleError(res) {
|
||||||
|
sttListening.value = false
|
||||||
|
startRequested = false
|
||||||
|
const msg = (res && res.msg) || ''
|
||||||
|
const retcode = res && (res.retcode ?? res.errCode ?? res.code)
|
||||||
|
// 上一段未停止就再次 start 触发:停止后若仍按住则自动重试,不打扰用户
|
||||||
|
if (/please stop after start/i.test(msg)) {
|
||||||
|
try {
|
||||||
|
wxRecordManager.stop()
|
||||||
|
} catch (e) {}
|
||||||
|
pendingStartSession = sttHolding.value ? holdSessionId : 0
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pendingStartSession = 0
|
||||||
|
sttHolding.value = false
|
||||||
|
const heldMs = Date.now() - holdStartTs
|
||||||
|
if (isBenignSttError(msg, retcode) || heldMs < MIN_HOLD_MS) {
|
||||||
|
if (typeof onError === 'function') onError(msg || '')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 上层(如一问一答流程)可接管错误并自行重试,返回 true 时不再弹默认提示
|
||||||
|
if (typeof onError === 'function') {
|
||||||
|
const handled = onError(msg || '')
|
||||||
|
if (handled === true) return
|
||||||
|
}
|
||||||
|
const tip = friendlySttError(msg)
|
||||||
|
if (tip) notify(tip)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wxRecordManager = getSharedRecordManager()
|
||||||
|
sttSupported.value = !!wxRecordManager
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
// #ifdef H5
|
||||||
|
try {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
const SR = window.SpeechRecognition || window.webkitSpeechRecognition
|
||||||
|
if (SR) {
|
||||||
|
h5Recognition = new SR()
|
||||||
|
h5Recognition.lang = 'zh-CN'
|
||||||
|
h5Recognition.interimResults = true
|
||||||
|
h5Recognition.continuous = false
|
||||||
|
h5Recognition.maxAlternatives = 1
|
||||||
|
sttSupported.value = true
|
||||||
|
|
||||||
|
h5Recognition.onresult = (event) => {
|
||||||
|
let interim = ''
|
||||||
|
let finalText = ''
|
||||||
|
for (let i = event.resultIndex; i < event.results.length; i++) {
|
||||||
|
const r = event.results[i]
|
||||||
|
const txt = r?.[0]?.transcript || ''
|
||||||
|
if (r.isFinal) finalText += txt
|
||||||
|
else interim += txt
|
||||||
|
}
|
||||||
|
if (interim && typeof onPartial === 'function') onPartial(interim)
|
||||||
|
if (finalText) {
|
||||||
|
h5GotResult = true
|
||||||
|
emitResult(finalText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
h5Recognition.onend = () => {
|
||||||
|
sttListening.value = false
|
||||||
|
startRequested = false
|
||||||
|
sttHolding.value = false
|
||||||
|
// 无识别结果也要兜底触发一次 settle,便于一问一答流程重试
|
||||||
|
if (!h5GotResult) {
|
||||||
|
emitResult('')
|
||||||
|
}
|
||||||
|
h5GotResult = false
|
||||||
|
}
|
||||||
|
h5Recognition.onerror = (event) => {
|
||||||
|
sttListening.value = false
|
||||||
|
startRequested = false
|
||||||
|
sttHolding.value = false
|
||||||
|
const err = event?.error || ''
|
||||||
|
if (err === 'aborted') return
|
||||||
|
if (typeof onError === 'function') {
|
||||||
|
const handled = onError(err)
|
||||||
|
if (handled === true) return
|
||||||
|
}
|
||||||
|
if (err === 'not-allowed') {
|
||||||
|
notify('请允许浏览器使用麦克风')
|
||||||
|
} else {
|
||||||
|
notify('语音识别失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('H5 SpeechRecognition not available', e)
|
||||||
|
}
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
sttHolding.value = false
|
||||||
|
holdSessionId += 1
|
||||||
|
pendingStartSession = 0
|
||||||
|
stopListening()
|
||||||
|
// #ifdef MP-WEIXIN
|
||||||
|
// 释放全局回调指向,避免事件继续派发给已销毁的实例
|
||||||
|
if (activeCtl === controller) {
|
||||||
|
activeCtl = null
|
||||||
|
}
|
||||||
|
// #endif
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
sttListening,
|
||||||
|
sttHolding,
|
||||||
|
sttSupported,
|
||||||
|
beginHoldSpeech,
|
||||||
|
endHoldSpeech,
|
||||||
|
stopSpeechToText: endHoldSpeech
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { isBenignSttError, friendlySttError }
|
||||||
@@ -0,0 +1,596 @@
|
|||||||
|
<!DOCTYPE html><html class="light" lang="zh-CN"><head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta content="width=device-width, initial-scale=1.0" name="viewport">
|
||||||
|
<title>Daily Care - Health Dashboard</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet">
|
||||||
|
<script id="tailwind-config">
|
||||||
|
tailwind.config = {
|
||||||
|
darkMode: "class",
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
"colors": {
|
||||||
|
"secondary-fixed": "#d9e6dd",
|
||||||
|
"surface-container": "#e8f0e9",
|
||||||
|
"surface-dim": "#d4dcd5",
|
||||||
|
"surface-variant": "#dde4dd",
|
||||||
|
"error-container": "#ffdad6",
|
||||||
|
"inverse-surface": "#2b322d",
|
||||||
|
"secondary": "#55615a",
|
||||||
|
"primary": "#006c49",
|
||||||
|
"on-secondary-fixed": "#131e19",
|
||||||
|
"surface-bright": "#f4fbf4",
|
||||||
|
"on-tertiary-container": "#711419",
|
||||||
|
"on-primary-fixed-variant": "#005236",
|
||||||
|
"outline-variant": "#bbcabf",
|
||||||
|
"primary-container": "#10b981",
|
||||||
|
"surface": "#f4fbf4",
|
||||||
|
"on-tertiary-fixed-variant": "#842225",
|
||||||
|
"tertiary-fixed-dim": "#ffb3af",
|
||||||
|
"tertiary-fixed": "#ffdad7",
|
||||||
|
"error": "#ba1a1a",
|
||||||
|
"surface-container-lowest": "#ffffff",
|
||||||
|
"surface-container-highest": "#dde4dd",
|
||||||
|
"primary-fixed": "#6ffbbe",
|
||||||
|
"on-primary-container": "#00422b",
|
||||||
|
"secondary-fixed-dim": "#bdcac1",
|
||||||
|
"on-tertiary": "#ffffff",
|
||||||
|
"on-primary-fixed": "#002113",
|
||||||
|
"on-error-container": "#93000a",
|
||||||
|
"tertiary-container": "#fc7c78",
|
||||||
|
"on-error": "#ffffff",
|
||||||
|
"inverse-primary": "#4edea3",
|
||||||
|
"inverse-on-surface": "#ebf3eb",
|
||||||
|
"on-secondary-container": "#5b6760",
|
||||||
|
"surface-container-high": "#e3eae3",
|
||||||
|
"surface-container-low": "#eef6ee",
|
||||||
|
"on-secondary-fixed-variant": "#3e4943",
|
||||||
|
"outline": "#6c7a71",
|
||||||
|
"secondary-container": "#d9e6dd",
|
||||||
|
"on-primary": "#ffffff",
|
||||||
|
"on-surface": "#161d19",
|
||||||
|
"on-background": "#161d19",
|
||||||
|
"tertiary": "#a43a3a",
|
||||||
|
"primary-fixed-dim": "#4edea3",
|
||||||
|
"on-secondary": "#ffffff",
|
||||||
|
"background": "#f4fbf4",
|
||||||
|
"on-surface-variant": "#3c4a42",
|
||||||
|
"surface-tint": "#006c49",
|
||||||
|
"on-tertiary-fixed": "#410005"
|
||||||
|
},
|
||||||
|
"borderRadius": {
|
||||||
|
"DEFAULT": "0.25rem",
|
||||||
|
"lg": "0.5rem",
|
||||||
|
"xl": "0.75rem",
|
||||||
|
"2xl": "1rem",
|
||||||
|
"3xl": "1.5rem",
|
||||||
|
"full": "9999px"
|
||||||
|
},
|
||||||
|
"spacing": {
|
||||||
|
"card-padding": "20px",
|
||||||
|
"section-margin": "32px",
|
||||||
|
"inline-gap": "12px",
|
||||||
|
"stack-gap": "16px",
|
||||||
|
"container-margin": "20px"
|
||||||
|
},
|
||||||
|
"fontFamily": {
|
||||||
|
"headline-md": ["Manrope"],
|
||||||
|
"headline-lg-mobile": ["Manrope"],
|
||||||
|
"body-lg": ["Manrope"],
|
||||||
|
"headline-lg": ["Manrope"],
|
||||||
|
"display-lg": ["Manrope"],
|
||||||
|
"body-md": ["Manrope"],
|
||||||
|
"label-md": ["Manrope"]
|
||||||
|
},
|
||||||
|
"fontSize": {
|
||||||
|
"headline-md": ["20px", {"lineHeight": "28px", "fontWeight": "700"}],
|
||||||
|
"headline-lg-mobile": ["22px", {"lineHeight": "28px", "fontWeight": "700"}],
|
||||||
|
"body-lg": ["16px", {"lineHeight": "24px", "fontWeight": "500"}],
|
||||||
|
"headline-lg": ["24px", {"lineHeight": "32px", "letterSpacing": "-0.01em", "fontWeight": "700"}],
|
||||||
|
"display-lg": ["32px", {"lineHeight": "40px", "letterSpacing": "-0.02em", "fontWeight": "800"}],
|
||||||
|
"body-md": ["14px", {"lineHeight": "20px", "fontWeight": "400"}],
|
||||||
|
"label-md": ["12px", {"lineHeight": "16px", "letterSpacing": "0.02em", "fontWeight": "600"}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
body { font-family: 'Manrope', sans-serif; -webkit-tap-highlight-color: transparent; }
|
||||||
|
.material-symbols-outlined { font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24; display: inline-block; vertical-align: middle; }
|
||||||
|
.ambient-shadow { box-shadow: 0 20px 30px -10px rgba(0, 108, 73, 0.08); }
|
||||||
|
.active-scale:active { transform: scale(0.98); transition: all 0.2s ease; }
|
||||||
|
.hide-scrollbar::-webkit-scrollbar { display: none; }
|
||||||
|
.hide-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
|
||||||
|
|
||||||
|
/* Watering Animation Styles */
|
||||||
|
@keyframes fall {
|
||||||
|
0% { transform: translateY(-20px); opacity: 0; }
|
||||||
|
50% { opacity: 1; }
|
||||||
|
100% { transform: translateY(40px); opacity: 0; }
|
||||||
|
}
|
||||||
|
.droplet {
|
||||||
|
position: absolute;
|
||||||
|
width: 4px;
|
||||||
|
height: 10px;
|
||||||
|
background: #3b82f6;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: fall 0.6s linear infinite;
|
||||||
|
}
|
||||||
|
@keyframes pulse-growth {
|
||||||
|
0% { transform: scale(1); filter: drop-shadow(0 0 0px rgba(78, 222, 163, 0)); }
|
||||||
|
50% { transform: scale(1.15); filter: drop-shadow(0 0 15px rgba(78, 222, 163, 0.6)); }
|
||||||
|
100% { transform: scale(1); filter: drop-shadow(0 0 0px rgba(78, 222, 163, 0)); }
|
||||||
|
}
|
||||||
|
.growth-pulse {
|
||||||
|
animation: pulse-growth 0.8s ease-out;
|
||||||
|
}
|
||||||
|
@keyframes shimmer {
|
||||||
|
0% { opacity: 0; transform: scale(0) rotate(0deg); }
|
||||||
|
50% { opacity: 1; transform: scale(1.2) rotate(180deg); }
|
||||||
|
100% { opacity: 0; transform: scale(0) rotate(360deg); }
|
||||||
|
}
|
||||||
|
.shimmer-particle {
|
||||||
|
position: absolute;
|
||||||
|
color: #4edea3;
|
||||||
|
font-size: 14px;
|
||||||
|
pointer-events: none;
|
||||||
|
animation: shimmer 1s ease-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Blood Sugar Decrease Animation */
|
||||||
|
@keyframes float-up-fade {
|
||||||
|
0% { transform: translateY(20px); opacity: 0; }
|
||||||
|
20% { opacity: 1; }
|
||||||
|
80% { opacity: 1; }
|
||||||
|
100% { transform: translateY(-100px); opacity: 0; }
|
||||||
|
}
|
||||||
|
.sugar-decrease-text {
|
||||||
|
position: fixed;
|
||||||
|
color: #10b981;
|
||||||
|
font-weight: 800;
|
||||||
|
font-size: 24px;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 100;
|
||||||
|
text-shadow: 0 4px 12px rgba(16, 185, 129, 0.3);
|
||||||
|
animation: float-up-fade 2s ease-out forwards;
|
||||||
|
}
|
||||||
|
@keyframes fall-arrow {
|
||||||
|
0% { transform: translateY(-50vh) scale(0.5); opacity: 0; }
|
||||||
|
50% { opacity: 0.8; }
|
||||||
|
100% { transform: translateY(110vh) scale(1.2); opacity: 0; }
|
||||||
|
}
|
||||||
|
.sugar-particle {
|
||||||
|
position: fixed;
|
||||||
|
color: #10b981;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 90;
|
||||||
|
animation: fall-arrow 1.5s linear forwards;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
min-height: max(884px, 100dvh);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="bg-background text-on-background min-h-screen pb-24">
|
||||||
|
<!-- Top App Bar -->
|
||||||
|
<header class="bg-primary docked full-width top-0 h-48 flex items-end pb-6 px-5 fixed left-0 right-0 z-40">
|
||||||
|
<div class="flex justify-between items-center w-full">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="w-10 h-10 rounded-full border-2 border-on-primary overflow-hidden">
|
||||||
|
<img class="w-full h-full object-cover" data-alt="A professional studio portrait" src="https://lh3.googleusercontent.com/aida-public/AB6AXuCXIPbeivladmw0-SmLno2vkwVvsObW2D5HUtodItlBnxdhFF8TvZdQYrTt14QCk3n5VXXIpdk3cuWTJHXJgDXd_clQyjLsb477e5h2313zh76yRVns01Q5pLpM-2_RbheRcUioLvObwJtIPJbkDcbkNA61-LyBnbStRcPoh6YOvNHSHjUUR_nDJpPm18ga87ggphbFWF-pt7lRF027swmyitUeVF7HXQDfcG7Mq7E0pqsbhyA7jKxtzmbN-HLh7GIrhc6LOAUaXiX9">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h1 class="text-on-primary font-bold text-headline-lg-mobile leading-tight">Daily Care</h1>
|
||||||
|
<p class="text-on-primary/80 text-body-md">吕帅</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<button class="bg-on-primary/10 hover:bg-on-primary/20 text-on-primary rounded-full px-4 py-1.5 flex items-center gap-2 active-scale transition-all">
|
||||||
|
<span class="material-symbols-outlined text-[18px]" style="font-variation-settings: 'FILL' 1;">bloodtype</span>
|
||||||
|
<span class="text-label-md">血糖</span>
|
||||||
|
</button>
|
||||||
|
<button class="bg-on-primary/10 hover:bg-on-primary/20 text-on-primary w-10 h-10 rounded-full flex items-center justify-center active-scale transition-all">
|
||||||
|
<span class="material-symbols-outlined text-[20px]">refresh</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<main class="pt-52 px-container-margin space-y-6">
|
||||||
|
<!-- Toggle Switch -->
|
||||||
|
<div class="bg-surface-container-high rounded-full p-1 flex items-center">
|
||||||
|
<button class="flex-1 bg-primary text-on-primary rounded-full py-2 text-label-md font-bold shadow-md transition-all">最近 7 天</button>
|
||||||
|
<button class="flex-1 text-on-surface-variant py-2 text-label-md font-bold transition-all">最近 30 天</button>
|
||||||
|
</div>
|
||||||
|
<!-- Health Calendar -->
|
||||||
|
<section class="bg-surface-container-lowest rounded-3xl p-card-padding ambient-shadow">
|
||||||
|
<div class="flex justify-between items-start mb-4">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-headline-md font-headline-md text-on-surface">健康日历</h2>
|
||||||
|
<p class="text-body-md text-outline">2026年5月 · 已记录 3 / 7 天</p>
|
||||||
|
</div>
|
||||||
|
<button class="text-primary"><span class="material-symbols-outlined">chevron_right</span></button>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-7 gap-1 text-center mb-4">
|
||||||
|
<span class="text-label-md text-outline">一</span>
|
||||||
|
<span class="text-label-md text-outline">二</span>
|
||||||
|
<span class="text-label-md text-outline">三</span>
|
||||||
|
<span class="text-label-md text-outline">四</span>
|
||||||
|
<span class="text-label-md text-outline">五</span>
|
||||||
|
<span class="text-label-md text-outline">六</span>
|
||||||
|
<span class="text-label-md text-tertiary">日</span>
|
||||||
|
<span class="py-2 text-body-md text-outline/30">18</span>
|
||||||
|
<span class="py-2 text-body-md text-outline/30">19</span>
|
||||||
|
<span class="py-2 text-body-md text-outline/30">20</span>
|
||||||
|
<span class="py-2 text-body-md text-outline/30">21</span>
|
||||||
|
<span class="py-2 text-body-md text-outline/30">22</span>
|
||||||
|
<div class="py-2 relative"><span class="text-body-md font-bold text-on-surface">23</span></div>
|
||||||
|
<div class="py-2 relative bg-primary-container/10 rounded-lg">
|
||||||
|
<span class="text-body-md font-bold text-primary">24</span>
|
||||||
|
<span class="absolute bottom-1 left-1/2 -translate-x-1/2 w-1 h-1 bg-primary rounded-full"></span>
|
||||||
|
</div>
|
||||||
|
<div class="py-2 relative bg-primary-container/10 rounded-lg">
|
||||||
|
<span class="text-body-md font-bold text-primary">25</span>
|
||||||
|
<span class="absolute bottom-1 left-1/2 -translate-x-1/2 w-1 h-1 bg-primary rounded-full"></span>
|
||||||
|
</div>
|
||||||
|
<div class="py-2 relative bg-primary-container/10 rounded-lg">
|
||||||
|
<span class="text-body-md font-bold text-primary">26</span>
|
||||||
|
<span class="absolute bottom-1 left-1/2 -translate-x-1/2 w-1 h-1 bg-primary rounded-full"></span>
|
||||||
|
</div>
|
||||||
|
<span class="py-2 text-body-md text-on-surface">27</span>
|
||||||
|
<span class="py-2 text-body-md text-on-surface">28</span>
|
||||||
|
<div class="py-2 relative bg-tertiary-container rounded-lg">
|
||||||
|
<span class="text-body-md font-bold text-on-tertiary">29</span>
|
||||||
|
</div>
|
||||||
|
<span class="py-2 text-body-md text-outline/40">30</span>
|
||||||
|
<span class="py-2 text-body-md text-outline/40">31</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between border-t border-outline-variant/30 pt-4 mt-2">
|
||||||
|
<span class="text-body-md text-outline">今日还没有记录</span>
|
||||||
|
<button class="bg-primary text-on-primary px-6 py-3 rounded-2xl text-label-md font-bold active-scale transition-all flex items-center gap-2">去记血糖</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<!-- Blood Sugar Details Section -->
|
||||||
|
<section class="space-y-4">
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<h3 class="text-headline-md font-headline-md text-on-surface">血糖明细</h3>
|
||||||
|
<span class="text-body-md text-outline">共 3 天</span>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="bg-surface-container-lowest rounded-3xl p-card-padding ambient-shadow">
|
||||||
|
<div class="flex justify-between items-center mb-4">
|
||||||
|
<span class="text-headline-md font-bold">05-26</span>
|
||||||
|
<span class="text-body-md text-outline">周二</span>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div class="bg-background/50 border border-outline-variant/20 rounded-2xl p-4 flex justify-between items-center">
|
||||||
|
<span class="text-body-md text-outline">空腹</span>
|
||||||
|
<div class="text-right">
|
||||||
|
<span class="text-headline-lg font-extrabold text-error">10.4</span>
|
||||||
|
<span class="text-label-md text-outline ml-1">mmol/L</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-background/50 border border-outline-variant/20 rounded-2xl p-4 flex justify-between items-center">
|
||||||
|
<span class="text-body-md text-outline">餐后</span>
|
||||||
|
<span class="text-headline-md font-bold text-outline-variant">—— <small class="text-label-md">mmol/L</small></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-surface-container-lowest rounded-3xl p-card-padding ambient-shadow">
|
||||||
|
<div class="flex justify-between items-center mb-4">
|
||||||
|
<span class="text-headline-md font-bold">05-25</span>
|
||||||
|
<span class="text-body-md text-outline">周一</span>
|
||||||
|
</div>
|
||||||
|
<div class="bg-background/50 border border-outline-variant/20 rounded-2xl p-4 flex justify-between items-center">
|
||||||
|
<span class="text-body-md text-outline">空腹</span>
|
||||||
|
<div class="text-right">
|
||||||
|
<span class="text-headline-lg font-extrabold text-error">10.2</span>
|
||||||
|
<span class="text-label-md text-outline ml-1">mmol/L</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<!-- Reward Section: 稳糖乐园 -->
|
||||||
|
<section class="bg-surface-container-lowest rounded-3xl p-card-padding ambient-shadow overflow-hidden relative" id="sugar-paradise">
|
||||||
|
<div class="flex justify-between items-start mb-6">
|
||||||
|
<div>
|
||||||
|
<h3 class="text-headline-md font-headline-md text-on-surface">稳糖乐园</h3>
|
||||||
|
<p class="text-body-md text-outline">用健康打卡浇灌你的稳糖树</p>
|
||||||
|
</div>
|
||||||
|
<div class="bg-primary/10 rounded-xl px-3 py-1 text-center">
|
||||||
|
<span class="block text-headline-lg font-extrabold text-primary" id="total-points">110</span>
|
||||||
|
<span class="text-[10px] text-primary/70 font-bold">稳糖积分</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Mini Quick Actions -->
|
||||||
|
<div class="grid grid-cols-4 gap-2 mb-8">
|
||||||
|
<button class="bg-surface-container-low p-3 rounded-2xl flex flex-col items-center gap-1 active-scale transition-all">
|
||||||
|
<span class="material-symbols-outlined text-primary" style="font-variation-settings: 'FILL' 1;">bloodtype</span>
|
||||||
|
<span class="text-label-md text-on-surface">血糖</span>
|
||||||
|
</button>
|
||||||
|
<button class="bg-surface-container-low p-3 rounded-2xl flex flex-col items-center gap-1 active-scale transition-all">
|
||||||
|
<span class="material-symbols-outlined text-primary">monitoring</span>
|
||||||
|
<span class="text-label-md text-on-surface">血压</span>
|
||||||
|
</button>
|
||||||
|
<button class="bg-surface-container-low p-3 rounded-2xl flex flex-col items-center gap-1 active-scale transition-all">
|
||||||
|
<span class="material-symbols-outlined text-primary">restaurant</span>
|
||||||
|
<span class="text-label-md text-on-surface">饮食</span>
|
||||||
|
</button>
|
||||||
|
<button class="bg-surface-container-low p-3 rounded-2xl flex flex-col items-center gap-1 active-scale transition-all">
|
||||||
|
<span class="material-symbols-outlined text-primary">exercise</span>
|
||||||
|
<span class="text-label-md text-on-surface">运动</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<h4 class="text-label-md font-bold text-on-surface mb-3">今日控糖任务</h4>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="w-8 h-8 rounded-full border-2 border-primary flex items-center justify-center">
|
||||||
|
<span class="material-symbols-outlined text-[16px] text-primary" style="font-variation-settings: 'wght' 700;">check</span>
|
||||||
|
</div>
|
||||||
|
<span class="text-body-md text-on-surface">测血糖血压</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<span class="text-label-md text-primary font-bold">+10积分</span>
|
||||||
|
<span class="text-outline text-label-md font-bold italic">已完成</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="w-8 h-8 rounded-full border-2 border-outline-variant/30 flex items-center justify-center">
|
||||||
|
<span class="w-2 h-2 rounded-full bg-outline-variant"></span>
|
||||||
|
</div>
|
||||||
|
<span class="text-body-md text-on-surface">记今日饮食</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<span class="text-label-md text-primary font-bold">+10积分</span>
|
||||||
|
<button class="bg-on-surface text-surface rounded-full px-4 py-1 text-label-md font-bold active-scale transition-all">去记录</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Optimized Level System & Gamification -->
|
||||||
|
<div class="mt-8 bg-gradient-to-br from-primary/5 to-primary/20 rounded-3xl p-6 border border-primary/10 relative overflow-hidden">
|
||||||
|
<!-- Level Indicator Header -->
|
||||||
|
<div class="flex justify-between items-end mb-6">
|
||||||
|
<div class="space-y-1">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="bg-primary text-on-primary text-[10px] px-2 py-0.5 rounded-full font-extrabold" id="level-label">Lv.4</span>
|
||||||
|
<span class="text-body-lg font-extrabold text-primary" id="stage-name">枝繁叶茂</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-[11px] text-primary/60">还差 <span class="font-bold">15 XP</span> 升级至 Lv.4</p>
|
||||||
|
</div>
|
||||||
|
<div class="text-right">
|
||||||
|
<span class="text-label-md font-bold text-primary" id="xp-text">5/50 XP</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Progress Bar -->
|
||||||
|
<div class="w-full h-2.5 bg-white/50 rounded-full mb-8 overflow-hidden backdrop-blur-sm border border-white/20">
|
||||||
|
<div class="h-full bg-gradient-to-r from-primary to-primary-container rounded-full transition-all duration-500 shadow-inner" id="xp-progress" style="width: 10%;"></div>
|
||||||
|
</div>
|
||||||
|
<!-- Plant Growth Display -->
|
||||||
|
<div class="flex items-center justify-center py-8 relative min-h-[160px]">
|
||||||
|
<!-- Watering Animation Container -->
|
||||||
|
<div class="absolute top-0 left-0 w-full h-full pointer-events-none z-10" id="rain-container"></div>
|
||||||
|
<!-- Visual Stage Indicator Background -->
|
||||||
|
<div class="absolute inset-0 flex items-center justify-center opacity-10 pointer-events-none">
|
||||||
|
<span class="text-[120px] material-symbols-outlined text-primary" style="font-variation-settings: 'FILL' 1;">eco</span>
|
||||||
|
</div>
|
||||||
|
<!-- Current Plant State -->
|
||||||
|
<div class="relative z-20 transition-all duration-300" id="plant-container">
|
||||||
|
<span class="text-7xl drop-shadow-2xl filter block" id="plant-emoji">🪴</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Watering Action -->
|
||||||
|
<div class="relative z-30">
|
||||||
|
<button class="w-full bg-primary text-on-primary py-4 rounded-2xl text-body-lg font-bold flex items-center justify-center gap-3 shadow-lg active:scale-95 transition-all active:bg-primary-container group" id="water-btn">
|
||||||
|
<span class="material-symbols-outlined text-[24px] transition-transform group-active:-rotate-12 group-hover:scale-110">opacity</span>
|
||||||
|
给小树浇水
|
||||||
|
</button>
|
||||||
|
<p class="text-[11px] text-center text-outline mt-3 italic">浇水可消耗 5 积分,获得 10 XP</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<!-- Social Interaction -->
|
||||||
|
<section class="bg-surface-container-lowest rounded-3xl p-card-padding ambient-shadow flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="w-10 h-10 bg-tertiary-container/20 rounded-2xl flex items-center justify-center">
|
||||||
|
<span class="material-symbols-outlined text-tertiary" style="font-variation-settings: 'FILL' 1;">favorite</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 class="text-body-md font-bold text-on-surface">邀请家人点赞</h4>
|
||||||
|
<p class="text-[12px] text-outline">分享战报给家人,邀请他们为您点赞鼓励</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span class="material-symbols-outlined text-outline">chevron_right</span>
|
||||||
|
</section>
|
||||||
|
<!-- Status Tip -->
|
||||||
|
<div class="flex items-start gap-3 px-2">
|
||||||
|
<span class="material-symbols-outlined text-primary-container text-[20px]">chat_bubble</span>
|
||||||
|
<div>
|
||||||
|
<p class="text-body-md text-on-surface">先完成血糖记录,<span class="text-primary font-bold">今日第一格就亮起来</span>。</p>
|
||||||
|
<button class="text-outline text-label-md hover:text-primary transition-colors">轻触换一句鼓励 ›</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Care List Horizontal -->
|
||||||
|
<div class="flex items-center gap-3 overflow-x-auto hide-scrollbar -mx-container-margin px-container-margin pb-2">
|
||||||
|
<div class="shrink-0 flex items-center gap-2 bg-white px-3 py-2 rounded-xl shadow-sm border border-outline-variant/20">
|
||||||
|
<span class="text-label-md text-outline">就诊卡</span>
|
||||||
|
<span class="text-label-md font-extrabold text-on-surface">10 张</span>
|
||||||
|
</div>
|
||||||
|
<div class="shrink-0 flex items-center gap-2 bg-primary text-on-primary px-3 py-2 rounded-xl shadow-md">
|
||||||
|
<span class="text-label-md font-bold">吕帅</span>
|
||||||
|
<span class="text-[10px] bg-white/20 rounded px-1">23岁</span>
|
||||||
|
</div>
|
||||||
|
<div class="shrink-0 flex items-center gap-2 bg-surface-container-high text-on-surface-variant px-3 py-2 rounded-xl border border-outline-variant/30">
|
||||||
|
<span class="text-label-md font-bold">霍爱玲</span>
|
||||||
|
<span class="text-[10px] text-outline">57岁</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<!-- Footer Note -->
|
||||||
|
<footer class="mt-8 text-center px-container-margin pb-12">
|
||||||
|
<p class="text-[11px] text-outline-variant/60">数据每日同步 · 阈值仅作参考,请遵医嘱</p>
|
||||||
|
</footer>
|
||||||
|
<!-- Navigation Bar -->
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// State Management
|
||||||
|
let currentXP = 35;
|
||||||
|
let currentLevel = 3;
|
||||||
|
let totalPoints = 120;
|
||||||
|
const maxLevel = 10;
|
||||||
|
const xpPerLevel = 50;
|
||||||
|
|
||||||
|
const stageNames = [
|
||||||
|
"种子眠", "破土而出", "茁壮成长", "枝繁叶茂", "初绽花蕾",
|
||||||
|
"繁花似锦", "硕果初步", "丰收在望", "参天大树", "森林守护"
|
||||||
|
];
|
||||||
|
|
||||||
|
const plantEmojis = [
|
||||||
|
"🫘", "🌱", "🌿", "🪴", "🎍",
|
||||||
|
"🌸", "🍒", "🍎", "🌳", "🌲"
|
||||||
|
];
|
||||||
|
|
||||||
|
// DOM Elements
|
||||||
|
const waterBtn = document.getElementById('water-btn');
|
||||||
|
const rainContainer = document.getElementById('rain-container');
|
||||||
|
const plantContainer = document.getElementById('plant-container');
|
||||||
|
const plantEmoji = document.getElementById('plant-emoji');
|
||||||
|
const xpProgress = document.getElementById('xp-progress');
|
||||||
|
const xpText = document.getElementById('xp-text');
|
||||||
|
const levelLabel = document.getElementById('level-label');
|
||||||
|
const stageNameLabel = document.getElementById('stage-name');
|
||||||
|
const totalPointsLabel = document.getElementById('total-points');
|
||||||
|
|
||||||
|
// Watering Animation Logic
|
||||||
|
function createRain() {
|
||||||
|
for (let i = 0; i < 15; i++) {
|
||||||
|
const droplet = document.createElement('div');
|
||||||
|
droplet.className = 'droplet';
|
||||||
|
droplet.style.left = `${Math.random() * 100}%`;
|
||||||
|
droplet.style.animationDelay = `${Math.random() * 0.4}s`;
|
||||||
|
rainContainer.appendChild(droplet);
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
setTimeout(() => droplet.remove(), 600);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function triggerGrowthPulse() {
|
||||||
|
plantContainer.classList.add('growth-pulse');
|
||||||
|
setTimeout(() => {
|
||||||
|
plantContainer.classList.remove('growth-pulse');
|
||||||
|
}, 800);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createSparkles() {
|
||||||
|
for (let i = 0; i < 12; i++) {
|
||||||
|
const sparkle = document.createElement('span');
|
||||||
|
sparkle.className = 'material-symbols-outlined shimmer-particle';
|
||||||
|
sparkle.textContent = 'sparkles';
|
||||||
|
sparkle.style.left = `${40 + (Math.random() - 0.5) * 40}%`;
|
||||||
|
sparkle.style.top = `${40 + (Math.random() - 0.5) * 40}%`;
|
||||||
|
sparkle.style.animationDuration = `${0.6 + Math.random() * 0.4}s`;
|
||||||
|
plantContainer.appendChild(sparkle);
|
||||||
|
|
||||||
|
setTimeout(() => sparkle.remove(), 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function triggerSugarDecreaseEffect() {
|
||||||
|
// 1. Floating Text
|
||||||
|
const floatingText = document.createElement('div');
|
||||||
|
floatingText.className = 'sugar-decrease-text';
|
||||||
|
floatingText.textContent = '-0.5 mmol/L';
|
||||||
|
|
||||||
|
// Randomize spawn position near the plant
|
||||||
|
const rect = plantContainer.getBoundingClientRect();
|
||||||
|
floatingText.style.left = `${rect.left + rect.width / 2}px`;
|
||||||
|
floatingText.style.top = `${rect.top}px`;
|
||||||
|
|
||||||
|
document.body.appendChild(floatingText);
|
||||||
|
setTimeout(() => floatingText.remove(), 2000);
|
||||||
|
|
||||||
|
// 2. Full-screen Downward Particles
|
||||||
|
for (let i = 0; i < 20; i++) {
|
||||||
|
setTimeout(() => {
|
||||||
|
const particle = document.createElement('div');
|
||||||
|
particle.className = 'sugar-particle text-primary-container';
|
||||||
|
// Mix arrows and symbols
|
||||||
|
particle.innerHTML = Math.random() > 0.5 ? '<span class="material-symbols-outlined">south</span>' : '▼';
|
||||||
|
particle.style.left = `${Math.random() * 100}vw`;
|
||||||
|
particle.style.fontSize = `${16 + Math.random() * 16}px`;
|
||||||
|
particle.style.opacity = '0';
|
||||||
|
|
||||||
|
document.body.appendChild(particle);
|
||||||
|
setTimeout(() => particle.remove(), 1500);
|
||||||
|
}, i * 50);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateDisplay() {
|
||||||
|
const progress = (currentXP / xpPerLevel) * 100;
|
||||||
|
xpProgress.style.width = `${progress}%`;
|
||||||
|
xpText.textContent = `${currentXP}/${xpPerLevel} XP`;
|
||||||
|
levelLabel.textContent = `Lv.${currentLevel}`;
|
||||||
|
stageNameLabel.textContent = stageNames[currentLevel - 1];
|
||||||
|
plantEmoji.textContent = plantEmojis[currentLevel - 1];
|
||||||
|
totalPointsLabel.textContent = totalPoints;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Action Logic
|
||||||
|
waterBtn.addEventListener('click', () => {
|
||||||
|
if (totalPoints < 5) {
|
||||||
|
alert("积分不足,快去完成任务领积分吧!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lock button briefly
|
||||||
|
waterBtn.disabled = true;
|
||||||
|
waterBtn.classList.add('opacity-50');
|
||||||
|
|
||||||
|
// 1. Consume Points
|
||||||
|
totalPoints -= 5;
|
||||||
|
|
||||||
|
// 2. Trigger Animations
|
||||||
|
createRain();
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
triggerGrowthPulse();
|
||||||
|
createSparkles();
|
||||||
|
triggerSugarDecreaseEffect(); // New Effect
|
||||||
|
|
||||||
|
// 3. Update State
|
||||||
|
currentXP += 10;
|
||||||
|
if (currentXP >= xpPerLevel && currentLevel < maxLevel) {
|
||||||
|
currentXP -= xpPerLevel;
|
||||||
|
currentLevel++;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateDisplay();
|
||||||
|
|
||||||
|
// Unlock button
|
||||||
|
waterBtn.disabled = false;
|
||||||
|
waterBtn.classList.remove('opacity-50');
|
||||||
|
}, 600);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize
|
||||||
|
updateDisplay();
|
||||||
|
|
||||||
|
// Simple button micro-interactions for others
|
||||||
|
document.querySelectorAll('button:not(#water-btn)').forEach(btn => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
if(!btn.classList.contains('cursor-not-allowed')) {
|
||||||
|
// Interaction logic
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
</body></html>
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# 识糖小课堂平台接入说明
|
||||||
|
|
||||||
|
## 已接入能力
|
||||||
|
|
||||||
|
- 复用主小程序 `token` 与微信小程序登录,不创建第二套游戏账号。
|
||||||
|
- 按周一日期、性别自动分配最多 7 人的同行组。
|
||||||
|
- 首次入组使用数据库分配锁,多个用户同时进入也不会重复分组或超过 7 人。
|
||||||
|
- 以真实平台昵称、头像、认糖数和本周最高分排序。
|
||||||
|
- 每局用 `session_key` 上报绝对进度,断网重试不会重复加分。
|
||||||
|
- 待同步成绩最多本地保留 8 局,重新联网后自动补传。
|
||||||
|
- 微信好友与朋友圈分享使用随机分享码,不在链接中暴露用户 ID。
|
||||||
|
- 每张周分享卡对同一受邀人只记录一次轻量访问,不自动建立家庭或好友绑定。
|
||||||
|
|
||||||
|
## 接口
|
||||||
|
|
||||||
|
- `GET /api/tcm/gameWeeklyLeaderboard`:获取或创建当前周同行榜。
|
||||||
|
- `POST /api/tcm/gameSubmitProgress`:上报 `session_key`、`learned_count`、`score`、`ended`。
|
||||||
|
- `POST /api/tcm/gameRecordShare`:记录分享动作并获取本周分享码。
|
||||||
|
- `POST /api/tcm/gameAcceptShare`:受邀用户登录后提交 `invite_code`。
|
||||||
|
|
||||||
|
四个接口都使用现有 `LoginMiddleware` 校验主小程序 `token`。
|
||||||
|
|
||||||
|
## 部署顺序
|
||||||
|
|
||||||
|
1. 执行 `server/sql/1.9.20260717/add_tcm_endless_game_platform.sql`。
|
||||||
|
2. 发布 `server/app/api/logic/tcm/GamePlatformLogic.php` 和 `TcmController.php`。
|
||||||
|
3. 重新构建并上传小程序前端。
|
||||||
|
|
||||||
|
如果数据库已经执行过本功能的旧版建表脚本,再执行一次
|
||||||
|
`server/sql/1.9.20260717/upgrade_tcm_endless_game_platform_20260717.sql`,用于补充分组锁并把分享去重范围修正为“每张周分享卡”。
|
||||||
|
|
||||||
|
如果后端或数据表尚未发布,游戏仍可离线游玩,榜单会显示“离线记录中”;联网且接口可用后自动补传。
|
||||||
|
|
||||||
|
## 上线前检查
|
||||||
|
|
||||||
|
- 用男女各两个测试账号进入,确认被分入对应性别组。
|
||||||
|
- 同一局重复提交相同 `session_key`,确认周认糖数不重复增加。
|
||||||
|
- 断网完成几次消除,再联网打开榜单,确认成绩补传。
|
||||||
|
- 分享给另一个微信账号,确认能直接进入游戏且链接中没有用户 ID。
|
||||||
|
- 周一验证新周重新分组,旧周成绩不带入新周。
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
# 识糖小课堂独立功能包
|
||||||
|
|
||||||
|
此目录包含“识糖小课堂”无尽三消版的页面与运行代码。食品图片由远端 COS 提供,图标组件复用 `tongji` 分包的公共组件。
|
||||||
|
|
||||||
|
## 目录内容
|
||||||
|
|
||||||
|
- `index.vue`:页面、棋盘算法、关卡主题、任务、道具、三/四/五连奖励和适老化交互。
|
||||||
|
- `game-endless.scss`:完整页面与动画样式。
|
||||||
|
- `composables/useGameAuth.js`:复用主小程序账号并处理 token 过期重登。
|
||||||
|
- `composables/useGamePlatform.js`:周榜、成绩补传与微信分享的平台连接层。
|
||||||
|
- `composables/useGameSfx.js`:滑动、掉落、消除、连击和大奖音效。
|
||||||
|
|
||||||
|
## 迁移步骤
|
||||||
|
|
||||||
|
1. 复制 `endless-game` 文件夹到目标项目的 `tongji/` 目录,并确保目标项目同时提供 `tongji/components/TongjiIcon.vue` 与 `tongji/utils/svgDataUrl.js`。
|
||||||
|
2. 在目标项目 `pages.json` 的 `tongji` 分包 `pages` 数组中加入:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"path": "endless-game/index",
|
||||||
|
"style": {
|
||||||
|
"navigationStyle": "custom",
|
||||||
|
"navigationBarTitleText": "识糖小课堂",
|
||||||
|
"backgroundColor": "#eefbf4",
|
||||||
|
"disableScroll": false,
|
||||||
|
"enableShareAppMessage": true,
|
||||||
|
"enableShareTimeline": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. 使用 `/tongji/endless-game/index` 打开游戏。
|
||||||
|
4. 微信小程序后台需要将 `https://gz-1349751149.cos.ap-guangzhou.myqcloud.com` 配置为合法的音频与图片下载域名。
|
||||||
|
|
||||||
|
## 说明
|
||||||
|
|
||||||
|
- 旧版“糖分突袭”页面及其独占依赖已经移除,项目只注册 `/tongji/endless-game/index` 这一款小游戏。
|
||||||
|
- 食品图片固定使用 `https://gz-1349751149.cos.ap-guangzhou.myqcloud.com/games/food/` 前缀,文件名必须与 `FOODS` 配置 key 的大小写保持一致。
|
||||||
|
- 连消小目标按“连消×2”累计 2 次;连续 6 次普通消除没有连消时,下一次掉落会提供连消机会。“连消×3”保留为额外积分、驼乳粉与撒花惊喜,不阻挡主线任务。
|
||||||
|
- 横向 3 个与竖向 3 个相交形成 L/T 形五消时,会在交点生成带高对比 L 标记的范围爆破棋子;该棋子再次被消除时清除周围九格。
|
||||||
|
- 进入游戏时展示“本周 7 人同行榜”,包含真实平台昵称、前后名次、差距提示和可切换的同行/亲友鼓励;顶部“本周同行”可再次打开。后端部署方式见 `PLATFORM_INTEGRATION.md`。
|
||||||
|
- 本功能包依赖 uni-app Vue 3、`@dcloudio/uni-app` 以及 `tongji` 分包内的公共 `TongjiIcon` 组件。
|
||||||
|
- 食物风险等级和科普文案集中在 `index.vue` 的 `FOODS` 配置中,正式上线前仍需由医院医生审核。
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
/**
|
||||||
|
* 识糖小课堂独立登录适配层。复用主小程序账号,但不依赖 tongji 其他页面代码。
|
||||||
|
*/
|
||||||
|
export function useGameAuth(proxy) {
|
||||||
|
let loginPromise = null
|
||||||
|
|
||||||
|
function hasToken() {
|
||||||
|
return !!String(uni.getStorageSync('token') || '').trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearToken() {
|
||||||
|
uni.removeStorageSync('token')
|
||||||
|
}
|
||||||
|
|
||||||
|
function wxLoginCode() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
uni.login({
|
||||||
|
provider: 'weixin',
|
||||||
|
success: (res) => res?.code ? resolve(res.code) : reject(new Error('微信登录未返回 code')),
|
||||||
|
fail: reject
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loginWithCode(code) {
|
||||||
|
const res = await proxy.apiUrl({
|
||||||
|
url: '/api/login/mnpLogin',
|
||||||
|
method: 'POST',
|
||||||
|
data: { code }
|
||||||
|
}, false)
|
||||||
|
if (res?.code !== 1 || !res.data?.token) {
|
||||||
|
clearToken()
|
||||||
|
throw new Error(res?.msg || '登录失败')
|
||||||
|
}
|
||||||
|
uni.setStorageSync('token', res.data.token)
|
||||||
|
uni.setStorageSync('userData', res.data)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function verifyOrLogin() {
|
||||||
|
if (hasToken()) {
|
||||||
|
try {
|
||||||
|
const res = await proxy.apiUrl({ url: '/api/user/info', method: 'POST' }, false)
|
||||||
|
if (res?.code === 1 && res.data) {
|
||||||
|
uni.setStorageSync('userData', res.data)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// 只有明确的登录失效才重新换取 token;其他业务错误先保留原登录。
|
||||||
|
if (res?.code !== -1) return true
|
||||||
|
} catch (_) {
|
||||||
|
// 断网不清除仍可能有效的 token,成绩由离线队列稍后补传。
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
clearToken()
|
||||||
|
}
|
||||||
|
const code = await wxLoginCode()
|
||||||
|
return loginWithCode(code)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ensureLoggedIn() {
|
||||||
|
if (loginPromise) return loginPromise
|
||||||
|
loginPromise = verifyOrLogin()
|
||||||
|
.then(ok => !!ok)
|
||||||
|
.catch(() => false)
|
||||||
|
// 只合并同时发生的登录;成功结果不能永久缓存,否则 token 过期后无法恢复。
|
||||||
|
.finally(() => { loginPromise = null })
|
||||||
|
return loginPromise
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ensureLoggedIn }
|
||||||
|
}
|
||||||
@@ -0,0 +1,251 @@
|
|||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
const EMPTY_BOARD = {
|
||||||
|
week_start: '',
|
||||||
|
week_end: '',
|
||||||
|
sex_label: '同行',
|
||||||
|
group_size: 7,
|
||||||
|
member_count: 0,
|
||||||
|
players: [],
|
||||||
|
me: { count: 0, rank: 1, best_score: 0, distance: 0, is_first: true },
|
||||||
|
invite_code: ''
|
||||||
|
}
|
||||||
|
const PENDING_SYNC_KEY = 'tongji_endless_pending_sync_v1'
|
||||||
|
|
||||||
|
function createSessionKey() {
|
||||||
|
const random = Math.random().toString(36).slice(2, 12)
|
||||||
|
return `game_${Date.now().toString(36)}_${random}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function readPendingPayloads() {
|
||||||
|
try {
|
||||||
|
const stored = uni.getStorageSync(PENDING_SYNC_KEY)
|
||||||
|
if (!Array.isArray(stored)) return []
|
||||||
|
return stored.filter(item => (
|
||||||
|
item
|
||||||
|
&& /^[A-Za-z0-9_-]{16,64}$/.test(String(item.session_key || ''))
|
||||||
|
&& Number(item.learned_count) >= 0
|
||||||
|
)).slice(-8)
|
||||||
|
} catch (_) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 小游戏的平台连接层。复用主小程序 token,不在游戏里另建账号。
|
||||||
|
* 每次上报的是本局绝对值,后端按 session_key 去重,断网重试也不会重复加分。
|
||||||
|
*/
|
||||||
|
export function useGamePlatform(proxy, ensureLoggedIn) {
|
||||||
|
const connected = ref(false)
|
||||||
|
const loading = ref(false)
|
||||||
|
const syncStatus = ref('idle')
|
||||||
|
const leaderboard = ref({ ...EMPTY_BOARD })
|
||||||
|
const confirmedSessionLearned = ref(0)
|
||||||
|
|
||||||
|
let sessionKey = createSessionKey()
|
||||||
|
let syncTimer = null
|
||||||
|
let syncing = false
|
||||||
|
let queuedPayloads = readPendingPayloads()
|
||||||
|
let connectPromise = null
|
||||||
|
|
||||||
|
function persistPendingPayloads() {
|
||||||
|
try { uni.setStorageSync(PENDING_SYNC_KEY, queuedPayloads.slice(-8)) } catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function api(request) {
|
||||||
|
if (!proxy?.apiUrl) throw new Error('平台接口未初始化')
|
||||||
|
return proxy.apiUrl(request, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyLeaderboard(data, confirmedForSession = '') {
|
||||||
|
if (!data || !Array.isArray(data.players)) return
|
||||||
|
const inactiveSessionResponse = confirmedForSession && confirmedForSession !== sessionKey
|
||||||
|
if (!inactiveSessionResponse) {
|
||||||
|
leaderboard.value = {
|
||||||
|
...EMPTY_BOARD,
|
||||||
|
...data,
|
||||||
|
me: { ...EMPTY_BOARD.me, ...(data.me || {}) },
|
||||||
|
players: data.players
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (confirmedForSession === sessionKey && data.confirmed_session_learned != null) {
|
||||||
|
confirmedSessionLearned.value = Number(data.confirmed_session_learned) || 0
|
||||||
|
}
|
||||||
|
connected.value = true
|
||||||
|
syncStatus.value = 'synced'
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshLeaderboard() {
|
||||||
|
const res = await api({
|
||||||
|
url: '/api/tcm/gameWeeklyLeaderboard',
|
||||||
|
method: 'GET'
|
||||||
|
})
|
||||||
|
if (res?.code !== 1 || !res.data) {
|
||||||
|
throw new Error(res?.msg || '同行榜加载失败')
|
||||||
|
}
|
||||||
|
applyLeaderboard(res.data)
|
||||||
|
return res.data
|
||||||
|
}
|
||||||
|
|
||||||
|
async function acceptShare(inviteCode) {
|
||||||
|
const code = String(inviteCode || '').trim().toUpperCase()
|
||||||
|
if (!code) return
|
||||||
|
try {
|
||||||
|
await api({
|
||||||
|
url: '/api/tcm/gameAcceptShare',
|
||||||
|
method: 'POST',
|
||||||
|
data: { invite_code: code }
|
||||||
|
})
|
||||||
|
} catch (_) {
|
||||||
|
// 分享关系是辅助能力,不阻断进入游戏。
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function connect(inviteCode = '') {
|
||||||
|
if (connectPromise) return connectPromise
|
||||||
|
connectPromise = (async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const loggedIn = await ensureLoggedIn()
|
||||||
|
if (!loggedIn) throw new Error('登录失败')
|
||||||
|
await acceptShare(inviteCode)
|
||||||
|
await refreshLeaderboard()
|
||||||
|
if (queuedPayloads.length) flushProgress()
|
||||||
|
return true
|
||||||
|
} catch (_) {
|
||||||
|
connected.value = false
|
||||||
|
syncStatus.value = 'offline'
|
||||||
|
return false
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
connectPromise = null
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
return connectPromise
|
||||||
|
}
|
||||||
|
|
||||||
|
function beginSession() {
|
||||||
|
if (syncTimer) clearTimeout(syncTimer)
|
||||||
|
sessionKey = createSessionKey()
|
||||||
|
confirmedSessionLearned.value = 0
|
||||||
|
syncStatus.value = queuedPayloads.length ? 'pending' : (connected.value ? 'synced' : 'offline')
|
||||||
|
return sessionKey
|
||||||
|
}
|
||||||
|
|
||||||
|
function queueProgress(learnedCount, score, ended = false) {
|
||||||
|
const nextPayload = {
|
||||||
|
session_key: sessionKey,
|
||||||
|
learned_count: Math.max(0, Number(learnedCount) || 0),
|
||||||
|
score: Math.max(0, Number(score) || 0),
|
||||||
|
ended: ended ? 1 : 0
|
||||||
|
}
|
||||||
|
const existingIndex = queuedPayloads.findIndex(item => item.session_key === sessionKey)
|
||||||
|
if (existingIndex >= 0) {
|
||||||
|
const existing = queuedPayloads[existingIndex]
|
||||||
|
queuedPayloads[existingIndex] = {
|
||||||
|
...nextPayload,
|
||||||
|
learned_count: Math.max(existing.learned_count, nextPayload.learned_count),
|
||||||
|
score: Math.max(existing.score, nextPayload.score),
|
||||||
|
ended: Math.max(existing.ended, nextPayload.ended)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
queuedPayloads.push(nextPayload)
|
||||||
|
}
|
||||||
|
persistPendingPayloads()
|
||||||
|
syncStatus.value = 'pending'
|
||||||
|
if (syncTimer) clearTimeout(syncTimer)
|
||||||
|
if (ended) {
|
||||||
|
flushProgress()
|
||||||
|
} else {
|
||||||
|
syncTimer = setTimeout(flushProgress, 1000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function flushProgress() {
|
||||||
|
if (syncTimer) clearTimeout(syncTimer)
|
||||||
|
syncTimer = null
|
||||||
|
if (syncing || !queuedPayloads.length) return
|
||||||
|
const payload = queuedPayloads[0]
|
||||||
|
syncing = true
|
||||||
|
syncStatus.value = 'syncing'
|
||||||
|
try {
|
||||||
|
if (!connected.value) {
|
||||||
|
const loggedIn = await ensureLoggedIn()
|
||||||
|
if (!loggedIn) throw new Error('未登录')
|
||||||
|
}
|
||||||
|
const res = await api({
|
||||||
|
url: '/api/tcm/gameSubmitProgress',
|
||||||
|
method: 'POST',
|
||||||
|
data: payload
|
||||||
|
})
|
||||||
|
if (res?.code !== 1 || !res.data) {
|
||||||
|
throw new Error(res?.msg || '成绩保存失败')
|
||||||
|
}
|
||||||
|
applyLeaderboard(res.data, payload.session_key)
|
||||||
|
// 请求发出后玩家可能又完成了消除。只移除已经被本次请求覆盖的进度,
|
||||||
|
// 不能按 session_key 整局删除,否则会丢失请求进行期间产生的新进度。
|
||||||
|
queuedPayloads = queuedPayloads.filter(item => (
|
||||||
|
item.session_key !== payload.session_key
|
||||||
|
|| Number(item.learned_count) > Number(payload.learned_count)
|
||||||
|
|| Number(item.score) > Number(payload.score)
|
||||||
|
|| Number(item.ended) > Number(payload.ended)
|
||||||
|
))
|
||||||
|
persistPendingPayloads()
|
||||||
|
} catch (_) {
|
||||||
|
connected.value = false
|
||||||
|
syncStatus.value = 'offline'
|
||||||
|
// 队首保留原绝对值,下次连接或打开榜单时安全重试。
|
||||||
|
persistPendingPayloads()
|
||||||
|
} finally {
|
||||||
|
syncing = false
|
||||||
|
if (queuedPayloads.length && connected.value) {
|
||||||
|
setTimeout(flushProgress, 80)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function syncAndRefresh(learnedCount, score) {
|
||||||
|
queueProgress(learnedCount, score, false)
|
||||||
|
await flushProgress()
|
||||||
|
if (!connected.value) {
|
||||||
|
await connect()
|
||||||
|
if (queuedPayloads.length) await flushProgress()
|
||||||
|
} else {
|
||||||
|
try { await refreshLeaderboard() } catch (_) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function recordShare() {
|
||||||
|
try {
|
||||||
|
if (!connected.value && !(await connect())) return
|
||||||
|
const res = await api({ url: '/api/tcm/gameRecordShare', method: 'POST' })
|
||||||
|
if (res?.code === 1 && res.data?.invite_code) {
|
||||||
|
leaderboard.value = { ...leaderboard.value, invite_code: res.data.invite_code }
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
// 分享本身仍可进行,统计失败不影响用户。
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function dispose() {
|
||||||
|
if (syncTimer) clearTimeout(syncTimer)
|
||||||
|
syncTimer = null
|
||||||
|
persistPendingPayloads()
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
connected,
|
||||||
|
loading,
|
||||||
|
syncStatus,
|
||||||
|
leaderboard,
|
||||||
|
confirmedSessionLearned,
|
||||||
|
connect,
|
||||||
|
beginSession,
|
||||||
|
queueProgress,
|
||||||
|
flushProgress,
|
||||||
|
syncAndRefresh,
|
||||||
|
refreshLeaderboard,
|
||||||
|
recordShare,
|
||||||
|
dispose
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,350 @@
|
|||||||
|
/**
|
||||||
|
* 糖分突袭 · 游戏音效(InnerAudioContext 池化,H5/小程序通用)
|
||||||
|
* 每种操作独立音轨 + playbackRate 变调,避免“全是同一个声”
|
||||||
|
*/
|
||||||
|
import { ref, onUnmounted } from 'vue'
|
||||||
|
|
||||||
|
const STORAGE_KEY = 'tongji_game_sfx_enabled'
|
||||||
|
const COS = 'https://gz-1349751149.cos.ap-guangzhou.myqcloud.com/uploads/file'
|
||||||
|
|
||||||
|
/** 9 条基础素材(已上传 COS,小程序域名白名单内) */
|
||||||
|
const SRC = {
|
||||||
|
tap: `${COS}/20260526/20260526105128557ef8669.mp3`,
|
||||||
|
swoosh: `${COS}/20260527/202605271539371ebe13672.mp3`,
|
||||||
|
pop: `${COS}/20260526/202605261051282a0a94508.mp3`,
|
||||||
|
buzz: `${COS}/20260527/202605271539376ff628472.mp3`,
|
||||||
|
chime: `${COS}/20260528/202605280937077bad76716.mp3`,
|
||||||
|
fanfare: `${COS}/20260528/20260528093707b021f5794.mp3`,
|
||||||
|
blast: `${COS}/20260528/20260528093707ebd4d5733.mp3`,
|
||||||
|
sparkle: `${COS}/20260528/20260528093707016f07427.mp3`,
|
||||||
|
stinger: `${COS}/20260528/20260528093709c669f4659.mp3`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 音效表:src / volume / rate(playbackRate) / pool
|
||||||
|
* rate 不同可在同一素材上听出明显差异
|
||||||
|
*/
|
||||||
|
const SFX = {
|
||||||
|
// —— 基础素材别名(供分层音效直接调用)——
|
||||||
|
tap: { src: SRC.tap, volume: 0.32, rate: 1, pool: 3 },
|
||||||
|
pop: { src: SRC.pop, volume: 0.3, rate: 1, pool: 4 },
|
||||||
|
fanfare: { src: SRC.fanfare, volume: 0.56, rate: 1, pool: 2 },
|
||||||
|
blast: { src: SRC.blast, volume: 0.62, rate: 1, pool: 2 },
|
||||||
|
sparkle: { src: SRC.sparkle, volume: 0.5, rate: 1, pool: 2 },
|
||||||
|
|
||||||
|
// —— 交互 ——
|
||||||
|
select: { src: SRC.tap, volume: 0.32, rate: 1.05, pool: 3 },
|
||||||
|
deselect: { src: SRC.tap, volume: 0.22, rate: 0.82, pool: 2 },
|
||||||
|
hint: { src: SRC.tap, volume: 0.26, rate: 1.28, pool: 2 },
|
||||||
|
|
||||||
|
swap: { src: SRC.swoosh, volume: 0.38, rate: 1.0, pool: 3 },
|
||||||
|
swapFail: { src: SRC.buzz, volume: 0.36, rate: 1.18, pool: 2 },
|
||||||
|
|
||||||
|
drop: { src: SRC.pop, volume: 0.3, rate: 0.88, pool: 4 },
|
||||||
|
dropLight: { src: SRC.pop, volume: 0.22, rate: 1.22, pool: 2 },
|
||||||
|
|
||||||
|
// —— 消除(按连数)——
|
||||||
|
match3: { src: SRC.chime, volume: 0.48, rate: 1.0, pool: 4 },
|
||||||
|
match4: { src: SRC.fanfare, volume: 0.54, rate: 1.06, pool: 3 },
|
||||||
|
match5: { src: SRC.blast, volume: 0.62, rate: 1.0, pool: 3 },
|
||||||
|
|
||||||
|
// —— 消除(按 GI 主色)——
|
||||||
|
matchLowGi: { src: SRC.chime, volume: 0.42, rate: 0.86, pool: 2 },
|
||||||
|
matchMidGi: { src: SRC.chime, volume: 0.46, rate: 1.02, pool: 2 },
|
||||||
|
matchHighGi: { src: SRC.buzz, volume: 0.44, rate: 0.92, pool: 2 },
|
||||||
|
|
||||||
|
// —— 连消 ——
|
||||||
|
combo2: { src: SRC.fanfare, volume: 0.56, rate: 1.12, pool: 2 },
|
||||||
|
combo3: { src: SRC.blast, volume: 0.64, rate: 1.08, pool: 2 },
|
||||||
|
comboMega: { src: SRC.blast, volume: 0.72, rate: 1.22, pool: 2 },
|
||||||
|
|
||||||
|
// —— 道具 ——
|
||||||
|
insulin: { src: SRC.sparkle, volume: 0.5, rate: 1.15, pool: 2 },
|
||||||
|
fiber: { src: SRC.swoosh, volume: 0.48, rate: 1.32, pool: 2 },
|
||||||
|
meal: { src: SRC.stinger, volume: 0.42, rate: 1.35, pool: 1 },
|
||||||
|
reshuffle: { src: SRC.swoosh, volume: 0.46, rate: 0.72, pool: 2 },
|
||||||
|
|
||||||
|
// —— 反馈 ——
|
||||||
|
score: { src: SRC.tap, volume: 0.28, rate: 1.38, pool: 2 },
|
||||||
|
goalTick: { src: SRC.chime, volume: 0.36, rate: 1.45, pool: 2 },
|
||||||
|
meterWarn: { src: SRC.buzz, volume: 0.5, rate: 1.0, pool: 2 },
|
||||||
|
meterDanger: { src: SRC.buzz, volume: 0.58, rate: 0.78, pool: 2 },
|
||||||
|
meterStable: { src: SRC.pop, volume: 0.24, rate: 1.05, pool: 1 },
|
||||||
|
|
||||||
|
win: { src: SRC.fanfare, volume: 0.66, rate: 1.0, pool: 1 },
|
||||||
|
winStinger: { src: SRC.stinger, volume: 0.38, rate: 1.5, pool: 1 },
|
||||||
|
lose: { src: SRC.buzz, volume: 0.52, rate: 0.68, pool: 1 },
|
||||||
|
|
||||||
|
// 兼容旧名
|
||||||
|
match: { src: SRC.chime, volume: 0.48, rate: 1.0, pool: 4 },
|
||||||
|
combo: { src: SRC.fanfare, volume: 0.56, rate: 1.1, pool: 3 },
|
||||||
|
heal: { src: SRC.pop, volume: 0.44, rate: 1.18, pool: 2 },
|
||||||
|
boost: { src: SRC.sparkle, volume: 0.52, rate: 1.0, pool: 2 },
|
||||||
|
invalid: { src: SRC.buzz, volume: 0.4, rate: 1.15, pool: 2 },
|
||||||
|
danger: { src: SRC.blast, volume: 0.58, rate: 0.95, pool: 2 }
|
||||||
|
}
|
||||||
|
|
||||||
|
const WARMUP_SFX = [
|
||||||
|
'select', 'swap', 'swapFail', 'drop', 'dropLight',
|
||||||
|
'match3', 'match4', 'match5', 'combo2', 'combo3', 'comboMega',
|
||||||
|
'insulin', 'sparkle', 'reshuffle', 'lose'
|
||||||
|
]
|
||||||
|
|
||||||
|
function createAudioContext() {
|
||||||
|
if (typeof Audio !== 'undefined') {
|
||||||
|
const audio = new Audio()
|
||||||
|
audio.preload = 'auto'
|
||||||
|
return {
|
||||||
|
get src() { return audio.src },
|
||||||
|
set src(value) { audio.src = value },
|
||||||
|
get volume() { return audio.volume },
|
||||||
|
set volume(value) { audio.volume = value },
|
||||||
|
get playbackRate() { return audio.playbackRate },
|
||||||
|
set playbackRate(value) { audio.playbackRate = value },
|
||||||
|
play() {
|
||||||
|
const promise = audio.play()
|
||||||
|
if (promise?.catch) promise.catch(() => {})
|
||||||
|
},
|
||||||
|
stop() {
|
||||||
|
audio.pause()
|
||||||
|
try { audio.currentTime = 0 } catch (_) {}
|
||||||
|
},
|
||||||
|
seek(time) {
|
||||||
|
try { audio.currentTime = time } catch (_) {}
|
||||||
|
},
|
||||||
|
destroy() {
|
||||||
|
audio.pause()
|
||||||
|
audio.removeAttribute('src')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return uni.createInnerAudioContext()
|
||||||
|
}
|
||||||
|
|
||||||
|
class SfxPool {
|
||||||
|
constructor(src, size, volume, rate = 1) {
|
||||||
|
this.src = src
|
||||||
|
this.size = size
|
||||||
|
this.volume = volume
|
||||||
|
this.rate = rate
|
||||||
|
this.list = []
|
||||||
|
this.cursor = 0
|
||||||
|
this.warmedUp = false
|
||||||
|
}
|
||||||
|
|
||||||
|
createOne() {
|
||||||
|
const ctx = createAudioContext()
|
||||||
|
ctx.src = this.src
|
||||||
|
ctx.obeyMuteSwitch = false
|
||||||
|
ctx.autoplay = false
|
||||||
|
ctx.volume = this.volume
|
||||||
|
try {
|
||||||
|
ctx.playbackRate = this.rate
|
||||||
|
} catch (_) {}
|
||||||
|
this.list.push(ctx)
|
||||||
|
return ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
create() {
|
||||||
|
while (this.list.length < this.size) this.createOne()
|
||||||
|
}
|
||||||
|
|
||||||
|
warmUp() {
|
||||||
|
if (this.warmedUp) return
|
||||||
|
const ctx = this.list[0] || this.createOne()
|
||||||
|
try {
|
||||||
|
ctx.volume = 0
|
||||||
|
ctx.play()
|
||||||
|
setTimeout(() => {
|
||||||
|
try {
|
||||||
|
ctx.stop()
|
||||||
|
ctx.volume = this.volume
|
||||||
|
} catch (_) {}
|
||||||
|
}, 60)
|
||||||
|
} catch (_) {}
|
||||||
|
if (this.size > 1) this.cursor = 1
|
||||||
|
this.warmedUp = true
|
||||||
|
}
|
||||||
|
|
||||||
|
play(scale = 1, rateMul = 1) {
|
||||||
|
if (this.list.length < this.size) this.create()
|
||||||
|
const ctx = this.list[this.cursor % this.list.length]
|
||||||
|
this.cursor += 1
|
||||||
|
const vol = Math.min(1, this.volume * scale)
|
||||||
|
const rate = Math.min(2, Math.max(0.5, this.rate * rateMul))
|
||||||
|
try {
|
||||||
|
ctx.volume = vol
|
||||||
|
try {
|
||||||
|
ctx.playbackRate = rate
|
||||||
|
} catch (_) {}
|
||||||
|
ctx.stop()
|
||||||
|
ctx.seek(0)
|
||||||
|
ctx.play()
|
||||||
|
} catch (_) {
|
||||||
|
try {
|
||||||
|
ctx.play()
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
this.list.forEach((ctx) => {
|
||||||
|
try {
|
||||||
|
ctx.stop()
|
||||||
|
ctx.destroy()
|
||||||
|
} catch (_) {}
|
||||||
|
})
|
||||||
|
this.list = []
|
||||||
|
this.warmedUp = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function dominantGi(tiles = []) {
|
||||||
|
const cnt = { low: 0, mid: 0, high: 0 }
|
||||||
|
tiles.forEach((t) => {
|
||||||
|
const g = t?.type?.gi
|
||||||
|
if (g && cnt[g] !== undefined) cnt[g] += 1
|
||||||
|
})
|
||||||
|
let best = 'mid'
|
||||||
|
let max = 0
|
||||||
|
Object.entries(cnt).forEach(([k, v]) => {
|
||||||
|
if (v > max) {
|
||||||
|
max = v
|
||||||
|
best = k
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return best
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useGameSfx() {
|
||||||
|
const enabled = ref(true)
|
||||||
|
const pools = {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const stored = uni.getStorageSync(STORAGE_KEY)
|
||||||
|
if (stored === false || stored === '0' || stored === 0) {
|
||||||
|
enabled.value = false
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
|
||||||
|
function ensureAudioOption() {
|
||||||
|
// #ifdef MP-WEIXIN
|
||||||
|
try {
|
||||||
|
uni.setInnerAudioOption({
|
||||||
|
obeyMuteSwitch: false,
|
||||||
|
mixWithOther: true
|
||||||
|
})
|
||||||
|
} catch (_) {}
|
||||||
|
// #endif
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPool(name) {
|
||||||
|
const cfg = SFX[name]
|
||||||
|
if (!cfg) return null
|
||||||
|
if (!pools[name]) {
|
||||||
|
pools[name] = new SfxPool(cfg.src, cfg.pool, cfg.volume, cfg.rate ?? 1)
|
||||||
|
}
|
||||||
|
return pools[name]
|
||||||
|
}
|
||||||
|
|
||||||
|
function warmUp() {
|
||||||
|
if (!enabled.value) return
|
||||||
|
ensureAudioOption()
|
||||||
|
const names = typeof Audio !== 'undefined' ? ['select'] : WARMUP_SFX
|
||||||
|
names.forEach((name) => {
|
||||||
|
getPool(name)?.warmUp()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function play(name, scale = 1, rateMul = 1) {
|
||||||
|
if (!enabled.value) return
|
||||||
|
getPool(name)?.play(scale, rateMul)
|
||||||
|
}
|
||||||
|
|
||||||
|
function playLayer(primary, secondary, delayMs = 70, secScale = 0.6) {
|
||||||
|
play(primary)
|
||||||
|
if (secondary) {
|
||||||
|
setTimeout(() => play(secondary, secScale), delayMs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function playMatch({ matchCount = 3, chain = 0, tiles = [] } = {}) {
|
||||||
|
if (!enabled.value) return
|
||||||
|
const giTrack = (() => {
|
||||||
|
const gi = dominantGi(tiles)
|
||||||
|
if (gi === 'low') return 'matchLowGi'
|
||||||
|
if (gi === 'high') return 'matchHighGi'
|
||||||
|
return 'matchMidGi'
|
||||||
|
})()
|
||||||
|
|
||||||
|
if (chain >= 3) {
|
||||||
|
playLayer('comboMega', giTrack, 90, 0.55)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (chain === 2) {
|
||||||
|
playLayer('combo3', giTrack, 80, 0.5)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (chain === 1) {
|
||||||
|
playLayer('combo2', giTrack, 70, 0.45)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matchCount >= 5) {
|
||||||
|
playLayer('match5', 'matchHighGi', 85, 0.5)
|
||||||
|
} else if (matchCount >= 4) {
|
||||||
|
playLayer('match4', giTrack, 65, 0.48)
|
||||||
|
} else {
|
||||||
|
play(giTrack, 1, 1)
|
||||||
|
setTimeout(() => play('match3', 0.85), 40)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function playDrop(count = 1) {
|
||||||
|
if (!enabled.value || count <= 0) return
|
||||||
|
const name = count >= 4 ? 'drop' : 'dropLight'
|
||||||
|
play(name, Math.min(1.2, 0.85 + count * 0.04), count >= 6 ? 0.9 : 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
function playMeter(delta, level) {
|
||||||
|
if (!enabled.value) return
|
||||||
|
if (level > 70) play('meterDanger', 1 + (level - 70) * 0.02)
|
||||||
|
else if (level < 30) play('meterWarn', 0.9, 0.95)
|
||||||
|
else if (delta > 8) play('meterWarn', 0.75)
|
||||||
|
else if (delta < -5) play('meterStable', 1.1)
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleEnabled() {
|
||||||
|
enabled.value = !enabled.value
|
||||||
|
try {
|
||||||
|
uni.setStorageSync(STORAGE_KEY, enabled.value ? '1' : '0')
|
||||||
|
} catch (_) {}
|
||||||
|
if (enabled.value) {
|
||||||
|
warmUp()
|
||||||
|
play('select')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function destroyAll() {
|
||||||
|
Object.keys(pools).forEach((key) => {
|
||||||
|
pools[key]?.destroy()
|
||||||
|
delete pools[key]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
destroyAll()
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
enabled,
|
||||||
|
play,
|
||||||
|
playLayer,
|
||||||
|
playMatch,
|
||||||
|
playDrop,
|
||||||
|
playMeter,
|
||||||
|
warmUp,
|
||||||
|
toggleEnabled,
|
||||||
|
destroyAll
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,570 @@
|
|||||||
|
.eg-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
color: #173b32;
|
||||||
|
background: linear-gradient(180deg, #eefbf4 0%, #f9f3dd 56%, #fffaf1 100%);
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Microsoft YaHei', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eg-confetti-layer { position: fixed; z-index: 80; inset: 0; overflow: hidden; pointer-events: none; }
|
||||||
|
.eg-confetti-piece {
|
||||||
|
position: absolute;
|
||||||
|
top: -70rpx;
|
||||||
|
display: block;
|
||||||
|
border-radius: 3rpx;
|
||||||
|
box-shadow: 0 2rpx 4rpx rgba(0,0,0,.12);
|
||||||
|
animation-name: egConfettiFall;
|
||||||
|
animation-timing-function: cubic-bezier(.18,.72,.35,1);
|
||||||
|
animation-fill-mode: both;
|
||||||
|
}
|
||||||
|
.eg-confetti-piece.is-emoji { width: auto !important; height: auto !important; background: transparent !important; box-shadow: none; font-size: 42rpx; }
|
||||||
|
|
||||||
|
.eg-nav {
|
||||||
|
position: relative;
|
||||||
|
display: grid;
|
||||||
|
box-sizing: border-box;
|
||||||
|
grid-template-columns: 156rpx minmax(0, 1fr) 156rpx;
|
||||||
|
align-items: center;
|
||||||
|
column-gap: 12rpx;
|
||||||
|
padding-right: 28rpx;
|
||||||
|
padding-bottom: 16rpx;
|
||||||
|
padding-left: 28rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eg-nav-leading { display: flex; width: 156rpx; justify-content: flex-start; }
|
||||||
|
|
||||||
|
.eg-nav-btn {
|
||||||
|
display: flex;
|
||||||
|
width: 72rpx;
|
||||||
|
height: 72rpx;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 2rpx solid rgba(21, 94, 75, .12);
|
||||||
|
border-radius: 24rpx;
|
||||||
|
background: rgba(255,255,255,.82);
|
||||||
|
}
|
||||||
|
|
||||||
|
.eg-nav-actions { display: flex; width: 156rpx; flex-shrink: 0; justify-content: flex-end; gap: 10rpx; }
|
||||||
|
.eg-nav-btn--small { width: 68rpx; height: 68rpx; border-radius: 22rpx; }
|
||||||
|
|
||||||
|
.eg-title-wrap { display: flex; min-width: 0; flex-direction: column; align-items: center; }
|
||||||
|
.eg-title, .eg-subtitle { overflow: hidden; max-width: 100%; white-space: nowrap; text-overflow: ellipsis; }
|
||||||
|
.eg-title { color: #155e4b; font-size: 40rpx; font-weight: 800; }
|
||||||
|
.eg-subtitle { margin-top: 2rpx; color: #648278; font-size: 24rpx; }
|
||||||
|
.eg-content { padding: 0 16rpx 48rpx; }
|
||||||
|
|
||||||
|
.eg-score-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
padding: 18rpx 24rpx;
|
||||||
|
border: 2rpx solid rgba(21, 94, 75, .1);
|
||||||
|
border-radius: 28rpx;
|
||||||
|
background: rgba(255,255,255,.9);
|
||||||
|
box-shadow: 0 10rpx 28rpx rgba(32, 78, 43, .08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.eg-stat { display: flex; flex: 1; flex-direction: column; align-items: center; }
|
||||||
|
.eg-stat--main { border-right: 2rpx solid #e2eee8; border-left: 2rpx solid #e2eee8; }
|
||||||
|
.eg-stat-label { color: #71847d; font-size: 24rpx; }
|
||||||
|
.eg-stat-value { margin-top: 4rpx; color: #204e2b; font-size: 38rpx; font-weight: 800; }
|
||||||
|
.eg-stat-score { margin-top: 2rpx; color: #e16f24; font-size: 46rpx; font-weight: 900; }
|
||||||
|
.eg-stat--rank { position: relative; cursor: pointer; }
|
||||||
|
.eg-stat--rank.is-locked { opacity: .52; }
|
||||||
|
.eg-stat-rank-value { margin-top: 1rpx; color: #176b52; font-size: 35rpx; font-weight: 1000; line-height: 1.08; }
|
||||||
|
.eg-stat-rank-hint { margin-top: 2rpx; color: #df7427; font-size: 18rpx; font-weight: 800; }
|
||||||
|
|
||||||
|
.eg-task-card {
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
padding: 18rpx 22rpx;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
background: rgba(255, 249, 223, .94);
|
||||||
|
box-shadow: 0 8rpx 22rpx rgba(118, 89, 25, .08);
|
||||||
|
}
|
||||||
|
.eg-task-card { position: relative; overflow: visible; }
|
||||||
|
|
||||||
|
.eg-task-reward-flight {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 72;
|
||||||
|
top: 4rpx;
|
||||||
|
left: 42%;
|
||||||
|
display: flex;
|
||||||
|
width: 112rpx;
|
||||||
|
height: 92rpx;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 4rpx solid rgba(255,255,255,.96);
|
||||||
|
border-radius: 24rpx;
|
||||||
|
background: #fff7e8;
|
||||||
|
box-shadow: 0 10rpx 24rpx rgba(159,94,23,.28);
|
||||||
|
animation: egTaskRewardFlight 1.12s cubic-bezier(.18,.78,.22,1) both;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.eg-task-reward-flight > image { position: relative; z-index: 2; width: 78rpx; height: 66rpx; }
|
||||||
|
.eg-task-reward-flight > text { position: absolute; z-index: 3; top: -14rpx; right: -16rpx; display: flex; width: 48rpx; height: 48rpx; align-items: center; justify-content: center; border: 4rpx solid #fff; border-radius: 50%; color: #fff; background: #ed7625; box-shadow: 0 5rpx 12rpx rgba(191,75,19,.3); font-size: 25rpx; font-weight: 1000; }
|
||||||
|
.eg-task-reward-glow { position: absolute; z-index: 1; inset: -15rpx; border-radius: 32rpx; background: radial-gradient(circle, rgba(255,210,67,.52), rgba(255,210,67,0) 68%); animation: egRewardGlow .42s ease-in-out infinite alternate; }
|
||||||
|
|
||||||
|
.eg-task-head { display: flex; align-items: center; justify-content: space-between; color: #7c5b1b; font-size: 27rpx; font-weight: 700; }
|
||||||
|
.eg-task-name { display: flex; align-items: center; gap: 10rpx; }
|
||||||
|
.eg-task-count { color: #9a6718; font-size: 29rpx; }
|
||||||
|
.eg-progress { height: 14rpx; margin-top: 12rpx; overflow: hidden; border-radius: 999rpx; background: #eadfbd; }
|
||||||
|
.eg-progress-fill { height: 100%; border-radius: inherit; background: linear-gradient(90deg, #f0a128, #f4c34f); transition: width .25s; }
|
||||||
|
|
||||||
|
.eg-board-shell {
|
||||||
|
position: relative;
|
||||||
|
padding: 14rpx;
|
||||||
|
border: 2rpx solid rgba(34, 103, 79, .16);
|
||||||
|
border-radius: 34rpx;
|
||||||
|
background: rgba(255,255,255,.92);
|
||||||
|
box-shadow: 0 18rpx 44rpx rgba(29, 78, 59, .12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.eg-board-top { display: flex; align-items: center; justify-content: space-between; margin: 0 4rpx 14rpx; }
|
||||||
|
.eg-board-bonuses { display: flex; align-items: center; gap: 8rpx; }
|
||||||
|
.eg-combo { padding: 8rpx 16rpx; border-radius: 999rpx; color: #45685d; background: #e8f3ee; font-size: 25rpx; font-weight: 700; }
|
||||||
|
.eg-combo.is-hot { color: #fff; background: linear-gradient(135deg, #f59e0b, #ea580c); }
|
||||||
|
.eg-double-state { padding: 8rpx 14rpx; border: 2rpx solid rgba(255,255,255,.9); border-radius: 999rpx; color: #fff; background: linear-gradient(135deg, #7c3aed, #d946ef); box-shadow: 0 4rpx 12rpx rgba(124,58,237,.24); font-size: 23rpx; font-weight: 900; white-space: nowrap; }
|
||||||
|
.eg-risk { display: flex; align-items: center; gap: 10rpx; color: #8d512c; font-size: 24rpx; }
|
||||||
|
.eg-risk-dots { display: flex; gap: 5rpx; }
|
||||||
|
.eg-risk-dot { width: 12rpx; height: 12rpx; border-radius: 50%; background: #ead8c9; }
|
||||||
|
.eg-risk-dot.on { background: #e96b3b; box-shadow: 0 0 0 3rpx rgba(233,107,59,.12); }
|
||||||
|
.eg-board { display: flex; flex-direction: column; gap: 8rpx; opacity: 1; transition: opacity .15s; }
|
||||||
|
.eg-board.is-busy { opacity: .82; }
|
||||||
|
.eg-row { display: flex; gap: 8rpx; }
|
||||||
|
|
||||||
|
.eg-cell {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
height: 166rpx;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 5rpx solid transparent;
|
||||||
|
border-radius: 26rpx;
|
||||||
|
box-shadow: inset 0 -5rpx 0 rgba(0,0,0,.05), 0 5rpx 12rpx rgba(31, 66, 53, .08);
|
||||||
|
transition: transform .16s cubic-bezier(.2,.8,.2,1), border-color .15s, opacity .16s, filter .16s;
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eg-cell.is-dragging { transition: transform .055s linear, border-color .15s; }
|
||||||
|
.eg-cell.is-selected { z-index: 2; border-color: #167d61; transform: scale(1.06); box-shadow: 0 0 0 6rpx rgba(22,125,97,.14); }
|
||||||
|
.eg-cell.is-camel-target { border-color: #f59e0b; animation: egPulse 1s infinite; }
|
||||||
|
.eg-replace-target { position: absolute; z-index: 7; right: 7rpx; bottom: 6rpx; padding: 4rpx 10rpx; border: 2rpx solid #fff; border-radius: 999rpx; color: #fff; background: #1570a6; box-shadow: 0 3rpx 9rpx rgba(20,91,135,.28); font-size: 20rpx; font-weight: 900; }
|
||||||
|
.eg-cell.is-clearing {
|
||||||
|
z-index: 8;
|
||||||
|
border-color: #fff;
|
||||||
|
animation: egHit .22s cubic-bezier(.2,.9,.3,1) forwards;
|
||||||
|
filter: brightness(1.28) saturate(1.25);
|
||||||
|
}
|
||||||
|
.eg-cell.is-yellow,
|
||||||
|
.eg-cell.is-cream,
|
||||||
|
.eg-cell.is-red,
|
||||||
|
.eg-cell.is-purple { background: #fffaf0; }
|
||||||
|
.eg-cell.is-level-low { background: #f4faf6; }
|
||||||
|
.eg-cell.is-level-mid { background: #fff9eb; }
|
||||||
|
.eg-cell.is-orange,
|
||||||
|
.eg-cell.is-level-high { background: #fff0e4; }
|
||||||
|
.eg-cell.is-milk { background: #edf7ff; }
|
||||||
|
.eg-cell.is-high { border-color: #ef7b45; }
|
||||||
|
.eg-food-image { width: 132rpx; height: 92rpx; margin-top: 7rpx; border-radius: 17rpx; filter: saturate(1.06) contrast(1.03); }
|
||||||
|
.eg-food-name { margin-top: 8rpx; color: #203d34; font-size: 29rpx; font-weight: 900; letter-spacing: 1rpx; line-height: 1.1; }
|
||||||
|
.eg-sugar-tag { position: absolute; top: 5rpx; right: 5rpx; min-width: 52rpx; padding: 4rpx 10rpx; border: 2rpx solid #fff; border-radius: 999rpx; color: #fff; box-shadow: 0 3rpx 8rpx rgba(25,55,45,.22); font-size: 23rpx; font-weight: 900; line-height: 1.25; text-align: center; }
|
||||||
|
.eg-sugar-tag.is-low { color: #316a54; border-color: rgba(255,255,255,.82); background: #dcefe6; box-shadow: 0 2rpx 5rpx rgba(32,101,75,.1); }
|
||||||
|
.eg-sugar-tag.is-mid { color: #735a15; border-color: rgba(255,255,255,.9); background: #f7e7a8; box-shadow: 0 2rpx 6rpx rgba(137,91,0,.15); }
|
||||||
|
.eg-sugar-tag.is-high { background: #d9431f; box-shadow: 0 4rpx 10rpx rgba(163,45,21,.32); }
|
||||||
|
.eg-sugar-tag.is-prop { background: #2563a8; }
|
||||||
|
.eg-special-mark { position: absolute; z-index: 12; bottom: 5rpx; left: 6rpx; display: flex; width: 40rpx; height: 40rpx; align-items: center; justify-content: center; border: 3rpx solid rgba(255,255,255,.96); border-radius: 50%; color: #fff; background: #6d28d9; box-shadow: 0 4rpx 11rpx rgba(73,31,130,.3); font-size: 23rpx; font-weight: 900; }
|
||||||
|
.eg-special-mark.is-row { width: 58rpx; height: 40rpx; border-radius: 999rpx; background: linear-gradient(135deg, #ffd64d, #ff8a18 48%, #e83b22); box-shadow: 0 0 0 4rpx rgba(255,172,30,.2), 0 4rpx 15rpx rgba(203,59,26,.45); animation: egFlameReady .7s ease-in-out infinite alternate; }
|
||||||
|
.eg-special-mark.is-col { width: 40rpx; height: 58rpx; border-radius: 999rpx; background: linear-gradient(180deg, #ffd64d, #ff8a18 48%, #e83b22); box-shadow: 0 0 0 4rpx rgba(255,172,30,.2), 0 4rpx 15rpx rgba(203,59,26,.45); animation: egFlameReady .7s ease-in-out infinite alternate; }
|
||||||
|
.eg-special-mark.is-burst { width: 48rpx; height: 48rpx; border-color: #fff3a8; border-radius: 15rpx 50% 50%; background: linear-gradient(145deg, #7c3aed 5%, #dd3b70 48%, #ff8a18 100%); box-shadow: 0 4rpx 13rpx rgba(108,35,155,.38), 0 0 12rpx rgba(255,179,38,.3); animation: egLReady 1.25s ease-in-out infinite alternate; }
|
||||||
|
.eg-mini-flame { position: relative; width: 25rpx; height: 30rpx; border-radius: 70% 32% 68% 40%; background: linear-gradient(135deg, #fff8a8 8%, #ffc21f 42%, #f04420 84%); box-shadow: 0 0 14rpx rgba(255,230,92,.95); transform: rotate(43deg); }
|
||||||
|
.eg-mini-flame-core { position: absolute; right: 4rpx; bottom: 3rpx; width: 10rpx; height: 15rpx; border-radius: 70% 35% 70% 42%; background: #fffbd1; box-shadow: 0 0 7rpx rgba(255,255,210,.95); }
|
||||||
|
.eg-l-special { position: relative; width: 29rpx; height: 29rpx; filter: drop-shadow(0 1rpx 2rpx rgba(73,22,105,.3)); }
|
||||||
|
.eg-l-arm { position: absolute; left: 3rpx; bottom: 3rpx; border-radius: 999rpx; background: #fff9b8; box-shadow: 0 0 6rpx rgba(255,249,184,.92); }
|
||||||
|
.eg-l-arm.is-vertical { width: 8rpx; height: 25rpx; }
|
||||||
|
.eg-l-arm.is-horizontal { width: 25rpx; height: 8rpx; }
|
||||||
|
.eg-l-core { position: absolute; left: 1rpx; bottom: 1rpx; width: 12rpx; height: 12rpx; border: 2rpx solid #fff; border-radius: 50%; background: #ff5b25; }
|
||||||
|
.eg-cell.is-fire-clearing { border-color: #ff7a1a; box-shadow: inset 0 0 25rpx rgba(255,102,18,.36), 0 0 18rpx rgba(255,105,20,.48); filter: brightness(1.35) saturate(1.42); }
|
||||||
|
.eg-cell.is-clearing.is-fire-clearing { animation: egFireCellHit .36s cubic-bezier(.2,.78,.22,1) forwards; }
|
||||||
|
.eg-fire-clear { position: absolute; z-index: 14; inset: 2rpx; overflow: hidden; border: 3rpx solid rgba(255,210,71,.92); border-radius: 22rpx; opacity: .96; background: linear-gradient(180deg, rgba(255,230,86,.12), rgba(239,63,27,.22)); pointer-events: none; }
|
||||||
|
.eg-fire-glow { position: absolute; inset: 6%; border-radius: 18rpx; background: radial-gradient(circle, rgba(255,246,177,.74), rgba(255,131,22,.32) 47%, rgba(222,47,23,0) 75%); animation: egFireGlow .34s ease-out both; }
|
||||||
|
.eg-fire-sweep { position: absolute; top: 23%; left: -48%; width: 168%; height: 55%; border-radius: 60% 45% 55% 42%; background: linear-gradient(90deg, rgba(255,184,38,0), rgba(255,238,124,.94) 28%, rgba(255,133,22,.92) 53%, rgba(226,54,25,.82) 72%, rgba(255,184,38,0)); box-shadow: 0 0 27rpx rgba(255,91,17,.72); animation: egFireSweep .34s cubic-bezier(.2,.78,.22,1) both; }
|
||||||
|
.eg-fire-flame { position: absolute; bottom: 13%; width: 25rpx; height: 39rpx; border-radius: 72% 35% 68% 42%; background: linear-gradient(145deg, #fff7a6 4%, #ffc329 39%, #ff7419 66%, #dc321f 100%); box-shadow: 0 0 15rpx rgba(255,124,21,.88); transform: rotate(42deg); animation: egFireFlame .34s ease-out both; }
|
||||||
|
.eg-fire-flame > view { position: absolute; right: 5rpx; bottom: 4rpx; width: 10rpx; height: 18rpx; border-radius: 70% 35% 70% 42%; background: #fffbd6; }
|
||||||
|
.eg-fire-flame.is-left { left: 16%; animation-delay: .01s; }
|
||||||
|
.eg-fire-flame.is-center { left: 43%; bottom: 19%; transform: rotate(42deg) scale(1.18); animation-delay: .035s; }
|
||||||
|
.eg-fire-flame.is-right { right: 15%; animation-delay: .065s; }
|
||||||
|
.eg-fire-spark { position: absolute; width: 10rpx; height: 17rpx; border-radius: 70% 35% 70% 42%; background: #ffe45e; box-shadow: 0 0 11rpx rgba(255,139,28,.9); animation: egFireSpark .34s ease-out both; }
|
||||||
|
.eg-fire-spark.is-one { top: 46%; left: 25%; }
|
||||||
|
.eg-fire-spark.is-two { top: 34%; left: 62%; animation-delay: .035s; }
|
||||||
|
.eg-fire-spark.is-three { top: 52%; left: 78%; animation-delay: .07s; }
|
||||||
|
.eg-board-tip { display: flex; align-items: center; justify-content: center; gap: 8rpx; min-height: 58rpx; margin-top: 12rpx; color: #684817; font-size: 26rpx; font-weight: 700; text-align: center; }
|
||||||
|
.eg-impact-text {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 20;
|
||||||
|
top: 46%;
|
||||||
|
left: 50%;
|
||||||
|
padding: 10rpx 24rpx;
|
||||||
|
border: 4rpx solid rgba(255,255,255,.9);
|
||||||
|
border-radius: 999rpx;
|
||||||
|
color: #fff;
|
||||||
|
background: linear-gradient(135deg, #f59e0b, #e4572e);
|
||||||
|
box-shadow: 0 10rpx 28rpx rgba(190, 70, 25, .35);
|
||||||
|
font-size: 34rpx;
|
||||||
|
font-weight: 900;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
animation: egImpact .52s cubic-bezier(.16,.82,.3,1) forwards;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.eg-impact-text.is-level-2 { font-size: 40rpx; background: linear-gradient(135deg, #8b5cf6, #ec4899); }
|
||||||
|
.eg-impact-text.is-level-3 { font-size: 46rpx; background: linear-gradient(135deg, #ef4444, #f59e0b); box-shadow: 0 12rpx 36rpx rgba(239,68,68,.42); }
|
||||||
|
.eg-jackpot {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 30;
|
||||||
|
top: 48%;
|
||||||
|
left: 50%;
|
||||||
|
width: 540rpx;
|
||||||
|
padding: 24rpx 22rpx 20rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 7rpx solid #ffd83d;
|
||||||
|
border-radius: 36rpx;
|
||||||
|
color: #fff;
|
||||||
|
background: radial-gradient(circle at 50% 0%, #ef4444, #9f1239 65%, #64102a);
|
||||||
|
box-shadow: 0 0 0 7rpx #fff2a8, 0 22rpx 52rpx rgba(100,16,42,.5);
|
||||||
|
text-align: center;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
animation: egJackpotIn 1.35s cubic-bezier(.16,.85,.25,1) forwards;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.eg-jackpot.is-match-4 { background: radial-gradient(circle at 50% 0%, #8b5cf6, #5b21b6 68%, #35106d); }
|
||||||
|
.eg-jackpot.is-match-5 { border-color: #fff36a; background: radial-gradient(circle at 50% 0%, #ff6b24, #d7193f 58%, #760c2a); animation-duration: 1.9s; }
|
||||||
|
.eg-jackpot.is-match-3 {
|
||||||
|
width: 414rpx;
|
||||||
|
padding: 20rpx 22rpx 18rpx;
|
||||||
|
border-width: 4rpx;
|
||||||
|
border-color: #f1c663;
|
||||||
|
color: #31574b;
|
||||||
|
background: linear-gradient(155deg, #f7fff9, #fff5d9);
|
||||||
|
box-shadow: 0 0 0 4rpx rgba(255,255,255,.84), 0 14rpx 34rpx rgba(98,82,39,.2);
|
||||||
|
animation: egTripleIn 1.12s cubic-bezier(.2,.82,.28,1) forwards;
|
||||||
|
}
|
||||||
|
.eg-jackpot.is-match-3 .eg-jackpot-lights { display: none; }
|
||||||
|
.eg-jackpot.is-match-3 .eg-jackpot-kicker { color: #5b8c73; font-size: 21rpx; letter-spacing: 3rpx; text-shadow: none; }
|
||||||
|
.eg-jackpot.is-match-3 .eg-jackpot-title { color: #365b4e; font-size: 31rpx; text-shadow: none; }
|
||||||
|
.eg-jackpot.is-match-3 .eg-jackpot-reward { color: #ad7021; font-size: 25rpx; }
|
||||||
|
.eg-jackpot.is-match-3 .eg-jackpot-note { color: #829087; }
|
||||||
|
.eg-triple-cans { position: relative; display: flex; justify-content: center; gap: 12rpx; margin: 12rpx 0 10rpx; }
|
||||||
|
.eg-triple-can { display: flex; width: 78rpx; height: 76rpx; align-items: center; justify-content: center; border: 3rpx solid #f4d996; border-radius: 19rpx; background: rgba(255,255,255,.9); box-shadow: 0 5rpx 12rpx rgba(128,92,30,.12); animation: egTripleCan .42s cubic-bezier(.2,.86,.26,1) both; }
|
||||||
|
.eg-triple-can:nth-child(2) { animation-delay: .07s; }
|
||||||
|
.eg-triple-can:nth-child(3) { animation-delay: .14s; }
|
||||||
|
.eg-triple-can > image { width: 62rpx; height: 58rpx; }
|
||||||
|
|
||||||
|
.eg-jackpot.is-match-4 {
|
||||||
|
width: 548rpx;
|
||||||
|
padding: 25rpx 22rpx 22rpx;
|
||||||
|
border-color: #ffd95b;
|
||||||
|
background: radial-gradient(circle at 50% -10%, #a97cff, #6031b5 58%, #341066);
|
||||||
|
box-shadow: 0 0 0 6rpx #fff0a9, 0 0 36rpx rgba(255,210,76,.5), 0 24rpx 58rpx rgba(53,16,109,.48);
|
||||||
|
animation: egFourIn 1.88s cubic-bezier(.16,.84,.24,1) forwards;
|
||||||
|
}
|
||||||
|
.eg-jackpot.is-match-4 .eg-jackpot-kicker { color: #ffef8d; font-size: 24rpx; letter-spacing: 3rpx; }
|
||||||
|
.eg-jackpot.is-match-4 .eg-jackpot-title { margin-top: 9rpx; color: #fff; font-size: 39rpx; }
|
||||||
|
.eg-jackpot.is-match-4 .eg-jackpot-reward { color: #fff2a8; }
|
||||||
|
.eg-four-teaser { position: relative; margin-top: 13rpx; }
|
||||||
|
.eg-four-cans { display: flex; align-items: center; justify-content: center; gap: 8rpx; }
|
||||||
|
.eg-four-can { display: flex; width: 76rpx; height: 92rpx; align-items: center; justify-content: center; border: 4rpx solid #ffe078; border-radius: 18rpx; background: linear-gradient(180deg, #fffef7, #ffe9a5); box-shadow: inset 0 -5rpx 0 rgba(171,106,17,.12), 0 7rpx 13rpx rgba(34,12,67,.28); animation: egFourCan .48s cubic-bezier(.18,.9,.25,1.2) both; }
|
||||||
|
.eg-four-can:nth-child(2) { animation-delay: .07s; }
|
||||||
|
.eg-four-can:nth-child(3) { animation-delay: .14s; }
|
||||||
|
.eg-four-can:nth-child(4) { animation-delay: .21s; }
|
||||||
|
.eg-four-can > image { width: 64rpx; height: 70rpx; }
|
||||||
|
.eg-four-can.is-locked { border-style: dashed; border-color: rgba(255,241,161,.86); color: #fff6ae; background: rgba(48,18,91,.54); box-shadow: inset 0 0 16rpx rgba(255,225,91,.18), 0 0 18rpx rgba(255,222,91,.36); font-size: 48rpx; font-weight: 1000; animation: egLockedCan .68s .34s ease-in-out infinite alternate; }
|
||||||
|
.eg-four-promise { display: inline-flex; margin-top: 15rpx; padding: 8rpx 18rpx; border: 2rpx solid rgba(255,247,184,.72); border-radius: 999rpx; color: #3d176f; background: linear-gradient(90deg, #fff3a3, #ffd85b); box-shadow: 0 5rpx 15rpx rgba(28,7,62,.22); font-size: 25rpx; font-weight: 1000; animation: egPromisePulse .72s ease-in-out infinite alternate; }
|
||||||
|
.eg-five-celebration { position: relative; margin: 15rpx 0 10rpx; }
|
||||||
|
.eg-five-cans { display: flex; align-items: center; justify-content: center; gap: 8rpx; }
|
||||||
|
.eg-five-can { display: flex; width: 78rpx; height: 93rpx; align-items: center; justify-content: center; border: 4rpx solid #fff17b; border-radius: 18rpx; background: linear-gradient(180deg, #fff, #fff2ac); box-shadow: inset 0 -5rpx 0 rgba(184,91,12,.13), 0 7rpx 0 #bd5b13, 0 0 16rpx rgba(255,240,101,.5); animation: egFiveCan .56s cubic-bezier(.16,.92,.24,1.2) both; }
|
||||||
|
.eg-five-can:nth-child(2) { animation-delay: .07s; }
|
||||||
|
.eg-five-can:nth-child(3) { animation-delay: .14s; }
|
||||||
|
.eg-five-can:nth-child(4) { animation-delay: .21s; }
|
||||||
|
.eg-five-can:nth-child(5) { animation-delay: .28s; }
|
||||||
|
.eg-five-can > image { width: 65rpx; height: 72rpx; }
|
||||||
|
.eg-five-sevens { display: flex; justify-content: center; gap: 8rpx; margin-top: 13rpx; }
|
||||||
|
.eg-five-sevens > text { display: flex; width: 58rpx; height: 55rpx; align-items: center; justify-content: center; border: 3rpx solid #fff5a7; border-radius: 13rpx; color: #fff36a; background: linear-gradient(155deg, #d7193f, #9f1239); box-shadow: 0 5rpx 0 #70102b, 0 0 13rpx rgba(255,243,106,.45); font-size: 43rpx; font-weight: 1000; line-height: 1; text-shadow: 0 3rpx 0 #8f1733; }
|
||||||
|
.eg-jackpot-lights { position: absolute; inset: 8rpx; border: 4rpx dotted rgba(255,255,255,.88); border-radius: 25rpx; animation: egLights .24s steps(2) infinite; }
|
||||||
|
.eg-jackpot-kicker { position: relative; display: block; color: #fff36a; font-size: 28rpx; font-weight: 900; letter-spacing: 6rpx; text-shadow: 0 3rpx 0 rgba(83,19,25,.45); }
|
||||||
|
.eg-jackpot-reels { position: relative; display: flex; justify-content: center; gap: 12rpx; margin: 13rpx 0; }
|
||||||
|
.eg-jackpot-reel { display: flex; width: 112rpx; height: 126rpx; flex-direction: column; align-items: center; justify-content: center; border: 6rpx solid #ffcf31; border-radius: 20rpx; background: linear-gradient(180deg, #fff 0%, #fff7cf 48%, #ffd55e 50%, #fff 53%, #fff8dc 100%); box-shadow: inset 0 0 18rpx rgba(137,78,10,.25), 0 7rpx 0 #a85a0a; animation: egReelStop .52s cubic-bezier(.2,.9,.25,1) both; }
|
||||||
|
.eg-jackpot-reel:nth-child(2) { animation-delay: .09s; }
|
||||||
|
.eg-jackpot-reel:nth-child(3) { animation-delay: .18s; }
|
||||||
|
.eg-jackpot-seven { color: #e11d48; font-size: 61rpx; font-weight: 1000; line-height: .85; text-shadow: 0 3rpx 0 #ffd1d8; }
|
||||||
|
.eg-jackpot-milk { width: 54rpx; height: 36rpx; margin-top: 3rpx; border-radius: 7rpx; }
|
||||||
|
.eg-jackpot-title { position: relative; display: block; font-size: 37rpx; font-weight: 900; text-shadow: 0 4rpx 0 rgba(73,10,30,.5); }
|
||||||
|
.eg-jackpot-points { position: relative; display: block; margin-top: 2rpx; color: #fff36a; font-size: 66rpx; font-weight: 1000; line-height: 1.05; letter-spacing: 2rpx; text-shadow: 0 5rpx 0 #9e2813, 0 0 18rpx rgba(255,243,106,.85); animation: egPointsPop .62s .42s cubic-bezier(.16,.9,.25,1.25) both; }
|
||||||
|
.eg-jackpot-reward { position: relative; display: block; margin-top: 7rpx; color: #fff5a5; font-size: 27rpx; font-weight: 800; line-height: 1.35; }
|
||||||
|
.eg-jackpot-note { position: relative; display: block; margin-top: 8rpx; color: rgba(255,255,255,.8); font-size: 22rpx; }
|
||||||
|
.eg-tools { margin-top: 16rpx; }
|
||||||
|
.eg-tool { display: flex; align-items: center; padding: 16rpx 18rpx; border: 3rpx solid #c9e0d6; border-radius: 26rpx; background: rgba(255,255,255,.92); box-shadow: 0 7rpx 18rpx rgba(24, 84, 63, .07); }
|
||||||
|
.eg-tool.active { border-color: #f59e0b; background: #fffbeb; }
|
||||||
|
.eg-tool.disabled { opacity: .45; }
|
||||||
|
.eg-tool-icon { position: relative; display: flex; width: 88rpx; height: 88rpx; align-items: center; justify-content: center; overflow: visible; border-radius: 24rpx; font-size: 46rpx; background: linear-gradient(145deg, #e8f7ff, #d4ecff); box-shadow: inset 0 -4rpx 0 rgba(33,104,151,.08); }
|
||||||
|
.eg-tool-food-image { width: 76rpx; height: 58rpx; border-radius: 14rpx; }
|
||||||
|
.eg-replace-badge { position: absolute; right: -12rpx; bottom: -8rpx; display: flex; min-width: 78rpx; height: 38rpx; align-items: center; justify-content: center; gap: 2rpx; padding: 0 8rpx; border: 3rpx solid #fff; border-radius: 999rpx; color: #fff; background: linear-gradient(135deg, #2389b9, #145d91); box-shadow: 0 5rpx 12rpx rgba(20,93,145,.3); }
|
||||||
|
.eg-replace-badge .tj-icon-wrap { width: 22rpx; height: 22rpx; }
|
||||||
|
.eg-replace-badge > text { font-size: 20rpx; font-weight: 900; line-height: 1; }
|
||||||
|
.eg-tool-copy { display: flex; flex: 1; flex-direction: column; margin-left: 16rpx; }
|
||||||
|
.eg-tool-name { color: #244a3d; font-size: 29rpx; font-weight: 800; }
|
||||||
|
.eg-tool-desc { margin-top: 5rpx; color: #6c8179; font-size: 25rpx; }
|
||||||
|
.eg-tool-count { color: #155e4b; font-size: 32rpx; font-weight: 900; }
|
||||||
|
.eg-legend { display: flex; justify-content: center; gap: 18rpx; margin-top: 18rpx; color: #53685f; font-size: 25rpx; font-weight: 700; }
|
||||||
|
.eg-dot--safe { color: #4cae7b; }
|
||||||
|
.eg-dot--mid { color: #e2aa19; }
|
||||||
|
.eg-dot--high { color: #e4572e; }
|
||||||
|
|
||||||
|
.eg-weekly-overlay { position: fixed; z-index: 90; inset: 0; display: flex; align-items: center; justify-content: center; padding: 28rpx; background: rgba(16, 55, 43, .66); backdrop-filter: blur(5px); }
|
||||||
|
.eg-weekly-card { width: 100%; max-width: 664rpx; max-height: calc(100vh - 56rpx); overflow-y: auto; padding: 28rpx 26rpx 22rpx; border: 5rpx solid rgba(255,255,255,.96); border-radius: 38rpx; background: linear-gradient(160deg, #f5fff9 0%, #fffdf3 56%, #fff4db 100%); box-shadow: 0 30rpx 80rpx rgba(5,45,32,.38); animation: egWeeklyIn .36s cubic-bezier(.18,.88,.27,1.08) both; }
|
||||||
|
.eg-weekly-head { display: flex; align-items: center; justify-content: space-between; }
|
||||||
|
.eg-weekly-title-line { display: flex; align-items: center; gap: 10rpx; }
|
||||||
|
.eg-weekly-title { color: #155e4b; font-size: 37rpx; font-weight: 1000; }
|
||||||
|
.eg-weekly-preview { padding: 5rpx 10rpx; border-radius: 999rpx; color: #9a5b14; background: #fff0bd; font-size: 19rpx; font-weight: 900; }
|
||||||
|
.eg-weekly-preview.is-offline { color: #69766f; background: #e8eeeb; }
|
||||||
|
.eg-weekly-sub { display: block; margin-top: 5rpx; color: #6b837a; font-size: 23rpx; font-weight: 700; }
|
||||||
|
.eg-weekly-medal { display: flex; width: 76rpx; height: 76rpx; align-items: center; justify-content: center; border: 5rpx solid #fff3b2; border-radius: 50%; color: #fff; background: linear-gradient(145deg, #f5ad24, #e26925); box-shadow: 0 8rpx 0 #b85421, 0 12rpx 24rpx rgba(181,84,33,.24); font-size: 39rpx; font-weight: 1000; }
|
||||||
|
.eg-weekly-progress-card { display: flex; align-items: center; justify-content: space-between; margin-top: 20rpx; padding: 16rpx 18rpx; border: 3rpx solid #d7ebe2; border-radius: 24rpx; background: rgba(255,255,255,.9); }
|
||||||
|
.eg-weekly-progress-label { display: block; color: #657b73; font-size: 22rpx; font-weight: 700; }
|
||||||
|
.eg-weekly-progress-value { display: block; margin-top: -2rpx; color: #e06b24; font-size: 43rpx; font-weight: 1000; }
|
||||||
|
.eg-weekly-progress-copy { display: flex; flex-direction: column; align-items: flex-end; color: #235b49; font-size: 24rpx; font-weight: 900; line-height: 1.55; }
|
||||||
|
.eg-weekly-progress-copy text:last-child { color: #b86820; }
|
||||||
|
.eg-weekly-list { display: flex; flex-direction: column; gap: 7rpx; margin-top: 15rpx; }
|
||||||
|
.eg-weekly-row { display: flex; min-height: 68rpx; align-items: center; padding: 7rpx 14rpx; border: 2rpx solid transparent; border-radius: 19rpx; color: #345c50; background: rgba(255,255,255,.72); }
|
||||||
|
.eg-weekly-row.is-me { border-color: #f2bd48; color: #174f3e; background: linear-gradient(90deg, #fff2bb, #fff9e3); box-shadow: 0 5rpx 14rpx rgba(153,102,17,.12); transform: scale(1.015); }
|
||||||
|
.eg-weekly-place { width: 43rpx; color: #6b7d76; font-size: 27rpx; font-weight: 1000; text-align: center; }
|
||||||
|
.eg-weekly-row:nth-child(1) .eg-weekly-place { color: #dd7b17; font-size: 31rpx; }
|
||||||
|
.eg-weekly-avatar { display: flex; width: 52rpx; height: 52rpx; flex: 0 0 52rpx; align-items: center; justify-content: center; margin-left: 6rpx; border: 3rpx solid rgba(255,255,255,.9); border-radius: 50%; color: #fff; background: #5d9b84; box-shadow: 0 3rpx 9rpx rgba(30,78,61,.16); font-size: 24rpx; font-weight: 1000; }
|
||||||
|
.eg-weekly-avatar-image { display: block; width: 100%; height: 100%; border-radius: 50%; }
|
||||||
|
.eg-weekly-avatar.is-tone-2 { background: #e39932; }
|
||||||
|
.eg-weekly-avatar.is-tone-3 { background: #6c83c8; }
|
||||||
|
.eg-weekly-avatar.is-tone-4 { background: #e5683e; }
|
||||||
|
.eg-weekly-avatar.is-tone-5 { background: #a673bd; }
|
||||||
|
.eg-weekly-avatar.is-tone-6 { background: #448e9c; }
|
||||||
|
.eg-weekly-avatar.is-tone-7 { background: #c27b49; }
|
||||||
|
.eg-weekly-name { flex: 1; margin-left: 13rpx; overflow: hidden; font-size: 27rpx; font-weight: 850; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.eg-weekly-me { margin-right: 8rpx; padding: 3rpx 9rpx; border-radius: 999rpx; color: #fff; background: #dd6b27; font-size: 18rpx; font-weight: 900; }
|
||||||
|
.eg-weekly-count { min-width: 70rpx; color: #315b4d; font-size: 29rpx; font-weight: 1000; text-align: right; }
|
||||||
|
.eg-cheer-card { margin-top: 16rpx; padding: 15rpx 17rpx 14rpx; border: 3rpx solid #f0d38d; border-radius: 23rpx; background: linear-gradient(135deg, #fff9d9, #fff1c2); }
|
||||||
|
.eg-cheer-card.is-family { border-color: #d9c8ef; background: linear-gradient(135deg, #f8f2ff, #efe7ff); }
|
||||||
|
.eg-cheer-card.is-bright { border-color: #f4c77b; background: linear-gradient(135deg, #fff8dc, #ffeec7); }
|
||||||
|
.eg-cheer-top { display: flex; align-items: center; justify-content: space-between; }
|
||||||
|
.eg-cheer-source { color: #8e5c18; font-size: 21rpx; font-weight: 900; }
|
||||||
|
.eg-cheer-card.is-family .eg-cheer-source { color: #77539c; }
|
||||||
|
.eg-cheer-change { padding: 5rpx 12rpx; border-radius: 999rpx; color: #176b52; background: rgba(255,255,255,.75); font-size: 19rpx; font-weight: 900; }
|
||||||
|
.eg-cheer-copy { display: block; margin-top: 7rpx; color: #55462d; font-size: 28rpx; font-weight: 900; line-height: 1.42; }
|
||||||
|
.eg-cheer-note { display: block; margin-top: 5rpx; color: #8d7c77; font-size: 18rpx; }
|
||||||
|
.eg-weekly-start { display: flex; height: 84rpx; align-items: center; justify-content: center; margin-top: 18rpx; border-radius: 24rpx; color: #fff; background: linear-gradient(135deg, #24906c, #12654a); box-shadow: 0 9rpx 20rpx rgba(18,101,74,.24); font-size: 30rpx; font-weight: 1000; }
|
||||||
|
.eg-weekly-share { display: flex; width: 100%; height: 76rpx; align-items: center; justify-content: center; margin: 12rpx 0 0; padding: 0; border: 3rpx solid #b9d9cc; border-radius: 22rpx; color: #17684f; background: rgba(255,255,255,.82); font-size: 27rpx; font-weight: 900; line-height: 1; }
|
||||||
|
.eg-weekly-share::after { border: 0; }
|
||||||
|
.eg-weekly-footnote { display: block; margin-top: 10rpx; color: #8b8b7d; font-size: 18rpx; text-align: center; }
|
||||||
|
|
||||||
|
.eg-stage-overlay { position: fixed; z-index: 58; inset: 0; display: flex; align-items: center; justify-content: center; padding: 40rpx; background: rgba(22,62,50,.42); pointer-events: none; }
|
||||||
|
.eg-stage-card { width: 100%; max-width: 620rpx; padding: 34rpx 28rpx 30rpx; border: 6rpx solid #fff; border-radius: 38rpx; background: linear-gradient(155deg, #f5fff9, #fff8da); box-shadow: 0 26rpx 70rpx rgba(10,56,41,.3); text-align: center; animation: egStageIn 1.8s cubic-bezier(.16,.86,.28,1) both; }
|
||||||
|
.eg-stage-kicker { display: block; color: #16815f; font-size: 23rpx; font-weight: 900; letter-spacing: 3rpx; }
|
||||||
|
.eg-stage-number { display: block; margin-top: 5rpx; color: #e47722; font-size: 48rpx; font-weight: 1000; line-height: 1.1; }
|
||||||
|
.eg-stage-title { display: block; margin-top: 7rpx; color: #194e3d; font-size: 38rpx; font-weight: 900; }
|
||||||
|
.eg-stage-foods { display: flex; justify-content: center; gap: 9rpx; margin-top: 22rpx; }
|
||||||
|
.eg-stage-food { display: flex; width: 92rpx; min-height: 110rpx; flex-direction: column; align-items: center; justify-content: center; border: 3rpx solid #dbece4; border-radius: 20rpx; color: #315b4e; background: #fff; font-size: 20rpx; font-weight: 800; }
|
||||||
|
.eg-stage-food > image { width: 72rpx; height: 52rpx; border-radius: 12rpx; }
|
||||||
|
.eg-stage-food > text { margin-top: 8rpx; }
|
||||||
|
.eg-stage-food.is-danger { border-color: #ef8b52; color: #a43c20; background: #fff1e6; box-shadow: 0 0 0 4rpx rgba(239,139,82,.12); }
|
||||||
|
.eg-stage-tip { display: block; margin-top: 20rpx; color: #6e776d; font-size: 21rpx; font-weight: 700; }
|
||||||
|
.eg-reshuffle-overlay { position: fixed; z-index: 65; inset: 0; display: flex; align-items: center; justify-content: center; padding: 46rpx; background: rgba(19,55,45,.5); }
|
||||||
|
.eg-reshuffle-card { width: 100%; max-width: 540rpx; padding: 38rpx 30rpx; border: 5rpx solid rgba(255,255,255,.95); border-radius: 36rpx; background: linear-gradient(155deg, #f4fff9, #e6f6ef); box-shadow: 0 24rpx 64rpx rgba(10,48,37,.3); text-align: center; }
|
||||||
|
.eg-reshuffle-icon { display: flex; width: 94rpx; height: 94rpx; align-items: center; justify-content: center; margin: 0 auto 18rpx; border-radius: 50%; background: linear-gradient(135deg, #2aa87b, #126a50); box-shadow: 0 10rpx 24rpx rgba(18,106,80,.28); animation: egReshuffleSpin .85s linear infinite; }
|
||||||
|
.eg-reshuffle-title { display: block; color: #174e3d; font-size: 36rpx; font-weight: 900; }
|
||||||
|
.eg-reshuffle-desc { display: block; margin-top: 10rpx; color: #5b746b; font-size: 24rpx; font-weight: 700; }
|
||||||
|
|
||||||
|
.eg-overlay { position: fixed; z-index: 50; top: 0; right: 0; bottom: 0; left: 0; display: flex; align-items: center; justify-content: center; padding: 36rpx; background: rgba(20, 48, 40, .62); }
|
||||||
|
.eg-modal { width: 100%; max-width: 650rpx; padding: 34rpx 28rpx 28rpx; border-radius: 38rpx; background: #fffdf8; box-shadow: 0 28rpx 70rpx rgba(0,0,0,.22); }
|
||||||
|
.eg-modal-icon { display: block; font-size: 68rpx; text-align: center; }
|
||||||
|
.eg-modal-title { display: block; margin-top: 6rpx; color: #204e3d; font-size: 38rpx; font-weight: 900; text-align: center; }
|
||||||
|
.eg-modal-sub { display: block; margin: 8rpx 0 22rpx; color: #718078; font-size: 24rpx; text-align: center; }
|
||||||
|
.eg-result-warning { display: flex; width: 92rpx; height: 92rpx; align-items: center; justify-content: center; margin: 0 auto; border-radius: 50%; color: #fff; background: #e4572e; font-size: 60rpx; font-weight: 900; }
|
||||||
|
.eg-result-food { display: block; margin: 12rpx 0 18rpx; color: #c34b27; font-size: 28rpx; font-weight: 800; text-align: center; }
|
||||||
|
.eg-knowledge-card { padding: 22rpx; border-radius: 24rpx; background: #fff3e7; }
|
||||||
|
.eg-knowledge-title { display: block; color: #8d4427; font-size: 26rpx; font-weight: 800; }
|
||||||
|
.eg-knowledge-copy { display: block; margin-top: 9rpx; color: #5f514a; font-size: 23rpx; line-height: 1.65; }
|
||||||
|
.eg-knowledge-review { display: block; margin-top: 12rpx; color: #9b7768; font-size: 19rpx; }
|
||||||
|
.eg-result-score { display: flex; align-items: center; justify-content: space-between; margin: 20rpx 6rpx; color: #466258; font-size: 25rpx; }
|
||||||
|
.eg-result-score text:last-child { color: #e16f24; font-size: 36rpx; font-weight: 900; }
|
||||||
|
.eg-result-best { display: flex; align-items: center; justify-content: space-between; margin: -12rpx 6rpx 20rpx; color: #75867f; font-size: 22rpx; }
|
||||||
|
.eg-result-best text:last-child { color: #315d4e; font-size: 27rpx; font-weight: 900; }
|
||||||
|
.eg-primary-btn, .eg-secondary-btn { display: flex; height: 88rpx; align-items: center; justify-content: center; border-radius: 24rpx; font-size: 28rpx; font-weight: 800; }
|
||||||
|
.eg-primary-btn { color: #fff; background: linear-gradient(135deg, #238b68, #126649); box-shadow: 0 10rpx 22rpx rgba(18,102,73,.23); }
|
||||||
|
.eg-share-btn { display: flex; width: 100%; height: 80rpx; align-items: center; justify-content: center; margin: 12rpx 0 0; padding: 0; border: 3rpx solid #bddbce; border-radius: 24rpx; color: #17664e; background: #f4fbf7; font-size: 27rpx; font-weight: 900; line-height: 1; }
|
||||||
|
.eg-share-btn::after { border: 0; }
|
||||||
|
.eg-secondary-btn { margin-top: 12rpx; color: #45665b; background: #edf4f1; }
|
||||||
|
@keyframes egPulse {
|
||||||
|
0%, 100% { box-shadow: 0 0 0 0 rgba(245,158,11,.2); }
|
||||||
|
50% { box-shadow: 0 0 0 8rpx rgba(245,158,11,.12); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes egWeeklyIn {
|
||||||
|
0% { opacity: 0; transform: translateY(34rpx) scale(.91); }
|
||||||
|
100% { opacity: 1; transform: translateY(0) scale(1); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes egHit {
|
||||||
|
0% { opacity: 1; transform: scale(1); }
|
||||||
|
42% { opacity: 1; transform: scale(1.18) rotate(-3deg); }
|
||||||
|
100% { opacity: 0; transform: scale(.32) rotate(7deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes egTaskRewardFlight {
|
||||||
|
0% { opacity: 0; transform: translate3d(-20rpx, 0, 0) scale(.62) rotate(-8deg); }
|
||||||
|
16% { opacity: 1; transform: translate3d(0, -24rpx, 0) scale(1.1) rotate(3deg); }
|
||||||
|
72% { opacity: 1; transform: translate3d(270rpx, 900rpx, 0) scale(.68) rotate(8deg); }
|
||||||
|
100% { opacity: 0; transform: translate3d(350rpx, 1205rpx, 0) scale(.3) rotate(2deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes egRewardGlow {
|
||||||
|
from { opacity: .4; transform: scale(.86); }
|
||||||
|
to { opacity: .9; transform: scale(1.12); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes egFireSweep {
|
||||||
|
0% { opacity: 0; transform: translate3d(-38%, 20rpx, 0) scaleX(.6); }
|
||||||
|
28% { opacity: 1; }
|
||||||
|
72% { opacity: .96; }
|
||||||
|
100% { opacity: 0; transform: translate3d(58%, -15rpx, 0) scaleX(1.08); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes egFireSpark {
|
||||||
|
0% { opacity: 0; transform: translateY(14rpx) scale(.5) rotate(35deg); }
|
||||||
|
38% { opacity: 1; }
|
||||||
|
100% { opacity: 0; transform: translateY(-38rpx) scale(1.05) rotate(58deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes egFireGlow {
|
||||||
|
0% { opacity: 0; transform: scale(.72); }
|
||||||
|
38% { opacity: 1; transform: scale(1.08); }
|
||||||
|
100% { opacity: 0; transform: scale(1.28); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes egFireCellHit {
|
||||||
|
0% { opacity: 1; transform: scale(1); }
|
||||||
|
24% { opacity: 1; transform: scale(1.09); }
|
||||||
|
72% { opacity: 1; transform: scale(1.02); }
|
||||||
|
100% { opacity: 0; transform: scale(.48); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes egFireFlame {
|
||||||
|
0% { opacity: 0; transform: translateY(20rpx) rotate(42deg) scale(.55); }
|
||||||
|
38% { opacity: 1; transform: translateY(0) rotate(42deg) scale(1.05); }
|
||||||
|
100% { opacity: 0; transform: translateY(-31rpx) rotate(48deg) scale(.86); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes egFlameReady {
|
||||||
|
from { transform: scale(.94); filter: brightness(.96); }
|
||||||
|
to { transform: scale(1.08); filter: brightness(1.18); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes egLReady {
|
||||||
|
from { transform: scale(.94) rotate(-2deg); filter: brightness(.96); }
|
||||||
|
to { transform: scale(1.06) rotate(2deg); filter: brightness(1.12); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes egImpact {
|
||||||
|
0% { opacity: 0; transform: translate(-50%, -42%) scale(.55); }
|
||||||
|
35% { opacity: 1; transform: translate(-50%, -50%) scale(1.16); }
|
||||||
|
72% { opacity: 1; transform: translate(-50%, -54%) scale(1); }
|
||||||
|
100% { opacity: 0; transform: translate(-50%, -76%) scale(.92); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes egJackpotIn {
|
||||||
|
0% { opacity: 0; transform: translate(-50%, -45%) scale(.48) rotate(-4deg); }
|
||||||
|
20% { opacity: 1; transform: translate(-50%, -50%) scale(1.09) rotate(2deg); }
|
||||||
|
31% { transform: translate(-50%, -50%) scale(.98) rotate(0); }
|
||||||
|
78% { opacity: 1; transform: translate(-50%, -53%) scale(1); }
|
||||||
|
100% { opacity: 0; transform: translate(-50%, -68%) scale(.92); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes egTripleIn {
|
||||||
|
0% { opacity: 0; transform: translate(-50%, -44%) scale(.82); }
|
||||||
|
20% { opacity: 1; transform: translate(-50%, -50%) scale(1.03); }
|
||||||
|
74% { opacity: 1; transform: translate(-50%, -52%) scale(1); }
|
||||||
|
100% { opacity: 0; transform: translate(-50%, -60%) scale(.96); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes egTripleCan {
|
||||||
|
0% { opacity: 0; transform: translateY(18rpx) scale(.72); }
|
||||||
|
100% { opacity: 1; transform: translateY(0) scale(1); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes egFourIn {
|
||||||
|
0% { opacity: 0; transform: translate(-50%, -44%) scale(.62) rotate(-3deg); }
|
||||||
|
18% { opacity: 1; transform: translate(-50%, -50%) scale(1.08) rotate(1deg); }
|
||||||
|
29% { transform: translate(-50%, -50%) scale(.98) rotate(0); }
|
||||||
|
78% { opacity: 1; transform: translate(-50%, -52%) scale(1); }
|
||||||
|
100% { opacity: 0; transform: translate(-50%, -65%) scale(.94); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes egFourCan {
|
||||||
|
0% { opacity: 0; transform: translateY(-22rpx) rotate(-7deg) scale(.65); }
|
||||||
|
75% { opacity: 1; transform: translateY(5rpx) rotate(2deg) scale(1.05); }
|
||||||
|
100% { opacity: 1; transform: translateY(0) rotate(0) scale(1); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes egFiveCan {
|
||||||
|
0% { opacity: 0; transform: translateY(-40rpx) scale(.52) rotate(-8deg); filter: brightness(1.5); }
|
||||||
|
70% { opacity: 1; transform: translateY(7rpx) scale(1.08) rotate(2deg); }
|
||||||
|
100% { opacity: 1; transform: translateY(0) scale(1) rotate(0); filter: brightness(1); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes egLockedCan {
|
||||||
|
from { opacity: .62; transform: scale(.92); filter: brightness(.9); }
|
||||||
|
to { opacity: 1; transform: scale(1.06); filter: brightness(1.2); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes egPromisePulse {
|
||||||
|
from { transform: scale(.97); box-shadow: 0 5rpx 15rpx rgba(28,7,62,.22); }
|
||||||
|
to { transform: scale(1.03); box-shadow: 0 7rpx 21rpx rgba(255,221,89,.38); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes egReelStop {
|
||||||
|
0% { opacity: .2; transform: translateY(-48rpx) scaleY(1.4); filter: blur(5rpx); }
|
||||||
|
70% { opacity: 1; transform: translateY(7rpx) scaleY(.94); filter: blur(0); }
|
||||||
|
100% { transform: translateY(0) scaleY(1); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes egLights {
|
||||||
|
0% { opacity: .35; }
|
||||||
|
100% { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes egConfettiFall {
|
||||||
|
0% { opacity: 0; transform: translate3d(0, -8vh, 0) rotate(0deg) scale(.7); }
|
||||||
|
9% { opacity: 1; }
|
||||||
|
48% { transform: translate3d(42rpx, 48vh, 0) rotate(420deg) scale(1); }
|
||||||
|
100% { opacity: .92; transform: translate3d(-28rpx, 112vh, 0) rotate(920deg) scale(.86); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes egPointsPop {
|
||||||
|
0% { opacity: 0; transform: scale(.35) rotate(-7deg); }
|
||||||
|
55% { opacity: 1; transform: scale(1.28) rotate(3deg); }
|
||||||
|
78% { transform: scale(.92) rotate(0); }
|
||||||
|
100% { opacity: 1; transform: scale(1); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes egStageIn {
|
||||||
|
0% { opacity: 0; transform: scale(.7) translateY(34rpx); }
|
||||||
|
18% { opacity: 1; transform: scale(1.06) translateY(0); }
|
||||||
|
30%, 76% { opacity: 1; transform: scale(1); }
|
||||||
|
100% { opacity: 0; transform: scale(.96) translateY(-22rpx); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes egReshuffleSpin {
|
||||||
|
from { transform: rotate(0); }
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-height: 760px) {
|
||||||
|
.eg-task-card { padding-top: 13rpx; padding-bottom: 13rpx; }
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -419,7 +419,7 @@
|
|||||||
|
|
||||||
<view v-else class="ai-diet-body" :class="{ 'is-streaming': dietAiStreaming }">
|
<view v-else class="ai-diet-body" :class="{ 'is-streaming': dietAiStreaming }">
|
||||||
<view class="ai-diet-section-head">
|
<view class="ai-diet-section-head">
|
||||||
<text class="ai-diet-section-title">今日三餐</text>
|
<text class="ai-diet-section-title">今日饮食</text>
|
||||||
<text v-if="dietAiStreaming" class="ai-diet-section-tag">生成中…</text>
|
<text v-if="dietAiStreaming" class="ai-diet-section-tag">生成中…</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="ai-diet-meal-list">
|
<view class="ai-diet-meal-list">
|
||||||
@@ -434,6 +434,20 @@
|
|||||||
{{ dietAiRecommend.breakfast || (dietAiLoading ? '…' : '—') }}<text v-if="dietAiLoading && dietAiStreamField === 'breakfast'" class="ai-stream-cursor">▍</text>
|
{{ dietAiRecommend.breakfast || (dietAiLoading ? '…' : '—') }}<text v-if="dietAiLoading && dietAiStreamField === 'breakfast'" class="ai-stream-cursor">▍</text>
|
||||||
</text>
|
</text>
|
||||||
</view>
|
</view>
|
||||||
|
<view
|
||||||
|
v-if="dietAiRecommend.drinks || (dietAiLoading && dietAiStreamField === 'drinks')"
|
||||||
|
class="ai-diet-meal-card meal-drinks"
|
||||||
|
>
|
||||||
|
<view class="ai-diet-meal-head">
|
||||||
|
<view class="ai-diet-meal-icon" aria-hidden="true">
|
||||||
|
<TongjiIcon name="droplet" size="sm" color="#ffffff" />
|
||||||
|
</view>
|
||||||
|
<text class="ai-diet-meal-label">喝的</text>
|
||||||
|
</view>
|
||||||
|
<text class="ai-diet-meal-text">
|
||||||
|
{{ dietAiRecommend.drinks || '…' }}<text v-if="dietAiLoading && dietAiStreamField === 'drinks'" class="ai-stream-cursor">▍</text>
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
<view class="ai-diet-meal-card meal-lunch">
|
<view class="ai-diet-meal-card meal-lunch">
|
||||||
<view class="ai-diet-meal-head">
|
<view class="ai-diet-meal-head">
|
||||||
<view class="ai-diet-meal-icon" aria-hidden="true">
|
<view class="ai-diet-meal-icon" aria-hidden="true">
|
||||||
@@ -2657,16 +2671,13 @@ async function bootstrap(options) {
|
|||||||
if (!target) target = list[0]
|
if (!target) target = list[0]
|
||||||
await applyCard(target)
|
await applyCard(target)
|
||||||
} else {
|
} else {
|
||||||
if (passedDiagnosisId) diagnosisId.value = passedDiagnosisId
|
diagnosisId.value = 0
|
||||||
if (passedPatientId) patientId.value = passedPatientId
|
patientId.value = 0
|
||||||
if (passedPatientName) patientName.value = passedPatientName
|
patientName.value = ''
|
||||||
if (passedAge) age.value = passedAge
|
age.value = 0
|
||||||
if (passedGender) gender.value = passedGender
|
gender.value = 0
|
||||||
if (!diagnosisId.value || !patientId.value) {
|
|
||||||
readUserContext()
|
|
||||||
}
|
|
||||||
readUserMobile()
|
readUserMobile()
|
||||||
if (!diagnosisId.value && userMobile.value) {
|
if (userMobile.value) {
|
||||||
await ensureDailyContextByPhone()
|
await ensureDailyContextByPhone()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6558,6 +6569,12 @@ async function onFamilyLike() {
|
|||||||
.ai-diet-meal-card.meal-breakfast::before {
|
.ai-diet-meal-card.meal-breakfast::before {
|
||||||
background: linear-gradient(180deg, #fbbf24 0%, #f59e0b 100%);
|
background: linear-gradient(180deg, #fbbf24 0%, #f59e0b 100%);
|
||||||
}
|
}
|
||||||
|
.ai-diet-meal-card.meal-drinks {
|
||||||
|
background: linear-gradient(120deg, rgba(14, 165, 233, 0.14) 0%, rgba(255, 255, 255, 0) 45%);
|
||||||
|
}
|
||||||
|
.ai-diet-meal-card.meal-drinks::before {
|
||||||
|
background: linear-gradient(180deg, #38bdf8 0%, #0ea5e9 100%);
|
||||||
|
}
|
||||||
.ai-diet-meal-card.meal-lunch {
|
.ai-diet-meal-card.meal-lunch {
|
||||||
background: linear-gradient(120deg, rgba(16, 185, 129, 0.16) 0%, rgba(255, 255, 255, 0) 45%);
|
background: linear-gradient(120deg, rgba(16, 185, 129, 0.16) 0%, rgba(255, 255, 255, 0) 45%);
|
||||||
}
|
}
|
||||||
@@ -6588,6 +6605,9 @@ async function onFamilyLike() {
|
|||||||
.meal-breakfast .ai-diet-meal-icon {
|
.meal-breakfast .ai-diet-meal-icon {
|
||||||
background: linear-gradient(150deg, #fde68a 0%, #fbbf24 100%);
|
background: linear-gradient(150deg, #fde68a 0%, #fbbf24 100%);
|
||||||
}
|
}
|
||||||
|
.meal-drinks .ai-diet-meal-icon {
|
||||||
|
background: linear-gradient(150deg, #7dd3fc 0%, #0ea5e9 100%);
|
||||||
|
}
|
||||||
.meal-lunch .ai-diet-meal-icon {
|
.meal-lunch .ai-diet-meal-icon {
|
||||||
background: linear-gradient(150deg, #6ee7b7 0%, #10b981 100%);
|
background: linear-gradient(150deg, #6ee7b7 0%, #10b981 100%);
|
||||||
}
|
}
|
||||||
|
|||||||
+2330
-521
File diff suppressed because it is too large
Load Diff
@@ -45,19 +45,21 @@
|
|||||||
</view>
|
</view>
|
||||||
</scroll-view>
|
</scroll-view>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<view class="st-main">
|
<view class="st-main">
|
||||||
<!-- 1. 录入今日血糖 -->
|
<!-- 快捷建档失败提示(toast 字数有限,完整文案展示在页面) -->
|
||||||
<view class="st-cta" @click="openInputForm()">
|
<view v-if="dailyContextError && !diagnosisId" class="st-daily-context-error" role="alert">
|
||||||
<TongjiIcon name="plus" size="md" color="#FFFFFF" />
|
<text class="st-daily-context-error-text">{{ dailyContextError }}</text>
|
||||||
<text>录入今日血糖</text>
|
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 2. 血糖趋势 -->
|
<!-- 2. 血糖趋势(无就诊卡或累计录入不足 7 天显示示例曲线) -->
|
||||||
<view v-if="diagnosisId" class="st-chart-card">
|
<view class="st-chart-card">
|
||||||
<view class="st-chart-head">
|
<view class="st-chart-head">
|
||||||
<view>
|
<view style="padding: 0 20rpx;">
|
||||||
<text class="st-chart-title">血糖趋势</text>
|
<text class="st-chart-title">血糖趋势</text>
|
||||||
<text class="st-chart-sub">{{ rangeText }} · mmol/L</text>
|
<text class="st-chart-sub">{{ rangeText }} · mmol/L</text>
|
||||||
|
|
||||||
</view>
|
</view>
|
||||||
<view class="st-chart-legend">
|
<view class="st-chart-legend">
|
||||||
<view
|
<view
|
||||||
@@ -78,82 +80,41 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view v-if="bloodHasData" class="st-chart-summary">
|
<view class="st-chart-area" @click="onChartTap">
|
||||||
<view class="st-chart-summary-item">
|
|
||||||
<text class="st-chart-summary-num is-high">{{ bloodHighDayCount }}</text>
|
|
||||||
<text class="st-chart-summary-label">天偏高</text>
|
|
||||||
</view>
|
|
||||||
<view class="st-chart-summary-item">
|
|
||||||
<text class="st-chart-summary-num is-good">{{ bloodNormalDayCount }}</text>
|
|
||||||
<text class="st-chart-summary-label">天正常</text>
|
|
||||||
</view>
|
|
||||||
<view class="st-chart-summary-item">
|
|
||||||
<text class="st-chart-summary-num">{{ bloodRecordDayCount }}</text>
|
|
||||||
<text class="st-chart-summary-label">天有记录</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
<view class="st-chart-area">
|
|
||||||
<canvas
|
<canvas
|
||||||
v-if="!recordOverlayOpen"
|
v-if="!recordOverlayOpen"
|
||||||
id="bloodWaveChart"
|
id="bloodWaveChart"
|
||||||
type="2d"
|
type="2d"
|
||||||
class="st-chart-canvas"
|
class="st-chart-canvas"
|
||||||
@touchstart="onChartTouch"
|
@click.stop="onChartTap"
|
||||||
@touchmove="onChartTouch"
|
|
||||||
/>
|
/>
|
||||||
<view v-if="recordOverlayOpen" class="st-chart-placeholder">
|
<view v-if="recordOverlayOpen" class="st-chart-placeholder">
|
||||||
<text>图表已暂停显示</text>
|
<text>图表已暂停显示</text>
|
||||||
</view>
|
</view>
|
||||||
<view v-if="!bloodHasData && !loading && !recordOverlayOpen" class="st-chart-empty">
|
<view v-if="!chartHasDisplayData && !loading && !recordOverlayOpen" class="st-chart-empty">
|
||||||
<text>当前时间范围暂无血糖数据</text>
|
<text>当前时间范围暂无血糖数据</text>
|
||||||
</view>
|
</view>
|
||||||
<cover-view v-if="hoverInfo && !recordOverlayOpen" class="chart-tip" :style="{ left: hoverInfo.left + 'px', top: hoverInfo.top + 'px' }">
|
|
||||||
<cover-view class="chart-tip-date">{{ hoverInfo.date }}</cover-view>
|
|
||||||
<cover-view v-if="hoverInfo.fasting != null" class="chart-tip-row">
|
|
||||||
<cover-view class="st-dot st-dot-fasting" />
|
|
||||||
<cover-view>空腹 {{ hoverInfo.fasting }}</cover-view>
|
|
||||||
</cover-view>
|
|
||||||
<cover-view v-if="hoverInfo.postprandial != null" class="chart-tip-row">
|
|
||||||
<cover-view class="st-dot st-dot-post" />
|
|
||||||
<cover-view>餐后 {{ hoverInfo.postprandial }}</cover-view>
|
|
||||||
</cover-view>
|
|
||||||
</cover-view>
|
|
||||||
</view>
|
</view>
|
||||||
|
<view v-if="chartUseMockData" class="st-chart-foot">
|
||||||
|
<text class="st-chart-mock-hint">示例数据,累计记录满 7 天后显示您的真实趋势</text>
|
||||||
</view>
|
</view>
|
||||||
|
<view class="st-game-entry-list">
|
||||||
<!-- 3. 最新测量 & 概览 -->
|
<view class="st-game-entry st-game-entry--in-card" @click="goEndlessGamePage">
|
||||||
<view class="st-latest-section">
|
<view class="st-game-entry-icon">
|
||||||
<view
|
<TongjiIcon name="sparkles" size="sm" color="#006c49" />
|
||||||
v-if="latestMeasurement.hasValue"
|
|
||||||
class="st-latest-hero"
|
|
||||||
:class="{ 'is-high': latestMeasurement.isHigh }"
|
|
||||||
>
|
|
||||||
<view class="st-latest-hero-pattern" aria-hidden="true" />
|
|
||||||
<text class="st-latest-label">最新测量 · {{ latestMeasurement.typeLabel }}</text>
|
|
||||||
<view class="st-latest-value-row">
|
|
||||||
<text class="st-latest-value">{{ latestMeasurement.value }}</text>
|
|
||||||
<text class="st-latest-unit">mmol/L</text>
|
|
||||||
</view>
|
</view>
|
||||||
<view v-if="latestMeasurement.isHigh" class="st-latest-flag">
|
<view class="st-game-entry-body">
|
||||||
<TongjiIcon name="alert-triangle" size="sm" color="#93000a" />
|
<text class="st-game-entry-title">识糖小课堂</text>
|
||||||
<text>偏高,请遵医嘱</text>
|
<text class="st-game-entry-sub">三消认食物,学习哪些食物含糖高</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
<TongjiIcon name="chevron-right" size="sm" color="#64748b" />
|
||||||
<view class="st-summary-grid">
|
|
||||||
<view class="st-summary-card">
|
|
||||||
<text class="st-summary-label">空腹</text>
|
|
||||||
<text class="st-summary-value" :class="{ 'is-high': elderFasting.isHigh }">{{ elderFasting.value }}</text>
|
|
||||||
<text v-if="elderFasting.isHigh" class="st-summary-tag">偏高</text>
|
|
||||||
</view>
|
|
||||||
<view class="st-summary-card">
|
|
||||||
<text class="st-summary-label">餐后</text>
|
|
||||||
<text class="st-summary-value" :class="{ 'is-high': elderPostprandial.isHigh }">{{ elderPostprandial.value }}</text>
|
|
||||||
<text v-if="elderPostprandial.isHigh" class="st-summary-tag">偏高</text>
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
<view class="st-training-section">
|
||||||
<!-- 4. AI 饮食建议 -->
|
<dev-training-entry inline tone="weekly" />
|
||||||
|
</view>
|
||||||
|
<!-- 3. AI 饮食建议 -->
|
||||||
<view v-if="diagnosisId" class="st-ai-section">
|
<view v-if="diagnosisId" class="st-ai-section">
|
||||||
<view class="st-ai-deco" aria-hidden="true" />
|
<view class="st-ai-deco" aria-hidden="true" />
|
||||||
<view class="st-ai-head">
|
<view class="st-ai-head">
|
||||||
@@ -172,7 +133,7 @@
|
|||||||
<text>{{ dietAiLoading ? '生成中' : '换一换' }}</text>
|
<text>{{ dietAiLoading ? '生成中' : '换一换' }}</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<text class="st-ai-sub">读您近 7 天 · 30 天血糖,定制今日三餐</text>
|
<text class="st-ai-sub">读您近 7 天 · 30 天血糖,定制早餐(含驼奶粉)、喝的与三餐</text>
|
||||||
|
|
||||||
<view v-if="dietAiLoading && !hasDietAiMealContent && !dietAiStreaming && !dietAiReplacing" class="st-skeleton">
|
<view v-if="dietAiLoading && !hasDietAiMealContent && !dietAiStreaming && !dietAiReplacing" class="st-skeleton">
|
||||||
<view v-for="n in 2" :key="'sk-' + n" class="st-skeleton-line" />
|
<view v-for="n in 2" :key="'sk-' + n" class="st-skeleton-line" />
|
||||||
@@ -188,6 +149,18 @@
|
|||||||
{{ dietAiRecommend.breakfast || (dietAiLoading ? '…' : '—') }}<text v-if="dietAiLoading && dietAiStreamField === 'breakfast'" class="ai-stream-cursor">▍</text>
|
{{ dietAiRecommend.breakfast || (dietAiLoading ? '…' : '—') }}<text v-if="dietAiLoading && dietAiStreamField === 'breakfast'" class="ai-stream-cursor">▍</text>
|
||||||
</text>
|
</text>
|
||||||
</view>
|
</view>
|
||||||
|
<view
|
||||||
|
v-if="dietAiRecommend.drinks || (dietAiLoading && dietAiStreamField === 'drinks')"
|
||||||
|
class="st-meal-glass st-meal-glass--drinks"
|
||||||
|
>
|
||||||
|
<view class="st-meal-head">
|
||||||
|
<TongjiIcon name="droplet" size="sm" color="#0ea5e9" />
|
||||||
|
<text class="st-meal-label">喝的</text>
|
||||||
|
</view>
|
||||||
|
<text class="st-meal-text">
|
||||||
|
{{ dietAiRecommend.drinks || '…' }}<text v-if="dietAiLoading && dietAiStreamField === 'drinks'" class="ai-stream-cursor">▍</text>
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
<view class="st-meal-glass">
|
<view class="st-meal-glass">
|
||||||
<view class="st-meal-head">
|
<view class="st-meal-head">
|
||||||
<TongjiIcon name="leaf" size="sm" color="#006c49" />
|
<TongjiIcon name="leaf" size="sm" color="#006c49" />
|
||||||
@@ -212,22 +185,46 @@
|
|||||||
<view class="st-main-spacer" />
|
<view class="st-main-spacer" />
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<!-- 收起后的重新唤起按钮 -->
|
||||||
|
<view v-if="diagnosisId && floatAskClosed" class="st-float-reopen" @click="floatAskClosed = false">
|
||||||
|
<TongjiIcon name="sparkles" size="sm" color="#ffffff" />
|
||||||
|
</view>
|
||||||
|
|
||||||
<!-- 底部浮动 AI 输入栏 -->
|
<!-- 底部浮动 AI 输入栏 -->
|
||||||
<view v-if="diagnosisId" class="st-float-ask">
|
<view v-if="diagnosisId && !floatAskClosed" class="st-float-ask">
|
||||||
<view class="st-float-inner">
|
<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">
|
<view class="st-float-icon">
|
||||||
<TongjiIcon name="sparkles" size="sm" color="#006c49" />
|
<TongjiIcon name="sparkles" size="sm" color="#006c49" />
|
||||||
</view>
|
</view>
|
||||||
<input
|
<textarea
|
||||||
class="st-float-input"
|
class="st-float-input"
|
||||||
type="text"
|
|
||||||
:value="dietAiAskText"
|
:value="dietAiAskText"
|
||||||
placeholder="输入食物名称或健康疑问,咨询 AI..."
|
placeholder="输入或语音咨询 AI…"
|
||||||
placeholder-class="st-float-placeholder"
|
placeholder-class="st-float-placeholder"
|
||||||
|
:maxlength="-1"
|
||||||
|
auto-height
|
||||||
|
:show-confirm-bar="false"
|
||||||
|
:disable-default-padding="true"
|
||||||
|
:cursor-spacing="24"
|
||||||
confirm-type="send"
|
confirm-type="send"
|
||||||
|
:confirm-hold="false"
|
||||||
@input="onDietAiAskInput"
|
@input="onDietAiAskInput"
|
||||||
@confirm="askDietAiFood"
|
@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
|
<view
|
||||||
class="st-float-send"
|
class="st-float-send"
|
||||||
:class="{ disabled: dietAiAsking }"
|
:class="{ disabled: dietAiAsking }"
|
||||||
@@ -236,6 +233,9 @@
|
|||||||
<TongjiIcon name="send" size="sm" color="#ffffff" />
|
<TongjiIcon name="send" size="sm" color="#ffffff" />
|
||||||
</view>
|
</view>
|
||||||
</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="dietAiAsking || dietAiAskResult.advice" class="st-float-result">
|
||||||
<view v-if="dietAiAskResult.level_label" class="st-float-badge">
|
<view v-if="dietAiAskResult.level_label" class="st-float-badge">
|
||||||
<text>{{ dietAiAskResult.level_label }}</text>
|
<text>{{ dietAiAskResult.level_label }}</text>
|
||||||
@@ -246,6 +246,17 @@
|
|||||||
</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>
|
||||||
|
<text class="st-voice-title">{{ sttListening ? '正在聆听…' : '准备录音…' }}</text>
|
||||||
|
<text class="st-voice-sub">松开手指结束并识别</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
<!-- 无就诊卡:授权手机号后可录入 -->
|
<!-- 无就诊卡:授权手机号后可录入 -->
|
||||||
<view v-if="phoneGateVisible" class="input-modal-mask" @click="closePhoneGate">
|
<view v-if="phoneGateVisible" class="input-modal-mask" @click="closePhoneGate">
|
||||||
<view class="input-modal phone-gate-modal" @click.stop>
|
<view class="input-modal phone-gate-modal" @click.stop>
|
||||||
@@ -260,6 +271,9 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="phone-gate-body">
|
<view class="phone-gate-body">
|
||||||
|
<view v-if="dailyContextError" class="phone-gate-error" role="alert">
|
||||||
|
<text>{{ dailyContextError }}</text>
|
||||||
|
</view>
|
||||||
<text class="phone-gate-tip">微信仅提供手机号,请选择性别以便写入就诊档案。</text>
|
<text class="phone-gate-tip">微信仅提供手机号,请选择性别以便写入就诊档案。</text>
|
||||||
<view class="phone-gate-gender">
|
<view class="phone-gate-gender">
|
||||||
<text class="phone-gate-gender-label">性别</text>
|
<text class="phone-gate-gender-label">性别</text>
|
||||||
@@ -295,7 +309,7 @@
|
|||||||
|
|
||||||
<!-- 录入今日数据弹层 -->
|
<!-- 录入今日数据弹层 -->
|
||||||
<view v-if="inputForm.visible" class="input-modal-mask" @click="closeInputForm">
|
<view v-if="inputForm.visible" class="input-modal-mask" @click="closeInputForm">
|
||||||
<view class="input-modal" @click.stop>
|
<view class="input-modal" :style="keyboardHeight ? { marginBottom: keyboardHeight + 'px' } : {}" @click.stop>
|
||||||
<view class="input-modal-grip" />
|
<view class="input-modal-grip" />
|
||||||
<view class="input-modal-head">
|
<view class="input-modal-head">
|
||||||
<view class="input-modal-title-wrap">
|
<view class="input-modal-title-wrap">
|
||||||
@@ -324,6 +338,7 @@
|
|||||||
<view class="input-field-input">
|
<view class="input-field-input">
|
||||||
<input
|
<input
|
||||||
type="digit"
|
type="digit"
|
||||||
|
:adjust-position="false"
|
||||||
:value="inputForm.fasting_blood_sugar"
|
:value="inputForm.fasting_blood_sugar"
|
||||||
placeholder="如 6.5"
|
placeholder="如 6.5"
|
||||||
placeholder-class="input-placeholder"
|
placeholder-class="input-placeholder"
|
||||||
@@ -339,6 +354,7 @@
|
|||||||
<view class="input-field-input">
|
<view class="input-field-input">
|
||||||
<input
|
<input
|
||||||
type="digit"
|
type="digit"
|
||||||
|
:adjust-position="false"
|
||||||
:value="inputForm.postprandial_blood_sugar"
|
:value="inputForm.postprandial_blood_sugar"
|
||||||
placeholder="如 8.0"
|
placeholder="如 8.0"
|
||||||
placeholder-class="input-placeholder"
|
placeholder-class="input-placeholder"
|
||||||
@@ -354,6 +370,7 @@
|
|||||||
<view class="input-field-input">
|
<view class="input-field-input">
|
||||||
<input
|
<input
|
||||||
type="digit"
|
type="digit"
|
||||||
|
:adjust-position="false"
|
||||||
:value="inputForm.other_blood_sugar"
|
:value="inputForm.other_blood_sugar"
|
||||||
placeholder="如 7.2"
|
placeholder="如 7.2"
|
||||||
placeholder-class="input-placeholder"
|
placeholder-class="input-placeholder"
|
||||||
@@ -370,6 +387,7 @@
|
|||||||
placeholder-class="input-placeholder"
|
placeholder-class="input-placeholder"
|
||||||
:show-confirm-bar="false"
|
:show-confirm-bar="false"
|
||||||
:auto-height="true"
|
:auto-height="true"
|
||||||
|
:adjust-position="false"
|
||||||
@input="onInputField('remark', $event)"
|
@input="onInputField('remark', $event)"
|
||||||
/>
|
/>
|
||||||
</view>
|
</view>
|
||||||
@@ -395,6 +413,7 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</block>
|
</block>
|
||||||
|
<TabBarDock :active="1" />
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -404,6 +423,9 @@ import { onLoad, onShow, onHide, onUnload, onPullDownRefresh, onShareAppMessage
|
|||||||
import SugarTreeGraphic from '../components/SugarTreeGraphic.vue'
|
import SugarTreeGraphic from '../components/SugarTreeGraphic.vue'
|
||||||
import TongjiIcon from '../components/TongjiIcon.vue'
|
import TongjiIcon from '../components/TongjiIcon.vue'
|
||||||
import { useDietAi } from '../composables/useDietAi.js'
|
import { useDietAi } from '../composables/useDietAi.js'
|
||||||
|
import { useSpeechToText } from '../composables/useSpeechToText.js'
|
||||||
|
import TabBarDock from '@/components/app-tab-bar/tab-bar-dock.vue'
|
||||||
|
import DevTrainingEntry from '@/components/dev-training-entry/index.vue'
|
||||||
|
|
||||||
const { proxy } = getCurrentInstance()
|
const { proxy } = getCurrentInstance()
|
||||||
|
|
||||||
@@ -436,11 +458,42 @@ function showUserToast(title, options = {}) {
|
|||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: text,
|
title: text,
|
||||||
icon: options.icon || 'none',
|
icon: options.icon || 'none',
|
||||||
duration: options.duration
|
duration: options.duration || 2800
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 解析接口错误文案(兼容 msg / message / data.error) */
|
||||||
|
function extractApiError(res, fallback = '') {
|
||||||
|
if (!res) return fallback
|
||||||
|
const raw = res.msg ?? res.message ?? res.data?.error ?? res.data?.msg ?? res.errMsg
|
||||||
|
return formatUserMessage(raw, fallback)
|
||||||
|
}
|
||||||
|
|
||||||
const diagnosisId = ref(0)
|
const diagnosisId = ref(0)
|
||||||
|
/** 手机号快捷建档失败:页面常驻提示(避免 toast 被 7 字截断) */
|
||||||
|
const dailyContextError = ref('')
|
||||||
|
|
||||||
|
function clearDailyContextError() {
|
||||||
|
dailyContextError.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function reportDailyContextError(res, fallback = '无法开始录入', options = {}) {
|
||||||
|
const text = extractApiError(res, fallback)
|
||||||
|
if (!text) return
|
||||||
|
dailyContextError.value = text
|
||||||
|
if (options.alert) {
|
||||||
|
uni.showModal({
|
||||||
|
title: '无法录入血糖',
|
||||||
|
content: text,
|
||||||
|
showCancel: false,
|
||||||
|
confirmText: '知道了'
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
const short = text.length > 14 ? `${text.slice(0, 14)}…` : text
|
||||||
|
showUserToast(short, { duration: 3200 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const patientId = ref(0)
|
const patientId = ref(0)
|
||||||
const patientName = ref('')
|
const patientName = ref('')
|
||||||
const displayPatientName = computed(() => {
|
const displayPatientName = computed(() => {
|
||||||
@@ -472,6 +525,52 @@ const {
|
|||||||
openDietForm
|
openDietForm
|
||||||
} = useDietAi(proxy, { diagnosisId, showUserToast, formatUserMessage })
|
} = useDietAi(proxy, { diagnosisId, showUserToast, formatUserMessage })
|
||||||
|
|
||||||
|
/** 底部 AI 浮动卡片是否被用户收起 */
|
||||||
|
const floatAskClosed = ref(false)
|
||||||
|
|
||||||
|
/** AI 输入框是否聚焦:聚焦时输入框变大 */
|
||||||
|
const dietAiAskFocused = ref(false)
|
||||||
|
function onDietAiAskFocus() {
|
||||||
|
dietAiAskFocused.value = true
|
||||||
|
}
|
||||||
|
function onDietAiAskBlur() {
|
||||||
|
dietAiAskFocused.value = false
|
||||||
|
}
|
||||||
|
/** 聚焦或已有内容时展开为多行输入(内容多则自动增高) */
|
||||||
|
const dietAiAskExpanded = computed(
|
||||||
|
() => dietAiAskFocused.value || String(dietAiAskText.value || '').length > 0
|
||||||
|
)
|
||||||
|
|
||||||
|
/** 键盘高度(px):录入弹窗据此整体上移,避免输入框被键盘遮挡 */
|
||||||
|
const keyboardHeight = ref(0)
|
||||||
|
function onKeyboardHeightChange(res) {
|
||||||
|
keyboardHeight.value = res && res.height ? Math.round(res.height) : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
sttListening,
|
||||||
|
sttHolding,
|
||||||
|
sttSupported,
|
||||||
|
beginHoldSpeech,
|
||||||
|
endHoldSpeech,
|
||||||
|
stopSpeechToText
|
||||||
|
} = useSpeechToText({
|
||||||
|
onResult(text) {
|
||||||
|
const prev = String(dietAiAskText.value || '').trim()
|
||||||
|
dietAiAskText.value = prev ? `${prev} ${text}` : text
|
||||||
|
},
|
||||||
|
showToast: (msg) => showUserToast(msg)
|
||||||
|
})
|
||||||
|
|
||||||
|
function onDietAiSpeechStart() {
|
||||||
|
if (dietAiAsking.value) return
|
||||||
|
beginHoldSpeech()
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDietAiSpeechEnd() {
|
||||||
|
endHoldSpeech()
|
||||||
|
}
|
||||||
|
|
||||||
/** AI 饮食建议依据:近7天 / 近30天 血糖统计 */
|
/** AI 饮食建议依据:近7天 / 近30天 血糖统计 */
|
||||||
const dietAnalysisList = computed(() => {
|
const dietAnalysisList = computed(() => {
|
||||||
const list = dietAiRecommend.value && Array.isArray(dietAiRecommend.value.analysis)
|
const list = dietAiRecommend.value && Array.isArray(dietAiRecommend.value.analysis)
|
||||||
@@ -603,6 +702,19 @@ const currentRange = computed(() => {
|
|||||||
return { start: formatDate(start), end: formatDate(end) }
|
return { start: formatDate(start), end: formatDate(end) }
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/** 拉取血糖时向前多看几天,用于判断「累计满 7 天」是否达标(图表仍只画 currentRange) */
|
||||||
|
const BLOOD_LOOKBACK_DAYS = 30
|
||||||
|
|
||||||
|
const fetchRange = computed(() => {
|
||||||
|
const displayDays = rangeMode.value === '30' ? 30 : 7
|
||||||
|
const lookback = Math.max(BLOOD_LOOKBACK_DAYS, displayDays)
|
||||||
|
const end = new Date()
|
||||||
|
end.setHours(0, 0, 0, 0)
|
||||||
|
const start = new Date(end)
|
||||||
|
start.setDate(start.getDate() - (lookback - 1))
|
||||||
|
return { start: formatDate(start), end: formatDate(end) }
|
||||||
|
})
|
||||||
|
|
||||||
const visibleDates = computed(() => {
|
const visibleDates = computed(() => {
|
||||||
const { start, end } = currentRange.value
|
const { start, end } = currentRange.value
|
||||||
if (!start || !end) return []
|
if (!start || !end) return []
|
||||||
@@ -765,18 +877,73 @@ const recordDays = computed(() => {
|
|||||||
return result
|
return result
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function bloodDayHasSugar(b) {
|
||||||
|
if (!b) return false
|
||||||
|
return toNumber(b.fasting_blood_sugar) !== null
|
||||||
|
|| toNumber(b.postprandial_blood_sugar) !== null
|
||||||
|
|| toNumber(b.other_blood_sugar) !== null
|
||||||
|
}
|
||||||
|
|
||||||
const bloodHasData = computed(() => {
|
const bloodHasData = computed(() => {
|
||||||
return chartDates.value.some((date) => {
|
return chartDates.value.some((date) => bloodDayHasSugar(bloodByDate.value[date]))
|
||||||
const b = bloodByDate.value[date]
|
|
||||||
return b && (toNumber(b.fasting_blood_sugar) !== null || toNumber(b.postprandial_blood_sugar) !== null)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/** 当前图表时间范围内有真实录入的天数(用于小结统计) */
|
||||||
const bloodRecordDayCount = computed(() => {
|
const bloodRecordDayCount = computed(() => {
|
||||||
return chartDates.value.filter((date) => {
|
return chartDates.value.filter((date) => bloodDayHasSugar(bloodByDate.value[date])).length
|
||||||
const b = bloodByDate.value[date]
|
})
|
||||||
return b && (toNumber(b.fasting_blood_sugar) !== null || toNumber(b.postprandial_blood_sugar) !== null)
|
|
||||||
}).length
|
/** 拉取窗口内累计有血糖录入的天数(不要求连续) */
|
||||||
|
const bloodCumulativeDayCount = computed(() => {
|
||||||
|
let n = 0
|
||||||
|
for (const date of Object.keys(bloodByDate.value)) {
|
||||||
|
if (bloodDayHasSugar(bloodByDate.value[date])) n++
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
})
|
||||||
|
|
||||||
|
const REAL_BLOOD_DAYS_MIN = 7
|
||||||
|
|
||||||
|
/** 无就诊卡或累计录入不足 7 天时,图表用示例数据 */
|
||||||
|
const chartUseMockData = computed(() => {
|
||||||
|
if (!diagnosisId.value) return true
|
||||||
|
return bloodCumulativeDayCount.value < REAL_BLOOD_DAYS_MIN
|
||||||
|
})
|
||||||
|
|
||||||
|
/** 示例曲线:7 天内有波动的空腹/餐后,便于老人理解趋势图形态 */
|
||||||
|
const mockBloodByDate = computed(() => {
|
||||||
|
const map = {}
|
||||||
|
const dates = chartDates.value
|
||||||
|
dates.forEach((date, i) => {
|
||||||
|
const t = dates.length > 1 ? i / (dates.length - 1) : 0
|
||||||
|
const fasting = Number((5.2 + Math.sin(t * Math.PI) * 0.5 + (i % 2) * 0.15).toFixed(1))
|
||||||
|
const post = Number((7.6 + Math.cos(t * Math.PI) * 1.1 + (i % 3) * 0.25).toFixed(1))
|
||||||
|
const row = {
|
||||||
|
record_date: date,
|
||||||
|
fasting_blood_sugar: fasting,
|
||||||
|
postprandial_blood_sugar: post,
|
||||||
|
other_blood_sugar: '',
|
||||||
|
systolic_pressure: '',
|
||||||
|
diastolic_pressure: ''
|
||||||
|
}
|
||||||
|
row.isHighFasting = isHighFasting(row.fasting_blood_sugar)
|
||||||
|
row.isHighPostprandial = isHighPostprandial(row.postprandial_blood_sugar)
|
||||||
|
row.isHighOther = false
|
||||||
|
row.isHighBp = false
|
||||||
|
row.bpText = '—'
|
||||||
|
map[date] = row
|
||||||
|
})
|
||||||
|
return map
|
||||||
|
})
|
||||||
|
|
||||||
|
/** 图表绘制用数据源:示例或真实 */
|
||||||
|
const chartBloodByDate = computed(() => (
|
||||||
|
chartUseMockData.value ? mockBloodByDate.value : bloodByDate.value
|
||||||
|
))
|
||||||
|
|
||||||
|
const chartHasDisplayData = computed(() => {
|
||||||
|
if (chartUseMockData.value) return chartDates.value.length > 0
|
||||||
|
return bloodHasData.value
|
||||||
})
|
})
|
||||||
|
|
||||||
const bloodHighDayCount = computed(() => {
|
const bloodHighDayCount = computed(() => {
|
||||||
@@ -1080,8 +1247,14 @@ function syncPhoneGateSexFromUser() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function openPhoneGate() {
|
async function openPhoneGate() {
|
||||||
syncPhoneGateSexFromUser()
|
syncPhoneGateSexFromUser()
|
||||||
|
readUserMobile()
|
||||||
|
if (userMobile.value) {
|
||||||
|
phoneGateVisible.value = false
|
||||||
|
await resolveDailyPhoneContext(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
phoneGateVisible.value = true
|
phoneGateVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1098,8 +1271,11 @@ async function refreshUserMobile() {
|
|||||||
return userMobile.value
|
return userMobile.value
|
||||||
}
|
}
|
||||||
|
|
||||||
async function ensureDailyContextByPhone(sex) {
|
async function ensureDailyContextByPhone(sex, options = {}) {
|
||||||
if (diagnosisId.value) return true
|
if (diagnosisId.value) {
|
||||||
|
clearDailyContextError()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
const payload = {}
|
const payload = {}
|
||||||
const sexVal = sex ?? phoneGateSex.value
|
const sexVal = sex ?? phoneGateSex.value
|
||||||
@@ -1114,6 +1290,7 @@ async function ensureDailyContextByPhone(sex) {
|
|||||||
}, false)
|
}, false)
|
||||||
|
|
||||||
if (res && res.code === 1 && res.data && res.data.diagnosis_id) {
|
if (res && res.code === 1 && res.data && res.data.diagnosis_id) {
|
||||||
|
clearDailyContextError()
|
||||||
await applyCard({
|
await applyCard({
|
||||||
id: res.data.diagnosis_id,
|
id: res.data.diagnosis_id,
|
||||||
patient_id: res.data.patient_id,
|
patient_id: res.data.patient_id,
|
||||||
@@ -1121,17 +1298,32 @@ async function ensureDailyContextByPhone(sex) {
|
|||||||
age: res.data.age || 0,
|
age: res.data.age || 0,
|
||||||
gender: res.data.gender || 0
|
gender: res.data.gender || 0
|
||||||
})
|
})
|
||||||
if (res.data.created) {
|
if (res.data.created || res.data.linked_existing) {
|
||||||
await fetchCardList()
|
await fetchCardList()
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (res && res.data && res.data.need_mobile) {
|
if (res && res.data && res.data.need_mobile) {
|
||||||
|
clearDailyContextError()
|
||||||
return 'need_mobile'
|
return 'need_mobile'
|
||||||
}
|
}
|
||||||
|
|
||||||
showUserToast(formatUserMessage(res?.msg, '无法开始录入'))
|
reportDailyContextError(res, '无法开始录入', options)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 已绑定手机号时直接关联档案,不再弹出授权建档 */
|
||||||
|
async function resolveDailyPhoneContext(openFormAfter = false) {
|
||||||
|
const ok = await ensureDailyContextByPhone(phoneGateSex.value, { alert: true })
|
||||||
|
if (ok === true) {
|
||||||
|
if (openFormAfter) await openInputForm(true)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (ok === 'need_mobile') {
|
||||||
|
syncPhoneGateSexFromUser()
|
||||||
|
phoneGateVisible.value = true
|
||||||
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1144,7 +1336,7 @@ async function ensureCanRecordGlucose() {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const ok = await ensureDailyContextByPhone()
|
const ok = await ensureDailyContextByPhone(undefined, { alert: true })
|
||||||
if (ok === 'need_mobile') {
|
if (ok === 'need_mobile') {
|
||||||
openPhoneGate()
|
openPhoneGate()
|
||||||
return false
|
return false
|
||||||
@@ -1177,6 +1369,13 @@ async function onPhoneGateAuthorize(e) {
|
|||||||
|
|
||||||
phoneBinding.value = true
|
phoneBinding.value = true
|
||||||
try {
|
try {
|
||||||
|
readUserMobile()
|
||||||
|
if (userMobile.value) {
|
||||||
|
phoneGateVisible.value = false
|
||||||
|
await resolveDailyPhoneContext(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const res = await proxy.apiUrl({
|
const res = await proxy.apiUrl({
|
||||||
url: '/api/user/getMobileByMnp',
|
url: '/api/user/getMobileByMnp',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -1188,12 +1387,7 @@ async function onPhoneGateAuthorize(e) {
|
|||||||
}
|
}
|
||||||
await refreshUserMobile()
|
await refreshUserMobile()
|
||||||
phoneGateVisible.value = false
|
phoneGateVisible.value = false
|
||||||
const ok = await ensureDailyContextByPhone(phoneGateSex.value)
|
await resolveDailyPhoneContext(true)
|
||||||
if (ok === true) {
|
|
||||||
await openInputForm(true)
|
|
||||||
} else if (ok === 'need_mobile') {
|
|
||||||
openPhoneGate()
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showUserToast('网络异常,请稍后再试')
|
showUserToast('网络异常,请稍后再试')
|
||||||
} finally {
|
} finally {
|
||||||
@@ -1216,7 +1410,7 @@ async function onSelectCard(card) {
|
|||||||
async function fetchAll() {
|
async function fetchAll() {
|
||||||
if (!hasAuthToken() || !diagnosisId.value) return
|
if (!hasAuthToken() || !diagnosisId.value) return
|
||||||
loading.value = true
|
loading.value = true
|
||||||
const { start, end } = currentRange.value
|
const { start, end } = fetchRange.value
|
||||||
try {
|
try {
|
||||||
const [winRes, noteRes] = await Promise.all([
|
const [winRes, noteRes] = await Promise.all([
|
||||||
proxy.apiUrl({
|
proxy.apiUrl({
|
||||||
@@ -1290,16 +1484,51 @@ function onRefresh() {
|
|||||||
fetchAll()
|
fetchAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildMorePageUrl() {
|
||||||
|
if (!diagnosisId.value) return '/tongji/pages/more'
|
||||||
|
const q = [
|
||||||
|
`diagnosis_id=${diagnosisId.value}`,
|
||||||
|
patientId.value ? `patient_id=${patientId.value}` : ''
|
||||||
|
].filter(Boolean).join('&')
|
||||||
|
return `/tongji/pages/more?${q}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 点击血糖趋势图:进入记录页 more(无就诊卡时先手机号快捷建档) */
|
||||||
|
async function onChartTap() {
|
||||||
|
if (!diagnosisId.value) {
|
||||||
|
const ok = await ensureCanRecordGlucose()
|
||||||
|
if (!ok || !diagnosisId.value) return
|
||||||
|
}
|
||||||
|
uni.navigateTo({ url: buildMorePageUrl() })
|
||||||
|
}
|
||||||
|
|
||||||
|
async function goRecordsPage() {
|
||||||
|
onChartTap()
|
||||||
|
}
|
||||||
|
|
||||||
async function goMorePage() {
|
async function goMorePage() {
|
||||||
if (!diagnosisId.value) {
|
if (!diagnosisId.value) {
|
||||||
const ok = await ensureCanRecordGlucose()
|
const ok = await ensureCanRecordGlucose()
|
||||||
if (!ok || !diagnosisId.value) return
|
if (!ok || !diagnosisId.value) return
|
||||||
}
|
}
|
||||||
uni.navigateTo({ url: '/tongji/pages/more' })
|
uni.navigateTo({ url: buildMorePageUrl() })
|
||||||
|
}
|
||||||
|
|
||||||
|
function openGamePage(url) {
|
||||||
|
uni.navigateTo({
|
||||||
|
url,
|
||||||
|
fail() {
|
||||||
|
uni.showToast({ title: '暂时无法打开游戏', icon: 'none' })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function goEndlessGamePage() {
|
||||||
|
openGamePage('/tongji/endless-game/index')
|
||||||
}
|
}
|
||||||
|
|
||||||
function navToUser() {
|
function navToUser() {
|
||||||
uni.switchTab({ url: '/pages/user/user' })
|
uni.redirectTo({ url: '/pages/user/user' })
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============ 时段问候 / 打卡 / 健康日历 ============
|
// ============ 时段问候 / 打卡 / 健康日历 ============
|
||||||
@@ -1994,7 +2223,7 @@ function scheduleChartRedraw() {
|
|||||||
|
|
||||||
function buildSeries(field) {
|
function buildSeries(field) {
|
||||||
return chartDates.value.map((date) => {
|
return chartDates.value.map((date) => {
|
||||||
const b = bloodByDate.value[date]
|
const b = chartBloodByDate.value[date]
|
||||||
return b ? toNumber(b[field]) : null
|
return b ? toNumber(b[field]) : null
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -2028,7 +2257,7 @@ function drawChart() {
|
|||||||
|
|
||||||
ctx.clearRect(0, 0, w, h)
|
ctx.clearRect(0, 0, w, h)
|
||||||
|
|
||||||
const padding = { left: 40, right: 18, top: 26, bottom: 36 }
|
const padding = { left: 40, right: 18, top: 12, bottom: 28 }
|
||||||
const cw = w - padding.left - padding.right
|
const cw = w - padding.left - padding.right
|
||||||
const ch = h - padding.top - padding.bottom
|
const ch = h - padding.top - padding.bottom
|
||||||
|
|
||||||
@@ -2284,59 +2513,25 @@ function toggleChartSeries(key) {
|
|||||||
scheduleChartRedraw()
|
scheduleChartRedraw()
|
||||||
}
|
}
|
||||||
|
|
||||||
function onChartTouch(e) {
|
|
||||||
if (!chartCoords || !canvasWidth) return
|
|
||||||
if (!chartDates.value.length) return
|
|
||||||
let touchX = 0
|
|
||||||
let touchY = 0
|
|
||||||
if (e && e.touches && e.touches[0]) {
|
|
||||||
touchX = e.touches[0].x ?? e.touches[0].clientX ?? 0
|
|
||||||
touchY = e.touches[0].y ?? e.touches[0].clientY ?? 0
|
|
||||||
}
|
|
||||||
const { padding, cw, n } = chartCoords
|
|
||||||
if (touchX < padding.left || touchX > padding.left + cw) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const ratio = n <= 1 ? 0 : (touchX - padding.left) / cw
|
|
||||||
let idx = Math.round(ratio * (n - 1))
|
|
||||||
if (idx < 0) idx = 0
|
|
||||||
if (idx > n - 1) idx = n - 1
|
|
||||||
const date = chartDates.value[idx]
|
|
||||||
const fp = chartSeriesVisible.value.fasting ? chartCoords.fasting[idx] : null
|
|
||||||
const pp = chartSeriesVisible.value.postprandial ? chartCoords.postprandial[idx] : null
|
|
||||||
const anchorX = fp?.x ?? pp?.x ?? padding.left + cw * (idx / Math.max(1, n - 1))
|
|
||||||
|
|
||||||
const left = Math.max(8, Math.min(canvasWidth - 130, anchorX - 60))
|
|
||||||
const top = Math.max(4, touchY - 70)
|
|
||||||
|
|
||||||
hoverInfo.value = {
|
|
||||||
date: date.slice(5),
|
|
||||||
fasting: fp?.value ?? null,
|
|
||||||
postprandial: pp?.value ?? null,
|
|
||||||
left,
|
|
||||||
top
|
|
||||||
}
|
|
||||||
if (hoverTimer) clearTimeout(hoverTimer)
|
|
||||||
hoverTimer = setTimeout(() => {
|
|
||||||
hoverInfo.value = null
|
|
||||||
}, 2500)
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(rangeMode, () => {
|
watch(rangeMode, () => {
|
||||||
scheduleChartRedraw()
|
scheduleChartRedraw()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
watch([chartUseMockData, bloodRecords], () => {
|
||||||
|
scheduleChartRedraw()
|
||||||
|
})
|
||||||
|
|
||||||
// ============ 生命周期 ============
|
// ============ 生命周期 ============
|
||||||
function readUserContext() {
|
function readUserContext() {
|
||||||
try {
|
try {
|
||||||
const userData = uni.getStorageSync('userData')
|
const userData = uni.getStorageSync('userData')
|
||||||
if (userData && userData.diagnosis) {
|
if (!userData) return
|
||||||
if (!patientId.value) patientId.value = Number(userData.diagnosis.patient_id) || 0
|
// 勿用问诊会话里的 diagnosis 作为日常血糖诊单(无 view_records 会报无权)
|
||||||
if (!diagnosisId.value) diagnosisId.value = Number(userData.diagnosis.id) || 0
|
const name = String(userData.real_name || userData.nickname || '').trim()
|
||||||
if (!patientName.value) patientName.value = userData.diagnosis.patient_name || ''
|
if (!patientName.value && name) patientName.value = name
|
||||||
if (!age.value) age.value = Number(userData.diagnosis.age) || 0
|
const sex = Number(userData.sex)
|
||||||
if (!gender.value) gender.value = Number(userData.diagnosis.gender) || 0
|
if (!gender.value && (sex === 1 || sex === 2)) gender.value = sex === 1 ? 1 : 0
|
||||||
}
|
if (!age.value && userData.age) age.value = Number(userData.age) || 0
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2419,23 +2614,25 @@ async function bootstrap(options) {
|
|||||||
if (!target) target = list[0]
|
if (!target) target = list[0]
|
||||||
await applyCard(target)
|
await applyCard(target)
|
||||||
} else {
|
} else {
|
||||||
if (passedDiagnosisId) diagnosisId.value = passedDiagnosisId
|
// 无可见就诊卡:勿用 URL/缓存里的隐藏诊单 ID
|
||||||
if (passedPatientId) patientId.value = passedPatientId
|
diagnosisId.value = 0
|
||||||
if (passedPatientName) patientName.value = passedPatientName
|
patientId.value = 0
|
||||||
if (passedAge) age.value = passedAge
|
patientName.value = ''
|
||||||
if (passedGender) gender.value = passedGender
|
age.value = 0
|
||||||
if (!diagnosisId.value || !patientId.value) {
|
gender.value = 0
|
||||||
readUserContext()
|
|
||||||
}
|
|
||||||
readUserMobile()
|
readUserMobile()
|
||||||
if (!diagnosisId.value && userMobile.value) {
|
if (userMobile.value) {
|
||||||
await ensureDailyContextByPhone()
|
await ensureDailyContextByPhone(undefined, { alert: false })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
authChecking.value = false
|
authChecking.value = false
|
||||||
if (diagnosisId.value) {
|
if (diagnosisId.value) {
|
||||||
await fetchAll()
|
await fetchAll()
|
||||||
|
} else {
|
||||||
|
bloodRecords.value = []
|
||||||
|
await nextTick()
|
||||||
|
scheduleChartRedraw()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2445,6 +2642,7 @@ onLoad((options) => {
|
|||||||
|
|
||||||
onShow(() => {
|
onShow(() => {
|
||||||
initHeaderSafeArea()
|
initHeaderSafeArea()
|
||||||
|
try { uni.onKeyboardHeightChange(onKeyboardHeightChange) } catch (e) {}
|
||||||
if (authChecking.value) return
|
if (authChecking.value) return
|
||||||
resetCanvasCache()
|
resetCanvasCache()
|
||||||
nextTick(() => scheduleChartRedraw())
|
nextTick(() => scheduleChartRedraw())
|
||||||
@@ -2459,10 +2657,16 @@ onPullDownRefresh(async () => {
|
|||||||
|
|
||||||
onHide(() => {
|
onHide(() => {
|
||||||
ttsStop()
|
ttsStop()
|
||||||
|
stopSpeechToText()
|
||||||
|
try { uni.offKeyboardHeightChange(onKeyboardHeightChange) } catch (e) {}
|
||||||
|
keyboardHeight.value = 0
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnload(() => {
|
onUnload(() => {
|
||||||
ttsStop()
|
ttsStop()
|
||||||
|
stopSpeechToText()
|
||||||
|
try { uni.offKeyboardHeightChange(onKeyboardHeightChange) } catch (e) {}
|
||||||
|
keyboardHeight.value = 0
|
||||||
})
|
})
|
||||||
|
|
||||||
// ============ 邀请观看(家人分享页)============
|
// ============ 邀请观看(家人分享页)============
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,7 @@
|
|||||||
* 设计 token 与 stitch-weekly.html tailwind.config 一一对应
|
* 设计 token 与 stitch-weekly.html tailwind.config 一一对应
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.weekly-page {
|
.stitch-vitalmint {
|
||||||
--primary: #006c49;
|
--primary: #006c49;
|
||||||
--on-primary: #ffffff;
|
--on-primary: #ffffff;
|
||||||
--on-primary-fixed-variant: #005236;
|
--on-primary-fixed-variant: #005236;
|
||||||
@@ -29,6 +29,8 @@
|
|||||||
--error-container: #ffdad6;
|
--error-container: #ffdad6;
|
||||||
--tertiary: #a43a3a;
|
--tertiary: #a43a3a;
|
||||||
--tertiary-container: #fc7c78;
|
--tertiary-container: #fc7c78;
|
||||||
|
--on-tertiary: #ffffff;
|
||||||
|
--on-tertiary-container: #711419;
|
||||||
|
|
||||||
--st-container-margin: 40rpx;
|
--st-container-margin: 40rpx;
|
||||||
--st-section-gap: 64rpx;
|
--st-section-gap: 64rpx;
|
||||||
@@ -39,7 +41,9 @@
|
|||||||
--st-radius-xl: 24rpx;
|
--st-radius-xl: 24rpx;
|
||||||
--st-radius-lg: 16rpx;
|
--st-radius-lg: 16rpx;
|
||||||
--st-shadow-ambient: 0 20rpx 60rpx -20rpx rgba(0, 108, 73, 0.08);
|
--st-shadow-ambient: 0 20rpx 60rpx -20rpx rgba(0, 108, 73, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekly-page.stitch-vitalmint {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
color: var(--on-surface);
|
color: var(--on-surface);
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
inset: 0;
|
inset: 0;
|
||||||
background: rgba(15, 23, 42, 0.45);
|
background: rgba(15, 23, 42, 0.45);
|
||||||
backdrop-filter: blur(12px);
|
backdrop-filter: blur(12px);
|
||||||
z-index: 999;
|
z-index: 2000;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -89,6 +89,22 @@
|
|||||||
padding: 32rpx;
|
padding: 32rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.weekly-page .phone-gate-error {
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
padding: 20rpx 22rpx;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
background: #fef2f2;
|
||||||
|
border: 1rpx solid rgba(220, 38, 38, 0.2);
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
text {
|
||||||
|
font-size: 26rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #991b1b;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.weekly-page .phone-gate-tip {
|
.weekly-page .phone-gate-tip {
|
||||||
display: block;
|
display: block;
|
||||||
font-size: 28rpx;
|
font-size: 28rpx;
|
||||||
|
|||||||
@@ -6,6 +6,8 @@
|
|||||||
--st-shadow-ambient: 0 24rpx 60rpx -20rpx rgba(0, 108, 73, 0.08), 0 8rpx 20rpx -8rpx rgba(0, 0, 0, 0.03);
|
--st-shadow-ambient: 0 24rpx 60rpx -20rpx rgba(0, 108, 73, 0.08), 0 8rpx 20rpx -8rpx rgba(0, 0, 0, 0.03);
|
||||||
--st-shadow-cta: 0 16rpx 32rpx rgba(0, 108, 73, 0.2);
|
--st-shadow-cta: 0 16rpx 32rpx rgba(0, 108, 73, 0.2);
|
||||||
--st-shadow-float: 0 16rpx 64rpx rgba(0, 108, 73, 0.15);
|
--st-shadow-float: 0 16rpx 64rpx rgba(0, 108, 73, 0.15);
|
||||||
|
--tab-dock-full-offset: calc(220rpx + env(safe-area-inset-bottom));
|
||||||
|
padding-bottom: var(--tab-dock-full-offset);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===== Header ===== */
|
/* ===== Header ===== */
|
||||||
@@ -106,6 +108,27 @@
|
|||||||
color: var(--on-primary);
|
color: var(--on-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ===== 居家训练(DEV 内嵌) ===== */
|
||||||
|
.weekly-page .st-training-section {
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekly-page .st-training-section .dev-entry-inline--weekly {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekly-page .st-training-section .inline-scroll {
|
||||||
|
margin-left: -4rpx;
|
||||||
|
margin-right: -4rpx;
|
||||||
|
width: calc(100% + 8rpx);
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekly-page .st-training-section .inline-card:first-child {
|
||||||
|
border-color: rgba(0, 108, 73, 0.22);
|
||||||
|
background: linear-gradient(160deg, rgba(0, 108, 73, 0.08) 0%, #f4f7f5 55%);
|
||||||
|
}
|
||||||
|
|
||||||
/* ===== Main ===== */
|
/* ===== Main ===== */
|
||||||
.weekly-page .st-main {
|
.weekly-page .st-main {
|
||||||
padding: 0 var(--st-container-margin) 32rpx;
|
padding: 0 var(--st-container-margin) 32rpx;
|
||||||
@@ -115,6 +138,21 @@
|
|||||||
margin-top: 16rpx;
|
margin-top: 16rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.weekly-page .st-daily-context-error {
|
||||||
|
padding: 24rpx 28rpx;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
background: #fef2f2;
|
||||||
|
border: 1rpx solid rgba(220, 38, 38, 0.22);
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekly-page .st-daily-context-error-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #991b1b;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
.weekly-page .st-main-spacer {
|
.weekly-page .st-main-spacer {
|
||||||
height: 32rpx;
|
height: 32rpx;
|
||||||
}
|
}
|
||||||
@@ -139,25 +177,83 @@
|
|||||||
transform: scale(0.98);
|
transform: scale(0.98);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 游戏入口 */
|
||||||
|
.weekly-page .st-game-entry {
|
||||||
|
|
||||||
|
margin-top: 24rpx;
|
||||||
|
padding: 28rpx 32rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 24rpx;
|
||||||
|
border-radius: var(--st-radius-2xl);
|
||||||
|
background: var(--surface-container-lowest);
|
||||||
|
border: 1rpx solid rgba(0, 108, 73, 0.14);
|
||||||
|
box-shadow: var(--st-shadow-ambient);
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekly-page .st-game-entry:active {
|
||||||
|
transform: scale(0.98);
|
||||||
|
opacity: 0.92;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekly-page .st-game-entry-icon {
|
||||||
|
width: 88rpx;
|
||||||
|
height: 88rpx;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
background: rgba(0, 108, 73, 0.1);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekly-page .st-game-entry-body {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekly-page .st-game-entry-title {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--on-surface);
|
||||||
|
line-height: 1.25;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekly-page .st-game-entry-sub {
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--on-surface-variant, #64748b);
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
|
||||||
/* ===== Chart card — rounded-3xl ===== */
|
/* ===== Chart card — rounded-3xl ===== */
|
||||||
.weekly-page .st-chart-card {
|
.weekly-page .st-chart-card {
|
||||||
padding: 40rpx;
|
padding: 40rpx 10rpx 40rpx 10rpx;
|
||||||
border-radius: 48rpx;
|
border-radius: 48rpx;
|
||||||
background: var(--surface-container-lowest);
|
background: var(--surface-container-lowest);
|
||||||
border: 1rpx solid rgba(221, 228, 221, 0.3);
|
border: 1rpx solid rgba(221, 228, 221, 0.3);
|
||||||
box-shadow: var(--st-shadow-ambient);
|
box-shadow: var(--st-shadow-ambient);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.weekly-page .st-chart-head {
|
.weekly-page .st-chart-head {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
margin-bottom: 48rpx;
|
margin-bottom: 12rpx;
|
||||||
|
gap: 16rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.weekly-page .st-chart-title {
|
.weekly-page .st-chart-title {
|
||||||
display: block;
|
display: block;
|
||||||
font-size: 40rpx;
|
font-size: 30rpx;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--on-surface);
|
color: var(--on-surface);
|
||||||
}
|
}
|
||||||
@@ -165,11 +261,28 @@
|
|||||||
.weekly-page .st-chart-sub {
|
.weekly-page .st-chart-sub {
|
||||||
display: block;
|
display: block;
|
||||||
margin-top: 4rpx;
|
margin-top: 4rpx;
|
||||||
font-size: 24rpx;
|
font-size: 22rpx;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--on-surface-variant);
|
color: var(--on-surface-variant);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.weekly-page .st-chart-more {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4rpx;
|
||||||
|
margin-top: 6rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekly-page .st-chart-more-text {
|
||||||
|
font-size: 22rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekly-page .st-chart-more:active {
|
||||||
|
opacity: 0.75;
|
||||||
|
}
|
||||||
|
|
||||||
.weekly-page .st-chart-legend {
|
.weekly-page .st-chart-legend {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--st-inline-gap);
|
gap: var(--st-inline-gap);
|
||||||
@@ -243,10 +356,29 @@
|
|||||||
color: var(--on-surface-variant);
|
color: var(--on-surface-variant);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.weekly-page .st-chart-foot {
|
||||||
|
padding: 20rpx 0 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekly-page .st-chart-mock-hint {
|
||||||
|
display: block;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--outline);
|
||||||
|
line-height: 1.35;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
.weekly-page .st-chart-area {
|
.weekly-page .st-chart-area {
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 360rpx;
|
height: 360rpx;
|
||||||
border-bottom: 1rpx dashed var(--surface-container-highest);
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekly-page .st-chart-area:active {
|
||||||
|
opacity: 0.92;
|
||||||
}
|
}
|
||||||
|
|
||||||
.weekly-page .st-chart-canvas {
|
.weekly-page .st-chart-canvas {
|
||||||
@@ -255,6 +387,38 @@
|
|||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 识糖入口嵌在趋势卡内:轻量分隔,避免大块留白 */
|
||||||
|
.weekly-page .st-chart-card .st-game-entry-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12rpx;
|
||||||
|
margin: 12rpx 20rpx 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekly-page .st-chart-card .st-game-entry--in-card {
|
||||||
|
margin-top: 0;
|
||||||
|
padding: 16rpx 20rpx;
|
||||||
|
gap: 16rpx;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
background: var(--surface-container-low);
|
||||||
|
border: 1rpx solid rgba(0, 108, 73, 0.08);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekly-page .st-chart-card .st-game-entry--in-card .st-game-entry-icon {
|
||||||
|
width: 64rpx;
|
||||||
|
height: 64rpx;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekly-page .st-chart-card .st-game-entry--in-card .st-game-entry-title {
|
||||||
|
font-size: 28rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekly-page .st-chart-card .st-game-entry--in-card .st-game-entry-sub {
|
||||||
|
font-size: 22rpx;
|
||||||
|
}
|
||||||
|
|
||||||
.weekly-page .st-chart-empty,
|
.weekly-page .st-chart-empty,
|
||||||
.weekly-page .st-chart-placeholder {
|
.weekly-page .st-chart-placeholder {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -499,6 +663,11 @@
|
|||||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.04);
|
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.04);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.weekly-page .st-meal-glass--drinks {
|
||||||
|
background: rgba(224, 242, 254, 0.65);
|
||||||
|
border-color: rgba(14, 165, 233, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
.weekly-page .st-meal-head {
|
.weekly-page .st-meal-head {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -536,20 +705,64 @@
|
|||||||
position: fixed;
|
position: fixed;
|
||||||
left: var(--st-container-margin);
|
left: var(--st-container-margin);
|
||||||
right: var(--st-container-margin);
|
right: var(--st-container-margin);
|
||||||
bottom: calc(24rpx + env(safe-area-inset-bottom));
|
bottom: var(--tab-dock-full-offset);
|
||||||
z-index: 90;
|
z-index: 210;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekly-page .st-float-close {
|
||||||
|
position: absolute;
|
||||||
|
top: -18rpx;
|
||||||
|
right: -6rpx;
|
||||||
|
width: 44rpx;
|
||||||
|
height: 44rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(0, 0, 0, 0.45);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekly-page .st-float-close-icon {
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 30rpx;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekly-page .st-float-reopen {
|
||||||
|
position: fixed;
|
||||||
|
right: var(--st-container-margin);
|
||||||
|
bottom: var(--tab-dock-full-offset);
|
||||||
|
width: 88rpx;
|
||||||
|
height: 88rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: linear-gradient(180deg, #2d7340 0%, #006c49 100%);
|
||||||
|
box-shadow: var(--st-shadow-float);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 210;
|
||||||
}
|
}
|
||||||
|
|
||||||
.weekly-page .st-float-inner {
|
.weekly-page .st-float-inner {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 24rpx;
|
gap: 16rpx;
|
||||||
padding: 16rpx 16rpx 16rpx 40rpx;
|
padding: 16rpx 16rpx 16rpx 28rpx;
|
||||||
border-radius: 999rpx;
|
border-radius: 999rpx;
|
||||||
background: rgba(255, 255, 255, 0.9);
|
background: rgba(255, 255, 255, 0.9);
|
||||||
backdrop-filter: blur(20rpx);
|
backdrop-filter: blur(20rpx);
|
||||||
border: 1rpx solid rgba(0, 108, 73, 0.1);
|
border: 1rpx solid rgba(0, 108, 73, 0.1);
|
||||||
box-shadow: var(--st-shadow-float);
|
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;
|
||||||
|
border-color: rgba(0, 108, 73, 0.28);
|
||||||
}
|
}
|
||||||
|
|
||||||
.weekly-page .st-float-icon {
|
.weekly-page .st-float-icon {
|
||||||
@@ -566,15 +779,126 @@
|
|||||||
.weekly-page .st-float-input {
|
.weekly-page .st-float-input {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
height: 64rpx;
|
width: 100%;
|
||||||
|
min-height: 64rpx;
|
||||||
|
max-height: 240rpx;
|
||||||
font-size: 28rpx;
|
font-size: 28rpx;
|
||||||
|
line-height: 40rpx;
|
||||||
color: var(--on-surface);
|
color: var(--on-surface);
|
||||||
background: transparent;
|
background: transparent;
|
||||||
|
padding: 12rpx 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
transition: min-height 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 聚焦时输入框变大;内容超长时由 auto-height 继续向上增高,超过上限可滚动 */
|
||||||
|
.weekly-page .st-float-inner.is-expanded .st-float-input {
|
||||||
|
min-height: 104rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.weekly-page .st-float-placeholder {
|
.weekly-page .st-float-placeholder {
|
||||||
color: rgba(60, 74, 66, 0.6);
|
color: rgba(60, 74, 66, 0.6);
|
||||||
font-size: 28rpx;
|
font-size: 26rpx;
|
||||||
|
line-height: 40rpx;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekly-page .st-float-mic {
|
||||||
|
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 {
|
.weekly-page .st-float-send {
|
||||||
@@ -587,6 +911,8 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
box-shadow: 0 4rpx 12rpx rgba(0, 108, 73, 0.2);
|
box-shadow: 0 4rpx 12rpx rgba(0, 108, 73, 0.2);
|
||||||
|
line-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.weekly-page .st-float-send.disabled {
|
.weekly-page .st-float-send.disabled {
|
||||||
|
|||||||
@@ -64,10 +64,10 @@ export function parsePartialDietPlainText(text) {
|
|||||||
const out = {}
|
const out = {}
|
||||||
if (!text) return out
|
if (!text) return out
|
||||||
|
|
||||||
const lineRe = /^(早餐|午餐|晚餐|提示|少碰)[::]\s*(.*)$/gm
|
const lineRe = /^(早餐|喝的|午餐|晚餐|提示|少碰)[::]\s*(.*)$/gm
|
||||||
let match
|
let match
|
||||||
while ((match = lineRe.exec(text)) !== null) {
|
while ((match = lineRe.exec(text)) !== null) {
|
||||||
const key = { '早餐': 'breakfast', '午餐': 'lunch', '晚餐': 'dinner', '提示': 'tips', '少碰': 'avoid' }[match[1]]
|
const key = { '早餐': 'breakfast', '喝的': 'drinks', '午餐': 'lunch', '晚餐': 'dinner', '提示': 'tips', '少碰': 'avoid' }[match[1]]
|
||||||
const val = (match[2] || '').trim()
|
const val = (match[2] || '').trim()
|
||||||
if (key === 'avoid') {
|
if (key === 'avoid') {
|
||||||
out.avoid = val.split(/[、,,;;\s]+/).map((s) => s.trim()).filter(Boolean)
|
out.avoid = val.split(/[、,,;;\s]+/).map((s) => s.trim()).filter(Boolean)
|
||||||
@@ -76,9 +76,9 @@ export function parsePartialDietPlainText(text) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const tail = text.match(/(?:^|\n)(早餐|午餐|晚餐|提示|少碰)[::]\s*([^\n]*)$/)
|
const tail = text.match(/(?:^|\n)(早餐|喝的|午餐|晚餐|提示|少碰)[::]\s*([^\n]*)$/)
|
||||||
if (tail) {
|
if (tail) {
|
||||||
const key = { '早餐': 'breakfast', '午餐': 'lunch', '晚餐': 'dinner', '提示': 'tips', '少碰': 'avoid' }[tail[1]]
|
const key = { '早餐': 'breakfast', '喝的': 'drinks', '午餐': 'lunch', '晚餐': 'dinner', '提示': 'tips', '少碰': 'avoid' }[tail[1]]
|
||||||
const val = (tail[2] || '').trim()
|
const val = (tail[2] || '').trim()
|
||||||
if (key === 'avoid') {
|
if (key === 'avoid') {
|
||||||
if (val) out.avoid = val.split(/[、,,;;\s]+/).map((s) => s.trim()).filter(Boolean)
|
if (val) out.avoid = val.split(/[、,,;;\s]+/).map((s) => s.trim()).filter(Boolean)
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import { TREE_LEVELS, TREE_MAX_LEVEL, TREE_XP_PER_LEVEL } from './treeLevels.js'
|
||||||
|
|
||||||
|
/** 可选树种:每级 emoji 与 treeLevels.js Lv.0~9 一一对应 */
|
||||||
|
export const TREE_SPECIES_LIST = [
|
||||||
|
{
|
||||||
|
id: 'classic',
|
||||||
|
name: '稳糖灵树',
|
||||||
|
tagline: '经典路线,从种子到圆满',
|
||||||
|
stages: TREE_LEVELS.map((l) => l.emoji)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'bonsai',
|
||||||
|
name: '雅韵盆景',
|
||||||
|
tagline: '小巧精致,书桌旁的温柔陪伴',
|
||||||
|
stages: ['🫘', '🌱', '🪴', '🎋', '🪴', '🌳', '🎍', '🌸', '🏵️', '🏆']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'pine',
|
||||||
|
name: '苍劲青松',
|
||||||
|
tagline: '挺拔向上,越记越稳',
|
||||||
|
stages: ['🫘', '🌱', '🌿', '🌲', '🌲', '🌲', '⛰️', '🌲', '🏔️', '🏆']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'sakura',
|
||||||
|
name: '樱花小树',
|
||||||
|
tagline: '坚持打卡,枝头渐开',
|
||||||
|
stages: ['🫘', '🌱', '🌿', '🌳', '🌸', '🌸', '🌸', '🌸', '🌺', '🏆']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ginkgo',
|
||||||
|
name: '金秋银杏',
|
||||||
|
tagline: '叶色渐变,见证每一天',
|
||||||
|
stages: ['🫘', '🌱', '🍃', '🌳', '🍂', '🍂', '🌳', '🍁', '🌟', '🏆']
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
export const TREE_SPECIES_STORAGE_KEY = 'tongji_tree_species_v1'
|
||||||
|
|
||||||
|
/** 四项任务全勤时每日最多可领积分(与后端 taskDefs 一致) */
|
||||||
|
export const DAILY_MAX_TASK_POINTS = 40
|
||||||
|
|
||||||
|
export function getTreeSpecies(id) {
|
||||||
|
return TREE_SPECIES_LIST.find((s) => s.id === id) || TREE_SPECIES_LIST[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function speciesEmojiAtLevel(speciesId, level) {
|
||||||
|
const sp = getTreeSpecies(speciesId)
|
||||||
|
const lv = Math.min(TREE_MAX_LEVEL, Math.max(0, Number(level) || 0))
|
||||||
|
return sp.stages[lv] || sp.stages[0] || '🌱'
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 当前树种成长路线图(供选择面板展示) */
|
||||||
|
export function speciesGrowthRoadmap(speciesId) {
|
||||||
|
const sp = getTreeSpecies(speciesId)
|
||||||
|
return TREE_LEVELS.map((meta, i) => {
|
||||||
|
const xpTotal = i * TREE_XP_PER_LEVEL
|
||||||
|
const daysHint = i === 0
|
||||||
|
? 0
|
||||||
|
: Math.max(1, Math.ceil(xpTotal / DAILY_MAX_TASK_POINTS))
|
||||||
|
return {
|
||||||
|
level: i,
|
||||||
|
name: meta.name,
|
||||||
|
emoji: sp.stages[i] || meta.emoji,
|
||||||
|
desc: meta.desc,
|
||||||
|
xpTotal,
|
||||||
|
daysHint
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 全勤打卡大约多少天满级 */
|
||||||
|
export function estimateDaysToMaxLevel() {
|
||||||
|
const totalXp = TREE_MAX_LEVEL * TREE_XP_PER_LEVEL
|
||||||
|
return Math.max(1, Math.ceil(totalXp / DAILY_MAX_TASK_POINTS))
|
||||||
|
}
|
||||||
@@ -0,0 +1,307 @@
|
|||||||
|
/**
|
||||||
|
* 健康数据语音文本解析。
|
||||||
|
*
|
||||||
|
* 把语音识别得到的中文口语文本解析为结构化字段:
|
||||||
|
* - 血糖(空腹 / 餐后 / 其他)
|
||||||
|
* - 血压(高压 / 低压 / 西药 / 胰岛素)
|
||||||
|
* - 饮食(早 / 午 / 晚餐 + 备注)
|
||||||
|
* - 运动(类型 / 时长 / 强度)
|
||||||
|
*
|
||||||
|
* 兼容中文数字(六点五 / 一百二十 / 一百二)与阿拉伯数字(6.5 / 120)。
|
||||||
|
* 纯前端解析,无网络依赖,识别失败时返回空对象。
|
||||||
|
*/
|
||||||
|
|
||||||
|
const CN_DIGIT = {
|
||||||
|
零: 0, 〇: 0, 一: 1, 壹: 1, 幺: 1, 二: 2, 贰: 2, 两: 2,
|
||||||
|
三: 3, 叁: 3, 四: 4, 肆: 4, 五: 5, 伍: 5, 六: 6, 陆: 6,
|
||||||
|
七: 7, 柒: 7, 八: 8, 捌: 8, 九: 9, 玖: 9
|
||||||
|
}
|
||||||
|
const CN_UNIT = { 十: 10, 拾: 10, 百: 100, 佰: 100, 千: 1000, 仟: 1000, 万: 10000, 亿: 100000000 }
|
||||||
|
|
||||||
|
/** 中文整数串 → 数字,例如 "一百二十" → 120、"八十" → 80、"十" → 10、"一百二" → 120 */
|
||||||
|
function cnIntToNumber(s) {
|
||||||
|
let total = 0
|
||||||
|
let section = 0
|
||||||
|
let number = 0
|
||||||
|
let hadUnit = false
|
||||||
|
let lastUnit = 0
|
||||||
|
let sawZeroAfterUnit = false
|
||||||
|
for (const ch of s) {
|
||||||
|
if (CN_DIGIT[ch] !== undefined) {
|
||||||
|
number = CN_DIGIT[ch]
|
||||||
|
if (number === 0) sawZeroAfterUnit = true
|
||||||
|
} else if (CN_UNIT[ch] !== undefined) {
|
||||||
|
hadUnit = true
|
||||||
|
const unit = CN_UNIT[ch]
|
||||||
|
lastUnit = unit
|
||||||
|
sawZeroAfterUnit = false
|
||||||
|
if (unit >= 10000) {
|
||||||
|
section = (section + number) * unit
|
||||||
|
total += section
|
||||||
|
section = 0
|
||||||
|
} else {
|
||||||
|
if (number === 0) number = 1 // 十 = 10
|
||||||
|
section += number * unit
|
||||||
|
}
|
||||||
|
number = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 没有任何单位且为纯数字串(如 "一二零")时按位拼接更符合口语
|
||||||
|
if (!hadUnit && s.length > 1) {
|
||||||
|
let joined = ''
|
||||||
|
for (const ch of s) {
|
||||||
|
if (CN_DIGIT[ch] !== undefined) joined += CN_DIGIT[ch]
|
||||||
|
}
|
||||||
|
if (joined) return Number(joined)
|
||||||
|
}
|
||||||
|
// 口语省略尾部单位:"一百二" → 120、"一千五" → 1500(避开 "一百零五" 这类带零的)
|
||||||
|
if (number > 0 && lastUnit >= 100 && !sawZeroAfterUnit) {
|
||||||
|
number = number * (lastUnit / 10)
|
||||||
|
}
|
||||||
|
return total + section + number
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 中文数字串(含小数点)→ 数字,例如 "六点五" → 6.5 */
|
||||||
|
function cnSeqToNumber(seq) {
|
||||||
|
if (seq.includes('点')) {
|
||||||
|
const parts = seq.split('点')
|
||||||
|
const intPart = parts[0]
|
||||||
|
const decPart = parts.slice(1).join('')
|
||||||
|
const intVal = intPart ? cnIntToNumber(intPart) : 0
|
||||||
|
let decStr = ''
|
||||||
|
for (const ch of decPart) {
|
||||||
|
if (CN_DIGIT[ch] !== undefined) decStr += CN_DIGIT[ch]
|
||||||
|
}
|
||||||
|
if (!decStr) return intVal
|
||||||
|
return Number(`${intVal}.${decStr}`)
|
||||||
|
}
|
||||||
|
return cnIntToNumber(seq)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 是否包含中文数字字符(用于过滤“点”“重点”等误匹配) */
|
||||||
|
function hasCnDigit(seq) {
|
||||||
|
for (const ch of seq) {
|
||||||
|
if (CN_DIGIT[ch] !== undefined || CN_UNIT[ch] !== undefined) return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 把文本中的中文数字段落替换为阿拉伯数字 */
|
||||||
|
export function normalizeNumbers(text) {
|
||||||
|
const raw = String(text || '')
|
||||||
|
return raw.replace(/[零〇一壹幺二贰两三叁四肆五伍六陆七柒八捌九玖十拾百佰千仟万亿点]+/g, (m) => {
|
||||||
|
if (!hasCnDigit(m)) return m
|
||||||
|
const n = cnSeqToNumber(m)
|
||||||
|
return Number.isFinite(n) ? String(n) : m
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 在文本中找到关键词后紧随的第一个数字 */
|
||||||
|
function numAfter(text, keys) {
|
||||||
|
for (const k of keys) {
|
||||||
|
const i = text.indexOf(k)
|
||||||
|
if (i >= 0) {
|
||||||
|
const rest = text.slice(i + k.length)
|
||||||
|
const m = rest.match(/-?\d+(?:\.\d+)?/)
|
||||||
|
if (m) return m[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 关键词后紧随的一段文本(截止到标点或下一个分隔关键词) */
|
||||||
|
function textAfter(text, keys, stopKeys = []) {
|
||||||
|
for (const k of keys) {
|
||||||
|
const i = text.indexOf(k)
|
||||||
|
if (i >= 0) {
|
||||||
|
let rest = text.slice(i + k.length)
|
||||||
|
// 去掉口语连接词
|
||||||
|
rest = rest.replace(/^[是为吃了喝了吃的喝的有打了用了::,,、。\s]+/, '')
|
||||||
|
let end = rest.length
|
||||||
|
const mStop = rest.match(/[,,。.;;!!??\n]/)
|
||||||
|
if (mStop && mStop.index < end) end = mStop.index
|
||||||
|
for (const sk of stopKeys) {
|
||||||
|
const si = rest.indexOf(sk)
|
||||||
|
if (si >= 0 && si < end) end = si
|
||||||
|
}
|
||||||
|
const seg = rest.slice(0, end).trim()
|
||||||
|
if (seg) return seg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析血糖:返回 { fasting_blood_sugar, postprandial_blood_sugar, other_blood_sugar }
|
||||||
|
* 例:"空腹六点五餐后八点二" → { fasting:6.5, postprandial:8.2 }
|
||||||
|
*/
|
||||||
|
export function parseGlucose(raw) {
|
||||||
|
const t = normalizeNumbers(raw)
|
||||||
|
const result = {}
|
||||||
|
const fasting = numAfter(t, ['空腹'])
|
||||||
|
const post = numAfter(t, ['餐后', '饭后'])
|
||||||
|
const other = numAfter(t, ['其他', '随机', '睡前', '凌晨', '夜间', '晚上'])
|
||||||
|
if (fasting) result.fasting_blood_sugar = fasting
|
||||||
|
if (post) result.postprandial_blood_sugar = post
|
||||||
|
if (other) result.other_blood_sugar = other
|
||||||
|
// 没有关键词但只有一个数字 → 视为“其他血糖”
|
||||||
|
if (!fasting && !post && !other) {
|
||||||
|
const nums = t.match(/\d+(?:\.\d+)?/g)
|
||||||
|
if (nums && nums.length === 1) result.other_blood_sugar = nums[0]
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析血压:返回 { systolic_pressure, diastolic_pressure, western_medicine, insulin }
|
||||||
|
* 例:"高压一百二低压八十" → { systolic:120, diastolic:80 }
|
||||||
|
*/
|
||||||
|
export function parseBloodPressure(raw) {
|
||||||
|
const t = normalizeNumbers(raw)
|
||||||
|
const result = {}
|
||||||
|
let sys = numAfter(t, ['高压', '收缩压', '收缩'])
|
||||||
|
let dia = numAfter(t, ['低压', '舒张压', '舒张'])
|
||||||
|
if (!sys || !dia) {
|
||||||
|
const nums = (t.match(/\d{2,3}/g) || [])
|
||||||
|
.map(Number)
|
||||||
|
.filter((n) => n >= 30 && n <= 300)
|
||||||
|
if (!sys && !dia && nums.length >= 2) {
|
||||||
|
sys = String(nums[0])
|
||||||
|
dia = String(nums[1])
|
||||||
|
} else if (!sys && nums.length === 1 && nums[0] >= 90) {
|
||||||
|
sys = String(nums[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 高压应不低于低压,顺序异常时交换
|
||||||
|
if (sys && dia && Number(sys) < Number(dia)) {
|
||||||
|
const tmp = sys
|
||||||
|
sys = dia
|
||||||
|
dia = tmp
|
||||||
|
}
|
||||||
|
if (sys) result.systolic_pressure = sys
|
||||||
|
if (dia) result.diastolic_pressure = dia
|
||||||
|
|
||||||
|
const insulin = textAfter(raw, ['胰岛素'], ['高压', '低压', '血压', '西药', '备注'])
|
||||||
|
if (insulin) result.insulin = insulin
|
||||||
|
const western = textAfter(raw, ['西药', '降糖药', '口服药'], ['胰岛素', '高压', '低压', '血压', '备注'])
|
||||||
|
if (western) result.western_medicine = western
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const DIET_MARKERS = [
|
||||||
|
{ keys: ['早餐', '早饭', '早上', '早点', '早晨'], field: 'breakfast_foods' },
|
||||||
|
{ keys: ['午餐', '午饭', '中午'], field: 'lunch_foods' },
|
||||||
|
{ keys: ['晚餐', '晚饭', '晚上', '夜宵', '夜里'], field: 'dinner_foods' }
|
||||||
|
]
|
||||||
|
|
||||||
|
function cleanFoodSeg(seg) {
|
||||||
|
return String(seg || '')
|
||||||
|
.replace(/^[是为吃了吃的喝了喝的有::,,、。\s]+/, '')
|
||||||
|
.replace(/[。.\s]+$/, '')
|
||||||
|
.trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析饮食:返回 { breakfast_foods, lunch_foods, dinner_foods, note }
|
||||||
|
* 例:"早餐鸡蛋粥中午米饭炒青菜晚上面条" → 分别归位
|
||||||
|
* 无餐别关键词时整句写入 note
|
||||||
|
*/
|
||||||
|
export function parseDiet(raw) {
|
||||||
|
const t = String(raw || '').trim()
|
||||||
|
const result = {}
|
||||||
|
const hits = []
|
||||||
|
DIET_MARKERS.forEach((m) => {
|
||||||
|
for (const k of m.keys) {
|
||||||
|
const idx = t.indexOf(k)
|
||||||
|
if (idx >= 0) {
|
||||||
|
hits.push({ idx, field: m.field, klen: k.length })
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (!hits.length) {
|
||||||
|
if (t) result.note = t
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
hits.sort((a, b) => a.idx - b.idx)
|
||||||
|
hits.forEach((h, i) => {
|
||||||
|
const start = h.idx + h.klen
|
||||||
|
const end = i + 1 < hits.length ? hits[i + 1].idx : t.length
|
||||||
|
const seg = cleanFoodSeg(t.slice(start, end))
|
||||||
|
if (seg && !result[h.field]) result[h.field] = seg
|
||||||
|
})
|
||||||
|
// 第一个餐别关键词之前的内容作为备注
|
||||||
|
const head = cleanFoodSeg(t.slice(0, hits[0].idx))
|
||||||
|
if (head) result.note = head
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const EXERCISE_FILLERS = [
|
||||||
|
'今天', '我', '做了', '做', '进行了', '进行', '运动了', '运动', '锻炼了', '锻炼',
|
||||||
|
'了', '大概', '左右', '持续', '差不多', '一共', '总共', '的', '走了', '打了', '跑了'
|
||||||
|
]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析运动:返回 { exercise_type, duration, intensity }
|
||||||
|
* 例:"散步三十分钟中强度" → { type:'散步', duration:30, intensity:2 }
|
||||||
|
*/
|
||||||
|
export function parseExercise(raw) {
|
||||||
|
const t = normalizeNumbers(raw)
|
||||||
|
const result = {}
|
||||||
|
|
||||||
|
const durMatch = t.match(/(\d+(?:\.\d+)?)\s*(个小时|小时|钟头|时|分钟|分)/)
|
||||||
|
if (durMatch) {
|
||||||
|
const val = parseFloat(durMatch[1])
|
||||||
|
const isHour = /个小时|小时|钟头|时/.test(durMatch[2])
|
||||||
|
result.duration = String(Math.round(isHour ? val * 60 : val))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/高强度|剧烈|很累|大汗|气喘/.test(t)) result.intensity = 3
|
||||||
|
else if (/中强度|中等强度|中等|有点累|微微出汗|微汗/.test(t)) result.intensity = 2
|
||||||
|
else if (/低强度|轻松|轻微|溜达|缓慢/.test(t)) result.intensity = 1
|
||||||
|
|
||||||
|
// 运动类型:移除时长、强度、填充词后剩余文本
|
||||||
|
let type = t
|
||||||
|
if (durMatch) type = type.replace(durMatch[0], '')
|
||||||
|
type = type
|
||||||
|
.replace(/高强度|中强度|中等强度|低强度|剧烈|很累|大汗|气喘|有点累|微微出汗|微汗|轻松|轻微|缓慢/g, '')
|
||||||
|
.replace(/\d+(?:\.\d+)?/g, '')
|
||||||
|
.replace(/[,,。.;;、\s]+/g, '')
|
||||||
|
EXERCISE_FILLERS.forEach((f) => {
|
||||||
|
type = type.split(f).join('')
|
||||||
|
})
|
||||||
|
type = type.trim()
|
||||||
|
if (type && type.length <= 20) result.exercise_type = type
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const FIELD_LABELS = {
|
||||||
|
fasting_blood_sugar: '空腹',
|
||||||
|
postprandial_blood_sugar: '餐后',
|
||||||
|
other_blood_sugar: '其他血糖',
|
||||||
|
systolic_pressure: '高压',
|
||||||
|
diastolic_pressure: '低压',
|
||||||
|
western_medicine: '西药',
|
||||||
|
insulin: '胰岛素',
|
||||||
|
breakfast_foods: '早餐',
|
||||||
|
lunch_foods: '午餐',
|
||||||
|
dinner_foods: '晚餐',
|
||||||
|
note: '备注',
|
||||||
|
exercise_type: '运动',
|
||||||
|
duration: '时长',
|
||||||
|
intensity: '强度'
|
||||||
|
}
|
||||||
|
|
||||||
|
const INTENSITY_LABELS = { 1: '低强度', 2: '中强度', 3: '高强度' }
|
||||||
|
|
||||||
|
/** 把解析结果转成可读的反馈文案,例如 "空腹6.5 · 餐后8.2" */
|
||||||
|
export function summarizeParsed(parsed) {
|
||||||
|
const parts = []
|
||||||
|
Object.keys(parsed).forEach((k) => {
|
||||||
|
const label = FIELD_LABELS[k] || k
|
||||||
|
let val = parsed[k]
|
||||||
|
if (k === 'intensity') val = INTENSITY_LABELS[val] || val
|
||||||
|
parts.push(`${label}${val}`)
|
||||||
|
})
|
||||||
|
return parts.join(' · ')
|
||||||
|
}
|
||||||
@@ -1,10 +1,22 @@
|
|||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
import uni from '@dcloudio/vite-plugin-uni'
|
import uni from '@dcloudio/vite-plugin-uni'
|
||||||
|
import Optimization from '@uni-ku/bundle-optimizer'
|
||||||
|
|
||||||
// uni-app 工程根目录就是源码目录(HBuilderX 兼容)
|
// uni-app 工程根目录就是源码目录(HBuilderX 兼容)
|
||||||
// 编译产物默认输出到 ./dist/<mode>/<platform>
|
// 编译产物默认输出到 ./dist/<mode>/<platform>
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [uni()],
|
plugins: [
|
||||||
|
uni(),
|
||||||
|
Optimization({
|
||||||
|
enable: {
|
||||||
|
optimization: true,
|
||||||
|
'async-import': false,
|
||||||
|
'async-component': false,
|
||||||
|
},
|
||||||
|
dts: false,
|
||||||
|
logger: false,
|
||||||
|
}),
|
||||||
|
],
|
||||||
css: {
|
css: {
|
||||||
preprocessorOptions: {
|
preprocessorOptions: {
|
||||||
scss: {
|
scss: {
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
unpackage/
|
||||||
|
.DS_Store
|
||||||
|
*.log
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
.hbuilderx/
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
<script setup>
|
||||||
|
import { onLaunch } from '@dcloudio/uni-app'
|
||||||
|
import { getCurrentInstance } from 'vue'
|
||||||
|
|
||||||
|
const { proxy } = getCurrentInstance()
|
||||||
|
|
||||||
|
onLaunch(() => {
|
||||||
|
// iOS 静音键模式下 InnerAudioContext 默认不发声
|
||||||
|
// #ifdef MP-WEIXIN
|
||||||
|
try {
|
||||||
|
uni.setInnerAudioOption({
|
||||||
|
obeyMuteSwitch: false,
|
||||||
|
mixWithOther: true,
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('setInnerAudioOption failed:', e)
|
||||||
|
}
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
uni.login({
|
||||||
|
provider: 'weixin',
|
||||||
|
success: function (loginRes) {
|
||||||
|
if (uni.getStorageSync('token')) {
|
||||||
|
userinfo(loginRes.code)
|
||||||
|
} else {
|
||||||
|
wxcode(loginRes.code)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const wxcode = async (code) => {
|
||||||
|
try {
|
||||||
|
const res = await proxy.apiUrl(
|
||||||
|
{
|
||||||
|
url: '/api/login/mnpLogin',
|
||||||
|
method: 'POST',
|
||||||
|
data: {
|
||||||
|
code: code,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
false
|
||||||
|
)
|
||||||
|
if (res && res.data && res.data.token) {
|
||||||
|
uni.setStorageSync('token', res.data.token)
|
||||||
|
uni.setStorageSync('userData', res.data)
|
||||||
|
}
|
||||||
|
} catch (err) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const userinfo = async (code) => {
|
||||||
|
try {
|
||||||
|
const res = await proxy.apiUrl(
|
||||||
|
{
|
||||||
|
url: '/api/user/info',
|
||||||
|
method: 'POST',
|
||||||
|
},
|
||||||
|
false
|
||||||
|
)
|
||||||
|
if (res.code == -1) {
|
||||||
|
wxcode(code)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (res && res.data) {
|
||||||
|
uni.setStorageSync('userData', res.data)
|
||||||
|
}
|
||||||
|
} catch (err) {}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* 全局基础 */
|
||||||
|
page {
|
||||||
|
background-color: #f4fbf4;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial,
|
||||||
|
sans-serif;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# TongjiUniApp
|
||||||
|
|
||||||
|
从 `TUICallKit-Vue3` 抽出的独立 uni-app,保留血糖管理与「我的」相关能力。
|
||||||
|
|
||||||
|
## 包含功能
|
||||||
|
|
||||||
|
| 模块 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| 血糖管理 | `tongji/pages/weekly`(首页) |
|
||||||
|
| 日常护理 | `tongji/pages/more` |
|
||||||
|
| 血糖记录 | `tongji/pages/index` |
|
||||||
|
| 识糖小课堂 | `tongji/endless-game` |
|
||||||
|
| 我的 | 个人资料、就诊卡 |
|
||||||
|
| 居家耗糖训练 | `training` 分包(哑铃/脚蹬/瑜伽环/握力环/节拍器) |
|
||||||
|
|
||||||
|
底部导航:血糖 · 我的(已去掉原首页/视频问诊)。
|
||||||
|
|
||||||
|
## 开发
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
npm run dev:mp-weixin # 微信小程序 → dist/dev/mp-weixin
|
||||||
|
npm run build:mp-weixin
|
||||||
|
npm run dev:h5
|
||||||
|
```
|
||||||
|
|
||||||
|
- API:`main.js` → `https://admin.zhenyangtang.com.cn/`
|
||||||
|
- 微信语音插件 WechatSI 已配置(`manifest.json` / `pages.json`)
|
||||||
|
- 微信小程序 appid 沿用原工程:`wx79b9a0bfbfe7cbcd`(可在 `manifest.json` 修改)
|
||||||
|
|
||||||
|
## 说明
|
||||||
|
|
||||||
|
未移植:视频问诊(TUICallKit)、医生站、订单支付等。就诊卡未登录时回跳血糖首页,由 App 静默 `mnpLogin` 完成鉴权。
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
<template>
|
||||||
|
<view class="tab-dock">
|
||||||
|
<view class="tab-dock__inner">
|
||||||
|
<view class="tab-dock__item tab-dock__item--center" @click="goWeekly">
|
||||||
|
<view class="tab-dock__fab" :class="{ 'tab-dock__fab--active': active === 0 }">
|
||||||
|
<text class="tab-dock__fab-text">糖</text>
|
||||||
|
</view>
|
||||||
|
<text class="tab-dock__label tab-dock__label--center" :style="{ color: active === 0 ? activeColor : color }">血糖</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="tab-dock__item" @click="goUser">
|
||||||
|
<image
|
||||||
|
class="tab-dock__icon"
|
||||||
|
:src="active === 1 ? '/static/user/user_no.png' : '/static/user/user.png'"
|
||||||
|
mode="aspectFit"
|
||||||
|
/>
|
||||||
|
<text class="tab-dock__label" :style="{ color: active === 1 ? activeColor : color }">我的</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="tab-dock__safe" />
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
const props = defineProps({
|
||||||
|
/** 0 血糖 1 我的 */
|
||||||
|
active: {
|
||||||
|
type: Number,
|
||||||
|
default: 0
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const color = '#8a8a8a'
|
||||||
|
const activeColor = '#204e2b'
|
||||||
|
|
||||||
|
function goUser() {
|
||||||
|
if (props.active === 1) return
|
||||||
|
uni.redirectTo({ url: '/pages/user/user' })
|
||||||
|
}
|
||||||
|
|
||||||
|
function goWeekly() {
|
||||||
|
if (props.active === 0) return
|
||||||
|
uni.redirectTo({ url: '/tongji/pages/weekly' })
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.tab-dock {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 999;
|
||||||
|
background: #ffffff;
|
||||||
|
border-top: 1rpx solid rgba(0, 0, 0, 0.06);
|
||||||
|
box-shadow: 0 -6rpx 20rpx rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-dock__inner {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
height: 100rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-dock__item {
|
||||||
|
flex: 1;
|
||||||
|
height: 100rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-dock__item--center {
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding-bottom: 6rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-dock__icon {
|
||||||
|
width: 46rpx;
|
||||||
|
height: 46rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-dock__label {
|
||||||
|
margin-top: 6rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-dock__label--center {
|
||||||
|
margin-top: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-dock__fab {
|
||||||
|
width: 96rpx;
|
||||||
|
height: 96rpx;
|
||||||
|
margin-top: -48rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: linear-gradient(180deg, #2d7340 0%, #204e2b 100%);
|
||||||
|
border: 6rpx solid #ffffff;
|
||||||
|
box-shadow: 0 10rpx 24rpx rgba(32, 78, 43, 0.35);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-dock__fab--active {
|
||||||
|
transform: scale(1.04);
|
||||||
|
box-shadow: 0 12rpx 28rpx rgba(32, 78, 43, 0.42);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-dock__fab-text {
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 38rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-dock__safe {
|
||||||
|
height: env(safe-area-inset-bottom);
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,729 @@
|
|||||||
|
<template>
|
||||||
|
<!-- 页面内嵌模块(如 weekly 统计页主内容区) -->
|
||||||
|
<view
|
||||||
|
v-if="visible && props.inline"
|
||||||
|
class="dev-entry-inline"
|
||||||
|
:class="{ 'dev-entry-inline--weekly': props.tone === 'weekly' }"
|
||||||
|
>
|
||||||
|
<view class="inline-hero">
|
||||||
|
<view class="inline-hero-icon" aria-hidden="true">
|
||||||
|
<view class="inline-hero-wave" />
|
||||||
|
</view>
|
||||||
|
<view class="inline-hero-text">
|
||||||
|
<view class="inline-hero-title-row">
|
||||||
|
<text class="inline-title">居家耗糖训练</text>
|
||||||
|
<text v-if="props.tone === 'weekly'" class="inline-pill">体验</text>
|
||||||
|
</view>
|
||||||
|
<text class="inline-sub">低强度动起来,帮助身体消耗糖分</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<scroll-view
|
||||||
|
scroll-x
|
||||||
|
class="inline-scroll"
|
||||||
|
:show-scrollbar="false"
|
||||||
|
:enable-flex="true"
|
||||||
|
>
|
||||||
|
<view class="inline-scroll-track">
|
||||||
|
<view
|
||||||
|
v-for="item in inlineItems"
|
||||||
|
:key="item.url"
|
||||||
|
class="inline-card"
|
||||||
|
@click="goto(item.url)"
|
||||||
|
>
|
||||||
|
<view v-if="item.featured" class="inline-card-badge">
|
||||||
|
<text>推荐</text>
|
||||||
|
</view>
|
||||||
|
<view class="menu-icon" :class="item.iconClass">
|
||||||
|
<view v-if="item.icon === 'dumbbell'" class="dumbbell-shape">
|
||||||
|
<view class="dumbbell-plate dumbbell-plate-left" />
|
||||||
|
<view class="dumbbell-bar" />
|
||||||
|
<view class="dumbbell-plate dumbbell-plate-right" />
|
||||||
|
</view>
|
||||||
|
<view v-else-if="item.icon === 'pedal'" class="foot-pedal-shape">
|
||||||
|
<view class="pedal-bar pedal-bar-left" />
|
||||||
|
<view class="pedal-bar pedal-bar-right" />
|
||||||
|
</view>
|
||||||
|
<view v-else-if="item.icon === 'pilates'" class="pilates-ring-shape" />
|
||||||
|
<view v-else-if="item.icon === 'grip'" class="grip-ring-shape" />
|
||||||
|
<view v-else-if="item.icon === 'metro'" class="menu-icon--metro-inner">
|
||||||
|
<view class="metro-bar metro-bar-1" />
|
||||||
|
<view class="metro-bar metro-bar-2" />
|
||||||
|
<view class="metro-bar metro-bar-3" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<text class="inline-card-title">{{ item.title }}</text>
|
||||||
|
<text class="inline-card-sub">{{ item.sub }}</text>
|
||||||
|
<text class="inline-card-go" aria-hidden="true">进入</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
|
||||||
|
<text v-if="props.tone === 'weekly'" class="inline-scroll-hint">左右滑动,选择适合您的训练</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-else-if="visible" class="dev-entry-wrap" :style="{ bottom: props.bottom + 'rpx' }">
|
||||||
|
<!-- 展开后的菜单卡片(从下往上动画) -->
|
||||||
|
<view v-if="expanded" class="menu-list" @click.stop>
|
||||||
|
<view class="menu-item" @click="goto('/training/pages/dumbbell')">
|
||||||
|
<view class="menu-icon menu-icon--dumbbell">
|
||||||
|
<view class="dumbbell-shape">
|
||||||
|
<view class="dumbbell-plate dumbbell-plate-left" />
|
||||||
|
<view class="dumbbell-bar" />
|
||||||
|
<view class="dumbbell-plate dumbbell-plate-right" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="menu-info">
|
||||||
|
<text class="menu-title">哑铃</text>
|
||||||
|
<text class="menu-sub">力量塑形</text>
|
||||||
|
</view>
|
||||||
|
<view class="menu-arrow">›</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="menu-item" @click="goto('/training/pages/foot-pedal')">
|
||||||
|
<view class="menu-icon menu-icon--pedal">
|
||||||
|
<view class="foot-pedal-shape">
|
||||||
|
<view class="pedal-bar pedal-bar-left" />
|
||||||
|
<view class="pedal-bar pedal-bar-right" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="menu-info">
|
||||||
|
<text class="menu-title">脚蹬器</text>
|
||||||
|
<text class="menu-sub">下肢有氧</text>
|
||||||
|
</view>
|
||||||
|
<view class="menu-arrow">›</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="menu-item" @click="goto('/training/pages/pilates-ring')">
|
||||||
|
<view class="menu-icon menu-icon--pilates">
|
||||||
|
<view class="pilates-ring-shape" />
|
||||||
|
</view>
|
||||||
|
<view class="menu-info">
|
||||||
|
<text class="menu-title">瑜伽环</text>
|
||||||
|
<text class="menu-sub">核心塑形</text>
|
||||||
|
</view>
|
||||||
|
<view class="menu-arrow">›</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="menu-item" @click="goto('/training/pages/grip-ring')">
|
||||||
|
<view class="menu-icon menu-icon--grip">
|
||||||
|
<view class="grip-ring-shape" />
|
||||||
|
</view>
|
||||||
|
<view class="menu-info">
|
||||||
|
<text class="menu-title">握力环</text>
|
||||||
|
<text class="menu-sub">握力训练</text>
|
||||||
|
</view>
|
||||||
|
<view class="menu-arrow">›</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="menu-item" @click="goto('/training/pages/metronome')">
|
||||||
|
<view class="menu-icon menu-icon--metro">
|
||||||
|
<view class="metro-bar metro-bar-1" />
|
||||||
|
<view class="metro-bar metro-bar-2" />
|
||||||
|
<view class="metro-bar metro-bar-3" />
|
||||||
|
</view>
|
||||||
|
<view class="menu-info">
|
||||||
|
<text class="menu-title">耗糖节拍器</text>
|
||||||
|
<text class="menu-sub">健走配速</text>
|
||||||
|
</view>
|
||||||
|
<view class="menu-arrow">›</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 主浮动按钮(单字"练" + 呼吸光环) -->
|
||||||
|
<view class="fab-container">
|
||||||
|
<view v-if="!expanded" class="fab-pulse" />
|
||||||
|
<view v-if="!expanded" class="fab-pulse fab-pulse-2" />
|
||||||
|
<view class="entry-fab" :class="{ expanded }" @click="toggle">
|
||||||
|
<view v-if="expanded" class="icon-close">
|
||||||
|
<view class="cross-bar bar-1" />
|
||||||
|
<view class="cross-bar bar-2" />
|
||||||
|
</view>
|
||||||
|
<!-- 心跳波形 icon: 健康主题,SVG 折线 -->
|
||||||
|
<view v-else class="fab-icon" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
|
||||||
|
type TrainingItem = {
|
||||||
|
url: string
|
||||||
|
title: string
|
||||||
|
sub: string
|
||||||
|
icon: 'dumbbell' | 'pedal' | 'pilates' | 'grip' | 'metro'
|
||||||
|
iconClass: string
|
||||||
|
featured?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const trainingItems: TrainingItem[] = [
|
||||||
|
{ url: '/training/pages/dumbbell', title: '哑铃', sub: '力量塑形', icon: 'dumbbell', iconClass: 'menu-icon--dumbbell' },
|
||||||
|
{ url: '/training/pages/foot-pedal', title: '脚蹬器', sub: '下肢有氧', icon: 'pedal', iconClass: 'menu-icon--pedal' },
|
||||||
|
{ url: '/training/pages/pilates-ring', title: '瑜伽环', sub: '核心塑形', icon: 'pilates', iconClass: 'menu-icon--pilates' },
|
||||||
|
{ url: '/training/pages/grip-ring', title: '握力环', sub: '握力训练', icon: 'grip', iconClass: 'menu-icon--grip' },
|
||||||
|
{ url: '/training/pages/metronome', title: '耗糖节拍器', sub: '健走配速', icon: 'metro', iconClass: 'menu-icon--metro', featured: true },
|
||||||
|
]
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
/** 悬浮按钮距底部 rpx(仅非 inline) */
|
||||||
|
bottom?: number
|
||||||
|
/** 内嵌到页面主内容区,非 fixed 悬浮 */
|
||||||
|
inline?: boolean
|
||||||
|
/** weekly 页:与统计页 Vital Mint 视觉一致 */
|
||||||
|
tone?: 'default' | 'weekly'
|
||||||
|
}>(),
|
||||||
|
{ bottom: 200, inline: false, tone: 'default' },
|
||||||
|
)
|
||||||
|
|
||||||
|
/** weekly 将健走节拍器置前并标推荐 */
|
||||||
|
const inlineItems = computed(() => {
|
||||||
|
if (props.tone !== 'weekly') return trainingItems
|
||||||
|
const metro = trainingItems.find((i) => i.icon === 'metro')
|
||||||
|
const rest = trainingItems.filter((i) => i.icon !== 'metro')
|
||||||
|
return metro ? [metro, ...rest] : trainingItems
|
||||||
|
})
|
||||||
|
|
||||||
|
// 居家耗糖训练是正式功能,开发版和发行版都需要展示。
|
||||||
|
const visible = ref(true)
|
||||||
|
const expanded = ref(false)
|
||||||
|
|
||||||
|
const toggle = () => {
|
||||||
|
expanded.value = !expanded.value
|
||||||
|
}
|
||||||
|
|
||||||
|
const goto = (url: string) => {
|
||||||
|
expanded.value = false
|
||||||
|
uni.navigateTo({ url })
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
$brand: #10b981;
|
||||||
|
$brand-deep: #047857;
|
||||||
|
$brand-light: #34d399;
|
||||||
|
|
||||||
|
.dev-entry-wrap {
|
||||||
|
position: fixed;
|
||||||
|
right: 24rpx;
|
||||||
|
z-index: 999;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 18rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
* 主浮动按钮
|
||||||
|
* ============================================================ */
|
||||||
|
.fab-container {
|
||||||
|
position: relative;
|
||||||
|
width: 108rpx;
|
||||||
|
height: 108rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.entry-fab {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: linear-gradient(140deg, $brand-light 0%, $brand-deep 100%);
|
||||||
|
box-shadow:
|
||||||
|
0 8rpx 20rpx rgba(16, 185, 129, 0.42),
|
||||||
|
0 2rpx 6rpx rgba(15, 23, 42, 0.12),
|
||||||
|
inset 0 -8rpx 16rpx rgba(0, 0, 0, 0.12),
|
||||||
|
inset 0 8rpx 16rpx rgba(255, 255, 255, 0.18);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1), background 0.2s;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: scale(0.92);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.expanded {
|
||||||
|
background: linear-gradient(140deg, #94a3b8 0%, #475569 100%);
|
||||||
|
transform: rotate(135deg);
|
||||||
|
box-shadow:
|
||||||
|
0 6rpx 16rpx rgba(15, 23, 42, 0.2),
|
||||||
|
inset 0 -6rpx 14rpx rgba(0, 0, 0, 0.12),
|
||||||
|
inset 0 6rpx 14rpx rgba(255, 255, 255, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fab-icon {
|
||||||
|
width: 60rpx;
|
||||||
|
height: 60rpx;
|
||||||
|
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23ffffff' stroke-width='2.6' stroke-linecap='round' stroke-linejoin='round'><polyline points='2,12 7,12 9.5,7 12,17 14.5,9 16.5,12 22,12'/></svg>");
|
||||||
|
background-size: contain;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center;
|
||||||
|
filter: drop-shadow(0 2rpx 6rpx rgba(0, 0, 0, 0.22));
|
||||||
|
animation: fab-icon-beat 1.6s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes fab-icon-beat {
|
||||||
|
0%, 60%, 100% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
20% {
|
||||||
|
transform: scale(1.12);
|
||||||
|
}
|
||||||
|
40% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 关闭态 X 图标 (CSS 几何) */
|
||||||
|
.icon-close {
|
||||||
|
position: relative;
|
||||||
|
width: 36rpx;
|
||||||
|
height: 36rpx;
|
||||||
|
|
||||||
|
.cross-bar {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 5rpx;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 3rpx;
|
||||||
|
transform-origin: center;
|
||||||
|
}
|
||||||
|
.bar-1 {
|
||||||
|
transform: translateY(-50%) rotate(45deg);
|
||||||
|
}
|
||||||
|
.bar-2 {
|
||||||
|
transform: translateY(-50%) rotate(-45deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 呼吸光环(2 圈错相位扩散) */
|
||||||
|
.fab-pulse {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(16, 185, 129, 0.25);
|
||||||
|
animation: fab-ring 2.4s ease-out infinite;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.fab-pulse-2 {
|
||||||
|
animation-delay: 1.2s;
|
||||||
|
}
|
||||||
|
@keyframes fab-ring {
|
||||||
|
0% {
|
||||||
|
transform: scale(0.95);
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(1.55);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
* 展开菜单
|
||||||
|
* ============================================================ */
|
||||||
|
.menu-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 14rpx;
|
||||||
|
animation: slide-up 0.22s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||||
|
}
|
||||||
|
@keyframes slide-up {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20rpx) scale(0.92);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0) scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.96);
|
||||||
|
padding: 14rpx 20rpx 14rpx 14rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
box-shadow:
|
||||||
|
0 8rpx 24rpx rgba(15, 23, 42, 0.1),
|
||||||
|
0 1rpx 2rpx rgba(15, 23, 42, 0.06);
|
||||||
|
min-width: 280rpx;
|
||||||
|
transition: transform 0.15s;
|
||||||
|
backdrop-filter: blur(12rpx);
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: scale(0.97);
|
||||||
|
background: #f0fdf4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-icon {
|
||||||
|
width: 64rpx;
|
||||||
|
height: 64rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
box-shadow: 0 2rpx 8rpx rgba(16, 185, 129, 0.32);
|
||||||
|
|
||||||
|
&--train {
|
||||||
|
background: linear-gradient(140deg, $brand-light, $brand-deep);
|
||||||
|
}
|
||||||
|
&--dumbbell {
|
||||||
|
background: linear-gradient(140deg, #93c5fd, #3b82f6);
|
||||||
|
box-shadow: 0 2rpx 8rpx rgba(59, 130, 246, 0.36);
|
||||||
|
}
|
||||||
|
&--pedal {
|
||||||
|
background: linear-gradient(140deg, #fdba74, #f97316);
|
||||||
|
box-shadow: 0 2rpx 8rpx rgba(249, 115, 22, 0.36);
|
||||||
|
}
|
||||||
|
&--pilates {
|
||||||
|
background: linear-gradient(140deg, #c4b5fd, #8b5cf6);
|
||||||
|
box-shadow: 0 2rpx 8rpx rgba(139, 92, 246, 0.36);
|
||||||
|
}
|
||||||
|
&--grip {
|
||||||
|
background: linear-gradient(140deg, #5eead4, #14b8a6);
|
||||||
|
box-shadow: 0 2rpx 8rpx rgba(20, 184, 166, 0.36);
|
||||||
|
}
|
||||||
|
&--metro {
|
||||||
|
background: linear-gradient(140deg, #fbbf24, #d97706);
|
||||||
|
box-shadow: 0 2rpx 8rpx rgba(245, 158, 11, 0.36);
|
||||||
|
gap: 4rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-icon-text {
|
||||||
|
font-size: 32rpx;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 哑铃小 icon */
|
||||||
|
.dumbbell-shape {
|
||||||
|
width: 40rpx;
|
||||||
|
height: 20rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dumbbell-plate {
|
||||||
|
width: 10rpx;
|
||||||
|
height: 18rpx;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 3rpx;
|
||||||
|
box-shadow: 0 1rpx 2rpx rgba(0, 0, 0, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dumbbell-bar {
|
||||||
|
width: 16rpx;
|
||||||
|
height: 5rpx;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 2rpx;
|
||||||
|
margin: 0 2rpx;
|
||||||
|
box-shadow: 0 1rpx 2rpx rgba(0, 0, 0, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 脚蹬器小 icon: 左右踏板 */
|
||||||
|
.foot-pedal-shape {
|
||||||
|
width: 40rpx;
|
||||||
|
height: 28rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 6rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pedal-bar {
|
||||||
|
width: 14rpx;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 4rpx 4rpx 2rpx 2rpx;
|
||||||
|
box-shadow: 0 1rpx 2rpx rgba(0, 0, 0, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pedal-bar-left {
|
||||||
|
height: 18rpx;
|
||||||
|
transform: rotate(-12deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pedal-bar-right {
|
||||||
|
height: 22rpx;
|
||||||
|
transform: rotate(12deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 瑜伽环小 icon: 双弧阻力环 */
|
||||||
|
.pilates-ring-shape {
|
||||||
|
width: 36rpx;
|
||||||
|
height: 36rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 5rpx solid #fff;
|
||||||
|
position: relative;
|
||||||
|
box-shadow:
|
||||||
|
0 1rpx 2rpx rgba(0, 0, 0, 0.18),
|
||||||
|
inset 0 1rpx 2rpx rgba(255, 255, 255, 0.6);
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
width: 18rpx;
|
||||||
|
height: 18rpx;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 3rpx solid rgba(255, 255, 255, 0.85);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 握力环小 icon: 白色描边的环(甜甜圈) */
|
||||||
|
.grip-ring-shape {
|
||||||
|
width: 32rpx;
|
||||||
|
height: 32rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 7rpx solid #fff;
|
||||||
|
box-shadow:
|
||||||
|
0 1rpx 2rpx rgba(0, 0, 0, 0.18),
|
||||||
|
inset 0 1rpx 2rpx rgba(255, 255, 255, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 节拍器小 icon: 3 根高低柱模拟均衡器/节拍 */
|
||||||
|
.metro-bar {
|
||||||
|
width: 4rpx;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 2rpx;
|
||||||
|
box-shadow: 0 1rpx 2rpx rgba(0, 0, 0, 0.18);
|
||||||
|
}
|
||||||
|
.metro-bar-1 {
|
||||||
|
height: 16rpx;
|
||||||
|
}
|
||||||
|
.metro-bar-2 {
|
||||||
|
height: 28rpx;
|
||||||
|
}
|
||||||
|
.metro-bar-3 {
|
||||||
|
height: 22rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2rpx;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-title {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #0f172a;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 1rpx;
|
||||||
|
}
|
||||||
|
.menu-sub {
|
||||||
|
font-size: 20rpx;
|
||||||
|
color: #94a3b8;
|
||||||
|
letter-spacing: 0.5rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-arrow {
|
||||||
|
font-size: 32rpx;
|
||||||
|
color: #cbd5e1;
|
||||||
|
font-weight: 300;
|
||||||
|
line-height: 1;
|
||||||
|
margin-left: 4rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-icon--metro-inner {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 4rpx;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
* 内嵌模块(weekly 等)
|
||||||
|
* ============================================================ */
|
||||||
|
.dev-entry-inline {
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dev-entry-inline--weekly {
|
||||||
|
padding: 32rpx 28rpx 24rpx;
|
||||||
|
border-radius: 48rpx;
|
||||||
|
background: var(--surface-container-lowest, #fff);
|
||||||
|
border: 1rpx solid rgba(0, 108, 73, 0.12);
|
||||||
|
box-shadow:
|
||||||
|
0 24rpx 60rpx -20rpx rgba(0, 108, 73, 0.08),
|
||||||
|
0 8rpx 20rpx -8rpx rgba(0, 0, 0, 0.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-hero {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20rpx;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-hero-icon {
|
||||||
|
width: 88rpx;
|
||||||
|
height: 88rpx;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
background: rgba(0, 108, 73, 0.1);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-hero-wave {
|
||||||
|
width: 52rpx;
|
||||||
|
height: 52rpx;
|
||||||
|
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23006c49' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'><polyline points='2,12 7,12 9.5,7 12,17 14.5,9 16.5,12 22,12'/></svg>");
|
||||||
|
background-size: contain;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-hero-text {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-hero-title-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-title {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #0f172a;
|
||||||
|
line-height: 1.25;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-pill {
|
||||||
|
font-size: 20rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #006c49;
|
||||||
|
padding: 4rpx 12rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
background: rgba(0, 108, 73, 0.1);
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-sub {
|
||||||
|
font-size: 26rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #475569;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-scroll {
|
||||||
|
width: 100%;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-scroll-track {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 16rpx;
|
||||||
|
padding: 4rpx 2rpx 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-card {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
width: 212rpx;
|
||||||
|
min-height: 220rpx;
|
||||||
|
padding: 20rpx 18rpx 18rpx;
|
||||||
|
border-radius: 28rpx;
|
||||||
|
background: #f4f7f5;
|
||||||
|
border: 1rpx solid rgba(0, 108, 73, 0.1);
|
||||||
|
box-sizing: border-box;
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: transform 0.2s ease, background 0.2s ease;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: scale(0.98);
|
||||||
|
background: #e8f3ee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-icon {
|
||||||
|
width: 72rpx;
|
||||||
|
height: 72rpx;
|
||||||
|
margin-bottom: 14rpx;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-card-badge {
|
||||||
|
position: absolute;
|
||||||
|
top: 12rpx;
|
||||||
|
right: 12rpx;
|
||||||
|
padding: 4rpx 10rpx;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
background: #006c49;
|
||||||
|
|
||||||
|
text {
|
||||||
|
font-size: 18rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #fff;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-card-title {
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #0f172a;
|
||||||
|
line-height: 1.25;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-card-sub {
|
||||||
|
margin-top: 6rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #64748b;
|
||||||
|
line-height: 1.35;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-card-go {
|
||||||
|
margin-top: auto;
|
||||||
|
padding-top: 12rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #006c49;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-scroll-hint {
|
||||||
|
display: block;
|
||||||
|
margin-top: 14rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #94a3b8;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.inline-card:active {
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("./common/vendor.js");exports.createApp=e.createApp;
|
||||||
+42
@@ -0,0 +1,42 @@
|
|||||||
|
{
|
||||||
|
"pages": [
|
||||||
|
"tongji/pages/weekly",
|
||||||
|
"pages/user/user",
|
||||||
|
"pages/user/userinfo",
|
||||||
|
"pages/Card/Card",
|
||||||
|
"pages/Card/edit_card",
|
||||||
|
"pages/Card/contact",
|
||||||
|
"tongji/pages/index",
|
||||||
|
"tongji/pages/more",
|
||||||
|
"tongji/endless-game/index"
|
||||||
|
],
|
||||||
|
"subPackages": [
|
||||||
|
{
|
||||||
|
"root": "training",
|
||||||
|
"pages": [
|
||||||
|
"pages/grip-ring",
|
||||||
|
"pages/pilates-ring",
|
||||||
|
"pages/foot-pedal",
|
||||||
|
"pages/dumbbell",
|
||||||
|
"pages/metronome"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"window": {
|
||||||
|
"navigationBarTextStyle": "black",
|
||||||
|
"navigationBarTitleText": "甄养堂血糖管理",
|
||||||
|
"navigationBarBackgroundColor": "#F8F8F8",
|
||||||
|
"backgroundColor": "#F8F8F8"
|
||||||
|
},
|
||||||
|
"lazyCodeLoading": "requiredComponents",
|
||||||
|
"requiredBackgroundModes": [
|
||||||
|
"audio"
|
||||||
|
],
|
||||||
|
"plugins": {
|
||||||
|
"WechatSI": {
|
||||||
|
"version": "0.3.5",
|
||||||
|
"provider": "wx069ba97219f66d99"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"usingComponents": {}
|
||||||
|
}
|
||||||
+2
@@ -0,0 +1,2 @@
|
|||||||
|
page{background-color:#f4fbf4;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif}
|
||||||
|
page::after{position:fixed;content:'';left:-1000px;top:-1000px;-webkit-animation:shadow-preload .1s;-webkit-animation-delay:3s;animation:shadow-preload .1s;animation-delay:3s}@-webkit-keyframes shadow-preload{0%{background-image:url(https://cdn1.dcloud.net.cn/5645394f5230704a4d44456c4a5864344e7a6c694f574577596d5a695a6d553359324a6a5a413d3d/img/shadow-grey.png)}100%{background-image:url(https://cdn1.dcloud.net.cn/5645394f5230704a4d44456c4a5864344e7a6c694f574577596d5a695a6d553359324a6a5a413d3d/img/shadow-grey.png)}}@keyframes shadow-preload{0%{background-image:url(https://cdn1.dcloud.net.cn/5645394f5230704a4d44456c4a5864344e7a6c694f574577596d5a695a6d553359324a6a5a413d3d/img/shadow-grey.png)}100%{background-image:url(https://cdn1.dcloud.net.cn/5645394f5230704a4d44456c4a5864344e7a6c694f574577596d5a695a6d553359324a6a5a413d3d/img/shadow-grey.png)}}page{--status-bar-height:25px;--top-window-height:0px;--window-top:0px;--window-bottom:0px;--window-left:0px;--window-right:0px;--window-magin:0px}[data-c-h="true"]{display: none !important;}
|
||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
|||||||
|
"use strict";const e=require("../../common/vendor.js"),t="#8a8a8a",c="#204e2b",r={__name:"tab-bar-dock",props:{active:{type:Number,default:0}},setup(r){const a=r;function o(){1!==a.active&&e.index.redirectTo({url:"/pages/user/user"})}function s(){0!==a.active&&e.index.redirectTo({url:"/tongji/pages/weekly"})}return(a,i)=>({a:0===r.active?1:"",b:0===r.active?c:t,c:e.o(s),d:1===r.active?"/static/user/user_no.png":"/static/user/user.png",e:1===r.active?c:t,f:e.o(o)})}},a=e._export_sfc(r,[["__scopeId","data-v-6c5d8930"]]);wx.createComponent(a);
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"component": true,
|
||||||
|
"usingComponents": {}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<view class="tab-dock data-v-6c5d8930"><view class="tab-dock__inner data-v-6c5d8930"><view class="tab-dock__item tab-dock__item--center data-v-6c5d8930" bindtap="{{c}}"><view class="{{['tab-dock__fab', 'data-v-6c5d8930', a && 'tab-dock__fab--active']}}"><text class="tab-dock__fab-text data-v-6c5d8930">糖</text></view><text class="tab-dock__label tab-dock__label--center data-v-6c5d8930" style="{{'color:' + b}}">血糖</text></view><view class="tab-dock__item data-v-6c5d8930" bindtap="{{f}}"><image class="tab-dock__icon data-v-6c5d8930" src="{{d}}" mode="aspectFit"/><text class="tab-dock__label data-v-6c5d8930" style="{{'color:' + e}}">我的</text></view></view><view class="tab-dock__safe data-v-6c5d8930"/></view>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
.tab-dock.data-v-6c5d8930{position:fixed;left:0;right:0;bottom:0;z-index:999;background:#fff;border-top:1rpx solid rgba(0,0,0,.06);box-shadow:0 -6rpx 20rpx rgba(0,0,0,.04)}.tab-dock__inner.data-v-6c5d8930{display:flex;align-items:flex-end;height:100rpx}.tab-dock__item.data-v-6c5d8930{flex:1;height:100rpx;display:flex;flex-direction:column;align-items:center;justify-content:center}.tab-dock__item--center.data-v-6c5d8930{justify-content:flex-end;padding-bottom:6rpx}.tab-dock__icon.data-v-6c5d8930{width:46rpx;height:46rpx}.tab-dock__label.data-v-6c5d8930{margin-top:6rpx;font-size:22rpx;line-height:1.2}.tab-dock__label--center.data-v-6c5d8930{margin-top:8rpx}.tab-dock__fab.data-v-6c5d8930{width:96rpx;height:96rpx;margin-top:-48rpx;border-radius:50%;background:linear-gradient(180deg,#2d7340,#204e2b);border:6rpx solid #ffffff;box-shadow:0 10rpx 24rpx rgba(32,78,43,.35);display:flex;align-items:center;justify-content:center}.tab-dock__fab--active.data-v-6c5d8930{transform:scale(1.04);box-shadow:0 12rpx 28rpx rgba(32,78,43,.42)}.tab-dock__fab-text.data-v-6c5d8930{color:#fff;font-size:38rpx;font-weight:700;line-height:1}.tab-dock__safe.data-v-6c5d8930{height:env(safe-area-inset-bottom);background:#fff}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"use strict";const e=require("../../common/vendor.js"),n=e.defineComponent({__name:"index",props:{bottom:{default:200},inline:{type:Boolean,default:!1},tone:{default:"default"}},setup(n){const i=[{url:"/training/pages/dumbbell",title:"哑铃",sub:"力量塑形",icon:"dumbbell",iconClass:"menu-icon--dumbbell"},{url:"/training/pages/foot-pedal",title:"脚蹬器",sub:"下肢有氧",icon:"pedal",iconClass:"menu-icon--pedal"},{url:"/training/pages/pilates-ring",title:"瑜伽环",sub:"核心塑形",icon:"pilates",iconClass:"menu-icon--pilates"},{url:"/training/pages/grip-ring",title:"握力环",sub:"握力训练",icon:"grip",iconClass:"menu-icon--grip"},{url:"/training/pages/metronome",title:"耗糖节拍器",sub:"健走配速",icon:"metro",iconClass:"menu-icon--metro",featured:!0}],o=n,l=e.computed(()=>{if("weekly"!==o.tone)return i;const e=i.find(e=>"metro"===e.icon),n=i.filter(e=>"metro"!==e.icon);return e?[e,...n]:i}),t=e.ref(!0),a=e.ref(!1),r=()=>{a.value=!a.value},u=n=>{a.value=!1,e.index.navigateTo({url:n})};return(n,i)=>e.e({a:t.value&&o.inline},t.value&&o.inline?e.e({b:"weekly"===o.tone},(o.tone,{}),{c:e.f(l.value,(n,i,o)=>e.e({a:n.featured},(n.featured,{}),{b:"dumbbell"===n.icon},("dumbbell"===n.icon||"pedal"===n.icon||"pilates"===n.icon||"grip"===n.icon||n.icon,{}),{c:"pedal"===n.icon,d:"pilates"===n.icon,e:"grip"===n.icon,f:"metro"===n.icon,g:e.n(n.iconClass),h:e.t(n.title),i:e.t(n.sub),j:n.url,k:e.o(e=>u(n.url),n.url)})),d:"weekly"===o.tone},(o.tone,{}),{e:"weekly"===o.tone?1:""}):t.value?e.e({g:a.value},a.value?{h:e.o(e=>u("/training/pages/dumbbell")),i:e.o(e=>u("/training/pages/foot-pedal")),j:e.o(e=>u("/training/pages/pilates-ring")),k:e.o(e=>u("/training/pages/grip-ring")),l:e.o(e=>u("/training/pages/metronome")),m:e.o(()=>{})}:{},{n:!a.value},(a.value,{}),{o:!a.value},(a.value,{}),{p:a.value},(a.value,{}),{q:a.value?1:"",r:e.o(r),s:o.bottom+"rpx"}):{},{f:t.value})}}),i=e._export_sfc(n,[["__scopeId","data-v-2ae07c93"]]);wx.createComponent(i);
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"component": true,
|
||||||
|
"usingComponents": {}
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
|||||||
|
"use strict";const e=require("../../common/vendor.js");if(!Array){e.resolveComponent("uni-icons")()}Math;const t={__name:"Card",setup(t){const{proxy:n}=e.getCurrentInstance(),a=e.ref([]),i=e.ref(!1);e.onMounted(()=>{o()}),e.onShow(()=>{o()});const o=async()=>{var t;i.value=!0;try{if(!e.index.getStorageSync("token"))return void e.index.showToast({title:"请先登录",icon:"none"});const i=e.index.getStorageSync("userData"),o=null==(t=null==i?void 0:i.diagnosis)?void 0:t.patient_id;if(!o)return;const d=await n.apiUrl({url:"/api/tcm/getCardList",method:"GET",data:{patient_id:o}});1===d.code?a.value=d.data||[]:e.index.showToast({title:d.msg||"获取失败",icon:"none"})}catch(o){console.error("加载就诊卡列表失败:",o)}finally{i.value=!1}},d=t=>{e.index.navigateTo({url:`/pages/Card/edit_card?id=${t.id}`})},s=()=>{e.index.getStorageSync("token")?e.index.navigateTo({url:"/pages/Card/edit_card?add=1"}):e.index.showToast({title:"请先登录",icon:"none"})},r=e=>1===e?"status-confirmed":"status-pending",c={first_visit:"初诊",follow_up:"复诊",consultation:"会诊"},l={qi_deficiency:"气虚",blood_deficiency:"血虚",yin_deficiency:"阴虚",yang_deficiency:"阳虚",qi_stagnation:"气滞",blood_stasis:"血瘀",phlegm_dampness:"痰湿",damp_heat:"湿热",cold_dampness:"寒湿",wind_cold:"风寒",wind_heat:"风热"},g=e=>l[e]||e||"-",_=e=>{if(!e)return"-";const t=new Date(1e3*e);return`${t.getFullYear()}-${String(t.getMonth()+1).padStart(2,"0")}-${String(t.getDate()).padStart(2,"0")}`};return(t,n)=>e.e({a:e.f(a.value,(t,n,a)=>{return{a:e.t(t.id),b:e.t(t.status_desc||(o=t.status,1===o?"启用":"禁用")),c:e.n(r(t.status)),d:e.t(t.patient_name||"-"),e:e.t(t.gender_desc||"-"),f:e.t(t.age),g:e.t((i=t.diagnosis_type,c[i]||i||"-")),h:e.t(g(t.syndrome_type)),i:e.t(t.diagnosis_date_text||_(t.diagnosis_date)),j:e.t(t.create_time||"-"),k:e.o(n=>(t=>{const n=`/tongji/pages/index?diagnosis_id=${t.id}&patient_id=${t.patient_id||""}&patient_name=${encodeURIComponent(t.patient_name||"")}&age=${t.age||""}&gender=${t.gender||""}`;e.index.navigateTo({url:n})})(t),t.id),l:e.o(e=>d(t),t.id),m:t.id,n:e.o(e=>d(t),t.id)};var i,o}),b:!i.value&&0===a.value.length},i.value||0!==a.value.length?{}:{c:e.p({type:"wallet",size:"80",color:"#1890ff"}),d:e.o(s)},{e:i.value},(i.value,{}))}},n=e._export_sfc(t,[["__scopeId","data-v-341940bb"]]);t.__runtimeHooks=2,wx.createPage(n);
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"navigationBarTitleText": "就诊卡",
|
||||||
|
"usingComponents": {
|
||||||
|
"uni-icons": "../../uni_modules/uni-icons/components/uni-icons/uni-icons"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<view class="card-container data-v-341940bb"><view class="card-list data-v-341940bb"><view wx:for="{{a}}" wx:for-item="card" wx:key="m" class="card-item data-v-341940bb" bindtap="{{card.n}}"><view class="card-header data-v-341940bb"><view class="card-id data-v-341940bb"><text class="label data-v-341940bb">诊单编号:</text><text class="value data-v-341940bb">{{card.a}}</text></view><view class="{{['data-v-341940bb', 'status-badge', card.c]}}">{{card.b}}</view></view><view class="card-info data-v-341940bb"><view class="info-row data-v-341940bb"><text class="info-label data-v-341940bb">患者姓名:</text><text class="info-value data-v-341940bb">{{card.d}}</text></view><view class="info-row data-v-341940bb"><text class="info-label data-v-341940bb">性别年龄:</text><text class="info-value data-v-341940bb">{{card.e}} / {{card.f}}岁</text></view><view class="info-row data-v-341940bb"><text class="info-label data-v-341940bb">诊断类型:</text><text class="info-value data-v-341940bb">{{card.g}}</text></view><view class="info-row data-v-341940bb"><text class="info-label data-v-341940bb">证型:</text><text class="info-value data-v-341940bb">{{card.h}}</text></view></view><view class="card-footer data-v-341940bb"><view class="time-info data-v-341940bb"><text class="time-label data-v-341940bb">诊断日期:</text><text class="time-value data-v-341940bb">{{card.i}}</text></view><view class="time-info data-v-341940bb"><text class="time-label data-v-341940bb">创建时间:</text><text class="time-value data-v-341940bb">{{card.j}}</text></view></view><view class="card-actions data-v-341940bb"><view class="card-action-btn primary data-v-341940bb" catchtap="{{card.k}}"><text class="card-action-icon data-v-341940bb">📈</text><text class="card-action-text data-v-341940bb">日常记录</text></view><view class="card-action-btn data-v-341940bb" catchtap="{{card.l}}"><text class="card-action-icon data-v-341940bb">📝</text><text class="card-action-text data-v-341940bb">查看 / 编辑</text></view></view></view><view wx:if="{{b}}" class="empty-state data-v-341940bb"><uni-icons wx:if="{{c}}" class="data-v-341940bb" u-i="341940bb-0" bind:__l="__l" u-p="{{c}}"></uni-icons><text class="empty-text data-v-341940bb">暂无就诊卡</text><view class="add-card-btn data-v-341940bb" bindtap="{{d}}">新建就诊卡</view></view><view wx:if="{{e}}" class="loading-state data-v-341940bb"><text class="loading-text data-v-341940bb">加载中...</text></view></view></view>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
.card-container.data-v-341940bb{min-height:100vh;background:linear-gradient(180deg,#e8f0fa,#fff);padding-bottom:56rpx}.header.data-v-341940bb{background:#fff;padding:48rpx 40rpx;box-shadow:0 2rpx 12rpx rgba(0,0,0,.06)}.header .header-title.data-v-341940bb{font-size:48rpx;font-weight:700;color:#1890ff}.card-list.data-v-341940bb{padding:40rpx}.card-item.data-v-341940bb{background:#fff;border-radius:28rpx;padding:44rpx;margin-bottom:36rpx;box-shadow:0 4rpx 20rpx rgba(0,0,0,.08);transition:all .3s;min-height:200rpx}.card-item.data-v-341940bb:active{transform:scale(.98);box-shadow:0 2rpx 12rpx rgba(0,0,0,.1)}.card-header.data-v-341940bb{display:flex;justify-content:space-between;align-items:center;margin-bottom:32rpx;padding-bottom:28rpx;border-bottom:4rpx solid #e8eaed}.card-id .label.data-v-341940bb{font-size:34rpx;color:#555}.card-id .value.data-v-341940bb{font-size:40rpx;font-weight:700;color:#1a1a1a}.status-badge.data-v-341940bb{padding:12rpx 28rpx;border-radius:28rpx;font-size:32rpx;font-weight:500}.status-badge.status-confirmed.data-v-341940bb{background:#f6ffed;color:#204e2b;border:4rpx solid #204e2b}.status-badge.status-pending.data-v-341940bb{background:#fff7e6;color:#fa8c16;border:4rpx solid #ffd591}.card-info.data-v-341940bb{margin-bottom:28rpx}.info-row.data-v-341940bb{display:flex;align-items:center;margin-bottom:24rpx}.info-row .info-label.data-v-341940bb{font-size:34rpx;color:#555;min-width:200rpx}.info-row .info-value.data-v-341940bb{font-size:36rpx;color:#1a1a1a;font-weight:500}.card-footer.data-v-341940bb{padding-top:28rpx;border-top:4rpx solid #e8eaed}.card-actions.data-v-341940bb{display:flex;gap:20rpx;margin-top:28rpx;padding-top:28rpx;border-top:4rpx solid #e8eaed}.card-action-btn.data-v-341940bb{flex:1;display:flex;align-items:center;justify-content:center;gap:12rpx;padding:24rpx 12rpx;background:#f6faff;border-radius:20rpx;border:2rpx solid #d6e6fb;min-height:80rpx;transition:all .2s}.card-action-btn.data-v-341940bb:active{transform:scale(.98);opacity:.85}.card-action-btn.primary.data-v-341940bb{background:linear-gradient(135deg,#1890ff,#0ea5a4);border-color:transparent}.card-action-btn.primary .card-action-icon.data-v-341940bb,.card-action-btn.primary .card-action-text.data-v-341940bb{color:#fff}.card-action-icon.data-v-341940bb{font-size:36rpx;color:#1890ff}.card-action-text.data-v-341940bb{font-size:30rpx;color:#1890ff;font-weight:500}.time-info.data-v-341940bb{display:flex;align-items:center;margin-bottom:20rpx}.time-info .time-label.data-v-341940bb{font-size:30rpx;color:#555;min-width:180rpx}.time-info .time-value.data-v-341940bb{font-size:32rpx;color:#555}.view-count.data-v-341940bb{display:flex;align-items:center;margin-top:12rpx}.view-count .count-icon.data-v-341940bb{font-size:34rpx;margin-right:12rpx}.view-count .count-text.data-v-341940bb{font-size:30rpx;color:#555}.empty-state.data-v-341940bb{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:160rpx 0}.empty-state .empty-icon.data-v-341940bb{font-size:140rpx;margin-bottom:40rpx}.empty-state .empty-text.data-v-341940bb{font-size:40rpx;color:#555;margin-bottom:56rpx}.add-card-btn.data-v-341940bb{padding:36rpx 88rpx;background:#1890ff;color:#fff;font-size:40rpx;font-weight:500;border-radius:56rpx;min-height:100rpx}.loading-state.data-v-341940bb{display:flex;justify-content:center;padding:120rpx 0}.loading-state .loading-text.data-v-341940bb{font-size:36rpx;color:#555}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"use strict";const e=require("../../common/vendor.js"),t={__name:"contact",setup(t){const{proxy:a}=e.getCurrentInstance(),n=e.ref({}),o=e.ref(!1),r=e.ref("");e.onMounted(()=>{c()});const c=async()=>{o.value=!0;try{const t=getCurrentPages(),o=t[t.length-1];r.value=o.options.type;const c=await a.apiUrl({url:"/api/index/policy",method:"GET",data:{type:r.value}});n.value=c.data,e.index.setNavigationBarTitle({title:c.data.title})}catch(t){console.error("加载就诊卡列表失败:",t)}finally{o.value=!1}};return(t,a)=>({a:e.t(n.value.title),b:n.value.content})}},a=e._export_sfc(t,[["__scopeId","data-v-5e5077b6"]]);wx.createPage(a);
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"navigationBarTitleText": "服务",
|
||||||
|
"usingComponents": {}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<view class="card-container data-v-5e5077b6"><view class="header data-v-5e5077b6"><text class="header-title data-v-5e5077b6">{{a}}</text></view><view class="card-list data-v-5e5077b6"><rich-text class="data-v-5e5077b6" nodes="{{b}}"/></view></view>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
.card-container.data-v-5e5077b6{min-height:100vh;background:linear-gradient(180deg,#f0f7ff,#fff);padding-bottom:40rpx}.header.data-v-5e5077b6{background:#fff;padding:40rpx 30rpx;box-shadow:0 2rpx 8rpx rgba(0,0,0,.05)}.header .header-title.data-v-5e5077b6{font-size:40rpx;font-weight:700;color:#1890ff}.card-list.data-v-5e5077b6{padding:30rpx}.card-item.data-v-5e5077b6{background:#fff;border-radius:20rpx;padding:30rpx;margin-bottom:24rpx;box-shadow:0 4rpx 12rpx rgba(0,0,0,.08);transition:all .3s}.card-item.data-v-5e5077b6:active{transform:scale(.98);box-shadow:0 2rpx 8rpx rgba(0,0,0,.1)}.card-header.data-v-5e5077b6{display:flex;justify-content:space-between;align-items:center;margin-bottom:24rpx;padding-bottom:20rpx;border-bottom:2rpx solid #f0f0f0}.card-id .label.data-v-5e5077b6{font-size:28rpx;color:#666}.card-id .value.data-v-5e5077b6{font-size:32rpx;font-weight:700;color:#333}.status-badge.data-v-5e5077b6{padding:8rpx 20rpx;border-radius:20rpx;font-size:24rpx;font-weight:500}.status-badge.status-confirmed.data-v-5e5077b6{background:#f6ffed;color:#52c41a;border:2rpx solid #b7eb8f}.status-badge.status-pending.data-v-5e5077b6{background:#fff7e6;color:#fa8c16;border:2rpx solid #ffd591}.card-info.data-v-5e5077b6{margin-bottom:20rpx}.info-row.data-v-5e5077b6{display:flex;align-items:center;margin-bottom:16rpx}.info-row .info-label.data-v-5e5077b6{font-size:28rpx;color:#666;min-width:160rpx}.info-row .info-value.data-v-5e5077b6{font-size:30rpx;color:#333;font-weight:500}.card-footer.data-v-5e5077b6{padding-top:20rpx;border-top:2rpx solid #f0f0f0}.time-info.data-v-5e5077b6{display:flex;align-items:center;margin-bottom:12rpx}.time-info .time-label.data-v-5e5077b6{font-size:24rpx;color:#999;min-width:140rpx}.time-info .time-value.data-v-5e5077b6{font-size:26rpx;color:#666}.view-count.data-v-5e5077b6{display:flex;align-items:center;margin-top:8rpx}.view-count .count-icon.data-v-5e5077b6{font-size:28rpx;margin-right:8rpx}.view-count .count-text.data-v-5e5077b6{font-size:24rpx;color:#999}.empty-state.data-v-5e5077b6{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:120rpx 0}.empty-state .empty-icon.data-v-5e5077b6{font-size:120rpx;margin-bottom:30rpx}.empty-state .empty-text.data-v-5e5077b6{font-size:32rpx;color:#999}.loading-state.data-v-5e5077b6{display:flex;justify-content:center;padding:80rpx 0}.loading-state .loading-text.data-v-5e5077b6{font-size:28rpx;color:#999}
|
||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"navigationBarTitleText": "修改就诊卡",
|
||||||
|
"usingComponents": {
|
||||||
|
"uni-icons": "../../uni_modules/uni-icons/components/uni-icons/uni-icons"
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
|||||||
|
"use strict";const e=require("../../common/vendor.js"),a={name:"profileB",components:{DevTrainingEntry:()=>"../../components/dev-training-entry/index.js",TabBarDock:()=>"../../components/app-tab-bar/tab-bar-dock.js"},data:()=>({userInfo:{avatar:"",nickname:"",userId:"",balance:"0",points:"0",coupons:"5"},orderTypes:[{name:"待付款",image:"/static/images/my/payment.png",path:"/pages/order/list?type=1",count:2},{name:"待发货",image:"/static/images/my/delivery.png",path:"/pages/order/list?type=2",count:1},{name:"待收货",image:"/static/images/my/shipping.png",path:"/pages/order/list?type=3",count:0},{name:"待评价",image:"/static/images/my/review.png",path:"/pages/order/list?type=4",count:3},{name:"退换/售后",image:"/static/images/my/after-sale.png",path:"/pages/order/list?type=5",count:0}],allServices:[{name:"就诊卡",icon:"folder-add",path:"/pages/Card/Card",class:"bg-cyan"},{name:"设置",icon:"gear",path:"/pages/user/userinfo",class:"bg-gray"}]}),onShow(){this.userInfo=e.index.getStorageSync("userData")},methods:{navigateTo(a){e.index.navigateTo({url:a})},showQRCode(){}}};if(!Array){(e.resolveComponent("uni-icons")+e.resolveComponent("dev-training-entry")+e.resolveComponent("TabBarDock"))()}Math;const n=e._export_sfc(a,[["render",function(a,n,t,o,s,r){return{a:s.userInfo.avatar,b:e.t(s.userInfo.nickname||"未登录"),c:e.t(s.userInfo.balance||0),d:e.t(s.userInfo.points||0),e:e.t(s.userInfo.coupons||0),f:e.o(e=>r.navigateTo("/pages/user/userinfo")),g:e.f(s.allServices,(a,n,t)=>e.e({a:"285a6397-0-"+t,b:e.p({type:a.icon,size:"28",color:"#fff"}),c:e.n(a.class),d:e.t(a.name),e:a.desc},a.desc?{f:e.t(a.desc)}:{},{g:"285a6397-1-"+t,h:n,i:e.o(e=>r.navigateTo(a.path),n)})),h:e.p({type:"right",size:"20",color:"#666"}),i:e.p({bottom:360}),j:e.p({active:1})}}],["__scopeId","data-v-285a6397"]]);a.__runtimeHooks=2,wx.createPage(n);
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"navigationBarTitleText": "我的",
|
||||||
|
"backgroundColor": "#e8eaed",
|
||||||
|
"backgroundColorTop": "#e8eaed",
|
||||||
|
"usingComponents": {
|
||||||
|
"dev-training-entry": "../../components/dev-training-entry/index",
|
||||||
|
"tab-bar-dock": "../../components/app-tab-bar/tab-bar-dock",
|
||||||
|
"uni-icons": "../../uni_modules/uni-icons/components/uni-icons/uni-icons"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<view class="mine-page data-v-285a6397"><view class="user-card data-v-285a6397" bindtap="{{f}}"><view class="user-header data-v-285a6397"><image class="avatar data-v-285a6397" src="{{a}}" mode="aspectFill"/><view class="user-info data-v-285a6397"><text class="nickname data-v-285a6397">{{b}}</text></view></view><view class="stat-row data-v-285a6397"><view class="stat-item data-v-285a6397"><text class="value data-v-285a6397">{{c}}</text><text class="label data-v-285a6397">钱包(元)</text></view><view class="stat-item data-v-285a6397"><text class="value data-v-285a6397">{{d}}</text><text class="label data-v-285a6397">积分</text></view><view class="stat-item data-v-285a6397"><text class="value data-v-285a6397">{{e}}</text><text class="label data-v-285a6397">优惠券</text></view></view></view><view class="service-list data-v-285a6397"><view wx:for="{{g}}" wx:for-item="item" wx:key="h" class="service-item data-v-285a6397" bindtap="{{item.i}}"><view class="left data-v-285a6397"><view class="{{['service-icon', 'data-v-285a6397', item.c]}}"><uni-icons wx:if="{{item.b}}" class="data-v-285a6397" u-i="{{item.a}}" bind:__l="__l" u-p="{{item.b}}"></uni-icons></view><text class="service-name data-v-285a6397">{{item.d}}</text></view><view class="right data-v-285a6397"><text wx:if="{{item.e}}" class="desc data-v-285a6397">{{item.f}}</text><uni-icons wx:if="{{h}}" class="data-v-285a6397" u-i="{{item.g}}" bind:__l="__l" u-p="{{h}}"></uni-icons></view></view></view><dev-training-entry wx:if="{{i}}" class="data-v-285a6397" u-i="285a6397-2" bind:__l="__l" u-p="{{i}}"/><tab-bar-dock wx:if="{{j}}" class="data-v-285a6397" u-i="285a6397-3" bind:__l="__l" u-p="{{j}}"/></view>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
.mine-page.data-v-285a6397{min-height:100vh;background:#e8eaed;padding:40rpx;padding-bottom:calc(240rpx + env(safe-area-inset-bottom))}.user-card.data-v-285a6397{background:#fff;border-radius:28rpx;padding:48rpx;margin-bottom:40rpx;box-shadow:0 4rpx 20rpx rgba(0,0,0,.06)}.user-card .user-header.data-v-285a6397{display:flex;align-items:center;margin-bottom:48rpx}.user-card .user-header .avatar.data-v-285a6397{width:160rpx;height:160rpx;border-radius:80rpx;margin-right:36rpx;border:4rpx solid #e0e0e0}.user-card .user-header .user-info.data-v-285a6397{flex:1}.user-card .user-header .user-info .nickname.data-v-285a6397{font-size:48rpx;font-weight:600;color:#1a1a1a;margin-bottom:16rpx;display:block;letter-spacing:1rpx}.user-card .user-header .user-info .vip-tag.data-v-285a6397{background:linear-gradient(90deg,gold,orange);color:#fff;font-size:28rpx;padding:6rpx 20rpx;border-radius:24rpx;display:inline-block}.user-card .user-header .qr-code.data-v-285a6397{width:88rpx;height:88rpx;border-radius:44rpx;background:#f5f6fa;display:flex;align-items:center;justify-content:center}.user-card .stat-row.data-v-285a6397{display:flex;padding-top:40rpx;border-top:4rpx solid #e8eaed}.user-card .stat-row .stat-item.data-v-285a6397{flex:1;text-align:center;min-height:120rpx}.user-card .stat-row .stat-item .value.data-v-285a6397{font-size:52rpx;font-weight:600;color:#1a1a1a;margin-bottom:12rpx;display:block}.user-card .stat-row .stat-item .label.data-v-285a6397{font-size:32rpx;color:#555;font-weight:500}.order-card.data-v-285a6397{background:#fff;border-radius:24rpx;padding:40rpx;margin-bottom:30rpx;box-shadow:0 4rpx 16rpx rgba(0,0,0,.04)}.order-card .card-header.data-v-285a6397{display:flex;justify-content:space-between;align-items:center;margin-bottom:40rpx}.order-card .card-header .title.data-v-285a6397{font-size:32rpx;font-weight:600;color:#333}.order-card .card-header .more.data-v-285a6397{display:flex;align-items:center}.order-card .card-header .more text.data-v-285a6397{font-size:26rpx;color:#999;margin-right:8rpx}.order-card .order-list.data-v-285a6397{display:flex;justify-content:space-between}.order-card .order-list .order-item.data-v-285a6397{flex:1;text-align:center}.order-card .order-list .order-item .icon-wrapper.data-v-285a6397{width:100rpx;height:100rpx;margin:0 auto;position:relative;display:flex;align-items:center;justify-content:center}.order-card .order-list .order-item .icon-wrapper .count-badge.data-v-285a6397{position:absolute;top:-10rpx;right:-10rpx;background:#ff4d4f;color:#fff;font-size:20rpx;min-width:32rpx;height:32rpx;line-height:32rpx;text-align:center;border-radius:16rpx;padding:0 8rpx}.order-card .order-list .order-item .icon-wrapper .order-icon.data-v-285a6397{width:56rpx;height:56rpx}.order-card .order-list .order-item .order-name.data-v-285a6397{font-size:26rpx;color:#666}.quick-actions.data-v-285a6397{display:grid;grid-template-columns:repeat(4,1fr);gap:30rpx;margin-bottom:30rpx}.quick-actions .action-item.data-v-285a6397{background:#fff;border-radius:24rpx;padding:30rpx 20rpx;text-align:center;box-shadow:0 4rpx 16rpx rgba(0,0,0,.04)}.quick-actions .action-item .action-icon.data-v-285a6397{width:80rpx;height:80rpx;border-radius:40rpx;margin:0 auto 16rpx;display:flex;align-items:center;justify-content:center}.quick-actions .action-item .action-name.data-v-285a6397{font-size:26rpx;color:#666}.service-list.data-v-285a6397{background:#fff;border-radius:28rpx;padding:20rpx 40rpx;box-shadow:0 4rpx 20rpx rgba(0,0,0,.06)}.service-list .service-item.data-v-285a6397{display:flex;align-items:center;justify-content:space-between;padding:44rpx 0;border-bottom:4rpx solid #e8eaed;min-height:120rpx}.service-list .service-item.data-v-285a6397:last-child{border-bottom:none}.service-list .service-item .left.data-v-285a6397{display:flex;align-items:center}.service-list .service-item .left .service-icon.data-v-285a6397{width:88rpx;height:88rpx;border-radius:44rpx;margin-right:32rpx;display:flex;align-items:center;justify-content:center}.service-list .service-item .left .service-name.data-v-285a6397{font-size:38rpx;color:#1a1a1a;font-weight:500}.service-list .service-item .right.data-v-285a6397{display:flex;align-items:center}.service-list .service-item .right .desc.data-v-285a6397{font-size:30rpx;color:#555;margin-right:20rpx}.bg-red.data-v-285a6397{background:#ff4d4f}.bg-blue.data-v-285a6397{background:#1890ff}.bg-green.data-v-285a6397{background:#52c41a}.bg-purple.data-v-285a6397{background:#722ed1}.bg-orange.data-v-285a6397{background:#fa8c16}.bg-cyan.data-v-285a6397{background:#13c2c2}.bg-pink.data-v-285a6397{background:#eb2f96}.bg-gray.data-v-285a6397{background:#666}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"use strict";const e=require("../../common/vendor.js");if(!Array){e.resolveComponent("uni-icons")()}Math;const a={__name:"userinfo",setup(a){const{proxy:n}=e.getCurrentInstance(),t=e.getCurrentInstance().appContext.config.globalProperties.$url,o=e.ref(!1),i=e.ref(!1),s=e.ref({avatar:"",patient_name:"",phone:"",nickname:"",sex:1,age:""});e.onMounted(()=>{l()});const l=async()=>{o.value=!0;try{const a=await n.apiUrl({url:"/api/user/info",method:"GET",data:{}});if(1===a.code){const e=a.data;s.value={avatar:e.avatar||"",patient_name:e.patient_name||e.nickname||"",phone:e.phone||"",nickname:e.nickname||"",sex:"男"==e.sex?1:2,age:e.age||""}}else e.index.showToast({title:a.msg||"加载失败",icon:"none"})}catch(a){console.error("加载用户信息失败:",a),e.index.showToast({title:"加载失败",icon:"none"})}finally{o.value=!1}},c=()=>{e.index.chooseImage({count:1,sizeType:["compressed"],sourceType:["album","camera"],success:async a=>{const n=a.tempFilePaths[0];try{e.index.showLoading({title:"上传中..."});const a=e.index.getStorageSync("token"),o=await new Promise((o,i)=>{e.index.uploadFile({url:t+"/api/upload/image",filePath:n,name:"file",header:{token:a,"content-type":"multipart/form-data"},success:e=>{const a=JSON.parse(e.data);o(a)},fail:e=>{i(e)}})});e.index.hideLoading(),1===o.code?(s.value.avatar=o.data.uri||o.data,e.index.showToast({title:"头像上传成功",icon:"success"})):e.index.showToast({title:o.msg||"上传失败",icon:"none"})}catch(o){e.index.hideLoading(),console.error("上传头像失败:",o),e.index.showToast({title:"上传失败",icon:"none"})}},fail:a=>{console.error("选择图片失败:",a),e.index.showToast({title:"选择图片失败",icon:"none"})}})},r=async()=>{if(s.value.patient_name)if(s.value.phone&&((a=s.value.phone)&&!/^1[3-9]\d{9}$/.test(a)))e.index.showToast({title:"手机号格式不正确",icon:"none"});else{var a;i.value=!0;try{const a=await n.apiUrl({url:"/api/user/setInfo",method:"POST",data:{avatar:s.value.avatar,phone:s.value.phone,nickname:s.value.nickname,sex:s.value.sex,age:s.value.age,patient_name:s.value.patient_name}});if(1===a.code){const a=e.index.getStorageSync("userData"),n={...a,avatar:s.value.avatar,phone:s.value.phone,nickname:s.value.nickname,sex:s.value.sex,age:s.value.age,patient_name:s.value.patient_name,diagnosis:{...a.diagnosis,patient_name:s.value.patient_name}};e.index.setStorageSync("userData",n),e.index.showToast({title:"保存成功",icon:"success"}),setTimeout(()=>{e.index.navigateBack()},1500)}else e.index.showToast({title:a.msg||"保存失败",icon:"none"})}catch(t){console.error("保存失败:",t),e.index.showToast({title:"保存失败",icon:"none"})}finally{i.value=!1}}else e.index.showToast({title:"姓名不能为空",icon:"none"})};return(a,n)=>e.e({a:o.value},o.value?{}:{b:s.value.avatar,c:e.p({type:"upload-filled",size:"24",color:"#FFA500"}),d:e.o(c),e:s.value.nickname,f:e.o(e=>s.value.nickname=e.detail.value),g:1===s.value.sex?1:"",h:e.o(e=>s.value.sex=1),i:2===s.value.sex?1:"",j:e.o(e=>s.value.sex=2),k:s.value.age,l:e.o(e.m(e=>s.value.age=e.detail.value,{number:!0})),m:e.t(i.value?"保存中...":"保存修改"),n:e.o(r),o:i.value})}},n=e._export_sfc(a,[["__scopeId","data-v-c609dd4e"]]);wx.createPage(n);
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"navigationBarTitleText": "个人资料",
|
||||||
|
"usingComponents": {
|
||||||
|
"uni-icons": "../../uni_modules/uni-icons/components/uni-icons/uni-icons"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<view class="userinfo-container data-v-c609dd4e"><view wx:if="{{a}}" class="loading-state data-v-c609dd4e"><text class="data-v-c609dd4e">加载中...</text></view><view wx:else class="form-wrapper data-v-c609dd4e"><view class="form-section data-v-c609dd4e"><view class="form-group data-v-c609dd4e"><text class="form-label data-v-c609dd4e">头像</text><view class="avatar-container data-v-c609dd4e"><image src="{{b}}" class="avatar-image data-v-c609dd4e" mode="aspectFill"/><view class="avatar-upload data-v-c609dd4e" bindtap="{{d}}"><uni-icons wx:if="{{c}}" class="upload-icon data-v-c609dd4e" u-i="c609dd4e-0" bind:__l="__l" u-p="{{c}}"></uni-icons><text class="upload-text data-v-c609dd4e">更换头像</text></view></view></view></view><view class="form-section data-v-c609dd4e"><text class="section-title data-v-c609dd4e">基本信息</text><view class="form-group data-v-c609dd4e"><text class="form-label data-v-c609dd4e">昵称</text><input class="form-input data-v-c609dd4e" placeholder="请输入昵称" maxlength="50" value="{{e}}" bindinput="{{f}}"/></view><view class="form-group data-v-c609dd4e"><text class="form-label data-v-c609dd4e">性别</text><view class="sex-group data-v-c609dd4e"><view class="{{['sex-btn', 'data-v-c609dd4e', g && 'active']}}" bindtap="{{h}}"> 男 </view><view class="{{['sex-btn', 'data-v-c609dd4e', i && 'active']}}" bindtap="{{j}}"> 女 </view></view></view><view class="form-group data-v-c609dd4e"><text class="form-label data-v-c609dd4e">年龄</text><input class="form-input data-v-c609dd4e" type="number" placeholder="请输入年龄" min="0" max="150" value="{{k}}" bindinput="{{l}}"/></view></view><view class="button-group data-v-c609dd4e"><button class="btn btn-submit data-v-c609dd4e" bindtap="{{n}}" disabled="{{o}}">{{m}}</button></view></view></view>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
.userinfo-container.data-v-c609dd4e{min-height:100vh;background:#e8eaed}.header.data-v-c609dd4e{display:flex;justify-content:space-between;align-items:center;background:#fff;padding:24rpx 40rpx;box-shadow:0 2rpx 12rpx rgba(0,0,0,.06)}.header-back.data-v-c609dd4e{font-size:48rpx;color:orange;width:88rpx;height:88rpx;display:flex;align-items:center;justify-content:center}.header-title.data-v-c609dd4e{font-size:44rpx;font-weight:700;color:#1a1a1a;flex:1;text-align:center}.header-placeholder.data-v-c609dd4e{width:88rpx}.loading-state.data-v-c609dd4e{display:flex;justify-content:center;align-items:center;height:100vh;font-size:36rpx;color:#555}.form-wrapper.data-v-c609dd4e{padding:40rpx}.form-section.data-v-c609dd4e{background:#fff;border-radius:24rpx;padding:40rpx;margin-bottom:36rpx;box-shadow:0 4rpx 16rpx rgba(0,0,0,.06)}.section-title.data-v-c609dd4e{font-size:40rpx;font-weight:700;color:orange;margin-bottom:32rpx;display:block;padding-bottom:24rpx;border-bottom:4rpx solid #f0f0f0}.form-group.data-v-c609dd4e{margin-bottom:36rpx}.form-group.data-v-c609dd4e:last-child{margin-bottom:0}.form-label.data-v-c609dd4e{display:block;font-size:36rpx;color:#1a1a1a;font-weight:500;margin-bottom:20rpx;line-height:1.6}.form-input.data-v-c609dd4e{width:100%;padding:28rpx;border:4rpx solid #e0e0e0;border-radius:16rpx;font-size:36rpx;line-height:1.6;color:#1a1a1a;background:#fff;box-sizing:border-box;display:block;height:auto;min-height:96rpx}.form-input.data-v-c609dd4e::-moz-placeholder{color:#999;font-size:34rpx}.form-input.data-v-c609dd4e::placeholder{color:#999;font-size:34rpx}.avatar-container.data-v-c609dd4e{display:flex;align-items:center;gap:32rpx}.avatar-image.data-v-c609dd4e{width:160rpx;height:160rpx;border-radius:80rpx;background:#f0f0f0;border:4rpx solid #e0e0e0}.avatar-upload.data-v-c609dd4e{flex:1;padding:36rpx;border:4rpx dashed #FFA500;border-radius:20rpx;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:16rpx;min-height:120rpx}.upload-icon.data-v-c609dd4e{font-size:56rpx}.upload-text.data-v-c609dd4e{font-size:34rpx;color:orange;font-weight:500}.sex-group.data-v-c609dd4e{display:flex;gap:28rpx}.sex-btn.data-v-c609dd4e{flex:1;padding:32rpx;border:4rpx solid #e0e0e0;border-radius:16rpx;text-align:center;font-size:38rpx;line-height:1.5;color:#555;background:#fff;transition:all .3s;display:flex;align-items:center;justify-content:center;min-height:96rpx}.sex-btn.active.data-v-c609dd4e{border-color:orange;background:#fff7e6;color:orange;font-weight:700}.button-group.data-v-c609dd4e{display:flex;gap:28rpx;margin-top:56rpx;margin-bottom:56rpx}.btn.data-v-c609dd4e{flex:1;padding:36rpx;border-radius:20rpx;font-size:40rpx;font-weight:700;border:none;cursor:pointer;transition:all .3s;min-height:100rpx}.btn.data-v-c609dd4e:active{transform:scale(.98)}.btn.data-v-c609dd4e:disabled{opacity:.6;cursor:not-allowed}.btn-cancel.data-v-c609dd4e{background:#f0f0f0;color:#555}.btn-submit.data-v-c609dd4e{background:orange;color:#fff}.btn-submit.data-v-c609dd4e:active:not(:disabled){background:#e69500}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"description": "项目配置文件。",
|
||||||
|
"packOptions": {
|
||||||
|
"ignore": []
|
||||||
|
},
|
||||||
|
"setting": {
|
||||||
|
"urlCheck": false,
|
||||||
|
"es6": true,
|
||||||
|
"postcss": false,
|
||||||
|
"minified": true,
|
||||||
|
"newFeature": true,
|
||||||
|
"bigPackageSizeSupport": true
|
||||||
|
},
|
||||||
|
"compileType": "miniprogram",
|
||||||
|
"libVersion": "",
|
||||||
|
"appid": "wx79b9a0bfbfe7cbcd",
|
||||||
|
"projectname": "tongji-uniapp",
|
||||||
|
"condition": {
|
||||||
|
"search": {
|
||||||
|
"current": -1,
|
||||||
|
"list": []
|
||||||
|
},
|
||||||
|
"conversation": {
|
||||||
|
"current": -1,
|
||||||
|
"list": []
|
||||||
|
},
|
||||||
|
"game": {
|
||||||
|
"current": -1,
|
||||||
|
"list": []
|
||||||
|
},
|
||||||
|
"miniprogram": {
|
||||||
|
"current": -1,
|
||||||
|
"list": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"use strict";const t=require("../../common/vendor.js");Math||e();const e=()=>"./TongjiIcon.js",a={__name:"CelebrateBurst",props:{show:{type:Boolean,default:!1},title:{type:String,default:""},subtitle:{type:String,default:""}},setup(e){const a=e,o=t.ref([]),s=t.ref(!1),l=["#204E2B","#386641","#AFE2B3","#FBBF24","#727970","#DC2626"];return t.watch(()=>a.show,t=>{t?(o.value=function(){const t=[];for(let e=0;e<18;e++){const a=8+84*Math.random(),o=.35*Math.random(),s=l[e%l.length],n=10+Math.floor(14*Math.random());t.push({id:`${Date.now()}_${e}`,style:{left:`${a}%`,width:`${n}rpx`,height:`${n}rpx`,background:s,animationDelay:`${o}s`}})}return t}(),s.value=!1,setTimeout(()=>{s.value=!0},30)):(o.value=[],s.value=!1)}),(a,l)=>t.e({a:e.show},e.show?t.e({b:t.f(o.value,(e,a,o)=>({a:e.id,b:t.s(e.style)})),c:e.title},e.title?t.e({d:t.p({name:"sparkles",size:"lg",color:"#204E2B"}),e:t.t(e.title),f:e.subtitle},e.subtitle?{g:t.t(e.subtitle)}:{},{h:s.value?1:""}):{},{i:t.o(()=>{})}):{})}},o=t._export_sfc(a,[["__scopeId","data-v-333a1623"]]);wx.createComponent(o);
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"component": true,
|
||||||
|
"usingComponents": {
|
||||||
|
"tongji-icon": "./TongjiIcon"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<view wx:if="{{a}}" class="celebrate-root data-v-333a1623" catchtouchmove="{{i}}"><view wx:for="{{b}}" wx:for-item="p" wx:key="a" class="celebrate-particle data-v-333a1623" style="{{p.b}}"/><view wx:if="{{c}}" class="{{['celebrate-card', 'data-v-333a1623', h && 'pop']}}"><view class="celebrate-card-glow data-v-333a1623"/><tongji-icon wx:if="{{d}}" class="data-v-333a1623" u-i="333a1623-0" bind:__l="__l" u-p="{{d}}"/><text class="celebrate-card-title data-v-333a1623">{{e}}</text><text wx:if="{{f}}" class="celebrate-card-sub data-v-333a1623">{{g}}</text></view></view>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
.celebrate-root.data-v-333a1623{position:fixed;top:0;right:0;bottom:0;left:0;z-index:9999;pointer-events:none;overflow:hidden}.celebrate-particle.data-v-333a1623{position:absolute;top:-20rpx;border-radius:4rpx;opacity:.9;animation:celebrate-fall-333a1623 1.6s ease-in forwards}@keyframes celebrate-fall-333a1623{0%{transform:translateY(0) rotate(0) scale(1);opacity:1}to{transform:translateY(110vh) rotate(540deg) scale(.4);opacity:0}}.celebrate-card.data-v-333a1623{position:absolute;left:50%;top:38%;transform:translate(-50%,-50%) scale(.82);width:78%;max-width:560rpx;padding:36rpx 32rpx 32rpx;background:rgba(255,255,255,.96);border-radius:28rpx;box-shadow:0 20rpx 60rpx rgba(8,145,178,.22);border:2rpx solid rgba(8,145,178,.12);display:flex;flex-direction:column;align-items:center;text-align:center;opacity:0;transition:transform .35s cubic-bezier(.34,1.56,.64,1),opacity .25s ease}.celebrate-card.pop.data-v-333a1623{opacity:1;transform:translate(-50%,-50%) scale(1)}.celebrate-card-glow.data-v-333a1623{position:absolute;top:-20rpx;right:-20rpx;bottom:-20rpx;left:-20rpx;border-radius:36rpx;background:radial-gradient(circle,rgba(34,211,238,.2),transparent 70%);pointer-events:none}.celebrate-card-title.data-v-333a1623{position:relative;z-index:1;margin-top:16rpx;font-size:36rpx;font-weight:800;color:#1b1c1a}.celebrate-card-sub.data-v-333a1623{position:relative;z-index:1;margin-top:10rpx;font-size:26rpx;color:#475569;line-height:1.45}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"use strict";const e=require("../../common/vendor.js"),l={__name:"SugarTreeGraphic",props:{level:{type:Number,default:0},size:{type:String,default:"md"},watering:{type:Boolean,default:!1}},setup(l){const t=e.TREE_MAX_LEVEL,c=e.ref(!0),a=l,r=e.computed(()=>Math.min(t,Math.max(0,Number(a.level)||0))),i=e.computed(()=>{const e=r.value;return e<=0?0:e<=2?1:e<=4?2:e<=6?3:e<=8?4:5}),n=e.computed(()=>(e.TREE_LEVELS[r.value]||e.TREE_LEVELS[0]).emoji);const o=e.computed(()=>e.svgToDataUrl(function(e){const l='\n <ellipse cx="24" cy="50" rx="15" ry="3" fill="#204e2b" opacity="0.12"/>\n <path d="M11 46h26c1.2 0 2 1 2 2.2v3.8c0 1-.8 1.8-1.8 1.8H10.8c-1 0-1.8-.8-1.8-1.8v-3.8c0-1.2.8-2.2 2-2.2z" fill="#E7E5E4"/>\n <path d="M12.5 46h23c.8 0 1.5.7 1.5 1.5v2.2c0 .6-.5 1.1-1.1 1.1H12.1c-.6 0-1.1-.5-1.1-1.1v-2.2c0-.8.7-1.5 1.5-1.5z" fill="#D6D3D1"/>\n <ellipse cx="24" cy="46.5" rx="9" ry="1.6" fill="#A8A29E" opacity="0.35"/>\n ',t='<ellipse cx="24" cy="44.5" rx="8" ry="2.2" fill="#386641" opacity="0.18"/>';if(e<=0)return`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 56" fill="none">\n ${l}${t}\n <circle cx="24" cy="41.5" r="2.2" fill="#a8a29e" opacity="0.7"/>\n <path d="M24 41.5v3.5" stroke="#78716c" stroke-width="1" stroke-linecap="round"/>\n </svg>`;const c=e>=6?16:e>=3?14:10,a=46-c,r=`<rect x="22.2" y="${a}" width="3.6" height="${c}" rx="1.8" fill="#78716C"/>\n <rect x="22.6" y="${a+1}" width="2.8" height="${c-1}" rx="1.4" fill="#A8A29E" opacity="0.35"/>`,i=(e,l,t,c,a=1)=>`<circle cx="${e}" cy="${l}" r="${t}" fill="${c}" opacity="${a}"/>\n <circle cx="${e-.25*t}" cy="${l-.2*t}" r="${.35*t}" fill="#eef6ef" opacity="0.55"/>`,n=e>=7?i(17,18,3,"#fda4af",.95)+i(31,17,2.8,"#f9a8d4",.9)+i(24,13,3.2,"#fb7185",.95)+'<circle cx="24" cy="12" r="1.3" fill="#fef3c7"/>':"",o=e>=9?i(12,22,6,"#34d399",.9)+i(36,22,6,"#34d399",.9)+i(24,10,7,"#386641",.95)+n:n;let u="";return u=1===e?i(24,38,4,"#afe2b3")+'<path d="M24 38v-5" stroke="#386641" stroke-width="1.2" stroke-linecap="round"/>':2===e?i(24,32,5.5,"#34d399")+i(20,34,3.5,"#6ee7b7",.9):e<=4?i(24,28,7,"#34d399")+i(17,30,5,"#6ee7b7",.9)+i(31,30,5,"#6ee7b7",.9):e<=6?i(24,24,9,"#22c55e")+i(14,26,7,"#4ade80",.92)+i(34,26,7,"#4ade80",.92)+i(24,16,6,"#386641",.88):i(24,20,10,"#204e2b")+i(13,24,8,"#34d399",.95)+i(35,24,8,"#34d399",.95)+o,`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 56" fill="none">${l}${t}${r}${u}</svg>`}(r.value)));return(a,u)=>e.e({a:r.value>=7},(r.value,{}),{b:!c.value},c.value?{e:e.t(n.value)}:{c:o.value,d:e.o(e=>c.value=!0)},{f:r.value>=7},(r.value,{}),{g:r.value>=8},(r.value,{}),{h:r.value>=e.unref(t)},(r.value,e.unref(t),{}),{i:e.n(`lv-${r.value}`),j:e.n(`tier-${i.value}`),k:e.n(`size-${l.size}`),l:e.n({watering:l.watering,"is-max":r.value>=e.unref(t)})})}},t=e._export_sfc(l,[["__scopeId","data-v-86839929"]]);wx.createComponent(t);
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"component": true,
|
||||||
|
"usingComponents": {}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<view class="{{['sugar-tree-graphic', 'data-v-86839929', i, j, k, l]}}"><view class="stg-glow data-v-86839929"/><view wx:if="{{a}}" class="stg-aura data-v-86839929"/><image wx:if="{{b}}" class="stg-image data-v-86839929" src="{{c}}" mode="aspectFit" binderror="{{d}}"/><text wx:else class="stg-emoji data-v-86839929">{{e}}</text><view wx:if="{{f}}" class="stg-sparkle stg-sparkle-a data-v-86839929"/><view wx:if="{{g}}" class="stg-sparkle stg-sparkle-b data-v-86839929"/><view wx:if="{{h}}" class="stg-sparkle stg-sparkle-c data-v-86839929"/></view>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
.sugar-tree-graphic.data-v-86839929{position:relative;display:flex;align-items:center;justify-content:center;box-sizing:border-box}.size-sm.data-v-86839929{width:64rpx;height:72rpx}.size-md.data-v-86839929{width:80rpx;height:88rpx}.size-lg.data-v-86839929{width:112rpx;height:124rpx}.stg-glow.data-v-86839929{position:absolute;top:6%;right:6%;bottom:6%;left:6%;border-radius:50%;background:radial-gradient(circle,rgba(32,78,43,.22) 0%,transparent 68%);pointer-events:none}.lv-0 .stg-glow.data-v-86839929{background:radial-gradient(circle,rgba(148,163,184,.25) 0%,transparent 70%)}.tier-4 .stg-glow.data-v-86839929,.tier-5 .stg-glow.data-v-86839929,.is-max .stg-glow.data-v-86839929{background:radial-gradient(circle,rgba(251,191,36,.3) 0%,rgba(32,78,43,.15) 55%,transparent 72%)}.stg-aura.data-v-86839929{position:absolute;top:-8%;right:-8%;bottom:-8%;left:-8%;border-radius:50%;border:2rpx solid rgba(253,224,71,.35);animation:stg-aura-pulse-86839929 2.4s ease-in-out infinite;pointer-events:none}@keyframes stg-aura-pulse-86839929{0%,to{transform:scale(.92);opacity:.5}50%{transform:scale(1.05);opacity:1}}.stg-image.data-v-86839929{position:relative;z-index:1;width:100%;height:100%;transition:transform .35s ease}.sugar-tree-graphic.watering .stg-image.data-v-86839929,.sugar-tree-graphic.watering .stg-emoji.data-v-86839929{animation:stg-water-bounce-86839929 .65s ease}.stg-emoji.data-v-86839929{position:relative;z-index:1;line-height:1;font-size:72rpx}.size-sm .stg-emoji.data-v-86839929{font-size:48rpx}.size-md .stg-emoji.data-v-86839929{font-size:60rpx}.size-lg .stg-emoji.data-v-86839929{font-size:88rpx}@keyframes stg-water-bounce-86839929{0%,to{transform:scale(1)}35%{transform:scale(1.1) translateY(-6rpx)}60%{transform:scale(.96) translateY(2rpx)}}.stg-sparkle.data-v-86839929{position:absolute;z-index:2;width:8rpx;height:8rpx;border-radius:50%;background:#fde68a;box-shadow:0 0 6rpx rgba(253,224,71,.8);pointer-events:none;animation:stg-sparkle-twinkle-86839929 1.8s ease-in-out infinite}.stg-sparkle-a.data-v-86839929{top:4%;right:16%}.stg-sparkle-b.data-v-86839929{top:12%;left:10%;width:6rpx;height:6rpx;animation-delay:.4s}.stg-sparkle-c.data-v-86839929{top:22%;right:28%;width:10rpx;height:10rpx;animation-delay:.8s}@keyframes stg-sparkle-twinkle-86839929{0%,to{opacity:.4;transform:scale(.8)}50%{opacity:1;transform:scale(1.2)}}
|
||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"component": true,
|
||||||
|
"usingComponents": {}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<view class="{{['tj-icon-wrap', 'data-v-d92a1022', f]}}"><image wx:if="{{a}}" class="{{['tj-icon', 'data-v-d92a1022', b, c]}}" src="{{d}}" mode="aspectFit" binderror="{{e}}"/></view>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
.tj-icon-wrap.data-v-d92a1022{display:inline-flex;align-items:center;justify-content:center;flex-shrink:0}.tj-icon-wrap--sm.data-v-d92a1022{width:32rpx;height:32rpx}.tj-icon-wrap--md.data-v-d92a1022{width:40rpx;height:40rpx}.tj-icon-wrap--lg.data-v-d92a1022{width:48rpx;height:48rpx}.tj-icon-wrap--xl.data-v-d92a1022{width:56rpx;height:56rpx}.tj-icon.data-v-d92a1022{display:block;width:100%;height:100%}.tj-icon--send.data-v-d92a1022{transform:translate(-10%,10%)}.tj-icon-fallback.data-v-d92a1022{display:block;line-height:1;font-weight:700;text-align:center}.tj-icon-fallback--sm.data-v-d92a1022{font-size:24rpx}.tj-icon-fallback--md.data-v-d92a1022{font-size:30rpx}.tj-icon-fallback--lg.data-v-d92a1022{font-size:36rpx}.tj-icon-fallback--xl.data-v-d92a1022{font-size:42rpx}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"use strict";const e=require("../../common/vendor.js"),t={__name:"VoiceSpeakMascot",props:{active:{type:Boolean,default:!1},onDark:{type:Boolean,default:!1},size:{type:String,default:"md"}},setup:t=>(a,o)=>e.e({a:t.active},(t.active,{}),{b:e.n(`vsm--${t.size}`),c:e.n({"vsm--active":t.active,"vsm--on-dark":t.onDark})})},a=e._export_sfc(t,[["__scopeId","data-v-c347644e"]]);wx.createComponent(a);
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"component": true,
|
||||||
|
"usingComponents": {}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<view class="{{['vsm', 'data-v-c347644e', b, c]}}"><view class="vsm-body data-v-c347644e"><view class="vsm-leaf data-v-c347644e" aria-hidden="true"/><view class="vsm-face data-v-c347644e"><view class="vsm-eye vsm-eye--l data-v-c347644e"/><view class="vsm-eye vsm-eye--r data-v-c347644e"/><view class="vsm-blush vsm-blush--l data-v-c347644e"/><view class="vsm-blush vsm-blush--r data-v-c347644e"/><view class="vsm-mouth data-v-c347644e"/></view><view wx:if="{{a}}" class="vsm-waves data-v-c347644e" aria-hidden="true"><view class="vsm-wave vsm-wave--1 data-v-c347644e"/><view class="vsm-wave vsm-wave--2 data-v-c347644e"/><view class="vsm-wave vsm-wave--3 data-v-c347644e"/></view></view></view>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
.vsm.data-v-c347644e{display:flex;align-items:center;justify-content:center;flex-shrink:0}.vsm--md.data-v-c347644e{width:72rpx;height:72rpx}.vsm--sm.data-v-c347644e{width:64rpx;height:64rpx}.vsm-body.data-v-c347644e{position:relative;width:100%;height:100%;display:flex;align-items:center;justify-content:center}.vsm-leaf.data-v-c347644e{position:absolute;top:2rpx;left:50%;width:16rpx;height:16rpx;margin-left:-8rpx;border-radius:0 100%;background:#34d399;transform:rotate(-18deg);z-index:2}.vsm--on-dark .vsm-leaf.data-v-c347644e{background:#a7f3d0}.vsm-face.data-v-c347644e{position:relative;width:52rpx;height:52rpx;border-radius:50%;background:#fef9c3;border:3rpx solid #047857;box-shadow:0 4rpx 10rpx rgba(4,120,87,.18);z-index:1}.vsm--sm .vsm-face.data-v-c347644e{width:46rpx;height:46rpx}.vsm--on-dark .vsm-face.data-v-c347644e{background:#fffbeb;border-color:rgba(255,255,255,.85);box-shadow:0 4rpx 12rpx rgba(0,0,0,.12)}.vsm-eye.data-v-c347644e{position:absolute;top:16rpx;width:6rpx;height:8rpx;border-radius:50%;background:#064e3b}.vsm--sm .vsm-eye.data-v-c347644e{top:14rpx;width:5rpx;height:7rpx}.vsm--on-dark .vsm-eye.data-v-c347644e{background:#134e4a}.vsm-eye--l.data-v-c347644e{left:12rpx}.vsm-eye--r.data-v-c347644e{right:12rpx}.vsm--sm .vsm-eye--l.data-v-c347644e{left:10rpx}.vsm--sm .vsm-eye--r.data-v-c347644e{right:10rpx}.vsm-blush.data-v-c347644e{position:absolute;top:24rpx;width:10rpx;height:6rpx;border-radius:50%;background:rgba(251,113,133,.45);opacity:.85}.vsm--sm .vsm-blush.data-v-c347644e{top:21rpx;width:8rpx;height:5rpx}.vsm-blush--l.data-v-c347644e{left:6rpx}.vsm-blush--r.data-v-c347644e{right:6rpx}.vsm--on-dark .vsm-blush.data-v-c347644e{background:rgba(255,255,255,.35)}.vsm-mouth.data-v-c347644e{position:absolute;left:50%;bottom:10rpx;width:18rpx;height:8rpx;margin-left:-9rpx;border-radius:0 0 18rpx 18rpx;background:#047857;transform-origin:center top}.vsm--sm .vsm-mouth.data-v-c347644e{bottom:9rpx;width:16rpx;height:7rpx;margin-left:-8rpx}.vsm--on-dark .vsm-mouth.data-v-c347644e{background:#ecfdf5}.vsm-waves.data-v-c347644e{position:absolute;right:-2rpx;top:50%;display:flex;align-items:flex-end;gap:4rpx;height:28rpx;margin-top:-14rpx;z-index:0}.vsm-wave.data-v-c347644e{width:5rpx;border-radius:4rpx;background:#047857;animation:vsm-wave-jump-c347644e .72s ease-in-out infinite}.vsm--on-dark .vsm-wave.data-v-c347644e{background:rgba(255,255,255,.9)}.vsm-wave--1.data-v-c347644e{height:10rpx;animation-delay:0s}.vsm-wave--2.data-v-c347644e{height:18rpx;animation-delay:.12s}.vsm-wave--3.data-v-c347644e{height:12rpx;animation-delay:.24s}.vsm--active .vsm-body.data-v-c347644e{animation:vsm-bob-c347644e .9s ease-in-out infinite}.vsm--active .vsm-mouth.data-v-c347644e{animation:vsm-talk-c347644e .32s ease-in-out infinite alternate}.vsm--active .vsm-eye.data-v-c347644e{animation:vsm-blink-c347644e 2.4s ease-in-out infinite}@keyframes vsm-bob-c347644e{0%,to{transform:translateY(0)}50%{transform:translateY(-4rpx)}}@keyframes vsm-talk-c347644e{0%{transform:scaleY(.45);border-radius:0 0 18rpx 18rpx}to{transform:scaleY(1.15);height:12rpx;border-radius:50%}}@keyframes vsm-blink-c347644e{0%,42%,46%,to{transform:scaleY(1)}44%{transform:scaleY(.15)}}@keyframes vsm-wave-jump-c347644e{0%,to{transform:scaleY(.45);opacity:.55}50%{transform:scaleY(1);opacity:1}}
|
||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"navigationStyle": "custom",
|
||||||
|
"navigationBarTitleText": "识糖小课堂",
|
||||||
|
"backgroundColor": "#eefbf4",
|
||||||
|
"disableScroll": false,
|
||||||
|
"enableShareAppMessage": true,
|
||||||
|
"enableShareTimeline": true,
|
||||||
|
"usingComponents": {
|
||||||
|
"tongji-icon": "../components/TongjiIcon"
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user