fix(webui): restore config page — naive-ui theme crash and missing controls

Two regressions from the component-system unification left the Config
page blank or control-less:

- App.vue passed var(--color-danger) as common.errorColor*; naive-ui
  runs JS color math (seemly rgba) on these tokens and threw
  "Invalid color value var(...)" while rendering NSelect, killing the
  whole page subtree. Error colors are now per-theme literals.
- ab-setting.vue still referenced the deleted AbSwitch/AbSelect
  primitives, which auto-resolution silently rendered as nothing —
  every switch/select in settings vanished. Replaced with themed
  NSwitch/NSelect plus an items→options adapter for the old API.
- Bumped naive-ui 2.39→2.44: 2.39 predates Vue 3.5 and crashed NTag
  rendering inside NDynamicTags ("theme.self undefined").

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTnuEgR78cBk32bLNKar3z
This commit is contained in:
Estrella Pan
2026-07-02 21:33:02 +02:00
parent a630d10cde
commit d80bf89cfa
5 changed files with 90 additions and 61 deletions

View File

@@ -24,7 +24,7 @@
"@vueuse/components": "^10.11.1",
"@vueuse/core": "^10.11.1",
"axios": "^1.7.9",
"naive-ui": "^2.39.0",
"naive-ui": "^2.44.1",
"pinia": "^2.2.2",
"vue": "^3.5.8",
"vue-i18n": "^9.14.2",

75
webui/pnpm-lock.yaml generated
View File

