feat(workbench): 接入签署投递与产物管理
This commit is contained in:
@@ -2,6 +2,7 @@
|
|||||||
import { computed, nextTick, ref, watch } from 'vue'
|
import { computed, nextTick, ref, watch } from 'vue'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
|
|
||||||
|
import { isSigningMockEnabled } from '@/api/workbench/signing'
|
||||||
import type { SigningTaskRecord, SigningTemplate } from '@/api/workbench/types'
|
import type { SigningTaskRecord, SigningTemplate } from '@/api/workbench/types'
|
||||||
import { useSigningTaskForm } from '@/composables/useSigningTaskForm'
|
import { useSigningTaskForm } from '@/composables/useSigningTaskForm'
|
||||||
import { useAppStore } from '@/stores/app'
|
import { useAppStore } from '@/stores/app'
|
||||||
@@ -41,6 +42,7 @@ const {
|
|||||||
selectedTemplateId,
|
selectedTemplateId,
|
||||||
selectedVisit,
|
selectedVisit,
|
||||||
selectedVisitId,
|
selectedVisitId,
|
||||||
|
smsDestination,
|
||||||
submit: submitForm,
|
submit: submitForm,
|
||||||
submitting,
|
submitting,
|
||||||
visits,
|
visits,
|
||||||
@@ -58,7 +60,9 @@ async function submitTask(keepOpen: boolean) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
emit('created', task)
|
emit('created', task)
|
||||||
ElMessage.success(`已创建 1 个签署任务${task.method === 'sms' ? ',已生成短信投递意图' : ''}`)
|
const smsMessage =
|
||||||
|
task.method === 'sms' ? (isSigningMockEnabled ? ',已生成短信投递意图' : ',短信已发送') : ''
|
||||||
|
ElMessage.success(`已创建 1 个签署任务${smsMessage}`)
|
||||||
|
|
||||||
if (keepOpen) {
|
if (keepOpen) {
|
||||||
resetForNextTask()
|
resetForNextTask()
|
||||||
@@ -285,7 +289,25 @@ watch(
|
|||||||
<strong>短信将发送至</strong>
|
<strong>短信将发送至</strong>
|
||||||
<span>{{ patient ? maskPhone(patient.phone) : '定位患者后显示' }}</span>
|
<span>{{ patient ? maskPhone(patient.phone) : '定位患者后显示' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<p>使用患者权威记录中的手机号,具体投递结果以服务端返回为准。</p>
|
<template v-if="isSigningMockEnabled">
|
||||||
|
<p>使用患者权威记录中的手机号,具体投递结果以服务端返回为准。</p>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<label class="sms-destination-field">
|
||||||
|
<span>真实投递手机号</span>
|
||||||
|
<input
|
||||||
|
v-model="smsDestination"
|
||||||
|
inputmode="tel"
|
||||||
|
autocomplete="tel"
|
||||||
|
placeholder="请输入完整手机号或 E.164 地址"
|
||||||
|
@input="errors.sms = ''"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<p>
|
||||||
|
患者接口仅返回脱敏手机号,真实联调时由当前操作人员确认投递地址;服务端不会回显完整号码。
|
||||||
|
</p>
|
||||||
|
<p v-if="errors.sms" class="field-error">{{ errors.sms }}</p>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -826,6 +848,42 @@ watch(
|
|||||||
font-size: 11.5px;
|
font-size: 11.5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sms-destination-field {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
padding-left: 28px;
|
||||||
|
margin-top: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sms-destination-field span {
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: var(--brand-d);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sms-destination-field input {
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
padding: 6px 9px;
|
||||||
|
color: var(--ink);
|
||||||
|
font: inherit;
|
||||||
|
font-size: 12px;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sms-destination-field input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--brand);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sms-confirm .field-error {
|
||||||
|
padding-left: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
.dialog-footer {
|
.dialog-footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
@@ -894,5 +952,14 @@ watch(
|
|||||||
.dialog-footer > span:first-child {
|
.dialog-footer > span:first-child {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sms-destination-field {
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sms-destination-field input {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
import { computed, reactive, ref, toValue, type MaybeRefOrGetter } from 'vue'
|
import { computed, reactive, ref, toValue, type MaybeRefOrGetter } from 'vue'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
|
|
||||||
import { createSigningTask, getPatientProfile, getSigningTemplates } from '@/api/workbench/signing'
|
import { sendSigningSms } from '@/api/workbench/deliveries'
|
||||||
|
import {
|
||||||
|
createSigningTask,
|
||||||
|
getPatientProfile,
|
||||||
|
getSigningTemplates,
|
||||||
|
isSigningMockEnabled,
|
||||||
|
} from '@/api/workbench/signing'
|
||||||
import type {
|
import type {
|
||||||
PatientProfile,
|
PatientProfile,
|
||||||
PatientVisit,
|
PatientVisit,
|
||||||
@@ -35,6 +41,7 @@ export function useSigningTaskForm(options: UseSigningTaskFormOptions) {
|
|||||||
const selectedTemplateId = ref('')
|
const selectedTemplateId = ref('')
|
||||||
const selectedVisitId = ref('')
|
const selectedVisitId = ref('')
|
||||||
const method = ref<SigningMethod>('pad')
|
const method = ref<SigningMethod>('pad')
|
||||||
|
const smsDestination = ref('')
|
||||||
const loadingTemplates = ref(false)
|
const loadingTemplates = ref(false)
|
||||||
const locating = ref(false)
|
const locating = ref(false)
|
||||||
const submitting = ref(false)
|
const submitting = ref(false)
|
||||||
@@ -43,6 +50,7 @@ export function useSigningTaskForm(options: UseSigningTaskFormOptions) {
|
|||||||
patient: '',
|
patient: '',
|
||||||
visit: '',
|
visit: '',
|
||||||
document: '',
|
document: '',
|
||||||
|
sms: '',
|
||||||
})
|
})
|
||||||
|
|
||||||
const selectedVisit = computed(
|
const selectedVisit = computed(
|
||||||
@@ -104,6 +112,7 @@ export function useSigningTaskForm(options: UseSigningTaskFormOptions) {
|
|||||||
errors.patient = ''
|
errors.patient = ''
|
||||||
errors.visit = ''
|
errors.visit = ''
|
||||||
errors.document = ''
|
errors.document = ''
|
||||||
|
errors.sms = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetForm() {
|
function resetForm() {
|
||||||
@@ -116,6 +125,7 @@ export function useSigningTaskForm(options: UseSigningTaskFormOptions) {
|
|||||||
selectedTemplateId.value = initialTemplate?.id ?? ''
|
selectedTemplateId.value = initialTemplate?.id ?? ''
|
||||||
selectedVisitId.value = ''
|
selectedVisitId.value = ''
|
||||||
method.value = initialTemplate?.supportedMethods[0] ?? 'pad'
|
method.value = initialTemplate?.supportedMethods[0] ?? 'pad'
|
||||||
|
smsDestination.value = ''
|
||||||
clearErrors()
|
clearErrors()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,7 +135,9 @@ export function useSigningTaskForm(options: UseSigningTaskFormOptions) {
|
|||||||
documentSearch.value = ''
|
documentSearch.value = ''
|
||||||
selectedTemplateId.value = initialTemplate?.id ?? ''
|
selectedTemplateId.value = initialTemplate?.id ?? ''
|
||||||
method.value = initialTemplate?.supportedMethods[0] ?? method.value
|
method.value = initialTemplate?.supportedMethods[0] ?? method.value
|
||||||
|
smsDestination.value = ''
|
||||||
errors.document = ''
|
errors.document = ''
|
||||||
|
errors.sms = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadTemplates() {
|
async function loadTemplates() {
|
||||||
@@ -228,6 +240,15 @@ export function useSigningTaskForm(options: UseSigningTaskFormOptions) {
|
|||||||
valid = false
|
valid = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!isSigningMockEnabled && method.value === 'sms') {
|
||||||
|
const destination = smsDestination.value.trim().replace(/\s+/g, '')
|
||||||
|
|
||||||
|
if (!/^\+?[1-9]\d{6,14}$/.test(destination)) {
|
||||||
|
errors.sms = '请输入完整的手机号或 E.164 地址,才能发送短信'
|
||||||
|
valid = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return valid
|
return valid
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,7 +260,7 @@ export function useSigningTaskForm(options: UseSigningTaskFormOptions) {
|
|||||||
submitting.value = true
|
submitting.value = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await createSigningTask({
|
const task = await createSigningTask({
|
||||||
campus: toValue(options.campus),
|
campus: toValue(options.campus),
|
||||||
patientId: patient.value.id,
|
patientId: patient.value.id,
|
||||||
visitId: selectedVisit.value.id,
|
visitId: selectedVisit.value.id,
|
||||||
@@ -251,6 +272,15 @@ export function useSigningTaskForm(options: UseSigningTaskFormOptions) {
|
|||||||
visitNo: selectedVisit.value.visitNo,
|
visitNo: selectedVisit.value.visitNo,
|
||||||
department: selectedVisit.value.department,
|
department: selectedVisit.value.department,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (!isSigningMockEnabled && method.value === 'sms') {
|
||||||
|
await sendSigningSms(task.id, {
|
||||||
|
destination: smsDestination.value.trim().replace(/\s+/g, ''),
|
||||||
|
templateCode: 'SIGN_LINK',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return task
|
||||||
} finally {
|
} finally {
|
||||||
submitting.value = false
|
submitting.value = false
|
||||||
}
|
}
|
||||||
@@ -277,6 +307,7 @@ export function useSigningTaskForm(options: UseSigningTaskFormOptions) {
|
|||||||
selectedTemplateId,
|
selectedTemplateId,
|
||||||
selectedVisit,
|
selectedVisit,
|
||||||
selectedVisitId,
|
selectedVisitId,
|
||||||
|
smsDestination,
|
||||||
submit,
|
submit,
|
||||||
submitting,
|
submitting,
|
||||||
templates,
|
templates,
|
||||||
|
|||||||
@@ -0,0 +1,182 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { SigningArtifact } from '@/api/workbench/types'
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
artifacts: SigningArtifact[]
|
||||||
|
loading: boolean
|
||||||
|
error: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
download: [artifact: SigningArtifact]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
function formatFileSize(size: number) {
|
||||||
|
if (!size) {
|
||||||
|
return '大小未知'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (size < 1024) {
|
||||||
|
return `${size} B`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (size < 1024 * 1024) {
|
||||||
|
return `${(size / 1024).toFixed(1)} KB`
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${(size / (1024 * 1024)).toFixed(1)} MB`
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section class="artifact-card" aria-labelledby="artifact-title">
|
||||||
|
<div class="artifact-heading">
|
||||||
|
<div>
|
||||||
|
<h3 id="artifact-title">签署文件</h3>
|
||||||
|
<p>原始文档、签名原图和签署后文档均由服务端留存。</p>
|
||||||
|
</div>
|
||||||
|
<span class="artifact-count">{{ artifacts.length }} 个文件</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="loading" class="artifact-loading">正在加载文件索引…</div>
|
||||||
|
<div v-else-if="error" class="artifact-empty">文件索引加载失败,请稍后重试。</div>
|
||||||
|
<div v-else-if="!artifacts.length" class="artifact-empty">当前任务暂无可下载的签署文件。</div>
|
||||||
|
<ul v-else class="artifact-list">
|
||||||
|
<li v-for="artifact in artifacts" :key="artifact.id" class="artifact-row">
|
||||||
|
<div class="artifact-icon" aria-hidden="true">
|
||||||
|
{{ artifact.mimeType === 'application/pdf' ? 'PDF' : '图' }}
|
||||||
|
</div>
|
||||||
|
<div class="artifact-info">
|
||||||
|
<strong>{{ artifact.label }}</strong>
|
||||||
|
<span>{{ artifact.fileName }} · {{ formatFileSize(artifact.size) }}</span>
|
||||||
|
<small v-if="artifact.createdAt">生成于 {{ artifact.createdAt }}</small>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="artifact-download" @click="emit('download', artifact)">
|
||||||
|
下载
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.artifact-card {
|
||||||
|
padding: 16px 18px;
|
||||||
|
margin-top: 14px;
|
||||||
|
background: var(--card);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: var(--r);
|
||||||
|
box-shadow: var(--sh);
|
||||||
|
}
|
||||||
|
|
||||||
|
.artifact-heading {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.artifact-heading h3 {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--ink);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.artifact-heading p {
|
||||||
|
margin: 4px 0 0;
|
||||||
|
color: var(--mut);
|
||||||
|
font-size: 11.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.artifact-count {
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 3px 8px;
|
||||||
|
color: var(--brand);
|
||||||
|
font-size: 11px;
|
||||||
|
background: var(--brand-l);
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.artifact-loading,
|
||||||
|
.artifact-empty {
|
||||||
|
padding: 14px 0 2px;
|
||||||
|
color: var(--mut);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.artifact-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.artifact-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 9px 10px;
|
||||||
|
background: #f8fbfc;
|
||||||
|
border: 1px solid #edf3f5;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.artifact-icon {
|
||||||
|
display: grid;
|
||||||
|
width: 34px;
|
||||||
|
height: 34px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: var(--brand);
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 800;
|
||||||
|
background: var(--brand-l);
|
||||||
|
border-radius: 7px;
|
||||||
|
place-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.artifact-info {
|
||||||
|
display: flex;
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.artifact-info strong {
|
||||||
|
overflow: hidden;
|
||||||
|
color: var(--ink);
|
||||||
|
font-size: 12.5px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.artifact-info span,
|
||||||
|
.artifact-info small {
|
||||||
|
overflow: hidden;
|
||||||
|
color: var(--mut);
|
||||||
|
font-size: 11px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.artifact-info small {
|
||||||
|
color: #9aabb3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.artifact-download {
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 5px 10px;
|
||||||
|
color: var(--brand);
|
||||||
|
font-size: 12px;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid var(--brand);
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.artifact-download:hover {
|
||||||
|
background: var(--brand-l);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { SigningTaskEvent, SigningTaskRecord } from '@/api/workbench/types'
|
import type { SigningArtifact, SigningTaskEvent, SigningTaskRecord } from '@/api/workbench/types'
|
||||||
|
|
||||||
import SigningDocumentPreview from './SigningDocumentPreview.vue'
|
import SigningDocumentPreview from './SigningDocumentPreview.vue'
|
||||||
|
import SigningArtifactList from './SigningArtifactList.vue'
|
||||||
import TaskAuditTimeline from './TaskAuditTimeline.vue'
|
import TaskAuditTimeline from './TaskAuditTimeline.vue'
|
||||||
import type { SigningTaskAction } from '../types'
|
import type { SigningTaskAction } from '../types'
|
||||||
|
|
||||||
@@ -12,10 +13,16 @@ defineProps<{
|
|||||||
auditLoading: boolean
|
auditLoading: boolean
|
||||||
auditError: boolean
|
auditError: boolean
|
||||||
allowLocalSigning: boolean
|
allowLocalSigning: boolean
|
||||||
|
artifacts: SigningArtifact[]
|
||||||
|
artifactsLoading: boolean
|
||||||
|
artifactsError: boolean
|
||||||
|
padSessionStatus: string | null
|
||||||
|
padSessionLoading: boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
action: [action: SigningTaskAction]
|
action: [action: SigningTaskAction]
|
||||||
|
downloadArtifact: [artifact: SigningArtifact]
|
||||||
}>()
|
}>()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -43,7 +50,20 @@ const emit = defineEmits<{
|
|||||||
转为短信发送
|
转为短信发送
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
<span v-else class="action-hint">手写板设备签署接口待接入</span>
|
<template v-else>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="action-button"
|
||||||
|
:disabled="padSessionLoading"
|
||||||
|
@click="emit('action', 'create-pad-session')"
|
||||||
|
>
|
||||||
|
{{ padSessionLoading ? '创建会话中…' : '创建手写板会话' }}
|
||||||
|
</button>
|
||||||
|
<span v-if="padSessionStatus" class="action-hint">
|
||||||
|
会话状态:{{ padSessionStatus }};等待设备适配器读取 challenge
|
||||||
|
</span>
|
||||||
|
<span v-else class="action-hint">创建会话后仍需接入厂商设备适配器完成签名采集</span>
|
||||||
|
</template>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-else-if="task.status === 'pending' && task.method === 'sms'">
|
<template v-else-if="task.status === 'pending' && task.method === 'sms'">
|
||||||
@@ -120,6 +140,12 @@ const emit = defineEmits<{
|
|||||||
|
|
||||||
<SigningDocumentPreview :task="task" />
|
<SigningDocumentPreview :task="task" />
|
||||||
<TaskAuditTimeline :events="auditEvents" :loading="auditLoading" :error="auditError" />
|
<TaskAuditTimeline :events="auditEvents" :loading="auditLoading" :error="auditError" />
|
||||||
|
<SigningArtifactList
|
||||||
|
:artifacts="artifacts"
|
||||||
|
:loading="artifactsLoading"
|
||||||
|
:error="artifactsError"
|
||||||
|
@download="emit('downloadArtifact', $event)"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div v-else class="detail-empty">
|
<div v-else class="detail-empty">
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ import { computed, onMounted, reactive, ref, watch } from 'vue'
|
|||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
|
|
||||||
|
import { getCampuses, getDepartments } from '@/api/management/organization'
|
||||||
|
import { downloadSigningArtifact, getSigningArtifacts } from '@/api/workbench/artifacts'
|
||||||
|
import { createPadSigningSession } from '@/api/workbench/deliveries'
|
||||||
import {
|
import {
|
||||||
completeSigningTask,
|
completeSigningTask,
|
||||||
getSigningTaskDetail,
|
getSigningTaskDetail,
|
||||||
@@ -18,6 +21,8 @@ import {
|
|||||||
import type {
|
import type {
|
||||||
SigningDateRange,
|
SigningDateRange,
|
||||||
SigningMethod,
|
SigningMethod,
|
||||||
|
SigningArtifact,
|
||||||
|
SignDeliveryResponseDto,
|
||||||
SigningTaskEvent,
|
SigningTaskEvent,
|
||||||
SigningTaskRecord,
|
SigningTaskRecord,
|
||||||
SigningTaskStatus,
|
SigningTaskStatus,
|
||||||
@@ -76,11 +81,19 @@ const total = ref(0)
|
|||||||
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 taskEvents = ref<SigningTaskEvent[]>([])
|
const taskEvents = ref<SigningTaskEvent[]>([])
|
||||||
|
const artifacts = ref<SigningArtifact[]>([])
|
||||||
const templates = ref<SigningTemplate[]>([])
|
const templates = ref<SigningTemplate[]>([])
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
const detailLoading = ref(false)
|
const detailLoading = ref(false)
|
||||||
const eventsLoading = ref(false)
|
const eventsLoading = ref(false)
|
||||||
const eventsError = ref(false)
|
const eventsError = ref(false)
|
||||||
|
const artifactsLoading = ref(false)
|
||||||
|
const artifactsError = ref(false)
|
||||||
|
const padSessionStatus = ref<string | null>(null)
|
||||||
|
const padSessionLoading = ref(false)
|
||||||
|
const campusIds = ref<Record<string, string>>({})
|
||||||
|
const campusNames = ref<Record<string, WorkbenchCampus>>({})
|
||||||
|
const departmentIds = ref<Record<string, string>>({})
|
||||||
const newDialogVisible = ref(false)
|
const newDialogVisible = ref(false)
|
||||||
const signatureDialogVisible = ref(false)
|
const signatureDialogVisible = ref(false)
|
||||||
const onlineDialogVisible = ref(false)
|
const onlineDialogVisible = ref(false)
|
||||||
@@ -131,6 +144,9 @@ const documentOptions = computed<SigningFilterOption[]>(() => [
|
|||||||
|
|
||||||
const signingApiOptions = computed(() => ({
|
const signingApiOptions = computed(() => ({
|
||||||
campus: appStore.selectedCampus,
|
campus: appStore.selectedCampus,
|
||||||
|
campusId: campusIds.value[appStore.selectedCampus],
|
||||||
|
campusNames: campusNames.value,
|
||||||
|
departmentIds: departmentIds.value,
|
||||||
templates: templates.value,
|
templates: templates.value,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
@@ -162,27 +178,67 @@ async function loadTemplates() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadOrganizationOptions() {
|
||||||
|
if (isSigningMockEnabled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const [campusesResult, departmentsResult] = await Promise.allSettled([
|
||||||
|
getCampuses(),
|
||||||
|
getDepartments(),
|
||||||
|
])
|
||||||
|
|
||||||
|
if (campusesResult.status === 'fulfilled') {
|
||||||
|
campusIds.value = Object.fromEntries(
|
||||||
|
campusesResult.value.map((campus) => [campus.name, campus.id]),
|
||||||
|
)
|
||||||
|
campusNames.value = Object.fromEntries(
|
||||||
|
campusesResult.value
|
||||||
|
.filter(
|
||||||
|
(campus): campus is typeof campus & { name: WorkbenchCampus } =>
|
||||||
|
campus.name === '本部院区' || campus.name === '东院区' || campus.name === '西院区',
|
||||||
|
)
|
||||||
|
.map((campus) => [campus.id, campus.name]),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (departmentsResult.status === 'fulfilled') {
|
||||||
|
departmentIds.value = Object.fromEntries(
|
||||||
|
departmentsResult.value.map((department) => [department.name, department.id]),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function loadTaskDetail(id: string | null) {
|
async function loadTaskDetail(id: string | null) {
|
||||||
const requestId = ++detailRequestId
|
const requestId = ++detailRequestId
|
||||||
|
|
||||||
if (!id) {
|
if (!id) {
|
||||||
selectedTask.value = null
|
selectedTask.value = null
|
||||||
taskEvents.value = []
|
taskEvents.value = []
|
||||||
|
artifacts.value = []
|
||||||
detailLoading.value = false
|
detailLoading.value = false
|
||||||
eventsLoading.value = false
|
eventsLoading.value = false
|
||||||
eventsError.value = false
|
eventsError.value = false
|
||||||
|
artifactsLoading.value = false
|
||||||
|
artifactsError.value = false
|
||||||
|
padSessionStatus.value = null
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
detailLoading.value = true
|
detailLoading.value = true
|
||||||
eventsLoading.value = true
|
eventsLoading.value = true
|
||||||
eventsError.value = false
|
eventsError.value = false
|
||||||
|
artifactsLoading.value = true
|
||||||
|
artifactsError.value = false
|
||||||
taskEvents.value = []
|
taskEvents.value = []
|
||||||
|
artifacts.value = []
|
||||||
|
padSessionStatus.value = null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [detailResult, eventsResult] = await Promise.allSettled([
|
const [detailResult, eventsResult, artifactsResult] = await Promise.allSettled([
|
||||||
getSigningTaskDetail(id, signingApiOptions.value),
|
getSigningTaskDetail(id, signingApiOptions.value),
|
||||||
getSigningTaskEvents(id),
|
getSigningTaskEvents(id),
|
||||||
|
getSigningArtifacts(id),
|
||||||
])
|
])
|
||||||
|
|
||||||
if (requestId === detailRequestId) {
|
if (requestId === detailRequestId) {
|
||||||
@@ -197,6 +253,12 @@ async function loadTaskDetail(id: string | null) {
|
|||||||
} else {
|
} else {
|
||||||
eventsError.value = true
|
eventsError.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (artifactsResult.status === 'fulfilled') {
|
||||||
|
artifacts.value = artifactsResult.value
|
||||||
|
} else {
|
||||||
|
artifactsError.value = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
if (requestId === detailRequestId) {
|
if (requestId === detailRequestId) {
|
||||||
@@ -208,6 +270,7 @@ async function loadTaskDetail(id: string | null) {
|
|||||||
if (requestId === detailRequestId) {
|
if (requestId === detailRequestId) {
|
||||||
detailLoading.value = false
|
detailLoading.value = false
|
||||||
eventsLoading.value = false
|
eventsLoading.value = false
|
||||||
|
artifactsLoading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -295,6 +358,22 @@ async function handleTaskAction(action: SigningTaskAction) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (action === 'create-pad-session') {
|
||||||
|
padSessionLoading.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const session: SignDeliveryResponseDto = await createPadSigningSession(task.id)
|
||||||
|
padSessionStatus.value = session.status
|
||||||
|
ElMessage.success('手写板签署会话已创建,请由设备适配器继续采集签名')
|
||||||
|
} catch {
|
||||||
|
ElMessage.error('手写板签署会话创建失败,请稍后重试')
|
||||||
|
} finally {
|
||||||
|
padSessionLoading.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (action === 'online-sign') {
|
if (action === 'online-sign') {
|
||||||
if (!isSigningMockEnabled) {
|
if (!isSigningMockEnabled) {
|
||||||
ElMessage.info('线上签署页面接口待接入')
|
ElMessage.info('线上签署页面接口待接入')
|
||||||
@@ -311,12 +390,26 @@ async function handleTaskAction(action: SigningTaskAction) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (action === 'download-pdf') {
|
if (action === 'download-pdf') {
|
||||||
ElMessage.info('原型演示:PDF 原件下载接口待接入')
|
const artifact =
|
||||||
|
artifacts.value.find((item) => item.artifactType === 'SIGNED_PDF') ??
|
||||||
|
artifacts.value.find((item) => item.artifactType === 'ORIGINAL_PDF')
|
||||||
|
|
||||||
|
if (artifact) {
|
||||||
|
await downloadArtifact(artifact)
|
||||||
|
} else {
|
||||||
|
ElMessage.info('当前任务暂无 PDF 文件')
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (action === 'download-signature') {
|
if (action === 'download-signature') {
|
||||||
ElMessage.info('原型演示:签名原图下载接口待接入')
|
const artifact = artifacts.value.find((item) => item.artifactType === 'SIGNATURE_IMAGE')
|
||||||
|
|
||||||
|
if (artifact) {
|
||||||
|
await downloadArtifact(artifact)
|
||||||
|
} else {
|
||||||
|
ElMessage.info('当前任务暂无签名原图')
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -365,6 +458,21 @@ async function handleTaskAction(action: SigningTaskAction) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function downloadArtifact(artifact: SigningArtifact) {
|
||||||
|
try {
|
||||||
|
const blob = await downloadSigningArtifact(artifact.id)
|
||||||
|
const url = URL.createObjectURL(blob)
|
||||||
|
const link = document.createElement('a')
|
||||||
|
link.href = url
|
||||||
|
link.download = artifact.fileName
|
||||||
|
link.click()
|
||||||
|
window.setTimeout(() => URL.revokeObjectURL(url), 0)
|
||||||
|
ElMessage.success(`${artifact.label}下载已开始`)
|
||||||
|
} catch {
|
||||||
|
ElMessage.error(`${artifact.label}下载失败,请稍后重试`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function confirmPadSignature(signatureDataUrl: string) {
|
async function confirmPadSignature(signatureDataUrl: string) {
|
||||||
const task = selectedTask.value
|
const task = selectedTask.value
|
||||||
|
|
||||||
@@ -421,6 +529,7 @@ async function handleOnlineResend() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function initializePage() {
|
async function initializePage() {
|
||||||
|
await loadOrganizationOptions()
|
||||||
await loadTemplates()
|
await loadTemplates()
|
||||||
await loadTasks()
|
await loadTasks()
|
||||||
}
|
}
|
||||||
@@ -494,7 +603,13 @@ onMounted(() => {
|
|||||||
:audit-loading="eventsLoading"
|
:audit-loading="eventsLoading"
|
||||||
:audit-error="eventsError"
|
:audit-error="eventsError"
|
||||||
:allow-local-signing="isSigningMockEnabled"
|
:allow-local-signing="isSigningMockEnabled"
|
||||||
|
:artifacts="artifacts"
|
||||||
|
:artifacts-loading="artifactsLoading"
|
||||||
|
:artifacts-error="artifactsError"
|
||||||
|
:pad-session-status="padSessionStatus"
|
||||||
|
:pad-session-loading="padSessionLoading"
|
||||||
@action="handleTaskAction"
|
@action="handleTaskAction"
|
||||||
|
@download-artifact="downloadArtifact"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type {
|
import type {
|
||||||
PatientProfile,
|
PatientProfile,
|
||||||
PatientVisit,
|
PatientVisit,
|
||||||
|
SignDeliveryStatus,
|
||||||
SigningDateRange,
|
SigningDateRange,
|
||||||
SigningMethod,
|
SigningMethod,
|
||||||
SigningTaskRecord,
|
SigningTaskRecord,
|
||||||
@@ -40,6 +41,7 @@ export interface NewSigningTaskState {
|
|||||||
|
|
||||||
export type SigningTaskAction =
|
export type SigningTaskAction =
|
||||||
| 'pad-sign'
|
| 'pad-sign'
|
||||||
|
| 'create-pad-session'
|
||||||
| 'online-sign'
|
| 'online-sign'
|
||||||
| 'resend-sms'
|
| 'resend-sms'
|
||||||
| 'switch-method'
|
| 'switch-method'
|
||||||
@@ -48,3 +50,5 @@ export type SigningTaskAction =
|
|||||||
| 'download-pdf'
|
| 'download-pdf'
|
||||||
| 'download-signature'
|
| 'download-signature'
|
||||||
| 'print'
|
| 'print'
|
||||||
|
|
||||||
|
export type SigningPadSessionStatus = SignDeliveryStatus | null
|
||||||
|
|||||||
Reference in New Issue
Block a user