Spark Workbench 自訂欄位類型插件開發指南
本篇面向需要二次擴展 Spark Workbench 任務欄位類型 的開發者。目標是讓第三方插件可以像 Spark 預設插件一樣,獨立交付欄位類型定義、系統欄位定義、欄位配置邏輯、TQL 操作符,以及對應的前端 Vue 組件。
預設欄位插件位於:
spark-plugins/spark-workbench-default-plugin/
Spark 內建的 Text、Rich Text、Markdown、User、Attachment 等欄位類型,以及 Title、Assignee、Description 等系統欄位,都透過這個插件註冊。平台主工程只消費擴展點,不再把具體欄位類型硬編碼在任務設計器中。
1. 欄位插件模型
一個欄位類型插件通常包含兩部分:
- 後端擴展點:透過 Lattice Ability 返回欄位類型、系統欄位、欄位配置表單、支援的 TQL 操作符和篩選器協議。
- 前端組件:透過 Vue 組件渲染建立 / 編輯態控制項,也可以提供查看態和任務詳情態組件。
推薦結構:
my-task-field-plugin/
pom.xml
src/main/java/
.../MyTaskFieldPlugin.java
.../MyFieldRegistryExt.java
.../PriorityFieldTypeProvider.java
src/main/frontend/
package.json
vite.config.js
src/
index.js
PriorityField.vue
PriorityDetailField.vue
插件 jar 應同時包含 Java 擴展點和前端靜態資源,運行期由 Spark 自動載入。
2. 業務身份
插件必須宣告自己的 Lattice 業務身份:
@Business(code = "my.task.field.plugin", name = "My Task Field Plugin")
public class MyTaskFieldPlugin extends BusinessTemplate {
}
欄位註冊時,Spark 會遍歷目前 Lattice 中所有已註冊業務身份,並對每個業務身份呼叫欄位註冊 Ability。第三方插件不要把 @Realization 寫到平台內建 code 上,應使用自己插件的業務身份。
3. 後端註冊欄位類型
欄位註冊擴展點為 SparkEraTaskFieldRegistryExt,通常繼承空實作:
@Realization(codes = "my.task.field.plugin")
public class MyFieldRegistryExt extends BlankSparkEraTaskFieldRegistryExt {
private static final String FRONTEND_RESOURCE =
"/spark-gadgets/my-task-field-plugin/index.umd.js";
@Override
public List<SparkEraTaskCustomFieldTypeProvider> listCustomFieldTypeProviders(
SparkEraTaskFieldRegistryContext context) {
return List.of(new PriorityFieldTypeProvider(FRONTEND_RESOURCE));
}
}
欄位類型 Provider 範例:
public class PriorityFieldTypeProvider implements SparkEraTaskCustomFieldTypeProvider {
private final String frontendResource;
public PriorityFieldTypeProvider(String frontendResource) {
this.frontendResource = frontendResource;
}
@Override
public String getCode() {
return "priority_score";
}
@Override
public String getTitle() {
return "Priority Score";
}
@Override
public SparkEraTaskFieldCategory getCategory() {
return SparkEraTaskFieldCategory.BASIC;
}
@Override
public String getDescription() {
return "A numeric priority score with visual emphasis.";
}
@Override
public String getComponentType() {
return "priorityScore";
}
@Override
public String getTaskFieldType() {
return "number";
}
@Override
public String getPreviewIcon() {
return "gauge";
}
@Override
public boolean isSortable() {
return true;
}
@Override
public List<String> supportOperators() {
return List.of("eq", "neq", "gt", "ge", "lt", "le");
}
@Override
public String getFrontendResource() {
return frontendResource;
}
@Override
public String getEditComponent() {
return "my$field-priority-score";
}
@Override
public String getDetailComponent() {
return "my$task-detail-priority-score-field";
}
}
關鍵欄位說明:
code:欄位類型編碼,建議全域唯一。componentType:運行期控制項類型語意。taskFieldType:底層值類型,影響查詢、索引和展示。previewIcon:任務設計器欄位調色盤圖示。supportOperators:TQL 可用操作符,不應直接返回全集。frontendResource:插件前端 UMD 入口。editComponent:建立 / 編輯態 Vue 組件 key。readonlyComponent:一般只讀表單組件 key,可為空。detailComponent:任務詳情渲染協議組件 key。
4. 註冊系統欄位
系統欄位使用 SparkEraTaskSystemFieldProvider:
@Override
public List<SparkEraTaskSystemFieldProvider> listSystemFieldProviders(
SparkEraTaskFieldRegistryContext context) {
return List.of(new EscalationLevelSystemFieldProvider(FRONTEND_RESOURCE));
}
系統欄位 Provider 需要返回穩定 code、展示 label、欄位類型、是否可列表展示、是否可搜尋、是否可排序以及操作符。系統欄位同樣可以宣告 frontendResource 和組件 key。
5. 前端註冊組件
插件前端入口透過宿主暴露的 window.__SPARK_TASK_FIELDS__ 註冊組件:
import PriorityField from "./PriorityField.vue";
import PriorityDetailField from "./PriorityDetailField.vue";
const api = typeof window === "undefined" ? null : window.__SPARK_TASK_FIELDS__;
if (api?.registerFieldComponent) {
api.registerFieldComponent("my$field-priority-score", PriorityField);
}
if (api?.registerDetailFieldComponent) {
api.registerDetailFieldComponent("my$task-detail-priority-score-field", PriorityDetailField);
}
建立 / 編輯態組件接收的主要 props:
defineProps({
modelValue: [String, Number, Boolean, Array, Object],
field: Object,
definition: Object,
placeholder: String,
disabled: Boolean,
context: Object
});
組件透過 emit("update:modelValue", value) 寫回欄位值。context 中包含宿主提供的工具能力,例如 t、apiFetch、openUserSelect、chooseAttachmentFiles 等;第三方組件應優先複用宿主能力,而不是重複實作平台通用互動。
任務詳情態組件接入 Spark 渲染協議,接收 componentModel、renderContext 和 t:
<template>
<div class="priority-detail">
<span>{{ field.label }}</span>
<strong>{{ field.value }}</strong>
</div>
</template>
<script setup>
import { computed } from "vue";
const props = defineProps({
componentModel: Object
});
const field = computed(() => props.componentModel?.props?.field || {});
</script>
6. Vite 建構
欄位插件前端和 Gadget 插件一樣,使用 Vite library 模式,並把 Vue 設為 external:
import { resolve } from "node:path";
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
export default defineConfig({
plugins: [vue()],
build: {
outDir: resolve(__dirname, "../../../target/classes/META-INF/resources/spark-gadgets/my-task-field-plugin"),
lib: {
entry: resolve(__dirname, "src/index.js"),
name: "MyTaskFieldPlugin",
formats: ["umd"],
fileName: () => "index.umd.js",
cssFileName: "index"
},
rollupOptions: {
external: ["vue"],
output: {
globals: {
vue: "__SPARK_VUE__"
}
}
}
}
});
建構後,運行期資源 URL 為:
/spark-gadgets/my-task-field-plugin/index.umd.js
7. 開發檢查清單
- 欄位類型編碼、系統欄位編碼必須穩定。
@Realization(codes = "...")必須使用插件自己的業務身份。- 查詢操作符要和欄位值類型匹配,不能簡單返回全集。
- 可排序能力應由欄位類型明確宣告,富文本、附件、長文本通常不建議排序。
- 前端組件不要打包獨立 Vue 運行時。
- 組件樣式使用 Spark 主題變數,避免寫死單一主題顏色。
- 欄位 label、description、配置項文案應透過後端多語言機制返回。