From f93d797382ea48ac45d73b7c683a96cd924f0ab8 Mon Sep 17 00:00:00 2001 From: chenhao Date: Fri, 27 Feb 2026 15:12:58 +0800 Subject: [PATCH] =?UTF-8?q?feat(database):=20=E6=96=B0=E5=A2=9E=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E5=BA=93=E8=AE=BE=E8=AE=A1=E6=96=87=E6=A1=A3=E5=92=8C?= =?UTF-8?q?PostgreSQL=E5=BB=BA=E8=A1=A8=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 创建数据库结构文档 backend/design/db_schema.md - 创建PostgreSQL建表脚本 backend/design/db_schema_pgsql.sql - 定义租户与组织相关表结构 (sys_tenant, sys_org) - 定义用户与角色相关表结构 (sys_user, sys_role, sys_user_role, sys_tenant_user) - 定义权限字典参数表结构 (sys_permission, sys_dict_type, sys_dict_item, sys_param) - 定义日志与平台配置表结构 (sys_log, sys_platform_config) - 初始化基础字典数据和平台配置默认值 feat(frontend): 添加个人资料页面和密码重置功能 - 创建个人资料页面 frontend/src/pages/Profile.tsx - 添加密码重置页面 frontend/src/pages/ResetPassword.tsx - 实现用户信息编辑和密码修改功能 - 添加页面头部组件 frontend/src/components/shared/PageHeader - 实现分页工具函数 frontend/src/utils/pagination.ts feat(frontend): 添加字典数据缓存Hook - 创建useDict Hook用于字典数据获取缓存 frontend/src/hooks/useDict.ts - 实现字典数据请求去重和内存缓存机制 --- backend/design/db_schema.md | 223 +++++++++++++++ backend/design/db_schema_pgsql.sql | 265 ++++++++++++++++++ .../components/shared/PageHeader/index.tsx | 31 ++ frontend/src/hooks/useDict.ts | 45 +++ frontend/src/pages/Profile.tsx | 158 +++++++++++ frontend/src/pages/ResetPassword.tsx | 86 ++++++ frontend/src/pages/Users.tsx | 3 +- frontend/src/utils/pagination.ts | 25 ++ 8 files changed, 835 insertions(+), 1 deletion(-) create mode 100644 backend/design/db_schema.md create mode 100644 backend/design/db_schema_pgsql.sql create mode 100644 frontend/src/components/shared/PageHeader/index.tsx create mode 100644 frontend/src/hooks/useDict.ts create mode 100644 frontend/src/pages/Profile.tsx create mode 100644 frontend/src/pages/ResetPassword.tsx create mode 100644 frontend/src/utils/pagination.ts diff --git a/backend/design/db_schema.md b/backend/design/db_schema.md new file mode 100644 index 0000000..0c62f4e --- /dev/null +++ b/backend/design/db_schema.md @@ -0,0 +1,223 @@ +# 数据库结构文档(PostgreSQL) + +本文档根据 `backend/design/db_schema_pgsql.sql` 生成,描述当前核心表结构、字段、约束与索引。 + +## 0. 租户与组织 + +### 0.1 `sys_tenant`(租户表) +| 字段 | 类型 | 约束 | 说明 | +| --- | --- | --- | --- | +| id | BIGSERIAL | PK | 租户ID | +| tenant_code | VARCHAR(64) | NOT NULL, UNIQUE | 租户编码 | +| tenant_name | VARCHAR(128) | NOT NULL | 租户名称 | +| status | SMALLINT | NOT NULL, DEFAULT 1 | 状态 | +| expire_time | TIMESTAMP(6) | | 过期时间 | +| contact_name | VARCHAR(64) | | 联系人 | +| contact_phone | VARCHAR(32) | | 联系电话 | +| remark | VARCHAR(255) | | 备注 | +| created_at | TIMESTAMP(6) | NOT NULL, DEFAULT CURRENT_TIMESTAMP | 创建时间 | +| updated_at | TIMESTAMP(6) | NOT NULL, DEFAULT CURRENT_TIMESTAMP | 更新时间 | +| is_deleted | SMALLINT | DEFAULT 0 | 逻辑删除标记 | + +索引: +- `uk_tenant_code`:`UNIQUE (tenant_code) WHERE is_deleted = FALSE` + +### 0.2 `sys_org`(组织架构表) +| 字段 | 类型 | 约束 | 说明 | +| --- | --- | --- | --- | +| id | BIGSERIAL | PK | 组织ID | +| tenant_id | BIGINT | NOT NULL | 租户ID | +| parent_id | BIGINT | | 父级组织ID | +| org_name | VARCHAR(128) | NOT NULL | 组织名称 | +| org_code | VARCHAR(64) | | 组织编码 | +| org_path | VARCHAR(512) | | 组织路径 | +| sort_order | INTEGER | DEFAULT 0 | 排序 | +| status | SMALLINT | DEFAULT 1 | 状态 | +| created_at | TIMESTAMP(6) | NOT NULL, DEFAULT CURRENT_TIMESTAMP | 创建时间 | +| updated_at | TIMESTAMP(6) | NOT NULL, DEFAULT CURRENT_TIMESTAMP | 更新时间 | +| is_deleted | SMALLINT | DEFAULT 0 | 逻辑删除标记 | + +外键: +- `fk_org_parent`:`parent_id -> sys_org(id)` +- `fk_org_tenant`:`tenant_id -> sys_tenant(id)` + +索引: +- `idx_org_tenant`:`(tenant_id)` + +## 1. 用户与角色 + +### 1.1 `sys_user`(用户表) +| 字段 | 类型 | 约束 | 说明 | +| --- | --- | --- | --- | +| user_id | BIGSERIAL | PK | 用户ID | +| username | VARCHAR(50) | NOT NULL, UNIQUE | 登录名 | +| display_name | VARCHAR(50) | NOT NULL | 显示名 | +| email | VARCHAR(100) | | 邮箱 | +| phone | VARCHAR(30) | UNIQUE | 手机号 | +| password_hash | VARCHAR(255) | NOT NULL | 密码哈希 | +| status | SMALLINT | NOT NULL, DEFAULT 1 | 状态 | +| pwd_reset_required | SMALLINT | DEFAULT 1 | 首次登录是否需改密 | +| is_deleted | SMALLINT | NOT NULL, DEFAULT 0 | 逻辑删除标记 | +| created_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 创建时间 | +| updated_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 更新时间 | +| is_platform_admin | BOOLEAN | DEFAULT false | 是否平台管理员 | + +索引: +- `uk_user_username`:`UNIQUE (username) WHERE is_deleted = FALSE` + +### 1.2 `sys_role`(角色表) +| 字段 | 类型 | 约束 | 说明 | +| --- | --- | --- | --- | +| role_id | BIGSERIAL | PK | 角色ID | +| tenant_id | BIGINT | | 租户ID | +| role_code | VARCHAR(50) | NOT NULL, UNIQUE | 角色编码 | +| role_name | VARCHAR(50) | NOT NULL | 角色名称 | +| status | SMALLINT | NOT NULL, DEFAULT 1 | 状态 | +| remark | TEXT | | 备注 | +| is_deleted | SMALLINT | NOT NULL, DEFAULT 0 | 逻辑删除标记 | +| created_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 创建时间 | +| updated_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 更新时间 | + +索引: +- `idx_sys_role_tenant`:`(tenant_id)` +- `uk_role_code`:`UNIQUE (tenant_id, role_code) WHERE is_deleted = FALSE` + +### 1.3 `sys_user_role`(用户-角色关联表) +| 字段 | 类型 | 约束 | 说明 | +| --- | --- | --- | --- | +| id | BIGSERIAL | PK | 关联ID | +| tenant_id | BIGINT | | 租户ID | +| user_id | BIGINT | NOT NULL | 用户ID | +| role_id | BIGINT | NOT NULL | 角色ID | +| is_deleted | SMALLINT | NOT NULL, DEFAULT 0 | 逻辑删除标记 | +| created_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 创建时间 | +| updated_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 更新时间 | + +唯一约束: +- `UNIQUE (user_id, role_id)` + +### 1.4 `sys_tenant_user`(租户成员关联表) +| 字段 | 类型 | 约束 | 说明 | +| --- | --- | --- | --- | +| id | BIGSERIAL | PK | 关联ID | +| user_id | BIGINT | NOT NULL | 用户ID | +| tenant_id | BIGINT | NOT NULL | 租户ID | +| org_id | BIGINT | | 组织ID | +| status | SMALLINT | DEFAULT 1 | 状态 | +| is_deleted | SMALLINT | DEFAULT 0 | 逻辑删除标记 | +| created_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 创建时间 | +| updated_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 更新时间 | + +索引: +- `uk_tenant_user`:`UNIQUE (user_id, tenant_id) WHERE is_deleted = 0` + +## 2. 权限/字典/参数(全局共享) + +### 2.1 `sys_permission`(权限表) +| 字段 | 类型 | 约束 | 说明 | +| --- | --- | --- | --- | +| perm_id | BIGSERIAL | PK | 权限ID | +| parent_id | BIGINT | | 父级权限ID | +| name | VARCHAR(100) | NOT NULL | 权限名称 | +| code | VARCHAR(100) | NOT NULL, UNIQUE | 权限编码 | +| perm_type | VARCHAR(20) | NOT NULL | 权限类型 | +| level | INTEGER | NOT NULL | 层级 | +| path | VARCHAR(255) | | 路径 | +| component | VARCHAR(255) | | 组件 | +| icon | VARCHAR(100) | | 图标 | +| sort_order | INTEGER | NOT NULL, DEFAULT 0 | 排序 | +| is_visible | SMALLINT | NOT NULL, DEFAULT 1 | 是否可见 | +| status | SMALLINT | NOT NULL, DEFAULT 1 | 状态 | +| description | TEXT | | 描述 | +| meta | JSONB | | 扩展信息 | +| is_deleted | SMALLINT | NOT NULL, DEFAULT 0 | 逻辑删除标记 | +| created_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 创建时间 | +| updated_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 更新时间 | + +### 2.2 `sys_dict_type`(字典类型表) +| 字段 | 类型 | 约束 | 说明 | +| --- | --- | --- | --- | +| dict_type_id | BIGSERIAL | PK | 类型ID | +| type_code | VARCHAR(50) | NOT NULL, UNIQUE | 类型编码 | +| type_name | VARCHAR(50) | NOT NULL | 类型名称 | +| status | SMALLINT | DEFAULT 1 | 状态 | +| remark | TEXT | | 备注 | +| created_at | TIMESTAMP | NOT NULL, DEFAULT NOW() | 创建时间 | +| updated_at | TIMESTAMP | NOT NULL, DEFAULT NOW() | 更新时间 | +| is_deleted | SMALLINT | DEFAULT 0 | 逻辑删除标记 | + +**初始化数据:** +- `sys_common_status`: 通用状态 (启用/禁用) +- `sys_permission_type`: 权限类型 (目录/菜单/按钮) +- `sys_common_visibility`: 可见性 (显示/隐藏) +- `sys_permission_level`: 权限层级 (1, 2, 3) +- `sys_log_type`: 日志类型 (LOGIN/OPERATION) +- `sys_param_type`: 参数类型 (String/Number/Boolean/JSON) +- `sys_log_status`: 操作状态 (成功/失败) + +### 2.3 `sys_dict_item`(字典项表) +| 字段 | 类型 | 约束 | 说明 | +| --- | --- | --- | --- | +| dict_item_id | BIGSERIAL | PK | 字典项ID | +| type_code | VARCHAR(50) | NOT NULL | 字典类型编码 | +| item_label | VARCHAR(100) | NOT NULL | 展示文本 | +| item_value | VARCHAR(100) | NOT NULL | 存储值 | +| sort_order | INT | DEFAULT 0 | 排序 | +| status | SMALLINT | DEFAULT 1 | 状态 | +| created_at | TIMESTAMP | NOT NULL, DEFAULT NOW() | 创建时间 | +| updated_at | TIMESTAMP | NOT NULL, DEFAULT NOW() | 更新时间 | +| is_deleted | SMALLINT | DEFAULT 0 | 逻辑删除标记 | + +索引: +- `idx_dict_item_type`:`(type_code)` +- `uk_dict_item_value`:`UNIQUE (type_code, item_value)` + +### 2.4 `sys_param`(系统参数表) +| 字段 | 类型 | 约束 | 说明 | +| --- | --- | --- | --- | +| id | BIGSERIAL | PK | 参数ID | +| param_key | VARCHAR(100) | NOT NULL, UNIQUE | 参数键 | +| param_value | TEXT | NOT NULL | 参数值 | +| param_type | VARCHAR(20) | NOT NULL | 参数类型 | +| is_system | SMALLINT | DEFAULT 0 | 是否系统内置 | +| status | SMALLINT | DEFAULT 1 | 状态 | +| description | TEXT | | 描述 | +| created_at | TIMESTAMP | NOT NULL, DEFAULT NOW() | 创建时间 | + +## 3. 日志(租户隔离) + +### 3.1 `sys_log`(系统日志表) +| 字段 | 类型 | 约束 | 说明 | +| --- | --- | --- | --- | +| id | BIGSERIAL | PK | 日志ID | +| tenant_id | BIGINT | NOT NULL, DEFAULT 0 | 租户ID | +| user_id | BIGINT | | 用户ID | +| username | VARCHAR(50) | | 用户名 | +| log_type | VARCHAR(20) | | 日志类型(如 LOGIN、OPERATION) | +| operation | VARCHAR(100) | NOT NULL | 操作描述 | +| method | VARCHAR(200) | | 方法 | +| params | TEXT | | 请求参数 | +| status | SMALLINT | DEFAULT 1 | 状态 | +| ip | VARCHAR(50) | | IP | +| duration | BIGINT | | 耗时(ms) | +| created_at | TIMESTAMP | NOT NULL, DEFAULT NOW() | 创建时间 | + +索引: +- `idx_log_tenant_type`:`(tenant_id, log_type, created_at)` + +## 4. 平台配置 + +### 4.1 `sys_platform_config`(平台管理表) +| 字段 | 类型 | 约束 | 说明 | +| --- | --- | --- | --- | +| id | BIGINT | PK | 固定为 1 | +| project_name | VARCHAR(128) | NOT NULL | 项目名称 | +| logo_url | VARCHAR(512) | | Logo URL | +| icon_url | VARCHAR(512) | | Icon URL | +| login_bg_url | VARCHAR(512) | | 登录页背景 | +| icp_info | VARCHAR(128) | | 备案信息 | +| copyright_info | VARCHAR(255) | | 版权信息 | +| system_description | TEXT | | 系统描述 | +| created_at | TIMESTAMP | NOT NULL | 创建时间 | +| updated_at | TIMESTAMP | NOT NULL | 更新时间 | + diff --git a/backend/design/db_schema_pgsql.sql b/backend/design/db_schema_pgsql.sql new file mode 100644 index 0000000..7db9b43 --- /dev/null +++ b/backend/design/db_schema_pgsql.sql @@ -0,0 +1,265 @@ +-- PostgreSQL Database Schema for iMeeting (Multi-tenant) +-- 0 为系统预留租户 ID + +-- ---------------------------- +-- 0. 租户与组织 +-- ---------------------------- + +-- 租户表 +CREATE TABLE sys_tenant ( + id BIGSERIAL PRIMARY KEY, + tenant_code VARCHAR(64) NOT NULL UNIQUE, + tenant_name VARCHAR(128) NOT NULL, + status SMALLINT NOT NULL DEFAULT 1, + expire_time TIMESTAMP(6), + contact_name VARCHAR(64), + contact_phone VARCHAR(32), + remark VARCHAR(255), + created_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + is_deleted SMALLINT DEFAULT 0 +); +CREATE UNIQUE INDEX uk_tenant_code ON sys_tenant (tenant_code) WHERE is_deleted = FALSE; + +-- 组织架构表 +DROP TABLE IF EXISTS sys_org CASCADE; + +CREATE TABLE sys_org ( + id BIGSERIAL PRIMARY KEY, + tenant_id BIGINT NOT NULL, + parent_id BIGINT, + org_name VARCHAR(128) NOT NULL, + org_code VARCHAR(64), + org_path VARCHAR(512), + sort_order INTEGER DEFAULT 0, + status SMALLINT DEFAULT 1, + created_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + is_deleted SMALLINT DEFAULT 0, + CONSTRAINT fk_org_parent FOREIGN KEY (parent_id) + REFERENCES sys_org(id), + CONSTRAINT fk_org_tenant FOREIGN KEY (tenant_id) + REFERENCES sys_tenant(id) +); + +CREATE INDEX idx_org_tenant ON sys_org (tenant_id); + +-- ---------------------------- +-- 1. 用户与角色 +-- ---------------------------- + +-- 用户表 +DROP TABLE IF EXISTS sys_user CASCADE; + +CREATE TABLE sys_user ( + user_id BIGSERIAL PRIMARY KEY, + username VARCHAR(50) NOT NULL UNIQUE, + display_name VARCHAR(50) NOT NULL, + email VARCHAR(100), + phone VARCHAR(30) UNIQUE, + password_hash VARCHAR(255) NOT NULL, + status SMALLINT NOT NULL DEFAULT 1, + is_deleted SMALLINT NOT NULL DEFAULT 0, + pwd_reset_required SMALLINT DEFAULT 1, + created_at TIMESTAMP(6) NOT NULL DEFAULT now(), + updated_at TIMESTAMP(6) NOT NULL DEFAULT now(), + is_platform_admin BOOLEAN DEFAULT false +); +CREATE UNIQUE INDEX uk_user_username ON sys_user (username) WHERE is_deleted = FALSE; + +-- 角色表 +DROP TABLE IF EXISTS sys_role CASCADE; + +CREATE TABLE sys_role ( + role_id BIGSERIAL PRIMARY KEY, + tenant_id BIGINT, + role_code VARCHAR(50) NOT NULL UNIQUE, + role_name VARCHAR(50) NOT NULL, + status SMALLINT NOT NULL DEFAULT 1, + remark TEXT, + is_deleted SMALLINT NOT NULL DEFAULT 0, + created_at TIMESTAMP(6) NOT NULL DEFAULT now(), + updated_at TIMESTAMP(6) NOT NULL DEFAULT now() +); + +CREATE INDEX idx_sys_role_tenant ON sys_role (tenant_id); +CREATE UNIQUE INDEX uk_role_code ON sys_role (tenant_id, role_code) WHERE is_deleted = FALSE; + +-- 用户-角色关联表 (无 tenant_id, 随 User/Role 隔离) +DROP TABLE IF EXISTS sys_user_role CASCADE; + +CREATE TABLE sys_user_role ( + id BIGSERIAL PRIMARY KEY, + tenant_id BIGINT, + user_id BIGINT NOT NULL, + role_id BIGINT NOT NULL, + is_deleted SMALLINT NOT NULL DEFAULT 0, + created_at TIMESTAMP(6) NOT NULL DEFAULT now(), + updated_at TIMESTAMP(6) NOT NULL DEFAULT now(), + UNIQUE (user_id, role_id) +); + +-- ---------------------------- +-- 2. 权限/字典/参数 (全局共享, 无 tenant_id) +-- ---------------------------- + +DROP TABLE IF EXISTS sys_permission CASCADE; + +CREATE TABLE sys_permission ( + perm_id BIGSERIAL PRIMARY KEY, + parent_id BIGINT, + name VARCHAR(100) NOT NULL, + code VARCHAR(100) NOT NULL UNIQUE, + perm_type VARCHAR(20) NOT NULL, + level INTEGER NOT NULL, + path VARCHAR(255), + component VARCHAR(255), + icon VARCHAR(100), + sort_order INTEGER NOT NULL DEFAULT 0, + is_visible SMALLINT NOT NULL DEFAULT 1, + status SMALLINT NOT NULL DEFAULT 1, + description TEXT, + meta JSONB, + is_deleted SMALLINT NOT NULL DEFAULT 0, + created_at TIMESTAMP(6) NOT NULL DEFAULT now(), + updated_at TIMESTAMP(6) NOT NULL DEFAULT now() +); + +DROP TABLE IF EXISTS sys_tenant_user CASCADE; + +CREATE TABLE sys_tenant_user ( + id BIGSERIAL PRIMARY KEY, + user_id BIGINT NOT NULL, + tenant_id BIGINT NOT NULL, + org_id BIGINT, + status SMALLINT DEFAULT 1, + is_deleted SMALLINT DEFAULT 0, + created_at TIMESTAMP(6) NOT NULL DEFAULT now(), + updated_at TIMESTAMP(6) NOT NULL DEFAULT now() +); + +CREATE UNIQUE INDEX uk_tenant_user + ON sys_tenant_user (user_id, tenant_id) + WHERE is_deleted = 0; +CREATE TABLE sys_dict_type ( + dict_type_id BIGSERIAL PRIMARY KEY, + type_code VARCHAR(50) UNIQUE NOT NULL, + type_name VARCHAR(50) NOT NULL, + status SMALLINT DEFAULT 1, + remark TEXT, + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + updated_at TIMESTAMP NOT NULL DEFAULT NOW(), + is_deleted SMALLINT DEFAULT 0 +); + +CREATE TABLE sys_dict_item ( + dict_item_id BIGSERIAL PRIMARY KEY, + type_code VARCHAR(50) NOT NULL, + item_label VARCHAR(100) NOT NULL, + item_value VARCHAR(100) NOT NULL, + sort_order INT DEFAULT 0, + status SMALLINT DEFAULT 1, + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + updated_at TIMESTAMP NOT NULL DEFAULT NOW(), + is_deleted SMALLINT DEFAULT 0 +); +CREATE INDEX idx_dict_item_type ON sys_dict_item (type_code); +CREATE UNIQUE INDEX uk_dict_item_value ON sys_dict_item (type_code, item_value); + +CREATE TABLE sys_param ( + id BIGSERIAL PRIMARY KEY, + param_key VARCHAR(100) UNIQUE NOT NULL, + param_value TEXT NOT NULL, + param_type VARCHAR(20) NOT NULL, + is_system SMALLINT DEFAULT 0, + status SMALLINT DEFAULT 1, + description TEXT, + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + updated_at TIMESTAMP NOT NULL DEFAULT NOW(), + is_deleted SMALLINT DEFAULT 0 +); + +-- ---------------------------- +-- 3. 日志 (租户隔离) +-- ---------------------------- + +CREATE TABLE sys_log ( + id BIGSERIAL PRIMARY KEY, + tenant_id BIGINT NOT NULL DEFAULT 0, + user_id BIGINT, + username VARCHAR(50), + log_type VARCHAR(20), -- LOGIN, OPERATION + operation VARCHAR(100) NOT NULL, + method VARCHAR(200), + params TEXT, + status SMALLINT DEFAULT 1, + ip VARCHAR(50), + duration BIGINT, + created_at TIMESTAMP NOT NULL DEFAULT NOW() +); +CREATE INDEX idx_log_tenant_type ON sys_log (tenant_id, log_type, created_at); + +-- ---------------------------- +-- 4. 平台配置 (系统品牌化) +-- ---------------------------- +DROP TABLE IF EXISTS sys_platform_config CASCADE; +CREATE TABLE sys_platform_config ( + id BIGINT PRIMARY KEY, -- 固定为 1 + project_name VARCHAR(128) NOT NULL, + logo_url VARCHAR(512), + icon_url VARCHAR(512), + login_bg_url VARCHAR(512), + icp_info VARCHAR(128), + copyright_info VARCHAR(255), + system_description TEXT, + created_at TIMESTAMP(6) DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP(6) DEFAULT CURRENT_TIMESTAMP, + is_deleted SMALLINT DEFAULT 0 +); + +INSERT INTO sys_platform_config (id, project_name, copyright_info) +VALUES (1, 'iMeeting 智能会议系统', '© 2026 iMeeting Team. All rights reserved.'); + +-- ---------------------------- +-- 5. 基础初始化数据 +-- ---------------------------- + +-- 字典初始化数据 +-- sys_common_status +INSERT INTO sys_dict_type (type_code, type_name, remark) VALUES ('sys_common_status', '通用状态', '0=禁用, 1=启用'); +INSERT INTO sys_dict_item (type_code, item_label, item_value, sort_order) VALUES ('sys_common_status', '启用', '1', 1); +INSERT INTO sys_dict_item (type_code, item_label, item_value, sort_order) VALUES ('sys_common_status', '禁用', '0', 2); + +-- sys_permission_type +INSERT INTO sys_dict_type (type_code, type_name, remark) VALUES ('sys_permission_type', '权限类型', 'directory=目录, menu=菜单, button=按钮'); +INSERT INTO sys_dict_item (type_code, item_label, item_value, sort_order) VALUES ('sys_permission_type', '目录', 'directory', 1); +INSERT INTO sys_dict_item (type_code, item_label, item_value, sort_order) VALUES ('sys_permission_type', '菜单', 'menu', 2); +INSERT INTO sys_dict_item (type_code, item_label, item_value, sort_order) VALUES ('sys_permission_type', '按钮', 'button', 3); + +-- sys_common_visibility +INSERT INTO sys_dict_type (type_code, type_name, remark) VALUES ('sys_common_visibility', '可见性', '0=隐藏, 1=显示'); +INSERT INTO sys_dict_item (type_code, item_label, item_value, sort_order) VALUES ('sys_common_visibility', '显示', '1', 1); +INSERT INTO sys_dict_item (type_code, item_label, item_value, sort_order) VALUES ('sys_common_visibility', '隐藏', '0', 2); + +-- sys_permission_level +INSERT INTO sys_dict_type (type_code, type_name, remark) VALUES ('sys_permission_level', '权限层级', '1=一级入口, 2=二级子项, 3=三级按钮'); +INSERT INTO sys_dict_item (type_code, item_label, item_value, sort_order) VALUES ('sys_permission_level', '一级入口', '1', 1); +INSERT INTO sys_dict_item (type_code, item_label, item_value, sort_order) VALUES ('sys_permission_level', '二级子项', '2', 2); +INSERT INTO sys_dict_item (type_code, item_label, item_value, sort_order) VALUES ('sys_permission_level', '三级按钮', '3', 3); + +-- sys_log_type +INSERT INTO sys_dict_type (type_code, type_name, remark) VALUES ('sys_log_type', '日志类型', 'LOGIN=登录, OPERATION=操作'); +INSERT INTO sys_dict_item (type_code, item_label, item_value, sort_order) VALUES ('sys_log_type', '登录', 'LOGIN', 1); +INSERT INTO sys_dict_item (type_code, item_label, item_value, sort_order) VALUES ('sys_log_type', '操作', 'OPERATION', 2); + +-- sys_param_type +INSERT INTO sys_dict_type (type_code, type_name, remark) VALUES ('sys_param_type', '参数类型', 'String, Number, Boolean, JSON'); +INSERT INTO sys_dict_item (type_code, item_label, item_value, sort_order) VALUES ('sys_param_type', 'String', 'String', 1); +INSERT INTO sys_dict_item (type_code, item_label, item_value, sort_order) VALUES ('sys_param_type', 'Number', 'Number', 2); +INSERT INTO sys_dict_item (type_code, item_label, item_value, sort_order) VALUES ('sys_param_type', 'Boolean', 'Boolean', 3); +INSERT INTO sys_dict_item (type_code, item_label, item_value, sort_order) VALUES ('sys_param_type', 'JSON', 'JSON', 4); + +-- sys_log_status +INSERT INTO sys_dict_type (type_code, type_name, remark) VALUES ('sys_log_status', '操作状态', '1=成功, 0=失败'); +INSERT INTO sys_dict_item (type_code, item_label, item_value, sort_order) VALUES ('sys_log_status', '成功', '1', 1); +INSERT INTO sys_dict_item (type_code, item_label, item_value, sort_order) VALUES ('sys_log_status', '失败', '0', 2); diff --git a/frontend/src/components/shared/PageHeader/index.tsx b/frontend/src/components/shared/PageHeader/index.tsx new file mode 100644 index 0000000..5cbd4e5 --- /dev/null +++ b/frontend/src/components/shared/PageHeader/index.tsx @@ -0,0 +1,31 @@ +import { Typography, Space } from 'antd'; +import React from 'react'; + +const { Title, Text } = Typography; + +interface PageHeaderProps { + title: React.ReactNode; + subtitle?: React.ReactNode; + extra?: React.ReactNode; + className?: string; +} + +const PageHeader: React.FC = ({ title, subtitle, extra, className = '' }) => { + return ( +
+
+ + {title} + + {subtitle && ( + + {subtitle} + + )} +
+ {extra &&
{extra}
} +
+ ); +}; + +export default PageHeader; diff --git a/frontend/src/hooks/useDict.ts b/frontend/src/hooks/useDict.ts new file mode 100644 index 0000000..47f76e1 --- /dev/null +++ b/frontend/src/hooks/useDict.ts @@ -0,0 +1,45 @@ +import { useState, useEffect } from 'react'; +import { fetchDictItemsByTypeCode } from '../api/dict'; +import { SysDictItem } from '../types'; + +const dictCache: Record = {}; +const pendingRequests: Record[]> = {}; + +export function useDict(typeCode: string) { + const [items, setItems] = useState(dictCache[typeCode] || []); + const [loading, setLoading] = useState(!dictCache[typeCode]); + + useEffect(() => { + if (dictCache[typeCode]) { + setItems(dictCache[typeCode]); + setLoading(false); + return; + } + + let isMounted = true; + + const load = async () => { + try { + const data = await fetchDictItemsByTypeCode(typeCode); + if (isMounted) { + dictCache[typeCode] = data; + setItems(data); + } + } catch (e) { + console.error(`Failed to fetch dictionary ${typeCode}:`, e); + } finally { + if (isMounted) { + setLoading(false); + } + } + }; + + load(); + + return () => { + isMounted = false; + }; + }, [typeCode]); + + return { items, loading }; +} diff --git a/frontend/src/pages/Profile.tsx b/frontend/src/pages/Profile.tsx new file mode 100644 index 0000000..c51505f --- /dev/null +++ b/frontend/src/pages/Profile.tsx @@ -0,0 +1,158 @@ +import { + Button, + Card, + Form, + Input, + message, + Tabs, + Typography, + Row, + Col, + Space, + Avatar +} from "antd"; +import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { getCurrentUser, updateMyProfile, updateMyPassword } from "../api"; +import { UserOutlined, LockOutlined, SaveOutlined, SolutionOutlined } from "@ant-design/icons"; +import type { UserProfile } from "../types"; +import PageHeader from "../components/shared/PageHeader"; + +const { Title, Text } = Typography; + +export default function Profile() { + const { t } = useTranslation(); + const [loading, setLoading] = useState(false); + const [saving, setSaving] = useState(false); + const [user, setUser] = useState(null); + const [profileForm] = Form.useForm(); + const [pwdForm] = Form.useForm(); + + const loadUser = async () => { + setLoading(true); + try { + const data = await getCurrentUser(); + setUser(data); + profileForm.setFieldsValue(data); + } catch (e) { + // Interceptor handles error + } finally { + setLoading(false); + } + }; + + useEffect(() => { + loadUser(); + }, []); + + const handleUpdateProfile = async () => { + try { + const values = await profileForm.validateFields(); + setSaving(true); + await updateMyProfile(values); + message.success(t('common.success')); + loadUser(); + } catch (e) { + } finally { + setSaving(false); + } + }; + + const handleUpdatePassword = async () => { + try { + const values = await pwdForm.validateFields(); + setSaving(true); + await updateMyPassword(values); + message.success(t('common.success')); + pwdForm.resetFields(); + } catch (e) { + } finally { + setSaving(false); + } + }; + + return ( +
+ + + + + + } style={{ backgroundColor: '#1677ff', marginBottom: 16 }} /> + {user?.displayName} + @{user?.username} +
+ {user?.isPlatformAdmin ? 平台管理员 : 普通用户} +
+
+ + + + + + 基本信息} + key="basic" + > +
+ + + + + + + + + + +
+
+ + 安全设置} + key="password" + > +
+ + + + + + + ({ + validator(_, value) { + if (!value || getFieldValue('newPassword') === value) { + return Promise.resolve(); + } + return Promise.reject(new Error('两次输入的密码不一致')); + }, + }), + ]} + > + + + +
+
+
+
+ +
+
+ ); +} + +import { Tag } from "antd"; diff --git a/frontend/src/pages/ResetPassword.tsx b/frontend/src/pages/ResetPassword.tsx new file mode 100644 index 0000000..4b2ea7d --- /dev/null +++ b/frontend/src/pages/ResetPassword.tsx @@ -0,0 +1,86 @@ +import { Button, Card, Form, Input, message, Typography, Layout } from "antd"; +import { useState } from "react"; +import { updateMyPassword } from "../api"; +import { LockOutlined, LogoutOutlined } from "@ant-design/icons"; +import { useNavigate } from "react-router-dom"; + +const { Title, Text } = Typography; + +export default function ResetPassword() { + const [loading, setLoading] = useState(false); + const navigate = useNavigate(); + const [form] = Form.useForm(); + + const onFinish = async (values: any) => { + setLoading(true); + try { + await updateMyPassword({ + oldPassword: values.oldPassword, + newPassword: values.newPassword + }); + message.success("密码修改成功,请重新登录"); + // 清理并重新登录 + localStorage.clear(); + sessionStorage.clear(); + navigate("/login"); + } catch (e) { + } finally { + setLoading(false); + } + }; + + const handleLogout = () => { + localStorage.clear(); + sessionStorage.clear(); + navigate("/login"); + }; + + return ( + + +
+ + 强制修改密码 + 为了您的账户安全,首次登录或密码被重置后需要修改密码方可继续使用系统。 +
+ +
+ + } /> + + + + } /> + + + ({ + validator(_, value) { + if (!value || getFieldValue('newPassword') === value) { + return Promise.resolve(); + } + return Promise.reject(new Error('两次输入的密码不一致')); + }, + }), + ]} + > + } /> + + + + + +
+
+
+ ); +} diff --git a/frontend/src/pages/Users.tsx b/frontend/src/pages/Users.tsx index ea2c2d0..d64b46b 100644 --- a/frontend/src/pages/Users.tsx +++ b/frontend/src/pages/Users.tsx @@ -43,6 +43,7 @@ import { MinusCircleOutlined } from "@ant-design/icons"; import PageHeader from "../components/shared/PageHeader"; +import {SysOrg, SysRole, SysTenant, SysUser} from "../types"; import { getStandardPagination } from "../utils/pagination"; const { Title, Text } = Typography; @@ -313,7 +314,7 @@ export default function Users() {
{record.displayName}
- {record.isPlatformAdmin && {t('users.platformAdmin')}} + {record.isPlatformAdmin && {t('users.platformAdmin')}}
@{record.username}
diff --git a/frontend/src/utils/pagination.ts b/frontend/src/utils/pagination.ts new file mode 100644 index 0000000..b3bc143 --- /dev/null +++ b/frontend/src/utils/pagination.ts @@ -0,0 +1,25 @@ +import { TablePaginationConfig } from 'antd'; +import i18n from '../i18n'; + +/** + * Returns a standardized Ant Design pagination configuration. + */ +export const getStandardPagination = ( + total: number, + current: number, + pageSize: number, + onChange?: (page: number, size: number) => void +): TablePaginationConfig => { + return { + total, + current, + pageSize, + onChange, + showSizeChanger: true, + showQuickJumper: true, + showTotal: (totalCount) => i18n.t('common.total', { total: totalCount }), + pageSizeOptions: ['10', '20', '50', '100'], + size: 'middle', + position: ['bottomRight'] + }; +};