Compare commits
217
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bc1228a310 | ||
|
|
8b9df1154c | ||
|
|
5f53aa0bbd | ||
|
|
ed040025e4 | ||
|
|
49309b7c57 | ||
|
|
18b92f1767 | ||
|
|
79601ea176 | ||
|
|
c1eee2d28a | ||
|
|
2fc8048844 | ||
|
|
18c15d1262 | ||
|
|
21790e35f4 | ||
|
|
cbe71905c0 | ||
|
|
72a5300b08 | ||
|
|
07188e82f8 | ||
|
|
c3713a8785 | ||
|
|
dddf2348d3 | ||
|
|
18ed3c48fa | ||
|
|
26839c2957 | ||
|
|
b2eb18c75f | ||
|
|
a1092c02c3 | ||
|
|
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
|
||||
/.codex
|
||||
/.agents
|
||||
/.claude
|
||||
/.claude
|
||||
/.claude
|
||||
/.kbctx
|
||||
/server/.spool
|
||||
/server/.claude
|
||||
/.spool
|
||||
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>
|
||||
// 2. 获取组件实例,通过 proxy 访问全局属性
|
||||
import { ref} from "vue";
|
||||
import * as GenerateTestUserSig from "@/debug/GenerateTestUserSig-es.js";
|
||||
import { CallManager } from "@/TUICallKit/src/TUICallService/serve/callManager";
|
||||
import { onLaunch, onShow, onHide, onError } from '@dcloudio/uni-app'
|
||||
import { getCurrentInstance } from 'vue'
|
||||
@@ -97,11 +96,7 @@ const video =async (data)=>{
|
||||
},
|
||||
}, false)
|
||||
|
||||
const {userId } = res.data;
|
||||
|
||||
const { userSig, SDKAppID } = GenerateTestUserSig.genTestUserSig({
|
||||
userID:userId,
|
||||
});
|
||||
const { userId, userSig, sdkAppId: SDKAppID } = res.data;
|
||||
console.log('SDKAppID',SDKAppID)
|
||||
getApp().globalData.userID = userId;
|
||||
getApp().globalData.userSig = userSig;
|
||||
@@ -115,4 +110,4 @@ const video =async (data)=>{
|
||||
uni.setStorageSync('CallManager', userId)
|
||||
await uni.CallManager.init(cc);
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -239,7 +239,6 @@
|
||||
import messageEnhancerPlugin from '@tencentcloud/lite-chat/plugins/message-enhancer';
|
||||
import richMediaMessagePlugin from '@tencentcloud/lite-chat/plugins/rich-media-message';
|
||||
import groupPlugin from '@tencentcloud/lite-chat/plugins/group';
|
||||
import * as GenerateTestUserSig from "@/debug/GenerateTestUserSig-es.js";
|
||||
import { TUICallKitAPI } from '@/TUICallKit/src/TUICallService/index';
|
||||
import { TUIStore } from '@/TUICallKit/src/TUICallService/CallService/index';
|
||||
import { StoreName, NAME, CallStatus } from '@/TUICallKit/src/TUICallService/const/index';
|
||||
@@ -561,17 +560,12 @@ const { proxy } = getCurrentInstance()
|
||||
},
|
||||
}, false)
|
||||
|
||||
const {userId } = res.data;
|
||||
|
||||
const { userId, userSig, sdkAppId: SDKAppID } = res.data;
|
||||
patient_data.value=res.data.data
|
||||
const { userSig, SDKAppID,secretKey } = GenerateTestUserSig.genTestUserSig({
|
||||
userID:userId,
|
||||
});
|
||||
|
||||
getApp().globalData.userID = userId;
|
||||
getApp().globalData.userSig = userSig;
|
||||
getApp().globalData.SDKAppID = SDKAppID;
|
||||
getApp().globalData.secretKey = secretKey;
|
||||
uni.setStorageSync('CallManager', userId)
|
||||
// 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>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -178,7 +178,6 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { getCurrentInstance } from 'vue'
|
||||
import * as GenerateTestUserSig from "@/debug/GenerateTestUserSig-es.js";
|
||||
import { CallManager } from "@/TUICallKit/src/TUICallService/serve/callManager";
|
||||
const { proxy } = getCurrentInstance()
|
||||
let userId = ref("");
|
||||
@@ -379,10 +378,11 @@ const loginHandler = async (patient_id) => {
|
||||
data: { patient_id: patient_id },
|
||||
}, false)
|
||||
|
||||
const { userId: patientUserId } = res.data
|
||||
const { userSig: patientUserSig, SDKAppID: patientSDKAppID } = GenerateTestUserSig.genTestUserSig({
|
||||
userID: patientUserId,
|
||||
})
|
||||
const {
|
||||
userId: patientUserId,
|
||||
userSig: patientUserSig,
|
||||
sdkAppId: patientSDKAppID,
|
||||
} = res.data
|
||||
console.log('获取签名成功:', patientUserSig)
|
||||
|
||||
// 更新ref值
|
||||
|
||||
@@ -51,15 +51,16 @@
|
||||
/* 小程序特有相关 */
|
||||
"mp-weixin" : {
|
||||
"appid" : "wx79b9a0bfbfe7cbcd",
|
||||
"lazyCodeLoading" : "requiredComponents",
|
||||
"setting" : {
|
||||
"urlCheck" : false
|
||||
"urlCheck" : false,
|
||||
"minified" : true
|
||||
},
|
||||
"optimization" : {
|
||||
"subPackages" : true
|
||||
},
|
||||
"usingComponents" : true,
|
||||
"requiredBackgroundModes" : ["audio"],
|
||||
|
||||
"requiredBackgroundModes" : [ "audio" ],
|
||||
"plugins" : {
|
||||
"WechatSI" : {
|
||||
"version" : "0.3.5",
|
||||
|
||||
Generated
-69
@@ -4142,9 +4142,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -4161,9 +4158,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -4180,9 +4174,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -4199,9 +4190,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -4433,9 +4421,6 @@
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -4456,9 +4441,6 @@
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -4479,9 +4461,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -4502,9 +4481,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -4525,9 +4501,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -4548,9 +4521,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -4729,9 +4699,6 @@
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -4745,9 +4712,6 @@
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -4761,9 +4725,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -4777,9 +4738,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -4793,9 +4751,6 @@
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -4809,9 +4764,6 @@
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -4825,9 +4777,6 @@
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -4841,9 +4790,6 @@
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -4857,9 +4803,6 @@
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -4873,9 +4816,6 @@
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -4889,9 +4829,6 @@
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -4905,9 +4842,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -4921,9 +4855,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
||||
+243
-233
@@ -1,233 +1,243 @@
|
||||
{
|
||||
"pages": [
|
||||
{
|
||||
"path": "pages/index/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "甄养堂"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/login/login",
|
||||
"style": {
|
||||
"navigationBarTitleText": "视频问诊"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/user/userinfo",
|
||||
"style": {
|
||||
"navigationBarTitleText": "个人资料"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/index/video",
|
||||
"style": {
|
||||
"navigationBarTitleText": "甄养堂"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "TUICallKit/src/Components/TUICallKit",
|
||||
"style": {
|
||||
"navigationBarTitleText": "视频问诊",
|
||||
"navigationBarHidden": true,
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/order/monad/monad",
|
||||
"style": {
|
||||
"navigationBarTitleText": "诊单确认",
|
||||
"navigationBarHidden": true,
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/user/user",
|
||||
"style": {
|
||||
"navigationBarTitleText": "我的"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/Card/Card",
|
||||
"style": {
|
||||
"navigationBarTitleText": "就诊卡"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/Card/edit_card",
|
||||
"style": {
|
||||
"navigationBarTitleText": "修改就诊卡"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/Card/contact",
|
||||
"style": {
|
||||
"navigationBarTitleText": "服务"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/order/order",
|
||||
"style": {
|
||||
"navigationBarTitleText": "付款"
|
||||
}
|
||||
}
|
||||
],
|
||||
"subPackages": [
|
||||
{
|
||||
"root": "TUIKit",
|
||||
"pages": [
|
||||
{
|
||||
"path": "pages/chat/chat",
|
||||
"style": {
|
||||
"navigationBarTitleText": "问诊"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"root": "doctor",
|
||||
"pages": [
|
||||
{
|
||||
"path": "pages/doctor/doctor",
|
||||
"style": {
|
||||
"navigationBarTitleText": "医生信息"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/doctor/list/list",
|
||||
"style": {
|
||||
"navigationBarTitleText": "执业医生列表"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/article/article",
|
||||
"style": {
|
||||
"navigationBarTitleText": "健康百科"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"root": "training",
|
||||
"pages": [
|
||||
{
|
||||
"path": "pages/grip-ring",
|
||||
"style": {
|
||||
"navigationBarTitleText": "握力环训练",
|
||||
"navigationBarBackgroundColor": "#f8fafc",
|
||||
"navigationBarTextStyle": "black",
|
||||
"backgroundColor": "#f8fafc"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/pilates-ring",
|
||||
"style": {
|
||||
"navigationBarTitleText": "瑜伽环训练",
|
||||
"navigationBarBackgroundColor": "#f8fafc",
|
||||
"navigationBarTextStyle": "black",
|
||||
"backgroundColor": "#f8fafc"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/foot-pedal",
|
||||
"style": {
|
||||
"navigationBarTitleText": "脚蹬器训练",
|
||||
"navigationBarBackgroundColor": "#f8fafc",
|
||||
"navigationBarTextStyle": "black",
|
||||
"backgroundColor": "#f8fafc"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/dumbbell",
|
||||
"style": {
|
||||
"navigationBarTitleText": "哑铃训练",
|
||||
"navigationBarBackgroundColor": "#f8fafc",
|
||||
"navigationBarTextStyle": "black",
|
||||
"backgroundColor": "#f8fafc"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/metronome",
|
||||
"style": {
|
||||
"navigationBarTitleText": "耗糖节拍器",
|
||||
"navigationBarBackgroundColor": "#f8fafc",
|
||||
"navigationBarTextStyle": "black",
|
||||
"backgroundColor": "#f8fafc"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"root": "tongji",
|
||||
"pages": [
|
||||
{
|
||||
"path": "pages/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "血糖记录",
|
||||
"navigationBarBackgroundColor": "#204e2b",
|
||||
"navigationBarTextStyle": "white",
|
||||
"backgroundColor": "#faf9f5",
|
||||
"enablePullDownRefresh": true,
|
||||
"mp-weixin": {
|
||||
"usingPlugins": {
|
||||
"WechatSI": {
|
||||
"version": "0.3.5",
|
||||
"provider": "wx069ba97219f66d99"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/more",
|
||||
"style": {
|
||||
"navigationBarTitleText": "日常护理",
|
||||
"navigationBarBackgroundColor": "#204e2b",
|
||||
"navigationBarTextStyle": "white",
|
||||
"backgroundColor": "#faf9f5",
|
||||
"enablePullDownRefresh": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/weekly",
|
||||
"style": {
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": "血糖管理",
|
||||
"backgroundColor": "#f4fbf4",
|
||||
"enablePullDownRefresh": true,
|
||||
"mp-weixin": {
|
||||
"usingPlugins": {
|
||||
"WechatSI": {
|
||||
"version": "0.3.5",
|
||||
"provider": "wx069ba97219f66d99"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"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": {
|
||||
"navigationBarTextStyle": "black",
|
||||
"navigationBarTitleText": "甄养堂互联网医院",
|
||||
"navigationBarBackgroundColor": "#F8F8F8",
|
||||
"backgroundColor": "#F8F8F8"
|
||||
}
|
||||
}
|
||||
{
|
||||
"pages": [
|
||||
{
|
||||
"path": "pages/index/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "甄养堂",
|
||||
"backgroundColor": "#F8F8F8",
|
||||
"backgroundColorTop": "#F8F8F8"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/login/login",
|
||||
"style": {
|
||||
"navigationBarTitleText": "视频问诊"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/user/userinfo",
|
||||
"style": {
|
||||
"navigationBarTitleText": "个人资料"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/index/video",
|
||||
"style": {
|
||||
"navigationBarTitleText": "甄养堂"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/order/monad/monad",
|
||||
"style": {
|
||||
"navigationBarTitleText": "诊单确认",
|
||||
"navigationBarHidden": true,
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/user/user",
|
||||
"style": {
|
||||
"navigationBarTitleText": "我的",
|
||||
"backgroundColor": "#e8eaed",
|
||||
"backgroundColorTop": "#e8eaed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/Card/Card",
|
||||
"style": {
|
||||
"navigationBarTitleText": "就诊卡"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/Card/edit_card",
|
||||
"style": {
|
||||
"navigationBarTitleText": "修改就诊卡"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/Card/contact",
|
||||
"style": {
|
||||
"navigationBarTitleText": "服务"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/order/order",
|
||||
"style": {
|
||||
"navigationBarTitleText": "付款"
|
||||
}
|
||||
}
|
||||
],
|
||||
"subPackages": [
|
||||
{
|
||||
"root": "TUICallKit/src/Components",
|
||||
"pages": [
|
||||
{
|
||||
"path": "TUICallKit",
|
||||
"style": {
|
||||
"navigationBarTitleText": "视频问诊",
|
||||
"navigationBarHidden": true,
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"root": "TUIKit",
|
||||
"pages": [
|
||||
{
|
||||
"path": "pages/chat/chat",
|
||||
"style": {
|
||||
"navigationBarTitleText": "问诊"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"root": "doctor",
|
||||
"pages": [
|
||||
{
|
||||
"path": "pages/doctor/doctor",
|
||||
"style": {
|
||||
"navigationBarTitleText": "医生信息"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/doctor/list/list",
|
||||
"style": {
|
||||
"navigationBarTitleText": "执业医生列表"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/article/article",
|
||||
"style": {
|
||||
"navigationBarTitleText": "健康百科"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"root": "training",
|
||||
"pages": [
|
||||
{
|
||||
"path": "pages/grip-ring",
|
||||
"style": {
|
||||
"navigationBarTitleText": "握力环训练",
|
||||
"navigationBarBackgroundColor": "#f8fafc",
|
||||
"navigationBarTextStyle": "black",
|
||||
"backgroundColor": "#f8fafc"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/pilates-ring",
|
||||
"style": {
|
||||
"navigationBarTitleText": "瑜伽环训练",
|
||||
"navigationBarBackgroundColor": "#f8fafc",
|
||||
"navigationBarTextStyle": "black",
|
||||
"backgroundColor": "#f8fafc"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/foot-pedal",
|
||||
"style": {
|
||||
"navigationBarTitleText": "脚蹬器训练",
|
||||
"navigationBarBackgroundColor": "#f8fafc",
|
||||
"navigationBarTextStyle": "black",
|
||||
"backgroundColor": "#f8fafc"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/dumbbell",
|
||||
"style": {
|
||||
"navigationBarTitleText": "哑铃训练",
|
||||
"navigationBarBackgroundColor": "#f8fafc",
|
||||
"navigationBarTextStyle": "black",
|
||||
"backgroundColor": "#f8fafc"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/metronome",
|
||||
"style": {
|
||||
"navigationBarTitleText": "耗糖节拍器",
|
||||
"navigationBarBackgroundColor": "#f8fafc",
|
||||
"navigationBarTextStyle": "black",
|
||||
"backgroundColor": "#f8fafc"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"root": "tongji",
|
||||
"pages": [
|
||||
{
|
||||
"path": "pages/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "血糖记录",
|
||||
"navigationBarBackgroundColor": "#204e2b",
|
||||
"navigationBarTextStyle": "white",
|
||||
"backgroundColor": "#faf9f5",
|
||||
"enablePullDownRefresh": true,
|
||||
"mp-weixin": {
|
||||
"usingPlugins": {
|
||||
"WechatSI": {
|
||||
"version": "0.3.5",
|
||||
"provider": "wx069ba97219f66d99"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/weekly",
|
||||
"style": {
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": "血糖管理",
|
||||
"backgroundColor": "#f4fbf4",
|
||||
"enablePullDownRefresh": true,
|
||||
"mp-weixin": {
|
||||
"usingPlugins": {
|
||||
"WechatSI": {
|
||||
"version": "0.3.5",
|
||||
"provider": "wx069ba97219f66d99"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"globalStyle": {
|
||||
"navigationBarTextStyle": "black",
|
||||
"navigationBarTitleText": "甄养堂互联网医院",
|
||||
"navigationBarBackgroundColor": "#F8F8F8",
|
||||
"backgroundColor": "#F8F8F8"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,13 +118,15 @@
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
<TabBarDock :active="0" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } 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 doctors = ref([])
|
||||
@@ -263,7 +265,8 @@ $outline: #727970;
|
||||
.container {
|
||||
background: $background;
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -62,7 +62,6 @@
|
||||
import { ref,onMounted,getCurrentInstance} from "vue";
|
||||
// 2. 获取组件实例,通过 proxy 访问全局属性
|
||||
const { proxy } = getCurrentInstance()
|
||||
import * as GenerateTestUserSig from "@/debug/GenerateTestUserSig-es.js";
|
||||
import { CallManager } from "@/TUICallKit/src/TUICallService/serve/callManager";
|
||||
import { onLaunch, onShow, onHide, onError ,onShareAppMessage} from '@dcloudio/uni-app'
|
||||
let userID = ref("4");
|
||||
@@ -140,13 +139,9 @@ const loginHandler = async (patient_id) => {
|
||||
|
||||
|
||||
|
||||
const {userId } = res.data;
|
||||
|
||||
const { userSig, SDKAppID } = GenerateTestUserSig.genTestUserSig({
|
||||
userID:userId,
|
||||
});
|
||||
const { userId, userSig, sdkAppId: SDKAppID } = res.data;
|
||||
console.log('获取签名成功:',userSig);
|
||||
getApp().globalData.userID = userId.value;
|
||||
getApp().globalData.userID = userId;
|
||||
getApp().globalData.userSig = userSig;
|
||||
getApp().globalData.SDKAppID = SDKAppID;
|
||||
|
||||
|
||||
@@ -52,7 +52,6 @@
|
||||
<script setup>
|
||||
import { ref, getCurrentInstance,onMounted } from "vue";
|
||||
import { onShow, onShareAppMessage } from '@dcloudio/uni-app'
|
||||
import * as GenerateTestUserSig from "@/debug/GenerateTestUserSig-es.js";
|
||||
import { CallManager } from "@/TUICallKit/src/TUICallService/serve/callManager";
|
||||
const { proxy } = getCurrentInstance()
|
||||
let userId = ref("");
|
||||
@@ -126,10 +125,7 @@ const loginHandler = async (patient_id) => {
|
||||
data: { patient_id: patient_id },
|
||||
}, false)
|
||||
|
||||
const {userId } = res.data;
|
||||
const { userSig, SDKAppID } = GenerateTestUserSig.genTestUserSig({
|
||||
userID:userId,
|
||||
});
|
||||
const { userId, userSig, sdkAppId: SDKAppID } = res.data;
|
||||
console.log('获取签名成功:',userSig);
|
||||
|
||||
getApp().globalData.userID = userId;
|
||||
|
||||
@@ -370,7 +370,6 @@
|
||||
import { ref,onMounted,getCurrentInstance } from "vue";
|
||||
import { onShow, onShareAppMessage,onLaunch } from '@dcloudio/uni-app'
|
||||
// 2. 获取组件实例,通过 proxy 访问全局属性
|
||||
import * as GenerateTestUserSig from "@/debug/GenerateTestUserSig-es.js";
|
||||
import { CallManager } from "@/TUICallKit/src/TUICallService/serve/callManager";
|
||||
const { proxy } = getCurrentInstance()
|
||||
|
||||
@@ -667,25 +666,25 @@ const closePatientModal = async () => {
|
||||
const granted = await requestAVPermissionsInModal();
|
||||
if (!granted) return; // 未授权,停止跳转
|
||||
const app = getApp();
|
||||
// const userID = app.globalData.userID;
|
||||
// const userSig = app.globalData.userSig;
|
||||
// const SDKAppID = app.globalData.SDKAppID;
|
||||
const doctor = 'doctor_' + doctorId.value;
|
||||
await confirmDiagnosis(diagnosisId);
|
||||
if(!diagnosisId.includes('patient')){
|
||||
diagnosisId='patient_'+diagnosisId
|
||||
}
|
||||
const { userSig, SDKAppID } = GenerateTestUserSig.genTestUserSig({
|
||||
userID:diagnosisId,
|
||||
});
|
||||
const signatureRes = await proxy.apiUrl({
|
||||
url: '/api/tcm/getPatientSignature',
|
||||
method: 'GET',
|
||||
data: { patient_id: diagnosisId },
|
||||
}, false);
|
||||
const { userId, userSig, sdkAppId: SDKAppID } = signatureRes.data;
|
||||
if (!doctorId.value) {
|
||||
uni.showToast({ title: '诊室号不存在,联系助理处理!', icon: 'none' });
|
||||
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;
|
||||
uni.redirectTo({
|
||||
@@ -707,11 +706,7 @@ const video =async (data)=>{
|
||||
},
|
||||
}, false)
|
||||
|
||||
const {userId } = res.data;
|
||||
|
||||
const { userSig, SDKAppID } = GenerateTestUserSig.genTestUserSig({
|
||||
userID:userId,
|
||||
});
|
||||
const { userId, userSig, sdkAppId: SDKAppID } = res.data;
|
||||
|
||||
getApp().globalData.userID = userId;
|
||||
getApp().globalData.userSig = userSig;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="mine-page">
|
||||
<view class="mine-page">
|
||||
<!-- 顶部用户信息卡片 -->
|
||||
<div class="user-card" @click="navigateTo('/pages/user/userinfo')">
|
||||
<div class="user-header">
|
||||
@@ -90,16 +90,18 @@
|
||||
</div>
|
||||
|
||||
<!-- DEV 开发悬浮入口(生产环境自动隐藏) -->
|
||||
<dev-training-entry />
|
||||
</div>
|
||||
<dev-training-entry :bottom="360" />
|
||||
<TabBarDock :active="2" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { onShow, onShareAppMessage } from '@dcloudio/uni-app'
|
||||
import DevTrainingEntry from '@/components/dev-training-entry/index.vue'
|
||||
import TabBarDock from '@/components/app-tab-bar/tab-bar-dock.vue'
|
||||
export default {
|
||||
name: 'profileB',
|
||||
components: { DevTrainingEntry },
|
||||
components: { DevTrainingEntry, TabBarDock },
|
||||
data() {
|
||||
return {
|
||||
userInfo: {
|
||||
@@ -170,6 +172,7 @@ export default {
|
||||
min-height: 100vh;
|
||||
background: #e8eaed;
|
||||
padding: 40rpx;
|
||||
padding-bottom: calc(40rpx + 200rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.user-card {
|
||||
@@ -420,4 +423,4 @@ export default {
|
||||
.bg-cyan { background: #13c2c2; }
|
||||
.bg-pink { background: #eb2f96; }
|
||||
.bg-gray { background: #666666; }
|
||||
</style>
|
||||
</style>
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 45 KiB |
@@ -8,12 +8,12 @@
|
||||
mode="aspectFit"
|
||||
@error="onImageError"
|
||||
/>
|
||||
<text
|
||||
<!-- <text
|
||||
v-else
|
||||
class="tj-icon-fallback"
|
||||
:class="[`tj-icon-fallback--${size}`]"
|
||||
:style="{ color }"
|
||||
>{{ fallbackGlyph }}</text>
|
||||
>{{ fallbackGlyph }}</text> -->
|
||||
</view>
|
||||
</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"/>',
|
||||
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"/>',
|
||||
'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 回退 */
|
||||
@@ -106,7 +111,12 @@ const ICON_FALLBACK = {
|
||||
egg: '🍳',
|
||||
home: '🏠',
|
||||
'plus-circle': '⊕',
|
||||
'chevron-right': '›'
|
||||
'chevron-right': '›',
|
||||
'chevron-left': '‹',
|
||||
check: '✓',
|
||||
settings: '⚙',
|
||||
syringe: '💉',
|
||||
zap: '⚡'
|
||||
}
|
||||
|
||||
const strokeColor = computed(() => {
|
||||
@@ -155,6 +165,10 @@ function onImageError() {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
/* Lucide send 路径重心偏右上,微调使圆形按钮内视觉居中 */
|
||||
.tj-icon--send {
|
||||
transform: translate(-10%, 10%);
|
||||
}
|
||||
.tj-icon-fallback {
|
||||
display: block;
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -1,312 +1,317 @@
|
||||
import { ref, computed } from 'vue'
|
||||
import { requestAiStream, parsePartialDietPlainText } from '../utils/aiStreamRequest.js'
|
||||
|
||||
const DIET_AI_PREFILL_KEY = 'tongji_diet_ai_prefill'
|
||||
|
||||
const emptyDietAiRecommend = () => ({
|
||||
breakfast: '',
|
||||
lunch: '',
|
||||
dinner: '',
|
||||
tips: '',
|
||||
avoid: [],
|
||||
analysis: [],
|
||||
exercise: null,
|
||||
disclaimer: '',
|
||||
rules_summary: '',
|
||||
source: '',
|
||||
date: ''
|
||||
})
|
||||
|
||||
/** AI 饮食助手(今日推荐 + 能不能吃) */
|
||||
export function useDietAi(proxy, { diagnosisId, showUserToast, formatUserMessage }) {
|
||||
const dietAiRecommend = ref(emptyDietAiRecommend())
|
||||
const dietAiLoading = ref(false)
|
||||
const dietAiStreaming = ref(false)
|
||||
const dietAiReplacing = ref(false)
|
||||
const dietAiStreamField = ref('')
|
||||
let dietStreamBuffer = ''
|
||||
const dietAiAskText = ref('')
|
||||
const dietAiAsking = ref(false)
|
||||
const dietAiAskResult = ref({ advice: '', level: '', level_label: '', portion: '', food: '' })
|
||||
|
||||
const hasDietAiMealContent = computed(() => {
|
||||
const r = dietAiRecommend.value
|
||||
return !!(r.breakfast || r.lunch || r.dinner || r.tips)
|
||||
})
|
||||
|
||||
function detectDietStreamField(text) {
|
||||
const tail = String(text || '').match(/(?:^|\n)(早餐|午餐|晚餐|提示|少碰)[::]\s*([^\n]*)$/)
|
||||
if (!tail) return 'breakfast'
|
||||
const map = { '早餐': 'breakfast', '午餐': 'lunch', '晚餐': 'dinner', '提示': 'tips', '少碰': 'tips' }
|
||||
return map[tail[1]] || 'breakfast'
|
||||
}
|
||||
|
||||
function mergePartialDietFromStream() {
|
||||
const partial = parsePartialDietPlainText(dietStreamBuffer)
|
||||
const next = { ...dietAiRecommend.value }
|
||||
for (const key of ['breakfast', 'lunch', 'dinner', 'tips']) {
|
||||
if (partial[key] != null) next[key] = partial[key]
|
||||
}
|
||||
if (Array.isArray(partial.avoid)) next.avoid = partial.avoid
|
||||
dietAiRecommend.value = next
|
||||
dietAiStreamField.value = detectDietStreamField(dietStreamBuffer)
|
||||
}
|
||||
|
||||
function applyDietRecommendPayload(payload) {
|
||||
if (!payload) return
|
||||
const prevAnalysis = dietAiRecommend.value.analysis
|
||||
const prevExercise = dietAiRecommend.value.exercise
|
||||
dietAiRecommend.value = {
|
||||
...emptyDietAiRecommend(),
|
||||
...payload,
|
||||
avoid: Array.isArray(payload.avoid) ? payload.avoid : [],
|
||||
analysis: Array.isArray(payload.analysis) && payload.analysis.length
|
||||
? payload.analysis
|
||||
: (Array.isArray(prevAnalysis) ? prevAnalysis : []),
|
||||
exercise: (payload.exercise && typeof payload.exercise === 'object')
|
||||
? payload.exercise
|
||||
: (prevExercise || null)
|
||||
}
|
||||
}
|
||||
|
||||
function resolveDietDonePayload(payload) {
|
||||
if (!payload) return payload
|
||||
if (payload.source !== 'rule' || !dietStreamBuffer) return payload
|
||||
|
||||
const streamed = parsePartialDietPlainText(dietStreamBuffer)
|
||||
if (!streamed.breakfast) return payload
|
||||
|
||||
return {
|
||||
...emptyDietAiRecommend(),
|
||||
...payload,
|
||||
breakfast: streamed.breakfast,
|
||||
lunch: streamed.lunch || payload.lunch || '',
|
||||
dinner: streamed.dinner || payload.dinner || '',
|
||||
tips: streamed.tips || payload.tips || '',
|
||||
avoid: (Array.isArray(streamed.avoid) && streamed.avoid.length) ? streamed.avoid : (payload.avoid || []),
|
||||
source: 'ai',
|
||||
disclaimer: 'AI 建议供参考,仍请结合医嘱与血糖监测。'
|
||||
}
|
||||
}
|
||||
|
||||
function onDietAiStreamEvent(event, payload) {
|
||||
if (event === 'analysis') {
|
||||
if (Array.isArray(payload?.analysis)) {
|
||||
dietAiRecommend.value = { ...dietAiRecommend.value, analysis: payload.analysis }
|
||||
}
|
||||
} else if (event === 'exercise') {
|
||||
if (payload?.exercise && typeof payload.exercise === 'object') {
|
||||
dietAiRecommend.value = { ...dietAiRecommend.value, exercise: payload.exercise }
|
||||
}
|
||||
} else if (event === 'delta') {
|
||||
dietAiStreaming.value = true
|
||||
if (payload.buffer != null) {
|
||||
dietStreamBuffer = payload.buffer
|
||||
} else {
|
||||
dietStreamBuffer += payload.text || ''
|
||||
}
|
||||
mergePartialDietFromStream()
|
||||
} else if (event === 'done') {
|
||||
applyDietRecommendPayload(resolveDietDonePayload(payload))
|
||||
dietStreamBuffer = ''
|
||||
dietAiStreamField.value = ''
|
||||
} else if (event === 'error') {
|
||||
showUserToast(formatUserMessage(payload?.message, '饮食建议加载失败'))
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchDietAiRecommendFallback(refresh) {
|
||||
const res = await proxy.apiUrl({
|
||||
url: '/api/tcm/dailyDietAiRecommend',
|
||||
method: 'GET',
|
||||
timeout: 35000,
|
||||
data: {
|
||||
diagnosis_id: diagnosisId.value,
|
||||
refresh: refresh ? 1 : 0,
|
||||
...(refresh ? { _t: Date.now() } : {})
|
||||
}
|
||||
}, false)
|
||||
if (res?.code === 1 && res.data) {
|
||||
applyDietRecommendPayload(res.data)
|
||||
} else if (res?.msg) {
|
||||
showUserToast(res.msg)
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchDietAiRecommend(refresh = false) {
|
||||
if (!diagnosisId.value || dietAiLoading.value) return
|
||||
dietAiLoading.value = true
|
||||
dietAiStreaming.value = false
|
||||
dietAiReplacing.value = !!refresh
|
||||
dietStreamBuffer = ''
|
||||
dietAiStreamField.value = ''
|
||||
if (refresh) {
|
||||
applyDietRecommendPayload(emptyDietAiRecommend())
|
||||
}
|
||||
try {
|
||||
await requestAiStream({
|
||||
baseUrl: proxy.$url,
|
||||
url: '/api/tcm/dailyDietAiRecommendStream',
|
||||
method: 'GET',
|
||||
data: {
|
||||
diagnosis_id: diagnosisId.value,
|
||||
refresh: refresh ? 1 : 0,
|
||||
...(refresh ? { _t: Date.now() } : {})
|
||||
},
|
||||
onEvent: onDietAiStreamEvent,
|
||||
fallback: () => fetchDietAiRecommendFallback(refresh)
|
||||
})
|
||||
} catch (e) {
|
||||
try {
|
||||
await fetchDietAiRecommendFallback(refresh)
|
||||
} catch (e2) {
|
||||
showUserToast('饮食建议加载失败')
|
||||
}
|
||||
} finally {
|
||||
dietAiLoading.value = false
|
||||
dietAiStreaming.value = false
|
||||
dietAiReplacing.value = false
|
||||
dietAiStreamField.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
function refreshDietAiRecommend() {
|
||||
if (dietAiLoading.value) return
|
||||
fetchDietAiRecommend(true)
|
||||
}
|
||||
|
||||
async function askDietAiFoodFallback(q) {
|
||||
const res = await proxy.apiUrl({
|
||||
url: '/api/tcm/dailyDietAiAsk',
|
||||
method: 'POST',
|
||||
timeout: 35000,
|
||||
data: {
|
||||
diagnosis_id: diagnosisId.value,
|
||||
question: q
|
||||
}
|
||||
}, false)
|
||||
if (res?.code === 1 && res.data) {
|
||||
dietAiAskResult.value = {
|
||||
advice: res.data.advice || '',
|
||||
level: res.data.level || '',
|
||||
level_label: res.data.level_label || '',
|
||||
portion: res.data.portion || '',
|
||||
food: res.data.food || q
|
||||
}
|
||||
} else {
|
||||
showUserToast(formatUserMessage(res?.msg, '咨询失败'))
|
||||
}
|
||||
}
|
||||
|
||||
async function askDietAiFood() {
|
||||
const q = String(dietAiAskText.value || '').trim()
|
||||
if (!q) {
|
||||
showUserToast('请输入想咨询的食物')
|
||||
return
|
||||
}
|
||||
if (!diagnosisId.value) return
|
||||
dietAiAsking.value = true
|
||||
dietAiAskResult.value = { advice: '', level: '', level_label: '', portion: '', food: '' }
|
||||
try {
|
||||
await requestAiStream({
|
||||
baseUrl: proxy.$url,
|
||||
url: '/api/tcm/dailyDietAiAskStream',
|
||||
method: 'POST',
|
||||
data: {
|
||||
diagnosis_id: diagnosisId.value,
|
||||
question: q
|
||||
},
|
||||
onEvent: (event, payload) => {
|
||||
if (event === 'start' && !dietAiAskResult.value.advice) {
|
||||
dietAiAskResult.value = { ...dietAiAskResult.value, advice: payload.message || '正在想…' }
|
||||
} else if (event === 'delta') {
|
||||
dietAiAskResult.value = {
|
||||
...dietAiAskResult.value,
|
||||
advice: payload.advice || dietAiAskResult.value.advice
|
||||
}
|
||||
} else if (event === 'done') {
|
||||
dietAiAskResult.value = {
|
||||
advice: payload.advice || '',
|
||||
level: payload.level || '',
|
||||
level_label: payload.level_label || '',
|
||||
portion: payload.portion || '',
|
||||
food: payload.food || q
|
||||
}
|
||||
} else if (event === 'error') {
|
||||
showUserToast(formatUserMessage(payload?.message, '咨询失败'))
|
||||
}
|
||||
},
|
||||
fallback: () => askDietAiFoodFallback(q)
|
||||
})
|
||||
} catch (e) {
|
||||
try {
|
||||
await askDietAiFoodFallback(q)
|
||||
} catch (e2) {
|
||||
showUserToast('网络异常,请稍后再试')
|
||||
}
|
||||
} finally {
|
||||
dietAiAsking.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function onDietAiAskInput(e) {
|
||||
dietAiAskText.value = e?.detail?.value ?? ''
|
||||
}
|
||||
|
||||
function saveDietPrefillToStorage() {
|
||||
const r = dietAiRecommend.value
|
||||
if (!r.breakfast) return
|
||||
try {
|
||||
uni.setStorageSync(DIET_AI_PREFILL_KEY, {
|
||||
breakfast: r.breakfast,
|
||||
lunch: r.lunch,
|
||||
dinner: r.dinner,
|
||||
tips: r.tips || ''
|
||||
})
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
function goMoreDietForm(withPrefill = false) {
|
||||
if (withPrefill) saveDietPrefillToStorage()
|
||||
uni.navigateTo({ url: '/tongji/pages/more?openDiet=1' })
|
||||
}
|
||||
|
||||
async function applyDietAiToForm() {
|
||||
if (!dietAiRecommend.value.breakfast) {
|
||||
await fetchDietAiRecommend(false)
|
||||
}
|
||||
goMoreDietForm(true)
|
||||
}
|
||||
|
||||
function openDietForm() {
|
||||
goMoreDietForm(false)
|
||||
}
|
||||
|
||||
return {
|
||||
dietAiRecommend,
|
||||
dietAiLoading,
|
||||
dietAiStreaming,
|
||||
dietAiReplacing,
|
||||
dietAiStreamField,
|
||||
dietAiAskText,
|
||||
dietAiAsking,
|
||||
dietAiAskResult,
|
||||
hasDietAiMealContent,
|
||||
fetchDietAiRecommend,
|
||||
refreshDietAiRecommend,
|
||||
askDietAiFood,
|
||||
onDietAiAskInput,
|
||||
applyDietAiToForm,
|
||||
openDietForm
|
||||
}
|
||||
}
|
||||
|
||||
/** more 页打开饮食表单时读取并清除预填 */
|
||||
export function consumeDietAiPrefill() {
|
||||
try {
|
||||
const data = uni.getStorageSync(DIET_AI_PREFILL_KEY)
|
||||
uni.removeStorageSync(DIET_AI_PREFILL_KEY)
|
||||
if (data && typeof data === 'object' && data.breakfast) return data
|
||||
} catch (e) {}
|
||||
return null
|
||||
}
|
||||
import { ref, computed } from 'vue'
|
||||
import { requestAiStream, parsePartialDietPlainText } from '../utils/aiStreamRequest.js'
|
||||
|
||||
const DIET_AI_PREFILL_KEY = 'tongji_diet_ai_prefill'
|
||||
|
||||
const emptyDietAiRecommend = () => ({
|
||||
breakfast: '',
|
||||
drinks: '',
|
||||
lunch: '',
|
||||
dinner: '',
|
||||
tips: '',
|
||||
avoid: [],
|
||||
analysis: [],
|
||||
exercise: null,
|
||||
disclaimer: '',
|
||||
rules_summary: '',
|
||||
source: '',
|
||||
date: ''
|
||||
})
|
||||
|
||||
/** AI 饮食助手(今日推荐 + 能不能吃) */
|
||||
export function useDietAi(proxy, { diagnosisId, showUserToast, formatUserMessage }) {
|
||||
const dietAiRecommend = ref(emptyDietAiRecommend())
|
||||
const dietAiLoading = ref(false)
|
||||
const dietAiStreaming = ref(false)
|
||||
const dietAiReplacing = ref(false)
|
||||
const dietAiStreamField = ref('')
|
||||
let dietStreamBuffer = ''
|
||||
const dietAiAskText = ref('')
|
||||
const dietAiAsking = ref(false)
|
||||
const dietAiAskResult = ref({ advice: '', level: '', level_label: '', portion: '', food: '' })
|
||||
|
||||
const hasDietAiMealContent = computed(() => {
|
||||
const r = dietAiRecommend.value
|
||||
return !!(r.breakfast || r.drinks || r.lunch || r.dinner || r.tips)
|
||||
})
|
||||
|
||||
function detectDietStreamField(text) {
|
||||
const tail = String(text || '').match(/(?:^|\n)(早餐|喝的|午餐|晚餐|提示|少碰)[::]\s*([^\n]*)$/)
|
||||
if (!tail) return 'breakfast'
|
||||
const map = { '早餐': 'breakfast', '喝的': 'drinks', '午餐': 'lunch', '晚餐': 'dinner', '提示': 'tips', '少碰': 'tips' }
|
||||
return map[tail[1]] || 'breakfast'
|
||||
}
|
||||
|
||||
function mergePartialDietFromStream() {
|
||||
const partial = parsePartialDietPlainText(dietStreamBuffer)
|
||||
const next = { ...dietAiRecommend.value }
|
||||
for (const key of ['breakfast', 'drinks', 'lunch', 'dinner', 'tips']) {
|
||||
if (partial[key] != null) next[key] = partial[key]
|
||||
}
|
||||
if (Array.isArray(partial.avoid)) next.avoid = partial.avoid
|
||||
dietAiRecommend.value = next
|
||||
dietAiStreamField.value = detectDietStreamField(dietStreamBuffer)
|
||||
}
|
||||
|
||||
function applyDietRecommendPayload(payload) {
|
||||
if (!payload) return
|
||||
const prevAnalysis = dietAiRecommend.value.analysis
|
||||
const prevExercise = dietAiRecommend.value.exercise
|
||||
dietAiRecommend.value = {
|
||||
...emptyDietAiRecommend(),
|
||||
...payload,
|
||||
avoid: Array.isArray(payload.avoid) ? payload.avoid : [],
|
||||
analysis: Array.isArray(payload.analysis) && payload.analysis.length
|
||||
? payload.analysis
|
||||
: (Array.isArray(prevAnalysis) ? prevAnalysis : []),
|
||||
exercise: (payload.exercise && typeof payload.exercise === 'object')
|
||||
? payload.exercise
|
||||
: (prevExercise || null)
|
||||
}
|
||||
}
|
||||
|
||||
function resolveDietDonePayload(payload) {
|
||||
if (!payload) return payload
|
||||
if (payload.source !== 'rule' || !dietStreamBuffer) return payload
|
||||
|
||||
const streamed = parsePartialDietPlainText(dietStreamBuffer)
|
||||
if (!streamed.breakfast) return payload
|
||||
|
||||
return {
|
||||
...emptyDietAiRecommend(),
|
||||
...payload,
|
||||
breakfast: streamed.breakfast,
|
||||
drinks: streamed.drinks || payload.drinks || '',
|
||||
lunch: streamed.lunch || payload.lunch || '',
|
||||
dinner: streamed.dinner || payload.dinner || '',
|
||||
tips: streamed.tips || payload.tips || '',
|
||||
avoid: (Array.isArray(streamed.avoid) && streamed.avoid.length) ? streamed.avoid : (payload.avoid || []),
|
||||
source: 'ai',
|
||||
disclaimer: 'AI 建议供参考,仍请结合医嘱与血糖监测。'
|
||||
}
|
||||
}
|
||||
|
||||
function onDietAiStreamEvent(event, payload) {
|
||||
if (event === 'analysis') {
|
||||
if (Array.isArray(payload?.analysis)) {
|
||||
dietAiRecommend.value = { ...dietAiRecommend.value, analysis: payload.analysis }
|
||||
}
|
||||
} else if (event === 'exercise') {
|
||||
if (payload?.exercise && typeof payload.exercise === 'object') {
|
||||
dietAiRecommend.value = { ...dietAiRecommend.value, exercise: payload.exercise }
|
||||
}
|
||||
} else if (event === 'delta') {
|
||||
dietAiStreaming.value = true
|
||||
if (payload.buffer != null) {
|
||||
dietStreamBuffer = payload.buffer
|
||||
} else {
|
||||
dietStreamBuffer += payload.text || ''
|
||||
}
|
||||
mergePartialDietFromStream()
|
||||
} else if (event === 'done') {
|
||||
applyDietRecommendPayload(resolveDietDonePayload(payload))
|
||||
dietStreamBuffer = ''
|
||||
dietAiStreamField.value = ''
|
||||
} else if (event === 'error') {
|
||||
showUserToast(formatUserMessage(payload?.message, '饮食建议加载失败'))
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchDietAiRecommendFallback(refresh) {
|
||||
const res = await proxy.apiUrl({
|
||||
url: '/api/tcm/dailyDietAiRecommend',
|
||||
method: 'GET',
|
||||
timeout: 35000,
|
||||
data: {
|
||||
diagnosis_id: diagnosisId.value,
|
||||
refresh: refresh ? 1 : 0,
|
||||
...(refresh ? { _t: Date.now() } : {})
|
||||
}
|
||||
}, false)
|
||||
if (res?.code === 1 && res.data) {
|
||||
applyDietRecommendPayload(res.data)
|
||||
} else if (res?.msg) {
|
||||
showUserToast(res.msg)
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchDietAiRecommend(refresh = false) {
|
||||
if (!diagnosisId.value || dietAiLoading.value) return
|
||||
dietAiLoading.value = true
|
||||
dietAiStreaming.value = false
|
||||
dietAiReplacing.value = !!refresh
|
||||
dietStreamBuffer = ''
|
||||
dietAiStreamField.value = ''
|
||||
if (refresh) {
|
||||
applyDietRecommendPayload(emptyDietAiRecommend())
|
||||
}
|
||||
try {
|
||||
await requestAiStream({
|
||||
baseUrl: proxy.$url,
|
||||
url: '/api/tcm/dailyDietAiRecommendStream',
|
||||
method: 'GET',
|
||||
data: {
|
||||
diagnosis_id: diagnosisId.value,
|
||||
refresh: refresh ? 1 : 0,
|
||||
...(refresh ? { _t: Date.now() } : {})
|
||||
},
|
||||
onEvent: onDietAiStreamEvent,
|
||||
fallback: () => fetchDietAiRecommendFallback(refresh)
|
||||
})
|
||||
} catch (e) {
|
||||
try {
|
||||
await fetchDietAiRecommendFallback(refresh)
|
||||
} catch (e2) {
|
||||
showUserToast('饮食建议加载失败')
|
||||
}
|
||||
} finally {
|
||||
dietAiLoading.value = false
|
||||
dietAiStreaming.value = false
|
||||
dietAiReplacing.value = false
|
||||
dietAiStreamField.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
function refreshDietAiRecommend() {
|
||||
if (dietAiLoading.value) return
|
||||
fetchDietAiRecommend(true)
|
||||
}
|
||||
|
||||
async function askDietAiFoodFallback(q) {
|
||||
const res = await proxy.apiUrl({
|
||||
url: '/api/tcm/dailyDietAiAsk',
|
||||
method: 'POST',
|
||||
timeout: 35000,
|
||||
data: {
|
||||
diagnosis_id: diagnosisId.value,
|
||||
question: q
|
||||
}
|
||||
}, false)
|
||||
if (res?.code === 1 && res.data) {
|
||||
dietAiAskResult.value = {
|
||||
advice: res.data.advice || '',
|
||||
level: res.data.level || '',
|
||||
level_label: res.data.level_label || '',
|
||||
portion: res.data.portion || '',
|
||||
food: res.data.food || q
|
||||
}
|
||||
} else {
|
||||
showUserToast(formatUserMessage(res?.msg, '咨询失败'))
|
||||
}
|
||||
}
|
||||
|
||||
async function askDietAiFood() {
|
||||
const q = String(dietAiAskText.value || '').trim()
|
||||
if (!q) {
|
||||
showUserToast('请输入想咨询的食物')
|
||||
return
|
||||
}
|
||||
if (!diagnosisId.value) return
|
||||
// 发送后立即清空输入框
|
||||
dietAiAskText.value = ''
|
||||
dietAiAsking.value = true
|
||||
dietAiAskResult.value = { advice: '', level: '', level_label: '', portion: '', food: '' }
|
||||
try {
|
||||
await requestAiStream({
|
||||
baseUrl: proxy.$url,
|
||||
url: '/api/tcm/dailyDietAiAskStream',
|
||||
method: 'POST',
|
||||
data: {
|
||||
diagnosis_id: diagnosisId.value,
|
||||
question: q
|
||||
},
|
||||
onEvent: (event, payload) => {
|
||||
if (event === 'start' && !dietAiAskResult.value.advice) {
|
||||
dietAiAskResult.value = { ...dietAiAskResult.value, advice: payload.message || '正在想…' }
|
||||
} else if (event === 'delta') {
|
||||
dietAiAskResult.value = {
|
||||
...dietAiAskResult.value,
|
||||
advice: payload.advice || dietAiAskResult.value.advice
|
||||
}
|
||||
} else if (event === 'done') {
|
||||
dietAiAskResult.value = {
|
||||
advice: payload.advice || '',
|
||||
level: payload.level || '',
|
||||
level_label: payload.level_label || '',
|
||||
portion: payload.portion || '',
|
||||
food: payload.food || q
|
||||
}
|
||||
} else if (event === 'error') {
|
||||
showUserToast(formatUserMessage(payload?.message, '咨询失败'))
|
||||
}
|
||||
},
|
||||
fallback: () => askDietAiFoodFallback(q)
|
||||
})
|
||||
} catch (e) {
|
||||
try {
|
||||
await askDietAiFoodFallback(q)
|
||||
} catch (e2) {
|
||||
showUserToast('网络异常,请稍后再试')
|
||||
}
|
||||
} finally {
|
||||
dietAiAsking.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function onDietAiAskInput(e) {
|
||||
dietAiAskText.value = e?.detail?.value ?? ''
|
||||
}
|
||||
|
||||
function saveDietPrefillToStorage() {
|
||||
const r = dietAiRecommend.value
|
||||
if (!r.breakfast) return
|
||||
try {
|
||||
uni.setStorageSync(DIET_AI_PREFILL_KEY, {
|
||||
breakfast: r.breakfast,
|
||||
drinks: r.drinks || '',
|
||||
lunch: r.lunch,
|
||||
dinner: r.dinner,
|
||||
tips: r.tips || ''
|
||||
})
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
function goMoreDietForm(withPrefill = false) {
|
||||
if (withPrefill) saveDietPrefillToStorage()
|
||||
uni.navigateTo({ url: '/tongji/pages/more?openDiet=1' })
|
||||
}
|
||||
|
||||
async function applyDietAiToForm() {
|
||||
if (!dietAiRecommend.value.breakfast) {
|
||||
await fetchDietAiRecommend(false)
|
||||
}
|
||||
goMoreDietForm(true)
|
||||
}
|
||||
|
||||
function openDietForm() {
|
||||
goMoreDietForm(false)
|
||||
}
|
||||
|
||||
return {
|
||||
dietAiRecommend,
|
||||
dietAiLoading,
|
||||
dietAiStreaming,
|
||||
dietAiReplacing,
|
||||
dietAiStreamField,
|
||||
dietAiAskText,
|
||||
dietAiAsking,
|
||||
dietAiAskResult,
|
||||
hasDietAiMealContent,
|
||||
fetchDietAiRecommend,
|
||||
refreshDietAiRecommend,
|
||||
askDietAiFood,
|
||||
onDietAiAskInput,
|
||||
applyDietAiToForm,
|
||||
openDietForm
|
||||
}
|
||||
}
|
||||
|
||||
/** more 页打开饮食表单时读取并清除预填 */
|
||||
export function consumeDietAiPrefill() {
|
||||
try {
|
||||
const data = uni.getStorageSync(DIET_AI_PREFILL_KEY)
|
||||
uni.removeStorageSync(DIET_AI_PREFILL_KEY)
|
||||
if (data && typeof data === 'object' && data.breakfast) return data
|
||||
} catch (e) {}
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
@@ -1,115 +1,115 @@
|
||||
import { ref } from 'vue'
|
||||
import { formatUserMessage } from '../utils/tongjiHelpers.js'
|
||||
|
||||
/** 登录 / 就诊卡门禁(index 与 more 共用) */
|
||||
export function useTongjiAuth(proxy) {
|
||||
const authChecking = ref(false)
|
||||
let gateRedirected = false
|
||||
let authSessionPromise = null
|
||||
|
||||
function hasAuthToken() {
|
||||
return !!String(uni.getStorageSync('token') || '').trim()
|
||||
}
|
||||
|
||||
function clearAuthStorage() {
|
||||
uni.removeStorageSync('token')
|
||||
authSessionPromise = null
|
||||
}
|
||||
|
||||
function wxLoginGetCode() {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.login({
|
||||
provider: 'weixin',
|
||||
success: (res) => {
|
||||
if (res && res.code) resolve(res.code)
|
||||
else reject(new Error('微信登录未返回 code'))
|
||||
},
|
||||
fail: reject
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async function mnpLoginWithCode(code) {
|
||||
const res = await proxy.apiUrl({
|
||||
url: '/api/login/mnpLogin',
|
||||
method: 'POST',
|
||||
data: { code }
|
||||
}, false)
|
||||
if (res && res.code === 1 && res.data && res.data.token) {
|
||||
uni.setStorageSync('token', res.data.token)
|
||||
uni.setStorageSync('userData', res.data)
|
||||
return res.data
|
||||
}
|
||||
clearAuthStorage()
|
||||
throw new Error(formatUserMessage(res?.msg, '登录失败'))
|
||||
}
|
||||
|
||||
async function doWxLogin() {
|
||||
const code = await wxLoginGetCode()
|
||||
await mnpLoginWithCode(code)
|
||||
return hasAuthToken()
|
||||
}
|
||||
|
||||
async function verifyOrLogin() {
|
||||
if (!hasAuthToken()) {
|
||||
return doWxLogin()
|
||||
}
|
||||
try {
|
||||
const res = await proxy.apiUrl({ url: '/api/user/info', method: 'POST' }, false)
|
||||
if (res && res.code === 1 && res.data) {
|
||||
uni.setStorageSync('userData', res.data)
|
||||
return true
|
||||
}
|
||||
} catch (e) {
|
||||
/* 网络异常走重新登录 */
|
||||
}
|
||||
clearAuthStorage()
|
||||
try {
|
||||
return await doWxLogin()
|
||||
} catch (e2) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureLoggedIn() {
|
||||
if (authSessionPromise) {
|
||||
return authSessionPromise
|
||||
}
|
||||
authSessionPromise = verifyOrLogin()
|
||||
.then((ok) => {
|
||||
if (!ok) authSessionPromise = null
|
||||
return !!ok
|
||||
})
|
||||
.catch(() => {
|
||||
authSessionPromise = null
|
||||
return false
|
||||
})
|
||||
return authSessionPromise
|
||||
}
|
||||
|
||||
function redirectToCardEntry(returnPath) {
|
||||
if (gateRedirected) return
|
||||
gateRedirected = true
|
||||
const returnUrl = encodeURIComponent(returnPath || '/tongji/pages/index')
|
||||
uni.redirectTo({
|
||||
url: `/pages/Card/edit_card?add=1&returnUrl=${returnUrl}`
|
||||
})
|
||||
}
|
||||
|
||||
function resetGateRedirect() {
|
||||
gateRedirected = false
|
||||
}
|
||||
|
||||
function isGateRedirected() {
|
||||
return gateRedirected
|
||||
}
|
||||
|
||||
return {
|
||||
authChecking,
|
||||
hasAuthToken,
|
||||
ensureLoggedIn,
|
||||
redirectToCardEntry,
|
||||
resetGateRedirect,
|
||||
isGateRedirected
|
||||
}
|
||||
}
|
||||
import { ref } from 'vue'
|
||||
import { formatUserMessage } from '../utils/tongjiHelpers.js'
|
||||
|
||||
/** 登录 / 就诊卡门禁(index 与 more 共用) */
|
||||
export function useTongjiAuth(proxy) {
|
||||
const authChecking = ref(false)
|
||||
let gateRedirected = false
|
||||
let authSessionPromise = null
|
||||
|
||||
function hasAuthToken() {
|
||||
return !!String(uni.getStorageSync('token') || '').trim()
|
||||
}
|
||||
|
||||
function clearAuthStorage() {
|
||||
uni.removeStorageSync('token')
|
||||
authSessionPromise = null
|
||||
}
|
||||
|
||||
function wxLoginGetCode() {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.login({
|
||||
provider: 'weixin',
|
||||
success: (res) => {
|
||||
if (res && res.code) resolve(res.code)
|
||||
else reject(new Error('微信登录未返回 code'))
|
||||
},
|
||||
fail: reject
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async function mnpLoginWithCode(code) {
|
||||
const res = await proxy.apiUrl({
|
||||
url: '/api/login/mnpLogin',
|
||||
method: 'POST',
|
||||
data: { code }
|
||||
}, false)
|
||||
if (res && res.code === 1 && res.data && res.data.token) {
|
||||
uni.setStorageSync('token', res.data.token)
|
||||
uni.setStorageSync('userData', res.data)
|
||||
return res.data
|
||||
}
|
||||
clearAuthStorage()
|
||||
throw new Error(formatUserMessage(res?.msg, '登录失败'))
|
||||
}
|
||||
|
||||
async function doWxLogin() {
|
||||
const code = await wxLoginGetCode()
|
||||
await mnpLoginWithCode(code)
|
||||
return hasAuthToken()
|
||||
}
|
||||
|
||||
async function verifyOrLogin() {
|
||||
if (!hasAuthToken()) {
|
||||
return doWxLogin()
|
||||
}
|
||||
try {
|
||||
const res = await proxy.apiUrl({ url: '/api/user/info', method: 'POST' }, false)
|
||||
if (res && res.code === 1 && res.data) {
|
||||
uni.setStorageSync('userData', res.data)
|
||||
return true
|
||||
}
|
||||
} catch (e) {
|
||||
/* 网络异常走重新登录 */
|
||||
}
|
||||
clearAuthStorage()
|
||||
try {
|
||||
return await doWxLogin()
|
||||
} catch (e2) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureLoggedIn() {
|
||||
if (authSessionPromise) {
|
||||
return authSessionPromise
|
||||
}
|
||||
authSessionPromise = verifyOrLogin()
|
||||
.then((ok) => {
|
||||
if (!ok) authSessionPromise = null
|
||||
return !!ok
|
||||
})
|
||||
.catch(() => {
|
||||
authSessionPromise = null
|
||||
return false
|
||||
})
|
||||
return authSessionPromise
|
||||
}
|
||||
|
||||
function redirectToCardEntry(returnPath) {
|
||||
if (gateRedirected) return
|
||||
gateRedirected = true
|
||||
const returnUrl = encodeURIComponent(returnPath || '/tongji/pages/index')
|
||||
uni.redirectTo({
|
||||
url: `/pages/Card/edit_card?add=1&returnUrl=${returnUrl}`
|
||||
})
|
||||
}
|
||||
|
||||
function resetGateRedirect() {
|
||||
gateRedirected = false
|
||||
}
|
||||
|
||||
function isGateRedirected() {
|
||||
return gateRedirected
|
||||
}
|
||||
|
||||
return {
|
||||
authChecking,
|
||||
hasAuthToken,
|
||||
ensureLoggedIn,
|
||||
redirectToCardEntry,
|
||||
resetGateRedirect,
|
||||
isGateRedirected
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 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>
|
||||
</view>
|
||||
<view class="ai-diet-meal-list">
|
||||
@@ -434,6 +434,20 @@
|
||||
{{ dietAiRecommend.breakfast || (dietAiLoading ? '…' : '—') }}<text v-if="dietAiLoading && dietAiStreamField === 'breakfast'" class="ai-stream-cursor">▍</text>
|
||||
</text>
|
||||
</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-head">
|
||||
<view class="ai-diet-meal-icon" aria-hidden="true">
|
||||
@@ -2657,16 +2671,13 @@ async function bootstrap(options) {
|
||||
if (!target) target = list[0]
|
||||
await applyCard(target)
|
||||
} else {
|
||||
if (passedDiagnosisId) diagnosisId.value = passedDiagnosisId
|
||||
if (passedPatientId) patientId.value = passedPatientId
|
||||
if (passedPatientName) patientName.value = passedPatientName
|
||||
if (passedAge) age.value = passedAge
|
||||
if (passedGender) gender.value = passedGender
|
||||
if (!diagnosisId.value || !patientId.value) {
|
||||
readUserContext()
|
||||
}
|
||||
diagnosisId.value = 0
|
||||
patientId.value = 0
|
||||
patientName.value = ''
|
||||
age.value = 0
|
||||
gender.value = 0
|
||||
readUserMobile()
|
||||
if (!diagnosisId.value && userMobile.value) {
|
||||
if (userMobile.value) {
|
||||
await ensureDailyContextByPhone()
|
||||
}
|
||||
}
|
||||
@@ -6558,6 +6569,12 @@ async function onFamilyLike() {
|
||||
.ai-diet-meal-card.meal-breakfast::before {
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
background: linear-gradient(150deg, #6ee7b7 0%, #10b981 100%);
|
||||
}
|
||||
|
||||
+8515
-6706
File diff suppressed because it is too large
Load Diff
+2819
-2615
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,47 +1,51 @@
|
||||
/**
|
||||
* Google Stitch MCP · VitalMint Health
|
||||
* 设计 token 与 stitch-weekly.html tailwind.config 一一对应
|
||||
*/
|
||||
|
||||
.weekly-page {
|
||||
--primary: #006c49;
|
||||
--on-primary: #ffffff;
|
||||
--on-primary-fixed-variant: #005236;
|
||||
--primary-container: #10b981;
|
||||
--on-primary-container: #00422b;
|
||||
--inverse-primary: #4edea3;
|
||||
|
||||
--surface: #f4fbf4;
|
||||
--on-surface: #161d19;
|
||||
--on-surface-variant: #3c4a42;
|
||||
--surface-container-lowest: #ffffff;
|
||||
--surface-container-low: #eef6ee;
|
||||
--surface-container: #e8f0e9;
|
||||
--surface-container-high: #e3eae3;
|
||||
--surface-container-highest: #dde4dd;
|
||||
--surface-variant: #dde4dd;
|
||||
|
||||
--outline: #6c7a71;
|
||||
--outline-variant: #bbcabf;
|
||||
|
||||
--error: #ba1a1a;
|
||||
--on-error-container: #93000a;
|
||||
--error-container: #ffdad6;
|
||||
--tertiary: #a43a3a;
|
||||
--tertiary-container: #fc7c78;
|
||||
|
||||
--st-container-margin: 40rpx;
|
||||
--st-section-gap: 64rpx;
|
||||
--st-card-padding: 40rpx;
|
||||
--st-stack-gap: 32rpx;
|
||||
--st-inline-gap: 24rpx;
|
||||
--st-radius-2xl: 48rpx;
|
||||
--st-radius-xl: 24rpx;
|
||||
--st-radius-lg: 16rpx;
|
||||
--st-shadow-ambient: 0 20rpx 60rpx -20rpx rgba(0, 108, 73, 0.08);
|
||||
|
||||
min-height: 100vh;
|
||||
background: var(--surface);
|
||||
color: var(--on-surface);
|
||||
padding-bottom: calc(200rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
/**
|
||||
* Google Stitch MCP · VitalMint Health
|
||||
* 设计 token 与 stitch-weekly.html tailwind.config 一一对应
|
||||
*/
|
||||
|
||||
.stitch-vitalmint {
|
||||
--primary: #006c49;
|
||||
--on-primary: #ffffff;
|
||||
--on-primary-fixed-variant: #005236;
|
||||
--primary-container: #10b981;
|
||||
--on-primary-container: #00422b;
|
||||
--inverse-primary: #4edea3;
|
||||
|
||||
--surface: #f4fbf4;
|
||||
--on-surface: #161d19;
|
||||
--on-surface-variant: #3c4a42;
|
||||
--surface-container-lowest: #ffffff;
|
||||
--surface-container-low: #eef6ee;
|
||||
--surface-container: #e8f0e9;
|
||||
--surface-container-high: #e3eae3;
|
||||
--surface-container-highest: #dde4dd;
|
||||
--surface-variant: #dde4dd;
|
||||
|
||||
--outline: #6c7a71;
|
||||
--outline-variant: #bbcabf;
|
||||
|
||||
--error: #ba1a1a;
|
||||
--on-error-container: #93000a;
|
||||
--error-container: #ffdad6;
|
||||
--tertiary: #a43a3a;
|
||||
--tertiary-container: #fc7c78;
|
||||
--on-tertiary: #ffffff;
|
||||
--on-tertiary-container: #711419;
|
||||
|
||||
--st-container-margin: 40rpx;
|
||||
--st-section-gap: 64rpx;
|
||||
--st-card-padding: 40rpx;
|
||||
--st-stack-gap: 32rpx;
|
||||
--st-inline-gap: 24rpx;
|
||||
--st-radius-2xl: 48rpx;
|
||||
--st-radius-xl: 24rpx;
|
||||
--st-radius-lg: 16rpx;
|
||||
--st-shadow-ambient: 0 20rpx 60rpx -20rpx rgba(0, 108, 73, 0.08);
|
||||
}
|
||||
|
||||
.weekly-page.stitch-vitalmint {
|
||||
min-height: 100vh;
|
||||
background: var(--surface);
|
||||
color: var(--on-surface);
|
||||
padding-bottom: calc(200rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
inset: 0;
|
||||
background: rgba(15, 23, 42, 0.45);
|
||||
backdrop-filter: blur(12px);
|
||||
z-index: 999;
|
||||
z-index: 2000;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
@@ -89,6 +89,22 @@
|
||||
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 {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -64,10 +64,10 @@ export function parsePartialDietPlainText(text) {
|
||||
const out = {}
|
||||
if (!text) return out
|
||||
|
||||
const lineRe = /^(早餐|午餐|晚餐|提示|少碰)[::]\s*(.*)$/gm
|
||||
const lineRe = /^(早餐|喝的|午餐|晚餐|提示|少碰)[::]\s*(.*)$/gm
|
||||
let match
|
||||
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()
|
||||
if (key === 'avoid') {
|
||||
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) {
|
||||
const key = { '早餐': 'breakfast', '午餐': 'lunch', '晚餐': 'dinner', '提示': 'tips', '少碰': 'avoid' }[tail[1]]
|
||||
const key = { '早餐': 'breakfast', '喝的': 'drinks', '午餐': 'lunch', '晚餐': 'dinner', '提示': 'tips', '少碰': 'avoid' }[tail[1]]
|
||||
const val = (tail[2] || '').trim()
|
||||
if (key === 'avoid') {
|
||||
if (val) out.avoid = val.split(/[、,,;;\s]+/).map((s) => s.trim()).filter(Boolean)
|
||||
|
||||
@@ -1,56 +1,56 @@
|
||||
/** 将接口 msg / 异常对象转为可展示的字符串,避免 [object Object] */
|
||||
export function formatUserMessage(msg, fallback = '') {
|
||||
if (msg == null || msg === '') return fallback
|
||||
if (typeof msg === 'string') return msg
|
||||
if (typeof msg === 'number' || typeof msg === 'boolean') return String(msg)
|
||||
if (Array.isArray(msg)) {
|
||||
const parts = msg.map((item) => {
|
||||
if (item == null) return ''
|
||||
if (typeof item === 'string') return item
|
||||
if (typeof item === 'number' || typeof item === 'boolean') return String(item)
|
||||
if (typeof item === 'object') {
|
||||
return item.msg || item.message || item.error || item.title || item.label || ''
|
||||
}
|
||||
return String(item)
|
||||
}).filter(Boolean)
|
||||
return parts.length ? parts.join(';') : fallback
|
||||
}
|
||||
if (typeof msg === 'object') {
|
||||
return msg.msg || msg.message || msg.error || msg.title || msg.label || fallback
|
||||
}
|
||||
return String(msg)
|
||||
}
|
||||
|
||||
export function showUserToast(title, options = {}) {
|
||||
const text = formatUserMessage(title, '')
|
||||
if (!text) return
|
||||
uni.showToast({
|
||||
title: text,
|
||||
icon: options.icon || 'none',
|
||||
duration: options.duration
|
||||
})
|
||||
}
|
||||
|
||||
export function formatDate(date) {
|
||||
const y = date.getFullYear()
|
||||
const m = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const d = String(date.getDate()).padStart(2, '0')
|
||||
return `${y}-${m}-${d}`
|
||||
}
|
||||
|
||||
export function parseDate(str) {
|
||||
if (!str) return new Date()
|
||||
const [y, m, d] = str.split('-').map(Number)
|
||||
return new Date(y, (m || 1) - 1, d || 1)
|
||||
}
|
||||
|
||||
export function toNumber(v) {
|
||||
if (v === null || v === undefined || v === '') return null
|
||||
const n = Number(v)
|
||||
return Number.isFinite(n) && n !== 0 ? n : null
|
||||
}
|
||||
|
||||
export function getWeekday(dateStr) {
|
||||
const w = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
|
||||
return w[parseDate(dateStr).getDay()]
|
||||
}
|
||||
/** 将接口 msg / 异常对象转为可展示的字符串,避免 [object Object] */
|
||||
export function formatUserMessage(msg, fallback = '') {
|
||||
if (msg == null || msg === '') return fallback
|
||||
if (typeof msg === 'string') return msg
|
||||
if (typeof msg === 'number' || typeof msg === 'boolean') return String(msg)
|
||||
if (Array.isArray(msg)) {
|
||||
const parts = msg.map((item) => {
|
||||
if (item == null) return ''
|
||||
if (typeof item === 'string') return item
|
||||
if (typeof item === 'number' || typeof item === 'boolean') return String(item)
|
||||
if (typeof item === 'object') {
|
||||
return item.msg || item.message || item.error || item.title || item.label || ''
|
||||
}
|
||||
return String(item)
|
||||
}).filter(Boolean)
|
||||
return parts.length ? parts.join(';') : fallback
|
||||
}
|
||||
if (typeof msg === 'object') {
|
||||
return msg.msg || msg.message || msg.error || msg.title || msg.label || fallback
|
||||
}
|
||||
return String(msg)
|
||||
}
|
||||
|
||||
export function showUserToast(title, options = {}) {
|
||||
const text = formatUserMessage(title, '')
|
||||
if (!text) return
|
||||
uni.showToast({
|
||||
title: text,
|
||||
icon: options.icon || 'none',
|
||||
duration: options.duration
|
||||
})
|
||||
}
|
||||
|
||||
export function formatDate(date) {
|
||||
const y = date.getFullYear()
|
||||
const m = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const d = String(date.getDate()).padStart(2, '0')
|
||||
return `${y}-${m}-${d}`
|
||||
}
|
||||
|
||||
export function parseDate(str) {
|
||||
if (!str) return new Date()
|
||||
const [y, m, d] = str.split('-').map(Number)
|
||||
return new Date(y, (m || 1) - 1, d || 1)
|
||||
}
|
||||
|
||||
export function toNumber(v) {
|
||||
if (v === null || v === undefined || v === '') return null
|
||||
const n = Number(v)
|
||||
return Number.isFinite(n) && n !== 0 ? n : null
|
||||
}
|
||||
|
||||
export function getWeekday(dateStr) {
|
||||
const w = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
|
||||
return w[parseDate(dateStr).getDay()]
|
||||
}
|
||||
|
||||
@@ -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 uni from '@dcloudio/vite-plugin-uni'
|
||||
import Optimization from '@uni-ku/bundle-optimizer'
|
||||
|
||||
// uni-app 工程根目录就是源码目录(HBuilderX 兼容)
|
||||
// 编译产物默认输出到 ./dist/<mode>/<platform>
|
||||
export default defineConfig({
|
||||
plugins: [uni()],
|
||||
plugins: [
|
||||
uni(),
|
||||
Optimization({
|
||||
enable: {
|
||||
optimization: true,
|
||||
'async-import': false,
|
||||
'async-component': false,
|
||||
},
|
||||
dts: false,
|
||||
logger: false,
|
||||
}),
|
||||
],
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
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
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user