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
chenhao 2026-02-27 15:12:58 +08:00
parent 3f31ec0eb1
commit f93d797382
8 changed files with 835 additions and 1 deletions

View File

@ -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 | 更新时间 |

View File

@ -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);

View File

@ -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;

View File

@ -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 };
}

View File

@ -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";

View File

@ -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>
);
}

View File

@ -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>

View File

@ -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']
};
};