Files
yxe-mp/packageUser/chronic/index.vue
T
weicw-dshsa 575b4a723a perf(门店选择): 门店明细移出页面 data,修复首屏卡顿
门店接口 /shop/merchant/groupByChain 一次返回 8105 家门店(约 1.1MB)。
此前把门店明细原样放进页面 data,data 树膨胀到 1118KB,导致此后每次
setData 都要重新 diff/序列化整棵树——即使载荷只有 1 个字段,耗时也从
约 490ms 涨到约 1000ms,首屏因此多花约 3.5s。

改为把明细挂在实例属性 merchantStore 上(不进页面 data),
merchantChainList 只保留 {id, text, merchantCount} 骨架,
新增 getMerchantsByChainId() / getVisibleMerchants() 按需取数。

涉及页面:
- packageUser/self-report/create.vue
- packageUser/chronic/index.vue
- pages/task/detail.vue
- pages/share/create.vue
- pages/share/index.vue
- pages/audit/components/AuditShareItem.vue

实测(微信开发者工具自动化):
- 页面 data 门店明细 1118KB -> 1KB
- 单字段 setData 998ms -> 10~53ms
- self-report/create 首屏 9055ms -> 5341ms
- 门店 picker 选项数不变(大参林 2700 项),搜索与选中回填均正常
2026-10-11 15:18:06 +08:00

