修复了一些问题
This commit is contained in:
1 parent
f3cf68a51a
commit
a77df5da0f
114 files changed
+5183
-1664
No files matched your search
@@ -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):
|
||||
|
||||
+1
-1
@@ -24,7 +24,7 @@
|
||||
<link rel="stylesheet" href="/assets/js/core/NeoUI/NeoToast/style.css">
|
||||
<!-- JS - 全部在模板内容之前加载,确保内联脚本可用 -->
|
||||
<script src="/assets/gerp/GERPThemeSync.js"></script>
|
||||
<script src="/assets/gerp/GERP.js"></script>
|
||||
<script src="/assets/gerp/GERP.js?v=20260625b"></script>
|
||||
<script src="/assets/gerp/GERPUI/GERPContextMenu.js"></script>
|
||||
<script src="/assets/gerp/GERPUI/GERPUI.js"></script>
|
||||
<script src="/assets/gerp/GERPUI/GERPLayui.js"></script>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -0,0 +1,468 @@
|
||||
/**
|
||||
* 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; 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 '<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; // 不自动关闭对话框
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
// ==================== 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 = `<div class="cp-error">加载列配置失败: ${CompanyUtils.esc(result.msg || '')}</div>`;
|
||||
return;
|
||||
}
|
||||
columns = result.data || [];
|
||||
} else {
|
||||
columns = this.defaultColumnsFn();
|
||||
}
|
||||
|
||||
// 强公司隔离:CompanyUUID 由后端自动注入,前端隐藏输入框
|
||||
columns = columns.map(c => {
|
||||
if (c.field === 'CompanyUUID') { c.editable = false; c.hideInForm = true; }
|
||||
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;
|
||||
@@ -3194,6 +3194,13 @@ class GERPUI {
|
||||
});
|
||||
});
|
||||
|
||||
// 桌面端:标题栏可拖动
|
||||
const dlgHeader = dialog.querySelector('.GERPDialogHeader');
|
||||
const dlgContent = dialog.querySelector('.GERPDialogContent');
|
||||
if (dlgHeader && dlgContent) {
|
||||
this._makeDraggable(dlgContent, dlgHeader);
|
||||
}
|
||||
|
||||
return this.State.CurrentDialog;
|
||||
}
|
||||
|
||||
@@ -3264,12 +3271,17 @@ class GERPUI {
|
||||
|
||||
OpenDrawer(options) {
|
||||
const { title, content, placement = 'right', width = '400px', showClose = true, onClose, footer } = options;
|
||||
const isMobile = this._isMobile();
|
||||
|
||||
const drawer = document.createElement('div');
|
||||
drawer.className = `GERPDrawer GERPDrawer${this.Capitalize(placement)}`;
|
||||
|
||||
// 手机端:全屏显示
|
||||
const actualWidth = isMobile ? '100%' : width;
|
||||
|
||||
drawer.innerHTML = `
|
||||
<div class="GERPDrawerMask"></div>
|
||||
<div class="GERPDrawerContent" style="${placement === 'left' || placement === 'right' ? 'width' : 'height'}: ${width}">
|
||||
<div class="GERPDrawerContent" style="${placement === 'left' || placement === 'right' ? 'width' : 'height'}: ${actualWidth}${isMobile ? '; height: 100%' : ''}">
|
||||
${title ? `
|
||||
<div class="GERPDrawerHeader">
|
||||
<h3 class="GERPDrawerTitle">${title}</h3>
|
||||
@@ -3298,6 +3310,15 @@ class GERPUI {
|
||||
drawer.querySelector('.GERPDrawerMask')?.addEventListener('click', closeDrawer);
|
||||
drawer.querySelector('.GERPDrawerClose')?.addEventListener('click', closeDrawer);
|
||||
|
||||
// 桌面端:标题栏可拖动
|
||||
if (!isMobile) {
|
||||
const header = drawer.querySelector('.GERPDrawerHeader');
|
||||
const contentEl = drawer.querySelector('.GERPDrawerContent');
|
||||
if (header && contentEl) {
|
||||
this._makeDraggable(contentEl, header);
|
||||
}
|
||||
}
|
||||
|
||||
return { element: drawer, close: closeDrawer };
|
||||
}
|
||||
|
||||
@@ -3425,6 +3446,82 @@ class GERPUI {
|
||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||
}
|
||||
|
||||
/** 判断是否为移动端 */
|
||||
_isMobile() {
|
||||
return window.matchMedia('(max-width: 768px)').matches || 'ontouchstart' in window;
|
||||
}
|
||||
|
||||
/**
|
||||
* 使元素可通过拖柄拖动(桌面端)。
|
||||
* 拖动时自动从 flex 居中切换到 absolute 定位,保持窗口在视口内。
|
||||
*/
|
||||
_makeDraggable(contentEl, handleEl) {
|
||||
if (this._isMobile()) return; // 手机端不启用拖动
|
||||
|
||||
let isDragging = false;
|
||||
let startX, startY, startLeft, startTop;
|
||||
let switchedToAbsolute = false;
|
||||
|
||||
handleEl.style.cursor = 'move';
|
||||
handleEl.style.userSelect = 'none';
|
||||
|
||||
const onPointerDown = (e) => {
|
||||
// 排除关闭按钮等子元素
|
||||
if (e.target.closest('.GERPDialogClose, .GERPDrawerClose, button')) return;
|
||||
isDragging = true;
|
||||
startX = e.clientX;
|
||||
startY = e.clientY;
|
||||
|
||||
if (!switchedToAbsolute) {
|
||||
// 首次拖动:从 flex 居中切换到 absolute 定位
|
||||
const rect = contentEl.getBoundingClientRect();
|
||||
const parentEl = contentEl.parentElement;
|
||||
// 取消父级 flex 居中
|
||||
parentEl.style.display = 'block';
|
||||
contentEl.style.position = 'absolute';
|
||||
contentEl.style.left = rect.left + 'px';
|
||||
contentEl.style.top = rect.top + 'px';
|
||||
contentEl.style.margin = '0';
|
||||
startLeft = rect.left;
|
||||
startTop = rect.top;
|
||||
switchedToAbsolute = true;
|
||||
} else {
|
||||
startLeft = parseFloat(contentEl.style.left) || 0;
|
||||
startTop = parseFloat(contentEl.style.top) || 0;
|
||||
}
|
||||
|
||||
document.addEventListener('mousemove', onPointerMove);
|
||||
document.addEventListener('mouseup', onPointerUp);
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
const onPointerMove = (e) => {
|
||||
if (!isDragging) return;
|
||||
const dx = e.clientX - startX;
|
||||
const dy = e.clientY - startY;
|
||||
let newLeft = startLeft + dx;
|
||||
let newTop = startTop + dy;
|
||||
|
||||
// 限制在视口内
|
||||
const rect = contentEl.getBoundingClientRect();
|
||||
const vw = window.innerWidth;
|
||||
const vh = window.innerHeight;
|
||||
newLeft = Math.max(0, Math.min(newLeft, vw - rect.width));
|
||||
newTop = Math.max(0, Math.min(newTop, vh - rect.height));
|
||||
|
||||
contentEl.style.left = newLeft + 'px';
|
||||
contentEl.style.top = newTop + 'px';
|
||||
};
|
||||
|
||||
const onPointerUp = () => {
|
||||
isDragging = false;
|
||||
document.removeEventListener('mousemove', onPointerMove);
|
||||
document.removeEventListener('mouseup', onPointerUp);
|
||||
};
|
||||
|
||||
handleEl.addEventListener('mousedown', onPointerDown);
|
||||
}
|
||||
|
||||
// ==================== 高级模态框 ====================
|
||||
ShowModal(options) {
|
||||
const { title, content, width = '520px', closable = true, maskClosable = true, footer = null, onOk, onCancel, className = '' } = options;
|
||||
@@ -3490,6 +3587,13 @@ class GERPUI {
|
||||
closeModal();
|
||||
});
|
||||
|
||||
// 桌面端:标题栏可拖动
|
||||
const modalHeader = modal.querySelector('.GERPModalHeader');
|
||||
const modalWrapper = modal.querySelector('.GERPModalWrapper');
|
||||
if (modalHeader && modalWrapper) {
|
||||
this._makeDraggable(modalWrapper, modalHeader);
|
||||
}
|
||||
|
||||
return { element: modal, close: closeModal };
|
||||
}
|
||||
|
||||
|
||||
@@ -343,9 +343,10 @@ class TableEngine {
|
||||
const cell = document.createElement('div');
|
||||
cell.className = 'GERPTableHeaderCell GERPTableActionsCell GERPTableFixedRight';
|
||||
cell.textContent = '操作';
|
||||
cell.style.flex = '0 0 100px';
|
||||
cell.style.width = '100px';
|
||||
cell.style.minWidth = '100px';
|
||||
const w = this.Options.onView ? '150px' : '100px';
|
||||
cell.style.flex = `0 0 ${w}`;
|
||||
cell.style.width = w;
|
||||
cell.style.minWidth = w;
|
||||
cell.style.justifyContent = 'center';
|
||||
return cell;
|
||||
}
|
||||
@@ -465,20 +466,42 @@ class TableEngine {
|
||||
_createActionsCell(row) {
|
||||
const cell = document.createElement('div');
|
||||
cell.className = 'GERPTableCell GERPTableActionsCell GERPTableFixedRight';
|
||||
cell.style.flex = '0 0 100px';
|
||||
cell.style.width = '100px';
|
||||
cell.style.minWidth = '100px';
|
||||
const hasView = !!this.Options.onView;
|
||||
const w = hasView ? '150px' : '100px';
|
||||
cell.style.flex = `0 0 ${w}`;
|
||||
cell.style.width = w;
|
||||
cell.style.minWidth = w;
|
||||
cell.style.justifyContent = 'center';
|
||||
|
||||
const actions = document.createElement('div');
|
||||
actions.className = 'GERPTableActions';
|
||||
|
||||
// 图标按钮通用样式(紧凑型)
|
||||
const iconBtnStyle = 'padding:4px 8px;font-size:13px;';
|
||||
|
||||
// 查看按钮(眼睛图标)
|
||||
if (hasView) {
|
||||
const viewBtn = document.createElement('button');
|
||||
viewBtn.className = 'GERPBtn Text';
|
||||
viewBtn.innerHTML = '<i class="fas fa-eye"></i>';
|
||||
viewBtn.title = '查看';
|
||||
viewBtn.style.cssText = iconBtnStyle + 'color:var(--PrimaryColor);';
|
||||
viewBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
console.log('[TableEngine] onView clicked, row:', row);
|
||||
this.Options.onView(row);
|
||||
});
|
||||
actions.appendChild(viewBtn);
|
||||
}
|
||||
|
||||
if (this.Options.editable) {
|
||||
const editBtn = document.createElement('button');
|
||||
editBtn.className = 'GERPBtn Text';
|
||||
editBtn.innerHTML = '<i class="fas fa-edit"></i>';
|
||||
editBtn.title = '编辑';
|
||||
editBtn.addEventListener('click', () => {
|
||||
editBtn.style.cssText = iconBtnStyle;
|
||||
editBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
this.ShowEditDialog(row);
|
||||
});
|
||||
actions.appendChild(editBtn);
|
||||
@@ -488,7 +511,11 @@ class TableEngine {
|
||||
deleteBtn.className = 'GERPBtn Text';
|
||||
deleteBtn.innerHTML = '<i class="fas fa-trash"></i>';
|
||||
deleteBtn.title = '删除';
|
||||
deleteBtn.addEventListener('click', () => this.HandleDelete(row));
|
||||
deleteBtn.style.cssText = iconBtnStyle;
|
||||
deleteBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
this.HandleDelete(row);
|
||||
});
|
||||
actions.appendChild(deleteBtn);
|
||||
|
||||
cell.appendChild(actions);
|
||||
@@ -684,14 +711,9 @@ class TableEngine {
|
||||
|
||||
if (column.type === 'entityPicker' && column.entityPicker) {
|
||||
const ep = column.entityPicker;
|
||||
// 尝试从行数据中获取关联实体的显示名称
|
||||
const labelField = ep.labelField || '';
|
||||
const valueField = ep.valueField || column.field;
|
||||
// 查找行中是否有 "字段名_标签" 或 "字段名__label" 等辅助字段
|
||||
// 优先使用后端附带的 _label 字段(关联实体的实际名称)
|
||||
let label = '';
|
||||
if (row[labelField]) {
|
||||
label = String(row[labelField]);
|
||||
} else if (row[column.field + '_label']) {
|
||||
if (row[column.field + '_label']) {
|
||||
label = String(row[column.field + '_label']);
|
||||
} else if (row[column.field + '__label']) {
|
||||
label = String(row[column.field + '__label']);
|
||||
@@ -808,20 +830,21 @@ class TableEngine {
|
||||
return;
|
||||
}
|
||||
|
||||
const checkbox = e.target.closest('.GERPTableRowCheckbox');
|
||||
if (checkbox) {
|
||||
const rowId = checkbox.dataset.id;
|
||||
this.ToggleRowSelection(rowId);
|
||||
}
|
||||
const checkboxLabel = e.target.closest('.GERPCheckbox');
|
||||
if (checkboxLabel) {
|
||||
const input = checkboxLabel.querySelector('input');
|
||||
if (input?.classList.contains('GERPTableRowCheckbox')) {
|
||||
const rowId = input.dataset.id;
|
||||
this.ToggleRowSelection(rowId);
|
||||
}
|
||||
|
||||
const selectAll = e.target.closest('.GERPTableSelectAll');
|
||||
if (selectAll) {
|
||||
const isChecked = selectAll.checked;
|
||||
if (selectAll.indeterminate) {
|
||||
selectAll.checked = false;
|
||||
this.ToggleSelectAll(false);
|
||||
} else {
|
||||
this.ToggleSelectAll(isChecked);
|
||||
if (input?.classList.contains('GERPTableSelectAll')) {
|
||||
if (input.indeterminate) {
|
||||
input.checked = false;
|
||||
this.ToggleSelectAll(false);
|
||||
} else {
|
||||
this.ToggleSelectAll(input.checked);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1529,7 +1552,7 @@ class TableEngine {
|
||||
|
||||
cell.classList.add('GERPTableCellEditing');
|
||||
|
||||
const input = this._createEditInput(value, column);
|
||||
const input = this._createEditInput(value, column, row);
|
||||
cell.innerHTML = '';
|
||||
cell.appendChild(input);
|
||||
|
||||
@@ -1557,7 +1580,7 @@ class TableEngine {
|
||||
if (input.select) input.select();
|
||||
}
|
||||
|
||||
_createEditInput(value, column) {
|
||||
_createEditInput(value, column, rowData) {
|
||||
let input;
|
||||
|
||||
switch (column.type) {
|
||||
@@ -1636,9 +1659,14 @@ class TableEngine {
|
||||
input.className = 'GERPEntityPicker';
|
||||
const ep = column.entityPicker || {};
|
||||
const epValue = value || '';
|
||||
// 从行数据中获取关联实体的显示名称
|
||||
const epLabel = (rowData && rowData[column.field + '_label'])
|
||||
|| (rowData && rowData[column.field + '__label'])
|
||||
|| '';
|
||||
const epDisplay = epLabel || (epValue ? String(epValue).substring(0, 8) + '...' : '');
|
||||
input.innerHTML = `
|
||||
<input type="hidden" name="${column.field}" value="${epValue}">
|
||||
<div class="GERPEntityPickerInput" contenteditable="true" data-placeholder="${ep.placeholder || '搜索...'}">${epValue ? String(epValue).substring(0, 8) + '...' : ''}</div>
|
||||
<div class="GERPEntityPickerInput" contenteditable="true" data-placeholder="${ep.placeholder || '搜索...'}">${this._escapeHtml(epDisplay)}</div>
|
||||
<div class="GERPEntityPickerDropdown" style="display:none;"></div>
|
||||
`;
|
||||
input.style.position = 'relative';
|
||||
|
||||
@@ -1000,11 +1000,17 @@ class GERP {
|
||||
|
||||
// 静态方法:供子页面调用
|
||||
static Navigate(page, title) {
|
||||
window.parent.postMessage({ type: 'NAVIGATE', data: { page, title } }, '*');
|
||||
if (window.parent !== window && window.parent.GERPApp && window.parent.GERPApp.UI) {
|
||||
// 在 GERP 父框架内:通过 postMessage 通知父框架打开新标签页
|
||||
window.parent.postMessage({ type: 'NAVIGATE', data: { page, title } }, '*');
|
||||
} else {
|
||||
// 非 GERP 父情况:在新标签页打开
|
||||
window.open(page, '_blank');
|
||||
}
|
||||
}
|
||||
|
||||
static SetTitle(title) {
|
||||
if (window.parent !== window) {
|
||||
if (window.parent !== window && window.parent.GERPApp && window.parent.GERPApp.UI) {
|
||||
window.parent.postMessage({ type: 'SET_TITLE', data: { title } }, '*');
|
||||
} else {
|
||||
document.title = title;
|
||||
@@ -1012,7 +1018,11 @@ class GERP {
|
||||
}
|
||||
|
||||
static Refresh() {
|
||||
window.parent.postMessage({ type: 'REFRESH' }, '*');
|
||||
if (window.parent !== window && window.parent.GERPApp && window.parent.GERPApp.UI) {
|
||||
window.parent.postMessage({ type: 'REFRESH' }, '*');
|
||||
} else {
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,244 @@
|
||||
<script src="/assets/gerp/CompanyCommon.js?v=20260625d"></script>
|
||||
|
||||
<div class="GERPPage">
|
||||
<div class="cp-page">
|
||||
<div class="cp-header">
|
||||
<a href="javascript:void(0)" class="cp-back-btn" onclick="goBack()"><i class="fas fa-arrow-left"></i> 返回</a>
|
||||
<h1 class="PageTitle">
|
||||
<i class="fas fa-building icon-primary icon-left-lg"></i>
|
||||
<span id="page-title">公司详情</span>
|
||||
</h1>
|
||||
<button class="GERPBtn Primary" onclick="handleEdit()" style="margin-left:auto;"><i class="fas fa-edit"></i> 编辑</button>
|
||||
</div>
|
||||
<div class="cp-container" id="detailContainer" style="overflow:auto;">
|
||||
<div class="cp-loading"><i class="fas fa-spinner fa-spin"></i> 加载中...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const currentServer = CompanyUtils.getServer();
|
||||
const companyUUID = new URLSearchParams(window.location.search).get('CompanyUUID');
|
||||
|
||||
CompanyUtils.setPageTitle('公司详情');
|
||||
|
||||
function goBack() {
|
||||
if (window.history.length > 1) {
|
||||
window.history.back();
|
||||
} else {
|
||||
GERP.Navigate('/pages/Company/company.html' + (currentServer !== 'gvsds-master' ? `?server=${encodeURIComponent(currentServer)}` : ''), '公司管理');
|
||||
}
|
||||
}
|
||||
|
||||
let currentData = null;
|
||||
let deptTable = null;
|
||||
let wgTable = null;
|
||||
let empTable = null;
|
||||
|
||||
function buildApiUrl(path, params = {}) {
|
||||
const allParams = {};
|
||||
if (currentServer !== 'gvsds-master') allParams.server = currentServer;
|
||||
Object.assign(allParams, params);
|
||||
const qs = new URLSearchParams(allParams).toString();
|
||||
return qs ? `${path}?${qs}` : path;
|
||||
}
|
||||
|
||||
async function init() {
|
||||
const containerEl = document.getElementById('detailContainer');
|
||||
|
||||
if (!companyUUID) {
|
||||
containerEl.innerHTML = `<div class="cp-error"><i class="fas fa-exclamation-triangle" style="margin-right:6px;"></i> 缺少 CompanyUUID 参数,无法加载公司详情。</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const dataResp = await fetch(buildApiUrl('/API/FormAPI/Get/Company', { CompanyUUID: companyUUID, PageSize: '1' }), { credentials: 'include' });
|
||||
const dataResult = await dataResp.json();
|
||||
if (dataResult.code !== 0) {
|
||||
containerEl.innerHTML = `<div class="cp-error"><i class="fas fa-exclamation-circle" style="margin-right:6px;"></i> 加载失败: ${CompanyUtils.esc(dataResult.msg || '未知错误')}</div>`;
|
||||
return;
|
||||
}
|
||||
const list = dataResult.data || [];
|
||||
const data = Array.isArray(list) ? (list.find(c => c.CompanyUUID === companyUUID) || list[0]) : list;
|
||||
if (!data) {
|
||||
containerEl.innerHTML = `<div class="cp-error">未找到公司数据</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
currentData = data;
|
||||
CompanyUtils.setPageTitle(data.CompanyName || '公司详情');
|
||||
|
||||
// 解析 UUID 字段为名称
|
||||
const uuidMap = {};
|
||||
if (data.ParentCompanyUUID) uuidMap.parentCompany = { table: 'Company', uuids: [data.ParentCompanyUUID] };
|
||||
if (data.TenantUUID) uuidMap.tenant = { table: 'Tenant', uuids: [data.TenantUUID] };
|
||||
const resolved = await CompanyUtils.resolveUUIDs(uuidMap);
|
||||
|
||||
// 渲染基本信息 + 表格容器
|
||||
containerEl.innerHTML = `
|
||||
<div class="cp-detail-section">
|
||||
<h3><i class="fas fa-building"></i> 基本信息</h3>
|
||||
<div class="cp-info-grid">
|
||||
<div class="cp-info-item"><div class="label">公司名称</div><div class="value">${CompanyUtils.esc(data.CompanyName) || '-'}</div></div>
|
||||
<div class="cp-info-item"><div class="label">公司简称</div><div class="value">${CompanyUtils.esc(data.CompanyShort) || '-'}</div></div>
|
||||
<div class="cp-info-item"><div class="label">公司状态</div><div class="value">${CompanyUtils.statusTag(data.CompanyStatus)}</div></div>
|
||||
<div class="cp-info-item"><div class="label">上级公司</div><div class="value">${CompanyUtils.esc(resolved.parentCompany?.[data.ParentCompanyUUID] || data.ParentCompanyUUID) || '-'}</div></div>
|
||||
<div class="cp-info-item"><div class="label">租户</div><div class="value">${CompanyUtils.esc(resolved.tenant?.[data.TenantUUID] || data.TenantUUID) || '-'}</div></div>
|
||||
<div class="cp-info-item"><div class="label">创建时间</div><div class="value">${CompanyUtils.formatTime(data.CompanyCreateTime)}</div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cp-detail-section">
|
||||
<h3><i class="fas fa-sitemap"></i> 部门</h3>
|
||||
<div id="dept-table-container" style="height:400px;"></div>
|
||||
</div>
|
||||
<div class="cp-detail-section">
|
||||
<h3><i class="fas fa-users-cog"></i> 工作组</h3>
|
||||
<div id="wg-table-container" style="height:400px;"></div>
|
||||
</div>
|
||||
<div class="cp-detail-section">
|
||||
<h3><i class="fas fa-user-friends"></i> 员工</h3>
|
||||
<div id="emp-table-container" style="height:400px;"></div>
|
||||
</div>`;
|
||||
|
||||
// 初始化 TableEngine 实例
|
||||
initDeptTable();
|
||||
initWorkgroupTable();
|
||||
initEmployeeTable();
|
||||
} catch (e) {
|
||||
containerEl.innerHTML = `<div class="cp-error"><i class="fas fa-exclamation-circle" style="margin-right:6px;"></i> 网络错误: ${CompanyUtils.esc(e.message)}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
function initDeptTable() {
|
||||
deptTable = new TableEngine({
|
||||
container: '#dept-table-container',
|
||||
height: '400px',
|
||||
tableName: 'Department',
|
||||
columns: [
|
||||
{ field: 'DeptUUID', title: '部门UUID', primaryKey: true, editable: false, hideInForm: true },
|
||||
{ field: 'DeptName', title: '部门名称' },
|
||||
{ field: 'DeptStatus', title: '状态', type: 'select', options: [{ value: 0, label: '禁用' }, { value: 1, label: '启用' }] },
|
||||
{ field: 'DeptSort', title: '排序', type: 'number' },
|
||||
{ field: 'ParentDeptUUID_label', title: '上级部门' },
|
||||
{ field: 'DeptCreateTime', title: '创建时间', type: 'datetime', editable: false },
|
||||
],
|
||||
data: [],
|
||||
pageSize: 20,
|
||||
columnDistribution: 'equal',
|
||||
showPagination: true,
|
||||
showCheckbox: true,
|
||||
editable: true,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
striped: true,
|
||||
hoverable: true,
|
||||
bordered: true,
|
||||
api: {
|
||||
get: buildApiUrl('/API/FormAPI/Get/Department'),
|
||||
add: buildApiUrl('/API/FormAPI/Add/Department'),
|
||||
edit: buildApiUrl('/API/FormAPI/Modify/Department/<id>'),
|
||||
modify: buildApiUrl('/API/FormAPI/ModifyItem/Department/<id>'),
|
||||
delete: buildApiUrl('/API/FormAPI/Delete/Department/<id>'),
|
||||
},
|
||||
onView: (row) => {
|
||||
let url = `/pages/Company/department-detail.html?DeptUUID=${encodeURIComponent(row.DeptUUID || '')}`;
|
||||
if (currentServer !== 'gvsds-master') url += `&server=${encodeURIComponent(currentServer)}`;
|
||||
GERP.Navigate(url, row.DeptName || '部门详情');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function initWorkgroupTable() {
|
||||
wgTable = new TableEngine({
|
||||
container: '#wg-table-container',
|
||||
tableName: 'Workgroup',
|
||||
columns: [
|
||||
{ field: 'WorkgroupUUID', title: '工作组UUID', primaryKey: true, editable: false, hideInForm: true },
|
||||
{ field: 'WorkgroupName', title: '工作组名称' },
|
||||
{ field: 'WorkgroupStatus', title: '状态', type: 'select', options: [{ value: 0, label: '禁用' }, { value: 1, label: '启用' }] },
|
||||
{ field: 'WorkgroupCreateTime', title: '创建时间', type: 'datetime', editable: false },
|
||||
],
|
||||
data: [],
|
||||
pageSize: 20,
|
||||
columnDistribution: 'equal',
|
||||
showPagination: true,
|
||||
showCheckbox: true,
|
||||
editable: true,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
striped: true,
|
||||
hoverable: true,
|
||||
bordered: true,
|
||||
api: {
|
||||
get: buildApiUrl('/API/FormAPI/Get/Workgroup'),
|
||||
add: buildApiUrl('/API/FormAPI/Add/Workgroup'),
|
||||
edit: buildApiUrl('/API/FormAPI/Modify/Workgroup/<id>'),
|
||||
modify: buildApiUrl('/API/FormAPI/ModifyItem/Workgroup/<id>'),
|
||||
delete: buildApiUrl('/API/FormAPI/Delete/Workgroup/<id>'),
|
||||
},
|
||||
onView: (row) => {
|
||||
let url = `/pages/Company/workgroup-detail.html?WorkgroupUUID=${encodeURIComponent(row.WorkgroupUUID || '')}`;
|
||||
if (currentServer !== 'gvsds-master') url += `&server=${encodeURIComponent(currentServer)}`;
|
||||
GERP.Navigate(url, row.WorkgroupName || '工作组详情');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function initEmployeeTable() {
|
||||
empTable = new TableEngine({
|
||||
container: '#emp-table-container',
|
||||
height: '400px',
|
||||
tableName: 'UserRole',
|
||||
columns: [
|
||||
{ field: 'UserRoleUUID', title: '关联UUID', primaryKey: true, editable: false, hideInForm: true },
|
||||
CompanyUtils.epColumn('UserUUID', '用户名', 'User', { editable: false }),
|
||||
CompanyUtils.epColumn('RoleUUID', '角色名', 'Role'),
|
||||
CompanyUtils.epColumn('CompanyUUID', '公司', 'Company', { editable: false, hideInForm: true }),
|
||||
CompanyUtils.epColumn('DeptUUID', '部门', 'Department'),
|
||||
CompanyUtils.epColumn('WorkgroupUUID', '工作组', 'Workgroup'),
|
||||
{ field: 'RoleType', title: '角色类型', type: 'select', options: [{ value: 0, label: '组员' }, { value: 1, label: '组长' }] },
|
||||
{ field: 'CreateTime', title: '创建时间', type: 'datetime', editable: false },
|
||||
],
|
||||
data: [],
|
||||
pageSize: 20,
|
||||
columnDistribution: 'equal',
|
||||
showPagination: true,
|
||||
showCheckbox: true,
|
||||
editable: true,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
striped: true,
|
||||
hoverable: true,
|
||||
bordered: true,
|
||||
api: {
|
||||
get: buildApiUrl('/API/FormAPI/Get/UserRole'),
|
||||
add: buildApiUrl('/API/FormAPI/Add/UserRole'),
|
||||
edit: buildApiUrl('/API/FormAPI/Modify/UserRole/<id>'),
|
||||
modify: buildApiUrl('/API/FormAPI/ModifyItem/UserRole/<id>'),
|
||||
delete: buildApiUrl('/API/FormAPI/Delete/UserRole/<id>'),
|
||||
},
|
||||
onView: (row) => {
|
||||
let url = `/pages/Company/employee-detail.html?UserUUID=${encodeURIComponent(row.UserUUID || '')}`;
|
||||
if (currentServer !== 'gvsds-master') url += `&server=${encodeURIComponent(currentServer)}`;
|
||||
GERP.Navigate(url, row.UserUUID_label || '员工详情');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** 编辑按钮:弹出编辑对话框 */
|
||||
async function handleEdit() {
|
||||
if (!currentData) return;
|
||||
CompanyUtils.showEditDialog({
|
||||
tableName: 'Company',
|
||||
data: currentData,
|
||||
onSave: () => {
|
||||
if (window.GERPUI && typeof window.GERPUI.ShowToast === 'function') {
|
||||
window.GERPUI.ShowToast({ message: '保存成功', type: 'success' });
|
||||
}
|
||||
init();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
init();
|
||||
</script>
|
||||
@@ -0,0 +1,38 @@
|
||||
<script src="/assets/gerp/CompanyCommon.js"></script>
|
||||
|
||||
<div class="GERPPage">
|
||||
<div class="cp-page">
|
||||
<div class="cp-header">
|
||||
<h1 class="PageTitle">
|
||||
<i class="fas fa-building icon-primary icon-left-lg"></i>
|
||||
<span id="page-title">公司管理</span>
|
||||
</h1>
|
||||
</div>
|
||||
<div class="cp-container" id="TableContainer"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const page = new CompanyTablePage({
|
||||
tableName: 'Company',
|
||||
title: '公司管理',
|
||||
pkField: 'CompanyUUID',
|
||||
defaultColumns: () => [
|
||||
{ field: 'CompanyUUID', title: '公司UUID', width: 140, primaryKey: true, editable: false, hideInForm: true },
|
||||
{ field: 'CompanyName', title: '公司全称', width: 200, type: 'text' },
|
||||
{ field: 'CompanyShort', title: '公司简称', width: 120, type: 'text' },
|
||||
{ field: 'CompanyStatus', title: '状态', width: 70, type: 'select', options: [{value:0,label:'禁用'},{value:1,label:'启用'}] },
|
||||
{ field: 'ParentCompanyUUID', title: '上级公司', width: 140, type: 'text' },
|
||||
{ field: 'CreateTime', title: '创建时间', width: 160, type: 'datetime', editable: false },
|
||||
],
|
||||
onView: (row) => {
|
||||
console.log('[company] onView row:', row);
|
||||
const server = new URLSearchParams(window.location.search).get('server');
|
||||
let url = `/pages/Company/company-detail.html?CompanyUUID=${encodeURIComponent(row.CompanyUUID || '')}`;
|
||||
if (server && server !== 'gvsds-master') url += `&server=${encodeURIComponent(server)}`;
|
||||
GERP.Navigate(url, row.CompanyName || '公司详情');
|
||||
},
|
||||
});
|
||||
|
||||
page.init();
|
||||
</script>
|
||||
@@ -0,0 +1,152 @@
|
||||
<script src="/assets/gerp/CompanyCommon.js?v=20260625d"></script>
|
||||
|
||||
<div class="GERPPage">
|
||||
<div class="cp-page">
|
||||
<div class="cp-header">
|
||||
<a href="javascript:void(0)" class="cp-back-btn" onclick="goBack()"><i class="fas fa-arrow-left"></i> 返回</a>
|
||||
<h1 class="PageTitle">
|
||||
<i class="fas fa-sitemap icon-primary icon-left-lg"></i>
|
||||
<span id="page-title">部门详情</span>
|
||||
</h1>
|
||||
<button class="GERPBtn Primary" onclick="handleEdit()" style="margin-left:auto;"><i class="fas fa-edit"></i> 编辑</button>
|
||||
</div>
|
||||
<div class="cp-container" id="detailContainer" style="overflow:auto;">
|
||||
<div class="cp-loading"><i class="fas fa-spinner fa-spin"></i> 加载中...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const currentServer = CompanyUtils.getServer();
|
||||
const deptUUID = new URLSearchParams(window.location.search).get('DeptUUID');
|
||||
|
||||
CompanyUtils.setPageTitle('部门详情');
|
||||
|
||||
function goBack() {
|
||||
if (window.history.length > 1) {
|
||||
window.history.back();
|
||||
} else {
|
||||
GERP.Navigate('/pages/Company/department.html' + (currentServer !== 'gvsds-master' ? `?server=${encodeURIComponent(currentServer)}` : ''), '部门管理');
|
||||
}
|
||||
}
|
||||
|
||||
let currentData = null;
|
||||
let empTable = null;
|
||||
|
||||
function buildApiUrl(path, params = {}) {
|
||||
const allParams = {};
|
||||
if (currentServer !== 'gvsds-master') allParams.server = currentServer;
|
||||
Object.assign(allParams, params);
|
||||
const qs = new URLSearchParams(allParams).toString();
|
||||
return qs ? `${path}?${qs}` : path;
|
||||
}
|
||||
|
||||
async function init() {
|
||||
const containerEl = document.getElementById('detailContainer');
|
||||
|
||||
if (!deptUUID) {
|
||||
containerEl.innerHTML = `<div class="cp-error"><i class="fas fa-exclamation-triangle" style="margin-right:6px;"></i> 缺少 DeptUUID 参数,无法加载部门详情。</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const dataResp = await fetch(buildApiUrl('/API/FormAPI/Get/Department', { DeptUUID: deptUUID, PageSize: '1' }), { credentials: 'include' });
|
||||
const dataResult = await dataResp.json();
|
||||
if (dataResult.code !== 0) {
|
||||
containerEl.innerHTML = `<div class="cp-error"><i class="fas fa-exclamation-circle" style="margin-right:6px;"></i> 加载失败: ${CompanyUtils.esc(dataResult.msg || '未知错误')}</div>`;
|
||||
return;
|
||||
}
|
||||
const list = dataResult.data || [];
|
||||
const data = Array.isArray(list) ? (list.find(d => d.DeptUUID === deptUUID) || list[0]) : list;
|
||||
if (!data) {
|
||||
containerEl.innerHTML = `<div class="cp-error">未找到部门数据</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
currentData = data;
|
||||
CompanyUtils.setPageTitle(data.DeptName || '部门详情');
|
||||
|
||||
// 解析 UUID 字段为名称
|
||||
const uuidMap = {};
|
||||
if (data.ParentDeptUUID) uuidMap.parentDept = { table: 'Department', uuids: [data.ParentDeptUUID] };
|
||||
const resolved = await CompanyUtils.resolveUUIDs(uuidMap);
|
||||
|
||||
// 渲染基本信息 + 表格容器
|
||||
containerEl.innerHTML = `
|
||||
<div class="cp-detail-section">
|
||||
<h3><i class="fas fa-sitemap"></i> 基本信息</h3>
|
||||
<div class="cp-info-grid">
|
||||
<div class="cp-info-item"><div class="label">部门名称</div><div class="value">${CompanyUtils.esc(data.DeptName) || '-'}</div></div>
|
||||
<div class="cp-info-item"><div class="label">部门状态</div><div class="value">${CompanyUtils.statusTag(data.DeptStatus)}</div></div>
|
||||
<div class="cp-info-item"><div class="label">上级部门</div><div class="value">${CompanyUtils.esc(resolved.parentDept?.[data.ParentDeptUUID] || data.ParentDeptUUID) || '-'}</div></div>
|
||||
<div class="cp-info-item"><div class="label">排序</div><div class="value">${data.DeptSort ?? '-'}</div></div>
|
||||
<div class="cp-info-item"><div class="label">创建时间</div><div class="value">${CompanyUtils.formatTime(data.DeptCreateTime)}</div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cp-detail-section">
|
||||
<h3><i class="fas fa-users"></i> 部门员工</h3>
|
||||
<div id="emp-table-container" style="height:400px;"></div>
|
||||
</div>`;
|
||||
|
||||
// 初始化 TableEngine 实例
|
||||
initEmployeeTable();
|
||||
} catch (e) {
|
||||
containerEl.innerHTML = `<div class="cp-error"><i class="fas fa-exclamation-circle" style="margin-right:6px;"></i> 网络错误: ${CompanyUtils.esc(e.message)}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
function initEmployeeTable() {
|
||||
empTable = new TableEngine({
|
||||
container: '#emp-table-container',
|
||||
height: '400px',
|
||||
tableName: 'UserRole',
|
||||
columns: [
|
||||
{ field: 'UserRoleUUID', title: '关联UUID', primaryKey: true, editable: false, hideInForm: true },
|
||||
CompanyUtils.epColumn('UserUUID', '用户名', 'User', { editable: false }),
|
||||
CompanyUtils.epColumn('RoleUUID', '角色名', 'Role'),
|
||||
{ field: 'RoleType', title: '角色类型', type: 'select', options: [{ value: 0, label: '员工' }, { value: 1, label: '负责人' }] },
|
||||
{ field: 'CreateTime', title: '创建时间', type: 'datetime', editable: false },
|
||||
],
|
||||
data: [],
|
||||
pageSize: 20,
|
||||
columnDistribution: 'equal',
|
||||
showPagination: true,
|
||||
showCheckbox: true,
|
||||
editable: true,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
striped: true,
|
||||
hoverable: true,
|
||||
bordered: true,
|
||||
api: {
|
||||
get: buildApiUrl('/API/FormAPI/Get/UserRole', { DeptUUID: deptUUID }),
|
||||
add: buildApiUrl('/API/FormAPI/Add/UserRole'),
|
||||
edit: buildApiUrl('/API/FormAPI/Modify/UserRole/<id>'),
|
||||
modify: buildApiUrl('/API/FormAPI/ModifyItem/UserRole/<id>'),
|
||||
delete: buildApiUrl('/API/FormAPI/Delete/UserRole/<id>'),
|
||||
},
|
||||
onView: (row) => {
|
||||
let url = `/pages/Company/employee-detail.html?UserUUID=${encodeURIComponent(row.UserUUID || '')}`;
|
||||
if (currentServer !== 'gvsds-master') url += `&server=${encodeURIComponent(currentServer)}`;
|
||||
GERP.Navigate(url, row.UserUUID_label || '员工详情');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** 编辑按钮:弹出编辑对话框 */
|
||||
async function handleEdit() {
|
||||
if (!currentData) return;
|
||||
CompanyUtils.showEditDialog({
|
||||
tableName: 'Department',
|
||||
data: currentData,
|
||||
onSave: () => {
|
||||
if (window.GERPUI && typeof window.GERPUI.ShowToast === 'function') {
|
||||
window.GERPUI.ShowToast({ message: '保存成功', type: 'success' });
|
||||
}
|
||||
init();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
init();
|
||||
</script>
|
||||
@@ -0,0 +1,39 @@
|
||||
<script src="/assets/gerp/CompanyCommon.js"></script>
|
||||
|
||||
<div class="GERPPage">
|
||||
<div class="cp-page">
|
||||
<div class="cp-header">
|
||||
<h1 class="PageTitle">
|
||||
<i class="fas fa-sitemap icon-primary icon-left-lg"></i>
|
||||
<span id="page-title">部门管理</span>
|
||||
</h1>
|
||||
</div>
|
||||
<div class="cp-container" id="TableContainer"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const page = new CompanyTablePage({
|
||||
tableName: 'Department',
|
||||
title: '部门管理',
|
||||
pkField: 'DeptUUID',
|
||||
defaultColumns: () => [
|
||||
{ field: 'DeptUUID', title: '部门UUID', width: 140, primaryKey: true, editable: false, hideInForm: true },
|
||||
{ field: 'DeptName', title: '部门名称', width: 160, type: 'text' },
|
||||
{ field: 'DeptSort', title: '排序', width: 80, type: 'number' },
|
||||
{ field: 'DeptStatus', title: '状态', width: 80, type: 'select', options: [{value:0,label:'禁用'},{value:1,label:'启用'}] },
|
||||
{ field: 'CompanyUUID', title: '所属公司', width: 160, type: 'text', editable: false, hideInForm: true },
|
||||
{ field: 'ParentDeptUUID', title: '上级部门', width: 160, type: 'text' },
|
||||
{ field: 'CreateTime', title: '创建时间', width: 160, type: 'datetime', editable: false },
|
||||
],
|
||||
onView: (row) => {
|
||||
console.log('[department] onView row:', row);
|
||||
const server = new URLSearchParams(window.location.search).get('server');
|
||||
let url = `/pages/Company/department-detail.html?DeptUUID=${encodeURIComponent(row.DeptUUID || '')}`;
|
||||
if (server && server !== 'gvsds-master') url += `&server=${encodeURIComponent(server)}`;
|
||||
GERP.Navigate(url, row.DeptName || '部门详情');
|
||||
},
|
||||
});
|
||||
|
||||
page.init();
|
||||
</script>
|
||||
@@ -0,0 +1,187 @@
|
||||
<script src="/assets/gerp/CompanyCommon.js?v=20260625d"></script>
|
||||
|
||||
<div class="GERPPage">
|
||||
<div class="cp-page">
|
||||
<div class="cp-header">
|
||||
<a href="javascript:void(0)" class="cp-back-btn" onclick="goBack()"><i class="fas fa-arrow-left"></i> 返回</a>
|
||||
<h1 class="PageTitle">
|
||||
<i class="fas fa-user-circle icon-primary icon-left-lg"></i>
|
||||
<span id="page-title">员工详情</span>
|
||||
</h1>
|
||||
<button class="GERPBtn Primary" onclick="handleEdit()" style="margin-left:auto;"><i class="fas fa-edit"></i> 编辑</button>
|
||||
</div>
|
||||
<div class="cp-container" id="detailContainer" style="overflow:auto;">
|
||||
<div class="cp-loading"><i class="fas fa-spinner fa-spin"></i> 加载中...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const currentServer = CompanyUtils.getServer();
|
||||
const userUUID = new URLSearchParams(window.location.search).get('UserUUID');
|
||||
|
||||
CompanyUtils.setPageTitle('员工详情');
|
||||
|
||||
function goBack() {
|
||||
if (window.history.length > 1) {
|
||||
window.history.back();
|
||||
} else {
|
||||
GERP.Navigate('/pages/system/users.html' + (currentServer !== 'gvsds-master' ? `?server=${encodeURIComponent(currentServer)}` : ''), '用户管理');
|
||||
}
|
||||
}
|
||||
|
||||
let currentData = null;
|
||||
let roleTable = null;
|
||||
let deptTable = null;
|
||||
|
||||
function buildApiUrl(path, params = {}) {
|
||||
const allParams = {};
|
||||
if (currentServer !== 'gvsds-master') allParams.server = currentServer;
|
||||
Object.assign(allParams, params);
|
||||
const qs = new URLSearchParams(allParams).toString();
|
||||
return qs ? `${path}?${qs}` : path;
|
||||
}
|
||||
|
||||
async function init() {
|
||||
const containerEl = document.getElementById('detailContainer');
|
||||
|
||||
if (!userUUID) {
|
||||
containerEl.innerHTML = `<div class="cp-error"><i class="fas fa-exclamation-triangle" style="margin-right:6px;"></i> 缺少 UserUUID 参数,无法加载员工详情。</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const dataResp = await fetch(buildApiUrl('/API/FormAPI/Get/User', { UserUUID: userUUID, PageSize: '1' }), { credentials: 'include' });
|
||||
const dataResult = await dataResp.json();
|
||||
if (dataResult.code !== 0) {
|
||||
containerEl.innerHTML = `<div class="cp-error"><i class="fas fa-exclamation-circle" style="margin-right:6px;"></i> 加载失败: ${CompanyUtils.esc(dataResult.msg || '未知错误')}</div>`;
|
||||
return;
|
||||
}
|
||||
const list = dataResult.data || [];
|
||||
const data = Array.isArray(list) ? (list.find(u => u.UserUUID === userUUID) || list[0]) : list;
|
||||
if (!data) {
|
||||
containerEl.innerHTML = `<div class="cp-error">未找到用户数据</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
currentData = data;
|
||||
CompanyUtils.setPageTitle(data.UserName || '员工详情');
|
||||
|
||||
// 渲染基本信息 + 表格容器
|
||||
containerEl.innerHTML = `
|
||||
<div class="cp-detail-section">
|
||||
<h3><i class="fas fa-user-circle"></i> 基本信息</h3>
|
||||
<div class="cp-info-grid">
|
||||
<div class="cp-info-item"><div class="label">用户名</div><div class="value">${CompanyUtils.esc(data.UserName) || '-'}</div></div>
|
||||
<div class="cp-info-item"><div class="label">真实姓名</div><div class="value">${CompanyUtils.esc(data.UserRealName) || '-'}</div></div>
|
||||
<div class="cp-info-item"><div class="label">邮箱</div><div class="value">${CompanyUtils.esc(data.UserEmail) || '-'}</div></div>
|
||||
<div class="cp-info-item"><div class="label">电话</div><div class="value">${CompanyUtils.esc(data.UserTele) || '-'}</div></div>
|
||||
<div class="cp-info-item"><div class="label">账号状态</div><div class="value">${CompanyUtils.statusTag(data.UserAccountLicense)}</div></div>
|
||||
<div class="cp-info-item"><div class="label">是否员工</div><div class="value">${data.IsStaff ? '<span style="color:#166534;">是</span>' : '<span style="color:#991b1b;">否</span>'}</div></div>
|
||||
<div class="cp-info-item"><div class="label">创建时间</div><div class="value">${CompanyUtils.formatTime(data.UserCreateTime)}</div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cp-detail-section">
|
||||
<h3><i class="fas fa-shield-alt"></i> 角色与组织</h3>
|
||||
<div id="role-table-container" style="height:400px;"></div>
|
||||
</div>
|
||||
<div class="cp-detail-section">
|
||||
<h3><i class="fas fa-sitemap"></i> 部门关联</h3>
|
||||
<div id="dept-table-container" style="height:400px;"></div>
|
||||
</div>`;
|
||||
|
||||
// 初始化 TableEngine 实例
|
||||
initRoleTable();
|
||||
initDeptTable();
|
||||
} catch (e) {
|
||||
containerEl.innerHTML = `<div class="cp-error"><i class="fas fa-exclamation-circle" style="margin-right:6px;"></i> 网络错误: ${CompanyUtils.esc(e.message)}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
function initRoleTable() {
|
||||
roleTable = new TableEngine({
|
||||
container: '#role-table-container',
|
||||
height: '400px',
|
||||
tableName: 'UserRole',
|
||||
columns: [
|
||||
{ field: 'UserRoleUUID', title: '角色UUID', primaryKey: true, editable: false, hideInForm: true },
|
||||
CompanyUtils.epColumn('RoleUUID', '角色名', 'Role'),
|
||||
CompanyUtils.epColumn('CompanyUUID', '公司', 'Company', { editable: false, hideInForm: true }),
|
||||
CompanyUtils.epColumn('DeptUUID', '部门', 'Department'),
|
||||
CompanyUtils.epColumn('WorkgroupUUID', '工作组', 'Workgroup'),
|
||||
{ field: 'RoleType', title: '角色类型', type: 'select', options: [{ value: 0, label: '组员' }, { value: 1, label: '组长' }] },
|
||||
{ field: 'RoleUUID_RoleStatus', title: '角色状态', type: 'select', options: [{ value: 0, label: '禁用' }, { value: 1, label: '启用' }], editable: false },
|
||||
],
|
||||
data: [],
|
||||
pageSize: 20,
|
||||
columnDistribution: 'equal',
|
||||
showPagination: true,
|
||||
showCheckbox: true,
|
||||
editable: true,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
striped: true,
|
||||
hoverable: true,
|
||||
bordered: true,
|
||||
api: {
|
||||
get: buildApiUrl('/API/FormAPI/Get/UserRole', { UserUUID: userUUID }),
|
||||
add: buildApiUrl('/API/FormAPI/Add/UserRole'),
|
||||
edit: buildApiUrl('/API/FormAPI/Modify/UserRole/<id>'),
|
||||
modify: buildApiUrl('/API/FormAPI/ModifyItem/UserRole/<id>'),
|
||||
delete: buildApiUrl('/API/FormAPI/Delete/UserRole/<id>'),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function initDeptTable() {
|
||||
deptTable = new TableEngine({
|
||||
container: '#dept-table-container',
|
||||
height: '400px',
|
||||
tableName: 'UserDept',
|
||||
columns: [
|
||||
{ field: 'UserDeptUUID', title: '关联UUID', primaryKey: true, editable: false, hideInForm: true },
|
||||
CompanyUtils.epColumn('DeptUUID', '部门名称', 'Department'),
|
||||
{ field: 'IsMainDept', title: '主部门', type: 'select', options: [{ value: 0, label: '兼职' }, { value: 1, label: '主部门' }] },
|
||||
],
|
||||
data: [],
|
||||
pageSize: 20,
|
||||
columnDistribution: 'equal',
|
||||
showCheckbox: true,
|
||||
editable: true,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
striped: true,
|
||||
hoverable: true,
|
||||
bordered: true,
|
||||
api: {
|
||||
get: buildApiUrl('/API/FormAPI/Get/UserDept', { UserUUID: userUUID }),
|
||||
add: buildApiUrl('/API/FormAPI/Add/UserDept'),
|
||||
edit: buildApiUrl('/API/FormAPI/Modify/UserDept/<id>'),
|
||||
modify: buildApiUrl('/API/FormAPI/ModifyItem/UserDept/<id>'),
|
||||
delete: buildApiUrl('/API/FormAPI/Delete/UserDept/<id>'),
|
||||
},
|
||||
onView: (row) => {
|
||||
let url = `/pages/Company/department-detail.html?DeptUUID=${encodeURIComponent(row.DeptUUID || '')}`;
|
||||
if (currentServer !== 'gvsds-master') url += `&server=${encodeURIComponent(currentServer)}`;
|
||||
GERP.Navigate(url, row.DeptUUID_label || '部门详情');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** 编辑按钮:弹出编辑对话框 */
|
||||
async function handleEdit() {
|
||||
if (!currentData) return;
|
||||
CompanyUtils.showEditDialog({
|
||||
tableName: 'User',
|
||||
data: currentData,
|
||||
onSave: () => {
|
||||
if (window.GERPUI && typeof window.GERPUI.ShowToast === 'function') {
|
||||
window.GERPUI.ShowToast({ message: '保存成功', type: 'success' });
|
||||
}
|
||||
init();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
init();
|
||||
</script>
|
||||
@@ -0,0 +1,145 @@
|
||||
<script src="/assets/gerp/CompanyCommon.js?v=20260625d"></script>
|
||||
|
||||
<div class="GERPPage">
|
||||
<div class="cp-page">
|
||||
<div class="cp-header">
|
||||
<a href="javascript:void(0)" class="cp-back-btn" onclick="goBack()"><i class="fas fa-arrow-left"></i> 返回</a>
|
||||
<h1 class="PageTitle">
|
||||
<i class="fas fa-users-cog icon-primary icon-left-lg"></i>
|
||||
<span id="page-title">工作组详情</span>
|
||||
</h1>
|
||||
<button class="GERPBtn Primary" onclick="handleEdit()" style="margin-left:auto;"><i class="fas fa-edit"></i> 编辑</button>
|
||||
</div>
|
||||
<div class="cp-container" id="detailContainer" style="overflow:auto;">
|
||||
<div class="cp-loading"><i class="fas fa-spinner fa-spin"></i> 加载中...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const currentServer = CompanyUtils.getServer();
|
||||
const workgroupUUID = new URLSearchParams(window.location.search).get('WorkgroupUUID');
|
||||
|
||||
CompanyUtils.setPageTitle('工作组详情');
|
||||
|
||||
function goBack() {
|
||||
if (window.history.length > 1) {
|
||||
window.history.back();
|
||||
} else {
|
||||
GERP.Navigate('/pages/Company/workgroup.html' + (currentServer !== 'gvsds-master' ? `?server=${encodeURIComponent(currentServer)}` : ''), '工作组管理');
|
||||
}
|
||||
}
|
||||
|
||||
let currentData = null;
|
||||
let memberTable = null;
|
||||
|
||||
function buildApiUrl(path, params = {}) {
|
||||
const allParams = {};
|
||||
if (currentServer !== 'gvsds-master') allParams.server = currentServer;
|
||||
Object.assign(allParams, params);
|
||||
const qs = new URLSearchParams(allParams).toString();
|
||||
return qs ? `${path}?${qs}` : path;
|
||||
}
|
||||
|
||||
async function init() {
|
||||
const containerEl = document.getElementById('detailContainer');
|
||||
|
||||
if (!workgroupUUID) {
|
||||
containerEl.innerHTML = `<div class="cp-error"><i class="fas fa-exclamation-triangle" style="margin-right:6px;"></i> 缺少 WorkgroupUUID 参数,无法加载工作组详情。</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const dataResp = await fetch(buildApiUrl('/API/FormAPI/Get/Workgroup', { WorkgroupUUID: workgroupUUID, PageSize: '1' }), { credentials: 'include' });
|
||||
const dataResult = await dataResp.json();
|
||||
if (dataResult.code !== 0) {
|
||||
containerEl.innerHTML = `<div class="cp-error"><i class="fas fa-exclamation-circle" style="margin-right:6px;"></i> 加载失败: ${CompanyUtils.esc(dataResult.msg || '未知错误')}</div>`;
|
||||
return;
|
||||
}
|
||||
const list = dataResult.data || [];
|
||||
const data = Array.isArray(list) ? (list.find(w => w.WorkgroupUUID === workgroupUUID) || list[0]) : list;
|
||||
if (!data) {
|
||||
containerEl.innerHTML = `<div class="cp-error">未找到工作组数据</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
currentData = data;
|
||||
CompanyUtils.setPageTitle(data.WorkgroupName || '工作组详情');
|
||||
|
||||
// 渲染基本信息 + 表格容器
|
||||
containerEl.innerHTML = `
|
||||
<div class="cp-detail-section">
|
||||
<h3><i class="fas fa-users-cog"></i> 基本信息</h3>
|
||||
<div class="cp-info-grid">
|
||||
<div class="cp-info-item"><div class="label">工作组名称</div><div class="value">${CompanyUtils.esc(data.WorkgroupName) || '-'}</div></div>
|
||||
<div class="cp-info-item"><div class="label">工作组状态</div><div class="value">${CompanyUtils.statusTag(data.WorkgroupStatus)}</div></div>
|
||||
<div class="cp-info-item"><div class="label">创建时间</div><div class="value">${CompanyUtils.formatTime(data.WorkgroupCreateTime)}</div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cp-detail-section">
|
||||
<h3><i class="fas fa-user-friends"></i> 工作组成员</h3>
|
||||
<div id="member-table-container" style="height:400px;"></div>
|
||||
</div>`;
|
||||
|
||||
// 初始化 TableEngine 实例
|
||||
initMemberTable();
|
||||
} catch (e) {
|
||||
containerEl.innerHTML = `<div class="cp-error"><i class="fas fa-exclamation-circle" style="margin-right:6px;"></i> 网络错误: ${CompanyUtils.esc(e.message)}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
function initMemberTable() {
|
||||
memberTable = new TableEngine({
|
||||
container: '#member-table-container',
|
||||
height: '400px',
|
||||
tableName: 'UserRole',
|
||||
columns: [
|
||||
{ field: 'UserRoleUUID', title: '关联UUID', primaryKey: true, editable: false, hideInForm: true },
|
||||
CompanyUtils.epColumn('UserUUID', '用户名', 'User', { editable: false }),
|
||||
CompanyUtils.epColumn('RoleUUID', '角色名', 'Role'),
|
||||
{ field: 'RoleType', title: '角色类型', type: 'select', options: [{ value: 0, label: '组员' }, { value: 1, label: '组长' }] },
|
||||
{ field: 'CreateTime', title: '创建时间', type: 'datetime', editable: false },
|
||||
],
|
||||
data: [],
|
||||
pageSize: 20,
|
||||
columnDistribution: 'equal',
|
||||
showPagination: true,
|
||||
showCheckbox: true,
|
||||
editable: true,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
striped: true,
|
||||
hoverable: true,
|
||||
bordered: true,
|
||||
api: {
|
||||
get: buildApiUrl('/API/FormAPI/Get/UserRole', { WorkgroupUUID: workgroupUUID }),
|
||||
add: buildApiUrl('/API/FormAPI/Add/UserRole'),
|
||||
edit: buildApiUrl('/API/FormAPI/Modify/UserRole/<id>'),
|
||||
modify: buildApiUrl('/API/FormAPI/ModifyItem/UserRole/<id>'),
|
||||
delete: buildApiUrl('/API/FormAPI/Delete/UserRole/<id>'),
|
||||
},
|
||||
onView: (row) => {
|
||||
let url = `/pages/Company/employee-detail.html?UserUUID=${encodeURIComponent(row.UserUUID || '')}`;
|
||||
if (currentServer !== 'gvsds-master') url += `&server=${encodeURIComponent(currentServer)}`;
|
||||
GERP.Navigate(url, row.UserUUID_label || '员工详情');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** 编辑按钮:弹出编辑对话框 */
|
||||
async function handleEdit() {
|
||||
if (!currentData) return;
|
||||
CompanyUtils.showEditDialog({
|
||||
tableName: 'Workgroup',
|
||||
data: currentData,
|
||||
onSave: () => {
|
||||
if (window.GERPUI && typeof window.GERPUI.ShowToast === 'function') {
|
||||
window.GERPUI.ShowToast({ message: '保存成功', type: 'success' });
|
||||
}
|
||||
init();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
init();
|
||||
</script>
|
||||
@@ -0,0 +1,35 @@
|
||||
<script src="/assets/gerp/CompanyCommon.js"></script>
|
||||
|
||||
<div class="GERPPage">
|
||||
<div class="cp-page">
|
||||
<div class="cp-header">
|
||||
<h1 class="PageTitle">
|
||||
<i class="fas fa-users-cog icon-primary icon-left-lg"></i>
|
||||
<span id="page-title">工作组管理</span>
|
||||
</h1>
|
||||
</div>
|
||||
<div class="cp-container" id="TableContainer"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const page = new CompanyTablePage({
|
||||
tableName: 'Workgroup',
|
||||
title: '工作组管理',
|
||||
defaultColumns: () => [
|
||||
{ field: 'WorkgroupUUID', title: '工作组UUID', width: 140, primaryKey: true, editable: false, hideInForm: true },
|
||||
{ field: 'WorkgroupName', title: '工作组名称', width: 160, type: 'text' },
|
||||
{ field: 'WorkgroupEnglishName', title: '英文名称', width: 160, type: 'text' },
|
||||
{ field: 'WorkgroupIntroduction', title: '工作组介绍', width: 240, type: 'textarea' },
|
||||
],
|
||||
onView: (row) => {
|
||||
console.log('[workgroup] onView row:', row);
|
||||
const server = new URLSearchParams(window.location.search).get('server');
|
||||
let url = `/pages/Company/workgroup-detail.html?WorkgroupUUID=${encodeURIComponent(row.WorkgroupUUID || '')}`;
|
||||
if (server && server !== 'gvsds-master') url += `&server=${encodeURIComponent(server)}`;
|
||||
GERP.Navigate(url, row.WorkgroupName || '工作组详情');
|
||||
},
|
||||
});
|
||||
|
||||
page.init();
|
||||
</script>
|
||||
@@ -0,0 +1,261 @@
|
||||
<script src="/assets/gerp/CompanyCommon.js?v=20260625d"></script>
|
||||
|
||||
<div class="GERPPage">
|
||||
<div class="cp-page">
|
||||
<div class="cp-header">
|
||||
<a href="javascript:void(0)" class="cp-back-btn" onclick="goBack()"><i class="fas fa-arrow-left"></i> 返回</a>
|
||||
<h1 class="PageTitle">
|
||||
<i class="fas fa-user-tag icon-primary icon-left-lg"></i>
|
||||
<span id="page-title">角色详情</span>
|
||||
</h1>
|
||||
<span id="admin-badge" style="display:none;margin-left:8px;padding:2px 10px;border-radius:4px;background:#1e40af;color:#fff;font-size:12px;font-weight:600;"><i class="fas fa-shield-alt"></i> 特权模式</span>
|
||||
<button class="GERPBtn Primary" onclick="handleEdit()" style="margin-left:auto;"><i class="fas fa-edit"></i> 编辑</button>
|
||||
</div>
|
||||
<div class="cp-container" id="detailContainer" style="overflow:auto;">
|
||||
<div class="cp-loading"><i class="fas fa-spinner fa-spin"></i> 加载中...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const currentServer = CompanyUtils.getServer();
|
||||
const roleUUID = new URLSearchParams(window.location.search).get('RoleUUID');
|
||||
|
||||
CompanyUtils.setPageTitle('角色详情');
|
||||
|
||||
function goBack() {
|
||||
if (window.history.length > 1) {
|
||||
window.history.back();
|
||||
} else {
|
||||
GERP.Navigate('/pages/system/roles.html' + (currentServer !== 'gvsds-master' ? `?server=${encodeURIComponent(currentServer)}` : ''), '角色管理');
|
||||
}
|
||||
}
|
||||
|
||||
let currentData = null;
|
||||
let userRoleTable = null;
|
||||
let permTable = null;
|
||||
let adminTable = null;
|
||||
let isAdminMode = false;
|
||||
|
||||
/** 检查当前用户是否具有管理员特权 */
|
||||
async function checkAdmin() {
|
||||
try {
|
||||
const resp = await fetch(buildApiUrl('/API/FormAPI/CheckAdmin'), { credentials: 'include' });
|
||||
const result = await resp.json();
|
||||
return result.code === 0 && result.data && result.data.is_admin;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function buildApiUrl(path, params = {}) {
|
||||
const allParams = {};
|
||||
if (currentServer !== 'gvsds-master') allParams.server = currentServer;
|
||||
if (isAdminMode) allParams.admin_verify = '1';
|
||||
Object.assign(allParams, params);
|
||||
const qs = new URLSearchParams(allParams).toString();
|
||||
return qs ? `${path}?${qs}` : path;
|
||||
}
|
||||
|
||||
function roleTypeText(t) {
|
||||
if (t === 0 || t === '0') return '组员';
|
||||
if (t === 1 || t === '1') return '组长';
|
||||
return '-';
|
||||
}
|
||||
|
||||
function assignScopeText(s) {
|
||||
if (s === 0 || s === '0') return '<span style="color:#166534;">全租户可分发</span>';
|
||||
if (s === 1 || s === '1') return '<span style="color:#1e40af;">指定公司可分发</span>';
|
||||
if (s === 2 || s === '2') return '<span style="color:#6d28d9;">指定部门可分发</span>';
|
||||
return '-';
|
||||
}
|
||||
|
||||
async function init() {
|
||||
const containerEl = document.getElementById('detailContainer');
|
||||
|
||||
if (!roleUUID) {
|
||||
containerEl.innerHTML = `<div class="cp-error"><i class="fas fa-exclamation-triangle" style="margin-right:6px;"></i> 缺少 RoleUUID 参数,无法加载角色详情。</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
// 先检查管理员特权
|
||||
isAdminMode = await checkAdmin();
|
||||
const adminBadge = document.getElementById('admin-badge');
|
||||
if (isAdminMode) {
|
||||
adminBadge.style.display = 'inline-block';
|
||||
}
|
||||
|
||||
try {
|
||||
const dataResp = await fetch(buildApiUrl('/API/FormAPI/Get/Role', { RoleUUID: roleUUID, PageSize: '1' }), { credentials: 'include' });
|
||||
const dataResult = await dataResp.json();
|
||||
if (dataResult.code !== 0) {
|
||||
containerEl.innerHTML = `<div class="cp-error"><i class="fas fa-exclamation-circle" style="margin-right:6px;"></i> 加载失败: ${CompanyUtils.esc(dataResult.msg || '未知错误')}</div>`;
|
||||
return;
|
||||
}
|
||||
const list = dataResult.data || [];
|
||||
const data = Array.isArray(list) ? (list.find(r => r.RoleUUID === roleUUID) || list[0]) : list;
|
||||
if (!data) {
|
||||
containerEl.innerHTML = `<div class="cp-error">未找到角色数据</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
currentData = data;
|
||||
CompanyUtils.setPageTitle(data.RoleName || '角色详情');
|
||||
|
||||
const isolationNotice = isAdminMode
|
||||
? `<div style="margin-bottom:12px;padding:8px 12px;background:#eff6ff;border:1px solid #bfdbfe;border-radius:4px;color:#1e40af;font-size:13px;"><i class="fas fa-info-circle"></i> 特权模式:显示该角色在所有公司的关联记录(无公司隔离)</div>`
|
||||
: `<div style="margin-bottom:12px;padding:8px 12px;background:#f9fafb;border:1px solid #e5e7eb;border-radius:4px;color:#6b7280;font-size:13px;"><i class="fas fa-info-circle"></i> 仅显示当前公司数据。如需查看所有公司记录,需管理员特权。</div>`;
|
||||
|
||||
containerEl.innerHTML = `
|
||||
<div class="cp-detail-section">
|
||||
<h3><i class="fas fa-user-tag"></i> 基本信息</h3>
|
||||
<div class="cp-info-grid">
|
||||
<div class="cp-info-item"><div class="label">角色名称</div><div class="value">${CompanyUtils.esc(data.RoleName) || '-'}</div></div>
|
||||
<div class="cp-info-item"><div class="label">角色类型</div><div class="value">${roleTypeText(data.RoleType)}</div></div>
|
||||
<div class="cp-info-item"><div class="label">角色状态</div><div class="value">${CompanyUtils.statusTag(data.RoleStatus)}</div></div>
|
||||
<div class="cp-info-item"><div class="label">分配范围</div><div class="value">${assignScopeText(data.AssignScope)}</div></div>
|
||||
<div class="cp-info-item"><div class="label">角色描述</div><div class="value">${CompanyUtils.esc(data.RoleDesc) || '-'}</div></div>
|
||||
<div class="cp-info-item"><div class="label">创建时间</div><div class="value">${CompanyUtils.formatTime(data.CreateTime)}</div></div>
|
||||
</div>
|
||||
</div>
|
||||
${isolationNotice}
|
||||
<div class="cp-detail-section">
|
||||
<h3><i class="fas fa-users"></i> 关联用户</h3>
|
||||
<div id="userrole-table-container" style="height:400px;"></div>
|
||||
</div>
|
||||
<div class="cp-detail-section">
|
||||
<h3><i class="fas fa-key"></i> 角色权限</h3>
|
||||
<div id="perm-table-container" style="height:400px;"></div>
|
||||
<div style="margin-top:8px;font-size:12px;color:#6b7280;"><i class="fas fa-info-circle"></i> 添加 <code>admin:super</code> 权限可使该角色成为超级管理员</div>
|
||||
</div>`;
|
||||
|
||||
initUserRoleTable();
|
||||
initPermTable();
|
||||
initAdminTable();
|
||||
} catch (e) {
|
||||
containerEl.innerHTML = `<div class="cp-error"><i class="fas fa-exclamation-circle" style="margin-right:6px;"></i> 网络错误: ${CompanyUtils.esc(e.message)}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
function initUserRoleTable() {
|
||||
userRoleTable = new TableEngine({
|
||||
container: '#userrole-table-container',
|
||||
height: '400px',
|
||||
tableName: 'UserRole',
|
||||
columns: [
|
||||
{ field: 'UserRoleUUID', title: '关联UUID', primaryKey: true, editable: false, hideInForm: true },
|
||||
CompanyUtils.epColumn('UserUUID', '用户名', 'User'),
|
||||
CompanyUtils.epColumn('CompanyUUID', '公司', 'Company', { editable: false }),
|
||||
CompanyUtils.epColumn('DeptUUID', '部门', 'Department'),
|
||||
CompanyUtils.epColumn('WorkgroupUUID', '工作组', 'Workgroup'),
|
||||
{ field: 'RoleType', title: '角色类型', type: 'select', options: [{ value: 0, label: '组员' }, { value: 1, label: '组长' }] },
|
||||
{ field: 'CreateTime', title: '创建时间', type: 'datetime', editable: false },
|
||||
],
|
||||
data: [],
|
||||
pageSize: 20,
|
||||
columnDistribution: 'equal',
|
||||
showPagination: true,
|
||||
showCheckbox: true,
|
||||
editable: true,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
striped: true,
|
||||
hoverable: true,
|
||||
bordered: true,
|
||||
api: {
|
||||
get: buildApiUrl('/API/FormAPI/Get/UserRole', { RoleUUID: roleUUID }),
|
||||
add: buildApiUrl('/API/FormAPI/Add/UserRole'),
|
||||
edit: buildApiUrl('/API/FormAPI/Modify/UserRole/<id>'),
|
||||
modify: buildApiUrl('/API/FormAPI/ModifyItem/UserRole/<id>'),
|
||||
delete: buildApiUrl('/API/FormAPI/Delete/UserRole/<id>'),
|
||||
},
|
||||
onView: (row) => {
|
||||
let url = `/pages/system/user-detail.html?UserUUID=${encodeURIComponent(row.UserUUID || '')}`;
|
||||
if (currentServer !== 'gvsds-master') url += `&server=${encodeURIComponent(currentServer)}`;
|
||||
GERP.Navigate(url, row.UserUUID_label || '用户详情');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function initPermTable() {
|
||||
permTable = new TableEngine({
|
||||
container: '#perm-table-container',
|
||||
height: '400px',
|
||||
tableName: 'RolePermission',
|
||||
columns: [
|
||||
{ field: 'RolePermUUID', title: '关联UUID', primaryKey: true, editable: false, hideInForm: true },
|
||||
CompanyUtils.epColumn('PermUUID', '权限名称', 'Permission'),
|
||||
CompanyUtils.epColumn('CompanyUUID', '公司', 'Company', { editable: false, hideInForm: true }),
|
||||
{ field: 'CreateTime', title: '创建时间', type: 'datetime', editable: false },
|
||||
],
|
||||
data: [],
|
||||
pageSize: 20,
|
||||
columnDistribution: 'equal',
|
||||
showPagination: true,
|
||||
showCheckbox: true,
|
||||
editable: true,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
striped: true,
|
||||
hoverable: true,
|
||||
bordered: true,
|
||||
api: {
|
||||
get: buildApiUrl('/API/FormAPI/Get/RolePermission', { RoleUUID: roleUUID }),
|
||||
add: buildApiUrl('/API/FormAPI/Add/RolePermission'),
|
||||
edit: buildApiUrl('/API/FormAPI/Modify/RolePermission/<id>'),
|
||||
modify: buildApiUrl('/API/FormAPI/ModifyItem/RolePermission/<id>'),
|
||||
delete: buildApiUrl('/API/FormAPI/Delete/RolePermission/<id>'),
|
||||
},
|
||||
onView: (row) => {
|
||||
// 权限没有独立详情页,暂不跳转
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function initAdminTable() {
|
||||
adminTable = new TableEngine({
|
||||
container: '#admin-table-container',
|
||||
height: '200px',
|
||||
tableName: 'RoleAdmin',
|
||||
columns: [
|
||||
{ field: 'RoleAdminUUID', title: '主键UUID', primaryKey: true, editable: false, hideInForm: true },
|
||||
{ field: 'RoleUUID', title: '角色UUID', editable: false, hideInForm: true },
|
||||
{ field: 'CreateTime', title: '创建时间', type: 'datetime', editable: false },
|
||||
],
|
||||
data: [],
|
||||
pageSize: 20,
|
||||
columnDistribution: 'equal',
|
||||
showPagination: true,
|
||||
showCheckbox: true,
|
||||
editable: true,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
striped: true,
|
||||
hoverable: true,
|
||||
bordered: true,
|
||||
api: {
|
||||
get: buildApiUrl('/API/FormAPI/Get/RoleAdmin', { RoleUUID: roleUUID }),
|
||||
add: buildApiUrl('/API/FormAPI/Add/RoleAdmin'),
|
||||
edit: buildApiUrl('/API/FormAPI/Modify/RoleAdmin/<id>'),
|
||||
modify: buildApiUrl('/API/FormAPI/ModifyItem/RoleAdmin/<id>'),
|
||||
delete: buildApiUrl('/API/FormAPI/Delete/RoleAdmin/<id>'),
|
||||
},
|
||||
onView: (row) => {
|
||||
// 无需跳转
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** 编辑按钮:弹出编辑对话框 */
|
||||
async function handleEdit() {
|
||||
if (!currentData) return;
|
||||
CompanyUtils.showEditDialog({
|
||||
tableName: 'Role',
|
||||
data: currentData,
|
||||
extraParams: isAdminMode ? { admin_verify: '1' } : {},
|
||||
onSave: () => init()
|
||||
});
|
||||
}
|
||||
|
||||
init();
|
||||
</script>
|
||||
@@ -0,0 +1,118 @@
|
||||
<style>
|
||||
.GERPPage { padding: 24px; max-width: 1400px; margin: 0 auto; height: 100%; display: flex; flex-direction: column; box-sizing: border-box; }
|
||||
.role-page { padding: 20px; flex: 1; min-height: 0; display: flex; flex-direction: column; box-sizing: border-box; }
|
||||
.role-header { display: flex; align-items: center; margin-bottom: 16px; gap: 12px; flex-shrink: 0; }
|
||||
.role-header h1 { margin: 0; flex: 1; }
|
||||
.role-container { flex: 1; min-height: 0; overflow: hidden; display: flex; flex-direction: column; }
|
||||
</style>
|
||||
|
||||
<div class="GERPPage">
|
||||
<div class="role-page">
|
||||
<div class="role-header">
|
||||
<h1 class="PageTitle">
|
||||
<i class="fas fa-user-tag icon-primary icon-left-lg"></i>
|
||||
<span id="page-title">角色管理</span>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div class="role-container" id="TableContainer"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const currentServer = urlParams.get('server') || 'gvsds-master';
|
||||
let _tableInstance = null;
|
||||
let _isAdminMode = false;
|
||||
|
||||
GERP.SetTitle(currentServer === 'gvsds-master' ? '角色管理' : `${currentServer} 角色管理`);
|
||||
document.getElementById('page-title').textContent = currentServer === 'gvsds-master' ? '角色管理' : `${currentServer} - 角色管理`;
|
||||
|
||||
function buildApiUrl(path, params = {}) {
|
||||
const allParams = {};
|
||||
if (currentServer !== 'gvsds-master') allParams.server = currentServer;
|
||||
if (_isAdminMode) allParams.admin_verify = '1';
|
||||
Object.assign(allParams, params);
|
||||
const qs = new URLSearchParams(allParams).toString();
|
||||
return qs ? `${path}?${qs}` : path;
|
||||
}
|
||||
|
||||
async function checkAdmin() {
|
||||
try {
|
||||
const resp = await fetch(buildApiUrl('/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 function initTable() {
|
||||
const containerEl = document.getElementById('TableContainer');
|
||||
|
||||
// 先检查管理员特权
|
||||
_isAdminMode = await checkAdmin();
|
||||
|
||||
try {
|
||||
let columns;
|
||||
if (currentServer === 'gvsds-master') {
|
||||
const columnsResp = await fetch(buildApiUrl('/API/FormAPI/Columns/Role'), { credentials: 'include' });
|
||||
const columnsResult = await columnsResp.json();
|
||||
if (columnsResult.code !== 0) {
|
||||
containerEl.innerHTML = `<div style="color:var(--DangerColor);padding:40px;text-align:center;">加载列配置失败: ${columnsResult.msg || ''}</div>`;
|
||||
return;
|
||||
}
|
||||
columns = columnsResult.data || [];
|
||||
} else {
|
||||
columns = getDefaultColumns();
|
||||
}
|
||||
|
||||
_tableInstance = new TableEngine({
|
||||
container: '#TableContainer',
|
||||
tableName: 'Role',
|
||||
columns: columns,
|
||||
data: [],
|
||||
pageSize: 20,
|
||||
showPagination: true,
|
||||
showCheckbox: true,
|
||||
editable: true,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
striped: true,
|
||||
hoverable: true,
|
||||
bordered: true,
|
||||
columnDistribution: 'equal',
|
||||
api: {
|
||||
get: buildApiUrl('/API/FormAPI/Get/Role'),
|
||||
add: buildApiUrl('/API/FormAPI/Add/Role'),
|
||||
edit: buildApiUrl('/API/FormAPI/Modify/Role/<id>'),
|
||||
modify: buildApiUrl('/API/FormAPI/ModifyItem/Role/<id>'),
|
||||
delete: buildApiUrl('/API/FormAPI/Delete/Role/<id>'),
|
||||
columns: buildApiUrl('/API/FormAPI/Columns/Role'),
|
||||
},
|
||||
onView: (row) => {
|
||||
let url = `/pages/system/role-detail.html?RoleUUID=${encodeURIComponent(row.RoleUUID || '')}`;
|
||||
if (currentServer !== 'gvsds-master') url += `&server=${encodeURIComponent(currentServer)}`;
|
||||
GERP.Navigate(url, row.RoleName || '角色详情');
|
||||
},
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('initTable error:', error);
|
||||
containerEl.innerHTML = `<div style="color:var(--DangerColor);padding:40px;text-align:center;">初始化失败: ${error.message}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
function getDefaultColumns() {
|
||||
return [
|
||||
{ field: 'RoleUUID', title: '角色UUID', primaryKey: true, editable: false, hideInForm: true },
|
||||
{ field: 'RoleName', title: '角色名称', type: 'text' },
|
||||
{ field: 'RoleType', title: '角色类型', type: 'select', options: [{value:0,label:'组员'},{value:1,label:'组长'},{value:2,label:'其他'}] },
|
||||
{ field: 'RoleStatus', title: '状态', type: 'select', options: [{value:0,label:'禁用'},{value:1,label:'启用'}] },
|
||||
{ field: 'RoleDesc', title: '描述', type: 'text' },
|
||||
{ field: 'CreateTime', title: '创建时间', type: 'datetime', editable: false },
|
||||
];
|
||||
}
|
||||
|
||||
initTable();
|
||||
</script>
|
||||
@@ -0,0 +1,227 @@
|
||||
<script src="/assets/gerp/CompanyCommon.js?v=20260625d"></script>
|
||||
|
||||
<div class="GERPPage">
|
||||
<div class="cp-page">
|
||||
<div class="cp-header">
|
||||
<a href="javascript:void(0)" class="cp-back-btn" onclick="goBack()"><i class="fas fa-arrow-left"></i> 返回</a>
|
||||
<h1 class="PageTitle">
|
||||
<i class="fas fa-user-circle icon-primary icon-left-lg"></i>
|
||||
<span id="page-title">用户详情</span>
|
||||
</h1>
|
||||
<span id="admin-badge" style="display:none;margin-left:8px;padding:2px 10px;border-radius:4px;background:#1e40af;color:#fff;font-size:12px;font-weight:600;"><i class="fas fa-shield-alt"></i> 特权模式</span>
|
||||
<button class="GERPBtn Primary" onclick="handleEdit()" style="margin-left:auto;"><i class="fas fa-edit"></i> 编辑</button>
|
||||
</div>
|
||||
<div class="cp-container" id="detailContainer" style="overflow:auto;">
|
||||
<div class="cp-loading"><i class="fas fa-spinner fa-spin"></i> 加载中...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const currentServer = CompanyUtils.getServer();
|
||||
const userUUID = new URLSearchParams(window.location.search).get('UserUUID');
|
||||
|
||||
CompanyUtils.setPageTitle('用户详情');
|
||||
|
||||
function goBack() {
|
||||
if (window.history.length > 1) {
|
||||
window.history.back();
|
||||
} else {
|
||||
GERP.Navigate('/pages/system/users.html' + (currentServer !== 'gvsds-master' ? `?server=${encodeURIComponent(currentServer)}` : ''), '用户管理');
|
||||
}
|
||||
}
|
||||
|
||||
let currentData = null;
|
||||
let roleTable = null;
|
||||
let deptTable = null;
|
||||
let isAdminMode = false;
|
||||
|
||||
/** 检查当前用户是否具有管理员特权 */
|
||||
async function checkAdmin() {
|
||||
try {
|
||||
const resp = await fetch(buildApiUrl('/API/FormAPI/CheckAdmin'), { credentials: 'include' });
|
||||
const result = await resp.json();
|
||||
return result.code === 0 && result.data && result.data.is_admin;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function buildApiUrl(path, params = {}) {
|
||||
const allParams = {};
|
||||
if (currentServer !== 'gvsds-master') allParams.server = currentServer;
|
||||
if (isAdminMode) allParams.admin_verify = '1';
|
||||
Object.assign(allParams, params);
|
||||
const qs = new URLSearchParams(allParams).toString();
|
||||
return qs ? `${path}?${qs}` : path;
|
||||
}
|
||||
|
||||
function genderText(g) {
|
||||
if (g === 1 || g === '1') return '男';
|
||||
if (g === 0 || g === '0') return '女';
|
||||
return '-';
|
||||
}
|
||||
|
||||
function yesNoText(val) {
|
||||
return val ? '<span style="color:#166534;">是</span>' : '<span style="color:#991b1b;">否</span>';
|
||||
}
|
||||
|
||||
async function init() {
|
||||
const containerEl = document.getElementById('detailContainer');
|
||||
|
||||
if (!userUUID) {
|
||||
containerEl.innerHTML = `<div class="cp-error"><i class="fas fa-exclamation-triangle" style="margin-right:6px;"></i> 缺少 UserUUID 参数,无法加载用户详情。</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
// 先检查管理员特权,决定是否启用无隔离模式
|
||||
isAdminMode = await checkAdmin();
|
||||
const adminBadge = document.getElementById('admin-badge');
|
||||
if (isAdminMode) {
|
||||
adminBadge.style.display = 'inline-block';
|
||||
}
|
||||
|
||||
try {
|
||||
const dataResp = await fetch(buildApiUrl('/API/FormAPI/Get/User', { UserUUID: userUUID, PageSize: '1' }), { credentials: 'include' });
|
||||
const dataResult = await dataResp.json();
|
||||
if (dataResult.code !== 0) {
|
||||
containerEl.innerHTML = `<div class="cp-error"><i class="fas fa-exclamation-circle" style="margin-right:6px;"></i> 加载失败: ${CompanyUtils.esc(dataResult.msg || '未知错误')}</div>`;
|
||||
return;
|
||||
}
|
||||
const list = dataResult.data || [];
|
||||
const data = Array.isArray(list) ? (list.find(u => u.UserUUID === userUUID) || list[0]) : list;
|
||||
if (!data) {
|
||||
containerEl.innerHTML = `<div class="cp-error">未找到用户数据</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
currentData = data;
|
||||
CompanyUtils.setPageTitle(data.UserName || '用户详情');
|
||||
|
||||
// 渲染基本信息 + 表格容器
|
||||
const isolationNotice = isAdminMode
|
||||
? `<div style="margin-bottom:12px;padding:8px 12px;background:#eff6ff;border:1px solid #bfdbfe;border-radius:4px;color:#1e40af;font-size:13px;"><i class="fas fa-info-circle"></i> 特权模式:显示该用户在所有公司、所有部门的关联记录(无公司隔离)</div>`
|
||||
: `<div style="margin-bottom:12px;padding:8px 12px;background:#f9fafb;border:1px solid #e5e7eb;border-radius:4px;color:#6b7280;font-size:13px;"><i class="fas fa-info-circle"></i> 仅显示当前公司数据。如需查看所有公司记录,需管理员特权。</div>`;
|
||||
|
||||
containerEl.innerHTML = `
|
||||
<div class="cp-detail-section">
|
||||
<h3><i class="fas fa-user-circle"></i> 基本信息</h3>
|
||||
<div class="cp-info-grid">
|
||||
<div class="cp-info-item"><div class="label">用户名</div><div class="value">${CompanyUtils.esc(data.UserName) || '-'}</div></div>
|
||||
<div class="cp-info-item"><div class="label">邮箱</div><div class="value">${CompanyUtils.esc(data.UserEmail) || '-'}</div></div>
|
||||
<div class="cp-info-item"><div class="label">电话</div><div class="value">${CompanyUtils.esc(data.UserTele) || '-'}</div></div>
|
||||
<div class="cp-info-item"><div class="label">性别</div><div class="value">${genderText(data.UserGender)}</div></div>
|
||||
<div class="cp-info-item"><div class="label">账号状态</div><div class="value">${CompanyUtils.statusTag(data.UserAccountLicense)}</div></div>
|
||||
<div class="cp-info-item"><div class="label">是否员工</div><div class="value">${yesNoText(data.IsStaff)}</div></div>
|
||||
<div class="cp-info-item"><div class="label">是否超级用户</div><div class="value">${yesNoText(data.IsSuperuser)}</div></div>
|
||||
<div class="cp-info-item"><div class="label">创建时间</div><div class="value">${CompanyUtils.formatTime(data.UserCreateTime)}</div></div>
|
||||
<div class="cp-info-item"><div class="label">最后登录</div><div class="value">${CompanyUtils.formatTime(data.UserLastLoginDate)}</div></div>
|
||||
</div>
|
||||
</div>
|
||||
${isolationNotice}
|
||||
<div class="cp-detail-section">
|
||||
<h3><i class="fas fa-shield-alt"></i> 角色与组织</h3>
|
||||
<div id="role-table-container" style="height:400px;"></div>
|
||||
</div>
|
||||
<div class="cp-detail-section">
|
||||
<h3><i class="fas fa-sitemap"></i> 部门关联</h3>
|
||||
<div id="dept-table-container" style="height:400px;"></div>
|
||||
</div>`;
|
||||
|
||||
// 初始化 TableEngine 实例
|
||||
initRoleTable();
|
||||
initDeptTable();
|
||||
} catch (e) {
|
||||
containerEl.innerHTML = `<div class="cp-error"><i class="fas fa-exclamation-circle" style="margin-right:6px;"></i> 网络错误: ${CompanyUtils.esc(e.message)}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
function initRoleTable() {
|
||||
roleTable = new TableEngine({
|
||||
container: '#role-table-container',
|
||||
height: '400px',
|
||||
tableName: 'UserRole',
|
||||
columns: [
|
||||
{ field: 'UserRoleUUID', title: '角色UUID', primaryKey: true, editable: false, hideInForm: true },
|
||||
CompanyUtils.epColumn('RoleUUID', '角色名', 'Role'),
|
||||
CompanyUtils.epColumn('CompanyUUID', '公司', 'Company', { editable: false }),
|
||||
CompanyUtils.epColumn('DeptUUID', '部门', 'Department'),
|
||||
CompanyUtils.epColumn('WorkgroupUUID', '工作组', 'Workgroup'),
|
||||
{ field: 'RoleType', title: '角色类型', type: 'select', options: [{ value: 0, label: '组员' }, { value: 1, label: '组长' }] },
|
||||
{ field: 'CreateTime', title: '创建时间', type: 'datetime', editable: false },
|
||||
],
|
||||
data: [],
|
||||
pageSize: 20,
|
||||
columnDistribution: 'equal',
|
||||
showPagination: true,
|
||||
showCheckbox: true,
|
||||
editable: true,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
striped: true,
|
||||
hoverable: true,
|
||||
bordered: true,
|
||||
api: {
|
||||
get: buildApiUrl('/API/FormAPI/Get/UserRole', { UserUUID: userUUID }),
|
||||
add: buildApiUrl('/API/FormAPI/Add/UserRole'),
|
||||
edit: buildApiUrl('/API/FormAPI/Modify/UserRole/<id>'),
|
||||
modify: buildApiUrl('/API/FormAPI/ModifyItem/UserRole/<id>'),
|
||||
delete: buildApiUrl('/API/FormAPI/Delete/UserRole/<id>'),
|
||||
},
|
||||
onView: (row) => {
|
||||
let url = `/pages/system/role-detail.html?RoleUUID=${encodeURIComponent(row.RoleUUID || '')}`;
|
||||
if (currentServer !== 'gvsds-master') url += `&server=${encodeURIComponent(currentServer)}`;
|
||||
GERP.Navigate(url, row.RoleUUID_label || '角色详情');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function initDeptTable() {
|
||||
deptTable = new TableEngine({
|
||||
container: '#dept-table-container',
|
||||
height: '400px',
|
||||
tableName: 'UserDept',
|
||||
columns: [
|
||||
{ field: 'UserDeptUUID', title: '关联UUID', primaryKey: true, editable: false, hideInForm: true },
|
||||
CompanyUtils.epColumn('DeptUUID', '部门名称', 'Department'),
|
||||
{ field: 'IsMainDept', title: '主部门', type: 'select', options: [{ value: 0, label: '兼职' }, { value: 1, label: '主部门' }] },
|
||||
],
|
||||
data: [],
|
||||
pageSize: 20,
|
||||
columnDistribution: 'equal',
|
||||
showPagination: true,
|
||||
showCheckbox: true,
|
||||
editable: true,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
striped: true,
|
||||
hoverable: true,
|
||||
bordered: true,
|
||||
api: {
|
||||
get: buildApiUrl('/API/FormAPI/Get/UserDept', { UserUUID: userUUID }),
|
||||
add: buildApiUrl('/API/FormAPI/Add/UserDept'),
|
||||
edit: buildApiUrl('/API/FormAPI/Modify/UserDept/<id>'),
|
||||
modify: buildApiUrl('/API/FormAPI/ModifyItem/UserDept/<id>'),
|
||||
delete: buildApiUrl('/API/FormAPI/Delete/UserDept/<id>'),
|
||||
},
|
||||
onView: (row) => {
|
||||
let url = `/pages/Company/department-detail.html?DeptUUID=${encodeURIComponent(row.DeptUUID || '')}`;
|
||||
if (currentServer !== 'gvsds-master') url += `&server=${encodeURIComponent(currentServer)}`;
|
||||
GERP.Navigate(url, row.DeptUUID_label || '部门详情');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** 编辑按钮:弹出编辑对话框 */
|
||||
async function handleEdit() {
|
||||
if (!currentData) return;
|
||||
CompanyUtils.showEditDialog({
|
||||
tableName: 'User',
|
||||
data: currentData,
|
||||
extraParams: isAdminMode ? { admin_verify: '1' } : {},
|
||||
onSave: () => init()
|
||||
});
|
||||
}
|
||||
|
||||
init();
|
||||
</script>
|
||||
@@ -4,24 +4,6 @@
|
||||
.user-header { display: flex; align-items: center; margin-bottom: 16px; gap: 12px; flex-shrink: 0; }
|
||||
.user-header h1 { margin: 0; flex: 1; }
|
||||
.user-container { flex: 1; min-height: 0; overflow: hidden; display: flex; flex-direction: column; }
|
||||
|
||||
/* 详情面板内样式 */
|
||||
.user-detail-section { margin-bottom: 20px; }
|
||||
.user-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; }
|
||||
.user-detail-section h3 i { color: var(--PrimaryColor); }
|
||||
.user-info-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 6px 16px; }
|
||||
.user-info-item { font-size: 13px; }
|
||||
.user-info-item .label { color: var(--TextSecondary); font-size: 12px; }
|
||||
.user-info-item .value { color: var(--TextPrimary); font-weight: 500; word-break: break-all; }
|
||||
.user-detail-list { list-style: none; padding: 0; margin: 0; }
|
||||
.user-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; }
|
||||
.user-detail-list li .item-name { font-weight: 500; flex: 1; }
|
||||
.user-detail-list li .item-desc { color: var(--TextSecondary); font-size: 12px; }
|
||||
.user-order-item { display: flex; align-items: center; gap: 12px; width: 100%; }
|
||||
.user-order-item .order-no { font-weight: 600; color: var(--PrimaryColor); }
|
||||
.user-order-item .order-amount { font-weight: 600; }
|
||||
.user-order-item .order-status { margin-left: auto; }
|
||||
.user-empty { text-align: center; padding: 20px; color: var(--TextSecondary); font-size: 13px; }
|
||||
</style>
|
||||
|
||||
<div class="GERPPage">
|
||||
@@ -40,9 +22,7 @@
|
||||
<script>
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const currentServer = urlParams.get('server') || 'gvsds-master';
|
||||
const API_BASE = '/api/gerp/users/';
|
||||
let _tableInstance = null;
|
||||
let _drawerClose = null;
|
||||
|
||||
GERP.SetTitle(currentServer === 'gvsds-master' ? '用户管理' : `${currentServer} 用户管理`);
|
||||
document.getElementById('page-title').textContent = currentServer === 'gvsds-master' ? '用户管理' : `${currentServer} - 用户管理`;
|
||||
@@ -78,17 +58,20 @@
|
||||
striped: true,
|
||||
hoverable: true,
|
||||
bordered: true,
|
||||
columnDistribution: 'equal',
|
||||
api: {
|
||||
get: `${API_BASE}list/?server=${currentServer}`,
|
||||
get: '/API/FormAPI/Get/User',
|
||||
add: '/API/FormAPI/Add/User',
|
||||
edit: '/API/FormAPI/Modify/User/<id>',
|
||||
modify: '/API/FormAPI/ModifyItem/User/<id>',
|
||||
delete: '/API/FormAPI/Delete/User/<id>',
|
||||
columns: '/API/FormAPI/Columns/User'
|
||||
},
|
||||
onRowClick: (row) => openDetail(row.UserUUID),
|
||||
onDelete: (row) => {},
|
||||
onEdit: (row) => {},
|
||||
onView: (row) => {
|
||||
let url = `/pages/system/user-detail.html?UserUUID=${encodeURIComponent(row.UserUUID || '')}`;
|
||||
if (currentServer !== 'gvsds-master') url += `&server=${encodeURIComponent(currentServer)}`;
|
||||
GERP.Navigate(url, row.UserName || '用户详情');
|
||||
},
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
@@ -99,181 +82,16 @@
|
||||
|
||||
function getDefaultColumns() {
|
||||
return [
|
||||
{ field: 'UserUUID', title: '用户UUID', width: 140, primaryKey: true, editable: false, hideInForm: true },
|
||||
{ field: 'UserName', title: '用户名', width: 120, type: 'text' },
|
||||
{ field: 'UserEmail', title: '邮箱', width: 180, type: 'text' },
|
||||
{ field: 'UserTele', title: '电话', width: 120, type: 'text' },
|
||||
{ field: 'UserGender', title: '性别', width: 60, type: 'select', options: [{value:0,label:'女'},{value:1,label:'男'}] },
|
||||
{ field: 'UserAccountLicense', title: '状态', width: 70, type: 'select', options: [{value:0,label:'禁用'},{value:1,label:'启用'}] },
|
||||
{ field: 'UserCreateTime', title: '创建时间', width: 160, type: 'datetime', editable: false },
|
||||
{ field: 'UserLastLoginDate', title: '最后登录', width: 160, type: 'datetime', editable: false },
|
||||
{ field: 'UserUUID', title: '用户UUID', primaryKey: true, editable: false, hideInForm: true },
|
||||
{ field: 'UserName', title: '用户名', type: 'text' },
|
||||
{ field: 'UserEmail', title: '邮箱', type: 'text' },
|
||||
{ field: 'UserTele', title: '电话', type: 'text' },
|
||||
{ field: 'UserGender', title: '性别', type: 'select', options: [{value:0,label:'女'},{value:1,label:'男'}] },
|
||||
{ field: 'UserAccountLicense', title: '状态', type: 'select', options: [{value:0,label:'禁用'},{value:1,label:'启用'}] },
|
||||
{ field: 'UserCreateTime', title: '创建时间', type: 'datetime', editable: false },
|
||||
{ field: 'UserLastLoginDate', title: '最后登录', type: 'datetime', editable: false },
|
||||
];
|
||||
}
|
||||
|
||||
// === 用户详情抽屉 ===
|
||||
|
||||
function openDetail(userUUID) {
|
||||
if (_drawerClose) _drawerClose();
|
||||
|
||||
WaitForUI((UI) => {
|
||||
const { element, close } = UI.OpenDrawer({
|
||||
title: '加载中...',
|
||||
placement: 'right',
|
||||
width: '520px',
|
||||
content: '<div style="text-align:center;padding:40px;color:var(--TextSecondary);"><i class="fas fa-spinner fa-spin"></i> 加载中...</div>',
|
||||
});
|
||||
_drawerClose = close;
|
||||
|
||||
const params = new URLSearchParams({ server: currentServer, UserUUID: userUUID });
|
||||
fetch(`${API_BASE}detail/?${params}`, { credentials: 'include' })
|
||||
.then(resp => resp.json())
|
||||
.then(json => {
|
||||
if (json.code !== 0) {
|
||||
element.querySelector('.GERPDrawerBody').innerHTML = `<div class="user-empty" style="color:var(--DangerColor);">${json.msg || '加载失败'}</div>`;
|
||||
return;
|
||||
}
|
||||
const d = json.data;
|
||||
element.querySelector('.GERPDrawerTitle').textContent = d.User.UserName || userUUID;
|
||||
element.querySelector('.GERPDrawerBody').innerHTML = renderDetail(d);
|
||||
})
|
||||
.catch(e => {
|
||||
element.querySelector('.GERPDrawerBody').innerHTML = `<div class="user-empty" style="color:var(--DangerColor);">网络错误: ${e.message}</div>`;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function WaitForUI(callback) {
|
||||
if (window.GERPUI && typeof window.GERPUI.OpenDrawer === 'function') {
|
||||
callback(window.GERPUI);
|
||||
return;
|
||||
}
|
||||
document.addEventListener('GERPUIReady', () => callback(window.GERPUI), { once: true });
|
||||
}
|
||||
|
||||
function renderDetail(d) {
|
||||
const u = d.User;
|
||||
let html = '';
|
||||
|
||||
html += `<div class="user-detail-section">
|
||||
<h3><i class="fas fa-id-card"></i> 基本信息</h3>
|
||||
<div class="user-info-grid">
|
||||
<div class="user-info-item"><div class="label">用户名</div><div class="value">${esc(u.UserName)}</div></div>
|
||||
<div class="user-info-item"><div class="label">邮箱</div><div class="value">${esc(u.UserEmail) || '-'}</div></div>
|
||||
<div class="user-info-item"><div class="label">电话</div><div class="value">${esc(u.UserTele) || '-'}</div></div>
|
||||
<div class="user-info-item"><div class="label">性别</div><div class="value">${genderText(u.UserGender)}</div></div>
|
||||
<div class="user-info-item"><div class="label">状态</div><div class="value">${statusTag(u.UserAccountLicense)}</div></div>
|
||||
<div class="user-info-item"><div class="label">职位状态</div><div class="value">${u.UserPositionStatus === 1 ? '在职' : '离职'}</div></div>
|
||||
<div class="user-info-item"><div class="label">创建时间</div><div class="value">${formatTime(u.UserCreateTime)}</div></div>
|
||||
<div class="user-info-item"><div class="label">最后登录</div><div class="value">${formatTime(u.UserLastLoginDate)}</div></div>
|
||||
<div class="user-info-item"><div class="label">UUID</div><div class="value" style="font-size:11px;font-family:monospace;">${esc(u.UserUUID)}</div></div>
|
||||
<div class="user-info-item"><div class="label">标签</div><div class="value">${u.IsSuperuser ? '<span style="display:inline-block;padding:2px 8px;border-radius:4px;font-size:11px;font-weight:600;background:#fef3c7;color:#92400e;">超级用户</span> ' : ''}${u.IsStaff ? '<span style="display:inline-block;padding:2px 8px;border-radius:4px;font-size:11px;font-weight:600;background:#dbeafe;color:#1e40af;">员工</span>' : ''}</div></div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
html += `<div class="user-detail-section">
|
||||
<h3><i class="fas fa-shield-alt"></i> 角色 (${d.Roles.length}) <a href="javascript:void(0)" onclick="openTable('Role')" style="font-size:12px;font-weight:400;margin-left:auto;">管理</a></h3>`;
|
||||
if (d.Roles.length === 0) {
|
||||
html += `<div class="user-empty">暂无角色</div>`;
|
||||
} else {
|
||||
html += `<ul class="user-detail-list">`;
|
||||
d.Roles.forEach(r => {
|
||||
html += `<li>
|
||||
<i class="fas fa-user-tag" style="color:var(--PrimaryColor);"></i>
|
||||
<span class="item-name">${esc(r.RoleName)}</span>
|
||||
${r.CompanyName ? `<span class="item-desc">${esc(r.CompanyName)}${r.CompanyShort ? ` (${esc(r.CompanyShort)})` : ''}</span>` : ''}
|
||||
${assignScopeTag(r.AssignScope)}
|
||||
${r.RoleStatus === 1 ? '<span style="display:inline-block;padding:2px 8px;border-radius:4px;font-size:11px;font-weight:600;background:#dcfce7;color:#166534;">启用</span>' : '<span style="display:inline-block;padding:2px 8px;border-radius:4px;font-size:11px;font-weight:600;background:#fee2e2;color:#991b1b;">禁用</span>'}
|
||||
</li>`;
|
||||
});
|
||||
html += `</ul>`;
|
||||
}
|
||||
html += `</div>`;
|
||||
|
||||
html += `<div class="user-detail-section">
|
||||
<h3><i class="fas fa-key"></i> 权限 (${d.Permissions.length}) <a href="javascript:void(0)" onclick="openTable('Permission')" style="font-size:12px;font-weight:400;margin-left:auto;">管理</a></h3>`;
|
||||
if (d.Permissions.length === 0) {
|
||||
html += `<div class="user-empty">暂无权限</div>`;
|
||||
} else {
|
||||
html += `<ul class="user-detail-list">`;
|
||||
d.Permissions.forEach(p => {
|
||||
html += `<li>
|
||||
<i class="fas fa-lock" style="color:#f59e0b;"></i>
|
||||
<span class="item-name">${esc(p.PermName)}</span>
|
||||
${p.PermCategory ? `<span style="display:inline-block;padding:2px 6px;border-radius:4px;font-size:10px;background:#f3f4f6;color:#6b7280;">${esc(p.PermCategory)}</span>` : ''}
|
||||
<span class="item-desc" style="font-family:monospace;font-size:11px;">${esc(p.PermCode)}</span>
|
||||
</li>`;
|
||||
});
|
||||
html += `</ul>`;
|
||||
}
|
||||
html += `</div>`;
|
||||
|
||||
html += `<div class="user-detail-section">
|
||||
<h3><i class="fas fa-receipt"></i> 订单 (${d.Orders.length}) <a href="javascript:void(0)" onclick="openTable('Order')" style="font-size:12px;font-weight:400;margin-left:auto;">管理</a></h3>`;
|
||||
if (d.Orders.length === 0) {
|
||||
html += `<div class="user-empty">暂无订单</div>`;
|
||||
} else {
|
||||
html += `<ul class="user-detail-list">`;
|
||||
d.Orders.forEach(o => {
|
||||
html += `<li>
|
||||
<div class="user-order-item">
|
||||
<i class="fas fa-file-invoice" style="color:#10b981;"></i>
|
||||
<span class="order-no">${esc(o.OrderNO)}</span>
|
||||
<span class="item-name">${esc(o.OrderTitle)}</span>
|
||||
<span class="order-amount">${o.Currency} ${o.PayAmount}</span>
|
||||
<span class="order-status">${orderStatusText(o.OrderStatus)}</span>
|
||||
</div>
|
||||
</li>`;
|
||||
});
|
||||
html += `</ul>`;
|
||||
}
|
||||
html += `</div>`;
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
// === 工具函数 ===
|
||||
|
||||
function openTable(tableName) {
|
||||
if (currentServer === 'gvsds-master') {
|
||||
GERP.Navigate(`/pages/system/dbtable.html?table=${encodeURIComponent(tableName)}`, tableName);
|
||||
} else {
|
||||
GERP.Navigate(`/pages/system/sub-dbtable.html?server=${encodeURIComponent(currentServer)}&table=${encodeURIComponent(tableName)}`, `${currentServer} / ${tableName}`);
|
||||
}
|
||||
}
|
||||
|
||||
function esc(s) { if (s == null) return ''; const d = document.createElement('div'); d.textContent = String(s); return d.innerHTML; }
|
||||
|
||||
function assignScopeTag(scope) {
|
||||
const map = {
|
||||
0: { label: '全租户', bg: '#ede9fe', color: '#6d28d9' },
|
||||
1: { label: '公司级', bg: '#dbeafe', color: '#1e40af' },
|
||||
2: { label: '部门级', bg: '#fef3c7', color: '#92400e' },
|
||||
};
|
||||
const s = map[scope];
|
||||
if (!s) return '';
|
||||
return `<span style="display:inline-block;padding:2px 8px;border-radius:4px;font-size:11px;font-weight:600;background:${s.bg};color:${s.color};">${s.label}</span>`;
|
||||
}
|
||||
|
||||
function genderText(g) {
|
||||
if (g === 1 || g === '1') return '男';
|
||||
if (g === 0 || g === '0') return '女';
|
||||
return '未知';
|
||||
}
|
||||
|
||||
function statusTag(license) {
|
||||
if (license === 1 || license === '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>';
|
||||
}
|
||||
|
||||
function formatTime(t) {
|
||||
if (!t) return '-';
|
||||
try { const d = new Date(t + (t.endsWith('Z') ? '' : 'Z')); return d.toLocaleString(); } catch { return t; }
|
||||
}
|
||||
|
||||
function orderStatusText(s) {
|
||||
const map = { 0:'待确认', 1:'已确认', 2:'已支付', 3:'处理中', 4:'已完成', 5:'已取消', 6:'退款中', 7:'已退款', 8:'已关闭' };
|
||||
return map[s] || String(s);
|
||||
}
|
||||
|
||||
initTable();
|
||||
</script>
|
||||
+168
-13
@@ -1,5 +1,8 @@
|
||||
<script>
|
||||
if (window.parent && window.parent !== window) {window.parent.location.reload(); }
|
||||
// 父框架刷新逻辑不变
|
||||
if (window.parent && window.parent !== window) {
|
||||
window.parent.location.reload();
|
||||
}
|
||||
</script>
|
||||
<div class="relative min-h-screen flex items-center justify-center bg-gray-50 dark:bg-gray-100 p-4">
|
||||
<div class="hidden md:flex md:w-1/2 items-center justify-start px-4 opacity-0 translate-x-[-20px] animate-fade-in-left">
|
||||
@@ -17,27 +20,50 @@ if (window.parent && window.parent !== window) {window.parent.location.reload();
|
||||
</div>
|
||||
<div class="w-full md:w-1/2 max-w-md opacity-0 translate-x-[20px] animate-fade-in-right">
|
||||
<div class="bg-white dark:bg-gray-50 rounded-xl shadow-xl overflow-hidden transition-all duration-300 hover:shadow-blue-500/5">
|
||||
<div class="relative h-24"> <img src="https://bing.img.run/uhd.php" alt="Card image" class="w-full h-full object-cover">
|
||||
<div class="relative h-24">
|
||||
<img src="https://bing.img.run/uhd.php" alt="Card image" class="w-full h-full object-cover">
|
||||
<div class="absolute inset-0 bg-gradient-to-t from-white to-transparent"></div>
|
||||
<h4 class="absolute bottom-4 left-6 text-xl font-bold text-gray-900">登录</h4>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<!-- Toast固定容器,永远放在表单上方 -->
|
||||
<div id="toastWrap" class="px-6 pt-6"></div>
|
||||
<div class="p-6 pt-0">
|
||||
<form class="space-y-4" id="loginForm">
|
||||
<div class="relative">
|
||||
<div class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none transition-colors duration-200"> <i class="tim-icons icon-single-02 text-gray-500 input-icon"></i> </div> <input type="text" name="UserName"
|
||||
<div class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none transition-colors duration-200">
|
||||
<i class="tim-icons icon-single-02 text-gray-500 input-icon"></i>
|
||||
</div>
|
||||
<input type="text" name="UserName"
|
||||
class="block w-full pl-10 pr-3 py-2.5 bg-gray-50 border border-gray-200 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-gray-900 placeholder-gray-500 transition-all duration-200 focus:scale-[1.01] focus:bg-white" placeholder="昵称">
|
||||
</div>
|
||||
<div class="relative">
|
||||
<div class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none transition-colors duration-200"> <i class="tim-icons icon-lock-circle text-gray-500 input-icon"></i> </div> <input type="password" name="UserPassword"
|
||||
<div class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none transition-colors duration-200">
|
||||
<i class="tim-icons icon-lock-circle text-gray-500 input-icon"></i>
|
||||
</div>
|
||||
<input type="password" name="UserPassword"
|
||||
class="block w-full pl-10 pr-3 py-2.5 bg-gray-50 border border-gray-200 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-gray-900 placeholder-gray-500 transition-all duration-200 focus:scale-[1.01] focus:bg-white" placeholder="密码" required>
|
||||
</div>
|
||||
<div class="flex items-start">
|
||||
<div class="flex items-center h-5"> <input id="UserConsent" name="UserConsent" type="checkbox" class="w-4 h-4 rounded border-gray-300 text-blue-500 focus:ring-blue-500 bg-gray-100 transition-all duration-200 hover:border-gray-400 checked:scale-[0.95]"> </div>
|
||||
<div class="ml-3 text-sm"> <label for="UserConsent" class="text-gray-700 transition-colors duration-200 hover:text-gray-900"> 我阅读并同意 <a href="https://doc.gvsds.com/d2ae1dd63e9c35d687464a160896e1ab" target="_blank"
|
||||
class="text-blue-600 hover:underline transition-colors duration-200 hover:text-blue-700">GVSDS用户协议</a> </label> </div>
|
||||
<div class="flex items-center h-5">
|
||||
<input id="UserConsent" name="UserConsent" type="checkbox" class="w-4 h-4 rounded border-gray-300 text-blue-500 focus:ring-blue-500 bg-gray-100 transition-all duration-200 hover:border-gray-400 checked:scale-[0.95]">
|
||||
</div>
|
||||
<div class="ml-3 text-sm">
|
||||
<label for="UserConsent" class="text-gray-700 transition-colors duration-200 hover:text-gray-900">
|
||||
我阅读并同意
|
||||
<a href="https://doc.gvsds.com/d2ae1dd63e9c35d687464a160896e1ab" target="_blank"
|
||||
class="text-blue-600 hover:underline transition-colors duration-200 hover:text-blue-700">GVSDS用户协议</a>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-sm text-gray-700"> 我还没有账号,去 <a href="/register.html" class="text-blue-600 hover:underline transition-colors duration-200 hover:text-blue-700">注册</a> </div>
|
||||
<div class="pt-2"> <button type="submit" class="w-full py-3 px-4 bg-blue-500 hover:bg-blue-600 rounded-lg text-white font-medium transition-all duration-200 flex items-center justify-center hover:scale-[1.02] active:scale-[0.98] hover:shadow-lg hover:shadow-blue-500/10"> 登录</button> </div>
|
||||
<div class="text-sm text-gray-700">
|
||||
我还没有账号,去
|
||||
<a href="/register.html" class="text-blue-600 hover:underline transition-colors duration-200 hover:text-blue-700">注册</a>
|
||||
</div>
|
||||
<div class="pt-2">
|
||||
<button type="submit" class="w-full py-3 px-4 bg-blue-500 hover:bg-blue-600 rounded-lg text-white font-medium transition-all duration-200 flex items-center justify-center hover:scale-[1.02] active:scale-[0.98] hover:shadow-lg hover:shadow-blue-500/10">
|
||||
登录
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -51,10 +77,99 @@ if (window.parent && window.parent !== window) {window.parent.location.reload();
|
||||
@keyframes fadeIn{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}
|
||||
.animate-fade-in{animation:fadeIn .3s ease-out forwards}
|
||||
input:focus~.input-icon{color:#a855f7}
|
||||
#LoginToast{animation:fadeIn .3s ease-out forwards}
|
||||
#LoginToast{
|
||||
animation:fadeIn .3s ease-out forwards;
|
||||
position:relative;
|
||||
z-index:999;
|
||||
width:100%;
|
||||
box-sizing:border-box;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
const msg=new URLSearchParams(window.location.search).get('msg');const urlRedirect=new URLSearchParams(window.location.search).get('redirect');if(msg){document.currentScript.insertAdjacentHTML('afterend',`<div id="LoginToast"class="mb-4 p-3 text-red-500 bg-red-500/5 rounded-lg animate-fade-in">${msg}</div>`)}const loginForm=document.getElementById('loginForm');const toastEl=document.getElementById('LoginToast');if(toastEl&&!toastEl.textContent.trim()){toastEl.style.display='none'}document.querySelectorAll('label[for]').forEach(label=>{label.addEventListener('click',()=>{const targetId=label.getAttribute('for');const targetEl=document.getElementById(targetId);if(targetEl)targetEl.focus()})});function showToast(content,type='error'){let toast=document.getElementById('LoginToast');if(!toast){toast=document.createElement('div');toast.id='LoginToast';const card=document.querySelector('.p-6');if(card){card.insertBefore(toast,card.firstChild)}else{document.body.appendChild(toast)}}toast.style.cssText='margin-bottom:16px;padding:12px 16px;border-radius:8px;font-size:14px;line-height:1.5;display:block;';if(type==='error'){toast.style.color='#dc2626';toast.style.background='#fef2f2';toast.style.border='1px solid #fecaca'}else{toast.style.color='#16a34a';toast.style.background='#f0fdf4';toast.style.border='1px solid #bbf7d0'}toast.textContent=content}loginForm.addEventListener('submit',function(e){e.preventDefault();const UserConsent=document.getElementById('UserConsent').checked;if(!UserConsent){showToast('必须签署协议');return}const submitButton=this.querySelector('button[type="submit"]');const originalText=submitButton.innerHTML;submitButton.disabled=true;submitButton.innerHTML='<i class="fa fa-spinner fa-spin mr-2"></i> 登录中...';const formData=new FormData(loginForm);fetch('https://ugm.gvsds.com/Auth/Login',{method:'POST',credentials:'include',body:formData}).then(res=>res.json()).then(result=>{submitButton.disabled=false;submitButton.innerHTML=originalText;if(result.code===0){showToast('登录成功,即将跳转...','success');setTimeout(()=>{window.location.href=urlRedirect||result.redirect||'/'},500)}else if(result.code===2&&result.require2FA){show2FAForm(result.pendingKey)}else{showToast(result.msg||'登录失败')}}).catch(err=>{submitButton.disabled=false;submitButton.innerHTML=originalText;showToast('网络错误:'+err.message)})});function show2FAForm(key){const _pendingKey=key;const formContainer=loginForm.parentElement;formContainer.innerHTML=`
|
||||
// 统一Toast渲染函数,替换原来插入逻辑
|
||||
function showToast(content, type='error'){
|
||||
const wrap = document.getElementById('toastWrap');
|
||||
let toast = document.getElementById('LoginToast');
|
||||
// 存在先清空内容
|
||||
if(toast) toast.remove();
|
||||
|
||||
toast = document.createElement('div');
|
||||
toast.id = 'LoginToast';
|
||||
toast.className = 'mb-4 p-3 rounded-lg animate-fade-in';
|
||||
|
||||
if(type === 'error'){
|
||||
toast.style.color='#dc2626';
|
||||
toast.style.background='#fef2f2';
|
||||
toast.style.border='1px solid #fecaca';
|
||||
}else{
|
||||
toast.style.color='#16a34a';
|
||||
toast.style.background='#f0fdf4';
|
||||
toast.style.border='1px solid #bbf7d0';
|
||||
}
|
||||
toast.textContent = content;
|
||||
wrap.appendChild(toast);
|
||||
}
|
||||
|
||||
// 页面URL参数消息,DOM加载完成后执行
|
||||
document.addEventListener('DOMContentLoaded', ()=>{
|
||||
const msg = new URLSearchParams(window.location.search).get('msg');
|
||||
const urlRedirect = new URLSearchParams(window.location.search).get('redirect');
|
||||
if(msg){
|
||||
showToast(msg, 'error');
|
||||
}
|
||||
|
||||
const loginForm = document.getElementById('loginForm');
|
||||
|
||||
document.querySelectorAll('label[for]').forEach(label=>{
|
||||
label.addEventListener('click',()=>{
|
||||
const targetId=label.getAttribute('for');
|
||||
const targetEl=document.getElementById(targetId);
|
||||
if(targetEl)targetEl.focus()
|
||||
})
|
||||
});
|
||||
|
||||
// 表单提交逻辑
|
||||
loginForm.addEventListener('submit',function(e){
|
||||
e.preventDefault();
|
||||
const UserConsent=document.getElementById('UserConsent').checked;
|
||||
if(!UserConsent){
|
||||
showToast('必须签署协议');
|
||||
return
|
||||
}
|
||||
const submitButton=this.querySelector('button[type="submit"]');
|
||||
const originalText=submitButton.innerHTML;
|
||||
submitButton.disabled=true;
|
||||
submitButton.innerHTML='<i class="fa fa-spinner fa-spin mr-2"></i> 登录中...';
|
||||
const formData=new FormData(loginForm);
|
||||
fetch('https://ugm.gvsds.com/Auth/Login',{
|
||||
method:'POST',
|
||||
credentials:'include',
|
||||
body:formData
|
||||
}).then(res=>res.json()).then(result=>{
|
||||
submitButton.disabled=false;
|
||||
submitButton.innerHTML=originalText;
|
||||
if(result.code===0){
|
||||
showToast('登录成功,即将跳转...','success');
|
||||
setTimeout(()=>{
|
||||
window.location.href=urlRedirect||result.redirect||'/'
|
||||
},500)
|
||||
}else if(result.code===2&&result.require2FA){
|
||||
show2FAForm(result.pendingKey)
|
||||
}else{
|
||||
showToast(result.msg||'登录失败')
|
||||
}
|
||||
}).catch(err=>{
|
||||
submitButton.disabled=false;
|
||||
submitButton.innerHTML=originalText;
|
||||
showToast('网络错误:'+err.message)
|
||||
})
|
||||
});
|
||||
|
||||
// 两步验证弹窗逻辑
|
||||
function show2FAForm(key){
|
||||
const _pendingKey=key;
|
||||
const formContainer=loginForm.parentElement;
|
||||
formContainer.innerHTML=`
|
||||
<div class="text-center mb-6">
|
||||
<div class="w-16 h-16 mx-auto mb-4 rounded-2xl bg-blue-500/10 flex items-center justify-center">
|
||||
<i class="fas fa-shield-halved text-2xl text-blue-500"></i>
|
||||
@@ -75,5 +190,45 @@ placeholder="000000" autocomplete="one-time-code" autofocus>
|
||||
返回登录
|
||||
</button>
|
||||
</div>
|
||||
`;const tokenInput=document.getElementById('twoFAToken');const verifyBtn=document.getElementById('verify2FABtn');const cancelBtn=document.getElementById('cancel2FABtn');tokenInput.focus();tokenInput.addEventListener('input',function(){this.value=this.value.replace(/[^0-9]/g,'')});verifyBtn.addEventListener('click',function(){const token=tokenInput.value.trim();if(!token||token.length!==6){showToast('请输入6位验证码');return}verifyBtn.disabled=true;verifyBtn.innerHTML='<i class="fa fa-spinner fa-spin mr-2"></i> 验证中...';fetch('https://ugm.gvsds.com/Auth/Verify2FA',{method:'POST',credentials:'include',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:`pendingKey=${encodeURIComponent(_pendingKey)}&token=${encodeURIComponent(token)}`}).then(res=>res.json()).then(result=>{if(result.code===0){showToast('验证成功,即将跳转...','success');setTimeout(()=>{window.location.href=urlRedirect||result.redirect||'/'},500)}else{verifyBtn.disabled=false;verifyBtn.textContent='验证';showToast(result.msg||'验证码错误')}}).catch(err=>{verifyBtn.disabled=false;verifyBtn.textContent='验证';showToast('网络错误')})});cancelBtn.addEventListener('click',()=>window.location.reload())}
|
||||
`;
|
||||
const tokenInput=document.getElementById('twoFAToken');
|
||||
const verifyBtn=document.getElementById('verify2FABtn');
|
||||
const cancelBtn=document.getElementById('cancel2FABtn');
|
||||
tokenInput.focus();
|
||||
tokenInput.addEventListener('input',function(){
|
||||
this.value=this.value.replace(/[^0-9]/g,'')
|
||||
});
|
||||
verifyBtn.addEventListener('click',function(){
|
||||
const token=tokenInput.value.trim();
|
||||
if(!token||token.length!==6){
|
||||
showToast('请输入6位验证码');
|
||||
return
|
||||
}
|
||||
verifyBtn.disabled=true;
|
||||
verifyBtn.innerHTML='<i class="fa fa-spinner fa-spin mr-2"></i> 验证中...';
|
||||
fetch('https://ugm.gvsds.com/Auth/Verify2FA',{
|
||||
method:'POST',
|
||||
credentials:'include',
|
||||
headers:{'Content-Type':'application/x-www-form-urlencoded'},
|
||||
body:`pendingKey=${encodeURIComponent(_pendingKey)}&token=${encodeURIComponent(token)}`
|
||||
}).then(res=>res.json()).then(result=>{
|
||||
if(result.code===0){
|
||||
showToast('验证成功,即将跳转...','success');
|
||||
setTimeout(()=>{
|
||||
window.location.href=urlRedirect||result.redirect||'/'
|
||||
},500)
|
||||
}else{
|
||||
verifyBtn.disabled=false;
|
||||
verifyBtn.textContent='验证';
|
||||
showToast(result.msg||'验证码错误')
|
||||
}
|
||||
}).catch(err=>{
|
||||
verifyBtn.disabled=false;
|
||||
verifyBtn.textContent='验证';
|
||||
showToast('网络错误')
|
||||
})
|
||||
});
|
||||
cancelBtn.addEventListener('click',()=>window.location.reload())
|
||||
}
|
||||
})
|
||||
</script>
|
||||
+129
-16
@@ -16,37 +16,60 @@ if (window.parent && window.parent !== window) { window.parent.location.reload()
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full md:w-1/2 max-w-md opacity-0 translate-x-[20px] animate-fade-in-right">
|
||||
<div class="bg-white dark:bg-gray-50 rounded-xl shadow-xl overflow-hidden transition-all duration-300 hover:shadow-purple-500/5">
|
||||
<div class="relative h-24"><img src="https://bing.img.run/uhd.php" alt="Card image" class="w-full h-full object-cover">
|
||||
<div class="bg-white dark:bg-gray-50 rounded-xl shadow-xl overflow-visible transition-all duration-300 hover:shadow-purple-500/5">
|
||||
<div class="relative h-24">
|
||||
<img src="https://bing.img.run/uhd.php" alt="Card image" class="w-full h-full object-cover">
|
||||
<div class="absolute inset-0 bg-gradient-to-t from-white to-transparent"></div>
|
||||
<h4 class="absolute bottom-4 left-6 text-xl font-bold text-gray-900">注册</h4>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<!-- Toast固定容器 -->
|
||||
<div id="toastWrap" class="px-6 pt-6"></div>
|
||||
<div class="p-6 pt-0">
|
||||
<form class="space-y-4" id="registerForm">
|
||||
<div class="relative">
|
||||
<div class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none transition-colors duration-200"><i class="tim-icons icon-single-02 text-gray-500 input-icon"></i></div><input type="text" name="UserName"
|
||||
<div class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none transition-colors duration-200">
|
||||
<i class="tim-icons icon-single-02 text-gray-500 input-icon"></i>
|
||||
</div>
|
||||
<input type="text" name="UserName"
|
||||
class="block w-full pl-10 pr-3 py-2.5 bg-gray-50 border border-gray-200 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent text-gray-900 placeholder-gray-500 transition-all duration-200 focus:scale-[1.01] focus:bg-white" placeholder="昵称" required>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<div class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none transition-colors duration-200"><i class="tim-icons icon-email-85 text-gray-500 input-icon"></i></div><input type="email" name="UserEmail"
|
||||
class="block w-full pl-10 pr-3 py-2.5 bg-gray-50 border border-gray-200 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent text-gray-900 placeholder-gray-500 transition-all duration-200 focus:scale-[1.01] focus:bg-white" placeholder="邮箱(注意:请填写有效的邮箱)"
|
||||
required>
|
||||
<div class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none transition-colors duration-200">
|
||||
<i class="tim-icons icon-email-85 text-gray-500 input-icon"></i>
|
||||
</div>
|
||||
<input type="email" name="UserEmail"
|
||||
class="block w-full pl-10 pr-3 py-2.5 bg-gray-50 border border-gray-200 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent text-gray-900 placeholder-gray-500 transition-all duration-200 focus:scale-[1.01] focus:bg-white" placeholder="邮箱(注意:请填写有效的邮箱)" required>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<div class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none transition-colors duration-200"><i class="tim-icons icon-lock-circle text-gray-500 input-icon"></i></div><input type="password" name="UserPassword"
|
||||
<div class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none transition-colors duration-200">
|
||||
<i class="tim-icons icon-lock-circle text-gray-500 input-icon"></i>
|
||||
</div>
|
||||
<input type="password" name="UserPassword"
|
||||
class="block w-full pl-10 pr-3 py-2.5 bg-gray-50 border border-gray-200 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent text-gray-900 placeholder-gray-500 transition-all duration-200 focus:scale-[1.01] focus:bg-white" placeholder="密码" required>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<div class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none transition-colors duration-200"><i class="tim-icons icon-lock-circle text-gray-500 input-icon"></i></div><input type="password" name="UserPassword2"
|
||||
<div class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none transition-colors duration-200">
|
||||
<i class="tim-icons icon-lock-circle text-gray-500 input-icon"></i>
|
||||
</div>
|
||||
<input type="password" name="UserPassword2"
|
||||
class="block w-full pl-10 pr-3 py-2.5 bg-gray-50 border border-gray-200 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent text-gray-900 placeholder-gray-500 transition-all duration-200 focus:scale-[1.01] focus:bg-white" placeholder="重复密码" required>
|
||||
</div>
|
||||
<div class="flex items-start">
|
||||
<div class="flex items-center h-5"><input id="UserConsent" name="UserConsent" type="checkbox" class="w-4 h-4 rounded border-gray-300 text-purple-500 focus:ring-purple-500 bg-gray-100 transition-all duration-200 hover:border-gray-400 checked:scale-[0.95]" required></div>
|
||||
<div class="ml-3 text-sm"><label for="UserConsent" class="text-gray-700 transition-colors duration-200 hover:text-gray-900"> 我阅读并同意 <a href="/Document/9bc298e9c1d03ed3bd4777181bf7411d/d2ae1dd63e9c35d687464a160896e1ab" target="_blank"
|
||||
class="text-purple-600 hover:underline transition-colors duration-200 hover:text-purple-700">GVSDS用户协议</a></label></div>
|
||||
<div class="flex items-center h-5">
|
||||
<input id="UserConsent" name="UserConsent" type="checkbox" class="w-4 h-4 rounded border-gray-300 text-purple-500 focus:ring-purple-500 bg-gray-100 transition-all duration-200 hover:border-gray-400 checked:scale-[0.95]" required>
|
||||
</div>
|
||||
<div class="ml-3 text-sm">
|
||||
<label for="UserConsent" class="text-gray-700 transition-colors duration-200 hover:text-gray-900"> 我阅读并同意
|
||||
<a href="/Document/9bc298e9c1d03ed3bd4777181bf7411d/d2ae1dd63e9c35d687464a160896e1ab" target="_blank" class="text-purple-600 hover:underline transition-colors duration-200 hover:text-purple-700">GVSDS用户协议</a>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-sm text-gray-700"> 我已有账号,去 <a href="/login.html" class="text-purple-600 hover:underline transition-colors duration-200 hover:text-purple-700">登录</a></div>
|
||||
<div class="pt-2"><button type="submit" class="w-full py-3 px-4 bg-purple-500 hover:bg-purple-600 rounded-lg text-white font-medium transition-all duration-200 flex items-center justify-center hover:scale-[1.02] active:scale-[0.98] hover:shadow-lg hover:shadow-purple-500/10"> 注册</button></div>
|
||||
<div class="text-sm text-gray-700"> 我已有账号,去
|
||||
<a href="/login.html" class="text-purple-600 hover:underline transition-colors duration-200 hover:text-purple-700">登录</a>
|
||||
</div>
|
||||
<div class="pt-2">
|
||||
<button type="submit" class="w-full py-3 px-4 bg-purple-500 hover:bg-purple-600 rounded-lg text-white font-medium transition-all duration-200 flex items-center justify-center hover:scale-[1.02] active:scale-[0.98] hover:shadow-lg hover:shadow-purple-500/10"> 注册</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -60,8 +83,98 @@ if (window.parent && window.parent !== window) { window.parent.location.reload()
|
||||
@keyframes fadeIn{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}
|
||||
.animate-fade-in{animation:fadeIn .3s ease-out forwards}
|
||||
input:focus~.input-icon{color:#a855f7}
|
||||
#RegisterToast{animation:fadeIn .3s ease-out forwards}
|
||||
#RegisterToast{
|
||||
animation:fadeIn .3s ease-out forwards;
|
||||
position:relative;
|
||||
z-index:999;
|
||||
width:100%;
|
||||
box-sizing:border-box;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
const msg=new URLSearchParams(window.location.search).get('msg');if(msg){document.currentScript.insertAdjacentHTML('afterend',`<div id="RegisterToast"class="mb-4 p-3 text-red-500 bg-red-500/5 rounded-lg animate-fade-in">${msg}</div>`)}const registerForm=document.getElementById('registerForm');const toastEl=document.getElementById('RegisterToast');if(toastEl&&!toastEl.textContent.trim()){toastEl.style.display='none'}document.querySelectorAll('label[for]').forEach(label=>{label.addEventListener('click',()=>{const targetId=label.getAttribute('for');const targetEl=document.getElementById(targetId);if(targetEl)targetEl.focus()})});function showToast(content,type='error'){let toast=document.getElementById('RegisterToast');if(!toast){toast=document.createElement('div');toast.id='RegisterToast';toast.className=`mb-4 p-3 text-${type==='error'?'red-500':'green-500'} bg-${type==='error'?'red':'green'}-500/5 rounded-lg animate-fade-in`;const formContainer=registerForm.parentElement;formContainer.insertBefore(toast,registerForm)}toast.textContent=content;toast.style.display='block'}registerForm.addEventListener('submit',function(e){e.preventDefault();const UserConsent=document.getElementById('UserConsent').checked;const UserPassword=this.querySelector('input[name="UserPassword"]').value;const UserPassword2=this.querySelector('input[name="UserPassword2"]').value;if(!UserConsent){showToast('必须阅读并同意用户协议');return}if(UserPassword!==UserPassword2){showToast('两次输入的密码不一致,请重新输入');return}const submitButton=this.querySelector('button[type="submit"]');const originalText=submitButton.innerHTML;submitButton.disabled=true;submitButton.innerHTML='<i class="fa fa-spinner fa-spin mr-2"></i> 注册中...';const formData=new FormData(registerForm);fetch('https://ugm.gvsds.com/Auth/Register',{method:'POST',credentials:'include',body:formData}).then(res=>res.json()).then(result=>{submitButton.disabled=false;submitButton.innerHTML=originalText;if(result.code===0){showToast('注册成功,即将跳转...','success');setTimeout(()=>{window.location.href=result.redirect||'/'},500)}else{showToast(result.msg||'注册失败')}}).catch(err=>{submitButton.disabled=false;submitButton.innerHTML=originalText;showToast('网络错误:'+err.message)})});
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const msg = new URLSearchParams(window.location.search).get('msg');
|
||||
const registerForm = document.getElementById('registerForm');
|
||||
const toastWrap = document.getElementById('toastWrap');
|
||||
|
||||
// 统一弹窗渲染函数
|
||||
function showToast(content, type = 'error') {
|
||||
let toast = document.getElementById('RegisterToast');
|
||||
if (toast) toast.remove();
|
||||
|
||||
toast = document.createElement('div');
|
||||
toast.id = 'RegisterToast';
|
||||
toast.className = 'mb-4 p-3 rounded-lg animate-fade-in';
|
||||
|
||||
if (type === 'error') {
|
||||
toast.style.color = '#dc2626';
|
||||
toast.style.background = '#fef2f2';
|
||||
toast.style.border = '1px solid #fecaca';
|
||||
} else {
|
||||
toast.style.color = '#16a34a';
|
||||
toast.style.background = '#f0fdf4';
|
||||
toast.style.border = '1px solid #bbf7d0';
|
||||
}
|
||||
toast.textContent = content;
|
||||
toastWrap.appendChild(toast);
|
||||
}
|
||||
|
||||
// 页面带错误参数时展示提示
|
||||
if (msg) {
|
||||
showToast(msg, 'error');
|
||||
}
|
||||
|
||||
// label点击聚焦对应输入框
|
||||
document.querySelectorAll('label[for]').forEach(label => {
|
||||
label.addEventListener('click', () => {
|
||||
const targetId = label.getAttribute('for');
|
||||
const targetEl = document.getElementById(targetId);
|
||||
if (targetEl) targetEl.focus();
|
||||
})
|
||||
});
|
||||
|
||||
// 注册表单提交事件
|
||||
registerForm.addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
const UserConsent = document.getElementById('UserConsent').checked;
|
||||
const UserPassword = this.querySelector('input[name="UserPassword"]').value;
|
||||
const UserPassword2 = this.querySelector('input[name="UserPassword2"]').value;
|
||||
|
||||
if (!UserConsent) {
|
||||
showToast('必须阅读并同意用户协议');
|
||||
return;
|
||||
}
|
||||
if (UserPassword !== UserPassword2) {
|
||||
showToast('两次输入的密码不一致,请重新输入');
|
||||
return;
|
||||
}
|
||||
|
||||
const submitButton = this.querySelector('button[type="submit"]');
|
||||
const originalText = submitButton.innerHTML;
|
||||
submitButton.disabled = true;
|
||||
submitButton.innerHTML = '<i class="fa fa-spinner fa-spin mr-2"></i> 注册中...';
|
||||
|
||||
const formData = new FormData(registerForm);
|
||||
fetch('https://ugm.gvsds.com/Auth/Register', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
body: formData
|
||||
}).then(res => res.json()).then(result => {
|
||||
submitButton.disabled = false;
|
||||
submitButton.innerHTML = originalText;
|
||||
if (result.code === 0) {
|
||||
showToast('注册成功,即将跳转...', 'success');
|
||||
setTimeout(() => {
|
||||
window.location.href = result.redirect || '/'
|
||||
}, 500)
|
||||
} else {
|
||||
showToast(result.msg || '注册失败')
|
||||
}
|
||||
}).catch(err => {
|
||||
submitButton.disabled = false;
|
||||
submitButton.innerHTML = originalText;
|
||||
showToast('网络错误:' + err.message)
|
||||
});
|
||||
});
|
||||
})
|
||||
</script>
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -221,12 +221,17 @@ class User(QModel):
|
||||
汇总所有角色的操作权限后判断。
|
||||
"""
|
||||
from gvsds.apps.gerp.models import Permission, RolePermission, UserRole
|
||||
user_role_uuids = UserRole.objects.filter(
|
||||
user_roles = UserRole.objects.filter(
|
||||
UserUUID=self.UserUUID
|
||||
).values_list('RoleUUID_id', flat=True)
|
||||
perm_uuids = RolePermission.objects.filter(
|
||||
)
|
||||
user_role_uuids = user_roles.values_list('RoleUUID_id', flat=True)
|
||||
company_uuids = list(set(user_roles.values_list('CompanyUUID_id', flat=True)))
|
||||
rp_qs = RolePermission.objects.filter(
|
||||
RoleUUID_id__in=user_role_uuids
|
||||
).values_list('PermUUID_id', flat=True)
|
||||
)
|
||||
if company_uuids:
|
||||
rp_qs = rp_qs.filter(CompanyUUID_id__in=company_uuids)
|
||||
perm_uuids = rp_qs.values_list('PermUUID_id', flat=True)
|
||||
return Permission.objects.filter(
|
||||
PermUUID__in=perm_uuids,
|
||||
PermCode=perm_code,
|
||||
|
||||
@@ -136,13 +136,12 @@ class SubdomainView(View):
|
||||
if subdomain == 'ugm':
|
||||
elt_token = request.COOKIES.get('ELToken')
|
||||
if not elt_token:
|
||||
# 未登录 → 跳转登录页
|
||||
# 未登录 → 跳转 www 登录页
|
||||
if file_path and file_path not in ('/', 'login.html', 'register.html'):
|
||||
from django.urls import reverse
|
||||
login_url = f"https://ugm.gvsds.com/login.html?redirect=https://gerp.gvsds.com/"
|
||||
login_url = f"https://www.gvsds.com/login.html?redirect=https://gerp.gvsds.com/"
|
||||
return HttpResponseRedirect(login_url)
|
||||
if not file_path or file_path == '/':
|
||||
return HttpResponseRedirect('https://ugm.gvsds.com/login.html')
|
||||
return HttpResponseRedirect('https://www.gvsds.com/login.html')
|
||||
else:
|
||||
# 已登录 → 跳转 gerp
|
||||
if not file_path or file_path == '/' or file_path == 'index.html':
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -2,6 +2,7 @@ import uuid
|
||||
import datetime
|
||||
from django.db import connection
|
||||
from django.db.models import Q
|
||||
from django.db.models.fields import NOT_PROVIDED
|
||||
|
||||
from gvsds.apps.gerp.models import (
|
||||
Tenant, CompanyInfo, Company, Department, Role, UserRole, RolePermission,
|
||||
@@ -16,6 +17,61 @@ from gvsdsdk.order.models import Order, OrderItem, OrderLevel, OrderTemplate, Or
|
||||
from gvsds.apps.gerp.i18n import translate_field, translate_table
|
||||
|
||||
|
||||
# 系统默认公司 UUID(GERP 后台)
|
||||
SYSTEM_COMPANY_UUID = uuid.UUID('00000000-0000-0000-0000-000000000001').bytes
|
||||
|
||||
|
||||
def create_default_role_for_user(user, admin_user=None):
|
||||
"""为新用户创建默认角色及用户-角色关联。
|
||||
|
||||
角色名格式为"{用户名}的默认角色",状态为启用。
|
||||
默认绑定系统公司(00000000-0000-0000-0000-000000000001),
|
||||
角色租户取自该系统公司所属租户,回退到管理员租户或系统首个租户。
|
||||
|
||||
Args:
|
||||
user: 新创建的 User 实例
|
||||
admin_user: 可选,操作管理员 User 实例(用于租户回退)
|
||||
"""
|
||||
try:
|
||||
company_uuid = SYSTEM_COMPANY_UUID
|
||||
# 角色租户优先取自系统公司所属租户
|
||||
tenant_uuid = None
|
||||
try:
|
||||
company = Company.objects.get(CompanyUUID=company_uuid)
|
||||
tenant_uuid = company.TenantUUID_id
|
||||
except Company.DoesNotExist:
|
||||
pass
|
||||
# 回退:当前操作管理员的首个角色租户
|
||||
if not tenant_uuid and admin_user is not None:
|
||||
admin_ur = UserRole.objects.filter(
|
||||
UserUUID_id=admin_user.UserUUID
|
||||
).select_related('RoleUUID').first()
|
||||
if admin_ur and admin_ur.RoleUUID_id and admin_ur.RoleUUID:
|
||||
tenant_uuid = admin_ur.RoleUUID.TenantUUID_id
|
||||
# 回退:系统首个租户
|
||||
if not tenant_uuid:
|
||||
tenant = Tenant.objects.first()
|
||||
if tenant:
|
||||
tenant_uuid = tenant.TenantUUID
|
||||
if not tenant_uuid:
|
||||
return
|
||||
user_name = user.UserName or '用户'
|
||||
role = Role.objects.create(
|
||||
RoleUUID=uuid.uuid4().bytes,
|
||||
TenantUUID_id=tenant_uuid,
|
||||
RoleName=f"{user_name}的默认角色",
|
||||
RoleStatus=1,
|
||||
)
|
||||
UserRole.objects.create(
|
||||
UserRoleUUID=uuid.uuid4().bytes,
|
||||
UserUUID_id=user.UserUUID,
|
||||
RoleUUID_id=role.RoleUUID,
|
||||
CompanyUUID_id=company_uuid,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _pascalize_record(record, model=None):
|
||||
result = {}
|
||||
column_map = {}
|
||||
@@ -87,8 +143,65 @@ class FormService:
|
||||
# 只读表:普通用户只能查询,不能增删改
|
||||
READONLY_TABLES_FOR_NORMAL = {'Company', 'CompanyInfo', 'Tenant', 'Role', 'Password', 'EphemeralToken'}
|
||||
|
||||
def __init__(self, user):
|
||||
def __init__(self, user, current_company_uuid=None, admin_verify=False, current_role_uuid=None):
|
||||
self.user = user
|
||||
self.current_role_uuid = None
|
||||
if current_role_uuid:
|
||||
if hasattr(current_role_uuid, 'bytes'):
|
||||
self.current_role_uuid = current_role_uuid.bytes
|
||||
elif isinstance(current_role_uuid, str):
|
||||
try:
|
||||
self.current_role_uuid = uuid.UUID(current_role_uuid).bytes
|
||||
except ValueError:
|
||||
self.current_role_uuid = current_role_uuid
|
||||
else:
|
||||
self.current_role_uuid = current_role_uuid
|
||||
# 当前角色所在公司的 UUID,统一转为 bytes 用于查询
|
||||
# 可能传入的类型:Company 实例、UUIDObj、uuid.UUID、bytes、str
|
||||
if current_company_uuid is not None:
|
||||
if hasattr(current_company_uuid, 'pk'):
|
||||
# Company 模型实例
|
||||
self.current_company_uuid = current_company_uuid.pk
|
||||
elif hasattr(current_company_uuid, 'bytes'):
|
||||
# UUIDObj 或 uuid.UUID
|
||||
self.current_company_uuid = current_company_uuid.bytes
|
||||
elif isinstance(current_company_uuid, str):
|
||||
try:
|
||||
self.current_company_uuid = uuid.UUID(current_company_uuid).bytes
|
||||
except ValueError:
|
||||
self.current_company_uuid = current_company_uuid
|
||||
else:
|
||||
self.current_company_uuid = current_company_uuid
|
||||
else:
|
||||
self.current_company_uuid = None
|
||||
# 强公司隔离:默认启用,仅当 admin_verify=True 且用户是管理员时才关闭
|
||||
if admin_verify and self._is_admin(user):
|
||||
self.company_scope = False
|
||||
else:
|
||||
self.company_scope = True
|
||||
import logging
|
||||
logging.info('[FormService] __init__: user=%s, company_scope=%s, current_company_uuid=%s (type=%s)',
|
||||
user, self.company_scope,
|
||||
self.current_company_uuid.hex() if isinstance(self.current_company_uuid, bytes) else self.current_company_uuid,
|
||||
type(self.current_company_uuid).__name__ if self.current_company_uuid else 'None')
|
||||
|
||||
def _is_admin(self, user):
|
||||
"""判断用户是否为管理员:超级用户或当前角色拥有 admin:super 权限"""
|
||||
if not user or not hasattr(user, 'is_authenticated') or not user.is_authenticated:
|
||||
return False
|
||||
# 超级用户始终是管理员
|
||||
if getattr(user, 'IsSuperuser', False):
|
||||
return True
|
||||
# 通过 RolePermission 判断:当前角色是否拥有 admin:super 权限
|
||||
if self.current_role_uuid:
|
||||
try:
|
||||
return RolePermission.objects.filter(
|
||||
RoleUUID_id=self.current_role_uuid,
|
||||
PermUUID__PermCode='admin:super'
|
||||
).exists()
|
||||
except Exception:
|
||||
pass
|
||||
return False
|
||||
|
||||
def _build_field_map(self, model):
|
||||
pascal_map = {}
|
||||
@@ -98,6 +211,80 @@ class FormService:
|
||||
pascal_map[f.name] = f
|
||||
return pascal_map
|
||||
|
||||
def _get_entity_picker_fields(self, model):
|
||||
"""获取模型中所有 entityPicker 类型的字段信息"""
|
||||
result = []
|
||||
for f in model._meta.get_fields():
|
||||
if not hasattr(f, 'name'):
|
||||
continue
|
||||
# ForeignKey 字段
|
||||
if f.__class__.__name__ == 'ForeignKey':
|
||||
try:
|
||||
related_model = getattr(f, 'related_model', None)
|
||||
if related_model:
|
||||
related_name = related_model.__name__
|
||||
ep_config = self.ENTITY_PICKER_MAP.get(related_name)
|
||||
if ep_config:
|
||||
result.append({
|
||||
'field_name': f.name,
|
||||
'label_field': ep_config['labelField'],
|
||||
'value_field': ep_config['valueField'],
|
||||
'entity_name': related_name,
|
||||
'extra_fields': ep_config.get('extraFields', []),
|
||||
})
|
||||
except Exception:
|
||||
pass
|
||||
# UUIDField 字段(非 ForeignKey)
|
||||
elif f.__class__.__name__ == 'UUIDField' and not getattr(f, 'primary_key', False):
|
||||
entity_name = self.UUID_FIELD_ENTITY_MAP.get(f.name)
|
||||
if entity_name:
|
||||
ep_config = self.ENTITY_PICKER_MAP.get(entity_name)
|
||||
if ep_config:
|
||||
result.append({
|
||||
'field_name': f.name,
|
||||
'label_field': ep_config['labelField'],
|
||||
'value_field': ep_config['valueField'],
|
||||
'entity_name': entity_name,
|
||||
'extra_fields': ep_config.get('extraFields', []),
|
||||
})
|
||||
return result
|
||||
|
||||
def _enrich_entity_labels(self, page_qs, ep_fields, records):
|
||||
"""为 records 中的 entityPicker 字段添加 _label 后缀字段,以及 extraFields"""
|
||||
try:
|
||||
obj_list = list(page_qs)
|
||||
except Exception:
|
||||
return records
|
||||
for i, obj in enumerate(obj_list):
|
||||
if i >= len(records):
|
||||
break
|
||||
for ep_info in ep_fields:
|
||||
field_name = ep_info['field_name']
|
||||
label_field = ep_info['label_field']
|
||||
extra_fields = ep_info.get('extra_fields', [])
|
||||
try:
|
||||
related_obj = getattr(obj, field_name, None)
|
||||
if related_obj is not None:
|
||||
# 获取 db_column 作为 key 前缀
|
||||
db_col = None
|
||||
for f in obj._meta.get_fields():
|
||||
if hasattr(f, 'name') and f.name == field_name and hasattr(f, 'db_column'):
|
||||
db_col = f.db_column or f.name
|
||||
break
|
||||
key_prefix = db_col or field_name
|
||||
# label 字段
|
||||
label = getattr(related_obj, label_field, None)
|
||||
if label is not None:
|
||||
records[i][key_prefix + '_label'] = str(label)
|
||||
# extraFields 字段
|
||||
for ef in extra_fields:
|
||||
ef_val = getattr(related_obj, ef, None)
|
||||
if ef_val is not None:
|
||||
records[i][key_prefix + '_' + ef] = ef_val
|
||||
except Exception:
|
||||
pass
|
||||
return records
|
||||
|
||||
def _resolve_field_name(self, key, pascal_map):
|
||||
if key in pascal_map:
|
||||
return pascal_map[key].name
|
||||
@@ -107,6 +294,17 @@ class FormService:
|
||||
if val is None or val == '':
|
||||
if getattr(field, 'null', False):
|
||||
return None
|
||||
# 非空字段:数值类型返回默认值或0,其他类型返回空字符串
|
||||
field_type = field.__class__.__name__
|
||||
if field_type in ('SmallIntegerField', 'IntegerField', 'BigIntegerField',
|
||||
'PositiveIntegerField', 'PositiveSmallIntegerField',
|
||||
'FloatField', 'DecimalField'):
|
||||
default = getattr(field, 'default', None)
|
||||
if default is not None and default is not NOT_PROVIDED:
|
||||
return default
|
||||
return 0
|
||||
if field_type == 'BooleanField':
|
||||
return False
|
||||
return val
|
||||
field_type = field.__class__.__name__
|
||||
if field_type == 'UUIDField' and isinstance(val, str):
|
||||
@@ -138,8 +336,16 @@ class FormService:
|
||||
return datetime.datetime.fromisoformat(val)
|
||||
except (ValueError, TypeError):
|
||||
return val
|
||||
if hasattr(field, 'max_length') and isinstance(val, str) and len(val) > field.max_length:
|
||||
val = val[:field.max_length]
|
||||
if field_type in ('BooleanField', 'NullBooleanField') and isinstance(val, str):
|
||||
low = val.strip().lower()
|
||||
if low in ('true', '1', 't', 'yes'):
|
||||
return True
|
||||
if low in ('false', '0', 'f', 'no'):
|
||||
return False
|
||||
return val
|
||||
max_length = getattr(field, 'max_length', None)
|
||||
if max_length and isinstance(val, str) and len(val) > max_length:
|
||||
val = val[:max_length]
|
||||
return val
|
||||
|
||||
def get_tables(self):
|
||||
@@ -166,6 +372,7 @@ class FormService:
|
||||
'labelField': 'RoleName',
|
||||
'valueField': 'RoleUUID',
|
||||
'placeholder': '输入角色名搜索...',
|
||||
'extraFields': ['RoleStatus'],
|
||||
},
|
||||
'User': {
|
||||
'api': '/API/FormAPI/UserSearch',
|
||||
@@ -223,6 +430,12 @@ class FormService:
|
||||
'valueField': 'MenuUUID',
|
||||
'placeholder': '输入菜单名搜索...',
|
||||
},
|
||||
'Workgroup': {
|
||||
'api': '/API/FormAPI/WorkgroupSearch',
|
||||
'labelField': 'WorkgroupName',
|
||||
'valueField': 'WorkgroupUUID',
|
||||
'placeholder': '输入工作组名搜索...',
|
||||
},
|
||||
}
|
||||
|
||||
# UUID 字段名后缀到关联模型的映射(用于 UUIDField 非 ForeignKey 的场景)
|
||||
@@ -271,6 +484,10 @@ class FormService:
|
||||
'primaryKey': is_pk,
|
||||
'hideInForm': is_pk,
|
||||
}
|
||||
# 强公司隔离:CompanyUUID 由后端自动注入,前端不可编辑
|
||||
if self.company_scope and f.name == 'CompanyUUID':
|
||||
col['editable'] = False
|
||||
col['hideInForm'] = True
|
||||
if field_type == 'select' and hasattr(f, 'choices') and f.choices:
|
||||
col['options'] = [{'value': k, 'label': str(v)} for k, v in f.choices]
|
||||
if field_type == 'number':
|
||||
@@ -358,6 +575,59 @@ class FormService:
|
||||
qs = model.objects.all()
|
||||
except Exception:
|
||||
return {'records': [], 'total': 0}
|
||||
|
||||
# 强公司隔离:非管理员自动启用,仅返回当前公司数据
|
||||
if self.company_scope:
|
||||
field_names = {f.name for f in model._meta.get_fields() if hasattr(f, 'name')}
|
||||
if 'CompanyUUID' in field_names:
|
||||
if not self.current_company_uuid:
|
||||
# 无公司上下文时返回空结果
|
||||
import logging
|
||||
logging.warning('[FormService] company_scope=True but current_company_uuid is None, user=%s', self.user)
|
||||
return {'records': [], 'total': 0, 'page': 1, 'PageSize': 20}
|
||||
# 判断 CompanyUUID 字段类型:ForeignKey 用 _id 后缀,UUIDField 直接用字段名
|
||||
company_field = None
|
||||
for f in model._meta.get_fields():
|
||||
if hasattr(f, 'name') and f.name == 'CompanyUUID':
|
||||
company_field = f
|
||||
break
|
||||
if company_field and company_field.__class__.__name__ == 'ForeignKey':
|
||||
qs = qs.filter(CompanyUUID_id=self.current_company_uuid)
|
||||
else:
|
||||
# UUIDField(包括 Company 表自身的主键)
|
||||
qs = qs.filter(CompanyUUID=self.current_company_uuid)
|
||||
|
||||
# 精确字段过滤:URL 参数中与模型字段名匹配的参数用于精确过滤
|
||||
# 已知的分页/搜索参数不参与过滤
|
||||
_skip_params = {'keyword', 'PageIndex', 'PageSize', 'page', 'format'}
|
||||
field_names = {f.name for f in model._meta.get_fields() if hasattr(f, 'name')}
|
||||
for key, value in params.items():
|
||||
if key in _skip_params:
|
||||
continue
|
||||
if key in field_names and value:
|
||||
# UUID 字段需要转换
|
||||
field_obj = None
|
||||
for f in model._meta.get_fields():
|
||||
if hasattr(f, 'name') and f.name == key:
|
||||
field_obj = f
|
||||
break
|
||||
if field_obj and field_obj.__class__.__name__ == 'ForeignKey':
|
||||
# ForeignKey: 使用 _id 后缀传入原始 PK 值
|
||||
try:
|
||||
uuid_bytes = uuid.UUID(str(value)).bytes
|
||||
qs = qs.filter(**{key + '_id': uuid_bytes})
|
||||
except (ValueError, AttributeError):
|
||||
qs = qs.filter(**{key + '_id': value})
|
||||
elif field_obj and field_obj.__class__.__name__ == 'UUIDField':
|
||||
# 普通UUIDField: 直接转 bytes
|
||||
try:
|
||||
uuid_bytes = uuid.UUID(str(value)).bytes
|
||||
qs = qs.filter(**{key: uuid_bytes})
|
||||
except (ValueError, AttributeError):
|
||||
qs = qs.filter(**{key: value})
|
||||
else:
|
||||
qs = qs.filter(**{key: value})
|
||||
|
||||
keyword = params.get('keyword', '')
|
||||
if keyword:
|
||||
or_conditions = {}
|
||||
@@ -380,8 +650,32 @@ class FormService:
|
||||
try:
|
||||
total = qs.count()
|
||||
start = (page - 1) * page_size
|
||||
records = [_pascalize_record(r, model) for r in qs[start:start + page_size].values()]
|
||||
except Exception:
|
||||
# 识别 entityPicker 字段,使用 select_related 预加载关联实体
|
||||
ep_fields = self._get_entity_picker_fields(model)
|
||||
if ep_fields:
|
||||
fk_names = [f['field_name'] for f in ep_fields]
|
||||
qs = qs.select_related(*fk_names)
|
||||
page_qs = qs[start:start + page_size]
|
||||
# 先获取模型实例列表(用于 _enrich_entity_labels)
|
||||
obj_list = list(page_qs)
|
||||
# 将模型实例转为字典再 pascalize
|
||||
records = []
|
||||
for obj in obj_list:
|
||||
row = {}
|
||||
for f in model._meta.concrete_fields:
|
||||
f_name = f.name
|
||||
# ForeignKey 的 attname 是 xxx_id,存储的是原始值
|
||||
if f.__class__.__name__ == 'ForeignKey':
|
||||
f_name = f.attname # e.g. UserUUID_id -> 但实际 attname 是字段名_id
|
||||
val = getattr(obj, f_name, None)
|
||||
row[f.name] = val
|
||||
records.append(_pascalize_record(row, model))
|
||||
# 为 entityPicker 字段附带关联实体的 label
|
||||
if ep_fields:
|
||||
records = self._enrich_entity_labels(obj_list, ep_fields, records)
|
||||
except Exception as e:
|
||||
import logging
|
||||
logging.error('[FormService] get_table error: table=%s, error=%s', table_name, e, exc_info=True)
|
||||
total = 0
|
||||
records = []
|
||||
return {'records': records, 'total': total, 'page': page, 'PageSize': page_size}
|
||||
@@ -409,14 +703,45 @@ class FormService:
|
||||
is_fk = field.__class__.__name__ == 'ForeignKey'
|
||||
attr_name = field.attname if is_fk else resolved
|
||||
create_data[attr_name] = val
|
||||
# 强公司隔离:当启用 company_scope 且当前角色有公司上下文时,
|
||||
# 自动注入当前公司 UUID(若模型有 CompanyUUID 字段且未显式提供)
|
||||
if self.company_scope and self.current_company_uuid:
|
||||
field_names = {f.name for f in model._meta.get_fields() if hasattr(f, 'name')}
|
||||
if 'CompanyUUID' in field_names:
|
||||
pk_field = model._meta.pk
|
||||
if pk_field and pk_field.name == 'CompanyUUID':
|
||||
# CompanyUUID 是主键(如 Company 表):直接设置,跳过后续 PK 自动生成
|
||||
if 'CompanyUUID' not in create_data:
|
||||
create_data['CompanyUUID'] = self.current_company_uuid
|
||||
else:
|
||||
# CompanyUUID 是外键:使用 _id 后缀
|
||||
has_company = any(
|
||||
k == 'CompanyUUID' or k == 'CompanyUUID_id' for k in create_data
|
||||
)
|
||||
if not has_company:
|
||||
create_data['CompanyUUID_id'] = self.current_company_uuid
|
||||
pk_field_name = model._meta.pk.name if model._meta.pk else None
|
||||
if pk_field_name and pk_field_name not in create_data:
|
||||
pk_field = model._meta.pk
|
||||
if pk_field.__class__.__name__ == 'UUIDField':
|
||||
create_data[pk_field_name] = uuid.uuid4().bytes
|
||||
obj = model.objects.create(**create_data)
|
||||
# 特殊处理:User 表的密码需要 bcrypt 加密,不能明文存储
|
||||
if table_name == 'User' and 'UserPassword' in create_data:
|
||||
raw_password = create_data.pop('UserPassword')
|
||||
obj = model(**create_data)
|
||||
obj.SetPassword(raw_password)
|
||||
obj.save()
|
||||
else:
|
||||
obj = model.objects.create(**create_data)
|
||||
# 新增用户时自动创建默认角色(与 register 流程保持一致)
|
||||
if table_name == 'User':
|
||||
self._create_default_role_for_user(obj)
|
||||
return {'PK': str(obj.pk) if obj.pk else None}
|
||||
|
||||
def _create_default_role_for_user(self, user):
|
||||
"""为新用户创建默认角色及用户-角色关联,委托给模块级函数。"""
|
||||
create_default_role_for_user(user, admin_user=self.user)
|
||||
|
||||
def modify_record(self, data):
|
||||
table_name = data.get('Table', '')
|
||||
model = self.TABLE_MAP.get(table_name)
|
||||
@@ -433,6 +758,9 @@ class FormService:
|
||||
obj = model.objects.get(**{pk_field_name: pk_value})
|
||||
except model.DoesNotExist:
|
||||
return None
|
||||
# 强公司隔离:company_scope 启用时校验目标记录归属当前公司
|
||||
if self.company_scope and self.current_company_uuid and not self._check_company_owner(obj):
|
||||
return None
|
||||
pascal_map = self._build_field_map(model)
|
||||
update_fields = []
|
||||
for key, val in data.items():
|
||||
@@ -449,6 +777,11 @@ class FormService:
|
||||
if isinstance(val, list) and len(val) == 1:
|
||||
val = val[0]
|
||||
val = self._convert_value_for_field(field, val)
|
||||
# 特殊处理:User 表的密码需要 bcrypt 加密
|
||||
if table_name == 'User' and resolved == 'UserPassword' and val:
|
||||
obj.SetPassword(val)
|
||||
update_fields.append('UserPassword')
|
||||
continue
|
||||
# ForeignKey 字段必须通过 attname(如 CompanyUUID_id)设置原始列值,
|
||||
# 否则 Django 的 FK 描述符会要求传入关联模型实例
|
||||
is_fk = field.__class__.__name__ == 'ForeignKey'
|
||||
@@ -475,10 +808,33 @@ class FormService:
|
||||
pk_field = model._meta.pk
|
||||
pk_value = self._convert_value_for_field(pk_field, pk)
|
||||
try:
|
||||
model.objects.get(**{pk_field_name: pk_value}).delete()
|
||||
return True
|
||||
obj = model.objects.get(**{pk_field_name: pk_value})
|
||||
except model.DoesNotExist:
|
||||
return False
|
||||
# 强公司隔离:company_scope 启用时校验目标记录归属当前公司
|
||||
if self.company_scope and self.current_company_uuid and not self._check_company_owner(obj):
|
||||
return False
|
||||
obj.delete()
|
||||
return True
|
||||
|
||||
def _check_company_owner(self, obj):
|
||||
"""校验对象是否归属当前公司(用于 company_scope 下的修改/删除保护)。
|
||||
若模型无 CompanyUUID 字段则放行(不隔离)。
|
||||
CompanyUUID 可能是外键(Department/Workgroup)或主键(Company 本身)。"""
|
||||
try:
|
||||
field_names = {f.name for f in obj._meta.get_fields() if hasattr(f, 'name')}
|
||||
if 'CompanyUUID' not in field_names:
|
||||
return True
|
||||
# self.current_company_uuid 已在 __init__ 中统一转为 bytes
|
||||
# CompanyUUID 是主键时直接比较 pk
|
||||
pk_field = obj._meta.pk
|
||||
if pk_field and pk_field.name == 'CompanyUUID':
|
||||
return obj.pk == self.current_company_uuid
|
||||
# CompanyUUID 是外键时比较 _id
|
||||
obj_company = getattr(obj, 'CompanyUUID_id', None)
|
||||
return obj_company == self.current_company_uuid
|
||||
except Exception:
|
||||
return True
|
||||
|
||||
def code_display(self, data):
|
||||
table_name = data.get('Table', '')
|
||||
|
||||
@@ -12,6 +12,6 @@ class Migration(migrations.Migration):
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='tenant',
|
||||
name='TenantCode',
|
||||
name='tenant_code',
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,36 @@
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import datetime
|
||||
import gvsds.apps.core.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('gerp', '0004_remove_tenant_code'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
# RolePermission 表已通过 --run-syncdb 在数据库中创建(含 CompanyUUID 列),
|
||||
# 此迁移仅同步迁移状态,不对数据库执行任何操作。
|
||||
migrations.SeparateDatabaseAndState(
|
||||
state_operations=[
|
||||
migrations.CreateModel(
|
||||
name='RolePermission',
|
||||
fields=[
|
||||
('role_perm_uuid', gvsds.apps.core.models.UUIDField(db_column='RolePermUUID', primary_key=True, serialize=False)),
|
||||
('create_time', models.DateTimeField(db_column='CreateTime', default=datetime.datetime.utcnow)),
|
||||
('role_uuid', models.ForeignKey(db_column='RoleUUID', on_delete=django.db.models.deletion.CASCADE, related_name='rolepermission_set', to='gerp.role', verbose_name='角色UUID')),
|
||||
('perm_uuid', models.ForeignKey(db_column='PermUUID', on_delete=django.db.models.deletion.CASCADE, related_name='rolepermission_set', to='gerp.permission', verbose_name='权限UUID')),
|
||||
('company_uuid', models.ForeignKey(db_column='CompanyUUID', on_delete=django.db.models.deletion.CASCADE, related_name='rolepermission_set', to='gerp.company', verbose_name='公司UUID')),
|
||||
],
|
||||
options={
|
||||
'db_table': 'RolePermission',
|
||||
'verbose_name': '角色权限关联',
|
||||
'verbose_name_plural': '角色权限关联',
|
||||
},
|
||||
),
|
||||
],
|
||||
database_operations=[],
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,22 @@
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('gerp', '0005_rolepermission_companyuuid'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='userrole',
|
||||
name='DeptUUID',
|
||||
field=models.ForeignKey(blank=True, db_column='DeptUUID', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='gerp.department', verbose_name='部门UUID'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='userrole',
|
||||
name='WorkgroupUUID',
|
||||
field=models.ForeignKey(blank=True, db_column='WorkgroupUUID', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='gerp.workgroup', verbose_name='工作组UUID'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,16 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('gerp', '0006_userrole_dept_workgroup'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='userrole',
|
||||
name='RoleType',
|
||||
field=models.SmallIntegerField(db_column='RoleType', default=0, verbose_name='角色类型'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,17 @@
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('gerp', '0007_userrole_roletype'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='workgroup',
|
||||
name='CompanyUUID',
|
||||
field=models.ForeignKey(db_column='CompanyUUID', on_delete=django.db.models.deletion.CASCADE, related_name='+', to='gerp.company', verbose_name='公司UUID'),
|
||||
),
|
||||
]
|
||||
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+60
-30
@@ -46,7 +46,7 @@ class CompanyInfo(QModel):
|
||||
"""企业信息表——工商注册与企业详情"""
|
||||
|
||||
CompanyInfoUUID = UUIDField(primary_key=True, verbose_name='企业信息UUID')
|
||||
TenantUUID = models.ForeignKey('Tenant', on_delete=models.SET_NULL, null=True, blank=True, related_name='+', verbose_name='租户UUID')
|
||||
TenantUUID = models.ForeignKey('Tenant', on_delete=models.SET_NULL, null=True, blank=True, related_name='+', db_column='TenantUUID', verbose_name='租户UUID')
|
||||
EnterpriseName = models.CharField(max_length=256, verbose_name='企业全称')
|
||||
EnterpriseShortName = models.CharField(max_length=100, verbose_name='企业简称')
|
||||
CreditCode = models.CharField(max_length=64, unique=True, verbose_name='统一社会信用代码')
|
||||
@@ -135,8 +135,8 @@ class Company(QModel):
|
||||
"""公司表——系统内组织机构"""
|
||||
|
||||
CompanyUUID = UUIDField(primary_key=True, verbose_name='公司UUID')
|
||||
TenantUUID = models.ForeignKey('Tenant', on_delete=models.CASCADE, related_name='+', verbose_name='租户UUID')
|
||||
ParentCompanyUUID = models.ForeignKey('self', on_delete=models.SET_NULL, null=True, blank=True, related_name='+', verbose_name='上级公司UUID')
|
||||
TenantUUID = models.ForeignKey('Tenant', on_delete=models.CASCADE, related_name='+', db_column='TenantUUID', verbose_name='租户UUID')
|
||||
ParentCompanyUUID = models.ForeignKey('self', on_delete=models.SET_NULL, null=True, blank=True, related_name='+', db_column='ParentCompanyUUID', verbose_name='上级公司UUID')
|
||||
CompanyName = models.CharField(max_length=256, verbose_name='公司全称')
|
||||
CompanyShort = models.CharField(max_length=100, verbose_name='公司简称')
|
||||
CompanyStatus = models.SmallIntegerField(default=1, verbose_name='公司状态')
|
||||
@@ -157,8 +157,8 @@ class Department(QModel):
|
||||
"""部门表——组织架构层级"""
|
||||
|
||||
DeptUUID = UUIDField(primary_key=True, verbose_name='部门UUID')
|
||||
CompanyUUID = models.ForeignKey('Company', on_delete=models.CASCADE, related_name='department_set', verbose_name='公司UUID')
|
||||
ParentDeptUUID = models.ForeignKey('self', on_delete=models.SET_NULL, null=True, blank=True, related_name='children', verbose_name='上级部门UUID')
|
||||
CompanyUUID = models.ForeignKey('Company', on_delete=models.CASCADE, related_name='department_set', db_column='CompanyUUID', verbose_name='公司UUID')
|
||||
ParentDeptUUID = models.ForeignKey('self', on_delete=models.SET_NULL, null=True, blank=True, related_name='children', db_column='ParentDeptUUID', verbose_name='上级部门UUID')
|
||||
DeptName = models.CharField(max_length=256, verbose_name='部门名称')
|
||||
DeptSort = models.IntegerField(default=0, verbose_name='排序')
|
||||
DeptStatus = models.SmallIntegerField(default=1, verbose_name='部门状态')
|
||||
@@ -186,7 +186,7 @@ class Role(QModel):
|
||||
"""
|
||||
|
||||
RoleUUID = UUIDField(primary_key=True, verbose_name='角色UUID')
|
||||
TenantUUID = models.ForeignKey('Tenant', on_delete=models.CASCADE, related_name='+', verbose_name='租户UUID')
|
||||
TenantUUID = models.ForeignKey('Tenant', on_delete=models.CASCADE, related_name='+', db_column='TenantUUID', verbose_name='租户UUID')
|
||||
RoleName = models.CharField(max_length=128, verbose_name='角色名称')
|
||||
RoleType = models.SmallIntegerField(default=2, verbose_name='角色类型')
|
||||
RoleStatus = models.SmallIntegerField(default=1, verbose_name='角色状态')
|
||||
@@ -218,18 +218,44 @@ class Role(QModel):
|
||||
return User.objects.filter(userrole__RoleUUID=self.RoleUUID)
|
||||
|
||||
|
||||
class RoleAdmin(QModel):
|
||||
"""管理员角色表——标记哪些角色具有管理员权限
|
||||
|
||||
只要在本表中存在对应 RoleUUID 记录,该角色即为管理员角色。
|
||||
不存在则非管理员。通过单独表实现,避免在 Role 表上增加冗余字段。
|
||||
"""
|
||||
|
||||
RoleAdminUUID = UUIDField(primary_key=True, verbose_name='主键UUID')
|
||||
RoleUUID = models.ForeignKey('Role', on_delete=models.CASCADE, related_name='roleadmin_set', db_column='RoleUUID', verbose_name='角色UUID')
|
||||
CreateTime = models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间')
|
||||
|
||||
class Meta:
|
||||
db_table = 'RoleAdmin'
|
||||
app_label = 'gerp'
|
||||
verbose_name = '管理员角色'
|
||||
verbose_name_plural = '管理员角色'
|
||||
|
||||
def __str__(self):
|
||||
return f'RoleAdmin({self.RoleUUID})'
|
||||
|
||||
|
||||
class UserRole(QModel):
|
||||
"""用户角色关联表——用户与角色的多对多映射
|
||||
|
||||
通过 CompanyUUID 标识该用户-角色关联所属的公司,
|
||||
用于菜单按公司差异化显示、数据权限按公司过滤等场景。
|
||||
同一用户在不同公司下可绑定不同角色。
|
||||
DeptUUID、WorkgroupUUID 为可选字段,用于将关联进一步限定到部门或工作组。
|
||||
RoleType 标识该用户在部门/工作组中的角色类型:0=普通成员/部门员工,1=组长/部门负责人。
|
||||
"""
|
||||
|
||||
UserRoleUUID = UUIDField(primary_key=True, verbose_name='关联UUID')
|
||||
UserUUID = models.ForeignKey(User, on_delete=models.CASCADE, related_name='userrole_set', db_index=True, verbose_name='用户UUID')
|
||||
RoleUUID = models.ForeignKey('Role', on_delete=models.CASCADE, related_name='userrole_set', verbose_name='角色UUID')
|
||||
CompanyUUID = models.ForeignKey('Company', on_delete=models.CASCADE, related_name='userrole_set', verbose_name='公司UUID')
|
||||
UserUUID = models.ForeignKey(User, on_delete=models.CASCADE, related_name='userrole_set', db_column='UserUUID', db_index=True, verbose_name='用户UUID')
|
||||
RoleUUID = models.ForeignKey('Role', on_delete=models.CASCADE, related_name='userrole_set', db_column='RoleUUID', verbose_name='角色UUID')
|
||||
CompanyUUID = models.ForeignKey('Company', on_delete=models.CASCADE, related_name='userrole_set', db_column='CompanyUUID', verbose_name='公司UUID')
|
||||
DeptUUID = models.ForeignKey('Department', on_delete=models.SET_NULL, null=True, blank=True, related_name='+', db_column='DeptUUID', verbose_name='部门UUID')
|
||||
WorkgroupUUID = models.ForeignKey('Workgroup', on_delete=models.SET_NULL, null=True, blank=True, related_name='+', db_column='WorkgroupUUID', verbose_name='工作组UUID')
|
||||
RoleType = models.SmallIntegerField(default=0, db_column='RoleType', verbose_name='角色类型')
|
||||
CreateTime = models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间')
|
||||
|
||||
class Meta:
|
||||
@@ -244,11 +270,13 @@ class RolePermission(QModel):
|
||||
|
||||
一个角色可以拥有多个权限,一个权限也可以被多个角色引用。
|
||||
角色和具体操作权限为多对多绑定关系,一套权限集合可以打包在一个角色内统一维护。
|
||||
同一角色在不同公司下可拥有不同权限集合,通过 CompanyUUID 区分。
|
||||
"""
|
||||
|
||||
RolePermUUID = UUIDField(primary_key=True, verbose_name='关联UUID')
|
||||
RoleUUID = models.ForeignKey('Role', on_delete=models.CASCADE, related_name='rolepermission_set', verbose_name='角色UUID')
|
||||
PermUUID = models.ForeignKey('Permission', on_delete=models.CASCADE, related_name='rolepermission_set', verbose_name='权限UUID')
|
||||
RoleUUID = models.ForeignKey('Role', on_delete=models.CASCADE, related_name='rolepermission_set', db_column='RoleUUID', verbose_name='角色UUID')
|
||||
PermUUID = models.ForeignKey('Permission', on_delete=models.CASCADE, related_name='rolepermission_set', db_column='PermUUID', verbose_name='权限UUID')
|
||||
CompanyUUID = models.ForeignKey('Company', on_delete=models.CASCADE, related_name='rolepermission_set', db_column='CompanyUUID', verbose_name='公司UUID')
|
||||
CreateTime = models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间')
|
||||
|
||||
class Meta:
|
||||
@@ -267,7 +295,7 @@ class Permission(QModel):
|
||||
"""
|
||||
|
||||
PermUUID = UUIDField(primary_key=True, verbose_name='权限UUID')
|
||||
TenantUUID = models.ForeignKey('Tenant', on_delete=models.CASCADE, related_name='+', verbose_name='租户UUID')
|
||||
TenantUUID = models.ForeignKey('Tenant', on_delete=models.CASCADE, related_name='+', db_column='TenantUUID', verbose_name='租户UUID')
|
||||
PermName = models.CharField(max_length=128, verbose_name='权限名称')
|
||||
PermCode = models.CharField(max_length=128, db_index=True, verbose_name='权限键')
|
||||
PermCategory = models.CharField(max_length=64, default='', verbose_name='权限分类')
|
||||
@@ -293,7 +321,7 @@ class FileAsset(QModel):
|
||||
"""文件资产表——文件存储与引用管理"""
|
||||
|
||||
AssetUUID = UUIDField(primary_key=True, verbose_name='资产UUID')
|
||||
CompanyUUID = models.ForeignKey('Company', on_delete=models.SET_NULL, null=True, blank=True, related_name='fileasset_set', verbose_name='公司UUID')
|
||||
CompanyUUID = models.ForeignKey('Company', on_delete=models.SET_NULL, null=True, blank=True, related_name='fileasset_set', db_column='CompanyUUID', verbose_name='公司UUID')
|
||||
FileName = models.CharField(max_length=256, verbose_name='文件名')
|
||||
FileSHA1 = models.CharField(max_length=40, unique=True, verbose_name='文件SHA1')
|
||||
FileSize = models.IntegerField(verbose_name='文件大小')
|
||||
@@ -320,8 +348,8 @@ class WorkOrder(QModel):
|
||||
Category = models.CharField(max_length=64, default='other', verbose_name='分类')
|
||||
Priority = models.SmallIntegerField(default=0, verbose_name='优先级')
|
||||
Status = models.SmallIntegerField(default=0, verbose_name='状态')
|
||||
CreatorUUID = models.ForeignKey(User, on_delete=models.CASCADE, related_name='+', db_index=True, verbose_name='创建人UUID')
|
||||
AssigneeUUID = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name='+', verbose_name='指派人UUID')
|
||||
CreatorUUID = models.ForeignKey(User, on_delete=models.CASCADE, related_name='+', db_index=True, db_column='CreatorUUID', verbose_name='创建人UUID')
|
||||
AssigneeUUID = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name='+', db_column='AssigneeUUID', verbose_name='指派人UUID')
|
||||
Resolution = models.CharField(max_length=4096, default='', verbose_name='解决方案')
|
||||
CreateTime = models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间')
|
||||
UpdateTime = models.DateTimeField(null=True, blank=True, verbose_name='更新时间')
|
||||
@@ -339,8 +367,8 @@ class Message(QModel):
|
||||
"""消息表——系统通知与站内信"""
|
||||
|
||||
MessageUUID = UUIDField(primary_key=True, verbose_name='消息UUID')
|
||||
SenderUUID = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name='+', verbose_name='发送者UUID')
|
||||
ReceiverUUID = models.ForeignKey(User, on_delete=models.CASCADE, related_name='+', db_index=True, verbose_name='接收者UUID')
|
||||
SenderUUID = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name='+', db_column='SenderUUID', verbose_name='发送者UUID')
|
||||
ReceiverUUID = models.ForeignKey(User, on_delete=models.CASCADE, related_name='+', db_index=True, db_column='ReceiverUUID', verbose_name='接收者UUID')
|
||||
Title = models.CharField(max_length=256, verbose_name='标题')
|
||||
Content = models.TextField(default='', verbose_name='内容')
|
||||
MsgType = models.SmallIntegerField(default=0, verbose_name='消息类型')
|
||||
@@ -361,8 +389,8 @@ class Menu(QModel):
|
||||
"""菜单表——系统导航与功能入口"""
|
||||
|
||||
MenuUUID = UUIDField(primary_key=True, verbose_name='菜单UUID')
|
||||
CompanyUUID = models.ForeignKey('Company', on_delete=models.SET_NULL, null=True, blank=True, related_name='menu_set', verbose_name='公司UUID')
|
||||
ParentMenuUUID = models.ForeignKey('self', on_delete=models.SET_NULL, null=True, blank=True, related_name='children', verbose_name='上级菜单UUID')
|
||||
CompanyUUID = models.ForeignKey('Company', on_delete=models.SET_NULL, null=True, blank=True, related_name='menu_set', db_column='CompanyUUID', verbose_name='公司UUID')
|
||||
ParentMenuUUID = models.ForeignKey('self', on_delete=models.SET_NULL, null=True, blank=True, related_name='children', db_column='ParentMenuUUID', verbose_name='上级菜单UUID')
|
||||
MenuName = models.CharField(max_length=128, verbose_name='菜单名称')
|
||||
MenuTitle = models.CharField(max_length=128, verbose_name='菜单标题')
|
||||
MenuIcon = models.CharField(max_length=128, default='', verbose_name='菜单图标')
|
||||
@@ -425,9 +453,10 @@ class DepartmentSMTM(QModel):
|
||||
|
||||
|
||||
class Workgroup(QModel):
|
||||
"""工作组表——跨部门协作单元"""
|
||||
"""工作组表——跨部门协作单元,归属特定公司"""
|
||||
|
||||
WorkgroupUUID = UUIDField(primary_key=True, verbose_name='工作组UUID')
|
||||
CompanyUUID = models.ForeignKey('Company', on_delete=models.CASCADE, related_name='+', db_column='CompanyUUID', verbose_name='公司UUID')
|
||||
WorkgroupName = models.CharField(max_length=256, verbose_name='工作组名称')
|
||||
WorkgroupEnglishName = models.CharField(max_length=256, verbose_name='工作组英文名')
|
||||
WorkgroupIntroduction = models.CharField(max_length=1024, verbose_name='工作组介绍')
|
||||
@@ -475,8 +504,8 @@ class Backpack(QModel):
|
||||
"""背包表——用户物品栏"""
|
||||
|
||||
BackpackItemUUID = UUIDField(primary_key=True, verbose_name='背包物品UUID')
|
||||
UserUUID = models.ForeignKey(User, on_delete=models.CASCADE, related_name='+', verbose_name='用户UUID')
|
||||
ItemTemplateUUID = models.ForeignKey('ItemReference', on_delete=models.CASCADE, related_name='+', verbose_name='物品模板UUID')
|
||||
UserUUID = models.ForeignKey(User, on_delete=models.CASCADE, related_name='+', db_column='UserUUID', verbose_name='用户UUID')
|
||||
ItemTemplateUUID = models.ForeignKey('ItemReference', on_delete=models.CASCADE, related_name='+', db_column='ItemTemplateUUID', verbose_name='物品模板UUID')
|
||||
_ItemName = models.CharField(max_length=128, null=True, blank=True, verbose_name='物品名称缓存')
|
||||
_ItemIcon = models.CharField(max_length=512, null=True, blank=True, verbose_name='物品图标缓存')
|
||||
_ItemIconColor = models.CharField(max_length=6, null=True, blank=True, verbose_name='图标颜色缓存')
|
||||
@@ -675,7 +704,7 @@ class PersonalPage(QModel):
|
||||
class UserSettings(QModel):
|
||||
"""用户设置表——存储每位用户的个性化系统设置"""
|
||||
|
||||
UserUUID = models.ForeignKey(User, on_delete=models.CASCADE, unique=True, verbose_name='用户UUID')
|
||||
UserUUID = models.ForeignKey(User, on_delete=models.CASCADE, unique=True, db_column='UserUUID', verbose_name='用户UUID')
|
||||
SettingsData = models.TextField(default='', verbose_name='设置数据')
|
||||
|
||||
class Meta:
|
||||
@@ -696,7 +725,7 @@ class PositionTemplate(QModel):
|
||||
"""
|
||||
|
||||
PositionTemplateUUID = UUIDField(primary_key=True, verbose_name='岗位模板UUID')
|
||||
TenantUUID = models.ForeignKey('Tenant', on_delete=models.CASCADE, related_name='+', verbose_name='租户UUID')
|
||||
TenantUUID = models.ForeignKey('Tenant', on_delete=models.CASCADE, related_name='+', db_column='TenantUUID', verbose_name='租户UUID')
|
||||
TemplateName = models.CharField(max_length=128, verbose_name='模板名称')
|
||||
TemplateCode = models.CharField(max_length=64, default='', verbose_name='模板编码')
|
||||
TemplateStatus = models.SmallIntegerField(default=1, verbose_name='模板状态')
|
||||
@@ -736,6 +765,7 @@ class PositionTemplate(QModel):
|
||||
RolePermUUID=uuid.uuid4().bytes,
|
||||
RoleUUID=role,
|
||||
PermUUID=tp.PermUUID,
|
||||
CompanyUUID_id=tenant_uuid, # 岗位模板复制时默认使用租户UUID作为公司
|
||||
)
|
||||
return role
|
||||
|
||||
@@ -748,8 +778,8 @@ class PositionTemplatePermission(QModel):
|
||||
"""
|
||||
|
||||
PTPermUUID = UUIDField(primary_key=True, verbose_name='关联UUID')
|
||||
PositionTemplateUUID = models.ForeignKey('PositionTemplate', on_delete=models.CASCADE, related_name='positiontemplatepermission_set', verbose_name='岗位模板UUID')
|
||||
PermUUID = models.ForeignKey('Permission', on_delete=models.CASCADE, related_name='positiontemplatepermission_set', verbose_name='权限UUID')
|
||||
PositionTemplateUUID = models.ForeignKey('PositionTemplate', on_delete=models.CASCADE, related_name='positiontemplatepermission_set', db_column='PositionTemplateUUID', verbose_name='岗位模板UUID')
|
||||
PermUUID = models.ForeignKey('Permission', on_delete=models.CASCADE, related_name='positiontemplatepermission_set', db_column='PermUUID', verbose_name='权限UUID')
|
||||
CreateTime = models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间')
|
||||
|
||||
class Meta:
|
||||
@@ -768,7 +798,7 @@ class Position(QModel):
|
||||
"""
|
||||
|
||||
PositionUUID = UUIDField(primary_key=True, verbose_name='职务UUID')
|
||||
TenantUUID = models.ForeignKey('Tenant', on_delete=models.CASCADE, related_name='+', verbose_name='租户UUID')
|
||||
TenantUUID = models.ForeignKey('Tenant', on_delete=models.CASCADE, related_name='+', db_column='TenantUUID', verbose_name='租户UUID')
|
||||
PositionName = models.CharField(max_length=128, verbose_name='职务名称')
|
||||
PositionCode = models.CharField(max_length=64, default='', verbose_name='职务编码')
|
||||
PositionLevel = models.SmallIntegerField(default=0, verbose_name='职务级别')
|
||||
@@ -797,9 +827,9 @@ class UserDept(QModel):
|
||||
"""
|
||||
|
||||
UserDeptUUID = UUIDField(primary_key=True, verbose_name='关联UUID')
|
||||
UserUUID = models.ForeignKey(User, on_delete=models.CASCADE, related_name='userdept_set', db_index=True, verbose_name='用户UUID')
|
||||
DeptUUID = models.ForeignKey('Department', on_delete=models.CASCADE, related_name='userdept_set', verbose_name='部门UUID')
|
||||
PositionUUID = models.ForeignKey('Position', on_delete=models.SET_NULL, null=True, blank=True, related_name='userdept_set', verbose_name='职务标签UUID')
|
||||
UserUUID = models.ForeignKey(User, on_delete=models.CASCADE, related_name='userdept_set', db_index=True, db_column='UserUUID', verbose_name='用户UUID')
|
||||
DeptUUID = models.ForeignKey('Department', on_delete=models.CASCADE, related_name='userdept_set', db_column='DeptUUID', verbose_name='部门UUID')
|
||||
PositionUUID = models.ForeignKey('Position', on_delete=models.SET_NULL, null=True, blank=True, related_name='userdept_set', db_column='PositionUUID', verbose_name='职务标签UUID')
|
||||
IsMainDept = models.SmallIntegerField(default=1, verbose_name='是否主部门')
|
||||
CreateTime = models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间')
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ from .viewsets import (
|
||||
PositionTemplatePermissionViewSet,
|
||||
PositionViewSet,
|
||||
ShopViewSet,
|
||||
WorkgroupViewSet,
|
||||
)
|
||||
from .twofa import (
|
||||
TwoFASetupView,
|
||||
@@ -81,7 +82,7 @@ from .asset import (
|
||||
)
|
||||
from .message import MessageCompatView
|
||||
from .menu import MenuListCompatView
|
||||
from .form_api import FormAPICompatView, FormAPIView
|
||||
from .form_api import FormAPICompatView, FormAPIView, CheckAdminView
|
||||
from .doc import DocView, SerieView
|
||||
from .position import (
|
||||
PositionListView,
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -3,16 +3,45 @@ from . import ELTAuthenticatedAPIView
|
||||
from gvsds.apps.gerp.form_service import FormService
|
||||
|
||||
|
||||
def _is_admin(user):
|
||||
"""判断用户是否为管理员"""
|
||||
def _is_admin(request):
|
||||
"""判断用户是否为管理员:超级用户或当前角色拥有 admin:super 权限"""
|
||||
user = request.user if hasattr(request, 'user') else None
|
||||
if not user or not user.is_authenticated:
|
||||
return False
|
||||
return getattr(user, 'IsStaff', False) or getattr(user, 'IsSuperuser', False)
|
||||
if getattr(user, 'IsSuperuser', False):
|
||||
return True
|
||||
current_role = getattr(request, 'current_role', None)
|
||||
if current_role:
|
||||
from gvsds.apps.gerp.models import RolePermission
|
||||
role_uuid = getattr(current_role, 'RoleUUID', None)
|
||||
if role_uuid:
|
||||
return RolePermission.objects.filter(
|
||||
RoleUUID_id=role_uuid,
|
||||
PermUUID__PermCode='admin:super'
|
||||
).exists()
|
||||
return False
|
||||
|
||||
|
||||
def _check_table_access(table_name, action, user):
|
||||
def _make_service(request, user):
|
||||
"""构造 FormService,自动传入当前公司 UUID 和角色 UUID"""
|
||||
admin_verify = request.query_params.get('admin_verify') == '1' if hasattr(request, 'query_params') else False
|
||||
if not admin_verify and hasattr(request, 'data'):
|
||||
admin_verify = request.data.get('admin_verify') == '1'
|
||||
current_role = getattr(request, 'current_role', None)
|
||||
current_role_uuid = None
|
||||
if current_role:
|
||||
current_role_uuid = getattr(current_role, 'RoleUUID', None)
|
||||
return FormService(
|
||||
user,
|
||||
current_company_uuid=getattr(request, 'current_company_uuid', None),
|
||||
admin_verify=admin_verify,
|
||||
current_role_uuid=current_role_uuid,
|
||||
)
|
||||
|
||||
|
||||
def _check_table_access(table_name, action, request):
|
||||
"""检查用户对指定表的访问权限,返回错误响应或 None"""
|
||||
if _is_admin(user):
|
||||
if _is_admin(request):
|
||||
return None
|
||||
if table_name in FormService.ADMIN_ONLY_TABLES:
|
||||
return Response({'code': 403, 'msg': f'无权访问表 {table_name},仅管理员可操作'}, status=403)
|
||||
@@ -34,7 +63,7 @@ class FormAPICompatView(ELTAuthenticatedAPIView):
|
||||
|
||||
def get(self, request, **kwargs):
|
||||
user = request.user if hasattr(request, 'user') and request.user.is_authenticated else None
|
||||
service = FormService(user)
|
||||
service = _make_service(request, user)
|
||||
path = request.path.rstrip('/')
|
||||
table_name = kwargs.get('TableName', '')
|
||||
|
||||
@@ -45,7 +74,7 @@ class FormAPICompatView(ELTAuthenticatedAPIView):
|
||||
result = service.get_columns(table_name)
|
||||
return Response({'code': 0, 'data': result})
|
||||
elif '/Get/' in path and table_name:
|
||||
access_error = _check_table_access(table_name, 'get', user)
|
||||
access_error = _check_table_access(table_name, 'get', request)
|
||||
if access_error:
|
||||
return access_error
|
||||
result = service.get_table(table_name, request.query_params)
|
||||
@@ -57,7 +86,7 @@ class FormAPICompatView(ELTAuthenticatedAPIView):
|
||||
table_name = request.query_params.get('Table', '')
|
||||
if not table_name:
|
||||
return Response({'code': 1, 'msg': '缺少table参数'}, status=400)
|
||||
access_error = _check_table_access(table_name, 'get', user)
|
||||
access_error = _check_table_access(table_name, 'get', request)
|
||||
if access_error:
|
||||
return access_error
|
||||
result = service.get_table(table_name, request.query_params)
|
||||
@@ -65,13 +94,13 @@ class FormAPICompatView(ELTAuthenticatedAPIView):
|
||||
|
||||
def post(self, request, **kwargs):
|
||||
user = request.user if hasattr(request, 'user') and request.user.is_authenticated else None
|
||||
service = FormService(user)
|
||||
service = _make_service(request, user)
|
||||
path = request.path.rstrip('/')
|
||||
table_name = kwargs.get('TableName', '')
|
||||
item_id = kwargs.get('id', '')
|
||||
|
||||
if '/Add/' in path and table_name:
|
||||
access_error = _check_table_access(table_name, 'add', user)
|
||||
access_error = _check_table_access(table_name, 'add', request)
|
||||
if access_error:
|
||||
return access_error
|
||||
data = dict(request.data)
|
||||
@@ -79,7 +108,7 @@ class FormAPICompatView(ELTAuthenticatedAPIView):
|
||||
result = service.add_record(data)
|
||||
return Response({'code': 0, 'msg': '添加成功', 'data': result}, status=201)
|
||||
elif '/ModifyItem/' in path and table_name:
|
||||
access_error = _check_table_access(table_name, 'modify', user)
|
||||
access_error = _check_table_access(table_name, 'modify', request)
|
||||
if access_error:
|
||||
return access_error
|
||||
data = dict(request.data)
|
||||
@@ -91,7 +120,7 @@ class FormAPICompatView(ELTAuthenticatedAPIView):
|
||||
return Response({'code': 1, 'msg': '修改失败,记录不存在'}, status=404)
|
||||
return Response({'code': 0, 'msg': '修改成功', 'data': result})
|
||||
elif '/Modify/' in path and table_name:
|
||||
access_error = _check_table_access(table_name, 'modify', user)
|
||||
access_error = _check_table_access(table_name, 'modify', request)
|
||||
if access_error:
|
||||
return access_error
|
||||
data = dict(request.data)
|
||||
@@ -103,7 +132,7 @@ class FormAPICompatView(ELTAuthenticatedAPIView):
|
||||
return Response({'code': 1, 'msg': '修改失败,记录不存在'}, status=404)
|
||||
return Response({'code': 0, 'msg': '修改成功', 'data': result})
|
||||
elif '/Delete/' in path and table_name:
|
||||
access_error = _check_table_access(table_name, 'delete', user)
|
||||
access_error = _check_table_access(table_name, 'delete', request)
|
||||
if access_error:
|
||||
return access_error
|
||||
data = dict(request.data)
|
||||
@@ -118,7 +147,7 @@ class FormAPICompatView(ELTAuthenticatedAPIView):
|
||||
action = request.data.get('Action', 'add')
|
||||
table_name = request.data.get('Table', '')
|
||||
if table_name:
|
||||
access_error = _check_table_access(table_name, action, user)
|
||||
access_error = _check_table_access(table_name, action, request)
|
||||
if access_error:
|
||||
return access_error
|
||||
if action == 'add':
|
||||
@@ -167,10 +196,10 @@ class FormAPIView(ELTAuthenticatedAPIView):
|
||||
table_name = request.query_params.get('table', '')
|
||||
if not table_name:
|
||||
return Response({'code': 1, 'msg': '缺少table参数'}, status=400)
|
||||
access_error = _check_table_access(table_name, 'get', request.user)
|
||||
access_error = _check_table_access(table_name, 'get', request)
|
||||
if access_error:
|
||||
return access_error
|
||||
service = FormService(request.user)
|
||||
service = _make_service(request, request.user)
|
||||
result = service.get_table(table_name, request.query_params)
|
||||
return Response({'code': 0, 'data': result['records'], 'total': result['total']})
|
||||
|
||||
@@ -178,10 +207,10 @@ class FormAPIView(ELTAuthenticatedAPIView):
|
||||
action = request.data.get('Action', 'add')
|
||||
table_name = request.data.get('Table', '')
|
||||
if table_name:
|
||||
access_error = _check_table_access(table_name, action, request.user)
|
||||
access_error = _check_table_access(table_name, action, request)
|
||||
if access_error:
|
||||
return access_error
|
||||
service = FormService(request.user)
|
||||
service = _make_service(request, request.user)
|
||||
if action == 'add':
|
||||
result = service.add_record(request.data)
|
||||
return Response({'code': 0, 'msg': '添加成功', 'data': result}, status=201)
|
||||
@@ -205,3 +234,26 @@ class FormAPIView(ELTAuthenticatedAPIView):
|
||||
result = service.code_display(request.data)
|
||||
return Response({'code': 0, 'data': result})
|
||||
return Response({'code': 1, 'msg': '未知操作'}, status=400)
|
||||
|
||||
|
||||
class CheckAdminView(ELTAuthenticatedAPIView):
|
||||
"""检查当前用户是否具有管理员特权(超级用户或当前角色拥有 admin:super 权限)"""
|
||||
|
||||
def get(self, request):
|
||||
user = request.user if hasattr(request, 'user') and request.user.is_authenticated else None
|
||||
is_superuser = bool(user and getattr(user, 'IsSuperuser', False))
|
||||
is_admin = _is_admin(request)
|
||||
current_role = getattr(request, 'current_role', None)
|
||||
current_company_uuid = getattr(request, 'current_company_uuid', None)
|
||||
role_name = ''
|
||||
if current_role:
|
||||
role_name = getattr(current_role, 'RoleName', '') or ''
|
||||
return Response({
|
||||
'code': 0,
|
||||
'data': {
|
||||
'is_admin': is_admin,
|
||||
'is_superuser': is_superuser,
|
||||
'current_role_name': role_name,
|
||||
'has_current_company': current_company_uuid is not None,
|
||||
}
|
||||
})
|
||||
@@ -91,13 +91,17 @@ class UserDetailView(ELTAuthenticatedAPIView):
|
||||
'CompanyShort': company_short,
|
||||
})
|
||||
|
||||
# 权限列表(通过 UserRole → RolePermission → Permission 链路查询)
|
||||
# 权限列表(通过 UserRole → RolePermission → Permission 链路查询,按公司过滤)
|
||||
permissions = []
|
||||
role_uuids = [ur.RoleUUID_id for ur in user_roles if ur.RoleUUID_id]
|
||||
company_uuids = [ur.CompanyUUID_id for ur in user_roles if ur.CompanyUUID_id]
|
||||
if role_uuids:
|
||||
perm_uuids = RolePermission.objects.filter(
|
||||
rp_qs = RolePermission.objects.filter(
|
||||
RoleUUID_id__in=role_uuids
|
||||
).values_list('PermUUID_id', flat=True)
|
||||
)
|
||||
if company_uuids:
|
||||
rp_qs = rp_qs.filter(CompanyUUID_id__in=company_uuids)
|
||||
perm_uuids = rp_qs.values_list('PermUUID_id', flat=True)
|
||||
perms = Permission.objects.filter(PermUUID__in=perm_uuids, PermStatus=1)
|
||||
seen = set()
|
||||
for p in perms:
|
||||
|
||||
@@ -18,6 +18,7 @@ from ..models import (
|
||||
UserABAC, DepartmentSMTM, ItemReference, Backpack,
|
||||
SimulatedTrigger, CashReceiptsPaymentsLedger, Asset, Liability,
|
||||
PositionTemplate, PositionTemplatePermission, Position,
|
||||
Workgroup,
|
||||
)
|
||||
from ..serializers import (
|
||||
TenantSerializer, CompanyInfoSerializer, CompanySerializer,
|
||||
@@ -81,9 +82,19 @@ class CompanyViewSet(viewsets.ModelViewSet):
|
||||
keyword = request.query_params.get('keyword', '')
|
||||
qs = Company.objects.all()
|
||||
if keyword:
|
||||
qs = qs.filter(CompanyName__icontains=keyword)
|
||||
serializer = self.get_serializer(qs[:20], many=True)
|
||||
return Response({'code': 0, 'data': serializer.data})
|
||||
from django.db.models import Q
|
||||
qs = qs.filter(
|
||||
Q(CompanyName__icontains=keyword) |
|
||||
Q(CompanyShort__icontains=keyword)
|
||||
)
|
||||
data = []
|
||||
for c in qs[:20]:
|
||||
data.append({
|
||||
'CompanyUUID': UUIDObj(c.CompanyUUID).str if c.CompanyUUID else '',
|
||||
'CompanyName': c.CompanyName,
|
||||
'CompanyShort': c.CompanyShort,
|
||||
})
|
||||
return Response({'code': 0, 'data': data})
|
||||
|
||||
|
||||
class DepartmentViewSet(CompanyScopedMixin, viewsets.ModelViewSet):
|
||||
@@ -142,6 +153,26 @@ class RoleViewSet(viewsets.ModelViewSet):
|
||||
return Response({'code': 0, 'data': serializer.data})
|
||||
|
||||
|
||||
class WorkgroupViewSet(viewsets.ModelViewSet):
|
||||
queryset = Workgroup.objects.all()
|
||||
authentication_classes = [ELTAuthentication]
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
@action(detail=False, methods=['get'])
|
||||
def search(self, request):
|
||||
keyword = request.query_params.get('keyword', '')
|
||||
qs = Workgroup.objects.all()
|
||||
if keyword:
|
||||
qs = qs.filter(WorkgroupName__icontains=keyword)
|
||||
data = []
|
||||
for w in qs[:20]:
|
||||
data.append({
|
||||
'WorkgroupUUID': UUIDObj(w.WorkgroupUUID).str if w.WorkgroupUUID else '',
|
||||
'WorkgroupName': w.WorkgroupName,
|
||||
})
|
||||
return Response({'code': 0, 'data': data})
|
||||
|
||||
|
||||
class UserRoleViewSet(viewsets.ModelViewSet):
|
||||
queryset = UserRole.objects.select_related('UserUUID', 'RoleUUID')
|
||||
serializer_class = UserRoleSerializer
|
||||
@@ -191,7 +222,7 @@ class UserDeptViewSet(viewsets.ModelViewSet):
|
||||
|
||||
|
||||
class RolePermissionViewSet(viewsets.ModelViewSet):
|
||||
queryset = RolePermission.objects.select_related('RoleUUID', 'PermUUID')
|
||||
queryset = RolePermission.objects.select_related('RoleUUID', 'PermUUID', 'CompanyUUID')
|
||||
serializer_class = RolePermissionSerializer
|
||||
authentication_classes = [ELTAuthentication]
|
||||
permission_classes = [IsAuthenticated, IsAdminUser]
|
||||
@@ -201,9 +232,11 @@ class RolePermissionViewSet(viewsets.ModelViewSet):
|
||||
ser = RolePermissionAssignSerializer(data=request.data)
|
||||
ser.is_valid(raise_exception=True)
|
||||
data = ser.validated_data
|
||||
company_uuid = data.get('CompanyUUID')
|
||||
existing = RolePermission.objects.filter(
|
||||
RoleUUID_id=data['RoleUUID'],
|
||||
PermUUID_id=data['PermUUID'],
|
||||
CompanyUUID_id=company_uuid,
|
||||
).first()
|
||||
if existing:
|
||||
return Response({'code': 409, 'msg': '关联已存在', 'data': RolePermissionSerializer(existing).data}, status=409)
|
||||
@@ -211,6 +244,7 @@ class RolePermissionViewSet(viewsets.ModelViewSet):
|
||||
RolePermUUID=uuid.uuid4().bytes,
|
||||
RoleUUID_id=data['RoleUUID'],
|
||||
PermUUID_id=data['PermUUID'],
|
||||
CompanyUUID_id=company_uuid,
|
||||
)
|
||||
rp.save()
|
||||
return Response({'code': 0, 'msg': '关联成功', 'data': RolePermissionSerializer(rp).data}, status=201)
|
||||
@@ -220,10 +254,14 @@ class RolePermissionViewSet(viewsets.ModelViewSet):
|
||||
ser = RolePermissionRemoveSerializer(data=request.data)
|
||||
ser.is_valid(raise_exception=True)
|
||||
data = ser.validated_data
|
||||
deleted, _ = RolePermission.objects.filter(
|
||||
company_uuid = data.get('CompanyUUID')
|
||||
qs = RolePermission.objects.filter(
|
||||
RoleUUID_id=data['RoleUUID'],
|
||||
PermUUID_id=data['PermUUID'],
|
||||
).delete()
|
||||
)
|
||||
if company_uuid:
|
||||
qs = qs.filter(CompanyUUID_id=company_uuid)
|
||||
deleted, _ = qs.delete()
|
||||
if deleted > 0:
|
||||
return Response({'code': 0, 'msg': '移除成功'})
|
||||
return Response({'code': 404, 'msg': '关联不存在'}, status=404)
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -14,7 +14,7 @@ class LoginSerializer(PascalCaseSerializer):
|
||||
class RegisterSerializer(PascalCaseSerializer):
|
||||
UserName = serializers.CharField(max_length=256)
|
||||
UserPassword = serializers.CharField(max_length=256)
|
||||
Email = serializers.CharField(max_length=256)
|
||||
UserEmail = serializers.CharField(max_length=256)
|
||||
|
||||
|
||||
class Verify2FASerializer(PascalCaseSerializer):
|
||||
|
||||
+58
-3
@@ -3,7 +3,7 @@ import uuid
|
||||
import datetime
|
||||
import io
|
||||
import bcrypt
|
||||
from django.http import HttpResponse
|
||||
from django.http import HttpResponse, HttpResponseRedirect
|
||||
from django.core.cache import cache
|
||||
from rest_framework import status
|
||||
from rest_framework.views import APIView
|
||||
@@ -140,7 +140,7 @@ class RegisterView(APIView):
|
||||
|
||||
username = serializer.ValidatedData['UserName']
|
||||
password = serializer.ValidatedData['UserPassword']
|
||||
email = serializer.ValidatedData['Email']
|
||||
email = serializer.ValidatedData['UserEmail']
|
||||
|
||||
if User.objects.filter(UserName=username).exists():
|
||||
return Response({'code': 1, 'msg': '用户名已存在'}, status=409)
|
||||
@@ -154,7 +154,47 @@ class RegisterView(APIView):
|
||||
user.SetPassword(password)
|
||||
user.save()
|
||||
|
||||
return Response({'code': 0, 'msg': '注册成功', 'redirect': '/'}, status=201)
|
||||
# 为新用户创建默认角色并绑定到系统公司(GERP 后台)
|
||||
from gvsds.apps.gerp.form_service import create_default_role_for_user
|
||||
create_default_role_for_user(user)
|
||||
|
||||
# 注册后自动登录:创建 ELT token 并设置 cookie
|
||||
elt = EphemeralToken.objects.create(
|
||||
ELTUUID=uuid.uuid4().bytes,
|
||||
ELTUserUUID=user.UserUUID,
|
||||
ELTEndTime=datetime.datetime.utcnow() + datetime.timedelta(days=_ELT_TOKEN_LIFETIME_DAYS),
|
||||
ELTType=0,
|
||||
ELTStatus=1,
|
||||
)
|
||||
user.UserLastLoginDate = datetime.datetime.utcnow()
|
||||
user.save(update_fields=['UserLastLoginDate'])
|
||||
|
||||
response = Response({
|
||||
'code': 0,
|
||||
'msg': '注册成功',
|
||||
'redirect': 'https://gerp.gvsds.com/',
|
||||
'data': {
|
||||
'EltToken': UUIDObj(elt.ELTUUID).str,
|
||||
'User': {
|
||||
'UUID': UUIDObj(user.UserUUID).str,
|
||||
'Name': user.UserName,
|
||||
'Email': user.UserEmail,
|
||||
},
|
||||
},
|
||||
}, status=201)
|
||||
SetELTCookie(response, UUIDObj(elt.ELTUUID).str)
|
||||
|
||||
# 设置 CurrentRoleUUID cookie(如果用户有角色)
|
||||
from gvsds.apps.gerp.models import UserRole as _UserRole
|
||||
first_role = _UserRole.objects.filter(UserUUID=user.UserUUID).select_related('RoleUUID').first()
|
||||
if first_role and first_role.RoleUUID:
|
||||
response.set_cookie(
|
||||
'CurrentRoleUUID', UUIDObj(first_role.RoleUUID.RoleUUID).str,
|
||||
domain=ELT_COOKIE_DOMAIN, max_age=ELT_COOKIE_MAX_AGE,
|
||||
secure=True, httponly=False, samesite='None',
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
class Verify2FAView(APIView):
|
||||
@@ -435,3 +475,18 @@ class LogoutView(APIView):
|
||||
response = Response({'code': 0, 'msg': '已退出登录'})
|
||||
DeleteELTCookie(response)
|
||||
return response
|
||||
|
||||
def get(self, request):
|
||||
"""GET 请求登出后重定向到登录页"""
|
||||
elt_token = request.COOKIES.get(ELT_COOKIE_NAME)
|
||||
if elt_token:
|
||||
try:
|
||||
elt = EphemeralToken.objects.get(ELTUUID=UUIDObj(elt_token).bytes)
|
||||
elt.ELTStatus = 0
|
||||
elt.save(update_fields=['ELTStatus'])
|
||||
except EphemeralToken.DoesNotExist:
|
||||
pass
|
||||
|
||||
response = HttpResponseRedirect('https://www.gvsds.com/login.html')
|
||||
DeleteELTCookie(response)
|
||||
return response
|
||||
+1
-2
@@ -1,8 +1,7 @@
|
||||
# Re-export from gvsdsdk — ORM 模型基类已迁移至 SDK
|
||||
from gvsdsdk.model_base import * # noqa: F401,F403
|
||||
from gvsdsdk.model_base import (
|
||||
ABBREVIATIONS, FieldExpression, FieldExpressionDescriptor,
|
||||
ForeignKeyIdDescriptor, QModel, QModelBase,
|
||||
ABBREVIATIONS, FieldExpression, QModel, QModelBase,
|
||||
_CastJsonExpression, _InvertedFieldExpression, _JsonKeyExpression,
|
||||
classproperty, pascal_to_snake, snake_to_pascal,
|
||||
)
|
||||
|
||||
@@ -128,6 +128,8 @@ urlpatterns = [
|
||||
path('API/FormAPI/PositionTemplateSearch', gerp_views.PositionTemplateViewSet.as_view({'get': 'search'}), name='form_position_template_search'),
|
||||
path('API/FormAPI/PositionSearch', gerp_views.PositionViewSet.as_view({'get': 'search'}), name='form_position_search'),
|
||||
path('API/FormAPI/ShopSearch', gerp_views.ShopViewSet.as_view({'get': 'search'}), name='form_shop_search'),
|
||||
path('API/FormAPI/WorkgroupSearch', gerp_views.WorkgroupViewSet.as_view({'get': 'search'}), name='form_workgroup_search'),
|
||||
path('API/FormAPI/CheckAdmin', gerp_views.CheckAdminView.as_view(), name='form_check_admin'),
|
||||
|
||||
path('API/doc/doc/GetDocs', gerp_views.DocView.as_view(), name='doc_list'),
|
||||
path('API/doc/doc/AddDoc', gerp_views.DocView.as_view(), name='doc_add'),
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -13,7 +13,8 @@ class ELTAuthentication:
|
||||
except (ValueError, AttributeError):
|
||||
return None
|
||||
|
||||
from gvsdsdk.models import User, EphemeralToken
|
||||
from gvsdsdk.models import EphemeralToken
|
||||
from gvsds.apps.core.models import User
|
||||
import datetime
|
||||
|
||||
try:
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
"""自定义 JWT 认证 — 向后兼容旧 token 的 user_id claim"""
|
||||
|
||||
from rest_framework_simplejwt.authentication import JWTAuthentication
|
||||
from rest_framework_simplejwt.exceptions import InvalidToken, AuthenticationFailed
|
||||
from rest_framework_simplejwt.settings import api_settings
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class CompatibleJWTAuthentication(JWTAuthentication):
|
||||
"""
|
||||
兼容旧 token 的 JWT 认证类。
|
||||
|
||||
旧 token(USER_ID_CLAIM='user_id' 时生成)payload 中使用 'user_id' claim,
|
||||
值为 user.id(UUID 字符串)。
|
||||
新 token(USER_ID_CLAIM='UserUID' 时生成)payload 中使用 'UserUID' claim,
|
||||
值为 user.UserUID(7位业务ID)。
|
||||
|
||||
此类先尝试新 claim,找不到时回退到旧 claim,并用对应字段查库。
|
||||
"""
|
||||
|
||||
def get_user(self, validated_token):
|
||||
# 1. 先尝试当前配置的 USER_ID_CLAIM
|
||||
user_id = validated_token.get(api_settings.USER_ID_CLAIM)
|
||||
lookup_field = api_settings.USER_ID_FIELD
|
||||
|
||||
if user_id is None:
|
||||
# 2. 回退:尝试旧 token 中的 'user_id' claim
|
||||
user_id = validated_token.get('user_id')
|
||||
if user_id is None:
|
||||
raise InvalidToken(
|
||||
{'detail': _('令牌未包含用户标识符'), 'code': 'token_not_valid'}
|
||||
)
|
||||
# 旧 token 的 user_id 来自 user.id(即 UserUUID),
|
||||
# 需要用 UserUUID 字段查库
|
||||
lookup_field = 'UserUUID'
|
||||
|
||||
try:
|
||||
user = self.user_model.objects.get(**{lookup_field: user_id})
|
||||
except self.user_model.DoesNotExist:
|
||||
raise AuthenticationFailed(_('用户不存在'), code='user_not_found')
|
||||
|
||||
if not user.is_active:
|
||||
raise AuthenticationFailed(_('用户已被禁用'), code='user_inactive')
|
||||
|
||||
if api_settings.CHECK_REVOKE_TOKEN:
|
||||
from rest_framework_simplejwt.utils import get_md5_hash_password
|
||||
if validated_token.get(
|
||||
api_settings.REVOKE_TOKEN_CLAIM
|
||||
) != get_md5_hash_password(user.password):
|
||||
raise AuthenticationFailed(
|
||||
_('用户密码已更改'), code='password_changed'
|
||||
)
|
||||
|
||||
return user
|
||||
@@ -87,32 +87,39 @@ class RoleManager:
|
||||
@staticmethod
|
||||
def GetUsersForRole(RoleUUID):
|
||||
"""获取拥有指定角色的所有用户"""
|
||||
from gvsdsdk.models import User
|
||||
from gvsds.apps.core.models import User
|
||||
user_uuids = UserRole.objects.filter(RoleUUID=RoleUUID).values_list('UserUUID', flat=True)
|
||||
return User.objects.filter(UserUUID__in=user_uuids)
|
||||
|
||||
@staticmethod
|
||||
def AssignPermission(RoleUUID, PermUUID):
|
||||
"""为角色分配权限"""
|
||||
def AssignPermission(RoleUUID, PermUUID, CompanyUUID):
|
||||
"""为角色分配权限(指定公司)"""
|
||||
rp = RolePermission(
|
||||
RolePermUUID=uuid.uuid4().bytes,
|
||||
RoleUUID=RoleUUID,
|
||||
PermUUID=PermUUID,
|
||||
CompanyUUID=CompanyUUID,
|
||||
CreateTime=datetime.datetime.utcnow(),
|
||||
)
|
||||
rp.save()
|
||||
return rp
|
||||
|
||||
@staticmethod
|
||||
def RemovePermission(RoleUUID, PermUUID):
|
||||
def RemovePermission(RoleUUID, PermUUID, CompanyUUID=None):
|
||||
"""移除角色的指定权限"""
|
||||
deleted, _ = RolePermission.objects.filter(RoleUUID=RoleUUID, PermUUID=PermUUID).delete()
|
||||
qs = RolePermission.objects.filter(RoleUUID=RoleUUID, PermUUID=PermUUID)
|
||||
if CompanyUUID is not None:
|
||||
qs = qs.filter(CompanyUUID=CompanyUUID)
|
||||
deleted, _ = qs.delete()
|
||||
return deleted > 0
|
||||
|
||||
@staticmethod
|
||||
def GetPermissionsForRole(RoleUUID):
|
||||
"""获取角色的所有权限"""
|
||||
perm_uuids = RolePermission.objects.filter(RoleUUID=RoleUUID).values_list('PermUUID', flat=True)
|
||||
def GetPermissionsForRole(RoleUUID, CompanyUUID=None):
|
||||
"""获取角色的所有权限(可按公司过滤)"""
|
||||
qs = RolePermission.objects.filter(RoleUUID=RoleUUID)
|
||||
if CompanyUUID is not None:
|
||||
qs = qs.filter(CompanyUUID=CompanyUUID)
|
||||
perm_uuids = qs.values_list('PermUUID', flat=True)
|
||||
return Permission.objects.filter(PermUUID__in=perm_uuids, PermStatus=1)
|
||||
|
||||
|
||||
|
||||
+65
-20
@@ -1,6 +1,9 @@
|
||||
from gvsdsdk.auth.cookie import ELT_COOKIE_NAME
|
||||
from gvsdsdk.models import UserRole, Role
|
||||
from gvsdsdk.model_utils import UUIDObj
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ELTAuthMiddleware:
|
||||
@@ -20,35 +23,77 @@ class CurrentRoleMiddleware:
|
||||
|
||||
request.current_role : gvsdsdk.models.Role 实例(或 None)
|
||||
request.current_company_uuid : 通过 UserRole.CompanyUUID 获取当前角色所属公司UUID
|
||||
|
||||
注意:此中间件在 DRF 认证之前运行,因此需要自行从 ELT token 解析用户。
|
||||
"""
|
||||
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
|
||||
def _get_user_from_elt(self, request):
|
||||
"""从 ELT token cookie 中解析用户(不依赖 DRF 认证)"""
|
||||
import uuid as _uuid
|
||||
import datetime as _datetime
|
||||
elt_token = request.COOKIES.get(ELT_COOKIE_NAME)
|
||||
if not elt_token:
|
||||
return None
|
||||
try:
|
||||
token_uuid = _uuid.UUID(elt_token)
|
||||
except (ValueError, AttributeError):
|
||||
return None
|
||||
try:
|
||||
from gvsdsdk.models import EphemeralToken
|
||||
elt = EphemeralToken.objects.get(ELTUUID=token_uuid, ELTStatus=1)
|
||||
except EphemeralToken.DoesNotExist:
|
||||
return None
|
||||
now_utc = _datetime.datetime.now(_datetime.timezone.utc)
|
||||
if elt.ELTEndTime.replace(tzinfo=None) < now_utc.replace(tzinfo=None):
|
||||
return None
|
||||
try:
|
||||
from gvsds.apps.core.models import User
|
||||
return User.objects.get(UserUUID=elt.ELTUserUUID)
|
||||
except User.DoesNotExist:
|
||||
return None
|
||||
|
||||
def __call__(self, request):
|
||||
request.current_role = None
|
||||
request.current_company_uuid = None
|
||||
|
||||
# 仅对已认证用户处理角色
|
||||
# 优先使用已认证的 request.user,否则自行从 ELT token 解析
|
||||
user = None
|
||||
if hasattr(request, 'user') and request.user and request.user.is_authenticated:
|
||||
role_uuid_str = request.COOKIES.get('CurrentRoleUUID', '')
|
||||
if role_uuid_str:
|
||||
try:
|
||||
role_uuid_bytes = UUIDObj(role_uuid_str).bytes
|
||||
# gvsdsdk.UserRole 的 RoleUUID 是 UUIDField,不能用 select_related
|
||||
user_role = UserRole.objects.filter(
|
||||
UserUUID=request.user.UserUUID,
|
||||
RoleUUID=role_uuid_bytes
|
||||
).first()
|
||||
if user_role:
|
||||
# 单独查询 Role 实例
|
||||
role = Role.objects.filter(RoleUUID=role_uuid_bytes).first()
|
||||
if role:
|
||||
request.current_role = role
|
||||
# 通过 UserRole.CompanyUUID 获取当前公司
|
||||
if user_role.CompanyUUID:
|
||||
request.current_company_uuid = user_role.CompanyUUID
|
||||
except Exception:
|
||||
pass
|
||||
user = request.user
|
||||
else:
|
||||
user = self._get_user_from_elt(request)
|
||||
|
||||
if not user:
|
||||
return self.get_response(request)
|
||||
|
||||
role_uuid_str = request.COOKIES.get('CurrentRoleUUID', '')
|
||||
logger.info('[CurrentRoleMiddleware] user=%s, CurrentRoleUUID cookie=%s', user, role_uuid_str or '(empty)')
|
||||
if role_uuid_str:
|
||||
try:
|
||||
role_uuid_bytes = UUIDObj(role_uuid_str).bytes
|
||||
user_uuid = user.UserUUID
|
||||
logger.info('[CurrentRoleMiddleware] querying UserRole: UserUUID=%s, RoleUUID=%s',
|
||||
user_uuid.hex() if hasattr(user_uuid, 'hex') else user_uuid,
|
||||
role_uuid_bytes.hex())
|
||||
user_role = UserRole.objects.filter(
|
||||
UserUUID=user.UserUUID,
|
||||
RoleUUID=role_uuid_bytes
|
||||
).first()
|
||||
logger.info('[CurrentRoleMiddleware] user_role found=%s', user_role is not None)
|
||||
if user_role:
|
||||
role = Role.objects.filter(RoleUUID=role_uuid_bytes).first()
|
||||
if role:
|
||||
request.current_role = role
|
||||
if user_role.CompanyUUID:
|
||||
request.current_company_uuid = user_role.CompanyUUID
|
||||
logger.info('[CurrentRoleMiddleware] set current_company_uuid=%s',
|
||||
user_role.CompanyUUID.hex() if hasattr(user_role.CompanyUUID, 'hex') else user_role.CompanyUUID)
|
||||
else:
|
||||
logger.warning('[CurrentRoleMiddleware] user_role.CompanyUUID is None')
|
||||
except Exception as e:
|
||||
logger.error('[CurrentRoleMiddleware] error: %s', e, exc_info=True)
|
||||
|
||||
return self.get_response(request)
|
||||
@@ -1,4 +1,5 @@
|
||||
from gvsdsdk.models import User, UserRole, Role, RolePermission, Permission, UserABAC
|
||||
from gvsds.apps.core.models import User
|
||||
from gvsdsdk.models import UserRole, Role, RolePermission, Permission, UserABAC
|
||||
from rest_framework.permissions import BasePermission
|
||||
|
||||
|
||||
@@ -48,7 +49,7 @@ def PermissionVerifier(permission, attributes, user=None, request=None):
|
||||
"""权限验证器——混合 ABAC + RBAC 检查
|
||||
|
||||
RBAC 仅检查当前角色(CurrentRoleUUID Cookie 指定的角色)的权限,
|
||||
通过 RolePermission → Permission 链路查询。
|
||||
通过 RolePermission → Permission 链路查询,按当前公司过滤。
|
||||
超级管理员/员工直接放行。
|
||||
"""
|
||||
if not permission:
|
||||
@@ -63,6 +64,7 @@ def PermissionVerifier(permission, attributes, user=None, request=None):
|
||||
return True
|
||||
|
||||
current_role = getattr(request, 'current_role', None) if request else None
|
||||
current_company = getattr(request, 'current_company_uuid', None) if request else None
|
||||
|
||||
for attr in attributes:
|
||||
# ABAC 检查
|
||||
@@ -73,13 +75,16 @@ def PermissionVerifier(permission, attributes, user=None, request=None):
|
||||
if abac_match:
|
||||
continue
|
||||
|
||||
# RBAC 检查:仅检查当前角色
|
||||
# RBAC 检查:仅检查当前角色,按公司过滤权限
|
||||
if current_role and isinstance(current_role, Role):
|
||||
if current_role.RoleStatus != 1:
|
||||
return False
|
||||
perm_uuids = RolePermission.objects.filter(
|
||||
rp_qs = RolePermission.objects.filter(
|
||||
RoleUUID=current_role.RoleUUID
|
||||
).values_list('PermUUID', flat=True)
|
||||
)
|
||||
if current_company:
|
||||
rp_qs = rp_qs.filter(CompanyUUID=current_company)
|
||||
perm_uuids = rp_qs.values_list('PermUUID', flat=True)
|
||||
rbac_result = Permission.objects.filter(
|
||||
PermUUID__in=perm_uuids,
|
||||
PermCode=attr,
|
||||
@@ -94,9 +99,12 @@ def PermissionVerifier(permission, attributes, user=None, request=None):
|
||||
RoleUUID__in=user_role_uuids,
|
||||
RoleStatus=1,
|
||||
).values_list('RoleUUID', flat=True)
|
||||
perm_uuids = RolePermission.objects.filter(
|
||||
rp_qs = RolePermission.objects.filter(
|
||||
RoleUUID__in=active_role_uuids
|
||||
).values_list('PermUUID', flat=True)
|
||||
)
|
||||
if current_company:
|
||||
rp_qs = rp_qs.filter(CompanyUUID=current_company)
|
||||
perm_uuids = rp_qs.values_list('PermUUID', flat=True)
|
||||
rbac_result = Permission.objects.filter(
|
||||
PermUUID__in=perm_uuids,
|
||||
PermCode=attr,
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+495
-247
File diff suppressed because it is too large.
Load diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,30 @@
|
||||
"""将 User 表中明文密码迁移为 bcrypt 哈希"""
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = '将 User 表中所有明文密码迁移为 bcrypt 哈希'
|
||||
|
||||
def handle(self, *args, **options):
|
||||
# 筛选非空且非 bcrypt 哈希的密码
|
||||
users = User.objects.exclude(UserPassword='')
|
||||
updated = 0
|
||||
skipped = 0
|
||||
|
||||
for user in users.iterator():
|
||||
stored = user.UserPassword or ''
|
||||
# 已是 bcrypt 哈希则跳过
|
||||
if stored.startswith(('$2b$', '$2a$')) and len(stored) == 60:
|
||||
skipped += 1
|
||||
continue
|
||||
# 明文密码 → bcrypt 哈希
|
||||
user.SetPassword(stored)
|
||||
user.save(update_fields=['UserPassword'])
|
||||
updated += 1
|
||||
|
||||
self.stdout.write(self.style.SUCCESS(
|
||||
f'完成:{updated} 条明文密码已转为 bcrypt,{skipped} 条已是哈希跳过'
|
||||
))
|
||||
@@ -1,8 +1,8 @@
|
||||
# Generated by Django 6.0.5 on 2026-06-13 11:43
|
||||
# Generated by Django 4.2.27 on 2026-06-19 18:42
|
||||
|
||||
import datetime
|
||||
import gvsdsdk.models
|
||||
from django.db import migrations, models
|
||||
import gvsdsdk.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
@@ -16,9 +16,9 @@ class Migration(migrations.Migration):
|
||||
migrations.CreateModel(
|
||||
name='AccountActivation',
|
||||
fields=[
|
||||
('AccountActivationCode', gvsdsdk.models.FixedBinaryField(binary_length=6, db_column='AccountActivationCode', max_length=6, primary_key=True, serialize=False)),
|
||||
('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID')),
|
||||
('CreateTime', models.DateTimeField(db_column='CreateTime', default=datetime.datetime.utcnow)),
|
||||
('AccountActivationCode', gvsdsdk.models.FixedBinaryField(binary_length=6, db_column='AccountActivationCode', max_length=6, primary_key=True, serialize=False, verbose_name='激活码')),
|
||||
('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID', verbose_name='用户UUID')),
|
||||
('CreateTime', models.DateTimeField(db_column='CreateTime', default=datetime.datetime.utcnow, verbose_name='创建时间')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '账户激活',
|
||||
@@ -30,13 +30,13 @@ class Migration(migrations.Migration):
|
||||
migrations.CreateModel(
|
||||
name='EphemeralToken',
|
||||
fields=[
|
||||
('ELTUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='ELTUUID', primary_key=True, serialize=False)),
|
||||
('ELTUserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='ELTUserUUID', db_index=True)),
|
||||
('ELTCreateTime', models.DateTimeField(db_column='ELTCreateTime', default=datetime.datetime.utcnow)),
|
||||
('ELTEndTime', models.DateTimeField(db_column='ELTEndTime', default=gvsdsdk.models._elt_end_time_default)),
|
||||
('ELTType', models.SmallIntegerField(db_column='ELTType', default=0)),
|
||||
('ELTAllowIP', models.JSONField(db_column='ELTAllowIP', default=list)),
|
||||
('ELTStatus', models.SmallIntegerField(db_column='ELTStatus', db_index=True, default=1)),
|
||||
('ELTUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='ELTUUID', primary_key=True, serialize=False, verbose_name='令牌UUID')),
|
||||
('ELTUserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='ELTUserUUID', db_index=True, verbose_name='用户UUID')),
|
||||
('ELTCreateTime', models.DateTimeField(db_column='ELTCreateTime', default=datetime.datetime.utcnow, verbose_name='创建时间')),
|
||||
('ELTEndTime', models.DateTimeField(db_column='ELTEndTime', default=gvsdsdk.models._EltEndTimeDefault, verbose_name='过期时间')),
|
||||
('ELTType', models.SmallIntegerField(db_column='ELTType', default=0, verbose_name='令牌类型')),
|
||||
('ELTAllowIP', models.JSONField(db_column='ELTAllowIP', default=list, verbose_name='允许IP')),
|
||||
('ELTStatus', models.SmallIntegerField(db_column='ELTStatus', db_index=True, default=1, verbose_name='状态')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '临时令牌',
|
||||
@@ -48,8 +48,8 @@ class Migration(migrations.Migration):
|
||||
migrations.CreateModel(
|
||||
name='Password',
|
||||
fields=[
|
||||
('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID', primary_key=True, serialize=False)),
|
||||
('Password', models.CharField(db_column='Password', max_length=1024)),
|
||||
('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID', primary_key=True, serialize=False, verbose_name='用户UUID')),
|
||||
('Password', models.CharField(db_column='Password', max_length=1024, verbose_name='密码哈希')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '密码',
|
||||
@@ -61,12 +61,14 @@ class Migration(migrations.Migration):
|
||||
migrations.CreateModel(
|
||||
name='Permission',
|
||||
fields=[
|
||||
('PermUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='PermUUID', primary_key=True, serialize=False)),
|
||||
('PermName', models.CharField(db_column='PermName', max_length=128)),
|
||||
('PermCode', models.CharField(db_column='PermCode', max_length=128)),
|
||||
('RoleUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='RoleUUID')),
|
||||
('PermStatus', models.SmallIntegerField(db_column='PermStatus', default=1)),
|
||||
('PermDesc', models.CharField(db_column='PermDesc', default='', max_length=512)),
|
||||
('PermUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='PermUUID', primary_key=True, serialize=False, verbose_name='权限UUID')),
|
||||
('TenantUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='TenantUUID', verbose_name='租户UUID')),
|
||||
('PermName', models.CharField(db_column='PermName', max_length=128, verbose_name='权限名称')),
|
||||
('PermCode', models.CharField(db_column='PermCode', max_length=128, verbose_name='权限键')),
|
||||
('PermCategory', models.CharField(db_column='PermCategory', default='', max_length=64, verbose_name='权限分类')),
|
||||
('PermStatus', models.SmallIntegerField(db_column='PermStatus', default=1, verbose_name='是否启用')),
|
||||
('PermDesc', models.CharField(db_column='PermDesc', default='', max_length=512, verbose_name='描述')),
|
||||
('CreateTime', models.DateTimeField(db_column='CreateTime', default=datetime.datetime.utcnow, verbose_name='创建时间')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '权限',
|
||||
@@ -75,16 +77,72 @@ class Migration(migrations.Migration):
|
||||
'managed': True,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Position',
|
||||
fields=[
|
||||
('PositionUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='PositionUUID', primary_key=True, serialize=False, verbose_name='职务UUID')),
|
||||
('TenantUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='TenantUUID', verbose_name='租户UUID')),
|
||||
('PositionName', models.CharField(db_column='PositionName', max_length=128, verbose_name='职务名称')),
|
||||
('PositionCode', models.CharField(db_column='PositionCode', default='', max_length=64, verbose_name='职务编码')),
|
||||
('PositionLevel', models.SmallIntegerField(db_column='PositionLevel', default=0, verbose_name='职务级别')),
|
||||
('PositionStatus', models.SmallIntegerField(db_column='PositionStatus', default=1, verbose_name='状态')),
|
||||
('PositionDesc', models.CharField(db_column='PositionDesc', default='', max_length=512, verbose_name='描述')),
|
||||
('CreateTime', models.DateTimeField(db_column='CreateTime', default=datetime.datetime.utcnow, verbose_name='创建时间')),
|
||||
('UpdateTime', models.DateTimeField(db_column='UpdateTime', default=datetime.datetime.utcnow, verbose_name='更新时间')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '人事职务标签',
|
||||
'verbose_name_plural': '人事职务标签',
|
||||
'db_table': 'Position',
|
||||
'managed': True,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='PositionTemplate',
|
||||
fields=[
|
||||
('PositionTemplateUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='PositionTemplateUUID', primary_key=True, serialize=False, verbose_name='岗位模板UUID')),
|
||||
('TenantUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='TenantUUID', verbose_name='租户UUID')),
|
||||
('TemplateName', models.CharField(db_column='TemplateName', max_length=128, verbose_name='模板名称')),
|
||||
('TemplateCode', models.CharField(db_column='TemplateCode', default='', max_length=64, verbose_name='模板编码')),
|
||||
('TemplateStatus', models.SmallIntegerField(db_column='TemplateStatus', default=1, verbose_name='模板状态')),
|
||||
('TemplateDesc', models.CharField(db_column='TemplateDesc', default='', max_length=512, verbose_name='模板描述')),
|
||||
('CreateTime', models.DateTimeField(db_column='CreateTime', default=datetime.datetime.utcnow, verbose_name='创建时间')),
|
||||
('UpdateTime', models.DateTimeField(db_column='UpdateTime', default=datetime.datetime.utcnow, verbose_name='更新时间')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '岗位模板',
|
||||
'verbose_name_plural': '岗位模板',
|
||||
'db_table': 'PositionTemplate',
|
||||
'managed': True,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='PositionTemplatePermission',
|
||||
fields=[
|
||||
('PTPermUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='PTPermUUID', primary_key=True, serialize=False, verbose_name='关联UUID')),
|
||||
('PositionTemplateUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='PositionTemplateUUID', verbose_name='岗位模板UUID')),
|
||||
('PermUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='PermUUID', verbose_name='权限UUID')),
|
||||
('CreateTime', models.DateTimeField(db_column='CreateTime', default=datetime.datetime.utcnow, verbose_name='创建时间')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '岗位模板权限关联',
|
||||
'verbose_name_plural': '岗位模板权限关联',
|
||||
'db_table': 'PositionTemplatePermission',
|
||||
'managed': True,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Role',
|
||||
fields=[
|
||||
('RoleUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='RoleUUID', primary_key=True, serialize=False)),
|
||||
('CompanyUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='CompanyUUID')),
|
||||
('DeptUUID', gvsdsdk.models.UUIDField(binary_length=16, blank=True, db_column='DeptUUID', null=True)),
|
||||
('RoleName', models.CharField(db_column='RoleName', max_length=128)),
|
||||
('RoleType', models.SmallIntegerField(db_column='RoleType', default=2)),
|
||||
('RoleStatus', models.SmallIntegerField(db_column='RoleStatus', default=1)),
|
||||
('CreateTime', models.DateTimeField(db_column='CreateTime', default=datetime.datetime.utcnow)),
|
||||
('RoleUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='RoleUUID', primary_key=True, serialize=False, verbose_name='角色UUID')),
|
||||
('TenantUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='TenantUUID', verbose_name='租户UUID')),
|
||||
('RoleName', models.CharField(db_column='RoleName', max_length=128, verbose_name='角色名称')),
|
||||
('RoleType', models.SmallIntegerField(db_column='RoleType', default=2, verbose_name='角色类型')),
|
||||
('RoleStatus', models.SmallIntegerField(db_column='RoleStatus', default=1, verbose_name='角色状态')),
|
||||
('AssignScope', models.SmallIntegerField(db_column='AssignScope', default=0, verbose_name='分配可见范围')),
|
||||
('AssignScopeUUID', gvsdsdk.models.UUIDField(binary_length=16, blank=True, db_column='AssignScopeUUID', null=True, verbose_name='分配范围UUID')),
|
||||
('RoleDesc', models.CharField(db_column='RoleDesc', default='', max_length=512, verbose_name='角色描述')),
|
||||
('CreateTime', models.DateTimeField(db_column='CreateTime', default=datetime.datetime.utcnow, verbose_name='创建时间')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '角色',
|
||||
@@ -94,55 +152,69 @@ class Migration(migrations.Migration):
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='RoleDept',
|
||||
name='RolePermission',
|
||||
fields=[
|
||||
('RoleDeptUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='RoleDeptUUID', primary_key=True, serialize=False)),
|
||||
('RoleUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='RoleUUID')),
|
||||
('DeptUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='DeptUUID')),
|
||||
('IsMainDept', models.SmallIntegerField(db_column='IsMainDept', default=1)),
|
||||
('RolePermUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='RolePermUUID', primary_key=True, serialize=False, verbose_name='关联UUID')),
|
||||
('RoleUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='RoleUUID', verbose_name='角色UUID')),
|
||||
('PermUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='PermUUID', verbose_name='权限UUID')),
|
||||
('CreateTime', models.DateTimeField(db_column='CreateTime', default=datetime.datetime.utcnow, verbose_name='创建时间')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '角色部门关联',
|
||||
'verbose_name_plural': '角色部门关联',
|
||||
'db_table': 'RoleDept',
|
||||
'verbose_name': '角色权限关联',
|
||||
'verbose_name_plural': '角色权限关联',
|
||||
'db_table': 'RolePermission',
|
||||
'managed': True,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='User',
|
||||
name='RoleShop',
|
||||
fields=[
|
||||
('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID', primary_key=True, serialize=False)),
|
||||
('UserName', models.CharField(db_column='UserName', max_length=256, unique=True)),
|
||||
('UserPassword', models.CharField(db_column='UserPassword', default='', max_length=60)),
|
||||
('UserPositionStatus', models.SmallIntegerField(db_column='UserPositionStatus', default=0)),
|
||||
('UserPersonalSignature', models.CharField(db_column='UserPersonalSignature', default='', max_length=1024)),
|
||||
('UserEmail', models.CharField(db_column='UserEmail', default='', max_length=256)),
|
||||
('UserEmailVerify', models.SmallIntegerField(db_column='UserEmailVerify', default=0)),
|
||||
('UserAccountLicense', models.SmallIntegerField(db_column='UserAccountLicense', default=1)),
|
||||
('UserGender', models.SmallIntegerField(db_column='UserGender', default=-1)),
|
||||
('UserTele', models.CharField(blank=True, db_column='UserTele', max_length=16, null=True)),
|
||||
('UserCreateTime', models.DateTimeField(db_column='UserCreateTime', default=datetime.datetime.utcnow)),
|
||||
('UserLastLoginDate', models.DateTimeField(db_column='UserLastLoginDate', default=datetime.datetime.utcnow)),
|
||||
('IsStaff', models.BooleanField(db_column='IsStaff', default=False)),
|
||||
('IsSuperuser', models.BooleanField(db_column='IsSuperuser', default=False)),
|
||||
('RoleShopUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='RoleShopUUID', primary_key=True, serialize=False, verbose_name='关联UUID')),
|
||||
('RoleUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='RoleUUID', verbose_name='角色UUID')),
|
||||
('ShopUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='ShopUUID', verbose_name='商铺UUID')),
|
||||
('IsMainShop', models.SmallIntegerField(db_column='IsMainShop', default=0, verbose_name='主商铺')),
|
||||
('CreateTime', models.DateTimeField(db_column='CreateTime', default=datetime.datetime.utcnow, verbose_name='创建时间')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '用户',
|
||||
'verbose_name_plural': '用户',
|
||||
'db_table': 'User',
|
||||
'verbose_name': '角色商铺关联',
|
||||
'verbose_name_plural': '角色商铺关联',
|
||||
'db_table': 'RoleShop',
|
||||
'managed': True,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Shop',
|
||||
fields=[
|
||||
('ShopUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='ShopUUID', primary_key=True, serialize=False, verbose_name='商铺UUID')),
|
||||
('CompanyUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='CompanyUUID', verbose_name='公司UUID')),
|
||||
('ShopName', models.CharField(db_column='ShopName', max_length=128, verbose_name='商铺名称')),
|
||||
('ShopCode', models.CharField(db_column='ShopCode', default='', max_length=64, verbose_name='商铺编码')),
|
||||
('ShopType', models.SmallIntegerField(db_column='ShopType', default=0, verbose_name='商铺类型')),
|
||||
('ShopStatus', models.SmallIntegerField(db_column='ShopStatus', default=1, verbose_name='商铺状态')),
|
||||
('ShopAddress', models.CharField(db_column='ShopAddress', default='', max_length=512, verbose_name='商铺地址')),
|
||||
('ShopContact', models.CharField(db_column='ShopContact', default='', max_length=128, verbose_name='联系人')),
|
||||
('ShopPhone', models.CharField(db_column='ShopPhone', default='', max_length=32, verbose_name='联系电话')),
|
||||
('ShopDesc', models.CharField(db_column='ShopDesc', default='', max_length=512, verbose_name='描述')),
|
||||
('CreateTime', models.DateTimeField(db_column='CreateTime', default=datetime.datetime.utcnow, verbose_name='创建时间')),
|
||||
('UpdateTime', models.DateTimeField(db_column='UpdateTime', default=datetime.datetime.utcnow, verbose_name='更新时间')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '商铺',
|
||||
'verbose_name_plural': '商铺',
|
||||
'db_table': 'Shop',
|
||||
'managed': True,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='User2FA',
|
||||
fields=[
|
||||
('User2FAUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='User2FAUUID', primary_key=True, serialize=False)),
|
||||
('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID', db_index=True)),
|
||||
('SecretKey', models.CharField(db_column='SecretKey', max_length=64)),
|
||||
('IsEnabled', models.SmallIntegerField(db_column='IsEnabled', default=0)),
|
||||
('Verified', models.SmallIntegerField(db_column='Verified', default=0)),
|
||||
('LastUsedAt', models.DateTimeField(blank=True, db_column='LastUsedAt', null=True)),
|
||||
('CreateTime', models.DateTimeField(db_column='CreateTime', default=datetime.datetime.utcnow)),
|
||||
('User2FAUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='User2FAUUID', primary_key=True, serialize=False, verbose_name='2FA UUID')),
|
||||
('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID', db_index=True, verbose_name='用户UUID')),
|
||||
('SecretKey', models.CharField(db_column='SecretKey', max_length=64, verbose_name='密钥')),
|
||||
('IsEnabled', models.SmallIntegerField(db_column='IsEnabled', default=0, verbose_name='是否启用')),
|
||||
('Verified', models.SmallIntegerField(db_column='Verified', default=0, verbose_name='已验证')),
|
||||
('LastUsedAt', models.DateTimeField(blank=True, db_column='LastUsedAt', null=True, verbose_name='最后使用')),
|
||||
('CreateTime', models.DateTimeField(db_column='CreateTime', default=datetime.datetime.utcnow, verbose_name='创建时间')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '双因素认证',
|
||||
@@ -154,9 +226,9 @@ class Migration(migrations.Migration):
|
||||
migrations.CreateModel(
|
||||
name='UserABAC',
|
||||
fields=[
|
||||
('UserABACUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserABACUUID', primary_key=True, serialize=False)),
|
||||
('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID')),
|
||||
('HierarchicalPermissionIdentifiers', models.CharField(db_column='HierarchicalPermissionIdentifiers', max_length=256)),
|
||||
('UserABACUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserABACUUID', primary_key=True, serialize=False, verbose_name='ABAC UUID')),
|
||||
('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID', verbose_name='用户UUID')),
|
||||
('HierarchicalPermissionIdentifiers', models.CharField(db_column='HierarchicalPermissionIdentifiers', max_length=256, verbose_name='层级权限标识')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '用户ABAC',
|
||||
@@ -168,10 +240,10 @@ class Migration(migrations.Migration):
|
||||
migrations.CreateModel(
|
||||
name='UserAvatar',
|
||||
fields=[
|
||||
('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID', primary_key=True, serialize=False)),
|
||||
('UserAvatar', models.BinaryField(db_column='UserAvatar', editable=True)),
|
||||
('AvatarUploadTime', models.DateTimeField(db_column='AvatarUploadTime', default=datetime.datetime.utcnow)),
|
||||
('AvatarFileType', models.CharField(db_column='AvatarFileType', default='png', max_length=16)),
|
||||
('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID', primary_key=True, serialize=False, verbose_name='用户UUID')),
|
||||
('UserAvatar', models.BinaryField(db_column='UserAvatar', editable=True, verbose_name='头像数据')),
|
||||
('AvatarUploadTime', models.DateTimeField(db_column='AvatarUploadTime', default=datetime.datetime.utcnow, verbose_name='上传时间')),
|
||||
('AvatarFileType', models.CharField(db_column='AvatarFileType', default='png', max_length=16, verbose_name='文件类型')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '用户头像',
|
||||
@@ -180,16 +252,33 @@ class Migration(migrations.Migration):
|
||||
'managed': True,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='UserDept',
|
||||
fields=[
|
||||
('UserDeptUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserDeptUUID', primary_key=True, serialize=False, verbose_name='关联UUID')),
|
||||
('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID', verbose_name='用户UUID')),
|
||||
('DeptUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='DeptUUID', verbose_name='部门UUID')),
|
||||
('PositionUUID', gvsdsdk.models.UUIDField(binary_length=16, blank=True, db_column='PositionUUID', null=True, verbose_name='职务标签UUID')),
|
||||
('IsMainDept', models.SmallIntegerField(db_column='IsMainDept', default=1, verbose_name='是否主部门')),
|
||||
('CreateTime', models.DateTimeField(db_column='CreateTime', default=datetime.datetime.utcnow, verbose_name='创建时间')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '员工部门关联',
|
||||
'verbose_name_plural': '员工部门关联',
|
||||
'db_table': 'UserDept',
|
||||
'managed': True,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='UserLoginLog',
|
||||
fields=[
|
||||
('LogUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='LogUUID', primary_key=True, serialize=False)),
|
||||
('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID', db_index=True)),
|
||||
('LoginIP', models.CharField(db_column='LoginIP', max_length=39)),
|
||||
('LoginPlatform', models.CharField(db_column='LoginPlatform', max_length=32)),
|
||||
('LoginStatus', models.SmallIntegerField(db_column='LoginStatus')),
|
||||
('LoginTime', models.DateTimeField(db_column='LoginTime', default=datetime.datetime.utcnow)),
|
||||
('UserAgent', models.CharField(db_column='UserAgent', default='', max_length=512)),
|
||||
('LogUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='LogUUID', primary_key=True, serialize=False, verbose_name='日志UUID')),
|
||||
('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID', db_index=True, verbose_name='用户UUID')),
|
||||
('LoginIP', models.CharField(db_column='LoginIP', max_length=39, verbose_name='登录IP')),
|
||||
('LoginPlatform', models.CharField(db_column='LoginPlatform', max_length=32, verbose_name='登录平台')),
|
||||
('LoginStatus', models.SmallIntegerField(db_column='LoginStatus', verbose_name='登录状态')),
|
||||
('LoginTime', models.DateTimeField(db_column='LoginTime', default=datetime.datetime.utcnow, verbose_name='登录时间')),
|
||||
('UserAgent', models.CharField(db_column='UserAgent', default='', max_length=512, verbose_name='用户代理')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '登录日志',
|
||||
@@ -201,10 +290,10 @@ class Migration(migrations.Migration):
|
||||
migrations.CreateModel(
|
||||
name='UserPending2FA',
|
||||
fields=[
|
||||
('PendingUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='PendingUUID', primary_key=True, serialize=False)),
|
||||
('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID', unique=True)),
|
||||
('SecretKey', models.CharField(db_column='SecretKey', max_length=64)),
|
||||
('ExpireAt', models.DateTimeField(db_column='ExpireAt')),
|
||||
('PendingUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='PendingUUID', primary_key=True, serialize=False, verbose_name='待确认UUID')),
|
||||
('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID', unique=True, verbose_name='用户UUID')),
|
||||
('SecretKey', models.CharField(db_column='SecretKey', max_length=64, verbose_name='密钥')),
|
||||
('ExpireAt', models.DateTimeField(db_column='ExpireAt', verbose_name='过期时间')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '2FA待确认',
|
||||
@@ -216,10 +305,11 @@ class Migration(migrations.Migration):
|
||||
migrations.CreateModel(
|
||||
name='UserRole',
|
||||
fields=[
|
||||
('UserRoleUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserRoleUUID', primary_key=True, serialize=False)),
|
||||
('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID')),
|
||||
('RoleUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='RoleUUID')),
|
||||
('CreateTime', models.DateTimeField(db_column='CreateTime', default=datetime.datetime.utcnow)),
|
||||
('UserRoleUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserRoleUUID', primary_key=True, serialize=False, verbose_name='关联UUID')),
|
||||
('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID', verbose_name='用户UUID')),
|
||||
('RoleUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='RoleUUID', verbose_name='角色UUID')),
|
||||
('CompanyUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='CompanyUUID', verbose_name='公司UUID')),
|
||||
('CreateTime', models.DateTimeField(db_column='CreateTime', default=datetime.datetime.utcnow, verbose_name='创建时间')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '用户角色关联',
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
# Generated by Django 4.2.27 on 2026-06-19 01:54
|
||||
|
||||
from django.db import migrations
|
||||
import gvsdsdk.models
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('gvsdsdk', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='user',
|
||||
name='UserUUID',
|
||||
field=gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID', default=uuid.uuid4, primary_key=True, serialize=False, verbose_name='用户UUID'),
|
||||
),
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
+331
-142
@@ -1,52 +1,86 @@
|
||||
"""QModel 兼容层基类模块
|
||||
|
||||
提供 SQLAlchemy 风格的链式 OOP 查询接口,使 Django ORM 支持:
|
||||
|
||||
- ``Model.query.filter(...)`` — 链式 FluentQuery 查询
|
||||
- ``Model.field_name`` — 类级别返回 FieldExpression(支持 ``==``, ``>``, ``in_()`` 等运算符)
|
||||
- ``Model.field_name`` — 实例级别返回字段值(与原生 Django Model 一致)
|
||||
- ``Model[condition]`` — 快捷 filter 语法
|
||||
|
||||
典型用法::
|
||||
|
||||
# 链式查询
|
||||
orders = Dingdan.query.filter(zhuangtai=8).order_by('-create_time').to_list()
|
||||
|
||||
# 字段表达式
|
||||
Dingdan.query.filter(Dingdan.jine > 100, Dingdan.zhuangtai.in_([1, 2, 3]))
|
||||
|
||||
# 聚合
|
||||
Dingdan.query.aggregate(total=Sum('jine'))
|
||||
|
||||
# 分页
|
||||
Dingdan.query.paginate(page=1, per_page=20)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Any, Callable, Dict, Generic, Optional, Tuple, Type, TypeVar, Union
|
||||
|
||||
try:
|
||||
from typing import Self
|
||||
except ImportError:
|
||||
from typing_extensions import Self
|
||||
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
|
||||
from .fluent import FluentQuery
|
||||
|
||||
class classproperty:
|
||||
def __init__(self, func):
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 工具函数
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
#: classproperty 返回值类型变量
|
||||
_RT = TypeVar('_RT')
|
||||
|
||||
#: QModel 子类类型变量(仅用于 __class_getitem__ 兼容)
|
||||
_QM = TypeVar('_QM', bound='QModel')
|
||||
|
||||
|
||||
class classproperty(Generic[_RT]):
|
||||
"""类属性描述符,支持 ``Model.query`` 等类级别属性访问。
|
||||
|
||||
泛型参数 ``_RT`` 传播函数返回类型,使 Pylance 能穿透描述符推断
|
||||
``Model.query`` 的返回类型为 ``FluentQuery[Self]``。
|
||||
"""
|
||||
|
||||
def __init__(self, func: Callable[[type], _RT]) -> None:
|
||||
self.fget = func
|
||||
|
||||
def __get__(self, obj, objtype=None):
|
||||
def __get__(self, obj: Any, objtype: Optional[type] = None) -> _RT:
|
||||
return self.fget(objtype if objtype is not None else type(obj))
|
||||
|
||||
|
||||
ABBREVIATIONS = {
|
||||
'uuid': 'UUID',
|
||||
'id': 'ID',
|
||||
'ip': 'IP',
|
||||
'url': 'URL',
|
||||
'uri': 'URI',
|
||||
'api': 'API',
|
||||
'html': 'HTML',
|
||||
'css': 'CSS',
|
||||
'js': 'JS',
|
||||
'json': 'JSON',
|
||||
'xml': 'XML',
|
||||
'sql': 'SQL',
|
||||
'http': 'HTTP',
|
||||
'https': 'HTTPS',
|
||||
'smtp': 'SMTP',
|
||||
'ssl': 'SSL',
|
||||
'tls': 'TLS',
|
||||
'ssh': 'SSH',
|
||||
'ftp': 'FTP',
|
||||
'2fa': '2FA',
|
||||
'elt': 'ELT',
|
||||
'sve': 'SVE',
|
||||
't4': 'T4',
|
||||
'mc': 'MC',
|
||||
'crb': 'CRB',
|
||||
'ccgo': 'CCGO',
|
||||
'ugm': 'UGM',
|
||||
'gerp': 'GERP',
|
||||
'abac': 'ABAC',
|
||||
'rbac': 'RBAC',
|
||||
'smtm': 'SMTM',
|
||||
'pwd': 'PWD',
|
||||
ABBREVIATIONS: Dict[str, str] = {
|
||||
'uuid': 'UUID', 'id': 'ID', 'ip': 'IP', 'url': 'URL', 'uri': 'URI',
|
||||
'api': 'API', 'html': 'HTML', 'css': 'CSS', 'js': 'JS', 'json': 'JSON',
|
||||
'xml': 'XML', 'sql': 'SQL', 'http': 'HTTP', 'https': 'HTTPS',
|
||||
'smtp': 'SMTP', 'ssl': 'SSL', 'tls': 'TLS', 'ssh': 'SSH', 'ftp': 'FTP',
|
||||
'2fa': '2FA', 'elt': 'ELT', 'sve': 'SVE', 't4': 'T4', 'mc': 'MC',
|
||||
'crb': 'CRB', 'ccgo': 'CCGO', 'ugm': 'UGM', 'gerp': 'GERP',
|
||||
'abac': 'ABAC', 'rbac': 'RBAC', 'smtm': 'SMTM', 'pwd': 'PWD',
|
||||
}
|
||||
|
||||
|
||||
def snake_to_pascal(name):
|
||||
def snake_to_pascal(name: str) -> str:
|
||||
"""将 snake_case 转换为 PascalCase,自动识别常见缩写。
|
||||
|
||||
>>> snake_to_pascal('user_uuid')
|
||||
'UserUUID'
|
||||
>>> snake_to_pascal('paifa_dingdan_shu')
|
||||
'PaifaDingdanShu'
|
||||
"""
|
||||
if '_' not in name:
|
||||
return name[0].upper() + name[1:] if name else name
|
||||
parts = name.split('_')
|
||||
@@ -60,46 +94,74 @@ def snake_to_pascal(name):
|
||||
return ''.join(result)
|
||||
|
||||
|
||||
def pascal_to_snake(name):
|
||||
import re
|
||||
def pascal_to_snake(name: str) -> str:
|
||||
"""将 PascalCase 转换为 snake_case。
|
||||
|
||||
>>> pascal_to_snake('UserUUID')
|
||||
'user_uuid'
|
||||
"""
|
||||
s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
|
||||
s2 = re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1)
|
||||
parts = s2.lower().split('_')
|
||||
return '_'.join(parts)
|
||||
return s2.lower()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 字段表达式(类级别访问 Model.field 时返回)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class _InvertedFieldExpression:
|
||||
def __init__(self, field_expr):
|
||||
"""取反字段表达式,由 ``~Model.field`` 产生,支持 ``~field.startswith(...)`` 等否定查询。"""
|
||||
|
||||
def __init__(self, field_expr: FieldExpression) -> None:
|
||||
self._field_expr = field_expr
|
||||
|
||||
def startswith(self, value):
|
||||
def startswith(self, value: str) -> Q:
|
||||
return ~Q(**{f'{self._field_expr._django_name}__startswith': value})
|
||||
|
||||
def contains(self, value):
|
||||
def contains(self, value: str) -> Q:
|
||||
return ~Q(**{f'{self._field_expr._django_name}__contains': value})
|
||||
|
||||
def like(self, pattern):
|
||||
def like(self, pattern: str) -> Q:
|
||||
clean = pattern.strip('%')
|
||||
return ~Q(**{f'{self._field_expr._django_name}__icontains': clean})
|
||||
|
||||
def in_(self, values):
|
||||
def in_(self, values) -> Q:
|
||||
return ~Q(**{f'{self._field_expr._django_name}__in': values})
|
||||
|
||||
def is_(self, other):
|
||||
def is_(self, other) -> Q:
|
||||
if other is None:
|
||||
return ~Q(**{f'{self._field_expr._django_name}__isnull': True})
|
||||
return ~Q(**{self._field_expr._django_name: other})
|
||||
|
||||
def between(self, low, high) -> Q:
|
||||
return ~(Q(**{f'{self._field_expr._django_name}__gte': low})
|
||||
& Q(**{f'{self._field_expr._django_name}__lte': high}))
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f'~{self._field_expr!r}'
|
||||
|
||||
|
||||
class FieldExpression:
|
||||
_resolve_cache = {}
|
||||
"""字段表达式,由类级别 ``Model.field_name`` 访问产生。
|
||||
|
||||
def __init__(self, model_class, field_name):
|
||||
支持通过 Python 运算符构建 Django Q 对象::
|
||||
|
||||
Model.field == value → Q(field=value)
|
||||
Model.field > value → Q(field__gt=value)
|
||||
Model.field.in_([1,2,3]) → Q(field__in=[1,2,3])
|
||||
Model.field.between(a, b) → Q(field__gte=a) & Q(field__lte=b)
|
||||
~Model.field → _InvertedFieldExpression(否定查询)
|
||||
"""
|
||||
|
||||
_resolve_cache: Dict[Tuple[str, str], str] = {}
|
||||
|
||||
def __init__(self, model_class: Type, field_name: str) -> None:
|
||||
self.model_class = model_class
|
||||
self.field_name = field_name
|
||||
self._django_name = self._resolve_django_name()
|
||||
self._django_name: str = self._resolve_django_name()
|
||||
|
||||
def _resolve_django_name(self):
|
||||
def _resolve_django_name(self) -> str:
|
||||
"""将 Python 字段名解析为 Django ORM 查询使用的字段名。"""
|
||||
cache_key = (self.model_class.__name__, self.field_name)
|
||||
if cache_key in FieldExpression._resolve_cache:
|
||||
return FieldExpression._resolve_cache[cache_key]
|
||||
@@ -111,221 +173,232 @@ class FieldExpression:
|
||||
FieldExpression._resolve_cache[cache_key] = self.field_name
|
||||
return self.field_name
|
||||
|
||||
def __eq__(self, other):
|
||||
# ---- 比较运算符 ----
|
||||
|
||||
def __eq__(self, other) -> Q: # type: ignore[override]
|
||||
if isinstance(other, FieldExpression):
|
||||
return NotImplemented
|
||||
if other is None:
|
||||
return Q(**{f'{self._django_name}__isnull': True})
|
||||
return Q(**{self._django_name: other})
|
||||
|
||||
def __ne__(self, other):
|
||||
def __ne__(self, other) -> Q: # type: ignore[override]
|
||||
if isinstance(other, FieldExpression):
|
||||
return NotImplemented
|
||||
return ~Q(**{self._django_name: other})
|
||||
|
||||
def __gt__(self, other):
|
||||
def __gt__(self, other) -> Q:
|
||||
return Q(**{f'{self._django_name}__gt': other})
|
||||
|
||||
def __ge__(self, other):
|
||||
def __ge__(self, other) -> Q:
|
||||
return Q(**{f'{self._django_name}__gte': other})
|
||||
|
||||
def __lt__(self, other):
|
||||
def __lt__(self, other) -> Q:
|
||||
return Q(**{f'{self._django_name}__lt': other})
|
||||
|
||||
def __le__(self, other):
|
||||
def __le__(self, other) -> Q:
|
||||
return Q(**{f'{self._django_name}__lte': other})
|
||||
|
||||
def __getitem__(self, key):
|
||||
return _JsonKeyExpression(self._django_name, key)
|
||||
|
||||
def __hash__(self):
|
||||
def __hash__(self) -> int:
|
||||
return hash((self.model_class.__name__, self.field_name))
|
||||
|
||||
def __and__(self, other):
|
||||
# ---- 逻辑运算符 ----
|
||||
|
||||
def __and__(self, other) -> Q:
|
||||
if isinstance(other, Q):
|
||||
return Q(self) & other
|
||||
if isinstance(other, FieldExpression):
|
||||
return Q(self) & Q(other)
|
||||
return NotImplemented
|
||||
|
||||
def __or__(self, other):
|
||||
def __or__(self, other) -> Q:
|
||||
if isinstance(other, Q):
|
||||
return Q(self) | other
|
||||
if isinstance(other, FieldExpression):
|
||||
return Q(self) | Q(other)
|
||||
return NotImplemented
|
||||
|
||||
def __invert__(self):
|
||||
def __invert__(self) -> _InvertedFieldExpression:
|
||||
return _InvertedFieldExpression(self)
|
||||
|
||||
def is_(self, other):
|
||||
# ---- 查询方法 ----
|
||||
|
||||
def is_(self, other) -> Q:
|
||||
"""``field IS NULL`` 或 ``field IS value``。"""
|
||||
if other is None:
|
||||
return Q(**{f'{self._django_name}__isnull': True})
|
||||
return Q(**{self._django_name: other})
|
||||
|
||||
def is_not(self, other):
|
||||
def is_not(self, other) -> Q:
|
||||
"""``field IS NOT NULL`` 或 ``field IS NOT value``。"""
|
||||
if other is None:
|
||||
return Q(**{f'{self._django_name}__isnull': False})
|
||||
return ~Q(**{self._django_name: other})
|
||||
|
||||
def like(self, pattern):
|
||||
def like(self, pattern: str) -> Q:
|
||||
"""模糊匹配(不区分大小写),自动去除 ``%`` 通配符。"""
|
||||
clean = pattern.strip('%')
|
||||
return Q(**{f'{self._django_name}__icontains': clean})
|
||||
|
||||
def contains(self, value):
|
||||
def contains(self, value: str) -> Q:
|
||||
"""包含子串(区分大小写)。"""
|
||||
return Q(**{f'{self._django_name}__contains': value})
|
||||
|
||||
def ilike(self, pattern):
|
||||
def ilike(self, pattern: str) -> Q:
|
||||
"""like() 的别名。"""
|
||||
return self.like(pattern)
|
||||
|
||||
def startswith(self, value):
|
||||
def startswith(self, value: str) -> Q:
|
||||
"""前缀匹配(区分大小写)。"""
|
||||
return Q(**{f'{self._django_name}__startswith': value})
|
||||
|
||||
def istartswith(self, value):
|
||||
def istartswith(self, value: str) -> Q:
|
||||
"""前缀匹配(不区分大小写)。"""
|
||||
return Q(**{f'{self._django_name}__istartswith': value})
|
||||
|
||||
def endswith(self, value):
|
||||
def endswith(self, value: str) -> Q:
|
||||
"""后缀匹配(区分大小写)。"""
|
||||
return Q(**{f'{self._django_name}__endswith': value})
|
||||
|
||||
def iendswith(self, value):
|
||||
def iendswith(self, value: str) -> Q:
|
||||
"""后缀匹配(不区分大小写)。"""
|
||||
return Q(**{f'{self._django_name}__iendswith': value})
|
||||
|
||||
def in_(self, values):
|
||||
def in_(self, values) -> Q:
|
||||
"""``field IN (values)``,支持列表和 FluentQuery 子查询。"""
|
||||
if hasattr(values, '_qs'):
|
||||
return Q(**{f'{self._django_name}__in': values._qs.values(self._django_name)})
|
||||
return Q(**{f'{self._django_name}__in': values})
|
||||
|
||||
def between(self, low, high):
|
||||
def between(self, low, high) -> Q:
|
||||
"""``field BETWEEN low AND high``。"""
|
||||
return Q(**{f'{self._django_name}__gte': low}) & Q(**{f'{self._django_name}__lte': high})
|
||||
|
||||
def regex(self, pattern):
|
||||
def regex(self, pattern: str) -> Q:
|
||||
"""正则匹配(区分大小写)。"""
|
||||
return Q(**{f'{self._django_name}__regex': pattern})
|
||||
|
||||
def iregex(self, pattern):
|
||||
def iregex(self, pattern: str) -> Q:
|
||||
"""正则匹配(不区分大小写)。"""
|
||||
return Q(**{f'{self._django_name}__iregex': pattern})
|
||||
|
||||
def desc(self):
|
||||
# ---- 排序辅助 ----
|
||||
|
||||
def desc(self) -> str:
|
||||
"""返回降序排序字符串 ``'-field_name'``。"""
|
||||
return f'-{self._django_name}'
|
||||
|
||||
def asc(self):
|
||||
def asc(self) -> str:
|
||||
"""返回升序排序字符串 ``'field_name'``。"""
|
||||
return self._django_name
|
||||
|
||||
def __repr__(self):
|
||||
# ---- JSON 字段访问 ----
|
||||
|
||||
def __getitem__(self, key: str) -> _JsonKeyExpression:
|
||||
"""访问 JSON 字段内的键,如 ``Model.meta['key'] == value``。"""
|
||||
return _JsonKeyExpression(self._django_name, key)
|
||||
|
||||
# ---- 显示 ----
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f'FieldExpr({self.model_class.__name__}.{self.field_name})'
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return self._django_name
|
||||
|
||||
|
||||
class _JsonKeyExpression:
|
||||
def __init__(self, field_name, key):
|
||||
"""JSON 字段键表达式,由 ``Model.json_field['key']`` 产生。"""
|
||||
|
||||
def __init__(self, field_name: str, key: str) -> None:
|
||||
self.field_name = field_name
|
||||
self.key = key
|
||||
|
||||
def _lookup(self, suffix=''):
|
||||
def _lookup(self, suffix: str = '') -> str:
|
||||
if suffix:
|
||||
return f'{self.field_name}__{self.key}__{suffix}'
|
||||
return f'{self.field_name}__{self.key}'
|
||||
|
||||
def cast(self, type_hint):
|
||||
def cast(self, type_hint) -> _CastJsonExpression:
|
||||
"""将 JSON 值转换为指定类型后再比较。"""
|
||||
return _CastJsonExpression(self.field_name, self.key, type_hint)
|
||||
|
||||
def __eq__(self, other):
|
||||
def __eq__(self, other) -> Q: # type: ignore[override]
|
||||
return Q(**{self._lookup(): other})
|
||||
|
||||
def __ne__(self, other):
|
||||
def __ne__(self, other) -> Q: # type: ignore[override]
|
||||
return ~Q(**{self._lookup(): other})
|
||||
|
||||
def __gt__(self, other):
|
||||
def __gt__(self, other) -> Q:
|
||||
return Q(**{self._lookup('gt'): other})
|
||||
|
||||
def __ge__(self, other):
|
||||
def __ge__(self, other) -> Q:
|
||||
return Q(**{self._lookup('gte'): other})
|
||||
|
||||
def __lt__(self, other):
|
||||
def __lt__(self, other) -> Q:
|
||||
return Q(**{self._lookup('lt'): other})
|
||||
|
||||
def __le__(self, other):
|
||||
def __le__(self, other) -> Q:
|
||||
return Q(**{self._lookup('lte'): other})
|
||||
|
||||
def between(self, low, high):
|
||||
def between(self, low, high) -> Q:
|
||||
return Q(**{self._lookup('gte'): low}) & Q(**{self._lookup('lte'): high})
|
||||
|
||||
def __repr__(self):
|
||||
def __repr__(self) -> str:
|
||||
return f'JsonKeyExpr({self.field_name}[{self.key!r}])'
|
||||
|
||||
|
||||
class _CastJsonExpression:
|
||||
def __init__(self, field_name, key, type_hint):
|
||||
"""类型转换后的 JSON 键表达式。"""
|
||||
|
||||
def __init__(self, field_name: str, key: str, type_hint) -> None:
|
||||
self.field_name = field_name
|
||||
self.key = key
|
||||
self.type_hint = type_hint
|
||||
|
||||
def _lookup(self, suffix=''):
|
||||
def _lookup(self, suffix: str = '') -> str:
|
||||
if suffix:
|
||||
return f'{self.field_name}__{self.key}__{suffix}'
|
||||
return f'{self.field_name}__{self.key}'
|
||||
|
||||
def between(self, low, high):
|
||||
return Q(**{self._lookup('gte'): low}) & Q(**{self._lookup('lte'): high})
|
||||
|
||||
def __eq__(self, other):
|
||||
def __eq__(self, other) -> Q: # type: ignore[override]
|
||||
return Q(**{self._lookup(): other})
|
||||
|
||||
def __ne__(self, other):
|
||||
def __ne__(self, other) -> Q: # type: ignore[override]
|
||||
return ~Q(**{self._lookup(): other})
|
||||
|
||||
def __gt__(self, other):
|
||||
def __gt__(self, other) -> Q:
|
||||
return Q(**{self._lookup('gt'): other})
|
||||
|
||||
def __ge__(self, other):
|
||||
def __ge__(self, other) -> Q:
|
||||
return Q(**{self._lookup('gte'): other})
|
||||
|
||||
def __lt__(self, other):
|
||||
def __lt__(self, other) -> Q:
|
||||
return Q(**{self._lookup('lt'): other})
|
||||
|
||||
def __le__(self, other):
|
||||
def __le__(self, other) -> Q:
|
||||
return Q(**{self._lookup('lte'): other})
|
||||
|
||||
def __repr__(self):
|
||||
def between(self, low, high) -> Q:
|
||||
return Q(**{self._lookup('gte'): low}) & Q(**{self._lookup('lte'): high})
|
||||
|
||||
def __repr__(self) -> str:
|
||||
type_name = getattr(self.type_hint, '__name__', str(self.type_hint))
|
||||
return f'CastJsonExpr({self.field_name}[{self.key!r}]->{type_name})'
|
||||
|
||||
|
||||
class ForeignKeyIdDescriptor:
|
||||
def __init__(self, fk_name):
|
||||
self.fk_name = fk_name
|
||||
self.id_attr = fk_name + '_id'
|
||||
|
||||
def __get__(self, obj, objtype=None):
|
||||
if obj is None:
|
||||
return FieldExpression(objtype, self.id_attr)
|
||||
return getattr(obj, self.id_attr)
|
||||
|
||||
def __set__(self, obj, value):
|
||||
setattr(obj, self.id_attr, value)
|
||||
|
||||
|
||||
class FieldExpressionDescriptor:
|
||||
def __init__(self, field_name):
|
||||
self.field_name = field_name
|
||||
|
||||
def __get__(self, obj, objtype=None):
|
||||
if obj is None:
|
||||
return FieldExpression(objtype, self.field_name)
|
||||
return getattr(obj, self.field_name)
|
||||
|
||||
def __set__(self, obj, value):
|
||||
setattr(obj, self.field_name, value)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# QModel 元类与基类
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class QModelBase(models.base.ModelBase):
|
||||
def __new__(mcs, name, bases, namespace, **kwargs):
|
||||
from django.db import models as dj_models
|
||||
for attr_name, attr_val in list(namespace.items()):
|
||||
if isinstance(attr_val, dj_models.Field) and not attr_val.db_column:
|
||||
# ForeignKey/OneToOneField 的数据库列名是 attr_name + '_id'
|
||||
if isinstance(attr_val, (dj_models.ForeignKey, dj_models.OneToOneField)):
|
||||
attr_val.db_column = attr_name + '_id'
|
||||
else:
|
||||
attr_val.db_column = attr_name
|
||||
"""QModel 元类,负责:
|
||||
|
||||
1. 根据 ``GVSDSDK_MANAGE_TABLES`` 设置动态控制 ``managed``
|
||||
2. 包装字段描述符,使类级别访问返回 FieldExpression
|
||||
"""
|
||||
|
||||
def __new__(mcs, name, bases, namespace, **kwargs):
|
||||
# 动态设置 managed:子服务器可设 GVSDSDK_MANAGE_TABLES=True 让 gvsdsdk 管理表
|
||||
meta = namespace.get('Meta')
|
||||
if meta and getattr(meta, 'app_label', None) == 'gvsdsdk':
|
||||
@@ -336,16 +409,132 @@ class QModelBase(models.base.ModelBase):
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return super().__new__(mcs, name, bases, namespace, **kwargs)
|
||||
cls = super().__new__(mcs, name, bases, namespace, **kwargs)
|
||||
|
||||
# 模型创建后,包装字段描述符以支持类级别 FieldExpression
|
||||
if not cls._meta.abstract:
|
||||
_wrap_field_descriptors(cls)
|
||||
|
||||
return cls
|
||||
|
||||
|
||||
def _wrap_field_descriptors(cls: Type) -> None:
|
||||
"""包装 Django 字段描述符,使类级别访问返回 FieldExpression,
|
||||
实例级别完全委托给 Django 原生描述符。
|
||||
|
||||
Django 的字段描述符(DeferredAttribute / ForwardManyToOneDescriptor):
|
||||
- ``__get__(None, cls)`` → 返回字段对象本身(类级别)
|
||||
- ``__get__(instance, cls)`` → 返回字段值(实例级别)
|
||||
|
||||
我们包装后:
|
||||
- ``__get__(None, cls)`` → 返回 FieldExpression(类级别,用于链式 OOP)
|
||||
- ``__get__(instance, cls)`` → 委托给原生描述符(实例级别,正常行为)
|
||||
"""
|
||||
from django.db.models.query_utils import DeferredAttribute
|
||||
from django.db.models.fields.related_descriptors import (
|
||||
ForwardManyToOneDescriptor,
|
||||
ForwardOneToOneDescriptor,
|
||||
)
|
||||
|
||||
for field in cls._meta.local_fields:
|
||||
fname = field.name
|
||||
if fname.startswith('_'):
|
||||
continue
|
||||
|
||||
original_desc = cls.__dict__.get(fname)
|
||||
if original_desc is None:
|
||||
continue
|
||||
|
||||
# 只包装已知的 Django 描述符类型
|
||||
if isinstance(original_desc, (DeferredAttribute, ForwardManyToOneDescriptor,
|
||||
ForwardOneToOneDescriptor)):
|
||||
wrapper = _FieldExpressionWrapper(original_desc, cls, fname)
|
||||
setattr(cls, fname, wrapper)
|
||||
|
||||
|
||||
class _FieldExpressionWrapper:
|
||||
"""包装 Django 字段描述符,类级别返回 FieldExpression,实例级别委托给原生描述符。
|
||||
|
||||
这是 QModel 字段表达式机制的核心:当你在类上访问 ``Model.field_name`` 时,
|
||||
返回 ``FieldExpression`` 对象,支持 ``Model.field == value`` 等运算符构建 Q 对象;
|
||||
当你在实例上访问 ``obj.field_name`` 时,行为与原生 Django 完全一致。
|
||||
"""
|
||||
|
||||
def __init__(self, original_descriptor, model_class: Type, field_name: str) -> None:
|
||||
self._original = original_descriptor
|
||||
self._model = model_class
|
||||
self._field_name = field_name
|
||||
self._has_set: bool = hasattr(original_descriptor, '__set__')
|
||||
|
||||
def __get__(self, obj, objtype=None):
|
||||
if obj is None:
|
||||
# 类级别访问:返回 FieldExpression
|
||||
return FieldExpression(self._model, self._field_name)
|
||||
# 实例级别访问:委托给原生 Django 描述符
|
||||
return self._original.__get__(obj, objtype)
|
||||
|
||||
def __set__(self, obj, value) -> None:
|
||||
if self._has_set:
|
||||
# FK/O2O 描述符有 __set__
|
||||
self._original.__set__(obj, value)
|
||||
else:
|
||||
# 普通字段(DeferredAttribute)没有 __set__,直接写 __dict__
|
||||
obj.__dict__[self._field_name] = value
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f'_FieldExpressionWrapper({self._model.__name__}.{self._field_name})'
|
||||
|
||||
|
||||
class QModel(models.Model, metaclass=QModelBase):
|
||||
"""QModel 兼容层基类,提供 SQLAlchemy 风格的链式 OOP 查询接口。
|
||||
|
||||
所有继承 QModel 的模型自动获得:
|
||||
|
||||
- ``Model.query`` : 返回 FluentQuery,支持链式查询
|
||||
- ``Model.field_name`` : 类级别访问返回 FieldExpression,支持表达式查询
|
||||
- ``Model.field_name`` : 实例级别访问返回字段值(与普通 Django Model 一致)
|
||||
- ``Model[condition]`` : 快捷 filter 语法
|
||||
|
||||
用法示例::
|
||||
|
||||
# 链式查询
|
||||
orders = Dingdan.query.filter(zhuangtai=8).order_by('-create_time').to_list()
|
||||
|
||||
# 字段表达式
|
||||
Dingdan.query.filter(Dingdan.jine > 100, Dingdan.zhuangtai.in_([1,2,3]))
|
||||
|
||||
# 聚合
|
||||
Dingdan.query.aggregate(total=Sum('jine'))
|
||||
|
||||
# values / annotate
|
||||
Dingdan.query.values('zhuangtai').annotate(cnt=Count('id'))
|
||||
|
||||
# 分页
|
||||
Dingdan.query.paginate(page=1, per_page=20)
|
||||
|
||||
# 快捷 filter
|
||||
Dingdan[Dingdan.zhuangtai == 8].order_by('-create_time').to_list()
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
@classproperty
|
||||
def query(cls):
|
||||
from .fluent import FluentQuery
|
||||
def query(cls) -> FluentQuery[Self]:
|
||||
"""返回 FluentQuery 兼容层查询构建器,支持链式 OOP 查询。"""
|
||||
return FluentQuery(cls)
|
||||
|
||||
@classmethod
|
||||
def __class_getitem__(cls, item) -> FluentQuery[Self]:
|
||||
"""支持 ``Model[condition]`` 语法,返回 filter 后的 FluentQuery。
|
||||
|
||||
用法::
|
||||
|
||||
Dingdan[Dingdan.zhuangtai == 8].order_by('-create_time').to_list()
|
||||
Dingdan[{'zhuangtai': 8}].to_list()
|
||||
"""
|
||||
if isinstance(item, Q):
|
||||
return cls.query.filter(item)
|
||||
if isinstance(item, dict):
|
||||
return cls.query.filter(**item)
|
||||
return cls.query.filter(item)
|
||||
+826
-813
File diff suppressed because it is too large.
Load diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loaded 100 of 114 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user