
TagStudio Fields 字段系统模板化扩展元数据的原理与实操【免费下载链接】TagStudioA User-Focused Photo File Management System项目地址: https://gitcode.com/GitHub_Trending/ta/TagStudioTagStudio 的 Fields字段是附加在文件条目Entry上的结构化扩展信息与标签Tags并列用于保存标题、作者、日期、注释、备注这类不适合做成标签的元数据。TagStudio 将字段拆分为字段模板Field Template与字段实例Field两层模板只预配置类型、名称和选项不携带数据把模板添加给条目时这些信息被复制到条目自己的字段记录中之后在条目上编辑字段内容不会反向修改模板。本文基于官方文档 docs/fields.md 逐节展开并结合 src/tagstudio/core/library/alchemy/fields.py、src/tagstudio/qt/controllers/inspector.py 等源码讲清字段系统的界面操作、数据模型、模板生命周期与底层 SQL 写入链路。一、字段与标签的定位差异官方文档将字段定义为可以添加到文件条目的额外信息片段类似于标签的添加方式原文见 docs/fields.md。两者的分工是标签Tags面向检索、归类、层级管理的信息例如题材、作者名、系列名字段Fields面向记录具体取值的信息例如标题是什么拍摄于哪天备注写了什么。与标签的关键区别在于模板机制。文档明确指出Unlike tags, fields are based on templates that contain pre-filled information such as the field type and title, and that information iscopiedto fields when adding them to entries. Editing field information on entries does not modify the template it was created from.复制而非引用这一点在源码中体现得非常清晰。src/tagstudio/core/library/alchemy/fields.py 中模板类与实例类是两套独立的 SQLAlchemy 模型模板通过to_field()方法物化出一个字段对象class TextFieldTemplate(BaseFieldTemplate): __tablename__ text_field_templates is_multiline: Mapped[bool] mapped_column(nullableFalse, defaultFalse) def to_field(self, value: str | None None) - TextField: return TextField(nameself.name, valuevalue, is_multilineself.is_multiline)to_field()返回的是一个全新的TextField对象——模板的id不会被带入value默认为空。字段实例随后绑定到具体条目entry_id外键写入text_fields/datetime_fields表。因此模板是蓝图字段是实例二者在数据库层面就是分开的表、分开的行编辑字段值永远不会触碰模板行。二、给条目添加字段的操作路径2.1 检查器Inspector中的 Add Field 按钮文档给出的主流程是点击检查器底部的Add Field按钮 → 在弹出面板中搜索/选择一个字段模板或直接通过搜索栏现建一个 → 选择后模板立即被添加给当前选中的条目支持多选批量。这条交互在 src/tagstudio/qt/controllers/inspector.py 中可以完整对应self.layout().add_field_button.clicked.connect( lambda: self._set_item_mode(_ItemMode.FIELD) ) # ... self.layout().field_search_box.item_chosen.connect(self._add_field_to_selected) def _add_field_to_selected(self, template: BaseFieldTemplate) - None: self.layout().containers.add_field_to_selected(template)按钮点击后检查器切换到字段模式并聚焦搜索框快捷键路径同样指向add_field_buttonFieldTemplateSearchPanel发出item_chosen信号后进入 src/tagstudio/qt/mixed/field_containers.py 的add_field_to_selected()for entry_id in self.driver.selected: for field_template in field_templates: self.lib.add_field_to_entries(entry_id, field_template.to_field())可以看到遍历当前驱动层选中的条目而非字段容器缓存对每个条目调用模板的to_field()生成字段实例再交给 Library 入库——这正是文档所说从模板选择后填写信息的落地过程。2.2 Library 层的入库实现Library.add_field_to_entries() 负责把字段实例写入数据库def add_field_to_entries(self, entry_ids: list[int] | int, field: BaseField) - bool: if isinstance(entry_ids, int): entry_ids [entry_ids] with Session(self.engine) as session: for entry_id in entry_ids: try: session.add(field.clone_with_entry_id(entry_id)) session.commit() except IntegrityError as e: logger.error(e) session.rollback() return False return True注意它调用的是field.clone_with_entry_id(entry_id)字段模型在 fields.py 中声明了该克隆接口TextField.clone_with_entry_id()会保留name / value / is_multiline仅替换entry_id并构造新行。也就是说同一个模板对 N 个条目生效时会落库 N 条互相独立的字段记录——这与复制语义完全一致也意味着删除某个条目上的字段不影响其他条目更不影响模板本身。2.3 菜单入口Edit - Manage Field Templates文档还提供第二条路径Alternatively you can create new field templates fromEdit - Manage Field Templates.。该菜单动作定义于 src/tagstudio/qt/controllers/main_window.py# Manage Field Templates self.field_template_manager_action QAction( Translations[menu.edit.manage_field_templates], self ) self.field_template_manager_action.setEnabled(False) self.edit_menu.addAction(self.field_template_manager_action)它挂靠在 Edit 菜单下且初始setEnabled(False)——从源码结构看只有在打开库有可用 Library后才会启用这与标签管理器菜单项的启用策略一致。三、字段模板Field Templates查看、创建、删除3.1 模板管理器的能力边界文档对模板管理窗口的描述是Field templates can be viewed, created, and deleted fromEdit - Manage Field Templates. You can also edit field templates from the Add Field menu, and create new ones on the fly from the search bar. Note that you can not currently delete field templates from the Add Field menu, just like tags.这句不能从 Add Field 菜单删除的限制有明确的代码依据。src/tagstudio/qt/controllers/field_template_search_panel.py 中搜索面板通过is_chooser参数区分两种场景override def _on_item_remove(self, item: BaseFieldTemplate) - None: if self._is_chooser: return # “Add Field”场景chooser直接禁删 message_box QMessageBox( QMessageBox.Icon.Question, Translations[field_template.delete], Translations.format(field_template.confirm_delete, field_template_nameitem.name), QMessageBox.StandardButton.Ok | QMessageBox.StandardButton.Cancel, ) ... self.__lib.remove_field_template(item)以及set_item_widget()中的field_template_widget.has_remove not self._is_chooserAdd Field 场景以 chooser 模式构造面板删除按钮不会显示、删除回调直接短路返回只有完整的管理器窗口is_chooserFalse才会渲染删除按钮并弹出二次确认。确认删除后调用 Library.remove_field_template()按类型TextFieldTemplate/DatetimeFieldTemplate定位对应表并按id删除该行。3.2 搜索栏顺手创建模板文档提到可以从搜索栏直接创建新模板on the fly。FieldTemplateSearchPanel.on_item_create() 的实现细节值得注意当前搜索框里的查询词会被自动填入新模板的名称输入框query: str self.get_search_query() panel: EditFieldTemplateModal EditFieldTemplateModal() modal: Modal Modal(panel, ..., is_savableTrue) if query.strip(): panel.name_field.setText(query) modal.saved.connect(lambda: self.create_item(panel, choose_itemadd_to_entry))如果勾选了创建并添加到条目add_to_entryTrue保存后模板会立即_on_item_chosen()即等价于走一遍第二节的add_field_to_selected流程——新建模板并批量挂到选中条目一步完成。3.3 模板编辑弹窗的字段类型切换模板的编辑/新建共用 EditFieldTemplateModal。其field_type_map目前只注册了两个类型field_type_map: dict[str, str] { TextFieldTemplate: Translations[field_type.text], DatetimeFieldTemplate: Translations[field_type.datetime], }名称必填__on_name_changed()在名称为空时禁用保存按钮并以高亮样式提示防止空名模板入库类型联动__on_type_changed()在切换到 Text 类型时显示多行复选框区域_text_field_attributes_widget切到 Datetime 时隐藏——源码注释明确写了 Future options specific to other type will go here说明该弹窗是为后续扩展更多字段类型预留了插槽构建产物build_field_template()根据选中类型返回TextFieldTemplate(name..., is_multiline...)或DatetimeFieldTemplate(name...)未知类型回退为TextFieldTemplate并记 warning 日志。3.4 模板更新的跨类型迁移逻辑模板的更新入口 Library.update_field_template() 处理了一个容易被忽视的边界情况——模板改类型。由于 Text 与 Datetime 模板分存两张表各自有is_multiline等专属列更新时若新旧类型相同直接UPDATE原行name、is_multiline若类型发生变化删除旧表中的行清空id源码注释 The id should not transfer between tables再向新表插入新行。这个删旧插新策略保证了模板 ID 不在跨表间复用避免外键语义混乱。模板搜索则由 Library.search_field_templates() 实现对两张模板表分别做name.icontains(query)的忽略大小写匹配合并后按前缀匹配优先、名称长度次之的规则排序返回前limit条并expunge make_transient脱离 session——这正是 Add Field 搜索框输入时的候选列表来源。四、字段类型Field Types文档说明字段有多种类型Single lines are good for fields like titles, while multiline blocks are good for things like comments and notes。当前版本提供 Text 与 Datetime 两种下面分别结合 schema 与渲染逻辑说明。4.1 Text 文本字段OptionValueDescriptionMultilineTrue/FalseIndicates if the text should be displayed on multiple lines or just one.对应的存储模型fields.pyclass TextField(BaseField): __tablename__ text_fields value: Mapped[str | None] mapped_column(sort_order4) is_multiline: Mapped[bool] mapped_column(nullableFalse, defaultFalse)value可空新加的字段初始无值is_multiline不可空且默认False。模板侧TextFieldTemplate结构同构。文本字段的编辑走模态框EditText保存回调 update_text_field_callback() 将name / value / is_multiline三要素交给 Library.update_text_field()后者用一条UPDATE text_fields SET name..., value..., is_multiline... WHERE id:fid AND entry_id IN (...)批量刷新所选条目。渲染侧有两个源码可考的细节换行符归一化write_text_container() 显示前执行text (field.value or ).replace(\r, \n)把 CRLF/CR 统一为 LF避免跨平台来源的文本显示错行链接自动识别显示组件 TextContainerWidget 使用正则linkify()把纯文本中的 http/ftp/https URL 改写为可点击的 Markdown 链接并设置TextBrowserInteraction因此URL类文本字段在检查器中天然可跳转。4.2 Datetime 日期时间字段文档说明Datetime 字段包含日期与时间值Dates are formatted using the format specified in your application settings.存储模型fields.pyclass DatetimeField(BaseField): __tablename__ datetime_fields value: Mapped[str | None] mapped_column(sort_order4)按应用设置格式化的机制在 src/tagstudio/qt/app_settings.pyproperty def datetime_format(self) - str: ... def format_datetime(self, dt: datetime) - str: return datetime.strftime(dt, self.datetime_format)检查器渲染时write_datetime_container()先DatetimePicker.string2dt(field.value)解析存储的 ISO 字符串再套settings.format_datetime()输出若解析失败ValueError/AssertionError则原样显示存储值保证脏数据不崩 UI。编辑入口是DatetimePicker弹窗带日期选择器即下图所示的展开状态保存后回调update_datetime_field_callback()→ Library.update_datetime_field()同样按field.id entry_id IN (...)批量更新。4.3 字段的通用能力编辑、删除、复制、混合态无论何种类型条目检查器中的每个字段都由 FieldContainer 承载标题显示为字段名 (类型)如 Comments (Text)类型名经由 translations.py 中的FIELD_TYPE_KEYS映射到国际化键field_type.text/field_type.datetime未知类型显示field_type.unknown鼠标悬停时浮现 复制 / 编辑 / 删除 三个操作按钮。删除会弹出确认框remove_message_box()提示文案来自field.confirm_remove确认后调用 Library.remove_entry_field() 按字段 ID 从数据库删除该行。另一个值得了解的细节是多条目混合态同时选中多个条目时若某字段并非所有条目都有容器会显示斜体 Mixed Datafield.mixed_data文案且不挂编辑回调只有全部条目共享该字段时才允许直接编辑——这防止了批量编辑只改了部分条目却无提示的坑。五、Legacy 字段映射与迁移兼容性fields.py 末尾维护了一张LEGACY_FIELD_MAP注释说明了它的用途# Used for migrating legacy libraries. # Legacy JSON libraries (v9.4) use an integer ID. # SQLite libraries 6 until 200 use a slugfield name (e.g. DATE_CREATED). LEGACY_FIELD_MAP { 0: {type: TextField, name: Title, is_multiline: False}, 1: {type: TextField, name: Author, is_multiline: False}, ... 10: {type: DatetimeField, name: Date, is_multiline: False}, 11: {type: DatetimeField, name: Date Created}, ... }从这张映射可以确认三条事实旧版 JSON 库 v9.4用整数 ID标识字段SQLite 6200 版本用斜杠式字段名如DATE_CREATED迁移时这些旧标识会被归一化为当前的TextField/DatetimeField实例is_multiline也按历史语义补齐如Description、Notes、Comments为多行内置的默认字段模板集合Title、Author、Artist、URL、Description、Notes、Date 系列等即来源于此映射——这就是文档所说 TagStudio includes a handful of field templates to start you off with 的具体内容你可以自由修改、删除或自建。相关测试可参考 tests/core/library/test_json_migration.pyJSON 库迁移与 tests/core/library/test_migrations.py。六、要点小结与延伸阅读操作面Add Field 按钮检查器与 Edit - Manage Field Templates菜单两条路径模板可搜可建仅管理器窗口可删搜索词会自动预填为新模板名称数据面模板表text_field_templates/datetime_field_templates与字段表text_fields/datetime_fields分离模板经to_field()复制为字段经clone_with_entry_id()逐条目落库编辑字段永不回写模板类型面Textis_multiline可配置显示端归一化换行并自动 linkify与 Datetime按应用设置的datetime_format格式化解析失败降级为原值显示边界面跨类型修改模板走删旧插新多选条目的字段编辑有 Mixed Data 混合态保护旧库字段经LEGACY_FIELD_MAP迁移归一化。延伸阅读条目概念见 docs/entries.md标签体系见 docs/tags.md字段数据模型核心文件为 src/tagstudio/core/library/alchemy/fields.pyUI 交互层核心为 src/tagstudio/qt/mixed/field_containers.py 与 src/tagstudio/qt/controllers/field_template_search_panel.py。【免费下载链接】TagStudioA User-Focused Photo File Management System项目地址: https://gitcode.com/GitHub_Trending/ta/TagStudio创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考