1423 lines
45 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="chronic-page">
<view class="page-hero">
<AppNavbar title="慢病信息收集" show-back/>
<view class="intro-card">
<view class="card-title">
<view class="title-bar"></view>
<text>慢病信息收集</text>
</view>
<text class="intro-text">收集三高信息,完善姓名、性别、电话、出生年月日、病史、用药史。</text>
<text class="intro-meta">信息仅用于慢病管理与跟进,提交后可在后台查看。</text>
</view>
</view>
<view class="card block">
<view class="section-title">基础信息</view>
<view class="form">
<view class="form-row">
<text>连锁</text>
<view class="picker-value" @tap="openChainPicker">
{{ form.chainName || (merchantLoading ? '加载中...' : '请选择') }}
</view>
</view>
<view class="form-row">
<text>门店</text>
<view class="picker-value" @tap="openMerchantPicker">
{{ form.merchantName || '请选择' }}
</view>
</view>
<view class="form-row">
<text>姓名</text>
<input v-model="form.name" placeholder="请输入姓名"/>
</view>
<view class="form-row">
<text>性别</text>
<view class="picker-value" @tap="openGenderPicker">{{ form.gender || '请选择' }}</view>
</view>
<view class="form-row">
<text>电话</text>
<input type="number" v-model="form.phone" placeholder="请输入联系方式"/>
</view>
<view class="form-row">
<text>出生年月日</text>
<view class="picker-value" @tap="openBirthPicker">{{ form.birthday || '请选择' }}</view>
</view>
</view>
</view>
<view class="card block">
<view class="section-title">血液信息</view>
<view class="form">
<view class="ref-row">
<view class="ref-btn" @tap="showHemoglobinReference">血红蛋白参考值</view>
</view>
<view class="form-row">
<text>血红蛋白数值</text>
<input v-model="form.hemoglobinValue" type="digit" placeholder="请输入血红蛋白数值"/>
</view>
<view class="form-row">
<text>是否贫血</text>
<view class="picker-value" @tap="openAnemiaPicker">{{ form.anemiaLevel || '请选择' }}</view>
</view>
<view class="form-row">
<text>是否地贫</text>
<view class="picker-value" @tap="openThalassemiaPicker">{{ form.thalassemiaType || '请选择' }}</view>
</view>
</view>
</view>
<view class="card block">
<view class="section-head">
<view class="section-title">用药追踪</view>
<view class="tracking-add-btn" @tap="addTrackingItem">添加</view>
</view>
<view class="tracking-tip">选择药品后填写本次购药时间和购买盒数,系统会按每盒服用天数自动计算预计服用完时间。</view>
<view v-if="trackingList.length === 0" class="tracking-empty">暂无用药追踪,可点击添加记录。</view>
<view v-for="(item, index) in trackingList" :key="item._key" class="tracking-item">
<view class="tracking-item-head">
<text>用药追踪{{ index + 1 }}</text>
<view class="tracking-remove" @tap="removeTrackingItem(index)">移除</view>
</view>
<view class="form">
<view class="form-row">
<text>药品</text>
<view class="picker-value tracking-product-value" @tap="openTrackingProductPicker(index)">
{{ item.productName || (trackingProductLoading ? '加载中...' : '请选择') }}
</view>
</view>
<view v-if="item.productName" class="tracking-product-meta">
<text>规格粒/片:{{ item.dailyUsageCount || '-' }}</text>
<text>说明书每天使用/次:{{ item.dosagePerTime || '-' }}</text>
<text>每次几粒/片:{{ item.boxUsageDays || '-' }}</text>
</view>
<view class="form-row">
<text>本次购药时间</text>
<view class="picker-value" @tap="openTrackingDatePicker(index)">{{ item.purchaseTime || '请选择' }}</view>
</view>
<view class="form-row">
<text>购买盒数</text>
<input
:value="item.boxCount"
type="number"
placeholder="请输入购买盒数"
@input="onTrackingBoxInput(index, $event)"
/>
</view>
<view class="form-row">
<text>预估服用完时间</text>
<view class="readonly-value">{{ item.estimatedFinishTime || '-' }}</view>
</view>
</view>
</view>
</view>
<view class="card block">
<view class="section-title">三高信息</view>
<checkbox-group @change="setConditions">
<view class="conditions">
<label v-for="item in conditionOptions" :key="item.value" class="condition">
<checkbox :value="item.value" :checked="form[item.value] === 1" color="#2563eb"/>
<text class="condition-text">{{ item.label }}</text>
</label>
</view>
</checkbox-group>
<view v-if="form.highBloodPressure === 1" class="form pressure-form">
<view class="ref-row">
<view class="ref-btn" @tap="showHighBloodPressureReference">高血压参考值</view>
</view>
<view class="form-row">
<text>收缩压</text>
<input
v-model="form.systolicPressure"
type="number"
placeholder="请输入收缩压"
/>
</view>
<view class="form-row">
<text>舒张压</text>
<input
v-model="form.diastolicPressure"
type="number"
placeholder="请输入舒张压"
/>
</view>
</view>
<view v-if="form.highBloodSugar === 1" class="form pressure-form">
<view class="ref-row">
<view class="ref-btn" @tap="showHighBloodSugarReference">高血糖参考值</view>
</view>
<view class="form-row">
<text>空腹血糖</text>
<input
v-model="form.fastingBloodGlucose"
type="digit"
placeholder="请输入空腹血糖"
/>
</view>
<view class="form-row">
<text>餐后2小时血糖</text>
<input
v-model="form.postprandial2hBloodGlucose"
type="digit"
placeholder="请输入餐后2小时血糖"
/>
</view>
<view class="form-row">
<text>糖化血红蛋白(HbA1c)</text>
<input
v-model="form.hba1c"
type="digit"
placeholder="请输入HbA1c"
/>
</view>
</view>
<view v-if="form.hyperlipidemia === 1" class="form pressure-form">
<view class="ref-row">
<view class="ref-btn" @tap="showHyperlipidemiaReference">高血脂参考值</view>
</view>
<view class="form-row">
<text>总胆固醇(TC)</text>
<input
v-model="form.totalCholesterol"
type="digit"
placeholder="请输入TC"
/>
</view>
<view class="form-row">
<text>甘油三酯(TG)</text>
<input
v-model="form.triglyceride"
type="digit"
placeholder="请输入TG"
/>
</view>
<view class="form-row">
<text>低密度脂蛋白(LDL-C)(“坏”胆固醇)</text>
<input
v-model="form.ldlC"
type="digit"
placeholder="请输入LDL-C"
/>
</view>
<view class="form-row">
<text>高密度脂蛋白(HDL-C)(“好”胆固醇)</text>
<input
v-model="form.hdlC"
type="digit"
placeholder="请输入HDL-C"
/>
</view>
</view>
</view>
<view class="card block">
<view class="section-title">病史</view>
<textarea class="textarea" v-model="form.medicalHistory" placeholder="请输入既往病史"></textarea>
</view>
<view class="card block">
<view class="section-title">用药史</view>
<textarea class="textarea" v-model="form.medicationHistory" placeholder="请输入当前/既往用药情况"></textarea>
</view>
<view class="primary-btn submit-btn" @tap="submitForm">提交信息</view>
<u-picker
:show="showGenderPicker"
:columns="[genderOptions]"
@confirm="onGenderConfirm"
@cancel="closeGenderPicker"
@close="closeGenderPicker"
/>
<u-picker
:show="showAnemiaPicker"
:columns="[anemiaOptions]"
@confirm="onAnemiaConfirm"
@cancel="closeAnemiaPicker"
@close="closeAnemiaPicker"
/>
<u-picker
:show="showThalassemiaPicker"
:columns="[thalassemiaOptions]"
@confirm="onThalassemiaConfirm"
@cancel="closeThalassemiaPicker"
@close="closeThalassemiaPicker"
/>
<u-datetime-picker
:show="showBirthPicker"
:value="birthPickerValue"
:min-date="minDate"
mode="date"
@confirm="onBirthConfirm"
@cancel="closeBirthPicker"
@close="closeBirthPicker"
/>
<u-picker
:show="showChainPicker"
:columns="[chainOptions]"
:loading="merchantLoading"
:defaultIndex="chainPickerIndex"
keyName="text"
@confirm="onChainConfirm"
@cancel="closeChainPicker"
@close="closeChainPicker"
/>
<u-popup :show="showMerchantPicker" mode="bottom" round="24rpx" @close="closeMerchantPicker">
<view class="merchant-picker">
<view class="merchant-picker-head">
<view class="picker-action" @tap="closeMerchantPicker">取消</view>
<text class="picker-title">选择门店</text>
<view class="picker-action primary" @tap="confirmMerchantPicker">确定</view>
</view>
<view class="merchant-picker-search">
<input
v-model="merchantSearchKeyword"
class="merchant-search-input"
placeholder="搜索门店"
placeholder-class="merchant-search-placeholder"
/>
</view>
<view v-if="merchantLoading" class="merchant-picker-loading">
<u-loading-icon mode="circle"></u-loading-icon>
</view>
<picker-view
v-else
class="merchant-picker-view"
:value="merchantPickerValue"
:indicator-style="merchantIndicatorStyle"
@change="onMerchantPickerChange"
>
<picker-view-column>
<view
v-for="(item, idx) in merchantFilteredOptions"
:key="`merchant-${idx}`"
class="merchant-picker-item"
>
{{ item.text }}
</view>
</picker-view-column>
</picker-view>
</view>
</u-popup>
<u-popup :show="showTrackingProductPicker" mode="bottom" round="24rpx" @close="closeTrackingProductPicker">
<view class="tracking-product-picker">
<view class="merchant-picker-head">
<view class="picker-action" @tap="closeTrackingProductPicker">取消</view>
<text class="picker-title">选择药品</text>
<view class="picker-action primary" @tap="closeTrackingProductPicker">完成</view>
</view>
<view class="merchant-picker-search">
<input
v-model="trackingProductKeyword"
class="merchant-search-input"
placeholder="搜索药品名称/规格粒片/使用次数"
placeholder-class="merchant-search-placeholder"
/>
</view>
<view v-if="trackingProductLoading" class="merchant-picker-loading">
<u-loading-icon mode="circle"></u-loading-icon>
</view>
<scroll-view v-else class="tracking-product-scroll" scroll-y>
<view v-if="filteredTrackingProducts.length === 0" class="tracking-empty">暂无药品</view>
<view
v-for="(item, idx) in filteredTrackingProducts"
:key="idx"
class="tracking-product-option"
@tap="selectTrackingProduct(idx)"
>
<view class="tracking-product-name">{{ item.productName || '-' }}</view>
<view class="tracking-product-option-meta">规格粒/片:{{ item.dailyUsageCount || '-' }}</view>
<view class="tracking-product-option-meta">说明书每天使用/次:{{ item.dosagePerTime || '-' }}</view>
<view class="tracking-product-option-meta">每次几粒/片:{{ item.boxUsageDays || '-' }}</view>
</view>
</scroll-view>
</view>
</u-popup>
<u-datetime-picker
:show="showTrackingDatePicker"
:value="trackingDatePickerValue"
:min-date="minDate"
mode="date"
@confirm="onTrackingDateConfirm"
@cancel="closeTrackingDatePicker"
@close="closeTrackingDatePicker"
/>
<u-popup
:show="showReferencePopup"
mode="center"
round="16"
closeable
@close="closeReferencePopup"
>
<view class="reference-popup">
<view class="reference-title">{{ referencePopupTitle }}</view>
<scroll-view scroll-y class="reference-content">
<text class="reference-text">{{ referencePopupContent }}</text>
</scroll-view>
<view class="reference-confirm" @tap="closeReferencePopup">我知道了</view>
</view>
</u-popup>
</view>
</template>
<script>
import {chronicDiseaseCollectionAddReq} from "@/api/reimburse";
import dayjs from "dayjs";
import AppNavbar from "@/components/AppNavbar.vue";
import {merchantGroupByChainReq} from "@/api/shop";
import {chronicDiseaseProductListReq} from "@/api/product";
export default {
components: {AppNavbar},
data() {
return {
genderOptions: ['男', '女', '其他'],
anemiaOptions: ['否', '轻度', '中度', '重度'],
thalassemiaOptions: ['否', '轻型', '中间型', '重型'],
conditionOptions: [
{label: '高血压', value: 'highBloodPressure'},
{label: '高血糖', value: 'highBloodSugar'},
{label: '高血脂', value: 'hyperlipidemia'}
],
form: {
chainId: '',
chainName: '',
merchantId: '',
merchantName: '',
name: '',
gender: '',
phone: '',
birthday: '',
hemoglobinValue: '',
anemiaLevel: '否',
thalassemiaType: '否',
highBloodPressure: 0,
highBloodPressureValue: '',
systolicPressure: '',
diastolicPressure: '',
highBloodSugar: 0,
fastingBloodGlucose: '',
postprandial2hBloodGlucose: '',
hba1c: '',
hyperlipidemia: 0,
totalCholesterol: '',
triglyceride: '',
ldlC: '',
hdlC: '',
medicalHistory: '',
medicationHistory: ''
},
trackingList: [],
showGenderPicker: false,
showAnemiaPicker: false,
showThalassemiaPicker: false,
showBirthPicker: false,
showChainPicker: false,
showMerchantPicker: false,
merchantLoading: false,
merchantChainList: [],
merchantSearchKeyword: '',
merchantPickerValue: [0],
showTrackingProductPicker: false,
trackingProductLoading: false,
trackingProductList: [],
trackingProductKeyword: '',
trackingProductLoadedChain: '',
activeTrackingIndex: -1,
showTrackingDatePicker: false,
activeTrackingDateIndex: -1,
showReferencePopup: false,
referencePopupTitle: '',
referencePopupContent: '',
minDate: dayjs().subtract(100, 'year').valueOf(),
}
},
computed: {
birthPickerValue() {
return this.form.birthday || Date.now()
},
trackingDatePickerValue() {
const item = this.trackingList[this.activeTrackingDateIndex]
return item && item.purchaseTime ? dayjs(item.purchaseTime).valueOf() : Date.now()
},
chainOptions() {
const list = Array.isArray(this.merchantChainList) ? this.merchantChainList : []
return list.length ? list.map(item => ({id: item.id, text: item.text})) : [{id: '', text: '暂无连锁'}]
},
merchantOptions() {
// 门店明细挂在实例的 merchantStore 上,不进页面 data(见 setMerchantChains)
const chainList = Array.isArray(this.merchantChainList) ? this.merchantChainList : []
const hasChain = chainList.some(item => String(item.id) === String(this.form.chainId))
const merchants = hasChain ? this.getMerchantsByChainId(this.form.chainId) : []
return merchants.length ? merchants : [{id: '', text: '无门店'}]
},
merchantFilteredOptions() {
const list = Array.isArray(this.merchantOptions) ? this.merchantOptions : []
const keyword = (this.merchantSearchKeyword || '').trim()
if (!keyword) return list
const filtered = list.filter(item => (item.text || '').includes(keyword))
return filtered.length ? filtered : [{id: '', text: '无匹配门店'}]
},
chainPickerIndex() {
const chainList = Array.isArray(this.merchantChainList) ? this.merchantChainList : []
if (!chainList.length) return [0]
let chainIndex = chainList.findIndex(item => String(item.id) === String(this.form.chainId))
if (chainIndex < 0) chainIndex = 0
return [chainIndex]
},
merchantPickerIndex() {
const merchants = Array.isArray(this.merchantFilteredOptions) ? this.merchantFilteredOptions : []
let merchantIndex = merchants.findIndex(item => String(item.id) === String(this.form.merchantId))
if (merchantIndex < 0) merchantIndex = 0
return [merchantIndex]
},
merchantIndicatorStyle() {
return 'height: 64px;'
},
filteredTrackingProducts() {
const list = Array.isArray(this.trackingProductList) ? this.trackingProductList : []
const keyword = String(this.trackingProductKeyword || '').trim()
if (!keyword) return list
return list.filter(item => {
const text = [
item.productName,
item.goodsCode,
item.dailyUsageCount,
item.dosagePerTime,
item.boxUsageDays
].join(' ')
return text.includes(keyword)
})
}
},
watch: {
merchantSearchKeyword() {
this.merchantPickerValue = [0]
}
},
created() {
// 门店明细挂实例上而非 data:data 里的一切都会进页面 data 树并被 setData 序列化
this.merchantStore = []
},
onShow() {
this.fetchMerchantList()
},
methods: {
async fetchMerchantList() {
this.merchantLoading = true
try {
const {data} = await merchantGroupByChainReq({})
const list = this.unwrapList(data)
const chains = list
.map((item, index) => this.normalizeMerchantChain(item, index))
.filter(item => Array.isArray(item.merchants) && item.merchants.length > 0)
this.setMerchantChains(chains)
} catch (error) {
this.setMerchantChains([])
uni.showToast({
title: '门店列表获取失败',
icon: 'none'
})
} finally {
this.merchantLoading = false
}
},
// 门店明细(全量可达 8000+ 条)只放在实例上,不进页面 data。
// 一旦放进 data,页面 data 树会膨胀到 1MB+,此后每次 setData 都要重新
// diff/序列化整棵树,连只含一个字段的 setData 也会从 ~490ms 涨到 ~1000ms。
setMerchantChains(chains) {
const list = Array.isArray(chains) ? chains : []
this.merchantStore = list.map(chain => ({
id: chain.id,
text: chain.text,
merchants: Array.isArray(chain.merchants) ? chain.merchants : []
}))
this.merchantChainList = this.merchantStore.map(chain => ({
id: chain.id,
text: chain.text,
merchantCount: chain.merchants.length
}))
},
getMerchantsByChainId(chainId) {
const store = Array.isArray(this.merchantStore) ? this.merchantStore : []
const chain = store.find(item => String(item.id) === String(chainId))
return chain && Array.isArray(chain.merchants) ? chain.merchants : []
},
openChainPicker() {
this.showChainPicker = true
},
closeChainPicker() {
this.showChainPicker = false
},
onChainConfirm(e) {
const selected = e && e.value && e.value.length ? e.value[0] : null
const nextChainId = selected ? (selected.id ?? '') : ''
const nextChainName = selected ? (selected.text || '') : ''
const changed = String(this.form.chainId) !== String(nextChainId) || String(this.form.chainName) !== String(nextChainName)
this.form.chainId = nextChainId
this.form.chainName = nextChainName
if (changed) {
this.form.merchantId = ''
this.form.merchantName = ''
this.resetTrackingList()
this.fetchTrackingProducts()
}
this.showChainPicker = false
},
openMerchantPicker() {
if (!this.form.chainId) {
uni.showToast({
title: '请先选择连锁',
icon: 'none'
})
return
}
this.merchantSearchKeyword = ''
this.merchantPickerValue = [...this.merchantPickerIndex]
this.showMerchantPicker = true
},
closeMerchantPicker() {
this.showMerchantPicker = false
},
onMerchantPickerChange(e) {
const value = Array.isArray(e.detail && e.detail.value) ? e.detail.value : [0]
this.merchantPickerValue = value
},
confirmMerchantPicker() {
const selected = this.merchantFilteredOptions[Number(this.merchantPickerValue[0] || 0)] || null
if (!selected || selected.id === '' || selected.id === null || selected.id === undefined) {
this.showMerchantPicker = false
return
}
this.form.merchantName = selected.text || selected.merchantName || ''
this.form.merchantId = selected.id ?? selected.merchantId ?? ''
this.showMerchantPicker = false
},
openGenderPicker() {
this.showGenderPicker = true
},
closeGenderPicker() {
this.showGenderPicker = false
},
onGenderConfirm(e) {
this.form.gender = e.value && e.value.length ? e.value[0] : ''
this.closeGenderPicker()
},
openAnemiaPicker() {
this.showAnemiaPicker = true
},
closeAnemiaPicker() {
this.showAnemiaPicker = false
},
onAnemiaConfirm(e) {
this.form.anemiaLevel = e.value && e.value.length ? e.value[0] : '否'
this.closeAnemiaPicker()
},
openThalassemiaPicker() {
this.showThalassemiaPicker = true
},
closeThalassemiaPicker() {
this.showThalassemiaPicker = false
},
onThalassemiaConfirm(e) {
this.form.thalassemiaType = e.value && e.value.length ? e.value[0] : '否'
this.closeThalassemiaPicker()
},
openBirthPicker() {
this.showBirthPicker = true
},
closeBirthPicker() {
this.showBirthPicker = false
},
onBirthConfirm(e) {
this.form.birthday = dayjs(e.value).format('YYYY-MM-DD')
this.closeBirthPicker()
},
openReferencePopup(title, content) {
this.referencePopupTitle = title
this.referencePopupContent = content
this.showReferencePopup = true
},
closeReferencePopup() {
this.showReferencePopup = false
},
showHemoglobinReference() {
this.openReferencePopup(
'血红蛋白参考值',
'成年男性\n120g/L-160g/L\n\n成年女性\n110g/L-150g/L\n\n新生儿\n170g/L-200g/L\n\n妊娠期贫血程度表\n轻度:90g/L-109g/L\n中度:70g/L-89g/L\n重度:<70g/L'
)
},
showHighBloodPressureReference() {
this.openReferencePopup(
'高血压参考值',
'收缩压(高压)\n正常参考值:<120 mmHg\n临界/需要注意:120~139 mmHg\n诊断标准:≥140 mmHg\n\n舒张压(低压)\n正常参考值:<80 mmHg\n临界/需要注意:80~89 mmHg\n诊断标准:≥90 mmHg'
)
},
showHighBloodSugarReference() {
this.openReferencePopup(
'高血糖参考值',
'空腹血糖\n正常参考值:3.9~6.1 mmol/L\n临界/需要注意:6.1~7.0 mmol/L(空腹血糖受损)\n诊断标准:≥7.0 mmol/L\n\n餐后2小时血糖\n正常参考值:<7.8 mmol/L\n临界/需要注意:7.8~11.1 mmol/L(糖耐量异常)\n诊断标准:≥11.1 mmol/L\n\n糖化血红蛋白(HbA1c)\n正常参考值:<5.7%\n临界/需要注意:5.7%~6.4%(糖尿病前期)\n诊断标准:≥6.5%'
)
},
showHyperlipidemiaReference() {
this.openReferencePopup(
'高血脂参考值',
'总胆固醇(TC)\n正常:<5.2 mmol/L\n临界/需要注意:5.2~6.2 mmol/L\n诊断标准:≥6.2 mmol/L\n\n甘油三酯(TG)\n正常:<1.7 mmol/L\n临界/需要注意:1.7~2.3 mmol/L\n诊断标准:≥2.3 mmol/L\n\n低密度脂蛋白(LDL-C)\n正常:<3.4 mmol/L\n临界/需要注意:3.4~4.1 mmol/L\n诊断标准:≥4.1 mmol/L\n\n高密度脂蛋白(HDL-C)\n正常:男>1.0 mmol/L,女>1.3 mmol/L\n临界/需要注意:-\n诊断标准:<1.0 mmol/L(数值越低风险越高)'
)
},
setConditions(e) {
const selected = e.detail.value || []
this.form.highBloodPressure = selected.includes('highBloodPressure') ? 1 : 0
this.form.highBloodSugar = selected.includes('highBloodSugar') ? 1 : 0
this.form.hyperlipidemia = selected.includes('hyperlipidemia') ? 1 : 0
if (this.form.highBloodPressure !== 1) {
this.form.highBloodPressureValue = ''
this.form.systolicPressure = ''
this.form.diastolicPressure = ''
}
if (this.form.highBloodSugar !== 1) {
this.form.fastingBloodGlucose = ''
this.form.postprandial2hBloodGlucose = ''
this.form.hba1c = ''
}
if (this.form.hyperlipidemia !== 1) {
this.form.totalCholesterol = ''
this.form.triglyceride = ''
this.form.ldlC = ''
this.form.hdlC = ''
}
},
createTrackingItem() {
return {
_key: `tracking_${Date.now()}_${Math.random().toString(16).slice(2)}`,
productId: '',
goodsCode: '',
productName: '',
dailyUsageCount: '',
dosagePerTime: '',
boxUsageDays: '',
purchaseTime: '',
boxCount: '',
estimatedFinishTime: ''
}
},
addTrackingItem() {
this.trackingList.push(this.createTrackingItem())
},
removeTrackingItem(index) {
this.trackingList.splice(index, 1)
},
resetTrackingList() {
this.trackingList = []
this.trackingProductList = []
this.trackingProductKeyword = ''
this.trackingProductLoadedChain = ''
this.activeTrackingIndex = -1
this.activeTrackingDateIndex = -1
this.showTrackingProductPicker = false
this.showTrackingDatePicker = false
},
normalizeTrackingProduct(item) {
const product = item && item.product && typeof item.product === 'object' ? item.product : {}
const productId = item.productId || item.product_id || item.id || item.goodsId || item.goods_id || product.productId || product.id || ''
const goodsCode = item.goodsCode || item.goods_code || item.code || item.productCode || item.product_code || product.goodsCode || product.code || ''
const productName = item.productName || item.product_name || item.goodsName || item.goods_name || item.name || item.title || product.productName || product.goodsName || product.name || product.title || ''
const dailyUsageCount = item.dailyUsageCount || item.daily_usage_count || product.dailyUsageCount || product.daily_usage_count || ''
const dosagePerTime = item.dosagePerTime || item.dosage_per_time || product.dosagePerTime || product.dosage_per_time || ''
const boxUsageDays = item.boxUsageDays || item.box_usage_days || item.usageDays || item.usage_days || product.boxUsageDays || product.box_usage_days || product.usageDays || product.usage_days || ''
return {
productId,
goodsCode,
productName,
dailyUsageCount,
dosagePerTime,
boxUsageDays
}
},
uniqueTrackingProducts(list) {
const map = {}
;(Array.isArray(list) ? list : []).forEach(item => {
const key = item.productId || item.goodsCode || `${item.productName}_${item.dailyUsageCount}_${item.dosagePerTime}_${item.boxUsageDays}`
if (!key || map[key]) return
map[key] = item
})
return Object.keys(map).map(key => map[key])
},
async fetchTrackingProducts() {
const chain = String(this.form.chainName || '').trim()
if (!chain) {
this.trackingProductList = []
this.trackingProductLoadedChain = ''
return
}
this.trackingProductLoading = true
try {
const {data} = await chronicDiseaseProductListReq({chain})
if (chain !== String(this.form.chainName || '').trim()) return
const list = this.unwrapList(data)
.map(item => this.normalizeTrackingProduct(item))
.filter(item => !!item.productName)
this.trackingProductList = this.uniqueTrackingProducts(list)
this.trackingProductLoadedChain = chain
} catch (error) {
this.trackingProductList = []
this.trackingProductLoadedChain = ''
uni.showToast({
title: '药品列表获取失败',
icon: 'none'
})
} finally {
this.trackingProductLoading = false
}
},
async openTrackingProductPicker(index) {
if (!this.form.chainName) {
uni.showToast({
title: '请先选择连锁',
icon: 'none'
})
return
}
this.activeTrackingIndex = index
this.trackingProductKeyword = ''
this.showTrackingProductPicker = true
const chain = String(this.form.chainName || '').trim()
if (this.trackingProductLoadedChain !== chain || !this.trackingProductList.length) {
await this.fetchTrackingProducts()
}
},
closeTrackingProductPicker() {
this.showTrackingProductPicker = false
},
selectTrackingProduct(productIndex) {
const product = this.filteredTrackingProducts[productIndex]
const trackingIndex = this.activeTrackingIndex
if (!product || trackingIndex < 0 || !this.trackingList[trackingIndex]) {
this.closeTrackingProductPicker()
return
}
const next = Object.assign({}, this.trackingList[trackingIndex], {
productId: product.productId || '',
goodsCode: product.goodsCode || '',
productName: product.productName || '',
dailyUsageCount: product.dailyUsageCount || '',
dosagePerTime: product.dosagePerTime || '',
boxUsageDays: product.boxUsageDays || ''
})
next.estimatedFinishTime = this.calculateTrackingFinish(next)
this.$set(this.trackingList, trackingIndex, next)
this.closeTrackingProductPicker()
},
openTrackingDatePicker(index) {
this.activeTrackingDateIndex = index
this.showTrackingDatePicker = true
},
closeTrackingDatePicker() {
this.showTrackingDatePicker = false
},
onTrackingDateConfirm(e) {
const index = this.activeTrackingDateIndex
if (index > -1 && this.trackingList[index]) {
const next = Object.assign({}, this.trackingList[index], {
purchaseTime: dayjs(e.value).format('YYYY-MM-DD')
})
next.estimatedFinishTime = this.calculateTrackingFinish(next)
this.$set(this.trackingList, index, next)
}
this.closeTrackingDatePicker()
},
onTrackingBoxInput(index, e) {
if (!this.trackingList[index]) return
const value = e && e.detail ? e.detail.value : ''
const next = Object.assign({}, this.trackingList[index], {
boxCount: value
})
next.estimatedFinishTime = this.calculateTrackingFinish(next)
this.$set(this.trackingList, index, next)
},
parseNumber(value) {
if (typeof value === 'number') return value
const match = String(value || '').match(/\d+(\.\d+)?/)
return match ? Number(match[0]) : NaN
},
calculateTrackingFinish(item) {
const purchaseTime = item && item.purchaseTime ? item.purchaseTime : ''
const boxUsageDays = this.parseNumber(item && item.boxUsageDays)
const boxCount = this.parseNumber(item && item.boxCount)
if (!purchaseTime || !Number.isFinite(boxUsageDays) || boxUsageDays <= 0 || !Number.isFinite(boxCount) || boxCount <= 0) {
return ''
}
return dayjs(purchaseTime).add(boxUsageDays * boxCount, 'day').format('YYYY-MM-DD')
},
buildTrackingPayload() {
const result = []
for (let i = 0; i < this.trackingList.length; i += 1) {
const item = this.trackingList[i]
const productName = String(item.productName || '').trim()
const purchaseTime = String(item.purchaseTime || '').trim()
const boxCount = this.parseNumber(item.boxCount)
const boxUsageDays = this.parseNumber(item.boxUsageDays)
if (!productName || !purchaseTime || !Number.isFinite(boxCount) || boxCount <= 0) {
uni.showToast({
title: `请完善第${i + 1}条用药追踪`,
icon: 'none'
})
return null
}
if (!Number.isFinite(boxUsageDays) || boxUsageDays <= 0 || !item.estimatedFinishTime) {
uni.showToast({
title: `第${i + 1}条药品缺少服用天数`,
icon: 'none'
})
return null
}
result.push({
productId: item.productId || '',
productName,
goodsCode: item.goodsCode || '',
dailyUsageCount: item.dailyUsageCount || '',
dosagePerTime: item.dosagePerTime || '',
boxUsageDays,
purchaseTime,
boxCount,
estimatedFinishTime: item.estimatedFinishTime
})
}
return result
},
async submitForm() {
if (!this.form.chainId || !this.form.merchantId) {
uni.showToast({
title: '请选择连锁和门店',
icon: 'none'
})
return
}
if (!this.form.name || !this.form.phone) {
uni.showToast({
title: '请先填写姓名和电话',
icon: 'none'
})
return
}
const systolicPressure = String(this.form.systolicPressure || '').trim()
const diastolicPressure = String(this.form.diastolicPressure || '').trim()
if (this.form.highBloodPressure === 1 && (!systolicPressure || !diastolicPressure)) {
uni.showToast({
title: '请填写收缩压和舒张压',
icon: 'none'
})
return
}
const highBloodPressureValue = this.form.highBloodPressure === 1
? `${systolicPressure}/${diastolicPressure}`
: ''
const fastingBloodGlucose = String(this.form.fastingBloodGlucose || '').trim()
const postprandial2hBloodGlucose = String(this.form.postprandial2hBloodGlucose || '').trim()
const hba1c = String(this.form.hba1c || '').trim()
if (this.form.highBloodSugar === 1 && (!fastingBloodGlucose || !postprandial2hBloodGlucose || !hba1c)) {
uni.showToast({
title: '请填写完整高血糖指标',
icon: 'none'
})
return
}
const totalCholesterol = String(this.form.totalCholesterol || '').trim()
const triglyceride = String(this.form.triglyceride || '').trim()
const ldlC = String(this.form.ldlC || '').trim()
const hdlC = String(this.form.hdlC || '').trim()
if (this.form.hyperlipidemia === 1 && (!totalCholesterol || !triglyceride || !ldlC || !hdlC)) {
uni.showToast({
title: '请填写完整高血脂指标',
icon: 'none'
})
return
}
const hemoglobinValue = String(this.form.hemoglobinValue || '').trim()
const trackingPayload = this.buildTrackingPayload()
if (trackingPayload === null) return
await chronicDiseaseCollectionAddReq({
...this.form,
tracking: JSON.stringify(trackingPayload),
hemoglobinValue,
anemiaLevel: this.form.anemiaLevel || '否',
thalassemiaType: this.form.thalassemiaType || '否',
systolicPressure,
diastolicPressure,
highBloodPressureValue,
fastingBloodGlucose,
postprandial2hBloodGlucose,
hba1c,
totalCholesterol,
triglyceride,
ldlC,
hdlC,
chainId: this.form.chainId,
chainName: this.form.chainName,
merchantId: this.form.merchantId,
merchantName: this.form.merchantName
})
uni.showToast({
title: '慢病信息已提交',
icon: 'success'
})
setTimeout(() => {
uni.navigateBack()
}, 1500)
},
normalizeMerchantChain(item, index) {
const chainName = this.pickFirstText(item, ['chainName', 'chain', 'chainAbb', 'name', 'title']) || `连锁${index + 1}`
const rawChainId = item.chainId ?? item.chain_id ?? item.id ?? item.chainCode ?? item.chain_code ?? chainName
const chainId = rawChainId === undefined || rawChainId === null || String(rawChainId).trim() === ''
? `chain_${index}`
: rawChainId
const childCandidates = ['children', 'merchantList', 'merchants', 'shopList', 'list', 'records']
let rawChildren = []
for (let i = 0; i < childCandidates.length; i++) {
const list = item[childCandidates[i]]
if (Array.isArray(list) && list.length) {
rawChildren = list
break
}
}
let merchants = rawChildren
.map(child => this.normalizeMerchant(child))
.filter(child => child.id !== undefined && child.id !== null && String(child.id).trim() !== '')
if (!merchants.length) {
const selfMerchant = this.normalizeMerchant(item)
if (selfMerchant.id !== undefined && selfMerchant.id !== null && String(selfMerchant.id).trim() !== '') {
merchants = [selfMerchant]
}
}
return {
id: chainId,
text: chainName,
merchants: merchants.map(merchant => Object.assign({}, merchant, {chainId, chainName}))
}
},
normalizeMerchant(item) {
const id = item.id ?? item.merchantId ?? item.merchant_id
const text = item.name || item.merchantName || item.merchant_name || item.title || '未命名门店'
return {
id,
text
}
},
applyMerchant(chain, merchant) {
if (!merchant) return
this.form.chainName = (chain && chain.text) || merchant.chainName || ''
this.form.chainId = (chain && chain.id) ?? merchant.chainId ?? ''
this.form.merchantName = merchant.text || merchant.merchantName || ''
this.form.merchantId = merchant.id ?? merchant.merchantId ?? ''
},
pickFirstText(source, keys = []) {
if (!source || typeof source !== 'object') return ''
for (let i = 0; i < keys.length; i++) {
const value = source[keys[i]]
if (value !== undefined && value !== null && String(value).trim()) {
return String(value).trim()
}
}
return ''
},
unwrapList(data) {
if (Array.isArray(data)) return data
if (!data || typeof data !== 'object') return []
const keys = ['list', 'records', 'rows', 'items', 'data', 'result', 'chainList']
for (let i = 0; i < keys.length; i++) {
const value = data[keys[i]]
if (Array.isArray(value)) return value
if (value && typeof value === 'object') {
const nested = this.unwrapList(value)
if (nested.length) return nested
}
}
return [data]
}
}
}
</script>
<style lang="scss" scoped>
.chronic-page {
min-height: 100vh;
//padding: 0 20rpx 40rpx;
background: #f5f5f5;
}
.page-hero {
//padding: 30rpx 6rpx 18rpx;
background: url('https://yaodongxiao.oss-cn-guangzhou.aliyuncs.com/20260126/462bfa04cf0eff8698884c38b9d17519.png') no-repeat
top center / 100% auto;
}
.page-navbar {
padding: 0 6rpx;
}
.intro-card {
margin: 18rpx 20rpx 0;
padding: 20rpx 18rpx;
border-radius: 20rpx;
background: rgba(255, 255, 255, 0.9);
box-shadow: 0 10rpx 20rpx rgba(0, 0, 0, 0.08);
}
.card-title {
display: flex;
align-items: center;
gap: 12rpx;
font-size: 28rpx;
font-weight: 700;
color: #1f2937;
}
.title-bar {
width: 6rpx;
height: 30rpx;
border-radius: 999rpx;
background: #2fc6a7;
}
.intro-text {
margin-top: 12rpx;
font-size: 26rpx;
color: #6b7280;
line-height: 1.6;
}
.intro-meta {
margin-top: 8rpx;
font-size: 24rpx;
color: #6b7280;
}
.block {
margin: 18rpx 20rpx 0;
}
.form {
display: flex;
flex-direction: column;
gap: 10rpx;
}
.form-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10rpx 0;
border-bottom: 1rpx solid #edf1f7;
gap: 16rpx;
}
.form-row:last-child {
border-bottom: none;
}
.form-row > text {
flex: 1;
max-width: 62%;
color: var(--text-main);
line-height: 1.4;
}
.form-row input,
.form-row .picker-value {
flex: 1;
text-align: right;
}
.picker-value {
color: var(--text-main);
font-weight: 600;
}
.readonly-value {
flex: 1;
text-align: right;
color: #2563eb;
font-weight: 600;
}
.section-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16rpx;
}
.tracking-add-btn {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 104rpx;
height: 52rpx;
padding: 0 24rpx;
border-radius: 999rpx;
background: #e8f7f3;
color: #0f9f83;
font-size: 24rpx;
font-weight: 600;
}
.tracking-tip {
margin-top: 12rpx;
font-size: 24rpx;
line-height: 1.6;
color: #6b7280;
}
.tracking-empty {
padding: 30rpx 0;
text-align: center;
font-size: 26rpx;
color: #9ca3af;
}
.tracking-item {
margin-top: 18rpx;
padding: 16rpx;
border-radius: 16rpx;
background: #f8fbff;
border: 1rpx solid #e3eaf4;
}
.tracking-item-head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8rpx;
color: #1f2937;
font-size: 26rpx;
font-weight: 700;
}
.tracking-remove {
color: #ef4444;
font-size: 24rpx;
font-weight: 500;
}
.tracking-product-value {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.tracking-product-meta {
display: flex;
flex-direction: column;
gap: 6rpx;
margin-bottom: 4rpx;
padding: 10rpx 14rpx;
border-radius: 12rpx;
background: #ffffff;
color: #6b7280;
font-size: 23rpx;
line-height: 1.4;
}
.conditions {
display: flex;
gap: 16rpx;
flex-wrap: wrap;
}
.pressure-form {
margin-top: 12rpx;
}
.ref-row {
display: flex;
justify-content: flex-end;
margin-bottom: 8rpx;
}
.ref-btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 8rpx 22rpx;
border-radius: 999rpx;
border: 1rpx solid #2563eb;
color: #2563eb;
font-size: 24rpx;
line-height: 1;
}
.condition {
display: inline-flex;
align-items: center;
gap: 8rpx;
padding: 10rpx 14rpx;
border-radius: 999rpx;
background: #f4f7fb;
border: 1rpx solid #e3eaf4;
}
.condition-text {
color: var(--text-main);
font-size: 26rpx;
}
.textarea {
width: 100%;
min-height: 160rpx;
padding: 16rpx;
background: #f7fbff;
border-radius: 12rpx;
border: 1rpx solid #e3eaf4;
box-sizing: border-box;
}
.submit-btn {
width: 90%;
text-align: center;
margin: 20rpx auto;
}
.reference-popup {
width: 620rpx;
max-height: 900rpx;
padding: 28rpx 24rpx 22rpx;
box-sizing: border-box;
background: #ffffff;
}
.reference-title {
font-size: 32rpx;
font-weight: 700;
color: #1f2937;
text-align: center;
}
.reference-content {
max-height: 680rpx;
margin-top: 22rpx;
padding: 0 8rpx;
}
.reference-text {
white-space: pre-line;
font-size: 26rpx;
line-height: 1.7;
color: #374151;
}
.reference-confirm {
margin-top: 26rpx;
height: 76rpx;
border-radius: 999rpx;
background: #2563eb;
color: #ffffff;
font-size: 28rpx;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
}
.merchant-picker {
padding-bottom: env(safe-area-inset-bottom);
background: #ffffff;
}
.tracking-product-picker {
padding-bottom: env(safe-area-inset-bottom);
background: #ffffff;
}
.merchant-picker-head {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24rpx 28rpx 12rpx;
}
.picker-action {
font-size: 26rpx;
color: #6b7280;
}
.picker-action.primary {
color: #2563eb;
font-weight: 600;
}
.picker-title {
font-size: 30rpx;
font-weight: 700;
color: #1f2937;
}
.merchant-picker-search {
padding: 0 24rpx 16rpx;
}
.merchant-search-input {
height: 72rpx;
padding: 0 24rpx;
border-radius: 999rpx;
background: #f3f6fb;
font-size: 26rpx;
color: #1f2937;
}
.merchant-search-placeholder {
color: #9ca3af;
}
.merchant-picker-loading {
height: 420rpx;
display: flex;
align-items: center;
justify-content: center;
}
.merchant-picker-view {
height: 420rpx;
}
.merchant-picker-item {
height: 84rpx;
line-height: 84rpx;
text-align: center;
font-size: 28rpx;
color: #1f2937;
}
.tracking-product-scroll {
height: 620rpx;
padding: 0 24rpx 24rpx;
box-sizing: border-box;
}
.tracking-product-option {
padding: 20rpx 18rpx;
border-radius: 16rpx;
background: #f8fbff;
border: 1rpx solid #e3eaf4;
}
.tracking-product-option + .tracking-product-option {
margin-top: 16rpx;
}
.tracking-product-name {
color: #1f2937;
font-size: 28rpx;
font-weight: 700;
line-height: 1.45;
}
.tracking-product-option-meta {
margin-top: 8rpx;
color: #6b7280;
font-size: 24rpx;
line-height: 1.4;
}
</style>