2026-08-11 10:24:25 +08:00
|
|
|
|
<template>
|
2026-09-01 14:46:32 +08:00
|
|
|
|
<page-meta :page-style="projectPickerVisible || todoExecutorPickerVisible ? 'overflow: hidden;' : ''"></page-meta>
|
2026-08-14 16:49:54 +08:00
|
|
|
|
<full-page ref="pageRef" pageStyle="background:#f5f6f8" pageClass="benefit-page-shell">
|
2026-08-11 10:24:25 +08:00
|
|
|
|
<view class="benefit-page">
|
|
|
|
|
|
<view class="patient-card">
|
|
|
|
|
|
<view class="patient-card-accent"></view>
|
|
|
|
|
|
<view class="patient-card-main">
|
|
|
|
|
|
<view class="patient-card-top">
|
|
|
|
|
|
<view class="patient-name-wrap">
|
|
|
|
|
|
<text class="patient-name">{{ patientName || "当前患者" }}</text>
|
|
|
|
|
|
<text class="patient-badge">患者档案</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<text class="patient-status">会话已关联</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view class="patient-meta">
|
|
|
|
|
|
<view class="patient-meta-item">
|
|
|
|
|
|
<text class="patient-meta-label">团队</text>
|
|
|
|
|
|
<text class="patient-meta-value">{{ teamName || "未关联团队" }}</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
|
|
|
|
|
|
<view class="section-title">权益项目</view>
|
2026-08-14 16:49:54 +08:00
|
|
|
|
<view v-for="(entry, index) in entries" :id="`entry-${entry.key}`" :key="entry.key" class="entry-card">
|
2026-08-27 14:10:42 +08:00
|
|
|
|
<template v-if="entry.isPackage">
|
|
|
|
|
|
<view class="package-card-head">
|
|
|
|
|
|
<view>
|
|
|
|
|
|
<view class="field-label">套餐名称</view>
|
|
|
|
|
|
<view class="package-name">{{ entry.packageName }}</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view class="package-price">¥{{ Number(entry.price || 0).toFixed(2) }}</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view class="package-summary">
|
|
|
|
|
|
<text>组合形式:{{ entry.packageType === "free" ? `${entry.projects.length}选${entry.projectNumber}` : "固定组合" }}</text>
|
|
|
|
|
|
<text>必选项:{{ entry.requiredCount }}</text>
|
|
|
|
|
|
<text v-if="entry.packageType === 'free'">可选项:{{ entry.optionalSelectedCount }} / {{ entry.optionalCount }}</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view v-if="showBenefitAmount" class="field-row">
|
|
|
|
|
|
<view class="field-block">
|
|
|
|
|
|
<view class="field-label">数量</view>
|
|
|
|
|
|
<input class="number-input" type="number" v-model="entry.usageCount" @input="calculatePackageTotal(entry)" />
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view class="field-block">
|
|
|
|
|
|
<view class="field-label">折扣</view>
|
2026-09-03 17:58:02 +08:00
|
|
|
|
<picker class="discount-picker" :class="{ 'discount-disabled': !entry.enableDiscount }" mode="multiSelector" :range="getDiscountPickerRange(entry)" :value="getDiscountPickerValue(entry)" :disabled="!entry.enableDiscount" @change="changeDiscount($event, entry)">
|
2026-09-01 14:46:32 +08:00
|
|
|
|
<view class="number-input picker-value value-text">{{ Number(entry.discount).toFixed(1) }}</view>
|
|
|
|
|
|
</picker>
|
2026-08-27 14:10:42 +08:00
|
|
|
|
</view>
|
|
|
|
|
|
<view class="field-block">
|
|
|
|
|
|
<view class="field-label">总价</view>
|
|
|
|
|
|
<view class="readonly-input">¥{{ Number(entry.totalPrice || 0).toFixed(2) }}</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view v-else class="field-row">
|
|
|
|
|
|
<view class="field-block">
|
|
|
|
|
|
<view class="field-label">数量</view>
|
|
|
|
|
|
<input class="number-input" type="number" v-model="entry.usageCount" @input="calculatePackageTotal(entry)" />
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
2026-09-01 15:42:31 +08:00
|
|
|
|
<view v-for="projectEntry in entry.projects" :key="projectEntry.key" class="package-project-card" :class="{ 'required-project-card': projectEntry.required }">
|
2026-08-27 14:10:42 +08:00
|
|
|
|
<view class="package-project-title">
|
2026-09-01 15:42:31 +08:00
|
|
|
|
<view v-if="entry.packageType === 'free' && projectEntry.required" class="required-checkbox">
|
|
|
|
|
|
<checkbox
|
|
|
|
|
|
:checked="projectEntry.selected"
|
|
|
|
|
|
disabled
|
|
|
|
|
|
color="#0877f1"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</view>
|
2026-08-27 14:10:42 +08:00
|
|
|
|
<checkbox
|
2026-09-01 15:42:31 +08:00
|
|
|
|
v-else-if="entry.packageType === 'free'"
|
2026-08-27 14:10:42 +08:00
|
|
|
|
:checked="projectEntry.selected"
|
2026-09-01 15:42:31 +08:00
|
|
|
|
:disabled="!projectEntry.selected && isPackageSelectionFull(entry)"
|
2026-08-27 14:10:42 +08:00
|
|
|
|
color="#0877f1"
|
|
|
|
|
|
@click="togglePackageProject(entry, projectEntry)"
|
|
|
|
|
|
/>
|
2026-09-01 15:42:31 +08:00
|
|
|
|
<text v-if="projectEntry.required" class="required-badge">必选</text>
|
2026-08-27 14:10:42 +08:00
|
|
|
|
<text>{{ projectEntry.project.projectName }}</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view class="package-project-detail">¥{{ Number(projectEntry.price || 0).toFixed(2) }} × {{ projectEntry.usageCount }},折扣 {{ Number(projectEntry.discount || 0).toFixed(1) }}</view>
|
|
|
|
|
|
<view v-if="projectEntry.selected" class="field-group">
|
|
|
|
|
|
<view class="field-label">治疗科室</view>
|
|
|
|
|
|
<picker :range="getProjectDepts(projectEntry)" range-key="deptName" :disabled="!getProjectDepts(projectEntry).length" @change="changeDept($event, projectEntry)">
|
2026-09-01 15:42:31 +08:00
|
|
|
|
<view class="project-input" :class="{ 'required-select': !projectEntry.dept }"><text :class="projectEntry.dept ? 'value-text' : 'placeholder-text'">{{ projectEntry.dept?.deptName || "请选择治疗科室" }}</text><uni-icons type="right" size="18" color="#999" /></view>
|
2026-08-27 14:10:42 +08:00
|
|
|
|
</picker>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view v-if="projectEntry.selected" class="field-group">
|
|
|
|
|
|
<view class="field-label">计划治疗医生</view>
|
|
|
|
|
|
<picker :range="getTreatmentDoctors(projectEntry)" range-key="anotherName" :disabled="!projectEntry.dept?._id || !getTreatmentDoctors(projectEntry).length" @change="changeTreatmentDoctor($event, projectEntry)">
|
|
|
|
|
|
<view class="project-input"><text :class="projectEntry.treatmentDoctorUserId ? 'value-text' : 'placeholder-text'">{{ projectEntry.treatmentDoctorName || "选择治疗医生" }}</text><uni-icons type="right" size="18" color="#999" /></view>
|
|
|
|
|
|
</picker>
|
|
|
|
|
|
</view>
|
2026-09-01 17:42:46 +08:00
|
|
|
|
<view v-if="projectEntry.selected && Number(projectEntry.validDays || 0) > 0" class="field-group">
|
2026-08-27 14:10:42 +08:00
|
|
|
|
<view class="field-label">有效期</view>
|
2026-09-01 17:42:46 +08:00
|
|
|
|
<view class="valid-time-wrap">
|
|
|
|
|
|
<view class="valid-time-picker disabled-picker">
|
|
|
|
|
|
<picker mode="multiSelector" :range="validTimeRange" :value="validTimeIndex"
|
|
|
|
|
|
disabled
|
|
|
|
|
|
@columnchange="onValidTimeColumnChange"
|
|
|
|
|
|
@change="changeValidTime($event, projectEntry)">
|
|
|
|
|
|
<view class="picker-value" :class="projectEntry.validTime ? 'value-text' : 'placeholder-text'">
|
|
|
|
|
|
{{ projectEntry.validTime || "选择日期/长期" }}
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</picker>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view class="valid-time-locked-tip">套餐有效期已锁定</view>
|
|
|
|
|
|
</view>
|
2026-08-27 14:10:42 +08:00
|
|
|
|
</view>
|
2026-08-27 14:47:13 +08:00
|
|
|
|
<view v-if="projectEntry.selected" class="todo-switch-row">
|
|
|
|
|
|
<text>生成待办</text>
|
|
|
|
|
|
<switch :checked="projectEntry.todoEnabled" color="#0877f1" @change="toggleTodo($event, projectEntry)" />
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view v-if="projectEntry.selected && projectEntry.todoEnabled" class="todo-config-card">
|
|
|
|
|
|
<input v-model="projectEntry.todoConfig.taskContent" class="todo-input" placeholder="任务内容" />
|
|
|
|
|
|
<picker :range="todoTypeOptions" range-key="label" @change="changeTodoType($event, projectEntry)"><view class="project-input"><text>类型:{{ getTodoTypeLabel(projectEntry.todoConfig.eventType) }}</text><uni-icons type="right" size="16" color="#999" /></view></picker>
|
|
|
|
|
|
<view class="field-row"><view class="field-block"><view class="field-label">回访日期</view><view class="valid-time-picker" @tap="prepareTodoDatePicker(projectEntry, 'planDate')"><picker mode="multiSelector" :range="validTimeRange" :value="validTimeIndex" @columnchange="onValidTimeColumnChange" @change="changeTodoDate($event)"><view class="picker-value value-text">{{ projectEntry.todoConfig.planDate }}</view></picker></view></view><view class="field-block"><view class="field-label">截止日期</view><view class="valid-time-picker" @tap="prepareTodoDatePicker(projectEntry, 'expireDate')"><picker mode="multiSelector" :range="validTimeRange" :value="validTimeIndex" @columnchange="onValidTimeColumnChange" @change="changeTodoDate($event)"><view class="picker-value value-text">{{ projectEntry.todoConfig.expireDate }}</view></picker></view></view></view>
|
2026-09-01 15:42:31 +08:00
|
|
|
|
<view class="project-input" :class="{ 'required-select': !projectEntry.todoConfig.executorUserId }" @tap="openTodoExecutorPicker(projectEntry)"><text :class="projectEntry.todoConfig.executorUserId ? 'value-text' : 'placeholder-text'">{{ projectEntry.todoConfig.executorName || '请选择团队和处理人' }}</text><uni-icons type="right" size="16" color="#999" /></view>
|
2026-08-27 14:47:13 +08:00
|
|
|
|
<view class="todo-deduct-row"><text>完成关联核销权益</text><switch :checked="projectEntry.todoConfig.deductBenefit" color="#0877f1" @change="projectEntry.todoConfig.deductBenefit = $event.detail.value" /></view>
|
|
|
|
|
|
</view>
|
2026-08-27 14:10:42 +08:00
|
|
|
|
</view>
|
|
|
|
|
|
<view class="remove-entry" @click="removeEntry(index)">删除套餐</view>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<template v-else>
|
2026-08-11 10:24:25 +08:00
|
|
|
|
<view class="field-label">项目名称</view>
|
2026-09-01 15:42:31 +08:00
|
|
|
|
<view class="project-input required-select" @click="openProjectPicker(index)">
|
2026-08-11 10:24:25 +08:00
|
|
|
|
<text :class="entry.project ? 'value-text' : 'placeholder-text'">
|
|
|
|
|
|
{{ entry.project?.projectName || "请选择权益项目" }}
|
|
|
|
|
|
</text>
|
|
|
|
|
|
<uni-icons type="right" size="18" color="#999" />
|
|
|
|
|
|
</view>
|
|
|
|
|
|
|
2026-08-11 17:28:46 +08:00
|
|
|
|
<view v-if="showBenefitAmount" class="field-row">
|
|
|
|
|
|
<view class="field-block">
|
2026-08-14 16:49:54 +08:00
|
|
|
|
<view class="field-label">单价</view>
|
|
|
|
|
|
<input class="number-input" type="digit" v-model="entry.price" @input="calculateDiscount(entry)" />
|
2026-08-11 17:28:46 +08:00
|
|
|
|
</view>
|
2026-08-14 16:49:54 +08:00
|
|
|
|
<view class="field-block">
|
|
|
|
|
|
<view class="field-label">数量</view>
|
|
|
|
|
|
<input class="number-input" type="number" v-model="entry.usageCount" @input="calculateTotalPrice(entry)" />
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view v-if="showBenefitAmount" class="field-row">
|
2026-08-11 17:28:46 +08:00
|
|
|
|
<view class="field-block">
|
|
|
|
|
|
<view class="field-label">折扣</view>
|
2026-08-14 16:49:54 +08:00
|
|
|
|
<view class="discount-input-wrap">
|
2026-09-03 17:58:02 +08:00
|
|
|
|
<picker class="discount-picker" :class="{ 'discount-disabled': entry.isFree || !entry.isDiscount }" mode="multiSelector" :range="getDiscountPickerRange(entry)" :value="getDiscountPickerValue(entry)" :disabled="entry.isFree || !entry.isDiscount" @change="changeDiscount($event, entry)">
|
2026-09-01 14:46:32 +08:00
|
|
|
|
<view class="number-input picker-value value-text">{{ Number(entry.discount).toFixed(1) }}</view>
|
|
|
|
|
|
</picker>
|
|
|
|
|
|
<checkbox-group v-if="entry.isGift" class="free-check" @change="handleFreeChange($event, entry)">
|
2026-08-14 16:49:54 +08:00
|
|
|
|
<label class="free-check-label">
|
|
|
|
|
|
<checkbox value="free" :checked="entry.isFree" color="#0877f1" />
|
|
|
|
|
|
<text>赠送</text>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
</checkbox-group>
|
|
|
|
|
|
</view>
|
2026-08-11 17:28:46 +08:00
|
|
|
|
</view>
|
2026-08-11 10:24:25 +08:00
|
|
|
|
<view class="field-block">
|
2026-08-14 16:49:54 +08:00
|
|
|
|
<view class="field-label">总价</view>
|
2026-09-01 14:46:32 +08:00
|
|
|
|
<input class="number-input total-input" type="digit" v-model="entry.totalPrice" :disabled="entry.isFree || !entry.isDiscount" @input="calculateDiscount(entry)" />
|
2026-08-11 10:24:25 +08:00
|
|
|
|
</view>
|
2026-08-11 17:28:46 +08:00
|
|
|
|
</view>
|
|
|
|
|
|
<view v-else class="field-row">
|
|
|
|
|
|
<view class="field-block">
|
|
|
|
|
|
<view class="field-label">数量</view>
|
|
|
|
|
|
<input class="number-input" type="number" v-model="entry.usageCount" />
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view class="field-block">
|
2026-08-11 10:24:25 +08:00
|
|
|
|
<view class="field-label">有效期</view>
|
2026-08-14 16:49:54 +08:00
|
|
|
|
<view class="valid-time-wrap">
|
|
|
|
|
|
<view class="valid-time-picker" @tap="prepareValidTimePicker(entry)">
|
|
|
|
|
|
<picker
|
|
|
|
|
|
mode="multiSelector"
|
|
|
|
|
|
:range="validTimeRange"
|
|
|
|
|
|
:value="validTimeIndex"
|
|
|
|
|
|
@columnchange="onValidTimeColumnChange"
|
|
|
|
|
|
@change="changeValidTime($event, entry)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<view class="picker-value" :class="entry.validTime ? 'value-text' : 'placeholder-text'">
|
|
|
|
|
|
{{ entry.validTime || "选择日期/长期" }}
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</picker>
|
2026-08-11 10:24:25 +08:00
|
|
|
|
</view>
|
2026-08-14 16:49:54 +08:00
|
|
|
|
<view class="unlimited-button" @click="clearValidTime(entry)">长期</view>
|
|
|
|
|
|
</view>
|
2026-08-11 10:24:25 +08:00
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
2026-08-11 17:28:46 +08:00
|
|
|
|
<view v-if="showBenefitAmount" class="field-row">
|
2026-08-14 16:49:54 +08:00
|
|
|
|
<view class="field-block field-block-full">
|
2026-08-11 17:28:46 +08:00
|
|
|
|
<view class="field-label">有效期</view>
|
2026-08-14 16:49:54 +08:00
|
|
|
|
<view class="valid-time-wrap">
|
|
|
|
|
|
<view class="valid-time-picker" @tap="prepareValidTimePicker(entry)">
|
|
|
|
|
|
<picker
|
|
|
|
|
|
mode="multiSelector"
|
|
|
|
|
|
:range="validTimeRange"
|
|
|
|
|
|
:value="validTimeIndex"
|
|
|
|
|
|
@columnchange="onValidTimeColumnChange"
|
|
|
|
|
|
@change="changeValidTime($event, entry)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<view class="picker-value" :class="entry.validTime ? 'value-text' : 'placeholder-text'">
|
|
|
|
|
|
{{ entry.validTime || "选择日期/长期" }}
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</picker>
|
2026-08-11 17:28:46 +08:00
|
|
|
|
</view>
|
2026-08-14 16:49:54 +08:00
|
|
|
|
<view class="unlimited-button" @click="clearValidTime(entry)">长期</view>
|
|
|
|
|
|
</view>
|
2026-08-11 10:24:25 +08:00
|
|
|
|
</view>
|
2026-08-11 17:28:46 +08:00
|
|
|
|
</view>
|
|
|
|
|
|
<view class="field-group">
|
|
|
|
|
|
<view class="field-label">治疗科室</view>
|
|
|
|
|
|
<picker
|
|
|
|
|
|
:range="getProjectDepts(entry)"
|
|
|
|
|
|
range-key="deptName"
|
|
|
|
|
|
:disabled="!entry.project?._id || !getProjectDepts(entry).length"
|
|
|
|
|
|
@change="changeDept($event, entry)"
|
|
|
|
|
|
>
|
2026-09-01 15:42:31 +08:00
|
|
|
|
<view class="project-input" :class="{ 'required-select': !entry.dept }">
|
2026-08-11 17:28:46 +08:00
|
|
|
|
<text :class="entry.dept ? 'value-text' : 'placeholder-text'">
|
|
|
|
|
|
{{ entry.dept?.deptName || (entry.project ? "该项目未配置治疗科室" : "请先选择权益项目") }}
|
|
|
|
|
|
</text>
|
|
|
|
|
|
<uni-icons type="right" size="18" color="#999" />
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</picker>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view class="field-group">
|
|
|
|
|
|
<view class="field-label">计划治疗医生</view>
|
|
|
|
|
|
<picker
|
|
|
|
|
|
:range="getTreatmentDoctors(entry)"
|
|
|
|
|
|
range-key="anotherName"
|
|
|
|
|
|
:disabled="!entry.dept?._id || !getTreatmentDoctors(entry).length"
|
|
|
|
|
|
@change="changeTreatmentDoctor($event, entry)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<view class="project-input">
|
|
|
|
|
|
<text :class="entry.treatmentDoctorUserId ? 'value-text' : 'placeholder-text'">
|
|
|
|
|
|
{{ entry.treatmentDoctorName || (entry.dept ? "选择治疗医生" : "请先选择治疗科室") }}
|
|
|
|
|
|
</text>
|
|
|
|
|
|
<uni-icons type="right" size="18" color="#999" />
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</picker>
|
|
|
|
|
|
</view>
|
2026-09-01 14:46:32 +08:00
|
|
|
|
<view v-if="entry.project?._id" class="todo-switch-row">
|
|
|
|
|
|
<text>生成待办</text>
|
|
|
|
|
|
<switch :checked="entry.todoEnabled" color="#0877f1" @change="toggleTodo($event, entry)" />
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view v-if="entry.project?._id && entry.todoEnabled" class="todo-config-card">
|
|
|
|
|
|
<input v-model="entry.todoConfig.taskContent" class="todo-input" placeholder="任务内容" />
|
|
|
|
|
|
<picker :range="todoTypeOptions" range-key="label" @change="changeTodoType($event, entry)"><view class="project-input"><text>类型:{{ getTodoTypeLabel(entry.todoConfig.eventType) }}</text><uni-icons type="right" size="16" color="#999" /></view></picker>
|
|
|
|
|
|
<view class="field-row"><view class="field-block"><view class="field-label">回访日期</view><view class="valid-time-picker" @tap="prepareTodoDatePicker(entry, 'planDate')"><picker mode="multiSelector" :range="validTimeRange" :value="validTimeIndex" @columnchange="onValidTimeColumnChange" @change="changeTodoDate($event)"><view class="picker-value value-text">{{ entry.todoConfig.planDate }}</view></picker></view></view><view class="field-block"><view class="field-label">截止日期</view><view class="valid-time-picker" @tap="prepareTodoDatePicker(entry, 'expireDate')"><picker mode="multiSelector" :range="validTimeRange" :value="validTimeIndex" @columnchange="onValidTimeColumnChange" @change="changeTodoDate($event)"><view class="picker-value value-text">{{ entry.todoConfig.expireDate }}</view></picker></view></view></view>
|
2026-09-01 15:42:31 +08:00
|
|
|
|
<view class="project-input" :class="{ 'required-select': !entry.todoConfig.executorUserId }" @tap="openTodoExecutorPicker(entry)"><text :class="entry.todoConfig.executorUserId ? 'value-text' : 'placeholder-text'">{{ entry.todoConfig.executorName || '请选择团队和处理人' }}</text><uni-icons type="right" size="16" color="#999" /></view>
|
2026-09-01 14:46:32 +08:00
|
|
|
|
<view class="todo-deduct-row"><text>完成关联核销权益</text><switch :checked="entry.todoConfig.deductBenefit" color="#0877f1" @change="entry.todoConfig.deductBenefit = $event.detail.value" /></view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
|
2026-08-11 10:24:25 +08:00
|
|
|
|
<view v-if="entries.length > 1" class="remove-entry" @click="removeEntry(index)">删除此项目</view>
|
2026-08-27 14:10:42 +08:00
|
|
|
|
</template>
|
2026-08-11 10:24:25 +08:00
|
|
|
|
</view>
|
|
|
|
|
|
|
|
|
|
|
|
<view class="add-entry" @click="addEntry">
|
|
|
|
|
|
<uni-icons type="plusempty" size="18" color="#0877f1" />
|
|
|
|
|
|
<text>添加权益项目</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
|
2026-08-27 14:47:13 +08:00
|
|
|
|
<view v-if="todoExecutorPickerVisible" class="picker-mask" @click="closeTodoExecutorPicker">
|
2026-09-01 14:46:32 +08:00
|
|
|
|
<view class="todo-executor-picker" :style="{ '--keyboard-height': `${keyboardHeight}px` }" @click.stop>
|
2026-08-27 14:47:13 +08:00
|
|
|
|
<view class="picker-header"><text class="picker-title">请选择团队和处理人</text><uni-icons type="closeempty" size="22" color="#666" @click="closeTodoExecutorPicker" /></view>
|
2026-09-01 14:46:32 +08:00
|
|
|
|
<view class="todo-executor-search">
|
|
|
|
|
|
<uni-icons type="search" size="18" color="#999" />
|
|
|
|
|
|
<input v-model="todoExecutorKeyword" class="todo-executor-search-input" :adjust-position="false" placeholder="搜索团队或处理人" />
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<scroll-view
|
|
|
|
|
|
scroll-y
|
|
|
|
|
|
class="todo-executor-list"
|
|
|
|
|
|
:refresher-enabled="true"
|
|
|
|
|
|
:refresher-triggered="todoExecutorRefreshing"
|
|
|
|
|
|
@refresherrefresh="refreshTodoExecutors"
|
|
|
|
|
|
>
|
|
|
|
|
|
<view v-for="team in filteredTodoTeams" :key="team.teamId" class="todo-team-group">
|
2026-08-27 14:47:13 +08:00
|
|
|
|
<view class="todo-team-title">{{ team.name }}</view>
|
|
|
|
|
|
<view v-for="member in getTodoExecutorsForTeam(team)" :key="member.userid" class="todo-executor-item" @tap="selectTodoExecutor(member, team)">
|
|
|
|
|
|
<text>{{ member.name }}</text><text v-if="isSelectedTodoExecutor(member, team)" class="todo-selected">已选择</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view v-if="!getTodoExecutorsForTeam(team).length" class="todo-empty">暂无团队成员</view>
|
|
|
|
|
|
</view>
|
2026-09-01 14:46:32 +08:00
|
|
|
|
<view v-if="!filteredTodoTeams.length" class="todo-empty">暂无匹配的团队或处理人</view>
|
2026-08-27 14:47:13 +08:00
|
|
|
|
</scroll-view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
|
2026-08-11 10:24:25 +08:00
|
|
|
|
<view v-if="projectPickerVisible" class="picker-mask" @click="closeProjectPicker">
|
2026-09-01 14:46:32 +08:00
|
|
|
|
<view class="project-picker" :style="{ '--keyboard-height': `${keyboardHeight}px` }" @click.stop>
|
2026-08-11 10:24:25 +08:00
|
|
|
|
<view class="picker-header">
|
2026-08-27 14:10:42 +08:00
|
|
|
|
<text class="picker-title">选择权益项目或套餐</text>
|
2026-08-11 10:24:25 +08:00
|
|
|
|
<uni-icons type="closeempty" size="22" color="#666" @click="closeProjectPicker" />
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view class="search-box">
|
|
|
|
|
|
<uni-icons type="search" size="18" color="#999" />
|
2026-09-01 14:46:32 +08:00
|
|
|
|
<input v-model="projectKeyword" class="search-input" :adjust-position="false" placeholder="输入项目或套餐名称搜索" @input="searchProjects" />
|
2026-08-11 10:24:25 +08:00
|
|
|
|
</view>
|
2026-08-14 16:49:54 +08:00
|
|
|
|
<scroll-view scroll-y class="project-list" @scrolltolower="loadMoreProjects">
|
2026-08-27 14:10:42 +08:00
|
|
|
|
<view v-if="projectList.length" class="picker-group-title">单项目</view>
|
|
|
|
|
|
<view v-for="project in projectList" :key="`project_${project._id}`" class="project-item"
|
2026-08-11 17:28:46 +08:00
|
|
|
|
@click="selectProject(project)">
|
2026-08-14 16:49:54 +08:00
|
|
|
|
<view class="project-info">
|
|
|
|
|
|
<view class="project-name">{{ project.projectName }}</view>
|
|
|
|
|
|
<view class="project-depts">{{ project.deptNames || "未配置治疗科室" }}</view>
|
|
|
|
|
|
</view>
|
2026-08-11 17:28:46 +08:00
|
|
|
|
<view v-if="showBenefitAmount" class="project-price">¥{{ Number(project.price || 0).toFixed(2) }}</view>
|
2026-08-11 10:24:25 +08:00
|
|
|
|
</view>
|
2026-08-27 14:10:42 +08:00
|
|
|
|
<view v-if="packageList.length" class="picker-group-title">套餐</view>
|
|
|
|
|
|
<view v-for="packageItem in packageList" :key="`package_${packageItem._id}`" class="project-item" @click="selectPackage(packageItem)">
|
|
|
|
|
|
<view class="project-info"><view class="project-name">{{ packageItem.name }}</view><view class="project-depts">{{ packageItem.packageType === "free" ? `自由组合:${packageItem.projects.length}选${packageItem.projectNumber}` : "固定组合" }} · {{ (packageItem.projects || []).map((item) => item.projectName).join("、") }}</view></view>
|
|
|
|
|
|
<view v-if="showBenefitAmount" class="project-price">¥{{ Number(packageItem.packagePrice || 0).toFixed(2) }}</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view v-if="!projectLoading && !projectList.length && !packageList.length" class="empty-text">暂无可用权益项目或套餐</view>
|
2026-08-11 10:24:25 +08:00
|
|
|
|
<view v-if="projectLoading" class="empty-text">加载中...</view>
|
2026-08-14 16:49:54 +08:00
|
|
|
|
<view v-else-if="projectList.length && !projectHasMore" class="empty-text">已加载全部权益项目</view>
|
2026-08-11 10:24:25 +08:00
|
|
|
|
</scroll-view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
|
|
|
|
|
|
<template #footer>
|
|
|
|
|
|
<view class="footer-bar">
|
2026-09-03 17:58:02 +08:00
|
|
|
|
<view v-if="showBenefitAmount" class="footer-summary">
|
|
|
|
|
|
<view class="total-summary" @tap="toggleBenefitDetail">
|
|
|
|
|
|
<text class="summary-label">总金额</text>
|
|
|
|
|
|
<text class="summary-total-value">¥{{ benefitSummary.totalAmount.toFixed(2) }}</text>
|
|
|
|
|
|
<uni-icons :type="benefitDetailVisible ? 'top' : 'bottom'" size="16" color="#6b7280" />
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<text class="detail-trigger" @tap="toggleBenefitDetail">明细</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view
|
|
|
|
|
|
v-if="showBenefitAmount"
|
|
|
|
|
|
class="benefit-detail-panel"
|
|
|
|
|
|
:class="{ 'benefit-detail-panel-visible': benefitDetailVisible }"
|
|
|
|
|
|
>
|
|
|
|
|
|
<view class="detail-header">
|
|
|
|
|
|
<text class="detail-project-name">权益项目</text>
|
|
|
|
|
|
<text class="detail-quantity">数量</text>
|
|
|
|
|
|
<text class="detail-discount">折扣</text>
|
|
|
|
|
|
<text class="detail-amount">最终金额</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view v-for="item in benefitDetailItems" :key="item.key" class="detail-row">
|
|
|
|
|
|
<text class="detail-project-name detail-project-name-value">{{ item.name }}</text>
|
|
|
|
|
|
<text class="detail-quantity">{{ item.quantity }}</text>
|
|
|
|
|
|
<text class="detail-discount">{{ item.isFree ? "赠送" : `${item.discount.toFixed(1)}折` }}</text>
|
|
|
|
|
|
<text class="detail-amount">¥{{ item.totalAmount.toFixed(2) }}</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view v-if="!benefitDetailItems.length" class="detail-empty">暂无已选择权益</view>
|
|
|
|
|
|
<view class="detail-row detail-row-total">
|
|
|
|
|
|
<text>合计</text>
|
|
|
|
|
|
<text class="detail-amount">¥{{ benefitSummary.totalAmount.toFixed(2) }}</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view class="footer-actions">
|
|
|
|
|
|
<button class="submit-button" :loading="saving" @click="submitEntries">生成权益</button>
|
|
|
|
|
|
</view>
|
2026-08-11 10:24:25 +08:00
|
|
|
|
</view>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</full-page>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup>
|
2026-09-01 14:46:32 +08:00
|
|
|
|
import { computed, nextTick, onUnmounted, ref } from "vue";
|
2026-08-11 10:24:25 +08:00
|
|
|
|
import { onLoad } from "@dcloudio/uni-app";
|
|
|
|
|
|
import api from "@/utils/api.js";
|
|
|
|
|
|
import useAccountStore from "@/store/account.js";
|
|
|
|
|
|
import fullPage from "@/components/full-page.vue";
|
2026-08-27 14:47:13 +08:00
|
|
|
|
import { TODO_EVENT_TYPE_LABELS } from "@/utils/todo-const.js";
|
2026-08-11 10:24:25 +08:00
|
|
|
|
|
2026-08-14 16:49:54 +08:00
|
|
|
|
const PENDING_FOLLOWUP_SEND_STORAGE_KEY = "ykt_followup_pending_send";
|
|
|
|
|
|
|
2026-08-11 10:24:25 +08:00
|
|
|
|
const accountStore = useAccountStore();
|
2026-08-14 16:49:54 +08:00
|
|
|
|
const pageRef = ref(null);
|
|
|
|
|
|
const groupId = ref("");
|
2026-08-11 10:24:25 +08:00
|
|
|
|
const patientId = ref("");
|
|
|
|
|
|
const patientName = ref("");
|
|
|
|
|
|
const teamId = ref("");
|
|
|
|
|
|
const teamName = ref("");
|
|
|
|
|
|
const corpId = ref("");
|
2026-08-11 17:28:46 +08:00
|
|
|
|
const showBenefitAmount = ref(true);
|
|
|
|
|
|
const staffList = ref([]);
|
2026-08-11 10:24:25 +08:00
|
|
|
|
const entries = ref([]);
|
|
|
|
|
|
const saving = ref(false);
|
|
|
|
|
|
const projectPickerVisible = ref(false);
|
|
|
|
|
|
const projectPickerIndex = ref(-1);
|
2026-09-01 14:46:32 +08:00
|
|
|
|
const keyboardHeight = ref(0);
|
2026-08-11 10:24:25 +08:00
|
|
|
|
const projectKeyword = ref("");
|
|
|
|
|
|
const projectList = ref([]);
|
2026-08-27 14:10:42 +08:00
|
|
|
|
const packageList = ref([]);
|
2026-08-11 10:24:25 +08:00
|
|
|
|
const projectLoading = ref(false);
|
2026-08-14 16:49:54 +08:00
|
|
|
|
const projectHasMore = ref(true);
|
2026-08-27 14:10:42 +08:00
|
|
|
|
const packageHasMore = ref(true);
|
2026-08-14 16:49:54 +08:00
|
|
|
|
const projectPage = ref(1);
|
|
|
|
|
|
const projectPageSize = 30;
|
|
|
|
|
|
const VALID_YEAR_SPAN = 50;
|
|
|
|
|
|
const validTimeRange = ref([[], [], []]);
|
|
|
|
|
|
const validTimeIndex = ref([0, 0, 0]);
|
2026-08-27 14:47:13 +08:00
|
|
|
|
const todoDateTarget = ref(null);
|
|
|
|
|
|
const teamMembers = ref([]);
|
|
|
|
|
|
const todoTeams = ref([]);
|
|
|
|
|
|
const todoExecutorPickerVisible = ref(false);
|
|
|
|
|
|
const todoExecutorTarget = ref(null);
|
2026-09-01 14:46:32 +08:00
|
|
|
|
const todoExecutorKeyword = ref("");
|
|
|
|
|
|
const todoExecutorRefreshing = ref(false);
|
2026-09-03 17:58:02 +08:00
|
|
|
|
const benefitDetailVisible = ref(false);
|
2026-08-27 14:47:13 +08:00
|
|
|
|
const todoTypeOptions = Object.entries(TODO_EVENT_TYPE_LABELS).map(([value, label]) => ({ value, label }));
|
2026-08-11 10:24:25 +08:00
|
|
|
|
let searchTimer = null;
|
|
|
|
|
|
let entryKey = 0;
|
|
|
|
|
|
|
2026-09-03 17:58:02 +08:00
|
|
|
|
function toggleBenefitDetail() {
|
|
|
|
|
|
benefitDetailVisible.value = !benefitDetailVisible.value;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const benefitDetailItems = computed(() => entries.value.flatMap((entry) => {
|
|
|
|
|
|
if (entry.isPackage) {
|
|
|
|
|
|
return (entry.projects || [])
|
|
|
|
|
|
.filter((project) => project.selected)
|
|
|
|
|
|
.map((project) => ({
|
|
|
|
|
|
key: `${entry.key}_${project.key}`,
|
|
|
|
|
|
name: project.project?.projectName || "未命名权益",
|
|
|
|
|
|
quantity: Math.max(0, toNumber(project.usageCount, 1)),
|
|
|
|
|
|
discount: toNumber(project.discount),
|
|
|
|
|
|
totalAmount: Math.max(0, toNumber(project.totalPrice)),
|
|
|
|
|
|
isFree: false,
|
|
|
|
|
|
}));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!entry.project?._id) return [];
|
|
|
|
|
|
return [{
|
|
|
|
|
|
key: entry.key,
|
|
|
|
|
|
name: entry.project.projectName || "未命名权益",
|
|
|
|
|
|
quantity: Math.max(0, toNumber(entry.usageCount, 1)),
|
|
|
|
|
|
discount: toNumber(entry.discount),
|
|
|
|
|
|
totalAmount: Math.max(0, toNumber(entry.isFree ? 0 : entry.totalPrice)),
|
|
|
|
|
|
isFree: Boolean(entry.isFree),
|
|
|
|
|
|
}];
|
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
|
|
const benefitSummary = computed(() => entries.value.reduce((summary, entry) => {
|
|
|
|
|
|
if (entry.isPackage) {
|
|
|
|
|
|
summary.selectedCount += 1;
|
|
|
|
|
|
const originalAmount = toNumber(entry.packagePrice || entry.price) * Math.max(0, toNumber(entry.usageCount, 1));
|
|
|
|
|
|
const totalAmount = entry.isFree ? 0 : Math.max(0, toNumber(entry.totalPrice));
|
|
|
|
|
|
summary.originalAmount += originalAmount;
|
|
|
|
|
|
summary.totalAmount += totalAmount;
|
|
|
|
|
|
summary.savedAmount += Math.max(0, originalAmount - totalAmount);
|
|
|
|
|
|
return summary;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!entry.project?._id) return summary;
|
|
|
|
|
|
summary.selectedCount += 1;
|
|
|
|
|
|
const usageCount = Math.max(0, toNumber(entry.usageCount, 1));
|
|
|
|
|
|
const originalAmount = toNumber(entry.price) * usageCount;
|
|
|
|
|
|
const totalAmount = entry.isFree ? 0 : Math.max(0, toNumber(entry.totalPrice));
|
|
|
|
|
|
summary.originalAmount += originalAmount;
|
|
|
|
|
|
summary.totalAmount += totalAmount;
|
|
|
|
|
|
summary.savedAmount += Math.max(0, originalAmount - totalAmount);
|
|
|
|
|
|
return summary;
|
|
|
|
|
|
}, {
|
|
|
|
|
|
selectedCount: 0,
|
|
|
|
|
|
originalAmount: 0,
|
|
|
|
|
|
savedAmount: 0,
|
|
|
|
|
|
totalAmount: 0,
|
|
|
|
|
|
}));
|
|
|
|
|
|
|
2026-08-14 16:49:54 +08:00
|
|
|
|
function formatDate(date) {
|
|
|
|
|
|
const year = date.getFullYear();
|
|
|
|
|
|
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
|
|
|
|
const day = String(date.getDate()).padStart(2, "0");
|
|
|
|
|
|
return `${year}-${month}-${day}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function getTodayParts() {
|
|
|
|
|
|
const now = new Date();
|
|
|
|
|
|
return {
|
|
|
|
|
|
year: now.getFullYear(),
|
|
|
|
|
|
month: now.getMonth() + 1,
|
|
|
|
|
|
day: now.getDate(),
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function getDaysInMonth(year, month) {
|
|
|
|
|
|
return new Date(year, month, 0).getDate();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildYearOptions() {
|
|
|
|
|
|
const { year } = getTodayParts();
|
|
|
|
|
|
return Array.from({ length: VALID_YEAR_SPAN + 1 }, (_, index) => `${year + index}年`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildMonthOptions(year) {
|
|
|
|
|
|
const today = getTodayParts();
|
|
|
|
|
|
const startMonth = year === today.year ? today.month : 1;
|
|
|
|
|
|
return Array.from({ length: 12 - startMonth + 1 }, (_, index) => `${startMonth + index}月`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildDayOptions(year, month) {
|
|
|
|
|
|
const today = getTodayParts();
|
|
|
|
|
|
const startDay = year === today.year && month === today.month ? today.day : 1;
|
|
|
|
|
|
const maxDay = getDaysInMonth(year, month);
|
|
|
|
|
|
return Array.from({ length: maxDay - startDay + 1 }, (_, index) => `${startDay + index}日`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function parseValidTime(value) {
|
|
|
|
|
|
const today = getTodayParts();
|
|
|
|
|
|
if (!value) return today;
|
|
|
|
|
|
const [year, month, day] = String(value).split("-").map(Number);
|
|
|
|
|
|
if (!year || !month || !day) return today;
|
|
|
|
|
|
const selected = { year, month, day };
|
|
|
|
|
|
const selectedText = `${year}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
|
|
|
|
|
|
return selectedText < formatDate(new Date()) ? today : selected;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function syncValidTimePicker(selected) {
|
|
|
|
|
|
const years = buildYearOptions();
|
|
|
|
|
|
const year = selected.year;
|
|
|
|
|
|
const months = buildMonthOptions(year);
|
|
|
|
|
|
const monthValues = months.map((item) => Number(item.replace("月", "")));
|
|
|
|
|
|
const month = monthValues.includes(selected.month) ? selected.month : monthValues[0];
|
|
|
|
|
|
const days = buildDayOptions(year, month);
|
|
|
|
|
|
const dayValues = days.map((item) => Number(item.replace("日", "")));
|
|
|
|
|
|
const day = dayValues.includes(selected.day) ? selected.day : dayValues[0];
|
|
|
|
|
|
validTimeRange.value = [years, months, days];
|
|
|
|
|
|
validTimeIndex.value = [
|
|
|
|
|
|
years.findIndex((item) => Number(item.replace("年", "")) === year),
|
|
|
|
|
|
monthValues.indexOf(month),
|
|
|
|
|
|
dayValues.indexOf(day),
|
|
|
|
|
|
];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function prepareValidTimePicker(entry) {
|
|
|
|
|
|
syncValidTimePicker(parseValidTime(entry.validTime));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function onValidTimeColumnChange(event) {
|
|
|
|
|
|
const { column, value } = event.detail;
|
|
|
|
|
|
const nextIndex = [...validTimeIndex.value];
|
|
|
|
|
|
nextIndex[column] = value;
|
|
|
|
|
|
|
|
|
|
|
|
const year = Number(validTimeRange.value[0][nextIndex[0]].replace("年", ""));
|
|
|
|
|
|
if (column === 0) {
|
|
|
|
|
|
const months = buildMonthOptions(year);
|
|
|
|
|
|
const month = Number(months[0].replace("月", ""));
|
|
|
|
|
|
const days = buildDayOptions(year, month);
|
|
|
|
|
|
validTimeRange.value = [validTimeRange.value[0], months, days];
|
|
|
|
|
|
nextIndex[1] = 0;
|
|
|
|
|
|
nextIndex[2] = 0;
|
|
|
|
|
|
} else if (column === 1) {
|
|
|
|
|
|
const month = Number(validTimeRange.value[1][nextIndex[1]].replace("月", ""));
|
|
|
|
|
|
const days = buildDayOptions(year, month);
|
|
|
|
|
|
validTimeRange.value = [validTimeRange.value[0], validTimeRange.value[1], days];
|
|
|
|
|
|
nextIndex[2] = Math.min(nextIndex[2], days.length - 1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
validTimeIndex.value = nextIndex;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
prepareValidTimePicker({ validTime: "" });
|
|
|
|
|
|
|
2026-08-11 10:24:25 +08:00
|
|
|
|
function createEntry() {
|
2026-08-11 17:28:46 +08:00
|
|
|
|
return {
|
|
|
|
|
|
key: ++entryKey,
|
|
|
|
|
|
project: null,
|
|
|
|
|
|
price: 0,
|
2026-08-14 16:49:54 +08:00
|
|
|
|
totalPrice: 0,
|
2026-08-11 17:28:46 +08:00
|
|
|
|
discount: 10,
|
2026-08-14 16:49:54 +08:00
|
|
|
|
isFree: false,
|
2026-09-01 14:46:32 +08:00
|
|
|
|
isGift: false,
|
|
|
|
|
|
isDiscount: false,
|
|
|
|
|
|
lowestDiscount: 10,
|
|
|
|
|
|
enableDiscount: false,
|
|
|
|
|
|
minDiscount: 10,
|
2026-08-11 17:28:46 +08:00
|
|
|
|
usageCount: 1,
|
|
|
|
|
|
validTime: "",
|
|
|
|
|
|
dept: null,
|
|
|
|
|
|
treatmentDoctorUserId: "",
|
|
|
|
|
|
treatmentDoctorName: "",
|
2026-08-27 14:47:13 +08:00
|
|
|
|
todoEnabled: false,
|
|
|
|
|
|
todoConfig: null,
|
2026-08-11 17:28:46 +08:00
|
|
|
|
};
|
2026-08-11 10:24:25 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function decodeRouteParam(value = "") {
|
|
|
|
|
|
try {
|
|
|
|
|
|
return decodeURIComponent(value || "");
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
return value || "";
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
onLoad((options = {}) => {
|
2026-09-01 14:46:32 +08:00
|
|
|
|
uni.onKeyboardHeightChange((res) => {
|
|
|
|
|
|
keyboardHeight.value = Number(res?.height || 0);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-08-14 16:49:54 +08:00
|
|
|
|
groupId.value = decodeRouteParam(options.groupId);
|
2026-08-11 10:24:25 +08:00
|
|
|
|
patientId.value = decodeRouteParam(options.patientId);
|
|
|
|
|
|
patientName.value = decodeRouteParam(options.patientName);
|
|
|
|
|
|
teamId.value = decodeRouteParam(options.teamId);
|
|
|
|
|
|
teamName.value = decodeRouteParam(options.teamName);
|
|
|
|
|
|
corpId.value = decodeRouteParam(options.corpId) || accountStore.account?.corpId || "";
|
|
|
|
|
|
entries.value = [createEntry()];
|
2026-08-11 17:28:46 +08:00
|
|
|
|
loadTeamContext().then(() => {
|
|
|
|
|
|
loadCorpSettings();
|
|
|
|
|
|
loadStaffList();
|
2026-08-27 14:47:13 +08:00
|
|
|
|
loadTodoTeams();
|
2026-08-11 17:28:46 +08:00
|
|
|
|
});
|
2026-08-11 10:24:25 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-09-01 14:46:32 +08:00
|
|
|
|
onUnmounted(() => {
|
|
|
|
|
|
uni.offKeyboardHeightChange();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-08-11 17:28:46 +08:00
|
|
|
|
async function loadTeamContext() {
|
|
|
|
|
|
if (!teamId.value || !corpId.value) return;
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await api("getTeamData", { teamId: teamId.value, corpId: corpId.value }, false);
|
|
|
|
|
|
const team = res?.data || {};
|
|
|
|
|
|
if (team.corpId && team.corpId !== corpId.value) corpId.value = team.corpId;
|
2026-08-27 14:47:13 +08:00
|
|
|
|
teamMembers.value = Array.isArray(team.memberList) ? team.memberList.map((member) => (
|
|
|
|
|
|
typeof member === "string" ? { userid: member, name: member } : member
|
|
|
|
|
|
)) : [];
|
2026-08-11 17:28:46 +08:00
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("加载服务团队失败", error);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function loadStaffList() {
|
|
|
|
|
|
if (!corpId.value) return;
|
2026-08-11 10:24:25 +08:00
|
|
|
|
try {
|
2026-08-11 17:28:46 +08:00
|
|
|
|
const res = await api("getCorpMember", { corpId: corpId.value, page: 1, pageSize: 1000 }, false);
|
|
|
|
|
|
const list = res?.data?.data || res?.data?.list || res?.data || [];
|
|
|
|
|
|
staffList.value = Array.isArray(list)
|
|
|
|
|
|
? list.filter((staff) => staff?.userid).map((staff) => ({ ...staff, anotherName: staff.anotherName || staff.name || staff.userid }))
|
|
|
|
|
|
: [];
|
2026-08-27 14:47:13 +08:00
|
|
|
|
const staffMap = new Map(staffList.value.map((staff) => [staff.userid, staff]));
|
|
|
|
|
|
teamMembers.value = teamMembers.value.map((member) => ({ ...member, ...(staffMap.get(member.userid) || {}) }));
|
|
|
|
|
|
todoTeams.value = todoTeams.value.map((team) => ({
|
|
|
|
|
|
...team,
|
|
|
|
|
|
memberList: (team.memberList || []).map((member) => {
|
|
|
|
|
|
const userid = typeof member === "string" ? member : member?.userid;
|
|
|
|
|
|
return { ...(typeof member === "object" ? member : {}), ...(staffMap.get(userid) || {}), userid };
|
|
|
|
|
|
}),
|
|
|
|
|
|
}));
|
2026-08-11 10:24:25 +08:00
|
|
|
|
} catch (error) {
|
2026-08-11 17:28:46 +08:00
|
|
|
|
staffList.value = [];
|
|
|
|
|
|
console.error("加载机构员工失败", error);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-27 14:47:13 +08:00
|
|
|
|
async function loadTodoTeams() {
|
2026-09-01 14:46:32 +08:00
|
|
|
|
if (!corpId.value) return;
|
2026-08-27 14:47:13 +08:00
|
|
|
|
try {
|
2026-09-01 14:46:32 +08:00
|
|
|
|
const res = await api("getAllTeamByCorp", { corpId: corpId.value }, false);
|
|
|
|
|
|
const list = Array.isArray(res?.data)
|
|
|
|
|
|
? res.data
|
|
|
|
|
|
: Array.isArray(res?.data?.data)
|
|
|
|
|
|
? res.data.data
|
|
|
|
|
|
: Array.isArray(res?.data?.list)
|
|
|
|
|
|
? res.data.list
|
|
|
|
|
|
: Array.isArray(res?.list)
|
|
|
|
|
|
? res.list
|
|
|
|
|
|
: [];
|
2026-08-27 14:47:13 +08:00
|
|
|
|
const baseTeams = list.length ? list : [{ teamId: teamId.value, name: teamName.value, memberList: teamMembers.value }];
|
|
|
|
|
|
const details = await Promise.all(baseTeams.map(async (team) => {
|
|
|
|
|
|
const currentTeamId = team.teamId || team._id || team.id;
|
|
|
|
|
|
if (!currentTeamId) return null;
|
2026-09-01 14:46:32 +08:00
|
|
|
|
if (Array.isArray(team.memberList)) {
|
|
|
|
|
|
return { teamId: currentTeamId, name: team.name || team.teamName || "未命名团队", memberList: team.memberList };
|
|
|
|
|
|
}
|
2026-08-27 14:47:13 +08:00
|
|
|
|
const detailRes = await api("getTeamData", { corpId: corpId.value, teamId: currentTeamId }, false);
|
|
|
|
|
|
const detail = detailRes?.data || {};
|
2026-09-01 14:46:32 +08:00
|
|
|
|
return { teamId: currentTeamId, name: detail.name || team.name || team.teamName || "未命名团队", memberList: detail.memberList || [] };
|
2026-08-27 14:47:13 +08:00
|
|
|
|
}));
|
|
|
|
|
|
todoTeams.value = details.filter(Boolean);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
todoTeams.value = teamId.value ? [{ teamId: teamId.value, name: teamName.value, memberList: teamMembers.value }] : [];
|
2026-09-01 14:46:32 +08:00
|
|
|
|
console.error("加载机构团队失败", error);
|
2026-08-27 14:47:13 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-11 17:28:46 +08:00
|
|
|
|
async function loadCorpSettings() {
|
|
|
|
|
|
if (!corpId.value) return;
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await api("getCorpInfo", { corpId: corpId.value }, false);
|
|
|
|
|
|
const corpInfo = res?.data || {};
|
|
|
|
|
|
showBenefitAmount.value = corpInfo.showBenefitAmount !== false;
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
showBenefitAmount.value = true;
|
|
|
|
|
|
console.error("加载机构权益配置失败", error);
|
2026-08-11 10:24:25 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function openProjectPicker(index) {
|
|
|
|
|
|
projectPickerIndex.value = index;
|
|
|
|
|
|
projectPickerVisible.value = true;
|
|
|
|
|
|
projectKeyword.value = "";
|
2026-08-14 16:49:54 +08:00
|
|
|
|
loadProjects({ reset: true });
|
2026-08-11 10:24:25 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function closeProjectPicker() {
|
|
|
|
|
|
projectPickerVisible.value = false;
|
|
|
|
|
|
projectKeyword.value = "";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function searchProjects() {
|
|
|
|
|
|
clearTimeout(searchTimer);
|
2026-08-14 16:49:54 +08:00
|
|
|
|
searchTimer = setTimeout(() => loadProjects({ reset: true }), 300);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function normalizeProject(project = {}) {
|
|
|
|
|
|
const depts = Array.isArray(project.depts) ? project.depts : [];
|
|
|
|
|
|
return {
|
|
|
|
|
|
...project,
|
|
|
|
|
|
depts,
|
|
|
|
|
|
deptNames: depts.map((dept) => dept?.deptName).filter(Boolean).join("、"),
|
|
|
|
|
|
};
|
2026-08-11 10:24:25 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-14 16:49:54 +08:00
|
|
|
|
async function loadProjects({ reset = false } = {}) {
|
|
|
|
|
|
if (projectLoading.value) return;
|
2026-08-27 14:10:42 +08:00
|
|
|
|
if (!reset && !projectHasMore.value && !packageHasMore.value) return;
|
2026-08-14 16:49:54 +08:00
|
|
|
|
|
|
|
|
|
|
const page = reset ? 1 : projectPage.value;
|
2026-08-11 10:24:25 +08:00
|
|
|
|
projectLoading.value = true;
|
2026-08-14 16:49:54 +08:00
|
|
|
|
if (reset) {
|
|
|
|
|
|
projectList.value = [];
|
2026-08-27 14:10:42 +08:00
|
|
|
|
packageList.value = [];
|
2026-08-14 16:49:54 +08:00
|
|
|
|
projectHasMore.value = true;
|
2026-08-27 14:10:42 +08:00
|
|
|
|
packageHasMore.value = true;
|
2026-08-14 16:49:54 +08:00
|
|
|
|
}
|
2026-08-11 10:24:25 +08:00
|
|
|
|
try {
|
2026-08-27 14:10:42 +08:00
|
|
|
|
const [res, packageRes] = await Promise.all([api("getProjectList", {
|
2026-08-11 10:24:25 +08:00
|
|
|
|
corpId: corpId.value,
|
2026-08-14 16:49:54 +08:00
|
|
|
|
page,
|
|
|
|
|
|
pageSize: projectPageSize,
|
2026-08-11 10:24:25 +08:00
|
|
|
|
projectName: projectKeyword.value.trim(),
|
|
|
|
|
|
projectStatus: "enable",
|
|
|
|
|
|
showDepts: true,
|
2026-08-27 14:10:42 +08:00
|
|
|
|
}, false), api("getProjectPackageList", {
|
|
|
|
|
|
corpId: corpId.value,
|
|
|
|
|
|
page,
|
|
|
|
|
|
pageSize: projectPageSize,
|
|
|
|
|
|
name: projectKeyword.value.trim(),
|
|
|
|
|
|
enable: true,
|
|
|
|
|
|
}, false)]);
|
2026-08-11 10:24:25 +08:00
|
|
|
|
const list = res?.data?.list || res?.list || res?.data || [];
|
2026-08-14 16:49:54 +08:00
|
|
|
|
const nextList = Array.isArray(list) ? list.map(normalizeProject) : [];
|
|
|
|
|
|
projectList.value = reset ? nextList : projectList.value.concat(nextList);
|
2026-08-27 14:10:42 +08:00
|
|
|
|
const packages = packageRes?.data?.list || packageRes?.list || packageRes?.data || [];
|
|
|
|
|
|
const nextPackages = Array.isArray(packages) ? packages.filter((item) => Array.isArray(item.projects) && item.projects.length) : [];
|
|
|
|
|
|
packageList.value = reset ? nextPackages : packageList.value.concat(nextPackages);
|
2026-08-14 16:49:54 +08:00
|
|
|
|
projectPage.value = page + 1;
|
|
|
|
|
|
projectHasMore.value = nextList.length >= projectPageSize;
|
2026-08-27 14:10:42 +08:00
|
|
|
|
packageHasMore.value = nextPackages.length >= projectPageSize;
|
2026-08-11 10:24:25 +08:00
|
|
|
|
} catch (error) {
|
2026-08-27 14:10:42 +08:00
|
|
|
|
if (reset) {
|
|
|
|
|
|
projectList.value = [];
|
|
|
|
|
|
packageList.value = [];
|
|
|
|
|
|
}
|
2026-08-14 16:49:54 +08:00
|
|
|
|
projectHasMore.value = false;
|
2026-08-27 14:10:42 +08:00
|
|
|
|
packageHasMore.value = false;
|
2026-08-11 10:24:25 +08:00
|
|
|
|
uni.showToast({ title: "项目加载失败", icon: "none" });
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
projectLoading.value = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-14 16:49:54 +08:00
|
|
|
|
function loadMoreProjects() {
|
|
|
|
|
|
loadProjects();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function toNumber(value, fallback = 0) {
|
|
|
|
|
|
const number = Number(value);
|
|
|
|
|
|
return Number.isFinite(number) ? number : fallback;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function toFixedNumber(value, precision = 2) {
|
|
|
|
|
|
return Number(toNumber(value).toFixed(precision));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function clampNumber(value, min, max) {
|
|
|
|
|
|
return Math.min(max, Math.max(min, value));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function calculateTotalPrice(entry) {
|
|
|
|
|
|
if (entry.isFree) {
|
|
|
|
|
|
entry.discount = 0;
|
|
|
|
|
|
entry.totalPrice = 0;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-09-01 14:46:32 +08:00
|
|
|
|
entry.discount = normalizeDiscount(entry);
|
2026-08-14 16:49:54 +08:00
|
|
|
|
const total = toNumber(entry.price) * toNumber(entry.usageCount, 1) * (toNumber(entry.discount) / 10);
|
|
|
|
|
|
entry.totalPrice = toFixedNumber(Math.max(0, total), 2);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-01 14:46:32 +08:00
|
|
|
|
function changeDiscount(event, entry) {
|
|
|
|
|
|
const pickerRange = getDiscountPickerRange(entry);
|
|
|
|
|
|
const integer = pickerRange[0][Number(event.detail.value[0])] || "10";
|
|
|
|
|
|
const decimal = pickerRange[1][Number(event.detail.value[1])] || "0";
|
|
|
|
|
|
entry.discount = normalizeDiscount({ ...entry, discount: `${integer}.${decimal}` });
|
|
|
|
|
|
if (entry.isPackage) {
|
|
|
|
|
|
calculatePackageTotal(entry);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
calculateTotalPrice(entry);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-14 16:49:54 +08:00
|
|
|
|
function calculateDiscount(entry) {
|
|
|
|
|
|
if (entry.isFree) {
|
|
|
|
|
|
entry.discount = 0;
|
|
|
|
|
|
entry.totalPrice = 0;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
const originalTotal = toNumber(entry.price) * toNumber(entry.usageCount);
|
|
|
|
|
|
entry.discount = originalTotal > 0
|
2026-09-01 14:46:32 +08:00
|
|
|
|
? normalizeDiscount({ ...entry, discount: toFixedNumber((toNumber(entry.totalPrice) / originalTotal) * 10, 1) })
|
|
|
|
|
|
: normalizeDiscount(entry);
|
2026-08-14 16:49:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function handleFreeChange(event, entry) {
|
2026-09-01 14:46:32 +08:00
|
|
|
|
if (!entry.isGift) {
|
|
|
|
|
|
entry.isFree = false;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-08-14 16:49:54 +08:00
|
|
|
|
entry.isFree = Array.isArray(event.detail.value) && event.detail.value.includes("free");
|
|
|
|
|
|
if (entry.isFree) {
|
|
|
|
|
|
entry.discount = 0;
|
|
|
|
|
|
entry.totalPrice = 0;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-09-01 14:46:32 +08:00
|
|
|
|
entry.discount = normalizeDiscount({ ...entry, discount: 10 });
|
2026-08-14 16:49:54 +08:00
|
|
|
|
calculateTotalPrice(entry);
|
2026-08-11 17:28:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-01 14:46:32 +08:00
|
|
|
|
function getDiscountMin(entry) {
|
|
|
|
|
|
const minDiscount = entry.isPackage ? entry.minDiscount : entry.lowestDiscount;
|
|
|
|
|
|
return Number(minDiscount) > 0 ? Number(minDiscount) : 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function getDiscountPickerRange(entry) {
|
|
|
|
|
|
const supportsDiscount = entry.isPackage ? entry.enableDiscount : entry.isDiscount;
|
|
|
|
|
|
if (!supportsDiscount) return [["10"], ["0"]];
|
|
|
|
|
|
const min = Math.min(10, Math.max(0, toNumber(getDiscountMin(entry))));
|
|
|
|
|
|
const integers = [];
|
|
|
|
|
|
for (let value = Math.floor(min); value <= 10; value += 1) {
|
|
|
|
|
|
integers.push(String(value));
|
|
|
|
|
|
}
|
|
|
|
|
|
return [integers, ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function getDiscountPickerValue(entry) {
|
|
|
|
|
|
const discount = normalizeDiscount(entry).toFixed(1).split(".");
|
|
|
|
|
|
const range = getDiscountPickerRange(entry);
|
|
|
|
|
|
const integerIndex = Math.max(0, range[0].indexOf(discount[0]));
|
|
|
|
|
|
const decimalIndex = Math.max(0, range[1].indexOf(discount[1] || "0"));
|
|
|
|
|
|
return [integerIndex, decimalIndex];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function normalizeDiscount(entry) {
|
|
|
|
|
|
const discount = toNumber(entry.discount, 0);
|
|
|
|
|
|
const supportsDiscount = entry.isPackage ? entry.enableDiscount : entry.isDiscount;
|
|
|
|
|
|
if (!supportsDiscount) return 10;
|
|
|
|
|
|
return toFixedNumber(clampNumber(discount, getDiscountMin(entry), 10), 1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-11 17:28:46 +08:00
|
|
|
|
function getProjectDepts(entry) {
|
|
|
|
|
|
return Array.isArray(entry?.project?.depts) ? entry.project.depts : [];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function getTreatmentDoctors(entry) {
|
|
|
|
|
|
if (!entry?.dept?._id) return [];
|
|
|
|
|
|
const allowedJobs = ["doctor", "expertDoctor", "therapist"];
|
|
|
|
|
|
const deptId = String(entry.dept._id);
|
|
|
|
|
|
|
|
|
|
|
|
return staffList.value.filter((staff) => {
|
|
|
|
|
|
const hasAllowedJob = Array.isArray(staff.job) && staff.job.some((job) => allowedJobs.includes(job));
|
|
|
|
|
|
const inDept = Array.isArray(staff.deptIds) && staff.deptIds.map(String).includes(deptId);
|
|
|
|
|
|
return hasAllowedJob && inDept;
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-11 10:24:25 +08:00
|
|
|
|
function selectProject(project) {
|
|
|
|
|
|
const entry = entries.value[projectPickerIndex.value];
|
|
|
|
|
|
if (!entry) return;
|
2026-08-14 16:49:54 +08:00
|
|
|
|
const normalizedProject = normalizeProject(project);
|
|
|
|
|
|
entry.project = normalizedProject;
|
|
|
|
|
|
entry.price = Number(normalizedProject.price || 0);
|
2026-09-01 14:46:32 +08:00
|
|
|
|
entry.isGift = normalizedProject.isGift === true;
|
|
|
|
|
|
entry.isDiscount = normalizedProject.isDiscount === true;
|
|
|
|
|
|
entry.lowestDiscount = Number(normalizedProject.lowestDiscount || 0);
|
|
|
|
|
|
entry.discount = normalizeDiscount({ ...entry, discount: 10 });
|
2026-08-14 16:49:54 +08:00
|
|
|
|
entry.isFree = false;
|
|
|
|
|
|
calculateTotalPrice(entry);
|
2026-08-11 17:28:46 +08:00
|
|
|
|
entry.dept = getProjectDepts(entry).length === 1 ? getProjectDepts(entry)[0] : null;
|
|
|
|
|
|
entry.treatmentDoctorUserId = "";
|
|
|
|
|
|
entry.treatmentDoctorName = "";
|
2026-08-11 10:24:25 +08:00
|
|
|
|
closeProjectPicker();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-27 14:10:42 +08:00
|
|
|
|
function createPackageProjectEntry(project = {}, packageItem = {}) {
|
|
|
|
|
|
const normalizedProject = normalizeProject(project);
|
|
|
|
|
|
const hasValidDays = packageItem.hasValidDays === true;
|
|
|
|
|
|
const validDays = Number(packageItem.validDays || 0);
|
|
|
|
|
|
const validDate = hasValidDays && validDays > 0
|
|
|
|
|
|
? formatDate(new Date(Date.now() + validDays * 24 * 60 * 60 * 1000))
|
|
|
|
|
|
: "";
|
|
|
|
|
|
return {
|
|
|
|
|
|
...createEntry(),
|
|
|
|
|
|
project: normalizedProject,
|
|
|
|
|
|
price: Number(normalizedProject.price ?? project.price ?? 0),
|
|
|
|
|
|
usageCount: Number(project.number || 1),
|
2026-08-27 14:47:13 +08:00
|
|
|
|
packageUnitUsageCount: Number(project.number || 1),
|
2026-08-27 14:10:42 +08:00
|
|
|
|
discount: packageItem.packageType === "free" ? 10 : Number(project.discount || 10),
|
2026-09-01 14:46:32 +08:00
|
|
|
|
enableDiscount: packageItem.enableDiscount === true,
|
|
|
|
|
|
minDiscount: Number(packageItem.minDiscount || 0),
|
2026-08-27 14:10:42 +08:00
|
|
|
|
totalPrice: Number(project.totalPrice || 0),
|
|
|
|
|
|
selected: packageItem.packageType === "fixed" || project.required === true,
|
|
|
|
|
|
required: project.required === true,
|
|
|
|
|
|
hasValidDays,
|
|
|
|
|
|
validDays,
|
|
|
|
|
|
validTime: validDate,
|
|
|
|
|
|
dept: normalizedProject.depts?.length === 1 ? normalizedProject.depts[0] : null,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function selectPackage(packageItem) {
|
|
|
|
|
|
const targetIndex = projectPickerIndex.value;
|
|
|
|
|
|
if (targetIndex < 0) return;
|
|
|
|
|
|
try {
|
|
|
|
|
|
uni.showLoading({ title: "加载套餐项目" });
|
|
|
|
|
|
const ids = packageItem.projects.map((item) => item._id).filter(Boolean);
|
|
|
|
|
|
const res = await api("getProjectList", { corpId: corpId.value, ids, page: 1, pageSize: ids.length, projectStatus: "enable", showDepts: true }, false);
|
|
|
|
|
|
const fullProjects = res?.data?.list || res?.list || res?.data || [];
|
|
|
|
|
|
const projects = packageItem.projects.map((item) => {
|
|
|
|
|
|
const fullProject = Array.isArray(fullProjects) ? fullProjects.find((project) => project._id === item._id) : null;
|
|
|
|
|
|
return createPackageProjectEntry({ ...(fullProject || {}), ...item }, packageItem);
|
|
|
|
|
|
});
|
|
|
|
|
|
const requiredCount = projects.filter((item) => item.required).length;
|
|
|
|
|
|
const entry = {
|
|
|
|
|
|
...createEntry(),
|
|
|
|
|
|
isPackage: true,
|
|
|
|
|
|
packageId: packageItem._id,
|
|
|
|
|
|
packageName: packageItem.name,
|
|
|
|
|
|
packageType: packageItem.packageType,
|
|
|
|
|
|
packagePrice: Number(packageItem.packagePrice || 0),
|
|
|
|
|
|
price: Number(packageItem.packagePrice || 0),
|
|
|
|
|
|
usageCount: 1,
|
|
|
|
|
|
discount: 10,
|
2026-09-01 14:46:32 +08:00
|
|
|
|
enableDiscount: packageItem.enableDiscount === true,
|
|
|
|
|
|
minDiscount: Number(packageItem.minDiscount || 0),
|
2026-08-27 14:10:42 +08:00
|
|
|
|
totalPrice: Number(packageItem.packagePrice || 0),
|
|
|
|
|
|
projectNumber: Number(packageItem.projectNumber || projects.length),
|
|
|
|
|
|
requiredCount,
|
|
|
|
|
|
optionalCount: Math.max(Number(packageItem.projectNumber || projects.length) - requiredCount, 0),
|
|
|
|
|
|
projects,
|
|
|
|
|
|
};
|
|
|
|
|
|
syncPackageSelection(entry);
|
|
|
|
|
|
syncPackageTotals(entry);
|
|
|
|
|
|
entries.value.splice(targetIndex, 1, entry);
|
|
|
|
|
|
closeProjectPicker();
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
uni.showToast({ title: "套餐加载失败", icon: "none" });
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
uni.hideLoading();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function syncPackageSelection(entry) {
|
|
|
|
|
|
entry.selectedCount = entry.projects.filter((item) => item.selected).length;
|
|
|
|
|
|
entry.optionalSelectedCount = Math.max(entry.selectedCount - entry.requiredCount, 0);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function isPackageSelectionFull(entry) {
|
|
|
|
|
|
return Number(entry.selectedCount || 0) >= Number(entry.projectNumber || 0);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function togglePackageProject(entry, projectEntry) {
|
|
|
|
|
|
if (projectEntry.required) return;
|
|
|
|
|
|
if (!projectEntry.selected && isPackageSelectionFull(entry)) {
|
|
|
|
|
|
uni.showToast({ title: "已达到可选项目上限", icon: "none" });
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
projectEntry.selected = !projectEntry.selected;
|
2026-08-27 14:47:13 +08:00
|
|
|
|
if (projectEntry.selected) projectEntry.usageCount = Number(entry.usageCount || 1) * Number(projectEntry.packageUnitUsageCount || 1);
|
2026-08-27 14:10:42 +08:00
|
|
|
|
syncPackageSelection(entry);
|
|
|
|
|
|
syncPackageTotals(entry);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function calculatePackageTotal(entry) {
|
|
|
|
|
|
entry.usageCount = Math.max(1, Number(entry.usageCount || 1));
|
2026-09-01 14:46:32 +08:00
|
|
|
|
entry.discount = normalizeDiscount(entry);
|
2026-08-27 14:10:42 +08:00
|
|
|
|
entry.totalPrice = toFixedNumber(Number(entry.price || 0) * entry.usageCount * entry.discount / 10, 2);
|
2026-08-27 14:47:13 +08:00
|
|
|
|
entry.projects.filter((project) => project.selected).forEach((project) => {
|
|
|
|
|
|
project.usageCount = entry.usageCount * Number(project.packageUnitUsageCount || 1);
|
|
|
|
|
|
});
|
2026-08-27 14:10:42 +08:00
|
|
|
|
syncPackageTotals(entry);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-27 14:47:13 +08:00
|
|
|
|
function getDefaultTodoConfig(entry) {
|
|
|
|
|
|
const planDate = formatDate(new Date());
|
|
|
|
|
|
return {
|
|
|
|
|
|
taskContent: entry.project?.projectName || "",
|
|
|
|
|
|
eventType: "benefitExecution",
|
|
|
|
|
|
planDate,
|
|
|
|
|
|
expireDate: entry.validTime || formatDate(new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)),
|
|
|
|
|
|
executorUserId: "",
|
|
|
|
|
|
executorName: "",
|
|
|
|
|
|
executeTeamId: "",
|
|
|
|
|
|
executeTeamName: "",
|
|
|
|
|
|
deductBenefit: false,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function toggleTodo(event, entry) {
|
|
|
|
|
|
entry.todoEnabled = Boolean(event.detail.value);
|
|
|
|
|
|
if (entry.todoEnabled) entry.todoConfig = entry.todoConfig || getDefaultTodoConfig(entry);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function getTodoTypeLabel(value) {
|
|
|
|
|
|
return TODO_EVENT_TYPE_LABELS[value] || "请选择类型";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function changeTodoType(event, entry) {
|
|
|
|
|
|
entry.todoConfig.eventType = todoTypeOptions[Number(event.detail.value)]?.value || "benefitExecution";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function getTodoExecutors(entry) {
|
|
|
|
|
|
const team = todoTeams.value.find((item) => String(item.teamId) === String(entry.todoConfig?.executeTeamId));
|
|
|
|
|
|
return (team?.memberList || []).map((member) => {
|
|
|
|
|
|
const userid = typeof member === "string" ? member : member?.userid;
|
|
|
|
|
|
const staff = staffList.value.find((item) => item.userid === userid);
|
|
|
|
|
|
return { userid, name: staff?.anotherName || staff?.name || member?.anotherName || member?.name || userid };
|
|
|
|
|
|
}).filter((member) => member.userid);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function openTodoExecutorPicker(entry) {
|
|
|
|
|
|
todoExecutorTarget.value = entry;
|
2026-09-01 14:46:32 +08:00
|
|
|
|
todoExecutorKeyword.value = "";
|
2026-08-27 14:47:13 +08:00
|
|
|
|
todoExecutorPickerVisible.value = true;
|
2026-09-01 14:46:32 +08:00
|
|
|
|
if (!todoTeams.value.length) refreshTodoExecutors();
|
2026-08-27 14:47:13 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function closeTodoExecutorPicker() {
|
|
|
|
|
|
todoExecutorPickerVisible.value = false;
|
|
|
|
|
|
todoExecutorTarget.value = null;
|
2026-09-01 14:46:32 +08:00
|
|
|
|
todoExecutorKeyword.value = "";
|
2026-08-27 14:47:13 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function getTodoExecutorsForTeam(team) {
|
|
|
|
|
|
return (team?.memberList || []).map((member) => {
|
|
|
|
|
|
const userid = typeof member === "string" ? member : member?.userid;
|
|
|
|
|
|
const staff = staffList.value.find((item) => item.userid === userid);
|
|
|
|
|
|
return { userid, name: staff?.anotherName || staff?.name || member?.anotherName || member?.name || userid };
|
|
|
|
|
|
}).filter((member) => member.userid);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-01 14:46:32 +08:00
|
|
|
|
const filteredTodoTeams = computed(() => {
|
|
|
|
|
|
const keyword = todoExecutorKeyword.value.trim().toLowerCase();
|
|
|
|
|
|
if (!keyword) return todoTeams.value;
|
|
|
|
|
|
return todoTeams.value.map((team) => {
|
|
|
|
|
|
const members = getTodoExecutorsForTeam(team).filter((member) => (
|
|
|
|
|
|
String(member.name || "").toLowerCase().includes(keyword)
|
|
|
|
|
|
|| String(member.userid || "").toLowerCase().includes(keyword)
|
|
|
|
|
|
));
|
|
|
|
|
|
const teamMatched = String(team.name || "").toLowerCase().includes(keyword);
|
|
|
|
|
|
return { team: teamMatched ? team : { ...team, memberList: members }, matched: teamMatched || members.length > 0 };
|
|
|
|
|
|
}).filter((item) => item.matched).map((item) => item.team);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
async function refreshTodoExecutors() {
|
|
|
|
|
|
if (todoExecutorRefreshing.value) return;
|
|
|
|
|
|
todoExecutorRefreshing.value = true;
|
|
|
|
|
|
try {
|
|
|
|
|
|
await Promise.all([loadStaffList(), loadTodoTeams()]);
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
todoExecutorRefreshing.value = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-27 14:47:13 +08:00
|
|
|
|
function selectTodoExecutor(member, team) {
|
|
|
|
|
|
const entry = todoExecutorTarget.value;
|
|
|
|
|
|
if (!entry?.todoConfig) return;
|
|
|
|
|
|
entry.todoConfig.executeTeamId = team.teamId;
|
|
|
|
|
|
entry.todoConfig.executeTeamName = team.name;
|
|
|
|
|
|
entry.todoConfig.executorUserId = member.userid;
|
|
|
|
|
|
entry.todoConfig.executorName = member.name;
|
|
|
|
|
|
closeTodoExecutorPicker();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function isSelectedTodoExecutor(member, team) {
|
|
|
|
|
|
const config = todoExecutorTarget.value?.todoConfig;
|
|
|
|
|
|
return config?.executeTeamId === team.teamId && config?.executorUserId === member.userid;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function prepareTodoDatePicker(entry, field) {
|
|
|
|
|
|
todoDateTarget.value = { entry, field };
|
|
|
|
|
|
prepareValidTimePicker({ validTime: entry.todoConfig[field] });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function changeTodoDate(event) {
|
|
|
|
|
|
const target = todoDateTarget.value;
|
|
|
|
|
|
if (!target) return;
|
|
|
|
|
|
const [yearIndex, monthIndex, dayIndex] = event.detail.value || validTimeIndex.value;
|
|
|
|
|
|
const year = Number(validTimeRange.value[0][yearIndex].replace("年", ""));
|
|
|
|
|
|
const month = Number(validTimeRange.value[1][monthIndex].replace("月", ""));
|
|
|
|
|
|
const day = Number(validTimeRange.value[2][dayIndex].replace("日", ""));
|
|
|
|
|
|
const selectedDate = `${year}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
|
|
|
|
|
|
target.entry.todoConfig[target.field] = selectedDate;
|
|
|
|
|
|
validTimeIndex.value = [yearIndex, monthIndex, dayIndex];
|
|
|
|
|
|
if (target.field === "planDate" && !target.entry.validTime) {
|
|
|
|
|
|
target.entry.todoConfig.expireDate = formatDate(new Date(new Date(`${target.entry.todoConfig.planDate}T00:00:00`).getTime() + 7 * 24 * 60 * 60 * 1000));
|
|
|
|
|
|
}
|
|
|
|
|
|
todoDateTarget.value = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-27 14:10:42 +08:00
|
|
|
|
function syncPackageTotals(entry) {
|
|
|
|
|
|
const selected = entry.projects.filter((item) => item.selected);
|
|
|
|
|
|
const baseTotal = selected.reduce((sum, item) => sum + Number(item.price || 0) * Number(item.usageCount || 1), 0);
|
|
|
|
|
|
let allocated = 0;
|
|
|
|
|
|
selected.forEach((item, index) => {
|
|
|
|
|
|
const total = index === selected.length - 1
|
|
|
|
|
|
? Number(entry.totalPrice || 0) - allocated
|
|
|
|
|
|
: (baseTotal ? Number(entry.totalPrice || 0) * (Number(item.price || 0) * Number(item.usageCount || 1)) / baseTotal : 0);
|
|
|
|
|
|
item.totalPrice = toFixedNumber(Math.max(total, 0), 2);
|
|
|
|
|
|
item.discount = item.price && item.usageCount ? toFixedNumber(item.totalPrice / (item.price * item.usageCount) * 10, 1) : 0;
|
|
|
|
|
|
allocated += item.totalPrice;
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-11 10:24:25 +08:00
|
|
|
|
function changeDept(event, entry) {
|
2026-08-11 17:28:46 +08:00
|
|
|
|
entry.dept = getProjectDepts(entry)[Number(event.detail.value)] || null;
|
|
|
|
|
|
entry.treatmentDoctorUserId = "";
|
|
|
|
|
|
entry.treatmentDoctorName = "";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function changeTreatmentDoctor(event, entry) {
|
|
|
|
|
|
const doctor = getTreatmentDoctors(entry)[Number(event.detail.value)] || null;
|
|
|
|
|
|
entry.treatmentDoctorUserId = doctor?.userid || "";
|
|
|
|
|
|
entry.treatmentDoctorName = doctor?.anotherName || doctor?.name || "";
|
2026-08-11 10:24:25 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function changeValidTime(event, entry) {
|
2026-08-14 16:49:54 +08:00
|
|
|
|
const [yearIndex, monthIndex, dayIndex] = event.detail.value || validTimeIndex.value;
|
|
|
|
|
|
const year = Number(validTimeRange.value[0][yearIndex].replace("年", ""));
|
|
|
|
|
|
const month = Number(validTimeRange.value[1][monthIndex].replace("月", ""));
|
|
|
|
|
|
const day = Number(validTimeRange.value[2][dayIndex].replace("日", ""));
|
|
|
|
|
|
const selectedDate = `${year}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
|
|
|
|
|
|
if (selectedDate < formatDate(new Date())) {
|
|
|
|
|
|
uni.showToast({
|
|
|
|
|
|
title: "不能选择过去的日期",
|
|
|
|
|
|
icon: "none",
|
|
|
|
|
|
duration: 2000,
|
|
|
|
|
|
});
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
entry.validTime = selectedDate;
|
|
|
|
|
|
validTimeIndex.value = [yearIndex, monthIndex, dayIndex];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function clearValidTime(entry) {
|
|
|
|
|
|
entry.validTime = "";
|
2026-08-11 10:24:25 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function addEntry() {
|
2026-08-14 16:49:54 +08:00
|
|
|
|
const entry = createEntry();
|
|
|
|
|
|
entries.value.push(entry);
|
|
|
|
|
|
nextTick(() => {
|
|
|
|
|
|
pageRef.value?.scrollToBottom?.();
|
|
|
|
|
|
});
|
2026-08-11 10:24:25 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function removeEntry(index) {
|
|
|
|
|
|
entries.value.splice(index, 1);
|
|
|
|
|
|
if (!entries.value.length) entries.value.push(createEntry());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function goBack() {
|
|
|
|
|
|
uni.navigateBack();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-27 14:10:42 +08:00
|
|
|
|
function goBackToConversation() {
|
|
|
|
|
|
if (!groupId.value) {
|
|
|
|
|
|
goBack();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
const conversationID = `GROUP${groupId.value}`;
|
|
|
|
|
|
uni.redirectTo({
|
|
|
|
|
|
url: `/pages/message/index?conversationID=${encodeURIComponent(conversationID)}&groupID=${encodeURIComponent(groupId.value)}&fromCase=true`,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-11 10:24:25 +08:00
|
|
|
|
function validateEntries() {
|
|
|
|
|
|
for (let index = 0; index < entries.value.length; index += 1) {
|
|
|
|
|
|
const entry = entries.value[index];
|
2026-08-27 14:10:42 +08:00
|
|
|
|
if (entry.isPackage) {
|
|
|
|
|
|
if (!Number(entry.usageCount) || Number(entry.usageCount) <= 0) return `${entry.packageName}数量必须大于0`;
|
|
|
|
|
|
if (!Number.isFinite(Number(entry.discount)) || Number(entry.discount) < 0 || Number(entry.discount) > 10) return `${entry.packageName}折扣需在0到10之间`;
|
2026-09-01 14:46:32 +08:00
|
|
|
|
if (!entry.enableDiscount && Number(entry.discount) !== 10) return `${entry.packageName}不支持折扣`;
|
|
|
|
|
|
if (entry.enableDiscount && Number(entry.discount) < getDiscountMin(entry)) {
|
|
|
|
|
|
return `${entry.packageName}折扣不能低于${getDiscountMin(entry)}折`;
|
|
|
|
|
|
}
|
2026-08-27 14:10:42 +08:00
|
|
|
|
if (entry.packageType === "free" && entry.selectedCount !== entry.projectNumber) {
|
|
|
|
|
|
return `${entry.packageName}需要选择${entry.projectNumber}项,当前已选择${entry.selectedCount}项`;
|
|
|
|
|
|
}
|
|
|
|
|
|
const selectedProjects = entry.projects.filter((item) => item.selected);
|
|
|
|
|
|
for (const projectEntry of selectedProjects) {
|
|
|
|
|
|
if (!projectEntry.dept?._id) return `${projectEntry.project.projectName}请选择治疗科室`;
|
2026-09-01 17:42:46 +08:00
|
|
|
|
if (Number(projectEntry.validDays || 0) > 0 && !projectEntry.validTime) {
|
|
|
|
|
|
return `${projectEntry.project.projectName}请选择有效日期`;
|
|
|
|
|
|
}
|
2026-08-27 14:47:13 +08:00
|
|
|
|
const todoError = validateTodo(projectEntry);
|
|
|
|
|
|
if (todoError) return todoError;
|
2026-08-27 14:10:42 +08:00
|
|
|
|
}
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
2026-08-11 10:24:25 +08:00
|
|
|
|
const label = `第${index + 1}个权益项目`;
|
|
|
|
|
|
if (!entry.project?._id) return `${label}请选择项目`;
|
2026-08-11 17:28:46 +08:00
|
|
|
|
if (Number(entry.price) < 0 || Number.isNaN(Number(entry.price))) return `${label}价格不能小于0`;
|
|
|
|
|
|
if (!Number.isFinite(Number(entry.discount)) || Number(entry.discount) < 0 || Number(entry.discount) > 10) return `${label}折扣需在0到10之间`;
|
2026-09-01 14:46:32 +08:00
|
|
|
|
if (!entry.isDiscount && Number(entry.discount) !== 10) return `${label}不支持折扣`;
|
|
|
|
|
|
if (entry.isDiscount && Number(entry.discount) < getDiscountMin(entry)) {
|
|
|
|
|
|
return `${label}折扣不能低于${getDiscountMin(entry)}折`;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!entry.isGift && entry.isFree) return `${label}不支持赠送`;
|
2026-08-14 16:49:54 +08:00
|
|
|
|
if (!Number.isFinite(Number(entry.totalPrice)) || Number(entry.totalPrice) < 0) return `${label}总价不能小于0`;
|
2026-08-11 10:24:25 +08:00
|
|
|
|
if (!Number(entry.usageCount) || Number(entry.usageCount) <= 0) return `${label}数量必须大于0`;
|
|
|
|
|
|
if (!entry.dept?._id) return `${label}请选择治疗科室`;
|
2026-08-27 14:47:13 +08:00
|
|
|
|
const todoError = validateTodo(entry);
|
|
|
|
|
|
if (todoError) return todoError;
|
2026-08-11 10:24:25 +08:00
|
|
|
|
}
|
|
|
|
|
|
if (!patientId.value) return "当前会话缺少患者档案关联";
|
|
|
|
|
|
return "";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-27 14:47:13 +08:00
|
|
|
|
function validateTodo(entry) {
|
|
|
|
|
|
if (!entry.todoEnabled) return "";
|
|
|
|
|
|
const config = entry.todoConfig || {};
|
|
|
|
|
|
if (!String(config.taskContent || "").trim()) return `${entry.project?.projectName || "权益项目"}请填写待办内容`;
|
|
|
|
|
|
if (!config.eventType) return `${entry.project?.projectName || "权益项目"}请选择待办类型`;
|
|
|
|
|
|
if (!config.planDate || !config.expireDate || config.expireDate < config.planDate) return `${entry.project?.projectName || "权益项目"}待办日期不正确`;
|
|
|
|
|
|
if (!config.executeTeamId) return `${entry.project?.projectName || "权益项目"}请选择处理团队`;
|
|
|
|
|
|
if (!config.executorUserId) return `${entry.project?.projectName || "权益项目"}请选择待办处理人`;
|
|
|
|
|
|
return "";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function getResponseId(data) {
|
|
|
|
|
|
if (typeof data === "string") return data;
|
|
|
|
|
|
if (!data || typeof data !== "object") return "";
|
|
|
|
|
|
return data._id || data.id || getResponseId(data.data);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function linkBenefitTodos(entry, treatmentRecordId, userId) {
|
|
|
|
|
|
if (!entry.todoEnabled) return;
|
|
|
|
|
|
const config = entry.todoConfig;
|
|
|
|
|
|
const todoIds = [];
|
|
|
|
|
|
const quantity = Math.max(1, Number(entry.usageCount || 1));
|
|
|
|
|
|
for (let sequence = 1; sequence <= quantity; sequence += 1) {
|
|
|
|
|
|
const res = await api("createTodo", {
|
|
|
|
|
|
corpId: corpId.value,
|
|
|
|
|
|
params: {
|
|
|
|
|
|
corpId: corpId.value,
|
|
|
|
|
|
customerId: patientId.value,
|
|
|
|
|
|
customerName: patientName.value,
|
|
|
|
|
|
taskContent: String(config.taskContent).trim(),
|
|
|
|
|
|
eventType: config.eventType,
|
|
|
|
|
|
eventStatus: "untreated",
|
|
|
|
|
|
executeMethod: "todo",
|
|
|
|
|
|
plannedExecutionTime: new Date(`${config.planDate}T00:00:00`).getTime(),
|
|
|
|
|
|
expireTime: new Date(`${config.expireDate}T23:59:59`).getTime(),
|
|
|
|
|
|
executorUserId: config.executorUserId,
|
|
|
|
|
|
executeTeamId: config.executeTeamId,
|
|
|
|
|
|
executeTeamName: config.executeTeamName,
|
|
|
|
|
|
creatorUserId: userId,
|
|
|
|
|
|
benefitRecordId: treatmentRecordId,
|
|
|
|
|
|
benefitProjectId: entry.project._id,
|
|
|
|
|
|
benefitUsageCount: 1,
|
|
|
|
|
|
benefitTotalUsageCount: quantity,
|
|
|
|
|
|
taskQuantity: 1,
|
|
|
|
|
|
taskSequence: sequence,
|
|
|
|
|
|
benefitDeductOnComplete: Boolean(config.deductBenefit),
|
|
|
|
|
|
},
|
|
|
|
|
|
}, false);
|
|
|
|
|
|
if (!res?.success) throw new Error(res?.message || "待办生成失败");
|
|
|
|
|
|
const todoId = getResponseId(res?.data || res);
|
|
|
|
|
|
if (!todoId) throw new Error("待办生成成功但未返回待办ID");
|
|
|
|
|
|
todoIds.push(todoId);
|
|
|
|
|
|
}
|
|
|
|
|
|
const updateRes = await api("updateTreatmentRecord", {
|
|
|
|
|
|
corpId: corpId.value,
|
|
|
|
|
|
id: treatmentRecordId,
|
|
|
|
|
|
params: {
|
|
|
|
|
|
benefitTodoId: todoIds[0],
|
|
|
|
|
|
benefitTodoIds: todoIds,
|
|
|
|
|
|
benefitTodoConfig: {
|
|
|
|
|
|
benefitDeductOnComplete: Boolean(config.deductBenefit),
|
|
|
|
|
|
plannedExecutionTime: new Date(`${config.planDate}T00:00:00`).getTime(),
|
|
|
|
|
|
expireTime: new Date(`${config.expireDate}T23:59:59`).getTime(),
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
}, false);
|
|
|
|
|
|
if (!updateRes?.success) throw new Error(updateRes?.message || "权益待办关联失败");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-14 16:49:54 +08:00
|
|
|
|
function buildBenefitMessage(createdEntries) {
|
|
|
|
|
|
const items = createdEntries.map((entry) => ({
|
|
|
|
|
|
projectId: entry.project._id,
|
|
|
|
|
|
projectName: entry.project.projectName,
|
|
|
|
|
|
usageCount: Number(entry.usageCount || 0),
|
|
|
|
|
|
validTime: entry.validTime ? new Date(`${entry.validTime} 23:59:59`).getTime() : null,
|
|
|
|
|
|
validTimeText: entry.validTime || "无限期",
|
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
conversationID: groupId.value ? `GROUP${groupId.value}` : "",
|
|
|
|
|
|
groupId: groupId.value,
|
|
|
|
|
|
createdAt: Date.now(),
|
|
|
|
|
|
messages: [
|
|
|
|
|
|
{
|
|
|
|
|
|
type: "benefit",
|
|
|
|
|
|
content: {
|
|
|
|
|
|
title: "医生为您录入专属权益",
|
|
|
|
|
|
patientId: patientId.value,
|
|
|
|
|
|
patientName: patientName.value,
|
|
|
|
|
|
corpId: corpId.value,
|
|
|
|
|
|
teamId: teamId.value,
|
|
|
|
|
|
items,
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
],
|
|
|
|
|
|
context: {
|
|
|
|
|
|
userId: accountStore.doctorInfo?.userid || accountStore.account?.userid || "",
|
|
|
|
|
|
customerId: patientId.value,
|
|
|
|
|
|
customerName: patientName.value,
|
|
|
|
|
|
corpId: corpId.value,
|
|
|
|
|
|
teamId: teamId.value,
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function queueBenefitMessage(createdEntries) {
|
|
|
|
|
|
if (!groupId.value || !createdEntries.length) return;
|
|
|
|
|
|
uni.setStorageSync(PENDING_FOLLOWUP_SEND_STORAGE_KEY, buildBenefitMessage(createdEntries));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-11 10:24:25 +08:00
|
|
|
|
async function submitEntries() {
|
|
|
|
|
|
if (saving.value) return;
|
|
|
|
|
|
const errorMessage = validateEntries();
|
|
|
|
|
|
if (errorMessage) {
|
|
|
|
|
|
uni.showToast({ title: errorMessage, icon: "none" });
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
saving.value = true;
|
|
|
|
|
|
const userId = accountStore.doctorInfo?.userid || accountStore.account?.userid || "";
|
|
|
|
|
|
try {
|
2026-08-14 16:49:54 +08:00
|
|
|
|
const createdEntries = [];
|
2026-08-27 14:10:42 +08:00
|
|
|
|
const submitEntries = entries.value.flatMap((entry) => {
|
|
|
|
|
|
if (!entry.isPackage) return [entry];
|
|
|
|
|
|
return entry.projects.filter((item) => item.selected).map((item) => ({
|
|
|
|
|
|
...item,
|
|
|
|
|
|
packageId: entry.packageId,
|
|
|
|
|
|
packageName: entry.packageName,
|
|
|
|
|
|
packageType: entry.packageType,
|
|
|
|
|
|
packagePrice: Number(entry.packagePrice || 0),
|
|
|
|
|
|
}));
|
|
|
|
|
|
});
|
|
|
|
|
|
for (const entry of submitEntries) {
|
2026-08-11 10:24:25 +08:00
|
|
|
|
const count = Number(entry.usageCount);
|
2026-08-11 17:28:46 +08:00
|
|
|
|
const price = Number(entry.price);
|
|
|
|
|
|
const discount = Number(entry.discount);
|
2026-08-14 16:49:54 +08:00
|
|
|
|
const totalPrice = entry.isFree ? 0 : Number(entry.totalPrice || 0);
|
2026-08-11 10:24:25 +08:00
|
|
|
|
const treatmentData = {
|
|
|
|
|
|
customerId: patientId.value,
|
|
|
|
|
|
customerName: patientName.value,
|
|
|
|
|
|
projectId: entry.project._id,
|
|
|
|
|
|
projectName: entry.project.projectName,
|
|
|
|
|
|
usageCount: count,
|
|
|
|
|
|
restUsageCount: count,
|
|
|
|
|
|
price,
|
2026-08-14 16:49:54 +08:00
|
|
|
|
totalPrice,
|
2026-08-11 17:28:46 +08:00
|
|
|
|
discount,
|
2026-08-14 16:49:54 +08:00
|
|
|
|
isFree: Boolean(entry.isFree),
|
2026-08-11 10:24:25 +08:00
|
|
|
|
treatmentDeptName: entry.dept.deptName,
|
|
|
|
|
|
treatmentDeptId: entry.dept._id,
|
|
|
|
|
|
treatmentDept_id: entry.dept._id,
|
2026-08-11 17:28:46 +08:00
|
|
|
|
treatmentDoctorUserId: entry.treatmentDoctorUserId,
|
2026-08-11 10:24:25 +08:00
|
|
|
|
treatmentStatus: "init",
|
|
|
|
|
|
deductUsageCount: 0,
|
|
|
|
|
|
billType: "门诊",
|
|
|
|
|
|
billdCreator: userId,
|
|
|
|
|
|
createTreatementTime: Date.now(),
|
|
|
|
|
|
billTime: Date.now(),
|
2026-08-14 16:49:54 +08:00
|
|
|
|
validTime: entry.validTime ? new Date(`${entry.validTime} 23:59:59`).getTime() : null,
|
2026-08-27 14:10:42 +08:00
|
|
|
|
hasValidDays: Boolean(entry.hasValidDays),
|
|
|
|
|
|
validDays: Number(entry.validDays || 0),
|
|
|
|
|
|
packageId: entry.packageId || "",
|
|
|
|
|
|
packageName: entry.packageName || "",
|
|
|
|
|
|
packageType: entry.packageType || "",
|
|
|
|
|
|
packagePrice: Number(entry.packagePrice || 0),
|
2026-08-11 10:24:25 +08:00
|
|
|
|
teamId: teamId.value,
|
|
|
|
|
|
corpId: corpId.value,
|
|
|
|
|
|
};
|
2026-08-27 14:47:13 +08:00
|
|
|
|
const res = await api("addTreatmentRecord", { corpId: corpId.value, userId, operationSource: "wxApp", params: treatmentData }, false);
|
2026-08-11 17:28:46 +08:00
|
|
|
|
if (!res?.success) throw new Error(res?.message || "权益录入失败");
|
2026-08-27 14:47:13 +08:00
|
|
|
|
const treatmentRecordId = getResponseId(res?.data || res);
|
|
|
|
|
|
if (!treatmentRecordId) throw new Error("权益录入成功但未返回权益记录ID");
|
|
|
|
|
|
await linkBenefitTodos(entry, treatmentRecordId, userId);
|
2026-08-14 16:49:54 +08:00
|
|
|
|
createdEntries.push(entry);
|
2026-08-11 10:24:25 +08:00
|
|
|
|
}
|
2026-08-14 16:49:54 +08:00
|
|
|
|
queueBenefitMessage(createdEntries);
|
2026-08-11 17:28:46 +08:00
|
|
|
|
uni.showToast({ title: "权益录入成功", icon: "success" });
|
2026-08-27 14:10:42 +08:00
|
|
|
|
setTimeout(goBackToConversation, 500);
|
2026-08-11 10:24:25 +08:00
|
|
|
|
} catch (error) {
|
2026-08-11 17:28:46 +08:00
|
|
|
|
uni.showToast({ title: error.message || "权益录入失败", icon: "none" });
|
2026-08-11 10:24:25 +08:00
|
|
|
|
} finally {
|
|
|
|
|
|
saving.value = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped lang="scss">
|
|
|
|
|
|
.benefit-page-shell {
|
|
|
|
|
|
background: #f5f6f8;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.benefit-page {
|
|
|
|
|
|
min-height: 100%;
|
2026-08-14 16:49:54 +08:00
|
|
|
|
padding: 20rpx 24rpx 32rpx;
|
2026-08-11 10:24:25 +08:00
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.patient-card {
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: stretch;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
border: 1rpx solid #e5e7eb;
|
|
|
|
|
|
border-radius: 20rpx;
|
|
|
|
|
|
padding: 24rpx;
|
|
|
|
|
|
margin-bottom: 20rpx;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
box-shadow: 0 10rpx 28rpx rgba(15, 23, 42, 0.06);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.patient-card-accent {
|
|
|
|
|
|
width: 8rpx;
|
|
|
|
|
|
border-radius: 999rpx;
|
|
|
|
|
|
background: #d1d5db;
|
|
|
|
|
|
margin-right: 18rpx;
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.patient-card-main {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.patient-card-top {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
gap: 16rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.patient-name-wrap {
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
gap: 12rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.patient-name {
|
|
|
|
|
|
color: #1f2937;
|
|
|
|
|
|
font-size: 36rpx;
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
line-height: 1.2;
|
|
|
|
|
|
max-width: 420rpx;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.patient-badge {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
height: 36rpx;
|
|
|
|
|
|
padding: 0 14rpx;
|
|
|
|
|
|
border-radius: 999rpx;
|
|
|
|
|
|
background: #f3f4f6;
|
|
|
|
|
|
color: #6b7280;
|
|
|
|
|
|
font-size: 22rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.patient-status {
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
height: 36rpx;
|
|
|
|
|
|
line-height: 36rpx;
|
|
|
|
|
|
padding: 0 12rpx;
|
|
|
|
|
|
border-radius: 999rpx;
|
|
|
|
|
|
background: #f3f4f6;
|
|
|
|
|
|
color: #6b7280;
|
|
|
|
|
|
font-size: 22rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.patient-meta {
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
margin-top: 20rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.patient-meta-item {
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
padding: 14rpx 16rpx;
|
|
|
|
|
|
border-radius: 12rpx;
|
|
|
|
|
|
background: #f8f9fb;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.patient-meta-label {
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
color: #9ca3af;
|
|
|
|
|
|
font-size: 20rpx;
|
|
|
|
|
|
line-height: 1.2;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.patient-meta-value {
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
margin-top: 6rpx;
|
|
|
|
|
|
color: #6b7280;
|
|
|
|
|
|
font-size: 24rpx;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
line-height: 1.35;
|
|
|
|
|
|
word-break: break-all;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.section-title {
|
|
|
|
|
|
margin: 12rpx 4rpx 16rpx;
|
|
|
|
|
|
color: #1f2937;
|
|
|
|
|
|
font-size: 28rpx;
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.entry-card {
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
border-radius: 18rpx;
|
|
|
|
|
|
padding: 24rpx;
|
|
|
|
|
|
margin-bottom: 20rpx;
|
|
|
|
|
|
box-shadow: 0 10rpx 28rpx rgba(15, 23, 42, 0.05);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-27 14:10:42 +08:00
|
|
|
|
.package-card-head {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
gap: 20rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.package-name {
|
|
|
|
|
|
color: #111827;
|
|
|
|
|
|
font-size: 30rpx;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
line-height: 1.4;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.package-price {
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
color: #e85c2f;
|
|
|
|
|
|
font-size: 30rpx;
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.package-summary {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
gap: 10rpx 20rpx;
|
|
|
|
|
|
margin-top: 18rpx;
|
|
|
|
|
|
padding: 16rpx;
|
|
|
|
|
|
border-radius: 12rpx;
|
|
|
|
|
|
background: #f5f7fa;
|
|
|
|
|
|
color: #606266;
|
|
|
|
|
|
font-size: 24rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.package-project-card {
|
|
|
|
|
|
margin-top: 20rpx;
|
|
|
|
|
|
padding: 20rpx;
|
|
|
|
|
|
border: 1rpx solid #e5e7eb;
|
|
|
|
|
|
border-radius: 14rpx;
|
|
|
|
|
|
background: #fafbfc;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.package-project-title {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 10rpx;
|
|
|
|
|
|
color: #1f2937;
|
|
|
|
|
|
font-size: 28rpx;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-01 15:42:31 +08:00
|
|
|
|
.required-checkbox {
|
|
|
|
|
|
width: 42rpx;
|
|
|
|
|
|
height: 42rpx;
|
|
|
|
|
|
margin-right: 4rpx;
|
|
|
|
|
|
border: 3rpx solid #0877f1;
|
|
|
|
|
|
border-radius: 8rpx;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.required-project-card {
|
|
|
|
|
|
border: 2rpx solid #0877f1;
|
|
|
|
|
|
background: #f5f9ff;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.required-badge {
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
padding: 4rpx 10rpx;
|
|
|
|
|
|
border: 2rpx solid #0877f1;
|
|
|
|
|
|
border-radius: 6rpx;
|
|
|
|
|
|
color: #0877f1;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
font-size: 22rpx;
|
|
|
|
|
|
line-height: 28rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-27 14:10:42 +08:00
|
|
|
|
.package-project-detail {
|
|
|
|
|
|
margin-top: 10rpx;
|
|
|
|
|
|
color: #6b7280;
|
|
|
|
|
|
font-size: 24rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-27 14:47:13 +08:00
|
|
|
|
.todo-switch-row,
|
|
|
|
|
|
.todo-deduct-row {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
margin-top: 20rpx;
|
|
|
|
|
|
color: #374151;
|
|
|
|
|
|
font-size: 26rpx;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.todo-config-card {
|
|
|
|
|
|
margin-top: 16rpx;
|
|
|
|
|
|
padding: 20rpx;
|
|
|
|
|
|
border: 1rpx solid #dbe5f0;
|
|
|
|
|
|
border-radius: 14rpx;
|
|
|
|
|
|
background: #f7f9fc;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.todo-config-card .project-input {
|
|
|
|
|
|
margin-top: 16rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.todo-input {
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 76rpx;
|
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
|
padding: 0 20rpx;
|
|
|
|
|
|
border: 1rpx solid #d1d5db;
|
|
|
|
|
|
border-radius: 12rpx;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
color: #111827;
|
|
|
|
|
|
font-size: 28rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.todo-executor-picker {
|
2026-09-01 14:46:32 +08:00
|
|
|
|
position: fixed;
|
|
|
|
|
|
right: 0;
|
|
|
|
|
|
bottom: var(--keyboard-height);
|
|
|
|
|
|
left: 0;
|
2026-08-27 14:47:13 +08:00
|
|
|
|
width: 100%;
|
2026-09-01 14:46:32 +08:00
|
|
|
|
height: 72vh;
|
|
|
|
|
|
max-height: calc(100vh - var(--keyboard-height));
|
2026-08-27 14:47:13 +08:00
|
|
|
|
box-sizing: border-box;
|
2026-09-01 14:46:32 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
min-height: 0;
|
|
|
|
|
|
overflow: hidden;
|
2026-08-27 14:47:13 +08:00
|
|
|
|
padding: 24rpx;
|
|
|
|
|
|
border-radius: 24rpx 24rpx 0 0;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-01 14:46:32 +08:00
|
|
|
|
.todo-executor-search {
|
|
|
|
|
|
flex: 0 0 64rpx;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 64rpx;
|
|
|
|
|
|
min-height: 64rpx;
|
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 12rpx;
|
|
|
|
|
|
margin: 20rpx 0;
|
|
|
|
|
|
padding: 0 20rpx;
|
|
|
|
|
|
border-radius: 10rpx;
|
|
|
|
|
|
background: #f5f6f8;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.todo-executor-search-input {
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
width: 0;
|
|
|
|
|
|
height: 64rpx;
|
|
|
|
|
|
min-height: 64rpx;
|
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
padding: 0;
|
|
|
|
|
|
border: 0;
|
|
|
|
|
|
background: transparent;
|
|
|
|
|
|
font-size: 28rpx;
|
|
|
|
|
|
line-height: 64rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-27 14:47:13 +08:00
|
|
|
|
.todo-executor-list {
|
2026-09-01 14:46:32 +08:00
|
|
|
|
flex: 1;
|
|
|
|
|
|
min-height: 0;
|
|
|
|
|
|
height: auto;
|
|
|
|
|
|
max-height: none;
|
2026-08-27 14:47:13 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.todo-team-group {
|
|
|
|
|
|
margin-top: 16rpx;
|
|
|
|
|
|
border: 1rpx solid #e5e7eb;
|
|
|
|
|
|
border-radius: 12rpx;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.todo-team-title {
|
|
|
|
|
|
padding: 16rpx 20rpx;
|
|
|
|
|
|
background: #f3f6fa;
|
|
|
|
|
|
color: #374151;
|
|
|
|
|
|
font-size: 26rpx;
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.todo-executor-item {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
min-height: 76rpx;
|
|
|
|
|
|
padding: 0 20rpx;
|
|
|
|
|
|
border-top: 1rpx solid #f0f2f5;
|
|
|
|
|
|
color: #1f2937;
|
|
|
|
|
|
font-size: 26rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.todo-selected {
|
|
|
|
|
|
color: #0877f1;
|
|
|
|
|
|
font-size: 22rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.todo-empty {
|
|
|
|
|
|
padding: 24rpx 20rpx;
|
|
|
|
|
|
color: #9ca3af;
|
|
|
|
|
|
font-size: 24rpx;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-27 14:10:42 +08:00
|
|
|
|
.readonly-input {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
height: 76rpx;
|
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
|
padding: 0 20rpx;
|
|
|
|
|
|
border-radius: 12rpx;
|
|
|
|
|
|
background: #f3f4f6;
|
|
|
|
|
|
color: #606266;
|
|
|
|
|
|
font-size: 28rpx;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-11 10:24:25 +08:00
|
|
|
|
.field-label {
|
|
|
|
|
|
margin-bottom: 12rpx;
|
|
|
|
|
|
color: #374151;
|
|
|
|
|
|
font-size: 26rpx;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.project-input,
|
|
|
|
|
|
.picker-value {
|
|
|
|
|
|
min-height: 76rpx;
|
|
|
|
|
|
box-sizing: border-box;
|
2026-08-14 16:49:54 +08:00
|
|
|
|
border: 1rpx solid #d1d5db;
|
2026-08-11 10:24:25 +08:00
|
|
|
|
border-radius: 12rpx;
|
|
|
|
|
|
padding: 0 20rpx;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
background: #fff;
|
2026-08-14 16:49:54 +08:00
|
|
|
|
color: #111827;
|
2026-08-11 10:24:25 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-01 15:42:31 +08:00
|
|
|
|
.project-input.required-select {
|
|
|
|
|
|
border: 2rpx solid #0877f1;
|
|
|
|
|
|
background: #f5f9ff;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-17 17:39:18 +08:00
|
|
|
|
.number-input {
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 76rpx;
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
padding: 0 20rpx;
|
|
|
|
|
|
border: 1rpx solid #b8c1cc;
|
|
|
|
|
|
border-radius: 12rpx;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
color: #1f2937;
|
|
|
|
|
|
-webkit-text-fill-color: #1f2937;
|
|
|
|
|
|
caret-color: #0877f1;
|
|
|
|
|
|
font-size: 28rpx;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
line-height: 76rpx;
|
|
|
|
|
|
opacity: 1;
|
|
|
|
|
|
text-align: left;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-11 17:28:46 +08:00
|
|
|
|
.locked-input {
|
|
|
|
|
|
background: #f3f4f6;
|
|
|
|
|
|
color: #9ca3af;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-11 10:24:25 +08:00
|
|
|
|
.value-text {
|
|
|
|
|
|
color: #111827;
|
|
|
|
|
|
font-size: 28rpx;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.placeholder-text {
|
|
|
|
|
|
color: #9ca3af;
|
|
|
|
|
|
font-size: 28rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-01 14:46:32 +08:00
|
|
|
|
.discount-picker {
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.discount-picker .number-input {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-03 17:58:02 +08:00
|
|
|
|
.discount-picker.discount-disabled {
|
|
|
|
|
|
opacity: 0.62;
|
|
|
|
|
|
pointer-events: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.discount-picker.discount-disabled .number-input {
|
|
|
|
|
|
border-color: #d1d5db;
|
|
|
|
|
|
background: #f3f4f6;
|
|
|
|
|
|
color: #9ca3af;
|
|
|
|
|
|
-webkit-text-fill-color: #9ca3af;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-11 10:24:25 +08:00
|
|
|
|
.field-row {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 20rpx;
|
|
|
|
|
|
margin-top: 20rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-11 17:28:46 +08:00
|
|
|
|
.field-group {
|
|
|
|
|
|
margin-top: 20rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-11 10:24:25 +08:00
|
|
|
|
.field-block {
|
|
|
|
|
|
flex: 1;
|
2026-08-14 16:49:54 +08:00
|
|
|
|
min-width: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.field-block-full {
|
|
|
|
|
|
flex-basis: 100%;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.discount-input-wrap {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 12rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.discount-input-wrap .number-input {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.free-check {
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.free-check-label {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 4rpx;
|
|
|
|
|
|
color: #374151;
|
|
|
|
|
|
font-size: 24rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.valid-time-wrap {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 12rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.valid-time-picker {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-01 17:42:46 +08:00
|
|
|
|
.disabled-picker {
|
|
|
|
|
|
opacity: 0.65;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.valid-time-locked-tip {
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
color: #9ca3af;
|
|
|
|
|
|
font-size: 24rpx;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-14 16:49:54 +08:00
|
|
|
|
.unlimited-button {
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
height: 76rpx;
|
|
|
|
|
|
line-height: 76rpx;
|
|
|
|
|
|
padding: 0 20rpx;
|
|
|
|
|
|
border: 1rpx solid #d1d5db;
|
|
|
|
|
|
border-radius: 12rpx;
|
|
|
|
|
|
color: #0877f1;
|
|
|
|
|
|
font-size: 26rpx;
|
|
|
|
|
|
background: #f8fbff;
|
2026-08-11 10:24:25 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-11 17:28:46 +08:00
|
|
|
|
.total-price {
|
|
|
|
|
|
min-height: 40rpx;
|
|
|
|
|
|
color: #e85c2f;
|
|
|
|
|
|
font-size: 30rpx;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
line-height: 40rpx;
|
2026-08-11 10:24:25 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.number-input {
|
2026-08-17 17:39:18 +08:00
|
|
|
|
overflow: visible;
|
2026-08-14 16:49:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.number-input[disabled] {
|
2026-08-17 17:39:18 +08:00
|
|
|
|
color: #374151;
|
|
|
|
|
|
-webkit-text-fill-color: #374151;
|
2026-08-14 16:49:54 +08:00
|
|
|
|
background: #f3f4f6;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.total-input {
|
|
|
|
|
|
color: #c2410c;
|
|
|
|
|
|
font-weight: 600;
|
2026-08-11 10:24:25 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.add-entry {
|
|
|
|
|
|
height: 80rpx;
|
|
|
|
|
|
border: 1rpx dashed #0877f1;
|
|
|
|
|
|
border-radius: 14rpx;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 8rpx;
|
|
|
|
|
|
color: #0877f1;
|
|
|
|
|
|
font-size: 28rpx;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
margin-top: 8rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.remove-entry {
|
|
|
|
|
|
margin-top: 20rpx;
|
|
|
|
|
|
color: #fa5151;
|
|
|
|
|
|
font-size: 26rpx;
|
|
|
|
|
|
text-align: right;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.footer-bar {
|
2026-09-03 17:58:02 +08:00
|
|
|
|
position: relative;
|
2026-08-11 10:24:25 +08:00
|
|
|
|
display: flex;
|
2026-09-03 17:58:02 +08:00
|
|
|
|
align-items: center;
|
2026-08-11 10:24:25 +08:00
|
|
|
|
gap: 20rpx;
|
2026-09-03 17:58:02 +08:00
|
|
|
|
padding: 14rpx 24rpx 18rpx;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
border-top: 1rpx solid #eef2f7;
|
|
|
|
|
|
box-shadow: 0 -4rpx 16rpx rgba(15, 23, 42, 0.04);
|
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
|
z-index: 10;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.footer-summary {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 14rpx;
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.total-summary {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: row;
|
|
|
|
|
|
align-items: baseline;
|
|
|
|
|
|
gap: 6rpx;
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.summary-label {
|
|
|
|
|
|
color: #6b7280;
|
|
|
|
|
|
font-size: 22rpx;
|
|
|
|
|
|
line-height: 1.2;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.summary-saving {
|
|
|
|
|
|
color: #16a34a;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.summary-total-value {
|
|
|
|
|
|
color: #e85c2f;
|
|
|
|
|
|
font-size: 32rpx;
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
line-height: 1.2;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.detail-trigger {
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
padding-left: 14rpx;
|
|
|
|
|
|
border-left: 1rpx solid #edf0f3;
|
|
|
|
|
|
color: #6b7280;
|
|
|
|
|
|
font-size: 24rpx;
|
|
|
|
|
|
line-height: 1.2;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.benefit-detail-panel {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
right: 0;
|
|
|
|
|
|
bottom: 100%;
|
|
|
|
|
|
left: 0;
|
|
|
|
|
|
max-height: calc(100vh - 220rpx);
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
padding: 20rpx 24rpx 18rpx;
|
2026-08-11 10:24:25 +08:00
|
|
|
|
border-top: 1rpx solid #eef2f7;
|
2026-09-03 17:58:02 +08:00
|
|
|
|
border-radius: 24rpx 24rpx 0 0;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
box-shadow: 0 -8rpx 24rpx rgba(15, 23, 42, 0.08);
|
2026-08-11 10:24:25 +08:00
|
|
|
|
box-sizing: border-box;
|
2026-09-03 17:58:02 +08:00
|
|
|
|
opacity: 0;
|
|
|
|
|
|
visibility: hidden;
|
|
|
|
|
|
pointer-events: none;
|
|
|
|
|
|
transform: translateY(100%);
|
|
|
|
|
|
transform-origin: bottom center;
|
|
|
|
|
|
transition:
|
|
|
|
|
|
opacity 180ms ease,
|
|
|
|
|
|
transform 180ms ease,
|
|
|
|
|
|
visibility 180ms ease;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.benefit-detail-panel-visible {
|
|
|
|
|
|
opacity: 1;
|
|
|
|
|
|
visibility: visible;
|
|
|
|
|
|
pointer-events: auto;
|
|
|
|
|
|
transform: translateY(0);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.detail-header,
|
|
|
|
|
|
.detail-row {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 12rpx;
|
|
|
|
|
|
min-height: 60rpx;
|
|
|
|
|
|
color: #6b7280;
|
|
|
|
|
|
font-size: 27rpx;
|
2026-08-11 10:24:25 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-03 17:58:02 +08:00
|
|
|
|
.detail-header {
|
|
|
|
|
|
min-height: 48rpx;
|
|
|
|
|
|
color: #9ca3af;
|
|
|
|
|
|
font-size: 24rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.detail-project-name {
|
2026-08-11 10:24:25 +08:00
|
|
|
|
flex: 1;
|
2026-09-03 17:58:02 +08:00
|
|
|
|
min-width: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.detail-project-name-value {
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
color: #374151;
|
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.detail-quantity,
|
|
|
|
|
|
.detail-discount {
|
|
|
|
|
|
flex: 0 0 72rpx;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.detail-amount {
|
|
|
|
|
|
flex: 0 0 130rpx;
|
|
|
|
|
|
color: #374151;
|
|
|
|
|
|
text-align: right;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.detail-empty {
|
|
|
|
|
|
padding: 22rpx 0;
|
|
|
|
|
|
color: #9ca3af;
|
|
|
|
|
|
font-size: 26rpx;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.detail-row-total {
|
|
|
|
|
|
margin-top: 8rpx;
|
|
|
|
|
|
padding-top: 14rpx;
|
|
|
|
|
|
border-top: 1rpx solid #eef2f7;
|
|
|
|
|
|
color: #1f2937;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.detail-row-total .detail-amount {
|
|
|
|
|
|
color: #e85c2f;
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.footer-actions {
|
|
|
|
|
|
flex: 0 0 250rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.footer-actions button {
|
|
|
|
|
|
width: 100%;
|
2026-08-11 10:24:25 +08:00
|
|
|
|
min-height: 88rpx;
|
|
|
|
|
|
padding: 0;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
border-radius: 14rpx;
|
|
|
|
|
|
font-size: 30rpx;
|
|
|
|
|
|
line-height: 1.2;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.submit-button {
|
|
|
|
|
|
color: #fff;
|
|
|
|
|
|
background: linear-gradient(135deg, #0f84ff 0%, #0877f1 100%);
|
|
|
|
|
|
box-shadow: 0 10rpx 24rpx rgba(8, 119, 241, 0.22);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.picker-mask {
|
|
|
|
|
|
position: fixed;
|
|
|
|
|
|
inset: 0;
|
2026-09-01 14:46:32 +08:00
|
|
|
|
z-index: 999;
|
2026-08-11 10:24:25 +08:00
|
|
|
|
background: rgba(0, 0, 0, 0.4);
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: flex-end;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.project-picker {
|
2026-09-01 14:46:32 +08:00
|
|
|
|
position: fixed;
|
|
|
|
|
|
right: 0;
|
|
|
|
|
|
left: 0;
|
|
|
|
|
|
bottom: var(--keyboard-height);
|
2026-08-11 10:24:25 +08:00
|
|
|
|
width: 100%;
|
2026-09-01 14:46:32 +08:00
|
|
|
|
height: 75vh;
|
|
|
|
|
|
max-height: calc(100vh - var(--keyboard-height));
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
min-height: 0;
|
|
|
|
|
|
overflow: hidden;
|
2026-08-11 10:24:25 +08:00
|
|
|
|
background: #fff;
|
|
|
|
|
|
border-radius: 24rpx 24rpx 0 0;
|
|
|
|
|
|
padding: 24rpx;
|
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.picker-header {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.picker-title {
|
|
|
|
|
|
color: #222;
|
|
|
|
|
|
font-size: 32rpx;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.search-box {
|
2026-09-01 14:46:32 +08:00
|
|
|
|
flex: 0 0 64rpx;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 64rpx;
|
|
|
|
|
|
min-height: 64rpx;
|
|
|
|
|
|
box-sizing: border-box;
|
2026-08-11 10:24:25 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 12rpx;
|
|
|
|
|
|
background: #f5f6f8;
|
|
|
|
|
|
border-radius: 10rpx;
|
|
|
|
|
|
padding: 0 20rpx;
|
|
|
|
|
|
margin: 20rpx 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.search-input {
|
2026-09-01 14:46:32 +08:00
|
|
|
|
display: block;
|
2026-08-11 10:24:25 +08:00
|
|
|
|
flex: 1;
|
2026-09-01 14:46:32 +08:00
|
|
|
|
width: 0;
|
|
|
|
|
|
height: 64rpx;
|
|
|
|
|
|
min-height: 64rpx;
|
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
padding: 0;
|
|
|
|
|
|
border: 0;
|
|
|
|
|
|
background: transparent;
|
2026-08-11 10:24:25 +08:00
|
|
|
|
font-size: 28rpx;
|
2026-09-01 14:46:32 +08:00
|
|
|
|
line-height: 64rpx;
|
2026-08-11 10:24:25 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.project-list {
|
2026-09-01 14:46:32 +08:00
|
|
|
|
flex: 1;
|
|
|
|
|
|
min-height: 0;
|
|
|
|
|
|
height: auto;
|
|
|
|
|
|
max-height: none;
|
2026-08-11 10:24:25 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-27 14:10:42 +08:00
|
|
|
|
.picker-group-title {
|
|
|
|
|
|
padding: 18rpx 8rpx 8rpx;
|
|
|
|
|
|
color: #374151;
|
|
|
|
|
|
font-size: 26rpx;
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-11 10:24:25 +08:00
|
|
|
|
.project-item {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
2026-08-14 16:49:54 +08:00
|
|
|
|
align-items: flex-start;
|
|
|
|
|
|
gap: 20rpx;
|
2026-08-11 10:24:25 +08:00
|
|
|
|
padding: 24rpx 8rpx;
|
|
|
|
|
|
border-bottom: 1rpx solid #f0f0f0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-14 16:49:54 +08:00
|
|
|
|
.project-info {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-11 10:24:25 +08:00
|
|
|
|
.project-name {
|
|
|
|
|
|
color: #333;
|
|
|
|
|
|
font-size: 28rpx;
|
2026-08-14 16:49:54 +08:00
|
|
|
|
line-height: 1.35;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.project-depts {
|
|
|
|
|
|
margin-top: 8rpx;
|
|
|
|
|
|
color: #8b95a1;
|
|
|
|
|
|
font-size: 24rpx;
|
|
|
|
|
|
line-height: 1.35;
|
|
|
|
|
|
word-break: break-all;
|
2026-08-11 10:24:25 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.project-price {
|
|
|
|
|
|
color: #fa5151;
|
|
|
|
|
|
font-size: 26rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.empty-text {
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
color: #999;
|
|
|
|
|
|
padding: 60rpx 0;
|
|
|
|
|
|
font-size: 28rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|