diff --git a/Main.py b/Main.py index 6761d9da..7c3bf3a4 100644 --- a/Main.py +++ b/Main.py @@ -214,9 +214,9 @@ class FilteredLoggingMiddleware: return self.app(environ, wrapped_start_response) -import re import threading import time +import re class SecurityMiddleware: def __init__(self, app): diff --git a/TempLate/GERP/_base b/TempLate/GERP/_base index 7ceff088..e10ca79a 100644 --- a/TempLate/GERP/_base +++ b/TempLate/GERP/_base @@ -24,7 +24,7 @@ - + diff --git a/TempLate/GERP/assets/css/Component.css b/TempLate/GERP/assets/css/Component.css index 176fd820..993b0b4c 100644 --- a/TempLate/GERP/assets/css/Component.css +++ b/TempLate/GERP/assets/css/Component.css @@ -233,6 +233,20 @@ margin-top: -2px; } +/* 半选状态(横线) */ +.GERPCheckbox input:indeterminate + .GERPCheckboxBox { + background: var(--PrimaryColor); + border-color: var(--PrimaryColor); +} + +.GERPCheckbox input:indeterminate + .GERPCheckboxBox::after { + content: ''; + width: 10px; + height: 0; + border-bottom: 2px solid white; + margin-top: 0; +} + .GERPCheckbox input:disabled + .GERPCheckboxBox { opacity: 0.5; cursor: not-allowed; @@ -458,6 +472,15 @@ align-items: center; justify-content: space-between; padding: 16px 20px 14px; + cursor: move; + user-select: none; +} + +/* 手机端对话框标题不显示拖动光标 */ +@media (max-width: 768px) { + .GERPDialogHeader { + cursor: default; + } } .GERPDialogTitle { diff --git a/TempLate/GERP/assets/css/GERP.css b/TempLate/GERP/assets/css/GERP.css index d1ff40f8..aac3c224 100644 --- a/TempLate/GERP/assets/css/GERP.css +++ b/TempLate/GERP/assets/css/GERP.css @@ -3396,6 +3396,15 @@ body.GERPSidebarOpen .GERPSidebar { padding: 20px 24px; border-bottom: 1px solid var(--BorderColor); flex-shrink: 0; + cursor: move; + user-select: none; +} + +/* 手机端抽屉标题不显示拖动光标 */ +@media (max-width: 768px) { + .GERPDrawerHeader { + cursor: default; + } } .GERPDrawerTitle { @@ -4369,6 +4378,14 @@ body.GERPSidebarOpen .GERPSidebar { justify-content: space-between; padding: 16px 20px; border-bottom: 1px solid var(--BorderColor); + cursor: move; + user-select: none; +} + +@media (max-width: 768px) { + .GERPDrawerHeader { + cursor: default; + } } .GERPDrawerTitle { diff --git a/TempLate/GERP/assets/gerp/CompanyCommon.js b/TempLate/GERP/assets/gerp/CompanyCommon.js new file mode 100644 index 00000000..5e801b04 --- /dev/null +++ b/TempLate/GERP/assets/gerp/CompanyCommon.js @@ -0,0 +1,468 @@ +/** + * CompanyCommon.js — Company/* 页面共享组件 + * 提供工具函数、TablePage 基类和共享样式,消除重复代码。 + * 所有 Company/* 页面通过 引入。 + */ + +// ==================== 共享样式(仅注入一次) ==================== +(function () { + if (document.getElementById('CompanyCommonStyles')) return; + const style = document.createElement('style'); + style.id = 'CompanyCommonStyles'; + style.textContent = ` + .GERPPage { padding: 24px; max-width: 1400px; 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 '启用'; + return '禁用'; + }, + + /** 时间格式化 */ + 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 `${label}`; + }, + + /** 等待 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; // 不自动关闭对话框 + } + } + ] + }); + }, +}; + +// ==================== 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' + */ + 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; + + CompanyUtils.setPageTitle(this.title); + } + + /** 构建带 server 的 FormAPI URL */ + apiUrl(path, params = {}) { + const allParams = {}; + if (this.server !== 'gvsds-master') allParams.server = this.server; + Object.assign(allParams, params); + const qs = new URLSearchParams(allParams).toString(); + return qs ? `${path}?${qs}` : path; + } + + /** 初始化表格 */ + async init() { + const containerEl = document.querySelector(this.container); + if (!containerEl) return; + + 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 = `