mirror of
https://github.com/imdap/ruoyi-plus-vben5.git
synced 2026-05-07 10:41:26 +08:00
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:
@@ -44,6 +44,7 @@
|
||||
"loginExpired": "Login Expired",
|
||||
"icons": "Icons",
|
||||
"watermark": "Watermark",
|
||||
"preferencesExtension": "Preferences Extension",
|
||||
"tabs": "Tabs",
|
||||
"tabDetail": "Tab Detail Page",
|
||||
"fullScreen": "FullScreen",
|
||||
@@ -52,6 +53,63 @@
|
||||
"openInNewWindow": "Open in New Window",
|
||||
"fileDownload": "File Download"
|
||||
},
|
||||
"preferencesExtensionDemo": {
|
||||
"description": "This page directly uses extension preferences defined in the playground app. You can modify them in “Preferences → Playground Extension” at the top right, or click the preset buttons below to see the page update in real time.",
|
||||
"currentConfig": "Current Extension Preferences",
|
||||
"currentTitle": "Current Title: {title}",
|
||||
"currentDescription": "Displaying {count} tasks by default; quick actions are {quickActionText}; the current highlight tone is “{tone}”.",
|
||||
"showQuickActions": "shown",
|
||||
"hideQuickActions": "hidden",
|
||||
"boardDescription": "This is a real usage example: the page title, action area, visible row count, and highlight styles are all driven by extension preferences.",
|
||||
"quickActionsEnabled": "Quick actions are currently disabled, so the page only keeps the read-only information view.",
|
||||
"quickActions": {
|
||||
"create": "Create Task",
|
||||
"export": "Batch Export",
|
||||
"refresh": "Refresh Data"
|
||||
},
|
||||
"presetTitle": "Quick Presets (demonstrates updateCustomPreferences)",
|
||||
"presetButtons": {
|
||||
"default": "Reset Default",
|
||||
"compact": "Switch to Compact",
|
||||
"review": "Switch to Review"
|
||||
},
|
||||
"presetTitles": {
|
||||
"compact": "Compact Dashboard",
|
||||
"default": "Weekly Operations Overview",
|
||||
"review": "Review Dashboard"
|
||||
},
|
||||
"tones": {
|
||||
"default": "Default",
|
||||
"success": "Success",
|
||||
"warning": "Warning"
|
||||
},
|
||||
"owner": "Owner"
|
||||
},
|
||||
"preferencesExtensionConfig": {
|
||||
"tabLabel": "Playground Extension",
|
||||
"title": "Playground Business Preferences",
|
||||
"fields": {
|
||||
"reportTitle": {
|
||||
"label": "Report Title",
|
||||
"placeholder": "Please enter the report title"
|
||||
},
|
||||
"defaultVisibleRows": {
|
||||
"label": "Default Visible Rows",
|
||||
"tip": "Controls how many task items are rendered by default on the demo page."
|
||||
},
|
||||
"enableQuickActions": {
|
||||
"label": "Show Quick Actions"
|
||||
},
|
||||
"highlightTone": {
|
||||
"label": "Highlight Tone",
|
||||
"options": {
|
||||
"default": "Default",
|
||||
"success": "Success",
|
||||
"warning": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"breadcrumb": {
|
||||
"navigation": "Breadcrumb Navigation",
|
||||
"lateral": "Lateral Mode",
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
"loginExpired": "登录过期",
|
||||
"icons": "图标",
|
||||
"watermark": "水印",
|
||||
"preferencesExtension": "偏好扩展示例",
|
||||
"tabs": "标签页",
|
||||
"tabDetail": "标签详情页",
|
||||
"fullScreen": "全屏",
|
||||
@@ -53,6 +54,63 @@
|
||||
"fileDownload": "文件下载",
|
||||
"requestParamsSerializer": "参数序列化"
|
||||
},
|
||||
"preferencesExtensionDemo": {
|
||||
"description": "这个页面直接读取 playground 子项目定义的拓展偏好。你可以在右上角的“偏好设置 → Playground 拓展”中修改字段,也可以点击下方预设按钮,页面会实时联动。",
|
||||
"currentConfig": "当前拓展配置",
|
||||
"currentTitle": "当前标题:{title}",
|
||||
"currentDescription": "默认展示 {count} 条任务;{quickActionText} 快捷操作;当前高亮风格为“{tone}”。",
|
||||
"showQuickActions": "显示",
|
||||
"hideQuickActions": "隐藏",
|
||||
"boardDescription": "这是一个“真实使用”的示例:页面标题、操作区、列表条数和高亮样式都由拓展偏好驱动。",
|
||||
"quickActionsEnabled": "当前已关闭快捷操作,页面只保留只读信息展示。",
|
||||
"quickActions": {
|
||||
"create": "新建任务",
|
||||
"export": "批量导出",
|
||||
"refresh": "刷新数据"
|
||||
},
|
||||
"presetTitle": "快捷预设(演示 updateCustomPreferences 用法)",
|
||||
"presetButtons": {
|
||||
"default": "恢复默认",
|
||||
"compact": "切换紧凑模式",
|
||||
"review": "切换评审模式"
|
||||
},
|
||||
"presetTitles": {
|
||||
"compact": "紧凑模式看板",
|
||||
"default": "本周运营概览",
|
||||
"review": "评审态工作看板"
|
||||
},
|
||||
"tones": {
|
||||
"default": "默认",
|
||||
"success": "成功",
|
||||
"warning": "警告"
|
||||
},
|
||||
"owner": "负责人"
|
||||
},
|
||||
"preferencesExtensionConfig": {
|
||||
"tabLabel": "Playground 拓展",
|
||||
"title": "Playground 业务偏好",
|
||||
"fields": {
|
||||
"reportTitle": {
|
||||
"label": "看板标题",
|
||||
"placeholder": "请输入看板标题"
|
||||
},
|
||||
"defaultVisibleRows": {
|
||||
"label": "默认展示条数",
|
||||
"tip": "用于控制示例页中任务列表默认渲染多少条数据。"
|
||||
},
|
||||
"enableQuickActions": {
|
||||
"label": "显示快捷操作"
|
||||
},
|
||||
"highlightTone": {
|
||||
"label": "高亮风格",
|
||||
"options": {
|
||||
"default": "默认",
|
||||
"success": "成功",
|
||||
"warning": "警告"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"breadcrumb": {
|
||||
"navigation": "面包屑导航",
|
||||
"lateral": "平级模式",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { initPreferences } from '@vben/preferences';
|
||||
import { unmountGlobalLoading } from '@vben/utils';
|
||||
|
||||
import { overridesPreferences } from './preferences';
|
||||
import { overridesPreferences, preferencesExtension } from './preferences';
|
||||
|
||||
/**
|
||||
* 应用初始化完成之后再进行页面加载渲染
|
||||
@@ -15,6 +15,7 @@ async function initApplication() {
|
||||
|
||||
// app偏好设置初始化
|
||||
await initPreferences({
|
||||
extension: preferencesExtension,
|
||||
namespace,
|
||||
overrides: overridesPreferences,
|
||||
});
|
||||
|
||||
@@ -1,4 +1,14 @@
|
||||
import { defineOverridesPreferences } from '@vben/preferences';
|
||||
import {
|
||||
defineOverridesPreferences,
|
||||
definePreferencesExtension,
|
||||
} from '@vben/preferences';
|
||||
|
||||
interface PlaygroundPreferencesExtension {
|
||||
defaultVisibleRows: number;
|
||||
enableQuickActions: boolean;
|
||||
highlightTone: 'default' | 'success' | 'warning';
|
||||
reportTitle: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 项目配置文件
|
||||
@@ -11,3 +21,64 @@ export const overridesPreferences = defineOverridesPreferences({
|
||||
name: import.meta.env.VITE_APP_TITLE,
|
||||
},
|
||||
});
|
||||
|
||||
export type { PlaygroundPreferencesExtension };
|
||||
|
||||
export const preferencesExtension =
|
||||
definePreferencesExtension<PlaygroundPreferencesExtension>({
|
||||
tabLabel: 'demos.preferencesExtensionConfig.tabLabel',
|
||||
title: 'demos.preferencesExtensionConfig.title',
|
||||
fields: [
|
||||
{
|
||||
component: 'input',
|
||||
defaultValue: '',
|
||||
key: 'reportTitle',
|
||||
label: 'demos.preferencesExtensionConfig.fields.reportTitle.label',
|
||||
placeholder:
|
||||
'demos.preferencesExtensionConfig.fields.reportTitle.placeholder',
|
||||
},
|
||||
{
|
||||
component: 'number',
|
||||
componentProps: {
|
||||
max: 8,
|
||||
min: 1,
|
||||
step: 1,
|
||||
},
|
||||
defaultValue: 4,
|
||||
key: 'defaultVisibleRows',
|
||||
label:
|
||||
'demos.preferencesExtensionConfig.fields.defaultVisibleRows.label',
|
||||
tip: 'demos.preferencesExtensionConfig.fields.defaultVisibleRows.tip',
|
||||
},
|
||||
{
|
||||
component: 'switch',
|
||||
defaultValue: true,
|
||||
key: 'enableQuickActions',
|
||||
label:
|
||||
'demos.preferencesExtensionConfig.fields.enableQuickActions.label',
|
||||
},
|
||||
{
|
||||
component: 'select',
|
||||
defaultValue: 'default',
|
||||
key: 'highlightTone',
|
||||
label: 'demos.preferencesExtensionConfig.fields.highlightTone.label',
|
||||
options: [
|
||||
{
|
||||
label:
|
||||
'demos.preferencesExtensionConfig.fields.highlightTone.options.default',
|
||||
value: 'default',
|
||||
},
|
||||
{
|
||||
label:
|
||||
'demos.preferencesExtensionConfig.fields.highlightTone.options.success',
|
||||
value: 'success',
|
||||
},
|
||||
{
|
||||
label:
|
||||
'demos.preferencesExtensionConfig.fields.highlightTone.options.warning',
|
||||
value: 'warning',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -124,6 +124,16 @@ const routes: RouteRecordRaw[] = [
|
||||
title: $t('demos.features.watermark'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'PreferencesExtensionDemo',
|
||||
path: '/demos/features/preferences-extension',
|
||||
component: () =>
|
||||
import('#/views/demos/features/preferences-extension/index.vue'),
|
||||
meta: {
|
||||
icon: 'lucide:sliders-horizontal',
|
||||
title: $t('demos.features.preferencesExtension'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'FeatureTabsDemo',
|
||||
path: '/demos/features/tabs',
|
||||
|
||||
@@ -0,0 +1,231 @@
|
||||
<script lang="ts" setup>
|
||||
import type { PlaygroundPreferencesExtension } from '#/preferences';
|
||||
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
import {
|
||||
getCustomPreferences,
|
||||
updateCustomPreferences,
|
||||
} from '@vben/preferences';
|
||||
|
||||
import { Alert, Button, Card, Space, Tag } from 'ant-design-vue';
|
||||
|
||||
import { $t } from '#/locales';
|
||||
|
||||
interface DemoTaskItem {
|
||||
id: number;
|
||||
owner: string;
|
||||
priority: 'P0' | 'P1' | 'P2';
|
||||
title: string;
|
||||
}
|
||||
|
||||
type HighlightTone = PlaygroundPreferencesExtension['highlightTone'];
|
||||
|
||||
const playgroundPreferences =
|
||||
getCustomPreferences<PlaygroundPreferencesExtension>();
|
||||
|
||||
const demoTasks: DemoTaskItem[] = [
|
||||
{ id: 1, owner: 'Luna', priority: 'P0', title: '同步租户配置到缓存' },
|
||||
{ id: 2, owner: 'Aiden', priority: 'P1', title: '补充角色权限回归用例' },
|
||||
{ id: 3, owner: 'Mia', priority: 'P0', title: '修复看板接口超时重试' },
|
||||
{ id: 4, owner: 'Noah', priority: 'P2', title: '整理本周运营周报模板' },
|
||||
{ id: 5, owner: 'Ethan', priority: 'P1', title: '验证暗黑主题下图表对比度' },
|
||||
{ id: 6, owner: 'Sophia', priority: 'P1', title: '更新埋点字段映射文档' },
|
||||
{ id: 7, owner: 'Lucas', priority: 'P2', title: '检查消息中心未读状态同步' },
|
||||
{ id: 8, owner: 'Emma', priority: 'P0', title: '补齐导出任务失败告警' },
|
||||
];
|
||||
|
||||
const toneMap = {
|
||||
default: {
|
||||
alertType: 'info',
|
||||
cardClass: 'border-primary/20 bg-primary/5',
|
||||
label: $t('demos.preferencesExtensionDemo.tones.default'),
|
||||
tagColor: 'processing',
|
||||
},
|
||||
success: {
|
||||
alertType: 'success',
|
||||
cardClass: 'border-emerald-500/20 bg-emerald-500/5',
|
||||
label: $t('demos.preferencesExtensionDemo.tones.success'),
|
||||
tagColor: 'success',
|
||||
},
|
||||
warning: {
|
||||
alertType: 'warning',
|
||||
cardClass: 'border-amber-500/20 bg-amber-500/5',
|
||||
label: $t('demos.preferencesExtensionDemo.tones.warning'),
|
||||
tagColor: 'warning',
|
||||
},
|
||||
} as const satisfies Record<
|
||||
HighlightTone,
|
||||
{
|
||||
alertType: 'info' | 'success' | 'warning';
|
||||
cardClass: string;
|
||||
label: string;
|
||||
tagColor: string;
|
||||
}
|
||||
>;
|
||||
|
||||
const visibleTasks = computed(() => {
|
||||
return demoTasks.slice(0, playgroundPreferences.defaultVisibleRows);
|
||||
});
|
||||
|
||||
const toneConfig = computed(() => {
|
||||
return toneMap[playgroundPreferences.highlightTone];
|
||||
});
|
||||
|
||||
const formattedPlaygroundPreferences = computed(() => {
|
||||
return JSON.stringify(playgroundPreferences, null, 2);
|
||||
});
|
||||
|
||||
const preClasses =
|
||||
'mt-4 overflow-auto rounded-lg border border-border bg-muted p-4 text-sm';
|
||||
|
||||
function applyPreset(type: 'compact' | 'focus' | 'review') {
|
||||
const presetMap: Record<
|
||||
typeof type,
|
||||
Partial<PlaygroundPreferencesExtension>
|
||||
> = {
|
||||
compact: {
|
||||
defaultVisibleRows: 3,
|
||||
enableQuickActions: false,
|
||||
highlightTone: 'warning',
|
||||
reportTitle: $t('demos.preferencesExtensionDemo.presetTitles.compact'),
|
||||
},
|
||||
focus: {
|
||||
defaultVisibleRows: 4,
|
||||
enableQuickActions: true,
|
||||
highlightTone: 'default',
|
||||
reportTitle: $t('demos.preferencesExtensionDemo.presetTitles.default'),
|
||||
},
|
||||
review: {
|
||||
defaultVisibleRows: 6,
|
||||
enableQuickActions: true,
|
||||
highlightTone: 'success',
|
||||
reportTitle: $t('demos.preferencesExtensionDemo.presetTitles.review'),
|
||||
},
|
||||
};
|
||||
|
||||
updateCustomPreferences<PlaygroundPreferencesExtension>(presetMap[type]);
|
||||
}
|
||||
|
||||
function getPriorityColor(priority: DemoTaskItem['priority']) {
|
||||
switch (priority) {
|
||||
case 'P0': {
|
||||
return 'error';
|
||||
}
|
||||
case 'P1': {
|
||||
return 'warning';
|
||||
}
|
||||
default: {
|
||||
return 'default';
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page :title="$t('demos.features.preferencesExtension')">
|
||||
<template #description>
|
||||
<div class="mt-2 text-foreground/80">
|
||||
{{ $t('demos.preferencesExtensionDemo.description') }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<Card
|
||||
class="mb-5"
|
||||
:title="$t('demos.preferencesExtensionDemo.currentConfig')"
|
||||
>
|
||||
<Alert :type="toneConfig.alertType" show-icon>
|
||||
<template #message>
|
||||
{{
|
||||
$t('demos.preferencesExtensionDemo.currentTitle', {
|
||||
title: playgroundPreferences.reportTitle,
|
||||
})
|
||||
}}
|
||||
</template>
|
||||
<template #description>
|
||||
{{
|
||||
$t('demos.preferencesExtensionDemo.currentDescription', {
|
||||
count: playgroundPreferences.defaultVisibleRows,
|
||||
quickActionText: playgroundPreferences.enableQuickActions
|
||||
? $t('demos.preferencesExtensionDemo.showQuickActions')
|
||||
: $t('demos.preferencesExtensionDemo.hideQuickActions'),
|
||||
tone: toneConfig.label,
|
||||
})
|
||||
}}
|
||||
</template>
|
||||
</Alert>
|
||||
|
||||
<div class="mt-4 rounded-xl border p-4" :class="toneConfig.cardClass">
|
||||
<div class="mb-4 flex flex-wrap items-center justify-between gap-3">
|
||||
<div>
|
||||
<div class="text-lg font-semibold">
|
||||
{{ playgroundPreferences.reportTitle }}
|
||||
</div>
|
||||
<div class="text-sm text-foreground/60">
|
||||
{{ $t('demos.preferencesExtensionDemo.boardDescription') }}
|
||||
</div>
|
||||
</div>
|
||||
<Tag :color="toneConfig.tagColor">
|
||||
{{ toneConfig.label }}
|
||||
</Tag>
|
||||
</div>
|
||||
|
||||
<Space
|
||||
v-if="playgroundPreferences.enableQuickActions"
|
||||
wrap
|
||||
class="mb-4"
|
||||
>
|
||||
<Button type="primary">
|
||||
{{ $t('demos.preferencesExtensionDemo.quickActions.create') }}
|
||||
</Button>
|
||||
<Button>
|
||||
{{ $t('demos.preferencesExtensionDemo.quickActions.export') }}
|
||||
</Button>
|
||||
<Button>
|
||||
{{ $t('demos.preferencesExtensionDemo.quickActions.refresh') }}
|
||||
</Button>
|
||||
</Space>
|
||||
<div v-else class="mb-4 text-sm text-foreground/60">
|
||||
{{ $t('demos.preferencesExtensionDemo.quickActionsEnabled') }}
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<div
|
||||
v-for="task in visibleTasks"
|
||||
:key="task.id"
|
||||
class="flex flex-wrap items-center justify-between gap-3 rounded-lg border border-border bg-background px-4 py-3"
|
||||
>
|
||||
<div>
|
||||
<div class="font-medium">{{ task.title }}</div>
|
||||
<div class="text-sm text-foreground/60">
|
||||
{{ $t('demos.preferencesExtensionDemo.owner') }}:{{
|
||||
task.owner
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
<Tag :color="getPriorityColor(task.priority)">
|
||||
{{ task.priority }}
|
||||
</Tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card :title="$t('demos.preferencesExtensionDemo.presetTitle')">
|
||||
<Space wrap>
|
||||
<Button @click="applyPreset('focus')">
|
||||
{{ $t('demos.preferencesExtensionDemo.presetButtons.default') }}
|
||||
</Button>
|
||||
<Button @click="applyPreset('compact')">
|
||||
{{ $t('demos.preferencesExtensionDemo.presetButtons.compact') }}
|
||||
</Button>
|
||||
<Button type="primary" @click="applyPreset('review')">
|
||||
{{ $t('demos.preferencesExtensionDemo.presetButtons.review') }}
|
||||
</Button>
|
||||
</Space>
|
||||
|
||||
<pre :class="preClasses">{{ formattedPlaygroundPreferences }}</pre>
|
||||
</Card>
|
||||
</Page>
|
||||
</template>
|
||||
Reference in New Issue
Block a user