u
This commit is contained in:
1 parent
78eb811ff7
commit
97fe2ff306
83 files changed
+10083
-3662
No files matched your search
@@ -0,0 +1,117 @@
|
||||
# FMS 后端架构设计
|
||||
|
||||
> 文档版本:V1.1(精简方案)
|
||||
> 编制日期:2026-10-12
|
||||
> 状态:待评审
|
||||
> 适用范围:沿用现有 FMS API 与 SQL Server 模块配置方式
|
||||
> 配套文档:[核心表结构设计.md](./核心表结构设计.md)
|
||||
|
||||
## 1. 设计目标
|
||||
|
||||
本文以已经使用的 FMS 数据库和 `fms-api` 接口为基础,只补充模块配置的版本历史能力。目标是让新 ERP 后端和其他 IT 团队能够沿用熟悉的表结构、SQL 和通用数据接口,而不是重新建设一套元数据运行时。
|
||||
|
||||
- 保留 `s_module`、`s_field`、`s_module_schema`、`s_relation` 的现有职责和字段约定。
|
||||
- 元数据描述常用模块、字段和页面配置;SQL 是可选的正式扩展方式,遇到配置不便表达的查询时使用。
|
||||
- 沿用通用数据 API、机构上下文和 SQL Server 数据源路由;不为每个业务模块新增专用 CRUD API。
|
||||
- 只新增轻量模块版本历史;不引入模板引擎、编译发布管线、运行时快照目录或复杂审批流程。
|
||||
|
||||
## 2. 总体结构
|
||||
|
||||
```text
|
||||
模块管理界面
|
||||
├─ s_module:模块树、表/视图、SQL、保存目标
|
||||
├─ s_field:模块字段及业务类型
|
||||
├─ s_module_schema:列表 / 表单 / 查询 JSON
|
||||
└─ s_relation:模块数据关系
|
||||
│
|
||||
├─ 普通数据请求 → 现有 /data 通用接口 → SQL Server
|
||||
├─ 自定义查询 SQL → 只读 SQL 校验 → SQL Server
|
||||
└─ 显式版本保存/恢复 → s_module_version
|
||||
```
|
||||
|
||||
运行时继续读取现有模块配置表。`s_module_version` 只做历史记录与恢复,不成为普通业务请求的读取入口。
|
||||
|
||||
## 3. 模块配置如何驱动请求
|
||||
|
||||
### 3.1 模块与页面配置
|
||||
|
||||
- 模块编码 `s_module.b_id` 是稳定业务编码,由配置方提供,不使用运行时雪花 ID。
|
||||
- `b_module_type` 使用现有 `module`、`data`、`virtual` 约定:分组、标准数据对象和虚拟业务对象。
|
||||
- `b_view_table`、`b_save_table`、`b_key_field` 描述普通查询和保存目标;数据库对象默认使用 `dbo`。
|
||||
- `s_field` 声明模块字段编码、名称和业务类型;具体 SQL 类型通过数据库结构探测获得。
|
||||
- `s_module_schema` 的 `view`、`edit`、`query` JSON 控制列表、表单和查询界面;字段语义不在多个 JSON 中重复定义。
|
||||
- `s_relation` 声明模块间字段关系;`b_parent_id` 只用于模块树,不表示业务数据关系。
|
||||
|
||||
### 3.2 SQL 兜底
|
||||
|
||||
- 常规简单查询使用配置的 `b_view_table` 和通用查询接口。
|
||||
- 关联、聚合或其他配置表达起来不合适的查询,可在模块配置中使用 `b_query_sql`;它是模块配置数据,不要求在后端代码中为每家公司登记 SQL 编码。
|
||||
- 对于按机构隔离的数据库,机构数据源路由继续使用现有 `OrgContext` / `OrgDataSourceManager`。各机构可在自己的配置库维护不同模块 SQL,不新增租户配置层。
|
||||
- 运行查询 SQL 必须经过现有只读校验路径;只允许查询语句,不允许用查询 SQL 执行新增、修改、删除或 DDL。写入走现有保存/删除接口或明确的业务服务。
|
||||
- `b_order_sql` 是模块默认排序;排序标识符按现有 `DbUtils` 校验规则处理。查询参数和值不得通过未校验的字符串拼接进入 SQL。
|
||||
|
||||
## 4. 沿用的通用数据 API
|
||||
|
||||
已有的 `/data/*` 契约继续作为数据访问入口,不因模块配置版本化而更换:
|
||||
|
||||
| 接口 | 用途 |
|
||||
|---|---|
|
||||
| `POST /data/loaddata` | 按表/视图、条件、排序和字段列表查询 |
|
||||
| `POST /data/page` | 分页查询 |
|
||||
| `POST /data/loaddatabysql` | 执行只读 `SELECT` / `WITH` 查询 |
|
||||
| `POST /data/saveobjt` | 在一个事务中保存多表新增、修改 |
|
||||
| `POST /data/delete` | 按模块或受控目标删除 |
|
||||
| `POST /data/describe` | 探测表或视图列结构 |
|
||||
| `POST /data/nextid`、`/data/nextcode` | 取雪花 ID 或模块业务编码 |
|
||||
|
||||
`loaddata` / `page` 使用表或视图名、结构化筛选和排序参数;`loaddatabysql` 用于复杂只读查询。已有 `DbUtils` 负责标识符验证、SQL 只读检查和 SQL 执行。通用保存服务依据真实表结构生成参数化写入,不通过 `b_query_sql` 写业务数据。
|
||||
|
||||
新接入系统应沿用这些请求/响应格式和既有身份、机构、权限上下文;模块管理页面负责读取模块配置并组织请求,不需要另造一套模块专属 CRUD API。
|
||||
|
||||
## 5. 模块配置版本历史
|
||||
|
||||
### 5.1 行为边界
|
||||
|
||||
- 版本功能只增加 `s_module_version` 一张表;快照 JSON 包含模块及其字段、界面、关系和相关模块级扩展配置。
|
||||
- 显式创建版本时,后端从现有配置表读取当前状态,分配递增版本号并保存不可变快照。
|
||||
- 查看历史只读取版本表;恢复历史版本时,后端校验后在一个事务中恢复现有模块配置表。
|
||||
- 恢复成功后业务运行仍读取原有 `s_module` 等表;不切换运行时版本指针,也不改变 `/data/*` 接口。
|
||||
- 恢复内容可另存为新的版本记录;禁止覆盖历史快照。
|
||||
|
||||
因此这项能力是**配置备份、版本查看与恢复**,不是草稿/发布隔离。配置当前仍按现有保存机制生效;若之后出现明确的灰度发布需求,再单独扩展。
|
||||
|
||||
### 5.2 最小后端改动
|
||||
|
||||
增加一个模块版本服务,负责:
|
||||
|
||||
1. 创建版本:在数据库事务中读取模块及子配置,验证并序列化快照。
|
||||
2. 查询版本:按模块编码列出版本号、说明、创建人和时间。
|
||||
3. 恢复版本:校验快照结构和关联目标,在同一事务内更新模块配置,并记录恢复结果。
|
||||
|
||||
版本操作可通过模块管理 API 暴露;业务数据 API、模块表主键、普通保存流程和 SQL 查询流程不变。版本号由后端分配,不依赖客户端提供。
|
||||
|
||||
## 6. SQL 与数据安全约定
|
||||
|
||||
- 客户端不能以任意表名或列名绕过后端标识符验证。
|
||||
- 普通查询的筛选值以参数传递;字段、排序方向等 SQL 标识由校验后的配置控制。
|
||||
- `b_query_sql` 和 `/data/loaddatabysql` 仅用于只读查询,并使用现有 `DbUtils.validateReadOnlySql()` 校验。
|
||||
- SQL 配置及执行需经过现有机构、用户和数据范围权限上下文;不能因为使用 SQL 扩展绕过权限校验。
|
||||
- 写操作使用 `/data/saveobjt`、`/data/delete` 或已有业务服务,并纳入现有事务和审计规则。
|
||||
- 版本恢复同样执行配置权限校验、引用校验和事务处理。
|
||||
|
||||
## 7. 不在本次范围内
|
||||
|
||||
- 不重构现有 FMS `DataController` 接口,也不改变 SQL Server、JDBC 或机构数据源路由方式。
|
||||
- 不增加 `s_template` / `s_template_version`、`s_module_snapshot`、用户偏好新表或权限/工作流新表。
|
||||
- 不强制运行时从 JSON 快照读取模块定义,不引入 `ModuleDefinition` 编译缓存层。
|
||||
- 不把 SQL 移到后端固定代码目录;保留模块配置中的可选 SQL 兜底。
|
||||
- 不重新定义业务表、菜单、用户、权限、工作流、文件和审计子系统。
|
||||
- 不把模块版本管理等同于数据库 migration;业务表结构仍通过已有 SQL migration 管理。
|
||||
|
||||
## 8. 接入检查清单
|
||||
|
||||
1. 确认机构/数据库连接按现有路由建立,业务对象位于 `dbo`。
|
||||
2. 普通表格和表单配置 `s_module`、`s_field`、`s_module_schema`;复杂查询按需配置 `b_query_sql`。
|
||||
3. 用 `/data/describe` 检查实际表/视图字段,用 `s_field` 和视图 JSON 保持编码一致。
|
||||
4. 查询、保存、删除分别调用现有通用接口;不从客户端接收未经验证的 DML SQL。
|
||||
5. 重要配置变更后显式保存模块版本;恢复时通过版本服务执行,不手工覆盖版本历史表。
|
||||
@@ -0,0 +1,195 @@
|
||||
# FMS 后端核心表结构设计
|
||||
|
||||
> 文档版本:V1.1(精简方案)
|
||||
> 编制日期:2026-10-12
|
||||
> 状态:待评审
|
||||
> 数据库:SQL Server
|
||||
> 配套文档:[后端架构设计.md](./后端架构设计.md)
|
||||
|
||||
## 1. 范围与原则
|
||||
|
||||
本文沿用现有 FMS 的模块表结构和 SQL 驱动方式,只说明模块运行需要的核心元数据表,并增加一张轻量的模块版本历史表。业务表、用户/权限、菜单、多语言、工作流等既有表和接口不在本文重定义。
|
||||
|
||||
- 业务数据仍保存在 SQL Server 的真实表和视图中。
|
||||
- `s_module`、`s_field`、`s_module_schema`、`s_relation` 描述模块及其界面、字段和关联;不采用 EAV 存业务记录。
|
||||
- 常规功能用模块配置;配置不够或 SQL 更直接时,可通过模块的 `b_query_sql` 使用 SQL 查询。
|
||||
- 运行时继续读取现有模块配置表;版本表仅保存可查看、可恢复的历史配置,不引入新的运行时快照目录。
|
||||
- 保持现有命名和数据类型;不增加模板体系、草稿/发布状态机、额外快照表或元数据编译器。
|
||||
- 与现有 FMS 数据库约定一致:模块关联完整性、JSON 和类型校验由应用层负责,不依赖外键、`CHECK` 约束或触发器。
|
||||
|
||||
## 2. 核心表清单
|
||||
|
||||
| 表 | 职责 | 是否新增 |
|
||||
|---|---|---|
|
||||
| `s_module` | 模块树、查询来源、保存目标和默认行为 | 否,沿用现有设计 |
|
||||
| `s_field` | 模块字段编码、名称和业务类型 | 否,沿用现有设计 |
|
||||
| `s_module_schema` | 列表、表单、查询等界面 JSON 配置 | 否,沿用现有设计 |
|
||||
| `s_relation` | 模块之间的字段关系 | 否,沿用现有设计 |
|
||||
| `s_module_version` | 手动保存的模块配置历史及恢复来源 | 是,唯一新增表 |
|
||||
|
||||
现有 `s_user_module_pref`、`s_autocode`、`s_delete_rule` 等辅助配置表继续按原设计使用;权限、菜单和工作流表也保持原状。它们不因本方案改名或迁移。
|
||||
|
||||
## 3. 模块 `s_module`
|
||||
|
||||
```sql
|
||||
create table dbo.s_module (
|
||||
b_id varchar(50) not null primary key, -- 模块编码,稳定业务键
|
||||
b_parent_id varchar(50) null, -- 父模块编码,根节点为空
|
||||
b_depth int not null default 0, -- 模块树层级,根节点为 0
|
||||
b_path varchar(1000) not null, -- 模块树路径,由应用层维护
|
||||
b_name nvarchar(200) not null, -- 模块名称
|
||||
b_i18n varchar(150) null, -- 多语言资源编码
|
||||
b_module_type varchar(20) not null, -- module / data / virtual
|
||||
b_view_table varchar(128) null, -- 默认查询表或视图名
|
||||
b_save_table varchar(128) null, -- 默认保存目标表名
|
||||
b_key_field varchar(50) null, -- 数据主键字段
|
||||
b_scope_field varchar(50) null, -- 数据范围字段,如记录归属人字段
|
||||
b_order_sql varchar(500) null, -- 默认排序规则
|
||||
b_query_sql nvarchar(max) null, -- 可选查询 SQL 或 SQL 模板
|
||||
b_config_json nvarchar(max) null, -- 低频模块扩展配置
|
||||
b_canuse tinyint not null default 1, -- 是否启用:1 启用,0 停用
|
||||
b_xh int not null default 0, -- 同级模块显示顺序
|
||||
b_bz nvarchar(2000) null, -- 备注
|
||||
b_updated_at datetime2 null -- 模块配置最后保存时间
|
||||
);
|
||||
|
||||
create index ix_s_module_parent
|
||||
on dbo.s_module (b_parent_id, b_canuse, b_depth, b_xh, b_id);
|
||||
|
||||
create index ix_s_module_path
|
||||
on dbo.s_module (b_path, b_canuse, b_id);
|
||||
|
||||
create index ix_s_module_type
|
||||
on dbo.s_module (b_module_type, b_canuse, b_xh, b_id);
|
||||
|
||||
create index ix_s_module_view_table
|
||||
on dbo.s_module (b_view_table, b_canuse, b_id);
|
||||
|
||||
create index ix_s_module_save_table
|
||||
on dbo.s_module (b_save_table, b_canuse, b_id);
|
||||
```
|
||||
|
||||
### 3.1 字段约定
|
||||
|
||||
- `module` 是模块分组;`data` 是标准数据模块;`virtual` 是由查询 SQL 或业务实现提供数据的虚拟模块。
|
||||
- `b_view_table` 是普通查询的数据源;配置 `b_query_sql` 时,模块查询使用该 SQL;两者都没有时,由虚拟模块的业务实现提供数据。
|
||||
- `b_save_table` 和 `b_key_field` 描述标准保存目标;业务写入仍走现有保存接口或业务服务。
|
||||
- `b_query_sql` 是可选的 SQL 兜底配置,不是后端代码中的固定 SQL 编码。可按现有机构/数据库配置维护不同 SQL。它用于查询,不替代标准保存流程。
|
||||
- `b_order_sql` 保存该模块的默认排序;值按现有 SQL 标识符和排序校验规则处理。
|
||||
- `b_parent_id` 是模块树的权威关系;`b_depth`、`b_path` 是派生字段,新增或移动节点时由应用层在事务内维护。
|
||||
|
||||
## 4. 字段 `s_field`
|
||||
|
||||
```sql
|
||||
create table dbo.s_field (
|
||||
b_module_id varchar(50) not null, -- 所属 data / virtual 模块编码
|
||||
b_field varchar(50) not null, -- 模块字段编码或查询结果别名
|
||||
b_name nvarchar(200) not null, -- 字段名称
|
||||
b_i18n varchar(150) null, -- 多语言资源编码
|
||||
b_type varchar(30) not null, -- 字段业务类型
|
||||
b_canuse tinyint not null default 1, -- 是否启用:1 启用,0 停用
|
||||
b_xh int not null default 0, -- 字段默认顺序
|
||||
primary key (b_module_id, b_field)
|
||||
);
|
||||
|
||||
create index ix_s_field_module_order
|
||||
on dbo.s_field (b_module_id, b_canuse, b_xh, b_field);
|
||||
```
|
||||
|
||||
`b_type` 表示业务层字段类型;SQL Server 实际类型通过表/视图结构探测获得,不在 `s_field` 重复保存。字段分组、控件、必填、只读、显示顺序等界面行为放在 `s_module_schema` 的 JSON 中。
|
||||
|
||||
## 5. 界面配置 `s_module_schema`
|
||||
|
||||
```sql
|
||||
create table dbo.s_module_schema (
|
||||
b_module_id varchar(50) not null, -- 所属 data / virtual 模块编码
|
||||
b_schema_type varchar(20) not null, -- view / edit / query
|
||||
b_schema_json nvarchar(max) not null, -- 对应界面的完整 JSON 配置
|
||||
b_canuse tinyint not null default 1, -- 是否启用:1 启用,0 停用
|
||||
b_updated_by varchar(50) null, -- 最后修改人编码
|
||||
b_updated_at datetime2 null, -- 最后修改时间
|
||||
primary key (b_module_id, b_schema_type)
|
||||
);
|
||||
|
||||
create index ix_s_module_schema_type
|
||||
on dbo.s_module_schema (b_module_id, b_schema_type, b_canuse);
|
||||
```
|
||||
|
||||
每个模块按需配置 `view`(列表)、`edit`(表单)、`query`(查询条件)。每种类型当前一份 JSON;JSON 结构版本由 JSON 顶层的 `schemaVersion` 管理。字段编码引用 `s_field.b_field`。个人偏好仍使用现有 `s_user_module_pref`,不混入系统默认配置。
|
||||
|
||||
## 6. 模块关系 `s_relation`
|
||||
|
||||
```sql
|
||||
create table dbo.s_relation (
|
||||
b_source_module_id varchar(50) not null, -- 源模块编码
|
||||
b_source_field varchar(50) not null, -- 源字段编码
|
||||
b_target_module_id varchar(50) not null, -- 目标模块编码
|
||||
b_target_field varchar(50) not null, -- 目标字段编码
|
||||
b_relation_type varchar(20) not null, -- one_to_one / one_to_many / many_to_one
|
||||
b_on_delete varchar(20) not null default 'restrict', -- restrict / cascade / none
|
||||
b_canuse tinyint not null default 1, -- 是否启用:1 启用,0 停用
|
||||
b_xh int not null default 0, -- 关系显示顺序
|
||||
primary key (
|
||||
b_source_module_id,
|
||||
b_source_field,
|
||||
b_target_module_id,
|
||||
b_target_field
|
||||
)
|
||||
);
|
||||
|
||||
create index ix_s_relation_source
|
||||
on dbo.s_relation (b_source_module_id, b_canuse, b_xh, b_source_field, b_target_module_id);
|
||||
|
||||
create index ix_s_relation_target
|
||||
on dbo.s_relation (b_target_module_id, b_canuse, b_source_module_id);
|
||||
```
|
||||
|
||||
模块树只表达业务分组;主子表、引用和多对多关系通过 `s_relation` 或中间表模块表达。关系目标及删除策略由应用层校验,不建立通用业务外键。
|
||||
|
||||
## 7. 模块版本历史 `s_module_version`
|
||||
|
||||
该表是本方案唯一新增表。它保存模块配置的历史副本,用于查看和恢复;不引入模板版本、独立快照表、发布状态机或运行时版本目录。
|
||||
|
||||
```sql
|
||||
create table dbo.s_module_version (
|
||||
b_module_id varchar(50) not null, -- 所属模块编码
|
||||
b_version_no int not null, -- 模块内递增版本号
|
||||
b_snapshot_json nvarchar(max) not null, -- 该版本的完整模块配置快照
|
||||
b_change_note nvarchar(1000) null, -- 版本说明
|
||||
b_created_by varchar(50) null, -- 创建版本的用户编码
|
||||
b_created_at datetime2 not null default sysdatetime(), -- 创建版本时间
|
||||
primary key (b_module_id, b_version_no)
|
||||
);
|
||||
```
|
||||
|
||||
快照 JSON 使用独立的 `snapshotVersion` 描述快照格式。它至少包含模块行、字段行、界面配置和相关关系;如需完整恢复模块行为,也包含该模块对应的自动编码、删除规则等配置。示意:
|
||||
|
||||
```json
|
||||
{
|
||||
"snapshotVersion": 1,
|
||||
"module": {},
|
||||
"fields": [],
|
||||
"schemas": [],
|
||||
"relations": [],
|
||||
"autocodes": [],
|
||||
"deleteRules": []
|
||||
}
|
||||
```
|
||||
|
||||
### 7.1 保存与恢复规则
|
||||
|
||||
1. 管理员显式创建版本时,后端在同一事务中读取当前模块配置并写入 `s_module_version`;版本记录创建后不修改。
|
||||
2. `b_version_no` 按模块递增;并发创建时由后端事务锁定模块并分配下一个号码。
|
||||
3. 恢复版本时先校验快照格式和模块引用,再在一个事务内将快照恢复到现有配置表。
|
||||
4. 恢复操作成功后,当前运行行为仍由 `s_module` 等现有配置表决定;为保留操作历史,可将恢复结果另存为一个新版本,不覆盖旧版本。
|
||||
5. 版本快照只保存模块配置,不保存业务数据、用户个人偏好、权限授权、菜单、翻译内容或数据库建表脚本。
|
||||
|
||||
这套版本历史不提供独立草稿或“线上发布版本”隔离:模块配置仍按现有方式生效。若未来确实需要草稿/发布隔离,再单独评估,不作为本次改造前提。
|
||||
|
||||
## 8. 既有表与校验边界
|
||||
|
||||
- `s_user_module_pref`:沿用个人视图偏好;它是用户级显示覆盖,不属于模块版本快照。
|
||||
- `s_autocode`、`s_delete_rule`:继续按现有规则工作;模块版本需要完整回滚时,其模块配置一并纳入快照。
|
||||
- `s_menu`、权限、多语言、日志、工作流及业务表:沿用各自现有设计,不在本文件重建。
|
||||
- 模块/字段/关系存在性、`b_module_type`、`b_schema_type` 和 JSON 内容由后端校验;不新增外键、`CHECK` 或触发器。
|
||||
- 业务数据库结构变更仍通过 SQL migration 管理;模块版本不替代数据库 migration。
|
||||
@@ -54,6 +54,15 @@ const showHeader = computed(() => pref.value.header.enable && !isContentFullscre
|
||||
/** 页签栏(局部全屏时保留) */
|
||||
const showTabbar = computed(() => pref.value.tabbar.enable)
|
||||
|
||||
/**
|
||||
* 页面缓存(KeepAlive)的 include 名单,受 tabbar.keepAlive 偏好控制。
|
||||
*
|
||||
* 关闭时传空数组而不是 undefined:KeepAlive 的 include 语义是「白名单」——
|
||||
* 空数组没有任何 name 能匹配上 → 一律不缓存(已缓存的实例会被其内部的
|
||||
* include watch 清掉);而 undefined 表示「不过滤」→ 反而会缓存所有页面。
|
||||
*/
|
||||
const keepAliveInclude = computed(() => (pref.value.tabbar.keepAlive ? appStore.cachedKeys : []))
|
||||
|
||||
/** 内容区样式:内边距走偏好 */
|
||||
const contentStyle = computed(() => ({
|
||||
padding: `${pref.value.app.contentPadding}px`,
|
||||
@@ -180,7 +189,7 @@ watch(
|
||||
:name="pref.transition.enable ? pref.transition.name : undefined"
|
||||
mode="out-in"
|
||||
>
|
||||
<KeepAlive :include="appStore.cachedKeys" :max="8">
|
||||
<KeepAlive :include="keepAliveInclude">
|
||||
<component :is="formatComponentInstance(Component, r.fullPath)" :key="r.fullPath" />
|
||||
</KeepAlive>
|
||||
</Transition>
|
||||
|
||||
@@ -214,7 +214,7 @@ onMounted(async () => {
|
||||
/* 首次挂载也要定位激活页签 —— 修复「刷新后页签不居中」。
|
||||
*
|
||||
* 为什么不能只靠下面的 watch:
|
||||
* 页签是持久化的(g3erp:app:v2),刷新时 pinia 在组件挂载【之前】就把 tabs 还原好了,
|
||||
* 页签是持久化的(g3erp:app),刷新时 pinia 在组件挂载【之前】就把 tabs 还原好了,
|
||||
* 而 route.fullPath 在路由解析完成时也已确定。
|
||||
* 于是 watch 的两个依赖在挂载这一刻【already 是初始值】,
|
||||
* 没有 immediate 的 watch 根本不会触发 → revealActiveTab 一次都没跑
|
||||
|
||||
@@ -10,6 +10,7 @@ import PrefBlock from './PrefBlock.vue'
|
||||
import SelectItem from './SelectItem.vue'
|
||||
import SwitchItem from './SwitchItem.vue'
|
||||
import ToggleItem from './ToggleItem.vue'
|
||||
import { resolvePrimaryColor } from '../style-presets'
|
||||
import type { Preferences } from '../types'
|
||||
|
||||
/**
|
||||
@@ -43,6 +44,12 @@ const tabs = [
|
||||
// 外观页签
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** 界面风格(换皮预设):default 保持现状,shadcn 现代极简 */
|
||||
const styleOptions = [
|
||||
{ label: '默认', value: 'default' },
|
||||
{ label: '现代极简', value: 'shadcn' },
|
||||
]
|
||||
|
||||
/** 主题模式卡片(对照 vben theme.vue 的 THEME_PRESET) */
|
||||
const themePreset = [
|
||||
{ icon: Sun, name: 'light' as const, label: '浅色' },
|
||||
@@ -159,9 +166,9 @@ function setThemeMode(name: 'light' | 'dark' | 'auto'): void {
|
||||
store.update({ theme: { mode: name } })
|
||||
}
|
||||
|
||||
/** 主题色切换(直接写三段式色值) */
|
||||
/** 主题色切换(直接写三段式色值);标记为用户显式选择,风格切换不再覆盖它 */
|
||||
function setColorPrimary(value: string): void {
|
||||
store.update({ theme: { colorPrimary: value } })
|
||||
store.update({ theme: { colorPrimary: value, colorPrimaryCustom: true } })
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -235,9 +242,13 @@ function hexToHslTriplet(hex: string): string {
|
||||
return `${Math.round(h)} ${Math.round(s * 100)}% ${Math.round(l * 100)}%`
|
||||
}
|
||||
|
||||
/** 当前主题色对应的 hex,喂给取色器。已是 hex/rgb/hsl() 的直接用。 */
|
||||
/**
|
||||
* 当前主题色对应的 hex,喂给取色器。
|
||||
* 未显式选过色时(colorPrimaryCustom=false)解析为「当前风格的内置默认色」,
|
||||
* 这样 shadcn 风格下取色器显示的是中性黑,而不是被忽略的 colorPrimary。
|
||||
*/
|
||||
const customColorHex = computed(() => {
|
||||
const value = pref.value.theme.colorPrimary
|
||||
const value = resolvePrimaryColor(pref.value.theme, pref.value.app.style, store.isDark)
|
||||
if (value.startsWith('#') || value.startsWith('rgb') || value.startsWith('hsl')) return value
|
||||
return hslTripletToHex(value) ?? '#165dff'
|
||||
})
|
||||
@@ -276,6 +287,19 @@ const hasDiff = computed(() => true)
|
||||
<div class="pref-drawer__body">
|
||||
<!-- ===================== 外观 ===================== -->
|
||||
<template v-if="activeTab === 'appearance'">
|
||||
<!-- 界面风格(换皮预设) -->
|
||||
<PrefBlock title="风格">
|
||||
<ToggleItem
|
||||
:model-value="pref.app.style"
|
||||
:items="styleOptions"
|
||||
@update:model-value="
|
||||
(v) => store.update({ app: { style: v as Preferences['app']['style'] } })
|
||||
"
|
||||
>
|
||||
界面风格
|
||||
</ToggleItem>
|
||||
</PrefBlock>
|
||||
|
||||
<!-- 主题模式(图标卡片) -->
|
||||
<PrefBlock title="主题模式">
|
||||
<div class="pref-card-row">
|
||||
@@ -320,7 +344,10 @@ const hasDiff = computed(() => true)
|
||||
>
|
||||
<div
|
||||
class="pref-card__box"
|
||||
:class="{ 'is-active': pref.theme.colorPrimary === theme.value }"
|
||||
:class="{
|
||||
'is-active':
|
||||
pref.theme.colorPrimaryCustom && pref.theme.colorPrimary === theme.value,
|
||||
}"
|
||||
>
|
||||
<div
|
||||
class="pref-card__swatch"
|
||||
@@ -336,7 +363,9 @@ const hasDiff = computed(() => true)
|
||||
<div
|
||||
class="pref-card__box"
|
||||
:class="{
|
||||
'is-active': !builtinThemes.some((t) => t.value === pref.theme.colorPrimary),
|
||||
'is-active':
|
||||
pref.theme.colorPrimaryCustom &&
|
||||
!builtinThemes.some((t) => t.value === pref.theme.colorPrimary),
|
||||
}"
|
||||
>
|
||||
<!-- 不传 presets:用组件默认值 true,面板自带常用色板便于快捷选色 -->
|
||||
@@ -607,6 +636,14 @@ const hasDiff = computed(() => true)
|
||||
>
|
||||
刷新后保留
|
||||
</SwitchItem>
|
||||
<SwitchItem
|
||||
:model-value="pref.tabbar.keepAlive"
|
||||
:disabled="!pref.tabbar.enable"
|
||||
tip="切换页签后再回来,保留页面状态(表单输入、滚动位置等)"
|
||||
@update:model-value="(v) => store.update({ tabbar: { keepAlive: v } })"
|
||||
>
|
||||
缓存页签
|
||||
</SwitchItem>
|
||||
<SwitchItem
|
||||
:model-value="pref.tabbar.wheelable"
|
||||
:disabled="!pref.tabbar.enable"
|
||||
|
||||
@@ -14,6 +14,7 @@ export const defaultPreferences: Preferences = {
|
||||
app: {
|
||||
name: 'G3Soft ERP',
|
||||
layout: 'sidebar-nav',
|
||||
style: 'default',
|
||||
contentPadding: 16,
|
||||
dynamicTitle: true,
|
||||
enablePreferences: true,
|
||||
@@ -22,6 +23,7 @@ export const defaultPreferences: Preferences = {
|
||||
|
||||
theme: {
|
||||
colorPrimary: '212 100% 45%',
|
||||
colorPrimaryCustom: false,
|
||||
colorSuccess: '144 57% 58%',
|
||||
colorWarning: '42 84% 61%',
|
||||
colorDestructive: '348 100% 61%',
|
||||
@@ -94,8 +96,7 @@ export const defaultPreferences: Preferences = {
|
||||
middleClickToClose: false,
|
||||
wheelable: true,
|
||||
persist: true,
|
||||
maxCount: 0,
|
||||
keepAlive: false,
|
||||
keepAlive: true,
|
||||
},
|
||||
|
||||
transition: {
|
||||
|
||||
@@ -15,6 +15,7 @@ export type {
|
||||
PageTransitionType,
|
||||
Preferences,
|
||||
SidebarPreferences,
|
||||
StyleType,
|
||||
TabbarPreferences,
|
||||
TabsStyleType,
|
||||
ThemeModeType,
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import type { StyleType } from './types'
|
||||
|
||||
/**
|
||||
* 应用风格预设表(换皮)。
|
||||
*
|
||||
* 只承载「必须由 JS 产出」的那部分 —— 也就是被 `applyPreferencesToCss`
|
||||
* 内联写进 `<html>` 的变量(内联样式压过一切样式表,皮肤样式块覆盖不了它们)。
|
||||
* 其余配色/几何一律由样式层的 `html[data-g3-style=...]` 覆盖,见 styles/skin-shadcn.scss。
|
||||
*/
|
||||
export interface StylePreset {
|
||||
/**
|
||||
* 风格的内置默认主色(HSL 三段式,按明暗)。
|
||||
* 仅在「用户没显式选过主色」时取用 —— 必须写成三段式:
|
||||
* toColor() 对 #hex 会早退、丢掉 alpha,导致 hover/active 退化。
|
||||
*/
|
||||
defaultPrimary: { light: string; dark: string }
|
||||
/**
|
||||
* 主色淡底(选中/激活底)。给定则固定使用(shadcn 用 accent 灰);
|
||||
* 缺省则按「主色 + 15%/20%」派生(default 沿用旧行为)。
|
||||
*/
|
||||
primarySoft?: { light: string; dark: string }
|
||||
}
|
||||
|
||||
export const stylePresets: Record<StyleType, StylePreset> = {
|
||||
default: {
|
||||
// 与 config.ts 的默认主题色一致
|
||||
defaultPrimary: { light: '212 100% 45%', dark: '212 100% 45%' },
|
||||
},
|
||||
shadcn: {
|
||||
// #0f172b / #e2e8f0
|
||||
defaultPrimary: { light: '223 48% 11%', dark: '214 32% 91%' },
|
||||
// shadcn 的 accent:选中/激活底一律是中性的浅灰/深灰,而非主色淡底
|
||||
primarySoft: { light: '#f1f5f9', dark: '#1d293d' },
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析当前实际生效的主色。
|
||||
*
|
||||
* - 用户显式选过色(colorPrimaryCustom)→ colorPrimary;
|
||||
* - 否则:shadcn 风格用该风格的内置中性主色(按明暗),default 风格仍用 colorPrimary
|
||||
* (保持旧行为,避免"用户此前存下的自定义色因缺少 colorPrimaryCustom 标记被重置")。
|
||||
*
|
||||
* 规则集中在这里,供 update-css-variables 与偏好面板取色器共用,避免两处漂移。
|
||||
*/
|
||||
export function resolvePrimaryColor(
|
||||
theme: { colorPrimary: string; colorPrimaryCustom: boolean },
|
||||
style: StyleType,
|
||||
dark: boolean,
|
||||
): string {
|
||||
if (theme.colorPrimaryCustom || style === 'default') return theme.colorPrimary
|
||||
return stylePresets[style].defaultPrimary[dark ? 'dark' : 'light']
|
||||
}
|
||||
@@ -40,6 +40,15 @@ export type BreadcrumbStyleType = 'background' | 'normal'
|
||||
/** 页签风格。 */
|
||||
export type TabsStyleType = 'card' | 'chrome' | 'plain'
|
||||
|
||||
/**
|
||||
* 应用风格(换皮预设)。
|
||||
* - default:保持现状(vben 风)
|
||||
* - shadcn:现代极简(shadcn admin 风)
|
||||
*
|
||||
* 只影响配色/几何等「皮」,不影响布局形态(页签/导航/面包屑的 styleType 各自独立)。
|
||||
*/
|
||||
export type StyleType = 'default' | 'shadcn'
|
||||
|
||||
/** 页面切换动画。 */
|
||||
export type PageTransitionType =
|
||||
| 'fade'
|
||||
@@ -53,6 +62,12 @@ export type PageTransitionType =
|
||||
export interface ThemePreferences {
|
||||
/** 主题色(HSL 三段式,如 '212 100% 45%' 或直接色值) */
|
||||
colorPrimary: string
|
||||
/**
|
||||
* 主题色是否由用户显式选择过。
|
||||
* false(默认)→ shadcn 风格下使用该风格的内置中性主色;default 风格沿用 colorPrimary。
|
||||
* true → 两种风格都使用 colorPrimary 里存的具体色值。
|
||||
*/
|
||||
colorPrimaryCustom: boolean
|
||||
/** 成功色 */
|
||||
colorSuccess: string
|
||||
/** 警告色 */
|
||||
@@ -81,6 +96,8 @@ export interface AppPreferences {
|
||||
name: string
|
||||
/** 布局方式 */
|
||||
layout: LayoutType
|
||||
/** 应用风格(换皮预设) */
|
||||
style: StyleType
|
||||
/** 内容区左右内边距 */
|
||||
contentPadding: number
|
||||
/** 动态标题(切换路由时改 document.title) */
|
||||
@@ -205,8 +222,6 @@ export interface TabbarPreferences {
|
||||
wheelable: boolean
|
||||
/** 持久化页签(刷新后保留) */
|
||||
persist: boolean
|
||||
/** 最大页签数,0 表示不限 */
|
||||
maxCount: number
|
||||
/** 开启 keepAlive 缓存 */
|
||||
keepAlive: boolean
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Preferences } from './types'
|
||||
import { resolvePrimaryColor, stylePresets } from './style-presets'
|
||||
|
||||
/**
|
||||
* 偏好设置 → CSS 变量。
|
||||
@@ -37,6 +38,7 @@ export function applyPreferencesToCss(preferences: Preferences): void {
|
||||
if (!root) return
|
||||
|
||||
const { theme, app, sidebar, header } = preferences
|
||||
const preset = stylePresets[app.style]
|
||||
|
||||
// ---- 深色模式:给 html 加 .dark 类,样式里用 .dark { ... } 覆盖变量 ----
|
||||
const dark = isDarkTheme(theme.mode)
|
||||
@@ -49,9 +51,16 @@ export function applyPreferencesToCss(preferences: Preferences): void {
|
||||
// 输入框文字发黑看不见,下拉/弹窗底色依旧雪白。
|
||||
root.dataset.g3Theme = dark ? 'dark' : 'light'
|
||||
|
||||
// ---- 风格钩子:皮肤样式层按 html[data-g3-style='shadcn'] 覆盖变量 ----
|
||||
// (只能在这里写内联/属性;皮肤样式块负责「非内联」变量,见 styles/skin-shadcn.scss)
|
||||
root.dataset.g3Style = app.style
|
||||
|
||||
// ---- 主题色 ----
|
||||
// 主色来源见 resolvePrimaryColor:用户显式选过就用它,否则 shadcn 用内置中性主色。
|
||||
const primary = resolvePrimaryColor(theme, app.style, dark)
|
||||
|
||||
const colors: Array<[string, string]> = [
|
||||
['--g3-color-primary', theme.colorPrimary],
|
||||
['--g3-color-primary', primary],
|
||||
['--g3-color-success', theme.colorSuccess],
|
||||
['--g3-color-warning', theme.colorWarning],
|
||||
['--g3-color-danger', theme.colorDestructive],
|
||||
@@ -61,18 +70,20 @@ export function applyPreferencesToCss(preferences: Preferences): void {
|
||||
}
|
||||
|
||||
// ---- 主色半透明底(选中态背景 / 页签激活态)----
|
||||
// 跟随主题色动态派生:选中背景 = 当前主色 + 15%/20% 透明度,
|
||||
// 这样在偏好里换主题色时,选中背景会一起变,而不是停在写死的蓝色。
|
||||
// 透明度对齐 vben:浅色 15%,深色 20%。
|
||||
root.style.setProperty('--g3-color-primary-soft', toColor(theme.colorPrimary, dark ? 0.2 : 0.15))
|
||||
// default:跟随主题色动态派生(当前主色 + 15%/20% 透明度),换主题色时选中背景一起变。
|
||||
// shadcn:固定用中性 accent 灰 —— shadcn 的选中态是灰底而非主色淡底。
|
||||
const soft = preset.primarySoft?.[dark ? 'dark' : 'light'] ?? toColor(primary, dark ? 0.2 : 0.15)
|
||||
root.style.setProperty('--g3-color-primary-soft', toColor(soft))
|
||||
|
||||
// ---- 主色的悬停 / 按下态(@g3soft/ui 的按钮、链接 hover/active 读这两个变量)----
|
||||
// 组件库在 @g3soft/tokens 里给的是「种子色 + 90% / 80% 透明度」,而种子色是 #18181b(近黑):
|
||||
// 只覆盖 --g3-color-primary 的话,蓝色按钮 hover 会直接变成黑色。这里按同一套算法跟随主题色。
|
||||
root.style.setProperty('--g3-color-primary-hover', toColor(theme.colorPrimary, 0.9))
|
||||
root.style.setProperty('--g3-color-primary-active', toColor(theme.colorPrimary, 0.8))
|
||||
root.style.setProperty('--g3-color-primary-hover', toColor(primary, 0.9))
|
||||
root.style.setProperty('--g3-color-primary-active', toColor(primary, 0.8))
|
||||
|
||||
// ---- 圆角与字号 ----
|
||||
// 注意:--g3-radius-base 目前无消费方(G3 组件读 --g3-radius/-sm/-lg,见
|
||||
// @g3soft/tokens);shadcn 风格的圆角由 styles/skin-shadcn.scss 覆盖 --g3-radius* 实现。
|
||||
root.style.setProperty('--g3-radius-base', `${theme.radius}rem`)
|
||||
root.style.setProperty('--g3-font-size-base', `${theme.fontSize}px`)
|
||||
|
||||
|
||||
@@ -19,11 +19,8 @@ export interface TabItem {
|
||||
affix?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* 存储键带版本号:页签结构变更(如 v1 的 name/path → v2 的 key/fullPath)时递增,
|
||||
* 旧键直接废弃 —— 仓库约定不兼容历史持久化数据,不做迁移。
|
||||
*/
|
||||
const STORAGE_KEY = 'g3erp:app:v2'
|
||||
/** 页签持久化存储键 */
|
||||
const STORAGE_KEY = 'g3erp:app'
|
||||
|
||||
export const useAppStore = defineStore(
|
||||
'app',
|
||||
@@ -35,9 +32,15 @@ export const useAppStore = defineStore(
|
||||
* keep-alive 的 include 列表:已打开页签的 fullPath。
|
||||
* 与 g3hd2026 的 tab.cachedKeys 同源 —— KeepAlive 按组件 name(= fullPath 字符串)
|
||||
* 匹配缓存,绕开「异步组件 vnode.type 引用不稳定」导致缓存失效的问题。
|
||||
* 开页签 push、关页签 splice,保证缓存槽与页签严格同步(关闭即销毁实例,不泄漏)。
|
||||
*
|
||||
* 【从 tabs 派生,而不是单独维护一份 ref】:
|
||||
* 曾用「ref + 开页签 push / 关页签 prune」的写法,会与 tabs 漂移 ——
|
||||
* hydrateTabs 恢复 tabs 时不会恢复 cachedKeys,且 addTab 对"已存在的页签"直接 return,
|
||||
* 于是刷新后恢复的页签永远进不了 include,KeepAlive 匹配不到 → 缓存失效。
|
||||
* 改为 computed 后二者天然一致:关闭页签会随之移出,KeepAlive 的 include watch
|
||||
* 也会照常销毁对应缓存实例,不会泄漏。
|
||||
*/
|
||||
const cachedKeys = ref<string[]>([])
|
||||
const cachedKeys = computed(() => tabs.value.map((item) => item.key))
|
||||
|
||||
/**
|
||||
* 路由刷新计数表:key = fullPath,值变化即强制该页签重建组件。
|
||||
@@ -62,13 +65,10 @@ export const useAppStore = defineStore(
|
||||
contentFullscreen.value = !contentFullscreen.value
|
||||
}
|
||||
|
||||
/** 打开(或激活)一个页签;已存在则不重复添加,新开则加入缓存列表 */
|
||||
/** 打开(或激活)一个页签;已存在则不重复添加(include 的 cachedKeys 由 tabs 派生) */
|
||||
function addTab(tab: TabItem): void {
|
||||
if (!tabs.value.some((item) => item.key === tab.key)) {
|
||||
tabs.value.push(tab)
|
||||
if (!cachedKeys.value.includes(tab.key)) {
|
||||
cachedKeys.value.push(tab.key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,9 +77,8 @@ export const useAppStore = defineStore(
|
||||
const index = tabs.value.findIndex((item) => item.key === key)
|
||||
if (index === -1) return undefined
|
||||
|
||||
// 关闭即销毁缓存实例:cachedKeys 由 tabs 派生,移出 tabs 后 include 自动收敛
|
||||
tabs.value.splice(index, 1)
|
||||
// 同步移出缓存列表(关闭即销毁缓存实例,避免泄漏)
|
||||
pruneCachedKeys()
|
||||
// 优先激活右侧页签,没有则激活左侧
|
||||
return tabs.value[index] ?? tabs.value[index - 1]
|
||||
}
|
||||
@@ -87,13 +86,11 @@ export const useAppStore = defineStore(
|
||||
/** 关闭除指定页签外的全部(保留 affix) */
|
||||
function removeOtherTabs(key: string): void {
|
||||
tabs.value = tabs.value.filter((item) => item.affix || item.key === key)
|
||||
pruneCachedKeys()
|
||||
}
|
||||
|
||||
/** 关闭全部(保留 affix) */
|
||||
function removeAllTabs(): void {
|
||||
tabs.value = tabs.value.filter((item) => item.affix)
|
||||
pruneCachedKeys()
|
||||
}
|
||||
|
||||
/** 关闭左侧:指定页签之前的、非 affix 的页签全部移除 */
|
||||
@@ -101,7 +98,6 @@ export const useAppStore = defineStore(
|
||||
const index = tabs.value.findIndex((item) => item.key === key)
|
||||
if (index === -1) return
|
||||
tabs.value = tabs.value.filter((item, i) => item.affix || i >= index)
|
||||
pruneCachedKeys()
|
||||
}
|
||||
|
||||
/** 关闭右侧:指定页签之后的、非 affix 的页签全部移除 */
|
||||
@@ -109,13 +105,6 @@ export const useAppStore = defineStore(
|
||||
const index = tabs.value.findIndex((item) => item.key === key)
|
||||
if (index === -1) return
|
||||
tabs.value = tabs.value.filter((item, i) => item.affix || i <= index)
|
||||
pruneCachedKeys()
|
||||
}
|
||||
|
||||
/** 把缓存列表收敛到当前打开的页签集合(关闭页签后调用,防缓存实例泄漏) */
|
||||
function pruneCachedKeys(): void {
|
||||
const keep = new Set(tabs.value.map((item) => item.key))
|
||||
cachedKeys.value = cachedKeys.value.filter((key) => keep.has(key))
|
||||
}
|
||||
|
||||
/** 取某页签的刷新计数(未刷新过为 0) */
|
||||
|
||||
@@ -3,8 +3,10 @@
|
||||
// 1) tokens 的 CSS 产物(:root 变量),必须在最前
|
||||
// 2) @g3soft/ui 的组件样式
|
||||
// 3) 本应用全局样式与 reset
|
||||
// 4) 风格层(换皮):必须最后,才能压过上面几层的同名变量
|
||||
|
||||
@use '@g3soft/tokens';
|
||||
@use '@g3soft/ui/style.css';
|
||||
|
||||
@use './global';
|
||||
@use './skin-shadcn';
|
||||
@@ -0,0 +1,174 @@
|
||||
// 风格层:现代极简(shadcn admin)。由偏好 app.style 决定是否命中。
|
||||
//
|
||||
// 触发钩子:applyPreferencesToCss 写 `html[data-g3-style='shadcn']`。
|
||||
//
|
||||
// 为什么是样式表 + JS 双管齐下:
|
||||
// - applyPreferencesToCss 用 root.style.setProperty 内联写了 13 个变量(主色系列、
|
||||
// primary-soft、radius-base、字号、布局尺寸),**内联样式压过一切样式表**,
|
||||
// 所以那几个必须在 JS 里按风格产出(见 preferences/update-css-variables.ts)。
|
||||
// - 其余变量(tokens 组件令牌族 + 业务外壳族)没有被内联,可以用本文件覆盖。
|
||||
//
|
||||
// 分层:
|
||||
// 1) 变量层(浅色 / 深色)
|
||||
// 2) 组件里写死颜色的局部块 —— 必须显式提权(非 scoped <style> 注入顺序不可靠)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 1) 变量层(浅色)
|
||||
// html[data-g3-style='shadcn'] 特异性 (0,2,0) > global.scss 的 .dark (0,1,0)
|
||||
// ---------------------------------------------------------------------------
|
||||
html[data-g3-style='shadcn'] {
|
||||
// ---- tokens 组件令牌族(@g3soft/ui 组件读这组)----
|
||||
--g3-text-1: #020618;
|
||||
--g3-text-2: #45556c;
|
||||
--g3-text-3: #62748e;
|
||||
--g3-text-disabled: #90a1b9;
|
||||
--g3-text-inverse: #ffffff;
|
||||
|
||||
--g3-bg-container: #ffffff;
|
||||
--g3-bg-disabled: #f1f5f9;
|
||||
--g3-border-color: #e2e8f0;
|
||||
|
||||
// 真正决定 G3 组件圆角的是这三个(--g3-radius-base 无消费方)
|
||||
--g3-radius-sm: 6px;
|
||||
--g3-radius: 8px;
|
||||
--g3-radius-lg: 10px;
|
||||
|
||||
--g3-shadow: 0 1px 2px 0 rgb(0 0 0 / 5%);
|
||||
--g3-shadow-popup: 0 4px 12px -2px rgb(0 0 0 / 12%);
|
||||
--g3-fill: #f1f5f9;
|
||||
--g3-fill-hover: #e2e8f0;
|
||||
--g3-mask-bg: rgb(2 6 24 / 45%);
|
||||
|
||||
// ---- 业务 / 外壳族 ----
|
||||
--g3-color-text: #020618;
|
||||
--g3-color-text-secondary: #45556c;
|
||||
--g3-color-text-tertiary: #62748e;
|
||||
--g3-color-bg: #ffffff;
|
||||
--g3-color-bg-container: #ffffff;
|
||||
--g3-color-border: #e2e8f0;
|
||||
--g3-color-fill-hover: #f1f5f9;
|
||||
--g3-color-primary-bg: #f1f5f9;
|
||||
--g3-color-accent: #f1f5f9;
|
||||
--g3-color-accent-hover: #e2e8f0;
|
||||
--g3-color-foreground: #020618;
|
||||
--g3-color-sidebar: #ffffff;
|
||||
--g3-color-header: #ffffff;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 2) 变量层(深色)
|
||||
// 必须逐变量齐全:浅色块 (0,2,0) 高于 global.scss 的 .dark (0,1,0),
|
||||
// 漏项会让深色下漏回浅色值(半 shadcn 半浅色)。
|
||||
// ---------------------------------------------------------------------------
|
||||
html[data-g3-style='shadcn'].dark {
|
||||
--g3-text-1: #f8fafc;
|
||||
--g3-text-2: #cad5e2;
|
||||
--g3-text-3: #90a1b9;
|
||||
--g3-text-disabled: #62748e;
|
||||
--g3-text-inverse: #020618;
|
||||
|
||||
--g3-bg-container: #0f172b;
|
||||
--g3-bg-disabled: #1d293d;
|
||||
--g3-border-color: rgb(255 255 255 / 10%);
|
||||
|
||||
--g3-shadow: 0 1px 2px 0 rgb(0 0 0 / 40%);
|
||||
--g3-shadow-popup: 0 4px 12px -2px rgb(0 0 0 / 50%);
|
||||
--g3-fill: #1d293d;
|
||||
--g3-fill-hover: #253047;
|
||||
--g3-mask-bg: rgb(0 0 0 / 60%);
|
||||
|
||||
--g3-color-text: #f8fafc;
|
||||
--g3-color-text-secondary: #cad5e2;
|
||||
--g3-color-text-tertiary: #90a1b9;
|
||||
--g3-color-bg: #020618;
|
||||
--g3-color-bg-container: #0f172b;
|
||||
--g3-color-border: rgb(255 255 255 / 10%);
|
||||
--g3-color-fill-hover: #1d293d;
|
||||
--g3-color-primary-bg: #1d293d;
|
||||
--g3-color-accent: #1d293d;
|
||||
--g3-color-accent-hover: #253047;
|
||||
--g3-color-foreground: #f8fafc;
|
||||
--g3-color-sidebar: #020618;
|
||||
--g3-color-header: #020618;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 3) 组件里写死的侧栏深色色板
|
||||
// DefaultLayout / LayoutSidebar / LayoutMixedRail / LayoutMixedColumn 的
|
||||
// `.dark &` 与 `.is-dark` 用字面量(#1d2129 / #2e3238 / #313438),
|
||||
// 只改 --g3-color-* 覆盖不到,必须显式提权覆盖 --sidebar-*。
|
||||
// ---------------------------------------------------------------------------
|
||||
$sidebar-dark: (
|
||||
'--sidebar-bg': #020618,
|
||||
'--sidebar-border': rgb(255 255 255 / 10%),
|
||||
'--sidebar-item-color': #cad5e2,
|
||||
'--sidebar-item-hover-bg': #1d293d,
|
||||
'--sidebar-item-hover-color': #f8fafc,
|
||||
'--sidebar-item-active-bg': #1d293d,
|
||||
'--sidebar-item-active-color': #f8fafc,
|
||||
'--sidebar-muted-color': #90a1b9,
|
||||
'--sidebar-title-color': #f8fafc,
|
||||
);
|
||||
|
||||
// .layout 是变量宿主(本身无背景),只覆盖变量即可 —— 双列菜单两列从它继承。
|
||||
html.dark[data-g3-style='shadcn'] .layout {
|
||||
@each $name, $value in $sidebar-dark {
|
||||
#{$name}: $value;
|
||||
}
|
||||
}
|
||||
|
||||
// 三个真实的侧栏列:除变量外还直写了 background / border-right-color。
|
||||
html[data-g3-style='shadcn'] .layout-sidebar.is-dark,
|
||||
html[data-g3-style='shadcn'] .layout-mixed-rail.is-dark,
|
||||
html[data-g3-style='shadcn'] .layout-mixed-column.is-dark,
|
||||
html.dark[data-g3-style='shadcn'] .layout-sidebar,
|
||||
html.dark[data-g3-style='shadcn'] .layout-mixed-rail,
|
||||
html.dark[data-g3-style='shadcn'] .layout-mixed-column {
|
||||
@each $name, $value in $sidebar-dark {
|
||||
#{$name}: $value;
|
||||
}
|
||||
|
||||
background: var(--sidebar-bg);
|
||||
border-right-color: var(--sidebar-border);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 4) 半深色顶栏(global.scss 里写死 #1d2129/#2e3238/#f7f8fa)
|
||||
// ---------------------------------------------------------------------------
|
||||
html[data-g3-style='shadcn'] .semi-dark-header .layout-header {
|
||||
color: #f8fafc;
|
||||
background: #0f172b;
|
||||
border-bottom-color: rgb(255 255 255 / 10%);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 5) 「主色底 + 写死白字」的两处
|
||||
// shadcn 深色下主色是近白(#e2e8f0),白字会压白底看不见。
|
||||
// 改用 --g3-color-primary-on(浅色模式=#fafafa / 深色=#18181b),自动取对比色。
|
||||
// ---------------------------------------------------------------------------
|
||||
html[data-g3-style='shadcn'] .layout-mixed-rail__item.is-active {
|
||||
color: var(--g3-color-primary-on, #fafafa);
|
||||
}
|
||||
|
||||
html[data-g3-style='shadcn']
|
||||
:is(
|
||||
.login__logo-mark,
|
||||
.layout-sidebar__logo-mark,
|
||||
.layout-header__logo-mark,
|
||||
.layout-mixed-rail__logo-mark
|
||||
) {
|
||||
color: var(--g3-color-primary-on, #fafafa);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 6) 登录页装饰光晕:默认跟随主色(shadcn 主色近黑时本已中性,
|
||||
// 但用户自定义主色后会变彩色)—— 这里固定成中性,保持极简观感。
|
||||
// ---------------------------------------------------------------------------
|
||||
html[data-g3-style='shadcn'] .login__glow {
|
||||
background: linear-gradient(
|
||||
154deg,
|
||||
rgb(2 6 24 / 6%) 30%,
|
||||
rgb(15 23 43 / 18%) 48%,
|
||||
rgb(2 6 24 / 6%) 64%
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user