@@ -21,8 +21,8 @@ importers:
specifier: ^1.7.9
version: 1.18.1
naive-ui:
specifier: ^2.39.0
version: 2.39.0(vue@3.5.8(typescript@4.9.5))
specifier: ^2.44.1
version: 2.44.1(vue@3.5.8(typescript@4.9.5))
pinia:
specifier: ^2.2.2
version: 2.2.2(typescript@4.9.5)(vue@3.5.8(typescript@4.9.5))
@@ -1912,12 +1912,12 @@ packages:
'@types/json5@0.0.29':
resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==}
'@types/katex@0.16.7':
resolution: {integrity: sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==}
'@types/lodash-es@4.17.12':
resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==}
'@types/lodash@4.17.24':
resolution: {integrity: sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==}
'@types/lodash@4.17.7':
resolution: {integrity: sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA==}
@@ -2794,14 +2794,13 @@ packages:
resolution: {integrity: sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==}
engines: {node: '>= 0.4'}
date-fns-tz@2.0.1:
resolution: {integrity: sha512-fJCG3Pwx8HUoLhkepdsP7Z5RsucUi+ZBOxyM5d0ZZ6c4SdYustq0VMmOu6Wf7bli+yS/Jwp91TOCqn9jMcVrUA==}
date-fns-tz@3.2.0:
resolution: {integrity: sha512-sg8HqoTEulcbbbVXeg84u5UnlsQa8GS5QXMqjjYIhS4abEVVKIUwe0/l/UhrZdKaL/W5eWZNlbTeEIiOXTcsBQ==}
peerDependencies:
date-fns: 2.x
date-fns: ^3.0.0 || ^4.0.0
date-fns@2.30.0:
resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==}
engines: {node: '>=0.11'}
date-fns@4.4.0:
resolution: {integrity: sha512-+1UMbeh68lH1SegH83CGWwpb6OHHbpSgr3+s5Eww5M4CAgswBpoWS0AjTOfEJ33HiYKz1hdj/KTFprzXHmq/6w==}
date-time@3.1.0:
resolution: {integrity: sha512-uqCUKXE5q1PNBXjPqvwhwJf9SwMoAHBgWJ6DcrnS5o+W2JOiIILl0JEdVD8SGujrNS02GGxgwAg2PN2zONgtjg==}
@@ -4367,8 +4366,9 @@ packages:
muggle-string@0.3.1:
resolution: {integrity: sha512-ckmWDJjphvd/FvZawgygcUeQCxzvohjFO5RxTjj4eq8kw359gFF3E1brjfI+viLMxss5JrHTDRHZvu2/tuy0Qg==}
naive-ui@2.39.0:
resolution: {integrity: sha512-5oUJzRG+rtLSH8eRU+fJvVYiQids2BxF9jp+fwGoAqHOptEINrBlgBu9uy+95RHE5FLJ7Q/z41o+qkoGnUrKxQ==}
naive-ui@2.44.1:
resolution: {integrity: sha512-reo8Esw0p58liZwbUutC7meW24Xbn3EwNv91zReWKm2W4JPu+zfgJRn/F7aO0BFmvN+h2brA2M5lRvYqLq4kuA==}
engines: {node: '>=20'}
peerDependencies:
vue: ^3.0.0
@@ -5158,8 +5158,8 @@ packages:
scule@1.3.0:
resolution: {integrity: sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==}
seemly@0.3.8:
resolution: {integrity: sha512-MW8Qs6vbzo0pHmDpFSYPna+lwpZ6Zk1ancbajw/7E8TKtHdV+1DfZZD+kKJEhG/cAoB/i+LiT+5msZOqj0DwRA==}
seemly@0.3.10:
resolution: {integrity: sha512-2+SMxtG1PcsL0uyhkumlOU6Qo9TAQ/WyH7tthnPIOQB05/12jz9naq6GZ6iZ6ApVsO3rr2gsnTf3++OV63kE1Q==}
semver@5.7.2:
resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==}
@@ -5926,8 +5926,8 @@ packages:
peerDependencies:
vue: ^3.0.1
vueuc@0.4.63:
resolution: {integrity: sha512-QJT0z9yYWXdKpUq6f6IrAgJ83e34iTYMCVHjcAP8lCjldG0JzHnDfJYMpPWkNuLB5SdBZCbYGmYTKnTR+ff7CQ==}
vueuc@0.4.65:
resolution: {integrity: sha512-lXuMl+8gsBmruudfxnMF9HW4be8rFziylXFu1VHVNbLVhRTXXV4njvpRuJapD/8q+oFEMSfQMH16E/85VoWRyQ==}
peerDependencies:
vue: ^3.0.11
@@ -8511,11 +8511,11 @@ snapshots:
'@types/json5@0.0.29': {}
'@types/katex@0.16.7': {}
'@types/lodash-es@4.17.12':
dependencies:
'@types/lodash': 4.17.7
'@types/lodash': 4.17.24
'@types/lodash@4.17.24': {}
'@types/lodash@4.17.7': {}
@@ -9620,13 +9620,11 @@ snapshots:
es-errors: 1.3.0
is-data-view: 1.0.1
date-fns-tz@2.0.1(date-fns@2.30.0):
date-fns-tz@3.2.0(date-fns@4.4.0):
dependencies:
date-fns: 2.30.0
date-fns: 4.4.0
date-fns@2.30.0:
dependencies:
'@babel/runtime': 7.25.6
date-fns@4.4.0: {}
date-time@3.1.0:
dependencies:
@@ -11375,28 +11373,27 @@ snapshots:
muggle-string@0.3.1: {}
naive-ui@2.39.0(vue@3.5.8(typescript@4.9.5)):
naive-ui@2.44.1(vue@3.5.8(typescript@4.9.5)):
dependencies:
'@css-render/plugin-bem': 0.15.14(css-render@0.15.14)
'@css-render/vue3-ssr': 0.15.14(vue@3.5.8(typescript@4.9.5))
'@types/katex': 0.16.7
'@types/lodash': 4.17.7
'@types/lodash': 4.17.24
'@types/lodash-es': 4.17.12
async-validator: 4.2.5
css-render: 0.15.14
csstype: 3.1.3
date-fns: 2.30.0
date-fns-tz: 2.0.1(date-fns@2.30.0)
date-fns: 4.4.0
date-fns-tz: 3.2.0(date-fns@4.4.0)
evtd: 0.2.4
highlight.js: 11.10.0
lodash: 4.17.21
lodash-es: 4.17.21
seemly: 0.3.8
seemly: 0.3.10
treemate: 0.3.11
vdirs: 0.1.8(vue@3.5.8(typescript@4.9.5))
vooks: 0.2.12(vue@3.5.8(typescript@4.9.5))
vue: 3.5.8(typescript@4.9.5)
vueuc: 0.4.63(vue@3.5.8(typescript@4.9.5))
vueuc: 0.4.65(vue@3.5.8(typescript@4.9.5))
nanoid@3.3.7: {}
@@ -12193,7 +12190,7 @@ snapshots:
scule@1.3.0: {}
seemly@0.3.8: {}
seemly@0.3.10: {}
semver@5.7.2: {}
@@ -12359,10 +12356,10 @@ snapshots:
define-properties: 1.2.1
es-abstract: 1.23.3
es-errors: 1.3.0
es-object-atoms: 1.0.0
get-intrinsic: 1.2.4
gopd: 1.0.1
has-symbols: 1.0.3
es-object-atoms: 1.1.2
get-intrinsic: 1.3.0
gopd: 1.2.0
has-symbols: 1.1.0
internal-slot: 1.0.7
regexp.prototype.flags: 1.5.2
set-function-name: 2.0.2
@@ -13048,13 +13045,13 @@ snapshots:
sortablejs: 1.14.0
vue: 3.5.8(typescript@4.9.5)
vueuc@0.4.63(vue@3.5.8(typescript@4.9.5)):
vueuc@0.4.65(vue@3.5.8(typescript@4.9.5)):
dependencies:
'@css-render/vue3-ssr': 0.15.14(vue@3.5.8(typescript@4.9.5))
'@juggle/resize-observer': 3.4.0
css-render: 0.15.14
evtd: 0.2.4
seemly: 0.3.8
seemly: 0.3.10
vdirs: 0.1.8(vue@3.5.8(typescript@4.9.5))
vooks: 0.2.12(vue@3.5.8(typescript@4.9.5))
vue: 3.5.8(typescript@4.9.5)

