增加员工档案
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
import request from '@/utils/request';
|
||||
|
||||
/**
|
||||
* 员工档案接口(OA 人事档案)
|
||||
*
|
||||
* 档案与员工(yz_backend_employee)一对一;子记录接口统一管理
|
||||
* 教育经历 / 工作经历 / 合同信息 / 证照附件四类记录。
|
||||
* 后端统一返回 { code, data, msg } 包装,调用方需自行取 res.data 使用。
|
||||
*/
|
||||
|
||||
const BASE = '/backend/oa/employeefile';
|
||||
|
||||
/* --------------------------------- 档案 --------------------------------- */
|
||||
|
||||
/** 分页查询档案列表,支持 keyword / status / org_id 筛选 */
|
||||
export function getEmployeeFileList(params) {
|
||||
return request({ url: `${BASE}/list`, method: 'get', params });
|
||||
}
|
||||
|
||||
/** 档案数量统计(总数 + 在职状态分布) */
|
||||
export function getEmployeeFileStats() {
|
||||
return request({ url: `${BASE}/stats`, method: 'get' });
|
||||
}
|
||||
|
||||
/** 档案详情(含员工展示信息与全部子记录) */
|
||||
export function getEmployeeFileDetail(id) {
|
||||
return request({ url: `${BASE}/detail/${id}`, method: 'get' });
|
||||
}
|
||||
|
||||
/** 为员工建档;file_no 留空由后端自动生成 */
|
||||
export function createEmployeeFile(data) {
|
||||
return request({ url: `${BASE}/create`, method: 'post', data });
|
||||
}
|
||||
|
||||
/** 更新档案基本资料(不允许修改员工归属) */
|
||||
export function updateEmployeeFile(id, data) {
|
||||
return request({ url: `${BASE}/update/${id}`, method: 'post', data });
|
||||
}
|
||||
|
||||
/** 删除档案(软删除) */
|
||||
export function deleteEmployeeFile(id) {
|
||||
return request({ url: `${BASE}/delete/${id}`, method: 'delete' });
|
||||
}
|
||||
|
||||
/** 已建档员工ID列表,用于过滤出可建档(未建档)员工 */
|
||||
export function getFiledEmployeeIds() {
|
||||
return request({ url: `${BASE}/filedEmployees`, method: 'get' });
|
||||
}
|
||||
|
||||
/* -------------------------------- 子记录 -------------------------------- */
|
||||
|
||||
/** 新增子记录,type: 1-教育经历 2-工作经历 3-合同信息 4-证照附件 */
|
||||
export function createFileRecord(data) {
|
||||
return request({ url: `${BASE}/record/create`, method: 'post', data });
|
||||
}
|
||||
|
||||
/** 更新子记录 */
|
||||
export function updateFileRecord(id, data) {
|
||||
return request({ url: `${BASE}/record/update/${id}`, method: 'post', data });
|
||||
}
|
||||
|
||||
/** 删除子记录(软删除) */
|
||||
export function deleteFileRecord(id) {
|
||||
return request({ url: `${BASE}/record/delete/${id}`, method: 'delete' });
|
||||
}
|
||||
@@ -110,6 +110,12 @@ const staticMainChildren = [
|
||||
props: { module: "oa" },
|
||||
meta: { requiresAuth: true, title: "职位管理", modulePath: "/apps/oa" }
|
||||
},
|
||||
{
|
||||
path: "/apps/oa/employeefile",
|
||||
name: "EmployeeFile",
|
||||
component: () => import("@/views/apps/oa/employeefile/index.vue"),
|
||||
meta: { requiresAuth: true, title: "员工档案", modulePath: "/apps/oa" }
|
||||
},
|
||||
{
|
||||
path: "/apps/oa/schedule",
|
||||
name: "OaSchedule",
|
||||
|
||||
@@ -0,0 +1,547 @@
|
||||
<template>
|
||||
<el-drawer v-model="visible" size="74%" :with-header="true" :title="`员工档案 - ${file.employee_name || ''}`">
|
||||
<div v-loading="loading" class="file-detail">
|
||||
<!-- 头部:员工概要 + 操作 -->
|
||||
<div class="detail-header">
|
||||
<div class="employee-info">
|
||||
<div class="name-line">
|
||||
<span class="name">{{ file.employee_name || '-' }}</span>
|
||||
<el-tag :type="statusMeta(file.employment_status).type" size="small">
|
||||
{{ statusMeta(file.employment_status).text }}
|
||||
</el-tag>
|
||||
<span class="file-no">{{ file.file_no || '-' }}</span>
|
||||
</div>
|
||||
<div class="meta-line">
|
||||
{{ file.department_name || '-' }} · {{ file.position || '-' }}
|
||||
<template v-if="file.employee_account"> · 工号 {{ file.employee_account }}</template>
|
||||
<template v-if="file.phone"> · {{ file.phone }}</template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<el-button :icon="Refresh" circle @click="loadDetail" />
|
||||
<el-button type="primary" plain :icon="Edit" @click="emit('edit', file)">编辑资料</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-tabs v-model="activeTab" class="detail-tabs">
|
||||
<!-- 基本资料 -->
|
||||
<el-tab-pane label="基本资料" name="basic">
|
||||
<div class="section-title">员工信息</div>
|
||||
<el-descriptions :column="3" border size="small">
|
||||
<el-descriptions-item label="姓名">{{ file.employee_name || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="性别">{{ genderText(file) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="生日">{{ file.birthday || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="学历">{{ file.education || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="民族">{{ file.nation || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="手机">{{ file.phone || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="邮箱">{{ file.email || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="隶属单位">
|
||||
{{ file.affiliate_unit_name || '-' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="部门">{{ file.department_name || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="家庭住址" :span="2">
|
||||
{{ file.home_address || '-' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="账号状态">
|
||||
<el-tag :type="employeeStatusTagType(file)" size="small">
|
||||
{{ employeeStatusText(file) }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<div class="section-title">档案信息</div>
|
||||
<el-descriptions :column="3" border size="small">
|
||||
<el-descriptions-item label="身份证号">{{ file.id_card || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="政治面貌">{{ file.political_status || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="婚姻状况">{{ maritalText(file.marital_status) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="籍贯">{{ file.native_place || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="工作邮箱">{{ file.work_email || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="入职日期">{{ file.hire_date || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="转正日期">{{ file.regular_date || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="离职日期">{{ file.leave_date || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="紧急联系人">
|
||||
<template v-if="file.emergency_contact">
|
||||
{{ file.emergency_contact }}{{ file.emergency_phone ? `(${file.emergency_phone})` : '' }}
|
||||
</template>
|
||||
<template v-else>-</template>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="户籍地址" :span="2">
|
||||
{{ file.household_address || '-' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="现居住址">{{ file.current_address || '-' }}</el-descriptions-item>
|
||||
<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})`"
|
||||
:name="conf.name"
|
||||
>
|
||||
<div class="tab-toolbar">
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
:icon="Plus"
|
||||
@click="openRecordDialog({ type: conf.type })"
|
||||
>
|
||||
新增{{ conf.label }}
|
||||
</el-button>
|
||||
</div>
|
||||
<el-table :data="recordsOf(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>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="conf.extraLabel" :label="conf.extraLabel" prop="extra" min-width="100">
|
||||
<template #default="{ row }">{{ row.extra || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="起止时间" min-width="190">
|
||||
<template #default="{ row }">{{ dateRangeText(row) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="conf.type === 3" label="状态" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="contractStatus(row).type" size="small">{{ contractStatus(row).text }}</el-tag>
|
||||
</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>
|
||||
</el-table-column>
|
||||
<template #empty>
|
||||
<el-empty :description="`暂无${conf.label}`" :image-size="70" />
|
||||
</template>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- 证照附件 -->
|
||||
<el-tab-pane :label="`证照附件(${recordsOf(4).length})`" name="attachment">
|
||||
<div class="tab-toolbar">
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
:icon="Plus"
|
||||
@click="openRecordDialog({ type: 4 })"
|
||||
>
|
||||
上传证照
|
||||
</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 class="attachment-body" @click="previewAttachment(item)">
|
||||
<img
|
||||
v-if="isImage(item.attachment_url)"
|
||||
:src="item.attachment_url"
|
||||
class="attachment-thumb"
|
||||
alt=""
|
||||
/>
|
||||
<div v-else class="attachment-file">
|
||||
<el-icon :size="34"><Document /></el-icon>
|
||||
<span>PDF 附件</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="attachment-name" :title="item.title">{{ item.title }}</div>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-empty v-else description="暂无证照附件" :image-size="70" />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
|
||||
<RecordEditDialog ref="recordDialogRef" @success="loadDetail" />
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, 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 {
|
||||
employeeStatusTagType,
|
||||
employeeStatusText,
|
||||
genderText,
|
||||
} from '@/views/apps/organization/composables';
|
||||
import RecordEditDialog from './recordEditDialog.vue';
|
||||
|
||||
/**
|
||||
* 员工档案详情抽屉:基本资料 + 教育/工作/合同/证照四类子记录管理。
|
||||
* 档案编辑(基本资料)通过 edit 事件交给父页面打开编辑弹窗。
|
||||
*/
|
||||
|
||||
const emit = defineEmits(['edit']);
|
||||
|
||||
const visible = ref(false);
|
||||
const loading = ref(false);
|
||||
const recordDialogRef = ref();
|
||||
|
||||
const file = ref({});
|
||||
const records = ref([]);
|
||||
const activeTab = ref('basic');
|
||||
|
||||
/** 教育经历/工作经历/合同信息三类同构表格的列配置 */
|
||||
const recordTabs = [
|
||||
{ type: 1, name: 'education', label: '教育经历', nameLabel: '学校', subLabel: '专业', extraLabel: '学历' },
|
||||
{ type: 2, name: 'work', label: '工作经历', nameLabel: '公司', subLabel: '职位', extraLabel: '所在部门' },
|
||||
{ type: 3, name: 'contract', label: '合同信息', nameLabel: '合同名称', subLabel: '合同类型', extraLabel: '签订主体' },
|
||||
];
|
||||
|
||||
const MARITAL_TEXT = { 0: '未知', 1: '未婚', 2: '已婚', 3: '离异', 4: '丧偶' };
|
||||
const STATUS_META = {
|
||||
1: { text: '试用', type: 'warning' },
|
||||
2: { text: '正式', type: 'success' },
|
||||
3: { text: '离职', type: 'info' },
|
||||
};
|
||||
|
||||
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 previewCert = (cert) => {
|
||||
// PDF 无法用 el-image 预览,新窗口打开;图片走 el-image 内置放大
|
||||
if (cert.url && !isImage(cert.url)) {
|
||||
window.open(cert.url, '_blank');
|
||||
}
|
||||
};
|
||||
|
||||
const recordsOf = (type) => records.value.filter((item) => Number(item.type) === Number(type));
|
||||
|
||||
const dateRangeText = (row) => {
|
||||
if (!row.start_date && !row.end_date) return '-';
|
||||
return `${row.start_date || '?'} ~ ${row.end_date || '至今'}`;
|
||||
};
|
||||
|
||||
/** 合同状态:以结束日期判断履行中 / 已到期 */
|
||||
const contractStatus = (row) => {
|
||||
if (!row.end_date) return { text: '长期', type: 'success' };
|
||||
return row.end_date >= new Date().toISOString().slice(0, 10)
|
||||
? { text: '履行中', type: 'success' }
|
||||
: { text: '已到期', type: 'warning' };
|
||||
};
|
||||
|
||||
const isImage = (url) => /\.(png|jpe?g|gif|webp|bmp)(\?.*)?$/i.test(String(url || ''));
|
||||
|
||||
const loadDetail = async () => {
|
||||
const id = file.value?.id;
|
||||
if (!id) return;
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await getEmployeeFileDetail(id);
|
||||
const data = res?.data || res || {};
|
||||
file.value = data.file || {};
|
||||
records.value = data.records || [];
|
||||
} catch (error) {
|
||||
ElMessage.error(error?.message || '加载档案详情失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
/** 打开抽屉;row 为列表行(至少含 id) */
|
||||
const open = (row) => {
|
||||
const id = typeof row === 'object' ? row?.id : row;
|
||||
if (!id) {
|
||||
ElMessage.error('未提供有效的档案ID');
|
||||
return;
|
||||
}
|
||||
file.value = typeof row === 'object' ? { ...row } : { id };
|
||||
records.value = [];
|
||||
visible.value = true;
|
||||
loadDetail();
|
||||
};
|
||||
|
||||
const openRecordDialog = (options) => {
|
||||
recordDialogRef.value?.open({ employeeId: file.value.employee_id, ...options });
|
||||
};
|
||||
|
||||
const previewAttachment = (item) => {
|
||||
const url = item.attachment_url;
|
||||
if (!url) {
|
||||
ElMessage.warning('该证照未上传附件');
|
||||
return;
|
||||
}
|
||||
window.open(url, '_blank');
|
||||
};
|
||||
|
||||
const handleDeleteRecord = async (row) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确定删除「${row.title}」这条记录吗?`, '删除确认', { type: 'warning' });
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await deleteFileRecord(row.id);
|
||||
ElMessage.success('删除成功');
|
||||
await loadDetail();
|
||||
} catch (error) {
|
||||
ElMessage.error(error?.message || '删除失败');
|
||||
}
|
||||
};
|
||||
|
||||
defineExpose({ open, reload: loadDetail });
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.file-detail {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.detail-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
padding: 14px 16px;
|
||||
border-radius: 8px;
|
||||
background: #f5f7fa;
|
||||
|
||||
.name-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.name {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.file-no {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
|
||||
.meta-line {
|
||||
margin-top: 6px;
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
}
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
margin: 14px 0 10px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
.attachment-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.attachment-card {
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
transition: box-shadow 0.2s;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.attachment-body {
|
||||
width: 100%;
|
||||
height: 130px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
background: #f5f7fa;
|
||||
cursor: pointer;
|
||||
|
||||
.attachment-thumb {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.attachment-file {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.attachment-name {
|
||||
margin-top: 8px;
|
||||
width: 100%;
|
||||
font-size: 13px;
|
||||
color: #303133;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.attachment-sub {
|
||||
width: 100%;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
min-height: 16px;
|
||||
}
|
||||
|
||||
.attachment-actions {
|
||||
margin-top: 4px;
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,428 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
:title="isEdit ? '编辑档案' : '新建档案'"
|
||||
width="680px"
|
||||
:close-on-click-modal="false"
|
||||
@closed="resetForm"
|
||||
>
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
|
||||
<el-form-item label="关联员工" prop="employee_id">
|
||||
<!-- 编辑时员工归属不可变更,只读展示 -->
|
||||
<el-input v-if="isEdit" :value="editEmployeeLabel" disabled />
|
||||
<el-select
|
||||
v-else
|
||||
v-model="form.employee_id"
|
||||
filterable
|
||||
placeholder="选择未建档的员工"
|
||||
:loading="employeesLoading"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="emp in candidateEmployees"
|
||||
:key="emp.id"
|
||||
:label="`${emp.name}(${emp.account})`"
|
||||
:value="Number(emp.id)"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="档案编号" prop="file_no">
|
||||
<el-input v-model="form.file_no" placeholder="留空自动生成" maxlength="50" />
|
||||
</el-form-item>
|
||||
|
||||
<el-divider content-position="left">个人信息</el-divider>
|
||||
<el-row :gutter="12">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="身份证号" prop="id_card">
|
||||
<el-input v-model="form.id_card" maxlength="30" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="政治面貌" prop="political_status">
|
||||
<el-select
|
||||
v-model="form.political_status"
|
||||
filterable
|
||||
allow-create
|
||||
default-first-option
|
||||
clearable
|
||||
placeholder="选择或输入"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option v-for="opt in politicalOptions" :key="opt" :label="opt" :value="opt" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="12">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="婚姻状况" prop="marital_status">
|
||||
<el-select v-model="form.marital_status" style="width: 100%">
|
||||
<el-option label="未知" :value="0" />
|
||||
<el-option label="未婚" :value="1" />
|
||||
<el-option label="已婚" :value="2" />
|
||||
<el-option label="离异" :value="3" />
|
||||
<el-option label="丧偶" :value="4" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="籍贯" prop="native_place">
|
||||
<el-input v-model="form.native_place" maxlength="100" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item label="户籍地址" prop="household_address">
|
||||
<el-input v-model="form.household_address" maxlength="255" />
|
||||
</el-form-item>
|
||||
<el-form-item label="现居住址" prop="current_address">
|
||||
<el-input v-model="form.current_address" maxlength="255" />
|
||||
</el-form-item>
|
||||
<el-form-item label="工作邮箱" prop="work_email">
|
||||
<el-input v-model="form.work_email" maxlength="100" />
|
||||
</el-form-item>
|
||||
|
||||
<el-divider content-position="left">任职信息</el-divider>
|
||||
<el-form-item label="在职状态" prop="employment_status">
|
||||
<el-radio-group v-model="form.employment_status">
|
||||
<el-radio-button :value="1">试用</el-radio-button>
|
||||
<el-radio-button :value="2">正式</el-radio-button>
|
||||
<el-radio-button :value="3">离职</el-radio-button>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="入职日期" prop="hire_date">
|
||||
<el-date-picker
|
||||
v-model="form.hire_date"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="选择日期"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="转正日期" prop="regular_date">
|
||||
<el-date-picker
|
||||
v-model="form.regular_date"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="选择日期"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="离职日期" prop="leave_date">
|
||||
<el-date-picker
|
||||
v-model="form.leave_date"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="选择日期"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</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">
|
||||
<el-form-item label="联系人" prop="emergency_contact">
|
||||
<el-input v-model="form.emergency_contact" maxlength="50" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="联系电话" prop="emergency_phone">
|
||||
<el-input v-model="form.emergency_phone" maxlength="20" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="与本人关系" prop="emergency_relation">
|
||||
<el-input v-model="form.emergency_relation" maxlength="30" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="form.remark" type="textarea" :rows="2" maxlength="500" show-word-limit />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="handleSave">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
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';
|
||||
|
||||
/**
|
||||
* 建档 / 编辑档案弹窗。
|
||||
* 新建时员工下拉只展示未建档员工(全部员工 - 已建档ID),
|
||||
* 选项先于表单回填加载完成,避免下拉异步回显问题。
|
||||
*/
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
|
||||
const visible = ref(false);
|
||||
const saving = ref(false);
|
||||
const employeesLoading = ref(false);
|
||||
const formRef = ref();
|
||||
|
||||
const isEdit = ref(false);
|
||||
const editId = ref(0);
|
||||
const editEmployeeLabel = ref('');
|
||||
|
||||
const allEmployees = ref([]);
|
||||
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: '',
|
||||
household_address: '',
|
||||
current_address: '',
|
||||
work_email: '',
|
||||
hire_date: '',
|
||||
regular_date: '',
|
||||
leave_date: '',
|
||||
employment_status: 2,
|
||||
emergency_contact: '',
|
||||
emergency_phone: '',
|
||||
emergency_relation: '',
|
||||
remark: '',
|
||||
});
|
||||
|
||||
const rules = {
|
||||
employee_id: [{ required: true, message: '请选择员工', trigger: 'change' }],
|
||||
work_email: [
|
||||
{
|
||||
type: 'email',
|
||||
message: '邮箱格式不正确',
|
||||
trigger: ['blur', 'change'],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
/** 未建档员工 = 全部员工 - 已建档员工 */
|
||||
const candidateEmployees = computed(() => {
|
||||
const filed = new Set(filedIds.value.map(Number));
|
||||
return allEmployees.value.filter((emp) => !filed.has(Number(emp.id)));
|
||||
});
|
||||
|
||||
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 = '';
|
||||
form.household_address = '';
|
||||
form.current_address = '';
|
||||
form.work_email = '';
|
||||
form.hire_date = '';
|
||||
form.regular_date = '';
|
||||
form.leave_date = '';
|
||||
form.employment_status = 2;
|
||||
form.emergency_contact = '';
|
||||
form.emergency_phone = '';
|
||||
form.emergency_relation = '';
|
||||
form.remark = '';
|
||||
formRef.value?.clearValidate();
|
||||
};
|
||||
|
||||
/** 加载员工下拉选项:全部员工 + 已建档ID并行请求 */
|
||||
const loadEmployeeOptions = async () => {
|
||||
employeesLoading.value = true;
|
||||
try {
|
||||
const [empRes, filedRes] = await Promise.all([
|
||||
oaOrganizationApi.getEmployeeList(),
|
||||
getFiledEmployeeIds(),
|
||||
]);
|
||||
allEmployees.value = empRes?.data || empRes || [];
|
||||
filedIds.value = filedRes?.data || filedRes || [];
|
||||
} catch (error) {
|
||||
allEmployees.value = [];
|
||||
filedIds.value = [];
|
||||
ElMessage.error(error?.message || '加载员工选项失败');
|
||||
} finally {
|
||||
employeesLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
/** 打开弹窗;file 为空表示新建,否则回填编辑 */
|
||||
const open = async (file = null) => {
|
||||
resetForm();
|
||||
isEdit.value = !!file?.id;
|
||||
editId.value = file?.id || 0;
|
||||
|
||||
if (isEdit.value) {
|
||||
// 编辑前先并行刷新选项数据,再回填表单
|
||||
await loadEmployeeOptions();
|
||||
editEmployeeLabel.value = file.employee_name
|
||||
? `${file.employee_name}(${file.employee_account || '-'})`
|
||||
: `员工 #${file.employee_id}`;
|
||||
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 || '';
|
||||
form.household_address = file.household_address || '';
|
||||
form.current_address = file.current_address || '';
|
||||
form.work_email = file.work_email || '';
|
||||
form.hire_date = file.hire_date || '';
|
||||
form.regular_date = file.regular_date || '';
|
||||
form.leave_date = file.leave_date || '';
|
||||
form.employment_status = Number(file.employment_status ?? 2);
|
||||
form.emergency_contact = file.emergency_contact || '';
|
||||
form.emergency_phone = file.emergency_phone || '';
|
||||
form.emergency_relation = file.emergency_relation || '';
|
||||
form.remark = file.remark || '';
|
||||
} else {
|
||||
await loadEmployeeOptions();
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
const payload = { ...form };
|
||||
delete payload.employee_id;
|
||||
|
||||
saving.value = true;
|
||||
try {
|
||||
if (isEdit.value) {
|
||||
await updateEmployeeFile(editId.value, payload);
|
||||
} else {
|
||||
await createEmployeeFile({ ...payload, employee_id: form.employee_id });
|
||||
}
|
||||
ElMessage.success(isEdit.value ? '保存成功' : '建档成功');
|
||||
visible.value = false;
|
||||
emit('success');
|
||||
} catch (error) {
|
||||
ElMessage.error(error?.message || '保存失败');
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
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>
|
||||
@@ -0,0 +1,290 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
:title="`${meta.title} - ${isEdit ? '编辑' : '新增'}`"
|
||||
width="560px"
|
||||
:close-on-click-modal="false"
|
||||
@closed="resetForm"
|
||||
>
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="90px">
|
||||
<el-form-item :label="meta.nameLabel" prop="title">
|
||||
<el-input v-model="form.title" :placeholder="`请输入${meta.nameLabel}`" maxlength="100" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="meta.subLabel" prop="sub_title">
|
||||
<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-select
|
||||
v-model="form.extra"
|
||||
filterable
|
||||
allow-create
|
||||
default-first-option
|
||||
clearable
|
||||
:placeholder="`选择或输入${meta.extraLabel}`"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option v-for="opt in extraOptions" :key="opt" :label="opt" :value="opt" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="起止时间" prop="dateRange">
|
||||
<el-date-picker
|
||||
v-model="form.dateRange"
|
||||
type="daterange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
value-format="YYYY-MM-DD"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="描述" prop="description">
|
||||
<el-input
|
||||
v-model="form.description"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
:placeholder="meta.type === 2 ? '工作内容简述' : '备注说明'"
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="meta.type === 4" label="附件" prop="attachment_url">
|
||||
<div class="upload-area">
|
||||
<el-upload
|
||||
:show-file-list="false"
|
||||
:http-request="handleUpload"
|
||||
:before-upload="beforeUpload"
|
||||
accept="image/*,.pdf"
|
||||
>
|
||||
<img v-if="form.attachment_url" :src="form.attachment_url" class="attachment-preview" alt="证照" />
|
||||
<div v-else class="upload-trigger">
|
||||
<el-icon><Plus /></el-icon>
|
||||
<span>上传证照</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>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="handleSave">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, reactive, ref } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { Plus } from '@element-plus/icons-vue';
|
||||
import { createFileRecord, updateFileRecord } from '@/api/employeeFile';
|
||||
import { uploadFile } from '@/api/file';
|
||||
|
||||
/**
|
||||
* 档案子记录编辑弹窗:教育经历 / 工作经历 / 合同信息 / 证照附件共用。
|
||||
* 字段语义随 type 变化(与后端建表注释保持一致):
|
||||
* 教育经历(1):title=学校 sub_title=专业 extra=学历
|
||||
* 工作经历(2):title=公司 sub_title=职位 extra=所在部门
|
||||
* 合同信息(3):title=合同名称 sub_title=合同类型 extra=签订主体
|
||||
* 证照附件(4):title=证照名称 sub_title=证件号码 extra 不展示
|
||||
*/
|
||||
|
||||
const TYPE_META = {
|
||||
1: { type: 1, title: '教育经历', nameLabel: '学校名称', subLabel: '专业', extraLabel: '学历' },
|
||||
2: { type: 2, title: '工作经历', nameLabel: '公司名称', subLabel: '职位', extraLabel: '所在部门' },
|
||||
3: { type: 3, title: '合同信息', nameLabel: '合同名称', subLabel: '合同类型', extraLabel: '签订主体' },
|
||||
4: { type: 4, title: '证照附件', nameLabel: '证照名称', subLabel: '证件号码', extraLabel: '' },
|
||||
};
|
||||
|
||||
const EXTRA_OPTIONS = {
|
||||
1: ['小学', '初中', '高中', '中专', '大专', '本科', '硕士', '博士'],
|
||||
2: [],
|
||||
3: ['固定期限', '无固定期限', '劳务合同', '实习协议'],
|
||||
4: [],
|
||||
};
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
|
||||
const visible = ref(false);
|
||||
const saving = ref(false);
|
||||
const uploading = ref(false);
|
||||
const formRef = ref();
|
||||
|
||||
const employeeId = ref(0);
|
||||
const meta = ref(TYPE_META[1]);
|
||||
const isEdit = ref(false);
|
||||
const editId = ref(0);
|
||||
|
||||
const form = reactive({
|
||||
title: '',
|
||||
sub_title: '',
|
||||
extra: '',
|
||||
dateRange: null,
|
||||
description: '',
|
||||
attachment_url: '',
|
||||
});
|
||||
|
||||
const rules = {
|
||||
title: [{ required: true, message: '请输入名称', trigger: 'blur' }],
|
||||
};
|
||||
|
||||
const extraOptions = computed(() => EXTRA_OPTIONS[meta.value.type] || []);
|
||||
|
||||
const resetForm = () => {
|
||||
form.title = '';
|
||||
form.sub_title = '';
|
||||
form.extra = '';
|
||||
form.dateRange = null;
|
||||
form.description = '';
|
||||
form.attachment_url = '';
|
||||
formRef.value?.clearValidate();
|
||||
};
|
||||
|
||||
/**
|
||||
* 打开弹窗。
|
||||
* @param {object} options
|
||||
* @param {number} options.employeeId 员工ID(记录挂载主体)
|
||||
* @param {number} options.type 记录类型 1-4
|
||||
* @param {object} [options.record] 编辑时传入的记录;缺省为新增
|
||||
*/
|
||||
const open = (options = {}) => {
|
||||
employeeId.value = Number(options.employeeId) || 0;
|
||||
meta.value = TYPE_META[options.type] || TYPE_META[1];
|
||||
resetForm();
|
||||
|
||||
const record = options.record;
|
||||
isEdit.value = !!record?.id;
|
||||
editId.value = record?.id || 0;
|
||||
if (record) {
|
||||
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;
|
||||
form.description = record.description || '';
|
||||
form.attachment_url = record.attachment_url || '';
|
||||
}
|
||||
|
||||
visible.value = true;
|
||||
};
|
||||
|
||||
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 handleUpload = async ({ file }) => {
|
||||
uploading.value = true;
|
||||
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.attachment_url = url;
|
||||
} catch (error) {
|
||||
ElMessage.error(error?.message || '上传失败');
|
||||
} finally {
|
||||
uploading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
const valid = await formRef.value?.validate().then(() => true).catch(() => false);
|
||||
if (!valid) return;
|
||||
if (!employeeId.value) {
|
||||
ElMessage.error('缺少员工信息');
|
||||
return;
|
||||
}
|
||||
|
||||
const range = Array.isArray(form.dateRange) ? form.dateRange : [null, null];
|
||||
const payload = {
|
||||
employee_id: employeeId.value,
|
||||
type: meta.value.type,
|
||||
title: form.title,
|
||||
sub_title: form.sub_title,
|
||||
extra: form.extra,
|
||||
start_date: range[0] || '',
|
||||
end_date: range[1] || '',
|
||||
description: form.description,
|
||||
attachment_url: form.attachment_url,
|
||||
sort: 0,
|
||||
};
|
||||
|
||||
saving.value = true;
|
||||
try {
|
||||
if (isEdit.value) {
|
||||
await updateFileRecord(editId.value, payload);
|
||||
} else {
|
||||
await createFileRecord(payload);
|
||||
}
|
||||
ElMessage.success('保存成功');
|
||||
visible.value = false;
|
||||
emit('success');
|
||||
} catch (error) {
|
||||
ElMessage.error(error?.message || '保存失败');
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
defineExpose({ open, uploading });
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.upload-area {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.attachment-preview {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
object-fit: cover;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #dcdfe6;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.upload-trigger {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
border: 1px dashed #c0ccda;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
color: #8c939d;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.2s;
|
||||
|
||||
&:hover {
|
||||
border-color: #3973ff;
|
||||
color: #3973ff;
|
||||
}
|
||||
}
|
||||
|
||||
.upload-tip {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,348 @@
|
||||
<template>
|
||||
<div class="employee-file-page">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2>员工档案</h2>
|
||||
<p>一人一档,集中管理人事资料、教育经历、工作经历、合同与证照</p>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<el-button :icon="Refresh" @click="loadAll">刷新</el-button>
|
||||
<el-button type="primary" :icon="Plus" @click="editDialogRef?.open()">新建档案</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 统计卡片 -->
|
||||
<div class="stats-bar">
|
||||
<div v-for="item in statCards" :key="item.label" class="stat-card" :class="item.tone">
|
||||
<div class="stat-value">{{ stats[item.key] ?? 0 }}</div>
|
||||
<div class="stat-label">{{ item.label }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-card shadow="never">
|
||||
<div class="filter-bar">
|
||||
<el-input
|
||||
v-model="filters.keyword"
|
||||
placeholder="姓名 / 账号 / 手机 / 档案编号 / 身份证"
|
||||
clearable
|
||||
class="filter-item"
|
||||
:prefix-icon="Search"
|
||||
@change="handleSearch"
|
||||
/>
|
||||
<el-tree-select
|
||||
v-model="filters.org_id"
|
||||
:data="departmentTree"
|
||||
:props="treeSelectProps"
|
||||
check-strictly
|
||||
clearable
|
||||
placeholder="按部门筛选(含下级)"
|
||||
class="filter-item"
|
||||
@change="handleSearch"
|
||||
/>
|
||||
<el-select
|
||||
v-model="filters.status"
|
||||
clearable
|
||||
placeholder="在职状态"
|
||||
class="filter-item status-filter"
|
||||
@change="handleSearch"
|
||||
>
|
||||
<el-option label="试用" :value="1" />
|
||||
<el-option label="正式" :value="2" />
|
||||
<el-option label="离职" :value="3" />
|
||||
</el-select>
|
||||
<el-button :icon="RefreshLeft" @click="resetFilters">重置</el-button>
|
||||
</div>
|
||||
|
||||
<el-table :data="rows" v-loading="loading" stripe>
|
||||
<el-table-column prop="file_no" label="档案编号" width="150">
|
||||
<template #default="{ row }">{{ row.file_no || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="employee_name" label="姓名" width="110">
|
||||
<template #default="{ row }">
|
||||
<span class="name-link" @click="detailDrawerRef?.open(row)">{{ row.employee_name || '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="性别" width="70" align="center">
|
||||
<template #default="{ row }">{{ genderText(row) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="部门" min-width="130">
|
||||
<template #default="{ row }">{{ row.department_name || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="职位" min-width="110">
|
||||
<template #default="{ row }">{{ row.position || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="手机" width="130">
|
||||
<template #default="{ row }">{{ row.phone || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="入职日期" width="110">
|
||||
<template #default="{ row }">{{ row.hire_date || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="在职状态" width="95" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusMeta(row.employment_status).type" size="small">
|
||||
{{ statusMeta(row.employment_status).text }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="190" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" size="small" @click="detailDrawerRef?.open(row)">查看档案</el-button>
|
||||
<el-button link type="primary" size="small" @click="editDialogRef?.open(row)">编辑</el-button>
|
||||
<el-button link type="danger" size="small" @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template #empty>
|
||||
<el-empty description="暂无员工档案" :image-size="90" />
|
||||
</template>
|
||||
</el-table>
|
||||
|
||||
<div class="pagination-bar">
|
||||
<el-pagination
|
||||
v-model:current-page="page"
|
||||
v-model:page-size="pageSize"
|
||||
:total="total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@current-change="loadData"
|
||||
@size-change="handleSearch"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<FileEditDialog ref="editDialogRef" @success="loadAll" />
|
||||
<FileDetailDrawer ref="detailDrawerRef" @edit="(file) => editDialogRef?.open(file)" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted, reactive, ref } from 'vue';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { Plus, Refresh, RefreshLeft, Search } from '@element-plus/icons-vue';
|
||||
import {
|
||||
deleteEmployeeFile,
|
||||
getEmployeeFileList,
|
||||
getEmployeeFileStats,
|
||||
} from '@/api/employeeFile';
|
||||
import { oaOrganizationApi } from '@/api/organization';
|
||||
import { buildOrgTree, genderText } from '@/views/apps/organization/composables';
|
||||
import FileEditDialog from './components/fileEditDialog.vue';
|
||||
import FileDetailDrawer from './components/fileDetailDrawer.vue';
|
||||
|
||||
/**
|
||||
* 员工档案页面(OA 人事档案)。
|
||||
* 列表后端分页;档案编辑/详情通过弹窗与抽屉完成,
|
||||
* 档案详情内直接管理教育经历、工作经历、合同与证照子记录。
|
||||
*/
|
||||
|
||||
const editDialogRef = ref();
|
||||
const detailDrawerRef = ref();
|
||||
|
||||
const rows = ref([]);
|
||||
const total = ref(0);
|
||||
const loading = ref(false);
|
||||
const page = ref(1);
|
||||
const pageSize = ref(10);
|
||||
const departmentTree = ref([]);
|
||||
|
||||
const stats = ref({});
|
||||
const statCards = [
|
||||
{ key: 'total', label: '档案总数', tone: '' },
|
||||
{ key: 'active', label: '正式', tone: 'success' },
|
||||
{ key: 'probation', label: '试用', tone: 'warning' },
|
||||
{ key: 'resigned', label: '离职', tone: 'info' },
|
||||
];
|
||||
|
||||
const STATUS_META = {
|
||||
1: { text: '试用', type: 'warning' },
|
||||
2: { text: '正式', type: 'success' },
|
||||
3: { text: '离职', type: 'info' },
|
||||
};
|
||||
|
||||
const statusMeta = (value) => STATUS_META[Number(value ?? 2)] || STATUS_META[2];
|
||||
|
||||
const treeSelectProps = { value: 'id', label: 'org_name', children: 'children' };
|
||||
|
||||
const filters = reactive({ keyword: '', org_id: null, status: '' });
|
||||
|
||||
const loadData = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const params = { page: page.value, pageSize: pageSize.value };
|
||||
if (filters.keyword) params.keyword = filters.keyword;
|
||||
if (filters.org_id) params.org_id = filters.org_id;
|
||||
if (filters.status !== '' && filters.status !== null) params.status = filters.status;
|
||||
|
||||
const res = await getEmployeeFileList(params);
|
||||
const data = res?.data || res || {};
|
||||
rows.value = data.list || [];
|
||||
total.value = Number(data.total || 0);
|
||||
} catch (error) {
|
||||
ElMessage.error(error?.message || '获取档案列表失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const loadStats = async () => {
|
||||
try {
|
||||
const res = await getEmployeeFileStats();
|
||||
stats.value = res?.data || res || {};
|
||||
} catch {
|
||||
stats.value = {};
|
||||
}
|
||||
};
|
||||
|
||||
const loadDepartments = async () => {
|
||||
try {
|
||||
const res = await oaOrganizationApi.getOrganizationList();
|
||||
departmentTree.value = buildOrgTree(res?.data || res || []);
|
||||
} catch {
|
||||
departmentTree.value = [];
|
||||
}
|
||||
};
|
||||
|
||||
/** 列表 + 统计一起刷新(增删改后调用) */
|
||||
const loadAll = () => {
|
||||
loadData();
|
||||
loadStats();
|
||||
};
|
||||
|
||||
const handleSearch = () => {
|
||||
page.value = 1;
|
||||
loadData();
|
||||
};
|
||||
|
||||
const resetFilters = () => {
|
||||
filters.keyword = '';
|
||||
filters.org_id = null;
|
||||
filters.status = '';
|
||||
handleSearch();
|
||||
};
|
||||
|
||||
const handleDelete = async (row) => {
|
||||
const name = row.employee_name || row.file_no || `#${row.id}`;
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定删除「${name}」的档案吗?档案下的教育经历、合同等记录仍会保留。`,
|
||||
'删除确认',
|
||||
{ type: 'warning' }
|
||||
);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await deleteEmployeeFile(row.id);
|
||||
ElMessage.success('删除成功');
|
||||
loadAll();
|
||||
} catch (error) {
|
||||
ElMessage.error(error?.message || '删除失败');
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loadDepartments();
|
||||
loadAll();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.employee-file-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
|
||||
h2 {
|
||||
margin: 0 0 6px;
|
||||
font-size: 20px;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.stats-bar {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
padding: 14px 18px;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
border: 1px solid #ebeef5;
|
||||
|
||||
.stat-value {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
margin-top: 4px;
|
||||
font-size: 13px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
&.success .stat-value {
|
||||
color: #67c23a;
|
||||
}
|
||||
|
||||
&.warning .stat-value {
|
||||
color: #e6a23c;
|
||||
}
|
||||
|
||||
&.info .stat-value {
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
width: 240px;
|
||||
}
|
||||
|
||||
.status-filter {
|
||||
width: 140px;
|
||||
}
|
||||
|
||||
.name-link {
|
||||
color: #3973ff;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.pagination-bar {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 14px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,846 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"server/models"
|
||||
"server/pkg/jwtutil"
|
||||
|
||||
"github.com/beego/beego/v2/client/orm"
|
||||
beego "github.com/beego/beego/v2/server/web"
|
||||
)
|
||||
|
||||
// BackendEmployeeFileController 员工档案管理(OA 人事档案)。
|
||||
// 档案与员工(yz_backend_employee)按 tid + employee_id 一对一,
|
||||
// 子记录表统一存放教育经历 / 工作经历 / 合同信息 / 证照附件。
|
||||
// 数据按 JWT 中的租户隔离。
|
||||
type BackendEmployeeFileController struct {
|
||||
beego.Controller
|
||||
}
|
||||
|
||||
func (c *BackendEmployeeFileController) efClaims() (*jwtutil.Claims, error) {
|
||||
auth := c.Ctx.Request.Header.Get("Authorization")
|
||||
if auth == "" {
|
||||
return nil, orm.ErrNoRows
|
||||
}
|
||||
parts := strings.SplitN(auth, " ", 2)
|
||||
if len(parts) != 2 || parts[0] != "Bearer" {
|
||||
return nil, orm.ErrNoRows
|
||||
}
|
||||
claims, err := jwtutil.ParseToken(parts[1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if claims.UserType != "backend" {
|
||||
return nil, orm.ErrNoRows
|
||||
}
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
func (c *BackendEmployeeFileController) efOk(data interface{}) {
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": data}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
func (c *BackendEmployeeFileController) efErr(httpStatus, bizCode int, msg string) {
|
||||
c.Ctx.Output.SetStatus(httpStatus)
|
||||
c.Data["json"] = map[string]interface{}{"code": bizCode, "msg": msg}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
var efDatePattern = regexp.MustCompile(`^\d{4}-\d{2}-\d{2}$`)
|
||||
|
||||
// efIsValidDate 校验 "YYYY-MM-DD" 日期字符串;允许为空(可空字段)。
|
||||
func efIsValidDate(s string) bool {
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" {
|
||||
return true
|
||||
}
|
||||
if !efDatePattern.MatchString(s) {
|
||||
return false
|
||||
}
|
||||
_, err := time.ParseInLocation("2006-01-02", s, time.Local)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// efNormalizeDate 将 ORM 读出的日期值统一为 "YYYY-MM-DD"。
|
||||
// DSN 开启 parseTime=True 后 DATE 列会被驱动解析为 time.Time,
|
||||
// 赋给 string 字段后是 Go 默认格式,取前 10 位(与 OA 日程一致)。
|
||||
func efNormalizeDate(s string) string {
|
||||
s = strings.TrimSpace(s)
|
||||
if len(s) >= 10 {
|
||||
return s[:10]
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// efQuery 档案查询基座:强制附加租户过滤与未删除条件。
|
||||
func (c *BackendEmployeeFileController) efQuery(tid int) orm.QuerySeter {
|
||||
return models.Orm.QueryTable(new(models.BackendEmployeeFile)).
|
||||
Filter("tid", tid).
|
||||
Filter("is_deleted", 0)
|
||||
}
|
||||
|
||||
// efEmployeeQuery 员工查询基座(与组织架构控制器保持同一过滤口径)。
|
||||
func efEmployeeQuery(tid int) orm.QuerySeter {
|
||||
return models.Orm.QueryTable(new(models.BackendEmployee)).
|
||||
Filter("tid", tid).
|
||||
Filter("delete_time__isnull", true)
|
||||
}
|
||||
|
||||
// efOrgNameMap 组织ID -> 名称映射,用于展示员工的隶属单位/部门名称。
|
||||
func (c *BackendEmployeeFileController) efOrgNameMap(tid int) map[uint64]string {
|
||||
var rows []models.BackendOrganization
|
||||
if _, err := models.Orm.QueryTable(new(models.BackendOrganization)).
|
||||
Filter("tid", tid).
|
||||
Filter("delete_time__isnull", true).
|
||||
All(&rows, "ID", "OrgName"); err != nil {
|
||||
return map[uint64]string{}
|
||||
}
|
||||
names := make(map[uint64]string, len(rows))
|
||||
for _, row := range rows {
|
||||
names[row.ID] = row.OrgName
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
// efOrgNameByID 组织ID字符串转名称;非数字时原样返回(手输文本兜底)。
|
||||
func efOrgNameByID(names map[uint64]string, id string) string {
|
||||
if id == "" {
|
||||
return ""
|
||||
}
|
||||
if v, err := strconv.ParseUint(id, 10, 64); err == nil {
|
||||
return names[v]
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
// efSubtreeOrgIDs 收集组织节点及其全部下级的ID(BFS,含环保护)。
|
||||
func (c *BackendEmployeeFileController) efSubtreeOrgIDs(tid int, rootID uint64) []uint64 {
|
||||
var rows []models.BackendOrganization
|
||||
if _, err := models.Orm.QueryTable(new(models.BackendOrganization)).
|
||||
Filter("tid", tid).
|
||||
Filter("delete_time__isnull", true).
|
||||
All(&rows, "ID", "ParentID"); err != nil {
|
||||
return []uint64{rootID}
|
||||
}
|
||||
childrenOf := map[uint64][]uint64{}
|
||||
for _, row := range rows {
|
||||
childrenOf[row.ParentID] = append(childrenOf[row.ParentID], row.ID)
|
||||
}
|
||||
|
||||
result := []uint64{rootID}
|
||||
queue := []uint64{rootID}
|
||||
visited := map[uint64]bool{rootID: true}
|
||||
for len(queue) > 0 {
|
||||
current := queue[0]
|
||||
queue = queue[1:]
|
||||
for _, child := range childrenOf[current] {
|
||||
if visited[child] {
|
||||
continue
|
||||
}
|
||||
visited[child] = true
|
||||
result = append(result, child)
|
||||
queue = append(queue, child)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// efEmployeeIDsInOrgs 取部门集合内的员工ID(档案按员工过滤时使用)。
|
||||
func (c *BackendEmployeeFileController) efEmployeeIDsInOrgs(tid int, orgIDs []uint64) []uint64 {
|
||||
values := make([]string, 0, len(orgIDs))
|
||||
for _, id := range orgIDs {
|
||||
values = append(values, strconv.FormatUint(id, 10))
|
||||
}
|
||||
var rows []models.BackendEmployee
|
||||
if _, err := efEmployeeQuery(tid).Filter("department__in", values).All(&rows, "ID"); err != nil {
|
||||
return nil
|
||||
}
|
||||
ids := make([]uint64, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
ids = append(ids, uint64(row.ID))
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
// efEmployeeIDsByKeyword 取姓名/账号/手机命中的员工ID(档案关键词搜索联动员工字段)。
|
||||
func (c *BackendEmployeeFileController) efEmployeeIDsByKeyword(tid int, keyword string) []uint64 {
|
||||
cond := orm.NewCondition().
|
||||
Or("name__icontains", keyword).
|
||||
Or("account__icontains", keyword).
|
||||
Or("phone__icontains", keyword)
|
||||
var rows []models.BackendEmployee
|
||||
if _, err := efEmployeeQuery(tid).SetCond(cond).All(&rows, "ID"); err != nil {
|
||||
return nil
|
||||
}
|
||||
ids := make([]uint64, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
ids = append(ids, uint64(row.ID))
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
// employeeFileDTO 档案 + 关联员工展示信息(JSON 平铺,前端表格/详情直接用)。
|
||||
type employeeFileDTO struct {
|
||||
models.BackendEmployeeFile
|
||||
EmployeeName string `json:"employee_name"`
|
||||
EmployeeAccount string `json:"employee_account"`
|
||||
Gender int8 `json:"gender"`
|
||||
Birthday string `json:"birthday"`
|
||||
AffiliateUnit string `json:"affiliate_unit"`
|
||||
AffiliateUnitName string `json:"affiliate_unit_name"`
|
||||
Department string `json:"department"`
|
||||
DepartmentName string `json:"department_name"`
|
||||
Position string `json:"position"`
|
||||
Phone string `json:"phone"`
|
||||
Email string `json:"email"`
|
||||
Education string `json:"education"`
|
||||
Nation string `json:"nation"`
|
||||
HomeAddress string `json:"home_address"`
|
||||
AccountStatus int8 `json:"account_status"`
|
||||
}
|
||||
|
||||
// efAssembleDTOList 档案列表批量组装员工展示信息。
|
||||
func (c *BackendEmployeeFileController) efAssembleDTOList(tid int, files []models.BackendEmployeeFile) []employeeFileDTO {
|
||||
list := make([]employeeFileDTO, 0, len(files))
|
||||
if len(files) == 0 {
|
||||
return list
|
||||
}
|
||||
|
||||
empIDs := make([]uint64, 0, len(files))
|
||||
for _, f := range files {
|
||||
empIDs = append(empIDs, f.EmployeeID)
|
||||
}
|
||||
var emps []models.BackendEmployee
|
||||
if _, err := efEmployeeQuery(tid).Filter("id__in", empIDs).All(&emps); err != nil {
|
||||
emps = nil
|
||||
}
|
||||
empByID := make(map[uint64]models.BackendEmployee, len(emps))
|
||||
for _, e := range emps {
|
||||
empByID[uint64(e.ID)] = e
|
||||
}
|
||||
|
||||
orgNames := c.efOrgNameMap(tid)
|
||||
|
||||
for _, f := range files {
|
||||
dto := employeeFileDTO{BackendEmployeeFile: f}
|
||||
dto.HireDate = efNormalizeDate(f.HireDate)
|
||||
dto.RegularDate = efNormalizeDate(f.RegularDate)
|
||||
dto.LeaveDate = efNormalizeDate(f.LeaveDate)
|
||||
if emp, ok := empByID[f.EmployeeID]; ok {
|
||||
birthday := ""
|
||||
if emp.Birthday != nil {
|
||||
birthday = emp.Birthday.Format("2006-01-02")
|
||||
}
|
||||
affiliate := derefString(emp.AffiliateUnit)
|
||||
department := derefString(emp.Department)
|
||||
dto.EmployeeName = emp.Name
|
||||
dto.EmployeeAccount = emp.Account
|
||||
dto.Gender = emp.Gender
|
||||
dto.Birthday = birthday
|
||||
dto.AffiliateUnit = affiliate
|
||||
dto.AffiliateUnitName = efOrgNameByID(orgNames, affiliate)
|
||||
dto.Department = department
|
||||
dto.DepartmentName = efOrgNameByID(orgNames, department)
|
||||
dto.Position = derefString(emp.Position)
|
||||
dto.Phone = derefString(emp.Phone)
|
||||
dto.Email = derefString(emp.Email)
|
||||
dto.Education = derefString(emp.Education)
|
||||
dto.Nation = derefString(emp.Nation)
|
||||
dto.HomeAddress = derefString(emp.HomeAddress)
|
||||
dto.AccountStatus = emp.AccountStatus
|
||||
}
|
||||
list = append(list, dto)
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
// List GET /backend/oa/employeefile/list
|
||||
// 分页查询档案,支持 keyword(档案编号/身份证/员工姓名/账号/手机)、
|
||||
// status(在职状态)、org_id(部门,含下级)筛选。
|
||||
func (c *BackendEmployeeFileController) List() {
|
||||
claims, err := c.efClaims()
|
||||
if err != nil {
|
||||
c.efErr(401, 401, "未登录或无权限")
|
||||
return
|
||||
}
|
||||
tid := claims.TenantId
|
||||
|
||||
page, _ := c.GetInt("page", 1)
|
||||
pageSize, _ := c.GetInt("pageSize", 10)
|
||||
keyword := strings.TrimSpace(c.GetString("keyword"))
|
||||
statusRaw := strings.TrimSpace(c.GetString("status"))
|
||||
orgID, _ := c.GetUint64("org_id")
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize < 1 || pageSize > 200 {
|
||||
pageSize = 10
|
||||
}
|
||||
|
||||
qs := c.efQuery(tid)
|
||||
|
||||
if orgID > 0 {
|
||||
empIDs := c.efEmployeeIDsInOrgs(tid, c.efSubtreeOrgIDs(tid, orgID))
|
||||
if len(empIDs) == 0 {
|
||||
c.efOk(map[string]interface{}{"list": []employeeFileDTO{}, "total": 0})
|
||||
return
|
||||
}
|
||||
qs = qs.Filter("employee_id__in", empIDs)
|
||||
}
|
||||
if statusRaw != "" {
|
||||
status, convErr := strconv.Atoi(statusRaw)
|
||||
if convErr != nil || status < 1 || status > 3 {
|
||||
c.efErr(400, 400, "在职状态参数无效")
|
||||
return
|
||||
}
|
||||
qs = qs.Filter("employment_status", int8(status))
|
||||
}
|
||||
if keyword != "" {
|
||||
// 关键词同时命中档案自身字段(编号/身份证)与员工字段(姓名/账号/手机)
|
||||
empIDs := c.efEmployeeIDsByKeyword(tid, keyword)
|
||||
cond := orm.NewCondition().
|
||||
Or("file_no__icontains", keyword).
|
||||
Or("id_card__icontains", keyword)
|
||||
if len(empIDs) > 0 {
|
||||
cond = cond.Or("employee_id__in", empIDs)
|
||||
}
|
||||
qs = qs.SetCond(orm.NewCondition().AndCond(cond))
|
||||
}
|
||||
|
||||
total, _ := qs.Count()
|
||||
|
||||
var files []models.BackendEmployeeFile
|
||||
if _, err := qs.OrderBy("-id").Limit(pageSize).Offset((page - 1) * pageSize).All(&files); err != nil && err != orm.ErrNoRows {
|
||||
c.efErr(500, 500, "查询失败")
|
||||
return
|
||||
}
|
||||
|
||||
c.efOk(map[string]interface{}{
|
||||
"list": c.efAssembleDTOList(tid, files),
|
||||
"total": total,
|
||||
})
|
||||
}
|
||||
|
||||
// Stats GET /backend/oa/employeefile/stats
|
||||
// 返回档案总数与在职状态分布,供页面顶部统计卡片展示。
|
||||
func (c *BackendEmployeeFileController) Stats() {
|
||||
claims, err := c.efClaims()
|
||||
if err != nil {
|
||||
c.efErr(401, 401, "未登录或无权限")
|
||||
return
|
||||
}
|
||||
tid := claims.TenantId
|
||||
|
||||
total, _ := c.efQuery(tid).Count()
|
||||
probation, _ := c.efQuery(tid).Filter("employment_status", 1).Count()
|
||||
active, _ := c.efQuery(tid).Filter("employment_status", 2).Count()
|
||||
resigned, _ := c.efQuery(tid).Filter("employment_status", 3).Count()
|
||||
|
||||
c.efOk(map[string]interface{}{
|
||||
"total": total,
|
||||
"probation": probation,
|
||||
"active": active,
|
||||
"resigned": resigned,
|
||||
})
|
||||
}
|
||||
|
||||
// Detail GET /backend/oa/employeefile/detail/:id
|
||||
// 返回档案详情(含员工展示信息)与全部子记录(教育/工作/合同/证照)。
|
||||
func (c *BackendEmployeeFileController) Detail() {
|
||||
claims, err := c.efClaims()
|
||||
if err != nil {
|
||||
c.efErr(401, 401, "未登录或无权限")
|
||||
return
|
||||
}
|
||||
tid := claims.TenantId
|
||||
|
||||
id, err := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
c.efErr(400, 400, "无效ID")
|
||||
return
|
||||
}
|
||||
|
||||
var file models.BackendEmployeeFile
|
||||
if err := c.efQuery(tid).Filter("id", id).One(&file); err != nil {
|
||||
c.efErr(404, 404, "档案不存在")
|
||||
return
|
||||
}
|
||||
|
||||
var records []models.BackendEmployeeFileRecord
|
||||
if _, err := models.Orm.QueryTable(new(models.BackendEmployeeFileRecord)).
|
||||
Filter("tid", tid).
|
||||
Filter("is_deleted", 0).
|
||||
Filter("employee_id", file.EmployeeID).
|
||||
OrderBy("sort", "id").
|
||||
All(&records); err != nil && err != orm.ErrNoRows {
|
||||
c.efErr(500, 500, "查询档案记录失败")
|
||||
return
|
||||
}
|
||||
if records == nil {
|
||||
records = []models.BackendEmployeeFileRecord{}
|
||||
}
|
||||
for i := range records {
|
||||
records[i].StartDate = efNormalizeDate(records[i].StartDate)
|
||||
records[i].EndDate = efNormalizeDate(records[i].EndDate)
|
||||
}
|
||||
|
||||
list := c.efAssembleDTOList(tid, []models.BackendEmployeeFile{file})
|
||||
c.efOk(map[string]interface{}{"file": list[0], "records": records})
|
||||
}
|
||||
|
||||
// employeeFilePayload 建档/编辑档案请求体。
|
||||
type employeeFilePayload struct {
|
||||
EmployeeID uint64 `json:"employee_id"`
|
||||
FileNo string `json:"file_no"`
|
||||
IDCard string `json:"id_card"`
|
||||
IDCardFront string `json:"id_card_front"`
|
||||
IDCardBack string `json:"id_card_back"`
|
||||
EducationPhoto string `json:"education_photo"`
|
||||
PoliticalStatus string `json:"political_status"`
|
||||
MaritalStatus int8 `json:"marital_status"`
|
||||
NativePlace string `json:"native_place"`
|
||||
HouseholdAddress string `json:"household_address"`
|
||||
CurrentAddress string `json:"current_address"`
|
||||
EmergencyContact string `json:"emergency_contact"`
|
||||
EmergencyPhone string `json:"emergency_phone"`
|
||||
EmergencyRelation string `json:"emergency_relation"`
|
||||
WorkEmail string `json:"work_email"`
|
||||
HireDate string `json:"hire_date"`
|
||||
RegularDate string `json:"regular_date"`
|
||||
LeaveDate string `json:"leave_date"`
|
||||
EmploymentStatus int8 `json:"employment_status"`
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
|
||||
// efParsePayload 读取并校验建档/编辑档案的请求体;校验失败时直接输出错误响应。
|
||||
func (c *BackendEmployeeFileController) efParsePayload() (employeeFilePayload, bool) {
|
||||
var payload employeeFilePayload
|
||||
raw, err := io.ReadAll(c.Ctx.Request.Body)
|
||||
if err != nil || json.Unmarshal(raw, &payload) != nil {
|
||||
c.efErr(400, 400, "参数错误")
|
||||
return payload, false
|
||||
}
|
||||
|
||||
payload.FileNo = strings.TrimSpace(payload.FileNo)
|
||||
payload.IDCard = strings.TrimSpace(payload.IDCard)
|
||||
payload.WorkEmail = strings.TrimSpace(payload.WorkEmail)
|
||||
if payload.IDCard != "" && len(payload.IDCard) > 30 {
|
||||
c.efErr(400, 400, "身份证号过长")
|
||||
return payload, false
|
||||
}
|
||||
// 证照附件地址长度限制(与列宽一致)
|
||||
for label, url := range map[string]string{
|
||||
"身份证正面": payload.IDCardFront,
|
||||
"身份证反面": payload.IDCardBack,
|
||||
"学历照片": payload.EducationPhoto,
|
||||
} {
|
||||
if len(url) > 500 {
|
||||
c.efErr(400, 400, label+"地址过长")
|
||||
return payload, false
|
||||
}
|
||||
}
|
||||
if payload.WorkEmail != "" && !strings.Contains(payload.WorkEmail, "@") {
|
||||
c.efErr(400, 400, "工作邮箱格式无效")
|
||||
return payload, false
|
||||
}
|
||||
for label, value := range map[string]string{
|
||||
"入职日期": payload.HireDate,
|
||||
"转正日期": payload.RegularDate,
|
||||
"离职日期": payload.LeaveDate,
|
||||
} {
|
||||
if !efIsValidDate(value) {
|
||||
c.efErr(400, 400, label+"格式无效")
|
||||
return payload, false
|
||||
}
|
||||
}
|
||||
if payload.EmploymentStatus < 1 || payload.EmploymentStatus > 3 {
|
||||
payload.EmploymentStatus = 2
|
||||
}
|
||||
if payload.MaritalStatus < 0 || payload.MaritalStatus > 4 {
|
||||
payload.MaritalStatus = 0
|
||||
}
|
||||
return payload, true
|
||||
}
|
||||
|
||||
// Create POST /backend/oa/employeefile/create
|
||||
// 为员工建档;一人一档,重复建档会被唯一约束与前置校验拦截。
|
||||
func (c *BackendEmployeeFileController) Create() {
|
||||
claims, err := c.efClaims()
|
||||
if err != nil {
|
||||
c.efErr(401, 401, "未登录或无权限")
|
||||
return
|
||||
}
|
||||
tid := claims.TenantId
|
||||
|
||||
payload, ok := c.efParsePayload()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if payload.EmployeeID == 0 {
|
||||
c.efErr(400, 400, "请选择员工")
|
||||
return
|
||||
}
|
||||
|
||||
// 员工必须存在
|
||||
var emp models.BackendEmployee
|
||||
if err := efEmployeeQuery(tid).Filter("id", payload.EmployeeID).One(&emp); err != nil {
|
||||
c.efErr(404, 404, "员工不存在")
|
||||
return
|
||||
}
|
||||
// 一人一档
|
||||
dup, _ := c.efQuery(tid).Filter("employee_id", payload.EmployeeID).Count()
|
||||
if dup > 0 {
|
||||
c.efErr(400, 400, "该员工已建档,请勿重复创建")
|
||||
return
|
||||
}
|
||||
|
||||
// 档案编号留空自动生成:EF + 时间戳
|
||||
fileNo := payload.FileNo
|
||||
if fileNo == "" {
|
||||
fileNo = "EF" + time.Now().Format("20060102150405")
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
item := &models.BackendEmployeeFile{
|
||||
Tid: tid,
|
||||
EmployeeID: payload.EmployeeID,
|
||||
FileNo: fileNo,
|
||||
IDCard: payload.IDCard,
|
||||
IDCardFront: payload.IDCardFront,
|
||||
IDCardBack: payload.IDCardBack,
|
||||
EducationPhoto: payload.EducationPhoto,
|
||||
PoliticalStatus: payload.PoliticalStatus,
|
||||
MaritalStatus: payload.MaritalStatus,
|
||||
NativePlace: payload.NativePlace,
|
||||
HouseholdAddress: payload.HouseholdAddress,
|
||||
CurrentAddress: payload.CurrentAddress,
|
||||
EmergencyContact: payload.EmergencyContact,
|
||||
EmergencyPhone: payload.EmergencyPhone,
|
||||
EmergencyRelation: payload.EmergencyRelation,
|
||||
WorkEmail: payload.WorkEmail,
|
||||
HireDate: strings.TrimSpace(payload.HireDate),
|
||||
RegularDate: strings.TrimSpace(payload.RegularDate),
|
||||
LeaveDate: strings.TrimSpace(payload.LeaveDate),
|
||||
EmploymentStatus: payload.EmploymentStatus,
|
||||
Remark: payload.Remark,
|
||||
IsDeleted: 0,
|
||||
CreateTime: now,
|
||||
UpdateTime: &now,
|
||||
}
|
||||
if _, err := models.Orm.Insert(item); err != nil {
|
||||
c.efErr(500, 500, "保存失败")
|
||||
return
|
||||
}
|
||||
|
||||
c.efOk(item)
|
||||
}
|
||||
|
||||
// Update POST /backend/oa/employeefile/update/:id
|
||||
// 更新档案基本资料;员工归属(employee_id)不允许修改。
|
||||
func (c *BackendEmployeeFileController) Update() {
|
||||
claims, err := c.efClaims()
|
||||
if err != nil {
|
||||
c.efErr(401, 401, "未登录或无权限")
|
||||
return
|
||||
}
|
||||
tid := claims.TenantId
|
||||
|
||||
id, err := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
c.efErr(400, 400, "无效ID")
|
||||
return
|
||||
}
|
||||
|
||||
payload, ok := c.efParsePayload()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
var item models.BackendEmployeeFile
|
||||
if err := c.efQuery(tid).Filter("id", id).One(&item); err != nil {
|
||||
c.efErr(404, 404, "档案不存在")
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
item.FileNo = payload.FileNo
|
||||
item.IDCard = payload.IDCard
|
||||
item.IDCardFront = payload.IDCardFront
|
||||
item.IDCardBack = payload.IDCardBack
|
||||
item.EducationPhoto = payload.EducationPhoto
|
||||
item.PoliticalStatus = payload.PoliticalStatus
|
||||
item.MaritalStatus = payload.MaritalStatus
|
||||
item.NativePlace = payload.NativePlace
|
||||
item.HouseholdAddress = payload.HouseholdAddress
|
||||
item.CurrentAddress = payload.CurrentAddress
|
||||
item.EmergencyContact = payload.EmergencyContact
|
||||
item.EmergencyPhone = payload.EmergencyPhone
|
||||
item.EmergencyRelation = payload.EmergencyRelation
|
||||
item.WorkEmail = payload.WorkEmail
|
||||
item.HireDate = strings.TrimSpace(payload.HireDate)
|
||||
item.RegularDate = strings.TrimSpace(payload.RegularDate)
|
||||
item.LeaveDate = strings.TrimSpace(payload.LeaveDate)
|
||||
item.EmploymentStatus = payload.EmploymentStatus
|
||||
item.Remark = payload.Remark
|
||||
item.UpdateTime = &now
|
||||
|
||||
if _, err := models.Orm.Update(&item,
|
||||
"FileNo", "IDCard", "IDCardFront", "IDCardBack", "EducationPhoto",
|
||||
"PoliticalStatus", "MaritalStatus", "NativePlace",
|
||||
"HouseholdAddress", "CurrentAddress", "EmergencyContact", "EmergencyPhone",
|
||||
"EmergencyRelation", "WorkEmail", "HireDate", "RegularDate", "LeaveDate",
|
||||
"EmploymentStatus", "Remark", "UpdateTime"); err != nil {
|
||||
c.efErr(500, 500, "保存失败")
|
||||
return
|
||||
}
|
||||
|
||||
c.efOk(item)
|
||||
}
|
||||
|
||||
// Delete DELETE /backend/oa/employeefile/delete/:id 软删除档案。
|
||||
func (c *BackendEmployeeFileController) Delete() {
|
||||
claims, err := c.efClaims()
|
||||
if err != nil {
|
||||
c.efErr(401, 401, "未登录或无权限")
|
||||
return
|
||||
}
|
||||
tid := claims.TenantId
|
||||
|
||||
id, err := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
c.efErr(400, 400, "无效ID")
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
n, err := c.efQuery(tid).Filter("id", id).
|
||||
Update(map[string]interface{}{
|
||||
"IsDeleted": int8(1),
|
||||
"DeleteTime": now,
|
||||
"UpdateTime": now,
|
||||
})
|
||||
if err != nil || n == 0 {
|
||||
c.efErr(404, 404, "档案不存在或已删除")
|
||||
return
|
||||
}
|
||||
|
||||
c.efOk(nil)
|
||||
}
|
||||
|
||||
// FiledEmployees GET /backend/oa/employeefile/filedEmployees
|
||||
// 返回已建档的员工ID列表,前端用于过滤出可建档(未建档)员工。
|
||||
func (c *BackendEmployeeFileController) FiledEmployees() {
|
||||
claims, err := c.efClaims()
|
||||
if err != nil {
|
||||
c.efErr(401, 401, "未登录或无权限")
|
||||
return
|
||||
}
|
||||
tid := claims.TenantId
|
||||
|
||||
var files []models.BackendEmployeeFile
|
||||
if _, err := c.efQuery(tid).All(&files, "EmployeeID"); err != nil {
|
||||
c.efErr(500, 500, "查询失败")
|
||||
return
|
||||
}
|
||||
ids := make([]uint64, 0, len(files))
|
||||
for _, f := range files {
|
||||
ids = append(ids, f.EmployeeID)
|
||||
}
|
||||
c.efOk(ids)
|
||||
}
|
||||
|
||||
// employeeFileRecordPayload 档案子记录请求体。
|
||||
// type: 1-教育经历 2-工作经历 3-合同信息 4-证照附件。
|
||||
type employeeFileRecordPayload struct {
|
||||
EmployeeID uint64 `json:"employee_id"`
|
||||
Type int8 `json:"type"`
|
||||
Title string `json:"title"`
|
||||
SubTitle string `json:"sub_title"`
|
||||
Extra string `json:"extra"`
|
||||
StartDate string `json:"start_date"`
|
||||
EndDate string `json:"end_date"`
|
||||
Description string `json:"description"`
|
||||
AttachmentURL string `json:"attachment_url"`
|
||||
Sort int `json:"sort"`
|
||||
}
|
||||
|
||||
// efParseRecordPayload 读取并校验子记录请求体;校验失败时直接输出错误响应。
|
||||
func (c *BackendEmployeeFileController) efParseRecordPayload() (employeeFileRecordPayload, bool) {
|
||||
var payload employeeFileRecordPayload
|
||||
raw, err := io.ReadAll(c.Ctx.Request.Body)
|
||||
if err != nil || json.Unmarshal(raw, &payload) != nil {
|
||||
c.efErr(400, 400, "参数错误")
|
||||
return payload, false
|
||||
}
|
||||
|
||||
if payload.EmployeeID == 0 {
|
||||
c.efErr(400, 400, "参数缺少员工")
|
||||
return payload, false
|
||||
}
|
||||
if payload.Type < 1 || payload.Type > 4 {
|
||||
c.efErr(400, 400, "记录类型无效")
|
||||
return payload, false
|
||||
}
|
||||
payload.Title = strings.TrimSpace(payload.Title)
|
||||
if payload.Title == "" {
|
||||
c.efErr(400, 400, "请输入名称")
|
||||
return payload, false
|
||||
}
|
||||
if len(payload.Title) > 100 {
|
||||
c.efErr(400, 400, "名称不能超过100字")
|
||||
return payload, false
|
||||
}
|
||||
payload.StartDate = strings.TrimSpace(payload.StartDate)
|
||||
payload.EndDate = strings.TrimSpace(payload.EndDate)
|
||||
if !efIsValidDate(payload.StartDate) || !efIsValidDate(payload.EndDate) {
|
||||
c.efErr(400, 400, "日期格式无效")
|
||||
return payload, false
|
||||
}
|
||||
if payload.StartDate != "" && payload.EndDate != "" && payload.EndDate < payload.StartDate {
|
||||
c.efErr(400, 400, "结束日期不能早于开始日期")
|
||||
return payload, false
|
||||
}
|
||||
if len(payload.AttachmentURL) > 500 {
|
||||
c.efErr(400, 400, "附件地址过长")
|
||||
return payload, false
|
||||
}
|
||||
return payload, true
|
||||
}
|
||||
|
||||
// CreateRecord POST /backend/oa/employeefile/record/create 新增子记录。
|
||||
func (c *BackendEmployeeFileController) CreateRecord() {
|
||||
claims, err := c.efClaims()
|
||||
if err != nil {
|
||||
c.efErr(401, 401, "未登录或无权限")
|
||||
return
|
||||
}
|
||||
tid := claims.TenantId
|
||||
|
||||
payload, ok := c.efParseRecordPayload()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// 档案必须存在(记录挂在员工上,但以建档为前提)
|
||||
exists, _ := c.efQuery(tid).Filter("employee_id", payload.EmployeeID).Count()
|
||||
if exists == 0 {
|
||||
c.efErr(404, 404, "档案不存在,请先建档")
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
item := &models.BackendEmployeeFileRecord{
|
||||
Tid: tid,
|
||||
EmployeeID: payload.EmployeeID,
|
||||
Type: payload.Type,
|
||||
Title: payload.Title,
|
||||
SubTitle: payload.SubTitle,
|
||||
Extra: payload.Extra,
|
||||
StartDate: payload.StartDate,
|
||||
EndDate: payload.EndDate,
|
||||
Description: payload.Description,
|
||||
AttachmentURL: payload.AttachmentURL,
|
||||
Sort: payload.Sort,
|
||||
IsDeleted: 0,
|
||||
CreateTime: now,
|
||||
UpdateTime: &now,
|
||||
}
|
||||
if _, err := models.Orm.Insert(item); err != nil {
|
||||
c.efErr(500, 500, "保存失败")
|
||||
return
|
||||
}
|
||||
|
||||
c.efOk(item)
|
||||
}
|
||||
|
||||
// UpdateRecord POST /backend/oa/employeefile/record/update/:id 更新子记录。
|
||||
func (c *BackendEmployeeFileController) UpdateRecord() {
|
||||
claims, err := c.efClaims()
|
||||
if err != nil {
|
||||
c.efErr(401, 401, "未登录或无权限")
|
||||
return
|
||||
}
|
||||
tid := claims.TenantId
|
||||
|
||||
id, err := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
c.efErr(400, 400, "无效ID")
|
||||
return
|
||||
}
|
||||
|
||||
payload, ok := c.efParseRecordPayload()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
var item models.BackendEmployeeFileRecord
|
||||
if err := models.Orm.QueryTable(new(models.BackendEmployeeFileRecord)).
|
||||
Filter("tid", tid).
|
||||
Filter("is_deleted", 0).
|
||||
Filter("id", id).
|
||||
One(&item); err != nil {
|
||||
c.efErr(404, 404, "记录不存在")
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
item.Type = payload.Type
|
||||
item.Title = payload.Title
|
||||
item.SubTitle = payload.SubTitle
|
||||
item.Extra = payload.Extra
|
||||
item.StartDate = payload.StartDate
|
||||
item.EndDate = payload.EndDate
|
||||
item.Description = payload.Description
|
||||
item.AttachmentURL = payload.AttachmentURL
|
||||
item.Sort = payload.Sort
|
||||
item.UpdateTime = &now
|
||||
|
||||
if _, err := models.Orm.Update(&item,
|
||||
"Type", "Title", "SubTitle", "Extra", "StartDate", "EndDate",
|
||||
"Description", "AttachmentURL", "Sort", "UpdateTime"); err != nil {
|
||||
c.efErr(500, 500, "保存失败")
|
||||
return
|
||||
}
|
||||
|
||||
c.efOk(item)
|
||||
}
|
||||
|
||||
// DeleteRecord DELETE /backend/oa/employeefile/record/delete/:id 软删除子记录。
|
||||
func (c *BackendEmployeeFileController) DeleteRecord() {
|
||||
claims, err := c.efClaims()
|
||||
if err != nil {
|
||||
c.efErr(401, 401, "未登录或无权限")
|
||||
return
|
||||
}
|
||||
tid := claims.TenantId
|
||||
|
||||
id, err := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
c.efErr(400, 400, "无效ID")
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
n, err := models.Orm.QueryTable(new(models.BackendEmployeeFileRecord)).
|
||||
Filter("tid", tid).
|
||||
Filter("is_deleted", 0).
|
||||
Filter("id", id).
|
||||
Update(map[string]interface{}{
|
||||
"IsDeleted": int8(1),
|
||||
"DeleteTime": now,
|
||||
"UpdateTime": now,
|
||||
})
|
||||
if err != nil || n == 0 {
|
||||
c.efErr(404, 404, "记录不存在或已删除")
|
||||
return
|
||||
}
|
||||
|
||||
c.efOk(nil)
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
// BackendEmployeeFile 员工档案主表: yz_backend_employee_file
|
||||
// 档案与员工(yz_backend_employee)按 tid + employee_id 一对一,
|
||||
// 供 OA 员工档案页面管理人事资料。日期字段与 OA 日程一致,
|
||||
// 以 "YYYY-MM-DD" 字符串存取,规避 parseTime 的时区换算问题。
|
||||
type BackendEmployeeFile struct {
|
||||
ID uint64 `orm:"column(id);pk;auto" json:"id"`
|
||||
Tid int `orm:"column(tid)" json:"tid"`
|
||||
EmployeeID uint64 `orm:"column(employee_id)" json:"employee_id"`
|
||||
FileNo string `orm:"column(file_no);size(50);default()" json:"file_no"`
|
||||
IDCard string `orm:"column(id_card);size(30);default()" json:"id_card"`
|
||||
IDCardFront string `orm:"column(id_card_front);size(500);default()" json:"id_card_front"`
|
||||
IDCardBack string `orm:"column(id_card_back);size(500);default()" json:"id_card_back"`
|
||||
EducationPhoto string `orm:"column(education_photo);size(500);default()" json:"education_photo"`
|
||||
PoliticalStatus string `orm:"column(political_status);size(30);default()" json:"political_status"`
|
||||
MaritalStatus int8 `orm:"column(marital_status);default(0)" json:"marital_status"`
|
||||
NativePlace string `orm:"column(native_place);size(100);default()" json:"native_place"`
|
||||
HouseholdAddress string `orm:"column(household_address);size(255);default()" json:"household_address"`
|
||||
CurrentAddress string `orm:"column(current_address);size(255);default()" json:"current_address"`
|
||||
EmergencyContact string `orm:"column(emergency_contact);size(50);default()" json:"emergency_contact"`
|
||||
EmergencyPhone string `orm:"column(emergency_phone);size(20);default()" json:"emergency_phone"`
|
||||
EmergencyRelation string `orm:"column(emergency_relation);size(30);default()" json:"emergency_relation"`
|
||||
WorkEmail string `orm:"column(work_email);size(100);default()" json:"work_email"`
|
||||
HireDate string `orm:"column(hire_date);type(date);null" json:"hire_date"`
|
||||
RegularDate string `orm:"column(regular_date);type(date);null" json:"regular_date"`
|
||||
LeaveDate string `orm:"column(leave_date);type(date);null" json:"leave_date"`
|
||||
EmploymentStatus int8 `orm:"column(employment_status);default(2)" json:"employment_status"`
|
||||
Remark string `orm:"column(remark);type(text);null" json:"remark"`
|
||||
IsDeleted int8 `orm:"column(is_deleted);default(0)" json:"is_deleted"`
|
||||
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
|
||||
UpdateTime *time.Time `orm:"column(update_time);auto_now;type(datetime);null" json:"update_time"`
|
||||
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"`
|
||||
}
|
||||
|
||||
func (m *BackendEmployeeFile) TableName() string {
|
||||
return "yz_backend_employee_file"
|
||||
}
|
||||
|
||||
// BackendEmployeeFileRecord 员工档案子记录表: yz_backend_employee_file_record
|
||||
// 用 type 区分四类记录:1-教育经历 2-工作经历 3-合同信息 4-证照附件。
|
||||
// title/sub_title/extra 的语义随类型变化(见建表脚本注释),
|
||||
// 避免为每类记录单独建表。
|
||||
type BackendEmployeeFileRecord struct {
|
||||
ID uint64 `orm:"column(id);pk;auto" json:"id"`
|
||||
Tid int `orm:"column(tid)" json:"tid"`
|
||||
EmployeeID uint64 `orm:"column(employee_id)" json:"employee_id"`
|
||||
Type int8 `orm:"column(type);default(1)" json:"type"`
|
||||
Title string `orm:"column(title);size(100);default()" json:"title"`
|
||||
SubTitle string `orm:"column(sub_title);size(100);default()" json:"sub_title"`
|
||||
Extra string `orm:"column(extra);size(100);default()" json:"extra"`
|
||||
StartDate string `orm:"column(start_date);type(date);null" json:"start_date"`
|
||||
EndDate string `orm:"column(end_date);type(date);null" json:"end_date"`
|
||||
Description string `orm:"column(description);type(text);null" json:"description"`
|
||||
AttachmentURL string `orm:"column(attachment_url);size(500);default()" json:"attachment_url"`
|
||||
Sort int `orm:"column(sort);default(0)" json:"sort"`
|
||||
IsDeleted int8 `orm:"column(is_deleted);default(0)" json:"is_deleted"`
|
||||
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
|
||||
UpdateTime *time.Time `orm:"column(update_time);auto_now;type(datetime);null" json:"update_time"`
|
||||
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"`
|
||||
}
|
||||
|
||||
func (m *BackendEmployeeFileRecord) TableName() string {
|
||||
return "yz_backend_employee_file_record"
|
||||
}
|
||||
@@ -101,6 +101,8 @@ func Init(_ string) {
|
||||
new(BackendScheduleReminderSendLog),
|
||||
|
||||
new(OaSchedule),
|
||||
new(BackendEmployeeFile),
|
||||
new(BackendEmployeeFileRecord),
|
||||
)
|
||||
|
||||
// 创建全局 Ormer
|
||||
|
||||
@@ -102,6 +102,18 @@ func RegisterAuthRoutes() {
|
||||
registerOrganizationRoutes("erp")
|
||||
registerOrganizationRoutes("oa")
|
||||
|
||||
// 员工档案(OA 人事档案:一人一档 + 教育/工作/合同/证照子记录)
|
||||
beego.Router("/backend/oa/employeefile/list", &controllers.BackendEmployeeFileController{}, "get:List")
|
||||
beego.Router("/backend/oa/employeefile/stats", &controllers.BackendEmployeeFileController{}, "get:Stats")
|
||||
beego.Router("/backend/oa/employeefile/detail/:id", &controllers.BackendEmployeeFileController{}, "get:Detail")
|
||||
beego.Router("/backend/oa/employeefile/create", &controllers.BackendEmployeeFileController{}, "post:Create")
|
||||
beego.Router("/backend/oa/employeefile/update/:id", &controllers.BackendEmployeeFileController{}, "post:Update")
|
||||
beego.Router("/backend/oa/employeefile/delete/:id", &controllers.BackendEmployeeFileController{}, "delete:Delete")
|
||||
beego.Router("/backend/oa/employeefile/filedEmployees", &controllers.BackendEmployeeFileController{}, "get:FiledEmployees")
|
||||
beego.Router("/backend/oa/employeefile/record/create", &controllers.BackendEmployeeFileController{}, "post:CreateRecord")
|
||||
beego.Router("/backend/oa/employeefile/record/update/:id", &controllers.BackendEmployeeFileController{}, "post:UpdateRecord")
|
||||
beego.Router("/backend/oa/employeefile/record/delete/:id", &controllers.BackendEmployeeFileController{}, "delete:DeleteRecord")
|
||||
|
||||
// 通讯录管理(与组织架构联动)
|
||||
beego.Router("/backend/erp/contact/list", &controllers.BackendErpContactController{}, "get:List")
|
||||
beego.Router("/backend/erp/contact/detail/:id", &controllers.BackendErpContactController{}, "get:Detail")
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
-- 员工档案主表增加证照字段:身份证正反面、学历照片。
|
||||
-- 仅在已执行过 yz_backend_employee_file.sql 建表时执行本脚本;
|
||||
-- 未建表的库直接执行最新版 yz_backend_employee_file.sql 即可,无需本脚本。
|
||||
|
||||
ALTER TABLE `yz_backend_employee_file`
|
||||
ADD COLUMN `id_card_front` varchar(500) NOT NULL DEFAULT '' COMMENT '身份证正面(人像面)图片URL' AFTER `id_card`,
|
||||
ADD COLUMN `id_card_back` varchar(500) NOT NULL DEFAULT '' COMMENT '身份证反面(国徽面)图片URL' AFTER `id_card_front`,
|
||||
ADD COLUMN `education_photo` varchar(500) NOT NULL DEFAULT '' COMMENT '学历照片URL' AFTER `id_card_back`;
|
||||
@@ -0,0 +1,27 @@
|
||||
-- 员工档案菜单(租户端):挂在办公自动化模块目录下。
|
||||
--
|
||||
-- 前置:yz_system_menu 中已存在 path 为 /apps/oa 的模块目录菜单;
|
||||
-- 若不存在,脚本会自动创建。
|
||||
-- views 字段:[2] 租户端。type 字段:2-页面。
|
||||
--
|
||||
-- 脚本可重复执行:按 path 判重,已存在的菜单会更新组件路径与标题。
|
||||
|
||||
INSERT INTO `yz_system_menu` (`pid`, `title`, `path`, `component_path`, `icon`, `sort`, `status`, `is_visible`, `views`, `type`, `remark`)
|
||||
SELECT 0, '办公自动化', '/apps/oa', '', 'Document', 31, 1, 1, '[2]', 1, '办公自动化模块'
|
||||
FROM DUAL
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM (SELECT * FROM `yz_system_menu`) AS t WHERE t.`path` = '/apps/oa'
|
||||
);
|
||||
|
||||
SET @oa_pid := (SELECT `id` FROM `yz_system_menu` WHERE `path` = '/apps/oa' ORDER BY `id` LIMIT 1);
|
||||
|
||||
INSERT INTO `yz_system_menu` (`pid`, `title`, `path`, `component_path`, `icon`, `sort`, `status`, `is_visible`, `views`, `type`, `remark`)
|
||||
SELECT @oa_pid, '员工档案', '/apps/oa/employeefile', '/apps/oa/employeefile/index.vue', 'Postcard', 4, 1, 1, '[2]', 2, '一人一档的人事档案管理'
|
||||
FROM DUAL
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM (SELECT * FROM `yz_system_menu`) AS t WHERE t.`path` = '/apps/oa/employeefile'
|
||||
);
|
||||
|
||||
-- 修正已有菜单的组件路径(历史数据可能指向旧组件)
|
||||
UPDATE `yz_system_menu` SET `component_path` = '/apps/oa/employeefile/index.vue', `type` = 2, `views` = '[2]'
|
||||
WHERE `path` = '/apps/oa/employeefile';
|
||||
@@ -0,0 +1,70 @@
|
||||
-- 员工档案表(租户端 OA 人事档案)
|
||||
-- 在租户业务库执行以下语句创建表结构。
|
||||
-- 数据按租户 tid 隔离;档案与员工(yz_backend_employee)一对一,
|
||||
-- 子记录表统一存放教育经历 / 工作经历 / 合同信息 / 证照附件。
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 1. 员工档案主表
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
CREATE TABLE `yz_backend_employee_file` (
|
||||
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`tid` int NOT NULL DEFAULT '0' COMMENT '租户ID',
|
||||
`employee_id` bigint unsigned NOT NULL DEFAULT '0' COMMENT '员工ID(yz_backend_employee.id)',
|
||||
`file_no` varchar(50) NOT NULL DEFAULT '' COMMENT '档案编号(留空自动生成EF+时间戳)',
|
||||
`id_card` varchar(30) NOT NULL DEFAULT '' COMMENT '身份证号',
|
||||
`id_card_front` varchar(500) NOT NULL DEFAULT '' COMMENT '身份证正面(人像面)图片URL',
|
||||
`id_card_back` varchar(500) NOT NULL DEFAULT '' COMMENT '身份证反面(国徽面)图片URL',
|
||||
`education_photo` varchar(500) NOT NULL DEFAULT '' COMMENT '学历照片URL',
|
||||
`political_status` varchar(30) NOT NULL DEFAULT '' COMMENT '政治面貌',
|
||||
`marital_status` tinyint NOT NULL DEFAULT '0' COMMENT '婚姻状况 0-未知 1-未婚 2-已婚 3-离异 4-丧偶',
|
||||
`native_place` varchar(100) NOT NULL DEFAULT '' COMMENT '籍贯',
|
||||
`household_address` varchar(255) NOT NULL DEFAULT '' COMMENT '户籍地址',
|
||||
`current_address` varchar(255) NOT NULL DEFAULT '' COMMENT '现居住址',
|
||||
`emergency_contact` varchar(50) NOT NULL DEFAULT '' COMMENT '紧急联系人',
|
||||
`emergency_phone` varchar(20) NOT NULL DEFAULT '' COMMENT '紧急联系电话',
|
||||
`emergency_relation` varchar(30) NOT NULL DEFAULT '' COMMENT '与本人关系',
|
||||
`work_email` varchar(100) NOT NULL DEFAULT '' COMMENT '工作邮箱',
|
||||
`hire_date` date DEFAULT NULL COMMENT '入职日期',
|
||||
`regular_date` date DEFAULT NULL COMMENT '转正日期',
|
||||
`leave_date` date DEFAULT NULL COMMENT '离职日期',
|
||||
`employment_status` tinyint NOT NULL DEFAULT '2' COMMENT '在职状态 1-试用 2-正式 3-离职',
|
||||
`remark` text NULL COMMENT '备注',
|
||||
`is_deleted` tinyint NOT NULL DEFAULT '0' COMMENT '是否删除 0-否 1-是',
|
||||
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
|
||||
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
|
||||
`delete_time` datetime DEFAULT NULL COMMENT '删除时间',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uniq_tid_employee` (`tid`,`employee_id`),
|
||||
KEY `idx_file_no` (`file_no`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='员工档案主表';
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 2. 员工档案子记录表(教育经历/工作经历/合同信息/证照附件)
|
||||
-- type 字段区分记录类型,title/sub_title/extra 的语义随类型变化:
|
||||
-- 教育经历(1):title=学校 sub_title=专业 extra=学历
|
||||
-- 工作经历(2):title=公司 sub_title=职位 extra=所在部门
|
||||
-- 合同信息(3):title=合同名称 sub_title=合同类型 extra=签订主体
|
||||
-- 证照附件(4):title=证照名称 sub_title=证件号码 extra=''
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
CREATE TABLE `yz_backend_employee_file_record` (
|
||||
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`tid` int NOT NULL DEFAULT '0' COMMENT '租户ID',
|
||||
`employee_id` bigint unsigned NOT NULL DEFAULT '0' COMMENT '员工ID(yz_backend_employee.id)',
|
||||
`type` tinyint NOT NULL DEFAULT '1' COMMENT '记录类型 1-教育经历 2-工作经历 3-合同信息 4-证照附件',
|
||||
`title` varchar(100) NOT NULL DEFAULT '' COMMENT '名称(学校/公司/合同/证照)',
|
||||
`sub_title` varchar(100) NOT NULL DEFAULT '' COMMENT '次级信息(专业/职位/合同类型/证件号码)',
|
||||
`extra` varchar(100) NOT NULL DEFAULT '' COMMENT '附加信息(学历/部门/签订主体)',
|
||||
`start_date` date DEFAULT NULL COMMENT '开始日期',
|
||||
`end_date` date DEFAULT NULL COMMENT '结束日期',
|
||||
`description` text NULL COMMENT '描述',
|
||||
`attachment_url` varchar(500) NOT NULL DEFAULT '' COMMENT '附件地址(证照图片等)',
|
||||
`sort` int NOT NULL DEFAULT '0' COMMENT '排序(越小越靠前)',
|
||||
`is_deleted` tinyint NOT NULL DEFAULT '0' COMMENT '是否删除 0-否 1-是',
|
||||
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
|
||||
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
|
||||
`delete_time` datetime DEFAULT NULL COMMENT '删除时间',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_tid_employee_type` (`tid`,`employee_id`,`type`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='员工档案子记录表';
|
||||
@@ -1,24 +0,0 @@
|
||||
FAIL _/E_/Demos/DemoOwns/Go/yunzerwebsiteallinone/go [setup failed]
|
||||
FAIL _/E_/Demos/DemoOwns/Go/yunzerwebsiteallinone/go/controllers [setup failed]
|
||||
FAIL _/E_/Demos/DemoOwns/Go/yunzerwebsiteallinone/go/middleware [setup failed]
|
||||
FAIL _/E_/Demos/DemoOwns/Go/yunzerwebsiteallinone/go/models [setup failed]
|
||||
FAIL _/E_/Demos/DemoOwns/Go/yunzerwebsiteallinone/go/pkg/jwtutil [setup failed]
|
||||
FAIL _/E_/Demos/DemoOwns/Go/yunzerwebsiteallinone/go/pkg/tokenprobe [setup failed]
|
||||
FAIL _/E_/Demos/DemoOwns/Go/yunzerwebsiteallinone/go/routers [setup failed]
|
||||
FAIL _/E_/Demos/DemoOwns/Go/yunzerwebsiteallinone/go/routers/api [setup failed]
|
||||
FAIL _/E_/Demos/DemoOwns/Go/yunzerwebsiteallinone/go/routers/app [setup failed]
|
||||
FAIL _/E_/Demos/DemoOwns/Go/yunzerwebsiteallinone/go/routers/backend [setup failed]
|
||||
FAIL _/E_/Demos/DemoOwns/Go/yunzerwebsiteallinone/go/routers/index [setup failed]
|
||||
FAIL _/E_/Demos/DemoOwns/Go/yunzerwebsiteallinone/go/routers/platform [setup failed]
|
||||
FAIL _/E_/Demos/DemoOwns/Go/yunzerwebsiteallinone/go/services [setup failed]
|
||||
FAIL backend [setup failed]
|
||||
FAIL docs [setup failed]
|
||||
FAIL frontend [setup failed]
|
||||
FAIL go [setup failed]
|
||||
FAIL platform [setup failed]
|
||||
FAIL sql [setup failed]
|
||||
FAIL uniapp [setup failed]
|
||||
? _/E_/Demos/DemoOwns/Go/yunzerwebsiteallinone/go/pkg/passwordutil [no test files]
|
||||
? _/E_/Demos/DemoOwns/Go/yunzerwebsiteallinone/go/pkg/versionutil [no test files]
|
||||
? _/E_/Demos/DemoOwns/Go/yunzerwebsiteallinone/go/version [no test files]
|
||||
FAIL
|
||||
Reference in New Issue
Block a user