feat: enable project-scoped preferences extension tabs (#7803)

* feat: enable project-scoped preferences extension tabs

Add a typed extension schema so subprojects can define extra settings,
render them in the shared preferences drawer only when configured, and
consume them in playground as a real feature demo. Extension labels now
follow locale keys instead of hardcoded app-specific strings.

Constraint: Reuse the shared preferences drawer and field blocks
Rejected: Add app-specific fields to core preferences | too tightly coupled
Rejected: Inline localized label objects | breaks existing locale-key flow
Confidence: high
Scope-risk: moderate
Reversibility: clean
Directive: Keep extension labels as locale keys rendered via $t in UI
Tested: Vitest preferences tests
Tested: Turbo typecheck for preferences, layouts, web-antd, and playground
Tested: ESLint for touched preferences and playground files
Not-tested: Manual browser interaction in playground preferences drawer

* fix: satisfy lint formatting for preferences extension demo

Adjust the playground preferences extension demo template so formatter and
Vue template lint rules agree on the rendered markup. This keeps CI green
without changing runtime behavior.

Constraint: Must preserve the existing demo behavior while fixing CI only
Rejected: Disable the Vue newline rule | would weaken shared lint guarantees
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Prefer computed/template structures that avoid formatter-vs-lint conflicts
Tested: pnpm run lint
Not-tested: Manual browser interaction in playground preferences extension demo

* fix: harden custom preferences validation and i18n labels

Tighten custom preferences handling so numeric extension fields respect
min, max, and step constraints. Number inputs now ignore NaN values,
and web-antd extension metadata uses locale keys instead of raw strings.
Also align tip-based hover guards in shared preference inputs/selects.

Constraint: Keep fixes scoped to verified findings only
Rejected: Broader refactor of preferences field components | not needed for these fixes
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Reuse the same validation path for updates and cache hydration
Tested: Vitest preferences tests
Tested: ESLint for touched preferences and widget files
Tested: Typecheck for web-antd, layouts, and core preferences
Not-tested: Manual browser interaction for all preference field variants

* fix: remove localized default from playground extension config

Drop the hardcoded Chinese default value from the playground extension
report title field and fall back to an empty string instead. This keeps
extension config locale-neutral while preserving localized labels and
placeholders through translation keys.

Constraint: Keep the fix limited to the verified localized default issue
Rejected: Compute the default from runtime locale in config | unnecessary for this finding
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Avoid embedding localized literals in extension default values
Tested: ESLint for playground/src/preferences.ts
Tested: Oxfmt check for playground/src/preferences.ts
Not-tested: Manual playground preferences interaction

* docs: document project-scoped preferences extension workflow

Add Chinese and English guide sections explaining how to define,
initialize, read, and update project-scoped preferences extensions.
Also document numeric field validation and point readers to the
playground demo for a complete example.

Constraint: Keep this docs-only and aligned with the shipped API
Rejected: Update only Chinese docs | would leave English docs inconsistent
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Keep zh/en examples and playground demo paths synchronized
Tested: git diff --check; pnpm build:docs
Not-tested: Manual browser review of the rendered docs site

* fix: harden custom preferences defaults and baselines

Use a locale-neutral default for the web-antd report title.
Also stop preference getters from exposing mutable baseline
or extension schema objects, and add a regression test for
external mutation attempts.

Constraint: Keep behavior compatible with the shipped preferences API
Rejected: Return raw refs with readonly typing only | callers could still mutate internals
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Keep defensive copies for baseline and schema getters unless storage semantics change
Tested: eslint, oxlint, targeted vitest, filtered typecheck, git diff --check
Not-tested: Full monorepo typecheck and test suite

* test: relax custom preference cache key matching

Avoid coupling the custom-number cache test to one exact
localStorage key string. Match the intended cache lookup
more loosely so the test still verifies filtering behavior
without depending on the full namespaced cache key.

Constraint: Focus the test on cache filtering behavior
Rejected: Assert one exact key | brittle with namespace changes
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Prefer behavior tests over literal storage keys
Tested: targeted vitest, eslint, git diff --check
Not-tested: Full monorepo test suite

---------

Co-authored-by: caisin <caisin@caisins-Mac-mini.local>
This commit is contained in:
Caisin
2026-04-13 15:11:57 +08:00
committed by GitHub
parent 5b84ac5b13
commit ccabbf0e97
24 changed files with 1720 additions and 39 deletions

View File

@@ -192,6 +192,156 @@ export const overridesPreferences = defineOverridesPreferences({
});
```
### 扩展项目级偏好
除了覆盖框架内置偏好外,还可以为每个应用追加一组“业务偏好”。配置后,偏好设置抽屉会新增一个独立标签页,并且这组数据会跟随当前应用的 `namespace` 一起存储,适合放租户模式、业务标题、默认分页条数等项目字段。
#### 1. 在应用的 `src/preferences.ts` 中定义扩展
```ts
import {
defineOverridesPreferences,
definePreferencesExtension,
} from '@vben/preferences';
interface ProjectPreferencesExtension {
defaultTableSize: number;
enableFormFullscreen: boolean;
reportTitle: string;
tenantMode: 'multi' | 'single';
}
export const overridesPreferences = defineOverridesPreferences({
app: {
name: import.meta.env.VITE_APP_TITLE,
},
});
export const preferencesExtension =
definePreferencesExtension<ProjectPreferencesExtension>({
tabLabel: 'preferences.antd.tabLabel',
title: 'preferences.antd.title',
fields: [
{
component: 'switch',
defaultValue: true,
key: 'enableFormFullscreen',
label: 'preferences.antd.fields.enableFormFullscreen.label',
tip: 'preferences.antd.fields.enableFormFullscreen.tip',
},
{
component: 'select',
defaultValue: 'single',
key: 'tenantMode',
label: 'preferences.antd.fields.tenantMode.label',
options: [
{
label: 'preferences.antd.fields.tenantMode.options.single.label',
value: 'single',
},
{
label: 'preferences.antd.fields.tenantMode.options.multi.label',
value: 'multi',
},
],
},
{
component: 'number',
componentProps: {
max: 200,
min: 10,
step: 10,
},
defaultValue: 20,
key: 'defaultTableSize',
label: 'preferences.antd.fields.defaultTableSize.label',
},
{
component: 'input',
defaultValue: '',
key: 'reportTitle',
label: 'preferences.antd.fields.reportTitle.label',
placeholder: 'preferences.antd.fields.reportTitle.placeholder',
},
],
});
```
- `tabLabel` 是标签名称,`title` 是该标签页标题;如果不传 `title`,会回退使用 `tabLabel`。
- `fields` 目前支持 `input`、`number`、`select`、`switch` 四种组件。
- `label`、`placeholder`、`tip`、`options[].label` 可以直接写 i18n key偏好设置面板会自动调用 `$t` 渲染。
#### 2. 初始化偏好设置时传入 `extension`
```ts
import { initPreferences } from '@vben/preferences';
import { overridesPreferences, preferencesExtension } from './preferences';
await initPreferences({
namespace,
overrides: overridesPreferences,
extension: preferencesExtension,
});
```
这里的 `namespace` 会同时隔离框架偏好和扩展偏好。因此同一浏览器中即使运行多个子项目,它们的业务偏好也不会互相污染。
#### 3. 在业务页面中读取或更新扩展偏好
```ts
import {
getCustomPreferences,
updateCustomPreferences,
usePreferences,
} from '@vben/preferences';
interface ProjectPreferencesExtension {
defaultTableSize: number;
enableFormFullscreen: boolean;
reportTitle: string;
tenantMode: 'multi' | 'single';
}
const projectPreferences = getCustomPreferences<ProjectPreferencesExtension>();
const { customPreferences, preferencesExtension } = usePreferences();
updateCustomPreferences<ProjectPreferencesExtension>({
defaultTableSize: 50,
tenantMode: 'multi',
});
```
- `getCustomPreferences` 返回当前应用扩展偏好的响应式对象,适合直接在页面中读取。
- `usePreferences` 中的 `customPreferences` 和 `preferencesExtension` 适合在组合式逻辑里统一使用。
- 调用 `resetPreferences()` 时,扩展偏好也会一起重置到默认值。
#### 4. 数字字段会自动校验 `min` / `max` / `step`
为 `number` 字段设置 `componentProps.min`、`componentProps.max`、`componentProps.step` 后,运行时保存也会遵守同样的规则。例如下面的配置:
```ts
{
component: 'number',
componentProps: {
min: 10,
max: 200,
step: 10,
},
defaultValue: 20,
key: 'defaultTableSize',
label: 'preferences.antd.fields.defaultTableSize.label',
}
```
此时只有 `10 ~ 200` 且按 `10` 递增的值会被保存;像 `15`、`205`,或者旧缓存里不满足约束的值,都会被自动忽略。
完整示例可以参考:
- `playground/src/preferences.ts`
- `playground/src/views/demos/features/preferences-extension/index.vue`
### 框架默认配置
::: details 查看框架默认配置