feat(database): 新增数据库设计文档和PostgreSQL建表脚本
- 创建数据库结构文档 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 - 实现字典数据请求去重和内存缓存机制master
parent
3f31ec0eb1
commit
f93d797382
|
|
@ -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 | 更新时间 |
|
||||||
|
|
||||||
|
|
@ -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);
|
||||||
|
|
@ -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<PageHeaderProps> = ({ title, subtitle, extra, className = '' }) => {
|
||||||
|
return (
|
||||||
|
<div className={`page-header flex justify-between items-end mb-6 ${className}`}>
|
||||||
|
<div>
|
||||||
|
<Title level={4} className="mb-1" style={{ margin: 0 }}>
|
||||||
|
{title}
|
||||||
|
</Title>
|
||||||
|
{subtitle && (
|
||||||
|
<Text type="secondary" style={{ display: 'block' }}>
|
||||||
|
{subtitle}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{extra && <div className="page-header-extra">{extra}</div>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PageHeader;
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { fetchDictItemsByTypeCode } from '../api/dict';
|
||||||
|
import { SysDictItem } from '../types';
|
||||||
|
|
||||||
|
const dictCache: Record<string, SysDictItem[]> = {};
|
||||||
|
const pendingRequests: Record<string, Promise<SysDictItem[]>[]> = {};
|
||||||
|
|
||||||
|
export function useDict(typeCode: string) {
|
||||||
|
const [items, setItems] = useState<SysDictItem[]>(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 };
|
||||||
|
}
|
||||||
|
|
@ -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<UserProfile | null>(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 (
|
||||||
|
<div className="p-6 max-w-4xl mx-auto">
|
||||||
|
<PageHeader
|
||||||
|
title="个人中心"
|
||||||
|
subtitle="管理您的个人基础信息及账号安全设置"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Row gutter={24}>
|
||||||
|
<Col span={8}>
|
||||||
|
<Card className="text-center shadow-sm">
|
||||||
|
<Avatar size={80} icon={<UserOutlined />} style={{ backgroundColor: '#1677ff', marginBottom: 16 }} />
|
||||||
|
<Title level={5} style={{ margin: 0 }}>{user?.displayName}</Title>
|
||||||
|
<Text type="secondary">@{user?.username}</Text>
|
||||||
|
<div className="mt-4">
|
||||||
|
{user?.isPlatformAdmin ? <Tag color="gold">平台管理员</Tag> : <Tag color="blue">普通用户</Tag>}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
<Col span={16}>
|
||||||
|
<Card className="shadow-sm">
|
||||||
|
<Tabs defaultActiveKey="basic">
|
||||||
|
<Tabs.TabPane
|
||||||
|
tab={<span><SolutionOutlined />基本信息</span>}
|
||||||
|
key="basic"
|
||||||
|
>
|
||||||
|
<Form form={profileForm} layout="vertical" onFinish={handleUpdateProfile} style={{ marginTop: 16 }}>
|
||||||
|
<Form.Item label="显示姓名" name="displayName" rules={[{ required: true }]}>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="邮箱" name="email">
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="手机号" name="phone">
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Button type="primary" icon={<SaveOutlined />} loading={saving} onClick={() => profileForm.submit()}>
|
||||||
|
保存修改
|
||||||
|
</Button>
|
||||||
|
</Form>
|
||||||
|
</Tabs.TabPane>
|
||||||
|
|
||||||
|
<Tabs.TabPane
|
||||||
|
tab={<span><LockOutlined />安全设置</span>}
|
||||||
|
key="password"
|
||||||
|
>
|
||||||
|
<Form form={pwdForm} layout="vertical" onFinish={handleUpdatePassword} style={{ marginTop: 16 }}>
|
||||||
|
<Form.Item label="旧密码" name="oldPassword" rules={[{ required: true }]}>
|
||||||
|
<Input.Password />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="新密码" name="newPassword" rules={[{ required: true, min: 6 }]}>
|
||||||
|
<Input.Password />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label="确认新密码"
|
||||||
|
name="confirmPassword"
|
||||||
|
dependencies={['newPassword']}
|
||||||
|
rules={[
|
||||||
|
{ required: true },
|
||||||
|
({ getFieldValue }) => ({
|
||||||
|
validator(_, value) {
|
||||||
|
if (!value || getFieldValue('newPassword') === value) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error('两次输入的密码不一致'));
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input.Password />
|
||||||
|
</Form.Item>
|
||||||
|
<Button type="primary" danger loading={saving} onClick={() => pwdForm.submit()}>
|
||||||
|
修改密码
|
||||||
|
</Button>
|
||||||
|
</Form>
|
||||||
|
</Tabs.TabPane>
|
||||||
|
</Tabs>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
import { Tag } from "antd";
|
||||||
|
|
@ -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 (
|
||||||
|
<Layout style={{ minHeight: '100vh', background: '#f0f2f5', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||||
|
<Card style={{ width: 400, borderRadius: 8, boxShadow: '0 4px 12px rgba(0,0,0,0.1)' }}>
|
||||||
|
<div className="text-center mb-6">
|
||||||
|
<LockOutlined style={{ fontSize: 40, color: '#1890ff' }} />
|
||||||
|
<Title level={3} style={{ marginTop: 16 }}>强制修改密码</Title>
|
||||||
|
<Text type="secondary">为了您的账户安全,首次登录或密码被重置后需要修改密码方可继续使用系统。</Text>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Form form={form} layout="vertical" onFinish={onFinish}>
|
||||||
|
<Form.Item label="当前初始密码" name="oldPassword" rules={[{ required: true }]}>
|
||||||
|
<Input.Password prefix={<LockOutlined />} />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item label="设置新密码" name="newPassword" rules={[{ required: true, min: 6 }]}>
|
||||||
|
<Input.Password prefix={<LockOutlined />} />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label="确认新密码"
|
||||||
|
name="confirmPassword"
|
||||||
|
dependencies={['newPassword']}
|
||||||
|
rules={[
|
||||||
|
{ required: true },
|
||||||
|
({ getFieldValue }) => ({
|
||||||
|
validator(_, value) {
|
||||||
|
if (!value || getFieldValue('newPassword') === value) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error('两次输入的密码不一致'));
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input.Password prefix={<LockOutlined />} />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Button type="primary" htmlType="submit" block size="large" loading={loading} style={{ marginTop: 8 }}>
|
||||||
|
立即修改并重新登录
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button type="link" block icon={<LogoutOutlined />} onClick={handleLogout} style={{ marginTop: 8 }}>
|
||||||
|
退出登录
|
||||||
|
</Button>
|
||||||
|
</Form>
|
||||||
|
</Card>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -43,6 +43,7 @@ import {
|
||||||
MinusCircleOutlined
|
MinusCircleOutlined
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
import PageHeader from "../components/shared/PageHeader";
|
import PageHeader from "../components/shared/PageHeader";
|
||||||
|
import {SysOrg, SysRole, SysTenant, SysUser} from "../types";
|
||||||
import { getStandardPagination } from "../utils/pagination";
|
import { getStandardPagination } from "../utils/pagination";
|
||||||
|
|
||||||
const { Title, Text } = Typography;
|
const { Title, Text } = Typography;
|
||||||
|
|
@ -313,7 +314,7 @@ export default function Users() {
|
||||||
<div>
|
<div>
|
||||||
<Space size={4}>
|
<Space size={4}>
|
||||||
<div className="user-display-name">{record.displayName}</div>
|
<div className="user-display-name">{record.displayName}</div>
|
||||||
{record.isPlatformAdmin && <Tag color="gold" size="small" style={{ fontSize: 10 }}>{t('users.platformAdmin')}</Tag>}
|
{record.isPlatformAdmin && <Tag color="gold" style={{ fontSize: 10 }}>{t('users.platformAdmin')}</Tag>}
|
||||||
</Space>
|
</Space>
|
||||||
<div className="user-username tabular-nums">@{record.username}</div>
|
<div className="user-username tabular-nums">@{record.username}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -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']
|
||||||
|
};
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue