@oneflowui/ui 0.8.4 → 0.8.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +219 -0
- package/README.md +219 -0
- package/dist/components/ai/AiMessageList.vue.js +1 -1
- package/dist/components/ai/AiMessageList.vue2.js +28 -27
- package/dist/components/common/ThemeScope.vue.d.ts +24 -0
- package/dist/components/common/ThemeScope.vue.js +24 -0
- package/dist/components/common/ThemeScope.vue2.js +4 -0
- package/dist/components/common/index.d.ts +1 -0
- package/dist/components/database/DatabaseView.vue.d.ts +4 -4
- package/dist/components/database/DatabaseView.vue.js +4 -4
- package/dist/components/database/DatabaseView.vue2.js +135 -134
- package/dist/components/database/index.d.ts +1 -1
- package/dist/components/kanban/KanbanColumn.vue.js +3 -3
- package/dist/components/kanban/KanbanColumn.vue2.js +31 -30
- package/dist/components/table/DataTable.vue.js +2 -2
- package/dist/components/table/DataTable.vue2.js +251 -249
- package/dist/composables/index.d.ts +5 -3
- package/dist/composables/useDataTableLayout.d.ts +1 -0
- package/dist/composables/useDataTableLayout.js +29 -26
- package/dist/composables/useDatabaseView.d.ts +1 -1
- package/dist/composables/useDatabaseView.js +311 -284
- package/dist/composables/useDatabaseViewMiddleware.d.ts +33 -0
- package/dist/composables/useDatabaseViewMiddleware.js +131 -0
- package/dist/composables/useVirtualList.d.ts +11 -0
- package/dist/composables/useVirtualList.js +146 -104
- package/dist/composables.js +71 -0
- package/dist/contracts/database.d.ts +23 -0
- package/dist/index.d.ts +6 -3
- package/dist/index.js +259 -250
- package/dist/style.css +1 -1
- package/dist/theme.d.ts +1 -0
- package/dist/theme.js +4 -0
- package/package.json +16 -2
package/README.en.md
CHANGED
|
@@ -110,6 +110,19 @@ import '@oneflowui/ui/styles'
|
|
|
110
110
|
|
|
111
111
|
Note: starting from `0.5.4`, the plugin entry is separated from the root entry. Use `@oneflowui/ui/plugin` for `app.use(...)`, and keep named imports on `@oneflowui/ui`.
|
|
112
112
|
|
|
113
|
+
If you want a stricter import boundary, stable subpath exports are also available:
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
import { useVirtualListStateCache } from '@oneflowui/ui/composables'
|
|
117
|
+
import type { DataRecord } from '@oneflowui/ui/types'
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Recommended convention:
|
|
121
|
+
- Import components and common capabilities from the root `@oneflowui/ui` entry.
|
|
122
|
+
- Import composables and pure types from `@oneflowui/ui/composables` and `@oneflowui/ui/types` when you want a clearer dependency boundary.
|
|
123
|
+
- If you only want tokens and theme layers without plugin registration, use `@oneflowui/ui/theme`.
|
|
124
|
+
- Keep `@oneflowui/ui/styles` as the full legacy-compatible style entry.
|
|
125
|
+
|
|
113
126
|
### Theme Layers
|
|
114
127
|
|
|
115
128
|
OneUI now ships with a neutral default theme and an optional product skin without changing component logic.
|
|
@@ -127,8 +140,99 @@ document.documentElement.dataset.ofTheme = 'neutral'
|
|
|
127
140
|
document.documentElement.dataset.ofTheme = 'ops-console'
|
|
128
141
|
```
|
|
129
142
|
|
|
143
|
+
If you want to decouple “style injection” from “plugin registration”, prefer the dedicated theme entry:
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
import '@oneflowui/ui/theme'
|
|
147
|
+
```
|
|
148
|
+
|
|
130
149
|
This structure is meant to keep the component layer reusable while letting product-specific styling live above it.
|
|
131
150
|
|
|
151
|
+
### ThemeScope Wrapper
|
|
152
|
+
|
|
153
|
+
When a single page needs two visual contexts at once, prefer the `ThemeScope` component. It automatically writes `data-of-theme` and `data-of-theme-scope` on the wrapper, so consumers do not need to hand-write attributes.
|
|
154
|
+
|
|
155
|
+
```vue
|
|
156
|
+
<script setup lang="ts">
|
|
157
|
+
import { ThemeScope } from '@oneflowui/ui'
|
|
158
|
+
</script>
|
|
159
|
+
|
|
160
|
+
<template>
|
|
161
|
+
<ThemeScope theme="ops-console" tag="section" class="ops-preview">
|
|
162
|
+
<div class="ops-preview__panel">
|
|
163
|
+
<h3>Scoped ops-console preview</h3>
|
|
164
|
+
<p>This subtree inherits ops-console tokens while the outer page stays neutral.</p>
|
|
165
|
+
</div>
|
|
166
|
+
</ThemeScope>
|
|
167
|
+
</template>
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Supported values currently match the global theme list:
|
|
171
|
+
|
|
172
|
+
- `neutral`
|
|
173
|
+
- `ops-console`
|
|
174
|
+
|
|
175
|
+
Direct `data-of-theme-scope` usage is still compatible for older code, but new consumers should use `ThemeScope`.
|
|
176
|
+
|
|
177
|
+
### Component-Level Example
|
|
178
|
+
|
|
179
|
+
A more realistic consumer pattern is to wrap an actual component subtree. The page keeps its global theme, while the local region switches to tokens that fit an ops or command-console context.
|
|
180
|
+
|
|
181
|
+
```vue
|
|
182
|
+
<script setup lang="ts">
|
|
183
|
+
import { DataTable, StatisticCard, ThemeScope } from '@oneflowui/ui'
|
|
184
|
+
</script>
|
|
185
|
+
|
|
186
|
+
<template>
|
|
187
|
+
<ThemeScope theme="ops-console" tag="section" class="task-surface">
|
|
188
|
+
<header class="task-surface__header">
|
|
189
|
+
<h3>Task Overview</h3>
|
|
190
|
+
<p>Only this region uses ops-console tokens.</p>
|
|
191
|
+
</header>
|
|
192
|
+
|
|
193
|
+
<div class="task-surface__metrics">
|
|
194
|
+
<StatisticCard icon="check-circle" :value="18" label="Done" />
|
|
195
|
+
<StatisticCard icon="clock" :value="6" label="In progress" />
|
|
196
|
+
</div>
|
|
197
|
+
|
|
198
|
+
<DataTable :rows="rows" :columns="columns" />
|
|
199
|
+
</ThemeScope>
|
|
200
|
+
</template>
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
This is non-breaking: no component API changes are required, and `ThemeScope` only adds a local token boundary on the wrapper.
|
|
204
|
+
|
|
205
|
+
### Virtual List State Cache
|
|
206
|
+
|
|
207
|
+
If a virtual list remounts often and you want to keep `scrollTop`, `containerHeight`, and `invalidateVersion`, reuse the same state by key.
|
|
208
|
+
`createVirtualListState()` is still available, while `useVirtualListStateCache()` is the helper for sharing one state across remounts.
|
|
209
|
+
|
|
210
|
+
```ts
|
|
211
|
+
import { useVirtualList, useVirtualListStateCache } from '@oneflowui/ui'
|
|
212
|
+
|
|
213
|
+
const virtualListState = useVirtualListStateCache('ai-message-list')
|
|
214
|
+
|
|
215
|
+
const { visibleItems, totalHeight, offsetY } = useVirtualList({
|
|
216
|
+
items: messages,
|
|
217
|
+
itemHeight: 60,
|
|
218
|
+
containerRef,
|
|
219
|
+
state: virtualListState,
|
|
220
|
+
})
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
## Release & Verification
|
|
224
|
+
|
|
225
|
+
If you need to review the current traceable release materials, start with these docs:
|
|
226
|
+
|
|
227
|
+
- Current evidence index: [`docs/plans/2026-03-22-release-0.8.5-evidence-index.md`](docs/plans/2026-03-22-release-0.8.5-evidence-index.md)
|
|
228
|
+
- Release proof: [`docs/plans/2026-03-22-release-0.8.5-proof.md`](docs/plans/2026-03-22-release-0.8.5-proof.md)
|
|
229
|
+
- Verification result: [`docs/plans/2026-03-22-release-0.8.5-verification.md`](docs/plans/2026-03-22-release-0.8.5-verification.md)
|
|
230
|
+
- Pre-release smoke: [`docs/plans/2026-03-22-oneui-theme-scope-middleware-composer-pre-release-verification.md`](docs/plans/2026-03-22-oneui-theme-scope-middleware-composer-pre-release-verification.md)
|
|
231
|
+
- Package entrypoints verification: [`docs/plans/2026-03-22-oneui-package-entrypoints-verification.md`](docs/plans/2026-03-22-oneui-package-entrypoints-verification.md)
|
|
232
|
+
- Changelog: [`docs/CHANGELOG-v0.8.5.md`](docs/CHANGELOG-v0.8.5.md)
|
|
233
|
+
|
|
234
|
+
The current public npm version will move to `0.8.5` after this release step, and publish, pack, dry-run, and dual-host smoke checks all have separate evidence.
|
|
235
|
+
|
|
132
236
|
---
|
|
133
237
|
|
|
134
238
|
## Usage Examples
|
|
@@ -204,6 +308,121 @@ toast.success('Saved successfully')
|
|
|
204
308
|
toast.error('Operation failed')
|
|
205
309
|
```
|
|
206
310
|
|
|
311
|
+
### DatabaseView middleware presets
|
|
312
|
+
|
|
313
|
+
You can compose reusable `DatabaseView` middleware presets for toast, analytics, and optimistic updates:
|
|
314
|
+
|
|
315
|
+
```ts
|
|
316
|
+
import {
|
|
317
|
+
composeDatabaseViewMiddlewares,
|
|
318
|
+
createDatabaseViewAnalyticsMiddleware,
|
|
319
|
+
createDatabaseViewOptimisticMiddleware,
|
|
320
|
+
createDatabaseViewToastMiddleware,
|
|
321
|
+
useDatabaseView,
|
|
322
|
+
} from '@oneflowui/ui'
|
|
323
|
+
|
|
324
|
+
const middleware = composeDatabaseViewMiddlewares(
|
|
325
|
+
createDatabaseViewToastMiddleware({
|
|
326
|
+
onSuccess: (message) => toast.success(message),
|
|
327
|
+
onError: (message) => toast.error(message),
|
|
328
|
+
}),
|
|
329
|
+
createDatabaseViewAnalyticsMiddleware({
|
|
330
|
+
onEvent: (event) => console.log('[db-view]', event.phase, event.action),
|
|
331
|
+
}),
|
|
332
|
+
createDatabaseViewOptimisticMiddleware({
|
|
333
|
+
apply: ({ payload }) => updateLocalRecord(payload),
|
|
334
|
+
revert: ({ payload }) => revertLocalRecord(payload),
|
|
335
|
+
}),
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
const view = useDatabaseView({
|
|
339
|
+
tableId: 'tbl-1',
|
|
340
|
+
actions: {
|
|
341
|
+
middleware,
|
|
342
|
+
onCellEdit: saveCellEdit,
|
|
343
|
+
},
|
|
344
|
+
})
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
### Dev Examples / Enterprise Demo
|
|
348
|
+
|
|
349
|
+
`DatabaseEnterpriseDemo` is a dev/examples-level consumption pattern used to show a more complete enterprise-style page composition. It is documentation and development sample material, not an npm-exported component, and it does not change the public export surface of `@oneflowui/ui`.
|
|
350
|
+
|
|
351
|
+
If your business app needs something similar, copy the composition idea from the example and wire it to your own data source and action contract instead of depending on a separate production export.
|
|
352
|
+
|
|
353
|
+
### Combined Consumption Example
|
|
354
|
+
|
|
355
|
+
If you want to use theme scoping, database action middleware, and virtual list state caching in the same business surface, keep them inside one wrapper component. This is the closest pattern to a real app page and the easiest one to copy.
|
|
356
|
+
|
|
357
|
+
```vue
|
|
358
|
+
<script setup lang="ts">
|
|
359
|
+
import { ref } from 'vue'
|
|
360
|
+
import {
|
|
361
|
+
ThemeScope,
|
|
362
|
+
composeDatabaseViewMiddlewares,
|
|
363
|
+
createDatabaseViewAnalyticsMiddleware,
|
|
364
|
+
createDatabaseViewOptimisticMiddleware,
|
|
365
|
+
createDatabaseViewToastMiddleware,
|
|
366
|
+
useDatabaseView,
|
|
367
|
+
useVirtualList,
|
|
368
|
+
useVirtualListStateCache,
|
|
369
|
+
} from '@oneflowui/ui'
|
|
370
|
+
|
|
371
|
+
const containerRef = ref<HTMLElement | null>(null)
|
|
372
|
+
const virtualListState = useVirtualListStateCache('task-feed')
|
|
373
|
+
|
|
374
|
+
const middleware = composeDatabaseViewMiddlewares(
|
|
375
|
+
createDatabaseViewToastMiddleware({
|
|
376
|
+
onSuccess: (message) => toast.success(message),
|
|
377
|
+
onError: (message) => toast.error(message),
|
|
378
|
+
}),
|
|
379
|
+
createDatabaseViewAnalyticsMiddleware({
|
|
380
|
+
onEvent: (event) => console.log('[db-view]', event.phase, event.action),
|
|
381
|
+
}),
|
|
382
|
+
createDatabaseViewOptimisticMiddleware({
|
|
383
|
+
apply: ({ payload }) => updateLocalRecord(payload),
|
|
384
|
+
revert: ({ payload }) => revertLocalRecord(payload),
|
|
385
|
+
}),
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
const view = useDatabaseView({
|
|
389
|
+
tableId: 'tbl-1',
|
|
390
|
+
actions: {
|
|
391
|
+
middleware,
|
|
392
|
+
onCellEdit: saveCellEdit,
|
|
393
|
+
},
|
|
394
|
+
})
|
|
395
|
+
|
|
396
|
+
const { visibleItems } = useVirtualList({
|
|
397
|
+
items: view.records,
|
|
398
|
+
itemHeight: 60,
|
|
399
|
+
containerRef,
|
|
400
|
+
state: virtualListState,
|
|
401
|
+
})
|
|
402
|
+
|
|
403
|
+
const columns = [
|
|
404
|
+
{ key: 'title', label: 'Task', width: 'fill' },
|
|
405
|
+
{ key: 'status', label: 'Status', width: 120 },
|
|
406
|
+
{ key: 'priority', label: 'Priority', width: 100 },
|
|
407
|
+
]
|
|
408
|
+
</script>
|
|
409
|
+
|
|
410
|
+
<template>
|
|
411
|
+
<ThemeScope theme="ops-console" tag="section" class="task-surface">
|
|
412
|
+
<header class="task-surface__header">
|
|
413
|
+
<h3>Task Overview</h3>
|
|
414
|
+
<p>The outer shell uses scoped ops-console tokens while actions, list rendering, and cached state share the same component entry points.</p>
|
|
415
|
+
</header>
|
|
416
|
+
|
|
417
|
+
<div ref="containerRef" class="task-surface__list">
|
|
418
|
+
<DataTable :rows="visibleItems" :columns="columns" />
|
|
419
|
+
</div>
|
|
420
|
+
</ThemeScope>
|
|
421
|
+
</template>
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
These capabilities can be used independently or combined like above; all of them are non-breaking additions and do not require consumer API changes.
|
|
425
|
+
|
|
207
426
|
---
|
|
208
427
|
|
|
209
428
|
## Local Development
|
package/README.md
CHANGED
|
@@ -88,6 +88,19 @@ app.use(OneflowUI)
|
|
|
88
88
|
app.mount('#app')
|
|
89
89
|
```
|
|
90
90
|
|
|
91
|
+
如果需要更细粒度的导入边界,可以直接使用稳定子路径入口:
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
import { useVirtualListStateCache } from '@oneflowui/ui/composables'
|
|
95
|
+
import type { DataRecord } from '@oneflowui/ui/types'
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
推荐约定:
|
|
99
|
+
- 组件与公共能力优先从 `@oneflowui/ui` 根入口导入。
|
|
100
|
+
- composables 和纯类型如果希望导入意图更清晰,可分别从 `@oneflowui/ui/composables` 与 `@oneflowui/ui/types` 导入。
|
|
101
|
+
- 如果只想注入 token 和主题层,不想顺带注册插件,可改用 `@oneflowui/ui/theme`。
|
|
102
|
+
- 样式全量入口仍保留 `@oneflowui/ui/styles`,兼容现有消费方式。
|
|
103
|
+
|
|
91
104
|
### 按需引入
|
|
92
105
|
|
|
93
106
|
```ts
|
|
@@ -114,12 +127,103 @@ document.documentElement.dataset.ofTheme = 'neutral'
|
|
|
114
127
|
document.documentElement.dataset.ofTheme = 'ops-console'
|
|
115
128
|
```
|
|
116
129
|
|
|
130
|
+
如果你希望把“样式注入”和“组件插件注册”拆开,推荐改用更明确的主题入口:
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
import '@oneflowui/ui/theme'
|
|
134
|
+
```
|
|
135
|
+
|
|
117
136
|
这套结构的目标是:
|
|
118
137
|
|
|
119
138
|
1. 组件默认保持中性、可复用
|
|
120
139
|
2. 业务系统通过主题皮肤注入品牌感或中控台气质
|
|
121
140
|
3. 后续可继续扩展更多主题,而不需要修改组件 API
|
|
122
141
|
|
|
142
|
+
### ThemeScope 包装组件
|
|
143
|
+
|
|
144
|
+
如果同一页面里需要并存两种视觉语境,优先使用 `ThemeScope`。这个组件会自动给 wrapper 注入 `data-of-theme` 和 `data-of-theme-scope`,业务方不需要手写属性。
|
|
145
|
+
|
|
146
|
+
```vue
|
|
147
|
+
<script setup lang="ts">
|
|
148
|
+
import { ThemeScope } from '@oneflowui/ui'
|
|
149
|
+
</script>
|
|
150
|
+
|
|
151
|
+
<template>
|
|
152
|
+
<ThemeScope theme="ops-console" tag="section" class="ops-preview">
|
|
153
|
+
<div class="ops-preview__panel">
|
|
154
|
+
<h3>局部 ops-console 预览</h3>
|
|
155
|
+
<p>这里会继承 ops-console 的 token,而外层仍然保持全局 neutral。</p>
|
|
156
|
+
</div>
|
|
157
|
+
</ThemeScope>
|
|
158
|
+
</template>
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
`ThemeScope` 当前支持的主题值与全局主题一致:
|
|
162
|
+
|
|
163
|
+
- `neutral`
|
|
164
|
+
- `ops-console`
|
|
165
|
+
|
|
166
|
+
老代码里直接手写的 `data-of-theme-scope` 仍然兼容,但新代码优先使用组件入口。
|
|
167
|
+
|
|
168
|
+
### 组件级示例
|
|
169
|
+
|
|
170
|
+
更贴近业务消费的写法,是把一个组件子树直接包进 `ThemeScope`。外层页面继续沿用全局主题,局部区域则切到适合命令台或运营视角的 token。
|
|
171
|
+
|
|
172
|
+
```vue
|
|
173
|
+
<script setup lang="ts">
|
|
174
|
+
import { DataTable, StatisticCard, ThemeScope } from '@oneflowui/ui'
|
|
175
|
+
</script>
|
|
176
|
+
|
|
177
|
+
<template>
|
|
178
|
+
<ThemeScope theme="ops-console" tag="section" class="task-surface">
|
|
179
|
+
<header class="task-surface__header">
|
|
180
|
+
<h3>任务总览</h3>
|
|
181
|
+
<p>仅这块区域使用 ops-console token。</p>
|
|
182
|
+
</header>
|
|
183
|
+
|
|
184
|
+
<div class="task-surface__metrics">
|
|
185
|
+
<StatisticCard icon="check-circle" :value="18" label="已完成" />
|
|
186
|
+
<StatisticCard icon="clock" :value="6" label="进行中" />
|
|
187
|
+
</div>
|
|
188
|
+
|
|
189
|
+
<DataTable :rows="rows" :columns="columns" />
|
|
190
|
+
</ThemeScope>
|
|
191
|
+
</template>
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
这是一种非 breaking 的增强:组件 API 不需要变,`ThemeScope` 只是在局部 wrapper 上增加一层 token 作用域。
|
|
195
|
+
|
|
196
|
+
### 虚拟列表状态缓存
|
|
197
|
+
|
|
198
|
+
如果虚拟列表会频繁 remount,但你希望保留 `scrollTop`、`containerHeight` 和 `invalidateVersion`,可以按 key 复用同一份状态。
|
|
199
|
+
`createVirtualListState()` 仍然可用,而 `useVirtualListStateCache()` 适合把同一份状态挂在多个 remount 之间。
|
|
200
|
+
|
|
201
|
+
```ts
|
|
202
|
+
import { useVirtualList, useVirtualListStateCache } from '@oneflowui/ui'
|
|
203
|
+
|
|
204
|
+
const virtualListState = useVirtualListStateCache('ai-message-list')
|
|
205
|
+
|
|
206
|
+
const { visibleItems, totalHeight, offsetY } = useVirtualList({
|
|
207
|
+
items: messages,
|
|
208
|
+
itemHeight: 60,
|
|
209
|
+
containerRef,
|
|
210
|
+
state: virtualListState,
|
|
211
|
+
})
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
## 发布与验收
|
|
215
|
+
|
|
216
|
+
如果你需要核对当前可追溯的发布材料,优先看这几份文档:
|
|
217
|
+
|
|
218
|
+
- 当前版本证据索引:[`docs/plans/2026-03-22-release-0.8.5-evidence-index.md`](docs/plans/2026-03-22-release-0.8.5-evidence-index.md)
|
|
219
|
+
- 发布 proof:[`docs/plans/2026-03-22-release-0.8.5-proof.md`](docs/plans/2026-03-22-release-0.8.5-proof.md)
|
|
220
|
+
- 验收结果:[`docs/plans/2026-03-22-release-0.8.5-verification.md`](docs/plans/2026-03-22-release-0.8.5-verification.md)
|
|
221
|
+
- 预发布验证:[`docs/plans/2026-03-22-oneui-theme-scope-middleware-composer-pre-release-verification.md`](docs/plans/2026-03-22-oneui-theme-scope-middleware-composer-pre-release-verification.md)
|
|
222
|
+
- 子路径入口治理:[`docs/plans/2026-03-22-oneui-package-entrypoints-verification.md`](docs/plans/2026-03-22-oneui-package-entrypoints-verification.md)
|
|
223
|
+
- 版本日志:[`docs/CHANGELOG-v0.8.5.md`](docs/CHANGELOG-v0.8.5.md)
|
|
224
|
+
|
|
225
|
+
当前 npm 公开版本会在本轮发版完成后更新到 `0.8.5`,对应的发布、pack、dry-run 和双宿主 smoke 都有独立留痕。
|
|
226
|
+
|
|
123
227
|
---
|
|
124
228
|
|
|
125
229
|
## 页面级方案
|
|
@@ -175,6 +279,121 @@ type DatabaseViewActions = {
|
|
|
175
279
|
|
|
176
280
|
如果接入的是 `provider` 模式,建议把 `onFetch` / `onRefresh` 作为必配项;如果接入的是 `local` 模式,则重点只需要保证 `onUpdateRecord`、`onCreateRecord`、`onDeleteRecord` 和 `onSaveView` 这几类页面动作可回传。
|
|
177
281
|
|
|
282
|
+
### Middleware presets
|
|
283
|
+
|
|
284
|
+
如果页面想复用 toast、分析埋点或乐观更新逻辑,可以直接组合 `useDatabaseView` 提供的 middleware presets:
|
|
285
|
+
|
|
286
|
+
```ts
|
|
287
|
+
import {
|
|
288
|
+
composeDatabaseViewMiddlewares,
|
|
289
|
+
createDatabaseViewAnalyticsMiddleware,
|
|
290
|
+
createDatabaseViewOptimisticMiddleware,
|
|
291
|
+
createDatabaseViewToastMiddleware,
|
|
292
|
+
useDatabaseView,
|
|
293
|
+
} from '@oneflowui/ui'
|
|
294
|
+
|
|
295
|
+
const middleware = composeDatabaseViewMiddlewares(
|
|
296
|
+
createDatabaseViewToastMiddleware({
|
|
297
|
+
onSuccess: (message) => toast.success(message),
|
|
298
|
+
onError: (message) => toast.error(message),
|
|
299
|
+
}),
|
|
300
|
+
createDatabaseViewAnalyticsMiddleware({
|
|
301
|
+
onEvent: (event) => console.log('[db-view]', event.phase, event.action),
|
|
302
|
+
}),
|
|
303
|
+
createDatabaseViewOptimisticMiddleware({
|
|
304
|
+
apply: ({ payload }) => updateLocalRecord(payload),
|
|
305
|
+
revert: ({ payload }) => revertLocalRecord(payload),
|
|
306
|
+
}),
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
const view = useDatabaseView({
|
|
310
|
+
tableId: 'tbl-1',
|
|
311
|
+
actions: {
|
|
312
|
+
middleware,
|
|
313
|
+
onCellEdit: saveCellEdit,
|
|
314
|
+
},
|
|
315
|
+
})
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
### Dev Examples / Enterprise Demo
|
|
319
|
+
|
|
320
|
+
`DatabaseEnterpriseDemo` 是 dev/examples 级的消费范式,用来展示更完整的企业版页面组合方式。它属于开发示例和文档参考,不是 npm 包对外导出的组件,不会改变 `@oneflowui/ui` 的公开导出面。
|
|
321
|
+
|
|
322
|
+
如果你在业务工程里需要类似的页面,建议直接复制示例里的组合思路,再按自己的数据源和动作契约接入,而不是依赖一个额外的生产级导出入口。
|
|
323
|
+
|
|
324
|
+
### 组合消费示例
|
|
325
|
+
|
|
326
|
+
如果你要在同一块业务区域里同时使用主题作用域、数据库动作中间件和虚拟列表状态缓存,可以把三者放进同一个组件壳层。这个写法更接近真实业务页,也最适合作为复制模板。
|
|
327
|
+
|
|
328
|
+
```vue
|
|
329
|
+
<script setup lang="ts">
|
|
330
|
+
import { ref } from 'vue'
|
|
331
|
+
import {
|
|
332
|
+
ThemeScope,
|
|
333
|
+
composeDatabaseViewMiddlewares,
|
|
334
|
+
createDatabaseViewAnalyticsMiddleware,
|
|
335
|
+
createDatabaseViewOptimisticMiddleware,
|
|
336
|
+
createDatabaseViewToastMiddleware,
|
|
337
|
+
useDatabaseView,
|
|
338
|
+
useVirtualList,
|
|
339
|
+
useVirtualListStateCache,
|
|
340
|
+
} from '@oneflowui/ui'
|
|
341
|
+
|
|
342
|
+
const containerRef = ref<HTMLElement | null>(null)
|
|
343
|
+
const virtualListState = useVirtualListStateCache('task-feed')
|
|
344
|
+
|
|
345
|
+
const middleware = composeDatabaseViewMiddlewares(
|
|
346
|
+
createDatabaseViewToastMiddleware({
|
|
347
|
+
onSuccess: (message) => toast.success(message),
|
|
348
|
+
onError: (message) => toast.error(message),
|
|
349
|
+
}),
|
|
350
|
+
createDatabaseViewAnalyticsMiddleware({
|
|
351
|
+
onEvent: (event) => console.log('[db-view]', event.phase, event.action),
|
|
352
|
+
}),
|
|
353
|
+
createDatabaseViewOptimisticMiddleware({
|
|
354
|
+
apply: ({ payload }) => updateLocalRecord(payload),
|
|
355
|
+
revert: ({ payload }) => revertLocalRecord(payload),
|
|
356
|
+
}),
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
const view = useDatabaseView({
|
|
360
|
+
tableId: 'tbl-1',
|
|
361
|
+
actions: {
|
|
362
|
+
middleware,
|
|
363
|
+
onCellEdit: saveCellEdit,
|
|
364
|
+
},
|
|
365
|
+
})
|
|
366
|
+
|
|
367
|
+
const { visibleItems } = useVirtualList({
|
|
368
|
+
items: view.records,
|
|
369
|
+
itemHeight: 60,
|
|
370
|
+
containerRef,
|
|
371
|
+
state: virtualListState,
|
|
372
|
+
})
|
|
373
|
+
|
|
374
|
+
const columns = [
|
|
375
|
+
{ key: 'title', label: '任务', width: 'fill' },
|
|
376
|
+
{ key: 'status', label: '状态', width: 120 },
|
|
377
|
+
{ key: 'priority', label: '优先级', width: 100 },
|
|
378
|
+
]
|
|
379
|
+
</script>
|
|
380
|
+
|
|
381
|
+
<template>
|
|
382
|
+
<ThemeScope theme="ops-console" tag="section" class="task-surface">
|
|
383
|
+
<header class="task-surface__header">
|
|
384
|
+
<h3>任务总览</h3>
|
|
385
|
+
<p>外层是局部 ops-console token,内部动作、列表和状态缓存都复用同一套组件入口。</p>
|
|
386
|
+
</header>
|
|
387
|
+
|
|
388
|
+
<div ref="containerRef" class="task-surface__list">
|
|
389
|
+
<DataTable :rows="visibleItems" :columns="columns" />
|
|
390
|
+
</div>
|
|
391
|
+
</ThemeScope>
|
|
392
|
+
</template>
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
这三个能力可以独立使用,也可以像上面这样组合使用;它们都是非 breaking 增强,不要求业务方修改现有组件 API。
|
|
396
|
+
|
|
178
397
|
### Selected record / detail workspace
|
|
179
398
|
|
|
180
399
|
当前 dev app 已经把“选中记录 -> detail workspace”这条链路接起来了:点击 table / kanban / gallery / timeline 中的条目,会把当前记录送入详情工作区,再由 `DetailLayout`、`PropPanel`、`CommentItem` 这组组件展示主内容、属性和活动记录。
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import o from "./AiMessageList.vue2.js";
|
|
2
2
|
/* empty css */
|
|
3
3
|
import t from "../../_virtual/_plugin-vue_export-helper.js";
|
|
4
|
-
const a = /* @__PURE__ */ t(o, [["__scopeId", "data-v-
|
|
4
|
+
const a = /* @__PURE__ */ t(o, [["__scopeId", "data-v-76bed64f"]]);
|
|
5
5
|
export {
|
|
6
6
|
a as default
|
|
7
7
|
};
|
|
@@ -1,55 +1,56 @@
|
|
|
1
|
-
import { defineComponent as
|
|
2
|
-
import { useVirtualList as A } from "../../composables/useVirtualList.js";
|
|
1
|
+
import { defineComponent as T, ref as b, computed as x, watch as M, nextTick as L, openBlock as t, createElementBlock as n, createElementVNode as u, normalizeStyle as f, unref as s, Fragment as p, renderList as S, createBlock as r, createCommentVNode as _ } from "vue";
|
|
2
|
+
import { createVirtualListState as V, useVirtualList as A } from "../../composables/useVirtualList.js";
|
|
3
3
|
import E from "./AiMessageBubble.vue.js";
|
|
4
4
|
import H from "./UserMessageBubble.vue.js";
|
|
5
5
|
import R from "./AiThinking.vue.js";
|
|
6
|
-
const
|
|
6
|
+
const F = /* @__PURE__ */ T({
|
|
7
7
|
__name: "AiMessageList",
|
|
8
8
|
props: {
|
|
9
9
|
messages: {},
|
|
10
10
|
isThinking: { type: Boolean }
|
|
11
11
|
},
|
|
12
12
|
setup(o) {
|
|
13
|
-
const
|
|
13
|
+
const a = o, l = b(null), g = V(), h = (m) => {
|
|
14
14
|
var c;
|
|
15
|
-
const
|
|
16
|
-
if (!
|
|
17
|
-
if (
|
|
18
|
-
const e = Math.ceil((((c =
|
|
15
|
+
const i = a.messages[m];
|
|
16
|
+
if (!i) return 80;
|
|
17
|
+
if (i.role === "user") return 60;
|
|
18
|
+
const e = Math.ceil((((c = i.content) == null ? void 0 : c.length) ?? 0) / 60);
|
|
19
19
|
return Math.max(80, 48 + e * 24);
|
|
20
20
|
}, {
|
|
21
|
-
visibleItems:
|
|
22
|
-
totalHeight:
|
|
23
|
-
offsetY:
|
|
24
|
-
scrollToBottom:
|
|
21
|
+
visibleItems: v,
|
|
22
|
+
totalHeight: k,
|
|
23
|
+
offsetY: d,
|
|
24
|
+
scrollToBottom: y
|
|
25
25
|
} = A({
|
|
26
|
-
items:
|
|
27
|
-
itemHeight:
|
|
26
|
+
items: x(() => a.messages),
|
|
27
|
+
itemHeight: h,
|
|
28
28
|
overscan: 3,
|
|
29
|
-
containerRef:
|
|
29
|
+
containerRef: l,
|
|
30
|
+
state: g
|
|
30
31
|
});
|
|
31
|
-
function
|
|
32
|
-
|
|
32
|
+
function B() {
|
|
33
|
+
y();
|
|
33
34
|
}
|
|
34
|
-
return
|
|
35
|
-
() => [
|
|
35
|
+
return M(
|
|
36
|
+
() => [a.messages.length, a.isThinking],
|
|
36
37
|
async () => {
|
|
37
|
-
await
|
|
38
|
+
await L(), B();
|
|
38
39
|
},
|
|
39
40
|
{ immediate: !0 }
|
|
40
|
-
), (
|
|
41
|
+
), (m, i) => (t(), n("div", {
|
|
41
42
|
ref_key: "listRef",
|
|
42
|
-
ref:
|
|
43
|
+
ref: l,
|
|
43
44
|
class: "of-ai-message-list"
|
|
44
45
|
}, [
|
|
45
46
|
u("div", {
|
|
46
|
-
style: f({ height: s(
|
|
47
|
+
style: f({ height: s(k) + "px", position: "relative" })
|
|
47
48
|
}, [
|
|
48
49
|
u("div", {
|
|
49
50
|
class: "of-ai-message-list-inner",
|
|
50
|
-
style: f({ transform: `translateY(${s(
|
|
51
|
+
style: f({ transform: `translateY(${s(d)}px)` })
|
|
51
52
|
}, [
|
|
52
|
-
(t(!0), n(p, null,
|
|
53
|
+
(t(!0), n(p, null, S(s(v), ({ data: e }) => (t(), n(p, {
|
|
53
54
|
key: e.id
|
|
54
55
|
}, [
|
|
55
56
|
e.role === "ai" ? (t(), r(E, {
|
|
@@ -69,10 +70,10 @@ const Y = /* @__PURE__ */ B({
|
|
|
69
70
|
], 64))), 128))
|
|
70
71
|
], 4)
|
|
71
72
|
], 4),
|
|
72
|
-
o.isThinking ? (t(), r(R, { key: 0 })) :
|
|
73
|
+
o.isThinking ? (t(), r(R, { key: 0 })) : _("", !0)
|
|
73
74
|
], 512));
|
|
74
75
|
}
|
|
75
76
|
});
|
|
76
77
|
export {
|
|
77
|
-
|
|
78
|
+
F as default
|
|
78
79
|
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
type ThemeScopeTheme = "neutral" | "ops-console";
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
theme: ThemeScopeTheme;
|
|
4
|
+
tag?: string;
|
|
5
|
+
};
|
|
6
|
+
declare function __VLS_template(): {
|
|
7
|
+
attrs: Partial<{}>;
|
|
8
|
+
slots: {
|
|
9
|
+
default?(_: {}): any;
|
|
10
|
+
};
|
|
11
|
+
refs: {};
|
|
12
|
+
rootEl: any;
|
|
13
|
+
};
|
|
14
|
+
type __VLS_TemplateResult = ReturnType<typeof __VLS_template>;
|
|
15
|
+
declare const __VLS_component: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
|
|
16
|
+
tag: string;
|
|
17
|
+
}, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>;
|
|
18
|
+
declare const _default: __VLS_WithTemplateSlots<typeof __VLS_component, __VLS_TemplateResult["slots"]>;
|
|
19
|
+
export default _default;
|
|
20
|
+
type __VLS_WithTemplateSlots<T, S> = T & {
|
|
21
|
+
new (): {
|
|
22
|
+
$slots: S;
|
|
23
|
+
};
|
|
24
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { defineComponent as a, openBlock as m, createBlock as n, resolveDynamicComponent as p, withCtx as c, renderSlot as r } from "vue";
|
|
2
|
+
const f = /* @__PURE__ */ a({
|
|
3
|
+
name: "ThemeScope",
|
|
4
|
+
__name: "ThemeScope",
|
|
5
|
+
props: {
|
|
6
|
+
theme: {},
|
|
7
|
+
tag: { default: "div" }
|
|
8
|
+
},
|
|
9
|
+
setup(t) {
|
|
10
|
+
const e = t;
|
|
11
|
+
return (o, s) => (m(), n(p(e.tag), {
|
|
12
|
+
"data-of-theme": e.theme,
|
|
13
|
+
"data-of-theme-scope": e.theme
|
|
14
|
+
}, {
|
|
15
|
+
default: c(() => [
|
|
16
|
+
r(o.$slots, "default")
|
|
17
|
+
]),
|
|
18
|
+
_: 3
|
|
19
|
+
}, 8, ["data-of-theme", "data-of-theme-scope"]));
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
export {
|
|
23
|
+
f as default
|
|
24
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as ThemeScope } from './ThemeScope.vue';
|
|
@@ -36,10 +36,10 @@ declare const _default: import('vue').DefineComponent<DatabaseViewProps, {}, {},
|
|
|
36
36
|
endDate?: string;
|
|
37
37
|
}) => any;
|
|
38
38
|
"update:records": (args_0: DataRecord[]) => any;
|
|
39
|
-
"update:currentViewId": (args_0: string) => any;
|
|
40
|
-
"update:selectedRecordId": (args_0: string | null) => any;
|
|
41
39
|
"select-record": (args_0: DataRecord | null) => any;
|
|
42
40
|
refresh: () => any;
|
|
41
|
+
"update:currentViewId": (args_0: string) => any;
|
|
42
|
+
"update:selectedRecordId": (args_0: string | null) => any;
|
|
43
43
|
}, string, import('vue').PublicProps, Readonly<DatabaseViewProps> & Readonly<{
|
|
44
44
|
onSort?: ((args_0: string) => any) | undefined;
|
|
45
45
|
onAdd?: (() => any) | undefined;
|
|
@@ -75,10 +75,10 @@ declare const _default: import('vue').DefineComponent<DatabaseViewProps, {}, {},
|
|
|
75
75
|
endDate?: string;
|
|
76
76
|
}) => any) | undefined;
|
|
77
77
|
"onUpdate:records"?: ((args_0: DataRecord[]) => any) | undefined;
|
|
78
|
-
"onUpdate:currentViewId"?: ((args_0: string) => any) | undefined;
|
|
79
|
-
"onUpdate:selectedRecordId"?: ((args_0: string | null) => any) | undefined;
|
|
80
78
|
"onSelect-record"?: ((args_0: DataRecord | null) => any) | undefined;
|
|
81
79
|
onRefresh?: (() => any) | undefined;
|
|
80
|
+
"onUpdate:currentViewId"?: ((args_0: string) => any) | undefined;
|
|
81
|
+
"onUpdate:selectedRecordId"?: ((args_0: string | null) => any) | undefined;
|
|
82
82
|
}>, {
|
|
83
83
|
mode: import('../..').DatabaseViewMode;
|
|
84
84
|
error: string | Error | null;
|