Files
SMANAGE/TempLate/GERP/assets/gerp/CompanyCommon.js
T

509 lines
22 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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;