diff --git a/src/components/cards/DirectoryCard.vue b/src/components/cards/DirectoryCard.vue index e17dffa8..e72c9c71 100644 --- a/src/components/cards/DirectoryCard.vue +++ b/src/components/cards/DirectoryCard.vue @@ -75,10 +75,12 @@ const transferSourceItems = computed(() => [ { title: t('directory.manualTransfer'), value: 'manual' }, ]) +/** 判断存储类型是否具备预设强调色。 */ function hasKnownStorageType(storageType?: string): storageType is keyof typeof STORAGE_ACCENT_COLOR_MAP { return !!storageType && Object.prototype.hasOwnProperty.call(STORAGE_ACCENT_COLOR_MAP, storageType) } +/** 将十六进制颜色转换为 CSS RGB 通道字符串。 */ function hexToRgbString(hexColor: string) { const normalizedColor = hexColor.replace('#', '') const colorValue = Number.parseInt(normalizedColor, 16) @@ -88,6 +90,7 @@ function hexToRgbString(hexColor: string) { return `${(colorValue >> 16) & 255}, ${(colorValue >> 8) & 255}, ${colorValue & 255}` } +/** 根据自定义存储序号选取离散的强调色。 */ function getCustomStoragePaletteColor(storageType?: string) { const customStorageIndex = Math.max(Number(storageType?.match(/\d+$/)?.[0] ?? 1) - 1, 0) const customStorageColors = ['#F97316', '#8B5CF6', '#06B6D4', '#84CC16', '#EC4899', '#14B8A6'] @@ -95,6 +98,7 @@ function getCustomStoragePaletteColor(storageType?: string) { return customStorageColors[customStorageIndex % customStorageColors.length] } +/** 获取指定存储类型在目录卡片中使用的强调色。 */ function getStorageAccentColor(storageType?: string) { if (hasKnownStorageType(storageType)) return STORAGE_ACCENT_COLOR_MAP[storageType] @@ -108,6 +112,7 @@ const directoryAccentStyle = computed(() => ({ '--app-card-accent-end-rgb': libraryAccentRgb.value, })) +/** 根据目录两端的存储类型刷新卡片强调色。 */ function updateDirectoryAccentColors() { const downloadStorage = props.directory.storage const libraryStorage = props.directory.library_storage || props.directory.storage @@ -270,6 +275,7 @@ watch( v-model="props.directory.name" variant="underlined" :label="t('directory.alias')" + mobile-control-width="65%" class="me-20 text-high-emphasis font-weight-bold" /> @@ -287,6 +293,7 @@ watch( variant="underlined" :items="typeItems" :label="t('directory.mediaType')" + mobile-control-width="65%" @update:modelValue="props.directory.media_category = ''" /> @@ -296,6 +303,7 @@ watch( variant="underlined" :items="getCategories" :label="t('directory.mediaCategory')" + mobile-control-width="65%" /> @@ -304,6 +312,7 @@ watch( variant="underlined" :items="resourceStorageOptions" :label="t('directory.resourceStorage')" + mobile-control-width="65%" /> @@ -312,6 +321,7 @@ watch( :storage="props.directory.storage" variant="underlined" :label="t('directory.resourceDirectory')" + mobile-control-width="65%" /> @@ -332,6 +342,7 @@ watch( variant="underlined" :items="transferSourceItems" :label="t('directory.autoTransfer')" + mobile-control-width="65%" /> @@ -342,6 +353,7 @@ watch( variant="underlined" :items="MonitorModeItems" :label="t('directory.monitorMode')" + mobile-control-width="65%" /> @@ -350,6 +362,7 @@ watch( variant="underlined" :items="libraryStorageOptions" :label="t('directory.libraryStorage')" + mobile-control-width="65%" /> @@ -358,6 +371,7 @@ watch( :storage="props.directory.library_storage" variant="underlined" :label="t('directory.libraryDirectory')" + mobile-control-width="65%" /> @@ -367,6 +381,7 @@ watch( :items="transferTypeItems" :label="t('directory.transferType')" :no-data-text="computedNoDataText" + mobile-control-width="65%" /> @@ -375,6 +390,7 @@ watch( variant="underlined" :items="overwriteModeItems" :label="t('directory.overwriteMode')" + mobile-control-width="65%" /> diff --git a/src/components/cards/FilterRuleCard.vue b/src/components/cards/FilterRuleCard.vue index 4ef210d3..d04f3399 100644 --- a/src/components/cards/FilterRuleCard.vue +++ b/src/components/cards/FilterRuleCard.vue @@ -17,12 +17,12 @@ const props = defineProps({ // 定义触发的自定义事件 const emit = defineEmits(['close', 'changed']) -// 按钮点击 +/** 关闭当前优先级规则卡片。 */ function onClose() { emit('close') } -// 选项变化 +/** 将当前优先级的规则选择结果通知父组件。 */ function filtersChanged(value: string[]) { emit('changed', props.pri, value) } @@ -61,6 +61,7 @@ const selectFilterOptions = computed<{ [key: string]: string }[]>(() => { :items="selectFilterOptions" chips :label="t('filterRule.rules')" + mobile-control-width="80%" multiple clearable @update:modelValue="filtersChanged" diff --git a/src/components/field/PathField.vue b/src/components/field/PathField.vue index 751f5886..bee8daab 100644 --- a/src/components/field/PathField.vue +++ b/src/components/field/PathField.vue @@ -30,6 +30,7 @@ const propsWithoutModelValue = computed(() => { return { ...rest, ...attrs } }) +/** 同步路径输入值并向父组件派发更新。 */ function updateModelValue(value: string) { innerValue.value = value emit('update:modelValue', value) diff --git a/src/plugins/vuetify/AppInput.ts b/src/plugins/vuetify/AppInput.ts index 80e1fc9d..49eaa1c2 100644 --- a/src/plugins/vuetify/AppInput.ts +++ b/src/plugins/vuetify/AppInput.ts @@ -54,6 +54,21 @@ function mergeDescribedBy(...values: unknown[]) { )].join(' ') } +/** + * 将移动端右栏宽度参数转换为可用的 CSS 长度。 + */ +function normalizeMobileControlWidth(value: number | string | undefined) { + if (typeof value === 'number') { + if (!Number.isFinite(value)) return undefined + + return `${Math.min(100, Math.max(0, value))}%` + } + + if (typeof value !== 'string') return undefined + + return value.trim() || undefined +} + /** * 为原生 Vuetify 录入组件创建小屏两栏适配器,桌面端保持原组件渲染路径。 */ @@ -66,6 +81,10 @@ export function createResponsiveInputAdapter(component: Component, options: Resp type: Boolean, default: true, }, + mobileControlWidth: { + type: [Number, String], + default: undefined, + }, }, /** * 根据 Vuetify 断点切换布局,并保留原生控件常用的公开方法。 @@ -107,6 +126,7 @@ export function createResponsiveInputAdapter(component: Component, options: Resp const hintId = `${controlId}-hint` const rootClass = attrs.class const rootStyle = attrs.style + const mobileControlWidth = normalizeMobileControlWidth(props.mobileControlWidth) const controlAttrs: Record = { ...attrs } for (const key of ['class', 'hint', 'label', 'persistent-hint', 'persistentHint', 'style']) { @@ -156,7 +176,12 @@ export function createResponsiveInputAdapter(component: Component, options: Resp }, rootClass, ], - style: rootStyle, + style: [ + rootStyle, + mobileControlWidth + ? { '--app-responsive-input-control-width': mobileControlWidth } + : undefined, + ], }, [ h('div', { class: 'app-responsive-input__meta' }, [ h('label', { class: 'app-responsive-input__label', for: controlId }, labelContent), diff --git a/src/styles/common.scss b/src/styles/common.scss index 82020748..44894ffa 100644 --- a/src/styles/common.scss +++ b/src/styles/common.scss @@ -508,7 +508,7 @@ html[data-theme-radius='extra'] { padding-block: 0.75rem; align-items: center; column-gap: 1rem; - grid-template-columns: minmax(0, 1fr) minmax(7rem, 42%); + grid-template-columns: minmax(0, 1fr) minmax(7rem, var(--app-responsive-input-control-width, 42%)); } .app-responsive-input__meta, @@ -611,23 +611,46 @@ html[data-theme-radius='extra'] { white-space: nowrap; } -.app-responsive-input__native.v-select--multiple .v-field__input { +.app-responsive-input__native:is( + .v-select--multiple, + .v-autocomplete--multiple, + .v-combobox--multiple +) .v-field__input { overflow: visible; flex-wrap: wrap; row-gap: 0.25rem; } -.app-responsive-input__native.v-select--multiple :is(.v-select__selection, .v-select__selection-text) { +.app-responsive-input__native:is( + .v-select--multiple, + .v-autocomplete--multiple, + .v-combobox--multiple +) :is( + .v-select__selection, + .v-select__selection-text, + .v-autocomplete__selection, + .v-autocomplete__selection-text, + .v-combobox__selection, + .v-combobox__selection-text +) { overflow: visible; text-overflow: clip; white-space: normal; } -.app-responsive-input__native.v-select--multiple .v-chip { +.app-responsive-input__native:is( + .v-select--multiple, + .v-autocomplete--multiple, + .v-combobox--multiple +) .v-chip { max-inline-size: 100%; } -.app-responsive-input__native.v-select--multiple .v-chip__content { +.app-responsive-input__native:is( + .v-select--multiple, + .v-autocomplete--multiple, + .v-combobox--multiple +) .v-chip__content { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; @@ -713,10 +736,12 @@ html[data-theme-radius='extra'] { padding-block: 0; } - // 从第二个录入项开始绘制顶部线,稳定形成项间分隔且不会产生末尾尾线。 + // 从第二个录入项开始绘制顶部线;只匹配列直属输入或一层字段包装,避免穿透嵌套卡片。 .app-responsive-input ~ .app-responsive-input, - .v-row > :is(.v-col, [class*='v-col-']) ~ :is(.v-col, [class*='v-col-']) .app-responsive-input, - .v-row + .v-row > :is(.v-col, [class*='v-col-']):first-child .app-responsive-input { + .v-row > :is(.v-col, [class*='v-col-']) ~ :is(.v-col, [class*='v-col-']) > .app-responsive-input, + .v-row > :is(.v-col, [class*='v-col-']) ~ :is(.v-col, [class*='v-col-']) > * > .app-responsive-input, + .v-row + .v-row > :is(.v-col, [class*='v-col-']):first-child > .app-responsive-input, + .v-row + .v-row > :is(.v-col, [class*='v-col-']):first-child > * > .app-responsive-input { border-block-start: 1px solid rgba(var(--v-theme-on-surface), 0.08); }