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' },
])
/** 判断存储类型是否具备预设强调色。 */
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"
/>
<span class="app-card-top-action absolute top-3 right-12">
@@ -287,6 +293,7 @@ watch(
variant="underlined"
:items="typeItems"
:label="t('directory.mediaType')"
mobile-control-width="65%"
@update:modelValue="props.directory.media_category = ''"
/>
</VCol>
@@ -296,6 +303,7 @@ watch(
variant="underlined"
:items="getCategories"
:label="t('directory.mediaCategory')"
mobile-control-width="65%"
/>
</VCol>
<VCol cols="4">
@@ -304,6 +312,7 @@ watch(
variant="underlined"
:items="resourceStorageOptions"
:label="t('directory.resourceStorage')"
mobile-control-width="65%"
/>
</VCol>
<VCol cols="8">
@@ -312,6 +321,7 @@ watch(
:storage="props.directory.storage"
variant="underlined"
:label="t('directory.resourceDirectory')"
mobile-control-width="65%"
/>
</VCol>
<VCol cols="6" v-if="!props.directory.media_type || props.directory.media_type === ''">
@@ -332,6 +342,7 @@ watch(
variant="underlined"
:items="transferSourceItems"
:label="t('directory.autoTransfer')"
mobile-control-width="65%"
/>
</VCol>
</VRow>
@@ -342,6 +353,7 @@ watch(
variant="underlined"
:items="MonitorModeItems"
:label="t('directory.monitorMode')"
mobile-control-width="65%"
/>
</VCol>
<VCol cols="4">
@@ -350,6 +362,7 @@ watch(
variant="underlined"
:items="libraryStorageOptions"
:label="t('directory.libraryStorage')"
mobile-control-width="65%"
/>
</VCol>
<VCol cols="8">
@@ -358,6 +371,7 @@ watch(
:storage="props.directory.library_storage"
variant="underlined"
:label="t('directory.libraryDirectory')"
mobile-control-width="65%"
/>
</VCol>
<VCol cols="4">
@@ -367,6 +381,7 @@ watch(
:items="transferTypeItems"
:label="t('directory.transferType')"
:no-data-text="computedNoDataText"
mobile-control-width="65%"
/>
</VCol>
<VCol cols="8">
@@ -375,6 +390,7 @@ watch(
variant="underlined"
:items="overwriteModeItems"
:label="t('directory.overwriteMode')"
mobile-control-width="65%"
/>
</VCol>
<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'])
// 按钮点击
/** 关闭当前优先级规则卡片。 */
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"
+1
View File
@@ -30,6 +30,7 @@ const propsWithoutModelValue = computed(() => {
return { ...rest, ...attrs }
})
/** 同步路径输入值并向父组件派发更新。 */
function updateModelValue(value: string) {
innerValue.value = value
emit('update:modelValue', value)
+26 -1
View File
@@ -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<string, unknown> = { ...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),
+33 -8
View File
@@ -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);
}