Improve responsive input layouts and mobile control widths

This commit is contained in:
jxxghp
2026-07-15 13:44:12 +08:00
parent 4e0191ba5c
commit 9c2c4d9fc4
5 changed files with 79 additions and 11 deletions
+16
View File
@@ -75,10 +75,12 @@ const transferSourceItems = computed(() => [
{ title: t('directory.manualTransfer'), value: 'manual' }, { title: t('directory.manualTransfer'), value: 'manual' },
]) ])
/** 判断存储类型是否具备预设强调色。 */
function hasKnownStorageType(storageType?: string): storageType is keyof typeof STORAGE_ACCENT_COLOR_MAP { function hasKnownStorageType(storageType?: string): storageType is keyof typeof STORAGE_ACCENT_COLOR_MAP {
return !!storageType && Object.prototype.hasOwnProperty.call(STORAGE_ACCENT_COLOR_MAP, storageType) return !!storageType && Object.prototype.hasOwnProperty.call(STORAGE_ACCENT_COLOR_MAP, storageType)
} }
/** 将十六进制颜色转换为 CSS RGB 通道字符串。 */
function hexToRgbString(hexColor: string) { function hexToRgbString(hexColor: string) {
const normalizedColor = hexColor.replace('#', '') const normalizedColor = hexColor.replace('#', '')
const colorValue = Number.parseInt(normalizedColor, 16) const colorValue = Number.parseInt(normalizedColor, 16)
@@ -88,6 +90,7 @@ function hexToRgbString(hexColor: string) {
return `${(colorValue >> 16) & 255}, ${(colorValue >> 8) & 255}, ${colorValue & 255}` return `${(colorValue >> 16) & 255}, ${(colorValue >> 8) & 255}, ${colorValue & 255}`
} }
/** 根据自定义存储序号选取离散的强调色。 */
function getCustomStoragePaletteColor(storageType?: string) { function getCustomStoragePaletteColor(storageType?: string) {
const customStorageIndex = Math.max(Number(storageType?.match(/\d+$/)?.[0] ?? 1) - 1, 0) const customStorageIndex = Math.max(Number(storageType?.match(/\d+$/)?.[0] ?? 1) - 1, 0)
const customStorageColors = ['#F97316', '#8B5CF6', '#06B6D4', '#84CC16', '#EC4899', '#14B8A6'] const customStorageColors = ['#F97316', '#8B5CF6', '#06B6D4', '#84CC16', '#EC4899', '#14B8A6']
@@ -95,6 +98,7 @@ function getCustomStoragePaletteColor(storageType?: string) {
return customStorageColors[customStorageIndex % customStorageColors.length] return customStorageColors[customStorageIndex % customStorageColors.length]
} }
/** 获取指定存储类型在目录卡片中使用的强调色。 */
function getStorageAccentColor(storageType?: string) { function getStorageAccentColor(storageType?: string) {
if (hasKnownStorageType(storageType)) return STORAGE_ACCENT_COLOR_MAP[storageType] if (hasKnownStorageType(storageType)) return STORAGE_ACCENT_COLOR_MAP[storageType]
@@ -108,6 +112,7 @@ const directoryAccentStyle = computed(() => ({
'--app-card-accent-end-rgb': libraryAccentRgb.value, '--app-card-accent-end-rgb': libraryAccentRgb.value,
})) }))
/** 根据目录两端的存储类型刷新卡片强调色。 */
function updateDirectoryAccentColors() { function updateDirectoryAccentColors() {
const downloadStorage = props.directory.storage const downloadStorage = props.directory.storage
const libraryStorage = props.directory.library_storage || props.directory.storage const libraryStorage = props.directory.library_storage || props.directory.storage
@@ -270,6 +275,7 @@ watch(
v-model="props.directory.name" v-model="props.directory.name"
variant="underlined" variant="underlined"
:label="t('directory.alias')" :label="t('directory.alias')"
mobile-control-width="65%"
class="me-20 text-high-emphasis font-weight-bold" class="me-20 text-high-emphasis font-weight-bold"
/> />
<span class="app-card-top-action absolute top-3 right-12"> <span class="app-card-top-action absolute top-3 right-12">
@@ -287,6 +293,7 @@ watch(
variant="underlined" variant="underlined"
:items="typeItems" :items="typeItems"
:label="t('directory.mediaType')" :label="t('directory.mediaType')"
mobile-control-width="65%"
@update:modelValue="props.directory.media_category = ''" @update:modelValue="props.directory.media_category = ''"
/> />
</VCol> </VCol>
@@ -296,6 +303,7 @@ watch(
variant="underlined" variant="underlined"
:items="getCategories" :items="getCategories"
:label="t('directory.mediaCategory')" :label="t('directory.mediaCategory')"
mobile-control-width="65%"
/> />
</VCol> </VCol>
<VCol cols="4"> <VCol cols="4">
@@ -304,6 +312,7 @@ watch(
variant="underlined" variant="underlined"
:items="resourceStorageOptions" :items="resourceStorageOptions"
:label="t('directory.resourceStorage')" :label="t('directory.resourceStorage')"
mobile-control-width="65%"
/> />
</VCol> </VCol>
<VCol cols="8"> <VCol cols="8">
@@ -312,6 +321,7 @@ watch(
:storage="props.directory.storage" :storage="props.directory.storage"
variant="underlined" variant="underlined"
:label="t('directory.resourceDirectory')" :label="t('directory.resourceDirectory')"
mobile-control-width="65%"
/> />
</VCol> </VCol>
<VCol cols="6" v-if="!props.directory.media_type || props.directory.media_type === ''"> <VCol cols="6" v-if="!props.directory.media_type || props.directory.media_type === ''">
@@ -332,6 +342,7 @@ watch(
variant="underlined" variant="underlined"
:items="transferSourceItems" :items="transferSourceItems"
:label="t('directory.autoTransfer')" :label="t('directory.autoTransfer')"
mobile-control-width="65%"
/> />
</VCol> </VCol>
</VRow> </VRow>
@@ -342,6 +353,7 @@ watch(
variant="underlined" variant="underlined"
:items="MonitorModeItems" :items="MonitorModeItems"
:label="t('directory.monitorMode')" :label="t('directory.monitorMode')"
mobile-control-width="65%"
/> />
</VCol> </VCol>
<VCol cols="4"> <VCol cols="4">
@@ -350,6 +362,7 @@ watch(
variant="underlined" variant="underlined"
:items="libraryStorageOptions" :items="libraryStorageOptions"
:label="t('directory.libraryStorage')" :label="t('directory.libraryStorage')"
mobile-control-width="65%"
/> />
</VCol> </VCol>
<VCol cols="8"> <VCol cols="8">
@@ -358,6 +371,7 @@ watch(
:storage="props.directory.library_storage" :storage="props.directory.library_storage"
variant="underlined" variant="underlined"
:label="t('directory.libraryDirectory')" :label="t('directory.libraryDirectory')"
mobile-control-width="65%"
/> />
</VCol> </VCol>
<VCol cols="4"> <VCol cols="4">
@@ -367,6 +381,7 @@ watch(
:items="transferTypeItems" :items="transferTypeItems"
:label="t('directory.transferType')" :label="t('directory.transferType')"
:no-data-text="computedNoDataText" :no-data-text="computedNoDataText"
mobile-control-width="65%"
/> />
</VCol> </VCol>
<VCol cols="8"> <VCol cols="8">
@@ -375,6 +390,7 @@ watch(
variant="underlined" variant="underlined"
:items="overwriteModeItems" :items="overwriteModeItems"
:label="t('directory.overwriteMode')" :label="t('directory.overwriteMode')"
mobile-control-width="65%"
/> />
</VCol> </VCol>
<VCol cols="6" v-if="!props.directory.media_type || props.directory.media_type === ''"> <VCol cols="6" v-if="!props.directory.media_type || props.directory.media_type === ''">
+3 -2
View File
@@ -17,12 +17,12 @@ const props = defineProps({
// 定义触发的自定义事件 // 定义触发的自定义事件
const emit = defineEmits(['close', 'changed']) const emit = defineEmits(['close', 'changed'])
// 按钮点击 /** 关闭当前优先级规则卡片。 */
function onClose() { function onClose() {
emit('close') emit('close')
} }
// 选项变化 /** 将当前优先级的规则选择结果通知父组件。 */
function filtersChanged(value: string[]) { function filtersChanged(value: string[]) {
emit('changed', props.pri, value) emit('changed', props.pri, value)
} }
@@ -61,6 +61,7 @@ const selectFilterOptions = computed<{ [key: string]: string }[]>(() => {
:items="selectFilterOptions" :items="selectFilterOptions"
chips chips
:label="t('filterRule.rules')" :label="t('filterRule.rules')"
mobile-control-width="80%"
multiple multiple
clearable clearable
@update:modelValue="filtersChanged" @update:modelValue="filtersChanged"
+1
View File
@@ -30,6 +30,7 @@ const propsWithoutModelValue = computed(() => {
return { ...rest, ...attrs } return { ...rest, ...attrs }
}) })
/** 同步路径输入值并向父组件派发更新。 */
function updateModelValue(value: string) { function updateModelValue(value: string) {
innerValue.value = value innerValue.value = value
emit('update:modelValue', value) emit('update:modelValue', value)
+26 -1
View File
@@ -54,6 +54,21 @@ function mergeDescribedBy(...values: unknown[]) {
)].join(' ') )].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 录入组件创建小屏两栏适配器,桌面端保持原组件渲染路径。 * 为原生 Vuetify 录入组件创建小屏两栏适配器,桌面端保持原组件渲染路径。
*/ */
@@ -66,6 +81,10 @@ export function createResponsiveInputAdapter(component: Component, options: Resp
type: Boolean, type: Boolean,
default: true, default: true,
}, },
mobileControlWidth: {
type: [Number, String],
default: undefined,
},
}, },
/** /**
* 根据 Vuetify 断点切换布局,并保留原生控件常用的公开方法。 * 根据 Vuetify 断点切换布局,并保留原生控件常用的公开方法。
@@ -107,6 +126,7 @@ export function createResponsiveInputAdapter(component: Component, options: Resp
const hintId = `${controlId}-hint` const hintId = `${controlId}-hint`
const rootClass = attrs.class const rootClass = attrs.class
const rootStyle = attrs.style const rootStyle = attrs.style
const mobileControlWidth = normalizeMobileControlWidth(props.mobileControlWidth)
const controlAttrs: Record<string, unknown> = { ...attrs } const controlAttrs: Record<string, unknown> = { ...attrs }
for (const key of ['class', 'hint', 'label', 'persistent-hint', 'persistentHint', 'style']) { for (const key of ['class', 'hint', 'label', 'persistent-hint', 'persistentHint', 'style']) {
@@ -156,7 +176,12 @@ export function createResponsiveInputAdapter(component: Component, options: Resp
}, },
rootClass, rootClass,
], ],
style: rootStyle, style: [
rootStyle,
mobileControlWidth
? { '--app-responsive-input-control-width': mobileControlWidth }
: undefined,
],
}, [ }, [
h('div', { class: 'app-responsive-input__meta' }, [ h('div', { class: 'app-responsive-input__meta' }, [
h('label', { class: 'app-responsive-input__label', for: controlId }, labelContent), h('label', { class: 'app-responsive-input__label', for: controlId }, labelContent),
+33 -8
View File
@@ -508,7 +508,7 @@ html[data-theme-radius='extra'] {
padding-block: 0.75rem; padding-block: 0.75rem;
align-items: center; align-items: center;
column-gap: 1rem; 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, .app-responsive-input__meta,
@@ -611,23 +611,46 @@ html[data-theme-radius='extra'] {
white-space: nowrap; 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; overflow: visible;
flex-wrap: wrap; flex-wrap: wrap;
row-gap: 0.25rem; 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; overflow: visible;
text-overflow: clip; text-overflow: clip;
white-space: normal; 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%; 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; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
@@ -713,10 +736,12 @@ html[data-theme-radius='extra'] {
padding-block: 0; padding-block: 0;
} }
// 从第二个录入项开始绘制顶部线,稳定形成项间分隔且不会产生末尾尾线 // 从第二个录入项开始绘制顶部线;只匹配列直属输入或一层字段包装,避免穿透嵌套卡片
.app-responsive-input ~ .app-responsive-input, .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 > :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 + .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); border-block-start: 1px solid rgba(var(--v-theme-on-surface), 0.08);
} }