diff --git a/TUICallKit-Vue3/main.js b/TUICallKit-Vue3/main.js index f1d13fa0..5fd1d831 100644 --- a/TUICallKit-Vue3/main.js +++ b/TUICallKit-Vue3/main.js @@ -1,5 +1,5 @@ import App from './App' -var baseUrl ='https://css.zhenyangtang.com.cn/'; +var baseUrl ='https://admin.zhenyangtang.com.cn/'; // #ifndef VUE3 import Vue from 'vue' import './uni.promisify.adaptor' diff --git a/TUICallKit-Vue3/pages.json b/TUICallKit-Vue3/pages.json index 4a6cf44e..69ccbfd3 100644 --- a/TUICallKit-Vue3/pages.json +++ b/TUICallKit-Vue3/pages.json @@ -133,10 +133,10 @@ { "path": "pages/index", "style": { - "navigationBarTitleText": "日常记录", - "navigationBarBackgroundColor": "#0ea5a4", + "navigationBarTitleText": "血糖记录", + "navigationBarBackgroundColor": "#204e2b", "navigationBarTextStyle": "white", - "backgroundColor": "#f1f5f9", + "backgroundColor": "#faf9f5", "enablePullDownRefresh": true, "mp-weixin": { "usingPlugins": { @@ -147,6 +147,16 @@ } } } + }, + { + "path": "pages/more", + "style": { + "navigationBarTitleText": "日常护理", + "navigationBarBackgroundColor": "#204e2b", + "navigationBarTextStyle": "white", + "backgroundColor": "#faf9f5", + "enablePullDownRefresh": true + } } ] } diff --git a/TUICallKit-Vue3/tongji/components/CelebrateBurst.vue b/TUICallKit-Vue3/tongji/components/CelebrateBurst.vue index c7aa159e..75499b8e 100644 --- a/TUICallKit-Vue3/tongji/components/CelebrateBurst.vue +++ b/TUICallKit-Vue3/tongji/components/CelebrateBurst.vue @@ -8,7 +8,7 @@ /> - + {{ title }} {{ subtitle }} @@ -28,7 +28,7 @@ const props = defineProps({ const particles = ref([]) const cardPop = ref(false) -const COLORS = ['#0891B2', '#22D3EE', '#059669', '#FBBF24', '#F472B6', '#A78BFA'] +const COLORS = ['#204E2B', '#386641', '#AFE2B3', '#FBBF24', '#727970', '#DC2626'] function buildParticles() { const list = [] @@ -129,7 +129,7 @@ watch( margin-top: 16rpx; font-size: 36rpx; font-weight: 800; - color: #164e63; + color: #1b1c1a; } .celebrate-card-sub { position: relative; diff --git a/TUICallKit-Vue3/tongji/components/SugarTreeGraphic.vue b/TUICallKit-Vue3/tongji/components/SugarTreeGraphic.vue index 59b12b27..c1401c42 100644 --- a/TUICallKit-Vue3/tongji/components/SugarTreeGraphic.vue +++ b/TUICallKit-Vue3/tongji/components/SugarTreeGraphic.vue @@ -47,12 +47,12 @@ const treeEmoji = computed(() => (TREE_LEVELS[clampedLevel.value] || TREE_LEVELS function buildTreeSvg(level) { const pot = ` - + ` - const soil = `` + const soil = `` if (level <= 0) { return ` @@ -69,7 +69,7 @@ function buildTreeSvg(level) { const leaf = (cx, cy, r, fill, opacity = 1) => ` - ` + ` const bloom = level >= 7 @@ -82,13 +82,13 @@ function buildTreeSvg(level) { level >= 9 ? leaf(12, 22, 6, '#34d399', 0.9) + leaf(36, 22, 6, '#34d399', 0.9) + - leaf(24, 10, 7, '#10b981', 0.95) + + leaf(24, 10, 7, '#386641', 0.95) + bloom : bloom let canopy = '' if (level === 1) { - canopy = leaf(24, 38, 4, '#6ee7b7') + `` + canopy = leaf(24, 38, 4, '#afe2b3') + `` } else if (level === 2) { canopy = leaf(24, 32, 5.5, '#34d399') + leaf(20, 34, 3.5, '#6ee7b7', 0.9) } else if (level <= 4) { @@ -101,10 +101,10 @@ function buildTreeSvg(level) { leaf(24, 24, 9, '#22c55e') + leaf(14, 26, 7, '#4ade80', 0.92) + leaf(34, 26, 7, '#4ade80', 0.92) + - leaf(24, 16, 6, '#10b981', 0.88) + leaf(24, 16, 6, '#386641', 0.88) } else { canopy = - leaf(24, 20, 10, '#059669') + + leaf(24, 20, 10, '#204e2b') + leaf(13, 24, 8, '#34d399', 0.95) + leaf(35, 24, 8, '#34d399', 0.95) + crown @@ -132,14 +132,14 @@ const treeImageSrc = computed(() => svgToDataUrl(buildTreeSvg(clampedLevel.value position: absolute; inset: 6%; border-radius: 50%; - background: radial-gradient(circle, rgba(14, 165, 164, 0.22) 0%, transparent 68%); + background: radial-gradient(circle, rgba(32, 78, 43, 0.22) 0%, transparent 68%); pointer-events: none; } .lv-0 .stg-glow { background: radial-gradient(circle, rgba(148, 163, 184, 0.25) 0%, transparent 70%); } .tier-4 .stg-glow, .tier-5 .stg-glow, .is-max .stg-glow { - background: radial-gradient(circle, rgba(251, 191, 36, 0.3) 0%, rgba(14, 165, 164, 0.15) 55%, transparent 72%); + background: radial-gradient(circle, rgba(251, 191, 36, 0.3) 0%, rgba(32, 78, 43, 0.15) 55%, transparent 72%); } .stg-aura { position: absolute; diff --git a/TUICallKit-Vue3/tongji/components/TongjiIcon.vue b/TUICallKit-Vue3/tongji/components/TongjiIcon.vue index fd707a21..32ecec3a 100644 --- a/TUICallKit-Vue3/tongji/components/TongjiIcon.vue +++ b/TUICallKit-Vue3/tongji/components/TongjiIcon.vue @@ -24,7 +24,7 @@ import { svgToDataUrl } from '../utils/svgDataUrl.js' const props = defineProps({ name: { type: String, required: true }, size: { type: String, default: 'md' }, - color: { type: String, default: '#0891B2' } + color: { type: String, default: '#204E2B' } }) const useFallback = ref(false) @@ -86,8 +86,8 @@ const ICON_FALLBACK = { } const strokeColor = computed(() => { - const c = String(props.color || '#0891B2').trim() - return /^#[0-9A-Fa-f]{3,8}$/.test(c) ? c : '#0891B2' + const c = String(props.color || '#204E2B').trim() + return /^#[0-9A-Fa-f]{3,8}$/.test(c) ? c : '#204E2B' }) const iconSrc = computed(() => { diff --git a/TUICallKit-Vue3/tongji/composables/useTongjiAuth.js b/TUICallKit-Vue3/tongji/composables/useTongjiAuth.js new file mode 100644 index 00000000..5b34f785 --- /dev/null +++ b/TUICallKit-Vue3/tongji/composables/useTongjiAuth.js @@ -0,0 +1,115 @@ +import { ref } from 'vue' +import { formatUserMessage } from '../utils/tongjiHelpers.js' + +/** 登录 / 就诊卡门禁(index 与 more 共用) */ +export function useTongjiAuth(proxy) { + const authChecking = ref(false) + let gateRedirected = false + let authSessionPromise = null + + function hasAuthToken() { + return !!String(uni.getStorageSync('token') || '').trim() + } + + function clearAuthStorage() { + uni.removeStorageSync('token') + authSessionPromise = null + } + + function wxLoginGetCode() { + return new Promise((resolve, reject) => { + uni.login({ + provider: 'weixin', + success: (res) => { + if (res && res.code) resolve(res.code) + else reject(new Error('微信登录未返回 code')) + }, + fail: reject + }) + }) + } + + async function mnpLoginWithCode(code) { + const res = await proxy.apiUrl({ + url: '/api/login/mnpLogin', + method: 'POST', + data: { code } + }, false) + if (res && res.code === 1 && res.data && res.data.token) { + uni.setStorageSync('token', res.data.token) + uni.setStorageSync('userData', res.data) + return res.data + } + clearAuthStorage() + throw new Error(formatUserMessage(res?.msg, '登录失败')) + } + + async function doWxLogin() { + const code = await wxLoginGetCode() + await mnpLoginWithCode(code) + return hasAuthToken() + } + + async function verifyOrLogin() { + if (!hasAuthToken()) { + return doWxLogin() + } + try { + const res = await proxy.apiUrl({ url: '/api/user/info', method: 'POST' }, false) + if (res && res.code === 1 && res.data) { + uni.setStorageSync('userData', res.data) + return true + } + } catch (e) { + /* 网络异常走重新登录 */ + } + clearAuthStorage() + try { + return await doWxLogin() + } catch (e2) { + return false + } + } + + async function ensureLoggedIn() { + if (authSessionPromise) { + return authSessionPromise + } + authSessionPromise = verifyOrLogin() + .then((ok) => { + if (!ok) authSessionPromise = null + return !!ok + }) + .catch(() => { + authSessionPromise = null + return false + }) + return authSessionPromise + } + + function redirectToCardEntry(returnPath) { + if (gateRedirected) return + gateRedirected = true + const returnUrl = encodeURIComponent(returnPath || '/tongji/pages/index') + uni.redirectTo({ + url: `/pages/Card/edit_card?add=1&returnUrl=${returnUrl}` + }) + } + + function resetGateRedirect() { + gateRedirected = false + } + + function isGateRedirected() { + return gateRedirected + } + + return { + authChecking, + hasAuthToken, + ensureLoggedIn, + redirectToCardEntry, + resetGateRedirect, + isGateRedirected + } +} diff --git a/TUICallKit-Vue3/tongji/pages/index.vue b/TUICallKit-Vue3/tongji/pages/index.vue index cc504e8d..a1374ce0 100644 --- a/TUICallKit-Vue3/tongji/pages/index.vue +++ b/TUICallKit-Vue3/tongji/pages/index.vue @@ -1,6 +1,5 @@ @@ -1344,8 +468,6 @@ import { ref, computed, getCurrentInstance, nextTick, watch } from 'vue' import { onLoad, onShow, onHide, onUnload, onPullDownRefresh, onShareAppMessage } from '@dcloudio/uni-app' import SugarTreeGraphic from '../components/SugarTreeGraphic.vue' import TongjiIcon from '../components/TongjiIcon.vue' -import CelebrateBurst from '../components/CelebrateBurst.vue' -import { TREE_LEVELS, TREE_MAX_LEVEL, calcTreeFromPoints, pickTreeWhisper } from '../utils/treeLevels.js' const { proxy } = getCurrentInstance() @@ -1628,21 +750,18 @@ const recordDays = computed(() => { const result = [] for (const date of visibleDates.value) { const blood = bloodByDate.value[date] - const diet = dietByDate.value[date] - const exercise = exerciseByDate.value[date] - const tracking = trackingByDate.value[date] - const hasAny = blood || diet || exercise || (tracking && tracking.content) - if (!hasAny) continue + if (!blood) continue + const hasSugar = toNumber(blood.fasting_blood_sugar) !== null + || toNumber(blood.postprandial_blood_sugar) !== null + || toNumber(blood.other_blood_sugar) !== null + if (!hasSugar) continue const [y, m, d] = date.split('-') result.push({ date, dateLabel: `${m}-${d}`, weekday: getWeekday(date), isToday: date === todayStr.value, - blood, - diet, - exercise, - tracking + blood }) } return result @@ -1754,6 +873,44 @@ const latestStat = computed(() => { return result }) +function elderMetricFromBlood(blood, field, label) { + if (!blood) return { label, value: '—', isHigh: false } + const num = toNumber(blood[field]) + if (num === null) return { label, value: '—', isHigh: false } + const isHigh = field === 'fasting_blood_sugar' + ? blood.isHighFasting + : field === 'postprandial_blood_sugar' + ? blood.isHighPostprandial + : blood.isHighOther + return { label, value: String(blood[field]), isHigh: !!isHigh } +} + +const todayBlood = computed(() => bloodByDate.value[todayStr.value] || null) + +const elderFasting = computed(() => { + const today = todayBlood.value + if (today && toNumber(today.fasting_blood_sugar) !== null) { + return elderMetricFromBlood(today, 'fasting_blood_sugar', '空腹') + } + const latest = latestStat.value.fasting + if (latest.value !== '—') { + return { label: '空腹(最近)', value: latest.value, isHigh: latest.isHigh } + } + return { label: '空腹', value: '—', isHigh: false } +}) + +const elderPostprandial = computed(() => { + const today = todayBlood.value + if (today && toNumber(today.postprandial_blood_sugar) !== null) { + return elderMetricFromBlood(today, 'postprandial_blood_sugar', '餐后') + } + const latest = latestStat.value.postprandial + if (latest.value !== '—') { + return { label: '餐后(最近)', value: latest.value, isHigh: latest.isHigh } + } + return { label: '餐后', value: '—', isHigh: false } +}) + // 给某天某字段计算 day-over-day 趋势(用于日常记录列表的指标小标签) function getDayTrend(date, field, precision = 1) { const b = bloodByDate.value[date] @@ -1901,14 +1058,6 @@ async function applyCard(card) { patientName.value = card.patient_name || '' age.value = Number(card.age) || 0 gender.value = Number(card.gender) || 0 - lastFamilyLikeCount.value = getStoredFamilyLikeSeen(Number(card.id) || 0) - familyLikeSummary.value = { - like_count: 0, - summary_line: '分享战报给家人,邀请他们为您点赞鼓劲', - recent: [] - } - gamifyStatusTip.value = '' - await fetchGamifyState() } async function onSelectCard(card) { @@ -1973,9 +1122,6 @@ async function fetchAll() { } else { trackingNotes.value = [] } - // 拉取游戏化积分/小树状态 - await fetchGamifyState() - await loadFamilyLikeSummary() } catch (e) { uni.showToast({ title: '加载失败,请稍后再试', icon: 'none' }) } finally { @@ -2002,6 +1148,14 @@ function onRefresh() { fetchAll() } +function goMorePage() { + if (!diagnosisId.value) { + showUserToast('请先选择就诊卡') + return + } + uni.navigateTo({ url: '/tongji/pages/more' }) +} + // ============ 时段问候 / 打卡 / 健康日历 ============ /** 时段问候:文案 + 统一 TongjiIcon(与全页图标体系一致)*/ function buildTimeGreetingMeta(hour = new Date().getHours()) { @@ -2023,27 +1177,17 @@ function dayHasBloodRecord(key) { if (!b) return false return toNumber(b.fasting_blood_sugar) !== null || toNumber(b.postprandial_blood_sugar) !== null || - toNumber(b.other_blood_sugar) !== null || - toNumber(b.systolic_pressure) !== null || - toNumber(b.diastolic_pressure) !== null + toNumber(b.other_blood_sugar) !== null } -// 给一天打个完整度分(0-3):0 无 / 1 仅 1 项 / 2 有 2~3 项 / 3 有 4 项以上 function dayCoverageLevel(key) { const b = bloodByDate.value[key] - const dt = dietByDate.value[key] - const ex = exerciseByDate.value[key] - if (!b && !dt && !ex) return 0 - let score = 0 - if (b) { - const fields = ['fasting_blood_sugar', 'postprandial_blood_sugar', 'other_blood_sugar', 'systolic_pressure', 'diastolic_pressure'] - score += fields.filter((f) => toNumber(b[f]) !== null).length - } - if (dt && (dt.breakfast_foods || dt.lunch_foods || dt.dinner_foods)) score += 1 - if (ex && (ex.exercise_type || ex.duration)) score += 1 + if (!b) return 0 + const fields = ['fasting_blood_sugar', 'postprandial_blood_sugar', 'other_blood_sugar'] + const score = fields.filter((f) => toNumber(b[f]) !== null).length if (score === 0) return 0 if (score === 1) return 1 - if (score <= 3) return 2 + if (score === 2) return 2 return 3 } @@ -2223,8 +1367,6 @@ const emptyInputForm = () => ({ fasting_blood_sugar: '', postprandial_blood_sugar: '', other_blood_sugar: '', - systolic_pressure: '', - diastolic_pressure: '', remark: '' }) const inputForm = ref(emptyInputForm()) @@ -2268,8 +1410,6 @@ async function openInputForm() { inputForm.value.fasting_blood_sugar = numToStr(r.fasting_blood_sugar) inputForm.value.postprandial_blood_sugar = numToStr(r.postprandial_blood_sugar) inputForm.value.other_blood_sugar = numToStr(r.other_blood_sugar) - inputForm.value.systolic_pressure = numToStr(r.systolic_pressure) - inputForm.value.diastolic_pressure = numToStr(r.diastolic_pressure) inputForm.value.remark = String(r.remark || '') } } catch (e) { @@ -2288,203 +1428,11 @@ function numToStr(v) { function closeInputForm() { if (inputForm.value.submitting) return inputForm.value.visible = false - // 关闭后 canvas 会重新挂载,需触发一次重绘 nextTick(() => scheduleChartRedraw()) } -const emptyDietForm = () => ({ - visible: false, - loading: false, - submitting: false, - id: 0, - breakfast_foods: '', - lunch_foods: '', - dinner_foods: '', - note: '' -}) -const dietForm = ref(emptyDietForm()) - -function onDietField(field, e) { - dietForm.value[field] = e?.detail?.value ?? '' -} - -async function openDietForm() { - if (!diagnosisId.value) { - showUserToast('请先选择就诊卡') - return - } - const token = uni.getStorageSync('token') - if (!token) { - showUserToast('请先登录后再录入') - return - } - prepareRecordOverlay() - dietForm.value = { ...emptyDietForm(), visible: true, loading: true } - try { - const res = await proxy.apiUrl({ - url: '/api/tcm/dailyTodayDietRecord', - method: 'GET', - data: { diagnosis_id: diagnosisId.value } - }, false) - if (res?.code === 1 && res.data?.record) { - const r = res.data.record - dietForm.value.id = Number(r.id) || 0 - dietForm.value.breakfast_foods = String(r.breakfast_foods || '') - dietForm.value.lunch_foods = String(r.lunch_foods || '') - dietForm.value.dinner_foods = String(r.dinner_foods || '') - dietForm.value.note = String(r.note || '') - } - } catch (e) {} - finally { - dietForm.value.loading = false - } -} - -function closeDietForm() { - if (dietForm.value.submitting) return - dietForm.value.visible = false - nextTick(() => scheduleChartRedraw()) -} - -async function submitDietForm() { - if (dietForm.value.submitting || dietForm.value.loading) return - const f = dietForm.value - if (!f.breakfast_foods.trim() && !f.lunch_foods.trim() && !f.dinner_foods.trim()) { - showUserToast('请至少填写一餐饮食') - return - } - dietForm.value.submitting = true - try { - const res = await proxy.apiUrl({ - url: '/api/tcm/dailySaveDietRecord', - method: 'POST', - data: { - diagnosis_id: diagnosisId.value, - breakfast_foods: f.breakfast_foods, - lunch_foods: f.lunch_foods, - dinner_foods: f.dinner_foods, - note: f.note - } - }, false) - if (res?.code === 1) { - showUserToast(res.msg || '已保存', { icon: 'success' }) - dietForm.value.visible = false - await fetchAll() - await fetchGamifyState() - if (claimablePoints.value > 0) { - showUserToast('饮食已记录,点击浇水领取积分', { duration: 2800 }) - } - } else { - showUserToast((res && res.msg) || '保存失败') - } - } catch (e) { - showUserToast('网络异常,请稍后再试') - } finally { - dietForm.value.submitting = false - } -} - -const exerciseIntensityOptions = [ - { value: 1, label: '低强度' }, - { value: 2, label: '中强度' }, - { value: 3, label: '高强度' } -] -const emptyExerciseForm = () => ({ - visible: false, - loading: false, - submitting: false, - id: 0, - exercise_type: '', - duration: '', - intensity: 2, - note: '' -}) -const exerciseForm = ref(emptyExerciseForm()) - /** 任一录入弹层打开时需卸载 canvas(小程序原生组件会盖住普通 view)*/ -const recordOverlayOpen = computed(() => - inputForm.value.visible || dietForm.value.visible || exerciseForm.value.visible -) - -function onExerciseField(field, e) { - exerciseForm.value[field] = e?.detail?.value ?? '' -} - -async function openExerciseForm() { - if (!diagnosisId.value) { - showUserToast('请先选择就诊卡') - return - } - const token = uni.getStorageSync('token') - if (!token) { - showUserToast('请先登录后再录入') - return - } - prepareRecordOverlay() - exerciseForm.value = { ...emptyExerciseForm(), visible: true, loading: true } - try { - const res = await proxy.apiUrl({ - url: '/api/tcm/dailyTodayExerciseRecord', - method: 'GET', - data: { diagnosis_id: diagnosisId.value } - }, false) - if (res?.code === 1 && res.data?.record) { - const r = res.data.record - exerciseForm.value.id = Number(r.id) || 0 - exerciseForm.value.exercise_type = String(r.exercise_type || '') - exerciseForm.value.duration = numToStr(r.duration) - exerciseForm.value.intensity = Number(r.intensity) || 2 - exerciseForm.value.note = String(r.note || '') - } - } catch (e) {} - finally { - exerciseForm.value.loading = false - } -} - -function closeExerciseForm() { - if (exerciseForm.value.submitting) return - exerciseForm.value.visible = false - nextTick(() => scheduleChartRedraw()) -} - -async function submitExerciseForm() { - if (exerciseForm.value.submitting || exerciseForm.value.loading) return - const f = exerciseForm.value - if (!f.exercise_type.trim() && !f.duration) { - showUserToast('请填写运动类型或时长') - return - } - exerciseForm.value.submitting = true - try { - const res = await proxy.apiUrl({ - url: '/api/tcm/dailySaveExerciseRecord', - method: 'POST', - data: { - diagnosis_id: diagnosisId.value, - exercise_type: f.exercise_type, - duration: f.duration, - intensity: f.intensity, - note: f.note - } - }, false) - if (res?.code === 1) { - showUserToast(res.msg || '已保存', { icon: 'success' }) - exerciseForm.value.visible = false - await fetchAll() - await fetchGamifyState() - if (claimablePoints.value > 0) { - showUserToast('运动已记录,点击浇水领取积分', { duration: 2800 }) - } - } else { - showUserToast((res && res.msg) || '保存失败') - } - } catch (e) { - showUserToast('网络异常,请稍后再试') - } finally { - exerciseForm.value.submitting = false - } -} +const recordOverlayOpen = computed(() => inputForm.value.visible) async function submitInputForm() { if (inputForm.value.submitting || inputForm.value.loading) return @@ -2492,8 +1440,7 @@ async function submitInputForm() { // 至少填一个有效字段 const f = inputForm.value const hasAny = [ - f.fasting_blood_sugar, f.postprandial_blood_sugar, f.other_blood_sugar, - f.systolic_pressure, f.diastolic_pressure + f.fasting_blood_sugar, f.postprandial_blood_sugar, f.other_blood_sugar ].some((v) => v !== '' && v !== null && Number(v) > 0) || !!(f.remark && f.remark.trim()) if (!hasAny) { uni.showToast({ title: '请至少填写一项数据', icon: 'none' }) @@ -2510,8 +1457,6 @@ async function submitInputForm() { fasting_blood_sugar: f.fasting_blood_sugar, postprandial_blood_sugar: f.postprandial_blood_sugar, other_blood_sugar: f.other_blood_sugar, - systolic_pressure: f.systolic_pressure, - diastolic_pressure: f.diastolic_pressure, remark: f.remark } }, false) @@ -2519,10 +1464,6 @@ async function submitInputForm() { showUserToast(res.msg || '已保存', { icon: 'success' }) inputForm.value.visible = false await fetchAll() - await fetchGamifyState() - if (claimablePoints.value > 0) { - showUserToast('记录已保存,点击浇水领取积分', { duration: 2800 }) - } } else { showUserToast((res && res.msg) || '保存失败') } @@ -2567,13 +1508,6 @@ function deleteInputRecord() { }) } -function splitTrackingLines(content) { - return String(content || '') - .split('\n') - .map((s) => s.trim()) - .filter(Boolean) -} - // ============ 语音播报(适老化)============ // 优先用 微信同声传译插件 (mp-weixin);H5 走 window.speechSynthesis; // 都不可用则轻提示,避免 showModal 挡住录入弹层。 @@ -2984,17 +1918,17 @@ function drawChart() { const seriesMeta = [ { data: fastingSeries, - color: '#0ea5a4', - fillTop: 'rgba(14,165,164,0.20)', - fillBottom: 'rgba(14,165,164,0)', + color: '#204E2B', + fillTop: 'rgba(32,78,43,0.18)', + fillBottom: 'rgba(32,78,43,0)', label: '空腹', highThreshold: threshold ? threshold.fasting : null }, { data: postSeries, - color: '#f59e0b', - fillTop: 'rgba(245,158,11,0.16)', - fillBottom: 'rgba(245,158,11,0)', + color: '#386641', + fillTop: 'rgba(56,102,65,0.14)', + fillBottom: 'rgba(56,102,65,0)', label: '餐后', highThreshold: threshold ? threshold.postprandial : null } @@ -3257,24 +2191,9 @@ onLoad((options) => { bootstrap(options) }) -onShareAppMessage(() => { - const d = weeklyReportData.value - const code = pendingShareInviteCode.value - const title = - shareTitle.value || - `【稳糖打卡】${d.patientLabel}本周记录${d.recordDays}天(邀请码今日有效)` - const path = code - ? `/tongji/pages/index?from=share&invite_code=${code}` - : '/tongji/pages/index' - return { title, path } -}) - onShow(() => { if (authChecking.value || gateRedirected) return - if (!isInviteViewer.value && diagnosisId.value) { - loadFamilyLikeSummary() - } - scheduleChartRedraw() + nextTick(() => scheduleChartRedraw()) }) onPullDownRefresh(async () => { @@ -3295,355 +2214,21 @@ onUnload(() => { ttsStop() }) -// ============ 稳糖乐园 (游戏化积分/小树) ============ -const gamifyPoints = ref(0) -const gamifyBadges = ref([]) -const gamifyTaskAwards = ref({}) -const gamifyTodayTasks = ref([]) -const gamifyClaimablePoints = ref(0) -const gamifyStatusTip = ref('') -const TREE_LEVEL_ROADMAP = TREE_LEVELS -const treeWatering = ref(false) - -function isApiSuccess(res) { - return !!(res && Number(res.code) === 1) -} - -function applyGamifyPayload(data) { - if (!data || typeof data !== 'object') return - gamifyPoints.value = Number(data.points) || 0 - gamifyBadges.value = Array.isArray(data.badges) ? [...data.badges] : [] - try { - gamifyTaskAwards.value = data.task_awards ? JSON.parse(JSON.stringify(data.task_awards)) : {} - } catch (e) { - gamifyTaskAwards.value = data.task_awards || {} - } - gamifyTodayTasks.value = Array.isArray(data.today_tasks) - ? data.today_tasks.map((t) => ({ - id: String(t.id || ''), - name: String(t.name || ''), - points: Number(t.points) || 0, - completed: !!t.completed, - claimed: !!t.claimed - })) - : [] - gamifyClaimablePoints.value = Number(data.claimable_points) || 0 - if (data.message) { - gamifyStatusTip.value = String(data.message) - } -} - -async function fetchGamifyState() { - if (!hasAuthToken() || !diagnosisId.value) return - try { - const res = await proxy.apiUrl({ - url: '/api/tcm/dailyGetGamify', - method: 'GET', - data: { diagnosis_id: diagnosisId.value } - }, false) - if (isApiSuccess(res) && res.data) { - applyGamifyPayload(res.data) - } - } catch (e) { - console.error('获取游戏化数据失败', e) - } -} - -const todayHasDiet = computed(() => { - const today = todayStr.value - const d = dietByDate.value[today] - return !!(d && (d.breakfast_foods || d.lunch_foods || d.dinner_foods)) -}) - -const todayHasExercise = computed(() => { - const today = todayStr.value - const ex = exerciseByDate.value[today] - return !!(ex && (ex.exercise_type || ex.duration)) -}) - -const todayTasks = computed(() => { - if (gamifyTodayTasks.value.length) { - return gamifyTodayTasks.value - } - const today = todayStr.value - const awards = gamifyTaskAwards.value[today] || {} - return [ - { - id: 'blood', - name: '测血糖血压', - points: 10, - completed: todayChecked.value, - claimed: !!awards['blood'] - }, - { - id: 'diet', - name: '记今日饮食', - points: 10, - completed: todayHasDiet.value, - claimed: !!awards['diet'] - }, - { - id: 'exercise', - name: '记今日运动', - points: 10, - completed: todayHasExercise.value, - claimed: !!awards['exercise'] - } - ] -}) - -const claimablePoints = computed(() => { - if (gamifyTodayTasks.value.length) { - return gamifyClaimablePoints.value - } - return todayTasks.value - .filter(t => t.completed && !t.claimed) - .reduce((sum, t) => sum + t.points, 0) -}) - -const allTasksCleared = computed(() => { - const tasks = todayTasks.value - return tasks.length > 0 && tasks.every((t) => t.completed && t.claimed) -}) - -const unlockedBadges = computed(() => { - const list = [] - const streak = Number(streakDays.value?.days) || 0 - if (todayChecked.value) { - list.push({ id: 'today', label: '今日打卡', icon: 'check-circle', color: '#059669' }) - } - if (streak >= 3) { - list.push({ id: 'streak', label: `连击${streak}天`, icon: 'flame', color: '#EA580C' }) - } - if (streak >= 7) { - list.push({ id: 'week', label: '一周达人', icon: 'trophy', color: '#CA8A04' }) - } - if (last7CheckedCount.value >= 5) { - list.push({ id: 'w5', label: '本周5天+', icon: 'calendar', color: '#0891B2' }) - } - if (gamifyPoints.value >= 50) { - list.push({ id: 'p50', label: '积分50+', icon: 'sparkles', color: '#0891B2' }) - } - if (gamifyPoints.value >= 100) { - list.push({ id: 'p100', label: '积分破百', icon: 'trophy', color: '#7C3AED' }) - } - if (gamifyPoints.value >= 200) { - list.push({ id: 'p200', label: '积分200+', icon: 'sparkles', color: '#7C3AED' }) - } - if (treeLevel.value >= 3) { - list.push({ id: 'tree3', label: '小树苗', icon: 'glucose', color: '#0D9488' }) - } - if (treeLevel.value >= 5) { - list.push({ id: 'tree5', label: '拔节高', icon: 'activity', color: '#059669' }) - } - if (treeLevel.value >= 7) { - list.push({ id: 'tree7', label: '初绽香', icon: 'heart', color: '#EA580C' }) - } - if (treeState.value.isMax) { - list.push({ id: 'tree9', label: '圆满树', icon: 'trophy', color: '#CA8A04' }) - } - return list -}) - -const celebrateShow = ref(false) -const celebrateTitle = ref('') -const celebrateSub = ref('') -const floatReward = ref(null) -const inviteHeartBurst = ref(false) -const taskJustDoneId = ref('') -let celebrateTimer = null -let taskPopTimer = null - -const INVITE_TREE_WHISPERS = [ - 'Ta 正在努力控糖,您的鼓励很重要。', - '好习惯比完美数值更珍贵,一起加油。', - '每天记一笔,就是在照顾未来的自己。', - '点赞一下,让 Ta 知道家人在身边。' -] - -function hapticLight() { - try { - uni.vibrateShort({ type: 'light' }) - } catch (e) {} -} - -function triggerCelebrate(title, sub = '') { - if (celebrateTimer) clearTimeout(celebrateTimer) - celebrateTitle.value = title - celebrateSub.value = sub - celebrateShow.value = true - celebrateTimer = setTimeout(() => { - celebrateShow.value = false - celebrateSub.value = '' - }, 2400) -} - -function showFloatReward(points) { - floatReward.value = { text: points, key: Date.now() } - setTimeout(() => { - floatReward.value = null - }, 1100) -} - -function popTaskDone(taskId) { - if (!taskId) return - taskJustDoneId.value = taskId - if (taskPopTimer) clearTimeout(taskPopTimer) - taskPopTimer = setTimeout(() => { - taskJustDoneId.value = '' - }, 700) -} - -function onTreeWhisper() { - if (treeWatering.value) return - const line = pickTreeWhisper(treeLevel.value) - showUserToast(line, { duration: 2800 }) - hapticLight() -} - -function onInviteTreeWhisper() { - const line = INVITE_TREE_WHISPERS[Math.floor(Math.random() * INVITE_TREE_WHISPERS.length)] - showUserToast(line, { duration: 2800 }) - hapticLight() -} - -async function waterTree() { - if (!diagnosisId.value) return - if (treeWatering.value) return - - const oldLevel = treeLevel.value - treeWatering.value = true - setTimeout(() => { - treeWatering.value = false - }, 1000) - - try { - const res = await proxy.apiUrl({ - url: '/api/tcm/dailyWaterTree', - method: 'POST', - data: { diagnosis_id: diagnosisId.value } - }, false) - if (!isApiSuccess(res)) { - showUserToast((res && res.msg) || '浇水失败') - return - } - const data = res.data && typeof res.data === 'object' ? res.data : {} - const addedPoints = Number(data.added_points) || 0 - applyGamifyPayload({ - ...data, - message: data.message || res.msg || '' - }) - await nextTick() - - const tip = formatUserMessage(data.message || res.msg, '') - if (addedPoints > 0) { - showFloatReward(addedPoints) - hapticLight() - triggerCelebrate('浇水成功', tip || `+${addedPoints} 稳糖积分,小树咕咚喝饱啦`) - const claimedIds = Array.isArray(data.claimed_tasks) ? data.claimed_tasks : [] - claimedIds.forEach((id) => popTaskDone(id)) - const newLevel = treeLevel.value - if (newLevel > oldLevel) { - const meta = TREE_LEVELS[newLevel] || TREE_LEVELS[0] - setTimeout(() => triggerCelebrate('小树升级啦', `Lv.${newLevel} ${meta.name} · ${meta.desc}`), 520) - } - setTimeout(() => { - if (todayTasks.value.every((t) => t.completed && t.claimed)) { - triggerCelebrate('今日任务大满贯', '明天继续来浇浇水吧') - } - }, 900) - return - } - - // 无可领积分:先同步 UI,再按服务端 pending 引导或提示 - if (tip) { - showUserToast(tip, { duration: 2800 }) - } - - const needRecord = todayTasks.value.find((t) => !t.completed && !t.claimed) - if (needRecord) { - onGamifyTaskTap(needRecord) - return - } - - const pending = Array.isArray(data.pending_tasks) ? data.pending_tasks : [] - if (pending.length) { - const match = todayTasks.value.find((t) => t.id === pending[0].id) - || todayTasks.value.find((t) => t.name === pending[0].name) - if (match && !match.completed) { - onGamifyTaskTap(match) - } else if (!tip) { - showUserToast(`先完成:${pending[0].name}`, { duration: 2800 }) - } - return - } - - if (Number(data.claimable_points) > 0) { - showUserToast('还有积分待领取,请再点一次浇水') - return - } - - if (allTasksCleared.value && !tip) { - showUserToast('今日奖励已全部领取', { duration: 2800 }) - } - } catch (e) { - showUserToast('浇水失败,请稍后重试') - } -} - -const treeState = computed(() => calcTreeFromPoints(gamifyPoints.value)) -const treeLevel = computed(() => treeState.value.level) -const treeProgress = computed(() => treeState.value.progress) -const treeLevelName = computed(() => treeState.value.name) -const treeEmoji = computed(() => treeState.value.emoji) -const treeMood = computed(() => treeState.value.mood) -const treeProgressLabel = computed(() => { - if (treeState.value.isMax) return '已满级 · 圆满树' - return `${treeState.value.xpInLevel}/${treeState.value.xpNeed} XP` -}) -const treeNextHint = computed(() => { - if (treeState.value.isMax) return '您已养成满级稳糖树,习惯就是最好的奖励' - if (claimablePoints.value > 0) return `浇水后可再积 ${claimablePoints.value} 分,离升级更近一步` - return `再积 ${treeState.value.pointsToNext} 分,升级「${treeState.value.nextName}」` -}) - -// ============ 分享 / 家人点赞 / 邀请观看 ============ +// ============ 邀请观看(家人分享页)============ const isInviteViewer = ref(false) const inviteLoading = ref(false) const inviteError = ref('') const invitePreview = ref({}) const inviteLiking = ref(false) const inviteLikeNickname = ref('家人') +const inviteHeartBurst = ref(false) const familyNicknameOptions = ['家人', '亲友', '老伴', '子女'] -const weeklyReportVisible = ref(false) -const shareTitle = ref('') -const pendingShareInviteCode = ref('') - -const familyLikeSummary = ref({ - like_count: 0, - summary_line: '分享战报给家人,邀请他们为您点赞鼓劲', - recent: [] -}) -const lastFamilyLikeCount = ref(0) - const VIEWER_KEY_STORAGE = 'tongji_viewer_key_v1' -function familyLikeSeenStorageKey(dId = diagnosisId.value) { - return `tongji_family_like_seen_${Number(dId) || 0}_${todayStr.value}` -} - -function getStoredFamilyLikeSeen(dId = diagnosisId.value) { +function hapticLight() { try { - return Number(uni.getStorageSync(familyLikeSeenStorageKey(dId))) || 0 - } catch (e) { - return 0 - } -} - -function setStoredFamilyLikeSeen(count, dId = diagnosisId.value) { - try { - uni.setStorageSync(familyLikeSeenStorageKey(dId), Number(count) || 0) + uni.vibrateShort({ type: 'light' }) } catch (e) {} } @@ -3660,55 +2245,6 @@ function getViewerKey() { } } -function getWeekOfYear(date) { - const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate())) - const dayNum = d.getUTCDay() || 7 - d.setUTCDate(d.getUTCDate() + 4 - dayNum) - const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1)) - return Math.ceil((((d - yearStart) / 86400000) + 1) / 7) -} - -const weeklyReportData = computed(() => { - if (isInviteViewer.value && invitePreview.value.invite_code) { - const p = invitePreview.value - return { - patientLabel: p.patient_label || '家人', - weekLabel: p.week_label || '', - recordDays: Number(p.record_days) || 0, - completeDays: Number(p.complete_days) || 0, - streakDays: Number(p.streak_days) || 0, - quote: p.quote || '' - } - } - const name = (patientName.value || '我').trim() - const patientLabel = name.length > 6 ? `${name.slice(0, 6)}…` : name - const now = new Date() - const weekLabel = `${now.getFullYear()}年 第${getWeekOfYear(now)}周` - const rd = last7CheckedCount.value - const cd = weekCompleteDays.value - let quote = '坚持记录,就是在照顾未来的自己。' - if (rd >= 7) quote = '本周每天都留下了记录,这份自律值得骄傲!' - else if (cd >= 5) quote = `本周有 ${cd} 天完成了空腹+餐后记录,习惯越来越稳。` - else if (rd >= 4) quote = `本周已记录 ${rd} 天,比不记录强一百倍。` - else if (rd > 0) quote = '好的开始!下周试着多记录几天。' - else quote = '从今天记一笔开始,小树就会发芽。' - return { - patientLabel, - weekLabel, - recordDays: rd, - completeDays: cd, - streakDays: streakDays.value.days, - quote - } -}) - -const reportTreeLevel = computed(() => { - if (isInviteViewer.value && invitePreview.value.invite_code) { - return Number(invitePreview.value.tree_level) || 0 - } - return treeLevel.value -}) - const inviteTreeLevel = computed(() => Number(invitePreview.value.tree_level) || 0) const inviteDailyRecords = computed(() => { @@ -3721,172 +2257,17 @@ function formatInviteSugar(v) { return String(v) } -const ENCOURAGE_LINES = [ +const INVITE_TREE_WHISPERS = [ + 'Ta 正在努力控糖,您的鼓励很重要。', + '好习惯比完美数值更珍贵,一起加油。', '每天记一笔,就是在照顾未来的自己。', - '坚持记录比纠结数值更重要,您做得很好。', - '给树浇浇水,也给自己点个赞。', - '家人会看到您的努力,习惯比完美更珍贵。', - '空腹、餐后各记一次,完整一日更有成就感。' + '点赞一下,让 Ta 知道家人在身边。' ] -const encourageIndex = ref(0) -const encourageLine = computed(() => { - const pending = todayTasks.value.find((t) => !t.completed) - if (pending?.id === 'blood') return '先完成血糖记录,今日第一格就亮起来。' - if (pending?.id === 'diet') return '记下今日饮食,离领奖更近一步。' - if (pending?.id === 'exercise') return '动一动、记一记,小树长得更快。' - return ENCOURAGE_LINES[encourageIndex.value % ENCOURAGE_LINES.length] -}) -function onEncourageTap() { - encourageIndex.value = (encourageIndex.value + 1) % ENCOURAGE_LINES.length - const line = ENCOURAGE_LINES[encourageIndex.value] - if (ttsEnabled.value) { - ttsSpeak(line) - } else { - showUserToast(line, { duration: 2800 }) - } -} - -function onGamifyTaskTap(task) { - if (!task) return - if (task.claimed) { - showUserToast('该任务积分已领取') - return - } - if (!task.completed) { - if (task.id === 'blood') { - openInputForm() - return - } - if (task.id === 'diet') { - openDietForm() - return - } - if (task.id === 'exercise') { - openExerciseForm() - return - } - showUserToast(`请先完成:${task.name}`) - return - } - waterTree() -} - -function shareInviteCacheKey() { - return `tongji_invite_${diagnosisId.value}_${todayStr.value}` -} - -async function ensureShareInvite() { - if (!diagnosisId.value) return '' - const cacheKey = shareInviteCacheKey() - try { - const cached = uni.getStorageSync(cacheKey) - if (cached && cached.invite_code && cached.invite_date === todayStr.value) { - pendingShareInviteCode.value = cached.invite_code - return cached.invite_code - } - } catch (e) {} - - try { - const res = await proxy.apiUrl({ - url: '/api/tcm/dailyCreateShareInvite', - method: 'POST', - data: { diagnosis_id: diagnosisId.value } - }, false) - if (res && res.code === 1 && res.data && res.data.invite_code) { - const code = res.data.invite_code - pendingShareInviteCode.value = code - try { - uni.setStorageSync(cacheKey, { - invite_code: code, - invite_date: todayStr.value - }) - } catch (e) {} - return code - } - if (res?.msg) showUserToast(res.msg) - } catch (e) { - showUserToast('生成邀请码失败') - } - return '' -} - -async function prepareShareWeekly() { - const code = await ensureShareInvite() - if (!code) return - const d = weeklyReportData.value - shareTitle.value = `【稳糖打卡】${d.patientLabel}本周记录${d.recordDays}天,邀请码 ${code}(今日有效)` -} - -function shareWeeklyFallback() { - prepareShareWeekly().then((code) => { - if (!code) return - const d = weeklyReportData.value - const text = `【稳糖打卡】${d.patientLabel} ${d.weekLabel}\n本周 ${d.recordDays} 天有记录、${d.completeDays} 个完整日、连续 ${d.streakDays} 天\n${d.quote}\n邀请码:${code}(仅今日有效)\n打开小程序「日常记录」输入邀请码查看` - uni.setClipboardData({ - data: text, - success: () => showUserToast('战报文案已复制,可发给家人') - }) - }) -} - -async function openWeeklyReport() { - if (!diagnosisId.value) { - showUserToast('请先选择就诊卡') - return - } - await ensureShareInvite() - weeklyReportVisible.value = true -} - -function closeWeeklyReport() { - weeklyReportVisible.value = false -} - -async function loadFamilyLikeSummary() { - if (!hasAuthToken() || !diagnosisId.value || isInviteViewer.value) return - const token = uni.getStorageSync('token') - if (!token) return - try { - const res = await proxy.apiUrl({ - url: '/api/tcm/dailyFamilyLikeSummary', - method: 'GET', - data: { diagnosis_id: diagnosisId.value } - }, false) - if (res && res.code === 1 && res.data) { - const storedSeen = getStoredFamilyLikeSeen() - const prev = Math.max(lastFamilyLikeCount.value, storedSeen) - const next = Number(res.data.like_count) || 0 - familyLikeSummary.value = { - like_count: next, - summary_line: res.data.summary_line || '', - recent: Array.isArray(res.data.recent) ? res.data.recent : [] - } - // 仅在有「新增」点赞时庆祝,避免刷新/重进页面重复弹层 - if (next > prev) { - const delta = next - prev - triggerCelebrate( - delta === 1 && prev === 0 ? '首个家人点赞' : '收到家人鼓励', - delta === 1 ? '分享真的有用,继续加油' : `新增 ${delta} 位家人点赞,共 ${next} 人` - ) - hapticLight() - } - lastFamilyLikeCount.value = next - setStoredFamilyLikeSeen(next) - } - } catch (e) {} -} - -function onFamilyLikeStripTap() { - if (familyLikeSummary.value.like_count > 0) { - if (ttsEnabled.value) { - ttsSpeak(familyLikeSummary.value.summary_line) - } else { - showUserToast(familyLikeSummary.value.summary_line, { duration: 2400 }) - } - return - } - openWeeklyReport() +function onInviteTreeWhisper() { + const line = INVITE_TREE_WHISPERS[Math.floor(Math.random() * INVITE_TREE_WHISPERS.length)] + showUserToast(line, { duration: 2800 }) + hapticLight() } async function bootstrapInviteViewer(inviteCode) { @@ -3895,7 +2276,6 @@ async function bootstrapInviteViewer(inviteCode) { inviteError.value = '' invitePreview.value = {} inviteHeartBurst.value = false - celebrateShow.value = false try { const res = await proxy.apiUrl({ url: '/api/tcm/dailySharePreview', @@ -3976,51 +2356,48 @@ async function onFamilyLike() { font-size: 28rpx; color: #64748b; } +.more-entry-bar { + margin: -72rpx 32rpx 24rpx; + padding: 28rpx 32rpx; + background: #ffffff; + border-radius: 24rpx; + box-shadow: var(--shadow-premium); + border: var(--border-premium); + display: flex; + align-items: center; + justify-content: space-between; + gap: 16rpx; + position: relative; + z-index: 2; +} +.more-entry-left { + flex: 1; + min-width: 0; +} +.more-entry-title { + display: block; + font-size: 30rpx; + font-weight: 600; + color: #0f172a; +} +.more-entry-sub { + display: block; + margin-top: 6rpx; + font-size: 24rpx; + color: #64748b; +} +.more-entry-arrow { + font-size: 40rpx; + color: #386641; + line-height: 1; +} .daily-page { - /* Premium UI Color System & Variables - Teal-Emerald Healing Redesign */ - --primary: #0d9488; - --primary-grad: linear-gradient(135deg, #0d9488 0%, #10b981 100%); - --primary-light: #f0fdfa; - --primary-glow: rgba(13, 148, 136, 0.12); - - --success: #10b981; - --success-grad: linear-gradient(135deg, #10b981 0%, #059669 100%); - --success-light: #ecfdf5; - - --warning: #f59e0b; - --warning-grad: linear-gradient(135deg, #f59e0b 0%, #d97706 100%); - --warning-light: #fffbeb; - - --danger: #ef4444; - --danger-grad: linear-gradient(135deg, #f87171 0%, #ef4444 100%); - --danger-light: #fef2f2; - --danger-glow: rgba(239, 68, 68, 0.12); - - --violet: #6366f1; - --violet-grad: linear-gradient(135deg, #6366f1 0%, #4f46e5 100%); - --violet-light: #f5f3ff; - - --slate-50: #f8fafc; - --slate-100: #f1f5f9; - --slate-200: #e2e8f0; - --slate-300: #cbd5e1; - --slate-400: #94a3b8; - --slate-600: #475569; - --slate-900: #0f172a; - - --radius-card: 32rpx; /* 更为圆润的设计 */ + /* 详见 ../styles/daily-theme.scss */ + --radius-card: 32rpx; --radius-pill: 999rpx; - - --shadow-premium: 0 12rpx 36rpx rgba(15, 23, 42, 0.03); - --shadow-glow-primary: 0 10rpx 24rpx rgba(13, 148, 136, 0.18); - --shadow-glow-success: 0 10rpx 24rpx rgba(16, 185, 129, 0.18); - --shadow-glow-danger: 0 10rpx 24rpx rgba(239, 68, 68, 0.18); - --transition-smooth: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1); - --border-premium: 1rpx solid rgba(13, 148, 136, 0.06); min-height: 100vh; - background: #f8fafc; padding-bottom: 64rpx; } @@ -4034,13 +2411,13 @@ async function onFamilyLike() { .hero-bg { position: absolute; inset: 0; - background: linear-gradient(135deg, #0f172a 0%, #0d9488 60%, #10b981 100%); + background: linear-gradient(135deg, #163d22 0%, #204e2b 55%, #386641 100%); z-index: 0; &::after { content: ''; position: absolute; inset: 0; - background: radial-gradient(circle at 85% 15%, rgba(34, 211, 238, 0.15) 0%, transparent 60%); + background: radial-gradient(circle at 85% 15%, rgba(175, 226, 179, 0.15) 0%, transparent 60%); } } .hero-content { @@ -4239,7 +2616,7 @@ async function onFamilyLike() { } .stat-fasting .stat-card-tag { background: rgba(13, 148, 136, 0.09); - color: #0d9488; + color: #386641; } .stat-postprandial .stat-card-tag { background: rgba(245, 158, 11, 0.09); @@ -5365,7 +3742,7 @@ async function onFamilyLike() { } .streak-dot.done .streak-dot-mark { background: var(--primary-grad); - box-shadow: 0 2rpx 6rpx rgba(14, 165, 164, 0.3); + box-shadow: 0 2rpx 6rpx rgba(32, 78, 43, 0.3); } .streak-dot.today { .streak-dot-label { @@ -5380,7 +3757,7 @@ async function onFamilyLike() { } .streak-dot.today.done .streak-dot-mark { border-color: #ffffff; - box-shadow: 0 0 0 3rpx var(--primary), 0 2rpx 8rpx rgba(14, 165, 164, 0.4); + box-shadow: 0 0 0 3rpx var(--primary), 0 2rpx 8rpx rgba(32, 78, 43, 0.4); } .streak-week-text { font-size: 24rpx; @@ -5410,14 +3787,14 @@ async function onFamilyLike() { align-items: flex-end; gap: 2rpx; padding: 4rpx 18rpx 4rpx 22rpx; - background: linear-gradient(135deg, #f0fdfa 0%, #ccfbf1 100%); + background: linear-gradient(135deg, #eef6ef 0%, #ccfbf1 100%); border-radius: 20rpx; border-left: 6rpx solid var(--primary); } .calendar-stat-num { font-size: 48rpx; font-weight: 800; - color: #0d9488; + color: #386641; line-height: 1.1; letter-spacing: -1rpx; } @@ -5530,7 +3907,7 @@ async function onFamilyLike() { box-shadow: 0 0 0 2rpx rgba(15, 23, 42, 0.08); } .calendar-cell.level-1 .calendar-cell-mark { - background: #14b8a6; + background: #386641; box-shadow: none; } .calendar-cell.today .calendar-cell-mark { @@ -5572,7 +3949,7 @@ async function onFamilyLike() { background: var(--slate-100); border: 1rpx solid var(--slate-200); &.level-1 { background: #ccfbf1; border-color: #99f6e4; } - &.level-2 { background: linear-gradient(135deg, #5eead4 0%, #2dd4bf 100%); border-color: #14b8a6; } + &.level-2 { background: linear-gradient(135deg, #cfe8d3 0%, #afe2b3 100%); border-color: #386641; } &.level-3 { background: var(--primary-grad); border-color: transparent; } &.legend-today { background: linear-gradient(135deg, #f97316 0%, #ea580c 100%); @@ -5917,14 +4294,14 @@ async function onFamilyLike() { display: flex; flex-direction: column; align-items: flex-end; - background: linear-gradient(135deg, #f0fdfa 0%, #ccfbf1 100%); + background: linear-gradient(135deg, #eef6ef 0%, #ccfbf1 100%); border-left: 6rpx solid var(--primary); padding: 8rpx 18rpx 8rpx 20rpx; border-radius: 16rpx; .pg-points-num { font-size: 38rpx; font-weight: 800; - color: #0d9488; + color: #386641; line-height: 1.1; } .pg-points-label { @@ -6013,7 +4390,7 @@ async function onFamilyLike() { .record-quick-status { font-size: 22rpx; font-weight: 700; - color: #0d9488; + color: #386641; .record-quick-chip.done & { color: var(--success); } @@ -6022,7 +4399,7 @@ async function onFamilyLike() { display: flex; align-items: flex-start; gap: 20rpx; - background: linear-gradient(145deg, #f0fdfa 0%, #f8fafc 55%, #fffbeb 100%); + background: linear-gradient(145deg, #eef6ef 0%, #f8fafc 55%, #fffbeb 100%); border-radius: 24rpx; padding: 24rpx 20rpx; border: 2rpx solid #99f6e4; @@ -6045,11 +4422,11 @@ async function onFamilyLike() { z-index: 2; padding: 4rpx 14rpx; border-radius: 999rpx; - background: linear-gradient(135deg, #0ea5a4, #14b8a6); + background: linear-gradient(135deg, #204e2b, #386641); color: #ffffff; font-size: 20rpx; font-weight: 800; - box-shadow: 0 4rpx 12rpx rgba(14, 165, 164, 0.35); + box-shadow: 0 4rpx 12rpx rgba(32, 78, 43, 0.35); } .tree-mood-pill { position: absolute; @@ -6100,7 +4477,7 @@ async function onFamilyLike() { min-width: 56rpx; padding: 8rpx 6rpx; border-radius: 14rpx; - background: #f1f5f9; + background: #faf9f5; border: 1rpx solid #e2e8f0; opacity: 0.55; &.done { @@ -6111,8 +4488,8 @@ async function onFamilyLike() { &.current { opacity: 1; background: linear-gradient(180deg, #ccfbf1, #ffffff); - border-color: #14b8a6; - box-shadow: 0 4rpx 14rpx rgba(14, 165, 164, 0.25); + border-color: #386641; + box-shadow: 0 4rpx 14rpx rgba(32, 78, 43, 0.25); transform: scale(1.06); } &.locked { @@ -6305,7 +4682,7 @@ async function onFamilyLike() { transition: var(--transition-smooth); &:active { transform: scale(0.98); - background: linear-gradient(135deg, #0d9488 0%, var(--primary) 100%); + background: linear-gradient(135deg, #386641 0%, var(--primary) 100%); } &.disabled { opacity: 0.7; @@ -6317,7 +4694,7 @@ async function onFamilyLike() { } @keyframes water-btn-pulse { 0%, 100% { box-shadow: var(--shadow-glow-primary); } - 50% { box-shadow: 0 4rpx 24rpx rgba(14, 165, 164, 0.5); } + 50% { box-shadow: 0 4rpx 24rpx rgba(32, 78, 43, 0.5); } } .today-tasks-zone { @@ -6365,7 +4742,7 @@ async function onFamilyLike() { } @keyframes task-pop { 0% { transform: scale(1); } - 40% { transform: scale(1.03); box-shadow: 0 8rpx 20rpx rgba(14, 165, 164, 0.2); } + 40% { transform: scale(1.03); box-shadow: 0 8rpx 20rpx rgba(32, 78, 43, 0.2); } 100% { transform: scale(1); } } .task-left { @@ -6431,7 +4808,7 @@ async function onFamilyLike() { &.pending { background: var(--primary); color: #ffffff; - box-shadow: 0 4rpx 10rpx rgba(14, 165, 164, 0.2); + box-shadow: 0 4rpx 10rpx rgba(32, 78, 43, 0.2); &:active { transform: scale(0.96); } @@ -6439,14 +4816,14 @@ async function onFamilyLike() { &:not(.disabled):not(.claimed):not(.pending) { background: var(--primary); color: #ffffff; - box-shadow: 0 4rpx 10rpx rgba(14, 165, 164, 0.2); + box-shadow: 0 4rpx 10rpx rgba(32, 78, 43, 0.2); &:active { transform: scale(0.96); } } &.claimed { background: #ccfbf1; - color: #0d9488; + color: #386641; } &.disabled { opacity: 0.6; @@ -6585,7 +4962,7 @@ async function onFamilyLike() { /* === 共享/邀请查看样式 === */ .invite-view-wrap { min-height: 100vh; - background: linear-gradient(180deg, #ecfeff 0%, #f0f9ff 42%, #f8fafc 100%); + background: linear-gradient(180deg, #eef6ef 0%, #f4f4f0 42%, #faf9f5 100%); padding-bottom: calc(32rpx + env(safe-area-inset-bottom)); } .invite-hero { @@ -6596,7 +4973,7 @@ async function onFamilyLike() { .invite-hero-bg { position: absolute; inset: 0; - background: linear-gradient(145deg, #0891b2 0%, #0e7490 55%, #155e75 100%); + background: linear-gradient(145deg, #204e2b 0%, #386641 55%, #163d22 100%); } .invite-hero-content { position: relative; @@ -6673,8 +5050,8 @@ async function onFamilyLike() { justify-content: center; flex-shrink: 0; } -.invite-card-icon-wrap--cyan { background: #ecfeff; } -.invite-card-icon-wrap--teal { background: #f0fdfa; } +.invite-card-icon-wrap--cyan { background: #eef6ef; } +.invite-card-icon-wrap--teal { background: #eef6ef; } .invite-card-icon-wrap--green { background: #ecfdf5; } .invite-card-head-text, .invite-section-head-text { @@ -6685,7 +5062,7 @@ async function onFamilyLike() { display: block; font-size: 28rpx; font-weight: 700; - color: #164e63; + color: #1b1c1a; } .invite-card-hint { display: block; @@ -6699,9 +5076,9 @@ async function onFamilyLike() { font-size: 44rpx; font-weight: 800; letter-spacing: 6rpx; - color: #0891b2; + color: #204e2b; padding: 20rpx 12rpx; - background: linear-gradient(180deg, #ecfeff, #f0fdfa); + background: linear-gradient(180deg, #eef6ef, #eef6ef); border-radius: 16rpx; border: 2rpx dashed rgba(8, 145, 178, 0.25); } @@ -6722,7 +5099,7 @@ async function onFamilyLike() { display: block; font-size: 30rpx; font-weight: 800; - color: #164e63; + color: #1b1c1a; } .invite-tree-desc { display: block; @@ -6764,7 +5141,7 @@ async function onFamilyLike() { display: block; font-size: 38rpx; font-weight: 800; - color: #164e63; + color: #1b1c1a; line-height: 1.1; } .invite-stat-label { @@ -6776,20 +5153,20 @@ async function onFamilyLike() { .invite-quote-box { margin-top: 16rpx; padding: 18rpx 20rpx; - background: #ecfeff; + background: #eef6ef; border-radius: 16rpx; - border-left: 6rpx solid #0891b2; + border-left: 6rpx solid #204e2b; } .invite-quote { font-size: 26rpx; - color: #164e63; + color: #1b1c1a; line-height: 1.55; } .invite-section-title { display: block; font-size: 30rpx; font-weight: 800; - color: #164e63; + color: #1b1c1a; } .invite-section-sub { display: block; @@ -6837,8 +5214,8 @@ async function onFamilyLike() { border: 3rpx solid #ffffff; box-shadow: 0 0 0 2rpx #cbd5e1; &.filled { - background: #0891b2; - box-shadow: 0 0 0 2rpx #67e8f9; + background: #204e2b; + box-shadow: 0 0 0 2rpx #b8dcc0; } } .invite-day-line { @@ -6897,7 +5274,7 @@ async function onFamilyLike() { .invite-metric-value { font-size: 34rpx; font-weight: 800; - color: #0891b2; + color: #204e2b; line-height: 1.2; &.is-high { color: var(--danger); @@ -7043,7 +5420,7 @@ async function onFamilyLike() { display: block; font-size: 32rpx; font-weight: 800; - color: #164e63; + color: #1b1c1a; margin-bottom: 10rpx; } .invite-error-text { @@ -7172,7 +5549,9 @@ async function onFamilyLike() { } .daily-page.is-invite-viewer { - background: #ecfeff; + background: var(--primary-light, #eef6ef); padding-bottom: 0; } + +@import '../styles/daily-theme.scss'; diff --git a/TUICallKit-Vue3/tongji/pages/more.vue b/TUICallKit-Vue3/tongji/pages/more.vue new file mode 100644 index 00000000..36342880 --- /dev/null +++ b/TUICallKit-Vue3/tongji/pages/more.vue @@ -0,0 +1,6397 @@ + + + + + diff --git a/TUICallKit-Vue3/tongji/styles/daily-theme.scss b/TUICallKit-Vue3/tongji/styles/daily-theme.scss new file mode 100644 index 00000000..432d8007 --- /dev/null +++ b/TUICallKit-Vue3/tongji/styles/daily-theme.scss @@ -0,0 +1,953 @@ +/** + + * 日常记录统一主题 — 与甄养堂小程序 TCM Care 品牌色一致 + + * 来源:pages/index/index.vue · tabBar selectedColor #204e2b + + */ + + + +.daily-page { + + /* 品牌主色 */ + + --primary: #204e2b; + + --primary-dark: #163d22; + + --primary-container: #386641; + + --primary-grad: linear-gradient(160deg, #204e2b 0%, #386641 100%); + + --primary-light: #eef6ef; + + --primary-soft: #cfe8d3; + + --primary-glow: rgba(32, 78, 43, 0.14); + + --on-primary-container: #afe2b3; + + + + --success: #386641; + + --success-light: #eef6ef; + + + + --danger: #dc2626; + + --danger-light: #fef2f2; + + --danger-glow: rgba(220, 38, 38, 0.1); + + + + --text-primary: #1b1c1a; + + --text-secondary: #414941; + + --text-muted: #727970; + + + + --surface: #ffffff; + + --surface-muted: #f4f4f0; + + --border: #e3e2df; + + --border-soft: rgba(32, 78, 43, 0.12); + + + + --slate-50: #faf9f5; + + --slate-100: #f4f4f0; + + --slate-200: #e9e8e4; + + --slate-300: #e3e2df; + + --slate-400: #727970; + + --slate-600: #414941; + + --slate-900: #1b1c1a; + + + + --shadow-premium: 0 8rpx 28rpx rgba(32, 78, 43, 0.06); + + --shadow-sm: 0 4rpx 16rpx rgba(32, 78, 43, 0.05); + + --shadow-glow-primary: 0 8rpx 20rpx rgba(32, 78, 43, 0.18); + + --shadow-glow-danger: 0 8rpx 20rpx rgba(220, 38, 38, 0.12); + + --border-premium: 1rpx solid var(--border-soft); + + + + background: var(--slate-50); + +} + + + +/* Hero:品牌绿渐变 */ + +.daily-page .hero-bg { + + background: var(--primary-grad); + + &::after { + + background: radial-gradient(circle at 88% 12%, rgba(255, 255, 255, 0.1) 0%, transparent 55%); + + } + +} + + + +.daily-page .hero-orb { + + display: none; + +} + + + +.daily-page .hero-pill.voice.speaking { + + background: rgba(255, 255, 255, 0.92); + + border-color: rgba(255, 255, 255, 0.95); + + animation: none; + + box-shadow: 0 0 0 3rpx rgba(255, 255, 255, 0.35); + +} + + + +.daily-page .hero-status-chip.pending { + + background: rgba(255, 255, 255, 0.16); + + border-color: rgba(255, 255, 255, 0.28); + +} + + + +.daily-page .hero-status-chip.done { + + background: rgba(255, 255, 255, 0.2); + + border-color: rgba(255, 255, 255, 0.32); + +} + + + +/* 指标卡 */ + +.daily-page .stat-fasting, + +.daily-page .stat-postprandial, + +.daily-page .stat-other { + + background: var(--surface); + +} + + + +.daily-page .stat-fasting .stat-card-tag, + +.daily-page .stat-postprandial .stat-card-tag, + +.daily-page .stat-other .stat-card-tag { + + background: var(--primary-light); + + color: var(--primary-dark); + +} + + + +.daily-page .stat-card-value { + + background: none; + + -webkit-background-clip: unset; + + background-clip: unset; + + color: var(--text-primary); + +} + + + +.daily-page .stat-card-value.is-high { + + background: none; + + color: var(--danger); + +} + + + +.daily-page .stat-card-trend.trend-down { + + background: var(--primary-light); + + .stat-card-trend-arrow, + + .stat-card-trend-delta { + + color: var(--primary-dark); + + } + +} + + + +.daily-page .more-entry-arrow { + + color: var(--primary); + +} + + + +.daily-page .card-chip.active { + + background: var(--primary); + + box-shadow: var(--shadow-glow-primary); + +} + + + +.daily-page .range-item.active { + + background: var(--primary); + + box-shadow: var(--shadow-glow-primary); + +} + + + +.daily-page .legend-dot-fasting { + + background: var(--primary); + +} + + + +.daily-page .legend-dot-postprandial { + + background: var(--primary-container); + +} + + + +.daily-page .streak-strip { + + background: var(--surface); + + border-color: var(--border-soft); + + &::after { + + background: radial-gradient(circle, rgba(32, 78, 43, 0.08) 0%, transparent 70%); + + } + +} + + + +.daily-page .streak-num { + + color: var(--primary-dark); + +} + + + +/* 日历:品牌绿深浅 */ + +.daily-page .calendar-cell.level-1 .calendar-cell-mark, + +.daily-page .calendar-legend-cell.level-1 { + + background: var(--primary-soft); + + border-color: #b8dcc0; + +} + + + +.daily-page .calendar-cell.level-2 .calendar-cell-mark, + +.daily-page .calendar-legend-cell.level-2 { + + background: var(--on-primary-container); + + border-color: #86c992; + +} + + + +.daily-page .calendar-cell.level-3 .calendar-cell-mark, + +.daily-page .calendar-legend-cell.level-3 { + + background: var(--primary); + + border-color: var(--primary-dark); + +} + + + +.daily-page .day-block-tag.tag-blood, + +.daily-page .day-block-tag.tag-diet, + +.daily-page .day-block-tag.tag-exercise, + +.daily-page .day-block-tag.tag-tracking { + + background: var(--primary-light); + + color: var(--primary-dark); + + border: 1rpx solid var(--border-soft); + +} + + + +.daily-page .day-block-tag.tag-diet, + +.daily-page .day-block-tag.tag-exercise { + + background: var(--surface-muted); + + color: var(--text-secondary); + + border-color: var(--border); + +} + + + +.daily-page .pg-points-badge { + + background: var(--primary-light); + + border-color: var(--border-soft); + +} + + + +.daily-page .pg-points-num { + + color: var(--primary-dark); + +} + + + +.daily-page .record-quick-chip.pending { + + border-color: var(--border); + + background: var(--surface-muted); + +} + + + +.daily-page .record-quick-chip.done { + + border-color: rgba(56, 102, 65, 0.35); + + background: var(--success-light); + +} + + + +.daily-page .task-action-btn.pending { + + background: var(--primary); + +} + + + +.daily-page .water-btn { + + background: var(--primary); + +} + + + +.daily-page .family-like-strip, + +.daily-page .encourage-strip { + + background: var(--surface); + + border: 1rpx solid var(--border); + + box-shadow: var(--shadow-premium); + +} + + + +.daily-page .card-title, + +.daily-page .hero-title { + + color: inherit; + +} + + + +.daily-page .report-share-btn, + +.daily-page .report-share-btn::after { + + background: var(--primary); + + box-shadow: var(--shadow-glow-primary); + +} + + + +.daily-page .input-modal-btn.primary { + + background: var(--primary); + +} + + + +.daily-page .input-field-dot-fasting { + + background: var(--primary); + +} + + + +.daily-page .input-field-dot-postprandial { + + background: var(--primary-container); + +} + + + +.daily-page .input-field-dot-other { + + background: var(--text-muted); + +} + + + +.daily-page .voice-switch.on .voice-switch-track { + + background: var(--primary); + +} + + + +.daily-page .heatmap-foot-cta { + + background: var(--primary); + +} + + + +/* ========== 适老极简首页(index) ========== */ + +.daily-page .elder-header { + + padding: 48rpx 32rpx 24rpx; + +} + + + +.daily-page .elder-greet { + + display: block; + + font-size: 44rpx; + + font-weight: 700; + + color: var(--text-primary); + + line-height: 1.35; + +} + + + +.daily-page .elder-status { + + display: block; + + margin-top: 16rpx; + + font-size: 32rpx; + + font-weight: 600; + + line-height: 1.4; + + + + &.is-done { + + color: var(--success); + + } + + + + &.is-pending { + + color: var(--text-secondary); + + } + +} + + + +.daily-page .elder-card-row { + + display: flex; + + flex-wrap: wrap; + + gap: 16rpx; + + margin-top: 24rpx; + +} + + + +.daily-page .elder-card-chip { + + padding: 12rpx 28rpx; + + border-radius: 999rpx; + + background: var(--surface); + + border: 2rpx solid var(--border); + + + + text { + + font-size: 28rpx; + + color: var(--text-secondary); + + } + + + + &.active { + + background: var(--primary); + + border-color: var(--primary); + + + + text { + + color: #fff; + + font-weight: 600; + + } + + } + +} + + + +.daily-page .elder-actions { + + padding: 0 32rpx 24rpx; + + display: flex; + + flex-direction: column; + + gap: 20rpx; + +} + + + +.daily-page .elder-btn { + + display: flex; + + align-items: center; + + justify-content: center; + + gap: 16rpx; + + min-height: 112rpx; + + border-radius: 24rpx; + + box-shadow: var(--shadow-sm); + +} + + + +.daily-page .elder-btn-text { + + font-size: 36rpx; + + font-weight: 700; + +} + + + +.daily-page .elder-btn-primary { + + background: var(--primary); + + + + .elder-btn-text { + + color: #fff; + + } + +} + + + +.daily-page .elder-btn-voice { + + background: var(--surface); + + border: 2rpx solid var(--border-soft); + + + + .elder-btn-text { + + color: var(--primary-dark); + + } + + + + &.speaking { + + border-color: var(--primary); + + background: var(--primary-light); + + } + + + + &.disabled { + + opacity: 0.45; + + } + +} + + + +.daily-page .elder-today-panel { + + margin: 0 32rpx 24rpx; + + padding: 32rpx; + + background: var(--surface); + + border-radius: 24rpx; + + border: 1rpx solid var(--border); + + box-shadow: var(--shadow-premium); + +} + + + +.daily-page .elder-panel-title { + + display: block; + + font-size: 34rpx; + + font-weight: 700; + + color: var(--text-primary); + + margin-bottom: 24rpx; + +} + + + +.daily-page .elder-today-grid { + + display: grid; + + grid-template-columns: 1fr 1fr; + + gap: 20rpx; + +} + + + +.daily-page .elder-metric { + + padding: 24rpx; + + border-radius: 20rpx; + + background: var(--surface-muted); + + border: 2rpx solid var(--border); + + + + &.is-high { + + border-color: rgba(220, 38, 38, 0.35); + + background: var(--danger-light); + + } + +} + + + +.daily-page .elder-metric-label { + + display: block; + + font-size: 28rpx; + + color: var(--text-secondary); + + margin-bottom: 12rpx; + +} + + + +.daily-page .elder-metric-value-row { + + display: flex; + + align-items: baseline; + + gap: 8rpx; + +} + + + +.daily-page .elder-metric-value { + + font-size: 56rpx; + + font-weight: 800; + + color: var(--text-primary); + + line-height: 1.1; + +} + + + +.daily-page .elder-metric.is-high .elder-metric-value { + + color: var(--danger); + +} + + + +.daily-page .elder-metric-unit { + + font-size: 26rpx; + + color: var(--text-muted); + +} + + + +.daily-page .elder-metric-flag { + + display: block; + + margin-top: 12rpx; + + font-size: 26rpx; + + color: var(--danger); + + font-weight: 600; + +} + + + +.daily-page .elder-more-link { + + margin: 0 32rpx 16rpx; + + padding: 28rpx 32rpx; + + display: flex; + + align-items: center; + + justify-content: space-between; + + background: var(--surface); + + border-radius: 24rpx; + + border: 1rpx solid var(--border); + +} + + + +.daily-page .elder-more-title { + + display: block; + + font-size: 32rpx; + + font-weight: 700; + + color: var(--text-primary); + +} + + + +.daily-page .elder-more-sub { + + display: block; + + margin-top: 8rpx; + + font-size: 26rpx; + + color: var(--text-muted); + + line-height: 1.45; + +} + + + +.daily-page .elder-more-arrow { + + font-size: 48rpx; + + color: var(--primary); + + font-weight: 300; + + padding-left: 16rpx; + +} + + + +.daily-page .footer-tip { + + text-align: center; + + font-size: 26rpx; + + color: var(--text-muted); + + padding: 16rpx 32rpx 48rpx; + + line-height: 1.6; + +} + + + +.daily-page .elder-chart-block { + margin: 0 32rpx 24rpx; +} + +.daily-page .elder-chart-block .range-bar { + margin-bottom: 16rpx; +} + +.daily-page .glucose-history-block { + + padding-bottom: 8rpx; + +} + + diff --git a/TUICallKit-Vue3/tongji/utils/tongjiHelpers.js b/TUICallKit-Vue3/tongji/utils/tongjiHelpers.js new file mode 100644 index 00000000..11b63942 --- /dev/null +++ b/TUICallKit-Vue3/tongji/utils/tongjiHelpers.js @@ -0,0 +1,56 @@ +/** 将接口 msg / 异常对象转为可展示的字符串,避免 [object Object] */ +export function formatUserMessage(msg, fallback = '') { + if (msg == null || msg === '') return fallback + if (typeof msg === 'string') return msg + if (typeof msg === 'number' || typeof msg === 'boolean') return String(msg) + if (Array.isArray(msg)) { + const parts = msg.map((item) => { + if (item == null) return '' + if (typeof item === 'string') return item + if (typeof item === 'number' || typeof item === 'boolean') return String(item) + if (typeof item === 'object') { + return item.msg || item.message || item.error || item.title || item.label || '' + } + return String(item) + }).filter(Boolean) + return parts.length ? parts.join(';') : fallback + } + if (typeof msg === 'object') { + return msg.msg || msg.message || msg.error || msg.title || msg.label || fallback + } + return String(msg) +} + +export function showUserToast(title, options = {}) { + const text = formatUserMessage(title, '') + if (!text) return + uni.showToast({ + title: text, + icon: options.icon || 'none', + duration: options.duration + }) +} + +export function formatDate(date) { + const y = date.getFullYear() + const m = String(date.getMonth() + 1).padStart(2, '0') + const d = String(date.getDate()).padStart(2, '0') + return `${y}-${m}-${d}` +} + +export function parseDate(str) { + if (!str) return new Date() + const [y, m, d] = str.split('-').map(Number) + return new Date(y, (m || 1) - 1, d || 1) +} + +export function toNumber(v) { + if (v === null || v === undefined || v === '') return null + const n = Number(v) + return Number.isFinite(n) && n !== 0 ? n : null +} + +export function getWeekday(dateStr) { + const w = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'] + return w[parseDate(dateStr).getDay()] +}