Compare commits

..
22 Commits
Author SHA1 Message Date
Your Name df65344d8f 更新 2026-06-03 14:20:51 +08:00
Your Name ec4e0b9f29 更新 2026-06-03 11:39:36 +08:00
Your Name a3bae1555a Merge branch 'master-6-2' 2026-06-02 18:35:14 +08:00
Your Name 15e9c486f9 更新 2026-06-02 18:33:25 +08:00
Your Name eadc82535d 更新 2026-06-02 16:54:07 +08:00
Your Name 62f74888f3 Merge branch 'master' into master-6-2 2026-06-02 16:49:42 +08:00
Your Name 79719b801e 更新 2026-06-02 16:47:50 +08:00
long 07dacbcb28 Merge branch 'master' of https://gitee.com/v_cms/zyt 2026-06-02 14:20:26 +08:00
long d364fe506a 1 2026-06-02 14:20:20 +08:00
Your Name 860b550180 更新 2026-06-02 14:15:49 +08:00
Your Name 707e4888ad 更新bug 2026-06-02 10:16:03 +08:00
Your Name b0a8bd83bc 更新 2026-06-01 18:39:09 +08:00
Your Name 574d37908a Merge branch 'master' into master-6-1 2026-06-01 18:36:15 +08:00
Your Name 62512d30f9 Merge branch 'master' of https://gitee.com/v_cms/zyt 2026-06-01 18:35:28 +08:00
long 1dd5133202 Merge branch '20260601-long' 2026-06-01 18:30:37 +08:00
long 51b34a98b5 Update list.vue 2026-06-01 18:30:29 +08:00
Your Name 1755185f50 Merge branch 'master' into master-5-28 2026-06-01 18:17:46 +08:00
Your Name b3d3048100 更新 2026-06-01 18:11:05 +08:00
Your Name a38af5bb13 更新 2026-06-01 18:04:23 +08:00
long 450be5852f Merge branch '20260601-long' 2026-06-01 17:02:47 +08:00
long 3c35ab67cd update
h5 :问诊列表
add 完成诊单  +医生备注
2026-06-01 17:00:38 +08:00
Your Name 7cfa4d269a 更新 2026-06-01 09:34:27 +08:00
352 changed files with 13100 additions and 1255 deletions
@@ -0,0 +1,137 @@
<template>
<view class="tab-dock">
<view class="tab-dock__inner">
<view class="tab-dock__item" @click="goHome">
<image
class="tab-dock__icon"
:src="active === 0 ? '/static/user/home_no.png' : '/static/user/home.png'"
mode="aspectFit"
/>
<text class="tab-dock__label" :style="{ color: active === 0 ? activeColor : color }">首页</text>
</view>
<view class="tab-dock__item tab-dock__item--center" @click="goWeekly">
<view class="tab-dock__fab" :class="{ 'tab-dock__fab--active': active === 1 }">
<text class="tab-dock__fab-text"></text>
</view>
<text class="tab-dock__label tab-dock__label--center" :style="{ color: active === 1 ? activeColor : color }">血糖</text>
</view>
<view class="tab-dock__item" @click="goUser">
<image
class="tab-dock__icon"
:src="active === 2 ? '/static/user/user_no.png' : '/static/user/user.png'"
mode="aspectFit"
/>
<text class="tab-dock__label" :style="{ color: active === 2 ? activeColor : color }">我的</text>
</view>
</view>
<view class="tab-dock__safe" />
</view>
</template>
<script setup>
const props = defineProps({
/** 0 首页 1 血糖 2 我的 */
active: {
type: Number,
default: 0
}
})
const color = '#8a8a8a'
const activeColor = '#204e2b'
function goHome() {
if (props.active === 0) return
uni.redirectTo({ url: '/pages/index/index' })
}
function goUser() {
if (props.active === 2) return
uni.redirectTo({ url: '/pages/user/user' })
}
function goWeekly() {
if (props.active === 1) return
uni.redirectTo({ url: '/tongji/pages/weekly' })
}
</script>
<style scoped lang="scss">
.tab-dock {
position: fixed;
left: 0;
right: 0;
bottom: 0;
z-index: 999;
background: #ffffff;
border-top: 1rpx solid rgba(0, 0, 0, 0.06);
box-shadow: 0 -6rpx 20rpx rgba(0, 0, 0, 0.04);
}
.tab-dock__inner {
display: flex;
align-items: flex-end;
height: 100rpx;
}
.tab-dock__item {
flex: 1;
height: 100rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.tab-dock__item--center {
justify-content: flex-end;
padding-bottom: 6rpx;
}
.tab-dock__icon {
width: 46rpx;
height: 46rpx;
}
.tab-dock__label {
margin-top: 6rpx;
font-size: 22rpx;
line-height: 1.2;
}
.tab-dock__label--center {
margin-top: 8rpx;
}
.tab-dock__fab {
width: 96rpx;
height: 96rpx;
margin-top: -48rpx;
border-radius: 50%;
background: linear-gradient(180deg, #2d7340 0%, #204e2b 100%);
border: 6rpx solid #ffffff;
box-shadow: 0 10rpx 24rpx rgba(32, 78, 43, 0.35);
display: flex;
align-items: center;
justify-content: center;
}
.tab-dock__fab--active {
transform: scale(1.04);
box-shadow: 0 12rpx 28rpx rgba(32, 78, 43, 0.42);
}
.tab-dock__fab-text {
color: #ffffff;
font-size: 38rpx;
font-weight: 700;
line-height: 1;
}
.tab-dock__safe {
height: env(safe-area-inset-bottom);
background: #ffffff;
}
</style>
@@ -1,5 +1,68 @@
<template>
<view v-if="visible" class="dev-entry-wrap" :style="{ bottom: props.bottom + 'rpx' }">
<!-- 页面内嵌模块 weekly 统计页主内容区 -->
<view
v-if="visible && props.inline"
class="dev-entry-inline"
:class="{ 'dev-entry-inline--weekly': props.tone === 'weekly' }"
>
<view class="inline-hero">
<view class="inline-hero-icon" aria-hidden="true">
<view class="inline-hero-wave" />
</view>
<view class="inline-hero-text">
<view class="inline-hero-title-row">
<text class="inline-title">居家耗糖训练</text>
<text v-if="props.tone === 'weekly'" class="inline-pill">体验</text>
</view>
<text class="inline-sub">低强度动起来帮助身体消耗糖分</text>
</view>
</view>
<scroll-view
scroll-x
class="inline-scroll"
:show-scrollbar="false"
:enable-flex="true"
>
<view class="inline-scroll-track">
<view
v-for="item in inlineItems"
:key="item.url"
class="inline-card"
@click="goto(item.url)"
>
<view v-if="item.featured" class="inline-card-badge">
<text>推荐</text>
</view>
<view class="menu-icon" :class="item.iconClass">
<view v-if="item.icon === 'dumbbell'" class="dumbbell-shape">
<view class="dumbbell-plate dumbbell-plate-left" />
<view class="dumbbell-bar" />
<view class="dumbbell-plate dumbbell-plate-right" />
</view>
<view v-else-if="item.icon === 'pedal'" class="foot-pedal-shape">
<view class="pedal-bar pedal-bar-left" />
<view class="pedal-bar pedal-bar-right" />
</view>
<view v-else-if="item.icon === 'pilates'" class="pilates-ring-shape" />
<view v-else-if="item.icon === 'grip'" class="grip-ring-shape" />
<view v-else-if="item.icon === 'metro'" class="menu-icon--metro-inner">
<view class="metro-bar metro-bar-1" />
<view class="metro-bar metro-bar-2" />
<view class="metro-bar metro-bar-3" />
</view>
</view>
<text class="inline-card-title">{{ item.title }}</text>
<text class="inline-card-sub">{{ item.sub }}</text>
<text class="inline-card-go" aria-hidden="true">进入</text>
</view>
</view>
</scroll-view>
<text v-if="props.tone === 'weekly'" class="inline-scroll-hint">左右滑动选择适合您的训练</text>
</view>
<view v-else-if="visible" class="dev-entry-wrap" :style="{ bottom: props.bottom + 'rpx' }">
<!-- 展开后的菜单卡片(从下往上动画) -->
<view v-if="expanded" class="menu-list" @click.stop>
<view class="menu-item" @click="goto('/training/pages/dumbbell')">
@@ -84,15 +147,45 @@
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { computed, ref } from 'vue'
type TrainingItem = {
url: string
title: string
sub: string
icon: 'dumbbell' | 'pedal' | 'pilates' | 'grip' | 'metro'
iconClass: string
featured?: boolean
}
const trainingItems: TrainingItem[] = [
{ url: '/training/pages/dumbbell', title: '哑铃', sub: '力量塑形', icon: 'dumbbell', iconClass: 'menu-icon--dumbbell' },
{ url: '/training/pages/foot-pedal', title: '脚蹬器', sub: '下肢有氧', icon: 'pedal', iconClass: 'menu-icon--pedal' },
{ url: '/training/pages/pilates-ring', title: '瑜伽环', sub: '核心塑形', icon: 'pilates', iconClass: 'menu-icon--pilates' },
{ url: '/training/pages/grip-ring', title: '握力环', sub: '握力训练', icon: 'grip', iconClass: 'menu-icon--grip' },
{ url: '/training/pages/metronome', title: '耗糖节拍器', sub: '健走配速', icon: 'metro', iconClass: 'menu-icon--metro', featured: true },
]
const props = withDefaults(
defineProps<{
/** 悬浮按钮距底部 rpx(仅非 inline */
bottom?: number
/** 内嵌到页面主内容区,非 fixed 悬浮 */
inline?: boolean
/** weekly 页:与统计页 Vital Mint 视觉一致 */
tone?: 'default' | 'weekly'
}>(),
{ bottom: 200 },
{ 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 isDevMode = (): boolean => {
/* HBuilderX 工程:发行(release)模式下 NODE_ENV === 'production' */
try {
@@ -449,4 +542,196 @@ $brand-light: #34d399;
line-height: 1;
margin-left: 4rpx;
}
.menu-icon--metro-inner {
display: flex;
align-items: flex-end;
justify-content: center;
gap: 4rpx;
width: 100%;
height: 100%;
}
/* ============================================================
* 内嵌模块(weekly 等)
* ============================================================ */
.dev-entry-inline {
width: 100%;
box-sizing: border-box;
}
.dev-entry-inline--weekly {
padding: 32rpx 28rpx 24rpx;
border-radius: 48rpx;
background: var(--surface-container-lowest, #fff);
border: 1rpx solid rgba(0, 108, 73, 0.12);
box-shadow:
0 24rpx 60rpx -20rpx rgba(0, 108, 73, 0.08),
0 8rpx 20rpx -8rpx rgba(0, 0, 0, 0.03);
}
.inline-hero {
display: flex;
align-items: center;
gap: 20rpx;
margin-bottom: 24rpx;
}
.inline-hero-icon {
width: 88rpx;
height: 88rpx;
border-radius: 24rpx;
background: rgba(0, 108, 73, 0.1);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.inline-hero-wave {
width: 52rpx;
height: 52rpx;
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23006c49' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'><polyline points='2,12 7,12 9.5,7 12,17 14.5,9 16.5,12 22,12'/></svg>");
background-size: contain;
background-repeat: no-repeat;
background-position: center;
}
.inline-hero-text {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 6rpx;
}
.inline-hero-title-row {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 10rpx;
}
.inline-title {
font-size: 32rpx;
font-weight: 700;
color: #0f172a;
line-height: 1.25;
}
.inline-pill {
font-size: 20rpx;
font-weight: 700;
color: #006c49;
padding: 4rpx 12rpx;
border-radius: 999rpx;
background: rgba(0, 108, 73, 0.1);
line-height: 1.2;
}
.inline-sub {
font-size: 26rpx;
font-weight: 500;
color: #475569;
line-height: 1.4;
}
.inline-scroll {
width: 100%;
white-space: nowrap;
}
.inline-scroll-track {
display: inline-flex;
flex-direction: row;
align-items: stretch;
gap: 16rpx;
padding: 4rpx 2rpx 8rpx;
}
.inline-card {
position: relative;
display: inline-flex;
flex-direction: column;
align-items: flex-start;
width: 212rpx;
min-height: 220rpx;
padding: 20rpx 18rpx 18rpx;
border-radius: 28rpx;
background: #f4f7f5;
border: 1rpx solid rgba(0, 108, 73, 0.1);
box-sizing: border-box;
flex-shrink: 0;
transition: transform 0.2s ease, background 0.2s ease;
&:active {
transform: scale(0.98);
background: #e8f3ee;
}
.menu-icon {
width: 72rpx;
height: 72rpx;
margin-bottom: 14rpx;
flex-shrink: 0;
}
}
.inline-card-badge {
position: absolute;
top: 12rpx;
right: 12rpx;
padding: 4rpx 10rpx;
border-radius: 8rpx;
background: #006c49;
text {
font-size: 18rpx;
font-weight: 700;
color: #fff;
line-height: 1.2;
}
}
.inline-card-title {
font-size: 28rpx;
font-weight: 700;
color: #0f172a;
line-height: 1.25;
width: 100%;
}
.inline-card-sub {
margin-top: 6rpx;
font-size: 22rpx;
font-weight: 500;
color: #64748b;
line-height: 1.35;
width: 100%;
}
.inline-card-go {
margin-top: auto;
padding-top: 12rpx;
font-size: 22rpx;
font-weight: 700;
color: #006c49;
line-height: 1.2;
}
.inline-scroll-hint {
display: block;
margin-top: 14rpx;
font-size: 22rpx;
font-weight: 600;
color: #94a3b8;
text-align: center;
line-height: 1.35;
}
@media (prefers-reduced-motion: reduce) {
.inline-card:active {
transform: none;
}
}
</style>
+1 -1
View File
@@ -1,5 +1,5 @@
import App from './App'
var baseUrl ='https://admin.zhenyangtang.com.cn/';
var baseUrl ='https://css.zhenyangtang.com.cn/';
function joinApiUrl(base, path) {
const b = String(base || '').replace(/\/+$/, '')
-69
View File
@@ -4142,9 +4142,6 @@
"cpu": [
"arm64"
],
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -4161,9 +4158,6 @@
"cpu": [
"arm64"
],
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -4180,9 +4174,6 @@
"cpu": [
"x64"
],
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -4199,9 +4190,6 @@
"cpu": [
"x64"
],
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -4433,9 +4421,6 @@
"cpu": [
"arm"
],
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -4456,9 +4441,6 @@
"cpu": [
"arm"
],
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -4479,9 +4461,6 @@
"cpu": [
"arm64"
],
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -4502,9 +4481,6 @@
"cpu": [
"arm64"
],
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -4525,9 +4501,6 @@
"cpu": [
"x64"
],
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -4548,9 +4521,6 @@
"cpu": [
"x64"
],
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -4729,9 +4699,6 @@
"cpu": [
"arm"
],
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -4745,9 +4712,6 @@
"cpu": [
"arm"
],
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -4761,9 +4725,6 @@
"cpu": [
"arm64"
],
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -4777,9 +4738,6 @@
"cpu": [
"arm64"
],
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -4793,9 +4751,6 @@
"cpu": [
"loong64"
],
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -4809,9 +4764,6 @@
"cpu": [
"loong64"
],
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -4825,9 +4777,6 @@
"cpu": [
"ppc64"
],
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -4841,9 +4790,6 @@
"cpu": [
"ppc64"
],
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -4857,9 +4803,6 @@
"cpu": [
"riscv64"
],
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -4873,9 +4816,6 @@
"cpu": [
"riscv64"
],
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -4889,9 +4829,6 @@
"cpu": [
"s390x"
],
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -4905,9 +4842,6 @@
"cpu": [
"x64"
],
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -4921,9 +4855,6 @@
"cpu": [
"x64"
],
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
+24 -29
View File
@@ -3,7 +3,9 @@
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "甄养堂"
"navigationBarTitleText": "甄养堂",
"backgroundColor": "#F8F8F8",
"backgroundColorTop": "#F8F8F8"
}
},
{
@@ -43,7 +45,9 @@
{
"path": "pages/user/user",
"style": {
"navigationBarTitleText": "我的"
"navigationBarTitleText": "我的",
"backgroundColor": "#e8eaed",
"backgroundColorTop": "#e8eaed"
}
},
{
@@ -177,16 +181,6 @@
}
}
},
{
"path": "pages/more",
"style": {
"navigationBarTitleText": "日常护理",
"navigationBarBackgroundColor": "#204e2b",
"navigationBarTextStyle": "white",
"backgroundColor": "#faf9f5",
"enablePullDownRefresh": true
}
},
{
"path": "pages/weekly",
"style": {
@@ -203,27 +197,28 @@
}
}
}
},
{
"path": "pages/more",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "日常护理",
"backgroundColor": "#f4fbf4",
"enablePullDownRefresh": true
}
},
{
"path": "pages/game",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "糖分突袭",
"backgroundColor": "#c7d2fe",
"disableScroll": true
}
}
]
}
],
"tabBar": {
"color": "#8a8a8a",
"selectedColor": "#204e2b",
"borderStyle": "black",
"backgroundColor": "#ffffff",
"list": [{
"pagePath": "pages/index/index",
"iconPath": "/static/user/home.png",
"selectedIconPath": "/static/user/home_no.png",
"text": "首页"
},{
"pagePath": "pages/user/user",
"iconPath": "/static/user/user.png",
"selectedIconPath": "/static/user/user_no.png",
"text": "我的"
}]
},
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "甄养堂互联网医院",
+5 -2
View File
@@ -118,13 +118,15 @@
</view>
</scroll-view>
</view>
<TabBarDock :active="0" />
</view>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { getCurrentInstance } from 'vue'
import { onShow, onShareAppMessage } from '@dcloudio/uni-app'
import { onShareAppMessage } from '@dcloudio/uni-app'
import TabBarDock from '@/components/app-tab-bar/tab-bar-dock.vue'
const { proxy } = getCurrentInstance()
const doctors = ref([])
@@ -263,7 +265,8 @@ $outline: #727970;
.container {
background: $background;
min-height: 100vh;
padding: 24rpx 32rpx 120rpx;
padding: 24rpx 32rpx;
padding-bottom: calc(160rpx + env(safe-area-inset-bottom));
font-family: 'Noto Sans SC', 'PingFang SC', 'Microsoft YaHei', sans-serif;
}
+8 -5
View File
@@ -1,5 +1,5 @@
<template>
<div class="mine-page">
<view class="mine-page">
<!-- 顶部用户信息卡片 -->
<div class="user-card" @click="navigateTo('/pages/user/userinfo')">
<div class="user-header">
@@ -90,16 +90,18 @@
</div>
<!-- DEV 开发悬浮入口生产环境自动隐藏 -->
<dev-training-entry />
</div>
<dev-training-entry :bottom="360" />
<TabBarDock :active="2" />
</view>
</template>
<script>
import { onShow, onShareAppMessage } from '@dcloudio/uni-app'
import DevTrainingEntry from '@/components/dev-training-entry/index.vue'
import TabBarDock from '@/components/app-tab-bar/tab-bar-dock.vue'
export default {
name: 'profileB',
components: { DevTrainingEntry },
components: { DevTrainingEntry, TabBarDock },
data() {
return {
userInfo: {
@@ -170,6 +172,7 @@ export default {
min-height: 100vh;
background: #e8eaed;
padding: 40rpx;
padding-bottom: calc(40rpx + 200rpx + env(safe-area-inset-bottom));
}
.user-card {
@@ -420,4 +423,4 @@ export default {
.bg-cyan { background: #13c2c2; }
.bg-pink { background: #eb2f96; }
.bg-gray { background: #666666; }
</style>
</style>
@@ -0,0 +1,116 @@
<template>
<view class="food-tile-icon" :class="[`food-tile-icon--${size}`]">
<text class="food-tile-icon__emoji">{{ fallbackIcon }}</text>
<image
v-if="src && !imageFailed"
class="food-tile-icon__img"
:src="src"
:mode="size === 'tile' ? 'aspectFill' : 'aspectFit'"
@error="onImageError"
/>
</view>
</template>
<script setup>
import { computed, ref, watch } from 'vue'
import { getFoodImgUrl } from '../data/gameFoodImg.js'
const props = defineProps({
food: { type: [Object, String], default: null },
size: { type: String, default: 'tile' }
})
const imageFailed = ref(false)
const fallbackIcon = computed(() => {
if (typeof props.food === 'object' && props.food?.icon) return props.food.icon
return '🍽'
})
const src = computed(() => {
if (typeof props.food === 'object' && props.food?.img) return props.food.img
if (typeof props.food === 'object' && props.food?.icon) return getFoodImgUrl(props.food.icon)
return ''
})
watch(
() => src.value,
() => {
imageFailed.value = false
}
)
function onImageError() {
imageFailed.value = true
}
</script>
<style lang="scss" scoped>
.food-tile-icon {
position: relative;
box-sizing: border-box;
overflow: hidden;
}
.food-tile-icon--tile {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
font-size: calc(var(--tile-emoji-size, 48px) * 0.6);
}
.food-tile-icon--tile .food-tile-icon__emoji,
.food-tile-icon--tile .food-tile-icon__img {
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 58%;
height: 58%;
z-index: 2;
}
.food-tile-icon--goal {
width: 40rpx;
height: 40rpx;
display: flex;
align-items: center;
justify-content: center;
}
.food-tile-icon--tooltip {
width: 36rpx;
height: 36rpx;
display: flex;
align-items: center;
justify-content: center;
}
.food-tile-icon__emoji {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
font-size: inherit;
line-height: 1;
z-index: 1;
}
.food-tile-icon--goal .food-tile-icon__emoji,
.food-tile-icon--tooltip .food-tile-icon__emoji {
position: static;
transform: none;
font-size: 34rpx;
}
.food-tile-icon__img {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
z-index: 2;
display: block;
}
</style>
@@ -8,12 +8,12 @@
mode="aspectFit"
@error="onImageError"
/>
<text
<!-- <text
v-else
class="tj-icon-fallback"
:class="[`tj-icon-fallback--${size}`]"
:style="{ color }"
>{{ fallbackGlyph }}</text>
>{{ fallbackGlyph }}</text> -->
</view>
</template>
@@ -66,7 +66,12 @@ const ICON_PATHS = {
egg: '<path d="M12 22c4.97 0 8-3.27 8-7.31C20 9.65 16.42 2 12 2S4 9.65 4 14.69C4 18.73 7.03 22 12 22Z"/>',
home: '<path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/>',
'plus-circle': '<circle cx="12" cy="12" r="10"/><path d="M8 12h8M12 8v8"/>',
'chevron-right': '<path d="m9 18 6-6-6-6"/>'
'chevron-right': '<path d="m9 18 6-6-6-6"/>',
'chevron-left': '<path d="m15 18-6-6 6-6"/>',
check: '<path d="M20 6 9 17l-5-5"/>',
settings: '<path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/>',
syringe: '<path d="m18 2 4 4"/><path d="m17 7 3-3"/><path d="M19 9 8.7 19.3c-1 1-2.5 1-3.4 0l-.6-.6c-1-1-1-2.5 0-3.4L15 5"/><path d="m9 11 4 4"/><path d="m5 19-3 3"/><path d="m14 4 6 6"/>',
zap: '<path d="M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z"/>'
}
/** 图片加载失败时的 emoji 回退 */
@@ -106,7 +111,12 @@ const ICON_FALLBACK = {
egg: '🍳',
home: '🏠',
'plus-circle': '⊕',
'chevron-right': ''
'chevron-right': '',
'chevron-left': '',
check: '✓',
settings: '⚙',
syringe: '💉',
zap: '⚡'
}
const strokeColor = computed(() => {
@@ -155,6 +165,10 @@ function onImageError() {
width: 100%;
height: 100%;
}
/* Lucide send 路径重心偏右上,微调使圆形按钮内视觉居中 */
.tj-icon--send {
transform: translate(-10%, 10%);
}
.tj-icon-fallback {
display: block;
line-height: 1;
@@ -5,6 +5,7 @@ const DIET_AI_PREFILL_KEY = 'tongji_diet_ai_prefill'
const emptyDietAiRecommend = () => ({
breakfast: '',
drinks: '',
lunch: '',
dinner: '',
tips: '',
@@ -31,20 +32,20 @@ export function useDietAi(proxy, { diagnosisId, showUserToast, formatUserMessage
const hasDietAiMealContent = computed(() => {
const r = dietAiRecommend.value
return !!(r.breakfast || r.lunch || r.dinner || r.tips)
return !!(r.breakfast || r.drinks || r.lunch || r.dinner || r.tips)
})
function detectDietStreamField(text) {
const tail = String(text || '').match(/(?:^|\n)(早餐|午餐|晚餐|提示|少碰)[:]\s*([^\n]*)$/)
const tail = String(text || '').match(/(?:^|\n)(早餐|喝的|午餐|晚餐|提示|少碰)[:]\s*([^\n]*)$/)
if (!tail) return 'breakfast'
const map = { '早餐': 'breakfast', '午餐': 'lunch', '晚餐': 'dinner', '提示': 'tips', '少碰': 'tips' }
const map = { '早餐': 'breakfast', '喝的': 'drinks', '午餐': 'lunch', '晚餐': 'dinner', '提示': 'tips', '少碰': 'tips' }
return map[tail[1]] || 'breakfast'
}
function mergePartialDietFromStream() {
const partial = parsePartialDietPlainText(dietStreamBuffer)
const next = { ...dietAiRecommend.value }
for (const key of ['breakfast', 'lunch', 'dinner', 'tips']) {
for (const key of ['breakfast', 'drinks', 'lunch', 'dinner', 'tips']) {
if (partial[key] != null) next[key] = partial[key]
}
if (Array.isArray(partial.avoid)) next.avoid = partial.avoid
@@ -80,6 +81,7 @@ export function useDietAi(proxy, { diagnosisId, showUserToast, formatUserMessage
...emptyDietAiRecommend(),
...payload,
breakfast: streamed.breakfast,
drinks: streamed.drinks || payload.drinks || '',
lunch: streamed.lunch || payload.lunch || '',
dinner: streamed.dinner || payload.dinner || '',
tips: streamed.tips || payload.tips || '',
@@ -259,6 +261,7 @@ export function useDietAi(proxy, { diagnosisId, showUserToast, formatUserMessage
try {
uni.setStorageSync(DIET_AI_PREFILL_KEY, {
breakfast: r.breakfast,
drinks: r.drinks || '',
lunch: r.lunch,
dinner: r.dinner,
tips: r.tips || ''
@@ -0,0 +1,188 @@
/**
* 糖分突袭 · 视觉特效状态(飘字、粒子、连消横幅、震屏、闪屏)
* 特效风格参考「开心消消乐」:糖果色星屑、弹性扩散光环、大消除闪屏。
*/
import { ref } from 'vue'
let fxSeq = 0
function nextFxId(prefix) {
fxSeq += 1
return `${prefix}-${fxSeq}-${Date.now()}`
}
// 开心消消乐式糖果色板(彩虹爆裂用)
const CANDY_COLORS = ['#FF5E9C', '#FFC93C', '#5BD16A', '#4D9DFF', '#FF8A3D', '#B57BFF', '#FF6F91', '#2FE6C8']
// 星屑字形
const STAR_GLYPHS = ['✦', '✧', '★', '✨']
export function useGameFx() {
const floatingTexts = ref([])
const burstParticles = ref([])
const ripples = ref([])
const comboBanner = ref({ show: false, text: '', tier: 1 })
const screenShaking = ref(false)
const flash = ref({ active: false, color: '#ffffff', opacity: 0 })
let comboBannerTimer = null
let shakeTimer = null
let flashTimer = null
let flashFadeTimer = null
function spawnFloatingText(payload) {
const id = nextFxId('ft')
floatingTexts.value.push({ id, ...payload })
const duration = payload.danger ? 1500 : payload.isCombo ? 1400 : 1200
setTimeout(() => {
floatingTexts.value = floatingTexts.value.filter((f) => f.id !== id)
}, duration)
return id
}
/**
* 糖果爆裂粒子(星屑 + 圆点混合,上抛后受重力下落,带旋转与缓动)。
* @param {number} cx,cy 爆裂中心(屏幕坐标)
* @param {string} color 基础颜色
* @param {number} count 粒子数量
* @param {object} opts { star, rainbow, gravity, sizeMin, sizeMax, distMin, distMax, life }
*/
function spawnBurst(cx, cy, color, count = 8, opts = {}) {
const {
star = true,
rainbow = false,
gravity = 64,
sizeMin = 8,
sizeMax = 16,
distMin = 36,
distMax = 110,
life = 850
} = opts
const dur = (life / 1000).toFixed(2)
for (let i = 0; i < count; i++) {
const id = nextFxId('p')
const isStar = star && Math.random() < 0.65
const pSize = Math.random() * (sizeMax - sizeMin) + sizeMin
const angle = Math.random() * Math.PI * 2
const distance = Math.random() * (distMax - distMin) + distMin
const tx = Math.cos(angle) * distance
// 先上抛、再叠加重力下落,形成喷泉抛物线
const ty = Math.sin(angle) * distance - distance * 0.35 + gravity
const half = pSize / 2
const pColor = rainbow
? CANDY_COLORS[Math.floor(Math.random() * CANDY_COLORS.length)]
: color
const rot = Math.random() * 720 - 360
const glyph = isStar ? STAR_GLYPHS[Math.floor(Math.random() * STAR_GLYPHS.length)] : ''
const particle = {
id,
left: cx - half,
top: cy - half,
size: pSize,
color: pColor,
char: glyph,
opacity: 1,
transform: 'scale(0.4) rotate(0deg)',
transition: 'none'
}
burstParticles.value.push(particle)
setTimeout(() => {
particle.transition =
`left ${dur}s cubic-bezier(0.16,0.7,0.3,1), top ${dur}s cubic-bezier(0.3,0.1,0.4,1), opacity ${dur}s ease-in, transform ${dur}s ease-out`
particle.left = cx - half + tx
particle.top = cy - half + ty
particle.opacity = 0
particle.transform = `scale(${isStar ? 1.15 : 0.5}) rotate(${rot}deg)`
}, 16)
setTimeout(() => {
burstParticles.value = burstParticles.value.filter((p) => p.id !== id)
}, life + 80)
}
}
/**
* 弹性扩散发光光环(开心消消乐式的扩散光圈)。
* @param {object} opts { thickness, life }
*/
function spawnRipple(cx, cy, color, tier = 1, opts = {}) {
const id = nextFxId('rp')
const size = 36 + tier * 18
const { thickness = Math.max(3, tier + 2), life = 600 } = opts
ripples.value.push({
id,
left: cx,
top: cy,
size,
color,
thickness,
opacity: 0.95
})
setTimeout(() => {
ripples.value = ripples.value.filter((r) => r.id !== id)
}, life)
}
/**
* 全屏闪光(大消除瞬间的爆闪),先点亮再淡出。
*/
function triggerFlash(color = '#ffffff', intensity = 0.5, life = 280) {
if (flashFadeTimer) clearTimeout(flashFadeTimer)
if (flashTimer) clearTimeout(flashTimer)
flash.value = { active: true, color, opacity: intensity }
flashFadeTimer = setTimeout(() => {
flash.value = { ...flash.value, opacity: 0 }
}, 24)
flashTimer = setTimeout(() => {
flash.value = { active: false, color, opacity: 0 }
}, life)
}
function showComboBanner(chain, points) {
if (chain <= 1) return
const tier = chain >= 4 ? 3 : chain >= 3 ? 2 : 1
comboBanner.value = {
show: true,
text: `连消 ×${chain} +${points}`,
tier
}
if (comboBannerTimer) clearTimeout(comboBannerTimer)
comboBannerTimer = setTimeout(() => {
comboBanner.value = { ...comboBanner.value, show: false }
}, 1100)
}
function triggerShake(intensity = 1) {
screenShaking.value = true
if (shakeTimer) clearTimeout(shakeTimer)
shakeTimer = setTimeout(() => {
screenShaking.value = false
}, intensity >= 2 ? 420 : 280)
}
function clearAll() {
floatingTexts.value = []
burstParticles.value = []
ripples.value = []
comboBanner.value = { show: false, text: '', tier: 1 }
screenShaking.value = false
flash.value = { active: false, color: '#ffffff', opacity: 0 }
}
return {
floatingTexts,
burstParticles,
ripples,
comboBanner,
screenShaking,
flash,
spawnFloatingText,
spawnBurst,
spawnRipple,
showComboBanner,
triggerShake,
triggerFlash,
clearAll
}
}
@@ -0,0 +1,112 @@
/**
* 游戏进度持久化:关卡、星级、连胜、每日打卡
*/
import { ref } from 'vue'
import { calcLevelStars } from '../data/gameLevels.js'
const STORAGE_KEY = 'tongji_game_progress_v1'
const DEFAULT = {
currentLevel: 1,
maxUnlocked: 1,
stars: {},
winStreak: 0,
dailyStreak: 0,
lastPlayDate: '',
totalWins: 0,
bestCombo: 0,
totalStars: 0
}
function todayStr() {
const d = new Date()
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
}
function sumStars(starsMap) {
return Object.values(starsMap || {}).reduce((s, n) => s + (Number(n) || 0), 0)
}
function loadRaw() {
try {
const raw = uni.getStorageSync(STORAGE_KEY)
if (raw && typeof raw === 'object') return { ...DEFAULT, ...raw }
} catch (_) {}
return { ...DEFAULT }
}
function saveRaw(data) {
try {
uni.setStorageSync(STORAGE_KEY, data)
} catch (_) {}
}
export function useGameProgress() {
const progress = ref(loadRaw())
function persist() {
progress.value.totalStars = sumStars(progress.value.stars)
saveRaw(progress.value)
}
/** 进入游戏时更新每日打卡 */
function touchDailyPlay() {
const today = todayStr()
const last = progress.value.lastPlayDate
if (last === today) return progress.value.dailyStreak
const yesterday = new Date()
yesterday.setDate(yesterday.getDate() - 1)
const yStr = `${yesterday.getFullYear()}-${String(yesterday.getMonth() + 1).padStart(2, '0')}-${String(yesterday.getDate()).padStart(2, '0')}`
if (last === yStr) {
progress.value.dailyStreak = (progress.value.dailyStreak || 0) + 1
} else {
progress.value.dailyStreak = 1
}
progress.value.lastPlayDate = today
persist()
return progress.value.dailyStreak
}
function getStars(levelId) {
return progress.value.stars[String(levelId)] || 0
}
function recordWin({ levelId, movesLeft, glucoseLevel, score, levelConfig, maxCombo }) {
const stars = calcLevelStars({ movesLeft, glucoseLevel, score, levelConfig })
const key = String(levelId)
const prev = progress.value.stars[key] || 0
if (stars > prev) progress.value.stars[key] = stars
progress.value.winStreak = (progress.value.winStreak || 0) + 1
progress.value.totalWins = (progress.value.totalWins || 0) + 1
if (maxCombo > (progress.value.bestCombo || 0)) progress.value.bestCombo = maxCombo
const next = levelId + 1
if (next > progress.value.maxUnlocked) progress.value.maxUnlocked = next
progress.value.currentLevel = next
persist()
return { stars, newBest: stars > prev }
}
function recordLoss() {
progress.value.winStreak = 0
persist()
}
function setCurrentLevel(levelId) {
progress.value.currentLevel = Math.max(1, Math.min(progress.value.maxUnlocked, levelId))
persist()
}
return {
progress,
touchDailyPlay,
getStars,
recordWin,
recordLoss,
setCurrentLevel,
persist
}
}
@@ -0,0 +1,303 @@
/**
* 糖分突袭 · 游戏音效(InnerAudioContext 池化,H5/小程序通用)
* 每种操作独立音轨 + playbackRate 变调,避免“全是同一个声”
*/
import { ref, onUnmounted } from 'vue'
const STORAGE_KEY = 'tongji_game_sfx_enabled'
const COS = 'https://gz-1349751149.cos.ap-guangzhou.myqcloud.com/uploads/file'
/** 9 条基础素材(已上传 COS,小程序域名白名单内) */
const SRC = {
tap: `${COS}/20260526/20260526105128557ef8669.mp3`,
swoosh: `${COS}/20260527/202605271539371ebe13672.mp3`,
pop: `${COS}/20260526/202605261051282a0a94508.mp3`,
buzz: `${COS}/20260527/202605271539376ff628472.mp3`,
chime: `${COS}/20260528/202605280937077bad76716.mp3`,
fanfare: `${COS}/20260528/20260528093707b021f5794.mp3`,
blast: `${COS}/20260528/20260528093707ebd4d5733.mp3`,
sparkle: `${COS}/20260528/20260528093707016f07427.mp3`,
stinger: `${COS}/20260528/20260528093709c669f4659.mp3`
}
/**
* 音效表:src / volume / rate(playbackRate) / pool
* rate 不同可在同一素材上听出明显差异
*/
const SFX = {
// —— 交互 ——
select: { src: SRC.tap, volume: 0.32, rate: 1.05, pool: 3 },
deselect: { src: SRC.tap, volume: 0.22, rate: 0.82, pool: 2 },
hint: { src: SRC.tap, volume: 0.26, rate: 1.28, pool: 2 },
swap: { src: SRC.swoosh, volume: 0.38, rate: 1.0, pool: 3 },
swapFail: { src: SRC.buzz, volume: 0.36, rate: 1.18, pool: 2 },
drop: { src: SRC.pop, volume: 0.3, rate: 0.88, pool: 4 },
dropLight: { src: SRC.pop, volume: 0.22, rate: 1.22, pool: 2 },
// —— 消除(按连数)——
match3: { src: SRC.chime, volume: 0.48, rate: 1.0, pool: 4 },
match4: { src: SRC.fanfare, volume: 0.54, rate: 1.06, pool: 3 },
match5: { src: SRC.blast, volume: 0.62, rate: 1.0, pool: 3 },
// —— 消除(按 GI 主色)——
matchLowGi: { src: SRC.chime, volume: 0.42, rate: 0.86, pool: 2 },
matchMidGi: { src: SRC.chime, volume: 0.46, rate: 1.02, pool: 2 },
matchHighGi: { src: SRC.buzz, volume: 0.44, rate: 0.92, pool: 2 },
// —— 连消 ——
combo2: { src: SRC.fanfare, volume: 0.56, rate: 1.12, pool: 2 },
combo3: { src: SRC.blast, volume: 0.64, rate: 1.08, pool: 2 },
comboMega: { src: SRC.blast, volume: 0.72, rate: 1.22, pool: 2 },
// —— 道具 ——
insulin: { src: SRC.sparkle, volume: 0.5, rate: 1.15, pool: 2 },
fiber: { src: SRC.swoosh, volume: 0.48, rate: 1.32, pool: 2 },
meal: { src: SRC.stinger, volume: 0.42, rate: 1.35, pool: 1 },
reshuffle: { src: SRC.swoosh, volume: 0.46, rate: 0.72, pool: 2 },
// —— 反馈 ——
score: { src: SRC.tap, volume: 0.28, rate: 1.38, pool: 2 },
goalTick: { src: SRC.chime, volume: 0.36, rate: 1.45, pool: 2 },
meterWarn: { src: SRC.buzz, volume: 0.5, rate: 1.0, pool: 2 },
meterDanger: { src: SRC.buzz, volume: 0.58, rate: 0.78, pool: 2 },
meterStable: { src: SRC.pop, volume: 0.24, rate: 1.05, pool: 1 },
win: { src: SRC.fanfare, volume: 0.66, rate: 1.0, pool: 1 },
winStinger: { src: SRC.stinger, volume: 0.38, rate: 1.5, pool: 1 },
lose: { src: SRC.buzz, volume: 0.52, rate: 0.68, pool: 1 },
// 兼容旧名
match: { src: SRC.chime, volume: 0.48, rate: 1.0, pool: 4 },
combo: { src: SRC.fanfare, volume: 0.56, rate: 1.1, pool: 3 },
heal: { src: SRC.pop, volume: 0.44, rate: 1.18, pool: 2 },
boost: { src: SRC.sparkle, volume: 0.52, rate: 1.0, pool: 2 },
invalid: { src: SRC.buzz, volume: 0.4, rate: 1.15, pool: 2 },
danger: { src: SRC.blast, volume: 0.58, rate: 0.95, pool: 2 }
}
class SfxPool {
constructor(src, size, volume, rate = 1) {
this.src = src
this.size = size
this.volume = volume
this.rate = rate
this.list = []
this.cursor = 0
this.warmedUp = false
}
create() {
for (let i = 0; i < this.size; i++) {
const ctx = uni.createInnerAudioContext()
ctx.src = this.src
ctx.obeyMuteSwitch = false
ctx.autoplay = false
ctx.volume = this.volume
try {
ctx.playbackRate = this.rate
} catch (_) {}
this.list.push(ctx)
}
}
warmUp() {
if (this.warmedUp) return
if (!this.list.length) this.create()
this.list.forEach((ctx) => {
try {
ctx.volume = 0
ctx.play()
setTimeout(() => {
try {
ctx.stop()
ctx.volume = this.volume
} catch (_) {}
}, 60)
} catch (_) {}
})
this.warmedUp = true
}
play(scale = 1, rateMul = 1) {
if (!this.list.length) this.create()
const ctx = this.list[this.cursor % this.list.length]
this.cursor += 1
const vol = Math.min(1, this.volume * scale)
const rate = Math.min(2, Math.max(0.5, this.rate * rateMul))
try {
ctx.volume = vol
try {
ctx.playbackRate = rate
} catch (_) {}
ctx.stop()
ctx.seek(0)
ctx.play()
} catch (_) {
try {
ctx.play()
} catch (e) {}
}
}
destroy() {
this.list.forEach((ctx) => {
try {
ctx.stop()
ctx.destroy()
} catch (_) {}
})
this.list = []
this.warmedUp = false
}
}
function dominantGi(tiles = []) {
const cnt = { low: 0, mid: 0, high: 0 }
tiles.forEach((t) => {
const g = t?.type?.gi
if (g && cnt[g] !== undefined) cnt[g] += 1
})
let best = 'mid'
let max = 0
Object.entries(cnt).forEach(([k, v]) => {
if (v > max) {
max = v
best = k
}
})
return best
}
export function useGameSfx() {
const enabled = ref(true)
const pools = {}
try {
const stored = uni.getStorageSync(STORAGE_KEY)
if (stored === false || stored === '0' || stored === 0) {
enabled.value = false
}
} catch (_) {}
function ensureAudioOption() {
// #ifdef MP-WEIXIN
try {
uni.setInnerAudioOption({
obeyMuteSwitch: false,
mixWithOther: true
})
} catch (_) {}
// #endif
}
function getPool(name) {
const cfg = SFX[name]
if (!cfg) return null
if (!pools[name]) {
pools[name] = new SfxPool(cfg.src, cfg.pool, cfg.volume, cfg.rate ?? 1)
}
return pools[name]
}
function warmUp() {
if (!enabled.value) return
ensureAudioOption()
Object.keys(SFX).forEach((name) => {
getPool(name)?.warmUp()
})
}
function play(name, scale = 1, rateMul = 1) {
if (!enabled.value) return
getPool(name)?.play(scale, rateMul)
}
function playLayer(primary, secondary, delayMs = 70, secScale = 0.6) {
play(primary)
if (secondary) {
setTimeout(() => play(secondary, secScale), delayMs)
}
}
function playMatch({ matchCount = 3, chain = 0, tiles = [] } = {}) {
if (!enabled.value) return
const giTrack = (() => {
const gi = dominantGi(tiles)
if (gi === 'low') return 'matchLowGi'
if (gi === 'high') return 'matchHighGi'
return 'matchMidGi'
})()
if (chain >= 3) {
playLayer('comboMega', giTrack, 90, 0.55)
return
}
if (chain === 2) {
playLayer('combo3', giTrack, 80, 0.5)
return
}
if (chain === 1) {
playLayer('combo2', giTrack, 70, 0.45)
return
}
if (matchCount >= 5) {
playLayer('match5', 'matchHighGi', 85, 0.5)
} else if (matchCount >= 4) {
playLayer('match4', giTrack, 65, 0.48)
} else {
play(giTrack, 1, 1)
setTimeout(() => play('match3', 0.85), 40)
}
}
function playDrop(count = 1) {
if (!enabled.value || count <= 0) return
const name = count >= 4 ? 'drop' : 'dropLight'
play(name, Math.min(1.2, 0.85 + count * 0.04), count >= 6 ? 0.9 : 1)
}
function playMeter(delta, level) {
if (!enabled.value) return
if (level > 70) play('meterDanger', 1 + (level - 70) * 0.02)
else if (level < 30) play('meterWarn', 0.9, 0.95)
else if (delta > 8) play('meterWarn', 0.75)
else if (delta < -5) play('meterStable', 1.1)
}
function toggleEnabled() {
enabled.value = !enabled.value
try {
uni.setStorageSync(STORAGE_KEY, enabled.value ? '1' : '0')
} catch (_) {}
if (enabled.value) {
warmUp()
play('select')
}
}
function destroyAll() {
Object.keys(pools).forEach((key) => {
pools[key]?.destroy()
delete pools[key]
})
}
onUnmounted(() => {
destroyAll()
})
return {
enabled,
play,
playLayer,
playMatch,
playDrop,
playMeter,
warmUp,
toggleEnabled,
destroyAll
}
}
@@ -0,0 +1,260 @@
import { ref, onUnmounted } from 'vue'
const MIN_HOLD_MS = 280
/**
* 语音转文字(长按说话):微信小程序 WechatSIH5 Web Speech API。
*
* @param {{ onResult?: (text: string) => void, showToast?: (msg: string) => void }} options
*/
export function useSpeechToText({ onResult, showToast } = {}) {
const sttListening = ref(false)
const sttHolding = ref(false)
const sttSupported = ref(false)
let wxRecordManager = null
let h5Recognition = null
let holdStartTs = 0
let holdSessionId = 0
let startRequested = false
let recordAuthorized = null
function notify(msg) {
if (!msg) return
if (typeof showToast === 'function') {
showToast(msg)
return
}
uni.showToast({ title: msg, icon: 'none', duration: 2000 })
}
function emitResult(text) {
const t = String(text || '').trim()
const heldMs = Date.now() - holdStartTs
if (!t) {
if (heldMs < MIN_HOLD_MS) return
notify('没听清,请再试一次')
return
}
if (typeof onResult === 'function') {
onResult(t)
}
}
function stopListening() {
// #ifdef MP-WEIXIN
if (wxRecordManager && (sttListening.value || startRequested)) {
try {
wxRecordManager.stop()
} catch (e) {}
}
// #endif
// #ifdef H5
if (h5Recognition && sttListening.value) {
try {
h5Recognition.stop()
} catch (e) {}
}
// #endif
startRequested = false
sttListening.value = false
}
function ensureRecordAuth(onGranted) {
// #ifdef MP-WEIXIN
if (recordAuthorized === true) {
onGranted()
return
}
uni.authorize({
scope: 'scope.record',
success() {
recordAuthorized = true
onGranted()
},
fail() {
recordAuthorized = false
sttHolding.value = false
uni.showModal({
title: '需要麦克风权限',
content: '请在设置中允许使用麦克风,以便长按说话',
confirmText: '去设置',
success(res) {
if (res.confirm) {
uni.openSetting({})
}
}
})
}
})
// #endif
// #ifndef MP-WEIXIN
onGranted()
// #endif
}
function startWechatRecord(session) {
// #ifdef MP-WEIXIN
if (!wxRecordManager || session !== holdSessionId || !sttHolding.value) {
return
}
try {
startRequested = true
wxRecordManager.start({
duration: 60000,
lang: 'zh_CN'
})
} catch (e) {
startRequested = false
sttHolding.value = false
notify('无法开始录音')
}
// #endif
}
function startH5Record() {
// #ifdef H5
if (!h5Recognition) {
notify('当前浏览器不支持语音输入')
sttHolding.value = false
return
}
try {
startRequested = true
h5Recognition.start()
sttListening.value = true
} catch (e) {
startRequested = false
sttHolding.value = false
notify('无法开始语音识别,请检查麦克风权限')
}
// #endif
}
/** 长按开始:手指按下 */
function beginHoldSpeech() {
if (!sttSupported.value) {
notify('当前环境不支持语音输入')
return
}
if (sttHolding.value) return
const session = ++holdSessionId
holdStartTs = Date.now()
sttHolding.value = true
// #ifdef MP-WEIXIN
ensureRecordAuth(() => startWechatRecord(session))
// #endif
// #ifdef H5
startH5Record()
// #endif
// #ifndef MP-WEIXIN
// #ifndef H5
sttHolding.value = false
notify('当前端暂不支持语音输入')
// #endif
// #endif
}
/** 松手结束:手指抬起 */
function endHoldSpeech() {
if (!sttHolding.value && !sttListening.value && !startRequested) {
return
}
sttHolding.value = false
if (sttListening.value || startRequested) {
stopListening()
return
}
holdSessionId += 1
}
// #ifdef MP-WEIXIN
try {
// eslint-disable-next-line no-undef
const plugin = requirePlugin('WechatSI')
wxRecordManager = plugin.getRecordRecognitionManager()
sttSupported.value = !!wxRecordManager
wxRecordManager.onStart = () => {
if (!sttHolding.value) {
try {
wxRecordManager.stop()
} catch (e) {}
return
}
startRequested = false
sttListening.value = true
}
wxRecordManager.onStop = (res) => {
sttListening.value = false
startRequested = false
emitResult(res?.result)
}
wxRecordManager.onError = (res) => {
sttListening.value = false
startRequested = false
sttHolding.value = false
notify(res?.msg || '语音识别失败')
}
} catch (e) {
console.warn('WechatSI record recognition not available', e)
}
// #endif
// #ifdef H5
try {
if (typeof window !== 'undefined') {
const SR = window.SpeechRecognition || window.webkitSpeechRecognition
if (SR) {
h5Recognition = new SR()
h5Recognition.lang = 'zh-CN'
h5Recognition.interimResults = false
h5Recognition.continuous = false
h5Recognition.maxAlternatives = 1
sttSupported.value = true
h5Recognition.onresult = (event) => {
const item = event.results?.[0]?.[0]
emitResult(item?.transcript || '')
}
h5Recognition.onend = () => {
sttListening.value = false
startRequested = false
sttHolding.value = false
}
h5Recognition.onerror = (event) => {
sttListening.value = false
startRequested = false
sttHolding.value = false
if (event?.error === 'not-allowed') {
notify('请允许浏览器使用麦克风')
} else if (event?.error !== 'aborted') {
notify('语音识别失败')
}
}
}
}
} catch (e) {
console.warn('H5 SpeechRecognition not available', e)
}
// #endif
onUnmounted(() => {
sttHolding.value = false
holdSessionId += 1
stopListening()
})
return {
sttListening,
sttHolding,
sttSupported,
beginHoldSpeech,
endHoldSpeech,
stopSpeechToText: endHoldSpeech
}
}
+654
View File
@@ -0,0 +1,654 @@
/**
* 棋盘食材 · 仿真插画 SVG(渐变 + 高光 + 阴影,开心消消乐式 2.5D)
*/
import { svgToDataUrl } from '../utils/svgDataUrl.js'
function gid(key, suffix) {
return `f-${String(key).replace(/[^a-z0-9]/gi, '')}-${suffix}`
}
function wrap(key, body) {
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">${body}</svg>`
}
function shadow(key) {
const id = gid(key, 'sh')
return `<filter id="${id}"><feDropShadow dx="0" dy="2.5" stdDeviation="2.2" flood-color="#1a1a2e" flood-opacity="0.28"/></filter>`
}
function shine(x, y, rx, ry) {
return `<ellipse cx="${x}" cy="${y}" rx="${rx}" ry="${ry}" fill="#fff" opacity="0.38"/>`
}
/** 圆形水果 */
function artRound(key, base, dark, light, extra = '') {
const g = gid(key, 'g')
return wrap(key, `
<defs>
<radialGradient id="${g}" cx="36%" cy="30%" r="68%">
<stop offset="0%" stop-color="${light}"/>
<stop offset="55%" stop-color="${base}"/>
<stop offset="100%" stop-color="${dark}"/>
</radialGradient>
${shadow(key)}
</defs>
<g filter="url(#${gid(key, 'sh')})">
<ellipse cx="32" cy="36" rx="21" ry="23" fill="url(#${g})"/>
${shine(24, 28, 7, 5)}
<path d="M32 13 Q35 9 37 15" stroke="#5d4037" stroke-width="2" fill="none" stroke-linecap="round"/>
<ellipse cx="38" cy="15" rx="5.5" ry="3" fill="#66bb6a" transform="rotate(28 38 15)"/>
${extra}
</g>`)
}
/** 叶菜 */
function artLeafy(key, light, mid, dark) {
const g1 = gid(key, 'g1')
const g2 = gid(key, 'g2')
return wrap(key, `
<defs>
<linearGradient id="${g1}" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="${light}"/><stop offset="100%" stop-color="${mid}"/>
</linearGradient>
<linearGradient id="${g2}" x1="0%" y1="100%" x2="100%" y2="0%">
<stop offset="0%" stop-color="${mid}"/><stop offset="100%" stop-color="${dark}"/>
</linearGradient>
${shadow(key)}
</defs>
<g filter="url(#${gid(key, 'sh')})">
<path d="M32 8 C18 18 14 34 20 48 C24 54 32 56 32 56 C32 56 40 54 44 48 C50 34 46 18 32 8Z" fill="url(#${g1})"/>
<path d="M32 14 C26 22 24 32 28 42 C30 46 32 48 32 48 C32 48 34 46 36 42 C40 32 38 22 32 14Z" fill="url(#${g2})" opacity="0.85"/>
${shine(26, 24, 6, 8)}
</g>`)
}
/** 西兰花 */
function artBroccoli(key) {
const g = gid(key, 'g')
return wrap(key, `
<defs>
<radialGradient id="${g}" cx="50%" cy="40%" r="55%">
<stop offset="0%" stop-color="#7cb342"/><stop offset="100%" stop-color="#33691e"/>
</radialGradient>
${shadow(key)}
</defs>
<g filter="url(#${gid(key, 'sh')})">
<rect x="28" y="38" width="8" height="16" rx="3" fill="#558b2f"/>
<circle cx="22" cy="28" r="9" fill="url(#${g})"/><circle cx="32" cy="22" r="10" fill="url(#${g})"/>
<circle cx="42" cy="28" r="9" fill="url(#${g})"/><circle cx="32" cy="32" r="8" fill="#689f38"/>
${shine(28, 20, 5, 4)}
</g>`)
}
/** 长条(黄瓜/香蕉/玉米) */
function artLong(key, base, dark, light, type = 'cucumber') {
const g = gid(key, 'g')
if (type === 'banana') {
return wrap(key, `
<defs><linearGradient id="${g}" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="${light}"/><stop offset="100%" stop-color="${dark}"/>
</linearGradient>${shadow(key)}</defs>
<g filter="url(#${gid(key, 'sh')})">
<path d="M18 46 Q14 30 28 14 Q38 8 46 16 Q52 24 44 38 Q36 52 22 50 Q16 48 18 46Z" fill="url(#${g})"/>
<path d="M24 42 Q20 32 30 20" stroke="${dark}" stroke-width="1.2" fill="none" opacity="0.5"/>
${shine(30, 26, 5, 3)}
</g>`)
}
if (type === 'corn') {
return wrap(key, `
<defs><linearGradient id="${g}" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="${light}"/><stop offset="100%" stop-color="${base}"/>
</linearGradient>${shadow(key)}</defs>
<g filter="url(#${gid(key, 'sh')})">
<ellipse cx="32" cy="34" rx="14" ry="22" fill="url(#${g})"/>
<g fill="${dark}" opacity="0.55">${Array.from({ length: 5 }, (_, r) =>
Array.from({ length: 4 }, (_, c) =>
`<circle cx="${22 + c * 5}" cy="${20 + r * 5}" r="1.2"/>`
).join('')
).join('')}</g>
<path d="M32 10 L34 18 M32 10 L30 18" stroke="#558b2f" stroke-width="2" stroke-linecap="round"/>
${shine(26, 26, 5, 8)}
</g>`)
}
return wrap(key, `
<defs><linearGradient id="${g}" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="${light}"/><stop offset="100%" stop-color="${dark}"/>
</linearGradient>${shadow(key)}</defs>
<g filter="url(#${gid(key, 'sh')})">
<ellipse cx="32" cy="32" rx="10" ry="24" fill="url(#${g})" transform="rotate(-12 32 32)"/>
<ellipse cx="28" cy="22" rx="3" ry="5" fill="#fff" opacity="0.25" transform="rotate(-12 28 22)"/>
</g>`)
}
/** 茄子 */
function artEggplant(key) {
const g = gid(key, 'g')
return wrap(key, `
<defs><linearGradient id="${g}" x1="20%" y1="0%" x2="80%" y2="100%">
<stop offset="0%" stop-color="#9575cd"/><stop offset="100%" stop-color="#4527a0"/>
</linearGradient>${shadow(key)}</defs>
<g filter="url(#${gid(key, 'sh')})">
<ellipse cx="32" cy="38" rx="13" ry="20" fill="url(#${g})"/>
<path d="M32 18 Q34 12 32 8 Q30 12 32 18" fill="#558b2f"/>
${shine(26, 32, 5, 7)}
</g>`)
}
/** 番茄 */
function artTomato(key) {
return artRound(key, '#e53935', '#b71c1c', '#ef5350',
'<path d="M24 16 Q32 12 40 16" stroke="#2e7d32" stroke-width="2.5" fill="none" stroke-linecap="round"/>')
}
/** 甜椒 */
function artPepper(key, color, dark) {
const g = gid(key, 'g')
return wrap(key, `
<defs><linearGradient id="${g}" x1="30%" y1="0%" x2="70%" y2="100%">
<stop offset="0%" stop-color="${color}"/><stop offset="100%" stop-color="${dark}"/>
</linearGradient>${shadow(key)}</defs>
<g filter="url(#${gid(key, 'sh')})">
<path d="M32 10 Q26 14 24 24 Q22 38 28 48 Q32 54 36 48 Q42 38 40 24 Q38 14 32 10Z" fill="url(#${g})"/>
<path d="M30 12 Q32 8 34 12" stroke="#33691e" stroke-width="2" fill="none"/>
${shine(28, 26, 5, 6)}
</g>`)
}
/** 洋葱 */
function artOnion(key) {
const g = gid(key, 'g')
return wrap(key, `
<defs><radialGradient id="${g}" cx="45%" cy="35%" r="60%">
<stop offset="0%" stop-color="#ffe0b2"/><stop offset="100%" stop-color="#bc8f5a"/>
</radialGradient>${shadow(key)}</defs>
<g filter="url(#${gid(key, 'sh')})">
<ellipse cx="32" cy="36" rx="18" ry="20" fill="url(#${g})"/>
<path d="M32 16 Q28 22 32 28 Q36 22 32 16" fill="#d7ccc8"/>
${shine(24, 30, 6, 5)}
</g>`)
}
/** 葡萄串 */
function artGrapes(key) {
const g = gid(key, 'g')
return wrap(key, `
<defs><radialGradient id="${g}" cx="40%" cy="30%" r="65%">
<stop offset="0%" stop-color="#ba68c8"/><stop offset="100%" stop-color="#6a1b9a"/>
</radialGradient>${shadow(key)}</defs>
<g filter="url(#${gid(key, 'sh')})">
<path d="M32 10 L32 18" stroke="#558b2f" stroke-width="2"/>
<ellipse cx="32" cy="22" rx="4" ry="4.5" fill="url(#${g})"/>
<ellipse cx="26" cy="28" rx="4" ry="4.5" fill="url(#${g})"/><ellipse cx="38" cy="28" rx="4" ry="4.5" fill="url(#${g})"/>
<ellipse cx="22" cy="34" rx="4" ry="4.5" fill="url(#${g})"/><ellipse cx="32" cy="34" rx="4" ry="4.5" fill="url(#${g})"/><ellipse cx="42" cy="34" rx="4" ry="4.5" fill="url(#${g})"/>
<ellipse cx="28" cy="40" rx="4" ry="4.5" fill="url(#${g})"/><ellipse cx="36" cy="40" rx="4" ry="4.5" fill="url(#${g})"/>
${shine(28, 24, 3, 2)}
</g>`)
}
/** 樱桃 */
function artCherry(key) {
const g = gid(key, 'g')
return wrap(key, `
<defs><radialGradient id="${g}" cx="35%" cy="30%" r="70%">
<stop offset="0%" stop-color="#f06292"/><stop offset="100%" stop-color="#880e4f"/>
</radialGradient>${shadow(key)}</defs>
<g filter="url(#${gid(key, 'sh')})">
<path d="M32 12 Q24 18 22 26 M32 12 Q40 18 42 26" stroke="#558b2f" stroke-width="2" fill="none"/>
<circle cx="22" cy="32" r="9" fill="url(#${g})"/><circle cx="42" cy="32" r="9" fill="url(#${g})"/>
${shine(19, 29, 3, 2)}${shine(39, 29, 3, 2)}
</g>`)
}
/** 草莓 */
function artStrawberry(key) {
const g = gid(key, 'g')
return wrap(key, `
<defs><linearGradient id="${g}" x1="50%" y1="0%" x2="50%" y2="100%">
<stop offset="0%" stop-color="#ef5350"/><stop offset="100%" stop-color="#c62828"/>
</linearGradient>${shadow(key)}</defs>
<g filter="url(#${gid(key, 'sh')})">
<path d="M32 14 Q18 28 22 46 Q26 54 32 56 Q38 54 42 46 Q46 28 32 14Z" fill="url(#${g})"/>
<path d="M22 16 Q32 10 42 16 Q32 20 22 16Z" fill="#43a047"/>
${shine(26, 28, 4, 6)}
<g fill="#ffeb3b" opacity="0.85">${[28, 34, 38, 30, 36].map((x, i) =>
`<circle cx="${x}" cy="${32 + (i % 3) * 5}" r="1.1"/>`
).join('')}</g>
</g>`)
}
/** 豆类 */
function artBeans(key) {
const g = gid(key, 'g')
return wrap(key, `
<defs><linearGradient id="${g}" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#d7ccc8"/><stop offset="100%" stop-color="#8d6e63"/>
</linearGradient>${shadow(key)}</defs>
<g filter="url(#${gid(key, 'sh')})">
<ellipse cx="24" cy="32" rx="9" ry="12" fill="url(#${g})" transform="rotate(-20 24 32)"/>
<ellipse cx="36" cy="30" rx="9" ry="12" fill="url(#${g})" transform="rotate(15 36 30)"/>
<ellipse cx="32" cy="42" rx="9" ry="12" fill="url(#${g})" transform="rotate(-5 32 42)"/>
</g>`)
}
/** 饮品杯 */
function artCup(key, liquid, cup = '#eceff1', type = 'glass') {
const g = gid(key, 'g')
if (type === 'bottle') {
return wrap(key, `
<defs><linearGradient id="${g}" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="${liquid}"/><stop offset="100%" stop-color="${cup}"/>
</linearGradient>${shadow(key)}</defs>
<g filter="url(#${gid(key, 'sh')})">
<rect x="24" y="14" width="16" height="6" rx="2" fill="#b0bec5"/>
<path d="M22 20 L24 52 Q32 56 40 52 L42 20Z" fill="url(#${g})"/>
${shine(28, 30, 4, 10)}
</g>`)
}
if (type === 'wine') {
return wrap(key, `
<defs><linearGradient id="${g}" x1="0%" y1="100%" x2="0%" y2="0%">
<stop offset="0%" stop-color="#4a0e16"/><stop offset="100%" stop-color="${liquid}"/>
</linearGradient>${shadow(key)}</defs>
<g filter="url(#${gid(key, 'sh')})">
<path d="M28 18 L26 38 Q32 46 38 38 L36 18Z" fill="url(#${g})"/>
<rect x="30" y="38" width="4" height="12" fill="#cfd8dc"/>
<ellipse cx="32" cy="52" rx="8" ry="2" fill="#cfd8dc"/>
</g>`)
}
return wrap(key, `
<defs><linearGradient id="${g}" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="${liquid}"/><stop offset="100%" stop-color="${cup}"/>
</linearGradient>${shadow(key)}</defs>
<g filter="url(#${gid(key, 'sh')})">
<path d="M18 24 L20 48 Q32 54 44 48 L46 24Z" fill="#eceff1" stroke="#b0bec5" stroke-width="1"/>
<path d="M20 28 L22 46 Q32 50 42 46 L44 28Z" fill="url(#${g})"/>
${shine(26, 32, 4, 8)}
</g>`)
}
/** 蘑菇 */
function artMushroom(key) {
const g = gid(key, 'g')
return wrap(key, `
<defs><radialGradient id="${g}" cx="40%" cy="35%" r="65%">
<stop offset="0%" stop-color="#a1887f"/><stop offset="100%" stop-color="#5d4037"/>
</radialGradient>${shadow(key)}</defs>
<g filter="url(#${gid(key, 'sh')})">
<rect x="28" y="34" width="8" height="16" rx="3" fill="#efebe9"/>
<ellipse cx="32" cy="30" rx="18" ry="14" fill="url(#${g})"/>
<ellipse cx="24" cy="26" rx="3" ry="2" fill="#efebe9" opacity="0.7"/>
<ellipse cx="36" cy="24" rx="2.5" ry="1.8" fill="#efebe9" opacity="0.7"/>
${shine(24, 24, 6, 4)}
</g>`)
}
/** 芹菜 */
function artCelery(key) {
return artLeafy(key, '#aed581', '#7cb342', '#558b2f')
}
/** 四季豆 */
function artGreenBean(key) {
const g = gid(key, 'g')
return wrap(key, `
<defs><linearGradient id="${g}" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#9ccc65"/><stop offset="100%" stop-color="#33691e"/>
</linearGradient>${shadow(key)}</defs>
<g filter="url(#${gid(key, 'sh')})">
<ellipse cx="26" cy="32" rx="5" ry="18" fill="url(#${g})" transform="rotate(-15 26 32)"/>
<ellipse cx="38" cy="34" rx="5" ry="18" fill="url(#${g})" transform="rotate(12 38 34)"/>
</g>`)
}
/** 饭碗 */
function artRiceBowl(key, rice, bowl = '#e0e0e0', dark = '#bdbdbd') {
const g = gid(key, 'g')
return wrap(key, `
<defs><linearGradient id="${g}" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="${rice}"/><stop offset="100%" stop-color="${dark}"/>
</linearGradient>${shadow(key)}</defs>
<g filter="url(#${gid(key, 'sh')})">
<path d="M12 36 Q32 58 52 36 L48 48 Q32 56 16 48Z" fill="${bowl}"/>
<ellipse cx="32" cy="36" rx="20" ry="8" fill="url(#${g})"/>
${shine(24, 34, 6, 3)}
</g>`)
}
/** 薯类 */
function artRoot(key, base, dark, light) {
const g = gid(key, 'g')
return wrap(key, `
<defs><radialGradient id="${g}" cx="40%" cy="35%" r="65%">
<stop offset="0%" stop-color="${light}"/><stop offset="100%" stop-color="${dark}"/>
</radialGradient>${shadow(key)}</defs>
<g filter="url(#${gid(key, 'sh')})">
<ellipse cx="32" cy="36" rx="16" ry="20" fill="url(#${g})"/>
<path d="M22 28 Q32 22 42 28" stroke="${base}" stroke-width="1.5" fill="none" opacity="0.4"/>
${shine(24, 30, 5, 6)}
</g>`)
}
/** 肉类 */
function artMeat(key, base, dark, type = 'steak') {
const g = gid(key, 'g')
if (type === 'chicken') {
return wrap(key, `
<defs><linearGradient id="${g}" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="${base}"/><stop offset="100%" stop-color="${dark}"/>
</linearGradient>${shadow(key)}</defs>
<g filter="url(#${gid(key, 'sh')})">
<ellipse cx="30" cy="38" rx="14" ry="16" fill="url(#${g})"/>
<ellipse cx="38" cy="28" rx="8" ry="10" fill="url(#${g})"/>
<circle cx="42" cy="24" r="3" fill="#efebe9"/>
${shine(26, 32, 5, 6)}
</g>`)
}
if (type === 'fish') {
return wrap(key, `
<defs><linearGradient id="${g}" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="${base}"/><stop offset="100%" stop-color="${dark}"/>
</linearGradient>${shadow(key)}</defs>
<g filter="url(#${gid(key, 'sh')})">
<ellipse cx="32" cy="34" rx="20" ry="12" fill="url(#${g})"/>
<path d="M52 34 L60 28 L60 40Z" fill="${dark}"/>
<circle cx="22" cy="32" r="2.5" fill="#fff"/>
${shine(28, 30, 6, 4)}
</g>`)
}
return wrap(key, `
<defs><linearGradient id="${g}" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="${base}"/><stop offset="100%" stop-color="${dark}"/>
</linearGradient>${shadow(key)}</defs>
<g filter="url(#${gid(key, 'sh')})">
<path d="M16 40 Q20 22 36 20 Q50 20 48 38 Q46 50 32 52 Q18 52 16 40Z" fill="url(#${g})"/>
<ellipse cx="28" cy="32" rx="4" ry="3" fill="#efebe9" opacity="0.5"/>
${shine(26, 28, 6, 4)}
</g>`)
}
/** 虾/蟹 */
function artSeafood(key, type = 'shrimp') {
const g = gid(key, 'g')
if (type === 'crab') {
return wrap(key, `
<defs><linearGradient id="${g}" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#ff7043"/><stop offset="100%" stop-color="#bf360c"/>
</linearGradient>${shadow(key)}</defs>
<g filter="url(#${gid(key, 'sh')})">
<ellipse cx="32" cy="36" rx="14" ry="12" fill="url(#${g})"/>
<circle cx="28" cy="32" r="2" fill="#fff"/><circle cx="36" cy="32" r="2" fill="#fff"/>
<path d="M18 30 L10 24 M18 38 L10 44 M46 30 L54 24 M46 38 L54 44" stroke="#bf360c" stroke-width="2.5" stroke-linecap="round"/>
</g>`)
}
return wrap(key, `
<defs><linearGradient id="${g}" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#ff8a65"/><stop offset="100%" stop-color="#e64a19"/>
</linearGradient>${shadow(key)}</defs>
<g filter="url(#${gid(key, 'sh')})">
<path d="M16 40 Q28 16 48 28 Q42 36 38 44 Q28 52 16 40Z" fill="url(#${g})"/>
${shine(30, 28, 5, 4)}
</g>`)
}
/** 菠萝 */
function artPineapple(key) {
const g = gid(key, 'g')
return wrap(key, `
<defs><linearGradient id="${g}" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#ffca28"/><stop offset="100%" stop-color="#f57f17"/>
</linearGradient>${shadow(key)}</defs>
<g filter="url(#${gid(key, 'sh')})">
<ellipse cx="32" cy="38" rx="16" ry="18" fill="url(#${g})"/>
<g stroke="#e65100" stroke-width="1" opacity="0.45">${[-8, 0, 8].map(o =>
`<line x1="${24 + o}" y1="24" x2="${24 + o}" y2="52"/>`
).join('')}</g>
<path d="M24 18 L32 8 L40 18" fill="#43a047"/>
${shine(26, 32, 5, 6)}
</g>`)
}
/** 奇异果 */
function artKiwi(key) {
const g = gid(key, 'g')
return wrap(key, `
<defs><radialGradient id="${g}" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="#dce775"/><stop offset="60%" stop-color="#9ccc65"/><stop offset="100%" stop-color="#558b2f"/>
</radialGradient>${shadow(key)}</defs>
<g filter="url(#${gid(key, 'sh')})">
<circle cx="32" cy="34" r="18" fill="#8d6e63"/>
<circle cx="32" cy="34" r="14" fill="url(#${g})"/>
<circle cx="32" cy="34" r="4" fill="#fff" opacity="0.5"/>
${shine(26, 28, 4, 3)}
</g>`)
}
/** 蜜瓜 */
function artMelon(key) {
return artRound(key, '#aed581', '#689f38', '#c5e1a5', '')
}
/** 蜂蜜 */
function artHoney(key) {
return artCup(key, '#ffb300', '#ff8f00', 'bottle')
}
/** 面包类 */
function artBread(key, base, dark, type = 'loaf') {
const g = gid(key, 'g')
if (type === 'donut') {
return wrap(key, `
<defs><linearGradient id="${g}" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="${base}"/><stop offset="100%" stop-color="${dark}"/>
</linearGradient>${shadow(key)}</defs>
<g filter="url(#${gid(key, 'sh')})">
<circle cx="32" cy="34" r="18" fill="url(#${g})"/>
<circle cx="32" cy="34" r="7" fill="#fff5f5"/>
<path d="M20 28 Q32 18 44 28" stroke="#f48fb1" stroke-width="4" fill="none" stroke-linecap="round"/>
${shine(24, 28, 5, 4)}
</g>`)
}
if (type === 'mantou') {
return wrap(key, `
<defs><radialGradient id="${g}" cx="40%" cy="30%" r="70%">
<stop offset="0%" stop-color="${base}"/><stop offset="100%" stop-color="${dark}"/>
</radialGradient>${shadow(key)}</defs>
<g filter="url(#${gid(key, 'sh')})">
<ellipse cx="32" cy="38" rx="16" ry="14" fill="url(#${g})"/>
<path d="M18 34 Q32 26 46 34" stroke="${dark}" stroke-width="1" fill="none" opacity="0.3"/>
${shine(24, 32, 6, 4)}
</g>`)
}
return wrap(key, `
<defs><linearGradient id="${g}" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="${base}"/><stop offset="100%" stop-color="${dark}"/>
</linearGradient>${shadow(key)}</defs>
<g filter="url(#${gid(key, 'sh')})">
<rect x="14" y="22" width="36" height="28" rx="8" fill="url(#${g})"/>
${shine(22, 28, 8, 5)}
</g>`)
}
/** 面条 */
function artNoodle(key, soup = '#d7ccc8') {
const g = gid(key, 'g')
return wrap(key, `
<defs><linearGradient id="${g}" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="${soup}"/><stop offset="100%" stop-color="#a1887f"/>
</linearGradient>${shadow(key)}</defs>
<g filter="url(#${gid(key, 'sh')})">
<ellipse cx="32" cy="42" rx="20" ry="10" fill="#eceff1"/>
<ellipse cx="32" cy="38" rx="18" ry="8" fill="url(#${g})"/>
<path d="M20 36 Q26 32 32 36 Q38 40 44 36" stroke="#ffe082" stroke-width="2.5" fill="none" stroke-linecap="round"/>
<path d="M22 40 Q30 44 38 40" stroke="#ffe082" stroke-width="2" fill="none" stroke-linecap="round"/>
</g>`)
}
/** 南瓜 */
function artPumpkin(key) {
const g = gid(key, 'g')
return wrap(key, `
<defs><radialGradient id="${g}" cx="40%" cy="35%" r="65%">
<stop offset="0%" stop-color="#ffb74d"/><stop offset="100%" stop-color="#e65100"/>
</radialGradient>${shadow(key)}</defs>
<g filter="url(#${gid(key, 'sh')})">
<ellipse cx="24" cy="36" rx="10" ry="16" fill="url(#${g})"/>
<ellipse cx="32" cy="34" rx="11" ry="18" fill="url(#${g})"/>
<ellipse cx="40" cy="36" rx="10" ry="16" fill="url(#${g})"/>
<path d="M32 16 Q34 10 32 8" stroke="#558b2f" stroke-width="2.5" fill="none"/>
${shine(26, 28, 5, 6)}
</g>`)
}
/** 薯条 */
function artFries(key) {
const g = gid(key, 'g')
return wrap(key, `
<defs><linearGradient id="${g}" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#ffe082"/><stop offset="100%" stop-color="#f9a825"/>
</linearGradient>${shadow(key)}</defs>
<g filter="url(#${gid(key, 'sh')})">
<path d="M18 48 L22 24 L46 24 L50 48Z" fill="#ef5350"/>
${[26, 32, 38, 44].map(x => `<rect x="${x}" y="18" width="4" height="22" rx="2" fill="url(#${g})"/>`).join('')}
</g>`)
}
/** 西瓜 */
function artWatermelon(key) {
const g = gid(key, 'g')
return wrap(key, `
<defs><linearGradient id="${g}" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#ef5350"/><stop offset="100%" stop-color="#c62828"/>
</linearGradient>${shadow(key)}</defs>
<g filter="url(#${gid(key, 'sh')})">
<path d="M12 40 Q32 8 52 40 Q32 56 12 40Z" fill="#2e7d32"/>
<path d="M16 40 Q32 14 48 40 Q32 52 16 40Z" fill="url(#${g})"/>
${[24, 32, 40].map(x => `<circle cx="${x}" cy="36" r="1.2" fill="#212121"/>`).join('')}
${shine(24, 30, 4, 3)}
</g>`)
}
/** 糖果 */
function artCandy(key, base, dark, type = 'lollipop') {
const g = gid(key, 'g')
if (type === 'wrap') {
return wrap(key, `
<defs><linearGradient id="${g}" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="${base}"/><stop offset="100%" stop-color="${dark}"/>
</linearGradient>${shadow(key)}</defs>
<g filter="url(#${gid(key, 'sh')})">
<rect x="20" y="22" width="24" height="20" rx="4" fill="url(#${g})"/>
<path d="M20 26 L16 30 L20 34 M44 26 L48 30 L44 34" stroke="${dark}" stroke-width="2" fill="none"/>
</g>`)
}
return wrap(key, `
<defs><radialGradient id="${g}" cx="35%" cy="30%" r="70%">
<stop offset="0%" stop-color="${base}"/><stop offset="100%" stop-color="${dark}"/>
</radialGradient>${shadow(key)}</defs>
<g filter="url(#${gid(key, 'sh')})">
<circle cx="32" cy="28" r="14" fill="url(#${g})"/>
<rect x="30" y="40" width="4" height="14" rx="2" fill="#eceff1"/>
${shine(26, 24, 4, 3)}
</g>`)
}
/** 糯米团 */
function artDango(key) {
const g = gid(key, 'g')
return wrap(key, `
<defs><radialGradient id="${g}" cx="40%" cy="30%" r="70%">
<stop offset="0%" stop-color="#f8bbd0"/><stop offset="100%" stop-color="#ec407a"/>
</radialGradient>${shadow(key)}</defs>
<g filter="url(#${gid(key, 'sh')})">
<circle cx="22" cy="36" r="8" fill="#fff"/>
<circle cx="32" cy="32" r="8" fill="url(#${g})"/>
<circle cx="42" cy="36" r="8" fill="#fff"/>
<rect x="14" y="44" width="36" height="3" rx="1.5" fill="#8d6e63"/>
</g>`)
}
/** 爆米花 */
function artPopcorn(key) {
const g = gid(key, 'g')
return wrap(key, `
<defs><radialGradient id="${g}" cx="40%" cy="30%" r="70%">
<stop offset="0%" stop-color="#fff9c4"/><stop offset="100%" stop-color="#fff176"/>
</radialGradient>${shadow(key)}</defs>
<g filter="url(#${gid(key, 'sh')})">
<path d="M16 48 L20 36 L48 36 L52 48Z" fill="#ef5350"/>
<circle cx="24" cy="30" r="7" fill="url(#${g})"/><circle cx="34" cy="24" r="8" fill="url(#${g})"/>
<circle cx="44" cy="30" r="7" fill="url(#${g})"/><circle cx="32" cy="32" r="6" fill="url(#${g})"/>
</g>`)
}
const FOOD_ART = {
lettuce: () => artLeafy('lettuce', '#c5e1a5', '#81c784', '#388e3c'),
broccoli: () => artBroccoli('broccoli'),
apple: () => artRound('apple', '#e53935', '#b71c1c', '#ef5350'),
cucumber: () => artLong('cucumber', '#66bb6a', '#2e7d32', '#a5d6a7'),
eggplant: () => artEggplant('eggplant'),
tomato: () => artTomato('tomato'),
pepper: () => artPepper('pepper', '#43a047', '#1b5e20'),
onion: () => artOnion('onion'),
pear: () => artRound('pear', '#c0ca33', '#827717', '#dce775', ''),
orange: () => artRound('orange', '#fb8c00', '#e65100', '#ffb74d'),
peach: () => artRound('peach', '#ff8a65', '#e64a19', '#ffab91'),
cherry: () => artCherry('cherry'),
grape: () => artGrapes('grape'),
strawberry: () => artStrawberry('strawberry'),
soybean: () => artBeans('soybean'),
milk: () => artCup('milk', '#eceff1', '#b0bec5'),
tea: () => artCup('tea', '#a1887f', '#6d4c41'),
mushroom: () => artMushroom('mushroom'),
celery: () => artCelery('celery'),
greenBean: () => artGreenBean('greenBean'),
brownRice: () => artRiceBowl('brownRice', '#bcaaa4', '#8d6e63', '#6d4c41'),
sweetPotato: () => artRoot('sweetPotato', '#c75b39', '#8d2600', '#e57373'),
taro: () => artRoot('taro', '#b39ddb', '#5e35b1', '#d1c4e9'),
fish: () => artMeat('fish', '#4fc3f7', '#0277bd', 'fish'),
chicken: () => artMeat('chicken', '#ffb74d', '#e65100', 'chicken'),
beef: () => artMeat('beef', '#c0504d', '#7f0000', 'steak'),
leanMeat: () => artMeat('leanMeat', '#b5654d', '#8d4000', 'steak'),
shrimp: () => artSeafood('shrimp', 'shrimp'),
crab: () => artSeafood('crab', 'crab'),
banana: () => artLong('banana', '#fdd835', '#f9a825', '#fff176', 'banana'),
mango: () => artRound('mango', '#ffb300', '#ff6f00', '#ffca28'),
pineapple: () => artPineapple('pineapple'),
kiwi: () => artKiwi('kiwi'),
melon: () => artMelon('melon'),
honey: () => artHoney('honey'),
wine: () => artCup('wine', '#9b2c3b', '#4a0e16', 'wine'),
beer: () => artCup('beer', '#ffb300', '#ff8f00'),
coffee: () => artCup('coffee', '#6f4e37', '#3e2723'),
corn: () => artLong('corn', '#f9c513', '#f57f17', '#fff176', 'corn'),
udon: () => artNoodle('udon', '#d9b88f'),
whiteRice: () => artRiceBowl('whiteRice', '#f5f5f5', '#eeeeee', '#bdbdbd'),
whiteBread: () => artBread('whiteBread', '#d9a85c', '#a1887f', 'loaf'),
mantou: () => artBread('mantou', '#ece0c8', '#bcaaa4', 'mantou'),
youtiao: () => artBread('youtiao', '#d4943f', '#a1660a', 'loaf'),
donut: () => artBread('donut', '#e87fb0', '#ad1457', 'donut'),
popcorn: () => artPopcorn('popcorn'),
ramen: () => artNoodle('ramen', '#e0a96d'),
pumpkin: () => artPumpkin('pumpkin'),
bakedPotato: () => artFries('bakedPotato'),
watermelon: () => artWatermelon('watermelon'),
sugar: () => artCandy('sugar', '#ff79b0', '#c2185b', 'lollipop'),
maltose: () => artCandy('maltose', '#ffa726', '#e65100', 'wrap'),
soda: () => artCup('soda', '#c62828', '#880e0e', 'bottle'),
orangeJuice: () => artCup('orangeJuice', '#ff9f1c', '#e65100', 'bottle'),
stickyRice: () => artDango('stickyRice')
}
const imgCache = {}
/** 获取食材插画 data URL(带缓存) */
export function getFoodImg(key) {
if (imgCache[key]) return imgCache[key]
const builder = FOOD_ART[key]
if (!builder) return ''
imgCache[key] = svgToDataUrl(builder())
return imgCache[key]
}
export function warmUpFoodImgs(keys) {
keys.forEach((k) => getFoodImg(k))
}
@@ -0,0 +1,49 @@
/**
* 食材图标 URLTwemoji PNG,小程序 / H5 通用)
* 微信小程序 <image> 不支持 SVG data URL,必须用 https PNG
*/
const TWEMOJI_BASE = 'https://cdn.jsdelivr.net/gh/twitter/twemoji@14.0.2/assets/72x72'
/** emoji → Twemoji PNG 地址 */
export function emojiToTwemojiUrl(emoji) {
if (!emoji) return ''
const parts = []
for (let i = 0; i < emoji.length;) {
const cp = emoji.codePointAt(i)
if (!cp) break
parts.push(cp.toString(16))
i += cp > 0xffff ? 2 : 1
}
return `${TWEMOJI_BASE}/${parts.join('-')}.png`
}
const imgCache = {}
export function getFoodImgUrl(emoji) {
if (!emoji) return ''
if (imgCache[emoji]) return imgCache[emoji]
const url = emojiToTwemojiUrl(emoji)
imgCache[emoji] = url
return url
}
/** 预加载本局食材图标(小程序提前拉取,减少首屏空白) */
export function warmUpFoodImgs(foods) {
const list = Array.isArray(foods) ? foods : []
list.forEach((f) => {
const icon = typeof f === 'string' ? f : f?.icon
const url = getFoodImgUrl(icon)
if (!url) return
// #ifdef MP-WEIXIN
try {
uni.getImageInfo({ src: url, fail: () => {} })
} catch (_) {}
// #endif
// #ifdef H5
try {
const img = new Image()
img.src = url
} catch (_) {}
// #endif
})
}
@@ -0,0 +1,175 @@
/**
* 全量食材库:key 用于消除匹配,img 为 Twemoji PNG(小程序可用)
*/
import { getFoodImgUrl } from './gameFoodImg.js'
export const GI_LABEL = { low: '低血糖', mid: '中血糖', high: '高血糖' }
/** 若实际食用时的升糖幅度(教育提示用,正值=升糖) */
export function getEatVal(food) {
if (food.eatVal != null) return food.eatVal
if (food.gi === 'high') return Math.abs(food.val)
if (food.gi === 'mid') return Math.max(1, food.val)
return Math.max(1, Math.abs(food.val))
}
/** 从餐盘消除该食物后的血糖变化(负值=控糖有益) */
export function getClearVal(food) {
if (food.clearVal != null) return food.clearVal
const eat = getEatVal(food)
if (food.gi === 'high') return -Math.max(8, Math.round(eat * 0.5))
if (food.gi === 'mid') return -1
return 0
}
/** 点击食材时的升糖说明文案 */
export function formatEatLabel(food) {
if (food.gi === 'low') return '升糖低'
return `若食用 +${getEatVal(food)}`
}
/** 含糖等级简称:低糖 / 中糖 / 高糖 */
export const GI_SHORT = { low: '低糖', mid: '中糖', high: '高糖' }
/** 含糖等级对应的提示主色(绿 / 橙 / 红) */
export const GI_COLOR = { low: '#16a34a', mid: '#ea580c', high: '#dc2626' }
/** 部分代表食物的专属科普文案(一句话,便于老人记忆) */
const FOOD_TIP_OVERRIDE = {
whiteRice: '精制主食,升糖快,建议小份',
stickyRice: '糯米黏软,升糖很快,要少吃',
whiteBread: '精制面食,升糖快',
mantou: '白面馒头,升糖快,配菜一起吃更稳',
youtiao: '油炸又高糖,少吃为好',
sugar: '纯糖,升糖最快,尽量不吃',
maltose: '糖分很高,升糖快',
soda: '含糖饮料,升糖很快,不建议喝',
orangeJuice: '果汁含糖高,吃整果更好',
watermelon: '很甜,升糖快,一次别吃太多',
donut: '又甜又油,升糖高',
banana: '偏甜水果,适量吃',
sweetPotato: '粗粮但有糖,可代替部分主食',
corn: '粗粮主食,适量吃',
honey: '本质是糖,别当健康品多吃',
broccoli: '新鲜蔬菜,升糖很慢,可多吃',
cucumber: '清爽蔬菜,几乎不升糖',
lettuce: '绿叶菜,升糖很低,可多吃',
celery: '高纤维蔬菜,升糖低',
apple: '带皮整果,升糖较慢',
milk: '低糖,含蛋白,适量喝好',
fish: '优质蛋白,升糖低',
beef: '蛋白为主,升糖低'
}
/** 含糖等级通用科普文案 */
const GI_TIP_DEFAULT = {
low: '升糖慢,相对放心,可常吃',
mid: '升糖中等,注意分量',
high: '含糖高、升糖快,要少吃'
}
/** 点击食物时展示的一句话科普(认知教育用) */
export function getFoodTip(food) {
if (!food) return ''
if (food.tip) return food.tip
if (FOOD_TIP_OVERRIDE[food.key]) return FOOD_TIP_OVERRIDE[food.key]
return GI_TIP_DEFAULT[food.gi] || ''
}
const RAW_FOODS = [
// 低 GI
{ key: 'lettuce', icon: '🥬', name: '生菜', gi: 'low', val: -3, color: '#8BC34A' },
{ key: 'broccoli', icon: '🥦', name: '西兰花', gi: 'low', val: -3, color: '#4E8A3A' },
{ key: 'apple', icon: '🍎', name: '苹果', gi: 'low', val: -2, color: '#E53935' },
{ key: 'cucumber', icon: '🥒', name: '黄瓜', gi: 'low', val: -4, color: '#66A82F' },
{ key: 'eggplant', icon: '🍆', name: '茄子', gi: 'low', val: -3, color: '#7E57C2' },
{ key: 'tomato', icon: '🍅', name: '番茄', gi: 'low', val: -2, color: '#EF5350' },
{ key: 'pepper', icon: '🫑', name: '青椒', gi: 'low', val: -3, color: '#43A047' },
{ key: 'onion', icon: '🧅', name: '洋葱', gi: 'low', val: -2, color: '#CDA06B' },
{ key: 'pear', icon: '🍐', name: '雪梨', gi: 'low', val: -2, color: '#C0CA33' },
{ key: 'orange', icon: '🍊', name: '橙子', gi: 'low', val: -2, color: '#FB8C00' },
{ key: 'peach', icon: '🍑', name: '桃子', gi: 'low', val: -2, color: '#FF8A65' },
{ key: 'cherry', icon: '🍒', name: '樱桃', gi: 'low', val: -2, color: '#C2185B' },
{ key: 'grape', icon: '🍇', name: '葡萄', gi: 'low', val: -1, color: '#8E24AA' },
{ key: 'strawberry', icon: '🍓', name: '草莓', gi: 'low', val: -2, color: '#EC407A' },
{ key: 'soybean', icon: '🫘', name: '黄豆', gi: 'low', val: -3, color: '#C9A063' },
{ key: 'milk', icon: '🥛', name: '牛奶', gi: 'low', val: -2, color: '#B0BEC5' },
{ key: 'tea', icon: '🍵', name: '红茶', gi: 'low', val: -1, color: '#A1573B' },
{ key: 'mushroom', icon: '🍄', name: '香菇', gi: 'low', val: -3, color: '#8D6E63' },
{ key: 'celery', icon: '🌿', name: '芹菜', gi: 'low', val: -4, color: '#7CB342' },
{ key: 'greenBean', icon: '🫛', name: '四季豆', gi: 'low', val: -3, color: '#689F38' },
// 中 GI
{ key: 'brownRice', icon: '🍙', name: '糙米饭', gi: 'mid', val: 4, color: '#C9A26B' },
{ key: 'sweetPotato', icon: '🍠', name: '番薯', gi: 'mid', val: 5, color: '#C75B39' },
{ key: 'taro', icon: '🥔', name: '芋头', gi: 'mid', val: 5, color: '#B39DDB' },
{ key: 'fish', icon: '🐟', name: '鱼肉', gi: 'mid', val: 2, color: '#4FA3C7' },
{ key: 'chicken', icon: '🍗', name: '鸡肉', gi: 'mid', val: 3, color: '#D6A15A' },
{ key: 'beef', icon: '🥩', name: '牛肉', gi: 'mid', val: 3, color: '#C0504D' },
{ key: 'leanMeat', icon: '🍖', name: '瘦肉', gi: 'mid', val: 3, color: '#B5654D' },
{ key: 'shrimp', icon: '🦐', name: '虾仁', gi: 'mid', val: 2, color: '#FF7043' },
{ key: 'crab', icon: '🦀', name: '螃蟹', gi: 'mid', val: 2, color: '#F4623A' },
{ key: 'banana', icon: '🍌', name: '香蕉', gi: 'mid', val: 6, color: '#FDD835' },
{ key: 'mango', icon: '🥭', name: '芒果', gi: 'mid', val: 6, color: '#FFB300' },
{ key: 'pineapple', icon: '🍍', name: '菠萝', gi: 'mid', val: 6, color: '#FBC02D' },
{ key: 'kiwi', icon: '🥝', name: '奇异果', gi: 'mid', val: 4, color: '#9CCC65' },
{ key: 'melon', icon: '🍈', name: '哈密瓜', gi: 'mid', val: 6, color: '#AED581' },
{ key: 'honey', icon: '🍯', name: '蜂蜜', gi: 'mid', val: 6, color: '#F9A825' },
{ key: 'wine', icon: '🍷', name: '红酒', gi: 'mid', val: 4, color: '#9B2C3B' },
{ key: 'beer', icon: '🍺', name: '啤酒', gi: 'mid', val: 5, color: '#E0A83E' },
{ key: 'coffee', icon: '☕', name: '咖啡', gi: 'mid', val: 2, color: '#6F4E37' },
{ key: 'corn', icon: '🌽', name: '玉米', gi: 'mid', val: 5, color: '#F9C513' },
{ key: 'udon', icon: '🍲', name: '乌冬面', gi: 'mid', val: 5, color: '#D9B88F' },
// 高 GI
{ key: 'whiteRice', icon: '🍚', name: '白米饭', gi: 'high', val: 18, color: '#E0E0E0' },
{ key: 'whiteBread', icon: '🍞', name: '白面包', gi: 'high', val: 16, color: '#D9A85C' },
{ key: 'mantou', icon: '🥯', name: '馒头', gi: 'high', val: 17, color: '#ECE0C8' },
{ key: 'youtiao', icon: '🥖', name: '油条', gi: 'high', val: 20, color: '#D4943F' },
{ key: 'donut', icon: '🍩', name: '甜甜圈', gi: 'high', val: 22, color: '#E87FB0' },
{ key: 'popcorn', icon: '🍿', name: '爆米花', gi: 'high', val: 18, color: '#F5E6B3' },
{ key: 'ramen', icon: '🍜', name: '拉面', gi: 'high', val: 17, color: '#E0A96D' },
{ key: 'pumpkin', icon: '🎃', name: '南瓜', gi: 'high', val: 15, color: '#EF6C00' },
{ key: 'bakedPotato', icon: '🍟', name: '焗薯', gi: 'high', val: 20, color: '#F2C14E' },
{ key: 'watermelon', icon: '🍉', name: '西瓜', gi: 'high', val: 16, color: '#F0506A' },
{ key: 'sugar', icon: '🍭', name: '砂糖', gi: 'high', val: 25, color: '#FF79B0' },
{ key: 'maltose', icon: '🍬', name: '麦芽糖', gi: 'high', val: 24, color: '#FFA726' },
{ key: 'soda', icon: '🥤', name: '汽水', gi: 'high', val: 25, color: '#C62828' },
{ key: 'orangeJuice', icon: '🧃', name: '柳橙汁', gi: 'high', val: 22, color: '#FF9F1C' },
{ key: 'stickyRice', icon: '🍡', name: '糯米饭', gi: 'high', val: 19, color: '#F2A7B8' }
]
export const FOOD_LIBRARY = RAW_FOODS.map((f, i) => {
const eatVal = getEatVal(f)
const clearVal = getClearVal({ ...f, eatVal })
return {
id: i + 1,
...f,
eatVal,
clearVal,
giLabel: GI_LABEL[f.gi],
get img() {
return getFoodImgUrl(f.icon)
}
}
})
/** 按 key 查找食材(目标栏等用) */
export function getFoodByKey(key) {
return FOOD_LIBRARY.find((f) => f.key === key) || null
}
/** 为棋盘 tile 使用的 plain 对象(img 预计算,避免 getter 在响应式里反复触发) */
export function cloneFoodType(food) {
return {
id: food.id,
key: food.key,
icon: food.icon,
name: food.name,
gi: food.gi,
giLabel: food.giLabel,
val: food.val,
color: food.color,
img: getFoodImgUrl(food.icon)
}
}
+33
View File
@@ -0,0 +1,33 @@
/**
* 关卡配置:面向 45+ 的认知练习,节奏放慢、无失败惩罚。
* 每组目标只比上一组略增,步数充裕(仅展示,不会因耗尽判负)。
*/
export function getLevelConfig(levelId) {
const lv = Math.max(1, Math.min(999, Number(levelId) || 1))
const tier = Math.floor((lv - 1) / 5)
return {
id: lv,
// 步数充裕:仅作展示,认全目标即可通关
moves: Math.max(40, 60 - tier * 2),
// 认识目标平缓增长,避免给老人压力
goalTargets: [6 + tier, 8 + tier],
scoreTarget: 2000 + lv * 300,
startGlucose: 50,
boosters: {
insulin: 3,
fiber: 1,
meal: 5
}
}
}
/** 根据本局表现计算 1~3 星:完成即得星,步数越省星越多(不依赖血糖) */
export function calcLevelStars({ movesLeft, levelConfig }) {
const totalMoves = levelConfig?.moves || 1
const ratio = totalMoves > 0 ? movesLeft / totalMoves : 0
let stars = 1
if (ratio >= 0.3) stars = 2
if (ratio >= 0.55) stars = 3
return stars
}
@@ -0,0 +1,596 @@
<!DOCTYPE html><html class="light" lang="zh-CN"><head>
<meta charset="utf-8">
<meta content="width=device-width, initial-scale=1.0" name="viewport">
<title>Daily Care - Health Dashboard</title>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&amp;display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet">
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
"colors": {
"secondary-fixed": "#d9e6dd",
"surface-container": "#e8f0e9",
"surface-dim": "#d4dcd5",
"surface-variant": "#dde4dd",
"error-container": "#ffdad6",
"inverse-surface": "#2b322d",
"secondary": "#55615a",
"primary": "#006c49",
"on-secondary-fixed": "#131e19",
"surface-bright": "#f4fbf4",
"on-tertiary-container": "#711419",
"on-primary-fixed-variant": "#005236",
"outline-variant": "#bbcabf",
"primary-container": "#10b981",
"surface": "#f4fbf4",
"on-tertiary-fixed-variant": "#842225",
"tertiary-fixed-dim": "#ffb3af",
"tertiary-fixed": "#ffdad7",
"error": "#ba1a1a",
"surface-container-lowest": "#ffffff",
"surface-container-highest": "#dde4dd",
"primary-fixed": "#6ffbbe",
"on-primary-container": "#00422b",
"secondary-fixed-dim": "#bdcac1",
"on-tertiary": "#ffffff",
"on-primary-fixed": "#002113",
"on-error-container": "#93000a",
"tertiary-container": "#fc7c78",
"on-error": "#ffffff",
"inverse-primary": "#4edea3",
"inverse-on-surface": "#ebf3eb",
"on-secondary-container": "#5b6760",
"surface-container-high": "#e3eae3",
"surface-container-low": "#eef6ee",
"on-secondary-fixed-variant": "#3e4943",
"outline": "#6c7a71",
"secondary-container": "#d9e6dd",
"on-primary": "#ffffff",
"on-surface": "#161d19",
"on-background": "#161d19",
"tertiary": "#a43a3a",
"primary-fixed-dim": "#4edea3",
"on-secondary": "#ffffff",
"background": "#f4fbf4",
"on-surface-variant": "#3c4a42",
"surface-tint": "#006c49",
"on-tertiary-fixed": "#410005"
},
"borderRadius": {
"DEFAULT": "0.25rem",
"lg": "0.5rem",
"xl": "0.75rem",
"2xl": "1rem",
"3xl": "1.5rem",
"full": "9999px"
},
"spacing": {
"card-padding": "20px",
"section-margin": "32px",
"inline-gap": "12px",
"stack-gap": "16px",
"container-margin": "20px"
},
"fontFamily": {
"headline-md": ["Manrope"],
"headline-lg-mobile": ["Manrope"],
"body-lg": ["Manrope"],
"headline-lg": ["Manrope"],
"display-lg": ["Manrope"],
"body-md": ["Manrope"],
"label-md": ["Manrope"]
},
"fontSize": {
"headline-md": ["20px", {"lineHeight": "28px", "fontWeight": "700"}],
"headline-lg-mobile": ["22px", {"lineHeight": "28px", "fontWeight": "700"}],
"body-lg": ["16px", {"lineHeight": "24px", "fontWeight": "500"}],
"headline-lg": ["24px", {"lineHeight": "32px", "letterSpacing": "-0.01em", "fontWeight": "700"}],
"display-lg": ["32px", {"lineHeight": "40px", "letterSpacing": "-0.02em", "fontWeight": "800"}],
"body-md": ["14px", {"lineHeight": "20px", "fontWeight": "400"}],
"label-md": ["12px", {"lineHeight": "16px", "letterSpacing": "0.02em", "fontWeight": "600"}]
}
}
}
}
</script>
<style>
body { font-family: 'Manrope', sans-serif; -webkit-tap-highlight-color: transparent; }
.material-symbols-outlined { font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24; display: inline-block; vertical-align: middle; }
.ambient-shadow { box-shadow: 0 20px 30px -10px rgba(0, 108, 73, 0.08); }
.active-scale:active { transform: scale(0.98); transition: all 0.2s ease; }
.hide-scrollbar::-webkit-scrollbar { display: none; }
.hide-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
/* Watering Animation Styles */
@keyframes fall {
0% { transform: translateY(-20px); opacity: 0; }
50% { opacity: 1; }
100% { transform: translateY(40px); opacity: 0; }
}
.droplet {
position: absolute;
width: 4px;
height: 10px;
background: #3b82f6;
border-radius: 50%;
animation: fall 0.6s linear infinite;
}
@keyframes pulse-growth {
0% { transform: scale(1); filter: drop-shadow(0 0 0px rgba(78, 222, 163, 0)); }
50% { transform: scale(1.15); filter: drop-shadow(0 0 15px rgba(78, 222, 163, 0.6)); }
100% { transform: scale(1); filter: drop-shadow(0 0 0px rgba(78, 222, 163, 0)); }
}
.growth-pulse {
animation: pulse-growth 0.8s ease-out;
}
@keyframes shimmer {
0% { opacity: 0; transform: scale(0) rotate(0deg); }
50% { opacity: 1; transform: scale(1.2) rotate(180deg); }
100% { opacity: 0; transform: scale(0) rotate(360deg); }
}
.shimmer-particle {
position: absolute;
color: #4edea3;
font-size: 14px;
pointer-events: none;
animation: shimmer 1s ease-out forwards;
}
/* Blood Sugar Decrease Animation */
@keyframes float-up-fade {
0% { transform: translateY(20px); opacity: 0; }
20% { opacity: 1; }
80% { opacity: 1; }
100% { transform: translateY(-100px); opacity: 0; }
}
.sugar-decrease-text {
position: fixed;
color: #10b981;
font-weight: 800;
font-size: 24px;
pointer-events: none;
z-index: 100;
text-shadow: 0 4px 12px rgba(16, 185, 129, 0.3);
animation: float-up-fade 2s ease-out forwards;
}
@keyframes fall-arrow {
0% { transform: translateY(-50vh) scale(0.5); opacity: 0; }
50% { opacity: 0.8; }
100% { transform: translateY(110vh) scale(1.2); opacity: 0; }
}
.sugar-particle {
position: fixed;
color: #10b981;
pointer-events: none;
z-index: 90;
animation: fall-arrow 1.5s linear forwards;
}
</style>
<style>
body {
min-height: max(884px, 100dvh);
}
</style>
</head>
<body class="bg-background text-on-background min-h-screen pb-24">
<!-- Top App Bar -->
<header class="bg-primary docked full-width top-0 h-48 flex items-end pb-6 px-5 fixed left-0 right-0 z-40">
<div class="flex justify-between items-center w-full">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-full border-2 border-on-primary overflow-hidden">
<img class="w-full h-full object-cover" data-alt="A professional studio portrait" src="https://lh3.googleusercontent.com/aida-public/AB6AXuCXIPbeivladmw0-SmLno2vkwVvsObW2D5HUtodItlBnxdhFF8TvZdQYrTt14QCk3n5VXXIpdk3cuWTJHXJgDXd_clQyjLsb477e5h2313zh76yRVns01Q5pLpM-2_RbheRcUioLvObwJtIPJbkDcbkNA61-LyBnbStRcPoh6YOvNHSHjUUR_nDJpPm18ga87ggphbFWF-pt7lRF027swmyitUeVF7HXQDfcG7Mq7E0pqsbhyA7jKxtzmbN-HLh7GIrhc6LOAUaXiX9">
</div>
<div>
<h1 class="text-on-primary font-bold text-headline-lg-mobile leading-tight">Daily Care</h1>
<p class="text-on-primary/80 text-body-md">吕帅</p>
</div>
</div>
<div class="flex gap-2">
<button class="bg-on-primary/10 hover:bg-on-primary/20 text-on-primary rounded-full px-4 py-1.5 flex items-center gap-2 active-scale transition-all">
<span class="material-symbols-outlined text-[18px]" style="font-variation-settings: 'FILL' 1;">bloodtype</span>
<span class="text-label-md">血糖</span>
</button>
<button class="bg-on-primary/10 hover:bg-on-primary/20 text-on-primary w-10 h-10 rounded-full flex items-center justify-center active-scale transition-all">
<span class="material-symbols-outlined text-[20px]">refresh</span>
</button>
</div>
</div>
</header>
<main class="pt-52 px-container-margin space-y-6">
<!-- Toggle Switch -->
<div class="bg-surface-container-high rounded-full p-1 flex items-center">
<button class="flex-1 bg-primary text-on-primary rounded-full py-2 text-label-md font-bold shadow-md transition-all">最近 7 天</button>
<button class="flex-1 text-on-surface-variant py-2 text-label-md font-bold transition-all">最近 30 天</button>
</div>
<!-- Health Calendar -->
<section class="bg-surface-container-lowest rounded-3xl p-card-padding ambient-shadow">
<div class="flex justify-between items-start mb-4">
<div>
<h2 class="text-headline-md font-headline-md text-on-surface">健康日历</h2>
<p class="text-body-md text-outline">2026年5月 · 已记录 3 / 7 天</p>
</div>
<button class="text-primary"><span class="material-symbols-outlined">chevron_right</span></button>
</div>
<div class="grid grid-cols-7 gap-1 text-center mb-4">
<span class="text-label-md text-outline"></span>
<span class="text-label-md text-outline"></span>
<span class="text-label-md text-outline"></span>
<span class="text-label-md text-outline"></span>
<span class="text-label-md text-outline"></span>
<span class="text-label-md text-outline"></span>
<span class="text-label-md text-tertiary"></span>
<span class="py-2 text-body-md text-outline/30">18</span>
<span class="py-2 text-body-md text-outline/30">19</span>
<span class="py-2 text-body-md text-outline/30">20</span>
<span class="py-2 text-body-md text-outline/30">21</span>
<span class="py-2 text-body-md text-outline/30">22</span>
<div class="py-2 relative"><span class="text-body-md font-bold text-on-surface">23</span></div>
<div class="py-2 relative bg-primary-container/10 rounded-lg">
<span class="text-body-md font-bold text-primary">24</span>
<span class="absolute bottom-1 left-1/2 -translate-x-1/2 w-1 h-1 bg-primary rounded-full"></span>
</div>
<div class="py-2 relative bg-primary-container/10 rounded-lg">
<span class="text-body-md font-bold text-primary">25</span>
<span class="absolute bottom-1 left-1/2 -translate-x-1/2 w-1 h-1 bg-primary rounded-full"></span>
</div>
<div class="py-2 relative bg-primary-container/10 rounded-lg">
<span class="text-body-md font-bold text-primary">26</span>
<span class="absolute bottom-1 left-1/2 -translate-x-1/2 w-1 h-1 bg-primary rounded-full"></span>
</div>
<span class="py-2 text-body-md text-on-surface">27</span>
<span class="py-2 text-body-md text-on-surface">28</span>
<div class="py-2 relative bg-tertiary-container rounded-lg">
<span class="text-body-md font-bold text-on-tertiary">29</span>
</div>
<span class="py-2 text-body-md text-outline/40">30</span>
<span class="py-2 text-body-md text-outline/40">31</span>
</div>
<div class="flex items-center justify-between border-t border-outline-variant/30 pt-4 mt-2">
<span class="text-body-md text-outline">今日还没有记录</span>
<button class="bg-primary text-on-primary px-6 py-3 rounded-2xl text-label-md font-bold active-scale transition-all flex items-center gap-2">去记血糖</button>
</div>
</section>
<!-- Blood Sugar Details Section -->
<section class="space-y-4">
<div class="flex justify-between items-center">
<h3 class="text-headline-md font-headline-md text-on-surface">血糖明细</h3>
<span class="text-body-md text-outline">共 3 天</span>
</div>
<div class="space-y-4">
<div class="bg-surface-container-lowest rounded-3xl p-card-padding ambient-shadow">
<div class="flex justify-between items-center mb-4">
<span class="text-headline-md font-bold">05-26</span>
<span class="text-body-md text-outline">周二</span>
</div>
<div class="space-y-2">
<div class="bg-background/50 border border-outline-variant/20 rounded-2xl p-4 flex justify-between items-center">
<span class="text-body-md text-outline">空腹</span>
<div class="text-right">
<span class="text-headline-lg font-extrabold text-error">10.4</span>
<span class="text-label-md text-outline ml-1">mmol/L</span>
</div>
</div>
<div class="bg-background/50 border border-outline-variant/20 rounded-2xl p-4 flex justify-between items-center">
<span class="text-body-md text-outline">餐后</span>
<span class="text-headline-md font-bold text-outline-variant">—— <small class="text-label-md">mmol/L</small></span>
</div>
</div>
</div>
<div class="bg-surface-container-lowest rounded-3xl p-card-padding ambient-shadow">
<div class="flex justify-between items-center mb-4">
<span class="text-headline-md font-bold">05-25</span>
<span class="text-body-md text-outline">周一</span>
</div>
<div class="bg-background/50 border border-outline-variant/20 rounded-2xl p-4 flex justify-between items-center">
<span class="text-body-md text-outline">空腹</span>
<div class="text-right">
<span class="text-headline-lg font-extrabold text-error">10.2</span>
<span class="text-label-md text-outline ml-1">mmol/L</span>
</div>
</div>
</div>
</div>
</section>
<!-- Reward Section: 稳糖乐园 -->
<section class="bg-surface-container-lowest rounded-3xl p-card-padding ambient-shadow overflow-hidden relative" id="sugar-paradise">
<div class="flex justify-between items-start mb-6">
<div>
<h3 class="text-headline-md font-headline-md text-on-surface">稳糖乐园</h3>
<p class="text-body-md text-outline">用健康打卡浇灌你的稳糖树</p>
</div>
<div class="bg-primary/10 rounded-xl px-3 py-1 text-center">
<span class="block text-headline-lg font-extrabold text-primary" id="total-points">110</span>
<span class="text-[10px] text-primary/70 font-bold">稳糖积分</span>
</div>
</div>
<!-- Mini Quick Actions -->
<div class="grid grid-cols-4 gap-2 mb-8">
<button class="bg-surface-container-low p-3 rounded-2xl flex flex-col items-center gap-1 active-scale transition-all">
<span class="material-symbols-outlined text-primary" style="font-variation-settings: 'FILL' 1;">bloodtype</span>
<span class="text-label-md text-on-surface">血糖</span>
</button>
<button class="bg-surface-container-low p-3 rounded-2xl flex flex-col items-center gap-1 active-scale transition-all">
<span class="material-symbols-outlined text-primary">monitoring</span>
<span class="text-label-md text-on-surface">血压</span>
</button>
<button class="bg-surface-container-low p-3 rounded-2xl flex flex-col items-center gap-1 active-scale transition-all">
<span class="material-symbols-outlined text-primary">restaurant</span>
<span class="text-label-md text-on-surface">饮食</span>
</button>
<button class="bg-surface-container-low p-3 rounded-2xl flex flex-col items-center gap-1 active-scale transition-all">
<span class="material-symbols-outlined text-primary">exercise</span>
<span class="text-label-md text-on-surface">运动</span>
</button>
</div>
<h4 class="text-label-md font-bold text-on-surface mb-3">今日控糖任务</h4>
<div class="space-y-3">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<div class="w-8 h-8 rounded-full border-2 border-primary flex items-center justify-center">
<span class="material-symbols-outlined text-[16px] text-primary" style="font-variation-settings: 'wght' 700;">check</span>
</div>
<span class="text-body-md text-on-surface">测血糖血压</span>
</div>
<div class="flex items-center gap-3">
<span class="text-label-md text-primary font-bold">+10积分</span>
<span class="text-outline text-label-md font-bold italic">已完成</span>
</div>
</div>
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<div class="w-8 h-8 rounded-full border-2 border-outline-variant/30 flex items-center justify-center">
<span class="w-2 h-2 rounded-full bg-outline-variant"></span>
</div>
<span class="text-body-md text-on-surface">记今日饮食</span>
</div>
<div class="flex items-center gap-3">
<span class="text-label-md text-primary font-bold">+10积分</span>
<button class="bg-on-surface text-surface rounded-full px-4 py-1 text-label-md font-bold active-scale transition-all">去记录</button>
</div>
</div>
</div>
<!-- Optimized Level System & Gamification -->
<div class="mt-8 bg-gradient-to-br from-primary/5 to-primary/20 rounded-3xl p-6 border border-primary/10 relative overflow-hidden">
<!-- Level Indicator Header -->
<div class="flex justify-between items-end mb-6">
<div class="space-y-1">
<div class="flex items-center gap-2">
<span class="bg-primary text-on-primary text-[10px] px-2 py-0.5 rounded-full font-extrabold" id="level-label">Lv.4</span>
<span class="text-body-lg font-extrabold text-primary" id="stage-name">枝繁叶茂</span>
</div>
<p class="text-[11px] text-primary/60">还差 <span class="font-bold">15 XP</span> 升级至 Lv.4</p>
</div>
<div class="text-right">
<span class="text-label-md font-bold text-primary" id="xp-text">5/50 XP</span>
</div>
</div>
<!-- Progress Bar -->
<div class="w-full h-2.5 bg-white/50 rounded-full mb-8 overflow-hidden backdrop-blur-sm border border-white/20">
<div class="h-full bg-gradient-to-r from-primary to-primary-container rounded-full transition-all duration-500 shadow-inner" id="xp-progress" style="width: 10%;"></div>
</div>
<!-- Plant Growth Display -->
<div class="flex items-center justify-center py-8 relative min-h-[160px]">
<!-- Watering Animation Container -->
<div class="absolute top-0 left-0 w-full h-full pointer-events-none z-10" id="rain-container"></div>
<!-- Visual Stage Indicator Background -->
<div class="absolute inset-0 flex items-center justify-center opacity-10 pointer-events-none">
<span class="text-[120px] material-symbols-outlined text-primary" style="font-variation-settings: 'FILL' 1;">eco</span>
</div>
<!-- Current Plant State -->
<div class="relative z-20 transition-all duration-300" id="plant-container">
<span class="text-7xl drop-shadow-2xl filter block" id="plant-emoji">🪴</span>
</div>
</div>
<!-- Watering Action -->
<div class="relative z-30">
<button class="w-full bg-primary text-on-primary py-4 rounded-2xl text-body-lg font-bold flex items-center justify-center gap-3 shadow-lg active:scale-95 transition-all active:bg-primary-container group" id="water-btn">
<span class="material-symbols-outlined text-[24px] transition-transform group-active:-rotate-12 group-hover:scale-110">opacity</span>
给小树浇水
</button>
<p class="text-[11px] text-center text-outline mt-3 italic">浇水可消耗 5 积分,获得 10 XP</p>
</div>
</div>
</section>
<!-- Social Interaction -->
<section class="bg-surface-container-lowest rounded-3xl p-card-padding ambient-shadow flex items-center justify-between">
<div class="flex items-center gap-3">
<div class="w-10 h-10 bg-tertiary-container/20 rounded-2xl flex items-center justify-center">
<span class="material-symbols-outlined text-tertiary" style="font-variation-settings: 'FILL' 1;">favorite</span>
</div>
<div>
<h4 class="text-body-md font-bold text-on-surface">邀请家人点赞</h4>
<p class="text-[12px] text-outline">分享战报给家人,邀请他们为您点赞鼓励</p>
</div>
</div>
<span class="material-symbols-outlined text-outline">chevron_right</span>
</section>
<!-- Status Tip -->
<div class="flex items-start gap-3 px-2">
<span class="material-symbols-outlined text-primary-container text-[20px]">chat_bubble</span>
<div>
<p class="text-body-md text-on-surface">先完成血糖记录,<span class="text-primary font-bold">今日第一格就亮起来</span></p>
<button class="text-outline text-label-md hover:text-primary transition-colors">轻触换一句鼓励 </button>
</div>
</div>
<!-- Care List Horizontal -->
<div class="flex items-center gap-3 overflow-x-auto hide-scrollbar -mx-container-margin px-container-margin pb-2">
<div class="shrink-0 flex items-center gap-2 bg-white px-3 py-2 rounded-xl shadow-sm border border-outline-variant/20">
<span class="text-label-md text-outline">就诊卡</span>
<span class="text-label-md font-extrabold text-on-surface">10 张</span>
</div>
<div class="shrink-0 flex items-center gap-2 bg-primary text-on-primary px-3 py-2 rounded-xl shadow-md">
<span class="text-label-md font-bold">吕帅</span>
<span class="text-[10px] bg-white/20 rounded px-1">23岁</span>
</div>
<div class="shrink-0 flex items-center gap-2 bg-surface-container-high text-on-surface-variant px-3 py-2 rounded-xl border border-outline-variant/30">
<span class="text-label-md font-bold">霍爱玲</span>
<span class="text-[10px] text-outline">57岁</span>
</div>
</div>
</main>
<!-- Footer Note -->
<footer class="mt-8 text-center px-container-margin pb-12">
<p class="text-[11px] text-outline-variant/60">数据每日同步 · 阈值仅作参考,请遵医嘱</p>
</footer>
<!-- Navigation Bar -->
<script>
// State Management
let currentXP = 35;
let currentLevel = 3;
let totalPoints = 120;
const maxLevel = 10;
const xpPerLevel = 50;
const stageNames = [
"种子眠", "破土而出", "茁壮成长", "枝繁叶茂", "初绽花蕾",
"繁花似锦", "硕果初步", "丰收在望", "参天大树", "森林守护"
];
const plantEmojis = [
"🫘", "🌱", "🌿", "🪴", "🎍",
"🌸", "🍒", "🍎", "🌳", "🌲"
];
// DOM Elements
const waterBtn = document.getElementById('water-btn');
const rainContainer = document.getElementById('rain-container');
const plantContainer = document.getElementById('plant-container');
const plantEmoji = document.getElementById('plant-emoji');
const xpProgress = document.getElementById('xp-progress');
const xpText = document.getElementById('xp-text');
const levelLabel = document.getElementById('level-label');
const stageNameLabel = document.getElementById('stage-name');
const totalPointsLabel = document.getElementById('total-points');
// Watering Animation Logic
function createRain() {
for (let i = 0; i < 15; i++) {
const droplet = document.createElement('div');
droplet.className = 'droplet';
droplet.style.left = `${Math.random() * 100}%`;
droplet.style.animationDelay = `${Math.random() * 0.4}s`;
rainContainer.appendChild(droplet);
// Cleanup
setTimeout(() => droplet.remove(), 600);
}
}
function triggerGrowthPulse() {
plantContainer.classList.add('growth-pulse');
setTimeout(() => {
plantContainer.classList.remove('growth-pulse');
}, 800);
}
function createSparkles() {
for (let i = 0; i < 12; i++) {
const sparkle = document.createElement('span');
sparkle.className = 'material-symbols-outlined shimmer-particle';
sparkle.textContent = 'sparkles';
sparkle.style.left = `${40 + (Math.random() - 0.5) * 40}%`;
sparkle.style.top = `${40 + (Math.random() - 0.5) * 40}%`;
sparkle.style.animationDuration = `${0.6 + Math.random() * 0.4}s`;
plantContainer.appendChild(sparkle);
setTimeout(() => sparkle.remove(), 1000);
}
}
function triggerSugarDecreaseEffect() {
// 1. Floating Text
const floatingText = document.createElement('div');
floatingText.className = 'sugar-decrease-text';
floatingText.textContent = '-0.5 mmol/L';
// Randomize spawn position near the plant
const rect = plantContainer.getBoundingClientRect();
floatingText.style.left = `${rect.left + rect.width / 2}px`;
floatingText.style.top = `${rect.top}px`;
document.body.appendChild(floatingText);
setTimeout(() => floatingText.remove(), 2000);
// 2. Full-screen Downward Particles
for (let i = 0; i < 20; i++) {
setTimeout(() => {
const particle = document.createElement('div');
particle.className = 'sugar-particle text-primary-container';
// Mix arrows and symbols
particle.innerHTML = Math.random() > 0.5 ? '<span class="material-symbols-outlined">south</span>' : '▼';
particle.style.left = `${Math.random() * 100}vw`;
particle.style.fontSize = `${16 + Math.random() * 16}px`;
particle.style.opacity = '0';
document.body.appendChild(particle);
setTimeout(() => particle.remove(), 1500);
}, i * 50);
}
}
function updateDisplay() {
const progress = (currentXP / xpPerLevel) * 100;
xpProgress.style.width = `${progress}%`;
xpText.textContent = `${currentXP}/${xpPerLevel} XP`;
levelLabel.textContent = `Lv.${currentLevel}`;
stageNameLabel.textContent = stageNames[currentLevel - 1];
plantEmoji.textContent = plantEmojis[currentLevel - 1];
totalPointsLabel.textContent = totalPoints;
}
// Action Logic
waterBtn.addEventListener('click', () => {
if (totalPoints < 5) {
alert("积分不足,快去完成任务领积分吧!");
return;
}
// Lock button briefly
waterBtn.disabled = true;
waterBtn.classList.add('opacity-50');
// 1. Consume Points
totalPoints -= 5;
// 2. Trigger Animations
createRain();
setTimeout(() => {
triggerGrowthPulse();
createSparkles();
triggerSugarDecreaseEffect(); // New Effect
// 3. Update State
currentXP += 10;
if (currentXP >= xpPerLevel && currentLevel < maxLevel) {
currentXP -= xpPerLevel;
currentLevel++;
}
updateDisplay();
// Unlock button
waterBtn.disabled = false;
waterBtn.classList.remove('opacity-50');
}, 600);
});
// Initialize
updateDisplay();
// Simple button micro-interactions for others
document.querySelectorAll('button:not(#water-btn)').forEach(btn => {
btn.addEventListener('click', () => {
if(!btn.classList.contains('cursor-not-allowed')) {
// Interaction logic
}
});
});
</script>
</body></html>
File diff suppressed because it is too large Load Diff
+30 -10
View File
@@ -419,7 +419,7 @@
<view v-else class="ai-diet-body" :class="{ 'is-streaming': dietAiStreaming }">
<view class="ai-diet-section-head">
<text class="ai-diet-section-title">今日三餐</text>
<text class="ai-diet-section-title">今日饮食</text>
<text v-if="dietAiStreaming" class="ai-diet-section-tag">生成中</text>
</view>
<view class="ai-diet-meal-list">
@@ -434,6 +434,20 @@
{{ dietAiRecommend.breakfast || (dietAiLoading ? '…' : '—') }}<text v-if="dietAiLoading && dietAiStreamField === 'breakfast'" class="ai-stream-cursor"></text>
</text>
</view>
<view
v-if="dietAiRecommend.drinks || (dietAiLoading && dietAiStreamField === 'drinks')"
class="ai-diet-meal-card meal-drinks"
>
<view class="ai-diet-meal-head">
<view class="ai-diet-meal-icon" aria-hidden="true">
<TongjiIcon name="droplet" size="sm" color="#ffffff" />
</view>
<text class="ai-diet-meal-label">喝的</text>
</view>
<text class="ai-diet-meal-text">
{{ dietAiRecommend.drinks || '…' }}<text v-if="dietAiLoading && dietAiStreamField === 'drinks'" class="ai-stream-cursor"></text>
</text>
</view>
<view class="ai-diet-meal-card meal-lunch">
<view class="ai-diet-meal-head">
<view class="ai-diet-meal-icon" aria-hidden="true">
@@ -2657,16 +2671,13 @@ async function bootstrap(options) {
if (!target) target = list[0]
await applyCard(target)
} else {
if (passedDiagnosisId) diagnosisId.value = passedDiagnosisId
if (passedPatientId) patientId.value = passedPatientId
if (passedPatientName) patientName.value = passedPatientName
if (passedAge) age.value = passedAge
if (passedGender) gender.value = passedGender
if (!diagnosisId.value || !patientId.value) {
readUserContext()
}
diagnosisId.value = 0
patientId.value = 0
patientName.value = ''
age.value = 0
gender.value = 0
readUserMobile()
if (!diagnosisId.value && userMobile.value) {
if (userMobile.value) {
await ensureDailyContextByPhone()
}
}
@@ -6558,6 +6569,12 @@ async function onFamilyLike() {
.ai-diet-meal-card.meal-breakfast::before {
background: linear-gradient(180deg, #fbbf24 0%, #f59e0b 100%);
}
.ai-diet-meal-card.meal-drinks {
background: linear-gradient(120deg, rgba(14, 165, 233, 0.14) 0%, rgba(255, 255, 255, 0) 45%);
}
.ai-diet-meal-card.meal-drinks::before {
background: linear-gradient(180deg, #38bdf8 0%, #0ea5e9 100%);
}
.ai-diet-meal-card.meal-lunch {
background: linear-gradient(120deg, rgba(16, 185, 129, 0.16) 0%, rgba(255, 255, 255, 0) 45%);
}
@@ -6588,6 +6605,9 @@ async function onFamilyLike() {
.meal-breakfast .ai-diet-meal-icon {
background: linear-gradient(150deg, #fde68a 0%, #fbbf24 100%);
}
.meal-drinks .ai-diet-meal-icon {
background: linear-gradient(150deg, #7dd3fc 0%, #0ea5e9 100%);
}
.meal-lunch .ai-diet-meal-icon {
background: linear-gradient(150deg, #6ee7b7 0%, #10b981 100%);
}
File diff suppressed because it is too large Load Diff
+481 -166
View File
@@ -45,115 +45,185 @@
</view>
</scroll-view>
<view class="st-main">
<!-- 1. 录入今日血糖 -->
<view class="st-cta" @click="openInputForm()">
<TongjiIcon name="plus" size="md" color="#FFFFFF" />
<text>录入今日血糖</text>
<!-- 快捷建档失败提示toast 字数有限完整文案展示在页面 -->
<view v-if="dailyContextError && !diagnosisId" class="st-daily-context-error" role="alert">
<text class="st-daily-context-error-text">{{ dailyContextError }}</text>
</view>
<!-- 2. 血糖趋势 -->
<view v-if="diagnosisId" class="st-chart-card">
<!-- 2. 血糖趋势无就诊卡或累计录入不足 7 天显示示例曲线 -->
<view class="st-chart-card">
<view class="st-chart-head">
<view>
<view class="st-chart-head-text">
<text class="st-chart-title">血糖趋势</text>
<text class="st-chart-sub">{{ rangeText }} · mmol/L</text>
</view>
<view class="st-chart-legend">
<view
class="st-legend-item"
:class="{ inactive: !chartSeriesVisible.fasting }"
@click="toggleChartSeries('fasting')"
>
<view class="st-dot st-dot-fasting" />
<text>空腹</text>
<view class="st-chart-toolbar">
<view class="st-chart-legend">
<view
class="st-legend-item"
:class="{ inactive: !chartSeriesVisible.fasting }"
@click="toggleChartSeries('fasting')"
>
<view class="st-dot st-dot-fasting" />
<text>空腹</text>
</view>
<view
class="st-legend-item"
:class="{ inactive: !chartSeriesVisible.postprandial }"
@click="toggleChartSeries('postprandial')"
>
<view class="st-dot st-dot-post" />
<text>餐后</text>
</view>
</view>
<view
class="st-legend-item"
:class="{ inactive: !chartSeriesVisible.postprandial }"
@click="toggleChartSeries('postprandial')"
v-if="bloodCareEligible"
class="st-chart-ai-chip"
:class="{
active: bloodCarePanelOpen,
loading: bloodCareLoading,
hint: !bloodCarePanelOpen && !bloodCareLoading
}"
role="button"
aria-label="AI 照护建议"
@click.stop="toggleBloodCareFromChart"
>
<view class="st-dot st-dot-post" />
<text>餐后</text>
<view class="st-chart-ai-chip-inner">
<view class="st-chart-ai-chip-icon">
<TongjiIcon
name="sparkles"
size="sm"
:color="bloodCarePanelOpen ? '#ffffff' : '#006c49'"
/>
</view>
<text class="st-chart-ai-chip-label">{{
bloodCareLoading
? '生成中'
: (bloodCarePanelOpen ? '收起建议' : '照护建议')
}}</text>
</view>
</view>
</view>
</view>
<view v-if="bloodHasData" class="st-chart-summary">
<view class="st-chart-summary-item">
<text class="st-chart-summary-num is-high">{{ bloodHighDayCount }}</text>
<text class="st-chart-summary-label">天偏高</text>
</view>
<view class="st-chart-summary-item">
<text class="st-chart-summary-num is-good">{{ bloodNormalDayCount }}</text>
<text class="st-chart-summary-label">天正常</text>
</view>
<view class="st-chart-summary-item">
<text class="st-chart-summary-num">{{ bloodRecordDayCount }}</text>
<text class="st-chart-summary-label">天有记录</text>
</view>
</view>
<view class="st-chart-area">
<view class="st-chart-area" @click="onChartTap">
<canvas
v-if="!recordOverlayOpen"
id="bloodWaveChart"
type="2d"
class="st-chart-canvas"
@touchstart="onChartTouch"
@touchmove="onChartTouch"
@click.stop="onChartTap"
/>
<view v-if="recordOverlayOpen" class="st-chart-placeholder">
<text>图表已暂停显示</text>
</view>
<view v-if="!bloodHasData && !loading && !recordOverlayOpen" class="st-chart-empty">
<view v-if="!chartHasDisplayData && !loading && !recordOverlayOpen" class="st-chart-empty">
<text>当前时间范围暂无血糖数据</text>
</view>
<cover-view v-if="hoverInfo && !recordOverlayOpen" class="chart-tip" :style="{ left: hoverInfo.left + 'px', top: hoverInfo.top + 'px' }">
<cover-view class="chart-tip-date">{{ hoverInfo.date }}</cover-view>
<cover-view v-if="hoverInfo.fasting != null" class="chart-tip-row">
<cover-view class="st-dot st-dot-fasting" />
<cover-view>空腹 {{ hoverInfo.fasting }}</cover-view>
</cover-view>
<cover-view v-if="hoverInfo.postprandial != null" class="chart-tip-row">
<cover-view class="st-dot st-dot-post" />
<cover-view>餐后 {{ hoverInfo.postprandial }}</cover-view>
</cover-view>
</cover-view>
</view>
<view v-if="chartUseMockData" class="st-chart-foot">
<text class="st-chart-mock-hint">示例数据累计记录满 7 天后显示您的真实趋势</text>
</view>
<!-- 趋势卡内点击标题栏 AI 按钮后展开 -->
<view v-if="bloodCareEligible && bloodCarePanelOpen" class="st-chart-care-panel">
<view class="st-chart-care-head">
<text class="st-chart-care-title">AI 照护建议</text>
<view class="st-chart-care-actions">
<view
class="st-chart-care-refresh"
:class="{ disabled: bloodCareLoading }"
@click.stop="refreshBloodCareAi"
>
<TongjiIcon name="refresh" size="sm" color="#006c49" />
<text>{{ bloodCareLoading ? '生成中' : '换一换' }}</text>
</view>
<view class="st-chart-care-close" @click.stop="closeBloodCarePanel">
<text>收起</text>
</view>
</view>
</view>
<text class="st-chart-care-sub">基于您累计 7 天血糖 · 监测与中医调理方向非处方</text>
<view v-if="bloodCareControlLabel" class="st-care-badge st-care-badge--in-chart">
<text>{{ bloodCareControlLabel }}</text>
</view>
<view v-if="bloodCareLoading && !hasBloodCareContent" class="st-skeleton st-skeleton--in-chart">
<view class="st-skeleton-line" />
<view class="st-skeleton-line short" />
</view>
<view v-else-if="hasBloodCareContent" class="st-care-cards st-care-cards--in-chart">
<view class="st-meal-glass st-care-block">
<view class="st-meal-head">
<text class="st-meal-label">总评</text>
</view>
<text class="st-meal-text">
{{ bloodCarePlan.summary || '…' }}<text v-if="bloodCareLoading && bloodCareStreamField === 'summary'" class="ai-stream-cursor"></text>
</text>
</view>
<view class="st-meal-glass st-care-block">
<view class="st-meal-head">
<text class="st-meal-label">血糖管理</text>
</view>
<text class="st-meal-text">
{{ bloodCarePlan.blood_advice || '…' }}<text v-if="bloodCareLoading && bloodCareStreamField === 'blood_advice'" class="ai-stream-cursor"></text>
</text>
</view>
<view class="st-meal-glass st-care-block st-care-block--tcm">
<view class="st-meal-head">
<text class="st-meal-label">中医调理</text>
</view>
<text class="st-meal-text">
{{ bloodCarePlan.tcm_plan || '…' }}<text v-if="bloodCareLoading && bloodCareStreamField === 'tcm_plan'" class="ai-stream-cursor"></text>
</text>
</view>
<view v-if="bloodCarePlan.watch_points && bloodCarePlan.watch_points.length" class="st-care-watch">
<text class="st-care-watch-title">留意</text>
<view v-for="(item, wi) in bloodCarePlan.watch_points" :key="wi" class="st-care-watch-item">
<text class="st-care-watch-dot">·</text>
<text class="st-care-watch-text">{{ item }}</text>
</view>
</view>
<view v-if="bloodCarePlan.next_steps" class="st-meal-glass st-care-block st-care-block--next">
<view class="st-meal-head">
<text class="st-meal-label">复诊提醒</text>
</view>
<text class="st-meal-text">
{{ bloodCarePlan.next_steps }}<text v-if="bloodCareLoading && bloodCareStreamField === 'next_steps'" class="ai-stream-cursor"></text>
</text>
</view>
</view>
<view v-if="bloodCareAnalysis.length" class="st-care-stats st-care-stats--in-chart">
<view v-for="(s, si) in bloodCareAnalysis" :key="si" class="st-care-stat-chip">
<text class="st-care-stat-label">{{ s.label }}</text>
<text class="st-care-stat-val">记录 {{ s.record_days }} · 达标 {{ s.compliance }}%</text>
</view>
</view>
<text v-if="bloodCareDisclaimer" class="st-care-disclaimer st-care-disclaimer--in-chart">{{ bloodCareDisclaimer }}</text>
</view>
<view class="st-game-entry st-game-entry--in-card" @click="goGamePage" style="margin: 0 20rpx 0 20rpx;">
<view class="st-game-entry-icon">
<TongjiIcon name="activity" size="sm" color="#006c49" />
</view>
<view class="st-game-entry-body">
<text class="st-game-entry-title">识糖小课堂</text>
<text class="st-game-entry-sub">认一认哪些食物含糖高</text>
</view>
<TongjiIcon name="chevron-right" size="sm" color="#64748b" />
</view>
</view>
<!-- 3. 最新测量 & 概览 -->
<view class="st-latest-section">
<view
v-if="latestMeasurement.hasValue"
class="st-latest-hero"
:class="{ 'is-high': latestMeasurement.isHigh }"
>
<view class="st-latest-hero-pattern" aria-hidden="true" />
<text class="st-latest-label">最新测量 · {{ latestMeasurement.typeLabel }}</text>
<view class="st-latest-value-row">
<text class="st-latest-value">{{ latestMeasurement.value }}</text>
<text class="st-latest-unit">mmol/L</text>
</view>
<view v-if="latestMeasurement.isHigh" class="st-latest-flag">
<TongjiIcon name="alert-triangle" size="sm" color="#93000a" />
<text>偏高请遵医嘱</text>
</view>
</view>
<view class="st-summary-grid">
<view class="st-summary-card">
<text class="st-summary-label">空腹</text>
<text class="st-summary-value" :class="{ 'is-high': elderFasting.isHigh }">{{ elderFasting.value }}</text>
<text v-if="elderFasting.isHigh" class="st-summary-tag">偏高</text>
</view>
<view class="st-summary-card">
<text class="st-summary-label">餐后</text>
<text class="st-summary-value" :class="{ 'is-high': elderPostprandial.isHigh }">{{ elderPostprandial.value }}</text>
<text v-if="elderPostprandial.isHigh" class="st-summary-tag">偏高</text>
</view>
</view>
<view class="st-training-section">
<dev-training-entry inline tone="weekly" />
</view>
<!-- 4. AI 饮食建议 -->
<!-- 3. AI 饮食建议 -->
<view v-if="diagnosisId" class="st-ai-section">
<view class="st-ai-deco" aria-hidden="true" />
<view class="st-ai-head">
@@ -172,7 +242,7 @@
<text>{{ dietAiLoading ? '生成中' : '换一换' }}</text>
</view>
</view>
<text class="st-ai-sub">读您近 7 · 30 天血糖定制今日三餐</text>
<text class="st-ai-sub">读您近 7 · 30 天血糖定制早餐含驼奶粉喝的与三餐</text>
<view v-if="dietAiLoading && !hasDietAiMealContent && !dietAiStreaming && !dietAiReplacing" class="st-skeleton">
<view v-for="n in 2" :key="'sk-' + n" class="st-skeleton-line" />
@@ -188,6 +258,18 @@
{{ dietAiRecommend.breakfast || (dietAiLoading ? '…' : '—') }}<text v-if="dietAiLoading && dietAiStreamField === 'breakfast'" class="ai-stream-cursor"></text>
</text>
</view>
<view
v-if="dietAiRecommend.drinks || (dietAiLoading && dietAiStreamField === 'drinks')"
class="st-meal-glass st-meal-glass--drinks"
>
<view class="st-meal-head">
<TongjiIcon name="droplet" size="sm" color="#0ea5e9" />
<text class="st-meal-label">喝的</text>
</view>
<text class="st-meal-text">
{{ dietAiRecommend.drinks || '…' }}<text v-if="dietAiLoading && dietAiStreamField === 'drinks'" class="ai-stream-cursor"></text>
</text>
</view>
<view class="st-meal-glass">
<view class="st-meal-head">
<TongjiIcon name="leaf" size="sm" color="#006c49" />
@@ -211,9 +293,17 @@
<view class="st-main-spacer" />
</view>
<!-- 收起后的重新唤起按钮 -->
<view v-if="diagnosisId && floatAskClosed" class="st-float-reopen" @click="floatAskClosed = false">
<TongjiIcon name="sparkles" size="sm" color="#ffffff" />
</view>
<!-- 底部浮动 AI 输入栏 -->
<view v-if="diagnosisId" class="st-float-ask">
<view v-if="diagnosisId && !floatAskClosed" class="st-float-ask">
<view class="st-float-close" @click="floatAskClosed = true">
<text class="st-float-close-icon">×</text>
</view>
<view class="st-float-inner">
<view class="st-float-icon">
<TongjiIcon name="sparkles" size="sm" color="#006c49" />
@@ -222,12 +312,21 @@
class="st-float-input"
type="text"
:value="dietAiAskText"
placeholder="输入食物名称或健康疑问,咨询 AI..."
placeholder="长按麦克风说话,或输入文字咨询 AI..."
placeholder-class="st-float-placeholder"
confirm-type="send"
@input="onDietAiAskInput"
@confirm="askDietAiFood"
/>
<view
class="st-float-mic"
:class="{ listening: sttListening || sttHolding, disabled: dietAiAsking || !sttSupported }"
@touchstart.stop.prevent="onDietAiSpeechStart"
@touchend.stop.prevent="onDietAiSpeechEnd"
@touchcancel.stop.prevent="onDietAiSpeechEnd"
>
<TongjiIcon name="mic" size="sm" :color="(sttListening || sttHolding) ? '#ffffff' : '#006c49'" />
</view>
<view
class="st-float-send"
:class="{ disabled: dietAiAsking }"
@@ -236,6 +335,9 @@
<TongjiIcon name="send" size="sm" color="#ffffff" />
</view>
</view>
<view v-if="sttHolding && !dietAiAsking" class="st-float-hold-hint">
<text>{{ sttListening ? '松手结束' : '准备录音…' }}</text>
</view>
<view v-if="dietAiAsking || dietAiAskResult.advice" class="st-float-result">
<view v-if="dietAiAskResult.level_label" class="st-float-badge">
<text>{{ dietAiAskResult.level_label }}</text>
@@ -246,6 +348,17 @@
</view>
</view>
<!-- 语音长按状态浮层居中显示避免被键盘/底栏遮挡 -->
<view v-if="sttHolding && !dietAiAsking" class="st-voice-overlay">
<view class="st-voice-card">
<view class="st-voice-icon" :class="{ active: sttListening }">
<TongjiIcon name="mic" size="lg" color="#ffffff" />
</view>
<text class="st-voice-title">{{ sttListening ? '正在聆听…' : '准备录音…' }}</text>
<text class="st-voice-sub">松开手指结束并识别</text>
</view>
</view>
<!-- 无就诊卡授权手机号后可录入 -->
<view v-if="phoneGateVisible" class="input-modal-mask" @click="closePhoneGate">
<view class="input-modal phone-gate-modal" @click.stop>
@@ -260,6 +373,9 @@
</view>
</view>
<view class="phone-gate-body">
<view v-if="dailyContextError" class="phone-gate-error" role="alert">
<text>{{ dailyContextError }}</text>
</view>
<text class="phone-gate-tip">微信仅提供手机号请选择性别以便写入就诊档案</text>
<view class="phone-gate-gender">
<text class="phone-gate-gender-label">性别</text>
@@ -295,7 +411,7 @@
<!-- 录入今日数据弹层 -->
<view v-if="inputForm.visible" class="input-modal-mask" @click="closeInputForm">
<view class="input-modal" @click.stop>
<view class="input-modal" :style="keyboardHeight ? { marginBottom: keyboardHeight + 'px' } : {}" @click.stop>
<view class="input-modal-grip" />
<view class="input-modal-head">
<view class="input-modal-title-wrap">
@@ -324,6 +440,7 @@
<view class="input-field-input">
<input
type="digit"
:adjust-position="false"
:value="inputForm.fasting_blood_sugar"
placeholder="如 6.5"
placeholder-class="input-placeholder"
@@ -339,6 +456,7 @@
<view class="input-field-input">
<input
type="digit"
:adjust-position="false"
:value="inputForm.postprandial_blood_sugar"
placeholder="如 8.0"
placeholder-class="input-placeholder"
@@ -354,6 +472,7 @@
<view class="input-field-input">
<input
type="digit"
:adjust-position="false"
:value="inputForm.other_blood_sugar"
placeholder="如 7.2"
placeholder-class="input-placeholder"
@@ -370,6 +489,7 @@
placeholder-class="input-placeholder"
:show-confirm-bar="false"
:auto-height="true"
:adjust-position="false"
@input="onInputField('remark', $event)"
/>
</view>
@@ -395,6 +515,7 @@
</view>
</view>
</block>
<TabBarDock :active="1" />
</view>
</template>
@@ -404,6 +525,10 @@ import { onLoad, onShow, onHide, onUnload, onPullDownRefresh, onShareAppMessage
import SugarTreeGraphic from '../components/SugarTreeGraphic.vue'
import TongjiIcon from '../components/TongjiIcon.vue'
import { useDietAi } from '../composables/useDietAi.js'
import { useBloodCareAi } from '../composables/useBloodCareAi.js'
import { useSpeechToText } from '../composables/useSpeechToText.js'
import TabBarDock from '@/components/app-tab-bar/tab-bar-dock.vue'
import DevTrainingEntry from '@/components/dev-training-entry/index.vue'
const { proxy } = getCurrentInstance()
@@ -436,11 +561,42 @@ function showUserToast(title, options = {}) {
uni.showToast({
title: text,
icon: options.icon || 'none',
duration: options.duration
duration: options.duration || 2800
})
}
/** 解析接口错误文案(兼容 msg / message / data.error */
function extractApiError(res, fallback = '') {
if (!res) return fallback
const raw = res.msg ?? res.message ?? res.data?.error ?? res.data?.msg ?? res.errMsg
return formatUserMessage(raw, fallback)
}
const diagnosisId = ref(0)
/** 手机号快捷建档失败:页面常驻提示(避免 toast 被 7 字截断) */
const dailyContextError = ref('')
function clearDailyContextError() {
dailyContextError.value = ''
}
function reportDailyContextError(res, fallback = '无法开始录入', options = {}) {
const text = extractApiError(res, fallback)
if (!text) return
dailyContextError.value = text
if (options.alert) {
uni.showModal({
title: '无法录入血糖',
content: text,
showCancel: false,
confirmText: '知道了'
})
} else {
const short = text.length > 14 ? `${text.slice(0, 14)}` : text
showUserToast(short, { duration: 3200 })
}
}
const patientId = ref(0)
const patientName = ref('')
const displayPatientName = computed(() => {
@@ -472,6 +628,39 @@ const {
openDietForm
} = useDietAi(proxy, { diagnosisId, showUserToast, formatUserMessage })
/** 底部 AI 浮动卡片是否被用户收起 */
const floatAskClosed = ref(false)
/** 键盘高度(px):录入弹窗据此整体上移,避免输入框被键盘遮挡 */
const keyboardHeight = ref(0)
function onKeyboardHeightChange(res) {
keyboardHeight.value = res && res.height ? Math.round(res.height) : 0
}
const {
sttListening,
sttHolding,
sttSupported,
beginHoldSpeech,
endHoldSpeech,
stopSpeechToText
} = useSpeechToText({
onResult(text) {
const prev = String(dietAiAskText.value || '').trim()
dietAiAskText.value = prev ? `${prev} ${text}` : text
},
showToast: (msg) => showUserToast(msg)
})
function onDietAiSpeechStart() {
if (dietAiAsking.value) return
beginHoldSpeech()
}
function onDietAiSpeechEnd() {
endHoldSpeech()
}
/** AI 饮食建议依据:近7天 / 近30天 血糖统计 */
const dietAnalysisList = computed(() => {
const list = dietAiRecommend.value && Array.isArray(dietAiRecommend.value.analysis)
@@ -603,6 +792,19 @@ const currentRange = computed(() => {
return { start: formatDate(start), end: formatDate(end) }
})
/** 拉取血糖时向前多看几天,用于判断「累计满 7 天」是否达标(图表仍只画 currentRange */
const BLOOD_LOOKBACK_DAYS = 30
const fetchRange = computed(() => {
const displayDays = rangeMode.value === '30' ? 30 : 7
const lookback = Math.max(BLOOD_LOOKBACK_DAYS, displayDays)
const end = new Date()
end.setHours(0, 0, 0, 0)
const start = new Date(end)
start.setDate(start.getDate() - (lookback - 1))
return { start: formatDate(start), end: formatDate(end) }
})
const visibleDates = computed(() => {
const { start, end } = currentRange.value
if (!start || !end) return []
@@ -765,18 +967,94 @@ const recordDays = computed(() => {
return result
})
function bloodDayHasSugar(b) {
if (!b) return false
return toNumber(b.fasting_blood_sugar) !== null
|| toNumber(b.postprandial_blood_sugar) !== null
|| toNumber(b.other_blood_sugar) !== null
}
const bloodHasData = computed(() => {
return chartDates.value.some((date) => {
const b = bloodByDate.value[date]
return b && (toNumber(b.fasting_blood_sugar) !== null || toNumber(b.postprandial_blood_sugar) !== null)
})
return chartDates.value.some((date) => bloodDayHasSugar(bloodByDate.value[date]))
})
/** 当前图表时间范围内有真实录入的天数(用于小结统计) */
const bloodRecordDayCount = computed(() => {
return chartDates.value.filter((date) => {
const b = bloodByDate.value[date]
return b && (toNumber(b.fasting_blood_sugar) !== null || toNumber(b.postprandial_blood_sugar) !== null)
}).length
return chartDates.value.filter((date) => bloodDayHasSugar(bloodByDate.value[date])).length
})
/** 拉取窗口内累计有血糖录入的天数(不要求连续) */
const bloodCumulativeDayCount = computed(() => {
let n = 0
for (const date of Object.keys(bloodByDate.value)) {
if (bloodDayHasSugar(bloodByDate.value[date])) n++
}
return n
})
const REAL_BLOOD_DAYS_MIN = 7
/** 无就诊卡或累计录入不足 7 天时,图表用示例数据 */
const chartUseMockData = computed(() => {
if (!diagnosisId.value) return true
return bloodCumulativeDayCount.value < REAL_BLOOD_DAYS_MIN
})
/** 示例曲线:7 天内有波动的空腹/餐后,便于老人理解趋势图形态 */
const mockBloodByDate = computed(() => {
const map = {}
const dates = chartDates.value
dates.forEach((date, i) => {
const t = dates.length > 1 ? i / (dates.length - 1) : 0
const fasting = Number((5.2 + Math.sin(t * Math.PI) * 0.5 + (i % 2) * 0.15).toFixed(1))
const post = Number((7.6 + Math.cos(t * Math.PI) * 1.1 + (i % 3) * 0.25).toFixed(1))
const row = {
record_date: date,
fasting_blood_sugar: fasting,
postprandial_blood_sugar: post,
other_blood_sugar: '',
systolic_pressure: '',
diastolic_pressure: ''
}
row.isHighFasting = isHighFasting(row.fasting_blood_sugar)
row.isHighPostprandial = isHighPostprandial(row.postprandial_blood_sugar)
row.isHighOther = false
row.isHighBp = false
row.bpText = '—'
map[date] = row
})
return map
})
/** 图表绘制用数据源:示例或真实 */
const chartBloodByDate = computed(() => (
chartUseMockData.value ? mockBloodByDate.value : bloodByDate.value
))
const chartHasDisplayData = computed(() => {
if (chartUseMockData.value) return chartDates.value.length > 0
return bloodHasData.value
})
const {
bloodCarePlan,
bloodCareLoading,
bloodCareStreamField,
bloodCareEligible,
bloodCarePanelOpen,
hasBloodCareContent,
bloodCareAnalysis,
bloodCareControlLabel,
bloodCareDisclaimer,
toggleBloodCareFromChart,
closeBloodCarePanel,
refreshBloodCareAi,
resetBloodCarePlan
} = useBloodCareAi(proxy, {
diagnosisId,
chartUseMockData,
showUserToast,
formatUserMessage
})
const bloodHighDayCount = computed(() => {
@@ -1080,8 +1358,14 @@ function syncPhoneGateSexFromUser() {
}
}
function openPhoneGate() {
async function openPhoneGate() {
syncPhoneGateSexFromUser()
readUserMobile()
if (userMobile.value) {
phoneGateVisible.value = false
await resolveDailyPhoneContext(false)
return
}
phoneGateVisible.value = true
}
@@ -1098,8 +1382,11 @@ async function refreshUserMobile() {
return userMobile.value
}
async function ensureDailyContextByPhone(sex) {
if (diagnosisId.value) return true
async function ensureDailyContextByPhone(sex, options = {}) {
if (diagnosisId.value) {
clearDailyContextError()
return true
}
const payload = {}
const sexVal = sex ?? phoneGateSex.value
@@ -1114,6 +1401,7 @@ async function ensureDailyContextByPhone(sex) {
}, false)
if (res && res.code === 1 && res.data && res.data.diagnosis_id) {
clearDailyContextError()
await applyCard({
id: res.data.diagnosis_id,
patient_id: res.data.patient_id,
@@ -1121,17 +1409,32 @@ async function ensureDailyContextByPhone(sex) {
age: res.data.age || 0,
gender: res.data.gender || 0
})
if (res.data.created) {
if (res.data.created || res.data.linked_existing) {
await fetchCardList()
}
return true
}
if (res && res.data && res.data.need_mobile) {
clearDailyContextError()
return 'need_mobile'
}
showUserToast(formatUserMessage(res?.msg, '无法开始录入'))
reportDailyContextError(res, '无法开始录入', options)
return false
}
/** 已绑定手机号时直接关联档案,不再弹出授权建档 */
async function resolveDailyPhoneContext(openFormAfter = false) {
const ok = await ensureDailyContextByPhone(phoneGateSex.value, { alert: true })
if (ok === true) {
if (openFormAfter) await openInputForm(true)
return true
}
if (ok === 'need_mobile') {
syncPhoneGateSexFromUser()
phoneGateVisible.value = true
}
return false
}
@@ -1144,7 +1447,7 @@ async function ensureCanRecordGlucose() {
return false
}
const ok = await ensureDailyContextByPhone()
const ok = await ensureDailyContextByPhone(undefined, { alert: true })
if (ok === 'need_mobile') {
openPhoneGate()
return false
@@ -1177,6 +1480,13 @@ async function onPhoneGateAuthorize(e) {
phoneBinding.value = true
try {
readUserMobile()
if (userMobile.value) {
phoneGateVisible.value = false
await resolveDailyPhoneContext(true)
return
}
const res = await proxy.apiUrl({
url: '/api/user/getMobileByMnp',
method: 'POST',
@@ -1188,12 +1498,7 @@ async function onPhoneGateAuthorize(e) {
}
await refreshUserMobile()
phoneGateVisible.value = false
const ok = await ensureDailyContextByPhone(phoneGateSex.value)
if (ok === true) {
await openInputForm(true)
} else if (ok === 'need_mobile') {
openPhoneGate()
}
await resolveDailyPhoneContext(true)
} catch (err) {
showUserToast('网络异常,请稍后再试')
} finally {
@@ -1208,6 +1513,7 @@ async function onSelectCard(card) {
exerciseRecords.value = []
trackingNotes.value = []
hoverInfo.value = null
resetBloodCarePlan()
await applyCard(card)
fetchAll()
}
@@ -1216,7 +1522,7 @@ async function onSelectCard(card) {
async function fetchAll() {
if (!hasAuthToken() || !diagnosisId.value) return
loading.value = true
const { start, end } = currentRange.value
const { start, end } = fetchRange.value
try {
const [winRes, noteRes] = await Promise.all([
proxy.apiUrl({
@@ -1264,6 +1570,9 @@ async function fetchAll() {
trackingNotes.value = []
}
await fetchDietAiRecommend(false)
if (!bloodCareEligible.value) {
resetBloodCarePlan()
}
} catch (e) {
uni.showToast({ title: '加载失败,请稍后再试', icon: 'none' })
} finally {
@@ -1290,16 +1599,47 @@ function onRefresh() {
fetchAll()
}
function buildMorePageUrl() {
if (!diagnosisId.value) return '/tongji/pages/more'
const q = [
`diagnosis_id=${diagnosisId.value}`,
patientId.value ? `patient_id=${patientId.value}` : ''
].filter(Boolean).join('&')
return `/tongji/pages/more?${q}`
}
/** 点击血糖趋势图:进入记录页 more(无就诊卡时先手机号快捷建档) */
async function onChartTap() {
if (!diagnosisId.value) {
const ok = await ensureCanRecordGlucose()
if (!ok || !diagnosisId.value) return
}
uni.navigateTo({ url: buildMorePageUrl() })
}
async function goRecordsPage() {
onChartTap()
}
async function goMorePage() {
if (!diagnosisId.value) {
const ok = await ensureCanRecordGlucose()
if (!ok || !diagnosisId.value) return
}
uni.navigateTo({ url: '/tongji/pages/more' })
uni.navigateTo({ url: buildMorePageUrl() })
}
function goGamePage() {
uni.navigateTo({
url: '/tongji/pages/game',
fail() {
uni.showToast({ title: '暂时无法打开游戏', icon: 'none' })
}
})
}
function navToUser() {
uni.switchTab({ url: '/pages/user/user' })
uni.redirectTo({ url: '/pages/user/user' })
}
// ============ / / ============
@@ -1994,7 +2334,7 @@ function scheduleChartRedraw() {
function buildSeries(field) {
return chartDates.value.map((date) => {
const b = bloodByDate.value[date]
const b = chartBloodByDate.value[date]
return b ? toNumber(b[field]) : null
})
}
@@ -2028,7 +2368,7 @@ function drawChart() {
ctx.clearRect(0, 0, w, h)
const padding = { left: 40, right: 18, top: 26, bottom: 36 }
const padding = { left: 40, right: 18, top: 12, bottom: 28 }
const cw = w - padding.left - padding.right
const ch = h - padding.top - padding.bottom
@@ -2284,59 +2624,25 @@ function toggleChartSeries(key) {
scheduleChartRedraw()
}
function onChartTouch(e) {
if (!chartCoords || !canvasWidth) return
if (!chartDates.value.length) return
let touchX = 0
let touchY = 0
if (e && e.touches && e.touches[0]) {
touchX = e.touches[0].x ?? e.touches[0].clientX ?? 0
touchY = e.touches[0].y ?? e.touches[0].clientY ?? 0
}
const { padding, cw, n } = chartCoords
if (touchX < padding.left || touchX > padding.left + cw) {
return
}
const ratio = n <= 1 ? 0 : (touchX - padding.left) / cw
let idx = Math.round(ratio * (n - 1))
if (idx < 0) idx = 0
if (idx > n - 1) idx = n - 1
const date = chartDates.value[idx]
const fp = chartSeriesVisible.value.fasting ? chartCoords.fasting[idx] : null
const pp = chartSeriesVisible.value.postprandial ? chartCoords.postprandial[idx] : null
const anchorX = fp?.x ?? pp?.x ?? padding.left + cw * (idx / Math.max(1, n - 1))
const left = Math.max(8, Math.min(canvasWidth - 130, anchorX - 60))
const top = Math.max(4, touchY - 70)
hoverInfo.value = {
date: date.slice(5),
fasting: fp?.value ?? null,
postprandial: pp?.value ?? null,
left,
top
}
if (hoverTimer) clearTimeout(hoverTimer)
hoverTimer = setTimeout(() => {
hoverInfo.value = null
}, 2500)
}
watch(rangeMode, () => {
scheduleChartRedraw()
})
watch([chartUseMockData, bloodRecords], () => {
scheduleChartRedraw()
})
// ============ ============
function readUserContext() {
try {
const userData = uni.getStorageSync('userData')
if (userData && userData.diagnosis) {
if (!patientId.value) patientId.value = Number(userData.diagnosis.patient_id) || 0
if (!diagnosisId.value) diagnosisId.value = Number(userData.diagnosis.id) || 0
if (!patientName.value) patientName.value = userData.diagnosis.patient_name || ''
if (!age.value) age.value = Number(userData.diagnosis.age) || 0
if (!gender.value) gender.value = Number(userData.diagnosis.gender) || 0
}
if (!userData) return
// diagnosis view_records
const name = String(userData.real_name || userData.nickname || '').trim()
if (!patientName.value && name) patientName.value = name
const sex = Number(userData.sex)
if (!gender.value && (sex === 1 || sex === 2)) gender.value = sex === 1 ? 1 : 0
if (!age.value && userData.age) age.value = Number(userData.age) || 0
} catch (e) {}
}
@@ -2419,23 +2725,25 @@ async function bootstrap(options) {
if (!target) target = list[0]
await applyCard(target)
} else {
if (passedDiagnosisId) diagnosisId.value = passedDiagnosisId
if (passedPatientId) patientId.value = passedPatientId
if (passedPatientName) patientName.value = passedPatientName
if (passedAge) age.value = passedAge
if (passedGender) gender.value = passedGender
if (!diagnosisId.value || !patientId.value) {
readUserContext()
}
// URL/ ID
diagnosisId.value = 0
patientId.value = 0
patientName.value = ''
age.value = 0
gender.value = 0
readUserMobile()
if (!diagnosisId.value && userMobile.value) {
await ensureDailyContextByPhone()
if (userMobile.value) {
await ensureDailyContextByPhone(undefined, { alert: false })
}
}
authChecking.value = false
if (diagnosisId.value) {
await fetchAll()
} else {
bloodRecords.value = []
await nextTick()
scheduleChartRedraw()
}
}
@@ -2445,6 +2753,7 @@ onLoad((options) => {
onShow(() => {
initHeaderSafeArea()
try { uni.onKeyboardHeightChange(onKeyboardHeightChange) } catch (e) {}
if (authChecking.value) return
resetCanvasCache()
nextTick(() => scheduleChartRedraw())
@@ -2459,10 +2768,16 @@ onPullDownRefresh(async () => {
onHide(() => {
ttsStop()
stopSpeechToText()
try { uni.offKeyboardHeightChange(onKeyboardHeightChange) } catch (e) {}
keyboardHeight.value = 0
})
onUnload(() => {
ttsStop()
stopSpeechToText()
try { uni.offKeyboardHeightChange(onKeyboardHeightChange) } catch (e) {}
keyboardHeight.value = 0
})
// ============ ============
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -3,7 +3,7 @@
* 设计 token stitch-weekly.html tailwind.config 一一对应
*/
.weekly-page {
.stitch-vitalmint {
--primary: #006c49;
--on-primary: #ffffff;
--on-primary-fixed-variant: #005236;
@@ -29,6 +29,8 @@
--error-container: #ffdad6;
--tertiary: #a43a3a;
--tertiary-container: #fc7c78;
--on-tertiary: #ffffff;
--on-tertiary-container: #711419;
--st-container-margin: 40rpx;
--st-section-gap: 64rpx;
@@ -39,7 +41,9 @@
--st-radius-xl: 24rpx;
--st-radius-lg: 16rpx;
--st-shadow-ambient: 0 20rpx 60rpx -20rpx rgba(0, 108, 73, 0.08);
}
.weekly-page.stitch-vitalmint {
min-height: 100vh;
background: var(--surface);
color: var(--on-surface);
@@ -5,7 +5,7 @@
inset: 0;
background: rgba(15, 23, 42, 0.45);
backdrop-filter: blur(12px);
z-index: 999;
z-index: 2000;
display: flex;
align-items: flex-end;
justify-content: center;
@@ -89,6 +89,22 @@
padding: 32rpx;
}
.weekly-page .phone-gate-error {
margin-bottom: 20rpx;
padding: 20rpx 22rpx;
border-radius: 16rpx;
background: #fef2f2;
border: 1rpx solid rgba(220, 38, 38, 0.2);
box-sizing: border-box;
text {
font-size: 26rpx;
font-weight: 600;
color: #991b1b;
line-height: 1.45;
}
}
.weekly-page .phone-gate-tip {
display: block;
font-size: 28rpx;
@@ -6,6 +6,8 @@
--st-shadow-ambient: 0 24rpx 60rpx -20rpx rgba(0, 108, 73, 0.08), 0 8rpx 20rpx -8rpx rgba(0, 0, 0, 0.03);
--st-shadow-cta: 0 16rpx 32rpx rgba(0, 108, 73, 0.2);
--st-shadow-float: 0 16rpx 64rpx rgba(0, 108, 73, 0.15);
--tab-dock-full-offset: calc(220rpx + env(safe-area-inset-bottom));
padding-bottom: var(--tab-dock-full-offset);
}
/* ===== Header ===== */
@@ -106,6 +108,27 @@
color: var(--on-primary);
}
/* ===== 居家训练(DEV 内嵌) ===== */
.weekly-page .st-training-section {
width: 100%;
box-sizing: border-box;
}
.weekly-page .st-training-section .dev-entry-inline--weekly {
overflow: hidden;
}
.weekly-page .st-training-section .inline-scroll {
margin-left: -4rpx;
margin-right: -4rpx;
width: calc(100% + 8rpx);
}
.weekly-page .st-training-section .inline-card:first-child {
border-color: rgba(0, 108, 73, 0.22);
background: linear-gradient(160deg, rgba(0, 108, 73, 0.08) 0%, #f4f7f5 55%);
}
/* ===== Main ===== */
.weekly-page .st-main {
padding: 0 var(--st-container-margin) 32rpx;
@@ -115,6 +138,21 @@
margin-top: 16rpx;
}
.weekly-page .st-daily-context-error {
padding: 24rpx 28rpx;
border-radius: 24rpx;
background: #fef2f2;
border: 1rpx solid rgba(220, 38, 38, 0.22);
box-sizing: border-box;
}
.weekly-page .st-daily-context-error-text {
font-size: 28rpx;
font-weight: 600;
color: #991b1b;
line-height: 1.5;
}
.weekly-page .st-main-spacer {
height: 32rpx;
}
@@ -139,25 +177,182 @@
transform: scale(0.98);
}
/* 糖分突袭游戏入口 */
.weekly-page .st-game-entry {
margin-top: 24rpx;
padding: 28rpx 32rpx;
display: flex;
align-items: center;
gap: 24rpx;
border-radius: var(--st-radius-2xl);
background: var(--surface-container-lowest);
border: 1rpx solid rgba(0, 108, 73, 0.14);
box-shadow: var(--st-shadow-ambient);
box-sizing: border-box;
}
.weekly-page .st-game-entry:active {
transform: scale(0.98);
opacity: 0.92;
}
.weekly-page .st-game-entry-icon {
width: 88rpx;
height: 88rpx;
border-radius: 24rpx;
background: rgba(0, 108, 73, 0.1);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.weekly-page .st-game-entry-body {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 6rpx;
}
.weekly-page .st-game-entry-title {
font-size: 32rpx;
font-weight: 700;
color: var(--on-surface);
line-height: 1.25;
}
.weekly-page .st-game-entry-sub {
font-size: 24rpx;
font-weight: 500;
color: var(--on-surface-variant, #64748b);
line-height: 1.35;
}
/* ===== Chart card — rounded-3xl ===== */
.weekly-page .st-chart-card {
padding: 40rpx;
padding: 40rpx 10rpx 40rpx 10rpx;
border-radius: 48rpx;
background: var(--surface-container-lowest);
border: 1rpx solid rgba(221, 228, 221, 0.3);
box-shadow: var(--st-shadow-ambient);
display: flex;
flex-direction: column;
gap: 0;
overflow: hidden;
}
.weekly-page .st-chart-head {
display: flex;
flex-direction: column;
align-items: stretch;
margin-bottom: 12rpx;
gap: 12rpx;
padding: 0 20rpx;
}
.weekly-page .st-chart-head-text {
width: 100%;
}
/* 图例 + AI 胶囊同一行,避免大块图标压过标题 */
.weekly-page .st-chart-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 48rpx;
gap: 16rpx;
width: 100%;
}
.weekly-page .st-chart-ai-chip {
flex-shrink: 0;
min-height: 88rpx;
display: flex;
align-items: center;
}
.weekly-page .st-chart-ai-chip-inner {
display: flex;
align-items: center;
gap: 10rpx;
min-height: 64rpx;
padding: 10rpx 22rpx 10rpx 12rpx;
border-radius: 999rpx;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.96) 0%, rgba(236, 253, 245, 0.92) 100%);
border: 1rpx solid rgba(0, 108, 73, 0.14);
box-shadow:
0 2rpx 6rpx rgba(15, 23, 42, 0.04),
inset 0 1rpx 0 rgba(255, 255, 255, 0.9);
transition:
background 0.22s ease,
border-color 0.22s ease,
box-shadow 0.22s ease,
transform 0.15s ease;
}
.weekly-page .st-chart-ai-chip-icon {
width: 44rpx;
height: 44rpx;
border-radius: 50%;
background: rgba(0, 108, 73, 0.08);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.weekly-page .st-chart-ai-chip-label {
font-size: 26rpx;
font-weight: 700;
color: #0f3d2e;
letter-spacing: 0.02em;
white-space: nowrap;
}
.weekly-page .st-chart-ai-chip.hint .st-chart-ai-chip-inner {
border-color: rgba(0, 108, 73, 0.28);
box-shadow:
0 2rpx 8rpx rgba(0, 108, 73, 0.1),
0 0 0 2rpx rgba(78, 222, 163, 0.22);
}
.weekly-page .st-chart-ai-chip.active .st-chart-ai-chip-inner {
background: linear-gradient(135deg, #1a8f5c 0%, #006c49 100%);
border-color: rgba(0, 108, 73, 0.35);
box-shadow: 0 6rpx 18rpx rgba(0, 108, 73, 0.2);
}
.weekly-page .st-chart-ai-chip.active .st-chart-ai-chip-icon {
background: rgba(255, 255, 255, 0.18);
}
.weekly-page .st-chart-ai-chip.active .st-chart-ai-chip-label {
color: #ffffff;
}
.weekly-page .st-chart-ai-chip.loading .st-chart-ai-chip-inner {
opacity: 0.82;
}
.weekly-page .st-chart-ai-chip:active .st-chart-ai-chip-inner {
transform: scale(0.98);
opacity: 0.92;
}
@media (prefers-reduced-motion: reduce) {
.weekly-page .st-chart-ai-chip-inner {
transition: background 0.01s, border-color 0.01s, opacity 0.01s;
}
.weekly-page .st-chart-ai-chip:active .st-chart-ai-chip-inner {
transform: none;
}
}
.weekly-page .st-chart-title {
display: block;
font-size: 40rpx;
font-size: 30rpx;
font-weight: 700;
color: var(--on-surface);
}
@@ -165,14 +360,131 @@
.weekly-page .st-chart-sub {
display: block;
margin-top: 4rpx;
font-size: 24rpx;
font-size: 22rpx;
font-weight: 600;
color: var(--on-surface-variant);
}
.weekly-page .st-chart-more {
display: inline-flex;
align-items: center;
gap: 4rpx;
margin-top: 6rpx;
}
.weekly-page .st-chart-more-text {
font-size: 22rpx;
font-weight: 600;
color: var(--primary);
}
.weekly-page .st-chart-more:active {
opacity: 0.75;
}
.weekly-page .st-chart-legend {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--st-inline-gap);
flex: 1;
min-width: 0;
}
/* 趋势卡内展开的 AI 建议 */
.weekly-page .st-chart-care-panel {
margin: 12rpx 20rpx 0;
padding: 28rpx 24rpx 24rpx;
border-radius: 28rpx;
background: rgba(255, 255, 255, 0.72);
border: 1rpx solid rgba(221, 228, 221, 0.65);
box-shadow: inset 0 1rpx 0 rgba(255, 255, 255, 0.95);
}
.weekly-page .st-chart-care-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12rpx;
margin-bottom: 8rpx;
}
.weekly-page .st-chart-care-title {
font-size: 30rpx;
font-weight: 700;
color: var(--on-surface);
}
.weekly-page .st-chart-care-actions {
display: flex;
align-items: center;
gap: 12rpx;
flex-shrink: 0;
}
.weekly-page .st-chart-care-refresh {
display: flex;
align-items: center;
gap: 6rpx;
padding: 10rpx 18rpx;
border-radius: 999rpx;
background: rgba(255, 255, 255, 0.7);
font-size: 22rpx;
font-weight: 600;
color: var(--primary);
}
.weekly-page .st-chart-care-refresh.disabled {
opacity: 0.5;
}
.weekly-page .st-chart-care-close {
min-height: 64rpx;
padding: 10rpx 18rpx;
display: flex;
align-items: center;
border-radius: 999rpx;
background: rgba(255, 255, 255, 0.55);
font-size: 22rpx;
font-weight: 600;
color: var(--on-surface-variant);
}
.weekly-page .st-chart-care-close:active {
opacity: 0.75;
}
.weekly-page .st-chart-care-sub {
display: block;
margin-bottom: 16rpx;
font-size: 24rpx;
line-height: 1.4;
color: var(--on-surface-variant);
}
.weekly-page .st-care-badge--in-chart {
margin-bottom: 16rpx;
}
.weekly-page .st-skeleton--in-chart {
padding: 8rpx 0;
}
.weekly-page .st-care-cards--in-chart {
gap: 16rpx;
}
.weekly-page .st-care-cards--in-chart .st-meal-glass {
padding: 24rpx;
}
.weekly-page .st-care-stats--in-chart {
margin-top: 16rpx;
}
.weekly-page .st-care-disclaimer--in-chart {
margin-top: 16rpx;
font-size: 22rpx;
}
.weekly-page .st-legend-item {
@@ -243,10 +555,29 @@
color: var(--on-surface-variant);
}
.weekly-page .st-chart-foot {
padding: 20rpx 0 20rpx;
}
.weekly-page .st-chart-mock-hint {
display: block;
margin: 0;
font-size: 30rpx;
font-weight: 600;
color: var(--outline);
line-height: 1.35;
text-align: center;
}
.weekly-page .st-chart-area {
position: relative;
height: 360rpx;
border-bottom: 1rpx dashed var(--surface-container-highest);
overflow: hidden;
cursor: pointer;
}
.weekly-page .st-chart-area:active {
opacity: 0.92;
}
.weekly-page .st-chart-canvas {
@@ -255,6 +586,31 @@
display: block;
}
/* 识糖入口嵌在趋势卡内:轻量分隔,避免大块留白 */
.weekly-page .st-chart-card .st-game-entry--in-card {
margin-top: 12rpx;
padding: 16rpx 20rpx;
gap: 16rpx;
border-radius: 24rpx;
background: var(--surface-container-low);
border: 1rpx solid rgba(0, 108, 73, 0.08);
box-shadow: none;
}
.weekly-page .st-chart-card .st-game-entry--in-card .st-game-entry-icon {
width: 64rpx;
height: 64rpx;
border-radius: 16rpx;
}
.weekly-page .st-chart-card .st-game-entry--in-card .st-game-entry-title {
font-size: 28rpx;
}
.weekly-page .st-chart-card .st-game-entry--in-card .st-game-entry-sub {
font-size: 22rpx;
}
.weekly-page .st-chart-empty,
.weekly-page .st-chart-placeholder {
position: absolute;
@@ -499,6 +855,11 @@
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.04);
}
.weekly-page .st-meal-glass--drinks {
background: rgba(224, 242, 254, 0.65);
border-color: rgba(14, 165, 233, 0.15);
}
.weekly-page .st-meal-head {
display: flex;
align-items: center;
@@ -531,13 +892,156 @@
text-align: center;
}
.weekly-page .st-skeleton-line.short {
width: 55%;
}
/* ===== AI 血糖照护(嵌在趋势卡内) ===== */
.weekly-page .st-care-badge {
position: relative;
z-index: 1;
display: inline-flex;
margin-bottom: 24rpx;
padding: 8rpx 20rpx;
border-radius: 999rpx;
background: rgba(255, 255, 255, 0.65);
font-size: 24rpx;
font-weight: 600;
color: var(--primary);
}
.weekly-page .st-care-cards {
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
gap: 20rpx;
}
.weekly-page .st-care-block--tcm {
background: rgba(254, 243, 199, 0.55);
border-color: rgba(217, 119, 6, 0.12);
}
.weekly-page .st-care-block--next {
background: rgba(224, 242, 254, 0.55);
border-color: rgba(14, 165, 233, 0.12);
}
.weekly-page .st-care-watch {
padding: 24rpx 32rpx;
border-radius: var(--st-radius-2xl);
background: rgba(255, 255, 255, 0.75);
border: 1rpx solid rgba(255, 255, 255, 0.9);
}
.weekly-page .st-care-watch-title {
display: block;
margin-bottom: 12rpx;
font-size: 28rpx;
font-weight: 700;
color: var(--on-surface);
}
.weekly-page .st-care-watch-item {
display: flex;
align-items: flex-start;
gap: 8rpx;
margin-bottom: 8rpx;
}
.weekly-page .st-care-watch-dot {
font-size: 32rpx;
line-height: 1.2;
color: var(--primary);
}
.weekly-page .st-care-watch-text {
flex: 1;
font-size: 28rpx;
line-height: 1.43;
color: var(--on-surface-variant);
}
.weekly-page .st-care-stats {
position: relative;
z-index: 1;
display: flex;
flex-wrap: wrap;
gap: 12rpx;
margin-top: 20rpx;
}
.weekly-page .st-care-stat-chip {
padding: 12rpx 20rpx;
border-radius: 16rpx;
background: rgba(255, 255, 255, 0.55);
}
.weekly-page .st-care-stat-label {
display: block;
font-size: 22rpx;
color: var(--outline);
}
.weekly-page .st-care-stat-val {
font-size: 24rpx;
font-weight: 600;
color: var(--on-surface-variant);
}
.weekly-page .st-care-disclaimer {
position: relative;
z-index: 1;
display: block;
margin-top: 20rpx;
font-size: 24rpx;
line-height: 1.4;
color: var(--outline);
}
/* ===== 底部浮动输入 ===== */
.weekly-page .st-float-ask {
position: fixed;
left: var(--st-container-margin);
right: var(--st-container-margin);
bottom: calc(24rpx + env(safe-area-inset-bottom));
z-index: 90;
bottom: var(--tab-dock-full-offset);
z-index: 210;
}
.weekly-page .st-float-close {
position: absolute;
top: -18rpx;
right: -6rpx;
width: 44rpx;
height: 44rpx;
border-radius: 50%;
background: rgba(0, 0, 0, 0.45);
display: flex;
align-items: center;
justify-content: center;
z-index: 2;
}
.weekly-page .st-float-close-icon {
color: #ffffff;
font-size: 30rpx;
line-height: 1;
}
.weekly-page .st-float-reopen {
position: fixed;
right: var(--st-container-margin);
bottom: var(--tab-dock-full-offset);
width: 88rpx;
height: 88rpx;
border-radius: 50%;
background: linear-gradient(180deg, #2d7340 0%, #006c49 100%);
box-shadow: var(--st-shadow-float);
display: flex;
align-items: center;
justify-content: center;
z-index: 210;
}
.weekly-page .st-float-inner {
@@ -577,6 +1081,102 @@
font-size: 28rpx;
}
.weekly-page .st-float-mic {
width: 72rpx;
height: 72rpx;
border-radius: 50%;
background: rgba(0, 108, 73, 0.1);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
transition: background 0.2s ease, transform 0.15s ease;
}
.weekly-page .st-float-mic.listening {
background: var(--primary);
animation: st-float-mic-pulse 1.2s ease-in-out infinite;
}
.weekly-page .st-float-mic.disabled {
opacity: 0.45;
pointer-events: none;
}
.weekly-page .st-float-mic:active {
transform: scale(0.94);
}
.weekly-page .st-float-hold-hint {
margin-top: 12rpx;
text-align: center;
font-size: 24rpx;
color: var(--primary);
line-height: 1.4;
}
.weekly-page .st-voice-overlay {
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
z-index: 3000;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.32);
pointer-events: none;
}
.weekly-page .st-voice-card {
display: flex;
flex-direction: column;
align-items: center;
gap: 20rpx;
padding: 56rpx 64rpx;
border-radius: 32rpx;
background: rgba(20, 24, 22, 0.86);
box-shadow: 0 16rpx 48rpx rgba(0, 0, 0, 0.35);
}
.weekly-page .st-voice-icon {
width: 120rpx;
height: 120rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(180deg, #2d7340 0%, #006c49 100%);
}
.weekly-page .st-voice-icon.active {
animation: st-voice-pulse 1.1s ease-in-out infinite;
}
@keyframes st-voice-pulse {
0%, 100% { box-shadow: 0 0 0 0 rgba(0, 108, 73, 0.45); }
50% { box-shadow: 0 0 0 20rpx rgba(0, 108, 73, 0); }
}
.weekly-page .st-voice-title {
font-size: 32rpx;
font-weight: 700;
color: #ffffff;
line-height: 1.3;
}
.weekly-page .st-voice-sub {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.7);
line-height: 1.3;
}
@keyframes st-float-mic-pulse {
0%, 100% { box-shadow: 0 0 0 0 rgba(0, 108, 73, 0.35); }
50% { box-shadow: 0 0 0 12rpx rgba(0, 108, 73, 0); }
}
.weekly-page .st-float-send {
width: 80rpx;
height: 80rpx;
@@ -587,6 +1187,8 @@
justify-content: center;
flex-shrink: 0;
box-shadow: 0 4rpx 12rpx rgba(0, 108, 73, 0.2);
line-height: 0;
overflow: hidden;
}
.weekly-page .st-float-send.disabled {
@@ -64,10 +64,10 @@ export function parsePartialDietPlainText(text) {
const out = {}
if (!text) return out
const lineRe = /^(早餐|午餐|晚餐|提示|少碰)[:]\s*(.*)$/gm
const lineRe = /^(早餐|喝的|午餐|晚餐|提示|少碰)[:]\s*(.*)$/gm
let match
while ((match = lineRe.exec(text)) !== null) {
const key = { '早餐': 'breakfast', '午餐': 'lunch', '晚餐': 'dinner', '提示': 'tips', '少碰': 'avoid' }[match[1]]
const key = { '早餐': 'breakfast', '喝的': 'drinks', '午餐': 'lunch', '晚餐': 'dinner', '提示': 'tips', '少碰': 'avoid' }[match[1]]
const val = (match[2] || '').trim()
if (key === 'avoid') {
out.avoid = val.split(/[、,;\s]+/).map((s) => s.trim()).filter(Boolean)
@@ -76,9 +76,9 @@ export function parsePartialDietPlainText(text) {
}
}
const tail = text.match(/(?:^|\n)(早餐|午餐|晚餐|提示|少碰)[:]\s*([^\n]*)$/)
const tail = text.match(/(?:^|\n)(早餐|喝的|午餐|晚餐|提示|少碰)[:]\s*([^\n]*)$/)
if (tail) {
const key = { '早餐': 'breakfast', '午餐': 'lunch', '晚餐': 'dinner', '提示': 'tips', '少碰': 'avoid' }[tail[1]]
const key = { '早餐': 'breakfast', '喝的': 'drinks', '午餐': 'lunch', '晚餐': 'dinner', '提示': 'tips', '少碰': 'avoid' }[tail[1]]
const val = (tail[2] || '').trim()
if (key === 'avoid') {
if (val) out.avoid = val.split(/[、,;\s]+/).map((s) => s.trim()).filter(Boolean)
@@ -90,6 +90,49 @@ export function parsePartialDietPlainText(text) {
return out
}
/** 从流式纯文本中提取血糖照护建议(支持未写完的最后一行) */
export function parsePartialBloodCarePlainText(text) {
const out = {}
if (!text) return out
const lineRe = /^(总评|血糖|中医|留意|复诊)[:]\s*(.*)$/gm
let match
while ((match = lineRe.exec(text)) !== null) {
const key = {
'总评': 'summary',
'血糖': 'blood_advice',
'中医': 'tcm_plan',
'留意': 'watch_points',
'复诊': 'next_steps'
}[match[1]]
const val = (match[2] || '').trim()
if (key === 'watch_points') {
out.watch_points = val.split(/[、,;\s]+/).map((s) => s.trim()).filter(Boolean)
} else {
out[key] = val
}
}
const tail = text.match(/(?:^|\n)(总评|血糖|中医|留意|复诊)[:]\s*([^\n]*)$/)
if (tail) {
const key = {
'总评': 'summary',
'血糖': 'blood_advice',
'中医': 'tcm_plan',
'留意': 'watch_points',
'复诊': 'next_steps'
}[tail[1]]
const val = (tail[2] || '').trim()
if (key === 'watch_points') {
if (val) out.watch_points = val.split(/[、,;\s]+/).map((s) => s.trim()).filter(Boolean)
} else {
out[key] = val
}
}
return out
}
/** @deprecated 使用 parsePartialDietPlainText */
export function parsePartialDietJson(text) {
return parsePartialDietPlainText(text)
+6 -1
View File
@@ -482,11 +482,16 @@ export function prescriptionOrderRequestCompletion(params: { id: number }) {
return request.post({ url: '/tcm.prescriptionOrder/requestCompletion', params })
}
/** 将「已发货/已签收」且支付审核通过的订单结案(3=已完成 或 7-12 业务状态) */
/** 将「已发货/已签收」且支付审核通过的订单结案(3=已完成 或 7-12 业务状态,不含退款 */
export function prescriptionOrderComplete(params: { id: number; fulfillment_status: number }) {
return request.post({ url: '/tcm.prescriptionOrder/complete', params })
}
/** 业务订单退款(须填写原因;关联收款单标记已退款、已付总额清零) */
export function prescriptionOrderRefund(params: { id: number; reason: string }) {
return request.post({ url: '/tcm.prescriptionOrder/refund', params })
}
/** 甘草药管家:中药处方下单(服务端 CTM_PREVIEW → CTM_SUBMIT_RECIPEL */
export function prescriptionOrderSubmitGancaoRecipel(params: { id: number }) {
return request.post({ url: '/tcm.prescriptionOrder/submitGancaoRecipel', params })
@@ -293,7 +293,7 @@
:fetch-fun="prescriptionOrderExport"
:params="prescriptionOrderExportParams"
:page-size="pager.size"
export-hint="导出范围与上方筛选一致(履约状态、创建时间及其他条件均会生效)。含「自媒体渠道(挂号渠道来源)」:优先取该单关联处方登记的挂号;无则诊单下同患者挂号取 id 最大的一条(与前台挂号选择的记录一致);业绩侧栏带渠道筛选导出时与同页列表高亮挂号同源。「服务套餐」按字典 server_order 解析展示。「签收日期」与详情/业绩看板同源:优先库表轨迹;库内无轨迹时导出会按 logisticsTrace 口径补查快递100 并落库(如 PO 单在详情已签收但库内为空)。"
export-hint="导出范围与上方筛选一致(履约状态、创建时间及其他条件均会生效)。含「自媒体渠道(挂号渠道来源)」:优先取该单关联处方登记的挂号;无则诊单下同患者挂号取 id 最大的一条(与前台挂号选择的记录一致);业绩侧栏带渠道筛选导出时与同页列表高亮挂号同源。「服务套餐」按字典 server_order 解析展示。「签收日期」与详情/业绩看板同源,仅读物流库(轨迹/签收时间):导出不再实时查快递100,速度只取决于数据库;签收时间由 `tcm:backfill-sign-time` 命令与物流自动更新定时任务落库,刚发货尚未同步的单子会暂时为空,待下次回填/定时任务刷新后显示。"
/>
</el-form-item>
</el-form>
@@ -530,6 +530,13 @@
link
@click="confirmComplete(row)"
>完成订单</el-button>
<el-button
v-if="canRefundRow(row)"
v-perms="['tcm.prescriptionOrder/refund']"
type="danger"
link
@click="openRefundOrder(row)"
>退款</el-button>
<el-button
v-if="canWithdrawRow(row)"
v-perms="['tcm.prescriptionOrder/withdraw']"
@@ -672,6 +679,16 @@
>
完成订单
</el-button>
<el-button
v-if="canRefundRow(detailData)"
v-perms="['tcm.prescriptionOrder/refund']"
type="danger"
size="small"
plain
@click="openRefundOrder(detailData)"
>
退款
</el-button>
</div>
</div>
</template>
@@ -2103,6 +2120,41 @@
</el-button>
</template>
</el-dialog>
<!-- 订单退款须填写原因 -->
<el-dialog
v-model="refundOrderDialogVisible"
title="订单退款"
width="480px"
:close-on-click-modal="false"
destroy-on-close
@closed="onRefundOrderDialogClosed"
>
<el-alert
type="warning"
:closable="false"
show-icon
class="mb-4 !items-start"
title="确认退款后,关联收款记录将全部标记为「已退款」,已付总额清零,且不可撤销。"
/>
<el-form ref="refundOrderFormRef" :model="refundOrderForm" :rules="refundOrderRules" label-width="88px">
<el-form-item label="退款原因" prop="reason">
<el-input
v-model="refundOrderForm.reason"
type="textarea"
:rows="4"
maxlength="500"
show-word-limit
placeholder="请填写退款原因(必填)"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="refundOrderDialogVisible = false">取消</el-button>
<el-button type="danger" :loading="refundOrderSubmitting" @click="submitRefundOrder">
确认退款
</el-button>
</template>
</el-dialog>
<!-- 补齐支付单 Dialog -->
<el-dialog
v-model="addPayOrderVisible"
@@ -2727,6 +2779,7 @@ import {
prescriptionOrderAddPayOrder,
prescriptionOrderRequestCompletion,
prescriptionOrderComplete,
prescriptionOrderRefund,
prescriptionOrderRevokeRxAudit,
prescriptionOrderRevokePayAudit,
prescriptionOrderPatchPrescriptionPatient,
@@ -3686,6 +3739,10 @@ function canCompleteRow(row: { fulfillment_status?: number; payment_slip_audit_s
return (fs === 5 || fs === 6) && Number(row.payment_slip_audit_status) === 1
}
function canRefundRow(row: { fulfillment_status?: number; payment_slip_audit_status?: number }) {
return canCompleteRow(row)
}
function canQuickTrackRow(row: { fulfillment_status?: number }) {
// (5) (6)
return Number(row.fulfillment_status) === 5
@@ -4087,7 +4144,8 @@ function logActionText(act: string) {
gancao_submit: '甘草下单',
patch_rx_patient: '处方患者信息',
update_amount: '修改订单金额',
complete: '完成订单'
complete: '完成订单',
refund: '退款'
}
return m[act] || act
}
@@ -5193,7 +5251,6 @@ const completeOrderStatusOptions = [
{ value: 7, label: '进行中' },
{ value: 8, label: '暂不制药' },
{ value: 9, label: '拒收' },
{ value: 10, label: '退款' },
{ value: 11, label: '保留药方' },
{ value: 12, label: '制药缓发' }
] as const
@@ -5269,6 +5326,54 @@ async function submitCompleteOrder() {
}
}
// 退/ +
const refundOrderDialogVisible = ref(false)
const refundOrderId = ref(0)
const refundOrderSubmitting = ref(false)
const refundOrderFormRef = ref<FormInstance>()
const refundOrderForm = reactive({ reason: '' })
const refundOrderRules: FormRules = {
reason: [{ required: true, message: '请填写退款原因', trigger: 'blur' }]
}
function onRefundOrderDialogClosed() {
refundOrderId.value = 0
refundOrderForm.reason = ''
refundOrderFormRef.value?.clearValidate()
}
function openRefundOrder(row: { id: number }) {
refundOrderId.value = row.id
refundOrderForm.reason = ''
refundOrderDialogVisible.value = true
}
async function submitRefundOrder() {
const id = refundOrderId.value
if (!id) return
const valid = await refundOrderFormRef.value?.validate().catch(() => false)
if (!valid) return
refundOrderSubmitting.value = true
try {
await prescriptionOrderRefund({ id, reason: refundOrderForm.reason.trim() })
feedback.msgSuccess('退款成功')
refundOrderDialogVisible.value = false
getLists()
if (detailVisible.value && detailData.value?.id === id) {
try {
const r: any = await prescriptionOrderDetail({ id })
const nd = r?.data ?? r ?? null
if (nd) detailData.value = nd
await fetchLogs(id)
} catch { /* 静默 */ }
}
} catch {
/* 拦截器已提示 */
} finally {
refundOrderSubmitting.value = false
}
}
//
const addPayOrderVisible = ref(false)
const addPayOrderSaving = ref(false)
@@ -498,6 +498,9 @@
<el-dropdown-menu>
<el-dropdown-item v-if="canAddPayOrderRow(row)" command="addPayOrder">补齐支付单</el-dropdown-item>
<el-dropdown-item v-if="canCompleteRow(row)" command="complete">完成订单</el-dropdown-item>
<el-dropdown-item v-if="canRefundRow(row)" command="refund">
<span class="text-red-500">退款</span>
</el-dropdown-item>
<el-dropdown-item v-if="canUploadGancaoRow(row)" command="submitGancao">上传药方</el-dropdown-item>
<el-dropdown-item v-if="canWithdrawRow(row)" command="withdraw" divided>
<span class="text-red-500">撤回订单</span>
@@ -612,6 +615,16 @@
>
完成订单
</el-button>
<el-button
v-if="canRefundRow(detailData)"
v-perms="['tcm.prescriptionOrder/refund']"
type="danger"
size="small"
plain
@click="openRefundOrder(detailData)"
>
退款
</el-button>
</div>
</div>
</template>
@@ -1854,6 +1867,42 @@
</el-button>
</template>
</el-dialog>
<!-- 订单退款须填写原因 -->
<el-dialog
v-model="refundOrderDialogVisible"
title="订单退款"
width="92%"
:close-on-click-modal="false"
destroy-on-close
class="po-h5-dialog"
@closed="onRefundOrderDialogClosed"
>
<el-alert
type="warning"
:closable="false"
show-icon
class="mb-4 !items-start"
title="确认退款后,关联收款记录将全部标记为「已退款」,已付总额清零,且不可撤销。"
/>
<el-form ref="refundOrderFormRef" :model="refundOrderForm" :rules="refundOrderRules" label-width="88px">
<el-form-item label="退款原因" prop="reason">
<el-input
v-model="refundOrderForm.reason"
type="textarea"
:rows="4"
maxlength="500"
show-word-limit
placeholder="请填写退款原因(必填)"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="refundOrderDialogVisible = false">取消</el-button>
<el-button type="danger" :loading="refundOrderSubmitting" @click="submitRefundOrder">
确认退款
</el-button>
</template>
</el-dialog>
<!-- 补齐支付单 Dialog -->
<el-dialog
v-model="addPayOrderVisible"
@@ -2416,6 +2465,7 @@ import {
prescriptionOrderUpdateAmount,
prescriptionOrderAddPayOrder,
prescriptionOrderComplete,
prescriptionOrderRefund,
prescriptionOrderRevokeRxAudit,
prescriptionOrderRevokePayAudit,
prescriptionOrderPatchPrescriptionPatient,
@@ -3237,6 +3287,10 @@ function canCompleteRow(row: { fulfillment_status?: number; payment_slip_audit_s
return (fs === 5 || fs === 6) && Number(row.payment_slip_audit_status) === 1
}
function canRefundRow(row: { fulfillment_status?: number; payment_slip_audit_status?: number }) {
return canCompleteRow(row)
}
function canQuickTrackRow(row: { fulfillment_status?: number }) {
return Number(row.fulfillment_status) === 5
}
@@ -3286,13 +3340,14 @@ const h5ActiveFilterCount = computed(() => {
// H5:
function hasMoreCardActions(row: Record<string, any>) {
return canAddPayOrderRow(row) || canCompleteRow(row) || canUploadGancaoRow(row) || canWithdrawRow(row)
return canAddPayOrderRow(row) || canCompleteRow(row) || canRefundRow(row) || canUploadGancaoRow(row) || canWithdrawRow(row)
}
// H5:
function handleCardMoreCommand(cmd: string, row: Record<string, any>) {
if (cmd === 'addPayOrder') openAddPayOrder(row as any)
else if (cmd === 'complete') confirmComplete(row as any)
else if (cmd === 'refund') openRefundOrder(row as any)
else if (cmd === 'submitGancao') confirmSubmitGancaoRecipel(row as any)
else if (cmd === 'withdraw') confirmWithdraw(row as any)
}
@@ -3607,7 +3662,8 @@ function logActionText(act: string) {
gancao_submit: '甘草下单',
patch_rx_patient: '处方患者信息',
update_amount: '修改订单金额',
complete: '完成订单'
complete: '完成订单',
refund: '退款'
}
return m[act] || act
}
@@ -4553,7 +4609,6 @@ const completeOrderStatusOptions = [
{ value: 7, label: '进行中' },
{ value: 8, label: '暂不制药' },
{ value: 9, label: '拒收' },
{ value: 10, label: '退款' },
{ value: 11, label: '保留药方' },
{ value: 12, label: '制药缓发' }
] as const
@@ -4586,6 +4641,53 @@ async function submitCompleteOrder() {
}
}
const refundOrderDialogVisible = ref(false)
const refundOrderId = ref(0)
const refundOrderSubmitting = ref(false)
const refundOrderFormRef = ref<FormInstance>()
const refundOrderForm = reactive({ reason: '' })
const refundOrderRules: FormRules = {
reason: [{ required: true, message: '请填写退款原因', trigger: 'blur' }]
}
function onRefundOrderDialogClosed() {
refundOrderId.value = 0
refundOrderForm.reason = ''
refundOrderFormRef.value?.clearValidate()
}
function openRefundOrder(row: { id: number }) {
refundOrderId.value = row.id
refundOrderForm.reason = ''
refundOrderDialogVisible.value = true
}
async function submitRefundOrder() {
const id = refundOrderId.value
if (!id) return
const valid = await refundOrderFormRef.value?.validate().catch(() => false)
if (!valid) return
refundOrderSubmitting.value = true
try {
await prescriptionOrderRefund({ id, reason: refundOrderForm.reason.trim() })
feedback.msgSuccess('退款成功')
refundOrderDialogVisible.value = false
getLists()
if (detailVisible.value && detailData.value?.id === id) {
try {
const r: any = await prescriptionOrderDetail({ id })
const nd = r?.data ?? r ?? null
if (nd) detailData.value = nd
await fetchLogs(id)
} catch { /* 静默 */ }
}
} catch {
/* 拦截器已提示 */
} finally {
refundOrderSubmitting.value = false
}
}
//
const addPayOrderVisible = ref(false)
const addPayOrderSaving = ref(false)
+79 -1
View File
@@ -509,6 +509,32 @@
</el-button>
</template>
</el-dialog>
<el-dialog
v-model="completeDialogVisible"
title="完成问诊"
width="420px"
append-to-body
:close-on-click-modal="false"
destroy-on-close
>
<div class="complete-dialog">
<p class="complete-dialog-tip">是否添加医生备注</p>
<el-input
v-if="canAddCompleteNote"
v-model="completeNoteContent"
type="textarea"
:rows="4"
maxlength="500"
show-word-limit
placeholder="填写完成备注,将追加到医生备注时间轴"
/>
</div>
<template #footer>
<el-button @click="completeDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="completeSubmitting" @click="submitComplete">确认完成</el-button>
</template>
</el-dialog>
</div>
</template>
@@ -518,7 +544,9 @@ import { defineAsyncComponent, onMounted, onUnmounted, watch } from 'vue'
import { appointmentLists, cancelAppointment, completeAppointment, appointmentDetail } from '@/api/doctor'
import { getCallSignature, generateMiniProgramQrcode, tcmDiagnosisDetail, prescriptionGetByAppointment } from '@/api/tcm'
import { getDictData } from '@/api/app'
import { addDoctorNote } from '@/api/patient'
import feedback from '@/utils/feedback'
import { hasPermission } from '@/utils/perm'
import {
formatDiabetesDiscoveryDisplay,
isDiabetesDiscoveryFilled
@@ -797,6 +825,12 @@ const ensureDetailDictOptions = async () => {
const editRef = ref()
const prescriptionRef = ref()
const chatDialogRef = ref()
const COMPLETE_NOTE_PERMISSION = ['doctor.appointment/addDoctorNote']
const completeDialogVisible = ref(false)
const completeSubmitting = ref(false)
const completeNoteContent = ref('')
const completeRow = ref<any>(null)
const canAddCompleteNote = computed(() => hasPermission(COMPLETE_NOTE_PERMISSION))
//
const qrcodeDialogVisible = ref(false)
@@ -866,8 +900,14 @@ const handleCancel = async (row: any) => {
//
const handleComplete = async (row: any) => {
completeRow.value = row
completeNoteContent.value = ''
if (canAddCompleteNote.value) {
completeDialogVisible.value = true
return
}
try {
await feedback.confirm('确认患者已就诊完成')
await feedback.confirm('是否添加医生备注')
await completeAppointment({ id: row.id })
feedback.msgSuccess('操作成功')
loadData()
@@ -875,6 +915,37 @@ const handleComplete = async (row: any) => {
//
}
}
const submitComplete = async () => {
const row = completeRow.value
if (!row?.id || completeSubmitting.value) return
completeSubmitting.value = true
try {
const note = completeNoteContent.value.trim()
await completeAppointment({ id: row.id })
let noteFailureMsg = ''
if (canAddCompleteNote.value && note) {
const diagnosisId = row.patient_id || row.diagnosis_id
if (diagnosisId) {
try {
await addDoctorNote({ diagnosis_id: Number(diagnosisId), content: note })
} catch (e) {
noteFailureMsg = '问诊已完成,但备注未保存'
}
} else {
noteFailureMsg = '问诊已完成,但该预约没有关联诊单,备注未保存'
}
}
noteFailureMsg ? feedback.msgWarning(noteFailureMsg) : feedback.msgSuccess('操作成功')
completeDialogVisible.value = false
completeNoteContent.value = ''
completeRow.value = null
loadData()
} catch (e: any) {
feedback.msgError(e?.msg || e?.message || '操作失败')
} finally {
completeSubmitting.value = false
}
}
//
const handleEditPatient = (row: any) => {
@@ -1330,6 +1401,13 @@ onUnmounted(() => {
color: var(--el-color-danger);
}
.complete-dialog-tip {
margin: 0 0 12px;
color: var(--el-text-color-primary);
font-size: 14px;
line-height: 22px;
}
.qrcode-content {
padding: 16px 0;
min-height: 200px;
File diff suppressed because it is too large Load Diff
@@ -147,7 +147,7 @@ const props = withDefaults(defineProps<Props>(), {
const caseTypeLabel = computed(() => {
const d: any = props.diag || {}
return d.consultation_type === 'follow_up' ? '复诊' : '诊'
return d.consultation_type === 'follow_up' ? '复诊' : '诊'
})
const textOf = makeTextOf(() => props.diag || {})
@@ -16,7 +16,7 @@
预约{{ apt?.appointment_date }} {{ apt?.appointment_time }} ·
{{ formatPeriod(apt?.period) }}
</div>
<div>医生{{ apt?.doctor_name || '—' }} 医助{{ apt?.assistant_name || '—' }}</div>
<div>医生{{ apt?.doctor_name || '—' }} 客服{{ apt?.assistant_name || '—' }}</div>
<div>
状态{{ apt?.status_desc || '—' }} ·
{{ apt?.has_prescription ? '已开方' : '未开方' }}
+20
View File
@@ -290,6 +290,23 @@
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="统计端就诊卡" prop="show_card">
<el-switch
v-model="formData.show_card"
:active-value="1"
:inactive-value="0"
active-text="显示"
inactive-text="隐藏"
inline-prompt
/>
<span class="form-tips ml-3">
关闭后该就诊卡将不会出现在小程序统计端
</span>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
@@ -874,6 +891,7 @@ const formData = ref({
current_medications: '',
status: 1,
create_source: 'admin',
show_card: 1,
external_userid: ''
})
@@ -1155,6 +1173,7 @@ async function loadDiagnosisDetailIntoForm(id: number): Promise<void> {
formData.value = data
formData.value.create_source = data.create_source ?? ''
formData.value.show_card = Number(data.show_card) === 0 ? 0 : 1
const y = data.diabetes_discovery_year
formData.value.diabetes_discovery_year = y == null || y === '' ? '' : String(y)
}
@@ -1317,6 +1336,7 @@ const handleClose = () => {
current_medications: '',
status: 1,
create_source: 'admin',
show_card: 1,
external_userid: ''
}
@@ -345,6 +345,25 @@ class PrescriptionOrderController extends BaseAdminController
return $this->success('操作成功', $result);
}
/**
* 业务订单退款(须填写原因;关联收款单标记已退款)
*/
public function refund()
{
$params = (new PrescriptionOrderValidate())->post()->goCheck('refund');
$result = PrescriptionOrderLogic::refund(
(int) $params['id'],
(string) ($params['reason'] ?? ''),
$this->adminId,
$this->adminInfo
);
if ($result === false) {
return $this->fail(PrescriptionOrderLogic::getError());
}
return $this->success('退款成功', $result);
}
/**
* 甘草药管家:处方下单(CTM_PREVIEW CTM_SUBMIT_RECIPEL
*
@@ -560,6 +560,8 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
$poIds = array_values(array_filter(array_map('intval', array_column($lists, 'id'))));
$paidSumByPo = [];
// 关联支付单数量:批量统计,避免后续逐行 count() 造成 N+1 查询(导出/大列表明显变慢)
$linkCountByPo = [];
if ($poIds !== []) {
$links = PrescriptionOrderPayOrder::whereIn('prescription_order_id', $poIds)
->field(['prescription_order_id', 'pay_order_id'])
@@ -569,6 +571,9 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
foreach ($links as $l) {
$poid = (int) ($l['prescription_order_id'] ?? 0);
$oid = (int) ($l['pay_order_id'] ?? 0);
if ($poid > 0) {
$linkCountByPo[$poid] = ($linkCountByPo[$poid] ?? 0) + 1;
}
if ($poid > 0 && $oid > 0) {
$byPo[$poid][] = $oid;
}
@@ -664,10 +669,7 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
PrescriptionOrderLogic::maskInternalCostIfNeeded($item, $this->adminInfo);
PrescriptionOrderLogic::maskRemarkExtraIfNeeded($item, $this->adminInfo);
$pid = (int) ($item['id'] ?? 0);
$item['linked_pay_order_count'] = (int) PrescriptionOrderPayOrder::where(
'prescription_order_id',
$pid
)->count();
$item['linked_pay_order_count'] = (int) ($linkCountByPo[$pid] ?? 0);
$item['linked_pay_paid_total'] = $paidSumByPo[$pid] ?? 0.0;
$item['deposit_min_amount'] = $depMin;
$item['can_upload_gancao_reciperl'] = PrescriptionOrderLogic::canUploadGancaoRecipel(
@@ -5638,20 +5638,33 @@ class YejiStatsLogic
*/
private static function commissionSettlementExpressRowSignExpr(string $etTable, string $traceTable): string
{
$sigTraceCond = "`tr_sig`.`status_code` IN ('3','301','302','304') OR `tr_sig`.`status` LIKE '%签收%' "
// 与 ExpressTrackingService::contextLooksSigned 对齐:
// 正向「签收/妥投/送达/本人签收/已放在」(顺丰末端 status=送达、context=快件已放在…)
// 先排除「准备签收/待签收/即将送达/预计」等「尚未签收」文案(含「签收」「送达」二字易误判)。
$sigByCode = "`tr_sig`.`status_code` IN ('3','301','302','304')";
$sigByText = "(`tr_sig`.`status` LIKE '%签收%' OR `tr_sig`.`status` LIKE '%送达%' "
. "OR `tr_sig`.`trace_context` LIKE '%签收%' OR `tr_sig`.`trace_context` LIKE '%妥投%' "
. "OR `tr_sig`.`trace_context` LIKE '%送达%' OR `tr_sig`.`trace_context` LIKE '%本人签收%'";
. "OR `tr_sig`.`trace_context` LIKE '%送达%' OR `tr_sig`.`trace_context` LIKE '%本人签收%' "
. "OR `tr_sig`.`trace_context` LIKE '%已放在%')";
$sigNotText = "(`tr_sig`.`trace_context` LIKE '%准备签收%' OR `tr_sig`.`trace_context` LIKE '%待签收%' "
. "OR `tr_sig`.`trace_context` LIKE '%等待签收%' OR `tr_sig`.`trace_context` LIKE '%即将送达%' "
. "OR `tr_sig`.`trace_context` LIKE '%预计%')";
$sigTraceCond = $sigByCode . ' OR (' . $sigByText . ' AND NOT ' . $sigNotText . ')';
return 'GREATEST('
. 'IF(IFNULL(`et`.`sign_time`, 0) > 0, `et`.`sign_time`, 0), '
. 'IFNULL((SELECT MAX(`tr_sig`.`trace_time_stamp`) FROM `' . $traceTable . '` `tr_sig` '
// 与 effectiveSignUnixFromTrackingDetail 一致:轨迹时间最权威,sign_time 列仅兜底。
// 用 COALESCE(NULLIF(...,0)) 实现「优先签收轨迹 → 已签收态最新轨迹 → sign_time 列」的取值顺序,
// 避免历史同步写入 sign_time 的「拉取当天」把签收时间顶高(旧 GREATEST 会取最大值)。
return 'COALESCE('
. 'NULLIF((SELECT MAX(`tr_sig`.`trace_time_stamp`) FROM `' . $traceTable . '` `tr_sig` '
. 'WHERE `tr_sig`.`tracking_id` = `et`.`id` AND IFNULL(`tr_sig`.`trace_time_stamp`, 0) > 0 AND ('
. $sigTraceCond
. ')), 0), '
. 'IF((`et`.`current_state` = \'3\' OR IFNULL(`et`.`is_signed`, 0) = 1), '
. 'NULLIF(IF((`et`.`current_state` = \'3\' OR IFNULL(`et`.`is_signed`, 0) = 1), '
. 'IFNULL((SELECT MAX(`tr_any`.`trace_time_stamp`) FROM `' . $traceTable . '` `tr_any` '
. 'WHERE `tr_any`.`tracking_id` = `et`.`id` AND IFNULL(`tr_any`.`trace_time_stamp`, 0) > 0), 0), '
. '0)'
. '0), 0), '
. 'NULLIF(IF(IFNULL(`et`.`sign_time`, 0) > 0, `et`.`sign_time`, 0), 0), '
. '0'
. ')';
}
@@ -210,6 +210,10 @@ class DiagnosisLogic extends BaseLogic
$params['diagnosis_date'] = strtotime($params['diagnosis_date']);
}
if (array_key_exists('show_card', $params)) {
$params['show_card'] = (int) $params['show_card'] === 0 ? 0 : 1;
}
Diagnosis::update($params);
// 如果是编辑,也确保患者有 TRTC 账号
@@ -3028,9 +3032,10 @@ class DiagnosisLogic extends BaseLogic
return [];
}
// 根据诊断ID列表查询诊单详情
// 根据诊断ID列表查询诊单详情show_card=0 的诊单不在统计端就诊卡列表中出现)
$cardList = Diagnosis::whereIn('id', $viewRecords)
->where('delete_time', null)
->where('show_card', 1)
->field([
'id', 'patient_id', 'patient_name', 'gender', 'age',
'diagnosis_date', 'diagnosis_type', 'syndrome_type',
@@ -2534,8 +2534,8 @@ class PrescriptionOrderLogic
return $out;
}
/** 完成订单时可选的履约终态:3=已完成,7-12=业务结案类状态 */
private const COMPLETE_FULFILLMENT_TARGETS = [3, 7, 8, 9, 10, 11, 12];
/** 完成订单时可选的履约终态:3=已完成,7-12 业务结案类状态(退款 10 走独立 refund 接口) */
private const COMPLETE_FULFILLMENT_TARGETS = [3, 7, 8, 9, 11, 12];
/** 完成时是否按「实付=关联支付单金额」重算并清空诊单医助(与 clearDiagnosisAssistantOnComplete 的终态判断一致) */
private const COMPLETE_FULFILLMENT_TERMINAL = [3, 8, 9, 10, 11, 12];
@@ -2592,13 +2592,7 @@ class PrescriptionOrderLogic
$order->fulfillment_status = $targetFulfillmentStatus;
}
$refundedPayCount = 0;
try {
if ($targetFulfillmentStatus === 10 && $linkedPayOrderIds !== []) {
$refundedPayCount = Order::whereIn('id', $linkedPayOrderIds)
->whereNull('delete_time')
->update(['status' => 4]);
}
$order->save();
} catch (\Throwable $e) {
self::$error = $e->getMessage();
@@ -2614,9 +2608,6 @@ class PrescriptionOrderLogic
$logLine = '订单完成,状态变更为「' . $label . '」,实付金额更新为 ¥' . $linkedPayPaidTotal . $unassignSummary;
} else {
$logLine = '订单处理完成,状态变更为「' . $label . '」' . $unassignSummary;
if ($targetFulfillmentStatus === 10 && $refundedPayCount > 0) {
$logLine .= ',关联支付单 ' . $refundedPayCount . ' 笔已标记为已退款';
}
}
self::writeLog(
$id,
@@ -2634,6 +2625,79 @@ class PrescriptionOrderLogic
return $out;
}
/**
* 业务订单退款:须填写原因;关联收款单标记为已退款,实付清零。
*
* @return array<string,mixed>|false
*/
public static function refund(int $id, string $reason, int $adminId, array $adminInfo)
{
self::$error = '';
$reason = trim($reason);
if ($reason === '') {
self::$error = '请填写退款原因';
return false;
}
$order = PrescriptionOrder::where('id', $id)->whereNull('delete_time')->find();
if (!$order) {
self::$error = '订单不存在';
return false;
}
if (!self::canAccessOrder($order, $adminId, $adminInfo)) {
self::$error = '无权限操作';
return false;
}
$curFs = (int) $order->fulfillment_status;
if ($curFs !== 5 && $curFs !== 6) {
self::$error = '仅「已发货」或「已签收」状态可退款';
return false;
}
if ((int) $order->payment_slip_audit_status !== 1) {
self::$error = '支付单审核尚未通过,不可退款';
return false;
}
$linkedPayOrderIds = self::linkedPayOrderIdList($id);
$refundedPayCount = 0;
try {
if ($linkedPayOrderIds !== []) {
$refundedPayCount = Order::whereIn('id', $linkedPayOrderIds)
->whereNull('delete_time')
->update(['status' => 4]);
}
$order->fulfillment_status = 10;
$order->paid = 0;
$order->save();
} catch (\Throwable $e) {
self::$error = $e->getMessage();
return false;
}
$unassignSummary = self::clearDiagnosisAssistantOnComplete((int) $order->diagnosis_id);
$logLine = '订单退款,状态变更为「退款」;原因:' . $reason;
if ($refundedPayCount > 0) {
$logLine .= ';关联支付单 ' . $refundedPayCount . ' 笔已标记为已退款';
}
$logLine .= ';已付总额清零' . $unassignSummary;
self::writeLog($id, $adminId, $adminInfo, 'refund', $logLine);
$out = $order->toArray();
self::maskInternalCostIfNeeded($out, $adminInfo);
self::maskRemarkExtraIfNeeded($out, $adminInfo);
self::attachLinkedPayOrders($out);
return $out;
}
public static function fulfillmentStatusLabel(int $fs): string
{
$m = [
@@ -3086,7 +3150,9 @@ class PrescriptionOrderLogic
}
}
// 签收时间:与详情 logisticsTrace / 业绩提成同源;库内无轨迹时导出阶段补查快递100 并落库
// 签收时间:纯读物流库(express_tracking / express_trace),不在导出阶段发起任何快递100 HTTP
// 签收时间的落库由 `php think tcm:backfill-sign-time` 与 `express:auto-update` 定时任务负责,
// 导出只读脚本/定时任务处理好的结果——避免逐单 HTTP 让导出变慢、耗时不可控。
/** @var array<int, int> $signTsByPoId order_id => Unix 秒 */
$signTsByPoId = [];
if ($poIds !== []) {
@@ -3107,24 +3173,6 @@ class PrescriptionOrderLogic
Log::warning('appendPrescriptionOrderListExportRows batch sign_time failed: ' . $e->getMessage());
$signTsByPoId = [];
}
foreach ($lists as $row) {
$oid = (int) ($row['id'] ?? 0);
if ($oid <= 0 || (int) ($signTsByPoId[$oid] ?? 0) > 0) {
continue;
}
if (trim((string) ($row['tracking_number'] ?? '')) === '') {
continue;
}
try {
$synced = ExpressTrackingService::syncSignUnixFromLogisticsForPrescriptionOrder($oid, true);
if ($synced > 0) {
$signTsByPoId[$oid] = $synced;
}
} catch (\Throwable $e) {
Log::warning('appendPrescriptionOrderListExportRows sync sign_time order ' . $oid . ': ' . $e->getMessage());
}
}
}
$firstVisitAssistantByDiag = self::batchFirstVisitAssistantNameByDiagnosis($diagIdList, $diagById);
@@ -33,6 +33,7 @@ class DiagnosisValidate extends BaseValidate
'age' => 'require|number|between:0,150',
'diagnosis_type' => 'require',
'status' => 'in:0,1',
'show_card' => 'in:0,1',
'create_source' => 'max:32',
'current_medications' => 'max:2000',
'tongue_images' => 'array',
@@ -78,7 +79,7 @@ class DiagnosisValidate extends BaseValidate
public function sceneEdit()
{
return $this->only(['id', 'patient_name', 'id_card', 'phone', 'gender', 'age', 'diagnosis_date', 'diagnosis_type', 'syndrome_type', 'marital_status', 'height', 'weight', 'region', 'systolic_pressure', 'diastolic_pressure', 'fasting_blood_sugar', 'diabetes_discovery_year', 'local_hospital_diagnosis', 'local_hospital_name', 'past_history', 'symptoms', 'tongue_coating', 'pulse', 'treatment_principle', 'prescription', 'doctor_advice', 'remark', 'current_medications', 'status', 'create_source']);
return $this->only(['id', 'patient_name', 'id_card', 'phone', 'gender', 'age', 'diagnosis_date', 'diagnosis_type', 'syndrome_type', 'marital_status', 'height', 'weight', 'region', 'systolic_pressure', 'diastolic_pressure', 'fasting_blood_sugar', 'diabetes_discovery_year', 'local_hospital_diagnosis', 'local_hospital_name', 'past_history', 'symptoms', 'tongue_coating', 'pulse', 'treatment_principle', 'prescription', 'doctor_advice', 'remark', 'current_medications', 'status', 'show_card', 'create_source']);
}
public function sceneId()
@@ -32,7 +32,8 @@ class PrescriptionOrderValidate extends BaseValidate
'remark_assistant' => 'max:500',
'action' => 'require|in:approve,reject',
'remark' => 'max:500',
'fulfillment_status' => 'require|integer|in:3,7,8,9,10,11,12',
'fulfillment_status' => 'require|integer|in:3,7,8,9,11,12',
'reason' => 'require|max:500',
'patient_name' => 'require|max:50',
'phone' => 'require|max:20',
'phone_tail' => 'max:20|regex:/^\\d*$/',
@@ -50,6 +51,7 @@ class PrescriptionOrderValidate extends BaseValidate
'patient_name.require' => '请输入患者姓名',
'phone.require' => '请输入手机号',
'phone_tail.regex' => '手机后四位仅支持数字',
'reason.require' => '请填写退款原因',
];
protected $scene = [
@@ -75,6 +77,7 @@ class PrescriptionOrderValidate extends BaseValidate
'linkPayOrder' => ['id', 'pay_order_id'],
'requestCompletion' => ['id'],
'complete' => ['id', 'fulfillment_status'],
'refund' => ['id', 'reason'],
'submitGancaoRecipel' => ['id'],
'previewGancaoRecipel' => ['id'],
'patchPrescriptionPatient' => ['id', 'patient_name', 'phone'],
+82 -1
View File
@@ -21,6 +21,7 @@ use app\api\logic\tcm\DailyFamilyLikeLogic;
use app\api\logic\tcm\DailyShareLogic;
use app\api\logic\tcm\DailyPhoneLogic;
use app\api\logic\tcm\DailyDietAiLogic;
use app\api\logic\tcm\DailyBloodCareAiLogic;
use app\adminapi\logic\ConfigLogic;
use app\common\model\tcm\Diagnosis;
use app\common\model\tcm\BloodRecord;
@@ -457,6 +458,10 @@ class TcmController extends BaseApiController
return ['ok' => false, 'error' => '诊单不存在'];
}
if ((int) ($diagnosis['show_card'] ?? 1) !== 1) {
return ['ok' => false, 'error' => '该就诊卡已在统计端隐藏'];
}
// 通过 diagnosis_view_records 校验当前小程序用户与该诊单的归属关系
// 该表是「就诊卡列表」的来源(参考 DiagnosisLogic::getCardList
$owned = \think\facade\Db::name('diagnosis_view_records')
@@ -623,7 +628,42 @@ class TcmController extends BaseApiController
];
if ($existing) {
$payload['id'] = $existing['id'];
// 分表单项更新时保留未提交字段,避免血糖/血压互相覆盖
$keepDecimal = function ($new, string $oldKey) use ($parseDecimal, $existing) {
if ($new !== null) {
return $new;
}
return $parseDecimal($existing[$oldKey] ?? null);
};
$keepInt = function ($new, string $oldKey) use ($parseInt, $existing) {
if ($new !== null) {
return $new;
}
return $parseInt($existing[$oldKey] ?? null);
};
$fasting = $keepDecimal($fasting, 'fasting_blood_sugar');
$postprandial = $keepDecimal($postprandial, 'postprandial_blood_sugar');
$other = $keepDecimal($other, 'other_blood_sugar');
$systolic = $keepInt($systolic, 'systolic_pressure');
$diastolic = $keepInt($diastolic, 'diastolic_pressure');
if ($remark === '') {
$remark = trim((string) ($existing['remark'] ?? ''));
}
$payload = [
'diagnosis_id' => $diagnosisId,
'patient_id' => (int) $diagnosis['patient_id'],
'record_date' => $todayStart,
'record_time' => date('H:i'),
'fasting_blood_sugar' => $fasting,
'postprandial_blood_sugar' => $postprandial,
'other_blood_sugar' => $other,
'systolic_pressure' => $systolic,
'diastolic_pressure' => $diastolic,
'remark' => $remark,
'source' => 1,
'id' => $existing['id'],
];
BloodRecord::update($payload);
return $this->success('已更新今日记录', ['id' => $existing['id']]);
}
@@ -890,6 +930,47 @@ class TcmController extends BaseApiController
});
}
/**
* @notes 日常记录-AI 血糖照护建议(累计 7 天有记录)
*
* 路由:GET /api/tcm/dailyBloodCareAiRecommend?diagnosis_id=:id&refresh=0|1
*/
public function dailyBloodCareAiRecommend()
{
$diagnosisId = (int) $this->request->get('diagnosis_id', 0);
$check = $this->ensurePatientOwnsDiagnosis($diagnosisId);
if (!$check['ok']) {
return $this->fail($check['error']);
}
$refresh = (int) $this->request->get('refresh', 0) === 1;
$result = DailyBloodCareAiLogic::getDailyCarePlan($diagnosisId, $refresh);
if (empty($result['ok'])) {
return $this->fail($result['error'] ?? '获取照护建议失败');
}
return $this->data($result['data']);
}
/**
* @notes 日常记录-AI 血糖照护建议(SSE 流式)
*
* 路由:GET /api/tcm/dailyBloodCareAiRecommendStream?diagnosis_id=:id&refresh=0|1
*/
public function dailyBloodCareAiRecommendStream()
{
$diagnosisId = (int) $this->request->get('diagnosis_id', 0);
$check = $this->ensurePatientOwnsDiagnosis($diagnosisId);
if (!$check['ok']) {
return $this->fail($check['error']);
}
$refresh = (int) $this->request->get('refresh', 0) === 1;
$this->runDailyDietSse(static function (callable $emit) use ($diagnosisId, $refresh): void {
DailyBloodCareAiLogic::streamDailyCarePlan($diagnosisId, $refresh, $emit);
});
}
/**
* @param callable(callable(string, array<string, mixed>):void):void $runner
*/
+47 -4
View File
@@ -273,6 +273,16 @@ class DailyDietAiLogic
return $idx;
}
/**
* 供血糖照护 AI 等模块复用:患者档案 + 7/30 天血糖统计
*
* @return array{ok:bool,error?:string,data?:array}
*/
public static function getPatientContext(int $diagnosisId): array
{
return self::buildPatientContext($diagnosisId);
}
/**
* @return array{ok:bool,error?:string,data?:array}
*/
@@ -499,8 +509,10 @@ class DailyDietAiLogic
3. 严禁:白馒头、油条、粘豆包、糯米饭、西瓜、含糖饮料、白稀饭(杂面窝头、玉米碴粥除外)。
4. GI 优先;农村家常;禁止轻食/沙拉/藜麦等词。
5. lunch 字段写法:蛋白菜放前面,淀粉(如有)放最后且注明「小半碗/一个」。
6. 只输出 JSON
{"breakfast":"...","lunch":"...","dinner":"...","tips":"...","avoid":["...","..."]}
6. 早餐必须包含「驼奶粉」(温水冲服,适量),可与粥、蛋、菜并列写。
7. drinks 单独写全天喝什么:温开水为主;可写驼奶粉冲饮时间;严禁含糖饮料、果汁、酒。
8. 只输出 JSON
{"breakfast":"...","drinks":"...","lunch":"...","dinner":"...","tips":"...","avoid":["...","..."]}
SYS;
$user = sprintf(
@@ -544,9 +556,11 @@ SYS;
2. 淀粉(粥////窝头//南瓜)每餐最多 1 种,早中晚互不相同。
3. 严禁白馒头、油条、粘豆包、糯米饭、西瓜、含糖饮料。
4. 农村家常饭,禁止轻食/沙拉/藜麦。
5. 早餐必须含驼奶粉(温水冲服);喝的单独一行。
严格按下面 5 行格式输出,不要 JSON、不要 markdown、不要多余解释:
早餐:(具体食物)
严格按下面 6 行格式输出,不要 JSON、不要 markdown、不要多余解释:
早餐:(具体食物,须含驼奶粉
喝的:(温开水、驼奶粉冲饮安排等,忌甜饮)
午餐:(具体食物,蛋白放前)
晚餐:(具体食物)
提示:(一句土话提醒)
@@ -593,6 +607,7 @@ SYS;
$map = [
'breakfast' => null,
'drinks' => null,
'lunch' => null,
'dinner' => null,
'tips' => null,
@@ -602,6 +617,9 @@ SYS;
if (preg_match('/早餐[:]\s*(.+)$/mu', $text, $m)) {
$map['breakfast'] = trim(preg_split('/\R/u', $m[1])[0] ?? $m[1]);
}
if (preg_match('/喝的[:]\s*(.+)$/mu', $text, $m)) {
$map['drinks'] = trim(preg_split('/\R/u', $m[1])[0] ?? $m[1]);
}
if (preg_match('/午餐[:]\s*(.+)$/mu', $text, $m)) {
$map['lunch'] = trim(preg_split('/\R/u', $m[1])[0] ?? $m[1]);
}
@@ -623,6 +641,7 @@ SYS;
$out = [
'breakfast' => (string) $map['breakfast'],
'drinks' => (string) ($map['drinks'] ?? ''),
'lunch' => (string) ($map['lunch'] ?? ''),
'dinner' => (string) ($map['dinner'] ?? ''),
'tips' => (string) ($map['tips'] ?? ''),
@@ -632,6 +651,29 @@ SYS;
return $out;
}
/**
* 产品要求:早餐含驼奶粉,并单独给出「喝的」
*
* @param array<string, mixed> $plan
* @return array<string, mixed>
*/
private static function applyDietProductDefaults(array $plan): array
{
$breakfast = trim((string) ($plan['breakfast'] ?? ''));
if ($breakfast !== '' && mb_strpos($breakfast, '驼奶粉') === false && mb_strpos($breakfast, '驼奶') === false) {
$plan['breakfast'] = '驼奶粉(温水冲服)、' . $breakfast;
}
$drinks = trim((string) ($plan['drinks'] ?? ''));
if ($drinks === '') {
$plan['drinks'] = '白天多喝温开水;早餐按量冲驼奶粉,别喝甜饮料、果汁。';
} elseif (mb_strpos($drinks, '驼奶') === false) {
$plan['drinks'] = $drinks . ';早餐已安排驼奶粉';
}
return $plan;
}
/**
* 规范化 AI 三餐方案;校验未过也保留 AI 文案(避免流式展示后被规则模板覆盖)
*
@@ -640,6 +682,7 @@ SYS;
*/
private static function finalizeAiRecommendPlan(array $parsed): array
{
$parsed = self::applyDietProductDefaults($parsed);
$checked = DietMealValidator::validateAndNormalize($parsed);
$data = $checked['plan'];
$data['source'] = 'ai';
@@ -4,6 +4,7 @@ namespace app\api\logic\tcm;
use app\common\model\tcm\DailyFamilyLike;
use app\common\model\tcm\DailyShareInvite;
use app\common\model\tcm\Diagnosis;
/**
* 家人点赞(邀请观看页 患者日常页展示)
@@ -204,6 +205,11 @@ class DailyFamilyLikeLogic
return self::setError('无权查看该诊单') ? false : false;
}
$diagnosis = Diagnosis::where('id', $diagnosisId)->where('delete_time', null)->field('show_card')->find();
if (!$diagnosis || (int) ($diagnosis['show_card'] ?? 1) !== 1) {
return self::setError('该就诊卡已在统计端隐藏') ? false : false;
}
$likeDate = date('Y-m-d');
$count = self::countToday($diagnosisId, $likeDate);
+42 -9
View File
@@ -16,9 +16,10 @@ class DailyGamifyLogic
/** @var array<string,array{name:string,points:int}> */
protected static array $taskDefs = [
'blood' => ['name' => '测血糖血压', 'points' => 10],
'diet' => ['name' => '记今日饮食', 'points' => 10],
'exercise' => ['name' => '记今日运动', 'points' => 10],
'glucose' => ['name' => '测血糖', 'points' => 10],
'bp' => ['name' => '测血压', 'points' => 10],
'diet' => ['name' => '饮食', 'points' => 10],
'exercise' => ['name' => '运动', 'points' => 10],
];
public static function getError(): string
@@ -48,6 +49,13 @@ class DailyGamifyLogic
if (!$owned) {
return self::setError('无权操作该诊单') ? false : false;
}
$diagnosis = \app\common\model\tcm\Diagnosis::where('id', $diagnosisId)
->where('delete_time', null)
->field('show_card')
->find();
if (!$diagnosis || (int) ($diagnosis['show_card'] ?? 1) !== 1) {
return self::setError('该就诊卡已在统计端隐藏') ? false : false;
}
return true;
}
@@ -83,12 +91,13 @@ class DailyGamifyLogic
->where('record_date', '<=', $end)
->find();
$bloodDone = false;
$glucoseDone = false;
$bpDone = false;
if ($blood) {
$bloodDone = self::hasValue($blood['fasting_blood_sugar'] ?? null)
$glucoseDone = self::hasValue($blood['fasting_blood_sugar'] ?? null)
|| self::hasValue($blood['postprandial_blood_sugar'] ?? null)
|| self::hasValue($blood['other_blood_sugar'] ?? null)
|| self::hasValue($blood['systolic_pressure'] ?? null)
|| self::hasValue($blood['other_blood_sugar'] ?? null);
$bpDone = self::hasValue($blood['systolic_pressure'] ?? null)
|| self::hasValue($blood['diastolic_pressure'] ?? null);
}
@@ -118,7 +127,8 @@ class DailyGamifyLogic
}
return [
'blood' => $bloodDone,
'glucose' => $glucoseDone,
'bp' => $bpDone,
'diet' => $dietDone,
'exercise' => $exerciseDone,
];
@@ -141,13 +151,36 @@ class DailyGamifyLogic
'name' => $def['name'],
'points' => $def['points'],
'completed' => !empty($completion[$id]),
'claimed' => !empty($awards[$id]),
'claimed' => self::isTaskClaimed($id, $awards, $completion),
];
}
return $list;
}
/**
* 任务是否已领取(兼容旧版 blood 合并任务)
*
* @param array<string,bool> $awards
* @param array<string,bool> $completion
*/
protected static function isTaskClaimed(string $id, array $awards, array $completion = []): bool
{
if (!empty($awards[$id])) {
return true;
}
// 旧版 blood 一次性领取:对应分项当日已有记录则视为已领,避免拆分后重复领奖/轮换引导
if (!empty($awards['blood'])) {
if ($id === 'glucose' && !empty($completion['glucose'])) {
return true;
}
if ($id === 'bp' && !empty($completion['bp'])) {
return true;
}
}
return false;
}
/** 与前端 tongji/utils/treeLevels.js 保持一致 */
protected const TREE_MAX_LEVEL = 9;
protected const TREE_XP_PER_LEVEL = 50;
+85 -15
View File
@@ -3,6 +3,8 @@
namespace app\api\logic\tcm;
use app\adminapi\logic\tcm\DiagnosisLogic;
use app\common\model\DiagnosisViewRecord;
use app\common\model\tcm\Diagnosis;
use app\common\model\user\User;
/**
@@ -74,19 +76,38 @@ class DailyPhoneLogic
if (!empty($cardList)) {
$card = $cardList[0];
return self::buildContextOk($card, $mobile, false, false);
}
// 该手机号已在诊单库建档:关联查看记录,禁止重复创建轻量档案
$existing = Diagnosis::where('phone', $mobile)
->where('delete_time', null)
->order('id', 'desc')
->find();
if ($existing && (int) ($existing['show_card'] ?? 1) !== 1) {
return [
'ok' => true,
'ok' => false,
'need_mobile' => false,
'created' => false,
'diagnosis_id' => (int) $card['id'],
'patient_id' => (int) $card['patient_id'],
'patient_name' => (string) ($card['patient_name'] ?? ''),
'gender' => (int) ($card['gender'] ?? 0),
'age' => (int) ($card['age'] ?? 0),
'mobile' => $mobile,
'create_source' => (string) ($card['create_source'] ?? ''),
'error' => '该手机号对应就诊卡因隐私设置已隐藏,请联系医生',
];
}
if ($existing) {
if (!self::ensureUserViewRecord($userId, (int) $existing['id'], (int) $existing['patient_id'])) {
return [
'ok' => false,
'need_mobile' => false,
'error' => '该手机号已建档,暂无法关联到当前账号',
];
}
return self::buildContextOk([
'id' => (int) $existing['id'],
'patient_id' => (int) $existing['patient_id'],
'patient_name' => (string) ($existing['patient_name'] ?? ''),
'gender' => (int) ($existing['gender'] ?? 0),
'age' => (int) ($existing['age'] ?? 0),
'create_source' => (string) ($existing['create_source'] ?? ''),
], $mobile, false, true);
}
$displayName = trim((string) (($user['real_name'] ?? '') ?: ($user['nickname'] ?? '')));
if ($displayName === '') {
@@ -114,17 +135,66 @@ class DailyPhoneLogic
];
}
return [
'ok' => true,
'need_mobile' => false,
'created' => true,
'diagnosis_id' => (int) $result['id'],
return self::buildContextOk([
'id' => (int) $result['id'],
'patient_id' => (int) $result['patient_id'],
'patient_name' => $displayName,
'gender' => $gender,
'age' => max(0, (int) ($user['age'] ?? 0)),
'mobile' => $mobile,
'create_source' => self::CREATE_SOURCE_MNP_DAILY,
], $mobile, true, false);
}
/**
* @param array{id:int,patient_id:int,patient_name?:string,gender?:int,age?:int,create_source?:string} $card
*/
private static function buildContextOk(array $card, string $mobile, bool $created, bool $linkedExisting): array
{
return [
'ok' => true,
'need_mobile' => false,
'created' => $created,
'linked_existing' => $linkedExisting,
'diagnosis_id' => (int) $card['id'],
'patient_id' => (int) $card['patient_id'],
'patient_name' => (string) ($card['patient_name'] ?? ''),
'gender' => (int) ($card['gender'] ?? 0),
'age' => (int) ($card['age'] ?? 0),
'mobile' => $mobile,
'create_source' => (string) ($card['create_source'] ?? ''),
];
}
/** 将已有诊单挂到当前小程序用户(diagnosis_view_records */
private static function ensureUserViewRecord(int $userId, int $diagnosisId, int $patientId): bool
{
if ($userId <= 0 || $diagnosisId <= 0) {
return false;
}
try {
$exists = DiagnosisViewRecord::where('user_id', $userId)
->where('diagnosis_id', $diagnosisId)
->where('delete_time', null)
->find();
if ($exists) {
return true;
}
$now = time();
DiagnosisViewRecord::create([
'user_id' => $userId,
'diagnosis_id' => $diagnosisId,
'patient_id' => $patientId,
'share_user_id' => 0,
'view_count' => 1,
'first_view_time' => $now,
'last_view_time' => $now,
'is_confirmed' => 0,
'create_time' => $now,
'update_time' => $now,
]);
return true;
} catch (\Throwable $e) {
return false;
}
}
}
@@ -50,6 +50,9 @@ class DailyShareLogic
if (!$diagnosis) {
return self::setError('诊单不存在') ? false : false;
}
if ((int) ($diagnosis['show_card'] ?? 1) !== 1) {
return self::setError('该就诊卡已在统计端隐藏') ? false : false;
}
$inviteDate = date('Y-m-d');
$code = self::generateUniqueCode();
@@ -114,6 +114,7 @@ class DietMealValidator
$normalized = [
'breakfast' => $breakfast,
'drinks' => trim((string) ($plan['drinks'] ?? '')),
'lunch' => $lunch,
'dinner' => $dinner,
'tips' => $tips,
+83 -9
View File
@@ -18,7 +18,7 @@ class GiKnowledge
'蔬菜类' => ['魔芋', '粟米', '大白菜', '黄瓜', '芹菜', '茄子', '青椒', '海带', '金针菇', '香菇', '菠菜', '番茄', '豆芽', '芦笋', '花椰菜', '洋葱', '生菜'],
'豆类' => ['黄豆', '眉豆', '鸡心豆', '豆腐', '豆角', '绿豆', '扁豆', '四季豆'],
'水果类' => ['苹果', '水梨', '橙子', '桃', '提子', '沙田柚', '雪梨', '车厘子', '柚子', '草莓', '樱桃', '金桔', '葡萄'],
'奶类' => ['牛奶', '低脂奶', '脱脂奶', '低脂乳酪', '红茶', '优格', '无糖豆浆'],
'奶类' => ['牛奶', '低脂奶', '脱脂奶', '驼奶粉', '驼奶', '低脂乳酪', '红茶', '优格', '无糖豆浆'],
],
self::LEVEL_MEDIUM => [
'主食类' => ['红米饭', '糙米饭', '西米', '乌冬面', '面包', '麦片', '番薯', '芋头'],
@@ -36,40 +36,46 @@ class GiKnowledge
],
];
/** @var list<array{breakfast:string,lunch:string,dinner:string,tips:string}> */
/** @var list<array{breakfast:string,drinks:string,lunch:string,dinner:string,tips:string}> */
private static array $mealTemplates = [
[
'breakfast' => '玉米碴子粥、煮鸡蛋、拌黄瓜',
'breakfast' => '驼奶粉(温水冲服)、玉米碴子粥、煮鸡蛋、拌黄瓜',
'drinks' => '白天多喝温开水;早餐冲驼奶粉,别喝甜饮料、果汁。',
'lunch' => '清炖鲤鱼、豆腐炖白菜、清炒豆角、米饭小半碗',
'dinner' => '玉米面饼一个、瘦肉丝、凉拌茄子',
'tips' => '中午鱼豆肉吃足;早中晚淀粉别重复,一顿一种主食就够。',
],
[
'breakfast' => '小米粥、煮鸡蛋、小碟咸菜(少盐)',
'breakfast' => '驼奶粉(温水冲服)、小米粥、煮鸡蛋、小碟咸菜(少盐)',
'drinks' => '温开水为主;驼奶粉跟早餐一起吃,下午也记得喝水。',
'lunch' => '去皮炖鸡肉、芹菜炒豆干、木耳炒鸡蛋',
'dinner' => '蒸红薯(小半块)、清炒菠菜、豆腐汤',
'tips' => '午餐最讲究蛋白;晚上才吃薯,别跟中午再配大米饭。',
],
[
'breakfast' => '豆腐脑(少卤)、鸡蛋、一个苹果',
'breakfast' => '驼奶粉(温水冲服)、豆腐脑(少卤)、鸡蛋',
'drinks' => '早餐饮驼奶粉;白天喝白开水,别碰含糖饮料。',
'lunch' => '清炖鸡肉、炖芸豆、番茄炒蛋、贴饼子一个',
'dinner' => '挂面一小碗、清蒸小鱼、拌海带丝',
'tips' => '中午饼配肉豆,蛋白管够;面条放晚上,别三顿都是粥饭。',
],
[
'breakfast' => '高粱米粥、茶叶蛋、拌萝卜丝',
'breakfast' => '驼奶粉(温水冲服)、高粱米粥、茶叶蛋、拌萝卜丝',
'drinks' => '温开水;早餐驼奶粉按说明冲,别加糖。',
'lunch' => '炖牛肉(瘦)、豆腐炒韭菜、清炒油菜、杂豆饭小半碗',
'dinner' => '蒸南瓜(小半碗)、鸡蛋羹、拍黄瓜',
'tips' => '中午肉蛋豆要多吃;南瓜算晚饭主食,别再加馒头稀饭。',
],
[
'breakfast' => '棒子面粥、水煮蛋、大拌菜(少油)',
'breakfast' => '驼奶粉(温水冲服)、棒子面粥、水煮蛋、大拌菜(少油)',
'drinks' => '多喝温开水;驼奶粉放早餐,忌可乐汽水果汁。',
'lunch' => '清炖鲫鱼、番茄炒蛋、炖扁豆、糙米饭小半碗',
'dinner' => '玉米面饼一个、清炒豆芽、豆腐脑',
'tips' => '午饭鱼蛋豆齐上;玉米面放晚上,跟中午米饭分开。',
],
[
'breakfast' => '小米粥、煮鸡蛋、拌生菜',
'breakfast' => '驼奶粉(温水冲服)、小米粥、煮鸡蛋、拌生菜',
'drinks' => '白天小口多次喝温水;早餐冲好驼奶粉再吃饭。',
'lunch' => '白切鸡(去皮)、炖豆腐、清炒豆角、二米饭小半碗',
'dinner' => '杂面馒头一个、瘦肉炒芹菜、凉拌黄瓜',
'tips' => '中午蛋白打主力;三顿别都喝粥吃面,一顿一种淀粉。',
@@ -92,7 +98,8 @@ class GiKnowledge
private static array $aliases = [
'棒子面' => '荞麦面', '玉米碴' => '粟米', '地瓜' => '番薯', '红薯' => '番薯',
'洋芋' => '芋头', '土豆' => '芋头', '窝头' => '通心粉', '贴饼子' => '通心粉',
'二米饭' => '红米饭', '稀饭' => '白饭', '白稀饭' => '白饭', '糖糕' => '麦芽糖',
'二米饭' => '红米饭', '稀饭' => '白饭', '白稀饭' => '白饭', '糖糕' => '麦芽糖',
'驼奶' => '驼奶粉',
];
/**
@@ -132,6 +139,8 @@ class GiKnowledge
public static function ruralMealHints(): string
{
return implode("\n", [
'【早餐】须安排驼奶粉温水冲服(低 GI 奶类,稳血糖),可与粥、蛋、菜同餐。',
'【喝的】单独说明:温开水为主;早餐驼奶粉;严禁含糖饮料、果汁、酒。',
'【午餐】蛋白质要最高:至少 2 样高蛋白(鱼/鸡鸭/瘦肉/蛋/豆腐/豆干),肉蛋豆是午饭主角。',
'【淀粉不重复】粥/饭/面/饼/窝头/红薯/土豆/玉米/南瓜等算淀粉;早中晚各最多 1 种,且三顿不能同一种、不要顿顿大主食。',
'推荐蛋白:鸡蛋、豆腐、豆干、自家鸡鸭蛋、瘦肉、鲫鱼鲤鱼;',
@@ -177,6 +186,7 @@ class GiKnowledge
$plan = [
'breakfast' => $meal['breakfast'],
'drinks' => $meal['drinks'] ?? '白天多喝温开水;早餐冲驼奶粉,别喝甜饮。',
'lunch' => $meal['lunch'],
'dinner' => $meal['dinner'],
'tips' => $meal['tips'],
@@ -310,4 +320,68 @@ class GiKnowledge
$q = trim($q);
return $q !== '' ? $q : $question;
}
/**
* 血糖照护 AI 关闭时的规则化方案(监测 + 中医调理方向)
*
* @param array<string,mixed> $stats7
* @param array<string,mixed> $stats30
* @param array<string,mixed> $extra syndrome_type, symptoms
* @return array<string,mixed>
*/
public static function fallbackBloodCarePlan(array $stats7 = [], array $stats30 = [], array $extra = []): array
{
$exercise = self::exercisePlan($stats7, $stats30);
$level = (string) ($exercise['level'] ?? 'unknown');
$compliance = (int) (($stats30['record_days'] ?? 0) > 0 ? ($stats30['compliance'] ?? 0) : ($stats7['compliance'] ?? 0));
$syndrome = trim((string) ($extra['syndrome_type'] ?? ''));
$symptoms = trim((string) ($extra['symptoms'] ?? ''));
$summary = match ($level) {
'high' => '近阶段血糖偏高天数偏多,要把监测、饮食和运动一起抓紧,别只靠感觉。',
'medium' => '血糖大体可控,但还有波动,坚持记录和餐后活动,能更稳。',
'good' => '近期血糖控制不错,保持现在的节奏,定期复查别松懈。',
default => '先坚持记血糖、看趋势,再按下面几条慢慢调整生活方式。',
};
$bloodAdvice = match ($level) {
'high' => '空腹、餐后都测,偏高那天记下吃了啥、动了没。主食换杂粮、每餐先吃菜和蛋白,甜饮糕点先停。三餐后按上面运动建议动起来,头晕心慌马上停、吃点糖。',
'medium' => '每周至少 4 天空腹+餐后配对记录。主食定量、晚餐别过饱,少喝粥配咸菜。餐后散步 20 分钟,睡眠规律,情绪紧张也会抬糖。',
'good' => '保持每周 3–4 次监测即可,注意节假日和串门聚餐别放松。继续低 GI 家常饭,运动别停,体重稳一点更好。',
default => '先把血糖记满 7 天再看趋势。吃饭定时定量,多喝白开水,少甜少油,饭后多走动。',
};
$tcmBase = '消渴病多与肺脾肾气虚、阴虚燥热或痰湿瘀血相关,需结合面诊脉象。';
if ($syndrome !== '') {
$tcmBase = '就诊辨证倾向「' . $syndrome . '」。';
} elseif ($symptoms !== '') {
$tcmBase = '结合您登记的症状(' . mb_substr($symptoms, 0, 40) . '…),宜辨证论治。';
}
$tcmPlan = match ($level) {
'high' => $tcmBase . '调理思路:益气养阴、清热生津为主,可配合健脾化湿;食疗如山药、麦冬、桑叶代茶饮方向;艾灸可选足三里、胰俞等穴(需医师指导)。勿自行加大西药或乱用偏方,务必复诊调方。',
'medium' => $tcmBase . '调理思路:健脾益气、滋阴润燥,少食肥甘,宜清淡易消化;可配合八段锦、太极拳;中药汤剂需医师面诊后开具,此处不给具体方名剂量。',
'good' => $tcmBase . '调理思路:顾护脾胃、养阴防燥,季节更替注意保暖;维持规律作息与情志舒畅,按医嘱定期复诊即可。',
default => $tcmBase . '建议到中医科面诊,明确证型后再定汤药、针灸方案;平时可饮食清淡、作息规律,配合记录血糖供医师参考。',
};
$watchPoints = match ($level) {
'high' => ['空腹≥7 或餐后明显升高要联系村医', '脚麻脚痛、视力模糊及时就诊', '用药时间饭点别乱改'],
'medium' => ['外出走亲也别忘了测糖', '感冒发烧会让血糖乱,多监测', '睡前别加餐太多'],
'good' => ['每 3–6 个月复查糖化血红蛋白', '血压血脂一并管', '家人提醒别擅自停药'],
default => ['坚持每日或隔日记录', '记录与饮食运动一起给大夫看', '不适及时线下就诊'],
};
return [
'summary' => $summary,
'blood_advice' => $bloodAdvice,
'tcm_plan' => $tcmPlan,
'watch_points' => $watchPoints,
'next_steps' => '下周继续记血糖;饮食可参考本页「AI 饮食建议」;若连续 3 天偏高或出现胸闷脚肿,请尽快线下就诊或视频问诊。',
'control_level' => $level,
'control_label' => (string) ($exercise['level_label'] ?? ''),
'disclaimer' => '本建议供日常参考,不能替代医师面诊、开方与调药。',
'source' => 'rule',
];
}
}
@@ -0,0 +1,303 @@
<?php
declare(strict_types=1);
namespace app\command;
use app\common\model\tcm\PrescriptionOrder;
use app\common\service\ExpressTrackingService;
use think\console\Command;
use think\console\Input;
use think\console\input\Option;
use think\console\Output;
/**
* 批量回填处方业务订单「签收时间」到物流库(express_tracking / express_trace)。
*
* 背景:导出列表的「签收日期」来自库表(ExpressTrackingService::batchResolveSignUnixFromDbForOrders,无 HTTP)。
* 终态订单(已完成3/已签收6)被 express:auto-update 排除、或从未创建过物流追踪记录时,库内就没有签收时间,
* 导出便显示为空。本命令对这些订单逐单走一次快递100 并落库(queryAndUpdate 会写入 sign_time + 轨迹),
* 之后导出直接读库即可,既快又不空。
*
* 使用方法:
* php think tcm:backfill-sign-time --dry-run
* php think tcm:backfill-sign-time --limit=300 --sleep-ms=200
* php think tcm:backfill-sign-time --fulfillment-status=3,5,6 --days=90
* php think tcm:backfill-sign-time --order-id=474
* php think tcm:backfill-sign-time --force # 库内已有签收时间也重查
* php think tcm:backfill-sign-time --include-gancao # 同时处理甘草托管单(默认跳过,由甘草侧拉取)
*
* 履约状态代码(fulfillment_status):1待双审通过 2待发货 3已完成 4已取消 5已发货 6已签收
* 7进行中 8暂不制药 9拒收 10退款 11保留药方 12制药缓发
*
* 建议配 crontab(如每小时一次,限量推进历史欠数据):
* 0 * * * * cd /path/to/server && php think tcm:backfill-sign-time --limit=300 >> /dev/null 2>&1
*/
class TcmBackfillPrescriptionOrderSignTime extends Command
{
/**
* 默认不按履约状态过滤:只要订单有运单号(tracking_number)即视为已发货,纳入回填。
* fulfillment_status 过滤会漏掉「物流已送达、但业务状态滞后(如进行中)」的单子。
* 仍可用 --fulfillment-status 显式收窄。
*/
private const DEFAULT_FULFILLMENT_STATUSES = [];
protected function configure(): void
{
$this->setName('tcm:backfill-sign-time')
->setDescription('批量回填处方业务订单签收时间到物流库(导出读库即可,不再逐单 HTTP)')
->addOption('dry-run', null, Option::VALUE_NONE, '只列出待处理订单,不发起查询/不落库')
->addOption('limit', 'l', Option::VALUE_OPTIONAL, '本次最多补查(HTTP)订单数', 300)
->addOption('order-id', null, Option::VALUE_OPTIONAL, '仅处理指定 prescription_order.id', null)
->addOption('min-id', null, Option::VALUE_OPTIONAL, '仅 id>=该值', null)
->addOption('max-id', null, Option::VALUE_OPTIONAL, '仅 id<=该值', null)
->addOption('days', null, Option::VALUE_OPTIONAL, '仅处理近 N 天创建的订单(0=不限)', 0)
->addOption(
'fulfillment-status',
null,
Option::VALUE_OPTIONAL,
'仅处理指定履约状态,逗号分隔数字(如 3,5,6);默认不限(有运单号即处理)',
null
)
->addOption('repair-db', null, Option::VALUE_NONE, '仅按库内轨迹重算并修复 sign_time 列(零 HTTP,修复历史「拉取当天」污染)')
->addOption('include-gancao', null, Option::VALUE_NONE, '同时处理甘草托管单(默认跳过)')
->addOption('force', 'f', Option::VALUE_NONE, '库内已有签收时间也重新查询落库')
->addOption('sleep-ms', null, Option::VALUE_OPTIONAL, '每次 HTTP 查询后休眠毫秒,减轻快递100 压力', 200)
->addOption('detail', 'd', Option::VALUE_NONE, '打印每条明细');
}
/**
* @return int[]|null 解析失败(传了非法值)返回 null;未传返回默认集
*/
private static function parseFulfillmentStatusOption(?string $raw): ?array
{
if ($raw === null || trim($raw) === '') {
return self::DEFAULT_FULFILLMENT_STATUSES;
}
$seen = [];
foreach (explode(',', $raw) as $part) {
$n = (int) trim($part);
if ($n >= 1 && $n <= 12) {
$seen[$n] = true;
}
}
$ids = array_keys($seen);
sort($ids);
return $ids !== [] ? $ids : null;
}
protected function execute(Input $input, Output $output): int
{
$dryRun = (bool) $input->getOption('dry-run');
$verbose = (bool) $input->getOption('detail');
$force = (bool) $input->getOption('force');
$includeGancao = (bool) $input->getOption('include-gancao');
$limit = max(1, (int) $input->getOption('limit'));
$sleepMs = max(0, (int) $input->getOption('sleep-ms'));
$days = max(0, (int) $input->getOption('days'));
$onlyOrderId = $input->getOption('order-id');
$onlyOrderId = $onlyOrderId !== null && $onlyOrderId !== '' ? (int) $onlyOrderId : null;
$minId = $input->getOption('min-id');
$minId = $minId !== null && $minId !== '' ? (int) $minId : null;
$maxId = $input->getOption('max-id');
$maxId = $maxId !== null && $maxId !== '' ? (int) $maxId : null;
$fsRaw = $input->getOption('fulfillment-status');
$fulfillmentStatuses = self::parseFulfillmentStatusOption($fsRaw !== null ? (string) $fsRaw : null);
if ($fulfillmentStatuses === null) {
$output->error('--fulfillment-status 格式无效,请传入 1–12 的数字,英文逗号分隔,例如:3,5,6');
return 1;
}
$output->writeln('========================================');
$output->writeln('批量回填业务订单签收时间(快递100 落库)');
$output->writeln('========================================');
// 纯库内修复模式:用现有轨迹重算签收时间,覆盖被写成「同步当天」的 sign_time 列(零 HTTP
if ((bool) $input->getOption('repair-db')) {
$output->writeln('模式:仅修复 sign_time 列(按库内轨迹重算,不查快递100)...');
$stat = ExpressTrackingService::repairSignTimeFromDbTraces($dryRun, $dryRun ? 0 : $limit);
$output->writeln('');
$output->writeln('========================================');
$output->writeln(sprintf(
'修复完成:扫描已签收运单 %d,修正 sign_time %d,无需修正 %ddry-run=%s',
$stat['scanned'],
$stat['updated'],
$stat['skipped'],
$dryRun ? 'yes' : 'no'
));
$output->writeln('========================================');
return 0;
}
$q = PrescriptionOrder::whereNull('delete_time')
->whereRaw("TRIM(COALESCE(`tracking_number`, '')) <> ''")
->order('id', 'desc');
if ($onlyOrderId !== null && $onlyOrderId > 0) {
$q->where('id', $onlyOrderId);
} else {
// 空集 = 不按履约状态过滤(默认):有运单号即纳入,避免漏掉业务状态滞后的已送达单
if ($fulfillmentStatuses !== []) {
$q->whereIn('fulfillment_status', $fulfillmentStatuses);
}
if (!$includeGancao) {
$q->whereRaw("TRIM(COALESCE(`gancao_reciperl_order_no`, '')) = ''");
}
if ($minId !== null && $minId > 0) {
$q->where('id', '>=', $minId);
}
if ($maxId !== null && $maxId > 0) {
$q->where('id', '<=', $maxId);
}
if ($days > 0) {
$q->where('create_time', '>=', time() - $days * 86400);
}
// 多取一些候选,DB 已可解析的会被跳过,确保「需要补查」的能凑够 limit
$q->limit($limit * 5);
}
$rows = $q->field(['id', 'order_no', 'tracking_number', 'fulfillment_status', 'create_time'])
->select()
->toArray();
if ($rows === []) {
$output->warning('没有符合条件的订单。');
return 0;
}
// 先批量从库表解析,跳过已有签收时间的(除非 --force),避免无谓 HTTP
$dbSignByPo = [];
if (!$force) {
$signItems = [];
foreach ($rows as $r) {
$signItems[] = [
'order_id' => (int) ($r['id'] ?? 0),
'tracking_number' => trim((string) ($r['tracking_number'] ?? '')),
];
}
try {
$dbSignByPo = ExpressTrackingService::batchResolveSignUnixFromDbForOrders($signItems);
} catch (\Throwable $e) {
$output->warning('批量库表解析失败,将逐单补查:' . $e->getMessage());
$dbSignByPo = [];
}
}
$candidates = [];
$alreadyInDb = 0;
foreach ($rows as $r) {
$id = (int) ($r['id'] ?? 0);
if ($id <= 0) {
continue;
}
if (!$force && (int) ($dbSignByPo[$id] ?? 0) > 0) {
$alreadyInDb++;
continue;
}
$candidates[] = $r;
if (count($candidates) >= $limit) {
break;
}
}
$output->writeln(sprintf(
'候选 %d 条(库内已有签收 %d 条已跳过);本次最多补查 %d 条(dry-run=%s force=%s sleep-ms=%d',
count($rows),
$alreadyInDb,
$limit,
$dryRun ? 'yes' : 'no',
$force ? 'yes' : 'no',
$sleepMs
));
if ($candidates === []) {
$output->writeln('无需补查:候选订单签收时间均已在库。');
return 0;
}
$resolved = 0; // 查到并落库了签收时间
$empty = 0; // 查询成功但暂无签收(在途等)
$failed = 0;
$totalToDo = count($candidates);
// 每单一次快递100 HTTP + sleep,串行较慢;定期打印进度,避免看起来卡住。
$progressEvery = max(1, (int) min(20, max(5, (int) ceil($totalToDo / 20))));
if (!$dryRun) {
$output->writeln(sprintf(
'开始逐单补查(共 %d 单,每单一次快递100 查询 + %dms 间隔,请耐心等待,每 %d 单报告一次进度)...',
$totalToDo,
$sleepMs,
$progressEvery
));
}
$startTs = microtime(true);
$done = 0;
foreach ($candidates as $r) {
$id = (int) ($r['id'] ?? 0);
$tn = trim((string) ($r['tracking_number'] ?? ''));
if ($dryRun) {
if ($verbose) {
$output->writeln(sprintf('#%d %s 运单=%s dry-run,跳过查询)', $id, (string) ($r['order_no'] ?? ''), $tn));
}
continue;
}
try {
$signTs = ExpressTrackingService::syncSignUnixFromLogisticsForPrescriptionOrder($id, true);
if ($signTs > 0) {
$resolved++;
if ($verbose) {
$output->writeln(sprintf('#%d 运单=%s → 签收 %s', $id, $tn, date('Y-m-d H:i:s', $signTs)));
}
} else {
$empty++;
if ($verbose) {
$output->writeln(sprintf('#%d 运单=%s → 暂无签收(在途/未匹配)', $id, $tn));
}
}
} catch (\Throwable $e) {
$failed++;
if ($verbose) {
$output->writeln("<error>#{$id} 运单={$tn} 失败:{$e->getMessage()}</error>");
}
}
$done++;
// 非明细模式也给出心跳进度(明细模式已逐单打印,无需重复)
if (!$verbose && ($done % $progressEvery === 0 || $done === $totalToDo)) {
$elapsed = max(0.001, microtime(true) - $startTs);
$rate = $done / $elapsed;
$eta = $rate > 0 ? (int) round(($totalToDo - $done) / $rate) : 0;
$output->writeln(sprintf(
' 进度 %d/%d(落库 %d,暂无 %d,失败 %d)已用 %ds,预计剩余 %ds',
$done,
$totalToDo,
$resolved,
$empty,
$failed,
(int) round($elapsed),
$eta
));
}
if ($sleepMs > 0) {
usleep($sleepMs * 1000);
}
}
$output->writeln('');
$output->writeln('========================================');
$output->writeln(sprintf('完成:落库签收 %d,暂无签收 %d,失败 %d', $resolved, $empty, $failed));
$output->writeln('========================================');
return $failed > 0 ? 2 : 0;
}
}
@@ -333,7 +333,14 @@ class ExpressTrackingService
// 检查是否签收
if (ExpressTracking::isFinalState($tracking->current_state)) {
$tracking->is_signed = 1;
$tracking->sign_time = $now;
// 签收时间只取真实「妥投/签收/送达」轨迹时间;解析不到就保持原值,
// 绝不用同步当天 $now 冒充签收日(否则 sign_time 列会被写成「拉取当天」,导出/详情/业绩全错)。
$signFromTrace = (!empty($result['traces']) && is_array($result['traces']))
? self::effectiveSignUnixFromKuaidiPayload($result)
: 0;
if ($signFromTrace > 0) {
$tracking->sign_time = $signFromTrace;
}
$tracking->auto_update = 0; // 停止自动更新
}
@@ -1247,8 +1254,24 @@ class ExpressTrackingService
*/
private static function effectiveSignUnixFromTrackingDetail(array $detail): int
{
$a = (int) ($detail['sign_time'] ?? 0);
$traceBased = self::traceBasedSignUnixFromTrackingDetail($detail);
if ($traceBased > 0) {
return $traceBased;
}
// 最后兜底才用 sign_time 列(历史同步可能把「拉取当天」写进该列,故放最后)。
return (int) ($detail['sign_time'] ?? 0);
}
/**
* 纯轨迹/主表推导的签收时间(不含 sign_time 列兜底):
* 签收/妥投/送达轨迹($b)→ 已签收态下最新轨迹($c)→ 主表 latest_trace_time 回填。
* 修复 sign_time 列时用它判断「列值是否被污染」(与轨迹真实时间不符)。
*
* @param array<string, mixed> $detail
*/
private static function traceBasedSignUnixFromTrackingDetail(array $detail): int
{
$traces = $detail['traces'] ?? [];
if (!is_array($traces)) {
$traces = [];
@@ -1283,7 +1306,12 @@ class ExpressTrackingService
}
}
return max($a, $b, $c, self::signedTrackingHeadFallbackUnix($detail, $stateOk));
$traceBased = $b > 0 ? $b : $c;
if ($traceBased > 0) {
return $traceBased;
}
return self::signedTrackingHeadFallbackUnix($detail, $stateOk);
}
/**
@@ -1316,8 +1344,28 @@ class ExpressTrackingService
}
$st = (string) ($tr['status'] ?? '');
$ctx = (string) ($tr['trace_context'] ?? '');
$hay = $st . ' ' . $ctx;
foreach (['签收', '妥投', '已送达', '本人签收'] as $k) {
return self::contextLooksSigned($st . ' ' . $ctx);
}
/**
* 文案是否表示「已签收/已送达」。
* 先排除「派送中,请您准备签收 / 待签收」等「尚未签收」文案(含「签收」二字易误判),再匹配正向关键词。
*/
private static function contextLooksSigned(string $hay): bool
{
if ($hay === '') {
return false;
}
// 负向:出库派送阶段的「准备签收/待签收」等,并非真正签收
foreach (['准备签收', '待签收', '等待签收', '预计', '即将送达'] as $neg) {
if (mb_stripos($hay, $neg) !== false) {
return false;
}
}
// 「送达」不带「已」(顺丰末端 status=送达 / context=快件已放在…)也算签收;
// 「预计送达 / 即将送达」已在上面负向词排除,不会误判。
foreach (['签收', '妥投', '送达', '本人签收', '已放在'] as $k) {
if (mb_stripos($hay, $k) !== false) {
return true;
}
@@ -1363,13 +1411,14 @@ class ExpressTrackingService
}
$tstr = (string) ($row['time'] ?? '');
$ctx = (string) ($row['context'] ?? '');
// 顺丰等末端把签收信息放在 status(如「送达」)而非 context,故连同 status 一起判断
$st = (string) ($row['status'] ?? '');
$p = strtotime($tstr);
$ts = $p !== false ? (int) $p : 0;
if ($ts <= 0) {
continue;
}
$signedLike = self::plainContextLooksSigned($ctx);
if ($signedLike) {
if (self::contextLooksSigned($ctx . ' ' . $st)) {
$best = max($best, $ts);
}
}
@@ -1395,17 +1444,6 @@ class ExpressTrackingService
return $best;
}
private static function plainContextLooksSigned(string $context): bool
{
foreach (['签收', '妥投', '已送达', '本人签收', '快件已送达'] as $k) {
if (mb_stripos($context, $k) !== false) {
return true;
}
}
return false;
}
/**
* 提成结算 overview 等大批量场景:按订单批量从库表推导签收时间(不调用快递100)。
* 语义与 YejiStatsLogic 物流子查询 / effectiveSignUnixFromTrackingDetail 对齐。
@@ -1443,27 +1481,35 @@ class ExpressTrackingService
return [];
}
$trackingById = [];
// 仅保留「每个运单算好的签收 Unix」(int),不保留整行轨迹,避免一次性 hydrate 大量 express_trace
// 行导致内存溢出(导出全量 / 批量回填会传入上千运单,每单又有几十条轨迹)。
$signUnixByTrackingId = [];
$orderToTrackingIds = [];
$tnToTrackingIds = [];
$attachTrackingRow = static function (array $etRow) use (&$trackingById, &$orderToTrackingIds, &$tnToTrackingIds): void {
$tid = (int) ($etRow['id'] ?? 0);
$consumeTracking = static function ($tracking) use (&$signUnixByTrackingId, &$orderToTrackingIds, &$tnToTrackingIds): void {
$tid = (int) ($tracking->id ?? 0);
if ($tid <= 0) {
return;
}
$trackingById[$tid] = $etRow;
$oid = (int) ($etRow['order_id'] ?? 0);
if (!array_key_exists($tid, $signUnixByTrackingId)) {
$row = $tracking->toArray();
$row['traces'] = $tracking->traces ? $tracking->traces->toArray() : [];
// 算完即丢弃整行轨迹,仅留下结果 int
$signUnixByTrackingId[$tid] = (int) self::effectiveSignUnixFromTrackingDetail($row);
}
$oid = (int) ($tracking->order_id ?? 0);
if ($oid > 0) {
$orderToTrackingIds[$oid][$tid] = true;
}
$tn = trim((string) ($etRow['tracking_number'] ?? ''));
$tn = trim((string) ($tracking->tracking_number ?? ''));
if ($tn !== '') {
$tnToTrackingIds[$tn][$tid] = true;
}
};
foreach (array_chunk($orderIds, 800) as $chunk) {
// 小批量流式处理,每批结束后大对象即可回收,峰值内存只与单批运单的轨迹量相关
foreach (array_chunk($orderIds, 200) as $chunk) {
$byOrderId = ExpressTracking::with(['traces' => static function ($query): void {
$query->order('trace_time_stamp', 'desc');
}])
@@ -1471,15 +1517,14 @@ class ExpressTrackingService
->whereNull('delete_time')
->select();
foreach ($byOrderId as $tracking) {
$row = $tracking->toArray();
$row['traces'] = $tracking->traces ? $tracking->traces->toArray() : [];
$attachTrackingRow($row);
$consumeTracking($tracking);
}
unset($byOrderId);
}
$trackingNumList = array_keys($trackingNums);
if ($trackingNumList !== []) {
foreach (array_chunk($trackingNumList, 400) as $chunk) {
foreach (array_chunk($trackingNumList, 200) as $chunk) {
$byTn = ExpressTracking::with(['traces' => static function ($query): void {
$query->order('trace_time_stamp', 'desc');
}])
@@ -1487,10 +1532,9 @@ class ExpressTrackingService
->whereNull('delete_time')
->select();
foreach ($byTn as $tracking) {
$row = $tracking->toArray();
$row['traces'] = $tracking->traces ? $tracking->traces->toArray() : [];
$attachTrackingRow($row);
$consumeTracking($tracking);
}
unset($byTn);
}
}
@@ -1509,11 +1553,7 @@ class ExpressTrackingService
}
$best = 0;
foreach (array_keys($candidateIds) as $tid) {
$detail = $trackingById[(int) $tid] ?? null;
if (!is_array($detail)) {
continue;
}
$best = max($best, self::effectiveSignUnixFromTrackingDetail($detail));
$best = max($best, (int) ($signUnixByTrackingId[(int) $tid] ?? 0));
}
if ($best > 0) {
$out[$oid] = $best;
@@ -1522,4 +1562,61 @@ class ExpressTrackingService
return $out;
}
/**
* 纯库内修复:按现有 express_trace 轨迹重算每个「已签收」运单的真实签收时间,
* 回写 express_tracking.sign_time。不调用快递100(零 HTTP)。
*
* 处理历史污染:早期 queryAndUpdate 把「同步当天 $now」写进 sign_time,签收后 auto_update=0 不再纠正,
* 导致 sign_time 列=拉取当天而非真实送达日。本方法用轨迹时间覆盖被污染的列值。
*
* @return array{scanned:int, updated:int, skipped:int} 统计
*/
public static function repairSignTimeFromDbTraces(bool $dryRun = false, int $limit = 0): array
{
$scanned = 0;
$updated = 0;
$skipped = 0;
$query = ExpressTracking::with(['traces' => static function ($q): void {
$q->order('trace_time_stamp', 'desc');
}])
->whereNull('delete_time')
->where(function ($q): void {
$q->where('is_signed', 1)
->whereOr('current_state', ExpressTracking::STATE_SIGNED);
})
->order('id', 'asc');
$query->chunk(200, function ($trackings) use (&$scanned, &$updated, &$skipped, $dryRun, $limit): bool {
foreach ($trackings as $tracking) {
$scanned++;
$detail = $tracking->toArray();
$detail['traces'] = $tracking->traces ? $tracking->traces->toArray() : [];
$trueSign = self::traceBasedSignUnixFromTrackingDetail($detail);
$cur = (int) ($tracking->sign_time ?? 0);
// 仅当轨迹能推导出真实签收时间、且与现列值不同(按秒)时才修复
if ($trueSign <= 0 || $trueSign === $cur) {
$skipped++;
continue;
}
if (!$dryRun) {
$tracking->sign_time = $trueSign;
$tracking->save();
}
$updated++;
if ($limit > 0 && $updated >= $limit) {
return false; // 终止 chunk 遍历
}
}
return true;
}, 'id');
return ['scanned' => $scanned, 'updated' => $updated, 'skipped' => $skipped];
}
}
+2
View File
@@ -34,6 +34,8 @@ return [
'gancao:sync-logistics' => 'app\\command\\GancaoSyncLogisticsRoute',
// 历史 internal_cost:批量甘草预报价回填(CTM_PREVIEW
'tcm:backfill-internal-cost' => 'app\\command\\TcmBackfillPrescriptionOrderInternalCost',
// 批量回填业务订单签收时间到物流库(导出读库即可,不再逐单 HTTP)
'tcm:backfill-sign-time' => 'app\\command\\TcmBackfillPrescriptionOrderSignTime',
// 迁移诊单图片到医生备注表
'migrate:images-to-doctor-note' => 'app\\command\\MigrateImagesToDoctorNote',
// 诊单待办事项:扫描到点的待执行项并向创建人发送企业微信消息
@@ -1 +1 @@
import r from"./error-NnOocA9Z.js";import{f as p,ak as i,I as m,a as e,aN as s,J as o}from"./@vue/runtime-core-C6bnekPw.js";import"./element-plus-lurTijPg.js";import"./@vue/runtime-dom-DDAG46FW.js";import"./lodash-D3kF6u-c.js";import"./@vue/shared-mAAVTE9n.js";import"./@vue/reactivity-DiY1c2vO.js";import"./@element-plus/icons-vue-HOEUG8sr.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./vue-router-QlpZ4wdW.js";import"./index-hTkp7Jz4.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./@vueuse/core-1S4fJlii.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";const a="/admin/assets/no_perms-jDxcYpYC.png",n={class:"error404"},W=p({__name:"403",setup(c){return(_,t)=>(i(),m("div",n,[e(r,{code:"403",title:"您的账号权限不足,请联系管理员添加权限!","show-btn":!1},{content:s(()=>[...t[0]||(t[0]=[o("div",{class:"flex justify-center"},[o("img",{class:"w-[150px] h-[150px]",src:a,alt:""})],-1)])]),_:1})]))}});export{W as default};
import r from"./error-BFWhnQ-x.js";import{f as p,ak as i,I as m,a as e,aN as s,J as o}from"./@vue/runtime-core-C6bnekPw.js";import"./element-plus-lurTijPg.js";import"./@vue/runtime-dom-DDAG46FW.js";import"./lodash-D3kF6u-c.js";import"./@vue/shared-mAAVTE9n.js";import"./@vue/reactivity-DiY1c2vO.js";import"./@element-plus/icons-vue-HOEUG8sr.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./vue-router-QlpZ4wdW.js";import"./index-Cr_vnZ4Y.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./@vueuse/core-1S4fJlii.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";const a="/admin/assets/no_perms-jDxcYpYC.png",n={class:"error404"},W=p({__name:"403",setup(c){return(_,t)=>(i(),m("div",n,[e(r,{code:"403",title:"您的账号权限不足,请联系管理员添加权限!","show-btn":!1},{content:s(()=>[...t[0]||(t[0]=[o("div",{class:"flex justify-center"},[o("img",{class:"w-[150px] h-[150px]",src:a,alt:""})],-1)])]),_:1})]))}});export{W as default};
@@ -1 +1 @@
import o from"./error-NnOocA9Z.js";import{f as r,ak as t,I as m,a as p}from"./@vue/runtime-core-C6bnekPw.js";import"./element-plus-lurTijPg.js";import"./@vue/runtime-dom-DDAG46FW.js";import"./lodash-D3kF6u-c.js";import"./@vue/shared-mAAVTE9n.js";import"./@vue/reactivity-DiY1c2vO.js";import"./@element-plus/icons-vue-HOEUG8sr.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./vue-router-QlpZ4wdW.js";import"./index-hTkp7Jz4.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./@vueuse/core-1S4fJlii.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";const i={class:"error404"},T=r({__name:"404",setup(e){return(a,s)=>(t(),m("div",i,[p(o,{code:"404",title:"哎呀,出错了!您访问的页面不存在…"})]))}});export{T as default};
import o from"./error-BFWhnQ-x.js";import{f as r,ak as t,I as m,a as p}from"./@vue/runtime-core-C6bnekPw.js";import"./element-plus-lurTijPg.js";import"./@vue/runtime-dom-DDAG46FW.js";import"./lodash-D3kF6u-c.js";import"./@vue/shared-mAAVTE9n.js";import"./@vue/reactivity-DiY1c2vO.js";import"./@element-plus/icons-vue-HOEUG8sr.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./vue-router-QlpZ4wdW.js";import"./index-Cr_vnZ4Y.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./@vueuse/core-1S4fJlii.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";const i={class:"error404"},T=r({__name:"404",setup(e){return(a,s)=>(t(),m("div",i,[p(o,{code:"404",title:"哎呀,出错了!您访问的页面不存在…"})]))}});export{T as default};
@@ -1 +1 @@
import{M as T,N as C,T as $,r as D,d as L,L as k}from"./element-plus-lurTijPg.js";import{W as E}from"./@element-plus/icons-vue-HOEUG8sr.js";import{a3 as P}from"./tcm-BWcL4HTv.js";import{f as A,w as B,ak as p,I as b,aP as F,G as h,aN as n,a as i,O as m,J as M}from"./@vue/runtime-core-C6bnekPw.js";import{Q as c}from"./@vue/shared-mAAVTE9n.js";import{y as V,n as w}from"./@vue/reactivity-DiY1c2vO.js";import{_ as K}from"./index-hTkp7Jz4.js";import"./@vue/runtime-dom-DDAG46FW.js";import"./lodash-D3kF6u-c.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./vue-router-QlpZ4wdW.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./@vueuse/core-1S4fJlii.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";const q={class:"assign-log-panel"},z={key:1,class:"text-gray-400"},G=A({__name:"AssignLogPanel",props:{diagnosisId:{}},setup(N,{expose:v}){const _=N,d=w(!1),u=w([]);function y(o){const e=o.related_po_creator_name;if(e!=null&&String(e).trim()!=="")return String(e);const t=Number(o.related_po_creator_id);return Number.isFinite(t)&&t>0?`ID:${t}`:"—"}function x(o){const e=o.related_po_create_time_text;if(e!=null&&String(e).trim()!=="")return String(e);const t=Number(o.related_po_create_time);if(!Number.isFinite(t)||t<=0)return"—";const a=new Date(t*1e3);if(Number.isNaN(a.getTime()))return"—";const r=l=>String(l).padStart(2,"0");return`${a.getFullYear()}-${r(a.getMonth()+1)}-${r(a.getDate())} ${r(a.getHours())}:${r(a.getMinutes())}:${r(a.getSeconds())}`}function f(o,e){const t=e==="from"?"from_assistant_name":"to_assistant_name",a=e==="from"?"from_assistant_id":"to_assistant_id",r=o[t];if(r!=null&&String(r).trim()!==""&&String(r)!=="—")return String(r);const l=Number(o[a]);return Number.isFinite(l)&&l>0?`ID:${l}`:"—"}const g=async()=>{if(_.diagnosisId){d.value=!0;try{const o=await P({id:_.diagnosisId}),e=Array.isArray(o)?o:[];u.value=e}catch(o){console.error(o),u.value=[]}finally{d.value=!1}}};return B(()=>_.diagnosisId,()=>{g()},{immediate:!0}),v({refresh:g}),(o,e)=>{const t=C,a=$,r=L,l=D,S=T,I=k;return p(),b("div",q,[F((p(),h(S,{data:u.value,border:"",stripe:"","empty-text":"暂无指派记录"},{default:n(()=>[i(t,{label:"操作时间",width:"175",prop:"create_time_text"}),i(t,{label:"原医助","min-width":"120"},{default:n(({row:s})=>[m(c(f(s,"from")),1)]),_:1}),i(t,{label:"新医助","min-width":"120"},{default:n(({row:s})=>[m(c(f(s,"to")),1)]),_:1}),i(t,{label:"继承",width:"72",align:"center"},{default:n(({row:s})=>[Number(s.is_inherit)===1?(p(),h(a,{key:0,type:"success",size:"small"},{default:n(()=>[...e[0]||(e[0]=[m("是",-1)])]),_:1})):(p(),b("span",z,"否"))]),_:1}),i(t,{label:"快照·业务单创建人","min-width":"130","show-overflow-tooltip":""},{default:n(({row:s})=>[m(c(y(s)),1)]),_:1}),i(t,{label:"快照·业务单创建时间",width:"190"},{header:n(()=>[e[1]||(e[1]=M("span",null,"快照·业务单创建时间",-1)),i(l,{placement:"top",content:"related_po_create_time:触发本次操作的处方业务订单 create_time;与原文助一致时表示医助创建订单时间"},{default:n(()=>[i(r,{class:"assign-log-col-hint"},{default:n(()=>[i(V(E))]),_:1})]),_:1})]),default:n(({row:s})=>[m(c(x(s)),1)]),_:1}),i(t,{label:"操作人",width:"110",prop:"operator_name"}),i(t,{label:"操作账号",width:"120",prop:"operator_account","show-overflow-tooltip":""}),i(t,{label:"IP",width:"130",prop:"ip","show-overflow-tooltip":""})]),_:1},8,["data"])),[[I,d.value]])])}}}),Ct=K(G,[["__scopeId","data-v-f670e3e6"]]);export{Ct as default};
import{M as T,N as C,T as $,r as D,d as L,L as k}from"./element-plus-lurTijPg.js";import{W as E}from"./@element-plus/icons-vue-HOEUG8sr.js";import{a4 as P}from"./tcm-Cjb9ca7N.js";import{f as A,w as B,ak as p,I as b,aP as F,G as h,aN as n,a as i,O as m,J as M}from"./@vue/runtime-core-C6bnekPw.js";import{Q as c}from"./@vue/shared-mAAVTE9n.js";import{y as V,n as w}from"./@vue/reactivity-DiY1c2vO.js";import{_ as K}from"./index-Cr_vnZ4Y.js";import"./@vue/runtime-dom-DDAG46FW.js";import"./lodash-D3kF6u-c.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./vue-router-QlpZ4wdW.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./@vueuse/core-1S4fJlii.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";const q={class:"assign-log-panel"},z={key:1,class:"text-gray-400"},G=A({__name:"AssignLogPanel",props:{diagnosisId:{}},setup(N,{expose:v}){const _=N,d=w(!1),u=w([]);function y(o){const e=o.related_po_creator_name;if(e!=null&&String(e).trim()!=="")return String(e);const t=Number(o.related_po_creator_id);return Number.isFinite(t)&&t>0?`ID:${t}`:"—"}function x(o){const e=o.related_po_create_time_text;if(e!=null&&String(e).trim()!=="")return String(e);const t=Number(o.related_po_create_time);if(!Number.isFinite(t)||t<=0)return"—";const a=new Date(t*1e3);if(Number.isNaN(a.getTime()))return"—";const r=l=>String(l).padStart(2,"0");return`${a.getFullYear()}-${r(a.getMonth()+1)}-${r(a.getDate())} ${r(a.getHours())}:${r(a.getMinutes())}:${r(a.getSeconds())}`}function f(o,e){const t=e==="from"?"from_assistant_name":"to_assistant_name",a=e==="from"?"from_assistant_id":"to_assistant_id",r=o[t];if(r!=null&&String(r).trim()!==""&&String(r)!=="—")return String(r);const l=Number(o[a]);return Number.isFinite(l)&&l>0?`ID:${l}`:"—"}const g=async()=>{if(_.diagnosisId){d.value=!0;try{const o=await P({id:_.diagnosisId}),e=Array.isArray(o)?o:[];u.value=e}catch(o){console.error(o),u.value=[]}finally{d.value=!1}}};return B(()=>_.diagnosisId,()=>{g()},{immediate:!0}),v({refresh:g}),(o,e)=>{const t=C,a=$,r=L,l=D,S=T,I=k;return p(),b("div",q,[F((p(),h(S,{data:u.value,border:"",stripe:"","empty-text":"暂无指派记录"},{default:n(()=>[i(t,{label:"操作时间",width:"175",prop:"create_time_text"}),i(t,{label:"原医助","min-width":"120"},{default:n(({row:s})=>[m(c(f(s,"from")),1)]),_:1}),i(t,{label:"新医助","min-width":"120"},{default:n(({row:s})=>[m(c(f(s,"to")),1)]),_:1}),i(t,{label:"继承",width:"72",align:"center"},{default:n(({row:s})=>[Number(s.is_inherit)===1?(p(),h(a,{key:0,type:"success",size:"small"},{default:n(()=>[...e[0]||(e[0]=[m("是",-1)])]),_:1})):(p(),b("span",z,"否"))]),_:1}),i(t,{label:"快照·业务单创建人","min-width":"130","show-overflow-tooltip":""},{default:n(({row:s})=>[m(c(y(s)),1)]),_:1}),i(t,{label:"快照·业务单创建时间",width:"190"},{header:n(()=>[e[1]||(e[1]=M("span",null,"快照·业务单创建时间",-1)),i(l,{placement:"top",content:"related_po_create_time:触发本次操作的处方业务订单 create_time;与原文助一致时表示医助创建订单时间"},{default:n(()=>[i(r,{class:"assign-log-col-hint"},{default:n(()=>[i(V(E))]),_:1})]),_:1})]),default:n(({row:s})=>[m(c(x(s)),1)]),_:1}),i(t,{label:"操作人",width:"110",prop:"operator_name"}),i(t,{label:"操作账号",width:"120",prop:"operator_account","show-overflow-tooltip":""}),i(t,{label:"IP",width:"130",prop:"ip","show-overflow-tooltip":""})]),_:1},8,["data"])),[[I,d.value]])])}}}),Ct=K(G,[["__scopeId","data-v-f670e3e6"]]);export{Ct as default};
@@ -1 +1 @@
import{i as L,L as T,N as V,T as D,M as z,W as M}from"./element-plus-lurTijPg.js";import{ab as O}from"./tcm-BWcL4HTv.js";import{f as P,b as F,w as j,ak as a,I as n,a as i,aN as s,O as m,H as w,aP as A,G as g,F as H,J as R}from"./@vue/runtime-core-C6bnekPw.js";import{y as d,n as k}from"./@vue/reactivity-DiY1c2vO.js";import{Q as c}from"./@vue/shared-mAAVTE9n.js";import{_ as G}from"./index-hTkp7Jz4.js";import"./@vue/runtime-dom-DDAG46FW.js";import"./lodash-D3kF6u-c.js";import"./@element-plus/icons-vue-HOEUG8sr.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./vue-router-QlpZ4wdW.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./@vueuse/core-1S4fJlii.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";const J={class:"case-record-list"},Q={key:0,class:"mb-3 flex justify-end"},W={key:0},Y={key:1,class:"text-gray-400"},q={class:"void-detail text-xs text-gray-500 mt-1"},K=P({__name:"CaseRecordList",props:{diagnosisId:{type:Number,default:0},readOnly:{type:Boolean,default:!1}},emits:["view","openPrescription"],setup(y,{expose:x,emit:C}){const _=y,b=C,l=k([]),p=k(!1),u=async()=>{if(_.diagnosisId){p.value=!0;try{const e=await O({diagnosis_id:_.diagnosisId});l.value=Array.isArray(e)?e:[]}catch(e){console.error("获取病历记录失败:",e),l.value=[]}finally{p.value=!1}}},S=e=>{if(!e)return"";const t=new Date(e*1e3);return`${t.getFullYear()}-${String(t.getMonth()+1).padStart(2,"0")}-${String(t.getDate()).padStart(2,"0")} ${String(t.getHours()).padStart(2,"0")}:${String(t.getMinutes()).padStart(2,"0")}`},$=e=>{b("view",e)},B=()=>{b("openPrescription")};return F(()=>{u()}),j(()=>_.diagnosisId,()=>{u()}),x({refresh:u}),(e,t)=>{const h=L,r=V,v=D,E=z,N=M,I=T;return a(),n("div",J,[y.readOnly?w("",!0):(a(),n("div",Q,[i(h,{type:"primary",size:"small",onClick:B},{default:s(()=>[...t[0]||(t[0]=[m("开方",-1)])]),_:1})])),A((a(),g(E,{data:d(l),border:""},{default:s(()=>[i(r,{prop:"prescription_date",label:"就诊日期",width:"120"}),i(r,{prop:"visit_no",label:"门诊号",width:"120"}),i(r,{prop:"clinical_diagnosis",label:"临床诊断","min-width":"160","show-overflow-tooltip":""}),i(r,{label:"处方摘要","min-width":"180"},{default:s(({row:o})=>[o.herbs&&o.herbs.length?(a(),n("span",W,c(o.herbs.slice(0,3).map(f=>`${f.name}${f.dosage}`).join("、"))+c(o.herbs.length>3?"...":""),1)):(a(),n("span",Y,"—"))]),_:1}),i(r,{prop:"doctor_name",label:"医师",width:"90","show-overflow-tooltip":""}),i(r,{label:"状态",width:"140",align:"center"},{default:s(({row:o})=>[o.void_status===1?(a(),n(H,{key:0},[i(v,{type:"danger",size:"small"},{default:s(()=>[...t[1]||(t[1]=[m("已作废",-1)])]),_:1}),R("div",q,c(o.void_by_name||"—")+" "+c(S(o.void_time)),1)],64)):(a(),g(v,{key:1,type:"success",size:"small"},{default:s(()=>[...t[2]||(t[2]=[m("正常",-1)])]),_:1}))]),_:1}),i(r,{label:"操作",width:"120",fixed:"right"},{default:s(({row:o})=>[i(h,{link:"",type:"primary",size:"small",onClick:f=>$(o)},{default:s(()=>[...t[3]||(t[3]=[m(" 查看 ",-1)])]),_:1},8,["onClick"])]),_:1})]),_:1},8,["data"])),[[I,d(p)]]),!d(p)&&d(l).length===0?(a(),g(N,{key:1,description:"暂无病历记录,开方后会自动显示",class:"mt-4"})):w("",!0)])}}}),zt=G(K,[["__scopeId","data-v-043d2738"]]);export{zt as default};
import{i as L,L as T,N as V,T as D,M as z,W as M}from"./element-plus-lurTijPg.js";import{ac as O}from"./tcm-Cjb9ca7N.js";import{f as P,b as F,w as j,ak as a,I as n,a as i,aN as s,O as m,H as w,aP as A,G as g,F as H,J as R}from"./@vue/runtime-core-C6bnekPw.js";import{y as d,n as k}from"./@vue/reactivity-DiY1c2vO.js";import{Q as c}from"./@vue/shared-mAAVTE9n.js";import{_ as G}from"./index-Cr_vnZ4Y.js";import"./@vue/runtime-dom-DDAG46FW.js";import"./lodash-D3kF6u-c.js";import"./@element-plus/icons-vue-HOEUG8sr.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./vue-router-QlpZ4wdW.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./@vueuse/core-1S4fJlii.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";const J={class:"case-record-list"},Q={key:0,class:"mb-3 flex justify-end"},W={key:0},Y={key:1,class:"text-gray-400"},q={class:"void-detail text-xs text-gray-500 mt-1"},K=P({__name:"CaseRecordList",props:{diagnosisId:{type:Number,default:0},readOnly:{type:Boolean,default:!1}},emits:["view","openPrescription"],setup(y,{expose:x,emit:C}){const _=y,h=C,l=k([]),p=k(!1),u=async()=>{if(_.diagnosisId){p.value=!0;try{const e=await O({diagnosis_id:_.diagnosisId});l.value=Array.isArray(e)?e:[]}catch(e){console.error("获取病历记录失败:",e),l.value=[]}finally{p.value=!1}}},S=e=>{if(!e)return"";const t=new Date(e*1e3);return`${t.getFullYear()}-${String(t.getMonth()+1).padStart(2,"0")}-${String(t.getDate()).padStart(2,"0")} ${String(t.getHours()).padStart(2,"0")}:${String(t.getMinutes()).padStart(2,"0")}`},$=e=>{h("view",e)},B=()=>{h("openPrescription")};return F(()=>{u()}),j(()=>_.diagnosisId,()=>{u()}),x({refresh:u}),(e,t)=>{const b=L,r=V,v=D,E=z,N=M,I=T;return a(),n("div",J,[y.readOnly?w("",!0):(a(),n("div",Q,[i(b,{type:"primary",size:"small",onClick:B},{default:s(()=>[...t[0]||(t[0]=[m("开方",-1)])]),_:1})])),A((a(),g(E,{data:d(l),border:""},{default:s(()=>[i(r,{prop:"prescription_date",label:"就诊日期",width:"120"}),i(r,{prop:"visit_no",label:"门诊号",width:"120"}),i(r,{prop:"clinical_diagnosis",label:"临床诊断","min-width":"160","show-overflow-tooltip":""}),i(r,{label:"处方摘要","min-width":"180"},{default:s(({row:o})=>[o.herbs&&o.herbs.length?(a(),n("span",W,c(o.herbs.slice(0,3).map(f=>`${f.name}${f.dosage}`).join("、"))+c(o.herbs.length>3?"...":""),1)):(a(),n("span",Y,"—"))]),_:1}),i(r,{prop:"doctor_name",label:"医师",width:"90","show-overflow-tooltip":""}),i(r,{label:"状态",width:"140",align:"center"},{default:s(({row:o})=>[o.void_status===1?(a(),n(H,{key:0},[i(v,{type:"danger",size:"small"},{default:s(()=>[...t[1]||(t[1]=[m("已作废",-1)])]),_:1}),R("div",q,c(o.void_by_name||"—")+" "+c(S(o.void_time)),1)],64)):(a(),g(v,{key:1,type:"success",size:"small"},{default:s(()=>[...t[2]||(t[2]=[m("正常",-1)])]),_:1}))]),_:1}),i(r,{label:"操作",width:"120",fixed:"right"},{default:s(({row:o})=>[i(b,{link:"",type:"primary",size:"small",onClick:f=>$(o)},{default:s(()=>[...t[3]||(t[3]=[m(" 查看 ",-1)])]),_:1},8,["onClick"])]),_:1})]),_:1},8,["data"])),[[I,d(p)]]),!d(p)&&d(l).length===0?(a(),g(N,{key:1,description:"暂无病历记录,开方后会自动显示",class:"mt-4"})):w("",!0)])}}}),zt=G(K,[["__scopeId","data-v-043d2738"]]);export{zt as default};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,2 +1,2 @@
import{W as Q,v as Z,d as q,a as K}from"./element-plus-lurTijPg.js";import{_ as Y}from"./picker-CIEIqcz7.js";import{e as ee,c as te,i as S,_ as ie}from"./index-hTkp7Jz4.js";import{s as A}from"./@vue/runtime-dom-DDAG46FW.js";import{b as B,G as se}from"./@element-plus/icons-vue-HOEUG8sr.js";import{d as oe,a as T}from"./patient-rB-xF1_j.js";import{f as ne,w as re,ak as i,I as n,a as l,aN as c,J as a,H as d,F as v,ap as k,G as I}from"./@vue/runtime-core-C6bnekPw.js";import{n as w,y as C}from"./@vue/reactivity-DiY1c2vO.js";import{Q as z}from"./@vue/shared-mAAVTE9n.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./lodash-D3kF6u-c.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./index-NODwdV06.js";import"./index-DQyvoLLY.js";import"./index.vue_vue_type_script_setup_true_lang-CB1JgtXC.js";import"./index-Qf24LWMn.js";import"./index-By8JT914.js";import"./cos-js-sdk-v5-DSMN0LUT.js";import"./file-CdkUkN_s.js";import"./index.vue_vue_type_script_setup_true_lang-SfFZFG0h.js";import"./@vueuse/core-1S4fJlii.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./usePaging-VsbTxSU0.js";import"./vuedraggable-5bKFmC7X.js";import"./vue-BVs68q8v.js";import"./@vue/compiler-dom-CqayqxS7.js";import"./@vue/compiler-core-CvbQOXIO.js";import"./sortablejs-dsEAUXlE.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./vue-router-QlpZ4wdW.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";const le={class:"note-timeline-wrap"},ae={key:0,class:"timeline-actions"},me={class:"upload-trigger"},de={class:"upload-trigger"},pe={key:1,class:"note-timeline"},ce={class:"timeline-date"},ue={class:"timeline-body"},ge={key:0,class:"timeline-content"},_e={key:1,class:"timeline-images"},fe={key:2,class:"timeline-images"},ve={key:0,class:"thumb-wrap"},he={key:1,class:"file-wrap"},ye=["href","title"],ke={class:"file-name"},G=8e3,Ie=ne({__name:"NoteTimeline",props:{notes:{},diagnosisId:{},readonly:{type:Boolean}},emits:["refresh"],setup(m,{emit:L}){const u=m,b=L,R=ee(),g=t=>R.getImageUrl(t),x=w([]),E=w([]),_=w(0),f=w(0),$=["jpg","jpeg","png","gif","bmp","webp","svg"],N=t=>{var s;const e=((s=t.split(".").pop())==null?void 0:s.toLowerCase().split("?")[0])||"";return $.includes(e)},M=t=>{var s;const e=t.split("/");return decodeURIComponent(((s=e[e.length-1])==null?void 0:s.split("?")[0])||"文件")},j=t=>t.filter(N).map(g),O=(t,e)=>{const s=t.filter(N),h=t[e];return s.indexOf(h)},W=t=>t?t.split(`
import{W as Q,v as Z,d as q,a as K}from"./element-plus-lurTijPg.js";import{_ as Y}from"./picker-C5hl36zR.js";import{e as ee,c as te,i as S,_ as ie}from"./index-Cr_vnZ4Y.js";import{s as A}from"./@vue/runtime-dom-DDAG46FW.js";import{b as B,G as se}from"./@element-plus/icons-vue-HOEUG8sr.js";import{d as oe,a as T}from"./patient-Ax4VlZP9.js";import{f as ne,w as re,ak as i,I as n,a as l,aN as c,J as a,H as d,F as v,ap as k,G as I}from"./@vue/runtime-core-C6bnekPw.js";import{n as w,y as C}from"./@vue/reactivity-DiY1c2vO.js";import{Q as z}from"./@vue/shared-mAAVTE9n.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./lodash-D3kF6u-c.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./index-CzGy7WTN.js";import"./index-BGQ88OEu.js";import"./index.vue_vue_type_script_setup_true_lang-CB1JgtXC.js";import"./index-C2qed6__.js";import"./index-BgCOA9Xa.js";import"./cos-js-sdk-v5-DSMN0LUT.js";import"./file-I7Uxb-66.js";import"./index.vue_vue_type_script_setup_true_lang-SfFZFG0h.js";import"./@vueuse/core-1S4fJlii.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./usePaging-VsbTxSU0.js";import"./vuedraggable-5bKFmC7X.js";import"./vue-BVs68q8v.js";import"./@vue/compiler-dom-CqayqxS7.js";import"./@vue/compiler-core-CvbQOXIO.js";import"./sortablejs-dsEAUXlE.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./vue-router-QlpZ4wdW.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";const le={class:"note-timeline-wrap"},ae={key:0,class:"timeline-actions"},me={class:"upload-trigger"},de={class:"upload-trigger"},pe={key:1,class:"note-timeline"},ce={class:"timeline-date"},ue={class:"timeline-body"},ge={key:0,class:"timeline-content"},_e={key:1,class:"timeline-images"},fe={key:2,class:"timeline-images"},ve={key:0,class:"thumb-wrap"},he={key:1,class:"file-wrap"},ye=["href","title"],ke={class:"file-name"},G=8e3,Ie=ne({__name:"NoteTimeline",props:{notes:{},diagnosisId:{},readonly:{type:Boolean}},emits:["refresh"],setup(m,{emit:L}){const u=m,b=L,R=ee(),g=t=>R.getImageUrl(t),x=w([]),E=w([]),_=w(0),f=w(0),$=["jpg","jpeg","png","gif","bmp","webp","svg"],N=t=>{var s;const e=((s=t.split(".").pop())==null?void 0:s.toLowerCase().split("?")[0])||"";return $.includes(e)},M=t=>{var s;const e=t.split("/");return decodeURIComponent(((s=e[e.length-1])==null?void 0:s.split("?")[0])||"文件")},j=t=>t.filter(N).map(g),O=(t,e)=>{const s=t.filter(N),h=t[e];return s.indexOf(h)},W=t=>t?t.split(`
`).filter(Boolean):[],X=t=>{if(!u.diagnosisId)return;const e=Array.isArray(t)?t:[t];if(e.length<=_.value){_.value=e.length;return}const s=e.slice(_.value);_.value=e.length,s.length>0&&T({diagnosis_id:u.diagnosisId,tongue_images:s}).then(()=>{S.msgSuccess("舌苔照片已添加"),b("refresh")})},H=t=>{if(!u.diagnosisId)return;const e=Array.isArray(t)?t:[t];if(e.length<=f.value){f.value=e.length;return}const s=e.slice(f.value);f.value=e.length,s.length>0&&T({diagnosis_id:u.diagnosisId,report_files:s}).then(()=>{S.msgSuccess("检查报告已添加"),b("refresh")})};re(()=>u.notes,()=>{x.value=[],E.value=[],_.value=0,f.value=0});const V=async(t,e,s)=>{try{await K.confirm("确认删除?","提示",{type:"warning"})}catch{return}await oe({note_id:t,image_type:e,image_path:s}),S.msgSuccess("已删除"),b("refresh")};return(t,e)=>{const s=te,h=Y,U=Z,y=q,J=Q;return i(),n("div",le,[!m.readonly&&m.diagnosisId?(i(),n("div",ae,[l(h,{modelValue:x.value,"onUpdate:modelValue":e[0]||(e[0]=o=>x.value=o),limit:99,type:"image","exclude-domain":!0,onChange:X},{upload:c(()=>[a("div",me,[l(s,{size:20,name:"el-icon-Plus"}),e[2]||(e[2]=a("span",null,"舌苔照片",-1))])]),_:1},8,["modelValue"]),l(h,{modelValue:E.value,"onUpdate:modelValue":e[1]||(e[1]=o=>E.value=o),limit:99,type:"file","exclude-domain":!0,onChange:H},{upload:c(()=>[a("div",de,[l(s,{size:20,name:"el-icon-Plus"}),e[3]||(e[3]=a("span",null,"检查报告",-1))])]),_:1},8,["modelValue"])])):d("",!0),m.notes.length?(i(),n("div",pe,[(i(!0),n(v,null,k(m.notes,o=>{var D,F;return i(),n("div",{key:o.id,class:"timeline-node"},[e[6]||(e[6]=a("div",{class:"timeline-dot"},null,-1)),a("div",ce,z(o.note_date),1),a("div",ue,[o.content?(i(),n("div",ge,[(i(!0),n(v,null,k(W(o.content),(r,p)=>(i(),n("div",{key:p,class:"content-line"},z(r),1))),128))])):d("",!0),(D=o.tongue_images)!=null&&D.length?(i(),n("div",_e,[e[4]||(e[4]=a("span",{class:"images-label"},"舌苔照片",-1)),(i(!0),n(v,null,k(o.tongue_images,(r,p)=>(i(),n("div",{key:p,class:"thumb-wrap"},[l(U,{src:g(r),"preview-src-list":o.tongue_images.map(g),"initial-index":p,"z-index":G,fit:"cover",class:"timeline-thumb","preview-teleported":""},null,8,["src","preview-src-list","initial-index"]),m.readonly?d("",!0):(i(),I(y,{key:0,class:"thumb-delete",onClick:A(P=>V(o.id,"tongue_images",r),["stop"])},{default:c(()=>[l(C(B))]),_:1},8,["onClick"]))]))),128))])):d("",!0),(F=o.report_files)!=null&&F.length?(i(),n("div",fe,[e[5]||(e[5]=a("span",{class:"images-label"},"检查报告",-1)),(i(!0),n(v,null,k(o.report_files,(r,p)=>(i(),n(v,{key:p},[N(r)?(i(),n("div",ve,[l(U,{src:g(r),"preview-src-list":j(o.report_files),"initial-index":O(o.report_files,p),"z-index":G,fit:"cover",class:"timeline-thumb","preview-teleported":""},null,8,["src","preview-src-list","initial-index"]),m.readonly?d("",!0):(i(),I(y,{key:0,class:"thumb-delete",onClick:A(P=>V(o.id,"report_files",r),["stop"])},{default:c(()=>[l(C(B))]),_:1},8,["onClick"]))])):(i(),n("div",he,[a("a",{href:g(r),target:"_blank",class:"file-link",title:M(r)},[l(y,{size:20},{default:c(()=>[l(C(se))]),_:1}),a("span",ke,z(M(r)),1)],8,ye),m.readonly?d("",!0):(i(),I(y,{key:0,class:"file-delete",onClick:A(P=>V(o.id,"report_files",r),["stop"])},{default:c(()=>[l(C(B))]),_:1},8,["onClick"]))]))],64))),128))])):d("",!0)])])}),128))])):d("",!0),!m.notes.length&&m.readonly?(i(),I(J,{key:2,description:"暂无备注","image-size":48})):d("",!0)])}}}),It=ie(Ie,[["__scopeId","data-v-f77bb3f4"]]);export{It as default};
@@ -0,0 +1 @@
.card[data-v-7b1b11be]{background:var(--rx-surface);border:1px solid var(--rx-line);border-radius:14px;padding:18px;display:flex;flex-direction:column;gap:14px;box-shadow:var(--rx-shadow-sm);transition:box-shadow .2s ease}.card[data-v-7b1b11be]:hover{box-shadow:var(--rx-shadow)}.card-title[data-v-7b1b11be]{font-size:15px;font-weight:700;color:var(--rx-ink);display:flex;align-items:center;gap:10px;padding-left:10px;position:relative}.card-title[data-v-7b1b11be]:before{content:"";position:absolute;left:0;top:50%;transform:translateY(-50%);width:3px;height:16px;border-radius:2px;background:linear-gradient(180deg,#4f8cff,#2563eb)}.case-sub[data-v-7b1b11be]{font-size:12px;font-weight:400;color:var(--rx-soft);font-family:var(--rx-font-data)}.grid-block[data-v-7b1b11be]{display:flex;flex-direction:column;gap:10px;padding-top:12px;border-top:1px dashed var(--rx-line)}.grid-block[data-v-7b1b11be]:first-of-type{border-top:none;padding-top:0}.grid-title[data-v-7b1b11be]{font-size:13px;font-weight:600;color:var(--rx-accent-strong);letter-spacing:.02em;display:flex;align-items:center;gap:6px}.grid-title[data-v-7b1b11be]:before{content:"";width:4px;height:4px;border-radius:50%;background:var(--rx-accent)}.kv-grid[data-v-7b1b11be]{display:grid;gap:8px 16px}.kv-grid.one[data-v-7b1b11be]{grid-template-columns:1fr}.kv-grid.two[data-v-7b1b11be]{grid-template-columns:repeat(2,1fr)}.kv-grid.three[data-v-7b1b11be]{grid-template-columns:repeat(3,1fr)}.kv-grid.four[data-v-7b1b11be]{grid-template-columns:repeat(4,1fr)}[data-v-7b1b11be] .kv-item{display:flex;gap:6px;font-size:12.5px;line-height:1.55;min-width:0}[data-v-7b1b11be] .kv-item.multiline{flex-direction:column;gap:2px}[data-v-7b1b11be] .kv-item .kv-label{color:var(--rx-soft);flex-shrink:0;min-width:58px}[data-v-7b1b11be] .kv-item .kv-value{color:var(--rx-ink);word-break:break-word;flex:1;font-weight:500}[data-v-7b1b11be] .kv-item.empty .kv-value{color:#c0c4cc;font-weight:400}[data-v-7b1b11be] .kv-item.multiline .kv-value{white-space:pre-wrap;font-weight:400}[data-v-7b1b11be] .metric-kv-item .metric-kv-value{display:inline-flex;align-items:center;gap:4px}[data-v-7b1b11be] .metric-kv-item .metric-kv-value.high{color:#dc2626;font-weight:700}[data-v-7b1b11be] .metric-up{font-size:14px;line-height:1;color:#dc2626}.rx-note[data-v-7b1b11be]{font-size:13px;color:var(--rx-muted);line-height:1.6;padding:12px 14px;background:linear-gradient(135deg,#fafbfd,#f5f7fb);border-radius:8px;border-left:3px solid var(--rx-accent)}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
.card[data-v-5de08f8e]{background:var(--rx-surface);border:1px solid var(--rx-line);border-radius:14px;padding:18px;display:flex;flex-direction:column;gap:14px;box-shadow:var(--rx-shadow-sm);transition:box-shadow .2s ease}.card[data-v-5de08f8e]:hover{box-shadow:var(--rx-shadow)}.card-title[data-v-5de08f8e]{font-size:15px;font-weight:700;color:var(--rx-ink);display:flex;align-items:center;gap:10px;padding-left:10px;position:relative}.card-title[data-v-5de08f8e]:before{content:"";position:absolute;left:0;top:50%;transform:translateY(-50%);width:3px;height:16px;border-radius:2px;background:linear-gradient(180deg,#4f8cff,#2563eb)}.case-sub[data-v-5de08f8e]{font-size:12px;font-weight:400;color:var(--rx-soft);font-family:var(--rx-font-data)}.grid-block[data-v-5de08f8e]{display:flex;flex-direction:column;gap:10px;padding-top:12px;border-top:1px dashed var(--rx-line)}.grid-block[data-v-5de08f8e]:first-of-type{border-top:none;padding-top:0}.grid-title[data-v-5de08f8e]{font-size:13px;font-weight:600;color:var(--rx-accent-strong);letter-spacing:.02em;display:flex;align-items:center;gap:6px}.grid-title[data-v-5de08f8e]:before{content:"";width:4px;height:4px;border-radius:50%;background:var(--rx-accent)}.kv-grid[data-v-5de08f8e]{display:grid;gap:8px 16px}.kv-grid.one[data-v-5de08f8e]{grid-template-columns:1fr}.kv-grid.two[data-v-5de08f8e]{grid-template-columns:repeat(2,1fr)}.kv-grid.three[data-v-5de08f8e]{grid-template-columns:repeat(3,1fr)}.kv-grid.four[data-v-5de08f8e]{grid-template-columns:repeat(4,1fr)}[data-v-5de08f8e] .kv-item{display:flex;gap:6px;font-size:12.5px;line-height:1.55;min-width:0}[data-v-5de08f8e] .kv-item.multiline{flex-direction:column;gap:2px}[data-v-5de08f8e] .kv-item .kv-label{color:var(--rx-soft);flex-shrink:0;min-width:58px}[data-v-5de08f8e] .kv-item .kv-value{color:var(--rx-ink);word-break:break-word;flex:1;font-weight:500}[data-v-5de08f8e] .kv-item.empty .kv-value{color:#c0c4cc;font-weight:400}[data-v-5de08f8e] .kv-item.multiline .kv-value{white-space:pre-wrap;font-weight:400}[data-v-5de08f8e] .metric-kv-item .metric-kv-value{display:inline-flex;align-items:center;gap:4px}[data-v-5de08f8e] .metric-kv-item .metric-kv-value.high{color:#dc2626;font-weight:700}[data-v-5de08f8e] .metric-up{font-size:14px;line-height:1;color:#dc2626}.rx-note[data-v-5de08f8e]{font-size:13px;color:var(--rx-muted);line-height:1.6;padding:12px 14px;background:linear-gradient(135deg,#fafbfd,#f5f7fb);border-radius:8px;border-left:3px solid var(--rx-accent)}
@@ -1 +1 @@
import{m as x,f as y,a as B}from"./diag-display-DCz_VAqj.js";import{f as w,ak as I,I as P,J as a,H as N}from"./@vue/runtime-core-C6bnekPw.js";import{Q as i}from"./@vue/shared-mAAVTE9n.js";import{y as e}from"./@vue/reactivity-DiY1c2vO.js";import{_ as V}from"./index-hTkp7Jz4.js";import"./lodash-D3kF6u-c.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./vue-router-QlpZ4wdW.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./element-plus-lurTijPg.js";import"./@vue/runtime-dom-DDAG46FW.js";import"./@element-plus/icons-vue-HOEUG8sr.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./@vueuse/core-1S4fJlii.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";const D={class:"card patient-card"},E={class:"patient-hero"},G={class:"patient-name"},H={class:"patient-meta"},J={key:0},Q=w({__name:"PatientInfoCard",props:{apt:{default:()=>({})},diag:{default:()=>({})}},setup(t){return(S,o)=>{var m,r,n,d,p,s,c,l,g,f,u,h,v,k,C;return I(),P("div",D,[o[0]||(o[0]=a("div",{class:"card-title"},"患者信息",-1)),a("div",E,[a("div",G,i(((m=t.apt)==null?void 0:m.patient_name)||"—"),1),a("div",H,[a("div",null,i(e(x)((r=t.apt)==null?void 0:r.patient_phone))+" · "+i(e(y)((n=t.diag)==null?void 0:n.gender))+" · "+i(((d=t.diag)==null?void 0:d.age)!=null?t.diag.age+"岁":"—"),1),a("div",null,i((p=t.diag)!=null&&p.height?t.diag.height+"cm":"—")+" / "+i((s=t.diag)!=null&&s.weight?t.diag.weight+"kg":"—")+" · "+i(((c=t.diag)==null?void 0:c.region)||"—"),1),a("div",null," 预约:"+i((l=t.apt)==null?void 0:l.appointment_date)+" "+i((g=t.apt)==null?void 0:g.appointment_time)+" · "+i(e(B)((f=t.apt)==null?void 0:f.period)),1),a("div",null,"医生:"+i(((u=t.apt)==null?void 0:u.doctor_name)||"—")+" 医助"+i(((h=t.apt)==null?void 0:h.assistant_name)||"—"),1),a("div",null," 状态:"+i(((v=t.apt)==null?void 0:v.status_desc)||"—")+" · "+i((k=t.apt)!=null&&k.has_prescription?"已开方":"未开方"),1),(C=t.apt)!=null&&C.remark?(I(),P("div",J,"备注:"+i(t.apt.remark),1)):N("",!0)])])])}}}),Ct=V(Q,[["__scopeId","data-v-e3476da5"]]);export{Ct as default};
import{m as x,f as y,a as B}from"./diag-display-DCz_VAqj.js";import{f as w,ak as I,I as P,J as a,H as N}from"./@vue/runtime-core-C6bnekPw.js";import{Q as i}from"./@vue/shared-mAAVTE9n.js";import{y as e}from"./@vue/reactivity-DiY1c2vO.js";import{_ as V}from"./index-Cr_vnZ4Y.js";import"./lodash-D3kF6u-c.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./vue-router-QlpZ4wdW.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./element-plus-lurTijPg.js";import"./@vue/runtime-dom-DDAG46FW.js";import"./@element-plus/icons-vue-HOEUG8sr.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./@vueuse/core-1S4fJlii.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";const b={class:"card patient-card"},D={class:"patient-hero"},E={class:"patient-name"},G={class:"patient-meta"},H={key:0},J=w({__name:"PatientInfoCard",props:{apt:{default:()=>({})},diag:{default:()=>({})}},setup(t){return(Q,o)=>{var m,r,n,d,p,s,c,l,g,f,u,h,v,k,C;return I(),P("div",b,[o[0]||(o[0]=a("div",{class:"card-title"},"患者信息",-1)),a("div",D,[a("div",E,i(((m=t.apt)==null?void 0:m.patient_name)||"—"),1),a("div",G,[a("div",null,i(e(x)((r=t.apt)==null?void 0:r.patient_phone))+" · "+i(e(y)((n=t.diag)==null?void 0:n.gender))+" · "+i(((d=t.diag)==null?void 0:d.age)!=null?t.diag.age+"岁":"—"),1),a("div",null,i((p=t.diag)!=null&&p.height?t.diag.height+"cm":"—")+" / "+i((s=t.diag)!=null&&s.weight?t.diag.weight+"kg":"—")+" · "+i(((c=t.diag)==null?void 0:c.region)||"—"),1),a("div",null," 预约:"+i((l=t.apt)==null?void 0:l.appointment_date)+" "+i((g=t.apt)==null?void 0:g.appointment_time)+" · "+i(e(B)((f=t.apt)==null?void 0:f.period)),1),a("div",null,"医生:"+i(((u=t.apt)==null?void 0:u.doctor_name)||"—")+" 客服"+i(((h=t.apt)==null?void 0:h.assistant_name)||"—"),1),a("div",null," 状态:"+i(((v=t.apt)==null?void 0:v.status_desc)||"—")+" · "+i((k=t.apt)!=null&&k.has_prescription?"已开方":"未开方"),1),(C=t.apt)!=null&&C.remark?(I(),P("div",H,"备注:"+i(t.apt.remark),1)):N("",!0)])])])}}}),Ct=V(J,[["__scopeId","data-v-1b0de09c"]]);export{Ct as default};
@@ -1 +1 @@
.card[data-v-e3476da5]{background:var(--rx-surface);border:1px solid var(--rx-line);border-radius:14px;padding:18px;display:flex;flex-direction:column;gap:14px;box-shadow:var(--rx-shadow-sm);transition:box-shadow .2s ease}.card[data-v-e3476da5]:hover{box-shadow:var(--rx-shadow)}.card-title[data-v-e3476da5]{font-size:15px;font-weight:700;color:var(--rx-ink);display:flex;align-items:center;gap:10px;padding-left:10px;position:relative}.card-title[data-v-e3476da5]:before{content:"";position:absolute;left:0;top:50%;transform:translateY(-50%);width:3px;height:16px;border-radius:2px;background:linear-gradient(180deg,#4f8cff,#2563eb)}.patient-hero[data-v-e3476da5]{background:linear-gradient(135deg,#f5f8ff,#eef3ff);border:1px solid #dde7ff;border-radius:12px;padding:16px 18px;display:flex;flex-direction:column;gap:6px;position:relative;overflow:hidden}.patient-hero[data-v-e3476da5]:after{content:"";position:absolute;right:-20px;top:-20px;width:100px;height:100px;border-radius:50%;background:radial-gradient(circle,rgba(79,140,255,.08) 0%,transparent 70%)}.patient-name[data-v-e3476da5]{font-size:22px;font-weight:700;color:var(--rx-ink);letter-spacing:-.01em}.patient-meta[data-v-e3476da5]{color:var(--rx-muted);font-size:13px;line-height:1.7;display:flex;flex-direction:column;gap:2px;position:relative;z-index:1}
.card[data-v-1b0de09c]{background:var(--rx-surface);border:1px solid var(--rx-line);border-radius:14px;padding:18px;display:flex;flex-direction:column;gap:14px;box-shadow:var(--rx-shadow-sm);transition:box-shadow .2s ease}.card[data-v-1b0de09c]:hover{box-shadow:var(--rx-shadow)}.card-title[data-v-1b0de09c]{font-size:15px;font-weight:700;color:var(--rx-ink);display:flex;align-items:center;gap:10px;padding-left:10px;position:relative}.card-title[data-v-1b0de09c]:before{content:"";position:absolute;left:0;top:50%;transform:translateY(-50%);width:3px;height:16px;border-radius:2px;background:linear-gradient(180deg,#4f8cff,#2563eb)}.patient-hero[data-v-1b0de09c]{background:linear-gradient(135deg,#f5f8ff,#eef3ff);border:1px solid #dde7ff;border-radius:12px;padding:16px 18px;display:flex;flex-direction:column;gap:6px;position:relative;overflow:hidden}.patient-hero[data-v-1b0de09c]:after{content:"";position:absolute;right:-20px;top:-20px;width:100px;height:100px;border-radius:50%;background:radial-gradient(circle,rgba(79,140,255,.08) 0%,transparent 70%)}.patient-name[data-v-1b0de09c]{font-size:22px;font-weight:700;color:var(--rx-ink);letter-spacing:-.01em}.patient-meta[data-v-1b0de09c]{color:var(--rx-muted);font-size:13px;line-height:1.7;display:flex;flex-direction:column;gap:2px;position:relative;z-index:1}
@@ -1 +1 @@
import{S as L}from"./element-plus-lurTijPg.js";import B from"./RecordingVideoPlayer-DDmfklnD.js";import{e as H,_ as I}from"./index-hTkp7Jz4.js";import{f as w,ak as n,I as m,J as p,F as v,G as u,aN as _,O as k,H as y,ap as R,A as d}from"./@vue/runtime-core-C6bnekPw.js";import{Q as q}from"./@vue/shared-mAAVTE9n.js";import"./@vue/runtime-dom-DDAG46FW.js";import"./lodash-D3kF6u-c.js";import"./@vue/reactivity-DiY1c2vO.js";import"./@element-plus/icons-vue-HOEUG8sr.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./vue-router-QlpZ4wdW.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./@vueuse/core-1S4fJlii.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";const C={key:0,class:"recording-list"},N={class:"recording-item"},P={key:1,class:"recording-alternates"},V={class:"recording-alternates__links"},A={key:1,class:"text-gray-400"},E=w({__name:"RecordingPlaybackBlock",props:{recordId:{},urls:{}},setup(c){const $=c,h=H(),l=d(()=>{const e=$.urls||[],t=new Set,r=[];for(const s of e){const o=String(s??"").trim();!o||t.has(o)||(t.add(o),r.push(o))}return r}),a=d(()=>{const e=l.value;if(!e.length)return null;const t=e.find(i=>/\.mp4(\?|#|$)/i.test(i));if(t)return t;const r=e.find(i=>/\.m3u8(\?|#|$)/i.test(i)&&/vod-qcloud\.com/i.test(i));if(r)return r;const s=e.find(i=>/\.m3u8(\?|#|$)/i.test(i)&&/\.cos\.[^/]+\.myqcloud\.com/i.test(i));if(s)return s;const o=e.find(i=>/\.m3u8(\?|#|$)/i.test(i));return o||e[0]}),f=d(()=>{const e=l.value,t=a.value;return t?e.filter(r=>r!==t):e.slice(1)});function S(e){if(!e||typeof e!="string")return!1;const t=e.trim();return/^https?:\/\//i.test(t)?/\.(mp4|webm|ogg|mov|mkv|m4v|m3u8)(\?|#|$)/i.test(t):/\.(mp4|webm|ogg|mov|mkv|m4v|m3u8)(\?|#|$)/i.test(t)||t.startsWith("/uploads/")}function g(e){return h.getImageUrl(String(e||"").trim())}function b(e,t){const r=e.trim();return/\.mp4(\?|#|$)/i.test(r)?`MP4 ${t+1}`:/vod-qcloud\.com/i.test(r)&&/\.m3u8/i.test(r)?`点播 ${t+1}`:/\.cos\.[^/]+\.myqcloud\.com/i.test(r)&&/\.m3u8/i.test(r)?`COS HLS ${t+1}`:/\.m3u8/i.test(r)?`HLS ${t+1}`:`链接 ${t+1}`}return(e,t)=>{const r=L;return l.value.length?(n(),m("div",C,[p("div",N,[a.value?(n(),m(v,{key:0},[S(a.value)?(n(),u(B,{key:`${c.recordId}-${a.value}`,src:a.value},null,8,["src"])):(n(),u(r,{key:1,href:g(a.value),target:"_blank",type:"primary"},{default:_(()=>[...t[0]||(t[0]=[k(" 打开回放 ",-1)])]),_:1},8,["href"]))],64)):y("",!0),f.value.length?(n(),m("div",P,[t[1]||(t[1]=p("span",{class:"recording-alternates__label"},"备用地址",-1)),p("div",V,[(n(!0),m(v,null,R(f.value,(s,o)=>(n(),u(r,{key:`${c.recordId}-alt-${o}-${s.slice(-32)}`,href:g(s),target:"_blank",type:"primary",class:"recording-alternates__link"},{default:_(()=>[k(q(b(s,o)),1)]),_:2},1032,["href"]))),128))])])):y("",!0)])])):(n(),m("span",A,"暂无"))}}}),ht=I(E,[["__scopeId","data-v-d67b2e91"]]);export{ht as default};
import{S as L}from"./element-plus-lurTijPg.js";import B from"./RecordingVideoPlayer-CCilj3yz.js";import{e as H,_ as I}from"./index-Cr_vnZ4Y.js";import{f as w,ak as n,I as m,J as p,F as v,G as u,aN as _,O as k,H as y,ap as R,A as d}from"./@vue/runtime-core-C6bnekPw.js";import{Q as q}from"./@vue/shared-mAAVTE9n.js";import"./@vue/runtime-dom-DDAG46FW.js";import"./lodash-D3kF6u-c.js";import"./@vue/reactivity-DiY1c2vO.js";import"./@element-plus/icons-vue-HOEUG8sr.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./vue-router-QlpZ4wdW.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./@vueuse/core-1S4fJlii.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";const C={key:0,class:"recording-list"},N={class:"recording-item"},P={key:1,class:"recording-alternates"},V={class:"recording-alternates__links"},A={key:1,class:"text-gray-400"},E=w({__name:"RecordingPlaybackBlock",props:{recordId:{},urls:{}},setup(c){const $=c,h=H(),l=d(()=>{const e=$.urls||[],t=new Set,r=[];for(const s of e){const o=String(s??"").trim();!o||t.has(o)||(t.add(o),r.push(o))}return r}),a=d(()=>{const e=l.value;if(!e.length)return null;const t=e.find(i=>/\.mp4(\?|#|$)/i.test(i));if(t)return t;const r=e.find(i=>/\.m3u8(\?|#|$)/i.test(i)&&/vod-qcloud\.com/i.test(i));if(r)return r;const s=e.find(i=>/\.m3u8(\?|#|$)/i.test(i)&&/\.cos\.[^/]+\.myqcloud\.com/i.test(i));if(s)return s;const o=e.find(i=>/\.m3u8(\?|#|$)/i.test(i));return o||e[0]}),f=d(()=>{const e=l.value,t=a.value;return t?e.filter(r=>r!==t):e.slice(1)});function S(e){if(!e||typeof e!="string")return!1;const t=e.trim();return/^https?:\/\//i.test(t)?/\.(mp4|webm|ogg|mov|mkv|m4v|m3u8)(\?|#|$)/i.test(t):/\.(mp4|webm|ogg|mov|mkv|m4v|m3u8)(\?|#|$)/i.test(t)||t.startsWith("/uploads/")}function g(e){return h.getImageUrl(String(e||"").trim())}function b(e,t){const r=e.trim();return/\.mp4(\?|#|$)/i.test(r)?`MP4 ${t+1}`:/vod-qcloud\.com/i.test(r)&&/\.m3u8/i.test(r)?`点播 ${t+1}`:/\.cos\.[^/]+\.myqcloud\.com/i.test(r)&&/\.m3u8/i.test(r)?`COS HLS ${t+1}`:/\.m3u8/i.test(r)?`HLS ${t+1}`:`链接 ${t+1}`}return(e,t)=>{const r=L;return l.value.length?(n(),m("div",C,[p("div",N,[a.value?(n(),m(v,{key:0},[S(a.value)?(n(),u(B,{key:`${c.recordId}-${a.value}`,src:a.value},null,8,["src"])):(n(),u(r,{key:1,href:g(a.value),target:"_blank",type:"primary"},{default:_(()=>[...t[0]||(t[0]=[k(" 打开回放 ",-1)])]),_:1},8,["href"]))],64)):y("",!0),f.value.length?(n(),m("div",P,[t[1]||(t[1]=p("span",{class:"recording-alternates__label"},"备用地址",-1)),p("div",V,[(n(!0),m(v,null,R(f.value,(s,o)=>(n(),u(r,{key:`${c.recordId}-alt-${o}-${s.slice(-32)}`,href:g(s),target:"_blank",type:"primary",class:"recording-alternates__link"},{default:_(()=>[k(q(b(s,o)),1)]),_:2},1032,["href"]))),128))])])):y("",!0)])])):(n(),m("span",A,"暂无"))}}}),ht=I(E,[["__scopeId","data-v-d67b2e91"]]);export{ht as default};
File diff suppressed because one or more lines are too long
@@ -1,2 +1,2 @@
import{C as I,i as V,W as w}from"./element-plus-lurTijPg.js";import{U as T}from"./tcm-BWcL4HTv.js";import{i as C,_ as E}from"./index-hTkp7Jz4.js";import{f as z,ak as t,I as e,a as p,J as l,aN as H,O as S,H as m,F as c,ap as u,G as F}from"./@vue/runtime-core-C6bnekPw.js";import{Q as f}from"./@vue/shared-mAAVTE9n.js";import{n as g}from"./@vue/reactivity-DiY1c2vO.js";/* empty css */import"./@vue/runtime-dom-DDAG46FW.js";import"./lodash-D3kF6u-c.js";import"./@element-plus/icons-vue-HOEUG8sr.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./vue-router-QlpZ4wdW.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./@vueuse/core-1S4fJlii.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";const L={class:"tracking-timeline-wrap"},M={key:0,class:"timeline-input"},U={class:"input-actions"},A={key:1,class:"tracking-timeline"},D={class:"timeline-date"},G={class:"timeline-body"},J={key:0,class:"timeline-content"},O=z({__name:"TrackingNoteTimeline",props:{notes:{},diagnosisId:{},readonly:{type:Boolean}},emits:["refresh"],setup(i,{emit:v}){const d=i,y=v,r=g(""),a=g(!1),_=o=>o?o.split(`
`).filter(Boolean):[],k=async()=>{if(!d.diagnosisId)return;const o=r.value.trim();if(o){a.value=!0;try{await T({diagnosis_id:d.diagnosisId,tracking_content:o}),C.msgSuccess("已添加"),r.value="",y("refresh")}finally{a.value=!1}}};return(o,n)=>{const h=I,b=V,N=w;return t(),e("div",L,[!i.readonly&&i.diagnosisId?(t(),e("div",M,[p(h,{modelValue:r.value,"onUpdate:modelValue":n[0]||(n[0]=s=>r.value=s),type:"textarea",rows:2,placeholder:"输入跟踪备注,回车换行;保存后将以「[HH:MM] 内容」追加到当天记录",maxlength:"1000","show-word-limit":"",resize:"none",disabled:a.value},null,8,["modelValue","disabled"]),l("div",U,[p(b,{type:"primary",size:"small",loading:a.value,disabled:!r.value.trim(),onClick:k},{default:H(()=>[...n[1]||(n[1]=[S(" 添加 ",-1)])]),_:1},8,["loading","disabled"])])])):m("",!0),i.notes.length?(t(),e("div",A,[(t(!0),e(c,null,u(i.notes,s=>(t(),e("div",{key:s.id,class:"timeline-node"},[n[2]||(n[2]=l("div",{class:"timeline-dot"},null,-1)),l("div",D,f(s.note_date),1),l("div",G,[s.content?(t(),e("div",J,[(t(!0),e(c,null,u(_(s.content),(x,B)=>(t(),e("div",{key:B,class:"content-line"},f(x),1))),128))])):m("",!0)])]))),128))])):m("",!0),!i.notes.length&&i.readonly?(t(),F(N,{key:2,description:"暂无跟踪备注","image-size":48})):m("",!0)])}}}),Tt=E(O,[["__scopeId","data-v-82b635bd"]]);export{Tt as default};
import{C as B,i as I,W as w}from"./element-plus-lurTijPg.js";import{V as T}from"./tcm-Cjb9ca7N.js";import{i as C,_ as E}from"./index-Cr_vnZ4Y.js";import{f as z,ak as t,I as e,a as p,J as l,aN as H,O as S,H as m,F as c,ap as u,G as F}from"./@vue/runtime-core-C6bnekPw.js";import{Q as f}from"./@vue/shared-mAAVTE9n.js";import{n as g}from"./@vue/reactivity-DiY1c2vO.js";/* empty css */import"./@vue/runtime-dom-DDAG46FW.js";import"./lodash-D3kF6u-c.js";import"./@element-plus/icons-vue-HOEUG8sr.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./vue-router-QlpZ4wdW.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./@vueuse/core-1S4fJlii.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";const L={class:"tracking-timeline-wrap"},M={key:0,class:"timeline-input"},A={class:"input-actions"},D={key:1,class:"tracking-timeline"},G={class:"timeline-date"},J={class:"timeline-body"},O={key:0,class:"timeline-content"},Q=z({__name:"TrackingNoteTimeline",props:{notes:{},diagnosisId:{},readonly:{type:Boolean}},emits:["refresh"],setup(i,{emit:v}){const d=i,y=v,r=g(""),a=g(!1),_=o=>o?o.split(`
`).filter(Boolean):[],k=async()=>{if(!d.diagnosisId)return;const o=r.value.trim();if(o){a.value=!0;try{await T({diagnosis_id:d.diagnosisId,tracking_content:o}),C.msgSuccess("已添加"),r.value="",y("refresh")}finally{a.value=!1}}};return(o,n)=>{const h=B,b=I,N=w;return t(),e("div",L,[!i.readonly&&i.diagnosisId?(t(),e("div",M,[p(h,{modelValue:r.value,"onUpdate:modelValue":n[0]||(n[0]=s=>r.value=s),type:"textarea",rows:2,placeholder:"输入跟踪备注,回车换行;保存后将以「[HH:MM] 内容」追加到当天记录",maxlength:"1000","show-word-limit":"",resize:"none",disabled:a.value},null,8,["modelValue","disabled"]),l("div",A,[p(b,{type:"primary",size:"small",loading:a.value,disabled:!r.value.trim(),onClick:k},{default:H(()=>[...n[1]||(n[1]=[S(" 添加 ",-1)])]),_:1},8,["loading","disabled"])])])):m("",!0),i.notes.length?(t(),e("div",D,[(t(!0),e(c,null,u(i.notes,s=>(t(),e("div",{key:s.id,class:"timeline-node"},[n[2]||(n[2]=l("div",{class:"timeline-dot"},null,-1)),l("div",G,f(s.note_date),1),l("div",J,[s.content?(t(),e("div",O,[(t(!0),e(c,null,u(_(s.content),(V,x)=>(t(),e("div",{key:x,class:"content-line"},f(V),1))),128))])):m("",!0)])]))),128))])):m("",!0),!i.notes.length&&i.readonly?(t(),F(N,{key:2,description:"暂无跟踪备注","image-size":48})):m("",!0)])}}}),Tt=E(Q,[["__scopeId","data-v-82b635bd"]]);export{Tt as default};
@@ -1 +1 @@
import{_ as o}from"./account-adjust.vue_vue_type_script_setup_true_lang-Bfdei3w2.js";import"./element-plus-lurTijPg.js";import"./@vue/runtime-dom-DDAG46FW.js";import"./lodash-D3kF6u-c.js";import"./@vue/runtime-core-C6bnekPw.js";import"./@vue/reactivity-DiY1c2vO.js";import"./@vue/shared-mAAVTE9n.js";import"./@element-plus/icons-vue-HOEUG8sr.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./index-NODwdV06.js";import"./index-hTkp7Jz4.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./vue-router-QlpZ4wdW.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./@vueuse/core-1S4fJlii.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";export{o as default};
import{_ as o}from"./account-adjust.vue_vue_type_script_setup_true_lang-B-3PLiTD.js";import"./element-plus-lurTijPg.js";import"./@vue/runtime-dom-DDAG46FW.js";import"./lodash-D3kF6u-c.js";import"./@vue/runtime-core-C6bnekPw.js";import"./@vue/reactivity-DiY1c2vO.js";import"./@vue/shared-mAAVTE9n.js";import"./@element-plus/icons-vue-HOEUG8sr.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./index-CzGy7WTN.js";import"./index-Cr_vnZ4Y.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./vue-router-QlpZ4wdW.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./@vueuse/core-1S4fJlii.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";export{o as default};
@@ -1 +1 @@
import{D as h,B as q,G as B,I,C as D}from"./element-plus-lurTijPg.js";import{_ as F}from"./index-NODwdV06.js";import{i as b}from"./index-hTkp7Jz4.js";import{f as G,w,ak as j,G as S,aN as r,J as U,a,O as u,A}from"./@vue/runtime-core-C6bnekPw.js";import{y as n,q as y,r as J}from"./@vue/reactivity-DiY1c2vO.js";import{Q as k}from"./@vue/shared-mAAVTE9n.js";const M={class:"pr-8"},L=G({__name:"account-adjust",props:{show:{type:Boolean,required:!0},value:{type:[Number,String],required:!0}},emits:["update:show","confirm"],setup(d,{emit:V}){const s=y(),i=d,f=V,o=J({action:1,num:"",remark:""}),m=y(),c=A(()=>Number(i.value)+Number(o.num)*(o.action==1?1:-1)),R={num:[{required:!0,message:"请输入调整的金额"}]},g=e=>{if(e.includes("-"))return b.msgError("请输入正整数");o.num=e},x=async()=>{var e;await((e=s.value)==null?void 0:e.validate()),f("confirm",o)},C=()=>{var e;f("update:show",!1),(e=s.value)==null||e.resetFields()};return w(()=>i.show,e=>{var t,l;e?(t=m.value)==null||t.open():(l=m.value)==null||l.close()}),w(c,e=>{e<0&&(b.msgError("调整后余额需大于0"),o.num="")}),(e,t)=>{const l=q,_=I,E=B,v=D,N=h;return j(),S(F,{ref_key:"popupRef",ref:m,title:"余额调整",width:"500px",onConfirm:x,async:!0,onClose:C},{default:r(()=>[U("div",M,[a(N,{ref_key:"formRef",ref:s,model:n(o),"label-width":"120px",rules:R},{default:r(()=>[a(l,{label:"当前余额"},{default:r(()=>[u("¥ "+k(d.value),1)]),_:1}),a(l,{label:"余额增减",required:"",prop:"action"},{default:r(()=>[a(E,{modelValue:n(o).action,"onUpdate:modelValue":t[0]||(t[0]=p=>n(o).action=p)},{default:r(()=>[a(_,{value:1},{default:r(()=>[...t[2]||(t[2]=[u("增加余额",-1)])]),_:1}),a(_,{value:2},{default:r(()=>[...t[3]||(t[3]=[u("扣减余额",-1)])]),_:1})]),_:1},8,["modelValue"])]),_:1}),a(l,{label:"调整余额",prop:"num"},{default:r(()=>[a(v,{"model-value":n(o).num,placeholder:"请输入调整的金额",type:"number",onInput:g},null,8,["model-value"])]),_:1}),a(l,{label:"调整后余额"},{default:r(()=>[u(" ¥ "+k(n(c)),1)]),_:1}),a(l,{label:"备注",prop:"remark"},{default:r(()=>[a(v,{modelValue:n(o).remark,"onUpdate:modelValue":t[1]||(t[1]=p=>n(o).remark=p),type:"textarea",rows:4},null,8,["modelValue"])]),_:1})]),_:1},8,["model"])])]),_:1},512)}}});export{L as _};
import{D as h,B as q,G as B,I,C as D}from"./element-plus-lurTijPg.js";import{_ as F}from"./index-CzGy7WTN.js";import{i as b}from"./index-Cr_vnZ4Y.js";import{f as G,w,ak as j,G as S,aN as r,J as U,a,O as u,A}from"./@vue/runtime-core-C6bnekPw.js";import{y as n,q as y,r as J}from"./@vue/reactivity-DiY1c2vO.js";import{Q as k}from"./@vue/shared-mAAVTE9n.js";const M={class:"pr-8"},L=G({__name:"account-adjust",props:{show:{type:Boolean,required:!0},value:{type:[Number,String],required:!0}},emits:["update:show","confirm"],setup(d,{emit:V}){const s=y(),i=d,f=V,o=J({action:1,num:"",remark:""}),m=y(),c=A(()=>Number(i.value)+Number(o.num)*(o.action==1?1:-1)),R={num:[{required:!0,message:"请输入调整的金额"}]},g=e=>{if(e.includes("-"))return b.msgError("请输入正整数");o.num=e},x=async()=>{var e;await((e=s.value)==null?void 0:e.validate()),f("confirm",o)},C=()=>{var e;f("update:show",!1),(e=s.value)==null||e.resetFields()};return w(()=>i.show,e=>{var t,l;e?(t=m.value)==null||t.open():(l=m.value)==null||l.close()}),w(c,e=>{e<0&&(b.msgError("调整后余额需大于0"),o.num="")}),(e,t)=>{const l=q,_=I,E=B,v=D,N=h;return j(),S(F,{ref_key:"popupRef",ref:m,title:"余额调整",width:"500px",onConfirm:x,async:!0,onClose:C},{default:r(()=>[U("div",M,[a(N,{ref_key:"formRef",ref:s,model:n(o),"label-width":"120px",rules:R},{default:r(()=>[a(l,{label:"当前余额"},{default:r(()=>[u("¥ "+k(d.value),1)]),_:1}),a(l,{label:"余额增减",required:"",prop:"action"},{default:r(()=>[a(E,{modelValue:n(o).action,"onUpdate:modelValue":t[0]||(t[0]=p=>n(o).action=p)},{default:r(()=>[a(_,{value:1},{default:r(()=>[...t[2]||(t[2]=[u("增加余额",-1)])]),_:1}),a(_,{value:2},{default:r(()=>[...t[3]||(t[3]=[u("扣减余额",-1)])]),_:1})]),_:1},8,["modelValue"])]),_:1}),a(l,{label:"调整余额",prop:"num"},{default:r(()=>[a(v,{"model-value":n(o).num,placeholder:"请输入调整的金额",type:"number",onInput:g},null,8,["model-value"])]),_:1}),a(l,{label:"调整后余额"},{default:r(()=>[u(" ¥ "+k(n(c)),1)]),_:1}),a(l,{label:"备注",prop:"remark"},{default:r(()=>[a(v,{modelValue:n(o).remark,"onUpdate:modelValue":t[1]||(t[1]=p=>n(o).remark=p),type:"textarea",rows:4},null,8,["modelValue"])]),_:1})]),_:1},8,["model"])])]),_:1},512)}}});export{L as _};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
import{_ as o}from"./add-nav.vue_vue_type_script_setup_true_lang-DJJZsT5O.js";import"./element-plus-lurTijPg.js";import"./@vue/runtime-dom-DDAG46FW.js";import"./lodash-D3kF6u-c.js";import"./@vue/runtime-core-C6bnekPw.js";import"./@vue/reactivity-DiY1c2vO.js";import"./@vue/shared-mAAVTE9n.js";import"./@element-plus/icons-vue-HOEUG8sr.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./index-Qf24LWMn.js";import"./index-hTkp7Jz4.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./vue-router-QlpZ4wdW.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./@vueuse/core-1S4fJlii.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";import"./picker-BKpCM2IP.js";import"./index-NODwdV06.js";import"./index.vue_vue_type_script_setup_true_lang-CB1JgtXC.js";import"./article-BQZdNCom.js";import"./usePaging-VsbTxSU0.js";import"./picker-CIEIqcz7.js";import"./index-DQyvoLLY.js";import"./index-By8JT914.js";import"./cos-js-sdk-v5-DSMN0LUT.js";import"./file-CdkUkN_s.js";import"./index.vue_vue_type_script_setup_true_lang-SfFZFG0h.js";import"./vuedraggable-5bKFmC7X.js";import"./vue-BVs68q8v.js";import"./@vue/compiler-dom-CqayqxS7.js";import"./@vue/compiler-core-CvbQOXIO.js";import"./sortablejs-dsEAUXlE.js";export{o as default};
import{_ as o}from"./add-nav.vue_vue_type_script_setup_true_lang-C3nyyEQa.js";import"./element-plus-lurTijPg.js";import"./@vue/runtime-dom-DDAG46FW.js";import"./lodash-D3kF6u-c.js";import"./@vue/runtime-core-C6bnekPw.js";import"./@vue/reactivity-DiY1c2vO.js";import"./@vue/shared-mAAVTE9n.js";import"./@element-plus/icons-vue-HOEUG8sr.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./index-C2qed6__.js";import"./index-Cr_vnZ4Y.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./vue-router-QlpZ4wdW.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./@vueuse/core-1S4fJlii.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";import"./picker-BK9vGai4.js";import"./index-CzGy7WTN.js";import"./index.vue_vue_type_script_setup_true_lang-CB1JgtXC.js";import"./article-DKkyktyF.js";import"./usePaging-VsbTxSU0.js";import"./picker-C5hl36zR.js";import"./index-BGQ88OEu.js";import"./index-BgCOA9Xa.js";import"./cos-js-sdk-v5-DSMN0LUT.js";import"./file-I7Uxb-66.js";import"./index.vue_vue_type_script_setup_true_lang-SfFZFG0h.js";import"./vuedraggable-5bKFmC7X.js";import"./vue-BVs68q8v.js";import"./@vue/compiler-dom-CqayqxS7.js";import"./@vue/compiler-core-CvbQOXIO.js";import"./sortablejs-dsEAUXlE.js";export{o as default};
@@ -1 +1 @@
import{C as E,B,g as C,i as N}from"./element-plus-lurTijPg.js";import{_ as $}from"./index-Qf24LWMn.js";import{_ as z}from"./picker-BKpCM2IP.js";import{_ as A}from"./picker-CIEIqcz7.js";import{c as D,i as r}from"./index-hTkp7Jz4.js";import{D as I}from"./vuedraggable-5bKFmC7X.js";import{f as R,ak as p,I as F,J as l,a,aN as d,G,O as J,A as L}from"./@vue/runtime-core-C6bnekPw.js";import{y as c,a as O}from"./@vue/reactivity-DiY1c2vO.js";const P={class:"bg-fill-light flex items-center w-full p-4 mb-4"},S={class:"upload-btn w-[60px] h-[60px]"},T={class:"ml-3 flex-1"},j={class:"flex items-center"},q={class:"flex items-center mt-[18px]"},H={class:"flex-1 flex items-center"},K={class:"drag-move cursor-move ml-auto"},oe=R({__name:"add-nav",props:{modelValue:{type:Array,default:()=>[]},max:{type:Number,default:100},min:{type:Number,default:1}},emits:["update:modelValue"],setup(_,{emit:f}){const t=_,V=f,m=L({get(){return t.modelValue},set(s){V("update:modelValue",s)}}),x=()=>{var s;((s=t.modelValue)==null?void 0:s.length)<t.max?m.value.push({image:"",name:"导航名称",link:{},is_show:"1"}):r.msgError(`最多添加${t.max}`)},g=s=>{var e;if(((e=t.modelValue)==null?void 0:e.length)<=t.min)return r.msgError(`最少保留${t.min}`);m.value.splice(s,1)};return(s,e)=>{const i=D,v=A,h=E,k=z,b=C,w=B,y=$,U=N;return p(),F("div",null,[l("div",null,[a(c(I),{class:"draggable",modelValue:c(m),"onUpdate:modelValue":e[0]||(e[0]=o=>O(m)?m.value=o:null),animation:"300",handle:".drag-move","item-key":"index"},{item:d(({element:o,index:u})=>[(p(),G(y,{class:"w-[467px]",key:u,onClose:n=>g(u)},{default:d(()=>[l("div",P,[a(v,{modelValue:o.image,"onUpdate:modelValue":n=>o.image=n,"upload-class":"bg-body",size:"60px","exclude-domain":""},{upload:d(()=>[l("div",S,[a(i,{name:"el-icon-Plus",size:20})])]),_:1},8,["modelValue","onUpdate:modelValue"]),l("div",T,[l("div",j,[e[1]||(e[1]=l("span",{class:"text-tx-regular flex-none mr-3"},"名称",-1)),a(h,{modelValue:o.name,"onUpdate:modelValue":n=>o.name=n,placeholder:"请输入名称"},null,8,["modelValue","onUpdate:modelValue"])]),l("div",q,[e[2]||(e[2]=l("span",{class:"text-tx-regular flex-none mr-3"},"链接",-1)),a(k,{modelValue:o.link,"onUpdate:modelValue":n=>o.link=n},null,8,["modelValue","onUpdate:modelValue"])]),a(w,{label:"是否显示",class:"mt-[18px]"},{default:d(()=>[l("div",H,[a(b,{modelValue:o.is_show,"onUpdate:modelValue":n=>o.is_show=n,"active-value":"1","inactive-value":"0"},null,8,["modelValue","onUpdate:modelValue"]),l("div",K,[a(i,{name:"el-icon-Rank",size:"18"})])])]),_:2},1024)])])]),_:2},1032,["onClose"]))]),_:1},8,["modelValue"])]),l("div",null,[a(U,{type:"primary",onClick:x},{default:d(()=>[...e[3]||(e[3]=[J("添加",-1)])]),_:1})])])}}});export{oe as _};
import{C as E,B,g as C,i as N}from"./element-plus-lurTijPg.js";import{_ as $}from"./index-C2qed6__.js";import{_ as z}from"./picker-BK9vGai4.js";import{_ as A}from"./picker-C5hl36zR.js";import{c as D,i as r}from"./index-Cr_vnZ4Y.js";import{D as I}from"./vuedraggable-5bKFmC7X.js";import{f as R,ak as p,I as F,J as l,a,aN as d,G,O as J,A as L}from"./@vue/runtime-core-C6bnekPw.js";import{y as c,a as O}from"./@vue/reactivity-DiY1c2vO.js";const P={class:"bg-fill-light flex items-center w-full p-4 mb-4"},S={class:"upload-btn w-[60px] h-[60px]"},T={class:"ml-3 flex-1"},j={class:"flex items-center"},q={class:"flex items-center mt-[18px]"},H={class:"flex-1 flex items-center"},K={class:"drag-move cursor-move ml-auto"},oe=R({__name:"add-nav",props:{modelValue:{type:Array,default:()=>[]},max:{type:Number,default:100},min:{type:Number,default:1}},emits:["update:modelValue"],setup(_,{emit:f}){const t=_,V=f,m=L({get(){return t.modelValue},set(s){V("update:modelValue",s)}}),x=()=>{var s;((s=t.modelValue)==null?void 0:s.length)<t.max?m.value.push({image:"",name:"导航名称",link:{},is_show:"1"}):r.msgError(`最多添加${t.max}`)},g=s=>{var e;if(((e=t.modelValue)==null?void 0:e.length)<=t.min)return r.msgError(`最少保留${t.min}`);m.value.splice(s,1)};return(s,e)=>{const i=D,v=A,h=E,k=z,b=C,w=B,y=$,U=N;return p(),F("div",null,[l("div",null,[a(c(I),{class:"draggable",modelValue:c(m),"onUpdate:modelValue":e[0]||(e[0]=o=>O(m)?m.value=o:null),animation:"300",handle:".drag-move","item-key":"index"},{item:d(({element:o,index:u})=>[(p(),G(y,{class:"w-[467px]",key:u,onClose:n=>g(u)},{default:d(()=>[l("div",P,[a(v,{modelValue:o.image,"onUpdate:modelValue":n=>o.image=n,"upload-class":"bg-body",size:"60px","exclude-domain":""},{upload:d(()=>[l("div",S,[a(i,{name:"el-icon-Plus",size:20})])]),_:1},8,["modelValue","onUpdate:modelValue"]),l("div",T,[l("div",j,[e[1]||(e[1]=l("span",{class:"text-tx-regular flex-none mr-3"},"名称",-1)),a(h,{modelValue:o.name,"onUpdate:modelValue":n=>o.name=n,placeholder:"请输入名称"},null,8,["modelValue","onUpdate:modelValue"])]),l("div",q,[e[2]||(e[2]=l("span",{class:"text-tx-regular flex-none mr-3"},"链接",-1)),a(k,{modelValue:o.link,"onUpdate:modelValue":n=>o.link=n},null,8,["modelValue","onUpdate:modelValue"])]),a(w,{label:"是否显示",class:"mt-[18px]"},{default:d(()=>[l("div",H,[a(b,{modelValue:o.is_show,"onUpdate:modelValue":n=>o.is_show=n,"active-value":"1","inactive-value":"0"},null,8,["modelValue","onUpdate:modelValue"]),l("div",K,[a(i,{name:"el-icon-Rank",size:"18"})])])]),_:2},1024)])])]),_:2},1032,["onClose"]))]),_:1},8,["modelValue"])]),l("div",null,[a(U,{type:"primary",onClick:x},{default:d(()=>[...e[3]||(e[3]=[J("添加",-1)])]),_:1})])])}}});export{oe as _};
@@ -1 +1 @@
import{r as n}from"./index-hTkp7Jz4.js";function e(t){return n.get({url:"/auth.admin/lists",params:t},{ignoreCancelToken:!0})}function i(t){return n.post({url:"/auth.admin/add",params:t})}function r(t){return n.post({url:"/auth.admin/edit",params:t})}function u(t){return n.post({url:"/auth.admin/delete",params:t})}function d(t){return n.get({url:"/auth.admin/detail",params:t})}export{e as a,r as b,u as c,i as d,d as e};
import{r as n}from"./index-Cr_vnZ4Y.js";function e(t){return n.get({url:"/auth.admin/lists",params:t},{ignoreCancelToken:!0})}function i(t){return n.post({url:"/auth.admin/add",params:t})}function r(t){return n.post({url:"/auth.admin/edit",params:t})}function u(t){return n.post({url:"/auth.admin/delete",params:t})}function d(t){return n.get({url:"/auth.admin/detail",params:t})}export{e as a,r as b,u as c,i as d,d as e};
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
import{r as e}from"./index-hTkp7Jz4.js";function a(t){return e.get({url:"/article.articleCate/lists",params:t})}function l(t){return e.get({url:"/article.articleCate/all",params:t})}function i(t){return e.post({url:"/article.articleCate/add",params:t})}function c(t){return e.post({url:"/article.articleCate/edit",params:t})}function u(t){return e.post({url:"/article.articleCate/delete",params:t})}function n(t){return e.get({url:"/article.articleCate/detail",params:t})}function s(t){return e.post({url:"/article.articleCate/updateStatus",params:t})}function o(t){return e.get({url:"/article.article/lists",params:t})}function d(t){return e.post({url:"/article.article/add",params:t})}function f(t){return e.post({url:"/article.article/edit",params:t})}function C(t){return e.post({url:"/article.article/delete",params:t})}function p(t){return e.get({url:"/article.article/detail",params:t})}function g(t){return e.post({url:"/article.article/updateStatus",params:t})}export{a,u as b,s as c,c as d,i as e,n as f,p as g,f as h,d as i,l as j,o as k,g as l,C as m};
import{r as e}from"./index-Cr_vnZ4Y.js";function a(t){return e.get({url:"/article.articleCate/lists",params:t})}function l(t){return e.get({url:"/article.articleCate/all",params:t})}function i(t){return e.post({url:"/article.articleCate/add",params:t})}function c(t){return e.post({url:"/article.articleCate/edit",params:t})}function u(t){return e.post({url:"/article.articleCate/delete",params:t})}function n(t){return e.get({url:"/article.articleCate/detail",params:t})}function s(t){return e.post({url:"/article.articleCate/updateStatus",params:t})}function o(t){return e.get({url:"/article.article/lists",params:t})}function d(t){return e.post({url:"/article.article/add",params:t})}function f(t){return e.post({url:"/article.article/edit",params:t})}function C(t){return e.post({url:"/article.article/delete",params:t})}function p(t){return e.get({url:"/article.article/detail",params:t})}function g(t){return e.post({url:"/article.article/updateStatus",params:t})}export{a,u as b,s as c,c as d,i as e,n as f,p as g,f as h,d as i,l as j,o as k,g as l,C as m};
@@ -1 +1 @@
import{r as e}from"./index-hTkp7Jz4.js";function r(s){return e.get({url:"/asset.AssetUser/lists",params:s})}function u(s){return e.post({url:"/asset.AssetUser/add",params:s})}function a(s){return e.post({url:"/asset.AssetUser/edit",params:s})}function i(s){return e.post({url:"/asset.AssetUser/delete",params:s})}function o(s){return e.get({url:"/asset.AssetResource/lists",params:s})}function n(s){return e.post({url:"/asset.AssetResource/add",params:s})}function A(s){return e.post({url:"/asset.AssetResource/edit",params:s})}function c(s){return e.post({url:"/asset.AssetResource/delete",params:s})}export{o as a,c as b,A as c,n as d,r as e,a as f,i as g,u as h};
import{r as e}from"./index-Cr_vnZ4Y.js";function r(s){return e.get({url:"/asset.AssetUser/lists",params:s})}function u(s){return e.post({url:"/asset.AssetUser/add",params:s})}function a(s){return e.post({url:"/asset.AssetUser/edit",params:s})}function i(s){return e.post({url:"/asset.AssetUser/delete",params:s})}function o(s){return e.get({url:"/asset.AssetResource/lists",params:s})}function n(s){return e.post({url:"/asset.AssetResource/add",params:s})}function A(s){return e.post({url:"/asset.AssetResource/edit",params:s})}function c(s){return e.post({url:"/asset.AssetResource/delete",params:s})}export{o as a,c as b,A as c,n as d,r as e,a as f,i as g,u as h};
@@ -1 +1 @@
import{_ as o}from"./attr.vue_vue_type_script_setup_true_lang-CgV6rUXF.js";import"./element-plus-lurTijPg.js";import"./@vue/runtime-dom-DDAG46FW.js";import"./lodash-D3kF6u-c.js";import"./@vue/runtime-core-C6bnekPw.js";import"./@vue/reactivity-DiY1c2vO.js";import"./@vue/shared-mAAVTE9n.js";import"./@element-plus/icons-vue-HOEUG8sr.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./add-nav.vue_vue_type_script_setup_true_lang-DJJZsT5O.js";import"./index-Qf24LWMn.js";import"./index-hTkp7Jz4.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./vue-router-QlpZ4wdW.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./@vueuse/core-1S4fJlii.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";import"./picker-BKpCM2IP.js";import"./index-NODwdV06.js";import"./index.vue_vue_type_script_setup_true_lang-CB1JgtXC.js";import"./article-BQZdNCom.js";import"./usePaging-VsbTxSU0.js";import"./picker-CIEIqcz7.js";import"./index-DQyvoLLY.js";import"./index-By8JT914.js";import"./cos-js-sdk-v5-DSMN0LUT.js";import"./file-CdkUkN_s.js";import"./index.vue_vue_type_script_setup_true_lang-SfFZFG0h.js";import"./vuedraggable-5bKFmC7X.js";import"./vue-BVs68q8v.js";import"./@vue/compiler-dom-CqayqxS7.js";import"./@vue/compiler-core-CvbQOXIO.js";import"./sortablejs-dsEAUXlE.js";export{o as default};
import{_ as o}from"./attr.vue_vue_type_script_setup_true_lang-BhEiQ_Ln.js";import"./element-plus-lurTijPg.js";import"./@vue/runtime-dom-DDAG46FW.js";import"./lodash-D3kF6u-c.js";import"./@vue/runtime-core-C6bnekPw.js";import"./@vue/reactivity-DiY1c2vO.js";import"./@vue/shared-mAAVTE9n.js";import"./@element-plus/icons-vue-HOEUG8sr.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./add-nav.vue_vue_type_script_setup_true_lang-C3nyyEQa.js";import"./index-C2qed6__.js";import"./index-Cr_vnZ4Y.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./vue-router-QlpZ4wdW.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./@vueuse/core-1S4fJlii.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";import"./picker-BK9vGai4.js";import"./index-CzGy7WTN.js";import"./index.vue_vue_type_script_setup_true_lang-CB1JgtXC.js";import"./article-DKkyktyF.js";import"./usePaging-VsbTxSU0.js";import"./picker-C5hl36zR.js";import"./index-BGQ88OEu.js";import"./index-BgCOA9Xa.js";import"./cos-js-sdk-v5-DSMN0LUT.js";import"./file-I7Uxb-66.js";import"./index.vue_vue_type_script_setup_true_lang-SfFZFG0h.js";import"./vuedraggable-5bKFmC7X.js";import"./vue-BVs68q8v.js";import"./@vue/compiler-dom-CqayqxS7.js";import"./@vue/compiler-core-CvbQOXIO.js";import"./sortablejs-dsEAUXlE.js";export{o as default};
@@ -1 +1 @@
import{_ as o}from"./attr.vue_vue_type_script_setup_true_lang-D1oKQlcj.js";import"./element-plus-lurTijPg.js";import"./@vue/runtime-dom-DDAG46FW.js";import"./lodash-D3kF6u-c.js";import"./@vue/runtime-core-C6bnekPw.js";import"./@vue/reactivity-DiY1c2vO.js";import"./@vue/shared-mAAVTE9n.js";import"./@element-plus/icons-vue-HOEUG8sr.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./index.vue_vue_type_script_setup_true_lang-8-xP7boW.js";import"./@vueuse/core-1S4fJlii.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./picker-CIEIqcz7.js";import"./index-NODwdV06.js";import"./index-hTkp7Jz4.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./vue-router-QlpZ4wdW.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";import"./index-DQyvoLLY.js";import"./index.vue_vue_type_script_setup_true_lang-CB1JgtXC.js";import"./index-Qf24LWMn.js";import"./index-By8JT914.js";import"./cos-js-sdk-v5-DSMN0LUT.js";import"./file-CdkUkN_s.js";import"./index.vue_vue_type_script_setup_true_lang-SfFZFG0h.js";import"./usePaging-VsbTxSU0.js";import"./vuedraggable-5bKFmC7X.js";import"./vue-BVs68q8v.js";import"./@vue/compiler-dom-CqayqxS7.js";import"./@vue/compiler-core-CvbQOXIO.js";import"./sortablejs-dsEAUXlE.js";export{o as default};
import{_ as o}from"./attr.vue_vue_type_script_setup_true_lang-CyPlAB74.js";import"./element-plus-lurTijPg.js";import"./@vue/runtime-dom-DDAG46FW.js";import"./lodash-D3kF6u-c.js";import"./@vue/runtime-core-C6bnekPw.js";import"./@vue/reactivity-DiY1c2vO.js";import"./@vue/shared-mAAVTE9n.js";import"./@element-plus/icons-vue-HOEUG8sr.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./index.vue_vue_type_script_setup_true_lang-8-xP7boW.js";import"./@vueuse/core-1S4fJlii.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./picker-C5hl36zR.js";import"./index-CzGy7WTN.js";import"./index-Cr_vnZ4Y.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./vue-router-QlpZ4wdW.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";import"./index-BGQ88OEu.js";import"./index.vue_vue_type_script_setup_true_lang-CB1JgtXC.js";import"./index-C2qed6__.js";import"./index-BgCOA9Xa.js";import"./cos-js-sdk-v5-DSMN0LUT.js";import"./file-I7Uxb-66.js";import"./index.vue_vue_type_script_setup_true_lang-SfFZFG0h.js";import"./usePaging-VsbTxSU0.js";import"./vuedraggable-5bKFmC7X.js";import"./vue-BVs68q8v.js";import"./@vue/compiler-dom-CqayqxS7.js";import"./@vue/compiler-core-CvbQOXIO.js";import"./sortablejs-dsEAUXlE.js";export{o as default};
@@ -1 +1 @@
import{_ as o}from"./attr.vue_vue_type_script_setup_true_lang-BmHZ8imK.js";import"./element-plus-lurTijPg.js";import"./@vue/runtime-dom-DDAG46FW.js";import"./lodash-D3kF6u-c.js";import"./@vue/runtime-core-C6bnekPw.js";import"./@vue/reactivity-DiY1c2vO.js";import"./@vue/shared-mAAVTE9n.js";import"./@element-plus/icons-vue-HOEUG8sr.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./picker-CIEIqcz7.js";import"./index-NODwdV06.js";import"./index-hTkp7Jz4.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./vue-router-QlpZ4wdW.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./@vueuse/core-1S4fJlii.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";import"./index-DQyvoLLY.js";import"./index.vue_vue_type_script_setup_true_lang-CB1JgtXC.js";import"./index-Qf24LWMn.js";import"./index-By8JT914.js";import"./cos-js-sdk-v5-DSMN0LUT.js";import"./file-CdkUkN_s.js";import"./index.vue_vue_type_script_setup_true_lang-SfFZFG0h.js";import"./usePaging-VsbTxSU0.js";import"./vuedraggable-5bKFmC7X.js";import"./vue-BVs68q8v.js";import"./@vue/compiler-dom-CqayqxS7.js";import"./@vue/compiler-core-CvbQOXIO.js";import"./sortablejs-dsEAUXlE.js";export{o as default};
import{_ as o}from"./attr.vue_vue_type_script_setup_true_lang-DNRuFJwY.js";import"./element-plus-lurTijPg.js";import"./@vue/runtime-dom-DDAG46FW.js";import"./lodash-D3kF6u-c.js";import"./@vue/runtime-core-C6bnekPw.js";import"./@vue/reactivity-DiY1c2vO.js";import"./@vue/shared-mAAVTE9n.js";import"./@element-plus/icons-vue-HOEUG8sr.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./picker-C5hl36zR.js";import"./index-CzGy7WTN.js";import"./index-Cr_vnZ4Y.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./vue-router-QlpZ4wdW.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./@vueuse/core-1S4fJlii.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";import"./index-BGQ88OEu.js";import"./index.vue_vue_type_script_setup_true_lang-CB1JgtXC.js";import"./index-C2qed6__.js";import"./index-BgCOA9Xa.js";import"./cos-js-sdk-v5-DSMN0LUT.js";import"./file-I7Uxb-66.js";import"./index.vue_vue_type_script_setup_true_lang-SfFZFG0h.js";import"./usePaging-VsbTxSU0.js";import"./vuedraggable-5bKFmC7X.js";import"./vue-BVs68q8v.js";import"./@vue/compiler-dom-CqayqxS7.js";import"./@vue/compiler-core-CvbQOXIO.js";import"./sortablejs-dsEAUXlE.js";export{o as default};
@@ -1 +1 @@
import{_ as o}from"./attr.vue_vue_type_script_setup_true_lang-CqK_9stV.js";import"./element-plus-lurTijPg.js";import"./@vue/runtime-dom-DDAG46FW.js";import"./lodash-D3kF6u-c.js";import"./@vue/runtime-core-C6bnekPw.js";import"./@vue/reactivity-DiY1c2vO.js";import"./@vue/shared-mAAVTE9n.js";import"./@element-plus/icons-vue-HOEUG8sr.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./add-nav.vue_vue_type_script_setup_true_lang-DJJZsT5O.js";import"./index-Qf24LWMn.js";import"./index-hTkp7Jz4.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./vue-router-QlpZ4wdW.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./@vueuse/core-1S4fJlii.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";import"./picker-BKpCM2IP.js";import"./index-NODwdV06.js";import"./index.vue_vue_type_script_setup_true_lang-CB1JgtXC.js";import"./article-BQZdNCom.js";import"./usePaging-VsbTxSU0.js";import"./picker-CIEIqcz7.js";import"./index-DQyvoLLY.js";import"./index-By8JT914.js";import"./cos-js-sdk-v5-DSMN0LUT.js";import"./file-CdkUkN_s.js";import"./index.vue_vue_type_script_setup_true_lang-SfFZFG0h.js";import"./vuedraggable-5bKFmC7X.js";import"./vue-BVs68q8v.js";import"./@vue/compiler-dom-CqayqxS7.js";import"./@vue/compiler-core-CvbQOXIO.js";import"./sortablejs-dsEAUXlE.js";export{o as default};
import{_ as o}from"./attr.vue_vue_type_script_setup_true_lang-DFCeJSN7.js";import"./element-plus-lurTijPg.js";import"./@vue/runtime-dom-DDAG46FW.js";import"./lodash-D3kF6u-c.js";import"./@vue/runtime-core-C6bnekPw.js";import"./@vue/reactivity-DiY1c2vO.js";import"./@vue/shared-mAAVTE9n.js";import"./@element-plus/icons-vue-HOEUG8sr.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./add-nav.vue_vue_type_script_setup_true_lang-C3nyyEQa.js";import"./index-C2qed6__.js";import"./index-Cr_vnZ4Y.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./vue-router-QlpZ4wdW.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./@vueuse/core-1S4fJlii.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";import"./picker-BK9vGai4.js";import"./index-CzGy7WTN.js";import"./index.vue_vue_type_script_setup_true_lang-CB1JgtXC.js";import"./article-DKkyktyF.js";import"./usePaging-VsbTxSU0.js";import"./picker-C5hl36zR.js";import"./index-BGQ88OEu.js";import"./index-BgCOA9Xa.js";import"./cos-js-sdk-v5-DSMN0LUT.js";import"./file-I7Uxb-66.js";import"./index.vue_vue_type_script_setup_true_lang-SfFZFG0h.js";import"./vuedraggable-5bKFmC7X.js";import"./vue-BVs68q8v.js";import"./@vue/compiler-dom-CqayqxS7.js";import"./@vue/compiler-core-CvbQOXIO.js";import"./sortablejs-dsEAUXlE.js";export{o as default};
@@ -1 +1 @@
import{_ as o}from"./attr.vue_vue_type_script_setup_true_lang-DFVzqRa_.js";import"./element-plus-lurTijPg.js";import"./@vue/runtime-dom-DDAG46FW.js";import"./lodash-D3kF6u-c.js";import"./@vue/runtime-core-C6bnekPw.js";import"./@vue/reactivity-DiY1c2vO.js";import"./@vue/shared-mAAVTE9n.js";import"./@element-plus/icons-vue-HOEUG8sr.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./index-Qf24LWMn.js";import"./index-hTkp7Jz4.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./vue-router-QlpZ4wdW.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./@vueuse/core-1S4fJlii.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";import"./picker-BKpCM2IP.js";import"./index-NODwdV06.js";import"./index.vue_vue_type_script_setup_true_lang-CB1JgtXC.js";import"./article-BQZdNCom.js";import"./usePaging-VsbTxSU0.js";import"./picker-CIEIqcz7.js";import"./index-DQyvoLLY.js";import"./index-By8JT914.js";import"./cos-js-sdk-v5-DSMN0LUT.js";import"./file-CdkUkN_s.js";import"./index.vue_vue_type_script_setup_true_lang-SfFZFG0h.js";import"./vuedraggable-5bKFmC7X.js";import"./vue-BVs68q8v.js";import"./@vue/compiler-dom-CqayqxS7.js";import"./@vue/compiler-core-CvbQOXIO.js";import"./sortablejs-dsEAUXlE.js";export{o as default};
import{_ as o}from"./attr.vue_vue_type_script_setup_true_lang-D7lNibN_.js";import"./element-plus-lurTijPg.js";import"./@vue/runtime-dom-DDAG46FW.js";import"./lodash-D3kF6u-c.js";import"./@vue/runtime-core-C6bnekPw.js";import"./@vue/reactivity-DiY1c2vO.js";import"./@vue/shared-mAAVTE9n.js";import"./@element-plus/icons-vue-HOEUG8sr.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./index-C2qed6__.js";import"./index-Cr_vnZ4Y.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./vue-router-QlpZ4wdW.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./@vueuse/core-1S4fJlii.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";import"./picker-BK9vGai4.js";import"./index-CzGy7WTN.js";import"./index.vue_vue_type_script_setup_true_lang-CB1JgtXC.js";import"./article-DKkyktyF.js";import"./usePaging-VsbTxSU0.js";import"./picker-C5hl36zR.js";import"./index-BGQ88OEu.js";import"./index-BgCOA9Xa.js";import"./cos-js-sdk-v5-DSMN0LUT.js";import"./file-I7Uxb-66.js";import"./index.vue_vue_type_script_setup_true_lang-SfFZFG0h.js";import"./vuedraggable-5bKFmC7X.js";import"./vue-BVs68q8v.js";import"./@vue/compiler-dom-CqayqxS7.js";import"./@vue/compiler-core-CvbQOXIO.js";import"./sortablejs-dsEAUXlE.js";export{o as default};
@@ -1 +1 @@
import{_ as o}from"./attr.vue_vue_type_script_setup_true_lang-C0llDfW4.js";import"./element-plus-lurTijPg.js";import"./@vue/runtime-dom-DDAG46FW.js";import"./lodash-D3kF6u-c.js";import"./@vue/runtime-core-C6bnekPw.js";import"./@vue/reactivity-DiY1c2vO.js";import"./@vue/shared-mAAVTE9n.js";import"./@element-plus/icons-vue-HOEUG8sr.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./index-Qf24LWMn.js";import"./index-hTkp7Jz4.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./vue-router-QlpZ4wdW.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./@vueuse/core-1S4fJlii.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";import"./picker-BKpCM2IP.js";import"./index-NODwdV06.js";import"./index.vue_vue_type_script_setup_true_lang-CB1JgtXC.js";import"./article-BQZdNCom.js";import"./usePaging-VsbTxSU0.js";import"./picker-CIEIqcz7.js";import"./index-DQyvoLLY.js";import"./index-By8JT914.js";import"./cos-js-sdk-v5-DSMN0LUT.js";import"./file-CdkUkN_s.js";import"./index.vue_vue_type_script_setup_true_lang-SfFZFG0h.js";import"./vuedraggable-5bKFmC7X.js";import"./vue-BVs68q8v.js";import"./@vue/compiler-dom-CqayqxS7.js";import"./@vue/compiler-core-CvbQOXIO.js";import"./sortablejs-dsEAUXlE.js";import"./index.vue_vue_type_script_setup_true_lang-8-xP7boW.js";export{o as default};
import{_ as o}from"./attr.vue_vue_type_script_setup_true_lang-Bl85SH4I.js";import"./element-plus-lurTijPg.js";import"./@vue/runtime-dom-DDAG46FW.js";import"./lodash-D3kF6u-c.js";import"./@vue/runtime-core-C6bnekPw.js";import"./@vue/reactivity-DiY1c2vO.js";import"./@vue/shared-mAAVTE9n.js";import"./@element-plus/icons-vue-HOEUG8sr.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./index-C2qed6__.js";import"./index-Cr_vnZ4Y.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./vue-router-QlpZ4wdW.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./@vueuse/core-1S4fJlii.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";import"./picker-BK9vGai4.js";import"./index-CzGy7WTN.js";import"./index.vue_vue_type_script_setup_true_lang-CB1JgtXC.js";import"./article-DKkyktyF.js";import"./usePaging-VsbTxSU0.js";import"./picker-C5hl36zR.js";import"./index-BGQ88OEu.js";import"./index-BgCOA9Xa.js";import"./cos-js-sdk-v5-DSMN0LUT.js";import"./file-I7Uxb-66.js";import"./index.vue_vue_type_script_setup_true_lang-SfFZFG0h.js";import"./vuedraggable-5bKFmC7X.js";import"./vue-BVs68q8v.js";import"./@vue/compiler-dom-CqayqxS7.js";import"./@vue/compiler-core-CvbQOXIO.js";import"./sortablejs-dsEAUXlE.js";import"./index.vue_vue_type_script_setup_true_lang-8-xP7boW.js";export{o as default};
@@ -1 +1 @@
import{m as b,l as c,D as V}from"./element-plus-lurTijPg.js";import{_ as l}from"./menu-set.vue_vue_type_script_setup_true_lang-BvD19j96.js";import{f as v,ak as x,I as k,J as E,a as o,aN as e,F as g,A as w}from"./@vue/runtime-core-C6bnekPw.js";import{y as r}from"./@vue/reactivity-DiY1c2vO.js";import"./@vue/runtime-dom-DDAG46FW.js";import"./lodash-D3kF6u-c.js";import"./@vue/shared-mAAVTE9n.js";import"./@element-plus/icons-vue-HOEUG8sr.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./index-Qf24LWMn.js";import"./index-hTkp7Jz4.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./vue-router-QlpZ4wdW.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./@vueuse/core-1S4fJlii.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";import"./picker-BKpCM2IP.js";import"./index-NODwdV06.js";import"./index.vue_vue_type_script_setup_true_lang-CB1JgtXC.js";import"./article-BQZdNCom.js";import"./usePaging-VsbTxSU0.js";import"./picker-CIEIqcz7.js";import"./index-DQyvoLLY.js";import"./index-By8JT914.js";import"./cos-js-sdk-v5-DSMN0LUT.js";import"./file-CdkUkN_s.js";import"./index.vue_vue_type_script_setup_true_lang-SfFZFG0h.js";import"./vuedraggable-5bKFmC7X.js";import"./vue-BVs68q8v.js";import"./@vue/compiler-dom-CqayqxS7.js";import"./@vue/compiler-core-CvbQOXIO.js";import"./sortablejs-dsEAUXlE.js";const Bt=v({__name:"attr",props:{modelValue:{type:Object,default:()=>({nav:[],menu:{}})}},emits:["update:modelValue"],setup(n,{emit:s}){const u=n,d=s,m=w({get(){return u.modelValue},set(i){d("update:modelValue",i)}});return(i,t)=>{const a=c,f=b,_=V;return x(),k(g,null,[t[2]||(t[2]=E("div",{class:"title flex items-center before:w-[3px] before:h-[14px] before:block before:bg-primary before:mr-2"}," pc导航设置 ",-1)),o(_,{class:"mt-4","label-width":"70px"},{default:e(()=>[o(f,{"model-value":"nav"},{default:e(()=>[o(a,{label:"主导航设置",name:"nav"},{default:e(()=>[o(l,{modelValue:r(m).nav,"onUpdate:modelValue":t[0]||(t[0]=p=>r(m).nav=p)},null,8,["modelValue"])]),_:1}),o(a,{label:"菜单设置",name:"menu"},{default:e(()=>[o(l,{modelValue:r(m).menu,"onUpdate:modelValue":t[1]||(t[1]=p=>r(m).menu=p)},null,8,["modelValue"])]),_:1})]),_:1})]),_:1})],64)}}});export{Bt as default};
import{m as b,l as c,D as V}from"./element-plus-lurTijPg.js";import{_ as l}from"./menu-set.vue_vue_type_script_setup_true_lang-aSeGcpGj.js";import{f as v,ak as x,I as k,J as E,a as o,aN as e,F as g,A as w}from"./@vue/runtime-core-C6bnekPw.js";import{y as r}from"./@vue/reactivity-DiY1c2vO.js";import"./@vue/runtime-dom-DDAG46FW.js";import"./lodash-D3kF6u-c.js";import"./@vue/shared-mAAVTE9n.js";import"./@element-plus/icons-vue-HOEUG8sr.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./index-C2qed6__.js";import"./index-Cr_vnZ4Y.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./vue-router-QlpZ4wdW.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./@vueuse/core-1S4fJlii.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";import"./picker-BK9vGai4.js";import"./index-CzGy7WTN.js";import"./index.vue_vue_type_script_setup_true_lang-CB1JgtXC.js";import"./article-DKkyktyF.js";import"./usePaging-VsbTxSU0.js";import"./picker-C5hl36zR.js";import"./index-BGQ88OEu.js";import"./index-BgCOA9Xa.js";import"./cos-js-sdk-v5-DSMN0LUT.js";import"./file-I7Uxb-66.js";import"./index.vue_vue_type_script_setup_true_lang-SfFZFG0h.js";import"./vuedraggable-5bKFmC7X.js";import"./vue-BVs68q8v.js";import"./@vue/compiler-dom-CqayqxS7.js";import"./@vue/compiler-core-CvbQOXIO.js";import"./sortablejs-dsEAUXlE.js";const Bt=v({__name:"attr",props:{modelValue:{type:Object,default:()=>({nav:[],menu:{}})}},emits:["update:modelValue"],setup(n,{emit:s}){const u=n,d=s,m=w({get(){return u.modelValue},set(i){d("update:modelValue",i)}});return(i,t)=>{const a=c,f=b,_=V;return x(),k(g,null,[t[2]||(t[2]=E("div",{class:"title flex items-center before:w-[3px] before:h-[14px] before:block before:bg-primary before:mr-2"}," pc导航设置 ",-1)),o(_,{class:"mt-4","label-width":"70px"},{default:e(()=>[o(f,{"model-value":"nav"},{default:e(()=>[o(a,{label:"主导航设置",name:"nav"},{default:e(()=>[o(l,{modelValue:r(m).nav,"onUpdate:modelValue":t[0]||(t[0]=p=>r(m).nav=p)},null,8,["modelValue"])]),_:1}),o(a,{label:"菜单设置",name:"menu"},{default:e(()=>[o(l,{modelValue:r(m).menu,"onUpdate:modelValue":t[1]||(t[1]=p=>r(m).menu=p)},null,8,["modelValue"])]),_:1})]),_:1})]),_:1})],64)}}});export{Bt as default};

Some files were not shown because too many files have changed in this diff Show More