構建介面

檢視

列表、表單、看板、日曆、甘特圖等 —— 每個物件介面是如何宣告的。

檢視是使用者檢視和編輯記錄的方式。檢視是宣告式後設資料 —— 與物件同一套 生命週期:宣告一次,隨包釋出,在任何地方渲染。

分兩類:

  • 列表檢視 —— 視覺化多條記錄(表格、看板、日曆 ……)
  • 表單檢視 —— 檢視 / 編輯單條記錄(simple、tabbed、wizard ……)

Schema 原始碼: packages/spec/src/ui/view.zod.ts。

最短的宣告

即使你一個檢視都不宣告,每個物件也會自動獲得一個預設的表格列表和一個 simple 表單。要覆蓋或擴充套件它們,就在一個 defineView 容器裡宣告該物件的 檢視,放在物件旁邊而不是物件內部:list 是預設列表,form 是預設表單, 每個檢視都通過 data 指定它的物件。

// src/ui/views/support_ticket.view.ts
import { defineView } from '@objectstack/spec';

const data = { provider: 'object' as const, object: 'support_ticket' };

export const SupportTicketViews = defineView({
  list: {
    type: 'grid',
    data,
    columns: ['subject', 'status', 'priority', 'assignee'],
  },
  form: {
    type: 'simple',
    data,
    sections: [
      { name: 'details', label: 'Details', fields: ['subject', 'description', 'priority', 'status', 'assignee'] },
    ],
  },
});

把容器註冊到技術棧上,緊挨著它所描述的物件:

// objectstack.config.ts
export default defineStack({
  // ...
  views: [SupportTicketViews],
});

對於有多個檢視的物件(例如同一批資料上既有看板又有日曆),在同一個 容器裡新增具名檢視:listViews 供檢視切換器使用,formViews 是按名稱 開啟的表單。每個列表檢視都保留一個頂層的 columns,無論它是什麼型別:

export const SupportTicketViews = defineView({
  list: { type: 'grid', data, columns: ['subject', 'status', 'priority', 'assignee'] },
  listViews: {
    by_status: {
      label: 'By status', type: 'kanban', data, columns: ['subject', 'priority'],
      kanban: { groupByField: 'status', columns: ['subject', 'priority'] },
    },
    schedule: {
      label: 'Schedule', type: 'calendar', data, columns: ['subject', 'due_at'],
      calendar: { startDateField: 'due_at', titleField: 'subject' },
    },
    by_owner: {
      label: 'By owner', type: 'grid', data, columns: ['assignee', 'subject', 'status', 'priority'],
      sort: [{ field: 'assignee', order: 'asc' }],
    },
  },
  formViews: {
    quick: { type: 'modal', data, sections: [{ name: 'quick', label: 'Quick edit', fields: ['status', 'priority', 'assignee'] }] },
    full: {
      type: 'tabbed', data,
      sections: [
        { name: 'details', label: 'Details', fields: ['subject', 'description'] },
        { name: 'triage', label: 'Triage', fields: ['status', 'priority', 'assignee'] },
      ],
    },
  },
});

這個容器及其規則在 ObjectStack 的 View metadata 中有完整說明。

列表檢視型別

每個列表檢視都需要一個頂層的 columns,無論它是什麼型別。某個型別的其餘 設定放在以該型別命名的配置塊中:

type渲染為配置塊及其必需的鍵常用的可選鍵
grid資料表格(預設)—
kanban分列的看板kanban: { groupByField, columns }summarizeField、titleField
gallery卡片牆gallery(無必需鍵)coverField、titleField
calendar月 / 周 / 日calendar: { startDateField }endDateField、titleField、colorField
timeline按時間排列的資訊流timeline: { startDateField, titleField }endDateField、groupByField
gantt專案時間線 + 依賴gantt: { startDateField, endDateField, titleField }progressField、dependenciesField
map地理位置標記map(無必需鍵)locationField,或 latitudeField + longitudeField
tree自引用的層級tree(無必需鍵)parentField、labelField
chart內嵌圖表chart: { dataset, values }chartType、dimensions

下面的片段都是單個列表檢視。每個都放在一個 defineView 容器中 —— 作為它 的 list,或作為 listViews 的一個條目 —— 並像上面那樣用 data 指定它的 物件。

常用列表選項

// One list view; the container and `data` are omitted (see above).
// P is the predicate template tag: import { P } from '@objectstack/spec'
{
  type: 'grid',
  columns: ['subject','status','priority','assignee','created_at'],

  filter: [ { field: 'archived', operator: 'equals', value: false } ],
  sort:   [ { field: 'created_at', order: 'desc' } ],
  pagination: { pageSize: 25 },
  searchableFields:   ['subject','description'],
  filterableFields:   ['status','priority','assignee'],

  navigation: { mode: 'drawer' },                       // 'page' | 'drawer' | 'modal' | 'split' | 'popover' | 'new_window' | 'none'
  selection:  { type: 'multiple' },                     // 'none' | 'single' | 'multiple'

  rowActions:  ['close_ticket','assign_to_me'],
  bulkActions: ['bulk_close','bulk_export'],

  conditionalFormatting: [
    { condition: P`record.priority == 'urgent'`, style: { background: '#fef2f2', fontWeight: '600' } }   // style values are strings
  ],

  exportOptions: ['csv','xlsx'],
  emptyState:   { title: 'No tickets yet', message: 'Create one to get started', icon: 'inbox' }
}

