This commit is contained in:
Your Name
2026-04-30 14:20:40 +08:00
parent 742be6b492
commit 4c21ee5938
5 changed files with 655 additions and 0 deletions
@@ -0,0 +1,592 @@
<template>
<div class="dept-card" :class="{ 'dept-card--saved': allSaved, 'dept-card--empty': isEmpty }">
<!-- 面包屑路径仅钻取卡显示 -->
<div v-if="!isRoot && breadcrumb" class="dept-card__crumb">
<span
v-for="(seg, i) in breadcrumb"
:key="i"
class="crumb-seg"
>
<span class="crumb-seg__text">{{ seg }}</span>
<el-icon v-if="i < breadcrumb.length - 1" class="crumb-sep"><ArrowRight /></el-icon>
</span>
</div>
<!-- 卡片头 -->
<div class="dept-card__header">
<div class="dept-card__avatar" :style="{ background: avatarBg }">
{{ node.dept_name.slice(0, 1) }}
</div>
<div class="dept-card__meta">
<div class="dept-card__name">{{ node.dept_name }}</div>
<div v-if="node.children?.length" class="dept-card__sub-count">
{{ node.children.length }} 个子部门
<span v-if="childTotal > 0" class="dept-card__sub-total">· ¥{{ formatMoneyShort(childTotal) }}</span>
</div>
</div>
<div class="dept-card__actions">
<span v-if="node.target_id" class="status-badge status-badge--saved">已保存</span>
<span v-else class="status-badge status-badge--new">未设置</span>
<!-- 钻取卡才有关闭按钮 -->
<button v-if="!isRoot" class="close-btn" title="关闭此卡片" @click="emit('close')">
<el-icon><Close /></el-icon>
</button>
</div>
</div>
<!-- 当前部门输入 -->
<div class="dept-card__body">
<div class="field-row">
<div class="field-block">
<label class="field-block__label">目标金额</label>
<el-input-number
v-model="node.target_amount"
:min="0"
:precision="2"
:step="1000"
controls-position="right"
placeholder="0.00"
class="field-block__input"
/>
<span v-if="node.target_amount > 0" class="field-block__hint">
¥ {{ formatMoney(node.target_amount) }}
</span>
</div>
<div class="field-block field-block--remark">
<label class="field-block__label">备注</label>
<el-input
v-model="node.remark"
maxlength="255"
placeholder="备注(选填)"
clearable
/>
</div>
</div>
</div>
<!-- 子部门列表 -->
<template v-if="node.children?.length">
<div class="child-header">
<span class="child-header__title">子部门</span>
<span class="child-header__count">{{ node.children.length }}</span>
</div>
<div class="child-list">
<div
v-for="child in node.children"
:key="child.dept_id"
class="child-row"
:class="{ 'child-row--active': drilledId === child.dept_id }"
>
<div class="child-row__name-wrap">
<div class="child-row__dot" />
<span class="child-row__name">{{ child.dept_name }}</span>
<el-tag
v-if="child.target_id"
size="small" type="success" effect="plain" round
class="child-row__tag"
>已保存</el-tag>
</div>
<div class="child-row__inputs">
<el-input-number
v-model="child.target_amount"
:min="0"
:precision="2"
:step="1000"
controls-position="right"
placeholder="0.00"
class="child-row__amount"
/>
<el-input
v-model="child.remark"
maxlength="255"
placeholder="备注"
clearable
size="small"
class="child-row__remark"
/>
<!-- 子部门若有下级显示子树合计 + 钻取按钮 -->
<template v-if="child.children?.length">
<span
v-if="subtreeSum(child) > 0"
class="child-subtree-total"
:title="`「${child.dept_name}」含下级合计`"
>
={{ formatMoneyShort(subtreeSum(child)) }}
</span>
<button
class="drill-btn"
:class="{ 'drill-btn--active': drilledId === child.dept_id }"
:title="`展开「${child.dept_name}」的 ${child.children.length} 个子部门`"
@click="emit('drill-in', child)"
>
<el-icon><ArrowRight /></el-icon>
<span>{{ child.children.length }}</span>
</button>
</template>
</div>
</div>
</div>
</template>
<!-- 底部合计 -->
<div class="dept-card__footer">
<span class="dept-card__footer-label">本卡合计</span>
<span class="dept-card__footer-total" :class="{ 'is-nonzero': cardTotal > 0 }">
¥ {{ formatMoney(cardTotal) }}
</span>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed } from 'vue'
import { ArrowRight, Close } from '@element-plus/icons-vue'
import type { DeptTargetRow } from './mubiao-dept-node.vue'
const props = defineProps<{
node: DeptTargetRow
isRoot: boolean
drilledId: number | null
}>()
const emit = defineEmits<{
(e: 'drill-in', node: DeptTargetRow): void
(e: 'close'): void
}>()
// 递归求一个节点的整棵子树金额合计(含自身)
function subtreeSum(node: DeptTargetRow): number {
let s = Number(node.target_amount ?? 0)
if (node.children?.length) {
for (const c of node.children) s += subtreeSum(c)
}
return s
}
const childTotal = computed(() => {
if (!props.node.children?.length) return 0
return props.node.children.reduce((s, c) => s + subtreeSum(c), 0)
})
const cardTotal = computed(() => Number(props.node.target_amount ?? 0) + childTotal.value)
const isEmpty = computed(() => cardTotal.value === 0)
const allSaved = computed(() => {
if (!props.node.target_id) return false
if (!props.node.children?.length) return true
return props.node.children.every(c => c.target_id > 0)
})
// 面包屑:从 dept_path 中提取,去掉最后一段(就是自己的名字)
const breadcrumb = computed(() => {
if (!props.node.dept_path) return null
const segs = props.node.dept_path.split(/\s*\/\s*/).filter(Boolean)
return segs.length > 1 ? segs : null
})
// 根据名称哈希生成确定性渐变
const avatarBg = computed(() => {
const palettes = [
'linear-gradient(135deg,#667eea,#764ba2)',
'linear-gradient(135deg,#f093fb,#f5576c)',
'linear-gradient(135deg,#4facfe,#00f2fe)',
'linear-gradient(135deg,#43e97b,#38f9d7)',
'linear-gradient(135deg,#fa709a,#fee140)',
'linear-gradient(135deg,#30cfd0,#330867)',
'linear-gradient(135deg,#a18cd1,#fbc2eb)',
'linear-gradient(135deg,#fccb90,#d57eeb)',
'linear-gradient(135deg,#e0c3fc,#8ec5fc)',
'linear-gradient(135deg,#f6d365,#fda085)',
]
let hash = 0
for (let i = 0; i < props.node.dept_name.length; i++) {
hash = (hash * 31 + props.node.dept_name.charCodeAt(i)) & 0xffff
}
return palettes[hash % palettes.length]
})
function formatMoney(v: number): string {
return Number(v || 0).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
}
function formatMoneyShort(v: number): string {
if (v >= 10000) return (v / 10000).toFixed(1) + 'w'
return v.toLocaleString('zh-CN', { maximumFractionDigits: 0 })
}
</script>
<style lang="scss" scoped>
.dept-card {
background: var(--el-bg-color);
border: 1px solid var(--el-border-color-lighter);
border-radius: 14px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
transition: box-shadow 0.2s ease, transform 0.15s ease;
display: flex;
flex-direction: column;
&:hover {
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.09);
transform: translateY(-1px);
}
&--saved {
border-color: color-mix(in srgb, #67c23a 30%, var(--el-border-color-lighter));
}
&--empty {
opacity: 0.8;
}
}
/* ── 面包屑 ── */
.dept-card__crumb {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 2px;
padding: 8px 14px 0;
font-size: 11px;
.crumb-seg {
display: inline-flex;
align-items: center;
gap: 2px;
&__text {
color: var(--el-text-color-placeholder);
&:last-child {
color: var(--el-color-primary);
font-weight: 600;
}
}
}
.crumb-sep {
font-size: 10px;
color: var(--el-border-color-darker);
}
}
/* ── 卡片头 ── */
.dept-card__header {
display: flex;
align-items: center;
gap: 12px;
padding: 14px 14px 12px;
border-bottom: 1px solid var(--el-border-color-lighter);
background: linear-gradient(180deg, var(--el-fill-color-extra-light), var(--el-bg-color));
}
.dept-card__avatar {
width: 38px;
height: 38px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 15px;
font-weight: 800;
color: #fff;
flex-shrink: 0;
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.18);
}
.dept-card__meta {
flex: 1;
min-width: 0;
}
.dept-card__name {
font-size: 14px;
font-weight: 700;
color: var(--el-text-color-primary);
line-height: 1.3;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.dept-card__sub-count {
font-size: 11.5px;
color: var(--el-text-color-placeholder);
margin-top: 2px;
}
.dept-card__sub-total {
color: var(--el-color-primary);
font-weight: 600;
}
.dept-card__actions {
display: flex;
align-items: center;
gap: 6px;
flex-shrink: 0;
}
.close-btn {
all: unset;
display: inline-flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
border-radius: 6px;
color: var(--el-text-color-placeholder);
cursor: pointer;
transition: background 0.15s, color 0.15s;
&:hover {
background: var(--el-fill-color);
color: var(--el-color-danger);
}
.el-icon { font-size: 13px; }
}
.status-badge {
display: inline-flex;
align-items: center;
padding: 3px 9px;
border-radius: 999px;
font-size: 11px;
font-weight: 600;
&--saved {
background: color-mix(in srgb, #67c23a 12%, transparent);
color: #529b2e;
border: 1px solid color-mix(in srgb, #67c23a 28%, transparent);
}
&--new {
background: var(--el-fill-color);
color: var(--el-text-color-placeholder);
border: 1px solid var(--el-border-color-lighter);
}
}
/* ── 主体输入 ── */
.dept-card__body {
padding: 14px;
border-bottom: 1px solid var(--el-border-color-lighter);
}
.field-row {
display: flex;
flex-direction: column;
gap: 10px;
}
.field-block {
display: flex;
flex-direction: column;
gap: 5px;
&__label {
font-size: 10.5px;
font-weight: 700;
color: var(--el-text-color-secondary);
letter-spacing: 0.07em;
text-transform: uppercase;
}
&__input {
width: 100%;
:deep(.el-input__wrapper) {
border-radius: 8px;
}
}
&__hint {
font-size: 11px;
color: var(--el-text-color-placeholder);
margin-top: -2px;
}
}
/* ── 子部门区 ── */
.child-header {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 14px;
background: var(--el-fill-color-extra-light);
border-bottom: 1px solid var(--el-border-color-lighter);
&__title {
font-size: 11px;
font-weight: 700;
color: var(--el-text-color-secondary);
letter-spacing: 0.07em;
text-transform: uppercase;
}
&__count {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 18px;
height: 18px;
padding: 0 5px;
border-radius: 999px;
font-size: 10px;
font-weight: 700;
background: var(--el-color-primary-light-8);
color: var(--el-color-primary);
}
}
.child-list {
display: flex;
flex-direction: column;
}
.child-row {
padding: 9px 14px;
border-bottom: 1px solid var(--el-border-color-lighter);
transition: background 0.12s;
&:last-child { border-bottom: none; }
&:hover { background: var(--el-fill-color-extra-light); }
&--active {
background: color-mix(in srgb, var(--el-color-primary) 5%, var(--el-bg-color)) !important;
border-left: 2px solid var(--el-color-primary-light-4);
}
&__name-wrap {
display: flex;
align-items: center;
gap: 7px;
margin-bottom: 7px;
}
&__dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--el-color-primary-light-4);
flex-shrink: 0;
box-shadow: 0 0 0 2px color-mix(in srgb, var(--el-color-primary) 15%, transparent);
}
&__name {
font-size: 13px;
font-weight: 600;
color: var(--el-text-color-primary);
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
&__tag { flex-shrink: 0; }
&__inputs {
display: flex;
align-items: center;
gap: 8px;
}
&__amount {
flex: 0 0 150px;
:deep(.el-input__wrapper) {
border-radius: 7px;
}
}
&__remark {
flex: 1;
min-width: 0;
:deep(.el-input__wrapper) {
border-radius: 7px;
}
}
}
/* 子树合计标签 */
.child-subtree-total {
flex-shrink: 0;
font-size: 11.5px;
font-weight: 700;
color: var(--el-color-primary);
white-space: nowrap;
letter-spacing: -0.01em;
}
/* 钻取按钮 */
.drill-btn {
all: unset;
display: inline-flex;
align-items: center;
gap: 3px;
flex-shrink: 0;
padding: 5px 10px;
border-radius: 7px;
font-size: 12px;
font-weight: 700;
color: var(--el-color-primary);
background: var(--el-color-primary-light-9);
border: 1px solid var(--el-color-primary-light-7);
cursor: pointer;
transition: background 0.15s, box-shadow 0.15s, transform 0.1s;
&:hover {
background: var(--el-color-primary-light-8);
box-shadow: 0 2px 6px color-mix(in srgb, var(--el-color-primary) 20%, transparent);
transform: translateX(1px);
}
&--active {
background: var(--el-color-primary);
color: #fff;
border-color: var(--el-color-primary);
box-shadow: 0 2px 8px color-mix(in srgb, var(--el-color-primary) 35%, transparent);
}
.el-icon {
font-size: 12px;
transition: transform 0.15s;
}
&:hover .el-icon { transform: translateX(2px); }
}
/* ── 底部 ── */
.dept-card__footer {
margin-top: auto;
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 14px;
background: var(--el-fill-color-extra-light);
border-top: 1px solid var(--el-border-color-lighter);
}
.dept-card__footer-label {
font-size: 11.5px;
color: var(--el-text-color-placeholder);
font-weight: 600;
letter-spacing: 0.04em;
}
.dept-card__footer-total {
font-size: 15px;
font-weight: 800;
color: var(--el-text-color-placeholder);
letter-spacing: -0.02em;
transition: color 0.2s;
&.is-nonzero { color: var(--el-color-primary); }
}
</style>
@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace app\common\model\tcm;
use app\common\model\BaseModel;
/**
* 诊单挂号 / 取消挂号操作日志(后台)
*/
class DiagnosisGuahaoLog extends BaseModel
{
protected $name = 'tcm_diagnosis_guahao_log';
protected $autoWriteTimestamp = false;
}
@@ -0,0 +1,14 @@
-- 中医诊单:查看挂号/取消挂号操作日志 API 权限
SET @diag_menu_id := (SELECT id FROM zyt_system_menu WHERE perms = 'tcm.diagnosis/lists' LIMIT 1);
INSERT INTO zyt_system_menu (
pid, type, name, icon, sort, perms, paths, component,
selected, params, is_cache, is_show, is_disable, create_time, update_time
)
SELECT
@diag_menu_id, 'A', '挂号操作日志', '', 53,
'tcm.diagnosis/guahaoLogList', '', '',
'', '', 0, 1, 0, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()
FROM DUAL
WHERE @diag_menu_id IS NOT NULL
AND NOT EXISTS (SELECT 1 FROM zyt_system_menu WHERE perms = 'tcm.diagnosis/guahaoLogList');
@@ -0,0 +1,16 @@
-- 处方业务订单:接口权限「修改关联处方患者姓名/手机」
-- 执行前确认表前缀为 zyt_;若已有相同 perms 则跳过。
SET @po_menu_id := (SELECT id FROM zyt_system_menu WHERE perms = 'tcm.prescriptionOrder/lists' LIMIT 1);
INSERT INTO zyt_system_menu (
pid, type, name, icon, sort, perms, paths, component,
selected, params, is_cache, is_show, is_disable, create_time, update_time
)
SELECT
@po_menu_id, 'A', '修改处方患者信息', '', 52,
'tcm.prescriptionOrder/patchPrescriptionPatient', '', '',
'', '', 0, 1, 0, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()
FROM DUAL
WHERE @po_menu_id IS NOT NULL
AND NOT EXISTS (SELECT 1 FROM zyt_system_menu WHERE perms = 'tcm.prescriptionOrder/patchPrescriptionPatient');
@@ -0,0 +1,16 @@
-- 诊单挂号 / 取消挂号操作日志(谁在何时操作)
-- 执行前确认表前缀为 zyt_
CREATE TABLE IF NOT EXISTS `zyt_tcm_diagnosis_guahao_log` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`diagnosis_id` int unsigned NOT NULL DEFAULT '0' COMMENT '诊单 iddoctor_appointment.patient_id',
`appointment_id` int unsigned NOT NULL DEFAULT '0' COMMENT '挂号记录 id',
`admin_id` int unsigned NOT NULL DEFAULT '0' COMMENT '操作人管理员 id',
`admin_name` varchar(64) NOT NULL DEFAULT '' COMMENT '操作人姓名',
`action` varchar(32) NOT NULL DEFAULT '' COMMENT 'create=挂号 cancel=取消挂号',
`summary` varchar(500) NOT NULL DEFAULT '' COMMENT '摘要',
`create_time` int unsigned NOT NULL DEFAULT '0',
PRIMARY KEY (`id`),
KEY `idx_diagnosis_id` (`diagnosis_id`),
KEY `idx_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='诊单挂号操作日志';