302 lines
8.6 KiB
Vue
302 lines
8.6 KiB
Vue
<!-- 采集协议单 — 状态变更弹窗(协议生效 / 协议过期 / 协议作废 / 协议转科研) -->
|
||
<template>
|
||
<el-dialog
|
||
:model-value="modelValue"
|
||
:title="dialogTitle"
|
||
width="560px"
|
||
class="dialog-form-layout"
|
||
align-center
|
||
append-to-body
|
||
modal-class="protocol-dialog-mask"
|
||
destroy-on-close
|
||
:close-on-click-modal="false"
|
||
@update:model-value="emit('update:modelValue', $event)"
|
||
@closed="resetForm"
|
||
>
|
||
<p v-if="targets.length > 1" class="status-change-hint">
|
||
将对已选
|
||
<strong>{{ targets.length }}</strong> 条协议单执行<strong>{{ actionLabel }}</strong
|
||
>操作,每条各记一次变更。
|
||
</p>
|
||
<p v-else-if="targets.length === 1" class="status-change-hint">
|
||
<strong>{{ formatProtocolDocSubject('cell_storage', targets[0]) }}</strong>
|
||
</p>
|
||
|
||
<el-form
|
||
ref="formRef"
|
||
:model="form"
|
||
:rules="rules"
|
||
label-position="top"
|
||
class="biz-dialog-form"
|
||
@submit.prevent
|
||
>
|
||
<el-form-item :label="timeLabel" prop="changeTime">
|
||
<el-date-picker
|
||
v-model="form.changeTime"
|
||
type="datetime"
|
||
placeholder="请选择时间"
|
||
style="width: 100%"
|
||
value-format="YYYY-MM-DD HH:mm:ss"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item v-if="actionType === 'void'" label="作废类型" prop="voidType">
|
||
<el-select v-model="form.voidType" placeholder="请选择作废类型" style="width: 100%">
|
||
<el-option
|
||
v-for="opt in voidTypeOptionsRef"
|
||
:key="opt.value"
|
||
:label="opt.label"
|
||
:value="opt.value"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item :label="descLabel">
|
||
<el-input
|
||
v-model="form.changeDesc"
|
||
type="textarea"
|
||
:rows="3"
|
||
placeholder="可选"
|
||
maxlength="500"
|
||
show-word-limit
|
||
/>
|
||
</el-form-item>
|
||
</el-form>
|
||
|
||
<template #footer>
|
||
<el-button class="btn-ghost" @click="emit('update:modelValue', false)">
|
||
<el-icon><Close /></el-icon>
|
||
取消
|
||
</el-button>
|
||
<el-button class="btn-main" type="primary" :loading="submitting" @click="handleSubmit">
|
||
<el-icon><Check /></el-icon>
|
||
确定
|
||
</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import type { FormInstance, FormRules } from 'element-plus'
|
||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||
import { Check, Close } from '@element-plus/icons-vue'
|
||
import { fetchUpdateCollectionProtocol } from '@/api/collection-protocol'
|
||
import { fetchConvertFromStorage } from '@/api/research-donation'
|
||
import { withFullscreenLoading, fullscreenLoadingText } from '@/utils/fullscreen-loading'
|
||
import { formatProtocolDocSubject } from '@/utils/protocol-doc-label'
|
||
import { loadVoidTypeDict, voidTypeOptionsRef } from '@/utils/void-type-dict'
|
||
|
||
export type StatusChangeAction = 'activate' | 'expire' | 'void' | 'toResearch'
|
||
export type StatusChangeTarget = {
|
||
id: string
|
||
motherName?: string
|
||
}
|
||
|
||
defineOptions({ name: 'ProtocolStatusChangeDialog' })
|
||
|
||
const props = defineProps<{
|
||
modelValue: boolean
|
||
actionType: StatusChangeAction
|
||
targets: StatusChangeTarget[]
|
||
}>()
|
||
|
||
const emit = defineEmits<{
|
||
(e: 'update:modelValue', val: boolean): void
|
||
(e: 'submitted'): void
|
||
}>()
|
||
|
||
const ACTION_CONFIG: Record<
|
||
StatusChangeAction,
|
||
{
|
||
label: string
|
||
timeLabel: string
|
||
descLabel: string
|
||
status: string
|
||
timeKey: string
|
||
descKey: string
|
||
}
|
||
> = {
|
||
activate: {
|
||
label: '协议生效',
|
||
timeLabel: '协议生效时间',
|
||
descLabel: '协议生效描述',
|
||
status: 'sxz',
|
||
timeKey: 'effectiveTime',
|
||
descKey: 'effectiveDesc'
|
||
},
|
||
expire: {
|
||
label: '协议过期',
|
||
timeLabel: '协议过期时间',
|
||
descLabel: '协议过期描述',
|
||
status: 'ygq',
|
||
timeKey: 'expireTime',
|
||
descKey: 'expireDesc'
|
||
},
|
||
void: {
|
||
label: '协议作废',
|
||
timeLabel: '协议作废时间',
|
||
descLabel: '协议作废描述',
|
||
status: 'yzf',
|
||
timeKey: 'voidTime',
|
||
descKey: 'voidDesc'
|
||
},
|
||
toResearch: {
|
||
label: '协议转科研',
|
||
timeLabel: '协议转科研时间',
|
||
descLabel: '协议转科研描述',
|
||
status: 'zky',
|
||
timeKey: 'toResearchTime',
|
||
descKey: 'toResearchDesc'
|
||
}
|
||
}
|
||
|
||
const config = computed(() => ACTION_CONFIG[props.actionType])
|
||
const actionLabel = computed(() => config.value.label)
|
||
const timeLabel = computed(() => config.value.timeLabel)
|
||
const descLabel = computed(() => config.value.descLabel)
|
||
|
||
const dialogTitle = computed(() => {
|
||
const label = config.value.label
|
||
return props.targets.length > 1 ? `批量${label}(${props.targets.length} 条)` : label
|
||
})
|
||
|
||
const formRef = ref<FormInstance>()
|
||
const submitting = ref(false)
|
||
const form = reactive({
|
||
changeTime: '',
|
||
changeDesc: '',
|
||
voidType: ''
|
||
})
|
||
|
||
const rules = computed<FormRules>(() => {
|
||
const base: FormRules = {
|
||
changeTime: [{ required: true, message: '请选择时间', trigger: 'change' }]
|
||
}
|
||
if (props.actionType === 'void') {
|
||
base.voidType = [{ required: true, message: '请选择作废类型', trigger: 'change' }]
|
||
}
|
||
return base
|
||
})
|
||
|
||
watch(
|
||
() => props.modelValue,
|
||
(visible) => {
|
||
if (visible && props.actionType === 'void') void loadVoidTypeDict()
|
||
}
|
||
)
|
||
|
||
function resetForm() {
|
||
form.changeTime = ''
|
||
form.changeDesc = ''
|
||
form.voidType = ''
|
||
formRef.value?.clearValidate()
|
||
}
|
||
|
||
async function handleSubmit() {
|
||
const valid = await formRef.value?.validate().catch(() => false)
|
||
if (!valid || !props.targets.length) return
|
||
|
||
const cfg = config.value
|
||
const rows = [...props.targets]
|
||
const isBatch = rows.length > 1
|
||
|
||
// 转科研:确认 + 调用专用接口
|
||
if (props.actionType === 'toResearch') {
|
||
try {
|
||
await ElMessageBox.confirm(
|
||
'确定将该协议转为科研捐赠吗?将创建新的科研捐赠记录,样本编码与源单一致。',
|
||
'转科研确认',
|
||
{ confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }
|
||
)
|
||
} catch {
|
||
return
|
||
}
|
||
submitting.value = true
|
||
try {
|
||
await withFullscreenLoading(
|
||
async () => {
|
||
let ok = 0
|
||
const failed: string[] = []
|
||
for (const row of rows) {
|
||
try {
|
||
await fetchConvertFromStorage(String(row.id))
|
||
ok += 1
|
||
} catch {
|
||
failed.push(row.motherName || String(row.id))
|
||
}
|
||
}
|
||
if (isBatch) {
|
||
if (failed.length) {
|
||
ElMessage.warning(`${cfg.label}成功 ${ok} 条,失败 ${failed.length} 条`)
|
||
} else {
|
||
ElMessage.success(`批量${cfg.label}成功,共 ${ok} 条`)
|
||
}
|
||
}
|
||
if (ok > 0) {
|
||
emit('update:modelValue', false)
|
||
emit('submitted')
|
||
}
|
||
},
|
||
isBatch ? fullscreenLoadingText.batchAudit : fullscreenLoadingText.save
|
||
)
|
||
} finally {
|
||
submitting.value = false
|
||
}
|
||
return
|
||
}
|
||
|
||
const payload: Record<string, unknown> = {
|
||
protocolStatus: cfg.status,
|
||
[cfg.timeKey]: form.changeTime,
|
||
[cfg.descKey]: form.changeDesc.trim() || undefined
|
||
}
|
||
if (props.actionType === 'void') {
|
||
payload.voidType = form.voidType
|
||
}
|
||
|
||
submitting.value = true
|
||
try {
|
||
await withFullscreenLoading(
|
||
async () => {
|
||
let ok = 0
|
||
const failed: string[] = []
|
||
for (const row of rows) {
|
||
try {
|
||
await fetchUpdateCollectionProtocol(String(row.id), payload)
|
||
ok += 1
|
||
} catch {
|
||
failed.push(row.motherName || String(row.id))
|
||
}
|
||
}
|
||
if (isBatch) {
|
||
if (failed.length) {
|
||
ElMessage.warning(`${cfg.label}成功 ${ok} 条,失败 ${failed.length} 条`)
|
||
} else {
|
||
ElMessage.success(`批量${cfg.label}成功,共 ${ok} 条`)
|
||
}
|
||
}
|
||
if (ok > 0) {
|
||
emit('update:modelValue', false)
|
||
emit('submitted')
|
||
}
|
||
},
|
||
isBatch ? fullscreenLoadingText.batchAudit : fullscreenLoadingText.save
|
||
)
|
||
} finally {
|
||
submitting.value = false
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.status-change-hint {
|
||
margin: 0 0 16px;
|
||
font-size: 13px;
|
||
line-height: 1.5;
|
||
color: var(--mint-text-muted, #64748b);
|
||
|
||
strong {
|
||
color: #1e293b;
|
||
font-weight: 600;
|
||
}
|
||
}
|
||
</style>
|