feat(签署): 保存并还原患者勾选结果
签署完成时将患者勾选的条目固化到任务记录,重新打开已签署文书时还原勾选状态,重新发起签署则清空历史勾选。
This commit is contained in:
@@ -346,7 +346,11 @@ function clonePatient(patient: PatientProfile): PatientProfile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function cloneTask(task: SigningTaskRecord): SigningTaskRecord {
|
function cloneTask(task: SigningTaskRecord): SigningTaskRecord {
|
||||||
return { ...task }
|
return {
|
||||||
|
...task,
|
||||||
|
// 勾选结果是数组,浅拷贝会让调用方和内存数据共享同一个引用
|
||||||
|
selectedItems: task.selectedItems ? [...task.selectedItems] : task.selectedItems,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRangeStart(range: SigningTaskQuery['dateRange']) {
|
function getRangeStart(range: SigningTaskQuery['dateRange']) {
|
||||||
@@ -1002,6 +1006,7 @@ export function updateSigningTaskMethod(
|
|||||||
export function completeSigningTask(
|
export function completeSigningTask(
|
||||||
id: string,
|
id: string,
|
||||||
signatureDataUrl: string,
|
signatureDataUrl: string,
|
||||||
|
selectedItems: string[] = [],
|
||||||
): Promise<SigningTaskRecord | null> {
|
): Promise<SigningTaskRecord | null> {
|
||||||
if (!useMockData) {
|
if (!useMockData) {
|
||||||
throw new Error('真实签署需要通过签署投递与文件上传接口完成')
|
throw new Error('真实签署需要通过签署投递与文件上传接口完成')
|
||||||
@@ -1017,6 +1022,8 @@ export function completeSigningTask(
|
|||||||
task.signatureDataUrl = signatureDataUrl
|
task.signatureDataUrl = signatureDataUrl
|
||||||
task.signedAt = formatMockDate()
|
task.signedAt = formatMockDate()
|
||||||
task.updatedAt = task.signedAt
|
task.updatedAt = task.signedAt
|
||||||
|
// 勾选结果随签署一起固化,否则重新打开文书会丢掉患者勾了哪些项
|
||||||
|
task.selectedItems = [...selectedItems]
|
||||||
persistMockRecords()
|
persistMockRecords()
|
||||||
return Promise.resolve(cloneTask(task))
|
return Promise.resolve(cloneTask(task))
|
||||||
}
|
}
|
||||||
@@ -1042,6 +1049,8 @@ export async function reopenSigningTask(
|
|||||||
task.status = 'pending'
|
task.status = 'pending'
|
||||||
task.signatureDataUrl = undefined
|
task.signatureDataUrl = undefined
|
||||||
task.signedAt = undefined
|
task.signedAt = undefined
|
||||||
|
// 重新发起等于重新签,上一次的勾选不再沿用
|
||||||
|
task.selectedItems = []
|
||||||
task.updatedAt = formatMockDate()
|
task.updatedAt = formatMockDate()
|
||||||
persistMockRecords()
|
persistMockRecords()
|
||||||
return Promise.resolve(cloneTask(task))
|
return Promise.resolve(cloneTask(task))
|
||||||
|
|||||||
@@ -190,6 +190,8 @@ export interface SigningTaskRecord extends WorkbenchTask {
|
|||||||
voidReason?: string
|
voidReason?: string
|
||||||
/** 原型:任务创建时固化的文书 HTML(对应真实实现的模板版本快照 + contentSha256)。 */
|
/** 原型:任务创建时固化的文书 HTML(对应真实实现的模板版本快照 + contentSha256)。 */
|
||||||
documentContentHtml?: string
|
documentContentHtml?: string
|
||||||
|
/** 原型:患者勾选结果,随签署一起留存(对应真实实现里随签署证据提交的勾选摘要)。 */
|
||||||
|
selectedItems?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SigningTaskListResponse = PageResult<SigningTaskRecord>
|
export type SigningTaskListResponse = PageResult<SigningTaskRecord>
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ export interface SigningDocumentData {
|
|||||||
visitDate: string
|
visitDate: string
|
||||||
signedAt?: string
|
signedAt?: string
|
||||||
signatureDataUrl?: string
|
signatureDataUrl?: string
|
||||||
|
/** 已勾选的条目名:签署完成后重新打开文书时用来还原勾选状态 */
|
||||||
|
selectedItems?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
const SIGNATURE_PLACEHOLDER = '{患者签名}'
|
const SIGNATURE_PLACEHOLDER = '{患者签名}'
|
||||||
@@ -105,7 +107,7 @@ function fillPatientPlaceholders(root: Element, data: SigningDocumentData) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function convertCheckboxes(root: Element, interactive: boolean) {
|
function convertCheckboxes(root: Element, interactive: boolean, selected: Set<string>) {
|
||||||
root.querySelectorAll('table').forEach((table) => {
|
root.querySelectorAll('table').forEach((table) => {
|
||||||
// 不能只认 <thead>:WangEditor 的表格节点是 <table><tbody><tr>,它导出时不会生成
|
// 不能只认 <thead>:WangEditor 的表格节点是 <table><tbody><tr>,它导出时不会生成
|
||||||
// thead,粘贴带 thead 的表格时其 preParseHtml 也把 thead 拆掉;.docx 经 mammoth
|
// thead,粘贴带 thead 的表格时其 preParseHtml 也把 thead 拆掉;.docx 经 mammoth
|
||||||
@@ -148,6 +150,13 @@ function convertCheckboxes(root: Element, interactive: boolean) {
|
|||||||
checkbox.setAttribute('data-sign-item', itemName)
|
checkbox.setAttribute('data-sign-item', itemName)
|
||||||
checkbox.disabled = !interactive
|
checkbox.disabled = !interactive
|
||||||
|
|
||||||
|
// 必须写成 attribute:innerHTML 只序列化 HTML 属性,只设 .checked 这个
|
||||||
|
// property 会在生成 HTML 时被丢掉,渲染回来就成了未勾选
|
||||||
|
if (selected.has(itemName)) {
|
||||||
|
checkbox.checked = true
|
||||||
|
checkbox.setAttribute('checked', '')
|
||||||
|
}
|
||||||
|
|
||||||
checkCell.textContent = ''
|
checkCell.textContent = ''
|
||||||
checkCell.appendChild(checkbox)
|
checkCell.appendChild(checkbox)
|
||||||
})
|
})
|
||||||
@@ -180,6 +189,7 @@ function replaceOptionMarkers(
|
|||||||
type: 'radio' | 'checkbox',
|
type: 'radio' | 'checkbox',
|
||||||
groupName: string,
|
groupName: string,
|
||||||
interactive: boolean,
|
interactive: boolean,
|
||||||
|
selected: Set<string>,
|
||||||
) {
|
) {
|
||||||
const parts = (textNode.textContent ?? '').split(OPTION_SPLIT_PATTERN)
|
const parts = (textNode.textContent ?? '').split(OPTION_SPLIT_PATTERN)
|
||||||
const fragment = document.createDocumentFragment()
|
const fragment = document.createDocumentFragment()
|
||||||
@@ -206,6 +216,12 @@ function replaceOptionMarkers(
|
|||||||
input.disabled = !interactive
|
input.disabled = !interactive
|
||||||
input.setAttribute('data-sign-item', label)
|
input.setAttribute('data-sign-item', label)
|
||||||
|
|
||||||
|
// 同表格勾选:只设 property 不会出现在生成的 HTML 里,必须落成属性
|
||||||
|
if (label !== '' && selected.has(label)) {
|
||||||
|
input.checked = true
|
||||||
|
input.setAttribute('checked', '')
|
||||||
|
}
|
||||||
|
|
||||||
if (type === 'radio') {
|
if (type === 'radio') {
|
||||||
input.name = groupName
|
input.name = groupName
|
||||||
input.value = label
|
input.value = label
|
||||||
@@ -234,7 +250,7 @@ function replaceOptionMarkers(
|
|||||||
* 同一段/同一单元格里出现两个及以上方框时按「二选一」处理成互斥单选;
|
* 同一段/同一单元格里出现两个及以上方框时按「二选一」处理成互斥单选;
|
||||||
* 只有一个方框则保持多选语义,避免把「□高血压」这类单项确认误判成互斥组。
|
* 只有一个方框则保持多选语义,避免把「□高血压」这类单项确认误判成互斥组。
|
||||||
*/
|
*/
|
||||||
function convertInlineOptions(root: Element, interactive: boolean) {
|
function convertInlineOptions(root: Element, interactive: boolean, selected: Set<string>) {
|
||||||
const groups = new Map<Element, Text[]>()
|
const groups = new Map<Element, Text[]>()
|
||||||
|
|
||||||
collectOptionTextNodes(root).forEach((textNode) => {
|
collectOptionTextNodes(root).forEach((textNode) => {
|
||||||
@@ -261,7 +277,9 @@ function convertInlineOptions(root: Element, interactive: boolean) {
|
|||||||
const type = markerCount > 1 ? 'radio' : 'checkbox'
|
const type = markerCount > 1 ? 'radio' : 'checkbox'
|
||||||
const groupName = `sign-option-${groupSeq}`
|
const groupName = `sign-option-${groupSeq}`
|
||||||
|
|
||||||
textNodes.forEach((textNode) => replaceOptionMarkers(textNode, type, groupName, interactive))
|
textNodes.forEach((textNode) =>
|
||||||
|
replaceOptionMarkers(textNode, type, groupName, interactive, selected),
|
||||||
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -297,8 +315,11 @@ export function buildSigningDocumentHtml(
|
|||||||
replaceTextPlaceholder(root, SIGNATURE_PLACEHOLDER, '(待患者签署)')
|
replaceTextPlaceholder(root, SIGNATURE_PLACEHOLDER, '(待患者签署)')
|
||||||
}
|
}
|
||||||
|
|
||||||
convertCheckboxes(root, interactive)
|
// 已签署的文书再次打开时,把上次记录的勾选状态还原出来
|
||||||
convertInlineOptions(root, interactive)
|
const selected = new Set(data.selectedItems ?? [])
|
||||||
|
|
||||||
|
convertCheckboxes(root, interactive, selected)
|
||||||
|
convertInlineOptions(root, interactive, selected)
|
||||||
|
|
||||||
return root.innerHTML
|
return root.innerHTML
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref } from 'vue'
|
import { computed, onMounted, ref, watch } from 'vue'
|
||||||
|
|
||||||
import type { SigningTaskRecord } from '@/api/workbench/types'
|
import type { SigningTaskRecord } from '@/api/workbench/types'
|
||||||
import { buildSigningDocumentHtml } from '@/utils/signing-document'
|
import { buildSigningDocumentHtml } from '@/utils/signing-document'
|
||||||
@@ -22,10 +22,44 @@ const emit = defineEmits<{
|
|||||||
const containerRef = ref<HTMLElement | null>(null)
|
const containerRef = ref<HTMLElement | null>(null)
|
||||||
const selectedItems = ref<string[]>([])
|
const selectedItems = ref<string[]>([])
|
||||||
|
|
||||||
|
// 任务自带勾选(已签署的文书)在挂载/切换任务时同步出来并上报,
|
||||||
|
// 否则父级拿到的是空数组,会当成"一项都没勾"
|
||||||
|
watch(
|
||||||
|
() => props.task,
|
||||||
|
(task) => {
|
||||||
|
selectedItems.value = [...(task.selectedItems ?? [])]
|
||||||
|
emit('selection-change', [...selectedItems.value])
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
)
|
||||||
|
|
||||||
const processedHtml = computed(() =>
|
const processedHtml = computed(() =>
|
||||||
buildSigningDocumentHtml(props.task.documentContentHtml ?? '', props.task, props.interactive),
|
buildSigningDocumentHtml(props.task.documentContentHtml ?? '', props.task, props.interactive),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 兜底:文书 HTML 更新后,按任务记录再同步一次 DOM 的勾选状态。
|
||||||
|
* 生成 HTML 时已经写了 checked 属性,这里不依赖 attribute 在序列化中的表现,
|
||||||
|
* 确保「签完再打开时勾选还在」这件事稳定成立。
|
||||||
|
*/
|
||||||
|
function applyStoredSelection() {
|
||||||
|
const container = containerRef.value
|
||||||
|
|
||||||
|
if (!container) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const selected = new Set(props.task.selectedItems ?? [])
|
||||||
|
|
||||||
|
container.querySelectorAll<HTMLInputElement>('input.sign-doc-checkbox').forEach((input) => {
|
||||||
|
input.checked = selected.has(input.getAttribute('data-sign-item') ?? '')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(processedHtml, applyStoredSelection, { flush: 'post' })
|
||||||
|
|
||||||
|
onMounted(applyStoredSelection)
|
||||||
|
|
||||||
function handleSelectionChange(event: Event) {
|
function handleSelectionChange(event: Event) {
|
||||||
const target = event.target as HTMLInputElement
|
const target = event.target as HTMLInputElement
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ defineProps<{
|
|||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
action: [action: SigningTaskAction]
|
action: [action: SigningTaskAction]
|
||||||
downloadArtifact: [artifact: SigningArtifact]
|
downloadArtifact: [artifact: SigningArtifact]
|
||||||
|
selectionChange: [items: string[]]
|
||||||
}>()
|
}>()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -144,6 +145,7 @@ const emit = defineEmits<{
|
|||||||
v-if="task.documentContentHtml"
|
v-if="task.documentContentHtml"
|
||||||
:task="task"
|
:task="task"
|
||||||
:interactive="allowLocalSigning && task.status !== 'signed' && task.status !== 'void'"
|
:interactive="allowLocalSigning && task.status !== 'signed' && task.status !== 'void'"
|
||||||
|
@selection-change="emit('selectionChange', $event)"
|
||||||
/>
|
/>
|
||||||
<SigningDocumentPreview v-else :task="task" />
|
<SigningDocumentPreview v-else :task="task" />
|
||||||
<TaskAuditTimeline :events="auditEvents" :loading="auditLoading" :error="auditError" />
|
<TaskAuditTimeline :events="auditEvents" :loading="auditLoading" :error="auditError" />
|
||||||
|
|||||||
@@ -80,6 +80,8 @@ const page = ref(1)
|
|||||||
const pageSize = ref(20)
|
const pageSize = ref(20)
|
||||||
const selectedTaskId = ref<string | null>(null)
|
const selectedTaskId = ref<string | null>(null)
|
||||||
const selectedTask = ref<SigningTaskRecord | null>(null)
|
const selectedTask = ref<SigningTaskRecord | null>(null)
|
||||||
|
/** 当前任务里患者已勾选的条目,签署完成时随签名一起固化到任务上 */
|
||||||
|
const signingSelection = ref<string[]>([])
|
||||||
const taskEvents = ref<SigningTaskEvent[]>([])
|
const taskEvents = ref<SigningTaskEvent[]>([])
|
||||||
const artifacts = ref<SigningArtifact[]>([])
|
const artifacts = ref<SigningArtifact[]>([])
|
||||||
const templates = ref<SigningTemplate[]>([])
|
const templates = ref<SigningTemplate[]>([])
|
||||||
@@ -213,6 +215,9 @@ async function loadOrganizationOptions() {
|
|||||||
|
|
||||||
async function loadTaskDetail(id: string | null) {
|
async function loadTaskDetail(id: string | null) {
|
||||||
const requestId = ++detailRequestId
|
const requestId = ++detailRequestId
|
||||||
|
// 换任务时先清空勾选,避免把上一个任务的勾选带到新任务上;
|
||||||
|
// 文书组件挂载后会用任务自身的勾选重新上报
|
||||||
|
signingSelection.value = []
|
||||||
|
|
||||||
if (!id) {
|
if (!id) {
|
||||||
selectedTask.value = null
|
selectedTask.value = null
|
||||||
@@ -339,6 +344,10 @@ function selectTask(id: string) {
|
|||||||
void loadTaskDetail(id)
|
void loadTaskDetail(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleSelectionChange(items: string[]) {
|
||||||
|
signingSelection.value = items
|
||||||
|
}
|
||||||
|
|
||||||
function handlePageChange(nextPage: number) {
|
function handlePageChange(nextPage: number) {
|
||||||
const pageCount = Math.max(Math.ceil(total.value / pageSize.value), 1)
|
const pageCount = Math.max(Math.ceil(total.value / pageSize.value), 1)
|
||||||
|
|
||||||
@@ -504,7 +513,10 @@ async function confirmPadSignature(signatureDataUrl: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await refreshTask(await completeSigningTask(task.id, signatureDataUrl), '手写签署已完成')
|
await refreshTask(
|
||||||
|
await completeSigningTask(task.id, signatureDataUrl, signingSelection.value),
|
||||||
|
'手写签署已完成',
|
||||||
|
)
|
||||||
signatureDialogVisible.value = false
|
signatureDialogVisible.value = false
|
||||||
} catch {
|
} catch {
|
||||||
ElMessage.error('手写签署失败,请稍后重试')
|
ElMessage.error('手写签署失败,请稍后重试')
|
||||||
@@ -519,7 +531,10 @@ async function completeOnlineSignature(signatureDataUrl: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await refreshTask(await completeSigningTask(task.id, signatureDataUrl), '线上签署已完成')
|
await refreshTask(
|
||||||
|
await completeSigningTask(task.id, signatureDataUrl, signingSelection.value),
|
||||||
|
'线上签署已完成',
|
||||||
|
)
|
||||||
onlineDialogVisible.value = false
|
onlineDialogVisible.value = false
|
||||||
} catch {
|
} catch {
|
||||||
ElMessage.error('线上签署失败,请稍后重试')
|
ElMessage.error('线上签署失败,请稍后重试')
|
||||||
@@ -639,6 +654,7 @@ onMounted(() => {
|
|||||||
:pad-session-loading="padSessionLoading"
|
:pad-session-loading="padSessionLoading"
|
||||||
@action="handleTaskAction"
|
@action="handleTaskAction"
|
||||||
@download-artifact="downloadArtifact"
|
@download-artifact="downloadArtifact"
|
||||||
|
@selection-change="handleSelectionChange"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user