Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
@@ -22,5 +22,3 @@ unpackage/dist
|
|||||||
*.sw?
|
*.sw?
|
||||||
|
|
||||||
TUICallKit
|
TUICallKit
|
||||||
# 本工程的通话源码复用仓库内 ../wx/TUICallKit;保留相对软链接以便干净克隆后可直接构建。
|
|
||||||
!TUICallKit
|
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
../wx/TUICallKit
|
|
||||||
@@ -63,7 +63,7 @@
|
|||||||
"requiredBackgroundModes" : [ "audio" ],
|
"requiredBackgroundModes" : [ "audio" ],
|
||||||
"plugins" : {
|
"plugins" : {
|
||||||
"WechatSI" : {
|
"WechatSI" : {
|
||||||
"version" : "0.3.9",
|
"version" : "0.3.5",
|
||||||
"provider" : "wx069ba97219f66d99"
|
"provider" : "wx069ba97219f66d99"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,243 +1,243 @@
|
|||||||
{
|
{
|
||||||
"pages": [
|
"pages": [
|
||||||
{
|
{
|
||||||
"path": "pages/index/index",
|
"path": "pages/index/index",
|
||||||
"style": {
|
"style": {
|
||||||
"navigationBarTitleText": "甄养堂",
|
"navigationBarTitleText": "甄养堂",
|
||||||
"backgroundColor": "#F8F8F8",
|
"backgroundColor": "#F8F8F8",
|
||||||
"backgroundColorTop": "#F8F8F8"
|
"backgroundColorTop": "#F8F8F8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "pages/login/login",
|
"path": "pages/login/login",
|
||||||
"style": {
|
"style": {
|
||||||
"navigationBarTitleText": "视频问诊"
|
"navigationBarTitleText": "视频问诊"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "pages/user/userinfo",
|
"path": "pages/user/userinfo",
|
||||||
"style": {
|
"style": {
|
||||||
"navigationBarTitleText": "个人资料"
|
"navigationBarTitleText": "个人资料"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "pages/index/video",
|
"path": "pages/index/video",
|
||||||
"style": {
|
"style": {
|
||||||
"navigationBarTitleText": "甄养堂"
|
"navigationBarTitleText": "甄养堂"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "pages/order/monad/monad",
|
"path": "pages/order/monad/monad",
|
||||||
"style": {
|
"style": {
|
||||||
"navigationBarTitleText": "诊单确认",
|
"navigationBarTitleText": "诊单确认",
|
||||||
"navigationBarHidden": true,
|
"navigationBarHidden": true,
|
||||||
"navigationStyle": "custom"
|
"navigationStyle": "custom"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "pages/user/user",
|
"path": "pages/user/user",
|
||||||
"style": {
|
"style": {
|
||||||
"navigationBarTitleText": "我的",
|
"navigationBarTitleText": "我的",
|
||||||
"backgroundColor": "#e8eaed",
|
"backgroundColor": "#e8eaed",
|
||||||
"backgroundColorTop": "#e8eaed"
|
"backgroundColorTop": "#e8eaed"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "pages/Card/Card",
|
"path": "pages/Card/Card",
|
||||||
"style": {
|
"style": {
|
||||||
"navigationBarTitleText": "就诊卡"
|
"navigationBarTitleText": "就诊卡"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "pages/Card/edit_card",
|
"path": "pages/Card/edit_card",
|
||||||
"style": {
|
"style": {
|
||||||
"navigationBarTitleText": "修改就诊卡"
|
"navigationBarTitleText": "修改就诊卡"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "pages/Card/contact",
|
"path": "pages/Card/contact",
|
||||||
"style": {
|
"style": {
|
||||||
"navigationBarTitleText": "服务"
|
"navigationBarTitleText": "服务"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "pages/order/order",
|
"path": "pages/order/order",
|
||||||
"style": {
|
"style": {
|
||||||
"navigationBarTitleText": "付款"
|
"navigationBarTitleText": "付款"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"subPackages": [
|
"subPackages": [
|
||||||
{
|
{
|
||||||
"root": "TUICallKit/src/Components",
|
"root": "TUICallKit/src/Components",
|
||||||
"pages": [
|
"pages": [
|
||||||
{
|
{
|
||||||
"path": "TUICallKit",
|
"path": "TUICallKit",
|
||||||
"style": {
|
"style": {
|
||||||
"navigationBarTitleText": "视频问诊",
|
"navigationBarTitleText": "视频问诊",
|
||||||
"navigationBarHidden": true,
|
"navigationBarHidden": true,
|
||||||
"navigationStyle": "custom"
|
"navigationStyle": "custom"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"root": "TUIKit",
|
"root": "TUIKit",
|
||||||
"pages": [
|
"pages": [
|
||||||
{
|
{
|
||||||
"path": "pages/chat/chat",
|
"path": "pages/chat/chat",
|
||||||
"style": {
|
"style": {
|
||||||
"navigationBarTitleText": "问诊"
|
"navigationBarTitleText": "问诊"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"root": "doctor",
|
"root": "doctor",
|
||||||
"pages": [
|
"pages": [
|
||||||
{
|
{
|
||||||
"path": "pages/doctor/doctor",
|
"path": "pages/doctor/doctor",
|
||||||
"style": {
|
"style": {
|
||||||
"navigationBarTitleText": "医生信息"
|
"navigationBarTitleText": "医生信息"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "pages/doctor/list/list",
|
"path": "pages/doctor/list/list",
|
||||||
"style": {
|
"style": {
|
||||||
"navigationBarTitleText": "执业医生列表"
|
"navigationBarTitleText": "执业医生列表"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "pages/article/article",
|
"path": "pages/article/article",
|
||||||
"style": {
|
"style": {
|
||||||
"navigationBarTitleText": "健康百科"
|
"navigationBarTitleText": "健康百科"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"root": "training",
|
"root": "training",
|
||||||
"pages": [
|
"pages": [
|
||||||
{
|
{
|
||||||
"path": "pages/grip-ring",
|
"path": "pages/grip-ring",
|
||||||
"style": {
|
"style": {
|
||||||
"navigationBarTitleText": "握力环训练",
|
"navigationBarTitleText": "握力环训练",
|
||||||
"navigationBarBackgroundColor": "#f8fafc",
|
"navigationBarBackgroundColor": "#f8fafc",
|
||||||
"navigationBarTextStyle": "black",
|
"navigationBarTextStyle": "black",
|
||||||
"backgroundColor": "#f8fafc"
|
"backgroundColor": "#f8fafc"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "pages/pilates-ring",
|
"path": "pages/pilates-ring",
|
||||||
"style": {
|
"style": {
|
||||||
"navigationBarTitleText": "瑜伽环训练",
|
"navigationBarTitleText": "瑜伽环训练",
|
||||||
"navigationBarBackgroundColor": "#f8fafc",
|
"navigationBarBackgroundColor": "#f8fafc",
|
||||||
"navigationBarTextStyle": "black",
|
"navigationBarTextStyle": "black",
|
||||||
"backgroundColor": "#f8fafc"
|
"backgroundColor": "#f8fafc"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "pages/foot-pedal",
|
"path": "pages/foot-pedal",
|
||||||
"style": {
|
"style": {
|
||||||
"navigationBarTitleText": "脚蹬器训练",
|
"navigationBarTitleText": "脚蹬器训练",
|
||||||
"navigationBarBackgroundColor": "#f8fafc",
|
"navigationBarBackgroundColor": "#f8fafc",
|
||||||
"navigationBarTextStyle": "black",
|
"navigationBarTextStyle": "black",
|
||||||
"backgroundColor": "#f8fafc"
|
"backgroundColor": "#f8fafc"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "pages/dumbbell",
|
"path": "pages/dumbbell",
|
||||||
"style": {
|
"style": {
|
||||||
"navigationBarTitleText": "哑铃训练",
|
"navigationBarTitleText": "哑铃训练",
|
||||||
"navigationBarBackgroundColor": "#f8fafc",
|
"navigationBarBackgroundColor": "#f8fafc",
|
||||||
"navigationBarTextStyle": "black",
|
"navigationBarTextStyle": "black",
|
||||||
"backgroundColor": "#f8fafc"
|
"backgroundColor": "#f8fafc"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "pages/metronome",
|
"path": "pages/metronome",
|
||||||
"style": {
|
"style": {
|
||||||
"navigationBarTitleText": "耗糖节拍器",
|
"navigationBarTitleText": "耗糖节拍器",
|
||||||
"navigationBarBackgroundColor": "#f8fafc",
|
"navigationBarBackgroundColor": "#f8fafc",
|
||||||
"navigationBarTextStyle": "black",
|
"navigationBarTextStyle": "black",
|
||||||
"backgroundColor": "#f8fafc"
|
"backgroundColor": "#f8fafc"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"root": "tongji",
|
"root": "tongji",
|
||||||
"pages": [
|
"pages": [
|
||||||
{
|
{
|
||||||
"path": "pages/index",
|
"path": "pages/index",
|
||||||
"style": {
|
"style": {
|
||||||
"navigationBarTitleText": "血糖记录",
|
"navigationBarTitleText": "血糖记录",
|
||||||
"navigationBarBackgroundColor": "#204e2b",
|
"navigationBarBackgroundColor": "#204e2b",
|
||||||
"navigationBarTextStyle": "white",
|
"navigationBarTextStyle": "white",
|
||||||
"backgroundColor": "#faf9f5",
|
"backgroundColor": "#faf9f5",
|
||||||
"enablePullDownRefresh": true,
|
"enablePullDownRefresh": true,
|
||||||
"mp-weixin": {
|
"mp-weixin": {
|
||||||
"usingPlugins": {
|
"usingPlugins": {
|
||||||
"WechatSI": {
|
"WechatSI": {
|
||||||
"version": "0.3.9",
|
"version": "0.3.5",
|
||||||
"provider": "wx069ba97219f66d99"
|
"provider": "wx069ba97219f66d99"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "pages/weekly",
|
"path": "pages/weekly",
|
||||||
"style": {
|
"style": {
|
||||||
"navigationStyle": "custom",
|
"navigationStyle": "custom",
|
||||||
"navigationBarTitleText": "血糖管理",
|
"navigationBarTitleText": "血糖管理",
|
||||||
"backgroundColor": "#f4fbf4",
|
"backgroundColor": "#f4fbf4",
|
||||||
"enablePullDownRefresh": true,
|
"enablePullDownRefresh": true,
|
||||||
"mp-weixin": {
|
"mp-weixin": {
|
||||||
"usingPlugins": {
|
"usingPlugins": {
|
||||||
"WechatSI": {
|
"WechatSI": {
|
||||||
"version": "0.3.9",
|
"version": "0.3.5",
|
||||||
"provider": "wx069ba97219f66d99"
|
"provider": "wx069ba97219f66d99"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "pages/more",
|
"path": "pages/more",
|
||||||
"style": {
|
"style": {
|
||||||
"navigationStyle": "custom",
|
"navigationStyle": "custom",
|
||||||
"navigationBarTitleText": "日常护理",
|
"navigationBarTitleText": "日常护理",
|
||||||
"backgroundColor": "#f4fbf4",
|
"backgroundColor": "#f4fbf4",
|
||||||
"enablePullDownRefresh": true,
|
"enablePullDownRefresh": true,
|
||||||
"mp-weixin": {
|
"mp-weixin": {
|
||||||
"usingPlugins": {
|
"usingPlugins": {
|
||||||
"WechatSI": {
|
"WechatSI": {
|
||||||
"version": "0.3.9",
|
"version": "0.3.5",
|
||||||
"provider": "wx069ba97219f66d99"
|
"provider": "wx069ba97219f66d99"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "endless-game/index",
|
"path": "endless-game/index",
|
||||||
"style": {
|
"style": {
|
||||||
"navigationStyle": "custom",
|
"navigationStyle": "custom",
|
||||||
"navigationBarTitleText": "识糖小课堂",
|
"navigationBarTitleText": "识糖小课堂",
|
||||||
"backgroundColor": "#eefbf4",
|
"backgroundColor": "#eefbf4",
|
||||||
"disableScroll": false,
|
"disableScroll": false,
|
||||||
"enableShareAppMessage": true,
|
"enableShareAppMessage": true,
|
||||||
"enableShareTimeline": true
|
"enableShareTimeline": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"globalStyle": {
|
"globalStyle": {
|
||||||
"navigationBarTextStyle": "black",
|
"navigationBarTextStyle": "black",
|
||||||
"navigationBarTitleText": "甄养堂互联网医院",
|
"navigationBarTitleText": "甄养堂互联网医院",
|
||||||
"navigationBarBackgroundColor": "#F8F8F8",
|
"navigationBarBackgroundColor": "#F8F8F8",
|
||||||
"backgroundColor": "#F8F8F8"
|
"backgroundColor": "#F8F8F8"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,317 +1,317 @@
|
|||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import { requestAiStream, parsePartialDietPlainText } from '../utils/aiStreamRequest.js'
|
import { requestAiStream, parsePartialDietPlainText } from '../utils/aiStreamRequest.js'
|
||||||
|
|
||||||
const DIET_AI_PREFILL_KEY = 'tongji_diet_ai_prefill'
|
const DIET_AI_PREFILL_KEY = 'tongji_diet_ai_prefill'
|
||||||
|
|
||||||
const emptyDietAiRecommend = () => ({
|
const emptyDietAiRecommend = () => ({
|
||||||
breakfast: '',
|
breakfast: '',
|
||||||
drinks: '',
|
drinks: '',
|
||||||
lunch: '',
|
lunch: '',
|
||||||
dinner: '',
|
dinner: '',
|
||||||
tips: '',
|
tips: '',
|
||||||
avoid: [],
|
avoid: [],
|
||||||
analysis: [],
|
analysis: [],
|
||||||
exercise: null,
|
exercise: null,
|
||||||
disclaimer: '',
|
disclaimer: '',
|
||||||
rules_summary: '',
|
rules_summary: '',
|
||||||
source: '',
|
source: '',
|
||||||
date: ''
|
date: ''
|
||||||
})
|
})
|
||||||
|
|
||||||
/** AI 饮食助手(今日推荐 + 能不能吃) */
|
/** AI 饮食助手(今日推荐 + 能不能吃) */
|
||||||
export function useDietAi(proxy, { diagnosisId, showUserToast, formatUserMessage }) {
|
export function useDietAi(proxy, { diagnosisId, showUserToast, formatUserMessage }) {
|
||||||
const dietAiRecommend = ref(emptyDietAiRecommend())
|
const dietAiRecommend = ref(emptyDietAiRecommend())
|
||||||
const dietAiLoading = ref(false)
|
const dietAiLoading = ref(false)
|
||||||
const dietAiStreaming = ref(false)
|
const dietAiStreaming = ref(false)
|
||||||
const dietAiReplacing = ref(false)
|
const dietAiReplacing = ref(false)
|
||||||
const dietAiStreamField = ref('')
|
const dietAiStreamField = ref('')
|
||||||
let dietStreamBuffer = ''
|
let dietStreamBuffer = ''
|
||||||
const dietAiAskText = ref('')
|
const dietAiAskText = ref('')
|
||||||
const dietAiAsking = ref(false)
|
const dietAiAsking = ref(false)
|
||||||
const dietAiAskResult = ref({ advice: '', level: '', level_label: '', portion: '', food: '' })
|
const dietAiAskResult = ref({ advice: '', level: '', level_label: '', portion: '', food: '' })
|
||||||
|
|
||||||
const hasDietAiMealContent = computed(() => {
|
const hasDietAiMealContent = computed(() => {
|
||||||
const r = dietAiRecommend.value
|
const r = dietAiRecommend.value
|
||||||
return !!(r.breakfast || r.drinks || r.lunch || r.dinner || r.tips)
|
return !!(r.breakfast || r.drinks || r.lunch || r.dinner || r.tips)
|
||||||
})
|
})
|
||||||
|
|
||||||
function detectDietStreamField(text) {
|
function detectDietStreamField(text) {
|
||||||
const tail = String(text || '').match(/(?:^|\n)(早餐|喝的|午餐|晚餐|提示|少碰)[::]\s*([^\n]*)$/)
|
const tail = String(text || '').match(/(?:^|\n)(早餐|喝的|午餐|晚餐|提示|少碰)[::]\s*([^\n]*)$/)
|
||||||
if (!tail) return 'breakfast'
|
if (!tail) return 'breakfast'
|
||||||
const map = { '早餐': 'breakfast', '喝的': 'drinks', '午餐': 'lunch', '晚餐': 'dinner', '提示': 'tips', '少碰': 'tips' }
|
const map = { '早餐': 'breakfast', '喝的': 'drinks', '午餐': 'lunch', '晚餐': 'dinner', '提示': 'tips', '少碰': 'tips' }
|
||||||
return map[tail[1]] || 'breakfast'
|
return map[tail[1]] || 'breakfast'
|
||||||
}
|
}
|
||||||
|
|
||||||
function mergePartialDietFromStream() {
|
function mergePartialDietFromStream() {
|
||||||
const partial = parsePartialDietPlainText(dietStreamBuffer)
|
const partial = parsePartialDietPlainText(dietStreamBuffer)
|
||||||
const next = { ...dietAiRecommend.value }
|
const next = { ...dietAiRecommend.value }
|
||||||
for (const key of ['breakfast', 'drinks', 'lunch', 'dinner', 'tips']) {
|
for (const key of ['breakfast', 'drinks', 'lunch', 'dinner', 'tips']) {
|
||||||
if (partial[key] != null) next[key] = partial[key]
|
if (partial[key] != null) next[key] = partial[key]
|
||||||
}
|
}
|
||||||
if (Array.isArray(partial.avoid)) next.avoid = partial.avoid
|
if (Array.isArray(partial.avoid)) next.avoid = partial.avoid
|
||||||
dietAiRecommend.value = next
|
dietAiRecommend.value = next
|
||||||
dietAiStreamField.value = detectDietStreamField(dietStreamBuffer)
|
dietAiStreamField.value = detectDietStreamField(dietStreamBuffer)
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyDietRecommendPayload(payload) {
|
function applyDietRecommendPayload(payload) {
|
||||||
if (!payload) return
|
if (!payload) return
|
||||||
const prevAnalysis = dietAiRecommend.value.analysis
|
const prevAnalysis = dietAiRecommend.value.analysis
|
||||||
const prevExercise = dietAiRecommend.value.exercise
|
const prevExercise = dietAiRecommend.value.exercise
|
||||||
dietAiRecommend.value = {
|
dietAiRecommend.value = {
|
||||||
...emptyDietAiRecommend(),
|
...emptyDietAiRecommend(),
|
||||||
...payload,
|
...payload,
|
||||||
avoid: Array.isArray(payload.avoid) ? payload.avoid : [],
|
avoid: Array.isArray(payload.avoid) ? payload.avoid : [],
|
||||||
analysis: Array.isArray(payload.analysis) && payload.analysis.length
|
analysis: Array.isArray(payload.analysis) && payload.analysis.length
|
||||||
? payload.analysis
|
? payload.analysis
|
||||||
: (Array.isArray(prevAnalysis) ? prevAnalysis : []),
|
: (Array.isArray(prevAnalysis) ? prevAnalysis : []),
|
||||||
exercise: (payload.exercise && typeof payload.exercise === 'object')
|
exercise: (payload.exercise && typeof payload.exercise === 'object')
|
||||||
? payload.exercise
|
? payload.exercise
|
||||||
: (prevExercise || null)
|
: (prevExercise || null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveDietDonePayload(payload) {
|
function resolveDietDonePayload(payload) {
|
||||||
if (!payload) return payload
|
if (!payload) return payload
|
||||||
if (payload.source !== 'rule' || !dietStreamBuffer) return payload
|
if (payload.source !== 'rule' || !dietStreamBuffer) return payload
|
||||||
|
|
||||||
const streamed = parsePartialDietPlainText(dietStreamBuffer)
|
const streamed = parsePartialDietPlainText(dietStreamBuffer)
|
||||||
if (!streamed.breakfast) return payload
|
if (!streamed.breakfast) return payload
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...emptyDietAiRecommend(),
|
...emptyDietAiRecommend(),
|
||||||
...payload,
|
...payload,
|
||||||
breakfast: streamed.breakfast,
|
breakfast: streamed.breakfast,
|
||||||
drinks: streamed.drinks || payload.drinks || '',
|
drinks: streamed.drinks || payload.drinks || '',
|
||||||
lunch: streamed.lunch || payload.lunch || '',
|
lunch: streamed.lunch || payload.lunch || '',
|
||||||
dinner: streamed.dinner || payload.dinner || '',
|
dinner: streamed.dinner || payload.dinner || '',
|
||||||
tips: streamed.tips || payload.tips || '',
|
tips: streamed.tips || payload.tips || '',
|
||||||
avoid: (Array.isArray(streamed.avoid) && streamed.avoid.length) ? streamed.avoid : (payload.avoid || []),
|
avoid: (Array.isArray(streamed.avoid) && streamed.avoid.length) ? streamed.avoid : (payload.avoid || []),
|
||||||
source: 'ai',
|
source: 'ai',
|
||||||
disclaimer: 'AI 建议供参考,仍请结合医嘱与血糖监测。'
|
disclaimer: 'AI 建议供参考,仍请结合医嘱与血糖监测。'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onDietAiStreamEvent(event, payload) {
|
function onDietAiStreamEvent(event, payload) {
|
||||||
if (event === 'analysis') {
|
if (event === 'analysis') {
|
||||||
if (Array.isArray(payload?.analysis)) {
|
if (Array.isArray(payload?.analysis)) {
|
||||||
dietAiRecommend.value = { ...dietAiRecommend.value, analysis: payload.analysis }
|
dietAiRecommend.value = { ...dietAiRecommend.value, analysis: payload.analysis }
|
||||||
}
|
}
|
||||||
} else if (event === 'exercise') {
|
} else if (event === 'exercise') {
|
||||||
if (payload?.exercise && typeof payload.exercise === 'object') {
|
if (payload?.exercise && typeof payload.exercise === 'object') {
|
||||||
dietAiRecommend.value = { ...dietAiRecommend.value, exercise: payload.exercise }
|
dietAiRecommend.value = { ...dietAiRecommend.value, exercise: payload.exercise }
|
||||||
}
|
}
|
||||||
} else if (event === 'delta') {
|
} else if (event === 'delta') {
|
||||||
dietAiStreaming.value = true
|
dietAiStreaming.value = true
|
||||||
if (payload.buffer != null) {
|
if (payload.buffer != null) {
|
||||||
dietStreamBuffer = payload.buffer
|
dietStreamBuffer = payload.buffer
|
||||||
} else {
|
} else {
|
||||||
dietStreamBuffer += payload.text || ''
|
dietStreamBuffer += payload.text || ''
|
||||||
}
|
}
|
||||||
mergePartialDietFromStream()
|
mergePartialDietFromStream()
|
||||||
} else if (event === 'done') {
|
} else if (event === 'done') {
|
||||||
applyDietRecommendPayload(resolveDietDonePayload(payload))
|
applyDietRecommendPayload(resolveDietDonePayload(payload))
|
||||||
dietStreamBuffer = ''
|
dietStreamBuffer = ''
|
||||||
dietAiStreamField.value = ''
|
dietAiStreamField.value = ''
|
||||||
} else if (event === 'error') {
|
} else if (event === 'error') {
|
||||||
showUserToast(formatUserMessage(payload?.message, '饮食建议加载失败'))
|
showUserToast(formatUserMessage(payload?.message, '饮食建议加载失败'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchDietAiRecommendFallback(refresh) {
|
async function fetchDietAiRecommendFallback(refresh) {
|
||||||
const res = await proxy.apiUrl({
|
const res = await proxy.apiUrl({
|
||||||
url: '/api/tcm/dailyDietAiRecommend',
|
url: '/api/tcm/dailyDietAiRecommend',
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
timeout: 35000,
|
timeout: 35000,
|
||||||
data: {
|
data: {
|
||||||
diagnosis_id: diagnosisId.value,
|
diagnosis_id: diagnosisId.value,
|
||||||
refresh: refresh ? 1 : 0,
|
refresh: refresh ? 1 : 0,
|
||||||
...(refresh ? { _t: Date.now() } : {})
|
...(refresh ? { _t: Date.now() } : {})
|
||||||
}
|
}
|
||||||
}, false)
|
}, false)
|
||||||
if (res?.code === 1 && res.data) {
|
if (res?.code === 1 && res.data) {
|
||||||
applyDietRecommendPayload(res.data)
|
applyDietRecommendPayload(res.data)
|
||||||
} else if (res?.msg) {
|
} else if (res?.msg) {
|
||||||
showUserToast(res.msg)
|
showUserToast(res.msg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchDietAiRecommend(refresh = false) {
|
async function fetchDietAiRecommend(refresh = false) {
|
||||||
if (!diagnosisId.value || dietAiLoading.value) return
|
if (!diagnosisId.value || dietAiLoading.value) return
|
||||||
dietAiLoading.value = true
|
dietAiLoading.value = true
|
||||||
dietAiStreaming.value = false
|
dietAiStreaming.value = false
|
||||||
dietAiReplacing.value = !!refresh
|
dietAiReplacing.value = !!refresh
|
||||||
dietStreamBuffer = ''
|
dietStreamBuffer = ''
|
||||||
dietAiStreamField.value = ''
|
dietAiStreamField.value = ''
|
||||||
if (refresh) {
|
if (refresh) {
|
||||||
applyDietRecommendPayload(emptyDietAiRecommend())
|
applyDietRecommendPayload(emptyDietAiRecommend())
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await requestAiStream({
|
await requestAiStream({
|
||||||
baseUrl: proxy.$url,
|
baseUrl: proxy.$url,
|
||||||
url: '/api/tcm/dailyDietAiRecommendStream',
|
url: '/api/tcm/dailyDietAiRecommendStream',
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
data: {
|
data: {
|
||||||
diagnosis_id: diagnosisId.value,
|
diagnosis_id: diagnosisId.value,
|
||||||
refresh: refresh ? 1 : 0,
|
refresh: refresh ? 1 : 0,
|
||||||
...(refresh ? { _t: Date.now() } : {})
|
...(refresh ? { _t: Date.now() } : {})
|
||||||
},
|
},
|
||||||
onEvent: onDietAiStreamEvent,
|
onEvent: onDietAiStreamEvent,
|
||||||
fallback: () => fetchDietAiRecommendFallback(refresh)
|
fallback: () => fetchDietAiRecommendFallback(refresh)
|
||||||
})
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
try {
|
try {
|
||||||
await fetchDietAiRecommendFallback(refresh)
|
await fetchDietAiRecommendFallback(refresh)
|
||||||
} catch (e2) {
|
} catch (e2) {
|
||||||
showUserToast('饮食建议加载失败')
|
showUserToast('饮食建议加载失败')
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
dietAiLoading.value = false
|
dietAiLoading.value = false
|
||||||
dietAiStreaming.value = false
|
dietAiStreaming.value = false
|
||||||
dietAiReplacing.value = false
|
dietAiReplacing.value = false
|
||||||
dietAiStreamField.value = ''
|
dietAiStreamField.value = ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function refreshDietAiRecommend() {
|
function refreshDietAiRecommend() {
|
||||||
if (dietAiLoading.value) return
|
if (dietAiLoading.value) return
|
||||||
fetchDietAiRecommend(true)
|
fetchDietAiRecommend(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function askDietAiFoodFallback(q) {
|
async function askDietAiFoodFallback(q) {
|
||||||
const res = await proxy.apiUrl({
|
const res = await proxy.apiUrl({
|
||||||
url: '/api/tcm/dailyDietAiAsk',
|
url: '/api/tcm/dailyDietAiAsk',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
timeout: 35000,
|
timeout: 35000,
|
||||||
data: {
|
data: {
|
||||||
diagnosis_id: diagnosisId.value,
|
diagnosis_id: diagnosisId.value,
|
||||||
question: q
|
question: q
|
||||||
}
|
}
|
||||||
}, false)
|
}, false)
|
||||||
if (res?.code === 1 && res.data) {
|
if (res?.code === 1 && res.data) {
|
||||||
dietAiAskResult.value = {
|
dietAiAskResult.value = {
|
||||||
advice: res.data.advice || '',
|
advice: res.data.advice || '',
|
||||||
level: res.data.level || '',
|
level: res.data.level || '',
|
||||||
level_label: res.data.level_label || '',
|
level_label: res.data.level_label || '',
|
||||||
portion: res.data.portion || '',
|
portion: res.data.portion || '',
|
||||||
food: res.data.food || q
|
food: res.data.food || q
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
showUserToast(formatUserMessage(res?.msg, '咨询失败'))
|
showUserToast(formatUserMessage(res?.msg, '咨询失败'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function askDietAiFood() {
|
async function askDietAiFood() {
|
||||||
const q = String(dietAiAskText.value || '').trim()
|
const q = String(dietAiAskText.value || '').trim()
|
||||||
if (!q) {
|
if (!q) {
|
||||||
showUserToast('请输入想咨询的食物')
|
showUserToast('请输入想咨询的食物')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!diagnosisId.value) return
|
if (!diagnosisId.value) return
|
||||||
// 发送后立即清空输入框
|
// 发送后立即清空输入框
|
||||||
dietAiAskText.value = ''
|
dietAiAskText.value = ''
|
||||||
dietAiAsking.value = true
|
dietAiAsking.value = true
|
||||||
dietAiAskResult.value = { advice: '', level: '', level_label: '', portion: '', food: '' }
|
dietAiAskResult.value = { advice: '', level: '', level_label: '', portion: '', food: '' }
|
||||||
try {
|
try {
|
||||||
await requestAiStream({
|
await requestAiStream({
|
||||||
baseUrl: proxy.$url,
|
baseUrl: proxy.$url,
|
||||||
url: '/api/tcm/dailyDietAiAskStream',
|
url: '/api/tcm/dailyDietAiAskStream',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
data: {
|
data: {
|
||||||
diagnosis_id: diagnosisId.value,
|
diagnosis_id: diagnosisId.value,
|
||||||
question: q
|
question: q
|
||||||
},
|
},
|
||||||
onEvent: (event, payload) => {
|
onEvent: (event, payload) => {
|
||||||
if (event === 'start' && !dietAiAskResult.value.advice) {
|
if (event === 'start' && !dietAiAskResult.value.advice) {
|
||||||
dietAiAskResult.value = { ...dietAiAskResult.value, advice: payload.message || '正在想…' }
|
dietAiAskResult.value = { ...dietAiAskResult.value, advice: payload.message || '正在想…' }
|
||||||
} else if (event === 'delta') {
|
} else if (event === 'delta') {
|
||||||
dietAiAskResult.value = {
|
dietAiAskResult.value = {
|
||||||
...dietAiAskResult.value,
|
...dietAiAskResult.value,
|
||||||
advice: payload.advice || dietAiAskResult.value.advice
|
advice: payload.advice || dietAiAskResult.value.advice
|
||||||
}
|
}
|
||||||
} else if (event === 'done') {
|
} else if (event === 'done') {
|
||||||
dietAiAskResult.value = {
|
dietAiAskResult.value = {
|
||||||
advice: payload.advice || '',
|
advice: payload.advice || '',
|
||||||
level: payload.level || '',
|
level: payload.level || '',
|
||||||
level_label: payload.level_label || '',
|
level_label: payload.level_label || '',
|
||||||
portion: payload.portion || '',
|
portion: payload.portion || '',
|
||||||
food: payload.food || q
|
food: payload.food || q
|
||||||
}
|
}
|
||||||
} else if (event === 'error') {
|
} else if (event === 'error') {
|
||||||
showUserToast(formatUserMessage(payload?.message, '咨询失败'))
|
showUserToast(formatUserMessage(payload?.message, '咨询失败'))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
fallback: () => askDietAiFoodFallback(q)
|
fallback: () => askDietAiFoodFallback(q)
|
||||||
})
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
try {
|
try {
|
||||||
await askDietAiFoodFallback(q)
|
await askDietAiFoodFallback(q)
|
||||||
} catch (e2) {
|
} catch (e2) {
|
||||||
showUserToast('网络异常,请稍后再试')
|
showUserToast('网络异常,请稍后再试')
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
dietAiAsking.value = false
|
dietAiAsking.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onDietAiAskInput(e) {
|
function onDietAiAskInput(e) {
|
||||||
dietAiAskText.value = e?.detail?.value ?? ''
|
dietAiAskText.value = e?.detail?.value ?? ''
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveDietPrefillToStorage() {
|
function saveDietPrefillToStorage() {
|
||||||
const r = dietAiRecommend.value
|
const r = dietAiRecommend.value
|
||||||
if (!r.breakfast) return
|
if (!r.breakfast) return
|
||||||
try {
|
try {
|
||||||
uni.setStorageSync(DIET_AI_PREFILL_KEY, {
|
uni.setStorageSync(DIET_AI_PREFILL_KEY, {
|
||||||
breakfast: r.breakfast,
|
breakfast: r.breakfast,
|
||||||
drinks: r.drinks || '',
|
drinks: r.drinks || '',
|
||||||
lunch: r.lunch,
|
lunch: r.lunch,
|
||||||
dinner: r.dinner,
|
dinner: r.dinner,
|
||||||
tips: r.tips || ''
|
tips: r.tips || ''
|
||||||
})
|
})
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
function goMoreDietForm(withPrefill = false) {
|
function goMoreDietForm(withPrefill = false) {
|
||||||
if (withPrefill) saveDietPrefillToStorage()
|
if (withPrefill) saveDietPrefillToStorage()
|
||||||
uni.navigateTo({ url: '/tongji/pages/more?openDiet=1' })
|
uni.navigateTo({ url: '/tongji/pages/more?openDiet=1' })
|
||||||
}
|
}
|
||||||
|
|
||||||
async function applyDietAiToForm() {
|
async function applyDietAiToForm() {
|
||||||
if (!dietAiRecommend.value.breakfast) {
|
if (!dietAiRecommend.value.breakfast) {
|
||||||
await fetchDietAiRecommend(false)
|
await fetchDietAiRecommend(false)
|
||||||
}
|
}
|
||||||
goMoreDietForm(true)
|
goMoreDietForm(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
function openDietForm() {
|
function openDietForm() {
|
||||||
goMoreDietForm(false)
|
goMoreDietForm(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
dietAiRecommend,
|
dietAiRecommend,
|
||||||
dietAiLoading,
|
dietAiLoading,
|
||||||
dietAiStreaming,
|
dietAiStreaming,
|
||||||
dietAiReplacing,
|
dietAiReplacing,
|
||||||
dietAiStreamField,
|
dietAiStreamField,
|
||||||
dietAiAskText,
|
dietAiAskText,
|
||||||
dietAiAsking,
|
dietAiAsking,
|
||||||
dietAiAskResult,
|
dietAiAskResult,
|
||||||
hasDietAiMealContent,
|
hasDietAiMealContent,
|
||||||
fetchDietAiRecommend,
|
fetchDietAiRecommend,
|
||||||
refreshDietAiRecommend,
|
refreshDietAiRecommend,
|
||||||
askDietAiFood,
|
askDietAiFood,
|
||||||
onDietAiAskInput,
|
onDietAiAskInput,
|
||||||
applyDietAiToForm,
|
applyDietAiToForm,
|
||||||
openDietForm
|
openDietForm
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** more 页打开饮食表单时读取并清除预填 */
|
/** more 页打开饮食表单时读取并清除预填 */
|
||||||
export function consumeDietAiPrefill() {
|
export function consumeDietAiPrefill() {
|
||||||
try {
|
try {
|
||||||
const data = uni.getStorageSync(DIET_AI_PREFILL_KEY)
|
const data = uni.getStorageSync(DIET_AI_PREFILL_KEY)
|
||||||
uni.removeStorageSync(DIET_AI_PREFILL_KEY)
|
uni.removeStorageSync(DIET_AI_PREFILL_KEY)
|
||||||
if (data && typeof data === 'object' && data.breakfast) return data
|
if (data && typeof data === 'object' && data.breakfast) return data
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,115 +1,115 @@
|
|||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { formatUserMessage } from '../utils/tongjiHelpers.js'
|
import { formatUserMessage } from '../utils/tongjiHelpers.js'
|
||||||
|
|
||||||
/** 登录 / 就诊卡门禁(index 与 more 共用) */
|
/** 登录 / 就诊卡门禁(index 与 more 共用) */
|
||||||
export function useTongjiAuth(proxy) {
|
export function useTongjiAuth(proxy) {
|
||||||
const authChecking = ref(false)
|
const authChecking = ref(false)
|
||||||
let gateRedirected = false
|
let gateRedirected = false
|
||||||
let authSessionPromise = null
|
let authSessionPromise = null
|
||||||
|
|
||||||
function hasAuthToken() {
|
function hasAuthToken() {
|
||||||
return !!String(uni.getStorageSync('token') || '').trim()
|
return !!String(uni.getStorageSync('token') || '').trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearAuthStorage() {
|
function clearAuthStorage() {
|
||||||
uni.removeStorageSync('token')
|
uni.removeStorageSync('token')
|
||||||
authSessionPromise = null
|
authSessionPromise = null
|
||||||
}
|
}
|
||||||
|
|
||||||
function wxLoginGetCode() {
|
function wxLoginGetCode() {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
uni.login({
|
uni.login({
|
||||||
provider: 'weixin',
|
provider: 'weixin',
|
||||||
success: (res) => {
|
success: (res) => {
|
||||||
if (res && res.code) resolve(res.code)
|
if (res && res.code) resolve(res.code)
|
||||||
else reject(new Error('微信登录未返回 code'))
|
else reject(new Error('微信登录未返回 code'))
|
||||||
},
|
},
|
||||||
fail: reject
|
fail: reject
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function mnpLoginWithCode(code) {
|
async function mnpLoginWithCode(code) {
|
||||||
const res = await proxy.apiUrl({
|
const res = await proxy.apiUrl({
|
||||||
url: '/api/login/mnpLogin',
|
url: '/api/login/mnpLogin',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
data: { code }
|
data: { code }
|
||||||
}, false)
|
}, false)
|
||||||
if (res && res.code === 1 && res.data && res.data.token) {
|
if (res && res.code === 1 && res.data && res.data.token) {
|
||||||
uni.setStorageSync('token', res.data.token)
|
uni.setStorageSync('token', res.data.token)
|
||||||
uni.setStorageSync('userData', res.data)
|
uni.setStorageSync('userData', res.data)
|
||||||
return res.data
|
return res.data
|
||||||
}
|
}
|
||||||
clearAuthStorage()
|
clearAuthStorage()
|
||||||
throw new Error(formatUserMessage(res?.msg, '登录失败'))
|
throw new Error(formatUserMessage(res?.msg, '登录失败'))
|
||||||
}
|
}
|
||||||
|
|
||||||
async function doWxLogin() {
|
async function doWxLogin() {
|
||||||
const code = await wxLoginGetCode()
|
const code = await wxLoginGetCode()
|
||||||
await mnpLoginWithCode(code)
|
await mnpLoginWithCode(code)
|
||||||
return hasAuthToken()
|
return hasAuthToken()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function verifyOrLogin() {
|
async function verifyOrLogin() {
|
||||||
if (!hasAuthToken()) {
|
if (!hasAuthToken()) {
|
||||||
return doWxLogin()
|
return doWxLogin()
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const res = await proxy.apiUrl({ url: '/api/user/info', method: 'POST' }, false)
|
const res = await proxy.apiUrl({ url: '/api/user/info', method: 'POST' }, false)
|
||||||
if (res && res.code === 1 && res.data) {
|
if (res && res.code === 1 && res.data) {
|
||||||
uni.setStorageSync('userData', res.data)
|
uni.setStorageSync('userData', res.data)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
/* 网络异常走重新登录 */
|
/* 网络异常走重新登录 */
|
||||||
}
|
}
|
||||||
clearAuthStorage()
|
clearAuthStorage()
|
||||||
try {
|
try {
|
||||||
return await doWxLogin()
|
return await doWxLogin()
|
||||||
} catch (e2) {
|
} catch (e2) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function ensureLoggedIn() {
|
async function ensureLoggedIn() {
|
||||||
if (authSessionPromise) {
|
if (authSessionPromise) {
|
||||||
return authSessionPromise
|
return authSessionPromise
|
||||||
}
|
}
|
||||||
authSessionPromise = verifyOrLogin()
|
authSessionPromise = verifyOrLogin()
|
||||||
.then((ok) => {
|
.then((ok) => {
|
||||||
if (!ok) authSessionPromise = null
|
if (!ok) authSessionPromise = null
|
||||||
return !!ok
|
return !!ok
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
authSessionPromise = null
|
authSessionPromise = null
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
return authSessionPromise
|
return authSessionPromise
|
||||||
}
|
}
|
||||||
|
|
||||||
function redirectToCardEntry(returnPath) {
|
function redirectToCardEntry(returnPath) {
|
||||||
if (gateRedirected) return
|
if (gateRedirected) return
|
||||||
gateRedirected = true
|
gateRedirected = true
|
||||||
const returnUrl = encodeURIComponent(returnPath || '/tongji/pages/index')
|
const returnUrl = encodeURIComponent(returnPath || '/tongji/pages/index')
|
||||||
uni.redirectTo({
|
uni.redirectTo({
|
||||||
url: `/pages/Card/edit_card?add=1&returnUrl=${returnUrl}`
|
url: `/pages/Card/edit_card?add=1&returnUrl=${returnUrl}`
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetGateRedirect() {
|
function resetGateRedirect() {
|
||||||
gateRedirected = false
|
gateRedirected = false
|
||||||
}
|
}
|
||||||
|
|
||||||
function isGateRedirected() {
|
function isGateRedirected() {
|
||||||
return gateRedirected
|
return gateRedirected
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
authChecking,
|
authChecking,
|
||||||
hasAuthToken,
|
hasAuthToken,
|
||||||
ensureLoggedIn,
|
ensureLoggedIn,
|
||||||
redirectToCardEntry,
|
redirectToCardEntry,
|
||||||
resetGateRedirect,
|
resetGateRedirect,
|
||||||
isGateRedirected
|
isGateRedirected
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,7 @@
|
|||||||
## 已接入能力
|
## 已接入能力
|
||||||
|
|
||||||
- 复用主小程序 `token` 与微信小程序登录,不创建第二套游戏账号。
|
- 复用主小程序 `token` 与微信小程序登录,不创建第二套游戏账号。
|
||||||
- 按周一日期自动分配最多 7 人的混合同行组,前期不区分男女。
|
- 按周一日期、性别自动分配最多 7 人的同行组。
|
||||||
- 本周已经进入旧男女组的账号,下次读取榜单时保留成绩并自动迁入混合组。
|
|
||||||
- 首次入组使用数据库分配锁,多个用户同时进入也不会重复分组或超过 7 人。
|
- 首次入组使用数据库分配锁,多个用户同时进入也不会重复分组或超过 7 人。
|
||||||
- 以真实平台昵称、头像、认糖数和本周最高分排序。
|
- 以真实平台昵称、头像、认糖数和本周最高分排序。
|
||||||
- 每局用 `session_key` 上报绝对进度,断网重试不会重复加分。
|
- 每局用 `session_key` 上报绝对进度,断网重试不会重复加分。
|
||||||
@@ -34,7 +33,7 @@
|
|||||||
|
|
||||||
## 上线前检查
|
## 上线前检查
|
||||||
|
|
||||||
- 用不同账号进入,确认按进入顺序加入同一个最多 7 人的混合同行组。
|
- 用男女各两个测试账号进入,确认被分入对应性别组。
|
||||||
- 同一局重复提交相同 `session_key`,确认周认糖数不重复增加。
|
- 同一局重复提交相同 `session_key`,确认周认糖数不重复增加。
|
||||||
- 断网完成几次消除,再联网打开榜单,确认成绩补传。
|
- 断网完成几次消除,再联网打开榜单,确认成绩补传。
|
||||||
- 分享给另一个微信账号,确认能直接进入游戏且链接中没有用户 ID。
|
- 分享给另一个微信账号,确认能直接进入游戏且链接中没有用户 ID。
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 71 KiB |
|
Before Width: | Height: | Size: 55 KiB |
@@ -1,577 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
// 识糖小课堂棋盘压力测试:复刻生产版的 5x4 棋盘、掉落、洗牌和渐进换组规则。
|
|
||||||
// 运行:node tongji/endless-game/build/stress-game.mjs --runs 10000
|
|
||||||
|
|
||||||
const ROWS = 5
|
|
||||||
const COLS = 4
|
|
||||||
const HIGH_CAP = 3
|
|
||||||
const MAX_CASCADE_WAVES = 3
|
|
||||||
const DANGER_DROP_CHANCE = 0.1
|
|
||||||
const COLUMN_GROUP_BIAS = 0.72
|
|
||||||
|
|
||||||
const FOOD_GROUPS = [
|
|
||||||
{ title: '中式早餐', safe: ['egg', 'cucumber', 'corn', 'grainMantou'], danger: 'congee' },
|
|
||||||
{ title: '家常粥餐', safe: ['celery', 'mushroom', 'chicken', 'taro'], danger: 'centuryCongee' },
|
|
||||||
{ title: '粗粮早餐', safe: ['broccoli', 'milk', 'brownRice', 'potato'], danger: 'milletCongee' },
|
|
||||||
{ title: '水果饮品', safe: ['apple', 'strawberry', 'banana', 'mango'], danger: 'orangeJuice' },
|
|
||||||
{ title: '日常饮品', safe: ['milk', 'tea', 'blackCoffee', 'coffee'], danger: 'soda' },
|
|
||||||
{ title: '下午茶甜饮', safe: ['milk', 'tea', 'apple', 'strawberry'], danger: 'milkTea' },
|
|
||||||
{ title: '南方汤粉', safe: ['pepper', 'greenBean', 'shrimp', 'udon'], danger: 'riceNoodleSoup' },
|
|
||||||
{ title: '北方面食', safe: ['onion', 'celery', 'corn', 'grainMantou'], danger: 'noodleSoup' },
|
|
||||||
{ title: '家常餐桌', safe: ['tofu', 'spinach', 'carrot', 'fish'], danger: 'redBeanCongee' }
|
|
||||||
]
|
|
||||||
|
|
||||||
const HIGH_KEYS = new Set(FOOD_GROUPS.map(group => group.danger))
|
|
||||||
const SAFE_KEYS = new Set(FOOD_GROUPS.flatMap(group => group.safe))
|
|
||||||
|
|
||||||
function readNumberArg(name, fallback) {
|
|
||||||
const index = process.argv.indexOf(name)
|
|
||||||
if (index < 0) return fallback
|
|
||||||
const value = Number(process.argv[index + 1])
|
|
||||||
return Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback
|
|
||||||
}
|
|
||||||
|
|
||||||
const RUNS = readNumberArg('--runs', 10000)
|
|
||||||
const TURNS = readNumberArg('--turns', 80)
|
|
||||||
const BASE_SEED = readNumberArg('--seed', 20260722)
|
|
||||||
|
|
||||||
function createRng(seed) {
|
|
||||||
let state = seed >>> 0 || 1
|
|
||||||
return () => {
|
|
||||||
state ^= state << 13
|
|
||||||
state ^= state >>> 17
|
|
||||||
state ^= state << 5
|
|
||||||
return (state >>> 0) / 4294967296
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function cloneBoard(source) {
|
|
||||||
return source.map(tile => tile ? { ...tile } : null)
|
|
||||||
}
|
|
||||||
|
|
||||||
function tile(key) {
|
|
||||||
return { key }
|
|
||||||
}
|
|
||||||
|
|
||||||
function isAdjacent(a, b) {
|
|
||||||
const ar = Math.floor(a / COLS)
|
|
||||||
const ac = a % COLS
|
|
||||||
const br = Math.floor(b / COLS)
|
|
||||||
const bc = b % COLS
|
|
||||||
return Math.abs(ar - br) + Math.abs(ac - bc) === 1
|
|
||||||
}
|
|
||||||
|
|
||||||
function swap(source, a, b) {
|
|
||||||
const next = cloneBoard(source)
|
|
||||||
;[next[a], next[b]] = [next[b], next[a]]
|
|
||||||
return next
|
|
||||||
}
|
|
||||||
|
|
||||||
function shuffle(source, rng) {
|
|
||||||
const result = cloneBoard(source)
|
|
||||||
for (let i = result.length - 1; i > 0; i -= 1) {
|
|
||||||
const j = Math.floor(rng() * (i + 1))
|
|
||||||
;[result[i], result[j]] = [result[j], result[i]]
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
function findMatches(source) {
|
|
||||||
const groups = []
|
|
||||||
for (let row = 0; row < ROWS; row += 1) {
|
|
||||||
let start = 0
|
|
||||||
while (start < COLS) {
|
|
||||||
let end = start + 1
|
|
||||||
while (end < COLS && source[row * COLS + end]?.key === source[row * COLS + start]?.key) end += 1
|
|
||||||
if (source[row * COLS + start] && end - start >= 3) {
|
|
||||||
groups.push({ direction: 'row', indices: Array.from({ length: end - start }, (_, n) => row * COLS + start + n) })
|
|
||||||
}
|
|
||||||
start = end
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (let col = 0; col < COLS; col += 1) {
|
|
||||||
let start = 0
|
|
||||||
while (start < ROWS) {
|
|
||||||
let end = start + 1
|
|
||||||
while (end < ROWS && source[end * COLS + col]?.key === source[start * COLS + col]?.key) end += 1
|
|
||||||
if (source[start * COLS + col] && end - start >= 3) {
|
|
||||||
groups.push({ direction: 'col', indices: Array.from({ length: end - start }, (_, n) => (start + n) * COLS + col) })
|
|
||||||
}
|
|
||||||
start = end
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return groups
|
|
||||||
}
|
|
||||||
|
|
||||||
function enumerateMatchingMoves(source) {
|
|
||||||
const moves = []
|
|
||||||
for (let i = 0; i < source.length; i += 1) {
|
|
||||||
const candidates = []
|
|
||||||
if (i % COLS < COLS - 1) candidates.push(i + 1)
|
|
||||||
if (i + COLS < source.length) candidates.push(i + COLS)
|
|
||||||
for (const j of candidates) {
|
|
||||||
const copy = swap(source, i, j)
|
|
||||||
const groups = findMatches(copy)
|
|
||||||
if (!groups.length) continue
|
|
||||||
const indices = new Set(groups.flatMap(group => group.indices))
|
|
||||||
const hitsHigh = [...indices].some(index => HIGH_KEYS.has(copy[index]?.key))
|
|
||||||
moves.push({ from: i, to: j, hitsHigh, groups })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return moves
|
|
||||||
}
|
|
||||||
|
|
||||||
function safeMoves(source) {
|
|
||||||
return enumerateMatchingMoves(source).filter(move => !move.hitsHigh)
|
|
||||||
}
|
|
||||||
|
|
||||||
function wouldCreateMatchAt(source, index, key) {
|
|
||||||
const row = Math.floor(index / COLS)
|
|
||||||
const col = index % COLS
|
|
||||||
const keyAt = target => target === index ? key : source[target]?.key
|
|
||||||
let horizontal = 1
|
|
||||||
for (let c = col - 1; c >= 0 && keyAt(row * COLS + c) === key; c -= 1) horizontal += 1
|
|
||||||
for (let c = col + 1; c < COLS && keyAt(row * COLS + c) === key; c += 1) horizontal += 1
|
|
||||||
if (horizontal >= 3) return true
|
|
||||||
let vertical = 1
|
|
||||||
for (let r = row - 1; r >= 0 && keyAt(r * COLS + col) === key; r -= 1) vertical += 1
|
|
||||||
for (let r = row + 1; r < ROWS && keyAt(r * COLS + col) === key; r += 1) vertical += 1
|
|
||||||
return vertical >= 3
|
|
||||||
}
|
|
||||||
|
|
||||||
function openingKeys(state) {
|
|
||||||
const [a, b, c, d] = state.activeSafeKeys
|
|
||||||
const danger = FOOD_GROUPS[state.themeIndex].danger
|
|
||||||
return [
|
|
||||||
a, b, c, d,
|
|
||||||
b, c, d, a,
|
|
||||||
c, d, a, danger,
|
|
||||||
d, a, danger, b,
|
|
||||||
a, b, c, d
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
function forcedPlayableKeys(state) {
|
|
||||||
const [a, b, c, d] = state.activeSafeKeys
|
|
||||||
const danger = FOOD_GROUPS[state.themeIndex].danger
|
|
||||||
return [
|
|
||||||
d, a, danger, c,
|
|
||||||
d, c, b, danger,
|
|
||||||
c, a, b, a,
|
|
||||||
d, d, a, a,
|
|
||||||
b, d, c, b
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildPlayableBoard(state, keys = openingKeys(state)) {
|
|
||||||
let candidate = keys.map(tile)
|
|
||||||
for (let attempt = 0; attempt < 240; attempt += 1) {
|
|
||||||
if (!findMatches(candidate).length && safeMoves(candidate).length) return candidate
|
|
||||||
candidate = shuffle(candidate, state.rng)
|
|
||||||
}
|
|
||||||
state.metrics.guaranteedFallbacks += 1
|
|
||||||
return forcedPlayableKeys(state).map(tile)
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeStablePlayableBoard(state, source) {
|
|
||||||
const tiles = source.filter(Boolean).map(item => ({ ...item }))
|
|
||||||
if (tiles.length === ROWS * COLS) {
|
|
||||||
for (let attempt = 0; attempt < 300; attempt += 1) {
|
|
||||||
const candidate = shuffle(tiles, state.rng)
|
|
||||||
if (!findMatches(candidate).length && safeMoves(candidate).length) return candidate
|
|
||||||
}
|
|
||||||
}
|
|
||||||
state.metrics.stableFallbacks += 1
|
|
||||||
return buildPlayableBoard(state)
|
|
||||||
}
|
|
||||||
|
|
||||||
function queueSafeGroupTransition(state, keys) {
|
|
||||||
const target = [...keys]
|
|
||||||
const last = state.queuedSafeGroups[state.queuedSafeGroups.length - 1]
|
|
||||||
if (!last || !last.every((key, index) => key === target[index])) {
|
|
||||||
state.queuedSafeGroups.push(target)
|
|
||||||
}
|
|
||||||
startNextTransition(state)
|
|
||||||
}
|
|
||||||
|
|
||||||
function startNextTransition(state) {
|
|
||||||
if (state.transition) return false
|
|
||||||
while (state.queuedSafeGroups.length) {
|
|
||||||
const target = state.queuedSafeGroups[0]
|
|
||||||
const slot = target.findIndex((key, index) => state.activeSafeKeys[index] !== key)
|
|
||||||
if (slot < 0) {
|
|
||||||
state.queuedSafeGroups.shift()
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
const from = state.activeSafeKeys[slot]
|
|
||||||
const to = target[slot]
|
|
||||||
state.activeSafeKeys[slot] = to
|
|
||||||
state.transition = { from, to, slot, age: 0 }
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
function finishTransitionIfReady(state) {
|
|
||||||
let changed = false
|
|
||||||
while (state.transition) {
|
|
||||||
const { from, to } = state.transition
|
|
||||||
const remaining = state.board.filter(item => item?.key === from).length
|
|
||||||
if (remaining > 2) break
|
|
||||||
if (remaining) {
|
|
||||||
state.board = state.board.map(item => item?.key === from ? { ...item, key: to } : item)
|
|
||||||
state.metrics.tailConversions += remaining
|
|
||||||
}
|
|
||||||
state.transition = null
|
|
||||||
changed = true
|
|
||||||
startNextTransition(state)
|
|
||||||
}
|
|
||||||
return changed
|
|
||||||
}
|
|
||||||
|
|
||||||
function prepareTransitionForReshuffle(state) {
|
|
||||||
const activeSet = new Set(state.activeSafeKeys)
|
|
||||||
const counts = Object.fromEntries(state.activeSafeKeys.map(key => [key, 0]))
|
|
||||||
state.board.forEach(item => {
|
|
||||||
if (item && activeSet.has(item.key)) counts[item.key] += 1
|
|
||||||
})
|
|
||||||
let imported = 0
|
|
||||||
const next = state.board.map(item => {
|
|
||||||
if (!item || item.key === 'camel' || HIGH_KEYS.has(item.key)) return item ? { ...item } : item
|
|
||||||
if (state.transition && item.key === state.transition.from) {
|
|
||||||
imported += 1
|
|
||||||
counts[state.transition.to] += 1
|
|
||||||
return { ...item, key: state.transition.to }
|
|
||||||
}
|
|
||||||
if (activeSet.has(item.key)) return { ...item }
|
|
||||||
const key = [...state.activeSafeKeys].sort((a, b) => counts[a] - counts[b])[0]
|
|
||||||
counts[key] += 1
|
|
||||||
imported += 1
|
|
||||||
return { ...item, key }
|
|
||||||
})
|
|
||||||
state.board = next
|
|
||||||
state.metrics.reshuffleImports += imported
|
|
||||||
}
|
|
||||||
|
|
||||||
function randomFoodKey(state, snapshot, col) {
|
|
||||||
const highs = snapshot.filter(item => item && HIGH_KEYS.has(item.key)).length
|
|
||||||
if (highs < HIGH_CAP && state.rng() < DANGER_DROP_CHANCE) {
|
|
||||||
return FOOD_GROUPS[state.themeIndex].danger
|
|
||||||
}
|
|
||||||
if (state.rng() < COLUMN_GROUP_BIAS) return state.activeSafeKeys[col % state.activeSafeKeys.length]
|
|
||||||
return state.activeSafeKeys[Math.floor(state.rng() * state.activeSafeKeys.length)]
|
|
||||||
}
|
|
||||||
|
|
||||||
function pickDropKey(state, snapshot, index, col) {
|
|
||||||
for (let attempt = 0; attempt < 16; attempt += 1) {
|
|
||||||
const key = randomFoodKey(state, snapshot, col)
|
|
||||||
if (!wouldCreateMatchAt(snapshot, index, key)) return key
|
|
||||||
}
|
|
||||||
const fallback = [...state.activeSafeKeys, FOOD_GROUPS[state.themeIndex].danger]
|
|
||||||
.find(key => !wouldCreateMatchAt(snapshot, index, key))
|
|
||||||
return fallback || state.activeSafeKeys[(index + col) % state.activeSafeKeys.length]
|
|
||||||
}
|
|
||||||
|
|
||||||
function collapseAndFill(state) {
|
|
||||||
const next = Array(ROWS * COLS).fill(null)
|
|
||||||
for (let col = 0; col < COLS; col += 1) {
|
|
||||||
const existing = []
|
|
||||||
for (let row = ROWS - 1; row >= 0; row -= 1) {
|
|
||||||
const item = state.board[row * COLS + col]
|
|
||||||
if (item) existing.push(item)
|
|
||||||
}
|
|
||||||
existing.forEach((item, offset) => {
|
|
||||||
next[(ROWS - 1 - offset) * COLS + col] = item
|
|
||||||
})
|
|
||||||
}
|
|
||||||
for (let row = 0; row < ROWS; row += 1) {
|
|
||||||
for (let col = 0; col < COLS; col += 1) {
|
|
||||||
const index = row * COLS + col
|
|
||||||
if (!next[index]) next[index] = tile(pickDropKey(state, next, index, col))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
state.board = next
|
|
||||||
}
|
|
||||||
|
|
||||||
function reshuffle(state) {
|
|
||||||
prepareTransitionForReshuffle(state)
|
|
||||||
let candidate = null
|
|
||||||
for (let attempt = 0; attempt < 240; attempt += 1) {
|
|
||||||
const shuffled = shuffle(state.board, state.rng)
|
|
||||||
if (findMatches(shuffled).length || !safeMoves(shuffled).length) continue
|
|
||||||
candidate = shuffled
|
|
||||||
break
|
|
||||||
}
|
|
||||||
state.board = candidate || buildPlayableBoard(state, forcedPlayableKeys(state))
|
|
||||||
finishTransitionIfReady(state)
|
|
||||||
state.metrics.reshuffles += 1
|
|
||||||
}
|
|
||||||
|
|
||||||
function advanceTheme(state) {
|
|
||||||
const previous = FOOD_GROUPS[state.themeIndex]
|
|
||||||
state.themeIndex = (state.themeIndex + 1) % FOOD_GROUPS.length
|
|
||||||
const next = FOOD_GROUPS[state.themeIndex]
|
|
||||||
state.board = state.board.map(item => item?.key === previous.danger ? { ...item, key: next.danger } : item)
|
|
||||||
queueSafeGroupTransition(state, next.safe)
|
|
||||||
state.metrics.themeChanges += 1
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveMatches(state, initialGroups) {
|
|
||||||
let groups = initialGroups
|
|
||||||
let wave = 0
|
|
||||||
let camelMatch = 0
|
|
||||||
while (groups.length && wave < MAX_CASCADE_WAVES) {
|
|
||||||
wave += 1
|
|
||||||
const remove = new Set(groups.flatMap(group => group.indices))
|
|
||||||
for (const group of groups) {
|
|
||||||
if (group.indices.every(index => state.board[index]?.key === 'camel')) {
|
|
||||||
camelMatch = Math.max(camelMatch, group.indices.length)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
remove.forEach(index => { state.board[index] = null })
|
|
||||||
collapseAndFill(state)
|
|
||||||
finishTransitionIfReady(state)
|
|
||||||
groups = findMatches(state.board)
|
|
||||||
const autoHigh = groups.some(group => group.indices.some(index => HIGH_KEYS.has(state.board[index]?.key)))
|
|
||||||
if (autoHigh) {
|
|
||||||
state.metrics.automaticHighAvoided += 1
|
|
||||||
state.board = makeStablePlayableBoard(state, state.board)
|
|
||||||
groups = []
|
|
||||||
} else if (wave >= MAX_CASCADE_WAVES && groups.length) {
|
|
||||||
state.metrics.cascadeCaps += 1
|
|
||||||
state.board = makeStablePlayableBoard(state, state.board)
|
|
||||||
groups = []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
state.metrics.maxCascade = Math.max(state.metrics.maxCascade, wave)
|
|
||||||
if (camelMatch >= 3) advanceTheme(state)
|
|
||||||
}
|
|
||||||
|
|
||||||
function useCamelTool(state) {
|
|
||||||
const danger = FOOD_GROUPS[state.themeIndex].danger
|
|
||||||
const targets = state.board
|
|
||||||
.map((item, index) => item?.key === danger ? index : -1)
|
|
||||||
.filter(index => index >= 0)
|
|
||||||
if (!targets.length) return false
|
|
||||||
const index = targets[Math.floor(state.rng() * targets.length)]
|
|
||||||
state.board[index] = tile('camel')
|
|
||||||
state.metrics.toolUses += 1
|
|
||||||
const groups = findMatches(state.board)
|
|
||||||
if (groups.length) resolveMatches(state, groups)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
function recordFailure(state, type, detail = '') {
|
|
||||||
state.metrics.failures[type] = (state.metrics.failures[type] || 0) + 1
|
|
||||||
if (state.metrics.samples.length < 20) {
|
|
||||||
state.metrics.samples.push({ run: state.run, turn: state.turn, type, detail, board: state.board.map(item => item?.key || '-') })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function validateState(state, expectStable = true) {
|
|
||||||
if (state.board.length !== ROWS * COLS || state.board.some(item => !item?.key)) {
|
|
||||||
recordFailure(state, 'invalid_board_size')
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
const currentDanger = FOOD_GROUPS[state.themeIndex].danger
|
|
||||||
const highKeys = state.board.filter(item => HIGH_KEYS.has(item.key)).map(item => item.key)
|
|
||||||
if (highKeys.some(key => key !== currentDanger)) recordFailure(state, 'wrong_theme_danger', highKeys.join(','))
|
|
||||||
if (highKeys.length > HIGH_CAP) recordFailure(state, 'danger_cap_exceeded', String(highKeys.length))
|
|
||||||
|
|
||||||
const allowed = new Set([...state.activeSafeKeys, 'camel'])
|
|
||||||
if (state.transition) allowed.add(state.transition.from)
|
|
||||||
const stale = state.board.filter(item => !HIGH_KEYS.has(item.key) && !allowed.has(item.key)).map(item => item.key)
|
|
||||||
if (stale.length) recordFailure(state, 'stale_food', stale.join(','))
|
|
||||||
|
|
||||||
const normalTypes = new Set(state.board.filter(item => SAFE_KEYS.has(item.key)).map(item => item.key))
|
|
||||||
if (normalTypes.size > 5) recordFailure(state, 'too_many_normal_types', String(normalTypes.size))
|
|
||||||
if (new Set(state.activeSafeKeys).size !== 4) recordFailure(state, 'duplicate_active_food')
|
|
||||||
if (state.transition) {
|
|
||||||
const remaining = state.board.filter(item => item.key === state.transition.from).length
|
|
||||||
if (remaining <= 2) recordFailure(state, 'orphan_transition_tail', `${state.transition.from}:${remaining}`)
|
|
||||||
}
|
|
||||||
if (expectStable && findMatches(state.board).length) recordFailure(state, 'unresolved_match')
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
function createMetrics() {
|
|
||||||
return {
|
|
||||||
runs: RUNS,
|
|
||||||
turnsPerRun: TURNS,
|
|
||||||
totalTurns: 0,
|
|
||||||
validMoves: 0,
|
|
||||||
reshuffles: 0,
|
|
||||||
reshuffleImports: 0,
|
|
||||||
guaranteedFallbacks: 0,
|
|
||||||
stableFallbacks: 0,
|
|
||||||
themeChanges: 0,
|
|
||||||
toolUses: 0,
|
|
||||||
tailConversions: 0,
|
|
||||||
automaticHighAvoided: 0,
|
|
||||||
cascadeCaps: 0,
|
|
||||||
maxCascade: 0,
|
|
||||||
unsafeHighMoveProbes: 0,
|
|
||||||
maxTransitionAge: 0,
|
|
||||||
maxQueuedThemes: 0,
|
|
||||||
transitionDrainTurns: 0,
|
|
||||||
unfinishedTransitions: 0,
|
|
||||||
failures: {},
|
|
||||||
samples: []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function runTargetedTailTests(metrics) {
|
|
||||||
for (let groupIndex = 0; groupIndex < FOOD_GROUPS.length; groupIndex += 1) {
|
|
||||||
const nextIndex = (groupIndex + 1) % FOOD_GROUPS.length
|
|
||||||
const previousGroup = FOOD_GROUPS[groupIndex]
|
|
||||||
const nextGroup = FOOD_GROUPS[nextIndex]
|
|
||||||
// 相邻主题允许在相同槽位保留同一种食物。定向测试应选择真正退出下一组的食物,
|
|
||||||
// 不能固定测试第 0 槽,否则“牛奶→牛奶”会被误报为尾巴未转换。
|
|
||||||
const slot = previousGroup.safe.findIndex(key => !nextGroup.safe.includes(key))
|
|
||||||
if (slot < 0) continue
|
|
||||||
for (const remaining of [0, 1, 2]) {
|
|
||||||
const state = {
|
|
||||||
run: -1,
|
|
||||||
turn: remaining,
|
|
||||||
rng: createRng(BASE_SEED + groupIndex * 17 + remaining),
|
|
||||||
metrics,
|
|
||||||
themeIndex: nextIndex,
|
|
||||||
activeSafeKeys: [...nextGroup.safe],
|
|
||||||
transition: {
|
|
||||||
from: previousGroup.safe[slot],
|
|
||||||
to: nextGroup.safe[slot],
|
|
||||||
slot,
|
|
||||||
age: 0
|
|
||||||
},
|
|
||||||
queuedSafeGroups: [[...nextGroup.safe]],
|
|
||||||
board: []
|
|
||||||
}
|
|
||||||
const fill = nextGroup.safe
|
|
||||||
state.board = Array.from({ length: ROWS * COLS }, (_, index) => tile(fill[index % fill.length]))
|
|
||||||
for (let index = 0; index < remaining; index += 1) state.board[index] = tile(state.transition.from)
|
|
||||||
finishTransitionIfReady(state)
|
|
||||||
if (state.board.some(item => item.key === state.transition?.from || item.key === previousGroup.safe[slot])) {
|
|
||||||
recordFailure(state, 'targeted_tail_not_converted', `${groupIndex}:${remaining}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const metrics = createMetrics()
|
|
||||||
runTargetedTailTests(metrics)
|
|
||||||
|
|
||||||
for (let run = 0; run < RUNS; run += 1) {
|
|
||||||
const state = {
|
|
||||||
run,
|
|
||||||
turn: 0,
|
|
||||||
rng: createRng(BASE_SEED + run * 2654435761),
|
|
||||||
metrics,
|
|
||||||
themeIndex: run % FOOD_GROUPS.length,
|
|
||||||
activeSafeKeys: [...FOOD_GROUPS[run % FOOD_GROUPS.length].safe],
|
|
||||||
transition: null,
|
|
||||||
queuedSafeGroups: [],
|
|
||||||
board: []
|
|
||||||
}
|
|
||||||
state.board = buildPlayableBoard(state)
|
|
||||||
validateState(state)
|
|
||||||
|
|
||||||
let toolsUsed = 0
|
|
||||||
for (let turn = 0; turn < TURNS; turn += 1) {
|
|
||||||
state.turn = turn
|
|
||||||
metrics.totalTurns += 1
|
|
||||||
if (state.transition) {
|
|
||||||
state.transition.age += 1
|
|
||||||
metrics.maxTransitionAge = Math.max(metrics.maxTransitionAge, state.transition.age)
|
|
||||||
}
|
|
||||||
metrics.maxQueuedThemes = Math.max(metrics.maxQueuedThemes, state.queuedSafeGroups.length)
|
|
||||||
|
|
||||||
// 模拟玩家积攒到驼乳后替换高糖,但每局限制5次,避免测试人为填满棋盘。
|
|
||||||
if (toolsUsed < 5 && turn > 0 && turn % 13 === 0 && useCamelTool(state)) {
|
|
||||||
toolsUsed += 1
|
|
||||||
finishTransitionIfReady(state)
|
|
||||||
if (findMatches(state.board).length) state.board = makeStablePlayableBoard(state, state.board)
|
|
||||||
}
|
|
||||||
|
|
||||||
let moves = safeMoves(state.board)
|
|
||||||
if (!moves.length) {
|
|
||||||
reshuffle(state)
|
|
||||||
moves = safeMoves(state.board)
|
|
||||||
}
|
|
||||||
if (!moves.length) {
|
|
||||||
recordFailure(state, 'dead_board_after_reshuffle')
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
// 额外探测“主动三消高糖会结束”的判定,不改变主模拟棋盘。
|
|
||||||
const unsafe = enumerateMatchingMoves(state.board).find(move => move.hitsHigh)
|
|
||||||
if (unsafe) {
|
|
||||||
const probe = swap(state.board, unsafe.from, unsafe.to)
|
|
||||||
const direct = findMatches(probe)
|
|
||||||
if (!direct.some(group => group.indices.some(index => HIGH_KEYS.has(probe[index]?.key)))) {
|
|
||||||
recordFailure(state, 'unsafe_high_probe_missed')
|
|
||||||
}
|
|
||||||
metrics.unsafeHighMoveProbes += 1
|
|
||||||
}
|
|
||||||
|
|
||||||
const move = moves[Math.floor(state.rng() * moves.length)]
|
|
||||||
state.board = swap(state.board, move.from, move.to)
|
|
||||||
const groups = findMatches(state.board)
|
|
||||||
if (!groups.length) {
|
|
||||||
recordFailure(state, 'safe_move_without_match')
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if (groups.some(group => group.indices.some(index => HIGH_KEYS.has(state.board[index]?.key)))) {
|
|
||||||
recordFailure(state, 'safe_move_hit_high')
|
|
||||||
break
|
|
||||||
}
|
|
||||||
resolveMatches(state, groups)
|
|
||||||
metrics.validMoves += 1
|
|
||||||
|
|
||||||
// 用较快节奏覆盖连续换组和排队逻辑,强度高于正常玩家。
|
|
||||||
if ((turn + 1) % 16 === 0 && turn + 1 < TURNS) advanceTheme(state)
|
|
||||||
finishTransitionIfReady(state)
|
|
||||||
if (findMatches(state.board).length) state.board = makeStablePlayableBoard(state, state.board)
|
|
||||||
if (!safeMoves(state.board).length) reshuffle(state)
|
|
||||||
validateState(state)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 停止新增主题后继续正常消除,验证排队中的旧食物能否全部退出,避免队列永久积压。
|
|
||||||
let drainTurn = 0
|
|
||||||
while ((state.transition || state.queuedSafeGroups.length) && drainTurn < 80) {
|
|
||||||
state.turn = TURNS + drainTurn
|
|
||||||
if (state.transition) {
|
|
||||||
state.transition.age += 1
|
|
||||||
metrics.maxTransitionAge = Math.max(metrics.maxTransitionAge, state.transition.age)
|
|
||||||
}
|
|
||||||
let moves = safeMoves(state.board)
|
|
||||||
if (!moves.length) {
|
|
||||||
reshuffle(state)
|
|
||||||
moves = safeMoves(state.board)
|
|
||||||
}
|
|
||||||
if (!moves.length) {
|
|
||||||
recordFailure(state, 'dead_board_while_draining')
|
|
||||||
break
|
|
||||||
}
|
|
||||||
const move = moves[Math.floor(state.rng() * moves.length)]
|
|
||||||
state.board = swap(state.board, move.from, move.to)
|
|
||||||
resolveMatches(state, findMatches(state.board))
|
|
||||||
metrics.validMoves += 1
|
|
||||||
finishTransitionIfReady(state)
|
|
||||||
if (findMatches(state.board).length) state.board = makeStablePlayableBoard(state, state.board)
|
|
||||||
if (!safeMoves(state.board).length) reshuffle(state)
|
|
||||||
validateState(state)
|
|
||||||
drainTurn += 1
|
|
||||||
}
|
|
||||||
metrics.transitionDrainTurns += drainTurn
|
|
||||||
if (state.transition || state.queuedSafeGroups.length) metrics.unfinishedTransitions += 1
|
|
||||||
}
|
|
||||||
|
|
||||||
const failureCount = Object.values(metrics.failures).reduce((sum, count) => sum + count, 0)
|
|
||||||
const result = {
|
|
||||||
ok: failureCount === 0,
|
|
||||||
...metrics,
|
|
||||||
failureCount
|
|
||||||
}
|
|
||||||
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`)
|
|
||||||
if (!result.ok) process.exitCode = 1
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
import { readFileSync } from 'node:fs'
|
|
||||||
import { resolve } from 'node:path'
|
|
||||||
import type { Plugin } from 'vite'
|
|
||||||
|
|
||||||
const FOOD_ICON_FILES = [
|
|
||||||
'tofu.jpg',
|
|
||||||
'spinach.jpg',
|
|
||||||
'carrot.jpg',
|
|
||||||
'fish.jpg',
|
|
||||||
'redBeanCongee.jpg',
|
|
||||||
'milkTea.jpg'
|
|
||||||
]
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Keep endless-game food icons inside the tongji subpackage.
|
|
||||||
* Importing them from Vue would make Vite emit them into the main-package assets folder.
|
|
||||||
*/
|
|
||||||
export function tongjiFoodAssetsPlugin(): Plugin {
|
|
||||||
return {
|
|
||||||
name: 'tongji-food-assets',
|
|
||||||
apply: 'build',
|
|
||||||
generateBundle() {
|
|
||||||
FOOD_ICON_FILES.forEach((fileName) => {
|
|
||||||
this.emitFile({
|
|
||||||
type: 'asset',
|
|
||||||
fileName: `tongji/endless-game/assets/food/${fileName}`,
|
|
||||||
source: readFileSync(resolve(process.cwd(), 'tongji/endless-game/assets/food', fileName))
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -39,7 +39,6 @@ export function useGamePlatform(proxy, ensureLoggedIn) {
|
|||||||
const connected = ref(false)
|
const connected = ref(false)
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const syncStatus = ref('idle')
|
const syncStatus = ref('idle')
|
||||||
const lastError = ref('')
|
|
||||||
const leaderboard = ref({ ...EMPTY_BOARD })
|
const leaderboard = ref({ ...EMPTY_BOARD })
|
||||||
const confirmedSessionLearned = ref(0)
|
const confirmedSessionLearned = ref(0)
|
||||||
|
|
||||||
@@ -74,19 +73,17 @@ export function useGamePlatform(proxy, ensureLoggedIn) {
|
|||||||
}
|
}
|
||||||
connected.value = true
|
connected.value = true
|
||||||
syncStatus.value = 'synced'
|
syncStatus.value = 'synced'
|
||||||
lastError.value = ''
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refreshLeaderboard() {
|
async function refreshLeaderboard() {
|
||||||
const res = await api({
|
const res = await api({
|
||||||
url: '/api/tcm/gameWeeklyLeaderboard',
|
url: '/api/tcm/gameWeeklyLeaderboard',
|
||||||
method: 'GET',
|
method: 'GET'
|
||||||
data: { session_key: sessionKey }
|
|
||||||
})
|
})
|
||||||
if (res?.code !== 1 || !res.data) {
|
if (res?.code !== 1 || !res.data) {
|
||||||
throw new Error(res?.msg || '同行榜加载失败')
|
throw new Error(res?.msg || '同行榜加载失败')
|
||||||
}
|
}
|
||||||
applyLeaderboard(res.data, sessionKey)
|
applyLeaderboard(res.data)
|
||||||
return res.data
|
return res.data
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,10 +112,9 @@ export function useGamePlatform(proxy, ensureLoggedIn) {
|
|||||||
await refreshLeaderboard()
|
await refreshLeaderboard()
|
||||||
if (queuedPayloads.length) flushProgress()
|
if (queuedPayloads.length) flushProgress()
|
||||||
return true
|
return true
|
||||||
} catch (error) {
|
} catch (_) {
|
||||||
connected.value = false
|
connected.value = false
|
||||||
syncStatus.value = 'offline'
|
syncStatus.value = 'offline'
|
||||||
lastError.value = String(error?.message || '平台连接失败,请点击重试')
|
|
||||||
return false
|
return false
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
@@ -128,21 +124,14 @@ export function useGamePlatform(proxy, ensureLoggedIn) {
|
|||||||
return connectPromise
|
return connectPromise
|
||||||
}
|
}
|
||||||
|
|
||||||
function beginSession(existingSessionKey = '') {
|
function beginSession() {
|
||||||
if (syncTimer) clearTimeout(syncTimer)
|
if (syncTimer) clearTimeout(syncTimer)
|
||||||
const restoredKey = String(existingSessionKey || '').trim()
|
sessionKey = createSessionKey()
|
||||||
sessionKey = /^[A-Za-z0-9_-]{16,64}$/.test(restoredKey)
|
|
||||||
? restoredKey
|
|
||||||
: createSessionKey()
|
|
||||||
confirmedSessionLearned.value = 0
|
confirmedSessionLearned.value = 0
|
||||||
syncStatus.value = queuedPayloads.length ? 'pending' : (connected.value ? 'synced' : 'offline')
|
syncStatus.value = queuedPayloads.length ? 'pending' : (connected.value ? 'synced' : 'offline')
|
||||||
return sessionKey
|
return sessionKey
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSessionKey() {
|
|
||||||
return sessionKey
|
|
||||||
}
|
|
||||||
|
|
||||||
function queueProgress(learnedCount, score, ended = false) {
|
function queueProgress(learnedCount, score, ended = false) {
|
||||||
const nextPayload = {
|
const nextPayload = {
|
||||||
session_key: sessionKey,
|
session_key: sessionKey,
|
||||||
@@ -202,10 +191,9 @@ export function useGamePlatform(proxy, ensureLoggedIn) {
|
|||||||
|| Number(item.ended) > Number(payload.ended)
|
|| Number(item.ended) > Number(payload.ended)
|
||||||
))
|
))
|
||||||
persistPendingPayloads()
|
persistPendingPayloads()
|
||||||
} catch (error) {
|
} catch (_) {
|
||||||
connected.value = false
|
connected.value = false
|
||||||
syncStatus.value = 'offline'
|
syncStatus.value = 'offline'
|
||||||
lastError.value = String(error?.message || '成绩暂未保存,联网后会自动重试')
|
|
||||||
// 队首保留原绝对值,下次连接或打开榜单时安全重试。
|
// 队首保留原绝对值,下次连接或打开榜单时安全重试。
|
||||||
persistPendingPayloads()
|
persistPendingPayloads()
|
||||||
} finally {
|
} finally {
|
||||||
@@ -249,12 +237,10 @@ export function useGamePlatform(proxy, ensureLoggedIn) {
|
|||||||
connected,
|
connected,
|
||||||
loading,
|
loading,
|
||||||
syncStatus,
|
syncStatus,
|
||||||
lastError,
|
|
||||||
leaderboard,
|
leaderboard,
|
||||||
confirmedSessionLearned,
|
confirmedSessionLearned,
|
||||||
connect,
|
connect,
|
||||||
beginSession,
|
beginSession,
|
||||||
getSessionKey,
|
|
||||||
queueProgress,
|
queueProgress,
|
||||||
flushProgress,
|
flushProgress,
|
||||||
syncAndRefresh,
|
syncAndRefresh,
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
# 识糖小课堂 3D 食物图标候选稿 v1
|
|
||||||
|
|
||||||
- 共 42 个食物图标,文件名与 `FOODS` 中的英文键名一致。
|
|
||||||
- 单图尺寸:320 × 320,JPEG。
|
|
||||||
- 风格:中式、圆润 3D、暖色奶油背景、大轮廓、适老化高辨识度。
|
|
||||||
- 糖级、边框和角标不画入图标,由游戏界面控制,方便医生调整分类。
|
|
||||||
- `milkTea.jpg` 是唯一自带角色表情的高糖小反派候选。
|
|
||||||
- 驼乳属于产品道具,不计入 42 个普通食物图标,本轮未替换现有产品罐素材。
|
|
||||||
- 本目录当前仅作设计候选,没有接入正式游戏,也不会进入微信小程序发布包。
|
|
||||||
|
|
||||||
目录说明:
|
|
||||||
|
|
||||||
- `icons/`:可直接用于游戏的独立图标。
|
|
||||||
- `sheets/`:11 张分组预览母版,方便整体审美检查。
|
|
||||||
- `manifest.json`:食物中文名、代码键名与文件路径清单。
|
|
||||||
|
|
||||||
医学提示:图标仅表达食物外观,不代表最终糖级结论;糖级和健康说明仍需甄养堂医生审核。
|
|
||||||
|
Before Width: | Height: | Size: 286 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 27 KiB |
@@ -1,53 +0,0 @@
|
|||||||
{
|
|
||||||
"version": 1,
|
|
||||||
"style": "chinese-rounded-3d-elderly-friendly",
|
|
||||||
"iconSize": {
|
|
||||||
"width": 320,
|
|
||||||
"height": 320,
|
|
||||||
"format": "jpg"
|
|
||||||
},
|
|
||||||
"items": [
|
|
||||||
{ "key": "corn", "name": "玉米", "file": "icons/corn.jpg" },
|
|
||||||
{ "key": "egg", "name": "鸡蛋", "file": "icons/egg.jpg" },
|
|
||||||
{ "key": "tomato", "name": "番茄", "file": "icons/tomato.jpg" },
|
|
||||||
{ "key": "potato", "name": "红薯", "file": "icons/potato.jpg" },
|
|
||||||
{ "key": "cucumber", "name": "黄瓜", "file": "icons/cucumber.jpg" },
|
|
||||||
{ "key": "lettuce", "name": "生菜", "file": "icons/lettuce.jpg" },
|
|
||||||
{ "key": "grainMantou", "name": "杂粮馒头", "file": "icons/grainMantou.jpg" },
|
|
||||||
{ "key": "wonton", "name": "馄饨", "file": "icons/wonton.jpg" },
|
|
||||||
{ "key": "celery", "name": "芹菜", "file": "icons/celery.jpg" },
|
|
||||||
{ "key": "mushroom", "name": "香菇", "file": "icons/mushroom.jpg" },
|
|
||||||
{ "key": "chicken", "name": "鸡肉", "file": "icons/chicken.jpg" },
|
|
||||||
{ "key": "taro", "name": "芋头", "file": "icons/taro.jpg" },
|
|
||||||
{ "key": "broccoli", "name": "西兰花", "file": "icons/broccoli.jpg" },
|
|
||||||
{ "key": "milk", "name": "牛奶", "file": "icons/milk.jpg" },
|
|
||||||
{ "key": "brownRice", "name": "糙米饭", "file": "icons/brownRice.jpg" },
|
|
||||||
{ "key": "apple", "name": "苹果", "file": "icons/apple.jpg" },
|
|
||||||
{ "key": "strawberry", "name": "草莓", "file": "icons/strawberry.jpg" },
|
|
||||||
{ "key": "banana", "name": "香蕉", "file": "icons/banana.jpg" },
|
|
||||||
{ "key": "mango", "name": "芒果", "file": "icons/mango.jpg" },
|
|
||||||
{ "key": "tea", "name": "红茶", "file": "icons/tea.jpg" },
|
|
||||||
{ "key": "blackCoffee", "name": "黑咖啡", "file": "icons/blackCoffee.jpg" },
|
|
||||||
{ "key": "coffee", "name": "咖啡", "file": "icons/coffee.jpg" },
|
|
||||||
{ "key": "pineapple", "name": "菠萝", "file": "icons/pineapple.jpg" },
|
|
||||||
{ "key": "pepper", "name": "青椒", "file": "icons/pepper.jpg" },
|
|
||||||
{ "key": "greenBean", "name": "四季豆", "file": "icons/greenBean.jpg" },
|
|
||||||
{ "key": "shrimp", "name": "虾仁", "file": "icons/shrimp.jpg" },
|
|
||||||
{ "key": "udon", "name": "乌冬面", "file": "icons/udon.jpg" },
|
|
||||||
{ "key": "onion", "name": "洋葱", "file": "icons/onion.jpg" },
|
|
||||||
{ "key": "congee", "name": "白米粥", "file": "icons/congee.jpg" },
|
|
||||||
{ "key": "eightCongee", "name": "八宝粥", "file": "icons/eightCongee.jpg" },
|
|
||||||
{ "key": "centuryCongee", "name": "皮蛋瘦肉粥", "file": "icons/centuryCongee.jpg" },
|
|
||||||
{ "key": "milletCongee", "name": "小米粥", "file": "icons/milletCongee.jpg" },
|
|
||||||
{ "key": "orangeJuice", "name": "柳橙汁", "file": "icons/orangeJuice.jpg" },
|
|
||||||
{ "key": "soda", "name": "含糖汽水", "file": "icons/soda.jpg" },
|
|
||||||
{ "key": "riceNoodleSoup", "name": "汤粉", "file": "icons/riceNoodleSoup.jpg" },
|
|
||||||
{ "key": "noodleSoup", "name": "汤面", "file": "icons/noodleSoup.jpg" },
|
|
||||||
{ "key": "milkTea", "name": "含糖奶茶", "file": "icons/milkTea.jpg" },
|
|
||||||
{ "key": "tofu", "name": "豆腐", "file": "icons/tofu.jpg" },
|
|
||||||
{ "key": "spinach", "name": "菠菜", "file": "icons/spinach.jpg" },
|
|
||||||
{ "key": "carrot", "name": "胡萝卜", "file": "icons/carrot.jpg" },
|
|
||||||
{ "key": "fish", "name": "鱼", "file": "icons/fish.jpg" },
|
|
||||||
{ "key": "redBeanCongee", "name": "软烂红豆粥", "file": "icons/redBeanCongee.jpg" }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 121 KiB |
|
Before Width: | Height: | Size: 151 KiB |
|
Before Width: | Height: | Size: 171 KiB |
|
Before Width: | Height: | Size: 159 KiB |
|
Before Width: | Height: | Size: 143 KiB |
|
Before Width: | Height: | Size: 126 KiB |
|
Before Width: | Height: | Size: 131 KiB |
|
Before Width: | Height: | Size: 198 KiB |
|
Before Width: | Height: | Size: 187 KiB |
|
Before Width: | Height: | Size: 143 KiB |
|
Before Width: | Height: | Size: 110 KiB |
@@ -46,44 +46,7 @@
|
|||||||
.eg-nav-actions { display: flex; width: 156rpx; flex-shrink: 0; justify-content: flex-end; gap: 10rpx; }
|
.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-nav-btn--small { width: 68rpx; height: 68rpx; border-radius: 22rpx; }
|
||||||
|
|
||||||
.eg-title-wrap { position: relative; display: flex; min-width: 0; flex-direction: column; align-items: center; }
|
.eg-title-wrap { display: flex; min-width: 0; flex-direction: column; align-items: center; }
|
||||||
.eg-title-wrap > .eg-title,
|
|
||||||
.eg-title-wrap > .eg-subtitle { transition: opacity .08s ease; }
|
|
||||||
.eg-title-wrap.is-noticing > .eg-title,
|
|
||||||
.eg-title-wrap.is-noticing > .eg-subtitle { opacity: .16; }
|
|
||||||
.eg-nav-notice {
|
|
||||||
position: absolute;
|
|
||||||
z-index: 1;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
display: flex;
|
|
||||||
width: 100%;
|
|
||||||
min-width: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
padding: 5rpx 8rpx;
|
|
||||||
border: 1rpx solid rgba(21,94,75,.09);
|
|
||||||
border-radius: 16rpx;
|
|
||||||
color: rgba(38,86,72,.82);
|
|
||||||
background: rgba(255,255,255,.58);
|
|
||||||
box-shadow: 0 4rpx 12rpx rgba(13,64,51,.06);
|
|
||||||
text-align: center;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
animation: egNavNotice 1.05s ease-out both;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
.eg-nav-notice-title,
|
|
||||||
.eg-nav-notice-detail {
|
|
||||||
display: block;
|
|
||||||
overflow: hidden;
|
|
||||||
max-width: 100%;
|
|
||||||
line-height: 1.15;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
.eg-nav-notice-title { font-size: 23rpx; font-weight: 800; }
|
|
||||||
.eg-nav-notice-detail { margin-top: 2rpx; color: rgba(154,106,42,.78); font-size: 20rpx; font-weight: 700; }
|
|
||||||
.eg-title, .eg-subtitle { overflow: hidden; max-width: 100%; white-space: nowrap; text-overflow: ellipsis; }
|
.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-title { color: #155e4b; font-size: 40rpx; font-weight: 800; }
|
||||||
.eg-subtitle { margin-top: 2rpx; color: #648278; font-size: 24rpx; }
|
.eg-subtitle { margin-top: 2rpx; color: #648278; font-size: 24rpx; }
|
||||||
@@ -154,20 +117,12 @@
|
|||||||
background: rgba(255,255,255,.92);
|
background: rgba(255,255,255,.92);
|
||||||
box-shadow: 0 18rpx 44rpx rgba(29, 78, 59, .12);
|
box-shadow: 0 18rpx 44rpx rgba(29, 78, 59, .12);
|
||||||
}
|
}
|
||||||
.eg-board-shell.is-flowing {
|
|
||||||
border-color: rgba(245, 158, 11, .72);
|
|
||||||
box-shadow: 0 0 0 5rpx rgba(255, 221, 89, .2), 0 18rpx 46rpx rgba(234, 88, 12, .2);
|
|
||||||
animation: egFlowShell 1.05s ease-in-out infinite alternate;
|
|
||||||
}
|
|
||||||
|
|
||||||
.eg-board-top { display: flex; align-items: center; justify-content: space-between; margin: 0 4rpx 14rpx; }
|
.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-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 { 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-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-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-flow-state { display: flex; align-items: center; gap: 8rpx; padding: 8rpx 14rpx; border: 3rpx solid #fff3a3; border-radius: 999rpx; color: #fff; background: linear-gradient(135deg, #f97316, #dc2626); box-shadow: 0 5rpx 16rpx rgba(220,38,38,.3); font-size: 25rpx; font-weight: 1000; white-space: nowrap; animation: egFlowBadge .62s ease-in-out infinite alternate; }
|
|
||||||
.eg-flow-state > text:last-child { min-width: 62rpx; color: #fff7a8; text-align: right; }
|
|
||||||
.eg-flow-state.is-paused { border-color: #d9f6ff; background: linear-gradient(135deg, #1687b4, #3155a6); box-shadow: 0 5rpx 16rpx rgba(34,109,166,.28); animation: none; }
|
|
||||||
.eg-risk { display: flex; align-items: center; gap: 10rpx; color: #8d512c; font-size: 24rpx; }
|
.eg-risk { display: flex; align-items: center; gap: 10rpx; color: #8d512c; font-size: 24rpx; }
|
||||||
.eg-risk-dots { display: flex; gap: 5rpx; }
|
.eg-risk-dots { display: flex; gap: 5rpx; }
|
||||||
.eg-risk-dot { width: 12rpx; height: 12rpx; border-radius: 50%; background: #ead8c9; }
|
.eg-risk-dot { width: 12rpx; height: 12rpx; border-radius: 50%; background: #ead8c9; }
|
||||||
@@ -212,15 +167,7 @@
|
|||||||
.eg-cell.is-level-high { background: #fff0e4; }
|
.eg-cell.is-level-high { background: #fff0e4; }
|
||||||
.eg-cell.is-milk { background: #edf7ff; }
|
.eg-cell.is-milk { background: #edf7ff; }
|
||||||
.eg-cell.is-high { border-color: #ef7b45; }
|
.eg-cell.is-high { border-color: #ef7b45; }
|
||||||
.eg-cell.is-villain {
|
|
||||||
background: radial-gradient(circle at 50% 38%, #fff7e8 0%, #ffe9d8 70%, #ffddc6 100%);
|
|
||||||
box-shadow: inset 0 -5rpx 0 rgba(154,57,25,.08), 0 6rpx 15rpx rgba(191,72,31,.16);
|
|
||||||
}
|
|
||||||
.eg-cell.is-villain .eg-food-image { animation: egVillainPeek 2.2s ease-in-out infinite; }
|
|
||||||
.eg-cell.is-villain.is-clearing .eg-food-image,
|
|
||||||
.eg-cell.is-villain.is-dragging .eg-food-image { animation: none; }
|
|
||||||
.eg-food-image { width: 132rpx; height: 92rpx; margin-top: 7rpx; border-radius: 17rpx; filter: saturate(1.06) contrast(1.03); }
|
.eg-food-image { width: 132rpx; height: 92rpx; margin-top: 7rpx; border-radius: 17rpx; filter: saturate(1.06) contrast(1.03); }
|
||||||
.eg-food-image.is-large-square { width: 136rpx; height: 102rpx; margin-top: 2rpx; filter: saturate(1.1) contrast(1.06); }
|
|
||||||
.eg-food-name { margin-top: 8rpx; color: #203d34; font-size: 29rpx; font-weight: 900; letter-spacing: 1rpx; line-height: 1.1; }
|
.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 { 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-low { color: #316a54; border-color: rgba(255,255,255,.82); background: #dcefe6; box-shadow: 0 2rpx 5rpx rgba(32,101,75,.1); }
|
||||||
@@ -390,9 +337,6 @@
|
|||||||
.eg-weekly-list { display: flex; flex-direction: column; gap: 7rpx; margin-top: 15rpx; }
|
.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 { 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-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-row.is-waiting { color: #8a9b94; border-style: dashed; border-color: #dbe8e2; background: rgba(247,251,249,.72); }
|
|
||||||
.eg-weekly-row.is-waiting .eg-weekly-avatar { color: #6e887e; border-color: #dce9e3; background: #edf4f1; box-shadow: none; }
|
|
||||||
.eg-weekly-row.is-waiting .eg-weekly-count { color: #9baba4; }
|
|
||||||
.eg-weekly-place { width: 43rpx; color: #6b7d76; font-size: 27rpx; font-weight: 1000; text-align: center; }
|
.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-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 { 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; }
|
||||||
@@ -406,9 +350,6 @@
|
|||||||
.eg-weekly-name { flex: 1; margin-left: 13rpx; overflow: hidden; font-size: 27rpx; font-weight: 850; text-overflow: ellipsis; white-space: nowrap; }
|
.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-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-weekly-count { min-width: 70rpx; color: #315b4d; font-size: 29rpx; font-weight: 1000; text-align: right; }
|
||||||
.eg-weekly-connection { display: flex; align-items: center; justify-content: space-between; gap: 12rpx; margin-top: 13rpx; padding: 12rpx 15rpx; border: 2rpx solid #e4d7b2; border-radius: 18rpx; color: #6b6655; background: #fff9e8; font-size: 21rpx; font-weight: 700; }
|
|
||||||
.eg-weekly-connection > text:first-child { flex: 1; }
|
|
||||||
.eg-weekly-retry { flex-shrink: 0; padding: 6rpx 12rpx; border-radius: 999rpx; color: #fff; background: #23765c; font-weight: 900; }
|
|
||||||
.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 { 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-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-card.is-bright { border-color: #f4c77b; background: linear-gradient(135deg, #fff8dc, #ffeec7); }
|
||||||
@@ -476,13 +417,6 @@
|
|||||||
100% { opacity: 0; transform: scale(.32) rotate(7deg); }
|
100% { opacity: 0; transform: scale(.32) rotate(7deg); }
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes egVillainPeek {
|
|
||||||
0%, 72%, 100% { transform: translateY(0) rotate(0); filter: saturate(1.08) contrast(1.05); }
|
|
||||||
80% { transform: translateY(-3rpx) rotate(-1.5deg); filter: saturate(1.15) contrast(1.08); }
|
|
||||||
88% { transform: translateY(0) rotate(1.5deg); }
|
|
||||||
95% { transform: translateY(-2rpx) rotate(0); }
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes egTaskRewardFlight {
|
@keyframes egTaskRewardFlight {
|
||||||
0% { opacity: 0; transform: translate3d(-20rpx, 0, 0) scale(.62) rotate(-8deg); }
|
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); }
|
16% { opacity: 1; transform: translate3d(0, -24rpx, 0) scale(1.1) rotate(3deg); }
|
||||||
@@ -490,13 +424,6 @@
|
|||||||
100% { opacity: 0; transform: translate3d(350rpx, 1205rpx, 0) scale(.3) rotate(2deg); }
|
100% { opacity: 0; transform: translate3d(350rpx, 1205rpx, 0) scale(.3) rotate(2deg); }
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes egNavNotice {
|
|
||||||
0% { opacity: 0; transform: translate(-50%, -42%); }
|
|
||||||
12% { opacity: .9; transform: translate(-50%, -50%); }
|
|
||||||
42% { opacity: .82; transform: translate(-50%, -50%); }
|
|
||||||
100% { opacity: 0; transform: translate(-50%, -58%); }
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes egRewardGlow {
|
@keyframes egRewardGlow {
|
||||||
from { opacity: .4; transform: scale(.86); }
|
from { opacity: .4; transform: scale(.86); }
|
||||||
to { opacity: .9; transform: scale(1.12); }
|
to { opacity: .9; transform: scale(1.12); }
|
||||||
@@ -626,16 +553,6 @@
|
|||||||
100% { opacity: 1; transform: scale(1); }
|
100% { opacity: 1; transform: scale(1); }
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes egFlowShell {
|
|
||||||
from { filter: saturate(1); }
|
|
||||||
to { filter: saturate(1.08); }
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes egFlowBadge {
|
|
||||||
from { transform: scale(.98); filter: brightness(.96); }
|
|
||||||
to { transform: scale(1.035); filter: brightness(1.12); }
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes egStageIn {
|
@keyframes egStageIn {
|
||||||
0% { opacity: 0; transform: scale(.7) translateY(34rpx); }
|
0% { opacity: 0; transform: scale(.7) translateY(34rpx); }
|
||||||
18% { opacity: 1; transform: scale(1.06) translateY(0); }
|
18% { opacity: 1; transform: scale(1.06) translateY(0); }
|
||||||
|
|||||||
@@ -1,51 +1,51 @@
|
|||||||
/**
|
/**
|
||||||
* Google Stitch MCP · VitalMint Health
|
* Google Stitch MCP · VitalMint Health
|
||||||
* 设计 token 与 stitch-weekly.html tailwind.config 一一对应
|
* 设计 token 与 stitch-weekly.html tailwind.config 一一对应
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.stitch-vitalmint {
|
.stitch-vitalmint {
|
||||||
--primary: #006c49;
|
--primary: #006c49;
|
||||||
--on-primary: #ffffff;
|
--on-primary: #ffffff;
|
||||||
--on-primary-fixed-variant: #005236;
|
--on-primary-fixed-variant: #005236;
|
||||||
--primary-container: #10b981;
|
--primary-container: #10b981;
|
||||||
--on-primary-container: #00422b;
|
--on-primary-container: #00422b;
|
||||||
--inverse-primary: #4edea3;
|
--inverse-primary: #4edea3;
|
||||||
|
|
||||||
--surface: #f4fbf4;
|
--surface: #f4fbf4;
|
||||||
--on-surface: #161d19;
|
--on-surface: #161d19;
|
||||||
--on-surface-variant: #3c4a42;
|
--on-surface-variant: #3c4a42;
|
||||||
--surface-container-lowest: #ffffff;
|
--surface-container-lowest: #ffffff;
|
||||||
--surface-container-low: #eef6ee;
|
--surface-container-low: #eef6ee;
|
||||||
--surface-container: #e8f0e9;
|
--surface-container: #e8f0e9;
|
||||||
--surface-container-high: #e3eae3;
|
--surface-container-high: #e3eae3;
|
||||||
--surface-container-highest: #dde4dd;
|
--surface-container-highest: #dde4dd;
|
||||||
--surface-variant: #dde4dd;
|
--surface-variant: #dde4dd;
|
||||||
|
|
||||||
--outline: #6c7a71;
|
--outline: #6c7a71;
|
||||||
--outline-variant: #bbcabf;
|
--outline-variant: #bbcabf;
|
||||||
|
|
||||||
--error: #ba1a1a;
|
--error: #ba1a1a;
|
||||||
--on-error-container: #93000a;
|
--on-error-container: #93000a;
|
||||||
--error-container: #ffdad6;
|
--error-container: #ffdad6;
|
||||||
--tertiary: #a43a3a;
|
--tertiary: #a43a3a;
|
||||||
--tertiary-container: #fc7c78;
|
--tertiary-container: #fc7c78;
|
||||||
--on-tertiary: #ffffff;
|
--on-tertiary: #ffffff;
|
||||||
--on-tertiary-container: #711419;
|
--on-tertiary-container: #711419;
|
||||||
|
|
||||||
--st-container-margin: 40rpx;
|
--st-container-margin: 40rpx;
|
||||||
--st-section-gap: 64rpx;
|
--st-section-gap: 64rpx;
|
||||||
--st-card-padding: 40rpx;
|
--st-card-padding: 40rpx;
|
||||||
--st-stack-gap: 32rpx;
|
--st-stack-gap: 32rpx;
|
||||||
--st-inline-gap: 24rpx;
|
--st-inline-gap: 24rpx;
|
||||||
--st-radius-2xl: 48rpx;
|
--st-radius-2xl: 48rpx;
|
||||||
--st-radius-xl: 24rpx;
|
--st-radius-xl: 24rpx;
|
||||||
--st-radius-lg: 16rpx;
|
--st-radius-lg: 16rpx;
|
||||||
--st-shadow-ambient: 0 20rpx 60rpx -20rpx rgba(0, 108, 73, 0.08);
|
--st-shadow-ambient: 0 20rpx 60rpx -20rpx rgba(0, 108, 73, 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
.weekly-page.stitch-vitalmint {
|
.weekly-page.stitch-vitalmint {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
color: var(--on-surface);
|
color: var(--on-surface);
|
||||||
padding-bottom: calc(200rpx + env(safe-area-inset-bottom));
|
padding-bottom: calc(200rpx + env(safe-area-inset-bottom));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,56 +1,56 @@
|
|||||||
/** 将接口 msg / 异常对象转为可展示的字符串,避免 [object Object] */
|
/** 将接口 msg / 异常对象转为可展示的字符串,避免 [object Object] */
|
||||||
export function formatUserMessage(msg, fallback = '') {
|
export function formatUserMessage(msg, fallback = '') {
|
||||||
if (msg == null || msg === '') return fallback
|
if (msg == null || msg === '') return fallback
|
||||||
if (typeof msg === 'string') return msg
|
if (typeof msg === 'string') return msg
|
||||||
if (typeof msg === 'number' || typeof msg === 'boolean') return String(msg)
|
if (typeof msg === 'number' || typeof msg === 'boolean') return String(msg)
|
||||||
if (Array.isArray(msg)) {
|
if (Array.isArray(msg)) {
|
||||||
const parts = msg.map((item) => {
|
const parts = msg.map((item) => {
|
||||||
if (item == null) return ''
|
if (item == null) return ''
|
||||||
if (typeof item === 'string') return item
|
if (typeof item === 'string') return item
|
||||||
if (typeof item === 'number' || typeof item === 'boolean') return String(item)
|
if (typeof item === 'number' || typeof item === 'boolean') return String(item)
|
||||||
if (typeof item === 'object') {
|
if (typeof item === 'object') {
|
||||||
return item.msg || item.message || item.error || item.title || item.label || ''
|
return item.msg || item.message || item.error || item.title || item.label || ''
|
||||||
}
|
}
|
||||||
return String(item)
|
return String(item)
|
||||||
}).filter(Boolean)
|
}).filter(Boolean)
|
||||||
return parts.length ? parts.join(';') : fallback
|
return parts.length ? parts.join(';') : fallback
|
||||||
}
|
}
|
||||||
if (typeof msg === 'object') {
|
if (typeof msg === 'object') {
|
||||||
return msg.msg || msg.message || msg.error || msg.title || msg.label || fallback
|
return msg.msg || msg.message || msg.error || msg.title || msg.label || fallback
|
||||||
}
|
}
|
||||||
return String(msg)
|
return String(msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function showUserToast(title, options = {}) {
|
export function showUserToast(title, options = {}) {
|
||||||
const text = formatUserMessage(title, '')
|
const text = formatUserMessage(title, '')
|
||||||
if (!text) return
|
if (!text) return
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: text,
|
title: text,
|
||||||
icon: options.icon || 'none',
|
icon: options.icon || 'none',
|
||||||
duration: options.duration
|
duration: options.duration
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatDate(date) {
|
export function formatDate(date) {
|
||||||
const y = date.getFullYear()
|
const y = date.getFullYear()
|
||||||
const m = String(date.getMonth() + 1).padStart(2, '0')
|
const m = String(date.getMonth() + 1).padStart(2, '0')
|
||||||
const d = String(date.getDate()).padStart(2, '0')
|
const d = String(date.getDate()).padStart(2, '0')
|
||||||
return `${y}-${m}-${d}`
|
return `${y}-${m}-${d}`
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseDate(str) {
|
export function parseDate(str) {
|
||||||
if (!str) return new Date()
|
if (!str) return new Date()
|
||||||
const [y, m, d] = str.split('-').map(Number)
|
const [y, m, d] = str.split('-').map(Number)
|
||||||
return new Date(y, (m || 1) - 1, d || 1)
|
return new Date(y, (m || 1) - 1, d || 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toNumber(v) {
|
export function toNumber(v) {
|
||||||
if (v === null || v === undefined || v === '') return null
|
if (v === null || v === undefined || v === '') return null
|
||||||
const n = Number(v)
|
const n = Number(v)
|
||||||
return Number.isFinite(n) && n !== 0 ? n : null
|
return Number.isFinite(n) && n !== 0 ? n : null
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getWeekday(dateStr) {
|
export function getWeekday(dateStr) {
|
||||||
const w = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
|
const w = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
|
||||||
return w[parseDate(dateStr).getDay()]
|
return w[parseDate(dateStr).getDay()]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
import uni from '@dcloudio/vite-plugin-uni'
|
import uni from '@dcloudio/vite-plugin-uni'
|
||||||
import Optimization from '@uni-ku/bundle-optimizer'
|
import Optimization from '@uni-ku/bundle-optimizer'
|
||||||
import { tongjiFoodAssetsPlugin } from './tongji/endless-game/build/tongjiFoodAssetsPlugin'
|
|
||||||
|
|
||||||
// uni-app 工程根目录就是源码目录(HBuilderX 兼容)
|
// uni-app 工程根目录就是源码目录(HBuilderX 兼容)
|
||||||
// 编译产物默认输出到 ./dist/<mode>/<platform>
|
// 编译产物默认输出到 ./dist/<mode>/<platform>
|
||||||
@@ -17,7 +16,6 @@ export default defineConfig({
|
|||||||
dts: false,
|
dts: false,
|
||||||
logger: false,
|
logger: false,
|
||||||
}),
|
}),
|
||||||
tongjiFoodAssetsPlugin(),
|
|
||||||
],
|
],
|
||||||
css: {
|
css: {
|
||||||
preprocessorOptions: {
|
preprocessorOptions: {
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<script>
|
||||||
|
var coverSupport = 'CSS' in window && typeof CSS.supports === 'function' && (CSS.supports('top: env(a)') ||
|
||||||
|
CSS.supports('top: constant(a)'))
|
||||||
|
document.write(
|
||||||
|
'<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' +
|
||||||
|
(coverSupport ? ', viewport-fit=cover' : '') + '" />')
|
||||||
|
</script>
|
||||||
|
<title></title>
|
||||||
|
<!--preload-links-->
|
||||||
|
<!--app-context-->
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"><!--app-html--></div>
|
||||||
|
<script type="module" src="/main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
import App from './App'
|
||||||
|
|
||||||
|
var baseUrl = 'https://admin.zhenyangtang.com.cn/'
|
||||||
|
|
||||||
|
function joinApiUrl(base, path) {
|
||||||
|
const b = String(base || '').replace(/\/+$/, '')
|
||||||
|
const p = String(path || '').replace(/^\/+/, '')
|
||||||
|
return p ? `${b}/${p}` : b
|
||||||
|
}
|
||||||
|
|
||||||
|
// #ifndef VUE3
|
||||||
|
import Vue from 'vue'
|
||||||
|
import './uni.promisify.adaptor'
|
||||||
|
|
||||||
|
Vue.config.productionTip = false
|
||||||
|
Vue.prototype.$url = baseUrl
|
||||||
|
Vue.prototype.apiUrl = function apiurl(promise, Loading = true) {
|
||||||
|
let token = uni.getStorageSync('token')
|
||||||
|
promise.header = {
|
||||||
|
token: token,
|
||||||
|
'content-type': 'application/x-www-form-urlencoded',
|
||||||
|
}
|
||||||
|
let header = {}
|
||||||
|
if (promise.header) {
|
||||||
|
header = promise.header
|
||||||
|
}
|
||||||
|
let data = {}
|
||||||
|
if (promise.data) {
|
||||||
|
data = promise.data
|
||||||
|
}
|
||||||
|
let url = joinApiUrl(Vue.prototype.$url, promise.url)
|
||||||
|
|
||||||
|
return new Promise(function (resolve, reject) {
|
||||||
|
uni.request({
|
||||||
|
url: url,
|
||||||
|
data: data,
|
||||||
|
timeout: promise.timeout != null ? promise.timeout : 15000,
|
||||||
|
method: promise.method ? promise.method : 'POST',
|
||||||
|
header: header,
|
||||||
|
sslVerify: false,
|
||||||
|
success: (res) => {
|
||||||
|
const data = res.data
|
||||||
|
resolve(data)
|
||||||
|
},
|
||||||
|
fail(e) {
|
||||||
|
reject(e)
|
||||||
|
},
|
||||||
|
complete(e) {},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
App.mpType = 'app'
|
||||||
|
const app = new Vue({
|
||||||
|
...App,
|
||||||
|
})
|
||||||
|
app.$mount()
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
// #ifdef VUE3
|
||||||
|
import { createSSRApp } from 'vue'
|
||||||
|
|
||||||
|
function apiUrl(promise, Loading = true) {
|
||||||
|
const token = uni.getStorageSync('token')
|
||||||
|
|
||||||
|
const header = {
|
||||||
|
token: token,
|
||||||
|
'content-type': 'application/x-www-form-urlencoded',
|
||||||
|
...(promise.header || {}),
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = promise.data || {}
|
||||||
|
const url = joinApiUrl(baseUrl, promise.url || '')
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (Loading) {
|
||||||
|
uni.showLoading({ title: '加载中...', mask: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
uni.request({
|
||||||
|
url: url,
|
||||||
|
data: data,
|
||||||
|
timeout: promise.timeout != null ? promise.timeout : 15000,
|
||||||
|
method: promise.method || 'POST',
|
||||||
|
header: header,
|
||||||
|
sslVerify: false,
|
||||||
|
success: (res) => {
|
||||||
|
const data = res.data
|
||||||
|
|
||||||
|
if (data.code === -1) {
|
||||||
|
uni.removeStorageSync('token')
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(data)
|
||||||
|
},
|
||||||
|
fail: (e) => {
|
||||||
|
reject(e)
|
||||||
|
},
|
||||||
|
complete: (e) => {
|
||||||
|
if (Loading) {
|
||||||
|
uni.hideLoading()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析页面参数(支持普通参数和扫码 scene 参数)
|
||||||
|
* @param {Object} options - 页面 options
|
||||||
|
* @returns {Object} 解析后的参数对象
|
||||||
|
*/
|
||||||
|
function parsePageParams(options) {
|
||||||
|
const params = {}
|
||||||
|
if (!options) return params
|
||||||
|
|
||||||
|
if (options.scene) {
|
||||||
|
try {
|
||||||
|
const decodedScene = decodeURIComponent(options.scene)
|
||||||
|
decodedScene.split('&').forEach((item) => {
|
||||||
|
const [key, value] = item.split('=')
|
||||||
|
if (key && value) {
|
||||||
|
params[key] = value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('解析scene参数失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.keys(options).forEach((key) => {
|
||||||
|
if (key !== 'scene' && options[key]) {
|
||||||
|
params[key] = options[key]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createApp() {
|
||||||
|
const app = createSSRApp(App)
|
||||||
|
|
||||||
|
app.config.globalProperties.$url = baseUrl
|
||||||
|
app.config.globalProperties.apiUrl = apiUrl
|
||||||
|
app.config.globalProperties.$parsePageParams = parsePageParams
|
||||||
|
|
||||||
|
return {
|
||||||
|
app,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// #endif
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
{
|
||||||
|
"name" : "tongji-uniapp",
|
||||||
|
"appid" : "__UNI__TONGJI01",
|
||||||
|
"description" : "血糖管理 / 日常护理",
|
||||||
|
"versionName" : "1.0.0",
|
||||||
|
"versionCode" : "100",
|
||||||
|
"transformPx" : false,
|
||||||
|
"app-plus" : {
|
||||||
|
"usingComponents" : true,
|
||||||
|
"nvueStyleCompiler" : "uni-app",
|
||||||
|
"compilerVersion" : 3,
|
||||||
|
"splashscreen" : {
|
||||||
|
"alwaysShowBeforeRender" : true,
|
||||||
|
"waiting" : true,
|
||||||
|
"autoclose" : true,
|
||||||
|
"delay" : 0
|
||||||
|
},
|
||||||
|
"modules" : {},
|
||||||
|
"distribute" : {
|
||||||
|
"android" : {
|
||||||
|
"permissions" : [
|
||||||
|
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.RECORD_AUDIO\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"ios" : {},
|
||||||
|
"sdkConfigs" : {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"quickapp" : {},
|
||||||
|
"mp-weixin" : {
|
||||||
|
"appid" : "wx79b9a0bfbfe7cbcd",
|
||||||
|
"lazyCodeLoading" : "requiredComponents",
|
||||||
|
"setting" : {
|
||||||
|
"urlCheck" : false,
|
||||||
|
"minified" : true
|
||||||
|
},
|
||||||
|
"optimization" : {
|
||||||
|
"subPackages" : true
|
||||||
|
},
|
||||||
|
"usingComponents" : true,
|
||||||
|
"requiredBackgroundModes" : [ "audio" ],
|
||||||
|
"plugins" : {
|
||||||
|
"WechatSI" : {
|
||||||
|
"version" : "0.3.5",
|
||||||
|
"provider" : "wx069ba97219f66d99"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mp-alipay" : {
|
||||||
|
"usingComponents" : true
|
||||||
|
},
|
||||||
|
"mp-baidu" : {
|
||||||
|
"usingComponents" : true
|
||||||
|
},
|
||||||
|
"mp-toutiao" : {
|
||||||
|
"usingComponents" : true
|
||||||
|
},
|
||||||
|
"uniStatistics" : {
|
||||||
|
"enable" : false
|
||||||
|
},
|
||||||
|
"vueVersion" : "3"
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
{
|
||||||
|
"name": "tongji-uniapp",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Tongji blood-sugar uni-app extracted from TUICallKit-Vue3",
|
||||||
|
"main": "main.js",
|
||||||
|
"scripts": {
|
||||||
|
"dev:mp-weixin": "cross-env UNI_INPUT_DIR=. uni -p mp-weixin",
|
||||||
|
"dev:h5": "cross-env UNI_INPUT_DIR=. uni",
|
||||||
|
"dev:app": "cross-env UNI_INPUT_DIR=. uni -p app",
|
||||||
|
"build:mp-weixin": "cross-env UNI_INPUT_DIR=. uni build -p mp-weixin",
|
||||||
|
"build:h5": "cross-env UNI_INPUT_DIR=. uni build",
|
||||||
|
"build:app": "cross-env UNI_INPUT_DIR=. uni build -p app"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"uniapp",
|
||||||
|
"tongji"
|
||||||
|
],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"@dcloudio/uni-app": "3.0.0-4060620250520001",
|
||||||
|
"@dcloudio/uni-app-harmony": "3.0.0-4060620250520001",
|
||||||
|
"@dcloudio/uni-app-plus": "3.0.0-4060620250520001",
|
||||||
|
"@dcloudio/uni-components": "3.0.0-4060620250520001",
|
||||||
|
"@dcloudio/uni-h5": "3.0.0-4060620250520001",
|
||||||
|
"@dcloudio/uni-mp-alipay": "3.0.0-4060620250520001",
|
||||||
|
"@dcloudio/uni-mp-baidu": "3.0.0-4060620250520001",
|
||||||
|
"@dcloudio/uni-mp-jd": "3.0.0-4060620250520001",
|
||||||
|
"@dcloudio/uni-mp-kuaishou": "3.0.0-4060620250520001",
|
||||||
|
"@dcloudio/uni-mp-lark": "3.0.0-4060620250520001",
|
||||||
|
"@dcloudio/uni-mp-qq": "3.0.0-4060620250520001",
|
||||||
|
"@dcloudio/uni-mp-toutiao": "3.0.0-4060620250520001",
|
||||||
|
"@dcloudio/uni-mp-weixin": "3.0.0-4060620250520001",
|
||||||
|
"@dcloudio/uni-mp-xhs": "3.0.0-4060620250520001",
|
||||||
|
"@dcloudio/uni-quickapp-webview": "3.0.0-4060620250520001",
|
||||||
|
"vue": "^3.4.21"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@dcloudio/types": "^3.4.8",
|
||||||
|
"@dcloudio/uni-automator": "3.0.0-4060620250520001",
|
||||||
|
"@dcloudio/uni-cli-shared": "3.0.0-4060620250520001",
|
||||||
|
"@dcloudio/uni-stacktracey": "3.0.0-4060620250520001",
|
||||||
|
"@dcloudio/vite-plugin-uni": "3.0.0-4060620250520001",
|
||||||
|
"@types/node": "^20.11.0",
|
||||||
|
"@vue/runtime-core": "^3.4.21",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
|
"less": "^4.6.4",
|
||||||
|
"sass": "^1.77.8",
|
||||||
|
"typescript": "^5.4.0",
|
||||||
|
"vite": "^5.2.8"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
{
|
||||||
|
"pages": [
|
||||||
|
{
|
||||||
|
"path": "tongji/pages/weekly",
|
||||||
|
"style": {
|
||||||
|
"navigationStyle": "custom",
|
||||||
|
"navigationBarTitleText": "血糖管理",
|
||||||
|
"backgroundColor": "#f4fbf4",
|
||||||
|
"enablePullDownRefresh": true,
|
||||||
|
"mp-weixin": {
|
||||||
|
"usingPlugins": {
|
||||||
|
"WechatSI": {
|
||||||
|
"version": "0.3.5",
|
||||||
|
"provider": "wx069ba97219f66d99"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/user/user",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "我的",
|
||||||
|
"backgroundColor": "#e8eaed",
|
||||||
|
"backgroundColorTop": "#e8eaed"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/user/userinfo",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "个人资料"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/Card/Card",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "就诊卡"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/Card/edit_card",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "修改就诊卡"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/Card/contact",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "服务"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "tongji/pages/index",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "血糖记录",
|
||||||
|
"navigationBarBackgroundColor": "#204e2b",
|
||||||
|
"navigationBarTextStyle": "white",
|
||||||
|
"backgroundColor": "#faf9f5",
|
||||||
|
"enablePullDownRefresh": true,
|
||||||
|
"mp-weixin": {
|
||||||
|
"usingPlugins": {
|
||||||
|
"WechatSI": {
|
||||||
|
"version": "0.3.5",
|
||||||
|
"provider": "wx069ba97219f66d99"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "tongji/pages/more",
|
||||||
|
"style": {
|
||||||
|
"navigationStyle": "custom",
|
||||||
|
"navigationBarTitleText": "日常护理",
|
||||||
|
"backgroundColor": "#f4fbf4",
|
||||||
|
"enablePullDownRefresh": true,
|
||||||
|
"mp-weixin": {
|
||||||
|
"usingPlugins": {
|
||||||
|
"WechatSI": {
|
||||||
|
"version": "0.3.5",
|
||||||
|
"provider": "wx069ba97219f66d99"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "tongji/endless-game/index",
|
||||||
|
"style": {
|
||||||
|
"navigationStyle": "custom",
|
||||||
|
"navigationBarTitleText": "识糖小课堂",
|
||||||
|
"backgroundColor": "#eefbf4",
|
||||||
|
"disableScroll": false,
|
||||||
|
"enableShareAppMessage": true,
|
||||||
|
"enableShareTimeline": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"subPackages": [
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"globalStyle": {
|
||||||
|
"navigationBarTextStyle": "black",
|
||||||
|
"navigationBarTitleText": "甄养堂血糖管理",
|
||||||
|
"navigationBarBackgroundColor": "#F8F8F8",
|
||||||
|
"backgroundColor": "#F8F8F8"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,455 @@
|
|||||||
|
<template>
|
||||||
|
<view class="card-container">
|
||||||
|
<!-- 顶部标题栏 -->
|
||||||
|
|
||||||
|
|
||||||
|
<!-- 就诊卡列表 -->
|
||||||
|
<view class="card-list">
|
||||||
|
<view
|
||||||
|
v-for="card in cardList"
|
||||||
|
:key="card.id"
|
||||||
|
class="card-item"
|
||||||
|
@click="viewCardDetail(card)"
|
||||||
|
>
|
||||||
|
<!-- 卡片头部 -->
|
||||||
|
<view class="card-header">
|
||||||
|
<view class="card-id">
|
||||||
|
<text class="label">诊单编号:</text>
|
||||||
|
<text class="value">{{ card.id }}</text>
|
||||||
|
</view>
|
||||||
|
<view :class="['status-badge', getStatusClass(card.status)]">
|
||||||
|
{{ card.status_desc || getStatusText(card.status) }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 患者信息 -->
|
||||||
|
<view class="card-info">
|
||||||
|
<view class="info-row">
|
||||||
|
<text class="info-label">患者姓名:</text>
|
||||||
|
<text class="info-value">{{ card.patient_name || '-' }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-row">
|
||||||
|
<text class="info-label">性别年龄:</text>
|
||||||
|
<text class="info-value">{{ card.gender_desc || '-' }} / {{ card.age }}岁</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-row">
|
||||||
|
<text class="info-label">诊断类型:</text>
|
||||||
|
<text class="info-value">{{ getDiagnosisTypeName(card.diagnosis_type) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-row">
|
||||||
|
<text class="info-label">证型:</text>
|
||||||
|
<text class="info-value">{{ getSyndromeTypeName(card.syndrome_type) }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 时间信息 -->
|
||||||
|
<view class="card-footer">
|
||||||
|
<view class="time-info">
|
||||||
|
<text class="time-label">诊断日期:</text>
|
||||||
|
<text class="time-value">{{ card.diagnosis_date_text || formatDate(card.diagnosis_date) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="time-info">
|
||||||
|
<text class="time-label">创建时间:</text>
|
||||||
|
<text class="time-value">{{ card.create_time || '-' }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 快捷入口 -->
|
||||||
|
<view class="card-actions">
|
||||||
|
<view class="card-action-btn primary" @click.stop="viewDailyRecord(card)">
|
||||||
|
<text class="card-action-icon">📈</text>
|
||||||
|
<text class="card-action-text">日常记录</text>
|
||||||
|
</view>
|
||||||
|
<view class="card-action-btn" @click.stop="viewCardDetail(card)">
|
||||||
|
<text class="card-action-icon">📝</text>
|
||||||
|
<text class="card-action-text">查看 / 编辑</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 空状态 -->
|
||||||
|
<view v-if="!loading && cardList.length === 0" class="empty-state">
|
||||||
|
<uni-icons type="wallet" size="80" color="#1890ff"></uni-icons>
|
||||||
|
<text class="empty-text">暂无就诊卡</text>
|
||||||
|
<view class="add-card-btn" @click="createCard">新建就诊卡</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 加载状态 -->
|
||||||
|
<view v-if="loading" class="loading-state">
|
||||||
|
<text class="loading-text">加载中...</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, getCurrentInstance } from 'vue'
|
||||||
|
import { onLaunch, onShow, onHide, onError ,onShareAppMessage} from '@dcloudio/uni-app'
|
||||||
|
const { proxy } = getCurrentInstance()
|
||||||
|
|
||||||
|
// 数据
|
||||||
|
const cardList = ref([])
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
// 页面加载时获取数据
|
||||||
|
onMounted(() => {
|
||||||
|
loadCardList()
|
||||||
|
})
|
||||||
|
onShow(() => {
|
||||||
|
loadCardList()
|
||||||
|
})
|
||||||
|
// 加载就诊卡列表
|
||||||
|
const loadCardList = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
// 检查token是否存在
|
||||||
|
const token = uni.getStorageSync('token')
|
||||||
|
if (!token) {
|
||||||
|
uni.showToast({ title: '请先登录', icon: 'none' })
|
||||||
|
// setTimeout(() => {
|
||||||
|
// uni.redirectTo({ url: '/pages/login/login' })
|
||||||
|
// }, 1500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取当前用户的patient_id
|
||||||
|
const userData = uni.getStorageSync('userData')
|
||||||
|
const patientId = userData?.diagnosis?.patient_id
|
||||||
|
|
||||||
|
if (!patientId) {
|
||||||
|
//uni.showToast({ title: '患者信息不存在', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用后端接口获取就诊卡列表
|
||||||
|
const res = await proxy.apiUrl({
|
||||||
|
url: '/api/tcm/getCardList',
|
||||||
|
method: 'GET',
|
||||||
|
data: {
|
||||||
|
patient_id: patientId
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (res.code === 1) {
|
||||||
|
cardList.value = res.data || []
|
||||||
|
} else {
|
||||||
|
uni.showToast({ title: res.msg || '获取失败', icon: 'none' })
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('加载就诊卡列表失败:', err)
|
||||||
|
//uni.showToast({ title: '加载失败', icon: 'none' })
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查看卡片详情
|
||||||
|
const viewCardDetail = (card) => {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: `/pages/Card/edit_card?id=${card.id}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查看日常记录(血糖血压 / 饮食 / 运动 / 跟踪备注 + 波浪图)
|
||||||
|
const viewDailyRecord = (card) => {
|
||||||
|
const url = `/tongji/pages/index?diagnosis_id=${card.id}` +
|
||||||
|
`&patient_id=${card.patient_id || ''}` +
|
||||||
|
`&patient_name=${encodeURIComponent(card.patient_name || '')}` +
|
||||||
|
`&age=${card.age || ''}` +
|
||||||
|
`&gender=${card.gender || ''}`
|
||||||
|
uni.navigateTo({ url })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新建就诊卡(patient_id 创建后自动生成,仅需登录)
|
||||||
|
const createCard = () => {
|
||||||
|
const token = uni.getStorageSync('token')
|
||||||
|
if (!token) {
|
||||||
|
uni.showToast({ title: '请先登录', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uni.navigateTo({
|
||||||
|
url: '/pages/Card/edit_card?add=1'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取状态文本
|
||||||
|
const getStatusText = (status) => {
|
||||||
|
return status === 1 ? '启用' : '禁用'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取状态样式类
|
||||||
|
const getStatusClass = (status) => {
|
||||||
|
return status === 1 ? 'status-confirmed' : 'status-pending'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 诊断类型字典
|
||||||
|
const diagnosisTypeMap = {
|
||||||
|
'first_visit': '初诊',
|
||||||
|
'follow_up': '复诊',
|
||||||
|
'consultation': '会诊'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 证型字典
|
||||||
|
const syndromeTypeMap = {
|
||||||
|
'qi_deficiency': '气虚',
|
||||||
|
'blood_deficiency': '血虚',
|
||||||
|
'yin_deficiency': '阴虚',
|
||||||
|
'yang_deficiency': '阳虚',
|
||||||
|
'qi_stagnation': '气滞',
|
||||||
|
'blood_stasis': '血瘀',
|
||||||
|
'phlegm_dampness': '痰湿',
|
||||||
|
'damp_heat': '湿热',
|
||||||
|
'cold_dampness': '寒湿',
|
||||||
|
'wind_cold': '风寒',
|
||||||
|
'wind_heat': '风热'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取诊断类型名称
|
||||||
|
const getDiagnosisTypeName = (type) => {
|
||||||
|
return diagnosisTypeMap[type] || type || '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取证型名称
|
||||||
|
const getSyndromeTypeName = (type) => {
|
||||||
|
return syndromeTypeMap[type] || type || '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化日期(时间戳转日期)
|
||||||
|
const formatDate = (timestamp) => {
|
||||||
|
if (!timestamp) return '-'
|
||||||
|
const date = new Date(timestamp * 1000)
|
||||||
|
const year = date.getFullYear()
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||||
|
const day = String(date.getDate()).padStart(2, '0')
|
||||||
|
return `${year}-${month}-${day}`
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
/* 适老化设计:50-80岁 - 大字号、高对比、大触控区 */
|
||||||
|
.card-container {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: linear-gradient(180deg, #e8f0fa 0%, #ffffff 100%);
|
||||||
|
padding-bottom: 56rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
background: #ffffff;
|
||||||
|
padding: 48rpx 40rpx;
|
||||||
|
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
|
||||||
|
|
||||||
|
.header-title {
|
||||||
|
font-size: 48rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #1890ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-list {
|
||||||
|
padding: 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-item {
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 28rpx;
|
||||||
|
padding: 44rpx;
|
||||||
|
margin-bottom: 36rpx;
|
||||||
|
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.08);
|
||||||
|
transition: all 0.3s;
|
||||||
|
min-height: 200rpx;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: scale(0.98);
|
||||||
|
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 32rpx;
|
||||||
|
padding-bottom: 28rpx;
|
||||||
|
border-bottom: 4rpx solid #e8eaed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-id {
|
||||||
|
.label {
|
||||||
|
font-size: 34rpx;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
font-size: 40rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #1a1a1a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge {
|
||||||
|
padding: 12rpx 28rpx;
|
||||||
|
border-radius: 28rpx;
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
|
||||||
|
&.status-confirmed {
|
||||||
|
background: #f6ffed;
|
||||||
|
color: #204e2b;
|
||||||
|
border: 4rpx solid #204e2b;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.status-pending {
|
||||||
|
background: #fff7e6;
|
||||||
|
color: #fa8c16;
|
||||||
|
border: 4rpx solid #ffd591;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-info {
|
||||||
|
margin-bottom: 28rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
|
||||||
|
.info-label {
|
||||||
|
font-size: 34rpx;
|
||||||
|
color: #555;
|
||||||
|
min-width: 200rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-value {
|
||||||
|
font-size: 36rpx;
|
||||||
|
color: #1a1a1a;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-footer {
|
||||||
|
padding-top: 28rpx;
|
||||||
|
border-top: 4rpx solid #e8eaed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 20rpx;
|
||||||
|
margin-top: 28rpx;
|
||||||
|
padding-top: 28rpx;
|
||||||
|
border-top: 4rpx solid #e8eaed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-action-btn {
|
||||||
|
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 0.2s;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: scale(0.98);
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.primary {
|
||||||
|
background: linear-gradient(135deg, #1890ff, #0ea5a4);
|
||||||
|
border-color: transparent;
|
||||||
|
|
||||||
|
.card-action-icon,
|
||||||
|
.card-action-text {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-action-icon {
|
||||||
|
font-size: 36rpx;
|
||||||
|
color: #1890ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-action-text {
|
||||||
|
font-size: 30rpx;
|
||||||
|
color: #1890ff;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
|
||||||
|
.time-label {
|
||||||
|
font-size: 30rpx;
|
||||||
|
color: #555;
|
||||||
|
min-width: 180rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-value {
|
||||||
|
font-size: 32rpx;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-count {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 12rpx;
|
||||||
|
|
||||||
|
.count-icon {
|
||||||
|
font-size: 34rpx;
|
||||||
|
margin-right: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.count-text {
|
||||||
|
font-size: 30rpx;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 160rpx 0;
|
||||||
|
|
||||||
|
.empty-icon {
|
||||||
|
font-size: 140rpx;
|
||||||
|
margin-bottom: 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-text {
|
||||||
|
font-size: 40rpx;
|
||||||
|
color: #555;
|
||||||
|
margin-bottom: 56rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-card-btn {
|
||||||
|
padding: 36rpx 88rpx;
|
||||||
|
background: #1890ff;
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 40rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
border-radius: 56rpx;
|
||||||
|
min-height: 100rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-state {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 120rpx 0;
|
||||||
|
|
||||||
|
.loading-text {
|
||||||
|
font-size: 36rpx;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,287 @@
|
|||||||
|
<template>
|
||||||
|
<view class="card-container">
|
||||||
|
<!-- 顶部标题栏 -->
|
||||||
|
<view class="header">
|
||||||
|
<text class="header-title">{{ data.title }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 就诊卡列表 -->
|
||||||
|
<view class="card-list" v-html="data.content">
|
||||||
|
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, getCurrentInstance } from 'vue'
|
||||||
|
|
||||||
|
const { proxy } = getCurrentInstance()
|
||||||
|
|
||||||
|
// 数据
|
||||||
|
const data = ref({})
|
||||||
|
const loading = ref(false)
|
||||||
|
const type=ref('')
|
||||||
|
// 页面加载时获取数据
|
||||||
|
onMounted(() => {
|
||||||
|
loadCardList()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 加载就诊卡列表
|
||||||
|
const loadCardList = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
// 检查token是否存在
|
||||||
|
const pages = getCurrentPages()
|
||||||
|
const currentPage = pages[pages.length - 1]
|
||||||
|
type.value = currentPage.options.type
|
||||||
|
|
||||||
|
// 调用后端接口获取就诊卡列表
|
||||||
|
const res = await proxy.apiUrl({
|
||||||
|
url: '/api/index/policy',
|
||||||
|
method: 'GET',
|
||||||
|
data: {
|
||||||
|
type: type.value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
data.value = res.data
|
||||||
|
uni.setNavigationBarTitle({
|
||||||
|
title: res.data.title
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error('加载就诊卡列表失败:', err)
|
||||||
|
//uni.showToast({ title: '加载失败', icon: 'none' })
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查看卡片详情
|
||||||
|
const viewCardDetail = (card) => {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: `/pages/Card/edit_card?id=${card.id}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取状态文本
|
||||||
|
const getStatusText = (status) => {
|
||||||
|
return status === 1 ? '启用' : '禁用'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取状态样式类
|
||||||
|
const getStatusClass = (status) => {
|
||||||
|
return status === 1 ? 'status-confirmed' : 'status-pending'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 诊断类型字典
|
||||||
|
const diagnosisTypeMap = {
|
||||||
|
'first_visit': '初诊',
|
||||||
|
'follow_up': '复诊',
|
||||||
|
'consultation': '会诊'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 证型字典
|
||||||
|
const syndromeTypeMap = {
|
||||||
|
'qi_deficiency': '气虚',
|
||||||
|
'blood_deficiency': '血虚',
|
||||||
|
'yin_deficiency': '阴虚',
|
||||||
|
'yang_deficiency': '阳虚',
|
||||||
|
'qi_stagnation': '气滞',
|
||||||
|
'blood_stasis': '血瘀',
|
||||||
|
'phlegm_dampness': '痰湿',
|
||||||
|
'damp_heat': '湿热',
|
||||||
|
'cold_dampness': '寒湿',
|
||||||
|
'wind_cold': '风寒',
|
||||||
|
'wind_heat': '风热'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取诊断类型名称
|
||||||
|
const getDiagnosisTypeName = (type) => {
|
||||||
|
return diagnosisTypeMap[type] || type || '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取证型名称
|
||||||
|
const getSyndromeTypeName = (type) => {
|
||||||
|
return syndromeTypeMap[type] || type || '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化日期(时间戳转日期)
|
||||||
|
const formatDate = (timestamp) => {
|
||||||
|
if (!timestamp) return '-'
|
||||||
|
const date = new Date(timestamp * 1000)
|
||||||
|
const year = date.getFullYear()
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||||
|
const day = String(date.getDate()).padStart(2, '0')
|
||||||
|
return `${year}-${month}-${day}`
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.card-container {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: linear-gradient(180deg, #f0f7ff 0%, #ffffff 100%);
|
||||||
|
padding-bottom: 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
background: #ffffff;
|
||||||
|
padding: 40rpx 30rpx;
|
||||||
|
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
|
||||||
|
|
||||||
|
.header-title {
|
||||||
|
font-size: 40rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #1890ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-list {
|
||||||
|
padding: 30rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-item {
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
padding: 30rpx;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08);
|
||||||
|
transition: all 0.3s;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: scale(0.98);
|
||||||
|
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
padding-bottom: 20rpx;
|
||||||
|
border-bottom: 2rpx solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-id {
|
||||||
|
.label {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #666666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333333;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge {
|
||||||
|
padding: 8rpx 20rpx;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
|
||||||
|
&.status-confirmed {
|
||||||
|
background: #f6ffed;
|
||||||
|
color: #52c41a;
|
||||||
|
border: 2rpx solid #b7eb8f;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.status-pending {
|
||||||
|
background: #fff7e6;
|
||||||
|
color: #fa8c16;
|
||||||
|
border: 2rpx solid #ffd591;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-info {
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
|
||||||
|
.info-label {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #666666;
|
||||||
|
min-width: 160rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-value {
|
||||||
|
font-size: 30rpx;
|
||||||
|
color: #333333;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-footer {
|
||||||
|
padding-top: 20rpx;
|
||||||
|
border-top: 2rpx solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
|
||||||
|
.time-label {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #999999;
|
||||||
|
min-width: 140rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-value {
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #666666;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-count {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 8rpx;
|
||||||
|
|
||||||
|
.count-icon {
|
||||||
|
font-size: 28rpx;
|
||||||
|
margin-right: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.count-text {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #999999;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 120rpx 0;
|
||||||
|
|
||||||
|
.empty-icon {
|
||||||
|
font-size: 120rpx;
|
||||||
|
margin-bottom: 30rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-text {
|
||||||
|
font-size: 32rpx;
|
||||||
|
color: #999999;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-state {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 80rpx 0;
|
||||||
|
|
||||||
|
.loading-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #999999;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,426 @@
|
|||||||
|
<template>
|
||||||
|
<view class="mine-page">
|
||||||
|
<!-- 顶部用户信息卡片 -->
|
||||||
|
<div class="user-card" @click="navigateTo('/pages/user/userinfo')">
|
||||||
|
<div class="user-header">
|
||||||
|
<image class="avatar" :src="userInfo.avatar" mode="aspectFill" />
|
||||||
|
<div class="user-info">
|
||||||
|
<text class="nickname">{{ userInfo.nickname || '未登录' }}</text>
|
||||||
|
<!-- <text class="vip-tag">VIP会员</text> -->
|
||||||
|
</div>
|
||||||
|
<!-- <div class="qr-code" @click.stop="showQRCode">
|
||||||
|
<uni-icons type="scan" size="20" color="#333"></uni-icons>
|
||||||
|
</div> -->
|
||||||
|
</div>
|
||||||
|
<div class="stat-row">
|
||||||
|
<div class="stat-item">
|
||||||
|
<text class="value">{{ userInfo.balance || 0 }}</text>
|
||||||
|
<text class="label">钱包(元)</text>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<text class="value">{{ userInfo.points || 0 }}</text>
|
||||||
|
<text class="label">积分</text>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<text class="value">{{ userInfo.coupons || 0 }}</text>
|
||||||
|
<text class="label">优惠券</text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 订单卡片 -->
|
||||||
|
<!-- <div class="order-card">
|
||||||
|
<div class="card-header">
|
||||||
|
<text class="title">我的订单</text>
|
||||||
|
<div class="more" @click="navigateTo('/pages/order/list')">
|
||||||
|
<text>查看全部</text>
|
||||||
|
<uni-icons type="right" size="14" color="#999"></uni-icons>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="order-list">
|
||||||
|
<div
|
||||||
|
class="order-item"
|
||||||
|
v-for="(item, index) in orderTypes"
|
||||||
|
:key="index"
|
||||||
|
@click="navigateTo(item.path)"
|
||||||
|
>
|
||||||
|
<div class="icon-wrapper">
|
||||||
|
<image :src="item.image" mode="aspectFit" class="order-icon" />
|
||||||
|
<text v-if="item.count" class="count-badge">{{ item.count }}</text>
|
||||||
|
</div>
|
||||||
|
<text class="order-name">{{ item.name }}</text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div> -->
|
||||||
|
|
||||||
|
<!-- 快捷功能区 -->
|
||||||
|
<!-- <div class="quick-actions">
|
||||||
|
<div
|
||||||
|
class="action-item"
|
||||||
|
v-for="(item, index) in allServices.slice(0, 4)"
|
||||||
|
:key="index"
|
||||||
|
@click="navigateTo(item.path)"
|
||||||
|
>
|
||||||
|
<div class="action-icon" :class="item.class">
|
||||||
|
<uni-icons :type="item.icon" size="24" color="#fff"></uni-icons>
|
||||||
|
</div>
|
||||||
|
<text class="action-name">{{ item.name }}</text>
|
||||||
|
</div>
|
||||||
|
</div> -->
|
||||||
|
|
||||||
|
<!-- 服务菜单列表(适老化:显示全部,大触控区) -->
|
||||||
|
<div class="service-list">
|
||||||
|
<div
|
||||||
|
class="service-item"
|
||||||
|
v-for="(item, index) in allServices"
|
||||||
|
:key="index"
|
||||||
|
@click="navigateTo(item.path)"
|
||||||
|
>
|
||||||
|
<div class="left">
|
||||||
|
<div class="service-icon" :class="item.class">
|
||||||
|
<uni-icons :type="item.icon" size="28" color="#fff"></uni-icons>
|
||||||
|
</div>
|
||||||
|
<text class="service-name">{{ item.name }}</text>
|
||||||
|
</div>
|
||||||
|
<div class="right">
|
||||||
|
<text v-if="item.desc" class="desc">{{ item.desc }}</text>
|
||||||
|
<uni-icons type="right" size="20" color="#666"></uni-icons>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- DEV 开发悬浮入口(生产环境自动隐藏) -->
|
||||||
|
<dev-training-entry :bottom="360" />
|
||||||
|
<TabBarDock :active="1" />
|
||||||
|
</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, TabBarDock },
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
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' }// ,
|
||||||
|
// { name: '客服中心', icon: 'headphones', path: '/pages/service/index', class: 'bg-orange', desc: '在线客服' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onShow() {
|
||||||
|
this.userInfo=uni.getStorageSync('userData')
|
||||||
|
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
navigateTo(path) {
|
||||||
|
uni.navigateTo({ url: path })
|
||||||
|
},
|
||||||
|
showQRCode() {
|
||||||
|
// 显示二维码
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
/* 适老化设计:50-80岁老人 - 大字号、高对比、大触控区 */
|
||||||
|
.mine-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #e8eaed;
|
||||||
|
padding: 40rpx;
|
||||||
|
padding-bottom: calc(40rpx + 200rpx + env(safe-area-inset-bottom));
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 28rpx;
|
||||||
|
padding: 48rpx;
|
||||||
|
margin-bottom: 40rpx;
|
||||||
|
box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.06);
|
||||||
|
|
||||||
|
.user-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 48rpx;
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
width: 160rpx;
|
||||||
|
height: 160rpx;
|
||||||
|
border-radius: 80rpx;
|
||||||
|
margin-right: 36rpx;
|
||||||
|
border: 4rpx solid #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
.nickname {
|
||||||
|
font-size: 48rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1a1a1a;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
display: block;
|
||||||
|
letter-spacing: 1rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vip-tag {
|
||||||
|
background: linear-gradient(90deg, #FFD700 0%, #FFA500 100%);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 28rpx;
|
||||||
|
padding: 6rpx 20rpx;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-code {
|
||||||
|
width: 88rpx;
|
||||||
|
height: 88rpx;
|
||||||
|
border-radius: 44rpx;
|
||||||
|
background: #f5f6fa;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-row {
|
||||||
|
display: flex;
|
||||||
|
padding-top: 40rpx;
|
||||||
|
border-top: 4rpx solid #e8eaed;
|
||||||
|
|
||||||
|
.stat-item {
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
min-height: 120rpx;
|
||||||
|
|
||||||
|
.value {
|
||||||
|
font-size: 52rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1a1a1a;
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
font-size: 32rpx;
|
||||||
|
color: #555;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
padding: 40rpx;
|
||||||
|
margin-bottom: 30rpx;
|
||||||
|
box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.04);
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 40rpx;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.more {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
text {
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #999;
|
||||||
|
margin-right: 8rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-list {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
.order-item {
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
.icon-wrapper {
|
||||||
|
width: 100rpx;
|
||||||
|
height: 100rpx;
|
||||||
|
margin: 0 auto 0;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
.count-badge {
|
||||||
|
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-icon {
|
||||||
|
width: 56rpx;
|
||||||
|
height: 56rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-name {
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-actions {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 30rpx;
|
||||||
|
margin-bottom: 30rpx;
|
||||||
|
|
||||||
|
.action-item {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
padding: 30rpx 20rpx;
|
||||||
|
text-align: center;
|
||||||
|
box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.04);
|
||||||
|
|
||||||
|
.action-icon {
|
||||||
|
width: 80rpx;
|
||||||
|
height: 80rpx;
|
||||||
|
border-radius: 40rpx;
|
||||||
|
margin: 0 auto 16rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-name {
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-list {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 28rpx;
|
||||||
|
padding: 20rpx 40rpx;
|
||||||
|
box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.06);
|
||||||
|
|
||||||
|
.service-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 44rpx 0;
|
||||||
|
border-bottom: 4rpx solid #e8eaed;
|
||||||
|
min-height: 120rpx;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.service-icon {
|
||||||
|
width: 88rpx;
|
||||||
|
height: 88rpx;
|
||||||
|
border-radius: 44rpx;
|
||||||
|
margin-right: 32rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-name {
|
||||||
|
font-size: 38rpx;
|
||||||
|
color: #1a1a1a;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.desc {
|
||||||
|
font-size: 30rpx;
|
||||||
|
color: #555;
|
||||||
|
margin-right: 20rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 背景色
|
||||||
|
.bg-red { background: #ff4d4f; }
|
||||||
|
.bg-blue { background: #1890ff; }
|
||||||
|
.bg-green { background: #52c41a; }
|
||||||
|
.bg-purple { background: #722ed1; }
|
||||||
|
.bg-orange { background: #fa8c16; }
|
||||||
|
.bg-cyan { background: #13c2c2; }
|
||||||
|
.bg-pink { background: #eb2f96; }
|
||||||
|
.bg-gray { background: #666666; }
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,495 @@
|
|||||||
|
<template>
|
||||||
|
<view class="userinfo-container">
|
||||||
|
<!-- 顶部标题栏 -->
|
||||||
|
|
||||||
|
|
||||||
|
<!-- 加载状态 -->
|
||||||
|
<view v-if="loading" class="loading-state">
|
||||||
|
<text>加载中...</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 编辑表单 -->
|
||||||
|
<view v-else class="form-wrapper">
|
||||||
|
<!-- 头像 -->
|
||||||
|
<view class="form-section">
|
||||||
|
<view class="form-group">
|
||||||
|
<text class="form-label">头像</text>
|
||||||
|
<view class="avatar-container">
|
||||||
|
<image
|
||||||
|
:src="formData.avatar"
|
||||||
|
class="avatar-image"
|
||||||
|
mode="aspectFill"
|
||||||
|
/>
|
||||||
|
<view class="avatar-upload" @click="uploadAvatar">
|
||||||
|
<uni-icons type="upload-filled" size="24" color="#FFA500" class="upload-icon"></uni-icons>
|
||||||
|
<text class="upload-text">更换头像</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 基本信息 -->
|
||||||
|
<view class="form-section">
|
||||||
|
<text class="section-title">基本信息</text>
|
||||||
|
|
||||||
|
|
||||||
|
<view class="form-group">
|
||||||
|
<text class="form-label">昵称</text>
|
||||||
|
<input
|
||||||
|
v-model="formData.nickname"
|
||||||
|
class="form-input"
|
||||||
|
placeholder="请输入昵称"
|
||||||
|
maxlength="50"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="form-group">
|
||||||
|
<text class="form-label">性别</text>
|
||||||
|
<view class="sex-group">
|
||||||
|
<view
|
||||||
|
class="sex-btn"
|
||||||
|
:class="{ active: formData.sex === 1}"
|
||||||
|
@click="formData.sex = 1"
|
||||||
|
>
|
||||||
|
男
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
class="sex-btn"
|
||||||
|
:class="{ active: formData.sex === 2}"
|
||||||
|
@click="formData.sex = 2"
|
||||||
|
>
|
||||||
|
女
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="form-group">
|
||||||
|
<text class="form-label">年龄</text>
|
||||||
|
<input
|
||||||
|
v-model.number="formData.age"
|
||||||
|
class="form-input"
|
||||||
|
type="number"
|
||||||
|
placeholder="请输入年龄"
|
||||||
|
min="0"
|
||||||
|
max="150"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 操作按钮 -->
|
||||||
|
<view class="button-group">
|
||||||
|
|
||||||
|
<button class="btn btn-submit" @click="submitForm" :disabled="submitting">
|
||||||
|
{{ submitting ? '保存中...' : '保存修改' }}
|
||||||
|
</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, getCurrentInstance } from 'vue'
|
||||||
|
|
||||||
|
const { proxy } = getCurrentInstance()
|
||||||
|
const instance = getCurrentInstance();
|
||||||
|
// 2. 从实例中获取全局 $url
|
||||||
|
const $url = instance.appContext.config.globalProperties.$url;
|
||||||
|
|
||||||
|
// 数据
|
||||||
|
const loading = ref(false)
|
||||||
|
const submitting = ref(false)
|
||||||
|
|
||||||
|
// 表单数据
|
||||||
|
const formData = ref({
|
||||||
|
avatar: '',
|
||||||
|
patient_name: '',
|
||||||
|
phone: '',
|
||||||
|
nickname: '',
|
||||||
|
sex: 1,
|
||||||
|
age: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 页面加载
|
||||||
|
onMounted(() => {
|
||||||
|
|
||||||
|
loadUserInfo()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 加载用户信息
|
||||||
|
const loadUserInfo = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res = await proxy.apiUrl({
|
||||||
|
url: '/api/user/info',
|
||||||
|
method: 'GET',
|
||||||
|
data: {}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (res.code === 1) {
|
||||||
|
const userInfo = res.data
|
||||||
|
formData.value = {
|
||||||
|
avatar: userInfo.avatar || '',
|
||||||
|
patient_name: userInfo.patient_name || userInfo.nickname || '',
|
||||||
|
phone: userInfo.phone || '',
|
||||||
|
nickname: userInfo.nickname || '',
|
||||||
|
sex: userInfo.sex =='男'?1:2,
|
||||||
|
age: userInfo.age || ''
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
uni.showToast({ title: res.msg || '加载失败', icon: 'none' })
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('加载用户信息失败:', err)
|
||||||
|
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 上传头像
|
||||||
|
const uploadAvatar = () => {
|
||||||
|
uni.chooseImage({
|
||||||
|
count: 1,
|
||||||
|
sizeType: ['compressed'],
|
||||||
|
sourceType: ['album', 'camera'],
|
||||||
|
success: async (res) => {
|
||||||
|
const tempFilePath = res.tempFilePaths[0]
|
||||||
|
|
||||||
|
try {
|
||||||
|
uni.showLoading({ title: '上传中...' })
|
||||||
|
|
||||||
|
// 获取token
|
||||||
|
const token = uni.getStorageSync('token')
|
||||||
|
|
||||||
|
// 调用后端上传接口
|
||||||
|
const uploadRes = await new Promise((resolve, reject) => {
|
||||||
|
uni.uploadFile({
|
||||||
|
url: $url+'/api/upload/image',
|
||||||
|
filePath: tempFilePath,
|
||||||
|
name: 'file',
|
||||||
|
header: {
|
||||||
|
token: token,
|
||||||
|
'content-type': 'multipart/form-data'
|
||||||
|
},
|
||||||
|
success: (res) => {
|
||||||
|
const data = JSON.parse(res.data)
|
||||||
|
resolve(data)
|
||||||
|
},
|
||||||
|
fail: (err) => {
|
||||||
|
reject(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
uni.hideLoading()
|
||||||
|
|
||||||
|
if (uploadRes.code === 1) {
|
||||||
|
// 上传成功,更新头像
|
||||||
|
formData.value.avatar = uploadRes.data.uri || uploadRes.data
|
||||||
|
uni.showToast({ title: '头像上传成功', icon: 'success' })
|
||||||
|
} else {
|
||||||
|
uni.showToast({ title: uploadRes.msg || '上传失败', icon: 'none' })
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
uni.hideLoading()
|
||||||
|
console.error('上传头像失败:', err)
|
||||||
|
uni.showToast({ title: '上传失败', icon: 'none' })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fail: (err) => {
|
||||||
|
console.error('选择图片失败:', err)
|
||||||
|
uni.showToast({ title: '选择图片失败', icon: 'none' })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证手机号
|
||||||
|
const validatePhone = (phone) => {
|
||||||
|
if (!phone) return true // 可选字段
|
||||||
|
return /^1[3-9]\d{9}$/.test(phone)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交表单
|
||||||
|
const submitForm = async () => {
|
||||||
|
// 验证必填字段
|
||||||
|
if (!formData.value.patient_name) {
|
||||||
|
uni.showToast({ title: '姓名不能为空', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证手机号格式
|
||||||
|
if (formData.value.phone && !validatePhone(formData.value.phone)) {
|
||||||
|
uni.showToast({ title: '手机号格式不正确', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
submitting.value = true
|
||||||
|
try {
|
||||||
|
// 调用后端API更新用户信息
|
||||||
|
const res = await proxy.apiUrl({
|
||||||
|
url: '/api/user/setInfo',
|
||||||
|
method: 'POST',
|
||||||
|
data: {
|
||||||
|
avatar: formData.value.avatar,
|
||||||
|
phone: formData.value.phone,
|
||||||
|
nickname: formData.value.nickname,
|
||||||
|
sex: formData.value.sex,
|
||||||
|
age: formData.value.age,
|
||||||
|
patient_name: formData.value.patient_name
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (res.code === 1) {
|
||||||
|
// 更新本地存储
|
||||||
|
const userData = uni.getStorageSync('userData')
|
||||||
|
const updatedData = {
|
||||||
|
...userData,
|
||||||
|
avatar: formData.value.avatar,
|
||||||
|
phone: formData.value.phone,
|
||||||
|
nickname: formData.value.nickname,
|
||||||
|
sex: formData.value.sex,
|
||||||
|
age: formData.value.age,
|
||||||
|
patient_name: formData.value.patient_name,
|
||||||
|
diagnosis: {
|
||||||
|
...userData.diagnosis,
|
||||||
|
patient_name: formData.value.patient_name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uni.setStorageSync('userData', updatedData)
|
||||||
|
|
||||||
|
uni.showToast({ title: '保存成功', icon: 'success' })
|
||||||
|
setTimeout(() => {
|
||||||
|
uni.navigateBack()
|
||||||
|
}, 1500)
|
||||||
|
} else {
|
||||||
|
uni.showToast({ title: res.msg || '保存失败', icon: 'none' })
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('保存失败:', err)
|
||||||
|
uni.showToast({ title: '保存失败', icon: 'none' })
|
||||||
|
} finally {
|
||||||
|
submitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回上一页
|
||||||
|
const goBack = () => {
|
||||||
|
uni.navigateBack()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* 适老化设计:50-80岁 - 大字号、高对比、大触控区 */
|
||||||
|
.userinfo-container {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #e8eaed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
background: #ffffff;
|
||||||
|
padding: 24rpx 40rpx;
|
||||||
|
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-back {
|
||||||
|
font-size: 48rpx;
|
||||||
|
color: #FFA500;
|
||||||
|
width: 88rpx;
|
||||||
|
height: 88rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-title {
|
||||||
|
font-size: 44rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #1a1a1a;
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-placeholder {
|
||||||
|
width: 88rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-state {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 100vh;
|
||||||
|
font-size: 36rpx;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-wrapper {
|
||||||
|
padding: 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-section {
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
padding: 40rpx;
|
||||||
|
margin-bottom: 36rpx;
|
||||||
|
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 40rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #FFA500;
|
||||||
|
margin-bottom: 32rpx;
|
||||||
|
display: block;
|
||||||
|
padding-bottom: 24rpx;
|
||||||
|
border-bottom: 4rpx solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 36rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label {
|
||||||
|
display: block;
|
||||||
|
font-size: 36rpx;
|
||||||
|
color: #1a1a1a;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 28rpx;
|
||||||
|
border: 4rpx solid #e0e0e0;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
font-size: 36rpx;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #1a1a1a;
|
||||||
|
background: #ffffff;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: block;
|
||||||
|
height: auto;
|
||||||
|
min-height: 96rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input::placeholder {
|
||||||
|
color: #999;
|
||||||
|
font-size: 34rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-image {
|
||||||
|
width: 160rpx;
|
||||||
|
height: 160rpx;
|
||||||
|
border-radius: 80rpx;
|
||||||
|
background: #f0f0f0;
|
||||||
|
border: 4rpx solid #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-upload {
|
||||||
|
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 {
|
||||||
|
font-size: 56rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-text {
|
||||||
|
font-size: 34rpx;
|
||||||
|
color: #FFA500;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sex-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 28rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sex-btn {
|
||||||
|
flex: 1;
|
||||||
|
padding: 32rpx;
|
||||||
|
border: 4rpx solid #e0e0e0;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 38rpx;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #555;
|
||||||
|
background: #ffffff;
|
||||||
|
transition: all 0.3s;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 96rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sex-btn.active {
|
||||||
|
border-color: #FFA500;
|
||||||
|
background: #fff7e6;
|
||||||
|
color: #FFA500;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 28rpx;
|
||||||
|
margin-top: 56rpx;
|
||||||
|
margin-bottom: 56rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
flex: 1;
|
||||||
|
padding: 36rpx;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
font-size: 40rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
min-height: 100rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:active {
|
||||||
|
transform: scale(0.98);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-cancel {
|
||||||
|
background: #f0f0f0;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-submit {
|
||||||
|
background: #FFA500;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-submit:active:not(:disabled) {
|
||||||
|
background: #e69500;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg width="375px" height="268px" viewBox="0 0 375 268" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<!-- Generator: Sketch 61.2 (89653) - https://sketch.com -->
|
||||||
|
<title>背景</title>
|
||||||
|
<desc>Created with Sketch.</desc>
|
||||||
|
<defs>
|
||||||
|
<linearGradient x1="50%" y1="81.9277858%" x2="50%" y2="100%" id="linearGradient-1">
|
||||||
|
<stop stop-color="#006EFF" offset="0%"></stop>
|
||||||
|
<stop stop-color="#00C8DC" offset="100%"></stop>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<g id="浅色版本" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||||
|
<g id="主页" fill="url(#linearGradient-1)">
|
||||||
|
<g id="编组-4" transform="translate(-315.000000, -732.000000)">
|
||||||
|
<circle id="蒙版" cx="500" cy="500" r="500"></circle>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 934 B |
|
After Width: | Height: | Size: 5.5 KiB |