From 8daf6ce4e509a72c298e8fedf91f4ddecac04ff9 Mon Sep 17 00:00:00 2001 From: zhanchao Date: Thu, 27 Aug 2026 14:10:42 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8A=A0=E5=85=A5=E5=A5=97=E9=A4=90=E6=94=B9?= =?UTF-8?q?=E9=80=A0=E5=B9=B6=E4=B8=94=E5=8A=A0=E5=85=A5=E6=9D=83=E7=9B=8A?= =?UTF-8?q?=E8=AF=A6=E6=83=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pages/case/archive-detail.vue | 8 + .../archive-detail/benefit-project-tab.vue | 451 ++++++++++++++++++ pages/message/benefit-entry.vue | 332 ++++++++++++- pages/message/components/chat-input.vue | 14 +- pages/message/index.vue | 1 + utils/api.js | 3 + 6 files changed, 793 insertions(+), 16 deletions(-) create mode 100644 pages/case/components/archive-detail/benefit-project-tab.vue diff --git a/pages/case/archive-detail.vue b/pages/case/archive-detail.vue index e033313..cf84f4c 100644 --- a/pages/case/archive-detail.vue +++ b/pages/case/archive-detail.vue @@ -118,6 +118,12 @@ :reachBottomTime="reachBottomTime" :floatingBottom="floatingBottom" /> + + + + + {{ item.label }} + + + + + + + {{ item.projectName || '未命名权益项目' }} + + {{ item.statusLabel }} + + 已治疗 + + + + + + + + + 加载中... + 暂无{{ currentType === 'treatment' ? '权益记录' : '治疗明细' }} + + + + + + + + + + + + diff --git a/pages/message/benefit-entry.vue b/pages/message/benefit-entry.vue index 20cc06c..7e0a9ce 100644 --- a/pages/message/benefit-entry.vue +++ b/pages/message/benefit-entry.vue @@ -22,6 +22,71 @@ 权益项目 + + @@ -148,15 +214,16 @@ - 选择权益项目 + 选择权益项目或套餐 - + - 单项目 + {{ project.projectName }} @@ -164,7 +231,12 @@ ¥{{ Number(project.price || 0).toFixed(2) }} - 暂无可用权益项目 + 套餐 + + {{ packageItem.name }}{{ packageItem.packageType === "free" ? `自由组合:${packageItem.projects.length}选${packageItem.projectNumber}` : "固定组合" }} · {{ (packageItem.projects || []).map((item) => item.projectName).join("、") }} + ¥{{ Number(packageItem.packagePrice || 0).toFixed(2) }} + + 暂无可用权益项目或套餐 加载中... 已加载全部权益项目 @@ -206,8 +278,10 @@ const projectPickerVisible = ref(false); const projectPickerIndex = ref(-1); const projectKeyword = ref(""); const projectList = ref([]); +const packageList = ref([]); const projectLoading = ref(false); const projectHasMore = ref(true); +const packageHasMore = ref(true); const projectPage = ref(1); const projectPageSize = 30; const VALID_YEAR_SPAN = 50; @@ -413,31 +487,47 @@ function normalizeProject(project = {}) { async function loadProjects({ reset = false } = {}) { if (projectLoading.value) return; - if (!reset && !projectHasMore.value) return; + if (!reset && !projectHasMore.value && !packageHasMore.value) return; const page = reset ? 1 : projectPage.value; projectLoading.value = true; if (reset) { projectList.value = []; + packageList.value = []; projectHasMore.value = true; + packageHasMore.value = true; } try { - const res = await api("getProjectList", { + const [res, packageRes] = await Promise.all([api("getProjectList", { corpId: corpId.value, page, pageSize: projectPageSize, projectName: projectKeyword.value.trim(), projectStatus: "enable", showDepts: true, - }, false); + }, false), api("getProjectPackageList", { + corpId: corpId.value, + page, + pageSize: projectPageSize, + name: projectKeyword.value.trim(), + enable: true, + }, false)]); const list = res?.data?.list || res?.list || res?.data || []; const nextList = Array.isArray(list) ? list.map(normalizeProject) : []; projectList.value = reset ? nextList : projectList.value.concat(nextList); + 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); projectPage.value = page + 1; projectHasMore.value = nextList.length >= projectPageSize; + packageHasMore.value = nextPackages.length >= projectPageSize; } catch (error) { - if (reset) projectList.value = []; + if (reset) { + projectList.value = []; + packageList.value = []; + } projectHasMore.value = false; + packageHasMore.value = false; uni.showToast({ title: "项目加载失败", icon: "none" }); } finally { projectLoading.value = false; @@ -525,6 +615,110 @@ function selectProject(project) { closeProjectPicker(); } +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), + discount: packageItem.packageType === "free" ? 10 : Number(project.discount || 10), + 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, + 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; + syncPackageSelection(entry); + syncPackageTotals(entry); +} + +function calculatePackageTotal(entry) { + entry.usageCount = Math.max(1, Number(entry.usageCount || 1)); + entry.discount = clampNumber(toNumber(entry.discount, 10), 0, 10); + entry.totalPrice = toFixedNumber(Number(entry.price || 0) * entry.usageCount * entry.discount / 10, 2); + syncPackageTotals(entry); +} + +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; + }); +} + function changeDept(event, entry) { entry.dept = getProjectDepts(entry)[Number(event.detail.value)] || null; entry.treatmentDoctorUserId = ""; @@ -576,9 +770,33 @@ function goBack() { uni.navigateBack(); } +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`, + }); +} + function validateEntries() { for (let index = 0; index < entries.value.length; index += 1) { const entry = entries.value[index]; + 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之间`; + 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}请选择治疗科室`; + if (projectEntry.hasValidDays && !projectEntry.validTime) return `${projectEntry.project.projectName}请选择有效日期`; + } + continue; + } const label = `第${index + 1}个权益项目`; if (!entry.project?._id) return `${label}请选择项目`; if (Number(entry.price) < 0 || Number.isNaN(Number(entry.price))) return `${label}价格不能小于0`; @@ -643,7 +861,17 @@ async function submitEntries() { const userId = accountStore.doctorInfo?.userid || accountStore.account?.userid || ""; try { const createdEntries = []; - for (const entry of entries.value) { + 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) { const count = Number(entry.usageCount); const price = Number(entry.price); const discount = Number(entry.discount); @@ -670,16 +898,22 @@ async function submitEntries() { createTreatementTime: Date.now(), billTime: Date.now(), validTime: entry.validTime ? new Date(`${entry.validTime} 23:59:59`).getTime() : null, + hasValidDays: Boolean(entry.hasValidDays), + validDays: Number(entry.validDays || 0), + packageId: entry.packageId || "", + packageName: entry.packageName || "", + packageType: entry.packageType || "", + packagePrice: Number(entry.packagePrice || 0), teamId: teamId.value, corpId: corpId.value, }; - const res = await api("addTreatmentRecord", { params: treatmentData }, false); + const res = await api("addTreatmentRecord", { userId, operationSource: "wxApp", params: treatmentData }, false); if (!res?.success) throw new Error(res?.message || "权益录入失败"); createdEntries.push(entry); } queueBenefitMessage(createdEntries); uni.showToast({ title: "权益录入成功", icon: "success" }); - setTimeout(goBack, 500); + setTimeout(goBackToConversation, 500); } catch (error) { uni.showToast({ title: error.message || "权益录入失败", icon: "none" }); } finally { @@ -817,6 +1051,75 @@ async function submitEntries() { box-shadow: 0 10rpx 28rpx rgba(15, 23, 42, 0.05); } +.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; +} + +.package-project-detail { + margin-top: 10rpx; + color: #6b7280; + font-size: 24rpx; +} + +.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; +} + .field-label { margin-bottom: 12rpx; color: #374151; @@ -1068,6 +1371,13 @@ async function submitEntries() { max-height: 55vh; } +.picker-group-title { + padding: 18rpx 8rpx 8rpx; + color: #374151; + font-size: 26rpx; + font-weight: 700; +} + .project-item { display: flex; justify-content: space-between; diff --git a/pages/message/components/chat-input.vue b/pages/message/components/chat-input.vue index 67249d0..092d5b3 100644 --- a/pages/message/components/chat-input.vue +++ b/pages/message/components/chat-input.vue @@ -90,6 +90,7 @@ const props = defineProps({ corpId: { type: String, default: "" }, orderStatus: { type: String, default: "" }, isGenerating: { type: Boolean, default: false }, + canBenefitEntry: { type: Boolean, default: false }, }); // Emits @@ -481,11 +482,6 @@ const morePanelButtons = computed(() => { icon: "/static/icon/xuanjiaowenzhang.png", action: goToArticleList, }, - { - text: "权益录入", - icon: "/static/icon/quanyi.svg", - action: goToBenefitEntry, - }, { text: "问卷", icon: "/static/icon/wenjuan.png", @@ -493,6 +489,14 @@ const morePanelButtons = computed(() => { }, ]; + if (props.canBenefitEntry) { + buttons.splice(4, 0, { + text: "权益录入", + icon: "/static/icon/quanyi.svg", + action: goToBenefitEntry, + }); + } + // 根据订单状态显示不同的按钮 if (props.orderStatus === "finished") { // 已结束状态:显示"开启会话"按钮 diff --git a/pages/message/index.vue b/pages/message/index.vue index ea49ec0..4e416ba 100644 --- a/pages/message/index.vue +++ b/pages/message/index.vue @@ -157,6 +157,7 @@ :patientInfo="patientInfo" :orderStatus="orderStatus" :isGenerating="isGenerating" + :canBenefitEntry="doctorInfo?.benefitEntryEnabled === true" @scrollToBottom="() => scrollToBottom(true)" @messageSent="() => scrollToBottom(true)" @endConsult="handleEndConsult" diff --git a/utils/api.js b/utils/api.js index 3daa57c..f792b38 100644 --- a/utils/api.js +++ b/utils/api.js @@ -94,6 +94,9 @@ const urlsConfig = { // 健康档案相关接口 addMedicalRecord: 'addMedicalRecord', addTreatmentRecord: "addTreatmentRecord", + getProjectPackageList: "getProjectPackageList", + getTreatmentRecord: "getTreatmentRecord", + getDeductRecord: "getDeductRecord", getMedicalRecordById: 'getMedicalRecordById', updateMedicalRecord: 'updateMedicalRecord', removeMedicalRecord: 'removeMedicalRecord',