Merge branch 'master' of https://git.yunzer.cn/yunzerwebsite/yunzerwebsiteallinone
This commit is contained in:
@@ -1 +1 @@
|
|||||||
{"pid":26096,"startedAt":1787887230707}
|
{"pid":26648,"startedAt":1787847660038}
|
||||||
Vendored
+1
@@ -25,6 +25,7 @@ declare module 'vue' {
|
|||||||
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
|
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
|
||||||
ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
|
ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
|
||||||
ElCol: typeof import('element-plus/es')['ElCol']
|
ElCol: typeof import('element-plus/es')['ElCol']
|
||||||
|
ElCollapseTransition: typeof import('element-plus/es')['ElCollapseTransition']
|
||||||
ElColorPicker: typeof import('element-plus/es')['ElColorPicker']
|
ElColorPicker: typeof import('element-plus/es')['ElColorPicker']
|
||||||
ElContainer: typeof import('element-plus/es')['ElContainer']
|
ElContainer: typeof import('element-plus/es')['ElContainer']
|
||||||
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
|
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
|
||||||
|
|||||||
@@ -1,155 +0,0 @@
|
|||||||
import request from "@/utils/request";
|
|
||||||
|
|
||||||
/*************************************************
|
|
||||||
****************** 组织机构相关接口 ******************
|
|
||||||
*************************************************/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取组织机构列表
|
|
||||||
* @returns {Promise}
|
|
||||||
*/
|
|
||||||
export function getOrganizationList() {
|
|
||||||
return request({
|
|
||||||
url: '/backend/erp/getOrganization',
|
|
||||||
method: 'get'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取组织机构详情
|
|
||||||
* @param {number} id 组织机构ID
|
|
||||||
* @returns {Promise}
|
|
||||||
*/
|
|
||||||
export function getOrganizationDetail(id) {
|
|
||||||
return request({
|
|
||||||
url: `/backend/erp/getOrganizationDetail/${id}`,
|
|
||||||
method: "get",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建组织机构数据
|
|
||||||
* @param {Object} data 组织机构数据
|
|
||||||
* @returns {Promise}
|
|
||||||
*/
|
|
||||||
export function createOrganization(data) {
|
|
||||||
return request({
|
|
||||||
url: "/backend/erp/createOrganization",
|
|
||||||
method: "post",
|
|
||||||
data: data,
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "multipart/form-data"
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新组织机构信息
|
|
||||||
export function editOrganization(id, data) {
|
|
||||||
return request({
|
|
||||||
url: `/backend/erp/editOrganization/${id}`,
|
|
||||||
method: 'post',
|
|
||||||
data: data
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 删除组织机构数据
|
|
||||||
* @param {number} id 组织机构ID
|
|
||||||
* @returns {Promise}
|
|
||||||
*/
|
|
||||||
export function deleteOrganization(id) {
|
|
||||||
return request({
|
|
||||||
url: `/backend/erp/deleteOrganization/${id}`,
|
|
||||||
method: "delete",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取企业单位列表
|
|
||||||
* @returns {Promise}
|
|
||||||
*/
|
|
||||||
export function getCompanys() {
|
|
||||||
return request({
|
|
||||||
url: '/backend/erp/getCompanys',
|
|
||||||
method: 'get'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取部门列表
|
|
||||||
* @param {number} parentId 隶属单位ID
|
|
||||||
* @returns {Promise}
|
|
||||||
*/
|
|
||||||
export function getDepartments(parentId) {
|
|
||||||
return request({
|
|
||||||
url: '/backend/erp/getDepartments',
|
|
||||||
method: 'get',
|
|
||||||
params: parentId ? { parent_id: parentId } : {}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/*************************************************
|
|
||||||
****************** 员工相关接口 ******************
|
|
||||||
*************************************************/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取员工列表
|
|
||||||
* @param {number} tenantId 租户ID
|
|
||||||
* @returns {Promise}
|
|
||||||
*/
|
|
||||||
export function getEmployeeList(tenantId) {
|
|
||||||
return request({
|
|
||||||
url: '/backend/erp/getEmployee',
|
|
||||||
method: 'get',
|
|
||||||
params: { tid: tenantId }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取员工详情
|
|
||||||
* @param {number} id 员工ID
|
|
||||||
* @returns {Promise}
|
|
||||||
*/
|
|
||||||
export function getEmployeeDetail(id) {
|
|
||||||
return request({
|
|
||||||
url: `/backend/erp/getEmployeeDetail/${id}`,
|
|
||||||
method: "get",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建员工数据
|
|
||||||
* @param {Object} data 员工数据
|
|
||||||
* @returns {Promise}
|
|
||||||
*/
|
|
||||||
export function createEmployee(data) {
|
|
||||||
return request({
|
|
||||||
url: "/backend/erp/createEmployee",
|
|
||||||
method: "post",
|
|
||||||
data: data,
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "multipart/form-data"
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新员工信息
|
|
||||||
export function editEmployee(id, data) {
|
|
||||||
return request({
|
|
||||||
url: `/backend/erp/editEmployee/${id}`,
|
|
||||||
method: 'post',
|
|
||||||
data: data
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 删除员工数据
|
|
||||||
* @param {number} id 员工ID
|
|
||||||
* @returns {Promise}
|
|
||||||
*/
|
|
||||||
export function deleteEmployee(id) {
|
|
||||||
return request({
|
|
||||||
url: `/backend/erp/deleteEmployee/${id}`,
|
|
||||||
method: "delete",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
+13
-8
@@ -100,22 +100,27 @@ export function getFileById(id) {
|
|||||||
* 上传文件
|
* 上传文件
|
||||||
* @param {FormData} formData 文件数据
|
* @param {FormData} formData 文件数据
|
||||||
* @param {Object} options 额外选项
|
* @param {Object} options 额外选项
|
||||||
* @param {string} [options.cate]
|
* @param {string|number} [options.cate] 文件分组ID,0 为未分类
|
||||||
|
* @param {(e: { loaded: number; total?: number }) => void} [options.onUploadProgress] 上传进度
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
export function uploadFile(formData, options = {}) {
|
export function uploadFile(formData, options = {}) {
|
||||||
if (options.cate) {
|
// 0 表示「未分类」,不能用 truthy 判断
|
||||||
formData.append('cate', options.cate);
|
if (options.cate !== undefined && options.cate !== null && options.cate !== "") {
|
||||||
|
formData.append("cate", String(options.cate));
|
||||||
}
|
}
|
||||||
|
|
||||||
return request({
|
const config = {
|
||||||
url: "/backend/uploadfile",
|
url: "/backend/uploadfile",
|
||||||
method: "post",
|
method: "post",
|
||||||
data: formData,
|
data: formData,
|
||||||
headers: {
|
// 上传大文件不设超时;勿手写 Content-Type(需浏览器生成 multipart boundary)
|
||||||
"Content-Type": "multipart/form-data"
|
timeout: 0,
|
||||||
}
|
};
|
||||||
});
|
if (typeof options.onUploadProgress === "function") {
|
||||||
|
config.onUploadProgress = options.onUploadProgress;
|
||||||
|
}
|
||||||
|
return request(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+207
-277
@@ -1,285 +1,215 @@
|
|||||||
import request from '@/utils/request'
|
import request from '@/utils/request';
|
||||||
|
|
||||||
// 组织架构相关接口
|
/**
|
||||||
|
* 组织架构接口(组织 / 员工 / 职位)
|
||||||
|
*
|
||||||
|
* 组织架构是租户端通用基础数据:进销存(erp)与办公自动化(oa)两个模块各自有独立界面,
|
||||||
|
* 但读写的是同一份数据,后端按登录租户自动隔离。两端接口路径只有模块前缀不同,
|
||||||
|
* 因此这里用工厂函数按模块生成一套接口。
|
||||||
|
*/
|
||||||
|
|
||||||
// 获取组织架构列表
|
const MODULES = ['erp', 'oa'];
|
||||||
export function getOrganizationList(params) {
|
|
||||||
return request({
|
function normalizeModule(module) {
|
||||||
url: '/backend/erp/getOrganization',
|
const value = String(module || '').toLowerCase();
|
||||||
method: 'get',
|
return MODULES.includes(value) ? value : 'erp';
|
||||||
params
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取组织架构详情
|
export function createOrganizationApi(module) {
|
||||||
export function getOrganizationDetail(id) {
|
const base = `/backend/${normalizeModule(module)}`;
|
||||||
return request({
|
|
||||||
url: `/backend/erp/getOrganizationDetail/${id}`,
|
return {
|
||||||
method: 'get'
|
module: normalizeModule(module),
|
||||||
})
|
|
||||||
|
/* ------------------------------- 组织机构 ------------------------------- */
|
||||||
|
|
||||||
|
getOrganizationList(params) {
|
||||||
|
return request({ url: `${base}/getOrganization`, method: 'get', params });
|
||||||
|
},
|
||||||
|
|
||||||
|
getOrganizationTree(params) {
|
||||||
|
return request({ url: `${base}/getOrganizationTree`, method: 'get', params });
|
||||||
|
},
|
||||||
|
|
||||||
|
getOrganizationDetail(id) {
|
||||||
|
return request({ url: `${base}/getOrganizationDetail/${id}`, method: 'get' });
|
||||||
|
},
|
||||||
|
|
||||||
|
createOrganization(data) {
|
||||||
|
return request({ url: `${base}/createOrganization`, method: 'post', data });
|
||||||
|
},
|
||||||
|
|
||||||
|
updateOrganization(id, data) {
|
||||||
|
return request({ url: `${base}/editOrganization/${id}`, method: 'post', data });
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteOrganization(id) {
|
||||||
|
return request({ url: `${base}/deleteOrganization/${id}`, method: 'delete' });
|
||||||
|
},
|
||||||
|
|
||||||
|
getCompanyList(params) {
|
||||||
|
return request({ url: `${base}/getCompanys`, method: 'get', params });
|
||||||
|
},
|
||||||
|
|
||||||
|
getDepartmentList(parentId) {
|
||||||
|
return request({
|
||||||
|
url: `${base}/getDepartments`,
|
||||||
|
method: 'get',
|
||||||
|
params: parentId ? { parent_id: parentId } : {},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
searchOrganizations(keyword) {
|
||||||
|
return request({ url: `${base}/searchOrganizations`, method: 'get', params: { keyword } });
|
||||||
|
},
|
||||||
|
|
||||||
|
getOrganizationHierarchy(orgId) {
|
||||||
|
return request({ url: `${base}/getOrganizationHierarchy/${orgId}`, method: 'get' });
|
||||||
|
},
|
||||||
|
|
||||||
|
getOrganizationStats() {
|
||||||
|
return request({ url: `${base}/getOrganizationStats`, method: 'get' });
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 移动组织节点到新的上级(拖拽调整层级) */
|
||||||
|
moveOrganization(orgId, parentId, sort) {
|
||||||
|
const data = { org_id: orgId, parent_id: parentId || 0 };
|
||||||
|
if (sort !== undefined && sort !== null) {
|
||||||
|
data.sort = sort;
|
||||||
|
}
|
||||||
|
return request({ url: `${base}/moveOrganization`, method: 'post', data });
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 批量操作组织,action 取 enable / disable / delete */
|
||||||
|
batchOrganizeOrganizations(ids, action) {
|
||||||
|
return request({
|
||||||
|
url: `${base}/batchOrganizeOrganizations`,
|
||||||
|
method: 'post',
|
||||||
|
data: { ids, action },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
checkOrgCodeUnique(orgCode, excludeId) {
|
||||||
|
return request({
|
||||||
|
url: `${base}/checkOrgCodeUnique`,
|
||||||
|
method: 'get',
|
||||||
|
params: { org_code: orgCode, exclude_id: excludeId || '' },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/* --------------------------------- 员工 --------------------------------- */
|
||||||
|
|
||||||
|
getEmployeeList(params) {
|
||||||
|
return request({ url: `${base}/getEmployee`, method: 'get', params });
|
||||||
|
},
|
||||||
|
|
||||||
|
getEmployeeDetail(id) {
|
||||||
|
return request({ url: `${base}/getEmployeeDetail/${id}`, method: 'get' });
|
||||||
|
},
|
||||||
|
|
||||||
|
createEmployee(data) {
|
||||||
|
return request({ url: `${base}/createEmployee`, method: 'post', data });
|
||||||
|
},
|
||||||
|
|
||||||
|
updateEmployee(id, data) {
|
||||||
|
return request({ url: `${base}/editEmployee/${id}`, method: 'post', data });
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteEmployee(id) {
|
||||||
|
return request({ url: `${base}/deleteEmployee/${id}`, method: 'delete' });
|
||||||
|
},
|
||||||
|
|
||||||
|
getOrganizationEmployees(orgId) {
|
||||||
|
return request({ url: `${base}/getOrganizationEmployees/${orgId}`, method: 'get' });
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 员工调岗:支持单个或批量 */
|
||||||
|
moveEmployeeToOrg(employeeIds, orgId, position) {
|
||||||
|
const ids = Array.isArray(employeeIds) ? employeeIds : [employeeIds];
|
||||||
|
const data = { employee_ids: ids, org_id: orgId };
|
||||||
|
if (position !== undefined && position !== null) {
|
||||||
|
data.position = position;
|
||||||
|
}
|
||||||
|
return request({ url: `${base}/moveEmployeeToOrg`, method: 'post', data });
|
||||||
|
},
|
||||||
|
|
||||||
|
checkEmployeeAccountUnique(account, excludeId) {
|
||||||
|
return request({
|
||||||
|
url: `${base}/checkEmployeeAccountUnique`,
|
||||||
|
method: 'get',
|
||||||
|
params: { account, exclude_id: excludeId || '' },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/* --------------------------------- 职位 --------------------------------- */
|
||||||
|
|
||||||
|
getPositionList(params) {
|
||||||
|
return request({ url: `${base}/getPosition`, method: 'get', params });
|
||||||
|
},
|
||||||
|
|
||||||
|
getPositionDetail(id) {
|
||||||
|
return request({ url: `${base}/getPositionDetail/${id}`, method: 'get' });
|
||||||
|
},
|
||||||
|
|
||||||
|
createPosition(data) {
|
||||||
|
return request({ url: `${base}/createPosition`, method: 'post', data });
|
||||||
|
},
|
||||||
|
|
||||||
|
updatePosition(id, data) {
|
||||||
|
return request({ url: `${base}/editPosition/${id}`, method: 'post', data });
|
||||||
|
},
|
||||||
|
|
||||||
|
deletePosition(id) {
|
||||||
|
return request({ url: `${base}/deletePosition/${id}`, method: 'delete' });
|
||||||
|
},
|
||||||
|
|
||||||
|
checkPositionCodeUnique(positionCode, excludeId) {
|
||||||
|
return request({
|
||||||
|
url: `${base}/checkPositionCodeUnique`,
|
||||||
|
method: 'get',
|
||||||
|
params: { position_code: positionCode, exclude_id: excludeId || '' },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/* ----------------------------- 设置与导入导出 ----------------------------- */
|
||||||
|
|
||||||
|
getOrgSettings() {
|
||||||
|
return request({ url: `${base}/getOrgSettings`, method: 'get' });
|
||||||
|
},
|
||||||
|
|
||||||
|
saveOrgSettings(data) {
|
||||||
|
return request({ url: `${base}/saveOrgSettings`, method: 'post', data });
|
||||||
|
},
|
||||||
|
|
||||||
|
exportOrganization() {
|
||||||
|
return request({ url: `${base}/exportOrganization`, method: 'get', responseType: 'blob' });
|
||||||
|
},
|
||||||
|
|
||||||
|
downloadImportTemplate() {
|
||||||
|
return request({
|
||||||
|
url: `${base}/organizationImportTemplate`,
|
||||||
|
method: 'get',
|
||||||
|
responseType: 'blob',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
importOrganization(file) {
|
||||||
|
const data = new FormData();
|
||||||
|
data.append('file', file);
|
||||||
|
return request({
|
||||||
|
url: `${base}/importOrganization`,
|
||||||
|
method: 'post',
|
||||||
|
data,
|
||||||
|
headers: { 'Content-Type': 'multipart/form-data' },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建组织架构
|
/** 进销存模块的组织架构接口 */
|
||||||
export function createOrganization(data) {
|
export const erpOrganizationApi = createOrganizationApi('erp');
|
||||||
return request({
|
|
||||||
url: '/backend/erp/createOrganization',
|
|
||||||
method: 'post',
|
|
||||||
data
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新组织架构
|
/** 办公自动化模块的组织架构接口 */
|
||||||
export function updateOrganization(id, data) {
|
export const oaOrganizationApi = createOrganizationApi('oa');
|
||||||
return request({
|
|
||||||
url: `/backend/erp/editOrganization/${id}`,
|
|
||||||
method: 'post',
|
|
||||||
data
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 删除组织架构
|
export default erpOrganizationApi;
|
||||||
export function deleteOrganization(id) {
|
|
||||||
return request({
|
|
||||||
url: `/backend/erp/deleteOrganization/${id}`,
|
|
||||||
method: 'delete'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取企业单位列表
|
|
||||||
export function getCompanyList(params) {
|
|
||||||
return request({
|
|
||||||
url: '/backend/erp/getCompanys',
|
|
||||||
method: 'get',
|
|
||||||
params
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取部门列表
|
|
||||||
export function getDepartmentList(params) {
|
|
||||||
return request({
|
|
||||||
url: '/backend/erp/getDepartments',
|
|
||||||
method: 'get',
|
|
||||||
params
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 员工相关接口
|
|
||||||
|
|
||||||
// 获取员工列表
|
|
||||||
export function getEmployeeList(params) {
|
|
||||||
return request({
|
|
||||||
url: '/backend/erp/getEmployee',
|
|
||||||
method: 'get',
|
|
||||||
params
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取员工详情
|
|
||||||
export function getEmployeeDetail(id) {
|
|
||||||
return request({
|
|
||||||
url: `/backend/erp/getEmployeeDetail/${id}`,
|
|
||||||
method: 'get'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建员工
|
|
||||||
export function createEmployee(data) {
|
|
||||||
return request({
|
|
||||||
url: '/backend/erp/createEmployee',
|
|
||||||
method: 'post',
|
|
||||||
data
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新员工
|
|
||||||
export function updateEmployee(id, data) {
|
|
||||||
return request({
|
|
||||||
url: `/backend/erp/editEmployee/${id}`,
|
|
||||||
method: 'post',
|
|
||||||
data
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 删除员工
|
|
||||||
export function deleteEmployee(id) {
|
|
||||||
return request({
|
|
||||||
url: `/backend/erp/deleteEmployee/${id}`,
|
|
||||||
method: 'delete'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 职位相关接口
|
|
||||||
|
|
||||||
// 获取职位列表
|
|
||||||
export function getPositionList(params) {
|
|
||||||
return request({
|
|
||||||
url: '/backend/erp/getPosition',
|
|
||||||
method: 'get',
|
|
||||||
params
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取职位详情
|
|
||||||
export function getPositionDetail(id) {
|
|
||||||
return request({
|
|
||||||
url: `/backend/erp/getPositionDetail/${id}`,
|
|
||||||
method: 'get'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建职位
|
|
||||||
export function createPosition(data) {
|
|
||||||
return request({
|
|
||||||
url: '/backend/erp/createPosition',
|
|
||||||
method: 'post',
|
|
||||||
data
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新职位
|
|
||||||
export function updatePosition(id, data) {
|
|
||||||
return request({
|
|
||||||
url: `/backend/erp/editPosition/${id}`,
|
|
||||||
method: 'post',
|
|
||||||
data
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 删除职位
|
|
||||||
export function deletePosition(id) {
|
|
||||||
return request({
|
|
||||||
url: `/backend/erp/deletePosition/${id}`,
|
|
||||||
method: 'delete'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 组织设置相关接口
|
|
||||||
|
|
||||||
// 获取组织设置
|
|
||||||
export function getOrgSettings() {
|
|
||||||
return request({
|
|
||||||
url: '/backend/erp/getOrgSettings',
|
|
||||||
method: 'get'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存组织设置
|
|
||||||
export function saveOrgSettings(data) {
|
|
||||||
return request({
|
|
||||||
url: '/backend/erp/saveOrgSettings',
|
|
||||||
method: 'post',
|
|
||||||
data
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 组织架构导出
|
|
||||||
export function exportOrganization(params) {
|
|
||||||
return request({
|
|
||||||
url: '/backend/erp/exportOrganization',
|
|
||||||
method: 'get',
|
|
||||||
params,
|
|
||||||
responseType: 'blob'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 组织架构导入
|
|
||||||
export function importOrganization(data) {
|
|
||||||
return request({
|
|
||||||
url: '/backend/erp/importOrganization',
|
|
||||||
method: 'post',
|
|
||||||
data,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'multipart/form-data'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 批量操作组织
|
|
||||||
export function batchOrganizeOrganizations(ids, action) {
|
|
||||||
return request({
|
|
||||||
url: '/backend/erp/batchOrganizeOrganizations',
|
|
||||||
method: 'post',
|
|
||||||
data: {
|
|
||||||
ids,
|
|
||||||
action
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取组织架构统计信息
|
|
||||||
export function getOrganizationStats() {
|
|
||||||
return request({
|
|
||||||
url: '/backend/erp/getOrganizationStats',
|
|
||||||
method: 'get'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取组织架构树形结构
|
|
||||||
export function getOrganizationTree(params) {
|
|
||||||
return request({
|
|
||||||
url: '/backend/erp/getOrganizationTree',
|
|
||||||
method: 'get',
|
|
||||||
params
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 搜索组织架构
|
|
||||||
export function searchOrganizations(keyword) {
|
|
||||||
return request({
|
|
||||||
url: '/backend/erp/searchOrganizations',
|
|
||||||
method: 'get',
|
|
||||||
params: { keyword }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取组织架构下的所有员工
|
|
||||||
export function getOrganizationEmployees(orgId) {
|
|
||||||
return request({
|
|
||||||
url: `/backend/erp/getOrganizationEmployees/${orgId}`,
|
|
||||||
method: 'get'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 移动员工到其他组织
|
|
||||||
export function moveEmployeeToOrg(employeeId, orgId) {
|
|
||||||
return request({
|
|
||||||
url: '/backend/erp/moveEmployeeToOrg',
|
|
||||||
method: 'post',
|
|
||||||
data: {
|
|
||||||
employee_id: employeeId,
|
|
||||||
org_id: orgId
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取组织架构层级关系
|
|
||||||
export function getOrganizationHierarchy(orgId) {
|
|
||||||
return request({
|
|
||||||
url: `/backend/erp/getOrganizationHierarchy/${orgId}`,
|
|
||||||
method: 'get'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证组织编码唯一性
|
|
||||||
export function checkOrgCodeUnique(orgCode, excludeId) {
|
|
||||||
return request({
|
|
||||||
url: '/backend/erp/checkOrgCodeUnique',
|
|
||||||
method: 'get',
|
|
||||||
params: {
|
|
||||||
org_code: orgCode,
|
|
||||||
exclude_id: excludeId || ''
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证员工账号唯一性
|
|
||||||
export function checkEmployeeAccountUnique(account, excludeId) {
|
|
||||||
return request({
|
|
||||||
url: '/backend/erp/checkEmployeeAccountUnique',
|
|
||||||
method: 'get',
|
|
||||||
params: {
|
|
||||||
account,
|
|
||||||
exclude_id: excludeId || ''
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|||||||
Vendored
+6
-13
@@ -12,19 +12,12 @@ declare module '@/*' {
|
|||||||
export default component;
|
export default component;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module '@/api/erp' {
|
declare module '@/api/organization' {
|
||||||
export function getOrganizationList(): Promise<any>;
|
export function createOrganizationApi(module: string): any;
|
||||||
export function getOrganizationDetail(id: number | string): Promise<any>;
|
export const erpOrganizationApi: any;
|
||||||
export function createOrganization(data: any): Promise<any>;
|
export const oaOrganizationApi: any;
|
||||||
export function editOrganization(id: number | string, data: any): Promise<any>;
|
const api: any;
|
||||||
export function deleteOrganization(id: number | string): Promise<any>;
|
export default api;
|
||||||
export function getCompanys(): Promise<any>;
|
|
||||||
export function getDepartments(parentId?: number | string): Promise<any>;
|
|
||||||
export function getEmployeeList(tenantId?: number | string): Promise<any>;
|
|
||||||
export function getEmployeeDetail(id: number | string): Promise<any>;
|
|
||||||
export function createEmployee(data: any): Promise<any>;
|
|
||||||
export function editEmployee(id: number | string, data: any): Promise<any>;
|
|
||||||
export function deleteEmployee(id: number | string): Promise<any>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module '@/stores/auth' {
|
declare module '@/stores/auth' {
|
||||||
|
|||||||
@@ -69,7 +69,26 @@ const staticMainChildren = [
|
|||||||
component: () => import("@/views/user/userProfile.vue"),
|
component: () => import("@/views/user/userProfile.vue"),
|
||||||
meta: { requiresAuth: true, title: "用户中心" }
|
meta: { requiresAuth: true, title: "用户中心" }
|
||||||
},
|
},
|
||||||
// OA 组织架构
|
// 组织架构(组织 / 员工 / 职位)
|
||||||
|
// 进销存(erp)与办公自动化(oa)各有独立页面,但读写同一份组织数据。
|
||||||
|
{
|
||||||
|
path: "/apps/erp/organization",
|
||||||
|
name: "ErpOrganization",
|
||||||
|
component: () => import("@/views/apps/erp/organization/index.vue"),
|
||||||
|
meta: { requiresAuth: true, title: "组织架构", modulePath: "/apps/erp" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/apps/erp/employee",
|
||||||
|
name: "ErpEmployee",
|
||||||
|
component: () => import("@/views/apps/erp/employee/index.vue"),
|
||||||
|
meta: { requiresAuth: true, title: "员工管理", modulePath: "/apps/erp" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/apps/erp/position",
|
||||||
|
name: "ErpPosition",
|
||||||
|
component: () => import("@/views/apps/erp/position/index.vue"),
|
||||||
|
meta: { requiresAuth: true, title: "职位管理", modulePath: "/apps/erp" }
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/apps/oa/organization",
|
path: "/apps/oa/organization",
|
||||||
name: "Organization",
|
name: "Organization",
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ import axios from 'axios';
|
|||||||
// 获取API基础URL,添加调试信息
|
// 获取API基础URL,添加调试信息
|
||||||
const apiBaseURL = import.meta.env.VITE_API_BASE_URL;
|
const apiBaseURL = import.meta.env.VITE_API_BASE_URL;
|
||||||
|
|
||||||
// 创建axios实例
|
// 创建axios实例(普通接口 5min;大文件上传在 api/file.js 单独设置更长 timeout)
|
||||||
const service = axios.create({
|
const service = axios.create({
|
||||||
baseURL: apiBaseURL,
|
baseURL: apiBaseURL,
|
||||||
timeout: 10000,
|
timeout: 300000,
|
||||||
withCredentials: false // JWT 不需要 Cookie
|
withCredentials: false // JWT 不需要 Cookie
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -18,9 +18,13 @@ service.interceptors.request.use(
|
|||||||
config.headers['Authorization'] = `Bearer ${token}`;
|
config.headers['Authorization'] = `Bearer ${token}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 对于有 body 的请求(POST、PUT、PATCH),确保设置 Content-Type
|
// 对于有 body 的请求(POST、PUT、PATCH),默认 JSON;
|
||||||
|
// FormData 必须由浏览器自行设置 Content-Type,否则缺少 multipart boundary,服务端无法解析
|
||||||
if (config.data && ['post', 'put', 'patch'].includes(config.method?.toLowerCase())) {
|
if (config.data && ['post', 'put', 'patch'].includes(config.method?.toLowerCase())) {
|
||||||
if (!config.headers['Content-Type'] && !config.headers['content-type']) {
|
if (typeof FormData !== 'undefined' && config.data instanceof FormData) {
|
||||||
|
delete config.headers['Content-Type'];
|
||||||
|
delete config.headers['content-type'];
|
||||||
|
} else if (!config.headers['Content-Type'] && !config.headers['content-type']) {
|
||||||
config.headers['Content-Type'] = 'application/json';
|
config.headers['Content-Type'] = 'application/json';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -54,7 +58,11 @@ service.interceptors.response.use(
|
|||||||
console.error('请求失败,请稍后再试');
|
console.error('请求失败,请稍后再试');
|
||||||
}
|
}
|
||||||
} else if (error.request) {
|
} else if (error.request) {
|
||||||
console.error('请求失败,请检查网络连接');
|
if (error.code === 'ECONNABORTED') {
|
||||||
|
console.error('请求超时,请稍后重试');
|
||||||
|
} else {
|
||||||
|
console.error('请求失败,请检查网络连接');
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
console.error('请求配置错误');
|
console.error('请求配置错误');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,14 +47,19 @@
|
|||||||
|
|
||||||
<div class="category-actions">
|
<div class="category-actions">
|
||||||
<el-button-group size="small">
|
<el-button-group size="small">
|
||||||
<el-button type="text" title="编辑" @click.stop="$emit('edit', item)">
|
<el-button
|
||||||
|
v-if="!isGlobal"
|
||||||
|
type="text"
|
||||||
|
title="编辑"
|
||||||
|
@click.stop="$emit('edit', item)"
|
||||||
|
>
|
||||||
<el-icon><Edit /></el-icon>
|
<el-icon><Edit /></el-icon>
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button type="text" title="添加子分类" @click.stop="$emit('add-child', item)">
|
<el-button type="text" title="添加子分类" @click.stop="$emit('add-child', item)">
|
||||||
<el-icon><Plus /></el-icon>
|
<el-icon><Plus /></el-icon>
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button
|
<el-button
|
||||||
v-if="item.tid !== 0"
|
v-if="!isGlobal"
|
||||||
type="text"
|
type="text"
|
||||||
class="danger"
|
class="danger"
|
||||||
title="删除"
|
title="删除"
|
||||||
@@ -62,7 +67,7 @@
|
|||||||
>
|
>
|
||||||
<el-icon><Delete /></el-icon>
|
<el-icon><Delete /></el-icon>
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-tag v-else type="info" size="small" title="全局分类不可删除">
|
<el-tag v-if="isGlobal" type="info" size="small" title="全局分类不可修改或删除">
|
||||||
全局
|
全局
|
||||||
</el-tag>
|
</el-tag>
|
||||||
</el-button-group>
|
</el-button-group>
|
||||||
@@ -88,6 +93,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import { computed } from "vue";
|
||||||
import { ArrowDown, ArrowRight, Edit, Plus, Delete, Document } from "@element-plus/icons-vue";
|
import { ArrowDown, ArrowRight, Edit, Plus, Delete, Document } from "@element-plus/icons-vue";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -97,6 +103,11 @@ const props = defineProps({
|
|||||||
|
|
||||||
const emit = defineEmits(['edit', 'add-child', 'delete']);
|
const emit = defineEmits(['edit', 'add-child', 'delete']);
|
||||||
|
|
||||||
|
// 全局分类(tid = 0)由平台维护,租户端不能编辑或删除
|
||||||
|
const isGlobal = computed(
|
||||||
|
() => props.item.is_global === true || Number(props.item.tid) === 0
|
||||||
|
);
|
||||||
|
|
||||||
const toggleExpand = () => {
|
const toggleExpand = () => {
|
||||||
props.item.expanded = !props.item.expanded;
|
props.item.expanded = !props.item.expanded;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -245,9 +245,8 @@ async function handleSubmit() {
|
|||||||
if (fileList.value.length > 0 && fileList.value[0].raw) {
|
if (fileList.value.length > 0 && fileList.value[0].raw) {
|
||||||
const uploadFormData = new FormData()
|
const uploadFormData = new FormData()
|
||||||
uploadFormData.append('file', fileList.value[0].raw)
|
uploadFormData.append('file', fileList.value[0].raw)
|
||||||
uploadFormData.append('cate', 'category')
|
|
||||||
|
|
||||||
const uploadRes = await uploadFile(uploadFormData)
|
const uploadRes: any = await uploadFile(uploadFormData)
|
||||||
if (uploadRes?.data?.url) {
|
if (uploadRes?.data?.url) {
|
||||||
imageUrl = uploadRes.data.url
|
imageUrl = uploadRes.data.url
|
||||||
}
|
}
|
||||||
@@ -262,17 +261,20 @@ async function handleSubmit() {
|
|||||||
cid: formData.parentId,
|
cid: formData.parentId,
|
||||||
}
|
}
|
||||||
if (isEdit.value) {
|
if (isEdit.value) {
|
||||||
await editCategory(formData.id, payload)
|
const res: any = await editCategory(formData.id, payload)
|
||||||
|
if (res?.code && res.code !== 200) throw res
|
||||||
ElMessage.success('更新成功')
|
ElMessage.success('更新成功')
|
||||||
} else {
|
} else {
|
||||||
await createCategory(payload)
|
const res: any = await createCategory(payload)
|
||||||
|
if (res?.code && res.code !== 200) throw res
|
||||||
ElMessage.success('创建成功')
|
ElMessage.success('创建成功')
|
||||||
}
|
}
|
||||||
emit('saved')
|
emit('saved')
|
||||||
closeDialog()
|
closeDialog()
|
||||||
} catch (err) {
|
} catch (err: any) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
ElMessage.error(isEdit.value ? '更新失败' : '创建失败')
|
const msg = err?.response?.data?.msg || err?.msg
|
||||||
|
ElMessage.error(msg || (isEdit.value ? '更新失败' : '创建失败'))
|
||||||
} finally {
|
} finally {
|
||||||
submitLoading.value = false
|
submitLoading.value = false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,9 +70,8 @@ const searchText = ref("");
|
|||||||
const categories = ref([]);
|
const categories = ref([]);
|
||||||
|
|
||||||
// 新增分类
|
// 新增分类
|
||||||
const addCategory = ref(null);
|
|
||||||
const handleAdd = () => {
|
const handleAdd = () => {
|
||||||
addCategory.value = { parentId: 0 };
|
currentEdit.value = { parentId: 0 };
|
||||||
dialogVisible.value = true;
|
dialogVisible.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,6 +127,10 @@ function handleCreate() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleEdit(category) {
|
function handleEdit(category) {
|
||||||
|
if (Number(category.tid) === 0 || category.is_global === true) {
|
||||||
|
ElMessage.warning("全局分类不可修改");
|
||||||
|
return;
|
||||||
|
}
|
||||||
currentEdit.value = category;
|
currentEdit.value = category;
|
||||||
dialogVisible.value = true;
|
dialogVisible.value = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,122 +0,0 @@
|
|||||||
<template>
|
|
||||||
<el-dialog v-model="visible" title="修改密码" width="420px" :close-on-click-modal="false">
|
|
||||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="90px">
|
|
||||||
<el-form-item label="用户">
|
|
||||||
<span>{{ currentUser.name || currentUser.account || "-" }}</span>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<el-form-item label="新密码" prop="password">
|
|
||||||
<el-input
|
|
||||||
v-model="form.password"
|
|
||||||
type="password"
|
|
||||||
placeholder="请输入新密码"
|
|
||||||
show-password
|
|
||||||
clearable
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<el-form-item label="确认密码" prop="confirmPassword">
|
|
||||||
<el-input
|
|
||||||
v-model="form.confirmPassword"
|
|
||||||
type="password"
|
|
||||||
placeholder="请再次输入新密码"
|
|
||||||
show-password
|
|
||||||
clearable
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
|
|
||||||
<template #footer>
|
|
||||||
<el-button @click="visible = false">取消</el-button>
|
|
||||||
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">确定</el-button>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { reactive, ref } from "vue";
|
|
||||||
import { ElMessage, type FormInstance, type FormRules } from "element-plus";
|
|
||||||
import { changePassword } from "@/api/user";
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: "submit"): void;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const visible = ref(false);
|
|
||||||
const submitLoading = ref(false);
|
|
||||||
const formRef = ref<FormInstance>();
|
|
||||||
const currentUser = ref<any>({});
|
|
||||||
|
|
||||||
const form = reactive({
|
|
||||||
password: "",
|
|
||||||
confirmPassword: "",
|
|
||||||
});
|
|
||||||
|
|
||||||
const validateConfirmPassword = (_rule: any, value: string, callback: (error?: Error) => void) => {
|
|
||||||
if (!value) {
|
|
||||||
callback(new Error("请再次输入新密码"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value !== form.password) {
|
|
||||||
callback(new Error("两次输入的密码不一致"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
callback();
|
|
||||||
};
|
|
||||||
|
|
||||||
const rules: FormRules = {
|
|
||||||
password: [
|
|
||||||
{ required: true, message: "请输入新密码", trigger: "blur" },
|
|
||||||
{ min: 6, message: "密码长度不能少于 6 位", trigger: "blur" },
|
|
||||||
],
|
|
||||||
confirmPassword: [
|
|
||||||
{ required: true, message: "请再次输入新密码", trigger: "blur" },
|
|
||||||
{ validator: validateConfirmPassword, trigger: "blur" },
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const resetForm = () => {
|
|
||||||
form.password = "";
|
|
||||||
form.confirmPassword = "";
|
|
||||||
formRef.value?.clearValidate();
|
|
||||||
};
|
|
||||||
|
|
||||||
const openChangePass = (row: any) => {
|
|
||||||
currentUser.value = row || {};
|
|
||||||
resetForm();
|
|
||||||
visible.value = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
|
||||||
if (!currentUser.value?.id) {
|
|
||||||
ElMessage.error("用户 ID 不存在");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await formRef.value?.validate();
|
|
||||||
|
|
||||||
submitLoading.value = true;
|
|
||||||
try {
|
|
||||||
await changePassword(currentUser.value.id, {
|
|
||||||
newPassword: form.password,
|
|
||||||
});
|
|
||||||
|
|
||||||
ElMessage.success("密码修改成功");
|
|
||||||
visible.value = false;
|
|
||||||
emit("submit");
|
|
||||||
} catch (e: any) {
|
|
||||||
ElMessage.error(e?.response?.data?.message || e?.message || "密码修改失败");
|
|
||||||
} finally {
|
|
||||||
submitLoading.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
defineExpose({
|
|
||||||
openChangePass,
|
|
||||||
open: openChangePass,
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="less" scoped></style>
|
|
||||||
@@ -1,583 +0,0 @@
|
|||||||
<template>
|
|
||||||
<el-drawer v-model="visible" :title="dialogTitle" width="500px">
|
|
||||||
<el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
|
|
||||||
<div class="form-title">账号信息</div>
|
|
||||||
<!-- 账号 -->
|
|
||||||
<el-form-item label="账号" required>
|
|
||||||
<el-input v-model="form.account" :disabled="!isAdd" placeholder="请输入账号" />
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<!-- 密码 -->
|
|
||||||
<el-form-item label="密码" prop="password" v-if="isAdd" required>
|
|
||||||
<el-input
|
|
||||||
v-model="form.password"
|
|
||||||
type="password"
|
|
||||||
autocomplete="new-password"
|
|
||||||
show-password
|
|
||||||
:placeholder="isAdd ? '请输入密码(至少6位)' : '留空则不修改密码'"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<!-- 确认密码 -->
|
|
||||||
<el-form-item label="确认密码" prop="confirmPassword" v-if="isAdd" required>
|
|
||||||
<el-input
|
|
||||||
v-model="form.confirmPassword"
|
|
||||||
type="password"
|
|
||||||
autocomplete="new-password"
|
|
||||||
show-password
|
|
||||||
:placeholder="isAdd ? '请再次输入密码' : '留空则不修改密码'"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<el-divider></el-divider>
|
|
||||||
|
|
||||||
<div class="form-title">个人信息</div>
|
|
||||||
<!-- 姓名 -->
|
|
||||||
<el-form-item label="姓名" required>
|
|
||||||
<el-input v-model="form.name" placeholder="请输入姓名" />
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<!-- 性别 -->
|
|
||||||
<el-form-item label="性别" required>
|
|
||||||
<el-radio-group v-model="form.sex" placeholder="请选择性别">
|
|
||||||
<el-radio-button label="未知" :value="0" />
|
|
||||||
<el-radio-button label="男" :value="1" />
|
|
||||||
<el-radio-button label="女" :value="2" />
|
|
||||||
</el-radio-group>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<!-- 生日 -->
|
|
||||||
<el-form-item label="生日" required>
|
|
||||||
<el-date-picker
|
|
||||||
v-model="form.birthday"
|
|
||||||
type="date"
|
|
||||||
value-format="YYYY-MM-DD"
|
|
||||||
placeholder="请选择生日"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<!-- 隶属单位 -->
|
|
||||||
<el-form-item label="隶属单位" required>
|
|
||||||
<el-select v-model="form.affiliate_unit" placeholder="请选择隶属单位" clearable style="width: 100%">
|
|
||||||
<el-option v-for="item in companyList" :key="item.id" :label="item.org_name" :value="String(item.id)" />
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<!-- 部门 -->
|
|
||||||
<el-form-item label="部门" required>
|
|
||||||
<el-select v-model="form.department" placeholder="请先选择隶属单位" clearable style="width: 100%" :disabled="!form.affiliate_unit">
|
|
||||||
<el-option v-for="item in departmentList" :key="item.id" :label="item.org_name" :value="String(item.id)" />
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<!-- 职位 -->
|
|
||||||
<el-form-item label="职位" required>
|
|
||||||
<el-input v-model="form.position" placeholder="请输入职位" />
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<!-- 学历 -->
|
|
||||||
<el-form-item label="学历">
|
|
||||||
<el-input v-model="form.education" placeholder="请输入学历" />
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<!-- 民族 -->
|
|
||||||
<el-form-item label="民族" required>
|
|
||||||
<el-input v-model="form.nation" placeholder="请输入民族" />
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<!-- 手机 -->
|
|
||||||
<el-form-item label="手机" required>
|
|
||||||
<el-input v-model="form.phone" placeholder="请输入手机号" />
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<!-- 微信 -->
|
|
||||||
<el-form-item label="微信">
|
|
||||||
<el-input v-model="form.wechat" placeholder="请输入微信" />
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<!-- 邮箱 -->
|
|
||||||
<el-form-item label="邮箱" required>
|
|
||||||
<el-input v-model="form.email" placeholder="请输入邮箱" />
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<!-- 家庭住址 -->
|
|
||||||
<el-form-item label="家庭住址" required>
|
|
||||||
<el-input v-model="form.home_address" placeholder="请输入家庭住址" />
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<!-- 状态 -->
|
|
||||||
<el-form-item label="状态">
|
|
||||||
<el-select
|
|
||||||
v-model="form.account_status"
|
|
||||||
placeholder="请选择状态"
|
|
||||||
style="width: 100%"
|
|
||||||
>
|
|
||||||
<el-option label="离职" :value="2" v-if="!isAdd" />
|
|
||||||
<el-option label="启用" :value="1" />
|
|
||||||
<el-option label="禁用" :value="0" />
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
|
|
||||||
<!-- 对话框脚部 -->
|
|
||||||
<template #footer>
|
|
||||||
<el-button @click="handleCancel">取消</el-button>
|
|
||||||
<el-button type="primary" @click="handleSubmit">保存</el-button>
|
|
||||||
</template>
|
|
||||||
</el-drawer>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref, computed, watch, nextTick } from "vue";
|
|
||||||
import { ElMessage } from "element-plus";
|
|
||||||
import { createEmployee, editEmployee, getEmployeeDetail, getCompanys, getDepartments } from "@/api/erp";
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
modelValue: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
isEdit: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
statusDict: {
|
|
||||||
type: Array,
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const emit = defineEmits(["update:modelValue", "submit", "close"]);
|
|
||||||
|
|
||||||
const visible = ref(false);
|
|
||||||
const formRef = ref<any>(null);
|
|
||||||
const isAdd = ref(false);
|
|
||||||
const companyList = ref<any[]>([]);
|
|
||||||
const departmentList = ref<any[]>([]);
|
|
||||||
const isHydratingUserData = ref(false);
|
|
||||||
|
|
||||||
// 获取公司列表
|
|
||||||
const loadCompanyList = async () => {
|
|
||||||
try {
|
|
||||||
const res = await getCompanys();
|
|
||||||
companyList.value = res.data || res || [];
|
|
||||||
} catch (e) {
|
|
||||||
console.error("获取公司列表失败", e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
// 获取部门列表
|
|
||||||
const loadDepartments = async (parentId?: number) => {
|
|
||||||
try {
|
|
||||||
const res = await getDepartments(parentId);
|
|
||||||
departmentList.value = res.data || res || [];
|
|
||||||
} catch (e) {
|
|
||||||
console.error("获取部门列表失败", e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const form = ref<any>({
|
|
||||||
id: null,
|
|
||||||
tid: 1,
|
|
||||||
tenant_id: 1,
|
|
||||||
account: "",
|
|
||||||
name: "",
|
|
||||||
phone: "",
|
|
||||||
qq: "",
|
|
||||||
sex: 0,
|
|
||||||
birthday: "",
|
|
||||||
affiliate_unit: "",
|
|
||||||
department: "",
|
|
||||||
position: "",
|
|
||||||
education: "",
|
|
||||||
nation: "",
|
|
||||||
wechat: "",
|
|
||||||
home_address: "",
|
|
||||||
password: "",
|
|
||||||
confirmPassword: "",
|
|
||||||
email: "",
|
|
||||||
account_status: 1,
|
|
||||||
status: 1,
|
|
||||||
});
|
|
||||||
|
|
||||||
// 监听隶属单位变化,加载对应部门
|
|
||||||
watch(() => form.value.affiliate_unit, (newVal) => {
|
|
||||||
if (isHydratingUserData.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
form.value.department = "";
|
|
||||||
departmentList.value = [];
|
|
||||||
if (newVal) {
|
|
||||||
loadDepartments(Number(newVal));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const dialogTitle = computed(() => {
|
|
||||||
return isAdd.value ? "添加用户" : "编辑用户";
|
|
||||||
});
|
|
||||||
|
|
||||||
// 密码验证规则
|
|
||||||
const validatePassword = (rule: any, value: any, callback: any) => {
|
|
||||||
if (isAdd.value) {
|
|
||||||
// 新增用户时,密码必填
|
|
||||||
if (!value) {
|
|
||||||
callback(new Error("请输入密码"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (value.length < 6) {
|
|
||||||
callback(new Error("密码长度不能少于6位"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 编辑用户时,如果填写了密码,则必须符合规则
|
|
||||||
if (value && value.length < 6) {
|
|
||||||
callback(new Error("密码长度不能少于6位"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
callback();
|
|
||||||
};
|
|
||||||
|
|
||||||
// 确认密码验证规则
|
|
||||||
const validateConfirmPassword = (rule: any, value: any, callback: any) => {
|
|
||||||
if (isAdd.value) {
|
|
||||||
// 新增用户时,确认密码必填
|
|
||||||
if (!value) {
|
|
||||||
callback(new Error("请再次输入密码"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (value !== form.value.password) {
|
|
||||||
callback(new Error("两次输入的密码不一致"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 编辑用户时,如果填写了密码,则确认密码必须一致
|
|
||||||
if (form.value.password && value !== form.value.password) {
|
|
||||||
callback(new Error("两次输入的密码不一致"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// 如果填写了确认密码但没填密码,提示错误
|
|
||||||
if (value && !form.value.password) {
|
|
||||||
callback(new Error("请先输入密码"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
callback();
|
|
||||||
};
|
|
||||||
|
|
||||||
// 表单验证规则
|
|
||||||
const rules = {
|
|
||||||
account: [
|
|
||||||
{ required: true, message: "请输入账号", trigger: "blur" },
|
|
||||||
{ min: 3, max: 20, message: "账号长度在 3 到 20 个字符", trigger: "blur" },
|
|
||||||
],
|
|
||||||
name: [{ required: true, message: "请输入姓名", trigger: "blur" }],
|
|
||||||
password: [{ validator: validatePassword, trigger: "blur" }],
|
|
||||||
confirmPassword: [{ validator: validateConfirmPassword, trigger: "blur" }],
|
|
||||||
email: [{ type: "email", message: "请输入正确的邮箱地址", trigger: "blur" }],
|
|
||||||
};
|
|
||||||
|
|
||||||
// 监听 modelValue
|
|
||||||
watch(
|
|
||||||
() => props.modelValue,
|
|
||||||
(newVal) => {
|
|
||||||
visible.value = newVal;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// 监听 visible 变化
|
|
||||||
watch(visible, (newVal) => {
|
|
||||||
if (!newVal) {
|
|
||||||
emit("update:modelValue", false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 监听 statusDict 变化,用于调试
|
|
||||||
watch(
|
|
||||||
() => props.statusDict,
|
|
||||||
(newVal) => {},
|
|
||||||
{ immediate: true, deep: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
const loadUserData = async (user: any) => {
|
|
||||||
try {
|
|
||||||
// 处理两种调用方式:传递用户对象或用户 ID
|
|
||||||
const userId = typeof user === "number" ? user : user?.id || user?.userId;
|
|
||||||
if (!userId) {
|
|
||||||
throw new Error("未提供有效的用户 ID");
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await getEmployeeDetail(userId);
|
|
||||||
|
|
||||||
const data = res.data || res;
|
|
||||||
|
|
||||||
// 确保 sex 和 status 都是数字类型
|
|
||||||
const sexValue =
|
|
||||||
data.sex !== undefined && data.sex !== null
|
|
||||||
? Number(data.sex)
|
|
||||||
: data.gender !== undefined && data.gender !== null
|
|
||||||
? Number(data.gender)
|
|
||||||
: 0;
|
|
||||||
|
|
||||||
const statusValue =
|
|
||||||
data.account_status !== undefined && data.account_status !== null
|
|
||||||
? Number(data.account_status)
|
|
||||||
: data.status !== undefined && data.status !== null
|
|
||||||
? Number(data.status)
|
|
||||||
: 1;
|
|
||||||
|
|
||||||
const affiliateUnitValue =
|
|
||||||
data.affiliate_unit !== undefined && data.affiliate_unit !== null
|
|
||||||
? String(data.affiliate_unit)
|
|
||||||
: "";
|
|
||||||
const departmentValue =
|
|
||||||
data.department !== undefined && data.department !== null
|
|
||||||
? String(data.department)
|
|
||||||
: "";
|
|
||||||
|
|
||||||
isHydratingUserData.value = true;
|
|
||||||
form.value = {
|
|
||||||
id: data.id,
|
|
||||||
tid: data.tid || data.tenant_id || 1,
|
|
||||||
tenant_id: data.tenant_id || data.tid || 1,
|
|
||||||
account: data.account || "",
|
|
||||||
name: data.name || "",
|
|
||||||
phone: data.phone || "",
|
|
||||||
qq: data.qq || "",
|
|
||||||
sex: sexValue,
|
|
||||||
birthday: data.birthday || "",
|
|
||||||
affiliate_unit: affiliateUnitValue,
|
|
||||||
department: departmentValue,
|
|
||||||
position: data.position || "",
|
|
||||||
education: data.education || "",
|
|
||||||
nation: data.nation || data.nationality || "",
|
|
||||||
wechat: data.wechat || "",
|
|
||||||
home_address: data.home_address || "",
|
|
||||||
password: "",
|
|
||||||
confirmPassword: "",
|
|
||||||
email: data.email || "",
|
|
||||||
account_status: statusValue,
|
|
||||||
status: statusValue,
|
|
||||||
};
|
|
||||||
|
|
||||||
await nextTick();
|
|
||||||
|
|
||||||
departmentList.value = [];
|
|
||||||
if (form.value.affiliate_unit) {
|
|
||||||
await loadDepartments(Number(form.value.affiliate_unit));
|
|
||||||
}
|
|
||||||
form.value.department = departmentValue;
|
|
||||||
isHydratingUserData.value = false;
|
|
||||||
} catch (e: any) {
|
|
||||||
console.error("Failed to load user data:", e);
|
|
||||||
const errorMsg = e?.response?.data?.message || e?.message || "加载用户失败";
|
|
||||||
ElMessage.error(errorMsg);
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCancel = () => {
|
|
||||||
visible.value = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
|
||||||
// 表单验证
|
|
||||||
if (!formRef.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await formRef.value.validate();
|
|
||||||
} catch (error) {
|
|
||||||
ElMessage.warning("请检查表单填写是否正确");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证密码一致性
|
|
||||||
if (isAdd.value) {
|
|
||||||
// 新增用户时,密码必填
|
|
||||||
if (!form.value.password) {
|
|
||||||
ElMessage.error("请输入密码");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (form.value.password !== form.value.confirmPassword) {
|
|
||||||
ElMessage.error("两次输入的密码不一致");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 编辑用户时,如果填写了密码,则必须填写确认密码且一致
|
|
||||||
if (form.value.password) {
|
|
||||||
if (form.value.password !== form.value.confirmPassword) {
|
|
||||||
ElMessage.error("两次输入的密码不一致");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (isAdd.value) {
|
|
||||||
// 新增用户
|
|
||||||
const submitData: any = {
|
|
||||||
tid: form.value.tid,
|
|
||||||
tenant_id: form.value.tenant_id,
|
|
||||||
account: form.value.account,
|
|
||||||
name: form.value.name,
|
|
||||||
phone: form.value.phone,
|
|
||||||
qq: form.value.qq,
|
|
||||||
sex: form.value.sex,
|
|
||||||
birthday: form.value.birthday,
|
|
||||||
affiliate_unit: form.value.affiliate_unit,
|
|
||||||
department: form.value.department,
|
|
||||||
position: form.value.position,
|
|
||||||
education: form.value.education,
|
|
||||||
nation: form.value.nation,
|
|
||||||
wechat: form.value.wechat,
|
|
||||||
email: form.value.email,
|
|
||||||
home_address: form.value.home_address,
|
|
||||||
account_status: form.value.account_status,
|
|
||||||
status: form.value.account_status,
|
|
||||||
password: form.value.password,
|
|
||||||
};
|
|
||||||
|
|
||||||
await createEmployee(submitData);
|
|
||||||
ElMessage.success("添加成功");
|
|
||||||
} else {
|
|
||||||
// 编辑用户
|
|
||||||
if (!form.value.id || form.value.id === 0) {
|
|
||||||
ElMessage.error("用户ID不能为空");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const submitData: any = {
|
|
||||||
id: form.value.id,
|
|
||||||
tid: form.value.tid,
|
|
||||||
tenant_id: form.value.tenant_id,
|
|
||||||
account: form.value.account,
|
|
||||||
name: form.value.name,
|
|
||||||
phone: form.value.phone,
|
|
||||||
qq: form.value.qq,
|
|
||||||
sex: form.value.sex,
|
|
||||||
birthday: form.value.birthday,
|
|
||||||
affiliate_unit: form.value.affiliate_unit,
|
|
||||||
department: form.value.department,
|
|
||||||
position: form.value.position,
|
|
||||||
education: form.value.education,
|
|
||||||
nation: form.value.nation,
|
|
||||||
wechat: form.value.wechat,
|
|
||||||
email: form.value.email,
|
|
||||||
home_address: form.value.home_address,
|
|
||||||
account_status: form.value.account_status,
|
|
||||||
status: form.value.account_status,
|
|
||||||
};
|
|
||||||
|
|
||||||
// 只有在填写了密码时才添加到提交数据中
|
|
||||||
if (form.value.password) {
|
|
||||||
submitData.password = form.value.password;
|
|
||||||
}
|
|
||||||
|
|
||||||
await editEmployee(form.value.id, submitData);
|
|
||||||
ElMessage.success("更新成功");
|
|
||||||
}
|
|
||||||
|
|
||||||
visible.value = false;
|
|
||||||
emit("submit");
|
|
||||||
} catch (e: any) {
|
|
||||||
const errorMsg = e?.response?.data?.message || e?.message || "操作失败";
|
|
||||||
ElMessage.error(errorMsg);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 暴露方法给父组件
|
|
||||||
defineExpose({
|
|
||||||
loadUserData,
|
|
||||||
openAdd: () => {
|
|
||||||
isAdd.value = true;
|
|
||||||
form.value = {
|
|
||||||
id: 0,
|
|
||||||
tid: 1,
|
|
||||||
tenant_id: 1,
|
|
||||||
account: "",
|
|
||||||
name: "",
|
|
||||||
phone: "",
|
|
||||||
qq: "",
|
|
||||||
sex: 0,
|
|
||||||
birthday: "",
|
|
||||||
affiliate_unit: "",
|
|
||||||
department: "",
|
|
||||||
position: "",
|
|
||||||
education: "",
|
|
||||||
nation: "",
|
|
||||||
wechat: "",
|
|
||||||
home_address: "",
|
|
||||||
password: "",
|
|
||||||
confirmPassword: "",
|
|
||||||
email: "",
|
|
||||||
account_status: 1,
|
|
||||||
status: 1,
|
|
||||||
};
|
|
||||||
loadCompanyList();
|
|
||||||
visible.value = true;
|
|
||||||
// 清除表单验证
|
|
||||||
if (formRef.value) {
|
|
||||||
formRef.value.clearValidate();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
openEdit: (user: any) => {
|
|
||||||
isAdd.value = false;
|
|
||||||
loadCompanyList();
|
|
||||||
visible.value = true;
|
|
||||||
// 清除表单验证
|
|
||||||
if (formRef.value) {
|
|
||||||
formRef.value.clearValidate();
|
|
||||||
}
|
|
||||||
// 异步加载用户详细信息
|
|
||||||
loadUserData(user);
|
|
||||||
},
|
|
||||||
open: (user?: any) => {
|
|
||||||
loadCompanyList();
|
|
||||||
if (user) {
|
|
||||||
isAdd.value = false;
|
|
||||||
loadUserData(user);
|
|
||||||
} else {
|
|
||||||
isAdd.value = true;
|
|
||||||
form.value = {
|
|
||||||
id: 0,
|
|
||||||
tid: 1,
|
|
||||||
tenant_id: 1,
|
|
||||||
account: "",
|
|
||||||
name: "",
|
|
||||||
phone: "",
|
|
||||||
qq: "",
|
|
||||||
sex: 0,
|
|
||||||
birthday: "",
|
|
||||||
affiliate_unit: "",
|
|
||||||
department: "",
|
|
||||||
position: "",
|
|
||||||
education: "",
|
|
||||||
nation: "",
|
|
||||||
wechat: "",
|
|
||||||
home_address: "",
|
|
||||||
password: "",
|
|
||||||
confirmPassword: "",
|
|
||||||
email: "",
|
|
||||||
account_status: 1,
|
|
||||||
status: 1,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
visible.value = true;
|
|
||||||
// 清除表单验证
|
|
||||||
if (formRef.value) {
|
|
||||||
formRef.value.clearValidate();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="less" scoped>
|
|
||||||
.form-title {
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 600;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
<template>
|
|
||||||
<el-drawer v-model="visible" title="查看用户" width="500px">
|
|
||||||
<el-descriptions :column="1" border>
|
|
||||||
<el-descriptions-item label="ID">{{ detail.id || "-" }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="账号">{{ detail.account || "-" }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="姓名">{{ detail.name || "-" }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="性别">{{ genderText }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="生日">{{ detail.birthday || "-" }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="隶属单位">{{ detail.affiliate_unit_name || detail.affiliate_unit || "-" }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="部门">{{ detail.department_name || detail.department || "-" }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="职位">{{ detail.position || "-" }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="学历">{{ detail.education || "-" }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="民族">{{ detail.nation || "-" }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="手机">{{ detail.phone || "-" }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="微信">{{ detail.wechat || "-" }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="邮箱">{{ detail.email || "-" }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="家庭住址">{{ detail.home_address || "-" }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="账号状态">
|
|
||||||
<el-tag :type="statusValue === 1 ? 'success' : 'danger'">
|
|
||||||
{{ statusText }}
|
|
||||||
</el-tag>
|
|
||||||
</el-descriptions-item>
|
|
||||||
</el-descriptions>
|
|
||||||
|
|
||||||
<template #footer>
|
|
||||||
<el-button @click="visible = false">关闭</el-button>
|
|
||||||
</template>
|
|
||||||
</el-drawer>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { computed, ref } from "vue";
|
|
||||||
import { ElMessage } from "element-plus";
|
|
||||||
import { getEmployeeDetail } from "@/api/erp";
|
|
||||||
|
|
||||||
const visible = ref(false);
|
|
||||||
const detail = ref<any>({});
|
|
||||||
|
|
||||||
const genderValue = computed(() => {
|
|
||||||
const value = detail.value.sex ?? detail.value.gender ?? 0;
|
|
||||||
return Number(value);
|
|
||||||
});
|
|
||||||
|
|
||||||
const genderText = computed(() => {
|
|
||||||
if (genderValue.value === 1) {
|
|
||||||
return "男";
|
|
||||||
}
|
|
||||||
if (genderValue.value === 2) {
|
|
||||||
return "女";
|
|
||||||
}
|
|
||||||
return "未知";
|
|
||||||
});
|
|
||||||
|
|
||||||
const statusValue = computed(() => {
|
|
||||||
const value = detail.value.account_status ?? detail.value.status ?? 0;
|
|
||||||
return Number(value);
|
|
||||||
});
|
|
||||||
|
|
||||||
const statusText = computed(() => {
|
|
||||||
if (statusValue.value === 1) {
|
|
||||||
return "启用";
|
|
||||||
}
|
|
||||||
if (statusValue.value === 2) {
|
|
||||||
return "离职";
|
|
||||||
}
|
|
||||||
return "禁用";
|
|
||||||
});
|
|
||||||
|
|
||||||
const loadUserData = async (user: any) => {
|
|
||||||
try {
|
|
||||||
const userId = typeof user === "number" ? user : user?.id || user?.userId;
|
|
||||||
if (!userId) {
|
|
||||||
throw new Error("未提供有效的用户 ID");
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await getEmployeeDetail(userId);
|
|
||||||
detail.value = res.data || res || {};
|
|
||||||
} catch (e: any) {
|
|
||||||
const errorMsg = e?.response?.data?.message || e?.message || "加载用户失败";
|
|
||||||
ElMessage.error(errorMsg);
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const openView = async (user: any) => {
|
|
||||||
visible.value = true;
|
|
||||||
await loadUserData(user);
|
|
||||||
};
|
|
||||||
|
|
||||||
defineExpose({
|
|
||||||
loadUserData,
|
|
||||||
openView,
|
|
||||||
open: openView,
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="less" scoped></style>
|
|
||||||
@@ -1,201 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="container-box">
|
<!-- 进销存的员工管理界面,数据与办公自动化共用 -->
|
||||||
<div class="header-bar">
|
<EmployeePage module="erp" />
|
||||||
<h2>用户管理</h2>
|
|
||||||
<div class="header-actions">
|
|
||||||
<el-button type="primary" @click="handleAddUser">
|
|
||||||
<el-icon>
|
|
||||||
<Plus />
|
|
||||||
</el-icon>
|
|
||||||
添加用户
|
|
||||||
</el-button>
|
|
||||||
<el-button @click="refresh">
|
|
||||||
<el-icon>
|
|
||||||
<Refresh />
|
|
||||||
</el-icon>
|
|
||||||
刷新
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<el-divider></el-divider>
|
|
||||||
|
|
||||||
<!-- 用户列表表格 -->
|
|
||||||
<el-table :data="users" style="width: 100%" v-loading="loading">
|
|
||||||
<el-table-column prop="id" label="ID" align="center" fixed="left" width="80" />
|
|
||||||
<el-table-column prop="account" label="账号" align="center" width="120" />
|
|
||||||
<el-table-column prop="name" label="姓名" align="center" width="120">
|
|
||||||
<template #default="scope">
|
|
||||||
<span class="name-link" @click="handlePreview(scope.row)">{{ scope.row.name }}</span>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="隶属单位" align="center">
|
|
||||||
<template #default="scope">
|
|
||||||
{{ scope.row.affiliate_unit_name || scope.row.affiliate_unit || "-" }}
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="部门" align="center">
|
|
||||||
<template #default="scope">
|
|
||||||
{{ scope.row.department_name || scope.row.department || "-" }}
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column prop="position" label="职位" align="center" />
|
|
||||||
<el-table-column prop="account_status" label="账号状态" width="80" align="center">
|
|
||||||
<template #default="scope">
|
|
||||||
<el-tag :type="getStatusValue(scope.row) === 1 ? 'success' : 'danger'">{{
|
|
||||||
getStatusValue(scope.row) === 1 ? "启用" : getStatusValue(scope.row) === 2 ? "离职" : "禁用"
|
|
||||||
}}</el-tag>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="操作" width="240" align="center" fixed="right">
|
|
||||||
<template #default="scope"><!-- -->
|
|
||||||
<el-button size="small" @click="handlePreview(scope.row)">查看</el-button>
|
|
||||||
<el-button size="small" @click="handleEdit(scope.row)">编辑</el-button>
|
|
||||||
<el-button size="small" type="warning" @click="handleChangePassword(scope.row)">
|
|
||||||
修改密码
|
|
||||||
</el-button>
|
|
||||||
<el-button v-if="scope.row.username !== 'admin' && scope.row.id !== 1" size="small" type="danger"
|
|
||||||
@click="handleDelete(scope.row)">删除</el-button>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
|
|
||||||
<!-- 分页 -->
|
|
||||||
<div class="pagination-bar">
|
|
||||||
<el-pagination :current-page="page" :page-size="pageSize" :total="total" @current-change="handlePageChange"
|
|
||||||
layout="total, prev, pager, next" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 查看弹窗 -->
|
|
||||||
<View ref="viewRef" />
|
|
||||||
|
|
||||||
<!-- 修改密码弹窗 -->
|
|
||||||
<ChangePass ref="changePassRef" @submit="fetchData" />
|
|
||||||
|
|
||||||
<!-- 编辑弹窗 -->
|
|
||||||
<Edit ref="editRef" @submit="fetchData" />
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup>
|
||||||
import { ref, onMounted } from "vue";
|
import EmployeePage from '@/views/apps/organization/components/EmployeePage.vue';
|
||||||
import { ElMessage, ElMessageBox } from "element-plus";
|
|
||||||
import { Plus, Refresh } from "@element-plus/icons-vue";
|
|
||||||
import { getEmployeeList, deleteEmployee } from "@/api/erp";
|
|
||||||
import Edit from "./components/edit.vue";
|
|
||||||
import View from "./components/view.vue";
|
|
||||||
import ChangePass from "./components/changepass.vue";
|
|
||||||
import { useAuthStore } from "@/stores/auth";
|
|
||||||
|
|
||||||
const users = ref<any[]>([]);
|
|
||||||
const loading = ref(false);
|
|
||||||
const page = ref(1);
|
|
||||||
const pageSize = ref(10);
|
|
||||||
const total = ref(0);
|
|
||||||
const editRef = ref<any>(null);
|
|
||||||
const viewRef = ref<any>(null);
|
|
||||||
const changePassRef = ref<any>(null);
|
|
||||||
|
|
||||||
const getStatusValue = (row: any) => {
|
|
||||||
const status = row.account_status ?? row.status ?? 0;
|
|
||||||
return Number(status);
|
|
||||||
};
|
|
||||||
|
|
||||||
const authStore = useAuthStore();
|
|
||||||
const tenantId = (authStore.user as any)?.tid;
|
|
||||||
|
|
||||||
// console.log(tenantId);
|
|
||||||
|
|
||||||
// 获取列表数据
|
|
||||||
const fetchData = async (currentTenantId = tenantId) => {
|
|
||||||
loading.value = true;
|
|
||||||
try {
|
|
||||||
const res = await getEmployeeList(currentTenantId);
|
|
||||||
users.value = res.data || res || [];
|
|
||||||
total.value = users.value.length;
|
|
||||||
} catch (e: any) {
|
|
||||||
ElMessage.error(e?.message || "获取列表失败");
|
|
||||||
} finally {
|
|
||||||
loading.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 刷新
|
|
||||||
const refresh = () => {
|
|
||||||
fetchData(tenantId);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 添加用户
|
|
||||||
const handleAddUser = () => {
|
|
||||||
editRef.value?.openAdd();
|
|
||||||
};
|
|
||||||
|
|
||||||
// 编辑用户
|
|
||||||
const handleEdit = (row: any) => {
|
|
||||||
editRef.value?.openEdit(row);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleChangePassword = (row: any) => {
|
|
||||||
changePassRef.value?.openChangePass(row);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 查看
|
|
||||||
const handlePreview = (row: any) => {
|
|
||||||
viewRef.value?.openView(row);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 删除用户
|
|
||||||
const handleDelete = async (row: any) => {
|
|
||||||
try {
|
|
||||||
await ElMessageBox.confirm(`确定删除用户 "${row.name}" 吗?`, "提示", {
|
|
||||||
type: "warning",
|
|
||||||
});
|
|
||||||
await deleteEmployee(row.id);
|
|
||||||
ElMessage.success("删除成功");
|
|
||||||
fetchData(tenantId);
|
|
||||||
} catch (e: any) {
|
|
||||||
if (e !== "cancel") {
|
|
||||||
ElMessage.error(e?.message || "删除失败");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 分页变化
|
|
||||||
const handlePageChange = (val: number) => {
|
|
||||||
page.value = val;
|
|
||||||
fetchData(tenantId);
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
fetchData(tenantId);
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
|
||||||
.card-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
span {
|
|
||||||
font-size: 1.4rem;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-alert__title) {
|
|
||||||
color: #f56c6c !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.name-link {
|
|
||||||
color: #3973ff;
|
|
||||||
cursor: pointer;
|
|
||||||
text-decoration: none;
|
|
||||||
transition: color 0.3s;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: #66b1ff;
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -1,252 +0,0 @@
|
|||||||
<template>
|
|
||||||
<el-dialog v-model="visible" :title="isEdit ? '编辑组织' : '添加组织'" width="500px" :close-on-click-modal="false">
|
|
||||||
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="100px">
|
|
||||||
|
|
||||||
<el-form-item label="上级组织" prop="parent_id">
|
|
||||||
<el-tree-select v-model="formData.parent_id" :data="extendedTreeData"
|
|
||||||
:props="props.treeProps || defaultTreeProps" check-strictly placeholder="请选择上级组织" clearable
|
|
||||||
style="width: 100%" />
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<el-form-item label="组织名称" prop="org_name">
|
|
||||||
<el-input v-model="formData.org_name" placeholder="请输入组织名称,例如:连云港云泽集团" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="组织编码" prop="org_code">
|
|
||||||
<el-input v-model="formData.org_code" placeholder="请输入组织编码,例如:LYG-YZ-GROUP" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="负责人" prop="leader_id">
|
|
||||||
<el-select v-model="formData.leader_id" placeholder="请选择负责人" filterable clearable style="width: 100%">
|
|
||||||
<el-option
|
|
||||||
v-for="item in leaderList"
|
|
||||||
:key="item.id"
|
|
||||||
:label="item.name || item.account || String(item.id)"
|
|
||||||
:value="item.id"
|
|
||||||
/>
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="排序" prop="sort">
|
|
||||||
<el-input-number v-model="formData.sort" :min="0" :max="999" controls-position="right" style="width: 100%" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="是否是公司" prop="is_company">
|
|
||||||
<el-radio-group v-model="formData.is_company">
|
|
||||||
<el-radio :label="1">是</el-radio>
|
|
||||||
<el-radio :label="0">否</el-radio>
|
|
||||||
</el-radio-group>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="状态" prop="status">
|
|
||||||
<el-radio-group v-model="formData.status">
|
|
||||||
<el-radio :label="1">启用</el-radio>
|
|
||||||
<el-radio :label="0">禁用</el-radio>
|
|
||||||
</el-radio-group>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="备注" prop="remark">
|
|
||||||
<el-input v-model="formData.remark" type="textarea" :rows="3" placeholder="请输入备注" />
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
<template #footer>
|
|
||||||
<el-button @click="handleClose">取消</el-button>
|
|
||||||
<el-button type="primary" :loading="loading" @click="handleSubmit">确定</el-button>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
</template>
|
|
||||||
<script setup lang="ts">
|
|
||||||
|
|
||||||
import { ref, reactive, onMounted, computed } from 'vue'
|
|
||||||
import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
|
|
||||||
import { createOrganization, editOrganization } from '@/api/erp'
|
|
||||||
import { useAuthStore } from '@/stores/auth'
|
|
||||||
import { getTenantUsers } from '@/api/user'
|
|
||||||
|
|
||||||
interface TreeNode {
|
|
||||||
id: number
|
|
||||||
org_name: string
|
|
||||||
org_code?: string
|
|
||||||
parent_id?: number
|
|
||||||
parent_name?: string
|
|
||||||
leader_id?: number
|
|
||||||
sort?: number
|
|
||||||
status?: number
|
|
||||||
is_company?: number
|
|
||||||
remark?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
treeData: TreeNode[]
|
|
||||||
treeProps?: {
|
|
||||||
label: string
|
|
||||||
children: string
|
|
||||||
value: string
|
|
||||||
}
|
|
||||||
}>()
|
|
||||||
|
|
||||||
// 默认 treeProps
|
|
||||||
const defaultTreeProps = {
|
|
||||||
label: 'org_name',
|
|
||||||
children: 'children',
|
|
||||||
value: 'id'
|
|
||||||
}
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
success: []
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const visible = ref(false)
|
|
||||||
const loading = ref(false)
|
|
||||||
const formRef = ref<FormInstance>()
|
|
||||||
const isEdit = ref(false)
|
|
||||||
const authStore = useAuthStore()
|
|
||||||
const tenantId = (authStore.user as any)?.tid
|
|
||||||
const leaderList = ref<Array<{ id: number; name?: string; account?: string }>>([])
|
|
||||||
const formData = reactive({
|
|
||||||
id: 0,
|
|
||||||
parent_id: undefined as number | undefined,
|
|
||||||
org_name: '',
|
|
||||||
org_code: '',
|
|
||||||
leader_id: undefined as number | undefined,
|
|
||||||
sort: 0,
|
|
||||||
is_company: 0,
|
|
||||||
status: 1,
|
|
||||||
remark: ''
|
|
||||||
})
|
|
||||||
|
|
||||||
const formRules: FormRules = {
|
|
||||||
org_name: [{ required: true, message: '请输入组织名称', trigger: 'blur' }],
|
|
||||||
org_code: [{ required: true, message: '请输入组织编码', trigger: 'blur' }]
|
|
||||||
}
|
|
||||||
|
|
||||||
const treeOptions = computed(() => {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
id: 0,
|
|
||||||
org_name: '顶级组织', // 显示名称
|
|
||||||
children: props.treeData // 将原有的树结构作为其子节点(或者并列,取决于你的业务)
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
|
|
||||||
// 如果你希望“顶级组织”是作为一个独立的根选项,而不是所有节点的父级:
|
|
||||||
const extendedTreeData = computed(() => {
|
|
||||||
const rootNode = { id: 0, org_name: '顶级组织' }
|
|
||||||
return [rootNode, ...props.treeData]
|
|
||||||
})
|
|
||||||
|
|
||||||
const getLeaderList = async () => {
|
|
||||||
if (!tenantId) {
|
|
||||||
leaderList.value = []
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const res: any = await getTenantUsers(tenantId)
|
|
||||||
const responseData = res?.data?.code !== undefined ? res.data : res
|
|
||||||
|
|
||||||
if (responseData?.code === 200) {
|
|
||||||
const list = responseData.data?.list || responseData.data || []
|
|
||||||
leaderList.value = list.map((item: any) => ({
|
|
||||||
...item,
|
|
||||||
id: Number(item.id)
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 打开弹窗
|
|
||||||
const open = (data?: TreeNode, parentId?: number) => {
|
|
||||||
getLeaderList()
|
|
||||||
if (data) {
|
|
||||||
// 编辑:只拷贝需要的字段,避免把 children 等多余字段带到表单
|
|
||||||
isEdit.value = true
|
|
||||||
formData.id = data.id
|
|
||||||
formData.parent_id = data.parent_id ?? 0
|
|
||||||
formData.org_name = data.org_name
|
|
||||||
formData.org_code = data.org_code || ''
|
|
||||||
formData.leader_id = data.leader_id === undefined || data.leader_id === null ? undefined : Number(data.leader_id)
|
|
||||||
formData.is_company = data.is_company ?? 0
|
|
||||||
formData.sort = data.sort ?? 0
|
|
||||||
formData.status = data.status ?? 1
|
|
||||||
formData.remark = data.remark || ''
|
|
||||||
} else {
|
|
||||||
// 新增
|
|
||||||
isEdit.value = false
|
|
||||||
formData.id = 0
|
|
||||||
formData.parent_id = parentId ?? 0
|
|
||||||
formData.org_name = ''
|
|
||||||
formData.org_code = ''
|
|
||||||
formData.leader_id = undefined
|
|
||||||
formData.is_company = 0
|
|
||||||
formData.sort = 0
|
|
||||||
formData.status = 1
|
|
||||||
formData.remark = ''
|
|
||||||
}
|
|
||||||
visible.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// 关闭弹窗
|
|
||||||
const handleClose = () => {
|
|
||||||
visible.value = false
|
|
||||||
formRef.value?.resetFields()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 提交
|
|
||||||
const handleSubmit = async () => {
|
|
||||||
if (!formRef.value) return
|
|
||||||
|
|
||||||
await formRef.value.validate()
|
|
||||||
loading.value = true
|
|
||||||
|
|
||||||
try {
|
|
||||||
let res
|
|
||||||
|
|
||||||
|
|
||||||
// 只允许提交到后台的字段白名单
|
|
||||||
const allowedKeys = ['parent_id', 'org_name', 'org_code', 'leader_id', 'sort', 'is_company', 'status', 'remark']
|
|
||||||
|
|
||||||
if (isEdit.value && formData.id) {
|
|
||||||
// 编辑:使用普通对象提交
|
|
||||||
const submitData: Record<string, any> = {}
|
|
||||||
allowedKeys.forEach(key => {
|
|
||||||
const value = (formData as any)[key]
|
|
||||||
if (value !== undefined && value !== null) {
|
|
||||||
submitData[key] = value
|
|
||||||
}
|
|
||||||
})
|
|
||||||
if (tenantId) {
|
|
||||||
submitData.tid = tenantId
|
|
||||||
}
|
|
||||||
res = await editOrganization(formData.id, submitData)
|
|
||||||
} else {
|
|
||||||
// 新增:使用 FormData 以兼容后端 multipart/form-data
|
|
||||||
const submitData = new FormData()
|
|
||||||
allowedKeys.forEach(key => {
|
|
||||||
const value = (formData as any)[key]
|
|
||||||
if (value !== undefined && value !== null) {
|
|
||||||
submitData.append(key, String(value))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
if (tenantId) {
|
|
||||||
submitData.append('tid', String(tenantId))
|
|
||||||
}
|
|
||||||
res = await createOrganization(submitData)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (res.code === 200) {
|
|
||||||
ElMessage.success(isEdit.value ? '编辑成功' : '添加成功')
|
|
||||||
emit('success')
|
|
||||||
handleClose()
|
|
||||||
} else {
|
|
||||||
ElMessage.error(res.msg || (isEdit.value ? '编辑失败' : '添加失败'))
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('提交失败:', error)
|
|
||||||
ElMessage.error('提交失败')
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
getLeaderList()
|
|
||||||
})
|
|
||||||
|
|
||||||
defineExpose({ open })
|
|
||||||
|
|
||||||
</script>
|
|
||||||
@@ -1,358 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="organization-container">
|
<!-- 进销存的组织架构界面,数据与办公自动化共用 -->
|
||||||
<el-row :gutter="16">
|
<OrganizationPage module="erp" />
|
||||||
<!-- 左侧:组织架构树 -->
|
|
||||||
<el-col :span="8">
|
|
||||||
<el-card shadow="hover" class="tree-card" v-loading="loading">
|
|
||||||
<template #header>
|
|
||||||
<div class="card-header">
|
|
||||||
<span>组织架构</span>
|
|
||||||
<el-button type="primary" link @click="handleAddRoot">
|
|
||||||
<el-icon><Plus /></el-icon>
|
|
||||||
添加
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<el-tree
|
|
||||||
ref="treeRef"
|
|
||||||
:data="treeData"
|
|
||||||
:props="treeProps"
|
|
||||||
node-key="id"
|
|
||||||
default-expand-all
|
|
||||||
:expand-on-click-node="false"
|
|
||||||
@node-click="handleNodeClick"
|
|
||||||
>
|
|
||||||
<template #default="{ node, data }">
|
|
||||||
<div class="tree-node">
|
|
||||||
<span class="node-label">{{ node.label }}</span>
|
|
||||||
<div class="node-actions" @click.stop>
|
|
||||||
<el-button link type="primary" size="small" @click="handleAdd(data)">
|
|
||||||
<el-icon><Plus /></el-icon>
|
|
||||||
</el-button>
|
|
||||||
<el-button link type="primary" size="small" @click="handleEdit(data)">
|
|
||||||
<el-icon><Edit /></el-icon>
|
|
||||||
</el-button>
|
|
||||||
<el-button link type="danger" size="small" @click="handleDelete(data)">
|
|
||||||
<el-icon><Delete /></el-icon>
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</el-tree>
|
|
||||||
</el-card>
|
|
||||||
</el-col>
|
|
||||||
|
|
||||||
<!-- 右侧:详情面板 -->
|
|
||||||
<el-col :span="16">
|
|
||||||
<el-card shadow="hover" class="detail-card">
|
|
||||||
<template #header>
|
|
||||||
<div class="card-header">
|
|
||||||
<span>{{ detailTitle }}</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<div v-if="selectedNode" class="detail-content">
|
|
||||||
<el-descriptions :column="2" border>
|
|
||||||
<el-descriptions-item label="组织名称">
|
|
||||||
{{ selectedNode.org_name }}
|
|
||||||
</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="组织编码">
|
|
||||||
{{ selectedNode.org_code || '-' }}
|
|
||||||
</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="上级组织">
|
|
||||||
{{ selectedNode.parent_name || '顶级组织' }}
|
|
||||||
</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="负责人">
|
|
||||||
{{ selectedNode.leader_name || '-' }}
|
|
||||||
</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="是否是公司">
|
|
||||||
<el-tag :type="selectedNode.is_company === 1 ? 'primary' : 'info'">
|
|
||||||
{{ selectedNode.is_company === 1 ? '是' : '否' }}
|
|
||||||
</el-tag>
|
|
||||||
</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="状态">
|
|
||||||
<el-tag :type="selectedNode.status === 1 ? 'success' : 'danger'">
|
|
||||||
{{ selectedNode.status === 1 ? '启用' : '禁用' }}
|
|
||||||
</el-tag>
|
|
||||||
</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="备注" :span="2">
|
|
||||||
{{ selectedNode.remark || '-' }}
|
|
||||||
</el-descriptions-item>
|
|
||||||
</el-descriptions>
|
|
||||||
|
|
||||||
<div class="detail-actions">
|
|
||||||
<el-button type="primary" @click="handleEdit(selectedNode)">
|
|
||||||
<el-icon><Edit /></el-icon>
|
|
||||||
编辑
|
|
||||||
</el-button>
|
|
||||||
<el-button type="danger" @click="handleDelete(selectedNode)">
|
|
||||||
<el-icon><Delete /></el-icon>
|
|
||||||
删除
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<el-empty v-else description="请选择左侧组织节点查看详情" />
|
|
||||||
</el-card>
|
|
||||||
</el-col>
|
|
||||||
</el-row>
|
|
||||||
|
|
||||||
<!-- 添加/编辑对话框 -->
|
|
||||||
<OrganizationEdit
|
|
||||||
ref="editRef"
|
|
||||||
:tree-data="treeData"
|
|
||||||
:tree-props="treeProps"
|
|
||||||
@success="handleSuccess"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup>
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import OrganizationPage from '@/views/apps/organization/components/OrganizationPage.vue';
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
|
||||||
import { Plus, Edit, Delete } from '@element-plus/icons-vue'
|
|
||||||
import OrganizationEdit from './components/edit.vue'
|
|
||||||
import { getOrganizationList, getOrganizationDetail, deleteOrganization } from '@/api/erp'
|
|
||||||
|
|
||||||
// 树形结构数据(与后台字段对应)
|
|
||||||
interface TreeNode {
|
|
||||||
id: number
|
|
||||||
tid?: number
|
|
||||||
org_name: string
|
|
||||||
org_code?: string
|
|
||||||
parent_id?: number
|
|
||||||
parent_name?: string
|
|
||||||
leader_id?: number
|
|
||||||
leader_name?: string
|
|
||||||
is_company?: number
|
|
||||||
sort?: number
|
|
||||||
status?: number
|
|
||||||
remark?: string
|
|
||||||
children?: TreeNode[]
|
|
||||||
}
|
|
||||||
|
|
||||||
// 树形数据
|
|
||||||
const treeData = ref<TreeNode[]>([])
|
|
||||||
|
|
||||||
const treeProps = {
|
|
||||||
label: 'org_name',
|
|
||||||
children: 'children',
|
|
||||||
value: 'id'
|
|
||||||
}
|
|
||||||
|
|
||||||
const treeRef = ref()
|
|
||||||
const editRef = ref()
|
|
||||||
const selectedNode = ref<TreeNode | null>(null)
|
|
||||||
const loading = ref(false)
|
|
||||||
|
|
||||||
// 将扁平列表构建为树形结构
|
|
||||||
const buildTree = (list: any[]): TreeNode[] => {
|
|
||||||
const nodeMap = new Map<number, TreeNode>()
|
|
||||||
const roots: TreeNode[] = []
|
|
||||||
|
|
||||||
list.forEach(item => {
|
|
||||||
const node: TreeNode = {
|
|
||||||
id: item.id,
|
|
||||||
tid: item.tid,
|
|
||||||
org_name: item.org_name,
|
|
||||||
org_code: item.org_code,
|
|
||||||
parent_id: item.parent_id,
|
|
||||||
parent_name: item.parent_name,
|
|
||||||
leader_id: item.leader_id,
|
|
||||||
leader_name: item.leader_name,
|
|
||||||
is_company: item.is_company,
|
|
||||||
sort: item.sort,
|
|
||||||
status: item.status,
|
|
||||||
remark: item.remark,
|
|
||||||
children: []
|
|
||||||
}
|
|
||||||
nodeMap.set(node.id, node)
|
|
||||||
})
|
|
||||||
|
|
||||||
nodeMap.forEach(node => {
|
|
||||||
if (node.parent_id && nodeMap.has(node.parent_id)) {
|
|
||||||
const parent = nodeMap.get(node.parent_id)!
|
|
||||||
if (!parent.children) {
|
|
||||||
parent.children = []
|
|
||||||
}
|
|
||||||
parent.children.push(node)
|
|
||||||
} else {
|
|
||||||
roots.push(node)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return roots
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取组织机构详情
|
|
||||||
const loadOrganizationDetail = async (id: number) => {
|
|
||||||
try {
|
|
||||||
const res = await getOrganizationDetail(id)
|
|
||||||
if (res.code === 200) {
|
|
||||||
const data = res.data || null
|
|
||||||
if (data) {
|
|
||||||
selectedNode.value = {
|
|
||||||
id: data.id,
|
|
||||||
tid: data.tid,
|
|
||||||
org_name: data.org_name,
|
|
||||||
org_code: data.org_code,
|
|
||||||
parent_id: data.parent_id,
|
|
||||||
parent_name: data.parent_name,
|
|
||||||
leader_id: data.leader_id,
|
|
||||||
leader_name: data.leader_name,
|
|
||||||
is_company: data.is_company,
|
|
||||||
sort: data.sort,
|
|
||||||
status: data.status,
|
|
||||||
remark: data.remark,
|
|
||||||
children: []
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
selectedNode.value = null
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ElMessage.error(res.msg || '获取组织机构详情失败')
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取组织机构详情失败:', error)
|
|
||||||
ElMessage.error('获取组织机构详情失败')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取组织机构列表
|
|
||||||
const fetchOrganizationList = async () => {
|
|
||||||
loading.value = true
|
|
||||||
try {
|
|
||||||
const res = await getOrganizationList()
|
|
||||||
if (res.code === 200) {
|
|
||||||
const list = res.data || []
|
|
||||||
treeData.value = buildTree(list)
|
|
||||||
|
|
||||||
// 如果当前没有选中的节点且有数据,默认加载第一个节点详情
|
|
||||||
if (!selectedNode.value && list.length) {
|
|
||||||
await loadOrganizationDetail(list[0].id)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ElMessage.error(res.msg || '获取组织机构列表失败')
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取组织机构列表失败:', error)
|
|
||||||
ElMessage.error('获取组织机构列表失败')
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 详情标题
|
|
||||||
const detailTitle = computed(() => {
|
|
||||||
return selectedNode.value ? `部门详情 - ${selectedNode.value.org_name}` : '部门详情'
|
|
||||||
})
|
|
||||||
|
|
||||||
// 点击节点,加载详情
|
|
||||||
const handleNodeClick = (data: TreeNode) => {
|
|
||||||
loadOrganizationDetail(data.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加根节点
|
|
||||||
const handleAddRoot = () => {
|
|
||||||
editRef.value?.open(undefined, undefined)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加子节点
|
|
||||||
const handleAdd = (data: TreeNode) => {
|
|
||||||
editRef.value?.open(undefined, data.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 编辑
|
|
||||||
const handleEdit = (data: TreeNode) => {
|
|
||||||
editRef.value?.open(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 提交成功回调,重新加载列表
|
|
||||||
const handleSuccess = () => {
|
|
||||||
fetchOrganizationList()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 删除
|
|
||||||
const handleDelete = (data: TreeNode) => {
|
|
||||||
ElMessageBox.confirm(`确定要删除部门「${data.org_name}」吗?`, '提示', {
|
|
||||||
type: 'warning'
|
|
||||||
})
|
|
||||||
.then(async () => {
|
|
||||||
try {
|
|
||||||
loading.value = true
|
|
||||||
const res = await deleteOrganization(data.id)
|
|
||||||
if (res.code === 200) {
|
|
||||||
ElMessage.success('删除成功')
|
|
||||||
fetchOrganizationList()
|
|
||||||
} else {
|
|
||||||
ElMessage.error(res.msg || '删除失败')
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('删除失败:', error)
|
|
||||||
ElMessage.error('删除失败')
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => {})
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
fetchOrganizationList()
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
|
||||||
.organization-container {
|
|
||||||
// padding: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tree-card {
|
|
||||||
.card-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.tree-node {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
width: 100%;
|
|
||||||
padding-right: 8px;
|
|
||||||
|
|
||||||
.node-label {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.node-actions {
|
|
||||||
display: none;
|
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover .node-actions {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-card {
|
|
||||||
.card-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-content {
|
|
||||||
.el-descriptions {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 12px;
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<template>
|
||||||
|
<!-- 进销存的职位管理界面,数据与办公自动化共用 -->
|
||||||
|
<PositionPage module="erp" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import PositionPage from '@/views/apps/organization/components/PositionPage.vue';
|
||||||
|
</script>
|
||||||
@@ -1,459 +0,0 @@
|
|||||||
<template>
|
|
||||||
<el-dialog
|
|
||||||
v-model="visible"
|
|
||||||
:title="title"
|
|
||||||
width="600px"
|
|
||||||
:close-on-click-modal="false"
|
|
||||||
@close="handleClose"
|
|
||||||
>
|
|
||||||
<el-form
|
|
||||||
ref="formRef"
|
|
||||||
:model="form"
|
|
||||||
:rules="rules"
|
|
||||||
label-width="120px"
|
|
||||||
label-position="right"
|
|
||||||
>
|
|
||||||
<el-form-item label="姓名" prop="name">
|
|
||||||
<el-input
|
|
||||||
v-model="form.name"
|
|
||||||
placeholder="请输入姓名"
|
|
||||||
maxlength="30"
|
|
||||||
show-word-limit
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<el-form-item label="账号" prop="account">
|
|
||||||
<el-input
|
|
||||||
v-model="form.account"
|
|
||||||
placeholder="请输入账号"
|
|
||||||
maxlength="50"
|
|
||||||
show-word-limit
|
|
||||||
/>
|
|
||||||
<div class="form-tip">
|
|
||||||
账号用于登录系统,建议使用英文或数字
|
|
||||||
</div>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<el-form-item label="密码" prop="password">
|
|
||||||
<el-input
|
|
||||||
v-model="form.password"
|
|
||||||
type="password"
|
|
||||||
placeholder="请输入密码"
|
|
||||||
maxlength="64"
|
|
||||||
show-word-limit
|
|
||||||
v-if="!isEdit"
|
|
||||||
/>
|
|
||||||
<el-input
|
|
||||||
v-model="form.password"
|
|
||||||
type="password"
|
|
||||||
placeholder="留空表示不修改密码"
|
|
||||||
maxlength="64"
|
|
||||||
show-word-limit
|
|
||||||
v-else
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<el-form-item label="性别" prop="gender">
|
|
||||||
<el-radio-group v-model="form.gender">
|
|
||||||
<el-radio :label="1">男</el-radio>
|
|
||||||
<el-radio :label="2">女</el-radio>
|
|
||||||
<el-radio :label="0">未知</el-radio>
|
|
||||||
</el-radio-group>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<el-form-item label="生日" prop="birthday">
|
|
||||||
<el-date-picker
|
|
||||||
v-model="form.birthday"
|
|
||||||
type="date"
|
|
||||||
placeholder="请选择生日"
|
|
||||||
style="width: 100%"
|
|
||||||
value-format="YYYY-MM-DD"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<el-form-item label="所属单位" prop="affiliate_unit">
|
|
||||||
<el-tree-select
|
|
||||||
v-model="form.affiliate_unit"
|
|
||||||
:data="orgOptions"
|
|
||||||
:props="treeProps"
|
|
||||||
placeholder="请选择所属单位"
|
|
||||||
clearable
|
|
||||||
check-strictly
|
|
||||||
style="width: 100%"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<el-form-item label="所属部门" prop="department">
|
|
||||||
<el-tree-select
|
|
||||||
v-model="form.department"
|
|
||||||
:data="orgOptions"
|
|
||||||
:props="treeProps"
|
|
||||||
placeholder="请选择所属部门"
|
|
||||||
clearable
|
|
||||||
check-strictly
|
|
||||||
style="width: 100%"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<el-form-item label="职位" prop="position">
|
|
||||||
<el-select
|
|
||||||
v-model="form.position"
|
|
||||||
:loading="positionLoading"
|
|
||||||
placeholder="请选择职位"
|
|
||||||
filterable
|
|
||||||
allow-create
|
|
||||||
default-first-option
|
|
||||||
clearable
|
|
||||||
style="width: 100%"
|
|
||||||
>
|
|
||||||
<el-option
|
|
||||||
v-for="item in positionOptions"
|
|
||||||
:key="item.id"
|
|
||||||
:label="item.position_name"
|
|
||||||
:value="item.position_name"
|
|
||||||
/>
|
|
||||||
</el-select>
|
|
||||||
<div class="form-tip">
|
|
||||||
可下拉选择职位表中职位,也可直接输入新职位
|
|
||||||
</div>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<el-form-item label="学历" prop="education">
|
|
||||||
<el-select
|
|
||||||
v-model="form.education"
|
|
||||||
placeholder="请选择学历"
|
|
||||||
clearable
|
|
||||||
style="width: 100%"
|
|
||||||
>
|
|
||||||
<el-option label="博士" value="博士" />
|
|
||||||
<el-option label="硕士" value="硕士" />
|
|
||||||
<el-option label="本科" value="本科" />
|
|
||||||
<el-option label="专科" value="专科" />
|
|
||||||
<el-option label="高中" value="高中" />
|
|
||||||
<el-option label="其他" value="其他" />
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<el-form-item label="民族" prop="nation">
|
|
||||||
<el-input
|
|
||||||
v-model="form.nation"
|
|
||||||
placeholder="请输入民族"
|
|
||||||
maxlength="20"
|
|
||||||
show-word-limit
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<el-form-item label="电话" prop="phone">
|
|
||||||
<el-input
|
|
||||||
v-model="form.phone"
|
|
||||||
placeholder="请输入电话"
|
|
||||||
maxlength="20"
|
|
||||||
show-word-limit
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<el-form-item label="微信" prop="wechat">
|
|
||||||
<el-input
|
|
||||||
v-model="form.wechat"
|
|
||||||
placeholder="请输入微信"
|
|
||||||
maxlength="50"
|
|
||||||
show-word-limit
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<el-form-item label="邮箱" prop="email">
|
|
||||||
<el-input
|
|
||||||
v-model="form.email"
|
|
||||||
placeholder="请输入邮箱"
|
|
||||||
maxlength="100"
|
|
||||||
show-word-limit
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<el-form-item label="家庭住址" prop="home_address">
|
|
||||||
<el-input
|
|
||||||
v-model="form.home_address"
|
|
||||||
placeholder="请输入家庭住址"
|
|
||||||
maxlength="255"
|
|
||||||
show-word-limit
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<el-form-item label="状态" prop="account_status">
|
|
||||||
<el-radio-group v-model="form.account_status">
|
|
||||||
<el-radio :label="1">正常</el-radio>
|
|
||||||
<el-radio :label="0">禁用</el-radio>
|
|
||||||
</el-radio-group>
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
|
|
||||||
<template #footer>
|
|
||||||
<div class="dialog-footer">
|
|
||||||
<el-button @click="handleClose">取消</el-button>
|
|
||||||
<el-button type="primary" @click="handleSubmit" :loading="loading">
|
|
||||||
{{ isEdit ? '保存' : '创建' }}
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { computed, ref, watch } from 'vue'
|
|
||||||
import { ElMessage } from 'element-plus'
|
|
||||||
import { getOrganizationList, getPositionList } from '@/api/organization'
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
modelValue: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
formData: {
|
|
||||||
type: Object,
|
|
||||||
default: () => ({})
|
|
||||||
},
|
|
||||||
isEdit: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const emit = defineEmits(['update:modelValue', 'save'])
|
|
||||||
|
|
||||||
const visible = computed({
|
|
||||||
get: () => props.modelValue,
|
|
||||||
set: (value) => emit('update:modelValue', value)
|
|
||||||
})
|
|
||||||
|
|
||||||
const formRef = ref()
|
|
||||||
const loading = ref(false)
|
|
||||||
|
|
||||||
const treeProps = {
|
|
||||||
value: 'id',
|
|
||||||
label: 'org_name',
|
|
||||||
children: 'children'
|
|
||||||
}
|
|
||||||
|
|
||||||
const orgOptions = ref([])
|
|
||||||
const positionOptions = ref([])
|
|
||||||
const positionLoading = ref(false)
|
|
||||||
|
|
||||||
const emptyForm = () => ({
|
|
||||||
name: '',
|
|
||||||
account: '',
|
|
||||||
gender: 0,
|
|
||||||
birthday: '',
|
|
||||||
affiliate_unit: '',
|
|
||||||
department: '',
|
|
||||||
position: '',
|
|
||||||
education: '',
|
|
||||||
nation: '',
|
|
||||||
phone: '',
|
|
||||||
wechat: '',
|
|
||||||
email: '',
|
|
||||||
home_address: '',
|
|
||||||
account_status: 1
|
|
||||||
})
|
|
||||||
|
|
||||||
const form = ref(emptyForm())
|
|
||||||
|
|
||||||
const rules = {
|
|
||||||
name: [
|
|
||||||
{ required: true, message: '请输入姓名', trigger: 'blur' },
|
|
||||||
{ min: 2, max: 30, message: '长度在 2 到 30 个字符', trigger: 'blur' }
|
|
||||||
],
|
|
||||||
account: [
|
|
||||||
{ required: true, message: '请输入账号', trigger: 'blur' },
|
|
||||||
{ min: 2, max: 50, message: '长度在 2 到 50 个字符', trigger: 'blur' },
|
|
||||||
{ pattern: /^[a-zA-Z0-9_-]+$/, message: '只能包含英文、数字、下划线和横线', trigger: 'blur' }
|
|
||||||
],
|
|
||||||
gender: [
|
|
||||||
{ required: true, message: '请选择性别', trigger: 'change' }
|
|
||||||
],
|
|
||||||
birthday: [
|
|
||||||
{ required: false }
|
|
||||||
],
|
|
||||||
affiliate_unit: [
|
|
||||||
{ required: false }
|
|
||||||
],
|
|
||||||
department: [
|
|
||||||
{ required: false }
|
|
||||||
],
|
|
||||||
position: [
|
|
||||||
{ required: false }
|
|
||||||
],
|
|
||||||
education: [
|
|
||||||
{ required: false }
|
|
||||||
],
|
|
||||||
nation: [
|
|
||||||
{ required: false }
|
|
||||||
],
|
|
||||||
phone: [
|
|
||||||
{ required: false },
|
|
||||||
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号码', trigger: 'blur' }
|
|
||||||
],
|
|
||||||
wechat: [
|
|
||||||
{ required: false }
|
|
||||||
],
|
|
||||||
email: [
|
|
||||||
{ required: false },
|
|
||||||
{ type: 'email', message: '请输入正确的邮箱地址', trigger: 'blur' }
|
|
||||||
],
|
|
||||||
home_address: [
|
|
||||||
{ required: false }
|
|
||||||
],
|
|
||||||
account_status: [
|
|
||||||
{ required: true, message: '请选择状态', trigger: 'change' }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
const title = computed(() => props.isEdit ? '编辑员工' : '新建员工')
|
|
||||||
|
|
||||||
const loadOrgOptions = async () => {
|
|
||||||
try {
|
|
||||||
const res = await getOrganizationList()
|
|
||||||
const data = res?.data || res || []
|
|
||||||
orgOptions.value = buildOrgTree(data)
|
|
||||||
} catch (error) {
|
|
||||||
ElMessage.error(error?.message || '加载组织列表失败')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 收集部门自身及其所有上级组织ID(职位可挂在任意层级组织上,如集团级“总经理”)
|
|
||||||
const collectOrgSelfAndAncestors = (deptId) => {
|
|
||||||
const ids = new Set()
|
|
||||||
if (!deptId) return ids
|
|
||||||
const parentMap = {}
|
|
||||||
const walk = (nodes) => {
|
|
||||||
nodes.forEach((n) => {
|
|
||||||
parentMap[n.id] = n.parent_id
|
|
||||||
if (n.children?.length) walk(n.children)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
walk(orgOptions.value)
|
|
||||||
let cur = Number(deptId)
|
|
||||||
while (cur && !ids.has(cur)) {
|
|
||||||
ids.add(cur)
|
|
||||||
cur = parentMap[cur]
|
|
||||||
}
|
|
||||||
return ids
|
|
||||||
}
|
|
||||||
|
|
||||||
// 根据所选部门加载职位选项
|
|
||||||
const loadPositionOptions = async () => {
|
|
||||||
const departmentId = parseInt(form.value.department, 10) || 0
|
|
||||||
positionLoading.value = true
|
|
||||||
try {
|
|
||||||
const res = await getPositionList()
|
|
||||||
const data = res?.data || res || []
|
|
||||||
const list = Array.isArray(data) ? data : []
|
|
||||||
const enabled = list.filter((p) => p.status === 1)
|
|
||||||
if (!departmentId) {
|
|
||||||
positionOptions.value = enabled
|
|
||||||
} else {
|
|
||||||
// 允许选择本部门及其上级组织的职位
|
|
||||||
const allowed = collectOrgSelfAndAncestors(departmentId)
|
|
||||||
positionOptions.value = enabled.filter((p) => allowed.has(Number(p.department_id)))
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
positionOptions.value = []
|
|
||||||
} finally {
|
|
||||||
positionLoading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 部门变化时联动刷新职位并清空已选职位
|
|
||||||
watch(() => form.value.department, (newVal, oldVal) => {
|
|
||||||
if (!props.modelValue) return
|
|
||||||
if (newVal !== oldVal) {
|
|
||||||
if (oldVal !== undefined && oldVal !== '') {
|
|
||||||
form.value.position = ''
|
|
||||||
}
|
|
||||||
loadPositionOptions()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const buildOrgTree = (data) => {
|
|
||||||
const tree = []
|
|
||||||
const map = {}
|
|
||||||
|
|
||||||
// 创建映射
|
|
||||||
data.forEach(item => {
|
|
||||||
map[item.id] = { ...item, children: [] }
|
|
||||||
})
|
|
||||||
|
|
||||||
// 构建树形结构
|
|
||||||
data.forEach(item => {
|
|
||||||
const node = map[item.id]
|
|
||||||
if (item.parent_id === 0) {
|
|
||||||
tree.push(node)
|
|
||||||
} else {
|
|
||||||
const parent = map[item.parent_id]
|
|
||||||
if (parent) {
|
|
||||||
parent.children.push(node)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return tree
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
|
||||||
if (!formRef.value) return
|
|
||||||
|
|
||||||
try {
|
|
||||||
await formRef.value.validate()
|
|
||||||
loading.value = true
|
|
||||||
|
|
||||||
const formData = { ...form.value }
|
|
||||||
|
|
||||||
emit('save', formData)
|
|
||||||
loading.value = false
|
|
||||||
} catch (error) {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleClose = () => {
|
|
||||||
visible.value = false
|
|
||||||
formRef.value?.resetFields()
|
|
||||||
form.value = emptyForm()
|
|
||||||
}
|
|
||||||
|
|
||||||
const initForm = () => {
|
|
||||||
if (props.isEdit && props.formData) {
|
|
||||||
// affiliate_unit/department 存组织ID(字符串),转数字以匹配树节点value;一次性赋值避免触发部门watch清空职位
|
|
||||||
const toOrgId = (val) => (val === null || val === undefined || val === '' ? '' : Number(val))
|
|
||||||
form.value = {
|
|
||||||
...props.formData,
|
|
||||||
affiliate_unit: toOrgId(props.formData.affiliate_unit),
|
|
||||||
department: toOrgId(props.formData.department)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
form.value = emptyForm()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(() => props.modelValue, async (newVal) => {
|
|
||||||
if (newVal) {
|
|
||||||
// 先加载下拉选项再回填表单,确保已有值能正确映射label
|
|
||||||
await Promise.all([loadOrgOptions(), loadPositionOptions()])
|
|
||||||
initForm()
|
|
||||||
}
|
|
||||||
}, { immediate: true })
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="less" scoped>
|
|
||||||
.form-tip {
|
|
||||||
margin-top: 4px;
|
|
||||||
font-size: 12px;
|
|
||||||
color: #909399;
|
|
||||||
line-height: 1.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog-footer {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,307 +0,0 @@
|
|||||||
<template>
|
|
||||||
<el-dialog
|
|
||||||
v-model="visible"
|
|
||||||
:title="title"
|
|
||||||
width="600px"
|
|
||||||
:close-on-click-modal="false"
|
|
||||||
@close="handleClose"
|
|
||||||
>
|
|
||||||
<el-form
|
|
||||||
ref="formRef"
|
|
||||||
:model="form"
|
|
||||||
:rules="rules"
|
|
||||||
label-width="120px"
|
|
||||||
label-position="right"
|
|
||||||
>
|
|
||||||
<el-form-item label="组织名称" prop="org_name">
|
|
||||||
<el-input
|
|
||||||
v-model="form.org_name"
|
|
||||||
placeholder="请输入组织名称"
|
|
||||||
maxlength="50"
|
|
||||||
show-word-limit
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<el-form-item label="组织编码" prop="org_code">
|
|
||||||
<el-input
|
|
||||||
v-model="form.org_code"
|
|
||||||
placeholder="请输入组织编码"
|
|
||||||
maxlength="20"
|
|
||||||
show-word-limit
|
|
||||||
/>
|
|
||||||
<div class="form-tip">
|
|
||||||
组织编码用于系统内部标识,建议使用英文或数字
|
|
||||||
</div>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<el-form-item label="上级组织" prop="parent_id">
|
|
||||||
<el-tree-select
|
|
||||||
v-model="form.parent_id"
|
|
||||||
:data="orgOptions"
|
|
||||||
:props="treeProps"
|
|
||||||
placeholder="请选择上级组织"
|
|
||||||
clearable
|
|
||||||
check-strictly
|
|
||||||
style="width: 100%"
|
|
||||||
/>
|
|
||||||
<div class="form-tip">
|
|
||||||
选择上级组织,顶级组织请留空
|
|
||||||
</div>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<el-form-item label="组织类型" prop="is_company">
|
|
||||||
<el-radio-group v-model="form.is_company">
|
|
||||||
<el-radio :label="1">企业单位</el-radio>
|
|
||||||
<el-radio :label="0">部门</el-radio>
|
|
||||||
</el-radio-group>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<el-form-item label="负责人" prop="leader_id">
|
|
||||||
<el-select
|
|
||||||
v-model="form.leader_id"
|
|
||||||
placeholder="请选择负责人"
|
|
||||||
clearable
|
|
||||||
style="width: 100%"
|
|
||||||
>
|
|
||||||
<el-option
|
|
||||||
v-for="employee in employeeOptions"
|
|
||||||
:key="employee.id"
|
|
||||||
:label="employee.name"
|
|
||||||
:value="employee.id"
|
|
||||||
/>
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<el-form-item label="排序" prop="sort">
|
|
||||||
<el-input-number
|
|
||||||
v-model="form.sort"
|
|
||||||
:min="0"
|
|
||||||
:max="9999"
|
|
||||||
placeholder="请输入排序值"
|
|
||||||
style="width: 100%"
|
|
||||||
/>
|
|
||||||
<div class="form-tip">
|
|
||||||
数字越小,排序越靠前
|
|
||||||
</div>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<el-form-item label="状态" prop="status">
|
|
||||||
<el-radio-group v-model="form.status">
|
|
||||||
<el-radio :label="1">启用</el-radio>
|
|
||||||
<el-radio :label="0">禁用</el-radio>
|
|
||||||
</el-radio-group>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<el-form-item label="备注" prop="remark">
|
|
||||||
<el-input
|
|
||||||
v-model="form.remark"
|
|
||||||
type="textarea"
|
|
||||||
:rows="3"
|
|
||||||
placeholder="请输入备注信息"
|
|
||||||
maxlength="500"
|
|
||||||
show-word-limit
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
|
|
||||||
<template #footer>
|
|
||||||
<div class="dialog-footer">
|
|
||||||
<el-button @click="handleClose">取消</el-button>
|
|
||||||
<el-button type="primary" @click="handleSubmit" :loading="loading">
|
|
||||||
{{ isEdit ? '保存' : '创建' }}
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { computed, ref, watch } from 'vue'
|
|
||||||
import { ElMessage } from 'element-plus'
|
|
||||||
import { getOrganizationList, getEmployeeList } from '@/api/organization'
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
modelValue: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
formData: {
|
|
||||||
type: Object,
|
|
||||||
default: () => ({})
|
|
||||||
},
|
|
||||||
isEdit: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const emit = defineEmits(['update:modelValue', 'save'])
|
|
||||||
|
|
||||||
const visible = computed({
|
|
||||||
get: () => props.modelValue,
|
|
||||||
set: (value) => emit('update:modelValue', value)
|
|
||||||
})
|
|
||||||
|
|
||||||
const formRef = ref()
|
|
||||||
const loading = ref(false)
|
|
||||||
|
|
||||||
const treeProps = {
|
|
||||||
value: 'id',
|
|
||||||
label: 'org_name',
|
|
||||||
children: 'children'
|
|
||||||
}
|
|
||||||
|
|
||||||
const orgOptions = ref([])
|
|
||||||
const employeeOptions = ref([])
|
|
||||||
|
|
||||||
const emptyForm = () => ({
|
|
||||||
org_name: '',
|
|
||||||
org_code: '',
|
|
||||||
parent_id: null,
|
|
||||||
leader_id: null,
|
|
||||||
is_company: 0,
|
|
||||||
sort: 0,
|
|
||||||
status: 1,
|
|
||||||
remark: ''
|
|
||||||
})
|
|
||||||
|
|
||||||
const form = ref(emptyForm())
|
|
||||||
|
|
||||||
const rules = {
|
|
||||||
org_name: [
|
|
||||||
{ required: true, message: '请输入组织名称', trigger: 'blur' },
|
|
||||||
{ min: 2, max: 50, message: '长度在 2 到 50 个字符', trigger: 'blur' }
|
|
||||||
],
|
|
||||||
org_code: [
|
|
||||||
{ required: true, message: '请输入组织编码', trigger: 'blur' },
|
|
||||||
{ min: 2, max: 20, message: '长度在 2 到 20 个字符', trigger: 'blur' },
|
|
||||||
{ pattern: /^[a-zA-Z0-9_-]+$/, message: '只能包含英文、数字、下划线和横线', trigger: 'blur' }
|
|
||||||
],
|
|
||||||
parent_id: [
|
|
||||||
{ required: true, message: '请选择上级组织', trigger: 'change' }
|
|
||||||
],
|
|
||||||
is_company: [
|
|
||||||
{ required: true, message: '请选择组织类型', trigger: 'change' }
|
|
||||||
],
|
|
||||||
sort: [
|
|
||||||
{ required: true, message: '请输入排序值', trigger: 'blur' }
|
|
||||||
],
|
|
||||||
status: [
|
|
||||||
{ required: true, message: '请选择状态', trigger: 'change' }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
const title = computed(() => props.isEdit ? '编辑组织' : '新建组织')
|
|
||||||
|
|
||||||
const loadOrgOptions = async () => {
|
|
||||||
try {
|
|
||||||
const res = await getOrganizationList()
|
|
||||||
const data = res?.data || res || []
|
|
||||||
orgOptions.value = buildOrgTree(Array.isArray(data) ? data : [])
|
|
||||||
} catch (error) {
|
|
||||||
ElMessage.error(error?.message || '加载组织列表失败')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadEmployeeOptions = async () => {
|
|
||||||
try {
|
|
||||||
const res = await getEmployeeList()
|
|
||||||
const data = res?.data || res || []
|
|
||||||
employeeOptions.value = (Array.isArray(data) ? data : []).map(item => ({
|
|
||||||
id: item.id,
|
|
||||||
name: item.name
|
|
||||||
}))
|
|
||||||
} catch (error) {
|
|
||||||
ElMessage.error(error?.message || '加载员工列表失败')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const buildOrgTree = (data) => {
|
|
||||||
const tree = []
|
|
||||||
const map = {}
|
|
||||||
|
|
||||||
// 创建映射
|
|
||||||
data.forEach(item => {
|
|
||||||
map[item.id] = { ...item, children: [] }
|
|
||||||
})
|
|
||||||
|
|
||||||
// 构建树形结构
|
|
||||||
data.forEach(item => {
|
|
||||||
const node = map[item.id]
|
|
||||||
if (item.parent_id === 0) {
|
|
||||||
tree.push(node)
|
|
||||||
} else {
|
|
||||||
const parent = map[item.parent_id]
|
|
||||||
if (parent) {
|
|
||||||
parent.children.push(node)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return tree
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
|
||||||
if (!formRef.value) return
|
|
||||||
|
|
||||||
try {
|
|
||||||
await formRef.value.validate()
|
|
||||||
loading.value = true
|
|
||||||
|
|
||||||
const formData = { ...form.value }
|
|
||||||
if (formData.parent_id === 0) {
|
|
||||||
formData.parent_id = null
|
|
||||||
}
|
|
||||||
|
|
||||||
emit('save', formData)
|
|
||||||
loading.value = false
|
|
||||||
} catch (error) {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleClose = () => {
|
|
||||||
visible.value = false
|
|
||||||
formRef.value?.resetFields()
|
|
||||||
form.value = emptyForm()
|
|
||||||
}
|
|
||||||
|
|
||||||
const initForm = () => {
|
|
||||||
if (props.isEdit && props.formData) {
|
|
||||||
form.value = {
|
|
||||||
...props.formData,
|
|
||||||
parent_id: props.formData.parent_id || null,
|
|
||||||
leader_id: props.formData.leader_id || null
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 新建:保留父组件预填值(如添加子组织时预设的上级组织)
|
|
||||||
form.value = {
|
|
||||||
...emptyForm(),
|
|
||||||
...props.formData
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(() => props.modelValue, async (newVal) => {
|
|
||||||
if (newVal) {
|
|
||||||
// 先加载下拉选项再回填表单,确保已有值能正确映射label
|
|
||||||
await Promise.all([loadOrgOptions(), loadEmployeeOptions()])
|
|
||||||
initForm()
|
|
||||||
}
|
|
||||||
}, { immediate: true })
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="less" scoped>
|
|
||||||
.form-tip {
|
|
||||||
margin-top: 4px;
|
|
||||||
font-size: 12px;
|
|
||||||
color: #909399;
|
|
||||||
line-height: 1.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog-footer {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,259 +0,0 @@
|
|||||||
<template>
|
|
||||||
<el-dialog
|
|
||||||
v-model="visible"
|
|
||||||
title="组织设置"
|
|
||||||
width="600px"
|
|
||||||
:close-on-click-modal="false"
|
|
||||||
@close="handleClose"
|
|
||||||
>
|
|
||||||
<el-form
|
|
||||||
ref="formRef"
|
|
||||||
:model="form"
|
|
||||||
:rules="rules"
|
|
||||||
label-width="120px"
|
|
||||||
label-position="right"
|
|
||||||
>
|
|
||||||
<el-form-item label="默认组织类型" prop="default_org_type">
|
|
||||||
<el-radio-group v-model="form.default_org_type">
|
|
||||||
<el-radio :label="1">企业单位</el-radio>
|
|
||||||
<el-radio :label="0">部门</el-radio>
|
|
||||||
</el-radio-group>
|
|
||||||
<div class="form-tip">
|
|
||||||
设置新建组织时的默认类型
|
|
||||||
</div>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<el-form-item label="自动生成编码" prop="auto_generate_code">
|
|
||||||
<el-switch
|
|
||||||
v-model="form.auto_generate_code"
|
|
||||||
active-text="开启"
|
|
||||||
inactive-text="关闭"
|
|
||||||
/>
|
|
||||||
<div class="form-tip">
|
|
||||||
开启后系统将自动生成组织编码
|
|
||||||
</div>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<el-form-item label="编码前缀" prop="code_prefix">
|
|
||||||
<el-input
|
|
||||||
v-model="form.code_prefix"
|
|
||||||
placeholder="请输入编码前缀"
|
|
||||||
maxlength="10"
|
|
||||||
show-word-limit
|
|
||||||
:disabled="!form.auto_generate_code"
|
|
||||||
/>
|
|
||||||
<div class="form-tip">
|
|
||||||
自动生成编码时的前缀,如:ORG、DEPT等
|
|
||||||
</div>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<el-form-item label="编码长度" prop="code_length">
|
|
||||||
<el-input-number
|
|
||||||
v-model="form.code_length"
|
|
||||||
:min="4"
|
|
||||||
:max="20"
|
|
||||||
placeholder="请输入编码长度"
|
|
||||||
:disabled="!form.auto_generate_code"
|
|
||||||
/>
|
|
||||||
<div class="form-tip">
|
|
||||||
自动生成编码时的长度,包含前缀
|
|
||||||
</div>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<el-form-item label="默认排序" prop="default_sort">
|
|
||||||
<el-input-number
|
|
||||||
v-model="form.default_sort"
|
|
||||||
:min="0"
|
|
||||||
:max="9999"
|
|
||||||
placeholder="请输入默认排序值"
|
|
||||||
/>
|
|
||||||
<div class="form-tip">
|
|
||||||
新建组织时的默认排序值
|
|
||||||
</div>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<el-form-item label="默认状态" prop="default_status">
|
|
||||||
<el-radio-group v-model="form.default_status">
|
|
||||||
<el-radio :label="1">启用</el-radio>
|
|
||||||
<el-radio :label="0">禁用</el-radio>
|
|
||||||
</el-radio-group>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<el-form-item label="显示层级" prop="max_level">
|
|
||||||
<el-input-number
|
|
||||||
v-model="form.max_level"
|
|
||||||
:min="1"
|
|
||||||
:max="10"
|
|
||||||
placeholder="请输入最大显示层级"
|
|
||||||
/>
|
|
||||||
<div class="form-tip">
|
|
||||||
组织树形结构最多显示的层级数
|
|
||||||
</div>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<el-form-item label="批量操作" prop="batch_operations">
|
|
||||||
<el-switch
|
|
||||||
v-model="form.batch_operations"
|
|
||||||
active-text="开启"
|
|
||||||
inactive-text="关闭"
|
|
||||||
/>
|
|
||||||
<div class="form-tip">
|
|
||||||
开启后支持批量操作组织
|
|
||||||
</div>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<el-form-item label="导出功能" prop="export_enabled">
|
|
||||||
<el-switch
|
|
||||||
v-model="form.export_enabled"
|
|
||||||
active-text="开启"
|
|
||||||
inactive-text="关闭"
|
|
||||||
/>
|
|
||||||
<div class="form-tip">
|
|
||||||
开启后支持导出组织架构数据
|
|
||||||
</div>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<el-form-item label="导入功能" prop="import_enabled">
|
|
||||||
<el-switch
|
|
||||||
v-model="form.import_enabled"
|
|
||||||
active-text="开启"
|
|
||||||
inactive-text="关闭"
|
|
||||||
/>
|
|
||||||
<div class="form-tip">
|
|
||||||
开启后支持导入组织架构数据
|
|
||||||
</div>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<el-form-item label="同步员工" prop="sync_employee">
|
|
||||||
<el-switch
|
|
||||||
v-model="form.sync_employee"
|
|
||||||
active-text="开启"
|
|
||||||
inactive-text="关闭"
|
|
||||||
/>
|
|
||||||
<div class="form-tip">
|
|
||||||
开启后创建组织时自动同步员工信息
|
|
||||||
</div>
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
|
|
||||||
<template #footer>
|
|
||||||
<div class="dialog-footer">
|
|
||||||
<el-button @click="handleClose">取消</el-button>
|
|
||||||
<el-button type="primary" @click="handleSubmit" :loading="loading">
|
|
||||||
保存设置
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { computed, ref, watch } from 'vue'
|
|
||||||
import { ElMessage } from 'element-plus'
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
modelValue: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const emit = defineEmits(['update:modelValue', 'changed'])
|
|
||||||
|
|
||||||
const visible = computed({
|
|
||||||
get: () => props.modelValue,
|
|
||||||
set: (value) => emit('update:modelValue', value)
|
|
||||||
})
|
|
||||||
|
|
||||||
const formRef = ref()
|
|
||||||
const loading = ref(false)
|
|
||||||
|
|
||||||
const emptyForm = () => ({
|
|
||||||
default_org_type: 1,
|
|
||||||
auto_generate_code: true,
|
|
||||||
code_prefix: 'ORG',
|
|
||||||
code_length: 8,
|
|
||||||
default_sort: 0,
|
|
||||||
default_status: 1,
|
|
||||||
max_level: 5,
|
|
||||||
batch_operations: true,
|
|
||||||
export_enabled: true,
|
|
||||||
import_enabled: true,
|
|
||||||
sync_employee: false
|
|
||||||
})
|
|
||||||
|
|
||||||
const form = ref(emptyForm())
|
|
||||||
|
|
||||||
const rules = {
|
|
||||||
default_org_type: [
|
|
||||||
{ required: true, message: '请选择默认组织类型', trigger: 'change' }
|
|
||||||
],
|
|
||||||
auto_generate_code: [
|
|
||||||
{ required: true, message: '请选择是否自动生成编码', trigger: 'change' }
|
|
||||||
],
|
|
||||||
code_prefix: [
|
|
||||||
{ required: true, message: '请输入编码前缀', trigger: 'blur' },
|
|
||||||
{ min: 1, max: 10, message: '长度在 1 到 10 个字符', trigger: 'blur' }
|
|
||||||
],
|
|
||||||
code_length: [
|
|
||||||
{ required: true, message: '请输入编码长度', trigger: 'blur' }
|
|
||||||
],
|
|
||||||
default_sort: [
|
|
||||||
{ required: true, message: '请输入默认排序值', trigger: 'blur' }
|
|
||||||
],
|
|
||||||
default_status: [
|
|
||||||
{ required: true, message: '请选择默认状态', trigger: 'change' }
|
|
||||||
],
|
|
||||||
max_level: [
|
|
||||||
{ required: true, message: '请输入最大显示层级', trigger: 'blur' }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
|
||||||
if (!formRef.value) return
|
|
||||||
|
|
||||||
try {
|
|
||||||
await formRef.value.validate()
|
|
||||||
loading.value = true
|
|
||||||
|
|
||||||
// 这里可以调用API保存设置
|
|
||||||
// await saveOrgSettings(form.value)
|
|
||||||
|
|
||||||
ElMessage.success('设置保存成功')
|
|
||||||
emit('changed')
|
|
||||||
loading.value = false
|
|
||||||
visible.value = false
|
|
||||||
} catch (error) {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleClose = () => {
|
|
||||||
visible.value = false
|
|
||||||
formRef.value?.resetFields()
|
|
||||||
form.value = emptyForm()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 监听自动生成编码开关
|
|
||||||
watch(() => form.value.auto_generate_code, (newVal) => {
|
|
||||||
if (!newVal) {
|
|
||||||
form.value.code_prefix = ''
|
|
||||||
form.value.code_length = 8
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="less" scoped>
|
|
||||||
.form-tip {
|
|
||||||
margin-top: 4px;
|
|
||||||
font-size: 12px;
|
|
||||||
color: #909399;
|
|
||||||
line-height: 1.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog-footer {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -0,0 +1,344 @@
|
|||||||
|
<template>
|
||||||
|
<el-drawer v-model="visible" :title="isEdit ? `编辑${labels.employeeLabel}` : `新增${labels.employeeLabel}`" size="520px">
|
||||||
|
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
|
||||||
|
<div class="form-title">账号信息</div>
|
||||||
|
|
||||||
|
<el-form-item label="账号" prop="account">
|
||||||
|
<el-input v-model="form.account" :disabled="isEdit" placeholder="留空时按组织设置自动生成" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="密码" prop="password">
|
||||||
|
<el-input
|
||||||
|
v-model="form.password"
|
||||||
|
type="password"
|
||||||
|
show-password
|
||||||
|
autocomplete="new-password"
|
||||||
|
:placeholder="isEdit ? '留空表示不修改密码' : '请输入密码(至少6位)'"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="确认密码" prop="confirmPassword">
|
||||||
|
<el-input
|
||||||
|
v-model="form.confirmPassword"
|
||||||
|
type="password"
|
||||||
|
show-password
|
||||||
|
autocomplete="new-password"
|
||||||
|
:placeholder="isEdit ? '留空表示不修改密码' : '请再次输入密码'"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-divider />
|
||||||
|
<div class="form-title">基本信息</div>
|
||||||
|
|
||||||
|
<el-form-item label="姓名" prop="name">
|
||||||
|
<el-input v-model="form.name" maxlength="30" show-word-limit />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="性别" prop="gender">
|
||||||
|
<el-radio-group v-model="form.gender">
|
||||||
|
<el-radio-button :value="0">未知</el-radio-button>
|
||||||
|
<el-radio-button :value="1">男</el-radio-button>
|
||||||
|
<el-radio-button :value="2">女</el-radio-button>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="生日" prop="birthday">
|
||||||
|
<el-date-picker v-model="form.birthday" type="date" value-format="YYYY-MM-DD" style="width: 100%" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-divider />
|
||||||
|
<div class="form-title">组织信息</div>
|
||||||
|
|
||||||
|
<el-form-item label="隶属单位" prop="affiliate_unit">
|
||||||
|
<el-select v-model="form.affiliate_unit" clearable placeholder="请选择隶属单位" style="width: 100%">
|
||||||
|
<el-option
|
||||||
|
v-for="item in companyOptions"
|
||||||
|
:key="item.id"
|
||||||
|
:label="item.org_name"
|
||||||
|
:value="String(item.id)"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="部门" prop="department">
|
||||||
|
<el-tree-select
|
||||||
|
v-model="form.department"
|
||||||
|
:data="departmentTree"
|
||||||
|
:props="treeSelectProps"
|
||||||
|
check-strictly
|
||||||
|
clearable
|
||||||
|
placeholder="请选择部门"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="职位" prop="position">
|
||||||
|
<el-select
|
||||||
|
v-model="form.position"
|
||||||
|
filterable
|
||||||
|
allow-create
|
||||||
|
default-first-option
|
||||||
|
clearable
|
||||||
|
placeholder="选择或输入职位"
|
||||||
|
style="width: 100%"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="item in positionOptions"
|
||||||
|
:key="item.id"
|
||||||
|
:label="item.position_name"
|
||||||
|
:value="item.position_name"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-divider />
|
||||||
|
<div class="form-title">联系与其它</div>
|
||||||
|
|
||||||
|
<el-form-item label="学历" prop="education">
|
||||||
|
<el-select v-model="form.education" clearable placeholder="请选择学历" style="width: 100%">
|
||||||
|
<el-option v-for="item in educationOptions" :key="item" :label="item" :value="item" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="民族" prop="nation">
|
||||||
|
<el-input v-model="form.nation" maxlength="20" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="手机" prop="phone">
|
||||||
|
<el-input v-model="form.phone" maxlength="20" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="微信" prop="wechat">
|
||||||
|
<el-input v-model="form.wechat" maxlength="50" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="邮箱" prop="email">
|
||||||
|
<el-input v-model="form.email" maxlength="100" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="家庭住址" prop="home_address">
|
||||||
|
<el-input v-model="form.home_address" maxlength="255" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="状态" prop="account_status">
|
||||||
|
<el-select v-model="form.account_status" style="width: 100%">
|
||||||
|
<el-option label="启用" :value="1" />
|
||||||
|
<el-option label="禁用" :value="0" />
|
||||||
|
<el-option v-if="isEdit" label="离职" :value="2" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="visible = false">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="loading" @click="handleSubmit">保存</el-button>
|
||||||
|
</template>
|
||||||
|
</el-drawer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, reactive, ref } from 'vue';
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
import { buildOrgTree, moduleLabels, unwrap, useOrganizationApi } from '../composables';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
/** 所属模块:erp(进销存)或 oa(办公自动化) */
|
||||||
|
module: { type: String, default: 'erp' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['success']);
|
||||||
|
|
||||||
|
const api = computed(() => useOrganizationApi(props.module));
|
||||||
|
const labels = computed(() => moduleLabels(props.module));
|
||||||
|
|
||||||
|
const visible = ref(false);
|
||||||
|
const loading = ref(false);
|
||||||
|
const isEdit = ref(false);
|
||||||
|
const editingId = ref(0);
|
||||||
|
const formRef = ref();
|
||||||
|
|
||||||
|
const companyOptions = ref([]);
|
||||||
|
const departmentTree = ref([]);
|
||||||
|
const positionOptions = ref([]);
|
||||||
|
|
||||||
|
const treeSelectProps = { value: 'idStr', label: 'org_name', children: 'children' };
|
||||||
|
const educationOptions = ['博士', '硕士', '本科', '专科', '高中', '其他'];
|
||||||
|
|
||||||
|
const emptyForm = () => ({
|
||||||
|
account: '',
|
||||||
|
password: '',
|
||||||
|
confirmPassword: '',
|
||||||
|
name: '',
|
||||||
|
gender: 0,
|
||||||
|
birthday: '',
|
||||||
|
affiliate_unit: '',
|
||||||
|
department: '',
|
||||||
|
position: '',
|
||||||
|
education: '',
|
||||||
|
nation: '',
|
||||||
|
phone: '',
|
||||||
|
wechat: '',
|
||||||
|
email: '',
|
||||||
|
home_address: '',
|
||||||
|
account_status: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
const form = reactive(emptyForm());
|
||||||
|
|
||||||
|
const validatePassword = (_rule, value, callback) => {
|
||||||
|
if (!isEdit.value && !value) {
|
||||||
|
callback(new Error('请输入密码'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (value && value.length < 6) {
|
||||||
|
callback(new Error('密码长度不能少于 6 位'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
callback();
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateConfirmPassword = (_rule, value, callback) => {
|
||||||
|
if (!form.password && !value) {
|
||||||
|
callback();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (value !== form.password) {
|
||||||
|
callback(new Error('两次输入的密码不一致'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
callback();
|
||||||
|
};
|
||||||
|
|
||||||
|
const rules = {
|
||||||
|
name: [
|
||||||
|
{ required: true, message: '请输入姓名', trigger: 'blur' },
|
||||||
|
{ min: 2, max: 30, message: '长度在 2 到 30 个字符', trigger: 'blur' },
|
||||||
|
],
|
||||||
|
account: [{ pattern: /^[a-zA-Z0-9_-]*$/, message: '只能包含英文、数字、下划线和横线', trigger: 'blur' }],
|
||||||
|
password: [{ validator: validatePassword, trigger: 'blur' }],
|
||||||
|
confirmPassword: [{ validator: validateConfirmPassword, trigger: 'blur' }],
|
||||||
|
phone: [{ pattern: /^$|^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' }],
|
||||||
|
email: [{ pattern: /^$|^[^\s@]+@[^\s@]+\.[^\s@]+$/, message: '请输入正确的邮箱', trigger: 'blur' }],
|
||||||
|
};
|
||||||
|
|
||||||
|
// el-tree-select 的 value 必须与表单中的字符串类型一致,这里补一个 idStr
|
||||||
|
const withIdStr = (nodes) =>
|
||||||
|
nodes.map((node) => ({
|
||||||
|
...node,
|
||||||
|
idStr: String(node.id),
|
||||||
|
children: withIdStr(node.children || []),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const loadOptions = async () => {
|
||||||
|
try {
|
||||||
|
const [companies, orgList, positions] = await Promise.all([
|
||||||
|
api.value.getCompanyList().then(unwrap),
|
||||||
|
api.value.getOrganizationList().then(unwrap),
|
||||||
|
api.value.getPositionList().then(unwrap),
|
||||||
|
]);
|
||||||
|
companyOptions.value = companies || [];
|
||||||
|
departmentTree.value = withIdStr(buildOrgTree(orgList));
|
||||||
|
positionOptions.value = positions || [];
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(error?.message || '加载选项失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const open = async (row, defaults = {}) => {
|
||||||
|
Object.assign(form, emptyForm());
|
||||||
|
isEdit.value = Boolean(row?.id);
|
||||||
|
editingId.value = row?.id || 0;
|
||||||
|
visible.value = true;
|
||||||
|
formRef.value?.clearValidate();
|
||||||
|
|
||||||
|
await loadOptions();
|
||||||
|
|
||||||
|
if (isEdit.value) {
|
||||||
|
try {
|
||||||
|
const detail = unwrap(await api.value.getEmployeeDetail(row.id)) || {};
|
||||||
|
Object.assign(form, {
|
||||||
|
account: detail.account || '',
|
||||||
|
name: detail.name || '',
|
||||||
|
gender: Number(detail.gender ?? detail.sex ?? 0),
|
||||||
|
birthday: detail.birthday || '',
|
||||||
|
affiliate_unit: detail.affiliate_unit ? String(detail.affiliate_unit) : '',
|
||||||
|
department: detail.department ? String(detail.department) : '',
|
||||||
|
position: detail.position || '',
|
||||||
|
education: detail.education || '',
|
||||||
|
nation: detail.nation || '',
|
||||||
|
phone: detail.phone || '',
|
||||||
|
wechat: detail.wechat || '',
|
||||||
|
email: detail.email || '',
|
||||||
|
home_address: detail.home_address || '',
|
||||||
|
account_status: Number(detail.account_status ?? detail.status ?? 1),
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(error?.message || '加载详情失败');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (defaults.department) {
|
||||||
|
form.department = String(defaults.department);
|
||||||
|
}
|
||||||
|
if (defaults.affiliate_unit) {
|
||||||
|
form.affiliate_unit = String(defaults.affiliate_unit);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (!formRef.value) return;
|
||||||
|
try {
|
||||||
|
await formRef.value.validate();
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
account: form.account.trim(),
|
||||||
|
name: form.name.trim(),
|
||||||
|
gender: form.gender,
|
||||||
|
birthday: form.birthday || '',
|
||||||
|
affiliate_unit: form.affiliate_unit || '',
|
||||||
|
department: form.department || '',
|
||||||
|
position: form.position || '',
|
||||||
|
education: form.education || '',
|
||||||
|
nation: form.nation || '',
|
||||||
|
phone: form.phone || '',
|
||||||
|
wechat: form.wechat || '',
|
||||||
|
email: form.email || '',
|
||||||
|
home_address: form.home_address || '',
|
||||||
|
account_status: form.account_status,
|
||||||
|
};
|
||||||
|
if (form.password) {
|
||||||
|
payload.password = form.password;
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
if (isEdit.value) {
|
||||||
|
unwrap(await api.value.updateEmployee(editingId.value, payload));
|
||||||
|
ElMessage.success('保存成功');
|
||||||
|
} else {
|
||||||
|
unwrap(await api.value.createEmployee(payload));
|
||||||
|
ElMessage.success('创建成功');
|
||||||
|
}
|
||||||
|
visible.value = false;
|
||||||
|
emit('success');
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(error?.message || '保存失败');
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
defineExpose({ open });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.form-title {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,284 @@
|
|||||||
|
<template>
|
||||||
|
<div class="employee-page">
|
||||||
|
<div class="page-header">
|
||||||
|
<div>
|
||||||
|
<h2>{{ labels.employeeLabel }}管理</h2>
|
||||||
|
<p>{{ labels.name }}与其它模块共用同一份{{ labels.employeeLabel }}数据</p>
|
||||||
|
</div>
|
||||||
|
<div class="header-actions">
|
||||||
|
<el-button :icon="Refresh" @click="loadData">刷新</el-button>
|
||||||
|
<el-button
|
||||||
|
:disabled="!selectedRows.length"
|
||||||
|
type="primary"
|
||||||
|
plain
|
||||||
|
@click="moveDialogRef?.open(selectedRows)"
|
||||||
|
>
|
||||||
|
批量调岗({{ selectedRows.length }})
|
||||||
|
</el-button>
|
||||||
|
<el-button type="primary" :icon="Plus" @click="editDialogRef?.open(null)">
|
||||||
|
新增{{ labels.employeeLabel }}
|
||||||
|
</el-button>
|
||||||
|
</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="0" />
|
||||||
|
<el-option label="离职" :value="2" />
|
||||||
|
</el-select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-table
|
||||||
|
:data="pagedRows"
|
||||||
|
v-loading="loading"
|
||||||
|
stripe
|
||||||
|
@selection-change="(rows) => (selectedRows = rows)"
|
||||||
|
>
|
||||||
|
<el-table-column type="selection" width="45" />
|
||||||
|
<el-table-column prop="id" label="ID" width="70" align="center" />
|
||||||
|
<el-table-column prop="account" label="账号" width="130" />
|
||||||
|
<el-table-column prop="name" label="姓名" width="110">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span class="name-link" @click="viewDialogRef?.open(row)">{{ row.name }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="隶属单位" min-width="140">
|
||||||
|
<template #default="{ row }">{{ row.affiliate_unit_name || '-' }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="部门" min-width="140">
|
||||||
|
<template #default="{ row }">{{ row.department_name || '-' }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="position" label="职位" min-width="110">
|
||||||
|
<template #default="{ row }">{{ row.position || '-' }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="phone" label="手机" width="130">
|
||||||
|
<template #default="{ row }">{{ row.phone || '-' }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="状态" width="90" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="employeeStatusTagType(row)" size="small">{{ employeeStatusText(row) }}</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="220" fixed="right" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button link type="primary" size="small" @click="viewDialogRef?.open(row)">查看</el-button>
|
||||||
|
<el-button link type="primary" size="small" @click="editDialogRef?.open(row)">编辑</el-button>
|
||||||
|
<el-button link type="primary" size="small" @click="moveDialogRef?.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="`暂无${labels.employeeLabel}数据`" :image-size="90" />
|
||||||
|
</template>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<div class="pagination-bar">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="page"
|
||||||
|
v-model:page-size="pageSize"
|
||||||
|
:total="rows.length"
|
||||||
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<EmployeeEditDialog ref="editDialogRef" :module="module" @success="loadData" />
|
||||||
|
<EmployeeViewDialog ref="viewDialogRef" :module="module" />
|
||||||
|
<MoveEmployeeDialog ref="moveDialogRef" :module="module" @success="handleMoved" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, onMounted, reactive, ref } from 'vue';
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||||
|
import { Plus, Refresh, Search } from '@element-plus/icons-vue';
|
||||||
|
import EmployeeEditDialog from './EmployeeEditDialog.vue';
|
||||||
|
import EmployeeViewDialog from './EmployeeViewDialog.vue';
|
||||||
|
import MoveEmployeeDialog from './MoveEmployeeDialog.vue';
|
||||||
|
import {
|
||||||
|
buildOrgTree,
|
||||||
|
employeeStatusTagType,
|
||||||
|
employeeStatusText,
|
||||||
|
moduleLabels,
|
||||||
|
unwrap,
|
||||||
|
useOrganizationApi,
|
||||||
|
} from '../composables';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
/** 所属模块:erp(进销存)或 oa(办公自动化) */
|
||||||
|
module: { type: String, default: 'erp' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const api = computed(() => useOrganizationApi(props.module));
|
||||||
|
const labels = computed(() => moduleLabels(props.module));
|
||||||
|
|
||||||
|
const editDialogRef = ref();
|
||||||
|
const viewDialogRef = ref();
|
||||||
|
const moveDialogRef = ref();
|
||||||
|
|
||||||
|
const rows = ref([]);
|
||||||
|
const selectedRows = ref([]);
|
||||||
|
const loading = ref(false);
|
||||||
|
const page = ref(1);
|
||||||
|
const pageSize = ref(10);
|
||||||
|
const departmentTree = ref([]);
|
||||||
|
|
||||||
|
const treeSelectProps = { value: 'id', label: 'org_name', children: 'children' };
|
||||||
|
|
||||||
|
const filters = reactive({ keyword: '', org_id: null, status: '' });
|
||||||
|
|
||||||
|
// 后端一次性返回列表,这里在前端做分页,避免额外接口改造
|
||||||
|
const pagedRows = computed(() => {
|
||||||
|
const start = (page.value - 1) * pageSize.value;
|
||||||
|
return rows.value.slice(start, start + pageSize.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
const loadDepartments = async () => {
|
||||||
|
try {
|
||||||
|
departmentTree.value = buildOrgTree(unwrap(await api.value.getOrganizationList()));
|
||||||
|
} catch {
|
||||||
|
departmentTree.value = [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadData = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const params = {};
|
||||||
|
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;
|
||||||
|
|
||||||
|
rows.value = unwrap(await api.value.getEmployeeList(params)) || [];
|
||||||
|
const maxPage = Math.max(1, Math.ceil(rows.value.length / pageSize.value));
|
||||||
|
if (page.value > maxPage) {
|
||||||
|
page.value = maxPage;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(error?.message || '获取列表失败');
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSearch = () => {
|
||||||
|
page.value = 1;
|
||||||
|
loadData();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMoved = () => {
|
||||||
|
selectedRows.value = [];
|
||||||
|
loadData();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async (row) => {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(`确定删除「${row.name}」吗?`, '删除确认', { type: 'warning' });
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
unwrap(await api.value.deleteEmployee(row.id));
|
||||||
|
ElMessage.success('删除成功');
|
||||||
|
await loadData();
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(error?.message || '删除失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadDepartments();
|
||||||
|
loadData();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.employee-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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-bar {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-item {
|
||||||
|
width: 240px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-filter {
|
||||||
|
width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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,77 @@
|
|||||||
|
<template>
|
||||||
|
<el-drawer v-model="visible" title="查看详情" size="480px">
|
||||||
|
<el-descriptions :column="1" border v-loading="loading">
|
||||||
|
<el-descriptions-item label="ID">{{ detail.id || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="账号">{{ detail.account || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="姓名">{{ detail.name || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="性别">{{ genderText(detail) }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="生日">{{ detail.birthday || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="隶属单位">
|
||||||
|
{{ detail.affiliate_unit_name || detail.affiliate_unit || '-' }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="部门">
|
||||||
|
{{ detail.department_name || detail.department || '-' }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="职位">{{ detail.position || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="学历">{{ detail.education || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="民族">{{ detail.nation || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="手机">{{ detail.phone || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="微信">{{ detail.wechat || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="邮箱">{{ detail.email || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="家庭住址">{{ detail.home_address || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="账号状态">
|
||||||
|
<el-tag :type="employeeStatusTagType(detail)" size="small">{{ employeeStatusText(detail) }}</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="创建时间">{{ formatDateTime(detail.create_time) }}</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="visible = false">关闭</el-button>
|
||||||
|
</template>
|
||||||
|
</el-drawer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
import {
|
||||||
|
employeeStatusTagType,
|
||||||
|
employeeStatusText,
|
||||||
|
formatDateTime,
|
||||||
|
genderText,
|
||||||
|
unwrap,
|
||||||
|
useOrganizationApi,
|
||||||
|
} from '../composables';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
/** 所属模块:erp(进销存)或 oa(办公自动化) */
|
||||||
|
module: { type: String, default: 'erp' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const api = computed(() => useOrganizationApi(props.module));
|
||||||
|
|
||||||
|
const visible = ref(false);
|
||||||
|
const loading = ref(false);
|
||||||
|
const detail = ref({});
|
||||||
|
|
||||||
|
const open = async (row) => {
|
||||||
|
const id = typeof row === 'number' ? row : row?.id;
|
||||||
|
if (!id) {
|
||||||
|
ElMessage.error('未提供有效的 ID');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
detail.value = {};
|
||||||
|
visible.value = true;
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
detail.value = unwrap(await api.value.getEmployeeDetail(id)) || {};
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(error?.message || '加载详情失败');
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
defineExpose({ open });
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog
|
||||||
|
v-model="visible"
|
||||||
|
title="导入 / 导出组织架构"
|
||||||
|
width="640px"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
>
|
||||||
|
<el-tabs v-model="activeTab">
|
||||||
|
<el-tab-pane label="导出" name="export">
|
||||||
|
<p class="hint">
|
||||||
|
导出当前租户的全部组织架构为 CSV 文件(UTF-8 带 BOM,Excel 可直接打开)。
|
||||||
|
</p>
|
||||||
|
<el-button type="primary" :loading="exporting" @click="handleExport">导出 CSV</el-button>
|
||||||
|
</el-tab-pane>
|
||||||
|
|
||||||
|
<el-tab-pane label="导入" name="import">
|
||||||
|
<p class="hint">
|
||||||
|
CSV 列顺序:组织编码、组织名称、上级组织编码、是否公司、排序、状态、备注。
|
||||||
|
编码已存在的组织执行更新,不存在的新增;上级组织通过编码关联,父行可以出现在子行之后。
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="actions">
|
||||||
|
<el-button link type="primary" @click="handleDownloadTemplate">下载导入模板</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-upload
|
||||||
|
drag
|
||||||
|
action="#"
|
||||||
|
accept=".csv"
|
||||||
|
:auto-upload="false"
|
||||||
|
:limit="1"
|
||||||
|
:file-list="fileList"
|
||||||
|
:on-change="handleFileChange"
|
||||||
|
:on-remove="handleFileRemove"
|
||||||
|
>
|
||||||
|
<el-icon class="el-icon--upload"><UploadFilled /></el-icon>
|
||||||
|
<div class="el-upload__text">将 CSV 文件拖到此处,或<em>点击选择</em></div>
|
||||||
|
</el-upload>
|
||||||
|
|
||||||
|
<div class="submit-row">
|
||||||
|
<el-button type="primary" :loading="importing" :disabled="!selectedFile" @click="handleImport">
|
||||||
|
开始导入
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-alert
|
||||||
|
v-if="result"
|
||||||
|
class="result"
|
||||||
|
:type="result.failed > 0 ? 'warning' : 'success'"
|
||||||
|
:closable="false"
|
||||||
|
show-icon
|
||||||
|
>
|
||||||
|
<template #title>
|
||||||
|
新增 {{ result.created }} 条,更新 {{ result.updated }} 条,失败 {{ result.failed }} 条
|
||||||
|
</template>
|
||||||
|
<ul v-if="result.failures && result.failures.length" class="failures">
|
||||||
|
<li v-for="(item, index) in result.failures" :key="index">{{ item }}</li>
|
||||||
|
</ul>
|
||||||
|
</el-alert>
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="visible = false">关闭</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
import { UploadFilled } from '@element-plus/icons-vue';
|
||||||
|
import { downloadBlob, unwrap, useOrganizationApi } from '../composables';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
/** 所属模块:erp(进销存)或 oa(办公自动化) */
|
||||||
|
module: { type: String, default: 'erp' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['imported']);
|
||||||
|
|
||||||
|
const api = computed(() => useOrganizationApi(props.module));
|
||||||
|
|
||||||
|
const visible = ref(false);
|
||||||
|
const activeTab = ref('export');
|
||||||
|
const exporting = ref(false);
|
||||||
|
const importing = ref(false);
|
||||||
|
const selectedFile = ref(null);
|
||||||
|
const fileList = ref([]);
|
||||||
|
const result = ref(null);
|
||||||
|
|
||||||
|
const open = (tab = 'export') => {
|
||||||
|
activeTab.value = tab;
|
||||||
|
selectedFile.value = null;
|
||||||
|
fileList.value = [];
|
||||||
|
result.value = null;
|
||||||
|
visible.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const timestamp = () => new Date().toISOString().slice(0, 19).replace(/[-:T]/g, '');
|
||||||
|
|
||||||
|
const handleExport = async () => {
|
||||||
|
exporting.value = true;
|
||||||
|
try {
|
||||||
|
const blob = await api.value.exportOrganization();
|
||||||
|
downloadBlob(blob, `organization_${timestamp()}.csv`);
|
||||||
|
ElMessage.success('导出完成');
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(error?.message || '导出失败');
|
||||||
|
} finally {
|
||||||
|
exporting.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDownloadTemplate = async () => {
|
||||||
|
try {
|
||||||
|
const blob = await api.value.downloadImportTemplate();
|
||||||
|
downloadBlob(blob, 'organization_template.csv');
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(error?.message || '下载模板失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFileChange = (file) => {
|
||||||
|
selectedFile.value = file.raw || null;
|
||||||
|
fileList.value = file.raw ? [file] : [];
|
||||||
|
result.value = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFileRemove = () => {
|
||||||
|
selectedFile.value = null;
|
||||||
|
fileList.value = [];
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleImport = async () => {
|
||||||
|
if (!selectedFile.value) return;
|
||||||
|
importing.value = true;
|
||||||
|
try {
|
||||||
|
result.value = unwrap(await api.value.importOrganization(selectedFile.value));
|
||||||
|
ElMessage.success('导入完成');
|
||||||
|
emit('imported');
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(error?.message || '导入失败');
|
||||||
|
} finally {
|
||||||
|
importing.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
defineExpose({ open });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.hint {
|
||||||
|
margin: 0 0 12px;
|
||||||
|
color: #606266;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-row {
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result {
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.failures {
|
||||||
|
margin: 8px 0 0;
|
||||||
|
padding-left: 18px;
|
||||||
|
max-height: 160px;
|
||||||
|
overflow-y: auto;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog
|
||||||
|
v-model="visible"
|
||||||
|
:title="`调岗(${targets.length} 人)`"
|
||||||
|
width="520px"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
>
|
||||||
|
<el-alert class="target-list" type="info" :closable="false" show-icon>
|
||||||
|
<template #title>
|
||||||
|
{{ targets.map((item) => item.name || item.account).join('、') }}
|
||||||
|
</template>
|
||||||
|
</el-alert>
|
||||||
|
|
||||||
|
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
|
||||||
|
<el-form-item label="目标组织" prop="org_id">
|
||||||
|
<el-tree-select
|
||||||
|
v-model="form.org_id"
|
||||||
|
:data="departmentTree"
|
||||||
|
:props="treeSelectProps"
|
||||||
|
check-strictly
|
||||||
|
clearable
|
||||||
|
placeholder="请选择目标组织"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
<div class="form-tip">隶属单位会自动同步为该组织所在的顶层公司</div>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="职位" prop="position">
|
||||||
|
<el-select
|
||||||
|
v-model="form.position"
|
||||||
|
filterable
|
||||||
|
allow-create
|
||||||
|
default-first-option
|
||||||
|
clearable
|
||||||
|
placeholder="留空表示不修改职位"
|
||||||
|
style="width: 100%"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="item in positionOptions"
|
||||||
|
:key="item.id"
|
||||||
|
:label="item.position_name"
|
||||||
|
:value="item.position_name"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="visible = false">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="loading" @click="handleSubmit">确认调岗</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, reactive, ref, watch } from 'vue';
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
import { buildOrgTree, unwrap, useOrganizationApi } from '../composables';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
/** 所属模块:erp(进销存)或 oa(办公自动化) */
|
||||||
|
module: { type: String, default: 'erp' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['success']);
|
||||||
|
|
||||||
|
const api = computed(() => useOrganizationApi(props.module));
|
||||||
|
|
||||||
|
const visible = ref(false);
|
||||||
|
const loading = ref(false);
|
||||||
|
const formRef = ref();
|
||||||
|
const targets = ref([]);
|
||||||
|
const departmentTree = ref([]);
|
||||||
|
const positionOptions = ref([]);
|
||||||
|
|
||||||
|
const treeSelectProps = { value: 'id', label: 'org_name', children: 'children' };
|
||||||
|
|
||||||
|
const form = reactive({ org_id: null, position: '' });
|
||||||
|
|
||||||
|
const rules = {
|
||||||
|
org_id: [{ required: true, message: '请选择目标组织', trigger: 'change' }],
|
||||||
|
};
|
||||||
|
|
||||||
|
// 目标组织变化时把职位候选收敛到该部门下
|
||||||
|
watch(
|
||||||
|
() => form.org_id,
|
||||||
|
async (orgId) => {
|
||||||
|
if (!orgId) {
|
||||||
|
positionOptions.value = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
positionOptions.value = unwrap(await api.value.getPositionList({ department_id: orgId })) || [];
|
||||||
|
} catch {
|
||||||
|
positionOptions.value = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const open = async (rows) => {
|
||||||
|
targets.value = Array.isArray(rows) ? rows : [rows];
|
||||||
|
form.org_id = null;
|
||||||
|
form.position = '';
|
||||||
|
visible.value = true;
|
||||||
|
formRef.value?.clearValidate();
|
||||||
|
|
||||||
|
try {
|
||||||
|
departmentTree.value = buildOrgTree(unwrap(await api.value.getOrganizationList()));
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(error?.message || '加载组织失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (!formRef.value) return;
|
||||||
|
try {
|
||||||
|
await formRef.value.validate();
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const ids = targets.value.map((item) => item.id);
|
||||||
|
unwrap(await api.value.moveEmployeeToOrg(ids, form.org_id, form.position || undefined));
|
||||||
|
ElMessage.success('调岗完成');
|
||||||
|
visible.value = false;
|
||||||
|
emit('success');
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(error?.message || '调岗失败');
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
defineExpose({ open });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.target-list {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-tip {
|
||||||
|
margin-top: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,241 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog
|
||||||
|
v-model="visible"
|
||||||
|
:title="isEdit ? '编辑组织' : '新建组织'"
|
||||||
|
width="600px"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
@closed="handleClosed"
|
||||||
|
>
|
||||||
|
<el-form ref="formRef" :model="form" :rules="rules" label-width="110px">
|
||||||
|
<el-form-item label="上级组织" prop="parent_id">
|
||||||
|
<el-tree-select
|
||||||
|
v-model="form.parent_id"
|
||||||
|
:data="parentOptions"
|
||||||
|
:props="treeSelectProps"
|
||||||
|
check-strictly
|
||||||
|
clearable
|
||||||
|
placeholder="顶级组织请留空"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
<div class="form-tip">留空表示顶级组织(企业单位)</div>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="组织名称" prop="org_name">
|
||||||
|
<el-input v-model="form.org_name" maxlength="50" show-word-limit placeholder="例如:连云港云泽集团" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="组织编码" prop="org_code">
|
||||||
|
<el-input v-model="form.org_code" maxlength="20" show-word-limit placeholder="留空时按组织设置自动生成" />
|
||||||
|
<div class="form-tip">同一租户内不允许重复,可留空由系统生成</div>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="组织类型" prop="is_company">
|
||||||
|
<el-radio-group v-model="form.is_company">
|
||||||
|
<el-radio :value="1">企业单位</el-radio>
|
||||||
|
<el-radio :value="0">部门</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="负责人" prop="leader_id">
|
||||||
|
<el-select v-model="form.leader_id" filterable clearable placeholder="请选择负责人" style="width: 100%">
|
||||||
|
<el-option
|
||||||
|
v-for="item in employeeOptions"
|
||||||
|
:key="item.id"
|
||||||
|
:label="item.name || item.account"
|
||||||
|
:value="item.id"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="排序" prop="sort">
|
||||||
|
<el-input-number v-model="form.sort" :min="0" :max="9999" controls-position="right" style="width: 100%" />
|
||||||
|
<div class="form-tip">数字越小越靠前</div>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="状态" prop="status">
|
||||||
|
<el-radio-group v-model="form.status">
|
||||||
|
<el-radio :value="1">启用</el-radio>
|
||||||
|
<el-radio :value="0">禁用</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="备注" prop="remark">
|
||||||
|
<el-input v-model="form.remark" type="textarea" :rows="3" maxlength="500" show-word-limit />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="visible = false">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="loading" @click="handleSubmit">
|
||||||
|
{{ isEdit ? '保存' : '创建' }}
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, reactive, ref } from 'vue';
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
import { buildOrgTree, collectSubtreeIds, unwrap, useOrganizationApi } from '../composables';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
/** 所属模块:erp(进销存)或 oa(办公自动化) */
|
||||||
|
module: { type: String, default: 'erp' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['success']);
|
||||||
|
|
||||||
|
const api = computed(() => useOrganizationApi(props.module));
|
||||||
|
|
||||||
|
const visible = ref(false);
|
||||||
|
const loading = ref(false);
|
||||||
|
const isEdit = ref(false);
|
||||||
|
const formRef = ref();
|
||||||
|
const orgTree = ref([]);
|
||||||
|
const employeeOptions = ref([]);
|
||||||
|
const editingId = ref(0);
|
||||||
|
|
||||||
|
const treeSelectProps = { value: 'id', label: 'org_name', children: 'children' };
|
||||||
|
|
||||||
|
const emptyForm = () => ({
|
||||||
|
parent_id: null,
|
||||||
|
org_name: '',
|
||||||
|
org_code: '',
|
||||||
|
leader_id: null,
|
||||||
|
sort: 0,
|
||||||
|
is_company: 0,
|
||||||
|
status: 1,
|
||||||
|
remark: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const form = reactive(emptyForm());
|
||||||
|
|
||||||
|
const rules = {
|
||||||
|
org_name: [
|
||||||
|
{ required: true, message: '请输入组织名称', trigger: 'blur' },
|
||||||
|
{ min: 2, max: 50, message: '长度在 2 到 50 个字符', trigger: 'blur' },
|
||||||
|
],
|
||||||
|
org_code: [
|
||||||
|
{ pattern: /^[a-zA-Z0-9_-]*$/, message: '只能包含英文、数字、下划线和横线', trigger: 'blur' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
// 编辑时排除自身及其子树,避免把组织挂到自己的下级从而形成环路
|
||||||
|
const parentOptions = computed(() => {
|
||||||
|
if (!isEdit.value || !editingId.value) return orgTree.value;
|
||||||
|
const excluded = new Set();
|
||||||
|
const walk = (nodes) => {
|
||||||
|
nodes.forEach((node) => {
|
||||||
|
if (Number(node.id) === Number(editingId.value)) {
|
||||||
|
collectSubtreeIds(node).forEach((id) => excluded.add(id));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
walk(node.children || []);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
walk(orgTree.value);
|
||||||
|
|
||||||
|
const filter = (nodes) =>
|
||||||
|
nodes
|
||||||
|
.filter((node) => !excluded.has(Number(node.id)))
|
||||||
|
.map((node) => ({ ...node, children: filter(node.children || []) }));
|
||||||
|
return filter(orgTree.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
const loadOptions = async () => {
|
||||||
|
try {
|
||||||
|
const [orgList, employees] = await Promise.all([
|
||||||
|
api.value.getOrganizationList().then(unwrap),
|
||||||
|
api.value.getEmployeeList().then(unwrap),
|
||||||
|
]);
|
||||||
|
orgTree.value = buildOrgTree(orgList);
|
||||||
|
employeeOptions.value = (employees || []).map((item) => ({
|
||||||
|
id: item.id,
|
||||||
|
name: item.name,
|
||||||
|
account: item.account,
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(error?.message || '加载选项失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const open = (row, parentId) => {
|
||||||
|
Object.assign(form, emptyForm());
|
||||||
|
isEdit.value = Boolean(row);
|
||||||
|
editingId.value = row?.id || 0;
|
||||||
|
|
||||||
|
if (row) {
|
||||||
|
Object.assign(form, {
|
||||||
|
parent_id: row.parent_id ? Number(row.parent_id) : null,
|
||||||
|
org_name: row.org_name || '',
|
||||||
|
org_code: row.org_code || '',
|
||||||
|
leader_id: row.leader_id ? Number(row.leader_id) : null,
|
||||||
|
sort: Number(row.sort || 0),
|
||||||
|
is_company: Number(row.is_company || 0),
|
||||||
|
status: Number(row.status ?? 1),
|
||||||
|
remark: row.remark || '',
|
||||||
|
});
|
||||||
|
} else if (parentId) {
|
||||||
|
form.parent_id = Number(parentId);
|
||||||
|
form.is_company = 0;
|
||||||
|
} else {
|
||||||
|
form.is_company = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
visible.value = true;
|
||||||
|
loadOptions();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (!formRef.value) return;
|
||||||
|
try {
|
||||||
|
await formRef.value.validate();
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
parent_id: form.parent_id || 0,
|
||||||
|
org_name: form.org_name.trim(),
|
||||||
|
org_code: form.org_code.trim(),
|
||||||
|
leader_id: form.leader_id || 0,
|
||||||
|
sort: form.sort,
|
||||||
|
is_company: form.is_company,
|
||||||
|
status: form.status,
|
||||||
|
remark: form.remark || '',
|
||||||
|
};
|
||||||
|
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
if (isEdit.value) {
|
||||||
|
unwrap(await api.value.updateOrganization(editingId.value, payload));
|
||||||
|
ElMessage.success('保存成功');
|
||||||
|
} else {
|
||||||
|
unwrap(await api.value.createOrganization(payload));
|
||||||
|
ElMessage.success('创建成功');
|
||||||
|
}
|
||||||
|
visible.value = false;
|
||||||
|
emit('success');
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(error?.message || '保存失败');
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClosed = () => {
|
||||||
|
formRef.value?.clearValidate();
|
||||||
|
Object.assign(form, emptyForm());
|
||||||
|
};
|
||||||
|
|
||||||
|
defineExpose({ open });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.form-tip {
|
||||||
|
margin-top: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog
|
||||||
|
v-model="visible"
|
||||||
|
title="组织架构设置"
|
||||||
|
width="620px"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
>
|
||||||
|
<el-form ref="formRef" :model="form" label-width="150px" v-loading="loading">
|
||||||
|
<el-divider content-position="left">编码规则</el-divider>
|
||||||
|
|
||||||
|
<el-form-item label="自动生成编码">
|
||||||
|
<el-switch v-model="form.auto_generate_codes" />
|
||||||
|
<div class="form-tip">开启后,新建组织/员工/职位时留空编码将由系统生成</div>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="组织编码前缀">
|
||||||
|
<el-input v-model="form.org_code_prefix" maxlength="10" :disabled="!form.auto_generate_codes" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="员工账号前缀">
|
||||||
|
<el-input v-model="form.employee_code_prefix" maxlength="10" :disabled="!form.auto_generate_codes" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="职位编码前缀">
|
||||||
|
<el-input v-model="form.position_code_prefix" maxlength="10" :disabled="!form.auto_generate_codes" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="编码长度">
|
||||||
|
<el-input-number
|
||||||
|
v-model="form.code_length"
|
||||||
|
:min="4"
|
||||||
|
:max="32"
|
||||||
|
:disabled="!form.auto_generate_codes"
|
||||||
|
controls-position="right"
|
||||||
|
/>
|
||||||
|
<div class="form-tip">含前缀的总长度</div>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="允许编码重复">
|
||||||
|
<el-switch v-model="form.allow_duplicate_codes" />
|
||||||
|
<div class="form-tip">关闭时,同一租户内编码必须唯一</div>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-divider content-position="left">层级限制</el-divider>
|
||||||
|
|
||||||
|
<el-form-item label="最大层级数">
|
||||||
|
<el-input-number v-model="form.max_org_levels" :min="1" :max="32" controls-position="right" />
|
||||||
|
<div class="form-tip">新建与移动组织时都会校验</div>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="单层最多子组织">
|
||||||
|
<el-input-number v-model="form.max_org_children" :min="1" :max="1000" controls-position="right" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-divider content-position="left">默认值</el-divider>
|
||||||
|
|
||||||
|
<el-form-item label="默认组织类型">
|
||||||
|
<el-radio-group v-model="form.default_org_type">
|
||||||
|
<el-radio :value="1">企业单位</el-radio>
|
||||||
|
<el-radio :value="0">部门</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="默认排序">
|
||||||
|
<el-input-number v-model="form.default_sort" :min="0" :max="9999" controls-position="right" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="默认状态">
|
||||||
|
<el-radio-group v-model="form.default_status">
|
||||||
|
<el-radio :value="1">启用</el-radio>
|
||||||
|
<el-radio :value="0">禁用</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-divider content-position="left">功能开关</el-divider>
|
||||||
|
|
||||||
|
<el-form-item label="批量操作">
|
||||||
|
<el-switch v-model="form.batch_operations" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="导出功能">
|
||||||
|
<el-switch v-model="form.export_enabled" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="导入功能">
|
||||||
|
<el-switch v-model="form.import_enabled" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="visible = false">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="saving" @click="handleSubmit">保存设置</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, reactive, ref } from 'vue';
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
import { unwrap, useOrganizationApi } from '../composables';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
/** 所属模块:erp(进销存)或 oa(办公自动化) */
|
||||||
|
module: { type: String, default: 'erp' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['saved']);
|
||||||
|
|
||||||
|
const api = computed(() => useOrganizationApi(props.module));
|
||||||
|
|
||||||
|
const visible = ref(false);
|
||||||
|
const loading = ref(false);
|
||||||
|
const saving = ref(false);
|
||||||
|
const formRef = ref();
|
||||||
|
|
||||||
|
const defaults = () => ({
|
||||||
|
org_code_prefix: 'ORG',
|
||||||
|
employee_code_prefix: 'EMP',
|
||||||
|
position_code_prefix: 'POS',
|
||||||
|
auto_generate_codes: true,
|
||||||
|
code_length: 8,
|
||||||
|
default_org_type: 0,
|
||||||
|
default_sort: 0,
|
||||||
|
default_status: 1,
|
||||||
|
max_org_levels: 10,
|
||||||
|
max_org_children: 50,
|
||||||
|
allow_duplicate_codes: false,
|
||||||
|
batch_operations: true,
|
||||||
|
export_enabled: true,
|
||||||
|
import_enabled: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const form = reactive(defaults());
|
||||||
|
|
||||||
|
const open = async () => {
|
||||||
|
visible.value = true;
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const data = unwrap(await api.value.getOrgSettings()) || {};
|
||||||
|
Object.assign(form, defaults(), data);
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(error?.message || '加载设置失败');
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
saving.value = true;
|
||||||
|
try {
|
||||||
|
unwrap(await api.value.saveOrgSettings({ ...form }));
|
||||||
|
ElMessage.success('设置已保存');
|
||||||
|
visible.value = false;
|
||||||
|
emit('saved');
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(error?.message || '保存设置失败');
|
||||||
|
} finally {
|
||||||
|
saving.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
defineExpose({ open });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.form-tip {
|
||||||
|
margin-top: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,617 @@
|
|||||||
|
<template>
|
||||||
|
<div class="org-page">
|
||||||
|
<div class="page-header">
|
||||||
|
<div>
|
||||||
|
<h2>组织架构</h2>
|
||||||
|
<p>{{ labels.name }}与其它模块共用同一份组织数据,修改后两端同步生效</p>
|
||||||
|
</div>
|
||||||
|
<div class="header-actions">
|
||||||
|
<el-button :icon="DataAnalysis" @click="statsVisible = !statsVisible">统计</el-button>
|
||||||
|
<el-button v-if="settings.import_enabled || settings.export_enabled" :icon="Upload" @click="openImportExport">
|
||||||
|
导入导出
|
||||||
|
</el-button>
|
||||||
|
<el-button :icon="Setting" @click="settingsRef?.open()">组织设置</el-button>
|
||||||
|
<el-button type="primary" :icon="Plus" @click="orgDialogRef?.open(null, null)">新建组织</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-collapse-transition>
|
||||||
|
<div v-show="statsVisible" class="stats-row">
|
||||||
|
<el-card v-for="item in statCards" :key="item.label" shadow="never" class="stat-card">
|
||||||
|
<div class="stat-value">{{ item.value }}</div>
|
||||||
|
<div class="stat-label">{{ item.label }}</div>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</el-collapse-transition>
|
||||||
|
|
||||||
|
<div class="org-body">
|
||||||
|
<!-- 左侧:组织树,支持拖拽调整层级 -->
|
||||||
|
<el-card class="tree-panel" shadow="never" v-loading="treeLoading">
|
||||||
|
<template #header>
|
||||||
|
<div class="panel-header">
|
||||||
|
<span>组织架构图</span>
|
||||||
|
<div class="panel-actions">
|
||||||
|
<el-tooltip content="拖动节点可调整层级">
|
||||||
|
<el-switch v-model="draggable" size="small" active-text="拖拽" />
|
||||||
|
</el-tooltip>
|
||||||
|
<el-button :icon="Refresh" circle @click="loadTree" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-input
|
||||||
|
v-model="treeKeyword"
|
||||||
|
class="tree-search"
|
||||||
|
placeholder="搜索组织名称或编码"
|
||||||
|
clearable
|
||||||
|
:prefix-icon="Search"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<el-tree
|
||||||
|
ref="treeRef"
|
||||||
|
:data="treeData"
|
||||||
|
:props="treeProps"
|
||||||
|
node-key="id"
|
||||||
|
default-expand-all
|
||||||
|
highlight-current
|
||||||
|
:expand-on-click-node="false"
|
||||||
|
:draggable="draggable"
|
||||||
|
:allow-drop="allowDrop"
|
||||||
|
:filter-node-method="filterNode"
|
||||||
|
@node-click="handleNodeClick"
|
||||||
|
@node-drop="handleNodeDrop"
|
||||||
|
>
|
||||||
|
<template #default="{ data }">
|
||||||
|
<div class="tree-node">
|
||||||
|
<span class="node-main">
|
||||||
|
<el-icon class="node-icon">
|
||||||
|
<OfficeBuilding v-if="data.is_company === 1" />
|
||||||
|
<Folder v-else />
|
||||||
|
</el-icon>
|
||||||
|
<span class="node-label">{{ data.org_name }}</span>
|
||||||
|
<el-tag v-if="data.status !== 1" type="danger" size="small">禁用</el-tag>
|
||||||
|
<el-tag v-if="data.employee_count" type="info" size="small">
|
||||||
|
{{ data.employee_count }} 人
|
||||||
|
</el-tag>
|
||||||
|
</span>
|
||||||
|
<span class="node-actions">
|
||||||
|
<el-button link type="primary" size="small" @click.stop="orgDialogRef?.open(null, data.id)">
|
||||||
|
加子级
|
||||||
|
</el-button>
|
||||||
|
<el-button link type="primary" size="small" @click.stop="orgDialogRef?.open(data)">编辑</el-button>
|
||||||
|
<el-button link type="danger" size="small" @click.stop="handleDeleteOrg(data)">删除</el-button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-tree>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 右侧:组织详情与人员 -->
|
||||||
|
<el-card class="detail-panel" shadow="never">
|
||||||
|
<template #header>
|
||||||
|
<div class="panel-header">
|
||||||
|
<span>{{ selectedOrg ? selectedOrg.org_name : '组织详情' }}</span>
|
||||||
|
<div class="panel-actions" v-if="selectedOrg">
|
||||||
|
<el-button :icon="Plus" @click="handleAddEmployee">添加{{ labels.employeeLabel }}</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-if="selectedOrg">
|
||||||
|
<el-descriptions :column="2" border size="small">
|
||||||
|
<el-descriptions-item label="组织名称">{{ selectedOrg.org_name }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="组织编码">{{ selectedOrg.org_code || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="上级组织">{{ selectedOrg.parent_name || '顶级组织' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="负责人">{{ selectedOrg.leader_name || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="组织类型">
|
||||||
|
<el-tag :type="selectedOrg.is_company === 1 ? 'primary' : 'success'" size="small">
|
||||||
|
{{ selectedOrg.is_company === 1 ? '企业单位' : '部门' }}
|
||||||
|
</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="状态">
|
||||||
|
<el-tag :type="selectedOrg.status === 1 ? 'success' : 'danger'" size="small">
|
||||||
|
{{ selectedOrg.status === 1 ? '启用' : '禁用' }}
|
||||||
|
</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="排序">{{ selectedOrg.sort }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="创建时间">{{ formatDateTime(selectedOrg.create_time) }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="备注" :span="2">{{ selectedOrg.remark || '-' }}</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
|
||||||
|
<div class="employee-section">
|
||||||
|
<div class="section-header">
|
||||||
|
<span class="section-title">{{ labels.employeeLabel }}列表</span>
|
||||||
|
<div class="section-tools">
|
||||||
|
<el-checkbox v-model="includeChildren" @change="loadEmployees">含下级组织</el-checkbox>
|
||||||
|
<el-input
|
||||||
|
v-model="employeeKeyword"
|
||||||
|
placeholder="姓名 / 账号 / 手机"
|
||||||
|
clearable
|
||||||
|
size="small"
|
||||||
|
class="employee-search"
|
||||||
|
:prefix-icon="Search"
|
||||||
|
@change="loadEmployees"
|
||||||
|
/>
|
||||||
|
<el-button
|
||||||
|
size="small"
|
||||||
|
type="primary"
|
||||||
|
plain
|
||||||
|
:disabled="!selectedEmployees.length"
|
||||||
|
@click="moveDialogRef?.open(selectedEmployees)"
|
||||||
|
>
|
||||||
|
批量调岗({{ selectedEmployees.length }})
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-table
|
||||||
|
:data="employees"
|
||||||
|
v-loading="employeeLoading"
|
||||||
|
stripe
|
||||||
|
size="small"
|
||||||
|
@selection-change="handleSelectionChange"
|
||||||
|
>
|
||||||
|
<el-table-column type="selection" width="42" />
|
||||||
|
<el-table-column prop="name" label="姓名" width="100" />
|
||||||
|
<el-table-column prop="account" label="账号" width="130" />
|
||||||
|
<el-table-column label="部门" min-width="120">
|
||||||
|
<template #default="{ row }">{{ row.department_name || '-' }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="position" label="职位" min-width="110">
|
||||||
|
<template #default="{ row }">{{ row.position || '-' }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="phone" label="手机" width="130">
|
||||||
|
<template #default="{ row }">{{ row.phone || '-' }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="状态" width="80">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="employeeStatusTagType(row)" size="small">{{ employeeStatusText(row) }}</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="170" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button link type="primary" size="small" @click="employeeDialogRef?.open(row)">编辑</el-button>
|
||||||
|
<el-button link type="primary" size="small" @click="moveDialogRef?.open([row])">调岗</el-button>
|
||||||
|
<el-button link type="danger" size="small" @click="handleDeleteEmployee(row)">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<template #empty>
|
||||||
|
<el-empty :description="`该组织下暂无${labels.employeeLabel}`" :image-size="80" />
|
||||||
|
</template>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-empty v-else description="请选择左侧组织节点查看详情" />
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<OrgEditDialog ref="orgDialogRef" :module="module" @success="handleOrgSaved" />
|
||||||
|
<EmployeeEditDialog ref="employeeDialogRef" :module="module" @success="loadEmployees" />
|
||||||
|
<MoveEmployeeDialog ref="moveDialogRef" :module="module" @success="handleEmployeeMoved" />
|
||||||
|
<OrgSettingsDialog ref="settingsRef" :module="module" @saved="loadSettings" />
|
||||||
|
<ImportExportDialog ref="importExportRef" :module="module" @imported="loadTree" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, onMounted, ref, watch } from 'vue';
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||||
|
import {
|
||||||
|
DataAnalysis,
|
||||||
|
Folder,
|
||||||
|
OfficeBuilding,
|
||||||
|
Plus,
|
||||||
|
Refresh,
|
||||||
|
Search,
|
||||||
|
Setting,
|
||||||
|
Upload,
|
||||||
|
} from '@element-plus/icons-vue';
|
||||||
|
import OrgEditDialog from './OrgEditDialog.vue';
|
||||||
|
import EmployeeEditDialog from './EmployeeEditDialog.vue';
|
||||||
|
import MoveEmployeeDialog from './MoveEmployeeDialog.vue';
|
||||||
|
import OrgSettingsDialog from './OrgSettingsDialog.vue';
|
||||||
|
import ImportExportDialog from './ImportExportDialog.vue';
|
||||||
|
import {
|
||||||
|
buildOrgTree,
|
||||||
|
collectSubtreeIds,
|
||||||
|
employeeStatusTagType,
|
||||||
|
employeeStatusText,
|
||||||
|
formatDateTime,
|
||||||
|
moduleLabels,
|
||||||
|
unwrap,
|
||||||
|
useOrganizationApi,
|
||||||
|
} from '../composables';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
/** 所属模块:erp(进销存)或 oa(办公自动化) */
|
||||||
|
module: { type: String, default: 'erp' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const api = computed(() => useOrganizationApi(props.module));
|
||||||
|
const labels = computed(() => moduleLabels(props.module));
|
||||||
|
|
||||||
|
const treeRef = ref();
|
||||||
|
const orgDialogRef = ref();
|
||||||
|
const employeeDialogRef = ref();
|
||||||
|
const moveDialogRef = ref();
|
||||||
|
const settingsRef = ref();
|
||||||
|
const importExportRef = ref();
|
||||||
|
|
||||||
|
const treeData = ref([]);
|
||||||
|
const treeLoading = ref(false);
|
||||||
|
const treeKeyword = ref('');
|
||||||
|
const draggable = ref(false);
|
||||||
|
const selectedOrg = ref(null);
|
||||||
|
|
||||||
|
const employees = ref([]);
|
||||||
|
const employeeLoading = ref(false);
|
||||||
|
const employeeKeyword = ref('');
|
||||||
|
const includeChildren = ref(true);
|
||||||
|
const selectedEmployees = ref([]);
|
||||||
|
|
||||||
|
const stats = ref({});
|
||||||
|
const statsVisible = ref(false);
|
||||||
|
const settings = ref({ import_enabled: true, export_enabled: true, batch_operations: true });
|
||||||
|
|
||||||
|
const treeProps = { label: 'org_name', children: 'children' };
|
||||||
|
|
||||||
|
const statCards = computed(() => [
|
||||||
|
{ label: '组织总数', value: stats.value.org_total ?? 0 },
|
||||||
|
{ label: '企业单位', value: stats.value.company_count ?? 0 },
|
||||||
|
{ label: '部门数', value: stats.value.department_count ?? 0 },
|
||||||
|
{ label: '最大层级', value: stats.value.max_depth ?? 0 },
|
||||||
|
{ label: `${labels.value.employeeLabel}总数`, value: stats.value.employee_total ?? 0 },
|
||||||
|
{ label: '在职人数', value: stats.value.employee_active ?? 0 },
|
||||||
|
{ label: '职位数', value: stats.value.position_total ?? 0 },
|
||||||
|
]);
|
||||||
|
|
||||||
|
watch(treeKeyword, (value) => {
|
||||||
|
treeRef.value?.filter(value);
|
||||||
|
});
|
||||||
|
|
||||||
|
const filterNode = (value, data) => {
|
||||||
|
if (!value) return true;
|
||||||
|
const keyword = String(value).toLowerCase();
|
||||||
|
return (
|
||||||
|
String(data.org_name || '').toLowerCase().includes(keyword) ||
|
||||||
|
String(data.org_code || '').toLowerCase().includes(keyword)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadSettings = async () => {
|
||||||
|
try {
|
||||||
|
settings.value = unwrap(await api.value.getOrgSettings()) || settings.value;
|
||||||
|
} catch {
|
||||||
|
// 设置读取失败时保留默认开关,不阻塞页面
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadStats = async () => {
|
||||||
|
try {
|
||||||
|
stats.value = unwrap(await api.value.getOrganizationStats()) || {};
|
||||||
|
} catch {
|
||||||
|
stats.value = {};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadTree = async () => {
|
||||||
|
treeLoading.value = true;
|
||||||
|
try {
|
||||||
|
const list = unwrap(await api.value.getOrganizationList()) || [];
|
||||||
|
treeData.value = buildOrgTree(list);
|
||||||
|
|
||||||
|
// 保持当前选中项,节点被删除时回退到第一个根节点
|
||||||
|
const currentId = selectedOrg.value?.id;
|
||||||
|
const found = currentId ? list.find((item) => Number(item.id) === Number(currentId)) : null;
|
||||||
|
selectedOrg.value = found || list[0] || null;
|
||||||
|
|
||||||
|
if (selectedOrg.value) {
|
||||||
|
await loadEmployees();
|
||||||
|
} else {
|
||||||
|
employees.value = [];
|
||||||
|
}
|
||||||
|
await loadStats();
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(error?.message || '加载组织架构失败');
|
||||||
|
} finally {
|
||||||
|
treeLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadEmployees = async () => {
|
||||||
|
if (!selectedOrg.value) {
|
||||||
|
employees.value = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
employeeLoading.value = true;
|
||||||
|
try {
|
||||||
|
const data = includeChildren.value
|
||||||
|
? unwrap(
|
||||||
|
await api.value.getEmployeeList({
|
||||||
|
org_id: selectedOrg.value.id,
|
||||||
|
keyword: employeeKeyword.value || undefined,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
: unwrap(await api.value.getOrganizationEmployees(selectedOrg.value.id));
|
||||||
|
|
||||||
|
let list = data || [];
|
||||||
|
// 不含下级时后端返回整棵子树,这里按当前组织再过滤一次
|
||||||
|
if (!includeChildren.value) {
|
||||||
|
list = list.filter((item) => String(item.department) === String(selectedOrg.value.id));
|
||||||
|
if (employeeKeyword.value) {
|
||||||
|
const keyword = employeeKeyword.value.toLowerCase();
|
||||||
|
list = list.filter((item) =>
|
||||||
|
[item.name, item.account, item.phone]
|
||||||
|
.filter(Boolean)
|
||||||
|
.some((field) => String(field).toLowerCase().includes(keyword))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
employees.value = list;
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(error?.message || `加载${labels.value.employeeLabel}失败`);
|
||||||
|
} finally {
|
||||||
|
employeeLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNodeClick = (data) => {
|
||||||
|
selectedOrg.value = data;
|
||||||
|
selectedEmployees.value = [];
|
||||||
|
loadEmployees();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelectionChange = (rows) => {
|
||||||
|
selectedEmployees.value = rows;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddEmployee = () => {
|
||||||
|
employeeDialogRef.value?.open(null, {
|
||||||
|
department: selectedOrg.value?.id,
|
||||||
|
affiliate_unit: selectedOrg.value?.is_company === 1 ? selectedOrg.value.id : undefined,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOrgSaved = () => {
|
||||||
|
loadTree();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEmployeeMoved = () => {
|
||||||
|
selectedEmployees.value = [];
|
||||||
|
loadEmployees();
|
||||||
|
loadStats();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteOrg = async (data) => {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(`确定删除组织「${data.org_name}」吗?`, '删除确认', { type: 'warning' });
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
unwrap(await api.value.deleteOrganization(data.id));
|
||||||
|
ElMessage.success('删除成功');
|
||||||
|
if (Number(selectedOrg.value?.id) === Number(data.id)) {
|
||||||
|
selectedOrg.value = null;
|
||||||
|
}
|
||||||
|
await loadTree();
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(error?.message || '删除失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteEmployee = async (row) => {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(`确定删除「${row.name}」吗?`, '删除确认', { type: 'warning' });
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
unwrap(await api.value.deleteEmployee(row.id));
|
||||||
|
ElMessage.success('删除成功');
|
||||||
|
await loadEmployees();
|
||||||
|
await loadStats();
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(error?.message || '删除失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 禁止把节点拖进自己的子树,后端也有同样的校验
|
||||||
|
const allowDrop = (draggingNode, dropNode, type) => {
|
||||||
|
const forbidden = new Set(collectSubtreeIds(draggingNode.data));
|
||||||
|
if (type === 'inner') {
|
||||||
|
return !forbidden.has(Number(dropNode.data.id));
|
||||||
|
}
|
||||||
|
const parentId = Number(dropNode.data.parent_id || 0);
|
||||||
|
return !forbidden.has(parentId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNodeDrop = async (draggingNode, dropNode, dropType) => {
|
||||||
|
const parentId =
|
||||||
|
dropType === 'inner' ? Number(dropNode.data.id) : Number(dropNode.data.parent_id || 0);
|
||||||
|
try {
|
||||||
|
unwrap(await api.value.moveOrganization(draggingNode.data.id, parentId));
|
||||||
|
ElMessage.success('层级已调整');
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(error?.message || '调整层级失败');
|
||||||
|
} finally {
|
||||||
|
await loadTree();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const openImportExport = () => {
|
||||||
|
importExportRef.value?.open(settings.value.export_enabled ? 'export' : 'import');
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadSettings();
|
||||||
|
loadTree();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.org-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-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
:deep(.el-card__body) {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
margin-top: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.org-body {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-panel {
|
||||||
|
width: 480px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-panel {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-search {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-node {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
padding-right: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-main {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-icon {
|
||||||
|
color: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-label {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-actions {
|
||||||
|
display: none;
|
||||||
|
gap: 2px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-node:hover .node-actions {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.employee-section {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-tools {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.employee-search {
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.org-body {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-panel {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,178 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog
|
||||||
|
v-model="visible"
|
||||||
|
:title="isEdit ? '编辑职位' : '新增职位'"
|
||||||
|
width="560px"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
@closed="handleClosed"
|
||||||
|
>
|
||||||
|
<el-form ref="formRef" :model="form" :rules="rules" label-width="110px">
|
||||||
|
<el-form-item label="所属部门" prop="department_id">
|
||||||
|
<el-tree-select
|
||||||
|
v-model="form.department_id"
|
||||||
|
:data="departmentTree"
|
||||||
|
:props="treeSelectProps"
|
||||||
|
check-strictly
|
||||||
|
clearable
|
||||||
|
placeholder="请选择所属部门"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="职位名称" prop="position_name">
|
||||||
|
<el-input v-model="form.position_name" maxlength="100" show-word-limit placeholder="例如:销售主管" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="职位编码" prop="position_code">
|
||||||
|
<el-input v-model="form.position_code" maxlength="50" show-word-limit placeholder="留空时按组织设置自动生成" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="职位类型" prop="position_type">
|
||||||
|
<el-radio-group v-model="form.position_type">
|
||||||
|
<el-radio :value="0">普通职位</el-radio>
|
||||||
|
<el-radio :value="1">主管</el-radio>
|
||||||
|
<el-radio :value="2">负责人</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="排序" prop="sort">
|
||||||
|
<el-input-number v-model="form.sort" :min="0" :max="9999" controls-position="right" style="width: 100%" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="状态" prop="status">
|
||||||
|
<el-radio-group v-model="form.status">
|
||||||
|
<el-radio :value="1">启用</el-radio>
|
||||||
|
<el-radio :value="0">禁用</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="备注" prop="remark">
|
||||||
|
<el-input v-model="form.remark" type="textarea" :rows="3" maxlength="500" show-word-limit />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="visible = false">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="loading" @click="handleSubmit">
|
||||||
|
{{ isEdit ? '保存' : '创建' }}
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, reactive, ref } from 'vue';
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
import { buildOrgTree, unwrap, useOrganizationApi } from '../composables';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
/** 所属模块:erp(进销存)或 oa(办公自动化) */
|
||||||
|
module: { type: String, default: 'erp' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['success']);
|
||||||
|
|
||||||
|
const api = computed(() => useOrganizationApi(props.module));
|
||||||
|
|
||||||
|
const visible = ref(false);
|
||||||
|
const loading = ref(false);
|
||||||
|
const isEdit = ref(false);
|
||||||
|
const editingId = ref(0);
|
||||||
|
const formRef = ref();
|
||||||
|
const departmentTree = ref([]);
|
||||||
|
|
||||||
|
const treeSelectProps = { value: 'id', label: 'org_name', children: 'children' };
|
||||||
|
|
||||||
|
const emptyForm = () => ({
|
||||||
|
department_id: null,
|
||||||
|
position_name: '',
|
||||||
|
position_code: '',
|
||||||
|
position_type: 0,
|
||||||
|
sort: 0,
|
||||||
|
status: 1,
|
||||||
|
remark: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const form = reactive(emptyForm());
|
||||||
|
|
||||||
|
const rules = {
|
||||||
|
position_name: [{ required: true, message: '请输入职位名称', trigger: 'blur' }],
|
||||||
|
position_code: [
|
||||||
|
{ pattern: /^[a-zA-Z0-9_-]*$/, message: '只能包含英文、数字、下划线和横线', trigger: 'blur' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadDepartments = async () => {
|
||||||
|
try {
|
||||||
|
departmentTree.value = buildOrgTree(unwrap(await api.value.getOrganizationList()));
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(error?.message || '加载部门失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const open = (row, departmentId) => {
|
||||||
|
Object.assign(form, emptyForm());
|
||||||
|
isEdit.value = Boolean(row?.id);
|
||||||
|
editingId.value = row?.id || 0;
|
||||||
|
|
||||||
|
if (row) {
|
||||||
|
Object.assign(form, {
|
||||||
|
department_id: row.department_id ? Number(row.department_id) : null,
|
||||||
|
position_name: row.position_name || '',
|
||||||
|
position_code: row.position_code || '',
|
||||||
|
position_type: Number(row.position_type || 0),
|
||||||
|
sort: Number(row.sort || 0),
|
||||||
|
status: Number(row.status ?? 1),
|
||||||
|
remark: row.remark || '',
|
||||||
|
});
|
||||||
|
} else if (departmentId) {
|
||||||
|
form.department_id = Number(departmentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
visible.value = true;
|
||||||
|
loadDepartments();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (!formRef.value) return;
|
||||||
|
try {
|
||||||
|
await formRef.value.validate();
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
department_id: form.department_id || 0,
|
||||||
|
position_name: form.position_name.trim(),
|
||||||
|
position_code: form.position_code.trim(),
|
||||||
|
position_type: form.position_type,
|
||||||
|
sort: form.sort,
|
||||||
|
status: form.status,
|
||||||
|
remark: form.remark || '',
|
||||||
|
};
|
||||||
|
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
if (isEdit.value) {
|
||||||
|
unwrap(await api.value.updatePosition(editingId.value, payload));
|
||||||
|
ElMessage.success('保存成功');
|
||||||
|
} else {
|
||||||
|
unwrap(await api.value.createPosition(payload));
|
||||||
|
ElMessage.success('创建成功');
|
||||||
|
}
|
||||||
|
visible.value = false;
|
||||||
|
emit('success');
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(error?.message || '保存失败');
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClosed = () => {
|
||||||
|
formRef.value?.clearValidate();
|
||||||
|
Object.assign(form, emptyForm());
|
||||||
|
};
|
||||||
|
|
||||||
|
defineExpose({ open });
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,240 @@
|
|||||||
|
<template>
|
||||||
|
<div class="position-page">
|
||||||
|
<div class="page-header">
|
||||||
|
<div>
|
||||||
|
<h2>职位管理</h2>
|
||||||
|
<p>职位与组织架构共用同一份数据,{{ labels.name }}与其它模块同步生效</p>
|
||||||
|
</div>
|
||||||
|
<div class="header-actions">
|
||||||
|
<el-button :icon="Refresh" @click="loadData">刷新</el-button>
|
||||||
|
<el-button type="primary" :icon="Plus" @click="editDialogRef?.open(null, filters.department_id)">
|
||||||
|
新增职位
|
||||||
|
</el-button>
|
||||||
|
</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.department_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="0" />
|
||||||
|
</el-select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-table :data="pagedRows" v-loading="loading" stripe>
|
||||||
|
<el-table-column prop="id" label="ID" width="70" align="center" />
|
||||||
|
<el-table-column prop="position_code" label="职位编码" width="150" />
|
||||||
|
<el-table-column prop="position_name" label="职位名称" min-width="140" />
|
||||||
|
<el-table-column label="所属部门" min-width="150">
|
||||||
|
<template #default="{ row }">{{ row.department_name || '-' }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="职位类型" width="110" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="POSITION_TYPE_TAG[row.position_type] || 'info'" size="small">
|
||||||
|
{{ POSITION_TYPE_TEXT[row.position_type] || '普通职位' }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="sort" label="排序" width="80" align="center" />
|
||||||
|
<el-table-column label="状态" width="90" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="row.status === 1 ? 'success' : 'danger'" size="small">
|
||||||
|
{{ row.status === 1 ? '启用' : '禁用' }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="备注" min-width="140">
|
||||||
|
<template #default="{ row }">{{ row.remark || '-' }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="150" fixed="right" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<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="rows.length"
|
||||||
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<PositionEditDialog ref="editDialogRef" :module="module" @success="loadData" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, onMounted, reactive, ref } from 'vue';
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||||
|
import { Plus, Refresh, Search } from '@element-plus/icons-vue';
|
||||||
|
import PositionEditDialog from './PositionEditDialog.vue';
|
||||||
|
import { buildOrgTree, moduleLabels, unwrap, useOrganizationApi } from '../composables';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
/** 所属模块:erp(进销存)或 oa(办公自动化) */
|
||||||
|
module: { type: String, default: 'erp' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const api = computed(() => useOrganizationApi(props.module));
|
||||||
|
const labels = computed(() => moduleLabels(props.module));
|
||||||
|
|
||||||
|
const POSITION_TYPE_TEXT = { 0: '普通职位', 1: '主管', 2: '负责人' };
|
||||||
|
const POSITION_TYPE_TAG = { 0: 'info', 1: 'warning', 2: 'primary' };
|
||||||
|
|
||||||
|
const editDialogRef = ref();
|
||||||
|
const rows = ref([]);
|
||||||
|
const loading = ref(false);
|
||||||
|
const page = ref(1);
|
||||||
|
const pageSize = ref(10);
|
||||||
|
const departmentTree = ref([]);
|
||||||
|
|
||||||
|
const treeSelectProps = { value: 'id', label: 'org_name', children: 'children' };
|
||||||
|
|
||||||
|
const filters = reactive({ keyword: '', department_id: null, status: '' });
|
||||||
|
|
||||||
|
const pagedRows = computed(() => {
|
||||||
|
const start = (page.value - 1) * pageSize.value;
|
||||||
|
return rows.value.slice(start, start + pageSize.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
const loadDepartments = async () => {
|
||||||
|
try {
|
||||||
|
departmentTree.value = buildOrgTree(unwrap(await api.value.getOrganizationList()));
|
||||||
|
} catch {
|
||||||
|
departmentTree.value = [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadData = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const params = {};
|
||||||
|
if (filters.keyword) params.keyword = filters.keyword;
|
||||||
|
if (filters.department_id) params.department_id = filters.department_id;
|
||||||
|
if (filters.status !== '' && filters.status !== null) params.status = filters.status;
|
||||||
|
|
||||||
|
rows.value = unwrap(await api.value.getPositionList(params)) || [];
|
||||||
|
const maxPage = Math.max(1, Math.ceil(rows.value.length / pageSize.value));
|
||||||
|
if (page.value > maxPage) {
|
||||||
|
page.value = maxPage;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(error?.message || '获取职位列表失败');
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSearch = () => {
|
||||||
|
page.value = 1;
|
||||||
|
loadData();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async (row) => {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(`确定删除职位「${row.position_name}」吗?`, '删除确认', { type: 'warning' });
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
unwrap(await api.value.deletePosition(row.id));
|
||||||
|
ElMessage.success('删除成功');
|
||||||
|
await loadData();
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(error?.message || '删除失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadDepartments();
|
||||||
|
loadData();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.position-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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-bar {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-item {
|
||||||
|
width: 240px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-filter {
|
||||||
|
width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-bar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 14px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
import { erpOrganizationApi, oaOrganizationApi } from '@/api/organization';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 组织架构模块前端共用逻辑。
|
||||||
|
*
|
||||||
|
* 进销存(erp)与办公自动化(oa)两端界面共用同一批组件,
|
||||||
|
* 只通过 module 属性切换调用的接口前缀。
|
||||||
|
*/
|
||||||
|
|
||||||
|
const API_BY_MODULE = {
|
||||||
|
erp: erpOrganizationApi,
|
||||||
|
oa: oaOrganizationApi,
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 按模块取对应的接口集合,模块名非法时回退到 erp */
|
||||||
|
export function useOrganizationApi(module) {
|
||||||
|
return API_BY_MODULE[String(module || '').toLowerCase()] || erpOrganizationApi;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 两端在文案上的差异:进销存偏组织与职位,办公自动化偏人员与审批链 */
|
||||||
|
export const MODULE_LABELS = {
|
||||||
|
erp: { name: '进销存', employeeLabel: '员工', orgLabel: '组织' },
|
||||||
|
oa: { name: '办公自动化', employeeLabel: '人员', orgLabel: '组织' },
|
||||||
|
};
|
||||||
|
|
||||||
|
export function moduleLabels(module) {
|
||||||
|
return MODULE_LABELS[String(module || '').toLowerCase()] || MODULE_LABELS.erp;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 后端统一返回 { code, msg, data },axios 拦截器已经剥掉一层 response。
|
||||||
|
* 这里把业务错误转成异常,让调用方用 try/catch 统一处理。
|
||||||
|
*/
|
||||||
|
export function unwrap(res) {
|
||||||
|
if (res && typeof res === 'object' && 'code' in res) {
|
||||||
|
if (Number(res.code) !== 200) {
|
||||||
|
throw new Error(res.msg || '请求失败');
|
||||||
|
}
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 把扁平的组织列表组装成树;上级缺失的节点作为根节点,避免数据丢失 */
|
||||||
|
export function buildOrgTree(list) {
|
||||||
|
const rows = Array.isArray(list) ? list : [];
|
||||||
|
const nodeMap = new Map();
|
||||||
|
rows.forEach((item) => {
|
||||||
|
nodeMap.set(Number(item.id), { ...item, children: [] });
|
||||||
|
});
|
||||||
|
|
||||||
|
const tree = [];
|
||||||
|
rows.forEach((item) => {
|
||||||
|
const node = nodeMap.get(Number(item.id));
|
||||||
|
const parentId = Number(item.parent_id || 0);
|
||||||
|
const parent = parentId ? nodeMap.get(parentId) : null;
|
||||||
|
if (parent) {
|
||||||
|
parent.children.push(node);
|
||||||
|
} else {
|
||||||
|
tree.push(node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return tree;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 收集节点及其所有子节点的 ID,用于判断"不能移动到自己的下级" */
|
||||||
|
export function collectSubtreeIds(node) {
|
||||||
|
const ids = [];
|
||||||
|
const walk = (item) => {
|
||||||
|
if (!item) return;
|
||||||
|
ids.push(Number(item.id));
|
||||||
|
(item.children || []).forEach(walk);
|
||||||
|
};
|
||||||
|
walk(node);
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 触发浏览器下载 */
|
||||||
|
export function downloadBlob(blob, filename) {
|
||||||
|
const url = window.URL.createObjectURL(
|
||||||
|
blob instanceof Blob ? blob : new Blob([blob], { type: 'text/csv;charset=utf-8' })
|
||||||
|
);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
link.download = filename;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatDateTime(value) {
|
||||||
|
if (!value) return '-';
|
||||||
|
return String(value).replace('T', ' ').slice(0, 19);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GENDER_TEXT = { 0: '未知', 1: '男', 2: '女' };
|
||||||
|
|
||||||
|
export function genderText(row) {
|
||||||
|
const value = Number(row?.gender ?? row?.sex ?? 0);
|
||||||
|
return GENDER_TEXT[value] || '未知';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 员工账号状态:1 启用 / 0 禁用 / 2 离职 */
|
||||||
|
export function employeeStatusText(row) {
|
||||||
|
const value = Number(row?.account_status ?? row?.status ?? 0);
|
||||||
|
if (value === 1) return '正常';
|
||||||
|
if (value === 2) return '离职';
|
||||||
|
return '禁用';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function employeeStatusTagType(row) {
|
||||||
|
const value = Number(row?.account_status ?? row?.status ?? 0);
|
||||||
|
if (value === 1) return 'success';
|
||||||
|
if (value === 2) return 'warning';
|
||||||
|
return 'danger';
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,479 @@
|
|||||||
|
package controllers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"server/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 本文件包含组织架构模块的 DTO 组装、树结构与层级校验,以及请求参数解析工具。
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// DTO 组装
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// organizationDTOList 批量组装组织 DTO。一次性把父组织名、负责人名、员工数查出来,
|
||||||
|
// 避免逐行查询导致的 N+1 问题。
|
||||||
|
func (c *BackendOrganizationController) organizationDTOList(tid uint64, rows []models.BackendOrganization) []organizationDTO {
|
||||||
|
list := make([]organizationDTO, 0, len(rows))
|
||||||
|
if len(rows) == 0 {
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
|
||||||
|
nameByID := c.orgNameMap(tid)
|
||||||
|
leaderNames := c.employeeNameMap(tid)
|
||||||
|
counts := c.employeeCountByDepartment(tid)
|
||||||
|
|
||||||
|
for _, row := range rows {
|
||||||
|
list = append(list, c.assembleOrganizationDTO(row, nameByID, leaderNames, counts))
|
||||||
|
}
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *BackendOrganizationController) organizationDTO(tid uint64, row models.BackendOrganization) organizationDTO {
|
||||||
|
return c.assembleOrganizationDTO(row,
|
||||||
|
c.orgNameMap(tid), c.employeeNameMap(tid), c.employeeCountByDepartment(tid))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *BackendOrganizationController) assembleOrganizationDTO(
|
||||||
|
row models.BackendOrganization,
|
||||||
|
nameByID map[uint64]string,
|
||||||
|
leaderNames map[uint64]string,
|
||||||
|
counts map[string]int64,
|
||||||
|
) organizationDTO {
|
||||||
|
leaderID := uint64(0)
|
||||||
|
if row.LeaderID != nil {
|
||||||
|
leaderID = *row.LeaderID
|
||||||
|
}
|
||||||
|
|
||||||
|
return organizationDTO{
|
||||||
|
ID: row.ID,
|
||||||
|
Tid: row.Tid,
|
||||||
|
TenantID: row.Tid,
|
||||||
|
OrgName: row.OrgName,
|
||||||
|
OrgCode: row.OrgCode,
|
||||||
|
ParentID: row.ParentID,
|
||||||
|
ParentName: nameByID[row.ParentID],
|
||||||
|
LeaderID: leaderID,
|
||||||
|
LeaderName: leaderNames[leaderID],
|
||||||
|
IsCompany: row.IsCompany,
|
||||||
|
Sort: row.Sort,
|
||||||
|
Status: row.Status,
|
||||||
|
Remark: derefString(row.Remark),
|
||||||
|
EmployeeCount: counts[strconv.FormatUint(row.ID, 10)],
|
||||||
|
CreateTime: formatDateTime(&row.CreateTime),
|
||||||
|
UpdateTime: formatDateTime(&row.UpdateTime),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *BackendOrganizationController) employeeDTOList(tid uint64, rows []models.BackendEmployee) []employeeDTO {
|
||||||
|
list := make([]employeeDTO, 0, len(rows))
|
||||||
|
if len(rows) == 0 {
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
|
||||||
|
nameByID := c.orgNameMap(tid)
|
||||||
|
for _, row := range rows {
|
||||||
|
list = append(list, c.assembleEmployeeDTO(row, nameByID))
|
||||||
|
}
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *BackendOrganizationController) employeeDTO(tid uint64, row models.BackendEmployee) employeeDTO {
|
||||||
|
return c.assembleEmployeeDTO(row, c.orgNameMap(tid))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *BackendOrganizationController) assembleEmployeeDTO(
|
||||||
|
row models.BackendEmployee,
|
||||||
|
nameByID map[uint64]string,
|
||||||
|
) employeeDTO {
|
||||||
|
tid := 0
|
||||||
|
if row.Tid != nil {
|
||||||
|
tid = *row.Tid
|
||||||
|
}
|
||||||
|
|
||||||
|
birthday := ""
|
||||||
|
if row.Birthday != nil {
|
||||||
|
birthday = row.Birthday.Format("2006-01-02")
|
||||||
|
}
|
||||||
|
|
||||||
|
affiliateUnit := derefString(row.AffiliateUnit)
|
||||||
|
department := derefString(row.Department)
|
||||||
|
|
||||||
|
return employeeDTO{
|
||||||
|
ID: row.ID,
|
||||||
|
Tid: tid,
|
||||||
|
TenantID: tid,
|
||||||
|
Account: row.Account,
|
||||||
|
Name: row.Name,
|
||||||
|
Gender: row.Gender,
|
||||||
|
Sex: row.Gender,
|
||||||
|
Birthday: birthday,
|
||||||
|
AffiliateUnit: affiliateUnit,
|
||||||
|
AffiliateUnitName: orgNameByIDString(nameByID, affiliateUnit),
|
||||||
|
Department: department,
|
||||||
|
DepartmentName: orgNameByIDString(nameByID, department),
|
||||||
|
Position: derefString(row.Position),
|
||||||
|
Education: derefString(row.Education),
|
||||||
|
Nation: derefString(row.Nation),
|
||||||
|
Phone: derefString(row.Phone),
|
||||||
|
Wechat: derefString(row.Wechat),
|
||||||
|
Email: derefString(row.Email),
|
||||||
|
HomeAddress: derefString(row.HomeAddress),
|
||||||
|
AccountStatus: row.AccountStatus,
|
||||||
|
Status: row.AccountStatus,
|
||||||
|
CreateTime: formatDateTime(&row.CreateTime),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *BackendOrganizationController) positionDTO(tid uint64, row models.BackendPosition) positionDTO {
|
||||||
|
nameByID := c.orgNameMap(tid)
|
||||||
|
return positionDTO{
|
||||||
|
ID: row.ID,
|
||||||
|
Tid: row.Tid,
|
||||||
|
TenantID: row.Tid,
|
||||||
|
DepartmentID: row.DepartmentID,
|
||||||
|
DepartmentName: nameByID[row.DepartmentID],
|
||||||
|
PositionCode: row.PositionCode,
|
||||||
|
PositionName: row.PositionName,
|
||||||
|
PositionType: row.PositionType,
|
||||||
|
Status: row.Status,
|
||||||
|
Sort: row.Sort,
|
||||||
|
Remark: derefString(row.Remark),
|
||||||
|
CreateTime: formatDateTime(&row.CreateTime),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 组织关系查询
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func (c *BackendOrganizationController) orgNameMap(tid uint64) map[uint64]string {
|
||||||
|
result := map[uint64]string{}
|
||||||
|
var rows []models.BackendOrganization
|
||||||
|
if _, err := c.orgQuery(tid).All(&rows, "ID", "OrgName"); err != nil {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
for _, row := range rows {
|
||||||
|
result[row.ID] = row.OrgName
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *BackendOrganizationController) employeeNameMap(tid uint64) map[uint64]string {
|
||||||
|
result := map[uint64]string{}
|
||||||
|
var rows []models.BackendEmployee
|
||||||
|
if _, err := c.employeeQuery(tid).All(&rows, "ID", "Name"); err != nil {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
for _, row := range rows {
|
||||||
|
result[uint64(row.ID)] = row.Name
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// employeeCountByDepartment 统计各部门(department 存的是组织ID字符串)的员工数。
|
||||||
|
func (c *BackendOrganizationController) employeeCountByDepartment(tid uint64) map[string]int64 {
|
||||||
|
result := map[string]int64{}
|
||||||
|
var rows []models.BackendEmployee
|
||||||
|
if _, err := c.employeeQuery(tid).All(&rows, "Department"); err != nil {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
for _, row := range rows {
|
||||||
|
key := strings.TrimSpace(derefString(row.Department))
|
||||||
|
if key == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
result[key]++
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *BackendOrganizationController) orgExists(tid, id uint64) bool {
|
||||||
|
if id == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return c.orgQuery(tid).Filter("id", id).Exist()
|
||||||
|
}
|
||||||
|
|
||||||
|
// parentMap 返回 组织ID -> 上级组织ID 的映射,用于层级与环路判断。
|
||||||
|
func (c *BackendOrganizationController) parentMap(tid uint64) map[uint64]uint64 {
|
||||||
|
result := map[uint64]uint64{}
|
||||||
|
var rows []models.BackendOrganization
|
||||||
|
if _, err := c.orgQuery(tid).All(&rows, "ID", "ParentID"); err != nil {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
for _, row := range rows {
|
||||||
|
result[row.ID] = row.ParentID
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// orgDepth 计算组织所在层级,顶级为 1。
|
||||||
|
func (c *BackendOrganizationController) orgDepth(tid, id uint64) (int, error) {
|
||||||
|
parents := c.parentMap(tid)
|
||||||
|
depth := 0
|
||||||
|
current := id
|
||||||
|
for current > 0 {
|
||||||
|
depth++
|
||||||
|
if depth > 64 {
|
||||||
|
return depth, errors.New("组织层级数据异常(可能存在环路)")
|
||||||
|
}
|
||||||
|
next, ok := parents[current]
|
||||||
|
if !ok {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
current = next
|
||||||
|
}
|
||||||
|
return depth, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// subtreeHeight 计算以 id 为根的子树高度(只有自身时为 1)。
|
||||||
|
func (c *BackendOrganizationController) subtreeHeight(tid, id uint64) int {
|
||||||
|
childrenOf := map[uint64][]uint64{}
|
||||||
|
var rows []models.BackendOrganization
|
||||||
|
if _, err := c.orgQuery(tid).All(&rows, "ID", "ParentID"); err != nil {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
for _, row := range rows {
|
||||||
|
childrenOf[row.ParentID] = append(childrenOf[row.ParentID], row.ID)
|
||||||
|
}
|
||||||
|
return subtreeHeightFrom(childrenOf, id, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func subtreeHeightFrom(childrenOf map[uint64][]uint64, id uint64, depth int) int {
|
||||||
|
if depth > 64 {
|
||||||
|
return depth
|
||||||
|
}
|
||||||
|
height := 1
|
||||||
|
for _, child := range childrenOf[id] {
|
||||||
|
if h := subtreeHeightFrom(childrenOf, child, depth+1) + 1; h > height {
|
||||||
|
height = h
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return height
|
||||||
|
}
|
||||||
|
|
||||||
|
// collectOrgIDs 返回 rootID 及其所有下级组织的ID。
|
||||||
|
func (c *BackendOrganizationController) collectOrgIDs(tid, rootID uint64) []uint64 {
|
||||||
|
childrenOf := map[uint64][]uint64{}
|
||||||
|
var rows []models.BackendOrganization
|
||||||
|
if _, err := c.orgQuery(tid).All(&rows, "ID", "ParentID"); err != nil {
|
||||||
|
return []uint64{rootID}
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// rootCompanyID 沿上级链向上找到所属的顶层公司ID。
|
||||||
|
func (c *BackendOrganizationController) rootCompanyID(tid uint64, org models.BackendOrganization) uint64 {
|
||||||
|
if org.IsCompany == 1 {
|
||||||
|
return org.ID
|
||||||
|
}
|
||||||
|
parents := c.parentMap(tid)
|
||||||
|
current := org.ParentID
|
||||||
|
for i := 0; i < 64 && current > 0; i++ {
|
||||||
|
var row models.BackendOrganization
|
||||||
|
if err := c.orgQuery(tid).Filter("id", current).One(&row); err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if row.IsCompany == 1 {
|
||||||
|
return row.ID
|
||||||
|
}
|
||||||
|
next, ok := parents[current]
|
||||||
|
if !ok {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
current = next
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateParentChange 校验把 orgID 挂到 newParentID 下是否合法:
|
||||||
|
// 不能挂到自己或自己的后代(形成环路),且移动后总层级不超过设置上限。
|
||||||
|
func (c *BackendOrganizationController) validateParentChange(tid, orgID, newParentID uint64, settings orgSettings) error {
|
||||||
|
if newParentID == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if newParentID == orgID {
|
||||||
|
return errors.New("上级组织不能是自己")
|
||||||
|
}
|
||||||
|
if !c.orgExists(tid, newParentID) {
|
||||||
|
return errors.New("上级组织不存在")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, id := range c.collectOrgIDs(tid, orgID) {
|
||||||
|
if id == newParentID {
|
||||||
|
return errors.New("不能将组织移动到自己的下级组织中")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
parentDepth, err := c.orgDepth(tid, newParentID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if settings.MaxOrgLevels > 0 && parentDepth+c.subtreeHeight(tid, orgID) > settings.MaxOrgLevels {
|
||||||
|
return fmt.Errorf("移动后组织层级将超过上限 %d 级", settings.MaxOrgLevels)
|
||||||
|
}
|
||||||
|
if settings.MaxOrgChildren > 0 {
|
||||||
|
count, err := c.orgQuery(tid).Filter("parent_id", newParentID).
|
||||||
|
Exclude("id", orgID).Exclude("status", 0).Count()
|
||||||
|
if err == nil && int(count) >= settings.MaxOrgChildren {
|
||||||
|
return fmt.Errorf("同一上级下最多 %d 个子组织", settings.MaxOrgChildren)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateEmployeeOrg 校验员工的隶属单位与部门必须是当前租户下已存在的组织。
|
||||||
|
func (c *BackendOrganizationController) validateEmployeeOrg(tid uint64, affiliateUnit, department string) error {
|
||||||
|
for label, raw := range map[string]string{"隶属单位": affiliateUnit, "部门": department} {
|
||||||
|
raw = strings.TrimSpace(raw)
|
||||||
|
if raw == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
id, err := strconv.ParseUint(raw, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%s格式不正确", label)
|
||||||
|
}
|
||||||
|
if !c.orgExists(tid, id) {
|
||||||
|
return fmt.Errorf("%s不存在", label)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildOrganizationTree 把扁平的组织 DTO 列表组装成树。
|
||||||
|
// 上级不在列表中的节点(如上级被禁用)作为根节点返回,避免数据丢失。
|
||||||
|
func buildOrganizationTree(list []organizationDTO) []map[string]interface{} {
|
||||||
|
nodeMap := make(map[uint64]map[string]interface{}, len(list))
|
||||||
|
order := make([]uint64, 0, len(list))
|
||||||
|
|
||||||
|
for _, item := range list {
|
||||||
|
raw, _ := json.Marshal(item)
|
||||||
|
node := map[string]interface{}{}
|
||||||
|
_ = json.Unmarshal(raw, &node)
|
||||||
|
node["children"] = make([]map[string]interface{}, 0)
|
||||||
|
nodeMap[item.ID] = node
|
||||||
|
order = append(order, item.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
tree := make([]map[string]interface{}, 0)
|
||||||
|
for _, id := range order {
|
||||||
|
node := nodeMap[id]
|
||||||
|
parentID := uint64(0)
|
||||||
|
if v, ok := node["parent_id"].(float64); ok {
|
||||||
|
parentID = uint64(v)
|
||||||
|
}
|
||||||
|
if parent, exists := nodeMap[parentID]; parentID > 0 && exists {
|
||||||
|
parent["children"] = append(parent["children"].([]map[string]interface{}), node)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
tree = append(tree, node)
|
||||||
|
}
|
||||||
|
return tree
|
||||||
|
}
|
||||||
|
|
||||||
|
// treeDepth 计算树的最大深度。
|
||||||
|
func treeDepth(childrenOf map[uint64][]uint64, rootID uint64, depth int) int {
|
||||||
|
if depth > 64 {
|
||||||
|
return depth
|
||||||
|
}
|
||||||
|
maxDepth := depth
|
||||||
|
for _, child := range childrenOf[rootID] {
|
||||||
|
if d := treeDepth(childrenOf, child, depth+1); d > maxDepth {
|
||||||
|
maxDepth = d
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return maxDepth
|
||||||
|
}
|
||||||
|
|
||||||
|
func orgNameByIDString(nameByID map[uint64]string, raw string) string {
|
||||||
|
raw = strings.TrimSpace(raw)
|
||||||
|
if raw == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
id, err := strconv.ParseUint(raw, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return nameByID[id]
|
||||||
|
}
|
||||||
|
|
||||||
|
func uniqueMessage(unique bool, subject string) string {
|
||||||
|
if unique {
|
||||||
|
return subject + "可用"
|
||||||
|
}
|
||||||
|
return subject + "已存在"
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatDateTime(t *time.Time) string {
|
||||||
|
if t == nil || t.IsZero() {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return t.Format("2006-01-02 15:04:05")
|
||||||
|
}
|
||||||
|
|
||||||
|
func clampInt(v, min, max int) int {
|
||||||
|
if v < min {
|
||||||
|
return min
|
||||||
|
}
|
||||||
|
if v > max {
|
||||||
|
return max
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func maxInt(a, b int) int {
|
||||||
|
if a > b {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateCode 生成形如 ORG20260827193012 的编码,长度不足时补时间戳末尾数字。
|
||||||
|
func (c *BackendOrganizationController) generateCode(prefix string, length int) string {
|
||||||
|
prefix = strings.TrimSpace(prefix)
|
||||||
|
stamp := time.Now().Format("20060102150405")
|
||||||
|
code := prefix + stamp
|
||||||
|
if length > 0 && len(code) > length && len(prefix) < length {
|
||||||
|
keep := length - len(prefix)
|
||||||
|
code = prefix + stamp[len(stamp)-keep:]
|
||||||
|
}
|
||||||
|
return code
|
||||||
|
}
|
||||||
|
|
||||||
|
// hashEmployeePassword 适配 password varchar(64),使用 sha256 hex;空密码返回空串。
|
||||||
|
func hashEmployeePassword(plain string) string {
|
||||||
|
plain = strings.TrimSpace(plain)
|
||||||
|
if plain == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
sum := sha256.Sum256([]byte(plain))
|
||||||
|
return hex.EncodeToString(sum[:])
|
||||||
|
}
|
||||||
@@ -0,0 +1,289 @@
|
|||||||
|
package controllers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 本文件包含组织架构模块的请求参数解析工具。
|
||||||
|
// 前端存在 JSON、form-urlencoded 与 multipart/form-data 三种提交方式,
|
||||||
|
// 因此每个取值函数都先读 JSON body,再回退到表单参数。
|
||||||
|
|
||||||
|
func (c *BackendOrganizationController) parseJSONBody() map[string]interface{} {
|
||||||
|
body := map[string]interface{}{}
|
||||||
|
contentType := strings.ToLower(c.Ctx.Input.Header("Content-Type"))
|
||||||
|
if !strings.Contains(contentType, "json") {
|
||||||
|
return body
|
||||||
|
}
|
||||||
|
if len(c.Ctx.Input.RequestBody) == 0 {
|
||||||
|
return body
|
||||||
|
}
|
||||||
|
_ = json.Unmarshal(c.Ctx.Input.RequestBody, &body)
|
||||||
|
return body
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensureFormParsed 在读取表单参数前确保请求体已被解析。
|
||||||
|
func (c *BackendOrganizationController) ensureFormParsed() {
|
||||||
|
if c.Ctx.Request.Form == nil && c.Ctx.Request.PostForm == nil && c.Ctx.Request.MultipartForm == nil {
|
||||||
|
_ = c.Ctx.Request.ParseMultipartForm(32 << 20)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *BackendOrganizationController) getStringValue(body map[string]interface{}, keys ...string) (string, bool) {
|
||||||
|
for _, key := range keys {
|
||||||
|
if v, ok := body[key]; ok {
|
||||||
|
switch val := v.(type) {
|
||||||
|
case string:
|
||||||
|
return val, true
|
||||||
|
case float64:
|
||||||
|
return strconv.FormatFloat(val, 'f', -1, 64), true
|
||||||
|
case bool:
|
||||||
|
return strconv.FormatBool(val), true
|
||||||
|
case nil:
|
||||||
|
return "", true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.ensureFormParsed()
|
||||||
|
if val := c.GetString(key); val != "" {
|
||||||
|
return val, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *BackendOrganizationController) getIntValue(body map[string]interface{}, keys ...string) (int, bool) {
|
||||||
|
for _, key := range keys {
|
||||||
|
if v, ok := body[key]; ok {
|
||||||
|
switch val := v.(type) {
|
||||||
|
case float64:
|
||||||
|
return int(val), true
|
||||||
|
case int:
|
||||||
|
return val, true
|
||||||
|
case bool:
|
||||||
|
return boolInt(val), true
|
||||||
|
case string:
|
||||||
|
trimmed := strings.TrimSpace(val)
|
||||||
|
if trimmed == "" {
|
||||||
|
return 0, true
|
||||||
|
}
|
||||||
|
parsed, err := strconv.Atoi(trimmed)
|
||||||
|
return parsed, err == nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.ensureFormParsed()
|
||||||
|
if val := c.GetString(key); val != "" {
|
||||||
|
parsed, err := strconv.Atoi(strings.TrimSpace(val))
|
||||||
|
return parsed, err == nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *BackendOrganizationController) getUintValue(body map[string]interface{}, keys ...string) (uint, bool) {
|
||||||
|
v, ok := c.getIntValue(body, keys...)
|
||||||
|
if !ok || v < 0 {
|
||||||
|
return 0, ok && v >= 0
|
||||||
|
}
|
||||||
|
return uint(v), true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *BackendOrganizationController) getUint64Value(body map[string]interface{}, keys ...string) (uint64, bool) {
|
||||||
|
for _, key := range keys {
|
||||||
|
if v, ok := body[key]; ok {
|
||||||
|
switch val := v.(type) {
|
||||||
|
case float64:
|
||||||
|
if val < 0 {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
return uint64(val), true
|
||||||
|
case int:
|
||||||
|
if val < 0 {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
return uint64(val), true
|
||||||
|
case nil:
|
||||||
|
return 0, true
|
||||||
|
case string:
|
||||||
|
trimmed := strings.TrimSpace(val)
|
||||||
|
if trimmed == "" {
|
||||||
|
return 0, true
|
||||||
|
}
|
||||||
|
parsed, err := strconv.ParseUint(trimmed, 10, 64)
|
||||||
|
return parsed, err == nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.ensureFormParsed()
|
||||||
|
if val := c.GetString(key); val != "" {
|
||||||
|
parsed, err := strconv.ParseUint(strings.TrimSpace(val), 10, 64)
|
||||||
|
return parsed, err == nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *BackendOrganizationController) getBoolValue(body map[string]interface{}, keys ...string) (bool, bool) {
|
||||||
|
for _, key := range keys {
|
||||||
|
if v, ok := body[key]; ok {
|
||||||
|
switch val := v.(type) {
|
||||||
|
case bool:
|
||||||
|
return val, true
|
||||||
|
case float64:
|
||||||
|
return val != 0, true
|
||||||
|
case string:
|
||||||
|
trimmed := strings.ToLower(strings.TrimSpace(val))
|
||||||
|
switch trimmed {
|
||||||
|
case "1", "true", "yes", "on":
|
||||||
|
return true, true
|
||||||
|
case "0", "false", "no", "off", "":
|
||||||
|
return false, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.ensureFormParsed()
|
||||||
|
if val := c.GetString(key); val != "" {
|
||||||
|
switch strings.ToLower(strings.TrimSpace(val)) {
|
||||||
|
case "1", "true", "yes", "on":
|
||||||
|
return true, true
|
||||||
|
case "0", "false", "no", "off":
|
||||||
|
return false, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// getUint64Slice 解析 ID 数组,兼容 JSON 数组与逗号分隔字符串。
|
||||||
|
func (c *BackendOrganizationController) getUint64Slice(body map[string]interface{}, keys ...string) []uint64 {
|
||||||
|
result := make([]uint64, 0)
|
||||||
|
seen := map[uint64]bool{}
|
||||||
|
|
||||||
|
appendID := func(id uint64) {
|
||||||
|
if id == 0 || seen[id] {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
seen[id] = true
|
||||||
|
result = append(result, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, key := range keys {
|
||||||
|
if v, ok := body[key]; ok {
|
||||||
|
switch val := v.(type) {
|
||||||
|
case []interface{}:
|
||||||
|
for _, item := range val {
|
||||||
|
switch num := item.(type) {
|
||||||
|
case float64:
|
||||||
|
appendID(uint64(num))
|
||||||
|
case string:
|
||||||
|
if parsed, err := strconv.ParseUint(strings.TrimSpace(num), 10, 64); err == nil {
|
||||||
|
appendID(parsed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case string:
|
||||||
|
for _, part := range strings.Split(val, ",") {
|
||||||
|
if parsed, err := strconv.ParseUint(strings.TrimSpace(part), 10, 64); err == nil {
|
||||||
|
appendID(parsed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case float64:
|
||||||
|
appendID(uint64(val))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(result) > 0 {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
c.ensureFormParsed()
|
||||||
|
if raw := c.GetString(key); raw != "" {
|
||||||
|
for _, part := range strings.Split(raw, ",") {
|
||||||
|
if parsed, err := strconv.ParseUint(strings.TrimSpace(part), 10, 64); err == nil {
|
||||||
|
appendID(parsed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(result) > 0 {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *BackendOrganizationController) getUintSlice(body map[string]interface{}, keys ...string) []uint {
|
||||||
|
ids := c.getUint64Slice(body, keys...)
|
||||||
|
result := make([]uint, 0, len(ids))
|
||||||
|
for _, id := range ids {
|
||||||
|
result = append(result, uint(id))
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *BackendOrganizationController) pathUint(name string) (uint, bool) {
|
||||||
|
id, err := strconv.ParseUint(c.Ctx.Input.Param(name), 10, 64)
|
||||||
|
return uint(id), err == nil && id > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *BackendOrganizationController) pathUint64(name string) (uint64, bool) {
|
||||||
|
id, err := strconv.ParseUint(c.Ctx.Input.Param(name), 10, 64)
|
||||||
|
return id, err == nil && id > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *BackendOrganizationController) nowString() string {
|
||||||
|
return time.Now().Format("2006-01-02 15:04:05")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 与 ORM 交互的通用小工具
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func strPtrIfNotEmpty(v string) *string {
|
||||||
|
v = strings.TrimSpace(v)
|
||||||
|
if v == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &v
|
||||||
|
}
|
||||||
|
|
||||||
|
// nullableString 空字符串写入 NULL,便于统一区分“未填写”与“空值”。
|
||||||
|
func nullableString(v string) interface{} {
|
||||||
|
v = strings.TrimSpace(v)
|
||||||
|
if v == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func nullableUint64(v uint64) interface{} {
|
||||||
|
if v == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func derefString(v *string) string {
|
||||||
|
if v == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return *v
|
||||||
|
}
|
||||||
|
|
||||||
|
func boolInt(v bool) int {
|
||||||
|
if v {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseDatePtr(v string) *time.Time {
|
||||||
|
v = strings.TrimSpace(v)
|
||||||
|
if v == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if t, err := time.Parse("2006-01-02", v); err == nil {
|
||||||
|
return &t
|
||||||
|
}
|
||||||
|
if t, err := time.Parse("2006-01-02 15:04:05", v); err == nil {
|
||||||
|
return &t
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,405 @@
|
|||||||
|
package controllers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/csv"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"server/models"
|
||||||
|
|
||||||
|
"github.com/beego/beego/v2/client/orm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 本文件承载组织架构模块的设置读写、CSV 导入导出,以及 DTO 组装与通用工具函数。
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 组织架构设置(按租户存放在 yz_backend_normal_setting 中)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// GetOrgSettings 获取当前租户的组织架构设置。
|
||||||
|
// GET /backend/{erp|oa}/getOrgSettings
|
||||||
|
func (c *BackendOrganizationController) GetOrgSettings() {
|
||||||
|
tid, ok := c.tenantID()
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.jsonOK(c.loadOrgSettings(tid))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveOrgSettings 保存当前租户的组织架构设置。
|
||||||
|
// POST /backend/{erp|oa}/saveOrgSettings
|
||||||
|
func (c *BackendOrganizationController) SaveOrgSettings() {
|
||||||
|
tid, ok := c.tenantID()
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
body := c.parseJSONBody()
|
||||||
|
settings := c.loadOrgSettings(tid)
|
||||||
|
|
||||||
|
if v, has := c.getStringValue(body, "org_code_prefix", "code_prefix"); has {
|
||||||
|
settings.OrgCodePrefix = strings.TrimSpace(v)
|
||||||
|
}
|
||||||
|
if v, has := c.getStringValue(body, "employee_code_prefix"); has {
|
||||||
|
settings.EmployeeCodePrefix = strings.TrimSpace(v)
|
||||||
|
}
|
||||||
|
if v, has := c.getStringValue(body, "position_code_prefix"); has {
|
||||||
|
settings.PositionCodePrefix = strings.TrimSpace(v)
|
||||||
|
}
|
||||||
|
if v, has := c.getBoolValue(body, "auto_generate_codes", "auto_generate_code"); has {
|
||||||
|
settings.AutoGenerateCodes = v
|
||||||
|
}
|
||||||
|
if v, has := c.getIntValue(body, "code_length"); has {
|
||||||
|
settings.CodeLength = clampInt(v, 4, 32)
|
||||||
|
}
|
||||||
|
if v, has := c.getIntValue(body, "default_org_type"); has {
|
||||||
|
settings.DefaultOrgType = v
|
||||||
|
}
|
||||||
|
if v, has := c.getIntValue(body, "default_sort"); has {
|
||||||
|
settings.DefaultSort = maxInt(v, 0)
|
||||||
|
}
|
||||||
|
if v, has := c.getIntValue(body, "default_status"); has {
|
||||||
|
settings.DefaultStatus = v
|
||||||
|
}
|
||||||
|
if v, has := c.getIntValue(body, "max_org_levels", "max_level"); has {
|
||||||
|
settings.MaxOrgLevels = clampInt(v, 1, 32)
|
||||||
|
}
|
||||||
|
if v, has := c.getIntValue(body, "max_org_children"); has {
|
||||||
|
settings.MaxOrgChildren = clampInt(v, 1, 1000)
|
||||||
|
}
|
||||||
|
if v, has := c.getBoolValue(body, "allow_duplicate_codes"); has {
|
||||||
|
settings.AllowDuplicateCode = v
|
||||||
|
}
|
||||||
|
if v, has := c.getBoolValue(body, "batch_operations"); has {
|
||||||
|
settings.BatchOperations = v
|
||||||
|
}
|
||||||
|
if v, has := c.getBoolValue(body, "export_enabled"); has {
|
||||||
|
settings.ExportEnabled = v
|
||||||
|
}
|
||||||
|
if v, has := c.getBoolValue(body, "import_enabled"); has {
|
||||||
|
settings.ImportEnabled = v
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.persistOrgSettings(tid, settings); err != nil {
|
||||||
|
c.jsonError(500, "保存组织设置失败: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.jsonOK(settings)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *BackendOrganizationController) orgSettingsCode(tid uint64) string {
|
||||||
|
return fmt.Sprintf("%s_%d", orgSettingsCodePrefix, tid)
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadOrgSettings 读取租户设置;无记录或解析失败时回退到默认值,保证接口始终可用。
|
||||||
|
func (c *BackendOrganizationController) loadOrgSettings(tid uint64) orgSettings {
|
||||||
|
settings := defaultOrgSettings()
|
||||||
|
|
||||||
|
var row models.BackendNormalSetting
|
||||||
|
err := models.Orm.QueryTable(new(models.BackendNormalSetting)).
|
||||||
|
Filter("code", c.orgSettingsCode(tid)).
|
||||||
|
Filter("delete_time__isnull", true).
|
||||||
|
One(&row)
|
||||||
|
if err != nil || strings.TrimSpace(row.Value) == "" {
|
||||||
|
return settings
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal([]byte(row.Value), &settings); err != nil {
|
||||||
|
return defaultOrgSettings()
|
||||||
|
}
|
||||||
|
if settings.CodeLength <= 0 {
|
||||||
|
settings.CodeLength = 8
|
||||||
|
}
|
||||||
|
return settings
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *BackendOrganizationController) persistOrgSettings(tid uint64, settings orgSettings) error {
|
||||||
|
raw, err := json.Marshal(settings)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
code := c.orgSettingsCode(tid)
|
||||||
|
|
||||||
|
var row models.BackendNormalSetting
|
||||||
|
err = models.Orm.QueryTable(new(models.BackendNormalSetting)).
|
||||||
|
Filter("code", code).
|
||||||
|
Filter("delete_time__isnull", true).
|
||||||
|
One(&row)
|
||||||
|
if err == nil {
|
||||||
|
now := time.Now()
|
||||||
|
row.Value = string(raw)
|
||||||
|
row.UpdateTime = &now
|
||||||
|
_, err = models.Orm.Update(&row, "value", "update_time")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
row = models.BackendNormalSetting{
|
||||||
|
Name: "组织架构设置",
|
||||||
|
Code: code,
|
||||||
|
Value: string(raw),
|
||||||
|
Remark: fmt.Sprintf("租户 %d 的组织架构设置", tid),
|
||||||
|
}
|
||||||
|
_, err = models.Orm.Insert(&row)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 导入 / 导出(CSV,带 UTF-8 BOM,Excel 可直接打开)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
var organizationExportHeader = []string{
|
||||||
|
"组织编码", "组织名称", "上级组织编码", "是否公司(1是0否)", "排序", "状态(1启用0禁用)", "备注",
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExportOrganization 导出当前租户组织架构为 CSV。
|
||||||
|
// GET /backend/{erp|oa}/exportOrganization
|
||||||
|
func (c *BackendOrganizationController) ExportOrganization() {
|
||||||
|
tid, ok := c.tenantID()
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !c.loadOrgSettings(tid).ExportEnabled {
|
||||||
|
c.jsonError(400, "导出功能已关闭")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var rows []models.BackendOrganization
|
||||||
|
if _, err := c.orgQuery(tid).OrderBy("sort", "id").All(&rows); err != nil {
|
||||||
|
c.jsonError(500, "导出组织架构失败: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
codeByID := map[uint64]string{}
|
||||||
|
for _, row := range rows {
|
||||||
|
codeByID[row.ID] = row.OrgCode
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Ctx.Output.Header("Content-Type", "text/csv; charset=utf-8")
|
||||||
|
c.Ctx.Output.Header("Content-Disposition",
|
||||||
|
fmt.Sprintf("attachment; filename=organization_%s.csv", time.Now().Format("20060102150405")))
|
||||||
|
|
||||||
|
// UTF-8 BOM,避免 Excel 打开中文乱码
|
||||||
|
_, _ = c.Ctx.ResponseWriter.Write([]byte{0xEF, 0xBB, 0xBF})
|
||||||
|
writer := csv.NewWriter(c.Ctx.ResponseWriter)
|
||||||
|
_ = writer.Write(organizationExportHeader)
|
||||||
|
for _, row := range rows {
|
||||||
|
_ = writer.Write([]string{
|
||||||
|
row.OrgCode,
|
||||||
|
row.OrgName,
|
||||||
|
codeByID[row.ParentID],
|
||||||
|
strconv.Itoa(row.IsCompany),
|
||||||
|
strconv.FormatUint(uint64(row.Sort), 10),
|
||||||
|
strconv.Itoa(int(row.Status)),
|
||||||
|
derefString(row.Remark),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
writer.Flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImportOrganization 从 CSV 导入组织架构。
|
||||||
|
// 已存在的组织编码执行更新,不存在的新增;上级组织通过编码关联,
|
||||||
|
// 上级关系在所有行入库后统一回填,因此 CSV 行序不影响结果。
|
||||||
|
// POST /backend/{erp|oa}/importOrganization (multipart/form-data, field=file)
|
||||||
|
func (c *BackendOrganizationController) ImportOrganization() {
|
||||||
|
tid, ok := c.tenantID()
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !c.loadOrgSettings(tid).ImportEnabled {
|
||||||
|
c.jsonError(400, "导入功能已关闭")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
file, _, err := c.GetFile("file")
|
||||||
|
if err != nil {
|
||||||
|
c.jsonError(400, "请上传 CSV 文件")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
reader := csv.NewReader(newBOMTrimReader(file))
|
||||||
|
reader.FieldsPerRecord = -1
|
||||||
|
records, err := reader.ReadAll()
|
||||||
|
if err != nil {
|
||||||
|
c.jsonError(400, "解析 CSV 失败: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(records) <= 1 {
|
||||||
|
c.jsonError(400, "CSV 中没有可导入的数据")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
idByCode := map[string]uint64{}
|
||||||
|
var existing []models.BackendOrganization
|
||||||
|
if _, err := c.orgQuery(tid).All(&existing); err != nil {
|
||||||
|
c.jsonError(500, "读取已有组织失败: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, row := range existing {
|
||||||
|
idByCode[row.OrgCode] = row.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
type pendingParent struct {
|
||||||
|
code string
|
||||||
|
parentCode string
|
||||||
|
}
|
||||||
|
|
||||||
|
created, updated := 0, 0
|
||||||
|
failures := make([]string, 0)
|
||||||
|
pending := make([]pendingParent, 0, len(records))
|
||||||
|
|
||||||
|
for i, record := range records[1:] {
|
||||||
|
lineNo := i + 2
|
||||||
|
if len(record) < 2 {
|
||||||
|
failures = append(failures, fmt.Sprintf("第 %d 行:列数不足", lineNo))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
orgCode := strings.TrimSpace(record[0])
|
||||||
|
orgName := strings.TrimSpace(record[1])
|
||||||
|
if orgCode == "" || orgName == "" {
|
||||||
|
failures = append(failures, fmt.Sprintf("第 %d 行:组织编码与名称不能为空", lineNo))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
parentCode := csvField(record, 2)
|
||||||
|
isCompany := csvInt(record, 3, 0)
|
||||||
|
sortVal := csvInt(record, 4, 0)
|
||||||
|
status := csvInt(record, 5, 1)
|
||||||
|
remark := csvField(record, 6)
|
||||||
|
|
||||||
|
if id, exists := idByCode[orgCode]; exists {
|
||||||
|
update := orm.Params{
|
||||||
|
"org_name": orgName,
|
||||||
|
"is_company": isCompany,
|
||||||
|
"sort": uint(maxInt(sortVal, 0)),
|
||||||
|
"status": int8(status),
|
||||||
|
"remark": nullableString(remark),
|
||||||
|
}
|
||||||
|
if _, err := c.orgQuery(tid).Filter("id", id).Update(update); err != nil {
|
||||||
|
failures = append(failures, fmt.Sprintf("第 %d 行:更新失败 %s", lineNo, err.Error()))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
updated++
|
||||||
|
} else {
|
||||||
|
row := models.BackendOrganization{
|
||||||
|
Tid: tid,
|
||||||
|
OrgName: orgName,
|
||||||
|
OrgCode: orgCode,
|
||||||
|
IsCompany: isCompany,
|
||||||
|
Sort: uint(maxInt(sortVal, 0)),
|
||||||
|
Status: int8(status),
|
||||||
|
Remark: strPtrIfNotEmpty(remark),
|
||||||
|
}
|
||||||
|
id, err := models.Orm.Insert(&row)
|
||||||
|
if err != nil {
|
||||||
|
failures = append(failures, fmt.Sprintf("第 %d 行:创建失败 %s", lineNo, err.Error()))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
idByCode[orgCode] = uint64(id)
|
||||||
|
created++
|
||||||
|
}
|
||||||
|
|
||||||
|
pending = append(pending, pendingParent{code: orgCode, parentCode: parentCode})
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, item := range pending {
|
||||||
|
selfID := idByCode[item.code]
|
||||||
|
if selfID == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
parentID := uint64(0)
|
||||||
|
if item.parentCode != "" {
|
||||||
|
parentID = idByCode[item.parentCode]
|
||||||
|
if parentID == 0 {
|
||||||
|
failures = append(failures, fmt.Sprintf("组织 %s:上级编码 %s 不存在", item.code, item.parentCode))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if parentID == selfID {
|
||||||
|
failures = append(failures, fmt.Sprintf("组织 %s:上级不能是自己", item.code))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, _ = c.orgQuery(tid).Filter("id", selfID).
|
||||||
|
Update(orm.Params{"parent_id": parentID, "is_company": boolInt(parentID == 0)})
|
||||||
|
}
|
||||||
|
|
||||||
|
c.jsonOK(map[string]interface{}{
|
||||||
|
"created": created,
|
||||||
|
"updated": updated,
|
||||||
|
"failed": len(failures),
|
||||||
|
"failures": failures,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetImportTemplate 下载导入模板(仅表头 + 一行示例)。
|
||||||
|
// GET /backend/{erp|oa}/organizationImportTemplate
|
||||||
|
func (c *BackendOrganizationController) GetImportTemplate() {
|
||||||
|
if _, ok := c.tenantID(); !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Ctx.Output.Header("Content-Type", "text/csv; charset=utf-8")
|
||||||
|
c.Ctx.Output.Header("Content-Disposition", "attachment; filename=organization_template.csv")
|
||||||
|
|
||||||
|
_, _ = c.Ctx.ResponseWriter.Write([]byte{0xEF, 0xBB, 0xBF})
|
||||||
|
writer := csv.NewWriter(c.Ctx.ResponseWriter)
|
||||||
|
_ = writer.Write(organizationExportHeader)
|
||||||
|
_ = writer.Write([]string{"COM001", "示例总公司", "", "1", "0", "1", "顶级组织,上级编码留空"})
|
||||||
|
_ = writer.Write([]string{"DEP001", "示例研发部", "COM001", "0", "1", "1", "隶属 COM001"})
|
||||||
|
writer.Flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
// bomTrimReader 去掉 CSV 文件开头可能存在的 UTF-8 BOM。
|
||||||
|
type bomTrimReader struct {
|
||||||
|
reader io.Reader
|
||||||
|
checked bool
|
||||||
|
buf []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func newBOMTrimReader(r io.Reader) io.Reader {
|
||||||
|
return &bomTrimReader{reader: r}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *bomTrimReader) Read(p []byte) (int, error) {
|
||||||
|
if !r.checked {
|
||||||
|
r.checked = true
|
||||||
|
head := make([]byte, 3)
|
||||||
|
n, err := io.ReadFull(r.reader, head)
|
||||||
|
if n == 3 && head[0] == 0xEF && head[1] == 0xBB && head[2] == 0xBF {
|
||||||
|
r.buf = nil
|
||||||
|
} else {
|
||||||
|
r.buf = head[:n]
|
||||||
|
}
|
||||||
|
if err != nil && err != io.ErrUnexpectedEOF && err != io.EOF {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(r.buf) > 0 {
|
||||||
|
n := copy(p, r.buf)
|
||||||
|
r.buf = r.buf[n:]
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
return r.reader.Read(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func csvField(record []string, index int) string {
|
||||||
|
if index >= len(record) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(record[index])
|
||||||
|
}
|
||||||
|
|
||||||
|
func csvInt(record []string, index int, fallback int) int {
|
||||||
|
raw := csvField(record, index)
|
||||||
|
if raw == "" {
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
v, err := strconv.Atoi(raw)
|
||||||
|
if err != nil {
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
@@ -0,0 +1,185 @@
|
|||||||
|
package controllers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 这些用例只覆盖组织架构模块中不依赖数据库的纯函数:树组装、层级计算、CSV 解析与编码生成。
|
||||||
|
|
||||||
|
func TestBuildOrganizationTree(t *testing.T) {
|
||||||
|
list := []organizationDTO{
|
||||||
|
{ID: 1, OrgName: "总公司", ParentID: 0},
|
||||||
|
{ID: 2, OrgName: "研发部", ParentID: 1},
|
||||||
|
{ID: 3, OrgName: "前端组", ParentID: 2},
|
||||||
|
{ID: 4, OrgName: "孤儿部门", ParentID: 999}, // 上级不在列表中,应作为根节点保留
|
||||||
|
}
|
||||||
|
|
||||||
|
tree := buildOrganizationTree(list)
|
||||||
|
if len(tree) != 2 {
|
||||||
|
t.Fatalf("期望 2 个根节点,实际 %d", len(tree))
|
||||||
|
}
|
||||||
|
|
||||||
|
root := tree[0]
|
||||||
|
if root["org_name"] != "总公司" {
|
||||||
|
t.Fatalf("第一个根节点应为总公司,实际 %v", root["org_name"])
|
||||||
|
}
|
||||||
|
|
||||||
|
children, ok := root["children"].([]map[string]interface{})
|
||||||
|
if !ok || len(children) != 1 {
|
||||||
|
t.Fatalf("总公司应有 1 个子节点,实际 %v", root["children"])
|
||||||
|
}
|
||||||
|
grandChildren, ok := children[0]["children"].([]map[string]interface{})
|
||||||
|
if !ok || len(grandChildren) != 1 || grandChildren[0]["org_name"] != "前端组" {
|
||||||
|
t.Fatalf("研发部下应有前端组,实际 %v", children[0]["children"])
|
||||||
|
}
|
||||||
|
|
||||||
|
if tree[1]["org_name"] != "孤儿部门" {
|
||||||
|
t.Fatalf("上级缺失的节点应作为根节点保留,实际 %v", tree[1]["org_name"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTreeDepth(t *testing.T) {
|
||||||
|
childrenOf := map[uint64][]uint64{
|
||||||
|
0: {1},
|
||||||
|
1: {2, 3},
|
||||||
|
2: {4},
|
||||||
|
}
|
||||||
|
if got := treeDepth(childrenOf, 0, 0); got != 3 {
|
||||||
|
t.Fatalf("期望深度 3,实际 %d", got)
|
||||||
|
}
|
||||||
|
if got := treeDepth(map[uint64][]uint64{}, 0, 0); got != 0 {
|
||||||
|
t.Fatalf("空树深度应为 0,实际 %d", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSubtreeHeightFrom(t *testing.T) {
|
||||||
|
childrenOf := map[uint64][]uint64{
|
||||||
|
1: {2, 3},
|
||||||
|
2: {4},
|
||||||
|
}
|
||||||
|
if got := subtreeHeightFrom(childrenOf, 1, 0); got != 3 {
|
||||||
|
t.Fatalf("以 1 为根的子树高度应为 3,实际 %d", got)
|
||||||
|
}
|
||||||
|
if got := subtreeHeightFrom(childrenOf, 4, 0); got != 1 {
|
||||||
|
t.Fatalf("叶子节点高度应为 1,实际 %d", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCSVHelpers(t *testing.T) {
|
||||||
|
record := []string{" COM001 ", "总公司", "", "1"}
|
||||||
|
|
||||||
|
if got := csvField(record, 0); got != "COM001" {
|
||||||
|
t.Fatalf("csvField 应去掉空格,实际 %q", got)
|
||||||
|
}
|
||||||
|
if got := csvField(record, 9); got != "" {
|
||||||
|
t.Fatalf("越界应返回空串,实际 %q", got)
|
||||||
|
}
|
||||||
|
if got := csvInt(record, 3, 0); got != 1 {
|
||||||
|
t.Fatalf("csvInt 应解析出 1,实际 %d", got)
|
||||||
|
}
|
||||||
|
if got := csvInt(record, 2, 7); got != 7 {
|
||||||
|
t.Fatalf("空值应返回默认值 7,实际 %d", got)
|
||||||
|
}
|
||||||
|
if got := csvInt(record, 1, 5); got != 5 {
|
||||||
|
t.Fatalf("非数字应返回默认值 5,实际 %d", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBOMTrimReader(t *testing.T) {
|
||||||
|
withBOM := append([]byte{0xEF, 0xBB, 0xBF}, []byte("编码,名称\n")...)
|
||||||
|
got, err := io.ReadAll(newBOMTrimReader(strings.NewReader(string(withBOM))))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("读取失败: %v", err)
|
||||||
|
}
|
||||||
|
if string(got) != "编码,名称\n" {
|
||||||
|
t.Fatalf("BOM 未被去掉,实际 %q", string(got))
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err = io.ReadAll(newBOMTrimReader(strings.NewReader("编码,名称\n")))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("读取失败: %v", err)
|
||||||
|
}
|
||||||
|
if string(got) != "编码,名称\n" {
|
||||||
|
t.Fatalf("无 BOM 时内容被破坏,实际 %q", string(got))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 内容短于 3 字节时不能丢数据
|
||||||
|
got, err = io.ReadAll(newBOMTrimReader(strings.NewReader("ab")))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("读取失败: %v", err)
|
||||||
|
}
|
||||||
|
if string(got) != "ab" {
|
||||||
|
t.Fatalf("短内容被破坏,实际 %q", string(got))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGenerateCode(t *testing.T) {
|
||||||
|
c := &BackendOrganizationController{}
|
||||||
|
|
||||||
|
code := c.generateCode("ORG", 8)
|
||||||
|
if len(code) != 8 || !strings.HasPrefix(code, "ORG") {
|
||||||
|
t.Fatalf("期望 8 位且以 ORG 开头,实际 %q", code)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 长度限制小于前缀长度时不截断前缀
|
||||||
|
code = c.generateCode("PREFIX", 3)
|
||||||
|
if !strings.HasPrefix(code, "PREFIX") {
|
||||||
|
t.Fatalf("前缀不应被截断,实际 %q", code)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 不限制长度时返回完整前缀 + 时间戳
|
||||||
|
code = c.generateCode("EMP", 0)
|
||||||
|
if len(code) != len("EMP")+14 {
|
||||||
|
t.Fatalf("未限制长度时应为前缀加 14 位时间戳,实际 %q", code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUniqueMessageAndClamp(t *testing.T) {
|
||||||
|
if got := uniqueMessage(true, "编码"); got != "编码可用" {
|
||||||
|
t.Fatalf("实际 %q", got)
|
||||||
|
}
|
||||||
|
if got := uniqueMessage(false, "账号"); got != "账号已存在" {
|
||||||
|
t.Fatalf("实际 %q", got)
|
||||||
|
}
|
||||||
|
if got := clampInt(0, 4, 32); got != 4 {
|
||||||
|
t.Fatalf("下界钳制失败,实际 %d", got)
|
||||||
|
}
|
||||||
|
if got := clampInt(99, 4, 32); got != 32 {
|
||||||
|
t.Fatalf("上界钳制失败,实际 %d", got)
|
||||||
|
}
|
||||||
|
if got := clampInt(10, 4, 32); got != 10 {
|
||||||
|
t.Fatalf("区间内不应改变,实际 %d", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHashEmployeePassword(t *testing.T) {
|
||||||
|
if got := hashEmployeePassword(" "); got != "" {
|
||||||
|
t.Fatalf("空密码应返回空串,实际 %q", got)
|
||||||
|
}
|
||||||
|
got := hashEmployeePassword("secret123")
|
||||||
|
if len(got) != 64 {
|
||||||
|
t.Fatalf("sha256 hex 应为 64 位,实际 %d", len(got))
|
||||||
|
}
|
||||||
|
if got != hashEmployeePassword("secret123") {
|
||||||
|
t.Fatal("相同输入应得到相同结果")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOrgNameByIDString(t *testing.T) {
|
||||||
|
nameByID := map[uint64]string{7: "研发部"}
|
||||||
|
|
||||||
|
if got := orgNameByIDString(nameByID, "7"); got != "研发部" {
|
||||||
|
t.Fatalf("实际 %q", got)
|
||||||
|
}
|
||||||
|
if got := orgNameByIDString(nameByID, ""); got != "" {
|
||||||
|
t.Fatalf("空值应返回空串,实际 %q", got)
|
||||||
|
}
|
||||||
|
if got := orgNameByIDString(nameByID, "abc"); got != "" {
|
||||||
|
t.Fatalf("非数字应返回空串,实际 %q", got)
|
||||||
|
}
|
||||||
|
if got := orgNameByIDString(nameByID, "99"); got != "" {
|
||||||
|
t.Fatalf("未命中应返回空串,实际 %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,9 @@ package controllers
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -17,6 +20,31 @@ type IndexPortalController struct {
|
|||||||
beego.Controller
|
beego.Controller
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// reNewsRow 从模板标签 {yz:newscenter row="N" ...} 中提取 row 值
|
||||||
|
var reNewsRow = regexp.MustCompile(`\{yz:newscenter[^}]*\s+row\s*=\s*"(\d+)"`)
|
||||||
|
|
||||||
|
// templateNewsRow 读取当前租户首页模板中 newscenter 标签的 row 参数,
|
||||||
|
// 作为 API 默认条数,使接口与前端模板保持一致。
|
||||||
|
func templateNewsRow(tid uint64) int {
|
||||||
|
code := strings.TrimSpace(cmsTenantTemplateCode(tid))
|
||||||
|
if code == "" || code == "default" {
|
||||||
|
code = "default"
|
||||||
|
}
|
||||||
|
if !cmsTemplateDirExists(code) {
|
||||||
|
code = "default"
|
||||||
|
}
|
||||||
|
data, err := os.ReadFile(filepath.Join(cmsThemesRoot(), code, "index.html"))
|
||||||
|
if err != nil {
|
||||||
|
return 10
|
||||||
|
}
|
||||||
|
if m := reNewsRow.FindSubmatch(data); len(m) == 2 {
|
||||||
|
if n, e := strconv.Atoi(string(m[1])); e == nil && n > 0 && n <= 50 {
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 10
|
||||||
|
}
|
||||||
|
|
||||||
// getTidByHost 根据访问域名识别租户 ID,默认返回 1
|
// getTidByHost 根据访问域名识别租户 ID,默认返回 1
|
||||||
func (c *IndexPortalController) getTidByHost() uint64 {
|
func (c *IndexPortalController) getTidByHost() uint64 {
|
||||||
host := c.Ctx.Request.Host
|
host := c.Ctx.Request.Host
|
||||||
@@ -267,13 +295,15 @@ func (c *IndexPortalController) queryCmsArticles(categoryName string, categoryID
|
|||||||
tid := c.getTidByHost()
|
tid := c.getTidByHost()
|
||||||
_ = models.EnsureCmsArticleTables()
|
_ = models.EnsureCmsArticleTables()
|
||||||
|
|
||||||
|
defaultLimit := templateNewsRow(tid)
|
||||||
|
|
||||||
page, _ := c.GetInt("page", 1)
|
page, _ := c.GetInt("page", 1)
|
||||||
limit, _ := c.GetInt("limit", 10)
|
limit, _ := c.GetInt("limit", defaultLimit)
|
||||||
if page < 1 {
|
if page < 1 {
|
||||||
page = 1
|
page = 1
|
||||||
}
|
}
|
||||||
if limit < 1 {
|
if limit < 1 {
|
||||||
limit = 10
|
limit = defaultLimit
|
||||||
}
|
}
|
||||||
|
|
||||||
qs := models.Orm.QueryTable(new(models.CmsArticle)).
|
qs := models.Orm.QueryTable(new(models.CmsArticle)).
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ func (c *TenantSiteController) rewriteStorageURLs(html string) string {
|
|||||||
if err != nil || strings.TrimSpace(cfg.QiniuDomain) == "" {
|
if err != nil || strings.TrimSpace(cfg.QiniuDomain) == "" {
|
||||||
return html
|
return html
|
||||||
}
|
}
|
||||||
return rewriteLegacyQiniuURLs(html, storagePublicBaseURL(cfg.QiniuDomain))
|
return rewriteLegacyQiniuURLs(html, storagePublicBaseURL(cfg.QiniuDomain), cfg.QiniuDomain)
|
||||||
}
|
}
|
||||||
|
|
||||||
func storagePublicBaseURL(domain string) string {
|
func storagePublicBaseURL(domain string) string {
|
||||||
@@ -147,13 +147,18 @@ func storagePublicBaseURL(domain string) string {
|
|||||||
return "https://" + base
|
return "https://" + base
|
||||||
}
|
}
|
||||||
|
|
||||||
func rewriteLegacyQiniuURLs(html, targetBase string) string {
|
func rewriteLegacyQiniuURLs(html, targetBase, qiniuDomain string) string {
|
||||||
if targetBase == "" {
|
if targetBase == "" {
|
||||||
return html
|
return html
|
||||||
}
|
}
|
||||||
for _, domain := range legacyQiniuDomains {
|
domains := append(legacyQiniuDomains, qiniuDomain)
|
||||||
|
for _, domain := range domains {
|
||||||
|
d := strings.TrimSpace(domain)
|
||||||
|
if d == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
for _, scheme := range []string{"http://", "https://"} {
|
for _, scheme := range []string{"http://", "https://"} {
|
||||||
html = strings.ReplaceAll(html, scheme+domain+"/", targetBase+"/")
|
html = strings.ReplaceAll(html, scheme+d+"/", targetBase+"/")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return html
|
return html
|
||||||
|
|||||||
@@ -21,10 +21,10 @@ type BackendErpOrganization struct {
|
|||||||
|
|
||||||
// TableName 自定义表名
|
// TableName 自定义表名
|
||||||
func (m *BackendErpOrganization) TableName() string {
|
func (m *BackendErpOrganization) TableName() string {
|
||||||
return "yz_backend_organization"
|
return "yz_backend_erp_organization"
|
||||||
}
|
}
|
||||||
|
|
||||||
// BackendErpEmployee 全局员工信息表 yz_backend_employee
|
// BackendErpEmployee 员工信息表 yz_backend_erp_employee
|
||||||
type BackendErpEmployee struct {
|
type BackendErpEmployee struct {
|
||||||
ID uint `orm:"column(id);pk;auto" json:"id"`
|
ID uint `orm:"column(id);pk;auto" json:"id"`
|
||||||
Tid *int `orm:"column(tid);null" json:"tid"`
|
Tid *int `orm:"column(tid);null" json:"tid"`
|
||||||
@@ -50,10 +50,10 @@ type BackendErpEmployee struct {
|
|||||||
|
|
||||||
// TableName 自定义表名
|
// TableName 自定义表名
|
||||||
func (m *BackendErpEmployee) TableName() string {
|
func (m *BackendErpEmployee) TableName() string {
|
||||||
return "yz_backend_employee"
|
return "yz_backend_erp_employee"
|
||||||
}
|
}
|
||||||
|
|
||||||
// BackendErpPosition 全局职位表 yz_backend_position
|
// BackendErpPosition 职位表 yz_backend_erp_position
|
||||||
type BackendErpPosition struct {
|
type BackendErpPosition struct {
|
||||||
ID uint64 `orm:"column(id);pk;auto" json:"id"`
|
ID uint64 `orm:"column(id);pk;auto" json:"id"`
|
||||||
Tid uint64 `orm:"column(tid)" json:"tid"`
|
Tid uint64 `orm:"column(tid)" json:"tid"`
|
||||||
@@ -69,5 +69,5 @@ type BackendErpPosition struct {
|
|||||||
|
|
||||||
// TableName 自定义表名
|
// TableName 自定义表名
|
||||||
func (m *BackendErpPosition) TableName() string {
|
func (m *BackendErpPosition) TableName() string {
|
||||||
return "yz_backend_position"
|
return "yz_backend_erp_position"
|
||||||
}
|
}
|
||||||
@@ -126,6 +126,23 @@ func CmsCategoryNameMap(tid uint64, ids []uint64) map[uint64]string {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func CmsCategoryImageMap(tid uint64, ids []uint64) map[uint64]string {
|
||||||
|
out := make(map[uint64]string)
|
||||||
|
if len(ids) == 0 {
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
var rows []CmsArticleCategory
|
||||||
|
qs := Orm.QueryTable(new(CmsArticleCategory)).
|
||||||
|
Filter("id__in", ids).
|
||||||
|
Filter("delete_time__isnull", true)
|
||||||
|
cond := orm.NewCondition().Or("tid", 0).Or("tid", tid)
|
||||||
|
_, _ = qs.SetCond(cond).All(&rows, "ID", "Image")
|
||||||
|
for _, r := range rows {
|
||||||
|
out[r.ID] = r.Image
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
func CmsFormatTime(t *time.Time) string {
|
func CmsFormatTime(t *time.Time) string {
|
||||||
if t == nil {
|
if t == nil {
|
||||||
return ""
|
return ""
|
||||||
|
|||||||
@@ -426,11 +426,12 @@ func arclistProvider(tid uint64, params map[string]string, ctx *RenderCtx) ([]ma
|
|||||||
if err != nil && err != orm.ErrNoRows {
|
if err != nil && err != orm.ErrNoRows {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return articleRows(rows, titleLen), nil
|
cateImgMap := models.CmsCategoryImageMap(tid, collectCateIDs(rows))
|
||||||
|
return articleRows(rows, titleLen, cateImgMap), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// articleRows 文章行转标签字段
|
// articleRows 文章行转标签字段
|
||||||
func articleRows(rows []models.CmsArticle, titleLen int) []map[string]string {
|
func articleRows(rows []models.CmsArticle, titleLen int, cateImgMap map[uint64]string) []map[string]string {
|
||||||
out := make([]map[string]string, 0, len(rows))
|
out := make([]map[string]string, 0, len(rows))
|
||||||
for _, a := range rows {
|
for _, a := range rows {
|
||||||
pub := ""
|
pub := ""
|
||||||
@@ -443,12 +444,16 @@ func articleRows(rows []models.CmsArticle, titleLen int) []map[string]string {
|
|||||||
if titleLen > 0 {
|
if titleLen > 0 {
|
||||||
title = truncateTitle(title, titleLen)
|
title = truncateTitle(title, titleLen)
|
||||||
}
|
}
|
||||||
|
img := a.Image
|
||||||
|
if img == "" {
|
||||||
|
img = cateImgMap[a.CateID]
|
||||||
|
}
|
||||||
out = append(out, map[string]string{
|
out = append(out, map[string]string{
|
||||||
"id": strconv.FormatUint(a.ID, 10),
|
"id": strconv.FormatUint(a.ID, 10),
|
||||||
"title": title,
|
"title": title,
|
||||||
"titlefull": a.Title,
|
"titlefull": a.Title,
|
||||||
"desc": a.Desc,
|
"desc": a.Desc,
|
||||||
"image": a.Image,
|
"image": img,
|
||||||
"arcurl": "/news/" + strconv.FormatUint(a.ID, 10),
|
"arcurl": "/news/" + strconv.FormatUint(a.ID, 10),
|
||||||
"pubdate": pub,
|
"pubdate": pub,
|
||||||
"views": strconv.Itoa(a.Views),
|
"views": strconv.Itoa(a.Views),
|
||||||
@@ -458,6 +463,19 @@ func articleRows(rows []models.CmsArticle, titleLen int) []map[string]string {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// collectCateIDs 从文章列表中收集不重复的分类 ID
|
||||||
|
func collectCateIDs(rows []models.CmsArticle) []uint64 {
|
||||||
|
seen := make(map[uint64]bool)
|
||||||
|
var ids []uint64
|
||||||
|
for _, a := range rows {
|
||||||
|
if a.CateID > 0 && !seen[a.CateID] {
|
||||||
|
seen[a.CateID] = true
|
||||||
|
ids = append(ids, a.CateID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ids
|
||||||
|
}
|
||||||
|
|
||||||
// arcviewProvider 文章详情(详情页模板内),取当前路由文章
|
// arcviewProvider 文章详情(详情页模板内),取当前路由文章
|
||||||
func arcviewProvider(tid uint64, params map[string]string, ctx *RenderCtx) ([]map[string]string, error) {
|
func arcviewProvider(tid uint64, params map[string]string, ctx *RenderCtx) ([]map[string]string, error) {
|
||||||
if ctx.ArticleID == 0 {
|
if ctx.ArticleID == 0 {
|
||||||
@@ -478,12 +496,17 @@ func arcviewProvider(tid uint64, params map[string]string, ctx *RenderCtx) ([]ma
|
|||||||
} else {
|
} else {
|
||||||
pub = a.CreateTime.Format(datetimeLayout)
|
pub = a.CreateTime.Format(datetimeLayout)
|
||||||
}
|
}
|
||||||
|
img := a.Image
|
||||||
|
if img == "" && a.CateID > 0 {
|
||||||
|
cateImgMap := models.CmsCategoryImageMap(tid, []uint64{a.CateID})
|
||||||
|
img = cateImgMap[a.CateID]
|
||||||
|
}
|
||||||
return []map[string]string{{
|
return []map[string]string{{
|
||||||
"id": strconv.FormatUint(a.ID, 10),
|
"id": strconv.FormatUint(a.ID, 10),
|
||||||
"title": a.Title,
|
"title": a.Title,
|
||||||
"content": a.Content,
|
"content": a.Content,
|
||||||
"desc": a.Desc,
|
"desc": a.Desc,
|
||||||
"image": a.Image,
|
"image": img,
|
||||||
"author": a.Author,
|
"author": a.Author,
|
||||||
"pubdate": pub,
|
"pubdate": pub,
|
||||||
"views": strconv.Itoa(a.Views),
|
"views": strconv.Itoa(a.Views),
|
||||||
@@ -547,12 +570,34 @@ func newscenterHandler(tid uint64, params map[string]string, ctx *RenderCtx, bod
|
|||||||
All(&children)
|
All(&children)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// collectDescendantIDs 递归收集分类及其所有子孙分类 ID
|
||||||
|
collectDescendantIDs := func(rootID uint64) []uint64 {
|
||||||
|
ids := []uint64{rootID}
|
||||||
|
queue := []uint64{rootID}
|
||||||
|
for len(queue) > 0 {
|
||||||
|
pid := queue[0]
|
||||||
|
queue = queue[1:]
|
||||||
|
var subs []models.CmsArticleCategory
|
||||||
|
_, _ = models.Orm.QueryTable(new(models.CmsArticleCategory)).
|
||||||
|
Filter("cid", pid).
|
||||||
|
Filter("tid__in", []uint64{0, tid}).
|
||||||
|
Filter("status", 1).
|
||||||
|
Filter("delete_time__isnull", true).
|
||||||
|
All(&subs)
|
||||||
|
for _, s := range subs {
|
||||||
|
ids = append(ids, s.ID)
|
||||||
|
queue = append(queue, s.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ids
|
||||||
|
}
|
||||||
|
|
||||||
renderCards := func(cateID uint64) string {
|
renderCards := func(cateID uint64) string {
|
||||||
qs := publishedCond(models.Orm.QueryTable(new(models.CmsArticle)).
|
qs := publishedCond(models.Orm.QueryTable(new(models.CmsArticle)).
|
||||||
Filter("tid", tid).
|
Filter("tid", tid).
|
||||||
Filter("delete_time__isnull", true))
|
Filter("delete_time__isnull", true))
|
||||||
if cateID > 0 {
|
if cateID > 0 {
|
||||||
qs = qs.Filter("cate_id", cateID)
|
qs = qs.Filter("cate_id__in", collectDescendantIDs(cateID))
|
||||||
} else if parent != nil {
|
} else if parent != nil {
|
||||||
ids := []uint64{parent.ID}
|
ids := []uint64{parent.ID}
|
||||||
for _, c := range children {
|
for _, c := range children {
|
||||||
@@ -562,8 +607,9 @@ func newscenterHandler(tid uint64, params map[string]string, ctx *RenderCtx, bod
|
|||||||
}
|
}
|
||||||
var arts []models.CmsArticle
|
var arts []models.CmsArticle
|
||||||
_, _ = qs.OrderBy("-top", "-id").Limit(row).All(&arts)
|
_, _ = qs.OrderBy("-top", "-id").Limit(row).All(&arts)
|
||||||
|
cateImgMap := models.CmsCategoryImageMap(tid, collectCateIDs(arts))
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
for _, a := range articleRows(arts, titleLen) {
|
for _, a := range articleRows(arts, titleLen, cateImgMap) {
|
||||||
sb.WriteString(applyFields(body, a))
|
sb.WriteString(applyFields(body, a))
|
||||||
}
|
}
|
||||||
return sb.String()
|
return sb.String()
|
||||||
|
|||||||
@@ -96,35 +96,11 @@ func RegisterAuthRoutes() {
|
|||||||
beego.Router("/backend/deleteUser/:id", &controllers.BackendAdminUserController{}, "delete:DeleteUser")
|
beego.Router("/backend/deleteUser/:id", &controllers.BackendAdminUserController{}, "delete:DeleteUser")
|
||||||
beego.Router("/backend/changePassword", &controllers.BackendAdminUserController{}, "post:ChangePassword")
|
beego.Router("/backend/changePassword", &controllers.BackendAdminUserController{}, "post:ChangePassword")
|
||||||
|
|
||||||
// ERP 接口
|
// 组织架构(组织 / 员工 / 职位)
|
||||||
beego.Router("/backend/erp/getOrganization", &controllers.BackendErpController{}, "get:GetOrganization")
|
// 组织架构是租户端通用基础数据:进销存(erp)与办公自动化(oa)两套界面读写同一份数据,
|
||||||
beego.Router("/backend/erp/getOrganizationDetail/:id", &controllers.BackendErpController{}, "get:GetOrganizationDetail")
|
// 数据按 JWT 中的租户ID 隔离。两个前缀注册到同一个控制器,便于两端各自的菜单与权限配置。
|
||||||
beego.Router("/backend/erp/createOrganization", &controllers.BackendErpController{}, "post:CreateOrganization")
|
registerOrganizationRoutes("erp")
|
||||||
beego.Router("/backend/erp/editOrganization/:id", &controllers.BackendErpController{}, "post:EditOrganization")
|
registerOrganizationRoutes("oa")
|
||||||
beego.Router("/backend/erp/deleteOrganization/:id", &controllers.BackendErpController{}, "delete:DeleteOrganization")
|
|
||||||
beego.Router("/backend/erp/getCompanys", &controllers.BackendErpController{}, "get:GetCompanys")
|
|
||||||
beego.Router("/backend/erp/getDepartments", &controllers.BackendErpController{}, "get:GetDepartments")
|
|
||||||
beego.Router("/backend/erp/getEmployee", &controllers.BackendErpController{}, "get:GetEmployee")
|
|
||||||
beego.Router("/backend/erp/getEmployeeDetail/:id", &controllers.BackendErpController{}, "get:GetEmployeeDetail")
|
|
||||||
beego.Router("/backend/erp/createEmployee", &controllers.BackendErpController{}, "post:CreateEmployee")
|
|
||||||
beego.Router("/backend/erp/editEmployee/:id", &controllers.BackendErpController{}, "post:EditEmployee")
|
|
||||||
beego.Router("/backend/erp/deleteEmployee/:id", &controllers.BackendErpController{}, "delete:DeleteEmployee")
|
|
||||||
beego.Router("/backend/erp/getPosition", &controllers.BackendErpController{}, "get:GetPosition")
|
|
||||||
beego.Router("/backend/erp/getPositionDetail/:id", &controllers.BackendErpController{}, "get:GetPositionDetail")
|
|
||||||
beego.Router("/backend/erp/createPosition", &controllers.BackendErpController{}, "post:CreatePosition")
|
|
||||||
beego.Router("/backend/erp/editPosition/:id", &controllers.BackendErpController{}, "post:EditPosition")
|
|
||||||
beego.Router("/backend/erp/deletePosition/:id", &controllers.BackendErpController{}, "delete:DeletePosition")
|
|
||||||
|
|
||||||
// 新增组织架构接口
|
|
||||||
beego.Router("/backend/erp/getOrgSettings", &controllers.BackendErpController{}, "get:GetOrgSettings")
|
|
||||||
beego.Router("/backend/erp/saveOrgSettings", &controllers.BackendErpController{}, "post:SaveOrgSettings")
|
|
||||||
beego.Router("/backend/erp/getOrganizationTree", &controllers.BackendErpController{}, "get:GetOrganizationTree")
|
|
||||||
beego.Router("/backend/erp/searchOrganizations", &controllers.BackendErpController{}, "get:SearchOrganizations")
|
|
||||||
beego.Router("/backend/erp/getOrganizationEmployees/:org_id", &controllers.BackendErpController{}, "get:GetOrganizationEmployees")
|
|
||||||
beego.Router("/backend/erp/moveEmployeeToOrg", &controllers.BackendErpController{}, "post:MoveEmployeeToOrg")
|
|
||||||
beego.Router("/backend/erp/getOrganizationHierarchy/:org_id", &controllers.BackendErpController{}, "get:GetOrganizationHierarchy")
|
|
||||||
beego.Router("/backend/erp/checkOrgCodeUnique", &controllers.BackendErpController{}, "get:CheckOrgCodeUnique")
|
|
||||||
beego.Router("/backend/erp/checkEmployeeAccountUnique", &controllers.BackendErpController{}, "get:CheckEmployeeAccountUnique")
|
|
||||||
|
|
||||||
// 通讯录管理(与组织架构联动)
|
// 通讯录管理(与组织架构联动)
|
||||||
beego.Router("/backend/erp/contact/list", &controllers.BackendErpContactController{}, "get:List")
|
beego.Router("/backend/erp/contact/list", &controllers.BackendErpContactController{}, "get:List")
|
||||||
@@ -252,3 +228,51 @@ func RegisterAuthRoutes() {
|
|||||||
beego.Router("/backend/reminder/batchDelete", &controllers.BackendReminderController{}, "post:BatchDeleteReminder")
|
beego.Router("/backend/reminder/batchDelete", &controllers.BackendReminderController{}, "post:BatchDeleteReminder")
|
||||||
beego.Router("/backend/reminder/finish/:id", &controllers.BackendReminderController{}, "post:FinishReminder")
|
beego.Router("/backend/reminder/finish/:id", &controllers.BackendReminderController{}, "post:FinishReminder")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// registerOrganizationRoutes 为指定模块前缀注册组织架构路由。
|
||||||
|
// 进销存(erp)与办公自动化(oa)各有独立界面,但共用 BackendOrganizationController 与同一套表,
|
||||||
|
// 因此这里用同一份定义注册两遍,只有 URL 前缀不同。
|
||||||
|
func registerOrganizationRoutes(module string) {
|
||||||
|
prefix := "/backend/" + module
|
||||||
|
|
||||||
|
// 组织机构
|
||||||
|
beego.Router(prefix+"/getOrganization", &controllers.BackendOrganizationController{}, "get:GetOrganization")
|
||||||
|
beego.Router(prefix+"/getOrganizationTree", &controllers.BackendOrganizationController{}, "get:GetOrganizationTree")
|
||||||
|
beego.Router(prefix+"/getOrganizationDetail/:id", &controllers.BackendOrganizationController{}, "get:GetOrganizationDetail")
|
||||||
|
beego.Router(prefix+"/createOrganization", &controllers.BackendOrganizationController{}, "post:CreateOrganization")
|
||||||
|
beego.Router(prefix+"/editOrganization/:id", &controllers.BackendOrganizationController{}, "post:EditOrganization")
|
||||||
|
beego.Router(prefix+"/deleteOrganization/:id", &controllers.BackendOrganizationController{}, "delete:DeleteOrganization")
|
||||||
|
beego.Router(prefix+"/getCompanys", &controllers.BackendOrganizationController{}, "get:GetCompanys")
|
||||||
|
beego.Router(prefix+"/getDepartments", &controllers.BackendOrganizationController{}, "get:GetDepartments")
|
||||||
|
beego.Router(prefix+"/searchOrganizations", &controllers.BackendOrganizationController{}, "get:SearchOrganizations")
|
||||||
|
beego.Router(prefix+"/getOrganizationHierarchy/:org_id", &controllers.BackendOrganizationController{}, "get:GetOrganizationHierarchy")
|
||||||
|
beego.Router(prefix+"/getOrganizationStats", &controllers.BackendOrganizationController{}, "get:GetOrganizationStats")
|
||||||
|
beego.Router(prefix+"/moveOrganization", &controllers.BackendOrganizationController{}, "post:MoveOrganization")
|
||||||
|
beego.Router(prefix+"/batchOrganizeOrganizations", &controllers.BackendOrganizationController{}, "post:BatchOrganizeOrganizations")
|
||||||
|
beego.Router(prefix+"/checkOrgCodeUnique", &controllers.BackendOrganizationController{}, "get:CheckOrgCodeUnique")
|
||||||
|
|
||||||
|
// 员工
|
||||||
|
beego.Router(prefix+"/getEmployee", &controllers.BackendOrganizationController{}, "get:GetEmployee")
|
||||||
|
beego.Router(prefix+"/getEmployeeDetail/:id", &controllers.BackendOrganizationController{}, "get:GetEmployeeDetail")
|
||||||
|
beego.Router(prefix+"/createEmployee", &controllers.BackendOrganizationController{}, "post:CreateEmployee")
|
||||||
|
beego.Router(prefix+"/editEmployee/:id", &controllers.BackendOrganizationController{}, "post:EditEmployee")
|
||||||
|
beego.Router(prefix+"/deleteEmployee/:id", &controllers.BackendOrganizationController{}, "delete:DeleteEmployee")
|
||||||
|
beego.Router(prefix+"/getOrganizationEmployees/:org_id", &controllers.BackendOrganizationController{}, "get:GetOrganizationEmployees")
|
||||||
|
beego.Router(prefix+"/moveEmployeeToOrg", &controllers.BackendOrganizationController{}, "post:MoveEmployeeToOrg")
|
||||||
|
beego.Router(prefix+"/checkEmployeeAccountUnique", &controllers.BackendOrganizationController{}, "get:CheckEmployeeAccountUnique")
|
||||||
|
|
||||||
|
// 职位
|
||||||
|
beego.Router(prefix+"/getPosition", &controllers.BackendOrganizationController{}, "get:GetPosition")
|
||||||
|
beego.Router(prefix+"/getPositionDetail/:id", &controllers.BackendOrganizationController{}, "get:GetPositionDetail")
|
||||||
|
beego.Router(prefix+"/createPosition", &controllers.BackendOrganizationController{}, "post:CreatePosition")
|
||||||
|
beego.Router(prefix+"/editPosition/:id", &controllers.BackendOrganizationController{}, "post:EditPosition")
|
||||||
|
beego.Router(prefix+"/deletePosition/:id", &controllers.BackendOrganizationController{}, "delete:DeletePosition")
|
||||||
|
beego.Router(prefix+"/checkPositionCodeUnique", &controllers.BackendOrganizationController{}, "get:CheckPositionCodeUnique")
|
||||||
|
|
||||||
|
// 设置与导入导出
|
||||||
|
beego.Router(prefix+"/getOrgSettings", &controllers.BackendOrganizationController{}, "get:GetOrgSettings")
|
||||||
|
beego.Router(prefix+"/saveOrgSettings", &controllers.BackendOrganizationController{}, "post:SaveOrgSettings")
|
||||||
|
beego.Router(prefix+"/exportOrganization", &controllers.BackendOrganizationController{}, "get:ExportOrganization")
|
||||||
|
beego.Router(prefix+"/importOrganization", &controllers.BackendOrganizationController{}, "post:ImportOrganization")
|
||||||
|
beego.Router(prefix+"/organizationImportTemplate", &controllers.BackendOrganizationController{}, "get:GetImportTemplate")
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package backend
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
// TestRegisterOrganizationRoutes 确认组织架构路由能为 erp 与 oa 两个前缀完成注册且不冲突。
|
||||||
|
// beego 在注册重复路由或非法方法名时会 panic,这里通过实际注册来兜住这类错误。
|
||||||
|
func TestRegisterOrganizationRoutes(t *testing.T) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
t.Fatalf("注册组织架构路由失败: %v", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
registerOrganizationRoutes("erp")
|
||||||
|
registerOrganizationRoutes("oa")
|
||||||
|
}
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
{yz:arclist row="4" titlelen="24"}
|
{yz:arclist row="4" titlelen="24"}
|
||||||
<li class="yz-news-card">
|
<li class="yz-news-card">
|
||||||
<a href="[field:arcurl/]">
|
<a href="[field:arcurl/]">
|
||||||
<img class="yz-news-thumb" src="[field:image/]" alt="[field:titlefull/]" onerror="this.style.display='none'">
|
<img class="yz-news-thumb" src="[field:image/]" alt="[field:titlefull/]">
|
||||||
<h3 title="[field:titlefull/]">[field:title/]</h3>
|
<h3 title="[field:titlefull/]">[field:title/]</h3>
|
||||||
<p class="yz-news-desc">[field:desc/]</p>
|
<p class="yz-news-desc">[field:desc/]</p>
|
||||||
<span class="yz-news-date">[field:pubdate/]</span>
|
<span class="yz-news-date">[field:pubdate/]</span>
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
<div class="col-lg-12">
|
<div class="col-lg-12">
|
||||||
<nav class="navbar navbar-expand-lg">
|
<nav class="navbar navbar-expand-lg">
|
||||||
<a class="navbar-brand" href="/">
|
<a class="navbar-brand" href="/">
|
||||||
<img src="{yz:logo}" alt="{yz:sitename}" />
|
<img src="{yz:logo}" alt="{yz:sitename}" style="width: 200px;" />
|
||||||
</a>
|
</a>
|
||||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNine"
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNine"
|
||||||
aria-controls="navbarNine" aria-expanded="false" aria-label="切换导航">
|
aria-controls="navbarNine" aria-expanded="false" aria-label="切换导航">
|
||||||
@@ -121,7 +121,7 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-lg-4 col-md-6 col-12">
|
<div class="col-lg-4 col-md-6 col-12">
|
||||||
<div class="footer-widget f-about">
|
<div class="footer-widget f-about">
|
||||||
<div class="logo"><a href="/"><img src="{yz:logo}" alt="{yz:sitename}" class="img-fluid" /></a></div>
|
<div class="logo"><a href="/"><img src="{yz:logo}" alt="{yz:sitename}" class="img-fluid" style="width: 250px;" /></a></div>
|
||||||
<p>{yz:description}</p>
|
<p>{yz:description}</p>
|
||||||
<p class="copyright-text">
|
<p class="copyright-text">
|
||||||
<span>备案号:{yz:icp}<br>{yz:copyright} <a href="www.yunzer.cn" rel="nofollow"> 云泽网 </a></span>
|
<span>备案号:{yz:icp}<br>{yz:copyright} <a href="www.yunzer.cn" rel="nofollow"> 云泽网 </a></span>
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
<div class="col-lg-12">
|
<div class="col-lg-12">
|
||||||
<nav class="navbar navbar-expand-lg">
|
<nav class="navbar navbar-expand-lg">
|
||||||
<a class="navbar-brand" href="/">
|
<a class="navbar-brand" href="/">
|
||||||
<img src="{yz:logo}" alt="{yz:sitename}" />
|
<img src="{yz:logo}" alt="{yz:sitename}" style="width: 200px;" />
|
||||||
</a>
|
</a>
|
||||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNine"
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNine"
|
||||||
aria-controls="navbarNine" aria-expanded="false" aria-label="切换导航">
|
aria-controls="navbarNine" aria-expanded="false" aria-label="切换导航">
|
||||||
@@ -185,7 +185,7 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-lg-4 col-md-6 col-12">
|
<div class="col-lg-4 col-md-6 col-12">
|
||||||
<div class="footer-widget f-about">
|
<div class="footer-widget f-about">
|
||||||
<div class="logo"><a href="/"><img src="{yz:logo}" alt="{yz:sitename}" class="img-fluid" /></a></div>
|
<div class="logo"><a href="/"><img src="{yz:logo}" alt="{yz:sitename}" class="img-fluid" style="width: 250px;" /></a></div>
|
||||||
<p>{yz:description}</p>
|
<p>{yz:description}</p>
|
||||||
<p class="copyright-text">
|
<p class="copyright-text">
|
||||||
<span>备案号:{yz:icp}<br>{yz:copyright} <a href="www.yunzer.cn" rel="nofollow"> 云泽网 </a></span>
|
<span>备案号:{yz:icp}<br>{yz:copyright} <a href="www.yunzer.cn" rel="nofollow"> 云泽网 </a></span>
|
||||||
|
|||||||
@@ -39,7 +39,7 @@
|
|||||||
<div class="col-lg-12">
|
<div class="col-lg-12">
|
||||||
<nav class="navbar navbar-expand-lg">
|
<nav class="navbar navbar-expand-lg">
|
||||||
<a class="navbar-brand" href="/">
|
<a class="navbar-brand" href="/">
|
||||||
<img src="{yz:logo}" alt="{yz:sitename}" />
|
<img src="{yz:logo}" alt="{yz:sitename}" style="width: 200px;" />
|
||||||
</a>
|
</a>
|
||||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNine"
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNine"
|
||||||
aria-controls="navbarNine" aria-expanded="false" aria-label="切换导航">
|
aria-controls="navbarNine" aria-expanded="false" aria-label="切换导航">
|
||||||
@@ -168,7 +168,7 @@
|
|||||||
<div class="col-lg-3 col-md-6 col-12">
|
<div class="col-lg-3 col-md-6 col-12">
|
||||||
<div class="single-news news-center-item">
|
<div class="single-news news-center-item">
|
||||||
<div class="image">
|
<div class="image">
|
||||||
<a href="[field:arcurl/]"><img class="thumb" src="[field:image/]" alt="[field:titlefull/]" onerror="this.style.display='none'" /></a>
|
<a href="[field:arcurl/]"><img class="thumb" src="[field:image/]" alt="[field:titlefull/]" /></a>
|
||||||
<div class="meta-details">
|
<div class="meta-details">
|
||||||
<span>[field:pubdate/]</span>
|
<span>[field:pubdate/]</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -500,11 +500,7 @@
|
|||||||
<div class="col-lg-4 col-md-6 col-12">
|
<div class="col-lg-4 col-md-6 col-12">
|
||||||
<!-- 单个小组件 -->
|
<!-- 单个小组件 -->
|
||||||
<div class="footer-widget f-about">
|
<div class="footer-widget f-about">
|
||||||
<div class="logo">
|
<div class="logo"><a href="/"><img src="{yz:logo}" alt="{yz:sitename}" class="img-fluid" style="width: 250px;" /></a></div>
|
||||||
<a href="/">
|
|
||||||
<img src="{yz:logo}" alt="云泽网" class="img-fluid" />
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<p>
|
<p>
|
||||||
{yz:description}
|
{yz:description}
|
||||||
</p>
|
</p>
|
||||||
@@ -606,15 +602,6 @@
|
|||||||
overlayLeft.classList.add("open");
|
overlayLeft.classList.add("open");
|
||||||
});
|
});
|
||||||
|
|
||||||
//========= 初始化 glightbox 视频播放
|
|
||||||
GLightbox({
|
|
||||||
'href': 'https://www.youtube.com/watch?v=r44RKWyfcFw&fbclid=IwAR21beSJORalzmzokxDRcGfkZA1AtRTE__l5N4r09HcGS5Y6vOluyouM9EM',
|
|
||||||
'type': 'video',
|
|
||||||
'source': 'youtube', // vimeo, youtube 或本地视频
|
|
||||||
'width': 900,
|
|
||||||
'autoplayVideos': true,
|
|
||||||
});
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
<div class="col-lg-12">
|
<div class="col-lg-12">
|
||||||
<nav class="navbar navbar-expand-lg">
|
<nav class="navbar navbar-expand-lg">
|
||||||
<a class="navbar-brand" href="/">
|
<a class="navbar-brand" href="/">
|
||||||
<img src="{yz:logo}" alt="{yz:sitename}" />
|
<img src="{yz:logo}" alt="{yz:sitename}" style="width: 200px;" />
|
||||||
</a>
|
</a>
|
||||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNine"
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNine"
|
||||||
aria-controls="navbarNine" aria-expanded="false" aria-label="切换导航">
|
aria-controls="navbarNine" aria-expanded="false" aria-label="切换导航">
|
||||||
@@ -122,11 +122,11 @@
|
|||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
{yz:arclist row="10" titlelen="40"}
|
{yz:arclist row="8" titlelen="40"}
|
||||||
<div class="col-lg-3 col-md-6 col-12">
|
<div class="col-lg-3 col-md-6 col-12">
|
||||||
<div class="single-news news-center-item">
|
<div class="single-news news-center-item">
|
||||||
<div class="image">
|
<div class="image">
|
||||||
<a href="[field:arcurl/]"><img class="thumb" src="[field:image/]" alt="[field:titlefull/]" onerror="this.style.display='none'" /></a>
|
<a href="[field:arcurl/]"><img class="thumb" src="[field:image/]" alt="[field:titlefull/]" /></a>
|
||||||
<div class="meta-details">
|
<div class="meta-details">
|
||||||
<span>[field:pubdate/]</span>
|
<span>[field:pubdate/]</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -159,11 +159,7 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-lg-4 col-md-6 col-12">
|
<div class="col-lg-4 col-md-6 col-12">
|
||||||
<div class="footer-widget f-about">
|
<div class="footer-widget f-about">
|
||||||
<div class="logo">
|
<div class="logo"><a href="/"><img src="{yz:logo}" alt="{yz:sitename}" class="img-fluid" style="width: 250px;" /></a></div>
|
||||||
<a href="/">
|
|
||||||
<img src="{yz:logo}" alt="{yz:sitename}" class="img-fluid" />
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<p>{yz:description}</p>
|
<p>{yz:description}</p>
|
||||||
<p class="copyright-text">
|
<p class="copyright-text">
|
||||||
<span>备案号:{yz:icp}<br>{yz:copyright} <a href="www.yunzer.cn" rel="nofollow"> 云泽网 </a></span>
|
<span>备案号:{yz:icp}<br>{yz:copyright} <a href="www.yunzer.cn" rel="nofollow"> 云泽网 </a></span>
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
<div class="col-lg-12">
|
<div class="col-lg-12">
|
||||||
<nav class="navbar navbar-expand-lg">
|
<nav class="navbar navbar-expand-lg">
|
||||||
<a class="navbar-brand" href="/">
|
<a class="navbar-brand" href="/">
|
||||||
<img src="{yz:logo}" alt="{yz:sitename}" />
|
<img src="{yz:logo}" alt="{yz:sitename}" style="width: 200px;" />
|
||||||
</a>
|
</a>
|
||||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNine"
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNine"
|
||||||
aria-controls="navbarNine" aria-expanded="false" aria-label="切换导航">
|
aria-controls="navbarNine" aria-expanded="false" aria-label="切换导航">
|
||||||
@@ -114,8 +114,8 @@
|
|||||||
<div class="col-lg-10 col-xl-9">
|
<div class="col-lg-10 col-xl-9">
|
||||||
{yz:arcview}
|
{yz:arcview}
|
||||||
<article class="article-detail-card">
|
<article class="article-detail-card">
|
||||||
<div class="article-detail-thumb" style="display:none;">
|
<div class="article-detail-thumb">
|
||||||
<img src="[field:image/]" alt="[field:title/]" class="article-cover" onerror="this.closest('.article-detail-thumb').style.display='none'" onload="this.closest('.article-detail-thumb').style.display='block'" />
|
<img src="[field:image/]" alt="[field:title/]" class="article-cover" onerror="this.closest('.article-detail-thumb').style.display='none'" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h1 class="article-detail-title">[field:title/]</h1>
|
<h1 class="article-detail-title">[field:title/]</h1>
|
||||||
@@ -151,11 +151,7 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-lg-4 col-md-6 col-12">
|
<div class="col-lg-4 col-md-6 col-12">
|
||||||
<div class="footer-widget f-about">
|
<div class="footer-widget f-about">
|
||||||
<div class="logo">
|
<div class="logo"><a href="/"><img src="{yz:logo}" alt="{yz:sitename}" class="img-fluid" style="width: 250px;" /></a></div>
|
||||||
<a href="/">
|
|
||||||
<img src="{yz:logo}" alt="{yz:sitename}" class="img-fluid" />
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<p>{yz:description}</p>
|
<p>{yz:description}</p>
|
||||||
<p class="copyright-text">
|
<p class="copyright-text">
|
||||||
<span>备案号:{yz:icp}<br>{yz:copyright} <a href="www.yunzer.cn" rel="nofollow"> 云泽网 </a></span>
|
<span>备案号:{yz:icp}<br>{yz:copyright} <a href="www.yunzer.cn" rel="nofollow"> 云泽网 </a></span>
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
<div class="col-lg-12">
|
<div class="col-lg-12">
|
||||||
<nav class="navbar navbar-expand-lg">
|
<nav class="navbar navbar-expand-lg">
|
||||||
<a class="navbar-brand" href="/">
|
<a class="navbar-brand" href="/">
|
||||||
<img src="{yz:logo}" alt="{yz:sitename}" />
|
<img src="{yz:logo}" alt="{yz:sitename}" style="width: 200px;" />
|
||||||
</a>
|
</a>
|
||||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNine"
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNine"
|
||||||
aria-controls="navbarNine" aria-expanded="false" aria-label="切换导航">
|
aria-controls="navbarNine" aria-expanded="false" aria-label="切换导航">
|
||||||
@@ -114,7 +114,7 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-lg-4 col-md-6 col-12">
|
<div class="col-lg-4 col-md-6 col-12">
|
||||||
<div class="footer-widget f-about">
|
<div class="footer-widget f-about">
|
||||||
<div class="logo"><a href="/"><img src="{yz:logo}" alt="{yz:sitename}" class="img-fluid" /></a></div>
|
<div class="logo"><a href="/"><img src="{yz:logo}" alt="{yz:sitename}" class="img-fluid" style="width: 250px;" /></a></div>
|
||||||
<p>{yz:description}</p>
|
<p>{yz:description}</p>
|
||||||
<p class="copyright-text">
|
<p class="copyright-text">
|
||||||
<span>备案号:{yz:icp}<br>{yz:copyright} <a href="www.yunzer.cn" rel="nofollow"> 云泽网 </a></span>
|
<span>备案号:{yz:icp}<br>{yz:copyright} <a href="www.yunzer.cn" rel="nofollow"> 云泽网 </a></span>
|
||||||
|
|||||||
@@ -0,0 +1,193 @@
|
|||||||
|
-- 组织架构模块:表名去掉 erp 字样,改为租户端通用命名
|
||||||
|
-- yz_backend_erp_organization -> yz_backend_organization
|
||||||
|
-- yz_backend_erp_employee -> yz_backend_employee
|
||||||
|
-- yz_backend_erp_position -> yz_backend_position
|
||||||
|
--
|
||||||
|
-- 组织架构由进销存(ERP)与办公自动化(OA)共用同一份数据,通过 tid 实现租户间隔离。
|
||||||
|
-- 执行前请确认当前数据库字符集为 utf8mb4,并先备份这三张表。
|
||||||
|
--
|
||||||
|
-- 说明:脚本可重复执行。如果旧表不存在(全新环境),RENAME 会被跳过,
|
||||||
|
-- 后面的 CREATE TABLE IF NOT EXISTS 会建出目标表结构。
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- 1. 重命名已有表(仅当旧表存在且新表不存在时执行)
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
SET @schema := DATABASE();
|
||||||
|
|
||||||
|
SET @sql := IF(
|
||||||
|
(SELECT COUNT(*) FROM information_schema.TABLES
|
||||||
|
WHERE TABLE_SCHEMA = @schema AND TABLE_NAME = 'yz_backend_erp_organization') = 1
|
||||||
|
AND (SELECT COUNT(*) FROM information_schema.TABLES
|
||||||
|
WHERE TABLE_SCHEMA = @schema AND TABLE_NAME = 'yz_backend_organization') = 0,
|
||||||
|
'RENAME TABLE `yz_backend_erp_organization` TO `yz_backend_organization`',
|
||||||
|
'SELECT "skip: yz_backend_organization"'
|
||||||
|
);
|
||||||
|
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
SET @sql := IF(
|
||||||
|
(SELECT COUNT(*) FROM information_schema.TABLES
|
||||||
|
WHERE TABLE_SCHEMA = @schema AND TABLE_NAME = 'yz_backend_erp_employee') = 1
|
||||||
|
AND (SELECT COUNT(*) FROM information_schema.TABLES
|
||||||
|
WHERE TABLE_SCHEMA = @schema AND TABLE_NAME = 'yz_backend_employee') = 0,
|
||||||
|
'RENAME TABLE `yz_backend_erp_employee` TO `yz_backend_employee`',
|
||||||
|
'SELECT "skip: yz_backend_employee"'
|
||||||
|
);
|
||||||
|
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
SET @sql := IF(
|
||||||
|
(SELECT COUNT(*) FROM information_schema.TABLES
|
||||||
|
WHERE TABLE_SCHEMA = @schema AND TABLE_NAME = 'yz_backend_erp_position') = 1
|
||||||
|
AND (SELECT COUNT(*) FROM information_schema.TABLES
|
||||||
|
WHERE TABLE_SCHEMA = @schema AND TABLE_NAME = 'yz_backend_position') = 0,
|
||||||
|
'RENAME TABLE `yz_backend_erp_position` TO `yz_backend_position`',
|
||||||
|
'SELECT "skip: yz_backend_position"'
|
||||||
|
);
|
||||||
|
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- 2. 全新环境建表(旧表不存在时生效)
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `yz_backend_organization` (
|
||||||
|
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||||
|
`tid` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '租户ID(数据隔离)',
|
||||||
|
`org_name` varchar(128) NOT NULL DEFAULT '' COMMENT '组织名称',
|
||||||
|
`org_code` varchar(64) NOT NULL DEFAULT '' COMMENT '组织编码(租户内唯一)',
|
||||||
|
`parent_id` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '上级组织ID,0为顶级',
|
||||||
|
`sort` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '排序,越小越靠前',
|
||||||
|
`leader_id` bigint(20) unsigned DEFAULT NULL COMMENT '负责人(员工ID)',
|
||||||
|
`is_company` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否企业单位:1-是 0-部门',
|
||||||
|
`status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '状态:1-启用 0-禁用',
|
||||||
|
`remark` varchar(512) DEFAULT NULL COMMENT '备注',
|
||||||
|
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
|
`delete_time` datetime DEFAULT NULL COMMENT '删除时间(软删除)',
|
||||||
|
PRIMARY KEY (`id`) USING BTREE,
|
||||||
|
KEY `idx_tid` (`tid`) USING BTREE,
|
||||||
|
KEY `idx_tid_code` (`tid`,`org_code`) USING BTREE,
|
||||||
|
KEY `idx_parent_id` (`parent_id`) USING BTREE,
|
||||||
|
KEY `idx_is_company` (`is_company`) USING BTREE,
|
||||||
|
KEY `idx_status` (`status`) USING BTREE,
|
||||||
|
KEY `idx_delete_time` (`delete_time`) USING BTREE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='组织架构表(ERP与OA共用)';
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `yz_backend_employee` (
|
||||||
|
`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||||
|
`tid` int(11) DEFAULT NULL COMMENT '租户ID(数据隔离)',
|
||||||
|
`account` varchar(50) NOT NULL DEFAULT '' COMMENT '登录账号(租户内唯一)',
|
||||||
|
`password` varchar(64) NOT NULL DEFAULT '' COMMENT '密码(sha256 hex)',
|
||||||
|
`name` varchar(30) NOT NULL DEFAULT '' COMMENT '姓名',
|
||||||
|
`gender` tinyint(1) NOT NULL DEFAULT '0' COMMENT '性别:0-未知 1-男 2-女',
|
||||||
|
`birthday` date DEFAULT NULL COMMENT '生日',
|
||||||
|
`affiliate_unit` varchar(100) DEFAULT NULL COMMENT '隶属单位(组织ID)',
|
||||||
|
`department` varchar(50) DEFAULT NULL COMMENT '部门(组织ID)',
|
||||||
|
`position` varchar(50) DEFAULT NULL COMMENT '职位',
|
||||||
|
`education` varchar(20) DEFAULT NULL COMMENT '学历',
|
||||||
|
`nation` varchar(20) DEFAULT NULL COMMENT '民族',
|
||||||
|
`phone` varchar(20) DEFAULT NULL COMMENT '手机号',
|
||||||
|
`wechat` varchar(50) DEFAULT NULL COMMENT '微信',
|
||||||
|
`email` varchar(100) DEFAULT NULL COMMENT '邮箱',
|
||||||
|
`home_address` varchar(255) DEFAULT NULL COMMENT '家庭住址',
|
||||||
|
`account_status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '账号状态:1-启用 0-禁用 2-离职',
|
||||||
|
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
|
`delete_time` datetime DEFAULT NULL COMMENT '删除时间(软删除)',
|
||||||
|
PRIMARY KEY (`id`) USING BTREE,
|
||||||
|
KEY `idx_tid` (`tid`) USING BTREE,
|
||||||
|
KEY `idx_tid_account` (`tid`,`account`) USING BTREE,
|
||||||
|
KEY `idx_department` (`department`) USING BTREE,
|
||||||
|
KEY `idx_affiliate_unit` (`affiliate_unit`) USING BTREE,
|
||||||
|
KEY `idx_account_status` (`account_status`) USING BTREE,
|
||||||
|
KEY `idx_delete_time` (`delete_time`) USING BTREE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='员工信息表(ERP与OA共用)';
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `yz_backend_position` (
|
||||||
|
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||||
|
`tid` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '租户ID(数据隔离)',
|
||||||
|
`department_id` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '所属部门(组织ID)',
|
||||||
|
`position_code` varchar(50) NOT NULL DEFAULT '' COMMENT '职位编码(租户内唯一)',
|
||||||
|
`position_name` varchar(100) NOT NULL DEFAULT '' COMMENT '职位名称',
|
||||||
|
`position_type` tinyint(1) NOT NULL DEFAULT '0' COMMENT '职位类型:0-普通 1-主管 2-负责人',
|
||||||
|
`status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '状态:1-启用 0-禁用',
|
||||||
|
`sort` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '排序,越小越靠前',
|
||||||
|
`remark` varchar(512) DEFAULT NULL COMMENT '备注',
|
||||||
|
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
|
`delete_time` datetime DEFAULT NULL COMMENT '删除时间(软删除)',
|
||||||
|
PRIMARY KEY (`id`) USING BTREE,
|
||||||
|
KEY `idx_tid` (`tid`) USING BTREE,
|
||||||
|
KEY `idx_tid_code` (`tid`,`position_code`) USING BTREE,
|
||||||
|
KEY `idx_department_id` (`department_id`) USING BTREE,
|
||||||
|
KEY `idx_status` (`status`) USING BTREE,
|
||||||
|
KEY `idx_delete_time` (`delete_time`) USING BTREE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='职位表(ERP与OA共用)';
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- 3. 补齐旧表缺失的字段与索引(重命名后的表可能来自更早的版本)
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
-- yz_backend_position 新增 remark / delete_time
|
||||||
|
SET @sql := IF(
|
||||||
|
(SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = @schema AND TABLE_NAME = 'yz_backend_position'
|
||||||
|
AND COLUMN_NAME = 'remark') = 0,
|
||||||
|
'ALTER TABLE `yz_backend_position` ADD COLUMN `remark` varchar(512) DEFAULT NULL COMMENT ''备注'' AFTER `sort`',
|
||||||
|
'SELECT "skip: yz_backend_position.remark"'
|
||||||
|
);
|
||||||
|
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
SET @sql := IF(
|
||||||
|
(SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = @schema AND TABLE_NAME = 'yz_backend_position'
|
||||||
|
AND COLUMN_NAME = 'delete_time') = 0,
|
||||||
|
'ALTER TABLE `yz_backend_position` ADD COLUMN `delete_time` datetime DEFAULT NULL COMMENT ''删除时间(软删除)''',
|
||||||
|
'SELECT "skip: yz_backend_position.delete_time"'
|
||||||
|
);
|
||||||
|
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
SET @sql := IF(
|
||||||
|
(SELECT COUNT(*) FROM information_schema.STATISTICS
|
||||||
|
WHERE TABLE_SCHEMA = @schema AND TABLE_NAME = 'yz_backend_position'
|
||||||
|
AND INDEX_NAME = 'idx_tid_code') = 0,
|
||||||
|
'ALTER TABLE `yz_backend_position` ADD INDEX `idx_tid_code` (`tid`,`position_code`) USING BTREE',
|
||||||
|
'SELECT "skip: yz_backend_position.idx_tid_code"'
|
||||||
|
);
|
||||||
|
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
SET @sql := IF(
|
||||||
|
(SELECT COUNT(*) FROM information_schema.STATISTICS
|
||||||
|
WHERE TABLE_SCHEMA = @schema AND TABLE_NAME = 'yz_backend_organization'
|
||||||
|
AND INDEX_NAME = 'idx_tid_code') = 0,
|
||||||
|
'ALTER TABLE `yz_backend_organization` ADD INDEX `idx_tid_code` (`tid`,`org_code`) USING BTREE',
|
||||||
|
'SELECT "skip: yz_backend_organization.idx_tid_code"'
|
||||||
|
);
|
||||||
|
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
SET @sql := IF(
|
||||||
|
(SELECT COUNT(*) FROM information_schema.STATISTICS
|
||||||
|
WHERE TABLE_SCHEMA = @schema AND TABLE_NAME = 'yz_backend_employee'
|
||||||
|
AND INDEX_NAME = 'idx_tid_account') = 0,
|
||||||
|
'ALTER TABLE `yz_backend_employee` ADD INDEX `idx_tid_account` (`tid`,`account`) USING BTREE',
|
||||||
|
'SELECT "skip: yz_backend_employee.idx_tid_account"'
|
||||||
|
);
|
||||||
|
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- 4. 组织架构设置存放在 yz_backend_normal_setting,按 backend_org_settings_{tid} 区分租户
|
||||||
|
-- 这里只保证该表存在,具体记录由后端首次保存设置时写入。
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `yz_backend_normal_setting` (
|
||||||
|
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||||
|
`name` varchar(128) NOT NULL DEFAULT '' COMMENT '配置名称',
|
||||||
|
`value` text COMMENT '配置内容(JSON)',
|
||||||
|
`code` varchar(64) NOT NULL DEFAULT '' COMMENT '配置标识',
|
||||||
|
`remark` varchar(255) NOT NULL DEFAULT '' COMMENT '备注',
|
||||||
|
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
|
||||||
|
`delete_time` datetime DEFAULT NULL COMMENT '删除时间(软删除)',
|
||||||
|
PRIMARY KEY (`id`) USING BTREE,
|
||||||
|
KEY `idx_code` (`code`) USING BTREE,
|
||||||
|
KEY `idx_delete_time` (`delete_time`) USING BTREE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='管理端通用配置表';
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
-- 组织架构菜单(租户端):进销存与办公自动化各自一套入口,指向共用页面组件。
|
||||||
|
--
|
||||||
|
-- 前置:yz_system_menu 中已存在 path 为 /apps/erp 与 /apps/oa 的模块目录菜单。
|
||||||
|
-- 若不存在,脚本会自动创建这两个目录菜单。
|
||||||
|
-- views 字段:[1] 平台端 / [2] 租户端 / [1,2] 双端。组织架构属于租户端,固定为 [2]。
|
||||||
|
-- type 字段:1-目录 2-页面。
|
||||||
|
--
|
||||||
|
-- 脚本可重复执行:按 path 判重,已存在的菜单会更新组件路径与标题。
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- 1. 确保模块目录存在
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
INSERT INTO `yz_system_menu` (`pid`, `title`, `path`, `component_path`, `icon`, `sort`, `status`, `is_visible`, `views`, `type`, `remark`)
|
||||||
|
SELECT 0, '进销存', '/apps/erp', '', 'Goods', 30, 1, 1, '[2]', 1, '进销存模块'
|
||||||
|
FROM DUAL
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM (SELECT * FROM `yz_system_menu`) AS t WHERE t.`path` = '/apps/erp'
|
||||||
|
);
|
||||||
|
|
||||||
|
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 @erp_pid := (SELECT `id` FROM `yz_system_menu` WHERE `path` = '/apps/erp' ORDER BY `id` LIMIT 1);
|
||||||
|
SET @oa_pid := (SELECT `id` FROM `yz_system_menu` WHERE `path` = '/apps/oa' ORDER BY `id` LIMIT 1);
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- 2. 进销存:组织架构 / 员工管理 / 职位管理
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
INSERT INTO `yz_system_menu` (`pid`, `title`, `path`, `component_path`, `icon`, `sort`, `status`, `is_visible`, `views`, `type`, `remark`)
|
||||||
|
SELECT @erp_pid, '组织架构', '/apps/erp/organization', '/apps/erp/organization/index.vue', 'OfficeBuilding', 1, 1, 1, '[2]', 2, '与OA共用组织数据'
|
||||||
|
FROM DUAL
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM (SELECT * FROM `yz_system_menu`) AS t WHERE t.`path` = '/apps/erp/organization'
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO `yz_system_menu` (`pid`, `title`, `path`, `component_path`, `icon`, `sort`, `status`, `is_visible`, `views`, `type`, `remark`)
|
||||||
|
SELECT @erp_pid, '员工管理', '/apps/erp/employee', '/apps/erp/employee/index.vue', 'User', 2, 1, 1, '[2]', 2, '与OA共用员工数据'
|
||||||
|
FROM DUAL
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM (SELECT * FROM `yz_system_menu`) AS t WHERE t.`path` = '/apps/erp/employee'
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO `yz_system_menu` (`pid`, `title`, `path`, `component_path`, `icon`, `sort`, `status`, `is_visible`, `views`, `type`, `remark`)
|
||||||
|
SELECT @erp_pid, '职位管理', '/apps/erp/position', '/apps/erp/position/index.vue', 'Postcard', 3, 1, 1, '[2]', 2, '与OA共用职位数据'
|
||||||
|
FROM DUAL
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM (SELECT * FROM `yz_system_menu`) AS t WHERE t.`path` = '/apps/erp/position'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- 3. 办公自动化:组织架构 / 人员管理 / 职位管理
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
INSERT INTO `yz_system_menu` (`pid`, `title`, `path`, `component_path`, `icon`, `sort`, `status`, `is_visible`, `views`, `type`, `remark`)
|
||||||
|
SELECT @oa_pid, '组织架构', '/apps/oa/organization', '/apps/oa/organization/index.vue', 'OfficeBuilding', 1, 1, 1, '[2]', 2, '与进销存共用组织数据'
|
||||||
|
FROM DUAL
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM (SELECT * FROM `yz_system_menu`) AS t WHERE t.`path` = '/apps/oa/organization'
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO `yz_system_menu` (`pid`, `title`, `path`, `component_path`, `icon`, `sort`, `status`, `is_visible`, `views`, `type`, `remark`)
|
||||||
|
SELECT @oa_pid, '人员管理', '/apps/oa/employee', '/apps/oa/employee/index.vue', 'User', 2, 1, 1, '[2]', 2, '与进销存共用员工数据'
|
||||||
|
FROM DUAL
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM (SELECT * FROM `yz_system_menu`) AS t WHERE t.`path` = '/apps/oa/employee'
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO `yz_system_menu` (`pid`, `title`, `path`, `component_path`, `icon`, `sort`, `status`, `is_visible`, `views`, `type`, `remark`)
|
||||||
|
SELECT @oa_pid, '职位管理', '/apps/oa/position', '/apps/oa/position/index.vue', 'Postcard', 3, 1, 1, '[2]', 2, '与进销存共用职位数据'
|
||||||
|
FROM DUAL
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM (SELECT * FROM `yz_system_menu`) AS t WHERE t.`path` = '/apps/oa/position'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- 4. 修正已有菜单的组件路径(历史数据可能指向旧组件)
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
UPDATE `yz_system_menu` SET `component_path` = '/apps/erp/organization/index.vue', `type` = 2, `views` = '[2]'
|
||||||
|
WHERE `path` = '/apps/erp/organization';
|
||||||
|
UPDATE `yz_system_menu` SET `component_path` = '/apps/erp/employee/index.vue', `type` = 2, `views` = '[2]'
|
||||||
|
WHERE `path` = '/apps/erp/employee';
|
||||||
|
UPDATE `yz_system_menu` SET `component_path` = '/apps/erp/position/index.vue', `type` = 2, `views` = '[2]'
|
||||||
|
WHERE `path` = '/apps/erp/position';
|
||||||
|
UPDATE `yz_system_menu` SET `component_path` = '/apps/oa/organization/index.vue', `type` = 2, `views` = '[2]'
|
||||||
|
WHERE `path` = '/apps/oa/organization';
|
||||||
|
UPDATE `yz_system_menu` SET `component_path` = '/apps/oa/employee/index.vue', `type` = 2, `views` = '[2]'
|
||||||
|
WHERE `path` = '/apps/oa/employee';
|
||||||
|
UPDATE `yz_system_menu` SET `component_path` = '/apps/oa/position/index.vue', `type` = 2, `views` = '[2]'
|
||||||
|
WHERE `path` = '/apps/oa/position';
|
||||||
Reference in New Issue
Block a user