509 lines
22 KiB
JavaScript
509 lines
22 KiB
JavaScript
/**
|
||
* CompanyCommon.js — Company/* 页面共享组件
|
||
* 提供工具函数、TablePage 基类和共享样式,消除重复代码。
|
||
* 所有 Company/* 页面通过 <script src="/assets/gerp/CompanyCommon.js"></script> 引入。
|
||
*/
|
||
|
||
// ==================== 共享样式(仅注入一次) ====================
|
||
(function () {
|
||
if (document.getElementById('CompanyCommonStyles')) return;
|
||
const style = document.createElement('style');
|
||
style.id = 'CompanyCommonStyles';
|
||
style.textContent = `
|
||
.GERPPage { padding: 24px; margin: 0 auto; height: 100%; display: flex; flex-direction: column; box-sizing: border-box; }
|
||
.cp-page { padding: 20px; flex: 1; min-height: 0; display: flex; flex-direction: column; box-sizing: border-box; }
|
||
.cp-header { display: flex; align-items: center; margin-bottom: 16px; gap: 12px; flex-shrink: 0; }
|
||
.cp-header h1 { margin: 0; flex: 1; }
|
||
.cp-container { flex: 1; min-height: 0; overflow: hidden; display: flex; flex-direction: column; }
|
||
|
||
/* 详情面板 */
|
||
.cp-detail-section { margin-bottom: 20px; }
|
||
.cp-detail-section h3 { font-size: 14px; font-weight: 600; margin: 0 0 10px 0; padding-bottom: 8px; border-bottom: 1px solid var(--BorderColor); display: flex; align-items: center; gap: 8px; }
|
||
.cp-detail-section h3 i { color: var(--PrimaryColor); }
|
||
.cp-info-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 6px 16px; }
|
||
.cp-info-item { font-size: 13px; }
|
||
.cp-info-item .label { color: var(--TextSecondary); font-size: 12px; }
|
||
.cp-info-item .value { color: var(--TextPrimary); font-weight: 500; word-break: break-all; }
|
||
.cp-sticky-info { position: sticky; top: 0; z-index: 10; background: var(--BgColor, #fff); padding: 12px 0; border-bottom: 1px solid var(--BorderColor); margin-bottom: 16px; }
|
||
.cp-detail-list { list-style: none; padding: 0; margin: 0; }
|
||
.cp-detail-list li { padding: 8px 12px; border: 1px solid var(--BorderColor); border-radius: 6px; margin-bottom: 6px; font-size: 13px; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
||
.cp-detail-list li .item-name { font-weight: 500; flex: 1; }
|
||
.cp-detail-list li .item-desc { color: var(--TextSecondary); font-size: 12px; }
|
||
.cp-empty { text-align: center; padding: 20px; color: var(--TextSecondary); font-size: 13px; }
|
||
.cp-error { text-align: center; padding: 40px; color: var(--DangerColor); font-size: 14px; }
|
||
.cp-loading { text-align: center; padding: 40px; color: var(--TextSecondary); font-size: 13px; }
|
||
|
||
/* 返回按钮 */
|
||
.cp-back-btn { display: inline-flex; align-items: center; gap: 6px; padding: 6px 12px; border: 1px solid var(--BorderColor); border-radius: 6px; background: var(--BgColor, #fff); color: var(--TextPrimary); font-size: 13px; cursor: pointer; transition: all .2s; text-decoration: none; }
|
||
.cp-back-btn:hover { border-color: var(--PrimaryColor); color: var(--PrimaryColor); }
|
||
`;
|
||
document.head.appendChild(style);
|
||
})();
|
||
|
||
// ==================== 工具函数 ====================
|
||
const CompanyUtils = {
|
||
// 实体选择器共享配置
|
||
EntityPickerConfigs: {
|
||
User: {
|
||
api: '/API/FormAPI/UserSearch',
|
||
labelField: 'UserName',
|
||
valueField: 'UserUUID',
|
||
secondaryField: 'UserEmail',
|
||
placeholder: '输入用户名搜索...',
|
||
},
|
||
Role: {
|
||
api: '/API/FormAPI/RoleSearch',
|
||
labelField: 'RoleName',
|
||
valueField: 'RoleUUID',
|
||
placeholder: '输入角色名搜索...',
|
||
},
|
||
Company: {
|
||
api: '/API/FormAPI/CompanySearch',
|
||
labelField: 'CompanyName',
|
||
valueField: 'CompanyUUID',
|
||
placeholder: '输入公司名搜索...',
|
||
},
|
||
Department: {
|
||
api: '/API/FormAPI/DepartmentSearch',
|
||
labelField: 'DeptName',
|
||
valueField: 'DeptUUID',
|
||
placeholder: '输入部门名搜索...',
|
||
},
|
||
Workgroup: {
|
||
api: '/API/FormAPI/WorkgroupSearch',
|
||
labelField: 'WorkgroupName',
|
||
valueField: 'WorkgroupUUID',
|
||
placeholder: '输入工作组名搜索...',
|
||
},
|
||
Permission: {
|
||
api: '/API/FormAPI/PermissionSearch',
|
||
labelField: 'PermName',
|
||
valueField: 'PermUUID',
|
||
secondaryField: 'PermCode',
|
||
placeholder: '输入权限名搜索...',
|
||
},
|
||
},
|
||
|
||
/** 生成 entityPicker 列配置 */
|
||
epColumn(field, title, entityName, extra = {}) {
|
||
const ep = this.EntityPickerConfigs[entityName];
|
||
if (!ep) return { field, title, ...extra };
|
||
return {
|
||
field, title, type: 'entityPicker', entityPicker: ep, ...extra,
|
||
};
|
||
},
|
||
|
||
/** HTML 转义 */
|
||
esc(s) {
|
||
if (s == null) return '';
|
||
const d = document.createElement('div');
|
||
d.textContent = String(s);
|
||
return d.innerHTML;
|
||
},
|
||
|
||
/** 状态标签 */
|
||
statusTag(status) {
|
||
if (status === 1 || status === '1') return '<span style="display:inline-block;padding:2px 8px;border-radius:4px;font-size:11px;font-weight:600;background:#dcfce7;color:#166534;">启用</span>';
|
||
return '<span style="display:inline-block;padding:2px 8px;border-radius:4px;font-size:11px;font-weight:600;background:#fee2e2;color:#991b1b;">禁用</span>';
|
||
},
|
||
|
||
/** 时间格式化 */
|
||
formatTime(t) {
|
||
if (!t) return '-';
|
||
try { const d = new Date(t + (t.endsWith('Z') ? '' : 'Z')); return d.toLocaleString(); } catch { return t; }
|
||
},
|
||
|
||
/** 角色类型标签(0=组员/员工, 1=组长/负责人) */
|
||
roleTypeTag(roleType, context) {
|
||
const blue = { bg: '#dbeafe', color: '#1e40af' };
|
||
const purple = { bg: '#ede9fe', color: '#6d28d9' };
|
||
let label = '', style = blue;
|
||
if (context === 'dept') {
|
||
if (roleType === 0) { label = '员工'; style = blue; }
|
||
else if (roleType === 1) { label = '负责人'; style = purple; }
|
||
else return '';
|
||
} else if (context === 'workgroup') {
|
||
if (roleType === 0) { label = '组员'; style = blue; }
|
||
else if (roleType === 1) { label = '组长'; style = purple; }
|
||
else return '';
|
||
} else {
|
||
if (roleType === 0) { label = '组员'; style = blue; }
|
||
else if (roleType === 1) { label = '组长'; style = purple; }
|
||
else return '';
|
||
}
|
||
return `<span style="display:inline-block;padding:2px 8px;border-radius:4px;font-size:11px;font-weight:600;background:${style.bg};color:${style.color};">${label}</span>`;
|
||
},
|
||
|
||
/** 等待 GERPUI 就绪 */
|
||
waitForUI(callback) {
|
||
if (window.GERPUI && typeof window.GERPUI.OpenDrawer === 'function') {
|
||
callback(window.GERPUI);
|
||
return;
|
||
}
|
||
document.addEventListener('GERPUIReady', () => callback(window.GERPUI), { once: true });
|
||
},
|
||
|
||
/** 从 URL 获取 server 参数 */
|
||
getServer() {
|
||
return new URLSearchParams(window.location.search).get('server') || 'gvsds-master';
|
||
},
|
||
|
||
/** 设置页面标题 */
|
||
setPageTitle(title) {
|
||
const server = this.getServer();
|
||
const fullTitle = server === 'gvsds-master' ? title : `${server} ${title}`;
|
||
if (typeof GERP !== 'undefined' && GERP.SetTitle) GERP.SetTitle(fullTitle);
|
||
const el = document.getElementById('page-title');
|
||
if (el) el.textContent = server === 'gvsds-master' ? title : `${server} - ${title}`;
|
||
},
|
||
|
||
/** 跳转到详情页 */
|
||
navigateTo(pagePath, pkName, pkValue, title) {
|
||
const server = this.getServer();
|
||
const params = new URLSearchParams({ [pkName]: pkValue });
|
||
if (server !== 'gvsds-master') params.set('server', server);
|
||
const url = `${pagePath}?${params}`;
|
||
console.log('[CompanyUtils.navigateTo]', { pagePath, pkName, pkValue, title, url });
|
||
if (window.GERP && typeof window.GERP.Navigate === 'function') {
|
||
window.GERP.Navigate(url, title || '');
|
||
} else {
|
||
console.warn('[CompanyUtils.navigateTo] GERP.Navigate 不可用,使用 location.href 跳转');
|
||
window.location.href = url;
|
||
}
|
||
},
|
||
|
||
// ---- UUID 解析缓存 ----
|
||
_uuidCache: {},
|
||
|
||
/**
|
||
* 批量解析 UUID 到名称
|
||
* @param {Object} uuidMap - 格式: { key: {table:'Company', uuids:['xxx','yyy']}, ... }
|
||
* @returns {Object} - 格式: { key: {'xxx':'公司A', 'yyy':'公司B'}, ... }
|
||
*/
|
||
async resolveUUIDs(uuidMap) {
|
||
const server = this.getServer();
|
||
const nameFieldMap = {
|
||
Company: 'CompanyName',
|
||
Department: 'DeptName',
|
||
Workgroup: 'WorkgroupName',
|
||
UserRole: 'RoleName',
|
||
};
|
||
const pkFieldMap = {
|
||
Company: 'CompanyUUID',
|
||
Department: 'DeptUUID',
|
||
Workgroup: 'WorkgroupUUID',
|
||
UserRole: 'UserRoleUUID',
|
||
};
|
||
|
||
const result = {};
|
||
const fetchPromises = [];
|
||
|
||
for (const [key, info] of Object.entries(uuidMap)) {
|
||
result[key] = {};
|
||
const { table, uuids } = info;
|
||
const nameField = nameFieldMap[table] || 'Name';
|
||
const pkField = pkFieldMap[table] || (table + 'UUID');
|
||
|
||
for (const uuid of uuids) {
|
||
if (!uuid) continue;
|
||
// 检查缓存
|
||
const cacheKey = `${table}:${uuid}`;
|
||
if (this._uuidCache[cacheKey] !== undefined) {
|
||
result[key][uuid] = this._uuidCache[cacheKey];
|
||
continue;
|
||
}
|
||
// 需要请求
|
||
fetchPromises.push(
|
||
this._fetchUUIDName(table, pkField, nameField, uuid, server).then(name => {
|
||
this._uuidCache[cacheKey] = name;
|
||
result[key][uuid] = name;
|
||
}).catch(() => {
|
||
result[key][uuid] = uuid;
|
||
})
|
||
);
|
||
}
|
||
}
|
||
|
||
await Promise.all(fetchPromises);
|
||
return result;
|
||
},
|
||
|
||
/** 内部:获取单个 UUID 对应的名称 */
|
||
async _fetchUUIDName(table, pkField, nameField, uuid, server) {
|
||
const params = { PageSize: '1' };
|
||
params[pkField] = uuid;
|
||
if (server && server !== 'gvsds-master') params.server = server;
|
||
const qs = new URLSearchParams(params).toString();
|
||
const url = `/API/FormAPI/Get/${table}?${qs}`;
|
||
const resp = await fetch(url, { credentials: 'include' });
|
||
const json = await resp.json();
|
||
if (json.code === 0 && json.data && json.data.length > 0) {
|
||
return json.data[0][nameField] || uuid;
|
||
}
|
||
return uuid;
|
||
},
|
||
|
||
/**
|
||
* 通用编辑对话框
|
||
* @param {Object} config
|
||
* @param {string} config.tableName - 表名
|
||
* @param {Object} config.data - 当前数据(含主键)
|
||
* @param {Array} [config.columns] - 列配置(可选,不传则自动获取)
|
||
* @param {string} [config.server] - server 参数
|
||
* @param {Function} [config.onSave] - 保存成功后的回调
|
||
*/
|
||
async showEditDialog(config) {
|
||
const { tableName, data, server, onSave } = config;
|
||
const svr = server || this.getServer();
|
||
const extraParams = config.extraParams || {};
|
||
|
||
// 获取列配置
|
||
let columns = config.columns;
|
||
if (!columns) {
|
||
const params = { ...extraParams };
|
||
if (svr !== 'gvsds-master') params.server = svr;
|
||
const qs = new URLSearchParams(params).toString();
|
||
const resp = await fetch(`/API/FormAPI/Columns/${tableName}?${qs}`, { credentials: 'include' });
|
||
const result = await resp.json();
|
||
if (result.code !== 0) {
|
||
alert('获取列配置失败: ' + (result.msg || '未知错误'));
|
||
return;
|
||
}
|
||
columns = result.data || [];
|
||
}
|
||
|
||
// 隐藏不可编辑字段
|
||
const pkFieldMap = {
|
||
Company: 'CompanyUUID',
|
||
Department: 'DeptUUID',
|
||
Workgroup: 'WorkgroupUUID',
|
||
UserRole: 'UserRoleUUID',
|
||
User: 'UserUUID',
|
||
};
|
||
const pkField = pkFieldMap[tableName] || (tableName + 'UUID');
|
||
columns = columns.map(c => {
|
||
if (c.field === 'CompanyUUID') { c.editable = false; c.hideInForm = true; }
|
||
if (c.field === pkField) { c.editable = false; c.hideInForm = true; }
|
||
if (c.field === 'UserPassword') { c.editable = false; c.hideInForm = true; }
|
||
return c;
|
||
});
|
||
|
||
// 生成表单
|
||
const formHtml = new FormEngine().GenerateForm(columns, data);
|
||
|
||
const pkValue = data[pkField];
|
||
|
||
// 保存对话框引用
|
||
let dialogEl = null;
|
||
|
||
window.GERPUI.ShowDialog({
|
||
title: '编辑' + (data._displayName || ''),
|
||
content: formHtml,
|
||
width: '600px',
|
||
buttons: [
|
||
{ text: '取消', type: 'Secondary' },
|
||
{
|
||
text: '保存', type: 'Primary', onClick: () => {
|
||
// 使用 FormEngine.GetFormData 收集表单数据
|
||
if (!dialogEl) dialogEl = document.querySelector('.GERPDialog');
|
||
const formEl = dialogEl ? dialogEl.querySelector('.GERPForm') : null;
|
||
if (!formEl) return false;
|
||
|
||
const formData = new FormEngine().GetFormData(formEl);
|
||
// 移除不可编辑字段
|
||
delete formData.CompanyUUID;
|
||
|
||
// 调用 ModifyItem 接口
|
||
const params = { ...extraParams };
|
||
if (svr !== 'gvsds-master') params.server = svr;
|
||
const qs = new URLSearchParams(params).toString();
|
||
const url = `/API/FormAPI/ModifyItem/${tableName}/${pkValue}?${qs}`;
|
||
|
||
fetch(url, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
credentials: 'include',
|
||
body: JSON.stringify(formData),
|
||
})
|
||
.then(resp => resp.json())
|
||
.then(result => {
|
||
if (result.code === 0) {
|
||
window.GERPUI.CloseDialog();
|
||
if (typeof onSave === 'function') onSave();
|
||
} else {
|
||
alert('保存失败: ' + (result.msg || '未知错误'));
|
||
}
|
||
})
|
||
.catch(err => {
|
||
alert('网络错误: ' + err.message);
|
||
});
|
||
|
||
return false; // 不自动关闭对话框
|
||
}
|
||
}
|
||
]
|
||
});
|
||
},
|
||
|
||
/** 判断当前 URL 是否显式指定 admin_verify=1 参数(特权模式按需启用) */
|
||
hasAdminVerifyArg() {
|
||
const urlParams = new URLSearchParams(window.location.search);
|
||
return urlParams.get('admin_verify') === '1';
|
||
}
|
||
};
|
||
|
||
// ==================== TablePage 基类 ====================
|
||
/**
|
||
* 强公司隔离的 TableEngine 页面基类。
|
||
* 自动处理:server 参数、列配置获取、CompanyUUID 隐藏、标准 TableEngine 选项。
|
||
* 子类只需提供 tableName、defaultColumns 和 onRowClick 回调。
|
||
*/
|
||
class CompanyTablePage {
|
||
/**
|
||
* @param {Object} config
|
||
* @param {string} config.tableName - 表名(如 'Company', 'Department', 'Workgroup')
|
||
* @param {string} config.title - 页面标题
|
||
* @param {Function} config.defaultColumns - 返回默认列数组的函数
|
||
* @param {Function} [config.onView] - 查看按钮回调 (row) => void,设置后显示眼睛按钮
|
||
* @param {string} [config.pkField] - 主键字段名,用于 fetchDetail 按字段过滤
|
||
* @param {string} [config.container] - 容器选择器,默认 '#TableContainer'
|
||
* @param {boolean} [config.adminBadge] - 是否启用特权模式标记(管理员可关闭公司隔离,跨公司查看/操作数据)
|
||
*/
|
||
constructor(config) {
|
||
this.tableName = config.tableName;
|
||
this.title = config.title;
|
||
this.defaultColumnsFn = config.defaultColumns;
|
||
this.onView = config.onView || null;
|
||
this.pkField = config.pkField || (this.tableName + 'UUID');
|
||
this.container = config.container || '#TableContainer';
|
||
this.server = CompanyUtils.getServer();
|
||
this.tableInstance = null;
|
||
this.drawerClose = null;
|
||
// 特权模式:管理员可关闭公司隔离,跨公司查看/操作数据
|
||
this.adminBadge = config.adminBadge || false;
|
||
this.isAdminMode = false;
|
||
|
||
CompanyUtils.setPageTitle(this.title);
|
||
}
|
||
|
||
/** 构建带 server 与 admin_verify 的 FormAPI URL */
|
||
apiUrl(path, params = {}) {
|
||
const allParams = {};
|
||
if (this.server !== 'gvsds-master') allParams.server = this.server;
|
||
if (this.isAdminMode) allParams.admin_verify = '1';
|
||
Object.assign(allParams, params);
|
||
const qs = new URLSearchParams(allParams).toString();
|
||
return qs ? `${path}?${qs}` : path;
|
||
}
|
||
|
||
/** 检查当前用户是否具有管理员特权(超级用户或当前角色拥有 admin:super 权限) */
|
||
async checkAdmin() {
|
||
try {
|
||
const resp = await fetch(this.apiUrl('/API/FormAPI/CheckAdmin'), { credentials: 'include' });
|
||
const result = await resp.json();
|
||
return result.code === 0 && result.data && result.data.is_admin;
|
||
} catch (e) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/** 初始化表格 */
|
||
async init() {
|
||
const containerEl = document.querySelector(this.container);
|
||
if (!containerEl) return;
|
||
|
||
// 特权模式:仅当 URL 显式指定 admin_verify=1 时才检查管理员特权并启用
|
||
// 管理员访问页面默认走普通公司隔离,需手动在 URL 加 ?admin_verify=1 才进入特权模式
|
||
if (this.adminBadge) {
|
||
this.isAdminMode = CompanyUtils.hasAdminVerifyArg() ? await this.checkAdmin() : false;
|
||
const badge = document.getElementById('admin-badge');
|
||
if (badge) badge.style.display = this.isAdminMode ? '' : 'none';
|
||
}
|
||
|
||
try {
|
||
let columns;
|
||
if (this.server === 'gvsds-master') {
|
||
const resp = await fetch(this.apiUrl(`/API/FormAPI/Columns/${this.tableName}`), { credentials: 'include' });
|
||
const result = await resp.json();
|
||
if (result.code !== 0) {
|
||
containerEl.innerHTML = `<div class="cp-error">加载列配置失败: ${CompanyUtils.esc(result.msg || '')}</div>`;
|
||
return;
|
||
}
|
||
columns = result.data || [];
|
||
} else {
|
||
columns = this.defaultColumnsFn();
|
||
}
|
||
|
||
// 强公司隔离:CompanyUUID 由后端自动注入,前端隐藏输入框
|
||
// 同时用 defaultColumns 的 type/options 覆盖动态列(后端 SmallIntegerField 生成 number,需前端补充 select 选项)
|
||
const defaultMap = {};
|
||
if (this.defaultColumnsFn) {
|
||
this.defaultColumnsFn().forEach(c => { defaultMap[c.field] = c; });
|
||
}
|
||
columns = columns.map(c => {
|
||
if (c.field === 'CompanyUUID') { c.editable = false; c.hideInForm = true; }
|
||
const def = defaultMap[c.field];
|
||
if (def) {
|
||
if (def.type) c.type = def.type;
|
||
if (def.options) c.options = def.options;
|
||
}
|
||
return c;
|
||
});
|
||
|
||
this.tableInstance = new TableEngine({
|
||
container: this.container,
|
||
tableName: this.tableName,
|
||
columns,
|
||
data: [],
|
||
pageSize: 20,
|
||
showPagination: true,
|
||
showCheckbox: true,
|
||
editable: true,
|
||
sortable: true,
|
||
filterable: true,
|
||
striped: true,
|
||
hoverable: true,
|
||
bordered: true,
|
||
api: {
|
||
get: this.apiUrl(`/API/FormAPI/Get/${this.tableName}`),
|
||
add: this.apiUrl(`/API/FormAPI/Add/${this.tableName}`),
|
||
edit: this.apiUrl(`/API/FormAPI/Modify/${this.tableName}/<id>`),
|
||
modify: this.apiUrl(`/API/FormAPI/ModifyItem/${this.tableName}/<id>`),
|
||
delete: this.apiUrl(`/API/FormAPI/Delete/${this.tableName}/<id>`),
|
||
},
|
||
onView: this.onView,
|
||
// 注意:不设置 onEdit/onDelete,让 TableEngine 使用内置的编辑/删除对话框
|
||
});
|
||
} catch (error) {
|
||
console.error('init error:', error);
|
||
containerEl.innerHTML = `<div class="cp-error">初始化失败: ${CompanyUtils.esc(error.message)}</div>`;
|
||
}
|
||
}
|
||
|
||
/** 通过 PK 字段获取单条记录 */
|
||
async fetchDetail(pkValue, pageSize = 1) {
|
||
const params = { PageSize: String(pageSize) };
|
||
params[this.pkField] = pkValue;
|
||
const url = this.apiUrl(`/API/FormAPI/Get/${this.tableName}`, params);
|
||
const resp = await fetch(url, { credentials: 'include' });
|
||
return resp.json();
|
||
}
|
||
|
||
/** 通过 FormAPI 获取关联列表(支持字段过滤或关键字搜索) */
|
||
async fetchRelated(relatedTable, filterValue, pageSize = 50, filterField = null) {
|
||
const params = { PageSize: String(pageSize) };
|
||
if (filterField) {
|
||
params[filterField] = filterValue;
|
||
} else {
|
||
params.keyword = filterValue;
|
||
}
|
||
const url = this.apiUrl(`/API/FormAPI/Get/${relatedTable}`, params);
|
||
const resp = await fetch(url, { credentials: 'include' });
|
||
return resp.json();
|
||
}
|
||
}
|
||
|
||
// 暴露到全局
|
||
window.CompanyUtils = CompanyUtils;
|
||
window.CompanyTablePage = CompanyTablePage;
|