filter 和 sort 會編譯為 ObjectQL; rowActions 和 bulkActions 按名稱引用動作。

看板

// One list view; the container and `data` are omitted (see above).
{
  type: 'kanban',
  columns: ['subject', 'priority', 'assignee'],  // still required at the top level
  kanban: {
    groupByField:   'status',                // discrete field — usually a select
    summarizeField: 'amount',                // optional total per column
    columns: ['subject', 'priority', 'assignee']  // fields shown on each card
  }
}

看板的列來自 groupByField 的取值;列的順序和顏色取自該欄位的 select 選項定義。

在列之間拖放會發出一次更新,設定被分組的欄位 —— 與手動編輯適用同樣的許可權 規則。

日曆

// One list view; the container and `data` are omitted (see above).
{
  type: 'calendar',
  columns: ['subject', 'start_at', 'end_at', 'priority'],
  calendar: {
    startDateField: 'start_at',
    endDateField:   'end_at',                // optional — single-point if omitted
    titleField:     'subject',
    colorField:     'priority'               // optional — colours events by value
  }
}

甘特圖

// One list view; the container and `data` are omitted (see above).
{
  type: 'gantt',
  columns: ['name', 'start_at', 'due_at', 'percent_complete'],
  gantt: {
    startDateField:    'start_at',
    endDateField:      'due_at',
    titleField:        'name',
    progressField:     'percent_complete',   // optional, drives the progress bar
    dependenciesField: 'depends_on'          // optional — multiselect lookup to same object
  }
}

樹

// One list view; the container and `data` are omitted (see above).
{
  type: 'tree',
  columns: ['name'],
  tree: {
    parentField: 'parent_id',                // self-lookup that builds the hierarchy
    labelField:  'name',                     // shown indented in the first column
    defaultExpandedDepth: 1                   // optional — 0 = roots only, omit = expand all
  }
}

對於自引用的物件(分類、BOM、組織單元、巢狀評論),樹形表格會按 parentField 指標把行巢狀起來。

圖表(內嵌)

chart 列表檢視繫結一個具名資料集 —— 語義分析層(ADR-0021)—— 並 按名稱選擇它的維度和度量。指標定義一次,繫結到同一個資料集的每個圖表、 儀表盤和報表就保持一致:

// One list view; the container and `data` are omitted (see above).
{
  type: 'chart',
  columns: ['status'],
  chart: {
    chartType:  'bar',                       // 'bar' | 'line' | 'pie' | 'area' | 'scatter'
    dataset:    'tickets_by_status',         // a dataset declared with defineDataset(...)
    dimensions: ['status'],                  // dataset dimension name(s) — X / group / split
    values:     ['ticket_count']             // dataset measure name(s) — the value axis
  }
}

內聯的 xAxisField / yAxisFields / aggregation / groupByField 形態已在 9.0 中移除 —— 現在每個圖表、儀表盤元件和報表都繫結一個數據集。更豐富的 跨物件分析請使用專門的報表介面。

表單檢視型別

type佈局
simple單列或分割槽塊(預設)
tabbed標籤頁式區塊
wizard分步流程
split主從雙欄
drawer側邊面板表單
modal對話方塊表單
// One form view; the container and `data` are omitted (see above).
{
  type: 'tabbed',
  sections: [
    { label: 'Overview', fields: ['subject','status','priority','assignee'] },
    { label: 'Customer', fields: ['customer','contact','email','phone'] },
    { label: 'Resolution', fields: ['resolution_notes','resolved_at'] }
  ],
  submitBehavior: { kind: 'next-record' }   // 'thank-you' | 'redirect' | 'continue' | 'next-record'
}

公開表單

表單可以設為允許匿名訪問:

// One form view; the container and `data` are omitted (see above).
{
  type: 'simple',
  sections: [ { fields: ['name','email','message'] } ],
  sharing: {
    enabled: true,
    publicLink: 'contact-us',                // slug under /forms/
    allowAnonymous: true
  },
  submitBehavior: { kind: 'thank-you' }
}

這會自動暴露 GET /api/v1/forms/contact-us 和 POST /api/v1/forms/contact-us/submit —— 不需要認證的公開表單路由。參見 REST API → 公開表單。

可見性、ARIA、主題

  • visibleOn: P\...`` —— 用在表單的區塊和欄位上,根據使用者 / 記錄 / 環境隱藏它們(CEL 謂詞)
  • aria: { label, description, ... } —— 供螢幕閱讀器使用的 ARIA 屬性 (列表檢視和表單檢視)
  • appearance: { showDescription, allowedVisualizations: [...] } —— 用在列表 檢視上,限制終端使用者可以切換到哪些列表型別

用對話構建

你通常不需要手寫檢視後設資料。告訴 AI Builder:

"為 support_ticket 物件新增一個按狀態分組的看板檢視,卡片顯示主題和 優先順序。按狀態把列標成紅 / 黃 / 綠。"

它會呼叫後設資料工具、把 diff 排入佇列,你批准之後檢視就會出現。參見 AI Builder。

另請參閱