Files
zkxw/web-admin/src/views/cell-storage/protocol/ProtocolStatusChangeDialog.vue
T
2026-07-01 16:37:33 +08:00

302 lines
8.6 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!-- 采集协议单 — 状态变更弹窗(协议生效 / 协议过期 / 协议作废 / 协议转科研) -->
<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>