From d3003ce0ac4854a43b946581b8dfaf4f82b915db Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 30 Apr 2026 14:04:12 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/settings.local.json | 3 +- admin/src/api/tcm.ts | 14 + .../src/views/consumer/prescription/index.vue | 35 +- .../consumer/prescription/order_list.vue | 464 ++++++++++++++++-- admin/src/views/fans/yeji.vue | 326 ++++++++++++ admin/src/views/finance/mubiao-dept-node.vue | 126 +++-- admin/src/views/finance/mubiao.vue | 390 ++++++++------- admin/src/views/tcm/diagnosis/index.vue | 142 +++++- .../doctor/AppointmentController.php | 4 +- .../controller/tcm/DiagnosisController.php | 20 +- .../tcm/PrescriptionOrderController.php | 20 + .../lists/tcm/PrescriptionOrderLists.php | 8 +- .../logic/doctor/AppointmentLogic.php | 81 ++- .../adminapi/logic/stats/YejiStatsLogic.php | 26 +- .../app/adminapi/logic/tcm/DiagnosisLogic.php | 33 ++ .../adminapi/logic/tcm/PrescriptionLogic.php | 3 + .../logic/tcm/PrescriptionOrderLogic.php | 168 +++++++ .../validate/tcm/DiagnosisValidate.php | 5 + .../tcm/PrescriptionOrderValidate.php | 5 + 19 files changed, 1560 insertions(+), 313 deletions(-) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 367834b4..a366c14b 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -9,7 +9,8 @@ "Bash(python .claude/skills/ui-ux-pro-max/scripts/search.py \"finance admin dashboard SaaS form professional\" --design-system -p \"Finance Target Setting\" -f markdown)", "Bash(python *)", "Bash(php think *)", - "Bash(python3 -c ' *)" + "Bash(python3 -c ' *)", + "Bash(python3 *)" ] } } diff --git a/admin/src/api/tcm.ts b/admin/src/api/tcm.ts index 7f5240d4..db473bea 100644 --- a/admin/src/api/tcm.ts +++ b/admin/src/api/tcm.ts @@ -30,6 +30,11 @@ export function tcmDiagnosisDetail(params: any) { return request.get({ url: '/tcm.diagnosis/detail', params }) } +/** 诊单挂号 / 取消挂号 操作日志 */ +export function tcmDiagnosisGuahaoLogList(params: { id: number }) { + return request.get({ url: '/tcm.diagnosis/guahaoLogList', params }) +} + // 检查手机号是否重复 export function checkPhone(params: any) { return request.post({ url: '/tcm.diagnosis/checkPhone', params }) @@ -335,6 +340,15 @@ export function prescriptionOrderEdit(params: Record) { return request.post({ url: '/tcm.prescriptionOrder/edit', params }) } +/** 业务订单详情:仅修改关联处方的患者姓名与手机号 */ +export function prescriptionOrderPatchPrescriptionPatient(params: { + id: number + patient_name: string + phone: string +}) { + return request.post({ url: '/tcm.prescriptionOrder/patchPrescriptionPatient', params }) +} + export function prescriptionOrderAuditPrescription(params: { id: number action: 'approve' | 'reject' diff --git a/admin/src/views/consumer/prescription/index.vue b/admin/src/views/consumer/prescription/index.vue index 44941ea7..6e5ea20d 100644 --- a/admin/src/views/consumer/prescription/index.vue +++ b/admin/src/views/consumer/prescription/index.vue @@ -437,16 +437,12 @@
{{ slipView.doctor_name || '—' }}
-
审方
-
配药
-
复核
-
发药
-
+
类型: {{ rxTypeText }}
- 剂数: + 天数: {{ slipView.dose_count || '—' }}剂
@@ -454,32 +450,7 @@ {{ rxPerDoseAmount }}克
-
-
- 医药费 - {{ rxFee.medicine }}元 -
-
- 诊金: - {{ rxFee.consult }}元 -
-
- 制作费: - {{ rxFee.making }}元 -
-
- 运费: - {{ rxFee.shipping }}元 -
-
- 优惠: - {{ rxFee.discount }}元 -
-
- 合计: - {{ rxFee.total }}元 -
-
+ diff --git a/admin/src/views/consumer/prescription/order_list.vue b/admin/src/views/consumer/prescription/order_list.vue index 5d719362..63b06a39 100644 --- a/admin/src/views/consumer/prescription/order_list.vue +++ b/admin/src/views/consumer/prescription/order_list.vue @@ -238,7 +238,20 @@ @@ -485,6 +498,29 @@
+ + + +
@@ -577,6 +613,20 @@ 开方人:{{ detailData.doctor_name }} + + 改患者信息 + {{ detailPrescription.gender_desc || '—' }} · {{ detailPrescription.age ?? '—' }}岁 - {{ detailPrescription.phone || '—' }} + + + {{ detailPrescription.phone || '—' }} + + + 与订单手机不一致 + + {{ detailPrescription.prescription_date || '—' }} {{ detailPrescription.doctor_name || '—' }} {{ detailPrescription.prescription_type || '—' }} @@ -814,8 +881,55 @@ {{ detailData.diagnosis_id }} {{ formatTime(detailData.create_time) }} {{ detailData.recipient_name }} - - {{ detailData.recipient_phone }} + + + + 暂无挂号预约记录(未绑定或诊单下无预约) + + + + + {{ detailData.recipient_phone || '—' }} + + + 与处方手机不一致 + + {{ detailFullAddress }} {{ detailData.is_follow_up ? '是' : '否' }} @@ -1611,6 +1725,47 @@ + + + + + + + + + + + + + 服药前请核对姓名、电话、医生等信息以及服法、医嘱等要点 - - 日期:{{ rxDateText }} - 编号:{{ rxSerialText }} - +
+
+ 日期 + {{ rxDateText }} +
+
+ 处方编号 + {{ rxPrescriptionSnText }} +
+
+ 流转编号(挂号) + {{ rxAppointmentFlowText }} +
+
+ 业务单号 + {{ rxBusinessOrderNoText }} +
+
-
@@ -1770,8 +1938,8 @@
-
-
医师
+
+
医师
医师签名 -
{{ prescriptionViewData.doctor_name || '—' }}
+
@@ -1788,11 +1956,17 @@ {{ rxTypeText }}
- 剂数: - {{ prescriptionViewData.dose_count || '—' }}剂 + 天数: + + {{ + detailData.medication_days != null && + String(detailData.medication_days).trim() !== '' + ? detailData.medication_days + ' 天' + : '—' + }}
- 单剂量: + 剂量: {{ rxPerDoseAmount }}克
@@ -1980,6 +2154,7 @@ import { prescriptionOrderComplete, prescriptionOrderRevokeRxAudit, prescriptionOrderRevokePayAudit, + prescriptionOrderPatchPrescriptionPatient, prescriptionOrderLinkPayOrder, prescriptionOrderSubmitGancaoRecipel, prescriptionOrderPreviewGancaoRecipel, @@ -2703,6 +2878,47 @@ const detailPrescription = computed(() => { return p && typeof p === 'object' ? p : null }) +function normalizeBizPhone(v: unknown): string { + if (v === null || v === undefined) return '' + return String(v).replace(/\s/g, '').trim() +} + +function recipientVsPrescriptionPhoneMismatch(recipient: unknown, rxPhone: unknown): boolean { + const a = normalizeBizPhone(recipient) + const b = normalizeBizPhone(rxPhone) + if (!a || !b) return false + return a !== b +} + +/** 详情抽屉:业务收货手机 vs 处方登记手机 */ +const detailRecipientRxPhoneMismatch = computed(() => { + const rx = detailPrescription.value?.phone + const biz = detailData.value?.recipient_phone + return recipientVsPrescriptionPhoneMismatch(biz, rx) +}) + +const detailPrescriptionPhoneDisplay = computed(() => { + const s = normalizeBizPhone(detailPrescription.value?.phone) + return s || '—' +}) + +const detailOrderPhoneDisplay = computed(() => { + const s = normalizeBizPhone(detailData.value?.recipient_phone) + return s || '—' +}) + +const detailLinkedAppointment = computed(() => { + const a = detailData.value?.linked_appointment + return a && typeof a === 'object' ? a : null +}) + +const detailLinkedAppointmentResolvedFromTag = computed(() => { + const from = String(detailLinkedAppointment.value?.resolved_from || '') + if (from === 'prescription') return '处方绑定挂号' + if (from === 'diagnosis') return '诊单关联挂号' + return '' +}) + const detailRxHerbs = computed(() => normalizeSlipHerbs(detailPrescription.value?.herbs)) /** false=无权限;true/缺省兼容旧接口(旧版未下发该字段时仍展示药材) */ @@ -2790,7 +3006,11 @@ function logActionText(act: string) { fill_tracking: '填快递单', ship: '确认发货', withdraw: '撤销', - add_pay_order: '补齐支付单', + link_pay_order: '关联支付单', + revoke_rx_audit: '撤回处方审核', + revoke_pay_audit: '撤回支付审核', + gancao_submit: '甘草下单', + patch_rx_patient: '处方患者信息', complete: '完成订单' } return m[act] || act @@ -2840,6 +3060,75 @@ async function fetchLogisticsTrace() { } } +const patchRxPatientVisible = ref(false) +const patchRxPatientSaving = ref(false) +const patchRxPatientFormRef = ref() +const patchRxPatientForm = reactive({ + patient_name: '', + phone: '' +}) +const patchRxPatientRules: FormRules = { + patient_name: [{ required: true, message: '请输入患者姓名', trigger: 'blur' }], + phone: [{ required: true, message: '请输入手机号', trigger: 'blur' }] +} + +function openPatchRxPatientDialog() { + const rx = detailPrescription.value + const ord = detailData.value + if (!rx || !ord?.id || !ord.prescription_id) { + feedback.msgWarning('无处方数据') + return + } + patchRxPatientForm.patient_name = String(rx.patient_name ?? '').trim() + patchRxPatientForm.phone = String(rx.phone ?? '').trim() + patchRxPatientVisible.value = true + nextTick(() => patchRxPatientFormRef.value?.clearValidate()) +} + +function resetPatchRxPatientForm() { + patchRxPatientForm.patient_name = '' + patchRxPatientForm.phone = '' +} + +async function refreshCurrentPrescriptionOrderDetail() { + const id = detailData.value?.id + if (!id) return + try { + const res: any = await prescriptionOrderDetail({ id }) + const d = res?.data ?? res ?? null + if (d) detailData.value = d + } catch { + /* 静默失败,避免打断当前抽屉 */ + } +} + +async function submitPatchRxPatient() { + const form = patchRxPatientFormRef.value + if (!form) return + try { + await form.validate() + } catch { + return + } + const ordId = detailData.value?.id + if (!ordId) return + patchRxPatientSaving.value = true + try { + await prescriptionOrderPatchPrescriptionPatient({ + id: ordId, + patient_name: patchRxPatientForm.patient_name.trim(), + phone: patchRxPatientForm.phone.trim() + }) + patchRxPatientVisible.value = false + await refreshCurrentPrescriptionOrderDetail() + await fetchLogs(ordId) + } catch (e) { + console.error(e) + } finally { + patchRxPatientSaving.value = false + } +} + async function openDetail(id: number) { // 修复 Bug:显式彻底清空缓存,防止前一次弹窗的数据残留 detailData.value = null @@ -3878,18 +4167,32 @@ const rxDateText = computed(() => { return v.prescription_date || '—' }) -const rxSerialText = computed(() => { +/** 处方笺「处方编号」:处方 sn,缺省 visit_no */ +const rxPrescriptionSnText = computed(() => { const v = prescriptionViewData.value as any if (!v) return '—' - return ( - v.order_no || - v.serial_no || - v.serial_number || - v.no || - v.prescription_no || - v.visit_no || - (v.id ? `G${v.id}` : '—') - ) + const sn = String(v.sn ?? '').trim() + if (sn) return sn + const vn = String(v.visit_no ?? '').trim() + if (vn) return vn + return '—' +}) + +/** 流转编号(挂号):预约/挂号主键 */ +const rxAppointmentFlowText = computed(() => { + const v = prescriptionViewData.value as any + if (!v) return '—' + const aid = Number(v.appointment_id) + if (Number.isFinite(aid) && aid > 0) return String(aid) + return '—' +}) + +/** 业务订单号(从打开详情的订单行合并,便于笺面核对) */ +const rxBusinessOrderNoText = computed(() => { + const v = prescriptionViewData.value as any + if (!v) return '' + const no = String(v.order_no ?? '').trim() + return no }) const rxRecipientText = computed(() => { @@ -4085,6 +4388,22 @@ async function openPrescriptionView(row: any) { if (row.recipient_phone && !merged.recipient_phone) merged.recipient_phone = row.recipient_phone if (row.shipping_address && !merged.shipping_address) merged.shipping_address = row.shipping_address if (row.amount != null && merged.order_amount == null) merged.order_amount = row.amount + /** 笺面剂数/单位与业务订单一致(甘草预览与订单详情同口径) */ + if (row.dose_count != null && String(row.dose_count).trim() !== '' && Number(row.dose_count) > 0) { + merged.dose_count = Number(row.dose_count) + } + if (row.dose_unit != null && String(row.dose_unit).trim() !== '') { + merged.dose_unit = String(row.dose_unit) + } + const linkedAp = row.linked_appointment + if ( + (!merged.appointment_id || Number(merged.appointment_id) <= 0) && + linkedAp && + typeof linkedAp === 'object' && + Number(linkedAp.id) > 0 + ) { + merged.appointment_id = Number(linkedAp.id) + } prescriptionViewData.value = merged } else { prescriptionViewData.value = null @@ -4389,25 +4708,84 @@ async function downloadPrescriptionSlipPdf() { overflow: visible; } -/* === 顶部告知 === */ +/* === 顶部告知(上下分块,避免左右 flex 把左侧提示挤成竖条)=== */ .rx-notice { display: flex; - align-items: center; - justify-content: space-between; + flex-direction: column; + align-items: stretch; + gap: 10px; background: #f3f4f6; border: 1px solid #e5e7eb; - padding: 16px 10px; + padding: 14px 12px; border-radius: 2px; font-size: 12px; color: #1f1f1f; margin-bottom: 6px; - .rx-notice-text { flex: 1; } + .rx-notice-text { + flex: none; + width: 100%; + line-height: 1.55; + word-break: break-word; + } .rx-notice-meta { + display: flex; + flex-flow: row nowrap; + align-items: center; + gap: 4px 14px; + width: 100%; + box-sizing: border-box; + margin-top: 4px; + padding: 8px 2px 2px; + border-top: 1px solid #e5e7eb; + font-size: 10px; + line-height: 2.35; + overflow-x: auto; + overflow-y: hidden; + -webkit-overflow-scrolling: touch; + } + + .rx-notice-meta .rx-meta-item { display: inline-flex; - gap: 24px; + flex: 0 0 auto; + align-items: baseline; + gap: 2px; + min-width: 0; + padding: 0; + margin: 0; + background: transparent; + border: none; + border-radius: 0; white-space: nowrap; + line-height: 2.35; + height:20px; + } + + .rx-notice-meta .rx-meta-item:not(:first-child) { + border-left: 1px solid #d1d5db; + padding-left: 14px; + margin-left: 2px; + line-height: 2.35; + } + + .rx-notice-meta .rx-meta-label { + font-weight: 600; + color: #6b7280; + line-height: 1.35; + flex-shrink: 0; + } + + .rx-notice-meta .rx-meta-label::after { + content: ':'; + font-weight: 500; + color: #9ca3af; + } + + .rx-notice-meta .rx-meta-value { + color: #111827; + line-height: 1.35; + font-variant-numeric: tabular-nums; } } @@ -4663,21 +5041,39 @@ async function downloadPrescriptionSlipPdf() { margin-bottom: 4px; } +.rx-bot-label--doctor { + flex-shrink: 0; + margin-bottom: 0; + margin-right: 6px; + line-height: 1.2; + align-self: center; +} + .rx-bot-doctor { + flex-direction: row; + align-items: center; + flex-wrap: nowrap; + .rx-bot-doctor-body { flex: 1; display: flex; + flex-direction: row; align-items: center; - gap: 6px; - min-height: 36px; + justify-content: flex-start; + flex-wrap: nowrap; + gap: 8px; + min-height: 0; } .rx-bot-doctor-name { font-size: 13px; color: #1f1f1f; + line-height: 1.2; + white-space: nowrap; } .rx-bot-sign-img { + flex-shrink: 0; max-height: 40px; max-width: 110px; object-fit: contain; diff --git a/admin/src/views/fans/yeji.vue b/admin/src/views/fans/yeji.vue index b884844d..5c08eabf 100644 --- a/admin/src/views/fans/yeji.vue +++ b/admin/src/views/fans/yeji.vue @@ -125,6 +125,57 @@ :closable="false" class="filter-alert" /> --> + +
+
+
{{ targetProgressCard.title }}
+
+
+
+ {{ sec.dept_name }}目标 + {{ formatMoneyW(sec.target_amount) }} +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
昨日诊金{{ formatInt(sec.yesterday_amount) }}
抖音总诊金{{ formatInt(sec.douyin_amount) }}
推粉总诊金{{ formatInt(sec.tuifen_amount) }}
总诊金{{ formatInt(sec.total_amount) }}
目标进度{{ formatMoney(sec.target_progress) }}
完成进度{{ formatAchievement(sec.completion_rate) }}
+
+
+
+
+
@@ -349,6 +400,7 @@ import { yejiStatsMulti, yejiStatsOverview, } from '@/api/stats' +import { deptPerformanceTargetMonthMatrix } from '@/api/finance' interface DeptOption { id: number @@ -440,6 +492,169 @@ const customRange = ref<[string, string] | null>(null) const tables = ref([]) const leaderboardBlock = ref(null) const leaderboardsLoading = ref(false) + +// ── 目标进度卡片 ────────────────────────────── +interface TargetProgressSection { + dept_id: number + dept_name: string + target_amount: number + total_amount: number + yesterday_amount: number + douyin_amount: number | null // 名称含「自媒体1」的子部门合计 + tuifen_amount: number | null // 名称含「自媒体3」或「自媒体4」的子部门合计 + target_progress: number // 按已过天数均摊的目标进度 + completion_rate: number | null // 总诊金 / 目标金额 +} +interface TargetProgressCard { + title: string + sections: TargetProgressSection[] +} +const targetProgressCard = ref(null) +const targetProgressLoading = ref(false) + +function ymCurrent(): string { + const d = new Date() + return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}` +} +function daysInMonth(ym: string): number { + const [y, m] = ym.split('-').map(Number) + return new Date(y, m, 0).getDate() +} + +async function loadTargetProgress() { + targetProgressLoading.value = true + targetProgressCard.value = null + try { + const ym = ymCurrent() + const targetRes: any = await deptPerformanceTargetMonthMatrix({ year_month: ym }) + const treeRows: any[] = targetRes?.rows ?? [] + if (!treeRows.length) return + + // 从目标树平铺,找到 depth>=1 且目标>0 的节点作为卡片分区 + function flatWithDepth(nodes: any[], depth = 0): Array<{ node: any; depth: number }> { + const r: Array<{ node: any; depth: number }> = [] + for (const n of nodes ?? []) { + r.push({ node: n, depth }) + r.push(...flatWithDepth(n.children ?? [], depth + 1)) + } + return r + } + const all = flatWithDepth(treeRows) + const cardDepts = all + .filter(x => x.depth >= 1 && Number(x.node.target_amount) > 0) + .map(x => x.node) + if (!cardDepts.length) return + + // 用 deptOptions(完整部门列表,含 pid)做层级查找 + // 目标树只包含有目标的节点,无法得到 0 目标的子部门(如自媒体1/3/4) + function descIdsFromOptions(deptId: number): number[] { + const result: number[] = [] + const queue = [deptId] + while (queue.length) { + const pid = queue.shift()! + for (const d of deptOptions.value) { + if (Number(d.pid) === pid) { + result.push(d.id) + queue.push(d.id) + } + } + } + return result + } + + const allIds = new Set() + const specs = cardDepts.map(dept => { + const deptId = Number(dept.dept_id) + const childIds = descIdsFromOptions(deptId) + const totalIds = childIds.length ? childIds : [deptId] + totalIds.forEach(id => allIds.add(id)) + return { + dept_id: deptId, + dept_name: String(dept.dept_name), + target_amount: Number(dept.target_amount), + totalIds, + } + }) + + if (!allIds.size) return + + // 从 channelGroups 找自媒体1/3/4 的 channel_code + const allChannels = channelGroups.value.flatMap(g => g.channels) + const douyinCode = allChannels.find(c => c.channel_name.includes('自媒体1'))?.channel_code + const t3Code = allChannels.find(c => c.channel_name.includes('自媒体3'))?.channel_code + const t4Code = allChannels.find(c => c.channel_name.includes('自媒体4'))?.channel_code + + const deptIdsStr = Array.from(allIds).join(',') + + // 并发拉取:无渠道(总诊金/昨日)+ 各渠道(completed_performance_amount) + const [perfRes, douyinRes, t3Res, t4Res] = await Promise.all([ + yejiStatsMulti({ dept_ids: deptIdsStr }) as any, + douyinCode ? yejiStatsMulti({ dept_ids: deptIdsStr, channel_code: douyinCode }) as any : Promise.resolve(null), + t3Code ? yejiStatsMulti({ dept_ids: deptIdsStr, channel_code: t3Code }) as any : Promise.resolve(null), + t4Code ? yejiStatsMulti({ dept_ids: deptIdsStr, channel_code: t4Code }) as any : Promise.resolve(null), + ]) + + const getMonthTbl = (res: any) => (res?.tables ?? []).find((t: any) => String(t.label).includes('月')) ?? res?.tables?.[0] + const getYdayTbl = (res: any) => (res?.tables ?? []).find((t: any) => String(t.label).includes('昨')) ?? res?.tables?.[res?.tables?.length - 1] + + function buildMap(tbl: any, field: string): Map { + const m = new Map() + for (const r of tbl?.rows ?? []) m.set(Number(r.dept_id), Number(r[field] ?? 0)) + return m + } + + const monthPerf = buildMap(getMonthTbl(perfRes), 'performance_amount') + const ydayPerf = buildMap(getYdayTbl(perfRes), 'performance_amount') + const douyinPerf = douyinRes ? buildMap(getMonthTbl(douyinRes), 'completed_performance_amount') : null + const t3Perf = t3Res ? buildMap(getMonthTbl(t3Res), 'completed_performance_amount') : null + const t4Perf = t4Res ? buildMap(getMonthTbl(t4Res), 'completed_performance_amount') : null + + const sumIds = (m: Map, ids: number[]) => ids.reduce((s, id) => s + (m.get(id) ?? 0), 0) + + const today = new Date() + const totalDays = daysInMonth(ym) + const elapsed = today.getDate() + + const sections: TargetProgressSection[] = specs.map(spec => { + const total = sumIds(monthPerf, spec.totalIds) + const yesterday = sumIds(ydayPerf, spec.totalIds) + + const douyinAmt = douyinPerf ? sumIds(douyinPerf, spec.totalIds) : null + const t3Amt = t3Perf ? sumIds(t3Perf, spec.totalIds) : null + const t4Amt = t4Perf ? sumIds(t4Perf, spec.totalIds) : null + // 推粉 = 自媒体3 + 自媒体4,有任一即显示 + const tuifenAmt = (t3Amt !== null || t4Amt !== null) ? (t3Amt ?? 0) + (t4Amt ?? 0) : null + + const targetProgress = Math.round(spec.target_amount * (elapsed / totalDays) * 100) / 100 + const completion = spec.target_amount > 0 ? total / spec.target_amount : null + return { + dept_id: spec.dept_id, + dept_name: spec.dept_name, + target_amount: spec.target_amount, + total_amount: total, + yesterday_amount: yesterday, + douyin_amount: douyinAmt && douyinAmt > 0 ? douyinAmt : null, + tuifen_amount: tuifenAmt && tuifenAmt > 0 ? tuifenAmt : null, + target_progress: targetProgress, + completion_rate: completion, + } + }) + + // 取根部门名作为卡片标题前缀 + const rootName = treeRows[0]?.dept_name ?? '' + const m = today.getMonth() + 1 + const d = today.getDate() + targetProgressCard.value = { + title: `${rootName}诊金-${m}.${d}`, + sections, + } + } catch { + // 静默失败,不影响主页面 + } finally { + targetProgressLoading.value = false + } +} + const channelFilterNote = computed(() => { for (const t of tables.value) { if (t.channel_filter_note) return t.channel_filter_note @@ -675,6 +890,20 @@ function roiClass(v: any) { if (n >= 0.8) return 'roi-mid' return 'roi-bad' } +function formatAchievement(v: number | null): string { + if (v === null) return '—' + return (v * 100).toFixed(2) + '%' +} +function achievementClass(v: number | null): string { + if (v === null) return '' + if (v >= 1) return 'tp-good' + if (v >= 0.8) return 'tp-mid' + return 'tp-bad' +} +function formatMoneyW(v: number): string { + if (v >= 10000) return (v / 10000).toFixed(1) + '万' + return String(v) +} /** 本地日历日 yyyy-mm-dd(排行榜「今日」、日期快捷与日期控件一致) */ function formatLocalYmd(d: Date): string { @@ -687,6 +916,7 @@ function formatLocalYmd(d: Date): string { onMounted(async () => { await Promise.all([loadDeptOptions(), loadChannelOptions()]) loadData() + loadTargetProgress() }) @@ -924,6 +1154,101 @@ onMounted(async () => { } } +/* ── 目标进度卡片 ── */ +.tp-wrap { + min-height: 40px; +} + +.tp-card { + display: inline-block; + border: 2px solid #e8b800; + border-radius: 6px; + overflow: hidden; + font-size: 13px; + background: #fff; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); + + &__title { + background: #ffe600; + color: #1f1f1f; + font-weight: 700; + font-size: 15px; + text-align: center; + padding: 8px 20px; + letter-spacing: 1px; + } + + &__body { + display: flex; + gap: 0; + } +} + +.tp-section { + min-width: 200px; + border-right: 1px solid #e8d800; + + &:last-child { + border-right: none; + } + + &__head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + background: #fff8c0; + padding: 6px 12px; + border-bottom: 1px solid #e8d800; + } + + &__name { + font-weight: 700; + font-size: 13px; + color: #333; + } + + &__target { + font-size: 13px; + font-weight: 700; + color: #b45309; + } +} + +.tp-table { + width: 100%; + border-collapse: collapse; + + .tp-lbl { + padding: 5px 10px; + color: #555; + font-size: 12px; + border-bottom: 1px solid #f0e860; + white-space: nowrap; + } + + .tp-val { + padding: 5px 12px; + font-weight: 600; + text-align: right; + border-bottom: 1px solid #f0e860; + white-space: nowrap; + color: #111; + + &--total { + color: #b45309; + } + } + + tr:last-child td { + border-bottom: none; + } +} + +.tp-good { color: #16a34a !important; font-weight: 700; } +.tp-mid { color: #d97706 !important; font-weight: 700; } +.tp-bad { color: #dc2626 !important; font-weight: 700; } + .leaderboard-sub { font-size: 13px; font-weight: 500; @@ -1025,6 +1350,7 @@ onMounted(async () => { background: #f0fdfa; } + .total-row { font-weight: 700; background: #fff8d6; diff --git a/admin/src/views/finance/mubiao-dept-node.vue b/admin/src/views/finance/mubiao-dept-node.vue index f66bf2f8..a149aa92 100644 --- a/admin/src/views/finance/mubiao-dept-node.vue +++ b/admin/src/views/finance/mubiao-dept-node.vue @@ -4,7 +4,10 @@
-
{{ node.dept_name }}
+
+ + {{ node.dept_name }} +
- 已保存 + 已保存 +
@@ -43,22 +47,19 @@