View File

@@ -17,13 +17,10 @@ if (isLoggedIn.value) {
// Button/Select/Switch tokens reference the app's own CSS custom properties
// directly (they resolve correctly under both `:root` and `.dark`), so they
// don't need to be duplicated between the light/dark override objects below.
// 注意:common 里的颜色 token 会进入 naive-ui (seemly) 的 JS 颜色计算,
// 写 CSS 变量会抛 "[seemly/rgba]: Invalid color value var(...)" 并让整个
// 页面渲染崩溃(Config 页曾因此白屏)——必须用字面量,按主题各写一份。
const primitiveOverrides: GlobalThemeOverrides = {
common: {
errorColor: 'var(--color-danger)',
errorColorHover: 'var(--color-danger)',
errorColorPressed: 'var(--color-danger)',
errorColorSuppl: 'var(--color-danger)',
},
Button: {
borderRadiusTiny: 'var(--radius-sm)',
borderRadiusSmall: 'var(--radius-sm)',
@@ -38,7 +35,10 @@ const primitiveOverrides: GlobalThemeOverrides = {
const lightOverrides: GlobalThemeOverrides = {
...primitiveOverrides,
common: {
...primitiveOverrides.common,
errorColor: '#EF4444',
errorColorHover: '#DC2626',
errorColorPressed: '#B91C1C',
errorColorSuppl: '#EF4444',
primaryColor: '#6C4AB6',
primaryColorHover: '#563A92',
primaryColorPressed: '#4A3291',
@@ -70,7 +70,10 @@ const lightOverrides: GlobalThemeOverrides = {
const darkOverrides: GlobalThemeOverrides = {
...primitiveOverrides,
common: {
...primitiveOverrides.common,
errorColor: '#F87171',
errorColorHover: '#EF4444',
errorColorPressed: '#DC2626',
errorColorSuppl: '#F87171',
primaryColor: '#8B6CC7',
primaryColorHover: '#A78BDB',
primaryColorPressed: '#7B5CB7',

View File

@@ -38,7 +38,7 @@ const abLabel = computed(() => {
// Make inputs full-width on mobile
:deep(input[ab-input]),
:deep(.ab-select),
:deep(.n-select),
:deep(.n-dynamic-tags) {
width: 100%;

View File

@@ -1,32 +1,56 @@
<script lang="ts" setup>
import { NDynamicTags } from 'naive-ui';
import type { AbSettingProps } from '#/components';
import { NDynamicTags, NSelect, NSwitch } from 'naive-ui';
import { isObject } from 'radash';
import type { SelectOption } from 'naive-ui';
import type { AbSettingProps, SelectItem } from '#/components';
withDefaults(defineProps<AbSettingProps>(), {
const props = withDefaults(defineProps<AbSettingProps>(), {
css: '',
bottomLine: false,
});
const data = defineModel<any>('data');
// 旧 AbSelect 接受 items: Array<SelectItem | string>;NSelect 需要
// options: Array<{ label, value }>,这里做一层适配
const selectOptions = computed<SelectOption[]>(() => {
const items: Array<SelectItem | string> = props.prop?.items ?? [];
return items.map((item) =>
isObject(item)
? { label: item.label ?? item.value, value: item.value }
: { label: item, value: item }
);
});
// 旧组件允许 v-model 绑定对象(SelectItem);NSelect 只接受基础类型,
// 读取时取 .value,写回时保持字符串(配置值均为字符串)
const selectValue = computed<string | number | null>({
get() {
return isObject(data.value) ? (data.value as SelectItem).value : data.value;
},
set(value) {
data.value = value;
},
});
</script>
<template>
<div class="setting-item">
<ab-label :label="label">
<AbSwitch
<NSwitch
v-if="type === 'switch'"
v-model:checked="data"
v-bind="prop"
v-model:value="data"
:class="css"
></AbSwitch>
></NSwitch>
<AbSelect
<NSelect
v-else-if="type === 'select'"
v-model="data"
v-bind="prop"
v-model:value="selectValue"
:options="selectOptions"
:class="css"
></AbSelect>
class="setting-select"
></NSelect>
<input
v-else-if="type === 'input'"
@@ -49,6 +73,11 @@ const data = defineModel<any>('data');
.setting-item {
width: 100%;
// 无宽度 class 的下拉框给一个下限,避免窄到只剩箭头
.setting-select {
min-width: 140px;
}
// Prevent fixed-width inputs from causing horizontal overflow on mobile
:deep(input),
:deep(select),