diff --git a/plugins/easytable/CHANGELOG.md b/plugins/easytable/CHANGELOG.md index f7847f4e4..9ba4f0f09 100644 --- a/plugins/easytable/CHANGELOG.md +++ b/plugins/easytable/CHANGELOG.md @@ -1,5 +1,33 @@ # Changelog +## 1.0.3 - 2026-09-30 + +### Added + +- 字段类型改成「单选 / 多选」时自动收集已有取值:切换类型后弹确认框列出将加入的选项,选「加入」即写入选项,老数据的值当场就能在下拉框里选到 +- 表设置的选项输入框下新增「从已有数据收集」:数据是后来导入或别处写入的,也能一键把新取值补进选项 +- 单选 / 多选下拉一律显示「已配置选项 + 已有数据里出现过的值」——编辑记录、记一笔、筛选「属于选项」三个入口都不会再出现「值在表里、下拉里选不到」 + +### Fixed + +- 改字段类型后已有数据没有按新类型转换(类型变更确认框写了「会尽量转换」,实际从未执行):文本改多选/列表后单元格显示为空、改数字/日期后残留原字符串 +- 「已回填 N 条记录」中的 N 是表内总行数,不是真正被回填的行数 + +## 1.0.2 - 2026-09-24 + +### Added + +- 新增「行高样式」开关:「⋯ 更多」菜单里可选「固定行高(超出省略)」与「自适应行高(随内容撑高)」两种样式,当前样式带 ✓ 标记,选择即生效并持久保存(默认固定行高) +- 高级筛选:升级为 AND 条件组,可添加多条条件自由组合;每个字段支持「为空 / 不为空」,单选/多选支持「属于选项」,文本/链接/数字支持「包含关键词」 +- 时间筛选:创建时间 / 修改时间 / 日期字段支持「今天 / 本周 / 本月」(周一起始);筛选面板顶部提供「本周创建」「本月创建」「今天更新」「本周更新」快捷按钮(点击切换) +- 组合场景:如「复制本周创建的、链接为空的数据」——添加两条条件后,不勾选行直接「导出 → 复制到 Excel」即可复制筛选结果 +- 字段默认值:表设置里可为每个字段配置默认值,新增/记一笔/导入时自动预填;新增字段设置了默认值时,可选择是否回填到已有记录(回填只补缺失行,不覆盖已有值) + +### Changed + +- 「新建表」从表切换下拉后的文字按钮改为工具栏左上角第一个按钮(primary plain 样式),更醒目、更好找 +- 筛选面板加宽至 360px,显示激活条件数(筛选 · N);切换表格时清空筛选条件 + ## 1.0.1 - 2026-09-22 ### Added diff --git a/plugins/easytable/README.md b/plugins/easytable/README.md index 422cb7f8f..067ca2123 100644 --- a/plugins/easytable/README.md +++ b/plugins/easytable/README.md @@ -6,15 +6,17 @@ ## 更新日志 -### v1.0.1 - 2026-09-22 -- 修复:导入 400+ 条从卡半分钟降到秒级(preload bulkDocs) -- 修复:导入对话框加入 loading 反馈 -- 修复:导入 JSON 备份时整段加全屏 loading -- 修复:列表不能上下滚动 -- 新增:更多菜单加入「清空当前表(仅记录)」,两段确认 + 全屏 loading - -### v1.0.0 - 2026-09-22 -- 首次发布:多表自定义字段、记一笔快捷录入、TSV 复制到 Excel、CSV/JSON 导入导出 +### v1.0.3 - 2026-09-30 +- 修复:字段从文本改成单选/多选后,已有数据里的值不会出现在下拉框里。现在改类型时会询问「是否把已有取值收进选项」,也可在表设置的选项框下点「从已有数据收集」补收;所有单选/多选下拉都会显示表里已经出现过的值 +- 修复:改字段类型后已有数据未按新类型转换(文本改多选/列表后单元格空白、改数字/日期残留旧字符串) +- 修复:「已回填 N 条记录」的条数显示(原来把表内总行数当成了回填条数) + +### v1.0.2 - 2026-09-24 +- 新增:行高样式开关,可在「固定行高(超出省略)」与「自适应行高(随内容撑高)」间切换 +- 变更:「新建表」按钮移到工具栏左上角第一个位置 +- 新增:高级筛选(AND 条件组),支持为空/不为空、包含、属于选项、今天/本周/本月 +- 新增:筛选快捷按钮(本周创建 / 本月创建 / 今天更新 / 本周更新) +- 新增:字段默认值,新增记录自动预填;新增字段可选回填已有记录 ## 这是什么? @@ -45,8 +47,10 @@ - **多张表**:下拉切换;新建表后可自定义字段 - **字段类型**:文本、长文本、列表、单选、多选/标签、链接、数字、日期、是否 -- **表设置**:改表名、字段、显示创建时间/修改时间 -- **查找**:搜索、按标签/模块筛选、点列头排序 +- **表设置**:改表名、字段、显示创建时间/修改时间;单选/多选可从已有数据一键收集选项 +- **查找**:搜索、高级筛选(为空/不为空、包含、选项、今天/本周/本月,可组合)、点列头排序 +- **字段默认值**:表设置里配置,新增记录自动预填;新增字段可回填已有记录 +- **选项自动收集**:字段改成单选/多选时自动把已有取值收进选项;下拉框始终包含表里出现过的值 - **系统列**:创建时间、修改时间(只读,可排序、可导出) - **本地保存**:数据存在本机,不联网;请用 JSON 备份做迁移 diff --git a/plugins/easytable/index.png b/plugins/easytable/index.png index b61d53aea..db58a4aaa 100644 Binary files a/plugins/easytable/index.png and b/plugins/easytable/index.png differ diff --git a/plugins/easytable/indx.png b/plugins/easytable/indx.png deleted file mode 100644 index db58a4aaa..000000000 Binary files a/plugins/easytable/indx.png and /dev/null differ diff --git a/plugins/easytable/package.json b/plugins/easytable/package.json index af8b564e3..6766ef6af 100644 --- a/plugins/easytable/package.json +++ b/plugins/easytable/package.json @@ -1,12 +1,11 @@ { "name": "easytable", - "version": "1.0.0", + "version": "1.0.3", "description": "自定义字段的简单记录表:文本/标签/单选等,一键复制到 Excel,支持 CSV/JSON 导入导出", "type": "module", "scripts": { "dev": "vite", - "build": "vue-tsc && vite build", - "test:codec": "node --experimental-strip-types scripts/test-codec.mjs" + "build": "vue-tsc && vite build" }, "dependencies": { "element-plus": "^2.14.2", diff --git a/plugins/easytable/scripts/make-logo.py b/plugins/easytable/scripts/make-logo.py deleted file mode 100644 index 5cb7acf8c..000000000 --- a/plugins/easytable/scripts/make-logo.py +++ /dev/null @@ -1,91 +0,0 @@ -"""绘制「简单记录」插件 logo(扁平几何图标,非 AI 生成)。 - -用法: python scripts/make-logo.py -输出: src-ztools/logo.png(1024)、logo-256.png(便于预览) -""" -from __future__ import annotations - -import math -from pathlib import Path - -from PIL import Image, ImageDraw - -ROOT = Path(__file__).resolve().parents[1] -OUT_ICON = ROOT / "src-ztools" / "logo.png" -OUT_256 = ROOT / "logo-256.png" - -# 色板:象牙卡片 / 墨蓝线条 / 珊瑚勾选 -IVORY = (250, 246, 238, 255) -IVORY_EDGE = (232, 224, 208, 255) -INK = (36, 52, 86, 255) -INK_SOFT = (36, 52, 86, 210) -CORAL = (232, 108, 92, 255) -CORAL_DEEP = (200, 78, 64, 255) -TRANSPARENT = (0, 0, 0, 0) - - -def rounded_rect(draw: ImageDraw.ImageDraw, box, radius, fill, outline=None, width=1): - draw.rounded_rectangle(box, radius=radius, fill=fill, outline=outline, width=width) - - -def draw_logo(size: int = 1024) -> Image.Image: - S = size / 1024 - img = Image.new("RGBA", (size, size), TRANSPARENT) - d = ImageDraw.Draw(img) - - # 外层圆角方垫(便于在深浅底上识别) - pad = int(64 * S) - rounded_rect(d, (pad, pad, size - pad, size - pad), int(220 * S), IVORY, IVORY_EDGE, int(10 * S)) - - # 卡片 - card_l, card_t = int(220 * S), int(210 * S) - card_r, card_b = int(820 * S), int(780 * S) - rounded_rect(d, (card_l, card_t, card_r, card_b), int(48 * S), (255, 255, 255, 255), INK, int(18 * S)) - - # 三条记录线 - line_x1 = int(300 * S) - line_x2 = int(720 * S) - line_ys = (int(340 * S), int(460 * S), int(580 * S)) - widths = (int(28 * S), int(28 * S), int(28 * S)) - radii = (int(14 * S), int(14 * S), int(14 * S)) - # 前两行短一点,第三行更短,像未写完的记录 - rights = (line_x2, line_x2 - int(40 * S), line_x2 - int(120 * S)) - for y, w, r, x2 in zip(line_ys, widths, radii, rights): - rounded_rect(d, (line_x1, y - w // 2, x2, y + w // 2), r, INK_SOFT) - - # 左侧小勾选点(每行行首) - for y in line_ys: - cx, cy = int(270 * S), y - rr = int(14 * S) - d.ellipse((cx - rr, cy - rr, cx + rr, cy + rr), fill=INK) - - # 右下角勾选徽章 - bx, by = int(740 * S), int(700 * S) - br = int(110 * S) - d.ellipse((bx - br, by - br, bx + br, by + br), fill=CORAL, outline=CORAL_DEEP, width=int(10 * S)) - # 对勾(两段折线,圆角感用粗线) - check = [ - (bx - int(48 * S), by + int(4 * S)), - (bx - int(12 * S), by + int(40 * S)), - (bx + int(52 * S), by - int(36 * S)), - ] - d.line(check, fill=(255, 255, 255, 255), width=int(28 * S), joint="curve") - # 线头圆帽 - r_cap = int(14 * S) - for x, y in check: - d.ellipse((x - r_cap, y - r_cap, x + r_cap, y + r_cap), fill=(255, 255, 255, 255)) - - return img - - -def main() -> None: - OUT_ICON.parent.mkdir(parents=True, exist_ok=True) - icon = draw_logo(1024) - icon.save(OUT_ICON, "PNG") - icon.resize((256, 256), Image.Resampling.LANCZOS).save(OUT_256, "PNG") - print("wrote", OUT_ICON) - print("wrote", OUT_256) - - -if __name__ == "__main__": - main() diff --git a/plugins/easytable/scripts/test-codec.mjs b/plugins/easytable/scripts/test-codec.mjs deleted file mode 100644 index 10a457772..000000000 --- a/plugins/easytable/scripts/test-codec.mjs +++ /dev/null @@ -1,133 +0,0 @@ -// 编解码往返测试(node 直跑,不依赖 DOM) -// 用法: node scripts/test-codec.mjs - -import { encodeCell, encodeRow, encodeTable, parseDelimited, encodeCsv, parseCsv, parseTsv } from '../src/domain/codec/tsv.ts' -import { - rowsToTsv, - parseImportText, - splitMulti, - mapRowValues, - autoColumnMap, - formatCell -} from '../src/domain/codec/tableCodec.ts' -import { toBackup, serializeBackup, parseBackup } from '../src/domain/codec/jsonBackup.ts' -import { coerceFieldValue, defaultValue, displayValue } from '../src/domain/fieldTypes.ts' - -let pass = 0 -let fail = 0 -function assert(name, cond, detail) { - if (cond) { - pass += 1 - console.log('PASS', name) - } else { - fail += 1 - console.error('FAIL', name, detail ?? '') - } -} -function assertEq(name, a, b) { - assert(name, JSON.stringify(a) === JSON.stringify(b), `got=${JSON.stringify(a)} expected=${JSON.stringify(b)}`) -} - -// --- cell encode --- -assertEq('empty cell', encodeCell(''), '') -assertEq('plain cell', encodeCell('成绩管理'), '成绩管理') -assertEq('quote cell', encodeCell('a"b'), '"a""b"') -assertEq('tab cell', encodeCell('a\tb'), '"a\tb"') -assertEq('newline cell', encodeCell('a\nb'), '"a\nb"') -assertEq('chinese', encodeCell('标签、中文'), '标签、中文') - -// --- roundtrip cells --- -const tricky = ['', '普通', '含\t制表', '含\n换行', '含"引号"', 'a""b', '#125189', 'https://x.com/?a=1&b=2', 'v1、成绩录入、成绩标记'] -const rowStr = encodeRow(tricky) -const parsedRow = parseDelimited(rowStr)[0] -assertEq('roundtrip row cells', parsedRow, tricky) - -// --- multi rows --- -const matrix = [ - ['pm号', '模块', '标签', '笔记链接'], - ['#125189', '成绩管理', 'v1、成绩录入', ''], - ['#103313', '成绩管理', 'v1', 'https://a.com/x'], - ['含\t与"混', '模\n块', 'a、b', ''] -] -const tsv = encodeTable(matrix) -assertEq('roundtrip matrix tsv', parseTsv(tsv), matrix) -const csv = encodeCsv(matrix) -assertEq('roundtrip matrix csv', parseCsv(csv), matrix) - -// --- splitMulti --- -assertEq('split 、', splitMulti('v1、成绩录入、成绩标记'), ['v1', '成绩录入', '成绩标记']) -assertEq('split ,', splitMulti('a, b ,c'), ['a', 'b', 'c']) -assertEq('split ,', splitMulti('x,y'), ['x', 'y']) -assertEq('split |', splitMulti('x|y'), ['x', 'y']) -assertEq('url intact', splitMulti('https://a.com/b/c'), ['https://a.com/b/c']) -assertEq('empty multi', splitMulti(' '), []) -assertEq('mixed seps', splitMulti('v1, v2|v3、v4'), ['v1', 'v2', 'v3', 'v4']) - -// --- field coerce --- -assertEq('coerce number', coerceFieldValue({ id: '1', name: 'n', type: 'number' }, '1,234'), 1234) -assertEq('coerce date', coerceFieldValue({ id: '1', name: 'd', type: 'date' }, '2026/9/3'), '2026-09-03') -assertEq('coerce checkbox', coerceFieldValue({ id: '1', name: 'c', type: 'checkbox' }, '是'), true) -assertEq('coerce multi', coerceFieldValue({ id: '1', name: 'm', type: 'list' }, 'a、b,c'), ['a', 'b', 'c']) - -// --- fields + rows export --- -const fields = [ - { id: 'f1', name: 'pm号', type: 'text' }, - { id: 'f2', name: '模块', type: 'select', options: ['成绩管理'] }, - { id: 'f3', name: '标签', type: 'multi_select', options: [] }, - { id: 'f4', name: '笔记链接', type: 'url' } -] -const rows = [ - { - id: 'r1', - tableId: 't1', - createdAt: 1, - updatedAt: 1, - values: { f1: '#125189', f2: '成绩管理', f3: ['v1', '成绩录入', '成绩标记'], f4: '' } - }, - { - id: 'r2', - tableId: 't1', - createdAt: 2, - updatedAt: 2, - values: { f1: '#103313', f2: '成绩管理', f3: ['v1', '学生端', '成绩查询'], f4: 'https://n.com/1' } - } -] -const outTsv = rowsToTsv(fields, rows) -const back = parseImportText(outTsv) -assertEq('export headers', back.headers, ['pm号', '模块', '标签', '笔记链接']) -assertEq('export row0 tags', back.dataRows[0][2], 'v1、成绩录入、成绩标记') -assertEq('export data rows', back.dataRows.length, 2) - -const cmap = autoColumnMap(fields, back.headers) -assertEq('auto map', cmap, { 0: 'f1', 1: 'f2', 2: 'f3', 3: 'f4' }) -const mapped = mapRowValues(fields, back.headers, back.dataRows[0], cmap) -assertEq('map tags back', mapped.f3, ['v1', '成绩录入', '成绩标记']) -assertEq('map pm', mapped.f1, '#125189') - -// --- JSON backup roundtrip --- -const schema = { - id: 't1', - name: '工作记录', - fields, - createdAt: 1, - updatedAt: 2 -} -const backup = toBackup([schema], { t1: rows }, '、') -const restored = parseBackup(serializeBackup(backup)) -assertEq('backup tables', restored.tables.length, 1) -assertEq('backup rows', restored.tables[0].rows.length, 2) -assertEq('backup fields', restored.tables[0].schema.fields.length, 4) - -// --- display --- -assertEq('display multi', formatCell(fields[2], ['a', 'b']), 'a、b') -assertEq('display empty multi', formatCell(fields[2], []), '') -assertEq('default multi', defaultValue('multi_select'), []) -assertEq('display checkbox true', displayValue({ id: 'x', name: 'c', type: 'checkbox' }, true), '是') - -// --- empty / edge import --- -assertEq('parse empty', parseImportText(''), { headers: [], dataRows: [] }) -const onlyHeader = parseImportText('a\tb\n') -assertEq('only header', onlyHeader.dataRows.length, 0) - -console.log(`\nResult: ${pass} passed, ${fail} failed`) -if (fail > 0) process.exit(1) diff --git a/plugins/easytable/src-ztools/plugin.json b/plugins/easytable/src-ztools/plugin.json index 37d54e03c..dd0aaeabc 100644 --- a/plugins/easytable/src-ztools/plugin.json +++ b/plugins/easytable/src-ztools/plugin.json @@ -4,7 +4,7 @@ "title": "简单记录", "description": "自定义字段的简单记录表:文本/标签/单选等,一键复制到 Excel,支持 CSV/JSON 导入导出", "author": "咖啡八杯", - "version": "1.0.1", + "version": "1.0.3", "categories": ["productivity"], "main": "dist/index.html", "preload": "preload/services.js", diff --git a/plugins/easytable/src-ztools/preload/services.js b/plugins/easytable/src-ztools/preload/services.js index 50cf5c6cc..4b290cef5 100644 --- a/plugins/easytable/src-ztools/preload/services.js +++ b/plugins/easytable/src-ztools/preload/services.js @@ -208,7 +208,8 @@ window.services = { tables: meta.tables, activeTableId: meta.activeTableId || (meta.tables[0] && meta.tables[0].id) || '', multiSeparator: meta.multiSeparator || '、', - quickTableId: meta.quickTableId || '' + quickTableId: meta.quickTableId || '', + rowHeightMode: meta.rowHeightMode === 'auto' ? 'auto' : 'fixed' } } } catch (e) {} @@ -221,7 +222,8 @@ window.services = { tables: (meta && meta.tables) || [], activeTableId: (meta && meta.activeTableId) || '', multiSeparator: (meta && meta.multiSeparator) || '、', - quickTableId: (meta && meta.quickTableId) || '' + quickTableId: (meta && meta.quickTableId) || '', + rowHeightMode: meta && meta.rowHeightMode === 'auto' ? 'auto' : 'fixed' } window.ztools.dbStorage.setItem(META_KEY, JSON.stringify(next)) return next diff --git a/plugins/easytable/src/App.vue b/plugins/easytable/src/App.vue index 6685c578e..9c599db53 100644 --- a/plugins/easytable/src/App.vue +++ b/plugins/easytable/src/App.vue @@ -1,13 +1,23 @@