增加单位功能

This commit is contained in:
hero920103 committed 2026-09-14 00:09:58 +08:00
1 parent a8c09c1db5
commit 6f106e43af
22 files changed
+1473 -128

No files matched your search

@@ -143,15 +143,15 @@ const handleClose = () => {
display: flex;
gap: 16px;
padding: 16px;
background: #f5f7fa;
background: var(--el-fill-color-light);
border-radius: 8px;
margin-bottom: 20px;
}
.detail-avatar {
flex-shrink: 0;
background: #409eff;
color: #fff;
background: var(--el-color-primary);
color: var(--el-color-white);
font-size: 22px;
}
@@ -171,12 +171,12 @@ const handleClose = () => {
.detail-name {
font-size: 18px;
font-weight: 600;
color: #303133;
color: var(--el-text-color-primary);
}
.detail-sub {
font-size: 13px;
color: #606266;
color: var(--el-text-color-regular);
}
.detail-section {
@@ -187,11 +187,11 @@ const handleClose = () => {
margin: 0 0 12px;
font-size: 14px;
font-weight: 600;
color: #303133;
color: var(--el-text-color-primary);
}
.contact-link {
color: #409eff;
color: var(--el-color-primary);
text-decoration: none;
}
@@ -200,7 +200,7 @@ const handleClose = () => {
}
.empty-text {
color: #c0c4cc;
color: var(--el-text-color-placeholder);
}
.dialog-footer {
@@ -16,7 +16,13 @@
<div class="graph-layout">
<!-- 左侧:图谱 -->
<div class="graph-card">
<div
ref="graphCardRef"
class="graph-card"
@contextmenu="onGraphContextMenu"
@mousedown="onGraphMouseDown"
@wheel="onGraphWheel"
>
<div class="filter-bar">
<el-input
v-model="filters.keyword"
@@ -66,12 +72,7 @@
</div>
<div v-loading="loading" class="chart-wrap">
<div
ref="chartRef"
class="chart"
@contextmenu.prevent
@mousedown="onChartMouseDown"
/>
<div ref="chartRef" class="chart" />
<el-empty
v-if="!loading && !displayNodes.length"
description="暂无可展示的分类或文档,请先到文档库新建分类并上传文档"
@@ -196,6 +197,7 @@ import { getDocCategories, getDocGraph } from "@/api/oaDocument";
import { DOC_RELATION_OPTIONS, docTypeColor, docTypeLabel } from "../utils";
const chartRef = ref(null);
const graphCardRef = ref(null);
let chart = null;
let resizeObserver = null;
let themeObserver = null;
@@ -216,7 +218,8 @@ const detailId = ref(0);
// 分层展开:默认展示顶级往下 3 级,点击分支节点后在该分支继续延展 3 级
const INITIAL_DEPTH = 3;
const expandedIds = ref(new Set());
// 按住右键拖拽平移画布(ECharts 原生 roam 只响应左键,右键需自行接管)
// 鼠标位于「图谱卡片」内时:滚轮整体缩放 + 按住右键拖拽平移
// (ECharts 原生 roam 只作用于画布内,且右键不响应,这里在卡片层统一接管)
const panning = ref(false);
const filters = reactive({
@@ -461,8 +464,9 @@ function buildOption() {
{
type: "graph",
layout: filters.layout,
// roam: 滚轮缩放 + 左键在空白处拖拽平移;节点左键可拖拽,右键拖拽整体平移
roam: true,
// roam='move':仅保留左键空白拖拽平移,滚轮缩放由卡片层 onGraphWheel 统一接管
// (避免原生缩放与自定义缩放在画布内叠加,导致一次滚动缩放两遍)
roam: "move",
scaleLimit: { min: 0.15, max: 6 },
draggable: true,
data,
@@ -546,23 +550,64 @@ function openNode(node) {
}
}
// ---------------- 按住右键拖拽平移 ----------------
// ECharts 的 roam 会忽略右键(isMiddleOrRightButtonOnMouseUpDown),
// 这里自行接管右键拖拽,通过 graphRoam 动作平移,不改动节点数据与布局。
// ---------------- 卡片内:滚轮缩放 + 按住右键拖拽平移 ----------------
// ECharts 的 roam 只作用于画布内且忽略右键,这里在卡片层统一接管,
// 通过 graphRoam 动作位移/缩放,不改动节点数据与布局。
let panLastX = 0;
let panLastY = 0;
function onChartMouseDown(e) {
/** 顶部筛选区(输入框 / 下拉等)保持原生行为,不接管滚轮与右键 */
function inFilterBar(target) {
return !!target?.closest?.(".filter-bar");
}
function onGraphContextMenu(e) {
if (inFilterBar(e.target)) return; // 输入框里仍可右键粘贴
e.preventDefault();
}
function onGraphMouseDown(e) {
if (e.button !== 2) return; // 仅响应右键
if (inFilterBar(e.target)) return;
e.preventDefault();
panning.value = true;
panLastX = e.clientX;
panLastY = e.clientY;
chartRef.value?.classList.add("is-panning");
graphCardRef.value?.classList.add("is-panning");
window.addEventListener("mousemove", onPanMouseMove);
window.addEventListener("mouseup", onPanMouseUp);
}
/**
* 滚轮缩放:指针在画布内时以指针位置为中心;在卡片其它区域(图例 / 空白)时
* 以画布中心为中心整体缩放。缩放比按 deltaY 指数换算,兼容鼠标滚轮与触控板。
*/
function onGraphWheel(e) {
if (!chart || panning.value) return;
if (e.ctrlKey) return; // Ctrl + 滚轮留给浏览器页面缩放
if (inFilterBar(e.target)) return;
const rect = chartRef.value?.getBoundingClientRect();
if (!rect?.width || !rect?.height) return;
e.preventDefault();
// deltaMode:0=像素 1=行 2=页,统一折算成像素量级
const raw = e.deltaMode === 1 ? e.deltaY * 16 : e.deltaMode === 2 ? e.deltaY * 100 : e.deltaY;
const zoom = Math.pow(0.999, raw); // 向下滚(raw>0)缩小,向上滚放大
const inside =
e.clientX >= rect.left &&
e.clientX <= rect.right &&
e.clientY >= rect.top &&
e.clientY <= rect.bottom;
chart.dispatchAction({
type: "graphRoam",
seriesIndex: 0,
zoom,
originX: inside ? e.clientX - rect.left : rect.width / 2,
originY: inside ? e.clientY - rect.top : rect.height / 2
});
}
function onPanMouseMove(e) {
if (!panning.value) return;
// 右键在画布外松开时 mouseup 不会触发,用 buttons 兜底结束拖拽
@@ -587,7 +632,7 @@ function onPanMouseUp(e) {
function stopPan() {
if (!panning.value) return;
panning.value = false;
chartRef.value?.classList.remove("is-panning");
graphCardRef.value?.classList.remove("is-panning");
window.removeEventListener("mousemove", onPanMouseMove);
window.removeEventListener("mouseup", onPanMouseUp);
}
@@ -765,12 +810,12 @@ onBeforeUnmount(() => {
.chart {
width: 100%;
height: 100%;
}
// 右键拖拽平移时强制抓手光标(zrender 会改写 canvas 内联 cursor)
&.is-panning,
&.is-panning * {
cursor: grabbing !important;
}
// 右键拖拽平移时整卡强制抓手光标(zrender 会改写 canvas 内联 cursor)
.graph-card.is-panning,
.graph-card.is-panning * {
cursor: grabbing !important;
}
.chart-empty {
@@ -159,29 +159,18 @@
<!-- 证照附件 -->
<el-tab-pane :label="`证照附件(${certificateAttachments.length})`" name="attachment">
<div class="tab-toolbar">
<span class="upload-tip">支持图片 / PDF,单个不超过 10MB,可多选批量上传,数量不限</span>
<el-upload
:show-file-list="false"
:http-request="(opt) => handleCertUpload('education_photo', opt)"
:http-request="handleCertUpload"
:before-upload="beforeCertUpload"
multiple
accept="image/*,.pdf"
>
<el-button
type="primary"
plain
size="small"
:loading="certSaving === 'education_photo'"
>
{{ file.education_photo ? '替换学历照片' : '上传学历照片' }}
<el-button type="primary" size="small" :icon="Upload" :loading="certUploading">
批量上传证照
</el-button>
</el-upload>
<el-button
type="primary"
size="small"
:icon="Plus"
@click="openRecordDialog({ type: 4 })"
>
上传证照
</el-button>
</div>
<div v-if="certificateAttachments.length" class="attachment-grid">
<div v-for="item in certificateAttachments" :key="item.id" class="attachment-card">
@@ -238,8 +227,9 @@
<script setup>
import { computed, reactive, ref } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { Document, Edit, Plus, Refresh } from '@element-plus/icons-vue';
import { Document, Edit, Plus, Refresh, Upload } from '@element-plus/icons-vue';
import {
createFileRecord,
deleteFileRecord,
getEmployeeFileDetail,
updateEmployeeFileCertificates,
@@ -263,6 +253,10 @@ const visible = ref(false);
const loading = ref(false);
const recordDialogRef = ref();
const certSaving = ref('');
/** 批量上传状态:pending 为并发中的文件数,全部结束后统一刷新 */
const certUploading = ref(false);
const certPending = ref(0);
const certDone = ref(0);
const certificateForm = reactive({
education_photo: '',
id_card_front: '',
@@ -299,30 +293,60 @@ const beforeCertUpload = (uploadedFile) => {
return isImageOrPdf && isLt10M;
};
/** 批量上传时用「文件名(去扩展名)」作为默认证照名称,便于后续辨认 */
const certTitleFromFile = (name) =>
String(name || '').replace(/\.[^.]+$/, '').trim() || '证照附件';
const saveCertificates = async () => {
await updateEmployeeFileCertificates(file.value.id, certificateForm);
await loadDetail();
emit('refresh');
};
const handleCertUpload = async (field, { file: uploadedFile }) => {
certSaving.value = field;
/**
* 批量上传证照:统一入口,不再区分学历照片 / 身份证等细分类别。
* 每个文件独立上传并生成一条「证照附件」记录,数量不限;
* el-upload 开启 multiple 后会对每个文件各调用一次本方法,全部结束后统一刷新列表。
*/
const handleCertUpload = async ({ file: uploadedFile }) => {
const employeeId = Number(file.value.employee_id) || 0;
if (!employeeId) {
ElMessage.error('档案缺少员工信息,无法上传证照');
return;
}
certPending.value += 1;
certUploading.value = true;
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('证照已保存');
if (!url) throw new Error(res?.msg || '上传失败');
await createFileRecord({
employee_id: employeeId,
type: 4,
title: certTitleFromFile(uploadedFile.name),
sub_title: '',
extra: '',
start_date: '',
end_date: '',
description: '',
attachment_url: url,
sort: 0,
});
certDone.value += 1;
} catch (error) {
ElMessage.error(error?.message || '证照保存失败');
ElMessage.error(`「${uploadedFile.name}」${error?.message || '上传失败'}`);
} finally {
certSaving.value = '';
certPending.value -= 1;
if (certPending.value <= 0) {
certUploading.value = false;
const done = certDone.value;
certDone.value = 0;
await loadDetail();
if (done) ElMessage.success(`已上传 ${done} 个证照附件`);
}
}
};
@@ -500,7 +524,7 @@ defineExpose({ open, reload: loadDetail });
gap: 12px;
padding: 14px 16px;
border-radius: 8px;
background: #f5f7fa;
background: var(--el-fill-color-light);
.name-line {
display: flex;
@@ -510,19 +534,19 @@ defineExpose({ open, reload: loadDetail });
.name {
font-size: 18px;
font-weight: 600;
color: #303133;
color: var(--el-text-color-primary);
}
.file-no {
font-size: 12px;
color: #909399;
color: var(--el-text-color-secondary);
}
}
.meta-line {
margin-top: 6px;
font-size: 13px;
color: #606266;
color: var(--el-text-color-regular);
}
}
@@ -536,7 +560,7 @@ defineExpose({ open, reload: loadDetail });
margin: 14px 0 10px;
font-size: 14px;
font-weight: 600;
color: #303133;
color: var(--el-text-color-primary);
&:first-child {
margin-top: 0;
@@ -551,6 +575,12 @@ defineExpose({ open, reload: loadDetail });
margin-bottom: 10px;
}
.upload-tip {
margin-right: auto;
font-size: 12px;
color: var(--el-text-color-secondary);
}
.attachment-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
@@ -558,7 +588,7 @@ defineExpose({ open, reload: loadDetail });
}
.attachment-card {
border: 1px solid #ebeef5;
border: 1px solid var(--el-border-color-lighter);
border-radius: 8px;
padding: 10px;
display: flex;
@@ -578,7 +608,7 @@ defineExpose({ open, reload: loadDetail });
justify-content: center;
border-radius: 6px;
overflow: hidden;
background: #f5f7fa;
background: var(--el-fill-color-light);
cursor: pointer;
.attachment-thumb {
@@ -592,7 +622,7 @@ defineExpose({ open, reload: loadDetail });
flex-direction: column;
align-items: center;
gap: 6px;
color: #909399;
color: var(--el-text-color-secondary);
font-size: 12px;
}
}
@@ -601,7 +631,7 @@ defineExpose({ open, reload: loadDetail });
margin-top: 8px;
width: 100%;
font-size: 13px;
color: #303133;
color: var(--el-text-color-primary);
text-align: center;
overflow: hidden;
text-overflow: ellipsis;
@@ -611,7 +641,7 @@ defineExpose({ open, reload: loadDetail });
.attachment-sub {
width: 100%;
font-size: 12px;
color: #909399;
color: var(--el-text-color-secondary);
text-align: center;
overflow: hidden;
text-overflow: ellipsis;