Files
kra-new/web/src/view/sw/components/AlertHandleDrawer.vue
T

158 lines
10 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-drawer class="sw-dialog" v-model="visible" append-to-body destroy-on-close title="预警处理 / 详情" size="min(560px, 100vw)" :close-on-click-modal="!busy" :close-on-press-escape="!busy" :show-close="!busy" @closed="reset">
<div v-loading="loading" tabindex="0" @paste="pasteImages">
<template v-if="current">
<h3 class="mb-3">渠道:{{ current.targetPromoteName.replace(/^渠道[::]\s*/, '') || '—' }} — {{ current.ruleName || '预警' }}</h3>
<el-descriptions :column="1" class="mb-4">
<el-descriptions-item label="归属商务">{{ current.businessName || '—' }}</el-descriptions-item>
<el-descriptions-item label="触发时间">{{ shortTime(current.createdAt) }}</el-descriptions-item>
<el-descriptions-item label="当前状态"><el-tag :type="statusTone(current.status)">{{ current.statusLabel || statuses[current.status] }}</el-tag></el-descriptions-item>
<el-descriptions-item label="预警内容">{{ current.alertContent || '—' }}</el-descriptions-item>
</el-descriptions>
<section v-if="canNote" class="mb-5">
<p class="mb-2">处理备注与图片附件</p>
<el-input v-model="note" type="textarea" :rows="4" maxlength="500" show-word-limit placeholder="填写处理备注..." :disabled="busy" />
<div class="alert-upload-zone" @dragover.prevent @drop.prevent="dropImages">
<el-button :disabled="busy" @click="fileInput.click()">上传图片(可多选)</el-button>
<input ref="fileInput" class="hidden" type="file" accept="image/*" multiple @change="pickImages" />
<p class="upload-hint">支持多选、拖拽或在备注区粘贴截图;图片先暂存,点击「添加备注」上传并提交。</p>
<div class="alert-images">
<div v-for="(item, index) in pending" :key="item.url" class="pending-image">
<el-image :src="item.url" :alt="item.file.name" :preview-src-list="pending.map(image => image.url)" :initial-index="index" preview-teleported fit="cover" />
<el-button size="small" :disabled="busy" @click="removeImage(index)">移除</el-button>
</div>
</div>
<el-button type="primary" link :loading="savingNote" :disabled="busy && !savingNote" @click="addNote">添加备注</el-button>
</div>
</section>
<p class="mb-4">操作记录(全流程追溯 · 不可删除)</p>
<el-empty v-if="!logs.length" description="暂无操作记录" :image-size="70" />
<el-timeline v-else>
<el-timeline-item v-for="log in logs" :key="log.id" :timestamp="shortTime(log.createdAt)" placement="top" type="primary">
<strong>{{ log.operatorName || '—' }}</strong>
<p>{{ actions[log.actionDetail] || log.logTypeLabel || log.actionDetail }}</p>
<p v-if="log.noteContent" class="alert-note">备注:{{ log.noteContent }}</p>
<div v-if="log.attachments.length" class="alert-images">
<el-image v-for="(url, index) in log.attachments" :key="index" :src="fileURL(url)" alt="附件" :preview-src-list="log.attachments.map(fileURL)" :initial-index="index" preview-teleported fit="cover" />
</div>
</el-timeline-item>
</el-timeline>
</template>
<el-empty v-else-if="!loading" description="暂时无法读取预警详情" />
</div>
<template #footer>
<el-button :disabled="busy" @click="visible = false">关闭</el-button>
<template v-if="current && !loading">
<el-button v-if="canProcess && current.status === 1" type="primary" :disabled="busy" @click="process('startProcess')">开始处理</el-button>
<el-button v-if="canProcess && current.status === 2" type="primary" :disabled="busy" @click="process('submitAudit')">提交审核</el-button>
<template v-if="auditAllowed && current.status === 3">
<el-button type="danger" plain :disabled="busy" @click="process('reject')">审核驳回</el-button>
<el-button type="primary" :disabled="busy" @click="process('approve')">审核通过</el-button>
</template>
<el-button v-if="canProcess && current.status === 4" type="primary" :disabled="busy" @click="process('reProcess')">重新处理</el-button>
</template>
</template>
</el-drawer>
</template>
<script setup>
import { computed, onBeforeUnmount, ref, watch } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { getAlertRecord, processAlertRecord, addAlertNote } from '@/api/alert'
import { uploadFile } from '@/api/fileUploadAndDownload'
import { getUrl } from '@/utils/image'
import { alertStatuses as statuses, alertStatusTone as statusTone, alertShortTime as shortTime, alertActions as actions } from '../alert-display'
const props = defineProps({ modelValue: Boolean, alertId: { type: [String, Number], default: '' }, canProcess: Boolean, canAudit: Boolean })
const emit = defineEmits(['update:modelValue', 'success'])
const visible = computed({ get: () => props.modelValue, set: (value) => emit('update:modelValue', value) })
const current = ref(null)
const logs = ref([])
const detailCanAudit = ref(false)
const auditAllowed = computed(() => props.canAudit && detailCanAudit.value)
const canNote = computed(() => props.canProcess && current.value && ([1, 2, 4].includes(current.value.status) || (current.value.status === 3 && auditAllowed.value)))
const loading = ref(false)
const savingNote = ref(false)
const processing = ref(false)
const busy = computed(() => savingNote.value || processing.value)
const note = ref('')
const pending = ref([])
const fileInput = ref()
let requestVersion = 0
// 图片只能作为图片地址展示,不将历史附件内容作为 HTML 或脚本执行。
const fileURL = (url) => /^https?:\/\//i.test(url) || !/^[a-z][a-z\d+.-]*:/i.test(url) ? getUrl(url) : ''
const clearImages = () => { pending.value.forEach(item => URL.revokeObjectURL(item.url)); pending.value = [] }
const reset = () => { ++requestVersion; clearImages(); note.value = ''; current.value = null; logs.value = []; detailCanAudit.value = false; loading.value = false }
const load = async () => {
const version = ++requestVersion
loading.value = true
try {
const res = await getAlertRecord(props.alertId)
if (version !== requestVersion) return
if (res.code === 0) { current.value = res.data.alertRecord; logs.value = res.data.operationLogs; detailCanAudit.value = res.data.canAudit === true }
else { current.value = null; logs.value = [] }
} catch { if (version === requestVersion) { current.value = null; logs.value = [] } } finally { if (version === requestVersion) loading.value = false }
}
const addImages = (files) => {
if (busy.value || !canNote.value) return
const images = Array.from(files).filter(file => file.type.startsWith('image/'))
if (!images.length) { ElMessage.warning('请选择图片文件'); return }
pending.value.push(...images.map(file => ({ file, url: URL.createObjectURL(file), uploaded: '' })))
}
const pickImages = (event) => { addImages(event.target.files); event.target.value = '' }
const dropImages = (event) => addImages(event.dataTransfer.files)
const pasteImages = (event) => {
const files = Array.from(event.clipboardData?.items || []).filter(item => item.type.startsWith('image/')).map(item => item.getAsFile()).filter(Boolean)
if (files.length && canNote.value) { event.preventDefault(); addImages(files) }
}
const removeImage = (index) => { const [image] = pending.value.splice(index, 1); URL.revokeObjectURL(image.url) }
const addNote = async () => {
if (busy.value || !canNote.value) return
if (!note.value.trim()) { ElMessage.warning('请填写处理备注后再提交附件'); return }
savingNote.value = true
try {
for (const item of pending.value) {
if (item.uploaded) continue
const data = new FormData()
data.append('file', item.file)
const res = await uploadFile(data)
const file = res.data?.file
if (res.code !== 0 || !file?.url) throw new Error('图片上传失败')
// 保留已经上传的结果,备注失败重试时不重复上传文件。
item.uploaded = file.ID ? String(file.ID) : file.url
}
const res = await addAlertNote({ alertId: props.alertId, noteContent: note.value.trim(), attachmentIds: pending.value.map(item => item.uploaded) })
if (res.code !== 0) return
clearImages(); note.value = ''; ElMessage.success('备注已添加'); await load()
} catch { ElMessage.error('备注提交失败,请重试') } finally { savingNote.value = false }
}
const confirmations = {
startProcess: '确认发起处理?预警将转为「处理中」状态。',
submitAudit: '确认提交审核?预警将转为「待审核」状态,等待一级商务审核。',
reject: '确认审核驳回?预警需补充处理后再提交。',
reProcess: '确认重新处理?预警将重新转为「处理中」状态。'
}
const process = async (action) => {
if (busy.value) return
if (pending.value.length) { ElMessage.warning('请先点击「添加备注」上传图片,或移除暂存图片后再操作'); return }
processing.value = true
try {
if (confirmations[action] && !await ElMessageBox.confirm(confirmations[action], '操作确认', { confirmButtonText: '确定', cancelButtonText: '取消' }).then(() => true).catch(() => false)) return
const res = await processAlertRecord(action, { alertId: props.alertId, note: note.value.trim() || undefined })
if (res.code !== 0) { await load(); return }
note.value = ''; ElMessage.success('操作成功'); emit('success'); await load()
} finally { processing.value = false }
}
watch(() => [props.modelValue, props.alertId], ([open]) => { if (open) { reset(); void load() } })
onBeforeUnmount(reset)
</script>
<style scoped>
.alert-upload-zone { margin-top: 12px; padding: 12px; border: 1px dashed var(--el-border-color); border-radius: 6px; }
.upload-hint { margin: 10px 0; font-size: 12px; line-height: 1.6; color: var(--el-text-color-secondary); }
.alert-images { display: flex; flex-wrap: wrap; gap: 8px; margin: 10px 0; }
.alert-images .el-image { width: 72px; height: 72px; border-radius: 6px; }
.pending-image { display: flex; flex-direction: column; gap: 4px; }
.alert-note { white-space: pre-wrap; overflow-wrap: anywhere; padding: 10px; background: var(--el-fill-color-light); border-radius: 6px; }
</style>