feat(signing): 支持点击文中签名位就地采集签名
This commit is contained in:
@@ -52,6 +52,7 @@ const signingProgress = ref<SigningFieldProgress>({
|
||||
filled: 0,
|
||||
missingRequired: [],
|
||||
abnormal: [],
|
||||
pendingSignatures: [],
|
||||
})
|
||||
const signingBodyRef = ref<HTMLElement | null>(null)
|
||||
|
||||
@@ -69,7 +70,13 @@ watch(
|
||||
if (visible) {
|
||||
activeTab.value = 'content'
|
||||
signingSelection.value = []
|
||||
signingProgress.value = { total: 0, filled: 0, missingRequired: [], abnormal: [] }
|
||||
signingProgress.value = {
|
||||
total: 0,
|
||||
filled: 0,
|
||||
missingRequired: [],
|
||||
abnormal: [],
|
||||
pendingSignatures: [],
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
@@ -1,21 +1,40 @@
|
||||
<script setup lang="ts">
|
||||
import { nextTick, ref, watch } from 'vue'
|
||||
import { nextTick, ref, useId, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
import type { SigningTaskRecord } from '@/api/workbench/types'
|
||||
|
||||
import SignatureCanvas from './SignatureCanvas.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean
|
||||
task: SigningTaskRecord | null
|
||||
}>()
|
||||
// 同一个签字板承担两件事:任务级的主签名采集,以及文书里单个签名位(医师签名、
|
||||
// 家属签名……)的采集。两者只有文案与「确认后做什么」不同,采集方式完全一样,
|
||||
// 因此用参数区分而不是复制一份弹窗。
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
visible: boolean
|
||||
task: SigningTaskRecord | null
|
||||
/** 弹窗标题:任务级是「手写板签署」,按签名位采集时传字段名(如「医师签名」) */
|
||||
title?: string
|
||||
/** 说明这一笔签名要落在哪里,避免医师签在了患者签名位 */
|
||||
hint?: string
|
||||
confirmLabel?: string
|
||||
}>(),
|
||||
{
|
||||
title: '手写板签署',
|
||||
hint: '请患者在下框内签名(当前为 Canvas 演示,真实手写板接入走设备适配器):',
|
||||
confirmLabel: '确认签署',
|
||||
},
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
close: []
|
||||
confirm: [signatureDataUrl: string]
|
||||
}>()
|
||||
|
||||
// 两个实例可能同时挂在 DOM 上(主签名位与某个签名位),标题 id 必须各自唯一,
|
||||
// 否则 aria-labelledby 会指到别人的标题上
|
||||
const titleId = useId()
|
||||
|
||||
const signatureDataUrl = ref<string | null>(null)
|
||||
const signatureCanvas = ref<{ clear: () => void } | null>(null)
|
||||
|
||||
@@ -37,7 +56,7 @@ function clearSignature() {
|
||||
|
||||
function confirmSignature() {
|
||||
if (!signatureDataUrl.value) {
|
||||
ElMessage.warning('请先完成患者签名')
|
||||
ElMessage.warning('请先完成签名')
|
||||
return
|
||||
}
|
||||
|
||||
@@ -52,19 +71,17 @@ function confirmSignature() {
|
||||
class="dialog-card signature-dialog"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="pad-title"
|
||||
:aria-labelledby="titleId"
|
||||
>
|
||||
<header class="dialog-header">
|
||||
<strong id="pad-title">手写板签署</strong>
|
||||
<strong :id="titleId">{{ title }}</strong>
|
||||
<span>{{ task.patientName }} · {{ task.documentName }}</span>
|
||||
<button type="button" class="dialog-close" aria-label="关闭" @click="emit('close')">
|
||||
×
|
||||
</button>
|
||||
</header>
|
||||
<div class="dialog-body">
|
||||
<p class="dialog-hint">
|
||||
请患者在下框内签名(当前为 Canvas 演示,真实手写板接入走设备适配器):
|
||||
</p>
|
||||
<p class="dialog-hint">{{ hint }}</p>
|
||||
<SignatureCanvas
|
||||
ref="signatureCanvas"
|
||||
:height="300"
|
||||
@@ -77,7 +94,9 @@ function confirmSignature() {
|
||||
<button type="button" class="action-button action-button--ghost" @click="clearSignature">
|
||||
清空重写
|
||||
</button>
|
||||
<button type="button" class="action-button" @click="confirmSignature">确认签署</button>
|
||||
<button type="button" class="action-button" @click="confirmSignature">
|
||||
{{ confirmLabel }}
|
||||
</button>
|
||||
</footer>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
+145
-2
@@ -3,11 +3,14 @@ import { computed, onMounted, ref, watch } from 'vue'
|
||||
|
||||
import type { SigningAnswers, SigningTaskRecord } from '@/api/workbench/types'
|
||||
import {
|
||||
applySignatureToRoot,
|
||||
buildSigningDocumentHtml,
|
||||
describeFieldProgress,
|
||||
readAnswersFromRoot,
|
||||
SIGNATURE_ACTION_SELECTOR,
|
||||
type SigningDocumentData,
|
||||
type SigningFieldProgress,
|
||||
type SigningSignatureRequest,
|
||||
} from '@/utils/signing-document'
|
||||
|
||||
// 签署文书交互化渲染:填写项渲染(HIS 预填 + 现场确认)、签名回显、勾选收集见
|
||||
@@ -17,13 +20,22 @@ const props = withDefaults(
|
||||
defineProps<{
|
||||
task: SigningTaskRecord
|
||||
interactive?: boolean
|
||||
/**
|
||||
* 在文书里点击签名位采集到的签名图(key 是字段键,如 doctor)。
|
||||
*
|
||||
* 不走 task:task 是服务端快照,这里装的是"本次会话刚采到、还没提交"的取值。
|
||||
* 单独一份也让「整篇重渲染」不会把刚采的签名冲掉。
|
||||
*/
|
||||
signatureInjections?: SigningAnswers
|
||||
}>(),
|
||||
{ interactive: false },
|
||||
{ interactive: false, signatureInjections: () => ({}) },
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
'selection-change': [items: string[]]
|
||||
'field-change': [answers: SigningAnswers, progress: SigningFieldProgress]
|
||||
/** 请求采集某个签名位,由签署页打开签字板并把结果回灌到 signatureInjections */
|
||||
'signature-request': [request: SigningSignatureRequest]
|
||||
}>()
|
||||
|
||||
const containerRef = ref<HTMLElement | null>(null)
|
||||
@@ -34,6 +46,7 @@ const progress = ref<SigningFieldProgress>({
|
||||
filled: 0,
|
||||
missingRequired: [],
|
||||
abnormal: [],
|
||||
pendingSignatures: [],
|
||||
})
|
||||
|
||||
// 只挑渲染真正需要的字段显式映射,不直接透传整个任务对象:
|
||||
@@ -55,6 +68,9 @@ const processedHtml = computed(() =>
|
||||
props.task.documentContentHtml ?? '',
|
||||
documentData.value,
|
||||
props.interactive,
|
||||
// 本组件就是签名位的点击宿主(见 handleDocumentClick),
|
||||
// 所以可采集与否只取决于文书当前是否可交互
|
||||
{ signatureActionable: props.interactive },
|
||||
),
|
||||
)
|
||||
|
||||
@@ -158,10 +174,100 @@ watch(
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
watch(processedHtml, applyStoredValues, { flush: 'post' })
|
||||
/**
|
||||
* 从事件目标往上找「可点击采集的签名位」。
|
||||
* 用 closest 而不是直接比对 target:签名位胶囊里还有类型标签、虚线框等子节点,
|
||||
* 点在这些子节点上时 target 并不是宿主节点本身。
|
||||
*/
|
||||
function resolveSignatureHost(target: EventTarget | null): HTMLElement | null {
|
||||
if (!(target instanceof Element)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const host = target.closest<HTMLElement>(SIGNATURE_ACTION_SELECTOR)
|
||||
|
||||
// closest 会一路向上穿过容器,事件也可能来自容器外,必须确认落在本文书里
|
||||
return host && containerRef.value?.contains(host) ? host : null
|
||||
}
|
||||
|
||||
function requestSignature(host: HTMLElement) {
|
||||
const fieldId = host.getAttribute('data-sign-field') ?? ''
|
||||
|
||||
if (!fieldId) {
|
||||
return
|
||||
}
|
||||
|
||||
emit('signature-request', {
|
||||
fieldId,
|
||||
label: host.getAttribute('data-sign-label') ?? fieldId,
|
||||
})
|
||||
}
|
||||
|
||||
function handleDocumentClick(event: MouseEvent) {
|
||||
if (!props.interactive) {
|
||||
return
|
||||
}
|
||||
|
||||
const host = resolveSignatureHost(event.target)
|
||||
|
||||
if (host) {
|
||||
requestSignature(host)
|
||||
}
|
||||
}
|
||||
|
||||
/** 签名位挂了 role=button 与 tabindex,键盘必须同样可用,否则只有鼠标点得动 */
|
||||
function handleDocumentKeydown(event: KeyboardEvent) {
|
||||
if (!props.interactive || (event.key !== 'Enter' && event.key !== ' ')) {
|
||||
return
|
||||
}
|
||||
|
||||
const host = resolveSignatureHost(event.target)
|
||||
|
||||
if (host) {
|
||||
// 空格默认会滚动容器,必须挡掉
|
||||
event.preventDefault()
|
||||
requestSignature(host)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 把外部采集到的签名图就地写进签名位,写完立刻回读 DOM 上报取值。
|
||||
*
|
||||
* 不重算 processedHtml:整篇重建会换掉 v-html 的 DOM,
|
||||
* 操作人员正在输入的填写项会连同光标一起丢。
|
||||
*/
|
||||
function patchSignatures() {
|
||||
const container = containerRef.value
|
||||
|
||||
if (!container) {
|
||||
return
|
||||
}
|
||||
|
||||
const patched = Object.entries(props.signatureInjections).some(
|
||||
([fieldId, value]) =>
|
||||
typeof value === 'string' && applySignatureToRoot(container, fieldId, value),
|
||||
)
|
||||
|
||||
if (patched) {
|
||||
syncFromDom()
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
processedHtml,
|
||||
() => {
|
||||
applyStoredValues()
|
||||
// 重渲染会丢掉就地写入的签名图,得重新贴一遍
|
||||
patchSignatures()
|
||||
},
|
||||
{ flush: 'post' },
|
||||
)
|
||||
|
||||
watch(() => props.signatureInjections, patchSignatures, { deep: true, flush: 'post' })
|
||||
|
||||
onMounted(() => {
|
||||
applyStoredValues()
|
||||
patchSignatures()
|
||||
syncFromDom()
|
||||
})
|
||||
|
||||
@@ -190,6 +296,8 @@ defineExpose({ getDocumentElement })
|
||||
class="sign-doc-body"
|
||||
@change="syncFromDom"
|
||||
@input="syncFromDom"
|
||||
@click="handleDocumentClick"
|
||||
@keydown="handleDocumentKeydown"
|
||||
v-html="processedHtml"
|
||||
/>
|
||||
<!-- eslint-enable vue/no-v-html -->
|
||||
@@ -204,6 +312,10 @@ defineExpose({ getDocumentElement })
|
||||
<span v-if="progress.missingRequired.length" class="is-missing">
|
||||
必填未完成:{{ progress.missingRequired.join('、') }},请补充后再采集签名。
|
||||
</span>
|
||||
<span v-else-if="progress.pendingSignatures.length" class="is-pending">
|
||||
还有签名位待采集:<strong>{{ progress.pendingSignatures.join('、') }}</strong
|
||||
>,可直接点击文中签名位完成。
|
||||
</span>
|
||||
<span v-else
|
||||
>完成填写后通过「手写板签署 / 线上签署」采集签名,填写项与勾选结果随签署证据一并提交。</span
|
||||
>
|
||||
@@ -297,6 +409,14 @@ defineExpose({ getDocumentElement })
|
||||
color: var(--err);
|
||||
}
|
||||
|
||||
.sign-doc-footer .is-pending {
|
||||
color: #a96900;
|
||||
}
|
||||
|
||||
.sign-doc-footer .is-pending strong {
|
||||
color: #a96900;
|
||||
}
|
||||
|
||||
/* 注入的文书 HTML(v-html 内容不携带 scoped 属性,统一用 :deep 覆盖基础文书样式) */
|
||||
.sign-doc-body :deep(p) {
|
||||
margin: 0 0 6px;
|
||||
@@ -469,6 +589,29 @@ defineExpose({ getDocumentElement })
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* 可点击采集的签名位:整块胶囊都是点击热区(子节点会冒泡到宿主),
|
||||
hover / 聚焦给出明确反馈,否则操作人员不知道这里能点 */
|
||||
.sign-doc-body :deep(.sign-field--actionable) {
|
||||
cursor: pointer;
|
||||
background: #fffaf0;
|
||||
border-color: #f0a21a;
|
||||
}
|
||||
|
||||
.sign-doc-body :deep(.sign-field--actionable:hover) {
|
||||
background: #fff3d8;
|
||||
border-color: #d98c05;
|
||||
box-shadow: 0 0 0 2px rgb(240 162 26 / 20%);
|
||||
}
|
||||
|
||||
.sign-doc-body :deep(.sign-field--actionable:focus-visible) {
|
||||
outline: 2px solid var(--brand);
|
||||
outline-offset: 1px;
|
||||
}
|
||||
|
||||
.sign-doc-body :deep(.sign-field--actionable .sign-field__signature-empty) {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* 签名位比其他填写项更宽,避免签名图被压扁 */
|
||||
.sign-doc-body :deep(.sign-field--signature) {
|
||||
padding: 3px 8px;
|
||||
|
||||
@@ -7,7 +7,7 @@ import type {
|
||||
SigningTaskEvent,
|
||||
SigningTaskRecord,
|
||||
} from '@/api/workbench/types'
|
||||
import type { SigningFieldProgress } from '@/utils/signing-document'
|
||||
import type { SigningFieldProgress, SigningSignatureRequest } from '@/utils/signing-document'
|
||||
|
||||
import SigningDocumentInteractive from './SigningDocumentInteractive.vue'
|
||||
import SigningDocumentPreview from './SigningDocumentPreview.vue'
|
||||
@@ -33,6 +33,8 @@ defineProps<{
|
||||
canViewAudit: boolean
|
||||
canViewArtifacts: boolean
|
||||
canDownloadArtifact: boolean
|
||||
/** 本次会话在文书里点击采集到的签名图,由签署页持有并回灌 */
|
||||
signatureInjections: SigningAnswers
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -40,6 +42,7 @@ const emit = defineEmits<{
|
||||
downloadArtifact: [artifact: SigningArtifact]
|
||||
selectionChange: [items: string[]]
|
||||
fieldChange: [answers: SigningAnswers, progress: SigningFieldProgress]
|
||||
signatureRequest: [request: SigningSignatureRequest]
|
||||
}>()
|
||||
|
||||
const documentRef = ref<InstanceType<typeof SigningDocumentInteractive> | null>(null)
|
||||
@@ -173,8 +176,10 @@ defineExpose({ getDocumentElement })
|
||||
ref="documentRef"
|
||||
:task="task"
|
||||
:interactive="allowLocalSigning && task.status !== 'signed' && task.status !== 'void'"
|
||||
:signature-injections="signatureInjections"
|
||||
@selection-change="emit('selectionChange', $event)"
|
||||
@field-change="(answers, progress) => emit('fieldChange', answers, progress)"
|
||||
@signature-request="emit('signatureRequest', $event)"
|
||||
/>
|
||||
<SigningDocumentPreview v-else :task="task" />
|
||||
<TaskAuditTimeline
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
import { getCampuses, getDepartments } from '@/api/management/organization'
|
||||
@@ -38,7 +38,7 @@ import type {
|
||||
import NewSigningTaskDialog from '@/components/signing/NewSigningTaskDialog.vue'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import { useLoginStore } from '@/stores/login'
|
||||
import type { SigningFieldProgress } from '@/utils/signing-document'
|
||||
import type { SigningFieldProgress, SigningSignatureRequest } from '@/utils/signing-document'
|
||||
import { downloadSigningPdf } from '@/utils/signing-pdf'
|
||||
|
||||
import OnlineSigningDialog from './components/OnlineSigningDialog.vue'
|
||||
@@ -95,6 +95,15 @@ const signingSelection = ref<string[]>([])
|
||||
const signingAnswers = ref<SigningAnswers>({})
|
||||
/** 文书填写项完成情况:签署前用它做必填校验、提示异常项 */
|
||||
const signingProgress = ref<SigningFieldProgress | null>(null)
|
||||
/**
|
||||
* 在文书里点击签名位采集到的签名图(key 是字段键,如 doctor)。
|
||||
* 与 signingAnswers 分开:signingAnswers 每次输入都会变,把它当渲染输入会让
|
||||
* 整篇文书重渲染、光标丢失;这份只在真的采到签名时变化。
|
||||
*/
|
||||
const fieldSignatureInjections = ref<SigningAnswers>({})
|
||||
/** 当前正在采集的签名位,null 表示弹窗不是由「点击文中签名位」打开的 */
|
||||
const pendingSignatureField = ref<SigningSignatureRequest | null>(null)
|
||||
const fieldSignatureDialogVisible = ref(false)
|
||||
const taskEvents = ref<SigningTaskEvent[]>([])
|
||||
const artifacts = ref<SigningArtifact[]>([])
|
||||
const templates = ref<SigningTemplate[]>([])
|
||||
@@ -253,6 +262,9 @@ async function loadTaskDetail(id: string | null) {
|
||||
signingSelection.value = []
|
||||
signingAnswers.value = {}
|
||||
signingProgress.value = null
|
||||
fieldSignatureInjections.value = {}
|
||||
pendingSignatureField.value = null
|
||||
fieldSignatureDialogVisible.value = false
|
||||
|
||||
if (!id) {
|
||||
selectedTask.value = null
|
||||
@@ -408,10 +420,14 @@ function handleFieldChange(answers: SigningAnswers, progress: SigningFieldProgre
|
||||
}
|
||||
|
||||
/**
|
||||
* 采集签名前的填写项校验。
|
||||
* 必填项没填完就采集签名,等于签了一份内容不完整的文书,因此在入口处挡住。
|
||||
* 采集主签名前的检查。
|
||||
*
|
||||
* ① 必填项没填完 → 直接挡住:签一份内容不完整的文书没有意义;
|
||||
* ② 文书里还有没采集的签名位 → 让操作人员确认后再继续。
|
||||
* 这一条不能硬挡:医师确实可能签在纸质件上,替业务做决定是越界;
|
||||
* 但也不能不提醒,否则文书会带着空签名位被固化。
|
||||
*/
|
||||
function assertFieldsReady() {
|
||||
async function assertSigningReady() {
|
||||
const missing = signingProgress.value?.missingRequired ?? []
|
||||
|
||||
if (missing.length) {
|
||||
@@ -419,7 +435,59 @@ function assertFieldsReady() {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
const pending = signingProgress.value?.pendingSignatures ?? []
|
||||
|
||||
if (!pending.length) {
|
||||
return true
|
||||
}
|
||||
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`文书中还有签名位未采集:${pending.join('、')}。可直接点击文中签名位完成,也可以继续采集主签名。`,
|
||||
'还有签名位待采集',
|
||||
{ confirmButtonText: '继续签署', cancelButtonText: '返回采集', type: 'warning' },
|
||||
)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/** 点击文中签名位:先记下采的是哪一个,再由弹窗把签名图回灌到该位置 */
|
||||
function handleSignatureRequest(request: SigningSignatureRequest) {
|
||||
if (!selectedTask.value) {
|
||||
return
|
||||
}
|
||||
|
||||
pendingSignatureField.value = request
|
||||
fieldSignatureDialogVisible.value = true
|
||||
}
|
||||
|
||||
function closeFieldSignatureDialog() {
|
||||
fieldSignatureDialogVisible.value = false
|
||||
pendingSignatureField.value = null
|
||||
}
|
||||
|
||||
/**
|
||||
* 单个签名位采集完成。
|
||||
*
|
||||
* 这里不调 completeSigningTask:医师签名只是文书上的一个填写项,
|
||||
* 任务级的「签署完成」仍要由主签名位(患者签名)触发,
|
||||
* 否则点一下医师签名就把整个任务标记成已签署了。
|
||||
*/
|
||||
function confirmFieldSignature(signatureDataUrl: string) {
|
||||
const request = pendingSignatureField.value
|
||||
|
||||
if (!request) {
|
||||
return
|
||||
}
|
||||
|
||||
fieldSignatureInjections.value = {
|
||||
...fieldSignatureInjections.value,
|
||||
[request.fieldId]: signatureDataUrl,
|
||||
}
|
||||
closeFieldSignatureDialog()
|
||||
ElMessage.success(`「${request.label}」签名已采集,将随本次签署一并固化`)
|
||||
}
|
||||
|
||||
function handlePageChange(nextPage: number) {
|
||||
@@ -471,7 +539,7 @@ async function handleTaskAction(action: SigningTaskAction) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!assertFieldsReady()) {
|
||||
if (!(await assertSigningReady())) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -501,7 +569,7 @@ async function handleTaskAction(action: SigningTaskAction) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!assertFieldsReady()) {
|
||||
if (!(await assertSigningReady())) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -803,10 +871,12 @@ onMounted(() => {
|
||||
:can-view-audit="canViewEvents"
|
||||
:can-view-artifacts="canViewArtifacts"
|
||||
:can-download-artifact="canDownloadArtifacts"
|
||||
:signature-injections="fieldSignatureInjections"
|
||||
@action="handleTaskAction"
|
||||
@download-artifact="downloadArtifact"
|
||||
@selection-change="handleSelectionChange"
|
||||
@field-change="handleFieldChange"
|
||||
@signature-request="handleSignatureRequest"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -821,6 +891,16 @@ onMounted(() => {
|
||||
@close="signatureDialogVisible = false"
|
||||
@confirm="confirmPadSignature"
|
||||
/>
|
||||
<!-- 文书里单个签名位(医师签名等)的采集入口:签完只回填该位置,不结束任务 -->
|
||||
<SignaturePadDialog
|
||||
:visible="fieldSignatureDialogVisible"
|
||||
:task="selectedTask"
|
||||
:title="`采集「${pendingSignatureField?.label ?? '签名'}」`"
|
||||
:hint="`请让签署人在下框内签名,签名图将回显到文书中的「${pendingSignatureField?.label ?? '签名'}」位置;本次采集不会结束签署任务:`"
|
||||
confirm-label="确认采集"
|
||||
@close="closeFieldSignatureDialog"
|
||||
@confirm="confirmFieldSignature"
|
||||
/>
|
||||
<OnlineSigningDialog
|
||||
:visible="onlineDialogVisible"
|
||||
:task="selectedTask"
|
||||
|
||||
Reference in New Issue
Block a user