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:
zlwuandCursor committed 2026-10-20 17:30:30 +08:00
1 parent 03fcc01d6d
commit ac8e471aca
41 files changed
+16929 -14162

No files matched your search

+12
View File
@@ -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>