fix: align sample registration, freight scope, and OA datetime display
Let customer-service quick register write storage, certificate, and insurance separately without extra menu or data-scope checks. List freight by document data scope like the web admin, allow protocol staff outside the collection region, and pad OA datetimes to seconds so PC date pickers can show historical minute values. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
03fcc01d6d
commit
ac8e471aca
41 files changed
+16929
-14162
No files matched your search
@@ -29,6 +29,18 @@ export function fetchSampleInfoQuickRegister(data: {
|
||||
handlerUserId?: string
|
||||
attachments?: string
|
||||
remark?: string
|
||||
storageHandleTime?: string
|
||||
storageHandlerUserId?: string
|
||||
storageAttachments?: string
|
||||
storageRemark?: string
|
||||
certHandleTime?: string
|
||||
certHandlerUserId?: string
|
||||
certAttachments?: string
|
||||
certRemark?: string
|
||||
insuranceHandleTime?: string
|
||||
insuranceHandlerUserId?: string
|
||||
insuranceAttachments?: string
|
||||
insuranceRemark?: string
|
||||
mailCertTime?: string
|
||||
policySendTime?: string
|
||||
smsSendDate?: string
|
||||
|
||||
@@ -0,0 +1,380 @@
|
||||
<template>
|
||||
<div
|
||||
class="attachment-upload"
|
||||
:class="{ 'is-dragover': isAttachDragover }"
|
||||
v-bind="attachDropHandlers"
|
||||
>
|
||||
<div class="attachment-list">
|
||||
<el-upload
|
||||
ref="attachmentUploadRef"
|
||||
:auto-upload="false"
|
||||
:show-file-list="false"
|
||||
:multiple="true"
|
||||
accept="image/*,video/*,.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.txt,.zip,.rar"
|
||||
@change="onFileChange"
|
||||
>
|
||||
<div class="upload-card">
|
||||
<el-icon :size="28" color="#047857"><Plus /></el-icon>
|
||||
<span class="upload-card-text">上传附件</span>
|
||||
</div>
|
||||
</el-upload>
|
||||
<div
|
||||
v-for="(file, index) in files"
|
||||
:key="file.id || index"
|
||||
class="attachment-item"
|
||||
:class="{ 'is-uploading': file._uploading }"
|
||||
>
|
||||
<div
|
||||
class="attachment-preview"
|
||||
@click="isImageFile(file) && file.fileUrl ? handlePreviewAttachment(file) : undefined"
|
||||
>
|
||||
<template v-if="isImageFile(file) && file.fileUrl">
|
||||
<img :src="fullFileUrl(file.fileUrl)" :alt="file.fileName" />
|
||||
</template>
|
||||
<template v-else-if="isImageFile(file) && !file.fileUrl">
|
||||
<el-icon :size="24" color="#94a3b8"><Loading /></el-icon>
|
||||
</template>
|
||||
<template v-else-if="isVideoFile(file)">
|
||||
<div class="attachment-preview__icon attachment-preview__icon--video">
|
||||
<el-icon :size="28"><VideoPlay /></el-icon>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="attachment-preview__icon attachment-preview__icon--doc">
|
||||
<el-icon :size="28"><Document /></el-icon>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="attachment-footer">
|
||||
<span class="attachment-name" :title="file.fileName">{{ file.fileName }}</span>
|
||||
<div class="attachment-actions">
|
||||
<el-icon class="action-btn" title="下载" @click="handleDownloadAttachment(file)">
|
||||
<Download />
|
||||
</el-icon>
|
||||
<el-icon
|
||||
class="action-btn action-btn--danger"
|
||||
title="删除"
|
||||
@click="files.splice(index, 1)"
|
||||
>
|
||||
<Delete />
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span class="upload-hint"
|
||||
>支持拖拽或点击上传图片、视频、PDF、Word、Excel等文件,单文件不超过 20MB</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<el-dialog
|
||||
v-model="previewVisible"
|
||||
title="文件预览"
|
||||
width="800px"
|
||||
align-center
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
>
|
||||
<div v-if="previewType === 'image'" class="preview-container">
|
||||
<img
|
||||
:src="previewUrl"
|
||||
alt="预览"
|
||||
style="max-width: 100%; max-height: 70vh; display: block; margin: 0 auto"
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="preview-container">
|
||||
<video
|
||||
:src="previewUrl"
|
||||
controls
|
||||
style="max-width: 100%; max-height: 70vh; display: block; margin: 0 auto"
|
||||
/>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Delete, Document, Download, Loading, Plus, VideoPlay } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { fetchUploadAttachment } from '@/api/business'
|
||||
import { attachmentFullUrl } from '@/composables/useAttachmentActions'
|
||||
import { useAttachmentDropZone } from '@/composables/useAttachmentDropZone'
|
||||
import { downloadAttachmentFile } from '@/utils/download-attachment'
|
||||
|
||||
export type QuickRegisterAttachmentItem = {
|
||||
id: string
|
||||
fileName: string
|
||||
fileUrl: string
|
||||
fileSize?: number
|
||||
fileType?: string
|
||||
_uploading?: boolean
|
||||
}
|
||||
|
||||
const props = defineProps<{ folder: string }>()
|
||||
const files = defineModel<QuickRegisterAttachmentItem[]>({ required: true })
|
||||
|
||||
const MAX_FILE_BYTES = 20 * 1024 * 1024
|
||||
const attachmentUploadRef = ref<{ clearFiles?: () => void } | null>(null)
|
||||
const previewVisible = ref(false)
|
||||
const previewUrl = ref('')
|
||||
const previewType = ref<'image' | 'video'>('image')
|
||||
|
||||
function isImageFile(file: QuickRegisterAttachmentItem) {
|
||||
const url = (file.fileUrl || '').toLowerCase()
|
||||
const name = (file.fileName || '').toLowerCase()
|
||||
return (
|
||||
/\.(jpg|jpeg|png|gif|bmp|webp|svg)(\?|$)/.test(url) ||
|
||||
/\.(jpg|jpeg|png|gif|bmp|webp|svg)$/.test(name) ||
|
||||
(file.fileType || '').startsWith('image/')
|
||||
)
|
||||
}
|
||||
|
||||
function isVideoFile(file: QuickRegisterAttachmentItem) {
|
||||
const url = (file.fileUrl || '').toLowerCase()
|
||||
const name = (file.fileName || '').toLowerCase()
|
||||
return (
|
||||
/\.(mp4|webm|ogg|mov|avi)(\?|$)/.test(url) ||
|
||||
/\.(mp4|webm|ogg|mov|avi)$/.test(name) ||
|
||||
(file.fileType || '').startsWith('video/')
|
||||
)
|
||||
}
|
||||
|
||||
function fullFileUrl(suffix: string) {
|
||||
return attachmentFullUrl(suffix)
|
||||
}
|
||||
|
||||
function handlePreviewAttachment(file: QuickRegisterAttachmentItem) {
|
||||
if (file._uploading || !file.fileUrl) return
|
||||
previewUrl.value = fullFileUrl(file.fileUrl)
|
||||
previewType.value = isVideoFile(file) ? 'video' : 'image'
|
||||
previewVisible.value = true
|
||||
}
|
||||
|
||||
function handleDownloadAttachment(file: QuickRegisterAttachmentItem) {
|
||||
downloadAttachmentFile(file, { fullFileUrl })
|
||||
}
|
||||
|
||||
async function onFileChange(uploadFile: any) {
|
||||
const file = uploadFile.raw || uploadFile
|
||||
if (!file) return
|
||||
if (file.size > MAX_FILE_BYTES) {
|
||||
ElMessage.error('单文件不超过 20MB')
|
||||
nextTick(() => attachmentUploadRef.value?.clearFiles?.())
|
||||
return
|
||||
}
|
||||
const tempId = `temp_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
|
||||
files.value.push({
|
||||
id: tempId,
|
||||
fileName: file.name || '附件',
|
||||
fileUrl: '',
|
||||
fileSize: file.size,
|
||||
fileType: file.type || '',
|
||||
_uploading: true
|
||||
})
|
||||
try {
|
||||
const fd = new FormData()
|
||||
fd.append('folder', props.folder)
|
||||
fd.append('file', file)
|
||||
const res = (await fetchUploadAttachment(fd)) as {
|
||||
id?: string
|
||||
fileName?: string
|
||||
fileUrl?: string
|
||||
fileType?: string
|
||||
}
|
||||
const idx = files.value.findIndex((item) => item.id === tempId)
|
||||
if (idx < 0) return
|
||||
files.value[idx] = {
|
||||
id: String(res.id || tempId),
|
||||
fileName: file.name || res.fileName || '附件',
|
||||
fileUrl: res.fileUrl || '',
|
||||
fileSize: file.size,
|
||||
fileType: res.fileType || file.type || '',
|
||||
_uploading: false
|
||||
}
|
||||
} catch {
|
||||
ElMessage.error('文件上传失败')
|
||||
const idx = files.value.findIndex((item) => item.id === tempId)
|
||||
if (idx >= 0) files.value.splice(idx, 1)
|
||||
} finally {
|
||||
nextTick(() => attachmentUploadRef.value?.clearFiles?.())
|
||||
}
|
||||
}
|
||||
|
||||
const { isDragover: isAttachDragover, dropZoneHandlers: attachDropHandlers } =
|
||||
useAttachmentDropZone({
|
||||
onFiles: (dropped) => {
|
||||
for (const file of dropped) {
|
||||
void onFileChange({ raw: file })
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.attachment-upload {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.upload-card {
|
||||
width: 140px;
|
||||
height: 96px;
|
||||
border: 1px dashed #a8d4c4;
|
||||
border-radius: 8px;
|
||||
background: #f0fdf4;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
border-color 0.15s,
|
||||
background 0.15s;
|
||||
|
||||
&:hover {
|
||||
border-color: #047857;
|
||||
border-style: solid;
|
||||
background: #e8f4ee;
|
||||
}
|
||||
}
|
||||
|
||||
.upload-card-text {
|
||||
font-size: 12px;
|
||||
color: #047857;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.upload-hint {
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
color: var(--mint-text-muted, #334155);
|
||||
line-height: 1;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.attachment-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.attachment-item {
|
||||
width: 140px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
overflow: hidden;
|
||||
transition:
|
||||
border-color 0.15s,
|
||||
box-shadow 0.15s;
|
||||
|
||||
&:hover {
|
||||
border-color: #047857;
|
||||
box-shadow: 0 2px 8px rgba(4, 120, 87, 0.1);
|
||||
}
|
||||
|
||||
&.is-uploading {
|
||||
opacity: 0.6;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.attachment-preview {
|
||||
width: 100%;
|
||||
height: 96px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #f8fafc;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.attachment-preview__icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&--video {
|
||||
background: #ede9fe;
|
||||
color: #7c3aed;
|
||||
}
|
||||
|
||||
&--doc {
|
||||
background: #e0f2fe;
|
||||
color: #0284c7;
|
||||
}
|
||||
}
|
||||
|
||||
.attachment-footer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
padding: 6px 8px;
|
||||
}
|
||||
|
||||
.attachment-name {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 11px;
|
||||
color: #334155;
|
||||
line-height: 1.4;
|
||||
word-break: break-all;
|
||||
min-height: calc(11px * 1.4 * 2);
|
||||
}
|
||||
|
||||
.attachment-actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
color: var(--mint-text-muted, #334155);
|
||||
cursor: pointer;
|
||||
background: #f1f5f9;
|
||||
transition:
|
||||
background 0.15s,
|
||||
color 0.15s;
|
||||
|
||||
&:hover {
|
||||
background: #047857;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
&--danger:hover {
|
||||
background: #ef4444;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.preview-container {
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
@@ -14,8 +14,7 @@
|
||||
<el-form label-position="top" class="biz-dialog-form" @submit.prevent>
|
||||
<p class="quick-register-hint">{{ selectionHint }}</p>
|
||||
|
||||
<template v-if="showHandleBlock">
|
||||
<div v-if="kind === 'all'" class="section-title">储存证 / 保险办理</div>
|
||||
<template v-if="showSingleHandle">
|
||||
<div class="form-grid-2">
|
||||
<el-form-item :label="'办理时间'" :required="handleRequired">
|
||||
<el-date-picker
|
||||
@@ -64,34 +63,168 @@
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="showAttachment" :label="attachmentLabel" class="form-col-span-2">
|
||||
<div class="attachment-upload">
|
||||
<div class="attachment-list">
|
||||
<el-upload
|
||||
:auto-upload="false"
|
||||
:show-file-list="false"
|
||||
:multiple="true"
|
||||
accept="image/*,video/*,.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.txt,.zip,.rar"
|
||||
@change="onFileChange"
|
||||
>
|
||||
<div class="upload-card">
|
||||
<el-icon :size="28" color="#047857"><Plus /></el-icon>
|
||||
<span class="upload-card-text">上传附件</span>
|
||||
</div>
|
||||
</el-upload>
|
||||
<div v-for="(file, index) in files" :key="file.id || index" class="attachment-item">
|
||||
<div class="attachment-footer">
|
||||
<span class="attachment-name" :title="file.fileName">{{ file.fileName }}</span>
|
||||
<el-icon class="action-btn--danger" @click="files.splice(index, 1)">
|
||||
<Delete />
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span class="upload-hint">支持图片、PDF、Office 等,单文件不超过 20MB</span>
|
||||
</div>
|
||||
<QuickRegisterAttachmentField v-model="files" :folder="singleAttachmentFolder" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="showAttachment" label="备注" class="form-col-span-2">
|
||||
<el-input v-model="form.remark" type="textarea" :rows="2" maxlength="500" show-word-limit />
|
||||
<el-input
|
||||
v-model="form.remark"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="kind === 'all'">
|
||||
<div class="section-title">登记入库</div>
|
||||
<div class="form-grid-2">
|
||||
<el-form-item label="办理时间">
|
||||
<el-date-picker
|
||||
v-model="form.storageHandleTime"
|
||||
type="datetime"
|
||||
placeholder="不填则不登记入库"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="办理人员">
|
||||
<el-select
|
||||
v-model="form.storageHandlerUserId"
|
||||
filterable
|
||||
remote
|
||||
clearable
|
||||
placeholder="输入姓名检索"
|
||||
:remote-method="searchUsers"
|
||||
:loading="userLoading"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="u in userOptions"
|
||||
:key="'storage-' + u.id"
|
||||
:label="u.nickName || u.userName"
|
||||
:value="u.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="入库附件" class="form-col-span-2">
|
||||
<QuickRegisterAttachmentField v-model="storageFiles" folder="sample-storage" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" class="form-col-span-2">
|
||||
<el-input
|
||||
v-model="form.storageRemark"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div class="section-title">储存证办理</div>
|
||||
<div class="form-grid-2">
|
||||
<el-form-item label="办理时间">
|
||||
<el-date-picker
|
||||
v-model="form.certHandleTime"
|
||||
type="datetime"
|
||||
placeholder="不填则不登记储存证"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="办理人员">
|
||||
<el-select
|
||||
v-model="form.certHandlerUserId"
|
||||
filterable
|
||||
remote
|
||||
clearable
|
||||
placeholder="输入姓名检索"
|
||||
:remote-method="searchUsers"
|
||||
:loading="userLoading"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="u in userOptions"
|
||||
:key="u.id"
|
||||
:label="u.nickName || u.userName"
|
||||
:value="u.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="邮寄储存证时间">
|
||||
<el-date-picker
|
||||
v-model="form.mailCertTime"
|
||||
type="datetime"
|
||||
placeholder="可选"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="储存证附件" class="form-col-span-2">
|
||||
<QuickRegisterAttachmentField v-model="certFiles" folder="storage-cert" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" class="form-col-span-2">
|
||||
<el-input
|
||||
v-model="form.certRemark"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div class="section-title">保险办理</div>
|
||||
<div class="form-grid-2">
|
||||
<el-form-item label="办理时间">
|
||||
<el-date-picker
|
||||
v-model="form.insHandleTime"
|
||||
type="datetime"
|
||||
placeholder="不填则不登记保险"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="办理人员">
|
||||
<el-select
|
||||
v-model="form.insHandlerUserId"
|
||||
filterable
|
||||
remote
|
||||
clearable
|
||||
placeholder="输入姓名检索"
|
||||
:remote-method="searchUsers"
|
||||
:loading="userLoading"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="u in userOptions"
|
||||
:key="'ins-' + u.id"
|
||||
:label="u.nickName || u.userName"
|
||||
:value="u.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="保单发送时间">
|
||||
<el-date-picker
|
||||
v-model="form.policySendTime"
|
||||
type="datetime"
|
||||
placeholder="可选"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="保险附件" class="form-col-span-2">
|
||||
<QuickRegisterAttachmentField v-model="insFiles" folder="fullmoon-todo" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" class="form-col-span-2">
|
||||
<el-input
|
||||
v-model="form.insRemark"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</template>
|
||||
@@ -211,16 +344,15 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Check, Close, Delete, Plus } from '@element-plus/icons-vue'
|
||||
import { Check, Close } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { fetchUserList } from '@/api/admin'
|
||||
import { fetchUploadAttachment } from '@/api/business'
|
||||
import { fetchCustomerServiceDict } from '@/api/customer-service-dict'
|
||||
import {
|
||||
fetchSampleInfoQuickRegister,
|
||||
type QuickRegisterKind
|
||||
} from '@/api/sample-info-handle'
|
||||
import { fetchSampleInfoQuickRegister, type QuickRegisterKind } from '@/api/sample-info-handle'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import QuickRegisterAttachmentField, {
|
||||
type QuickRegisterAttachmentItem
|
||||
} from './QuickRegisterAttachmentField.vue'
|
||||
|
||||
defineOptions({ name: 'SampleInfoQuickRegisterDialog' })
|
||||
|
||||
@@ -236,11 +368,16 @@
|
||||
(e: 'saved'): void
|
||||
}>()
|
||||
|
||||
type AttachmentItem = QuickRegisterAttachmentItem
|
||||
|
||||
const userStore = useUserStore()
|
||||
const saving = ref(false)
|
||||
const userLoading = ref(false)
|
||||
const userOptions = ref<Array<{ id: string; nickName?: string; userName?: string }>>([])
|
||||
const files = ref<Array<{ id: string; fileName: string; fileUrl: string }>>([])
|
||||
const files = ref<AttachmentItem[]>([])
|
||||
const storageFiles = ref<AttachmentItem[]>([])
|
||||
const certFiles = ref<AttachmentItem[]>([])
|
||||
const insFiles = ref<AttachmentItem[]>([])
|
||||
const smsOptions = ref<string[]>([])
|
||||
const microbeOptions = ref<string[]>([])
|
||||
const fullmoonOptions = ref<string[]>([])
|
||||
@@ -250,6 +387,15 @@
|
||||
handleTime: '',
|
||||
handlerUserId: '',
|
||||
remark: '',
|
||||
storageHandleTime: '',
|
||||
storageHandlerUserId: '',
|
||||
storageRemark: '',
|
||||
certHandleTime: '',
|
||||
certHandlerUserId: '',
|
||||
certRemark: '',
|
||||
insHandleTime: '',
|
||||
insHandlerUserId: '',
|
||||
insRemark: '',
|
||||
mailCertTime: '',
|
||||
policySendTime: '',
|
||||
smsSendDate: '',
|
||||
@@ -279,14 +425,20 @@
|
||||
const showHandleBlock = computed(() =>
|
||||
['storage', 'storage_cert', 'insurance', 'all'].includes(props.kind)
|
||||
)
|
||||
const showSingleHandle = computed(() => showHandleBlock.value && props.kind !== 'all')
|
||||
const handleRequired = computed(() =>
|
||||
['storage', 'storage_cert', 'insurance'].includes(props.kind)
|
||||
)
|
||||
const showMailCert = computed(() => ['storage_cert', 'all'].includes(props.kind))
|
||||
const showPolicySend = computed(() => ['insurance', 'all'].includes(props.kind))
|
||||
const showMailCert = computed(() => props.kind === 'storage_cert')
|
||||
const showPolicySend = computed(() => props.kind === 'insurance')
|
||||
const showAttachment = computed(() =>
|
||||
['storage', 'storage_cert', 'insurance', 'all'].includes(props.kind)
|
||||
['storage', 'storage_cert', 'insurance'].includes(props.kind)
|
||||
)
|
||||
const singleAttachmentFolder = computed(() => {
|
||||
if (props.kind === 'storage_cert') return 'storage-cert'
|
||||
if (props.kind === 'insurance') return 'fullmoon-todo'
|
||||
return 'sample-storage'
|
||||
})
|
||||
const showSmsBlock = computed(() => ['sms', 'all'].includes(props.kind))
|
||||
const showFollowupBlock = computed(() => ['followup', 'all'].includes(props.kind))
|
||||
const showFulfillmentBlock = computed(() => ['fulfillment', 'all'].includes(props.kind))
|
||||
@@ -294,7 +446,6 @@
|
||||
const attachmentLabel = computed(() => {
|
||||
if (props.kind === 'storage_cert') return '储存证附件'
|
||||
if (props.kind === 'insurance') return '保险附件'
|
||||
if (props.kind === 'all') return '办理附件(证/保险共用本次上传)'
|
||||
return '入库附件'
|
||||
})
|
||||
|
||||
@@ -349,15 +500,170 @@
|
||||
])
|
||||
}
|
||||
|
||||
function cellText(row: Record<string, unknown>, key: string) {
|
||||
return String(row[key] ?? '').trim()
|
||||
}
|
||||
|
||||
function fileNameFromUrl(url: string) {
|
||||
const base = url.split('?')[0].split('/').pop() || '附件'
|
||||
try {
|
||||
return decodeURIComponent(base) || '附件'
|
||||
} catch {
|
||||
return base || '附件'
|
||||
}
|
||||
}
|
||||
|
||||
function filesFromUrls(raw: string): AttachmentItem[] {
|
||||
const seen = new Set<string>()
|
||||
const items: AttachmentItem[] = []
|
||||
raw
|
||||
.split(',')
|
||||
.map((part) => part.trim())
|
||||
.filter(Boolean)
|
||||
.forEach((url) => {
|
||||
if (seen.has(url)) return
|
||||
seen.add(url)
|
||||
items.push({
|
||||
id: `echo-${items.length}-${url}`,
|
||||
fileName: fileNameFromUrl(url),
|
||||
fileUrl: url
|
||||
})
|
||||
})
|
||||
return items
|
||||
}
|
||||
|
||||
function ensureUserOption(id: string, name: string) {
|
||||
if (!id || userOptions.value.some((user) => user.id === id)) return
|
||||
userOptions.value.unshift({
|
||||
id,
|
||||
nickName: name || id,
|
||||
userName: name || id
|
||||
})
|
||||
}
|
||||
|
||||
function applyHandleEcho(
|
||||
target: 'single' | 'storage' | 'cert' | 'ins',
|
||||
source: {
|
||||
handleTime: string
|
||||
handlerUserId: string
|
||||
handlerName: string
|
||||
attachments: string
|
||||
remark: string
|
||||
}
|
||||
) {
|
||||
const echoedFiles = filesFromUrls(source.attachments)
|
||||
if (target === 'storage') {
|
||||
if (source.handleTime) form.storageHandleTime = source.handleTime
|
||||
if (source.handlerUserId) {
|
||||
form.storageHandlerUserId = source.handlerUserId
|
||||
ensureUserOption(source.handlerUserId, source.handlerName)
|
||||
}
|
||||
if (source.remark) form.storageRemark = source.remark
|
||||
if (echoedFiles.length) storageFiles.value = echoedFiles
|
||||
return
|
||||
}
|
||||
if (target === 'cert') {
|
||||
if (source.handleTime) form.certHandleTime = source.handleTime
|
||||
if (source.handlerUserId) {
|
||||
form.certHandlerUserId = source.handlerUserId
|
||||
ensureUserOption(source.handlerUserId, source.handlerName)
|
||||
}
|
||||
if (source.remark) form.certRemark = source.remark
|
||||
if (echoedFiles.length) certFiles.value = echoedFiles
|
||||
return
|
||||
}
|
||||
if (target === 'ins') {
|
||||
if (source.handleTime) form.insHandleTime = source.handleTime
|
||||
if (source.handlerUserId) {
|
||||
form.insHandlerUserId = source.handlerUserId
|
||||
ensureUserOption(source.handlerUserId, source.handlerName)
|
||||
}
|
||||
if (source.remark) form.insRemark = source.remark
|
||||
if (echoedFiles.length) insFiles.value = echoedFiles
|
||||
return
|
||||
}
|
||||
if (source.handleTime) form.handleTime = source.handleTime
|
||||
if (source.handlerUserId) {
|
||||
form.handlerUserId = source.handlerUserId
|
||||
ensureUserOption(source.handlerUserId, source.handlerName)
|
||||
}
|
||||
if (source.remark) form.remark = source.remark
|
||||
if (echoedFiles.length) files.value = echoedFiles
|
||||
}
|
||||
|
||||
function applyCustomerServiceEcho(row: Record<string, unknown>) {
|
||||
if (showSmsBlock.value) {
|
||||
form.smsSendDate = cellText(row, 'smsSendDate')
|
||||
form.smsSendStatus = cellText(row, 'smsSendStatus')
|
||||
}
|
||||
if (showFollowupBlock.value) {
|
||||
form.microbeFollowupDate = cellText(row, 'microbeFollowupDate')
|
||||
form.microbeFollowupStatus = cellText(row, 'microbeFollowupStatus')
|
||||
form.fullmoonFollowupDate = cellText(row, 'fullmoonFollowupDate')
|
||||
form.fullmoonFollowupStatus = cellText(row, 'fullmoonFollowupStatus')
|
||||
form.householdRegistered = cellText(row, 'householdRegistered')
|
||||
}
|
||||
if (showFulfillmentBlock.value) {
|
||||
form.einvoiceSendTime = cellText(row, 'einvoiceSendTime')
|
||||
form.effectiveAddress = cellText(row, 'effectiveAddress')
|
||||
}
|
||||
}
|
||||
|
||||
function applySavedEcho() {
|
||||
const row = props.rows[0]
|
||||
if (!row) return
|
||||
if (props.kind === 'storage' || props.kind === 'all') {
|
||||
applyHandleEcho(props.kind === 'all' ? 'storage' : 'single', {
|
||||
handleTime: cellText(row, 'storageTime'),
|
||||
handlerUserId: cellText(row, 'storageBy'),
|
||||
handlerName: cellText(row, 'storageByName'),
|
||||
attachments: cellText(row, 'storageAttachments'),
|
||||
remark: cellText(row, 'storageRemark')
|
||||
})
|
||||
if (props.kind === 'storage') return
|
||||
}
|
||||
if (props.kind === 'storage_cert' || props.kind === 'all') {
|
||||
applyHandleEcho(props.kind === 'all' ? 'cert' : 'single', {
|
||||
handleTime: cellText(row, 'storageCertHandleTime'),
|
||||
handlerUserId: cellText(row, 'storageCertHandlerUserId'),
|
||||
handlerName: cellText(row, 'storageCertHandlerName'),
|
||||
attachments: cellText(row, 'storageCertAttachments'),
|
||||
remark: cellText(row, 'storageCertRemark')
|
||||
})
|
||||
form.mailCertTime = cellText(row, 'mailCertTime')
|
||||
}
|
||||
if (props.kind === 'insurance' || props.kind === 'all') {
|
||||
applyHandleEcho(props.kind === 'all' ? 'ins' : 'single', {
|
||||
handleTime: cellText(row, 'insuranceHandleTime'),
|
||||
handlerUserId: cellText(row, 'insuranceHandlerUserId'),
|
||||
handlerName: cellText(row, 'insuranceHandlerName'),
|
||||
attachments: cellText(row, 'insuranceAttachments'),
|
||||
remark: cellText(row, 'insuranceRemark')
|
||||
})
|
||||
form.policySendTime = cellText(row, 'policySendTime')
|
||||
}
|
||||
applyCustomerServiceEcho(row)
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
const currentUser = userStore.getUserInfo as Record<string, any>
|
||||
const uid = String(currentUser?.id || currentUser?.userId || '')
|
||||
const name = String(
|
||||
currentUser?.realName || currentUser?.userName || currentUser?.username || ''
|
||||
)
|
||||
form.handleTime = showHandleBlock.value ? nowStr() : ''
|
||||
form.handlerUserId = showHandleBlock.value ? uid : ''
|
||||
const now = showHandleBlock.value ? nowStr() : ''
|
||||
form.handleTime = showSingleHandle.value ? now : ''
|
||||
form.handlerUserId = showSingleHandle.value ? uid : ''
|
||||
form.remark = ''
|
||||
form.storageHandleTime = props.kind === 'all' ? now : ''
|
||||
form.storageHandlerUserId = props.kind === 'all' ? uid : ''
|
||||
form.storageRemark = ''
|
||||
form.certHandleTime = props.kind === 'all' ? now : ''
|
||||
form.certHandlerUserId = props.kind === 'all' ? uid : ''
|
||||
form.certRemark = ''
|
||||
form.insHandleTime = props.kind === 'all' ? now : ''
|
||||
form.insHandlerUserId = props.kind === 'all' ? uid : ''
|
||||
form.insRemark = ''
|
||||
form.mailCertTime = ''
|
||||
form.policySendTime = ''
|
||||
form.smsSendDate = ''
|
||||
@@ -370,13 +676,19 @@
|
||||
form.einvoiceSendTime = ''
|
||||
form.effectiveAddress = ''
|
||||
files.value = []
|
||||
storageFiles.value = []
|
||||
certFiles.value = []
|
||||
insFiles.value = []
|
||||
if (uid) {
|
||||
userOptions.value = [{ id: uid, nickName: name, userName: name }]
|
||||
} else {
|
||||
userOptions.value = []
|
||||
}
|
||||
if (showSmsBlock.value || showFollowupBlock.value) {
|
||||
void loadDicts()
|
||||
if (props.modelValue) {
|
||||
applySavedEcho()
|
||||
if (showSmsBlock.value || showFollowupBlock.value) {
|
||||
void loadDicts()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -391,34 +703,45 @@
|
||||
userLoading.value = true
|
||||
try {
|
||||
const res = await fetchUserList({ page: 1, pageSize: 50, keyword: kw || '', status: '1' })
|
||||
userOptions.value = ((res as any)?.list || []).map((u: any) => ({
|
||||
const fetched = ((res as any)?.list || []).map((u: any) => ({
|
||||
id: String(u.id),
|
||||
nickName: u.nickName,
|
||||
userName: u.userName
|
||||
}))
|
||||
const selectedIds = [
|
||||
form.handlerUserId,
|
||||
form.storageHandlerUserId,
|
||||
form.certHandlerUserId,
|
||||
form.insHandlerUserId
|
||||
].filter(Boolean)
|
||||
const kept = userOptions.value.filter((user) => selectedIds.includes(user.id))
|
||||
const merged = new Map<string, { id: string; nickName?: string; userName?: string }>()
|
||||
for (const user of [...kept, ...fetched]) merged.set(user.id, user)
|
||||
userOptions.value = [...merged.values()]
|
||||
} finally {
|
||||
userLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function onFileChange(uploadFile: any) {
|
||||
const file = uploadFile.raw || uploadFile
|
||||
if (!file) return
|
||||
const folder =
|
||||
props.kind === 'storage_cert'
|
||||
? 'storage-cert'
|
||||
: props.kind === 'insurance'
|
||||
? 'fullmoon-todo'
|
||||
: 'sample-storage'
|
||||
const fd = new FormData()
|
||||
fd.append('folder', folder)
|
||||
fd.append('file', file)
|
||||
const res = (await fetchUploadAttachment(fd)) as { id?: string; fileName?: string; fileUrl?: string }
|
||||
files.value.push({
|
||||
id: String(res.id || Date.now()),
|
||||
fileName: file.name || res.fileName || '附件',
|
||||
fileUrl: res.fileUrl || ''
|
||||
})
|
||||
function attachmentUrls(list: AttachmentItem[]) {
|
||||
return list
|
||||
.filter((item) => item.fileUrl && !item._uploading)
|
||||
.map((item) => item.fileUrl)
|
||||
.join(',')
|
||||
}
|
||||
|
||||
function sectionTouched(
|
||||
handleTime: string,
|
||||
remark: string,
|
||||
extraTime: string,
|
||||
list: AttachmentItem[]
|
||||
) {
|
||||
return (
|
||||
nonempty(handleTime) ||
|
||||
nonempty(remark) ||
|
||||
nonempty(extraTime) ||
|
||||
list.some((item) => item.fileUrl)
|
||||
)
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
@@ -448,10 +771,52 @@
|
||||
ElMessage.warning('请至少填写一项')
|
||||
return
|
||||
}
|
||||
if (props.kind === 'all' && !nonempty(form.handleTime) && !csFilled) {
|
||||
const storageTouched = sectionTouched(
|
||||
form.storageHandleTime,
|
||||
form.storageRemark,
|
||||
'',
|
||||
storageFiles.value
|
||||
)
|
||||
const certTouched = sectionTouched(
|
||||
form.certHandleTime,
|
||||
form.certRemark,
|
||||
form.mailCertTime,
|
||||
certFiles.value
|
||||
)
|
||||
const insTouched = sectionTouched(
|
||||
form.insHandleTime,
|
||||
form.insRemark,
|
||||
form.policySendTime,
|
||||
insFiles.value
|
||||
)
|
||||
if (props.kind === 'all' && !storageTouched && !certTouched && !insTouched && !csFilled) {
|
||||
ElMessage.warning('请至少填写一项登记信息')
|
||||
return
|
||||
}
|
||||
if (props.kind === 'all' && storageTouched && !nonempty(form.storageHandleTime)) {
|
||||
ElMessage.warning('请填写入库办理时间')
|
||||
return
|
||||
}
|
||||
if (props.kind === 'all' && storageTouched && !nonempty(form.storageHandlerUserId)) {
|
||||
ElMessage.warning('请指定入库办理人员')
|
||||
return
|
||||
}
|
||||
if (props.kind === 'all' && certTouched && !nonempty(form.certHandleTime)) {
|
||||
ElMessage.warning('请填写储存证办理时间')
|
||||
return
|
||||
}
|
||||
if (props.kind === 'all' && certTouched && !nonempty(form.certHandlerUserId)) {
|
||||
ElMessage.warning('请指定储存证办理人员')
|
||||
return
|
||||
}
|
||||
if (props.kind === 'all' && insTouched && !nonempty(form.insHandleTime)) {
|
||||
ElMessage.warning('请填写保险办理时间')
|
||||
return
|
||||
}
|
||||
if (props.kind === 'all' && insTouched && !nonempty(form.insHandlerUserId)) {
|
||||
ElMessage.warning('请指定保险办理人员')
|
||||
return
|
||||
}
|
||||
|
||||
const items = props.rows
|
||||
.map((row) => ({
|
||||
@@ -464,38 +829,64 @@
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
[...files.value, ...storageFiles.value, ...certFiles.value, ...insFiles.value].some(
|
||||
(item) => item._uploading
|
||||
)
|
||||
) {
|
||||
ElMessage.warning('附件上传中,请稍候')
|
||||
return
|
||||
}
|
||||
|
||||
saving.value = true
|
||||
try {
|
||||
const urls = files.value.map((f) => f.fileUrl).filter(Boolean).join(',')
|
||||
const payload: Parameters<typeof fetchSampleInfoQuickRegister>[0] = {
|
||||
kind: props.kind,
|
||||
receptionSource: props.receptionSource,
|
||||
items
|
||||
}
|
||||
if (showHandleBlock.value && nonempty(form.handleTime)) {
|
||||
if (props.kind === 'all') {
|
||||
payload.storageHandleTime = form.storageHandleTime
|
||||
payload.storageHandlerUserId = form.storageHandlerUserId
|
||||
payload.storageAttachments = attachmentUrls(storageFiles.value)
|
||||
payload.storageRemark = form.storageRemark
|
||||
payload.certHandleTime = form.certHandleTime
|
||||
payload.certHandlerUserId = form.certHandlerUserId
|
||||
payload.certAttachments = attachmentUrls(certFiles.value)
|
||||
payload.certRemark = form.certRemark
|
||||
payload.insuranceHandleTime = form.insHandleTime
|
||||
payload.insuranceHandlerUserId = form.insHandlerUserId
|
||||
payload.insuranceAttachments = attachmentUrls(insFiles.value)
|
||||
payload.insuranceRemark = form.insRemark
|
||||
if (nonempty(form.mailCertTime)) payload.mailCertTime = form.mailCertTime
|
||||
if (nonempty(form.policySendTime)) payload.policySendTime = form.policySendTime
|
||||
} else if (showSingleHandle.value && nonempty(form.handleTime)) {
|
||||
payload.handleTime = form.handleTime
|
||||
payload.handlerUserId = form.handlerUserId
|
||||
payload.attachments = urls
|
||||
payload.attachments = attachmentUrls(files.value)
|
||||
payload.remark = form.remark
|
||||
}
|
||||
if (showMailCert.value && nonempty(form.mailCertTime)) {
|
||||
payload.mailCertTime = form.mailCertTime
|
||||
}
|
||||
if (showPolicySend.value && nonempty(form.policySendTime)) {
|
||||
payload.policySendTime = form.policySendTime
|
||||
if (showMailCert.value && nonempty(form.mailCertTime)) {
|
||||
payload.mailCertTime = form.mailCertTime
|
||||
}
|
||||
if (showPolicySend.value && nonempty(form.policySendTime)) {
|
||||
payload.policySendTime = form.policySendTime
|
||||
}
|
||||
}
|
||||
if (showSmsBlock.value) {
|
||||
if (nonempty(form.smsSendDate)) payload.smsSendDate = form.smsSendDate
|
||||
if (nonempty(form.smsSendStatus)) payload.smsSendStatus = form.smsSendStatus
|
||||
}
|
||||
if (showFollowupBlock.value) {
|
||||
if (nonempty(form.microbeFollowupDate)) payload.microbeFollowupDate = form.microbeFollowupDate
|
||||
if (nonempty(form.microbeFollowupDate))
|
||||
payload.microbeFollowupDate = form.microbeFollowupDate
|
||||
if (nonempty(form.microbeFollowupStatus))
|
||||
payload.microbeFollowupStatus = form.microbeFollowupStatus
|
||||
if (nonempty(form.fullmoonFollowupDate)) payload.fullmoonFollowupDate = form.fullmoonFollowupDate
|
||||
if (nonempty(form.fullmoonFollowupDate))
|
||||
payload.fullmoonFollowupDate = form.fullmoonFollowupDate
|
||||
if (nonempty(form.fullmoonFollowupStatus))
|
||||
payload.fullmoonFollowupStatus = form.fullmoonFollowupStatus
|
||||
if (nonempty(form.householdRegistered)) payload.householdRegistered = form.householdRegistered
|
||||
if (nonempty(form.householdRegistered))
|
||||
payload.householdRegistered = form.householdRegistered
|
||||
}
|
||||
if (showFulfillmentBlock.value) {
|
||||
if (nonempty(form.einvoiceSendTime)) payload.einvoiceSendTime = form.einvoiceSendTime
|
||||
@@ -548,71 +939,4 @@
|
||||
background: var(--mint-primary, #047857);
|
||||
}
|
||||
}
|
||||
|
||||
.action-btn--danger {
|
||||
cursor: pointer;
|
||||
color: var(--el-color-danger);
|
||||
}
|
||||
|
||||
.attachment-upload {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.upload-card {
|
||||
width: 140px;
|
||||
height: 96px;
|
||||
border: 1px dashed #a7f3d0;
|
||||
border-radius: 8px;
|
||||
background: #f0fdf4;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.upload-card-text {
|
||||
font-size: 12px;
|
||||
color: #047857;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.upload-hint {
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.attachment-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.attachment-item {
|
||||
width: 140px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.attachment-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 6px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.attachment-name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 12px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user