mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-05 23:56:42 +08:00
Improve responsive input layouts and mobile control widths
This commit is contained in:
@@ -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 === ''">
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user