20260914224947

This commit is contained in:
oneao committed 2026-09-14 22:49:48 +08:00
1 parent 63514476d7
commit d26afb5fec
69 files changed
+158 -1008

No files matched your search

+5 -5
View File
@@ -73,7 +73,7 @@ create table dbo.b_othercompany_partner_type (
b_updated_at datetime2 null
);
create table dbo.b_othercompany_bmfl (
create table dbo.b_othercompany_category (
b_id varchar(150) not null primary key,
b_parent_id varchar(150) null,
b_depth int not null default 0,
@@ -88,10 +88,10 @@ create table dbo.b_othercompany_bmfl (
b_updated_at datetime2 null
);
create index ix_b_othercompany_bmfl_parent
on dbo.b_othercompany_bmfl (b_parent_id, b_canuse, b_depth, b_xh, b_id);
create index ix_b_othercompany_bmfl_path
on dbo.b_othercompany_bmfl (b_path, b_canuse, b_id);
create index ix_b_othercompany_category_parent
on dbo.b_othercompany_category (b_parent_id, b_canuse, b_depth, b_xh, b_id);
create index ix_b_othercompany_category_path
on dbo.b_othercompany_category (b_path, b_canuse, b_id);
-- 注:b_grade 由「其他数据」迁移重建为雪花主键(原为级别编码)
create table dbo.b_grade (
+17 -10
View File
@@ -993,7 +993,7 @@ create table dbo.bf_files (
mx_fileext varchar(50) null, -- 扩展名(小写、不含点)
mx_mapfilename nvarchar(500) null, -- 访问地址:本地为 {url-prefix}/{org}/{yyyy}/{MM}/{dd}/{uuid}.{ext},OSS 为完整 URL
mx_moduleid varchar(50) null, -- 业务模块编码(s_module.b_id)
mx_cate_id varchar(50) null, -- 文件分类(bf_files_cateid.b_id)
mx_cate_id bigint null, -- 文件分类(bf_files_category.b_id,雪花主键)
mx_xh int not null default 0, -- 排序号(保留;当前实现固定 0,列表按 subid 倒序)
mx_sh tinyint not null default 0, -- 审核标识(保留;当前实现固定 0)
mx_shuser_id varchar(50) null, -- 审核人(保留)
@@ -1011,19 +1011,24 @@ create table dbo.bf_files (
create index ix_bf_files_host
on dbo.bf_files (father, mx_moduleid, mx_cate_id, mx_xh, subid);
-- 文件分类表
create table dbo.bf_files_cateid (
b_id varchar(50) not null primary key, -- 分类编码
b_name nvarchar(50) null, -- 分类名称
b_files_type varchar(50) null, -- 允许的扩展名(逗号分隔,空 = 不限;保留)
-- 文件分类表(按模块划分;b_module_id 为空表示跨模块通用分类)
create table dbo.bf_files_category (
b_id bigint not null primary key, -- 主键(雪花,新增行由前端 nextIdApi 生成)
b_module_id varchar(50) null, -- 归属模块(s_module.b_id);NULL = 通用分类
b_name nvarchar(50) not null, -- 分类名称
b_i18n varchar(150) null, -- 多语言资源键(s_i18n.b_key)
b_files_type varchar(50) null, -- 允许的扩展名(逗号分隔,空 = 不限;预留)
b_xh int not null default 0, -- 显示顺序
b_canuse tinyint not null default 1, -- 是否启用(0/1)
b_bz nvarchar(200) null, -- 备注
b_inputuser_id varchar(50) null, -- 创建人
b_inputdatetime datetime2 null, -- 创建时间
b_updateuser_id varchar(50) null, -- 最后修改人
b_updatedatetime datetime2 null -- 最后修改时间
b_created_by varchar(50) null, -- 创建人(用户账号)
b_created_at datetime2 null, -- 创建时间
b_updated_by varchar(50) null, -- 最后修改人
b_updated_at datetime2 null -- 最后修改时间
);
create index ix_bf_files_category_module
on dbo.bf_files_category (b_module_id, b_canuse, b_xh, b_id);
```
要点:
@@ -1032,6 +1037,8 @@ create table dbo.bf_files_cateid (
- **宿主关联**:`father` 存宿主业务主键(业务表 `b_id`,bigint 雪花),不建外键;删除业务数据时由业务层级联清理附件(先删存储、后删行)。
- **存储地址**:`mx_mapfilename` 即访问地址,前端预览 / 下载直接使用。本地模式写入 `{fms.file.local.url-prefix}/{orgId}/{yyyy}/{MM}/{dd}/{uuid}.{ext}`,物理文件落在 `{fms.file.local.storage-dir}` 下的同路径;OSS 模式写入 g3oss 返回的完整 URL。
- **保留字段**:`mx_sh*`(审核)、`mx_cut*`(缩略图)、`mx_orderid`、`mx_bz`、`mx_xh` 沿用旧库结构,当前实现未使用(写 0 / 空值),保留以兼容历史数据迁移。
- **分类归属模块**:`bf_files_category.b_module_id` 指定分类所属模块,不填(NULL)为跨模块通用分类;分类树按当前模块过滤(`b_module_id IS NULL OR b_module_id = 当前模块`)。分类主键与 `bf_files.mx_cate_id` 类型保持一致(bigint 雪花)。
- **风格**:分类表按新版字典表风格(应用层雪花主键、`b_created_*` 审计列、`b_canuse tinyint`);`bf_files` 的 `subid` / `father` / `mx_*` 列名已被前后端实现引用,保持现状。表名 `bf_*` 属于"明确的业务域前缀",符合第 1 节表名规范。
- **旧库遗留**:`bf_files_type`(模块文件类型 / 数量配置)在旧库存在,但新旧系统均未使用,不迁移。
存储配置(`application.yaml`):
@@ -88,7 +88,7 @@ public final class VerifyBusinessMigration {
}
private static void verifyCoreTables(Connection connection) throws Exception {
for (String table : List.of("b_othercompany", "b_contact", "b_othercompany_bmfl")) {
for (String table : List.of("b_othercompany", "b_contact", "b_othercompany_category")) {
if (!tableExists(connection, table)) {
throw new IllegalStateException("Missing core business table: dbo." + table);
}
@@ -140,7 +140,8 @@ public class FileService {
ps.setString(6, mapfilename);
// 字符串业务键:模块编码 / 分类编码 / 用户账号均按字符串存储
ps.setString(7, moduleId);
ps.setString(8, cateId);
// 分类主键为 bigint 雪花(bf_files_category.b_id),按数字写入
ps.setObject(8, toLongOrNull(cateId));
ps.setString(9, userId);
ps.executeUpdate();
}
@@ -0,0 +1,29 @@
=== 1. 视图 / 过程 / 函数中引用 b_othercompany_bmfl 的对象 ===
name | type_desc
(共 0 行)
=== 2. s_module 模块配置中的表名引用 ===
b_id | b_module_type | b_view_table | b_save_table
(共 0 行)
=== 3. v_b_othercompany 定义(确认是否 join 分类表) ===
definition
create view dbo.v_b_othercompany as
select o.b_id, o.b_ywid, o.b_pycode, o.b_type, o.b_name, o.b_shortname,
o.b_linkman, o.b_phone, o.b_addr, o.b_firstlinkdate,
o.b_htno, o.b_htdate, o.b_htexpiredate, o.b_black, o.b_canuse,
o.b_zl, o.b_tel, o.b_fax, o.b_www, o.b_email,
o.b_bankacount, o.b_invaccount, o.b_cate_id,
o.b_bz1, o.b_bz2, o.b_bz3, o.b_bz4,
o.b_province, o.b_city, o.b_area, o.b_country,
o.b_edicode, o.b_showcolor, o.b_type_id, o.b_from_id,
o.b_type1_id, o.b_type2_id, o.b_type3_id, o.b_type4_id, o.b_type5_id,
o.b_owner_id, ...(截断)
(共 1 行)
=== 4. 表与索引现状 ===
index_name | type_desc
PK__b_otherc__4E29C30D79EECCD3 | CLUSTERED
ix_b_othercompany_bmfl_parent | NONCLUSTERED
ix_b_othercompany_bmfl_path | NONCLUSTERED
(共 3 行)
@@ -2,7 +2,7 @@ import { ref } from 'vue'
import { loadDataApi } from '@/services/api'
/**
* 文件分类逻辑 —— 加载 bf_files_cateid 扁平表,供 FmsTree 展示(分类为平铺列表,无层级)。
* 文件分类逻辑 —— 加载 bf_files_category 扁平表,供 FmsTree 展示(分类为平铺列表,无层级)。
* 「全部分类」虚拟根由 FmsTree 的 showRoot 注入,本组合式函数只负责取数与选中状态。
*/
export function useFileCategory() {
@@ -16,11 +16,7 @@ export function useFileCategory() {
loading.value = true
error.value = null
try {
const res = await loadDataApi(
'bf_files_cateid',
'b_canuse = 1',
'b_xh ASC, b_id ASC',
)
const res = await loadDataApi('bf_files_category', 'b_canuse = 1', 'b_xh ASC, b_id ASC')
categories.value = res.data || []
} catch (e) {
error.value = e?.message || String(e)
@@ -2,7 +2,7 @@
/**
* 往来单位(合作伙伴)—— 左侧分类树 + 右侧模块列表。
*
* 左树是 b_othercompany_bmfl(b_parent_id 是权威关系,b_depth / b_path 为派生字段),
* 左树是 b_othercompany_category(b_parent_id 是权威关系,b_depth / b_path 为派生字段),
* 右表由 FmsModuleListPage 按模块 v_b_othercompany 的配置驱动。页面负责三件事:
* 选中分类 → 固定查询条件(含全部下级)、b_cate_id 列显示分类名称、分类树的维护
* (新增 / 修改 / 删除 / 刷新 / 拖拽调层级与顺序)。
@@ -30,7 +30,7 @@ import { usePermissionStore } from '@/stores/permissions'
import { buildTree, collectAllKeys, filterTreeByText, findTreeNode, sortTree } from '@/utils/tree'
const MODULE_CODE = 'v_b_othercompany' // 右侧列表模块
const CATEGORY_TABLE = 'b_othercompany_bmfl' // 左侧分类树表
const CATEGORY_TABLE = 'b_othercompany_category' // 左侧分类树表
/** 必须与 FmsTree 的 rootKey 默认值一致 */
const ALL_CATEGORY_KEY = '__fms_tree_root__'
@@ -1,5 +1,5 @@
/**
* 回归用例:往来单位分类树(b_othercompany_bmfl)的新增 / 修改 / 删除 / 拖拽排序。
* 回归用例:往来单位分类树(b_othercompany_category)的新增 / 修改 / 删除 / 拖拽排序。
*
* 1) b_path 为 not null 且无默认值(b_depth / b_xh / b_canuse 才有默认值),新增分类时
* 必须按父级派生 b_depth / b_path 一起写入,否则插入直接失败;
@@ -77,7 +77,7 @@ beforeEach(() => {
Message.warning.mockClear()
modalConfirm.mockReset()
loadDataApi.mockImplementation((view) => {
if (view === 'b_othercompany_bmfl')
if (view === 'b_othercompany_category')
return Promise.resolve({ data: CATEGORIES.map((row) => ({ ...row })) })
return Promise.resolve({ data: [] })
})
@@ -110,7 +110,7 @@ describe('往来单位:新增分类写派生树字段', () => {
await wrapper.vm.onCategorySubmit({ b_id: 'newcat', b_name: '新分类', b_xh: 30 })
await flushPromises()
const request = lastRequestOf('b_othercompany_bmfl')
const request = lastRequestOf('b_othercompany_category')
expect(request.key_field).toBe('b_id')
expect(request.inserts).toHaveLength(1)
expect(request.inserts[0]).toMatchObject({
@@ -131,7 +131,7 @@ describe('往来单位:新增分类写派生树字段', () => {
await wrapper.vm.onCategorySubmit({ b_id: 'child2', b_name: '三级分类', b_xh: 20 })
await flushPromises()
expect(lastRequestOf('b_othercompany_bmfl').inserts[0]).toMatchObject({
expect(lastRequestOf('b_othercompany_category').inserts[0]).toMatchObject({
b_id: 'child2',
b_parent_id: 'root1',
b_depth: 1,
@@ -148,7 +148,7 @@ describe('往来单位:修改分类', () => {
await wrapper.vm.onCategorySubmit({ b_id: 'child1', b_name: '改过的名字', b_xh: 99 })
await flushPromises()
const request = lastRequestOf('b_othercompany_bmfl')
const request = lastRequestOf('b_othercompany_category')
expect(request.inserts).toBeUndefined()
expect(request.updates).toHaveLength(1)
expect(request.updates[0]).toEqual({ b_id: 'child1', b_name: '改过的名字', b_xh: 99 })
@@ -190,7 +190,7 @@ describe('往来单位:删除分类的两条守卫', () => {
await modalConfirm.mock.calls[0][0].onOk()
await flushPromises()
const request = lastRequestOf('b_othercompany_bmfl')
const request = lastRequestOf('b_othercompany_category')
expect(request.deletes).toEqual([{ b_id: 'child1' }])
})
})
+6 -6
View File
@@ -22,7 +22,7 @@ GO
drop table if exists dbo.b_othercompany_accountbank;
GO
drop table if exists dbo.b_othercompany_bmfl;
drop table if exists dbo.b_othercompany_category;
GO
drop table if exists dbo.b_othercompany_partner_type;
@@ -126,7 +126,7 @@ GO
分类树
============================================================================ */
create table dbo.b_othercompany_bmfl (
create table dbo.b_othercompany_category (
b_id varchar(150) not null primary key, -- 节点编码(创建后不可改)
b_parent_id varchar(150) null, -- 父节点编码,NULL 表示根节点
b_depth int not null default 0, -- 树层级,根节点为 0
@@ -142,12 +142,12 @@ create table dbo.b_othercompany_bmfl (
);
GO
create index ix_b_othercompany_bmfl_parent
on dbo.b_othercompany_bmfl (b_parent_id, b_canuse, b_depth, b_xh, b_id);
create index ix_b_othercompany_category_parent
on dbo.b_othercompany_category (b_parent_id, b_canuse, b_depth, b_xh, b_id);
GO
create index ix_b_othercompany_bmfl_path
on dbo.b_othercompany_bmfl (b_path, b_canuse, b_id);
create index ix_b_othercompany_category_path
on dbo.b_othercompany_category (b_path, b_canuse, b_id);
GO
/* ============================================================================
+70
View File
@@ -0,0 +1,70 @@
/* ============================================================================
文件表:bf_files(文件记录)/ bf_files_category(文件分类)
----------------------------------------------------------------------------
文件统一由文件服务托管:本地模式由后端写盘后落库,OSS 模式由前端取号直传后
回调落库;业务表不存文件二进制,只通过 father 关联宿主业务主键。
主键为应用层雪花(subid),不使用 identity —— OSS 直传链路由前端先取号。
结构说明见《FMS新系统核心表结构设计.md》第 15 节。
============================================================================ */
/* ----------------------------------------------------------------------------
1. 文件分类(先建,被 bf_files.mx_cate_id 引用)
---------------------------------------------------------------------------- */
drop table if exists dbo.bf_files_category;
GO
create table dbo.bf_files_category (
b_id bigint not null primary key, -- 主键(雪花,新增行由前端 nextIdApi 生成)
b_module_id varchar(50) null, -- 归属模块(s_module.b_id);NULL = 通用分类
b_name nvarchar(50) not null, -- 分类名称
b_i18n varchar(150) null, -- 多语言资源键(s_i18n.b_key)
b_files_type varchar(50) null, -- 允许的扩展名(逗号分隔,空 = 不限;预留)
b_xh int not null default 0, -- 显示顺序
b_canuse tinyint not null default 1, -- 是否启用(0/1)
b_bz nvarchar(200) null, -- 备注
b_created_by varchar(50) null, -- 创建人(用户账号)
b_created_at datetime2 null, -- 创建时间
b_updated_by varchar(50) null, -- 最后修改人
b_updated_at datetime2 null -- 最后修改时间
);
GO
create index ix_bf_files_category_module
on dbo.bf_files_category (b_module_id, b_canuse, b_xh, b_id);
GO
/* ----------------------------------------------------------------------------
2. 文件记录
---------------------------------------------------------------------------- */
drop table if exists dbo.bf_files;
GO
create table dbo.bf_files (
subid bigint not null primary key, -- 文件主键(雪花,应用层取号:本地由后端、OSS 直传由前端先取号)
father bigint null, -- 宿主业务主键(业务表 b_id),列表按它聚合
mx_filename nvarchar(500) null, -- 原始文件名(含扩展名,展示与下载用)
mx_filesize varchar(50) null, -- 文件大小(字节,字符串存储)
mx_fileext varchar(50) null, -- 扩展名(小写、不含点)
mx_mapfilename nvarchar(500) null, -- 访问地址:本地为 {url-prefix}/{org}/{yyyy}/{MM}/{dd}/{uuid}.{ext},OSS 为完整 URL
mx_moduleid varchar(50) null, -- 业务模块编码(s_module.b_id)
mx_cate_id bigint null, -- 文件分类(bf_files_category.b_id)
mx_xh int not null default 0, -- 排序号(保留;当前实现固定 0,列表按 subid 倒序)
mx_sh tinyint not null default 0, -- 审核标识(保留;当前实现固定 0)
mx_shuser_id varchar(50) null, -- 审核人(保留)
mx_shdatetime datetime2 null, -- 审核时间(保留)
mx_cutfilename nvarchar(500) null, -- 缩略图地址(保留)
mx_cutfilesize varchar(50) null, -- 缩略图大小(保留)
mx_orderid varchar(50) null, -- 排序组(保留)
mx_bz nvarchar(500) null, -- 备注(保留)
mx_inputuser_id varchar(50) null, -- 上传人(用户账号)
mx_inputdatetime datetime2 null, -- 上传时间
mx_updateuser_id varchar(50) null, -- 最后修改人
mx_updatedatetime datetime2 null -- 最后修改时间
);
GO
create index ix_bf_files_host
on dbo.bf_files (father, mx_moduleid, mx_cate_id, mx_xh, subid);
GO
@@ -0,0 +1,18 @@
/* ============================================================================
分类表改名:b_othercompany_bmfl → b_othercompany_category
----------------------------------------------------------------------------
分类表命名统一为 <域>_category(去掉拼音缩写 bmfl)。
原地改名(sp_rename),数据不动;同步改两个自定义索引名。
已确认库内无视图 / 存储过程 / 模块配置引用旧表名,无需连带重建。
============================================================================ */
exec sp_rename 'dbo.b_othercompany_bmfl', 'b_othercompany_category';
GO
exec sp_rename 'dbo.b_othercompany_category.ix_b_othercompany_bmfl_parent',
'ix_b_othercompany_category_parent', 'INDEX';
GO
exec sp_rename 'dbo.b_othercompany_category.ix_b_othercompany_bmfl_path',
'ix_b_othercompany_category_path', 'INDEX';
GO
-971
View File
@@ -1,971 +0,0 @@
```html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>FMS Advanced Search Demo</title>
<style>
* {
box-sizing: border-box;
}
:root {
--primary: #2563eb;
--primary-soft: color-mix(in srgb, var(--primary) 8%, white);
--border: #dcdfe6;
--border-light: #ebeef5;
--text: #303133;
--text-secondary: #606266;
--text-placeholder: #a8abb2;
--bg: #f5f7fa;
--white: #fff;
--danger: #f56c6c;
--control-height: 32px;
}
body {
margin: 0;
font-family:
Inter,
"Microsoft YaHei",
"PingFang SC",
"Noto Sans JP",
Arial,
sans-serif;
color: var(--text);
background: var(--bg);
font-size: 13px;
}
/* ---------------- page ---------------- */
.page {
height: 100vh;
display: flex;
flex-direction: column;
}
.header {
height: 52px;
background: #fff;
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
padding: 0 18px;
gap: 14px;
}
.header-title {
font-size: 15px;
font-weight: 600;
}
.header-subtitle {
color: var(--text-secondary);
}
.header-spacer {
flex: 1;
}
.btn {
height: var(--control-height);
padding: 0 12px;
border: 1px solid var(--border);
border-radius: 4px;
background: #fff;
color: var(--text);
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 5px;
}
.btn:hover {
border-color: var(--primary);
color: var(--primary);
}
.btn-primary {
background: var(--primary);
border-color: var(--primary);
color: #fff;
}
.btn-primary:hover {
background: #1d4ed8;
color: #fff;
}
.btn-text {
border-color: transparent;
background: transparent;
color: var(--text-secondary);
}
.content {
flex: 1;
padding: 14px 18px;
overflow: hidden;
}
/* ---------------- search ---------------- */
.search-panel {
background: #fff;
border: 1px solid var(--border);
padding: 12px;
border-radius: 4px;
}
.search-row {
display: grid;
grid-template-columns: repeat(4, minmax(180px, 1fr));
gap: 10px 14px;
}
.field {
display: flex;
align-items: center;
min-width: 0;
}
.field-label {
flex: 0 0 auto;
margin-right: 8px;
white-space: nowrap;
color: var(--text-secondary);
}
.input,
.select {
height: var(--control-height);
min-width: 0;
width: 100%;
border: 1px solid var(--border);
border-radius: 4px;
background: #fff;
padding: 0 9px;
outline: none;
color: var(--text);
}
.input:focus,
.select:focus {
border-color: var(--primary);
}
.search-actions {
display: flex;
justify-content: flex-end;
margin-top: 10px;
gap: 6px;
}
/* ---------------- table ---------------- */
.table-panel {
margin-top: 12px;
background: #fff;
border: 1px solid var(--border);
border-radius: 4px;
height: calc(100% - 125px);
overflow: hidden;
}
table {
width: 100%;
border-collapse: collapse;
}
th {
height: 38px;
background: #fafafa;
color: var(--text-secondary);
font-weight: 500;
text-align: left;
border-bottom: 1px solid var(--border);
padding: 0 12px;
}
td {
height: 40px;
padding: 0 12px;
border-bottom: 1px solid var(--border-light);
}
.status {
display: inline-flex;
align-items: center;
padding: 2px 7px;
border-radius: 3px;
background: #f0f9eb;
color: #67c23a;
}
/* ---------------- drawer mask ---------------- */
.mask {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, .08);
opacity: 0;
pointer-events: none;
transition: .2s;
}
.mask.open {
opacity: 1;
pointer-events: auto;
}
/* ---------------- drawer ---------------- */
.drawer {
position: fixed;
top: 0;
right: 0;
width: 540px;
max-width: 90vw;
height: 100vh;
background: #fff;
box-shadow: -4px 0 14px rgba(0,0,0,.12);
transform: translateX(100%);
transition: transform .22s ease;
display: flex;
flex-direction: column;
}
.drawer.open {
transform: translateX(0);
}
.drawer-header {
height: 54px;
flex: 0 0 54px;
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
padding: 0 16px;
}
.drawer-title {
font-size: 15px;
font-weight: 600;
}
.drawer-spacer {
flex: 1;
}
.close {
width: 30px;
height: 30px;
border: 0;
background: transparent;
cursor: pointer;
font-size: 20px;
color: var(--text-secondary);
}
.drawer-body {
flex: 1;
overflow: auto;
padding: 14px 16px 80px;
}
/* ---------------- saved query ---------------- */
.query-scheme {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 16px;
}
.query-scheme-label {
white-space: nowrap;
color: var(--text-secondary);
}
.query-scheme .select {
width: 190px;
}
.save-btn {
margin-left: auto;
}
/* ---------------- query group ---------------- */
.group {
border: 1px solid var(--border);
border-radius: 4px;
}
.group-header {
height: 38px;
display: flex;
align-items: center;
padding: 0 10px;
background: #fafafa;
border-bottom: 1px solid var(--border-light);
}
.group-title {
font-weight: 500;
}
.group-actions {
margin-left: auto;
display: flex;
gap: 8px;
}
.link-btn {
border: 0;
background: transparent;
padding: 0;
cursor: pointer;
color: var(--primary);
font-size: 13px;
}
.conditions {
padding: 10px;
}
/* ---------------- condition ---------------- */
.condition {
display: grid;
grid-template-columns: 125px 115px minmax(110px, 1fr) 26px;
gap: 7px;
align-items: center;
min-height: 42px;
}
.condition + .condition {
margin-top: 5px;
}
.condition .select,
.condition .input {
width: 100%;
}
.remove-condition {
width: 26px;
height: 26px;
border: 0;
background: transparent;
color: #a8abb2;
cursor: pointer;
font-size: 16px;
}
.remove-condition:hover {
color: var(--danger);
}
/* ---------------- connector ---------------- */
.connector {
height: 28px;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.connector::before {
content: "";
position: absolute;
left: 18px;
right: 18px;
top: 50%;
height: 1px;
background: var(--border-light);
}
.connector span {
position: relative;
background: #fff;
padding: 0 7px;
color: var(--text-secondary);
font-size: 12px;
}
/* ---------------- group add ---------------- */
.add-condition {
margin-top: 8px;
}
.add-group {
margin-top: 10px;
display: flex;
justify-content: center;
}
/* ---------------- nested group ---------------- */
.nested-group {
margin-top: 10px;
margin-left: 12px;
border: 1px dashed #cfd3dc;
border-radius: 4px;
}
.nested-header {
height: 34px;
display: flex;
align-items: center;
padding: 0 10px;
background: #fafafa;
}
.nested-title {
color: var(--text-secondary);
}
/* ---------------- footer ---------------- */
.drawer-footer {
height: 58px;
flex: 0 0 58px;
border-top: 1px solid var(--border);
display: flex;
align-items: center;
justify-content: flex-end;
gap: 8px;
padding: 0 16px;
background: #fff;
}
/* ---------------- active filters ---------------- */
.active-filters {
display: flex;
align-items: center;
gap: 6px;
margin-top: 10px;
flex-wrap: wrap;
}
.active-label {
color: var(--text-secondary);
}
.tag {
height: 26px;
display: inline-flex;
align-items: center;
gap: 5px;
padding: 0 8px;
border: 1px solid #d9ecff;
border-radius: 3px;
background: #f4f9ff;
color: #409eff;
}
.tag-close {
cursor: pointer;
color: #79bbff;
}
/* ---------------- responsive ---------------- */
@media (max-width: 900px) {
.search-row {
grid-template-columns: repeat(2, 1fr);
}
.drawer {
width: 520px;
}
}
@media (max-width: 600px) {
.search-row {
grid-template-columns: 1fr;
}
.condition {
grid-template-columns: 1fr 1fr;
}
.condition .value {
grid-column: 1 / 3;
}
.condition .remove-condition {
position: absolute;
right: 10px;
}
}
</style>
</head>
<body>
<div class="page">
<!-- Header -->
<div class="header">
<div class="header-title">订舱管理</div>
<div class="header-subtitle">Booking Orders</div>
<div class="header-spacer"></div>
<button class="btn">新增</button>
<button class="btn">导出</button>
<button class="btn" onclick="openDrawer()">高级搜索</button>
</div>
<!-- Content -->
<div class="content">
<!-- Simple search -->
<div class="search-panel">
<div class="search-row">
<div class="field">
<div class="field-label">订舱号</div>
<input class="input" placeholder="请输入">
</div>
<div class="field">
<div class="field-label">客户</div>
<input class="input" placeholder="请输入">
</div>
<div class="field">
<div class="field-label">业务员</div>
<input class="input" placeholder="请输入">
</div>
<div class="field">
<div class="field-label">状态</div>
<select class="select">
<option>全部</option>
<option>草稿</option>
<option>已确认</option>
<option>已完成</option>
</select>
</div>
</div>
<div class="search-actions">
<button class="btn">重置</button>
<button class="btn btn-primary">查询</button>
</div>
</div>
<!-- Active conditions -->
<div class="active-filters">
<span class="active-label">当前条件:</span>
<span class="tag">
客户 = ABC Logistics
<span class="tag-close">×</span>
</span>
<span class="tag">
金额 &gt; 10000
<span class="tag-close">×</span>
</span>
<span class="tag">
日期 ≥ 2026-09-01
<span class="tag-close">×</span>
</span>
</div>
<!-- Table -->
<div class="table-panel">
<table>
<thead>
<tr>
<th width="150">订舱号</th>
<th>客户</th>
<th>业务员</th>
<th>金额</th>
<th>开船日期</th>
<th>状态</th>
</tr>
</thead>
<tbody>
<tr>
<td>BK202609001</td>
<td>ABC Logistics</td>
<td>张三</td>
<td>25,800.00</td>
<td>2026-09-05</td>
<td><span class="status">已确认</span></td>
</tr>
<tr>
<td>BK202609002</td>
<td>XYZ Shipping</td>
<td>李四</td>
<td>18,600.00</td>
<td>2026-09-08</td>
<td><span class="status">已完成</span></td>
</tr>
<tr>
<td>BK202609003</td>
<td>ABC Logistics</td>
<td>王五</td>
<td>12,500.00</td>
<td>2026-09-12</td>
<td><span class="status">已确认</span></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- Mask -->
<div id="mask" class="mask" onclick="closeDrawer()"></div>
<!-- Drawer -->
<div id="drawer" class="drawer">
<div class="drawer-header">
<div class="drawer-title">高级搜索</div>
<div class="drawer-spacer"></div>
<button class="close" onclick="closeDrawer()">×</button>
</div>
<div class="drawer-body">
<!-- Query scheme -->
<div class="query-scheme">
<span class="query-scheme-label">查询方案</span>
<select class="select">
<option>我的查询</option>
<option>最近一个月</option>
<option>未完成订单</option>
<option>高金额订单</option>
<option>系统默认</option>
</select>
<button class="btn save-btn">保存</button>
</div>
<!-- Main group -->
<div class="group">
<div class="group-header">
<span class="group-title">
满足以下全部条件
</span>
<div class="group-actions">
<button class="link-btn">全部</button>
<button class="link-btn">任一</button>
</div>
</div>
<div class="conditions" id="conditions">
<!-- condition 1 -->
<div class="condition">
<select class="select field-select">
<option value="customer">客户</option>
<option value="bookingNo">订舱号</option>
<option value="sales">业务员</option>
<option value="amount">金额</option>
<option value="date">开船日期</option>
<option value="status">状态</option>
</select>
<select class="select operator">
<option>=</option>
<option>≠</option>
<option>包含</option>
<option>不包含</option>
</select>
<input class="input value" value="ABC Logistics">
<button class="remove-condition">×</button>
</div>
<!-- connector -->
<div class="connector">
<span>AND</span>
</div>
<!-- condition 2 -->
<div class="condition">
<select class="select field-select">
<option value="amount">金额</option>
<option value="customer">客户</option>
<option value="bookingNo">订舱号</option>
<option value="sales">业务员</option>
<option value="date">开船日期</option>
<option value="status">状态</option>
</select>
<select class="select operator">
<option>&gt;</option>
<option>&gt;=</option>
<option>=</option>
<option>&lt;</option>
<option>&lt;=</option>
<option>≠</option>
</select>
<input class="input value" value="10000">
<button class="remove-condition">×</button>
</div>
<!-- connector -->
<div class="connector">
<span>AND</span>
</div>
<!-- condition 3 -->
<div class="condition">
<select class="select field-select">
<option value="date">开船日期</option>
<option value="customer">客户</option>
<option value="bookingNo">订舱号</option>
<option value="sales">业务员</option>
<option value="amount">金额</option>
<option value="status">状态</option>
</select>
<select class="select operator">
<option>&gt;=</option>
<option>&gt;</option>
<option>=</option>
<option>&lt;</option>
<option>&lt;=</option>
</select>
<input class="input value" type="date" value="2026-09-01">
<button class="remove-condition">×</button>
</div>
</div>
<div style="padding:0 10px 12px;">
<button class="link-btn" onclick="addCondition()">
+ 添加条件
</button>
</div>
</div>
<!-- Nested group -->
<div class="nested-group">
<div class="nested-header">
<span class="nested-title">
满足以下任一条件
</span>
<div class="group-actions">
<button class="link-btn">删除条件组</button>
</div>
</div>
<div class="conditions">
<div class="condition">
<select class="select">
<option>港口</option>
<option>客户</option>
<option>业务员</option>
</select>
<select class="select">
<option>=</option>
<option>包含</option>
</select>
<input class="input" value="Shanghai">
<button class="remove-condition">×</button>
</div>
<div class="connector">
<span>OR</span>
</div>
<div class="condition">
<select class="select">
<option>港口</option>
<option>客户</option>
<option>业务员</option>
</select>
<select class="select">
<option>=</option>
<option>包含</option>
</select>
<input class="input" value="Ningbo">
<button class="remove-condition">×</button>
</div>
</div>
</div>
<div class="add-group">
<button class="link-btn">
+ 添加条件组
</button>
</div>
</div>
<div class="drawer-footer">
<button class="btn">重置</button>
<button class="btn">取消</button>
<button class="btn btn-primary" onclick="doSearch()">查询</button>
</div>
</div>
<script>
function openDrawer() {
document.getElementById("drawer").classList.add("open");
document.getElementById("mask").classList.add("open");
}
function closeDrawer() {
document.getElementById("drawer").classList.remove("open");
document.getElementById("mask").classList.remove("open");
}
function addCondition() {
const container = document.getElementById("conditions");
const connector = document.createElement("div");
connector.className = "connector";
connector.innerHTML = "<span>AND</span>";
const condition = document.createElement("div");
condition.className = "condition";
condition.innerHTML = `
<select class="select">
<option>客户</option>
<option>订舱号</option>
<option>业务员</option>
<option>金额</option>
<option>开船日期</option>
<option>状态</option>
</select>
<select class="select">
<option>=</option>
<option>≠</option>
<option>包含</option>
<option>不包含</option>
<option>&gt;</option>
<option>&gt;=</option>
<option>&lt;</option>
<option>&lt;=</option>
</select>
<input class="input" placeholder="请输入">
<button class="remove-condition"
onclick="this.parentElement.remove()">×</button>
`;
container.appendChild(connector);
container.appendChild(condition);
}
function doSearch() {
/*
* 实际项目中这里不直接拼 SQL。
*
* UI 最终转换成统一 Query AST,例如:
*
* {
* type: "group",
* operator: "AND",
* children: [
* {
* field: "customer",
* operator: "=",
* value: "ABC Logistics"
* },
* {
* field: "amount",
* operator: ">",
* value: 10000
* },
* {
* field: "date",
* operator: ">=",
* value: "2026-09-01"
* },
* {
* type: "group",
* operator: "OR",
* children: [
* {
* field: "port",
* operator: "=",
* value: "Shanghai"
* },
* {
* field: "port",
* operator: "=",
* value: "Ningbo"
* }
* ]
* ]
* }
*/
closeDrawer();
alert("查询条件已应用");
}
</script>
</body>
</html>
```