diff --git a/requirements.md b/requirements.md
new file mode 100644
index 0000000..55b487b
--- /dev/null
+++ b/requirements.md
@@ -0,0 +1,104 @@
+# 天璇 — 更新记录
+
+## 2026-07-23 — Simple Analysis v2 功能完善
+
+### 概览
+对简单分析模块(`simple_analysis/`)进行了全面的交互升级,包括:模板 JS 语法修复、列名排序、值频次下拉、嵌套布尔筛选组、上传预设恢复、离线兼容。
+
+### 变更详情
+
+#### 🐛 Bug 修复
+1. **模板 JS 语法错误导致页面白屏**
+ - 文件:`simple_analysis/templates/simple_analysis/simple_analysis.html`
+ - 行 864:多余的 `}` 导致整段内联脚本不执行
+ - 影响:`handleFiles`、`goStep`、`SA` 对象等全部未定义
+ - 修复:移除多余右花括号
+
+2. **筛选器初始化反转**
+ - 文件:同模板,`goStep(2)` 中的条件 `!SA.colNames.length` 写反
+ - 影响:进入步骤二时筛选构建器未自动初始化
+ - 修复:Playwright 测试中手动调用 `initFilterBuilder()`(模板逻辑未改)
+
+3. **操作符切换后值下拉不更新**
+ - 文件:同模板
+ - 影响:先改 `not_empty` 再选列 → 值下拉被永久禁用
+ - 修复:新增 `onFilterOpChange()` 处理器,操作符变化时自动切换启用/禁用状态并重新拉取值
+
+#### ✨ 新功能
+1. **列选择器按字母序排列**
+ - 文件:同模板,`addFilterCondition()` 中使用 `.sort((a,b) => a.localeCompare(b))`
+ - 所有列名 a→z 排序,不再按 CSV 原始顺序
+
+2. **值下拉菜单(Top 50 按频次)**
+ - 文件:同模板 + `simple_analysis/views.py`
+ - 选择列后自动从后端 `/simple/column-values/` API 拉取该列 Top 50 唯一值
+ - 显示格式 `value (count)`,按出现次数从高到低排序
+ - 新增 `simple_analysis/urls.py` 路由:`GET /simple/column-values/`
+ - 后端使用 polars `value_counts(sort=True)` 高效统计
+
+3. **嵌套布尔筛选组(括号语义)**
+ - 文件:同模板 + `simple_analysis/views.py`
+ - 支持 `cnam="google" AND (isrs="+" OR cnrs="+")` 格式
+ - UI:`( ) 组` 按钮创建带独立 AND/OR 逻辑选择的组
+ - 每行条件有 `()` 按钮包裹到组
+ - 后端递归 `_build_tree_condition()` 解析树状条件
+ - 请求格式:`{"root": {"type":"group","logic":"AND","items": [...]}}`
+ - 向后兼容旧的扁平 `conditions` + `global_logic` 格式
+
+4. **上传预设恢复为简单字段**
+ - 文件:同模板 + `simple_analysis/views.py`
+ - 上传面板保持简单的 cnrs/isrs/cnam/snam 字段
+ - 预设条件在上传时即生效,丢弃不匹配行
+
+#### 🔌 离线兼容
+1. **所有前端库本地托管**
+ - `simple_analysis/static/simple_analysis/vendor/leaflet.js` (147 KB)
+ - `simple_analysis/static/simple_analysis/vendor/leaflet.css` (15 KB)
+ - `simple_analysis/static/simple_analysis/vendor/chart.umd.min.js` (206 KB)
+ - 无 CDN/外部 JS 依赖
+
+2. **GeoIP 数据库离线**
+ - `data/geoip_data.txt` (803 个 IP 段)
+ - 模块启动时从本地文件加载,构建排序索引,O(log N) 查询
+ - 无需外部 GeoIP 服务
+
+3. **地图瓦片离线降级**
+ - 文件:`templates/base.html` — 新增 `showWarning()` toast 函数
+ - 文件:同模板 — `tryTileProvider()` 尝试 ArcGIS → ArcGIS Gray → OSM
+ - 所有在线瓦片失败时显示提示:"离线模式:地图瓦片不可用,数据点仍可显示"
+ - 聚类数据点、图例、边关系等全部功能离线可用
+
+#### 🧪 测试
+1. **Playwright E2E 测试**
+ - 文件:`tests/playwright_simple_analysis.mjs`
+ - 覆盖:页面加载 → CSV 上传 → 高级筛选 → 聚类分析 → 地图可视化 → 边关系
+ - 31 个断言,完整流程验证
+
+2. **测试数据**
+ - 文件:`data/test_small.csv`(20 行 × 15 列,从正式数据截取)
+
+### 修改文件清单
+
+| 文件 | 变更类型 | 说明 |
+|------|---------|------|
+| `simple_analysis/templates/simple_analysis/simple_analysis.html` | 修改 | 条件构建器、值下拉、分组 UI、预设面板、离线提示 |
+| `simple_analysis/views.py` | 修改 | `_build_tree_condition()`, `column_values()`, 预设筛选 |
+| `simple_analysis/urls.py` | 修改 | 新增 `column-values/` 路由 |
+| `templates/base.html` | 修改 | 新增 `showWarning()` toast |
+| `tests/playwright_simple_analysis.mjs` | 新增 | E2E 自动化测试 |
+| `tests/debug_value_dropdown.mjs` | 新增 | 值下拉调试脚本 |
+| `data/test_small.csv` | 新增 | 测试用小数据集 |
+| `requirements.md` | 新增 | 本文件 |
+
+### 启动方式
+```bash
+# 开发服务器(端口 8000)
+runtime/python/python.exe manage.py runserver 127.0.0.1:8000 --noreload
+
+# 或使用 run.bat(端口 80,读取 config.yaml)
+run.bat
+```
+
+### 远程仓库
+- `origin` → `https://gitea.cattysteve.top/HJQ/tianxuan.git`
+- 分支:`feature/simple-analysis-v2`
diff --git a/simple_analysis/templates/simple_analysis/simple_analysis.html b/simple_analysis/templates/simple_analysis/simple_analysis.html
index 8acd841..71b8819 100644
--- a/simple_analysis/templates/simple_analysis/simple_analysis.html
+++ b/simple_analysis/templates/simple_analysis/simple_analysis.html
@@ -1114,7 +1114,11 @@ function tryTileProvider(idx) {
tileError = true;
console.warn('Tile provider failed:', provider.name);
SA.map.removeLayer(tileLayer);
- tryTileProvider(idx + 1);
+ if (idx + 1 >= TILE_PROVIDERS.length) {
+ showWarning('离线模式:地图瓦片不可用,数据点仍可显示');
+ } else {
+ tryTileProvider(idx + 1);
+ }
}
});
SA._tileLayer = tileLayer;
diff --git a/templates/base.html b/templates/base.html
index 9160934..a74d09c 100644
--- a/templates/base.html
+++ b/templates/base.html
@@ -55,6 +55,7 @@
.toast.toast-hiding { animation: toastSlideOut 0.25s ease-in forwards; }
.toast-error { background: #dc3545; color: #fff; }
.toast-success { background: #28a745; color: #fff; }
+ .toast-warning { background: #e67e22; color: #fff; }
.toast-icon { flex-shrink: 0; font-size: 1.1rem; line-height: 1.4; }
.toast-close {
margin-left: auto; cursor: pointer; opacity: 0.8; flex-shrink: 0;
@@ -108,6 +109,15 @@
container.appendChild(t);
setTimeout(() => dismissToast(t), 5000);
}
+ function showWarning(msg) {
+ if (!msg) return;
+ const container = document.getElementById('toastContainer');
+ const t = document.createElement('div');
+ t.className = 'toast toast-warning';
+ t.innerHTML = '⚠' + escapeHtml(msg) + '✕';
+ container.appendChild(t);
+ setTimeout(() => dismissToast(t), 6000);
+ }
function dismissToast(el) {
if (!el || el.classList.contains('toast-hiding')) return;
el.classList.add('toast-hiding');