增加档案功能
This commit is contained in:
@@ -72,44 +72,13 @@
|
||||
<el-descriptions-item label="备注" :span="3">{{ file.remark || '-' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<div class="section-title">证照资料</div>
|
||||
<div class="cert-grid">
|
||||
<div v-for="cert in certPhotos" :key="cert.label" class="cert-item">
|
||||
<div
|
||||
v-if="cert.url"
|
||||
class="cert-body"
|
||||
:title="`点击预览${cert.label}`"
|
||||
@click="previewCert(cert)"
|
||||
>
|
||||
<el-image
|
||||
v-if="isImage(cert.url)"
|
||||
:src="cert.url"
|
||||
:preview-src-list="[cert.url]"
|
||||
hide-on-click-modal
|
||||
preview-teleported
|
||||
fit="cover"
|
||||
class="cert-image"
|
||||
>
|
||||
<template #placeholder>
|
||||
<div class="cert-loading"><el-icon :size="22"><Loading /></el-icon></div>
|
||||
</template>
|
||||
</el-image>
|
||||
<div v-else class="cert-pdf">
|
||||
<el-icon :size="34"><Document /></el-icon>
|
||||
<span>PDF 附件</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="cert-body cert-empty">未上传</div>
|
||||
<div class="cert-label">{{ cert.label }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- 教育经历 / 工作经历 / 合同信息:同构表格,配置驱动 -->
|
||||
<el-tab-pane
|
||||
v-for="conf in recordTabs"
|
||||
:key="conf.type"
|
||||
:label="`${conf.label}(${recordsOf(conf.type).length})`"
|
||||
:label="`${conf.label}(${recordCount(conf.type)})`"
|
||||
:name="conf.name"
|
||||
>
|
||||
<div class="tab-toolbar">
|
||||
@@ -122,7 +91,7 @@
|
||||
新增{{ conf.label }}
|
||||
</el-button>
|
||||
</div>
|
||||
<el-table :data="recordsOf(conf.type)" stripe>
|
||||
<el-table :data="recordsForTab(conf.type)" stripe>
|
||||
<el-table-column :label="conf.nameLabel" prop="title" min-width="150" show-overflow-tooltip />
|
||||
<el-table-column :label="conf.subLabel" prop="sub_title" min-width="120" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ row.sub_title || '-' }}</template>
|
||||
@@ -138,20 +107,47 @@
|
||||
<el-tag :type="contractStatus(row).type" size="small">{{ contractStatus(row).text }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="conf.type === 1" label="学历照片" min-width="120" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
v-if="row.attachment_url"
|
||||
link
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="previewAttachment(row)"
|
||||
>
|
||||
查看附件
|
||||
</el-button>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="描述" prop="description" min-width="150" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ row.description || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="120" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="openRecordDialog({ type: conf.type, record: row })"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button link type="danger" size="small" @click="handleDeleteRecord(row)">删除</el-button>
|
||||
<template v-if="row.is_primary_file">
|
||||
<el-button
|
||||
link
|
||||
type="danger"
|
||||
size="small"
|
||||
:loading="certSaving === row.cert_field"
|
||||
@click="removePrimaryCertificate(row)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="openRecordDialog({ type: conf.type, record: row })"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button link type="danger" size="small" @click="handleDeleteRecord(row)">删除</el-button>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template #empty>
|
||||
@@ -161,8 +157,23 @@
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- 证照附件 -->
|
||||
<el-tab-pane :label="`证照附件(${recordsOf(4).length})`" name="attachment">
|
||||
<el-tab-pane :label="`证照附件(${certificateAttachments.length})`" name="attachment">
|
||||
<div class="tab-toolbar">
|
||||
<el-upload
|
||||
:show-file-list="false"
|
||||
:http-request="(opt) => handleCertUpload('education_photo', opt)"
|
||||
:before-upload="beforeCertUpload"
|
||||
accept="image/*,.pdf"
|
||||
>
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
size="small"
|
||||
:loading="certSaving === 'education_photo'"
|
||||
>
|
||||
{{ file.education_photo ? '替换学历照片' : '上传学历照片' }}
|
||||
</el-button>
|
||||
</el-upload>
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
@@ -172,8 +183,8 @@
|
||||
上传证照
|
||||
</el-button>
|
||||
</div>
|
||||
<div v-if="recordsOf(4).length" class="attachment-grid">
|
||||
<div v-for="item in recordsOf(4)" :key="item.id" class="attachment-card">
|
||||
<div v-if="certificateAttachments.length" class="attachment-grid">
|
||||
<div v-for="item in certificateAttachments" :key="item.id" class="attachment-card">
|
||||
<div class="attachment-body" @click="previewAttachment(item)">
|
||||
<img
|
||||
v-if="isImage(item.attachment_url)"
|
||||
@@ -190,15 +201,28 @@
|
||||
<div class="attachment-sub" :title="item.sub_title">{{ item.sub_title || ' ' }}</div>
|
||||
<div class="attachment-actions">
|
||||
<el-button link type="primary" size="small" @click="previewAttachment(item)">预览</el-button>
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="openRecordDialog({ type: 4, record: item })"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button link type="danger" size="small" @click="handleDeleteRecord(item)">删除</el-button>
|
||||
<template v-if="item.is_primary_file">
|
||||
<el-button
|
||||
link
|
||||
type="danger"
|
||||
size="small"
|
||||
:loading="certSaving === item.cert_field"
|
||||
@click="removePrimaryCertificate(item)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="openRecordDialog({ type: 4, record: item })"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button link type="danger" size="small" @click="handleDeleteRecord(item)">删除</el-button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -212,10 +236,15 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import { computed, reactive, ref } from 'vue';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { Document, Edit, Loading, Plus, Refresh } from '@element-plus/icons-vue';
|
||||
import { deleteFileRecord, getEmployeeFileDetail } from '@/api/employeeFile';
|
||||
import { Document, Edit, Plus, Refresh } from '@element-plus/icons-vue';
|
||||
import {
|
||||
deleteFileRecord,
|
||||
getEmployeeFileDetail,
|
||||
updateEmployeeFileCertificates,
|
||||
} from '@/api/employeeFile';
|
||||
import { uploadFile } from '@/api/file';
|
||||
import {
|
||||
employeeStatusTagType,
|
||||
employeeStatusText,
|
||||
@@ -228,11 +257,17 @@ import RecordEditDialog from './recordEditDialog.vue';
|
||||
* 档案编辑(基本资料)通过 edit 事件交给父页面打开编辑弹窗。
|
||||
*/
|
||||
|
||||
const emit = defineEmits(['edit']);
|
||||
const emit = defineEmits(['edit', 'refresh']);
|
||||
|
||||
const visible = ref(false);
|
||||
const loading = ref(false);
|
||||
const recordDialogRef = ref();
|
||||
const certSaving = ref('');
|
||||
const certificateForm = reactive({
|
||||
education_photo: '',
|
||||
id_card_front: '',
|
||||
id_card_back: '',
|
||||
});
|
||||
|
||||
const file = ref({});
|
||||
const records = ref([]);
|
||||
@@ -255,31 +290,133 @@ const STATUS_META = {
|
||||
const maritalText = (value) => MARITAL_TEXT[Number(value ?? 0)] || '未知';
|
||||
const statusMeta = (value) => STATUS_META[Number(value ?? 2)] || STATUS_META[2];
|
||||
|
||||
/** 证照资料:学历照片与身份证正反面(主表字段) */
|
||||
const certPhotos = computed(() => [
|
||||
{ label: '学历照片', url: file.value.education_photo || '' },
|
||||
{ label: '身份证正面', url: file.value.id_card_front || '' },
|
||||
{ label: '身份证反面', url: file.value.id_card_back || '' },
|
||||
]);
|
||||
const beforeCertUpload = (uploadedFile) => {
|
||||
const isImageOrPdf =
|
||||
uploadedFile.type.startsWith('image/') || uploadedFile.type === 'application/pdf';
|
||||
const isLt10M = uploadedFile.size / 1024 / 1024 < 10;
|
||||
if (!isImageOrPdf) ElMessage.error('仅支持图片或 PDF 格式');
|
||||
if (!isLt10M) ElMessage.error('文件大小不能超过 10MB');
|
||||
return isImageOrPdf && isLt10M;
|
||||
};
|
||||
|
||||
const previewCert = (cert) => {
|
||||
// PDF 无法用 el-image 预览,新窗口打开;图片走 el-image 内置放大
|
||||
if (cert.url && !isImage(cert.url)) {
|
||||
window.open(cert.url, '_blank');
|
||||
const saveCertificates = async () => {
|
||||
await updateEmployeeFileCertificates(file.value.id, certificateForm);
|
||||
await loadDetail();
|
||||
emit('refresh');
|
||||
};
|
||||
|
||||
const handleCertUpload = async (field, { file: uploadedFile }) => {
|
||||
certSaving.value = field;
|
||||
try {
|
||||
const data = new FormData();
|
||||
data.append('file', uploadedFile);
|
||||
const res = await uploadFile(data);
|
||||
const url = res?.url || res?.data?.url;
|
||||
if (!url) {
|
||||
ElMessage.error(res?.msg || '上传失败');
|
||||
return;
|
||||
}
|
||||
certificateForm[field] = url;
|
||||
await saveCertificates();
|
||||
ElMessage.success('证照已保存');
|
||||
} catch (error) {
|
||||
ElMessage.error(error?.message || '证照保存失败');
|
||||
} finally {
|
||||
certSaving.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
const removeCert = async (field) => {
|
||||
certSaving.value = field;
|
||||
try {
|
||||
certificateForm[field] = '';
|
||||
await saveCertificates();
|
||||
ElMessage.success('证照已移除');
|
||||
} catch (error) {
|
||||
ElMessage.error(error?.message || '证照移除失败');
|
||||
} finally {
|
||||
certSaving.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
const removePrimaryCertificate = async (item) => {
|
||||
if (!item.cert_field) return;
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定删除「${item.title}」吗?删除后将同步清空员工基础资料中的对应证照数据。`,
|
||||
'删除确认',
|
||||
{ type: 'warning' },
|
||||
);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
await removeCert(item.cert_field);
|
||||
};
|
||||
|
||||
const recordsOf = (type) => records.value.filter((item) => Number(item.type) === Number(type));
|
||||
|
||||
/**
|
||||
* 主档案证照字段同步展示在“证照附件”中。
|
||||
* 只读标志用于隐藏子记录的编辑/删除按钮,实际更新请通过证照附件页完成。
|
||||
*/
|
||||
const certificateAttachments = computed(() => {
|
||||
const manualRecords = recordsOf(4);
|
||||
const primaryFiles = [
|
||||
{
|
||||
id: 'primary-education-photo',
|
||||
title: '学历照片',
|
||||
url: file.value.education_photo,
|
||||
cert_field: 'education_photo',
|
||||
},
|
||||
{
|
||||
id: 'primary-id-card-front',
|
||||
title: '身份证正面',
|
||||
url: file.value.id_card_front,
|
||||
cert_field: 'id_card_front',
|
||||
},
|
||||
{
|
||||
id: 'primary-id-card-back',
|
||||
title: '身份证反面',
|
||||
url: file.value.id_card_back,
|
||||
cert_field: 'id_card_back',
|
||||
},
|
||||
]
|
||||
.filter((item) => item.url)
|
||||
.filter((item) => !manualRecords.some((record) => record.attachment_url === item.url))
|
||||
.map((item) => ({
|
||||
id: item.id,
|
||||
type: 4,
|
||||
title: item.title,
|
||||
sub_title: '建档时上传',
|
||||
attachment_url: item.url,
|
||||
is_primary_file: true,
|
||||
cert_field: item.cert_field,
|
||||
}));
|
||||
return [...primaryFiles, ...manualRecords];
|
||||
});
|
||||
|
||||
const recordsForTab = (type) => recordsOf(type);
|
||||
const recordCount = (type) => recordsForTab(type).length;
|
||||
|
||||
/** 后端可能返回 RFC3339 时间戳,经历列表仅展示日期部分。 */
|
||||
const formatDateOnly = (value) => {
|
||||
if (!value) return '';
|
||||
const matched = String(value).match(/^\d{4}-\d{2}-\d{2}/);
|
||||
return matched ? matched[0] : String(value);
|
||||
};
|
||||
|
||||
const dateRangeText = (row) => {
|
||||
if (!row.start_date && !row.end_date) return '-';
|
||||
return `${row.start_date || '?'} ~ ${row.end_date || '至今'}`;
|
||||
const startDate = formatDateOnly(row.start_date);
|
||||
const endDate = formatDateOnly(row.end_date);
|
||||
if (!startDate && !endDate) return '-';
|
||||
return `${startDate || '?'} ~ ${endDate || '至今'}`;
|
||||
};
|
||||
|
||||
/** 合同状态:以结束日期判断履行中 / 已到期 */
|
||||
const contractStatus = (row) => {
|
||||
if (!row.end_date) return { text: '长期', type: 'success' };
|
||||
return row.end_date >= new Date().toISOString().slice(0, 10)
|
||||
const endDate = formatDateOnly(row.end_date);
|
||||
if (!endDate) return { text: '长期', type: 'success' };
|
||||
return endDate >= new Date().toISOString().slice(0, 10)
|
||||
? { text: '履行中', type: 'success' }
|
||||
: { text: '已到期', type: 'warning' };
|
||||
};
|
||||
@@ -294,6 +431,9 @@ const loadDetail = async () => {
|
||||
const res = await getEmployeeFileDetail(id);
|
||||
const data = res?.data || res || {};
|
||||
file.value = data.file || {};
|
||||
certificateForm.education_photo = file.value.education_photo || '';
|
||||
certificateForm.id_card_front = file.value.id_card_front || '';
|
||||
certificateForm.id_card_back = file.value.id_card_back || '';
|
||||
records.value = data.records || [];
|
||||
} catch (error) {
|
||||
ElMessage.error(error?.message || '加载档案详情失败');
|
||||
@@ -406,68 +546,9 @@ defineExpose({ open, reload: loadDetail });
|
||||
.tab-toolbar {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.cert-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.cert-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
.cert-body {
|
||||
width: 100%;
|
||||
height: 150px;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
border: 1px solid #ebeef5;
|
||||
background: #f5f7fa;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.cert-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.cert-pdf {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.cert-empty {
|
||||
color: #c0c4cc;
|
||||
font-size: 12px;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.cert-loading {
|
||||
color: #c0c4cc;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.cert-label {
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
color: #606266;
|
||||
}
|
||||
gap: 8px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.attachment-grid {
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="档案编号" prop="file_no">
|
||||
<el-input v-model="form.file_no" placeholder="留空自动生成" maxlength="50" />
|
||||
<el-input v-model="form.file_no" placeholder="留空自动生成" disabled maxlength="50" />
|
||||
</el-form-item>
|
||||
|
||||
<el-divider content-position="left">个人信息</el-divider>
|
||||
@@ -117,37 +117,6 @@
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-divider content-position="left">证照资料</el-divider>
|
||||
<el-form-item
|
||||
v-for="cert in certFields"
|
||||
:key="cert.field"
|
||||
:label="cert.label"
|
||||
>
|
||||
<div class="upload-area">
|
||||
<el-upload
|
||||
:show-file-list="false"
|
||||
:http-request="(opt) => handleCertUpload(cert.field, opt)"
|
||||
:before-upload="beforeCertUpload"
|
||||
accept="image/*,.pdf"
|
||||
>
|
||||
<img v-if="form[cert.field]" :src="form[cert.field]" class="cert-preview" alt="" />
|
||||
<div v-else class="upload-trigger">
|
||||
<el-icon><Plus /></el-icon>
|
||||
<span>上传{{ cert.label }}</span>
|
||||
</div>
|
||||
</el-upload>
|
||||
<el-button
|
||||
v-if="form[cert.field]"
|
||||
link
|
||||
type="danger"
|
||||
size="small"
|
||||
@click="form[cert.field] = ''"
|
||||
>
|
||||
移除
|
||||
</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-divider content-position="left">紧急联系人</el-divider>
|
||||
<el-row :gutter="12">
|
||||
<el-col :span="8">
|
||||
@@ -184,7 +153,6 @@ import { computed, reactive, ref } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { createEmployeeFile, getFiledEmployeeIds, updateEmployeeFile } from '@/api/employeeFile';
|
||||
import { oaOrganizationApi } from '@/api/organization';
|
||||
import { uploadFile } from '@/api/file';
|
||||
|
||||
/**
|
||||
* 建档 / 编辑档案弹窗。
|
||||
@@ -208,20 +176,10 @@ const filedIds = ref([]);
|
||||
|
||||
const politicalOptions = ['群众', '中共党员', '中共预备党员', '共青团员', '民主党派', '无党派人士'];
|
||||
|
||||
/** 证照资料上传项:field 对应 form 字段与后端列名 */
|
||||
const certFields = [
|
||||
{ field: 'education_photo', label: '学历照片' },
|
||||
{ field: 'id_card_front', label: '身份证正面' },
|
||||
{ field: 'id_card_back', label: '身份证反面' },
|
||||
];
|
||||
|
||||
const form = reactive({
|
||||
employee_id: null,
|
||||
file_no: '',
|
||||
id_card: '',
|
||||
id_card_front: '',
|
||||
id_card_back: '',
|
||||
education_photo: '',
|
||||
political_status: '',
|
||||
marital_status: 0,
|
||||
native_place: '',
|
||||
@@ -259,9 +217,6 @@ const resetForm = () => {
|
||||
form.employee_id = null;
|
||||
form.file_no = '';
|
||||
form.id_card = '';
|
||||
form.id_card_front = '';
|
||||
form.id_card_back = '';
|
||||
form.education_photo = '';
|
||||
form.political_status = '';
|
||||
form.marital_status = 0;
|
||||
form.native_place = '';
|
||||
@@ -310,11 +265,10 @@ const open = async (file = null) => {
|
||||
editEmployeeLabel.value = file.employee_name
|
||||
? `${file.employee_name}(${file.employee_account || '-'})`
|
||||
: `员工 #${file.employee_id}`;
|
||||
// 编辑时员工归属不可修改,但必须回填校验字段,避免保存时触发“请选择员工”。
|
||||
form.employee_id = file.employee_id ? Number(file.employee_id) : null;
|
||||
form.file_no = file.file_no || '';
|
||||
form.id_card = file.id_card || '';
|
||||
form.id_card_front = file.id_card_front || '';
|
||||
form.id_card_back = file.id_card_back || '';
|
||||
form.education_photo = file.education_photo || '';
|
||||
form.political_status = file.political_status || '';
|
||||
form.marital_status = Number(file.marital_status ?? 0);
|
||||
form.native_place = file.native_place || '';
|
||||
@@ -336,31 +290,6 @@ const open = async (file = null) => {
|
||||
visible.value = true;
|
||||
};
|
||||
|
||||
const beforeCertUpload = (file) => {
|
||||
const isImageOrPdf = file.type.startsWith('image/') || file.type === 'application/pdf';
|
||||
const isLt10M = file.size / 1024 / 1024 < 10;
|
||||
if (!isImageOrPdf) ElMessage.error('仅支持图片或 PDF 格式');
|
||||
if (!isLt10M) ElMessage.error('文件大小不能超过 10MB');
|
||||
return isImageOrPdf && isLt10M;
|
||||
};
|
||||
|
||||
/** 通用证照上传:成功后写回对应表单字段 */
|
||||
const handleCertUpload = async (field, { file }) => {
|
||||
try {
|
||||
const data = new FormData();
|
||||
data.append('file', file);
|
||||
const res = await uploadFile(data);
|
||||
const url = res?.url || res?.data?.url;
|
||||
if (!url) {
|
||||
ElMessage.error(res?.msg || '上传失败');
|
||||
return;
|
||||
}
|
||||
form[field] = url;
|
||||
} catch (error) {
|
||||
ElMessage.error(error?.message || '上传失败');
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
const valid = await formRef.value?.validate().then(() => true).catch(() => false);
|
||||
if (!valid) return;
|
||||
@@ -387,42 +316,3 @@ const handleSave = async () => {
|
||||
|
||||
defineExpose({ open });
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.upload-area {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.cert-preview {
|
||||
width: 140px;
|
||||
height: 140px;
|
||||
object-fit: cover;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #dcdfe6;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.upload-trigger {
|
||||
width: 140px;
|
||||
height: 140px;
|
||||
border: 1px dashed #c0ccda;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
color: #8c939d;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
transition: border-color 0.2s;
|
||||
|
||||
&:hover {
|
||||
border-color: #3973ff;
|
||||
color: #3973ff;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -14,7 +14,14 @@
|
||||
<el-input v-model="form.sub_title" :placeholder="`请输入${meta.subLabel}`" maxlength="100" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="meta.extraLabel" :label="meta.extraLabel" prop="extra">
|
||||
<el-input
|
||||
v-if="meta.type === 2"
|
||||
v-model="form.extra"
|
||||
placeholder="请输入所在部门"
|
||||
maxlength="100"
|
||||
/>
|
||||
<el-select
|
||||
v-else
|
||||
v-model="form.extra"
|
||||
filterable
|
||||
allow-create
|
||||
@@ -47,30 +54,34 @@
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="meta.type === 4" label="附件" prop="attachment_url">
|
||||
<el-form-item v-if="meta.type === 1 || meta.type === 3 || meta.type === 4" :label="attachmentLabel" prop="attachment_url">
|
||||
<div class="upload-area">
|
||||
<el-upload
|
||||
:show-file-list="false"
|
||||
:http-request="handleUpload"
|
||||
:before-upload="beforeUpload"
|
||||
accept="image/*,.pdf"
|
||||
:accept="uploadAccept"
|
||||
>
|
||||
<img v-if="form.attachment_url" :src="form.attachment_url" class="attachment-preview" alt="证照" />
|
||||
<img
|
||||
v-if="form.attachment_url && isImageFile(form.attachment_url)"
|
||||
:src="form.attachment_url"
|
||||
class="attachment-preview"
|
||||
alt="附件预览"
|
||||
/>
|
||||
<div v-else-if="form.attachment_url" class="uploaded-file">
|
||||
<el-icon><Document /></el-icon>
|
||||
<span>已上传{{ attachmentLabel }}</span>
|
||||
</div>
|
||||
<div v-else class="upload-trigger">
|
||||
<el-icon><Plus /></el-icon>
|
||||
<span>上传证照</span>
|
||||
<span>上传{{ attachmentLabel }}</span>
|
||||
</div>
|
||||
</el-upload>
|
||||
<el-button
|
||||
v-if="form.attachment_url"
|
||||
link
|
||||
type="danger"
|
||||
size="small"
|
||||
@click="form.attachment_url = ''"
|
||||
>
|
||||
移除附件
|
||||
</el-button>
|
||||
<div class="upload-tip">支持图片 / PDF,大小不超过 10MB</div>
|
||||
<div v-if="form.attachment_url" class="attachment-actions">
|
||||
<el-button link type="primary" size="small" @click="openAttachment">查看 / 下载</el-button>
|
||||
<el-button link type="danger" size="small" @click="form.attachment_url = ''">移除附件</el-button>
|
||||
</div>
|
||||
<div class="upload-tip">{{ uploadTip }}</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
@@ -85,7 +96,7 @@
|
||||
<script setup>
|
||||
import { computed, reactive, ref } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { Plus } from '@element-plus/icons-vue';
|
||||
import { Document, Plus } from '@element-plus/icons-vue';
|
||||
import { createFileRecord, updateFileRecord } from '@/api/employeeFile';
|
||||
import { uploadFile } from '@/api/file';
|
||||
|
||||
@@ -138,6 +149,21 @@ const rules = {
|
||||
};
|
||||
|
||||
const extraOptions = computed(() => EXTRA_OPTIONS[meta.value.type] || []);
|
||||
const attachmentLabel = computed(() => {
|
||||
if (meta.value.type === 1) return '学历照片';
|
||||
if (meta.value.type === 3) return '电子合同';
|
||||
return '附件';
|
||||
});
|
||||
const uploadAccept = computed(() => (
|
||||
meta.value.type === 3
|
||||
? 'image/*,.pdf,.doc,.docx,.xls,.xlsx'
|
||||
: 'image/*,.pdf'
|
||||
));
|
||||
const uploadTip = computed(() => (
|
||||
meta.value.type === 3
|
||||
? '支持图片、PDF、Word、Excel,大小不超过 20MB'
|
||||
: '支持图片 / PDF,大小不超过 10MB'
|
||||
));
|
||||
|
||||
const resetForm = () => {
|
||||
form.title = '';
|
||||
@@ -149,6 +175,12 @@ const resetForm = () => {
|
||||
formRef.value?.clearValidate();
|
||||
};
|
||||
|
||||
/** 兼容后端 time.Time 返回的 RFC3339 时间戳,提交时统一使用 DATE 格式。 */
|
||||
const normalizeDate = (value) => {
|
||||
const matched = String(value || '').match(/^\d{4}-\d{2}-\d{2}/);
|
||||
return matched ? matched[0] : '';
|
||||
};
|
||||
|
||||
/**
|
||||
* 打开弹窗。
|
||||
* @param {object} options
|
||||
@@ -168,9 +200,9 @@ const open = (options = {}) => {
|
||||
form.title = record.title || '';
|
||||
form.sub_title = record.sub_title || '';
|
||||
form.extra = record.extra || '';
|
||||
form.dateRange = record.start_date || record.end_date
|
||||
? [record.start_date || '', record.end_date || '']
|
||||
: null;
|
||||
const startDate = normalizeDate(record.start_date);
|
||||
const endDate = normalizeDate(record.end_date);
|
||||
form.dateRange = startDate || endDate ? [startDate, endDate] : null;
|
||||
form.description = record.description || '';
|
||||
form.attachment_url = record.attachment_url || '';
|
||||
}
|
||||
@@ -178,12 +210,25 @@ const open = (options = {}) => {
|
||||
visible.value = true;
|
||||
};
|
||||
|
||||
const isImageFile = (url) => /\.(png|jpe?g|gif|webp|bmp|svg)(?:\?.*)?$/i.test(url || '');
|
||||
|
||||
const beforeUpload = (file) => {
|
||||
const isImageOrPdf = file.type.startsWith('image/') || file.type === 'application/pdf';
|
||||
const isLt10M = file.size / 1024 / 1024 < 10;
|
||||
if (!isImageOrPdf) ElMessage.error('仅支持图片或 PDF 格式');
|
||||
if (!isLt10M) ElMessage.error('文件大小不能超过 10MB');
|
||||
return isImageOrPdf && isLt10M;
|
||||
const isContract = meta.value.type === 3;
|
||||
const allowedTypes = isContract
|
||||
? ['application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet']
|
||||
: ['application/pdf'];
|
||||
const allowed = file.type.startsWith('image/') || allowedTypes.includes(file.type);
|
||||
const limitMB = isContract ? 20 : 10;
|
||||
const withinLimit = file.size / 1024 / 1024 < limitMB;
|
||||
if (!allowed) ElMessage.error(isContract ? '仅支持图片、PDF、Word 或 Excel 格式' : '仅支持图片或 PDF 格式');
|
||||
if (!withinLimit) ElMessage.error(`文件大小不能超过 ${limitMB}MB`);
|
||||
return allowed && withinLimit;
|
||||
};
|
||||
|
||||
const openAttachment = () => {
|
||||
if (form.attachment_url) {
|
||||
window.open(form.attachment_url, '_blank', 'noopener');
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpload = async ({ file }) => {
|
||||
@@ -220,8 +265,8 @@ const handleSave = async () => {
|
||||
title: form.title,
|
||||
sub_title: form.sub_title,
|
||||
extra: form.extra,
|
||||
start_date: range[0] || '',
|
||||
end_date: range[1] || '',
|
||||
start_date: normalizeDate(range[0]),
|
||||
end_date: normalizeDate(range[1]),
|
||||
description: form.description,
|
||||
attachment_url: form.attachment_url,
|
||||
sort: 0,
|
||||
@@ -254,13 +299,34 @@ defineExpose({ open, uploading });
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.attachment-preview {
|
||||
.attachment-preview,
|
||||
.uploaded-file {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
object-fit: cover;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #dcdfe6;
|
||||
display: block;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.uploaded-file {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
color: #3973ff;
|
||||
border: 1px solid #d9e4ff;
|
||||
background: #f5f8ff;
|
||||
font-size: 12px;
|
||||
|
||||
.el-icon {
|
||||
font-size: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.attachment-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.upload-trigger {
|
||||
|
||||
Reference in New Issue
Block a user