1. 开方页面代煎选择交互
Some checks failed
CI / Test (ubuntu-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Lint (ubuntu-latest) (push) Has been cancelled
CI / Lint (windows-latest) (push) Has been cancelled
CI / Check (ubuntu-latest) (push) Has been cancelled
CI / Check (windows-latest) (push) Has been cancelled
CI / CI OK (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Website on push / Deploy Push Playground Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Docs Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Antd Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Element Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Naive Ftp (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
Lock Threads / action (push) Has been cancelled
Issue Close Require / close-issues (push) Has been cancelled
Close stale issues / stale (push) Has been cancelled

This commit is contained in:
李琦
2026-07-10 15:11:18 +08:00
parent 026d0e0a48
commit e0cb5c660c
3 changed files with 402 additions and 0 deletions

View File

@@ -0,0 +1,28 @@
/** 展示用优先顶层合并医嘱fallback content 纯医嘱 */
export function formatDoctorOrderText(
item?: {
doctor_order?: string | string[];
content?: { doctor_order?: string };
} | null,
): string {
const raw = item?.doctor_order ?? item?.content?.doctor_order ?? '';
if (Array.isArray(raw)) {
const list = raw.map((s) => String(s).trim()).filter(Boolean);
return list.length ? list.join('') : '-';
}
const parts = String(raw)
.split('|')
.map((s) => s.trim())
.filter(Boolean);
return parts.length ? parts.join('') : raw ? String(raw) : '-';
}
/** 复用开方输入框detail.doctor_order 为数组时转回字符串 */
export function detailDoctorOrderToText(
doctorOrder?: string | string[] | null,
): string {
if (Array.isArray(doctorOrder)) {
return doctorOrder.map((s) => String(s).trim()).filter(Boolean).join('|');
}
return doctorOrder ?? '';
}

View File

@@ -0,0 +1,90 @@
export type ProcessRuleListItem = { id?: number | string };
export type ProcessRuleLoadedLevel = 'root' | 'child' | 'note';
/**
* 列表仅一项且当前未选时,返回该项 id
* 用于委托调剂制剂/煎法/备注的默认选中
*/
export function getSingleProcessRuleId(
list: ProcessRuleListItem[] | null | undefined,
currentId?: number | string | null,
): number | null {
if (!Array.isArray(list) || list.length !== 1) {
return null;
}
const cur = currentId == null || currentId === '' ? null : Number(currentId);
if (cur != null && Number.isFinite(cur) && cur > 0) {
return null;
}
const id = Number(list[0]?.id);
return Number.isFinite(id) && id > 0 ? id : null;
}
/** 根据 loadProcessRuleData 参数判断刚加载的是哪一级 */
export function resolveProcessRuleLoadedLevel(
pid: number,
ruleId: number,
): ProcessRuleLoadedLevel {
if (ruleId !== 0) {
return 'note';
}
if (pid === 0) {
return 'root';
}
return 'child';
}
export type ProcessRuleAutoSelectContext = {
ruleType: number;
disabled?: boolean;
processRuleId?: number | string | null;
childProcessRuleId?: number | string | null;
processRuleNoteId?: number | string | null;
processRuleList: ProcessRuleListItem[];
childProcessRuleList: ProcessRuleListItem[];
processRuleNoteList: ProcessRuleListItem[];
load: (pid: number, ruleId: number) => Promise<void>;
setProcessRuleId: (id: number) => void;
setChildProcessRuleId: (id: number) => void;
setProcessRuleNoteId: (id: number) => void;
clearBelowProcessRule: () => void;
clearBelowChild: () => void;
};
/**
* 子级仅一项时自动选中,并级联 load 下一级
* 不覆盖已有选中值(草稿恢复、用户手选、历史导入)
*/
export async function applySingleChildProcessRuleAutoSelect(
loadedLevel: ProcessRuleLoadedLevel,
ctx: ProcessRuleAutoSelectContext,
): Promise<void> {
if (ctx.disabled || ctx.ruleType !== 2) {
return;
}
if (loadedLevel === 'root') {
const id = getSingleProcessRuleId(ctx.processRuleList, ctx.processRuleId);
if (id == null) {
return;
}
ctx.clearBelowProcessRule();
ctx.setProcessRuleId(id);
await ctx.load(id, 0);
return;
}
if (loadedLevel === 'child') {
const id = getSingleProcessRuleId(ctx.childProcessRuleList, ctx.childProcessRuleId);
if (id == null) {
return;
}
ctx.clearBelowChild();
ctx.setChildProcessRuleId(id);
await ctx.load(0, id);
return;
}
const noteId = getSingleProcessRuleId(ctx.processRuleNoteList, ctx.processRuleNoteId);
if (noteId != null) {
ctx.setProcessRuleNoteId(noteId);
}
}

View File

@@ -0,0 +1,284 @@
<script setup lang="ts">
import { MinusOutlined, PlusOutlined } from '@ant-design/icons-vue';
import { InputNumber } from 'ant-design-vue';
/** 中药配置变更载荷 */
export type ChineseMedicineConfigChange = {
ruleType?: number;
packageMethodId?: number | null;
processRuleId?: number | null;
childProcessRuleId?: number | null;
processRuleNoteId?: number | null;
dosage?: number;
dayDosage?: number;
};
const props = defineProps<{
ruleType: number;
packageMethodId?: number | null;
processRuleId?: number | null;
childProcessRuleId?: number | null;
processRuleNoteId?: number | null;
dosage: number;
dayDosage: number;
processRuleList?: Array<{ id: number; name: string }>;
childProcessRuleList?: Array<{ id: number; name: string }>;
processRuleNoteList?: Array<{ id: number; note?: string; name?: string }>;
disabled?: boolean;
}>();
const emit = defineEmits<{
change: [payload: ChineseMedicineConfigChange];
}>();
/** 包法选项(与移动端一致) */
const packageMethodList = [
{ id: 1, name: '味包' },
{ id: 2, name: '剂包' },
];
/**
* 统一向父组件抛出配置变更
* 父组件负责级联加载制剂/煎法/备注列表
*/
function emitChange(payload: ChineseMedicineConfigChange) {
emit('change', payload);
}
/**
* 切换制剂方式:自制剂 / 委托调剂
*/
function handleRuleTypeChange(value: number) {
if (props.disabled || props.ruleType === value) return;
if (value === 1) {
emitChange({
ruleType: 1,
processRuleId: null,
childProcessRuleId: null,
processRuleNoteId: null,
packageMethodId: props.packageMethodId ?? packageMethodList[1].id,
});
} else {
emitChange({
ruleType: 2,
packageMethodId: null,
});
}
}
/** 选择包法标签 */
function handlePackageMethodClick(id: number) {
if (props.disabled) return;
emitChange({ packageMethodId: id });
}
/** 选择制剂标签,级联清空煎法/备注 */
function handleProcessRuleClick(item: { id: number }) {
if (props.disabled) return;
emitChange({
processRuleId: item.id,
childProcessRuleId: null,
processRuleNoteId: null,
});
}
/** 选择煎法标签,级联清空备注 */
function handleChildProcessRuleClick(item: { id: number }) {
if (props.disabled) return;
emitChange({
childProcessRuleId: item.id,
processRuleNoteId: null,
});
}
/** 选择备注标签 */
function handleProcessRuleNoteClick(item: { id: number }) {
if (props.disabled) return;
emitChange({ processRuleNoteId: item.id });
}
function updateDosage(val: number) {
emitChange({ dosage: val });
}
function updateDayDosage(val: number) {
emitChange({ dayDosage: val });
}
</script>
<template>
<div class="chinese-medicine-config">
<!-- 制剂方式 -->
<div class="config-section mb-4">
<div class="section-title mb-2 text-sm font-bold text-gray-800 dark:text-gray-200">制剂方式</div>
<div class="pill-group">
<span
class="pill"
:class="{ active: ruleType === 1, disabled }"
@click="handleRuleTypeChange(1)"
>自制剂</span>
<span
class="pill"
:class="{ active: ruleType === 2, disabled }"
@click="handleRuleTypeChange(2)"
>委托调剂</span>
</div>
</div>
<!-- 自制剂包法 -->
<div v-if="ruleType === 1" class="config-section mb-4 border-t border-dashed border-gray-200 dark:border-gray-700 pt-4">
<div class="section-title mb-2 text-sm font-bold text-gray-800 dark:text-gray-200">包法选择</div>
<div class="pill-group">
<span
v-for="item in packageMethodList"
:key="item.id"
class="pill outline"
:class="{ active: packageMethodId === item.id, disabled }"
@click="handlePackageMethodClick(item.id)"
>{{ item.name }}</span>
</div>
</div>
<!-- 委托调剂制剂 / 煎法 / 备注 -->
<template v-if="ruleType === 2">
<div class="config-section mb-4 border-t border-dashed border-gray-200 dark:border-gray-700 pt-4">
<div class="section-title mb-2 text-sm font-bold text-gray-800 dark:text-gray-200">制剂要求</div>
<div class="pill-group">
<span
v-for="item in processRuleList"
:key="item.id"
class="pill outline"
:class="{ active: processRuleId === item.id, disabled }"
@click="handleProcessRuleClick(item)"
>{{ item.name }}</span>
</div>
</div>
<div v-if="childProcessRuleList?.length" class="config-section mb-4">
<div class="section-title mb-2 text-sm font-bold text-gray-800 dark:text-gray-200">煎法说明</div>
<div class="pill-group">
<span
v-for="item in childProcessRuleList"
:key="item.id"
class="pill outline"
:class="{ active: childProcessRuleId === item.id, disabled }"
@click="handleChildProcessRuleClick(item)"
>{{ item.name }}</span>
</div>
</div>
<div v-if="processRuleNoteList?.length" class="config-section mb-4">
<div class="section-title mb-2 text-sm font-bold text-gray-800 dark:text-gray-200">附加备注</div>
<div class="pill-group">
<span
v-for="item in processRuleNoteList"
:key="item.id"
class="pill outline"
:class="{ active: processRuleNoteId === item.id, disabled }"
@click="handleProcessRuleNoteClick(item)"
>{{ item.note || item.name }}</span>
</div>
</div>
</template>
<!-- 剂数与频次 -->
<div class="config-section border-t border-dashed border-gray-200 dark:border-gray-700 pt-4">
<div class="mt-2 flex flex-wrap items-center gap-4">
<div class="flex items-center gap-2">
<span class="text-sm font-bold text-gray-800 dark:text-gray-200">剂数</span>
<InputNumber
:value="dosage"
:disabled="disabled"
:min="1"
:max="100"
class="w-24"
@update:value="(v) => updateDosage(Number(v) || 1)"
>
<template #addonBefore>
<MinusOutlined @click="!disabled && updateDosage(dosage > 1 ? dosage - 1 : 1)" />
</template>
<template #addonAfter>
<PlusOutlined @click="!disabled && updateDosage(dosage < 100 ? dosage + 1 : 100)" />
</template>
</InputNumber>
<span class="text-sm text-gray-500 dark:text-gray-400">()</span>
</div>
<div class="flex items-center gap-2">
<span class="text-sm font-bold text-gray-800 dark:text-gray-200">用药频次</span>
<InputNumber
:value="dayDosage"
:disabled="disabled"
:min="1"
:max="100"
class="w-24"
@update:value="(v) => updateDayDosage(Number(v) || 1)"
>
<template #addonBefore>
<MinusOutlined @click="!disabled && updateDayDosage(dayDosage > 1 ? dayDosage - 1 : 1)" />
</template>
<template #addonAfter>
<PlusOutlined @click="!disabled && updateDayDosage(dayDosage < 100 ? dayDosage + 1 : 100)" />
</template>
</InputNumber>
<span class="text-sm text-gray-500 dark:text-gray-400">/</span>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.pill-group {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.pill {
padding: 4px 16px;
background-color: #f4f6f8;
color: #6c7380;
border-radius: 20px;
font-size: 13px;
cursor: pointer;
border: 1px solid transparent;
transition: all 0.2s;
user-select: none;
}
.pill.active {
background-color: #00a88a;
color: #fff;
border-color: #00a88a;
font-weight: 600;
}
.pill.outline {
background-color: #fff;
border-color: #e2e8f0;
}
.pill.outline.active {
background-color: #e8f6f4;
color: #00a88a;
border-color: #00a88a;
}
.pill.disabled {
opacity: 0.5;
pointer-events: none;
}
/* 暗色模式:标签背景与选中态 */
.dark .pill {
background-color: #2a2a2a;
color: #a3a3a3;
}
.dark .pill.active {
background-color: #00a88a;
color: #fff;
border-color: #00a88a;
}
.dark .pill.outline {
background-color: #1f1f1f;
border-color: #404040;
color: #a3a3a3;
}
.dark .pill.outline.active {
background-color: rgba(0, 168, 138, 0.15);
color: #3dd6b5;
border-color: #00a88a;
}
</style>