Feature(custom): support adjust grid size in gallery page

ISSUES CLOSED: #419
This commit is contained in:
Kuingsmile
2025-12-30 16:01:48 +08:00
parent 391c52b160
commit a7691234b5
8 changed files with 128 additions and 13 deletions

View File

@@ -50,6 +50,7 @@
"@piclist/i18n": "^2.0.0", "@piclist/i18n": "^2.0.0",
"@piclist/store": "^3.0.0", "@piclist/store": "^3.0.0",
"@smithy/node-http-handler": "^4.4.7", "@smithy/node-http-handler": "^4.4.7",
"@vueuse/core": "^14.1.0",
"ali-oss": "^6.23.0", "ali-oss": "^6.23.0",
"axios": "^1.13.2", "axios": "^1.13.2",
"chalk": "^5.6.2", "chalk": "^5.6.2",

View File

@@ -45,6 +45,7 @@
"dateRange": "Date Range", "dateRange": "Date Range",
"delete": "Delete", "delete": "Delete",
"edit": "Edit", "edit": "Edit",
"gridSize": "Grid Size",
"gridView": "Grid", "gridView": "Grid",
"haveDuplicate": "There are naming duplicates among the selected images, do you want to continue?", "haveDuplicate": "There are naming duplicates among the selected images, do you want to continue?",
"hideFilters": "Filters", "hideFilters": "Filters",

View File

@@ -45,6 +45,7 @@
"dateRange": "日期范围", "dateRange": "日期范围",
"delete": "删除", "delete": "删除",
"edit": "编辑", "edit": "编辑",
"gridSize": "网格大小",
"gridView": "网格", "gridView": "网格",
"haveDuplicate": "已选中的图片中有命名重复, 是否继续?", "haveDuplicate": "已选中的图片中有命名重复, 是否继续?",
"hideFilters": "过滤器", "hideFilters": "过滤器",

View File

@@ -45,6 +45,7 @@
"dateRange": "日期範圍", "dateRange": "日期範圍",
"delete": "刪除", "delete": "刪除",
"edit": "編輯", "edit": "編輯",
"gridSize": "網格大小",
"gridView": "網格", "gridView": "網格",
"haveDuplicate": "已選中的圖片中有命名重複, 是否繼續?", "haveDuplicate": "已選中的圖片中有命名重複, 是否繼續?",
"hideFilters": "過濾器", "hideFilters": "過濾器",

View File

@@ -13,6 +13,18 @@
</div> </div>
</div> </div>
<div class="header-actions"> <div class="header-actions">
<div class="grid-size-control">
<GridIcon :size="14" />
<input
v-model.number="userGridColumns"
type="range"
min="1"
max="15"
step="1"
class="grid-slider"
:title="t('pages.gallery.gridSize')"
/>
</div>
<div class="sync-delete-toggle"> <div class="sync-delete-toggle">
<span class="toggle-label">{{ t('pages.gallery.isAlwaysForceReload') }}</span> <span class="toggle-label">{{ t('pages.gallery.isAlwaysForceReload') }}</span>
<label class="custom-switch"> <label class="custom-switch">
@@ -195,7 +207,7 @@
:items="filterList" :items="filterList"
:item-height="itemHeight" :item-height="itemHeight"
:grid-items="4" :grid-items="4"
:grid-breakpoints="gridBreakpoints" :grid-breakpoints="effectiveGridBreakpoints"
key-field="key" key-field="key"
:page-mode="true" :page-mode="true"
:buffer-factor="0.5" :buffer-factor="0.5"
@@ -468,6 +480,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { useStorage } from '@vueuse/core'
import { import {
CheckSquareIcon, CheckSquareIcon,
ChevronDownIcon, ChevronDownIcon,
@@ -564,18 +577,15 @@ const dateRangeEnd = ref('')
const picBedDropdownOpen = ref(false) const picBedDropdownOpen = ref(false)
const sortDropdownOpen = ref(false) const sortDropdownOpen = ref(false)
const showFormatInfo = ref(false) const showFormatInfo = ref(false)
const viewMode = ref<'list' | 'grid'>('grid') const viewMode = useStorage<'list' | 'grid'>('galleryViewMode', 'grid')
const componentKey = ref(0) const componentKey = ref(0)
const currentSortField = ref<'name' | 'time' | 'ext' | 'check'>('name') const currentSortField = ref<'name' | 'time' | 'ext' | 'check'>('name')
const userGridColumns = useStorage<number>('galleryGridColumns', 4)
const itemHeight = 300 const itemHeight = 300
const gridBreakpoints = [
{ min: 0, cols: 1 }, const effectiveGridBreakpoints = computed(() => {
{ min: 380, cols: 2 }, return [{ min: 0, cols: userGridColumns.value }]
{ min: 768, cols: 3 }, })
{ min: 1024, cols: 4 },
{ min: 1280, cols: 6 },
{ min: 1536, cols: 7 },
]
const imageLoadStates = reactive<Record<string, boolean>>({}) const imageLoadStates = reactive<Record<string, boolean>>({})
const imageErrorStates = reactive<Record<string, boolean>>({}) const imageErrorStates = reactive<Record<string, boolean>>({})
@@ -961,7 +971,6 @@ function handleImageTouchEnd(event: TouchEvent) {
function toggleViewMode() { function toggleViewMode() {
viewMode.value = viewMode.value === 'grid' ? 'list' : 'grid' viewMode.value = viewMode.value === 'grid' ? 'list' : 'grid'
localStorage.setItem('galleryViewMode', viewMode.value)
} }
function getViewModeIcon() { function getViewModeIcon() {
@@ -982,7 +991,6 @@ onBeforeRouteUpdate((to, from) => {
}) })
async function initConf() { async function initConf() {
viewMode.value = (localStorage.getItem('galleryViewMode') as 'list' | 'grid') || 'grid'
pasteStyle.value = (await getConfig(configPaths.settings.pasteStyle)) || IPasteStyle.MARKDOWN pasteStyle.value = (await getConfig(configPaths.settings.pasteStyle)) || IPasteStyle.MARKDOWN
useShortUrl.value = (await getConfig(configPaths.settings.useShortUrl)) useShortUrl.value = (await getConfig(configPaths.settings.useShortUrl))
? t('pages.gallery.shortUrl') ? t('pages.gallery.shortUrl')
@@ -1104,6 +1112,14 @@ watch(filterList, () => {
clearChoosedList() clearChoosedList()
}) })
watch(userGridColumns, _ => {
nextTick(() => {
if (virtualScrollerRef.value) {
virtualScrollerRef.value.refresh()
}
})
})
let searchDebounceTimer: ReturnType<typeof setTimeout> | null = null let searchDebounceTimer: ReturnType<typeof setTimeout> | null = null
let searchURLDebounceTimer: ReturnType<typeof setTimeout> | null = null let searchURLDebounceTimer: ReturnType<typeof setTimeout> | null = null

View File

@@ -719,7 +719,6 @@
<div class="form-group"> <div class="form-group">
<input v-model="customLink.value" type="text" class="form-input" :placeholder="'![$fileName]($url)'" /> <input v-model="customLink.value" type="text" class="form-input" :placeholder="'![$fileName]($url)'" />
</div> </div>
<small> ![$fileName]($url)</small>
</div> </div>
<div class="dialog-footer"> <div class="dialog-footer">
<button class="btn btn-secondary" @click="cancelCustomLink"> <button class="btn btn-secondary" @click="cancelCustomLink">

View File

@@ -181,6 +181,78 @@ input:checked + .switch-slider::before {
transform: translateX(20px); transform: translateX(20px);
} }
/* Grid Size Control */
.grid-size-control {
display: flex;
align-items: center;
border: 1px solid var(--color-border-secondary);
border-radius: var(--radius-md);
padding: 0.375rem 0.5rem;
background: var(--color-surface-elevated);
gap: 0.375rem;
}
.grid-size-control .control-label {
font-size: 0.75rem;
font-weight: 500;
white-space: nowrap;
color: var(--color-text-secondary);
}
.grid-slider {
border-radius: 2px;
width: 70px;
height: 4px;
background: var(--color-border);
outline: none;
appearance: none;
cursor: pointer;
}
.grid-slider::-webkit-slider-thumb {
border-radius: 50%;
width: 14px;
height: 14px;
background: var(--color-blue-common);
transition: var(--transition-fast);
appearance: none;
cursor: pointer;
}
.grid-slider::-webkit-slider-thumb:hover {
transform: scale(1.15);
box-shadow: 0 0 0 3px rgb(56 114 250 / 20%);
}
.grid-slider::-moz-range-thumb {
border: none;
border-radius: 50%;
width: 14px;
height: 14px;
background: var(--color-blue-common);
transition: var(--transition-fast);
cursor: pointer;
}
.grid-slider::-moz-range-thumb:hover {
transform: scale(1.15);
box-shadow: 0 0 0 3px rgb(56 114 250 / 20%);
}
.grid-value {
display: inline-flex;
justify-content: center;
align-items: center;
border-radius: var(--radius-sm);
padding: 0.125rem 0.375rem;
min-width: 20px;
font-size: 0.6875rem;
font-weight: 600;
color: white;
background: var(--color-blue-common);
line-height: 1;
}
/* Filter Card */ /* Filter Card */
.filter-card { .filter-card {
border-radius: var(--radius-lg); border-radius: var(--radius-lg);

View File

@@ -4461,6 +4461,11 @@
resolved "https://registry.yarnpkg.com/@types/video.js/-/video.js-7.3.58.tgz#7e8cdafee25c75d6eb18f530b93ac52edff53c03" resolved "https://registry.yarnpkg.com/@types/video.js/-/video.js-7.3.58.tgz#7e8cdafee25c75d6eb18f530b93ac52edff53c03"
integrity sha512-1CQjuSrgbv1/dhmcfQ83eVyYbvGyqhTvb2Opxr0QCV+iJ4J6/J+XWQ3Om59WiwCd1MN3rDUHasx5XRrpUtewYQ== integrity sha512-1CQjuSrgbv1/dhmcfQ83eVyYbvGyqhTvb2Opxr0QCV+iJ4J6/J+XWQ3Om59WiwCd1MN3rDUHasx5XRrpUtewYQ==
"@types/web-bluetooth@^0.0.21":
version "0.0.21"
resolved "https://registry.yarnpkg.com/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz#525433c784aed9b457aaa0ee3d92aeb71f346b63"
integrity sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==
"@types/write-file-atomic@^4.0.3": "@types/write-file-atomic@^4.0.3":
version "4.0.3" version "4.0.3"
resolved "https://registry.yarnpkg.com/@types/write-file-atomic/-/write-file-atomic-4.0.3.tgz#bda169b8369022e2c87028671fa4b742c08d98c9" resolved "https://registry.yarnpkg.com/@types/write-file-atomic/-/write-file-atomic-4.0.3.tgz#bda169b8369022e2c87028671fa4b742c08d98c9"
@@ -4934,6 +4939,25 @@
resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.5.26.tgz#1e02ef2d64aced818cd31d81ce5175711dc90a9f" resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.5.26.tgz#1e02ef2d64aced818cd31d81ce5175711dc90a9f"
integrity sha512-7Z6/y3uFI5PRoKeorTOSXKcDj0MSasfNNltcslbFrPpcw6aXRUALq4IfJlaTRspiWIUOEZbrpM+iQGmCOiWe4A== integrity sha512-7Z6/y3uFI5PRoKeorTOSXKcDj0MSasfNNltcslbFrPpcw6aXRUALq4IfJlaTRspiWIUOEZbrpM+iQGmCOiWe4A==
"@vueuse/core@^14.1.0":
version "14.1.0"
resolved "https://registry.yarnpkg.com/@vueuse/core/-/core-14.1.0.tgz#274e98e591a505333b7dfb2bcaf7b4530a10b9c9"
integrity sha512-rgBinKs07hAYyPF834mDTigH7BtPqvZ3Pryuzt1SD/lg5wEcWqvwzXXYGEDb2/cP0Sj5zSvHl3WkmMELr5kfWw==
dependencies:
"@types/web-bluetooth" "^0.0.21"
"@vueuse/metadata" "14.1.0"
"@vueuse/shared" "14.1.0"
"@vueuse/metadata@14.1.0":
version "14.1.0"
resolved "https://registry.yarnpkg.com/@vueuse/metadata/-/metadata-14.1.0.tgz#70fc2e94775e4a07369f11f86f6f0a465b04a381"
integrity sha512-7hK4g015rWn2PhKcZ99NyT+ZD9sbwm7SGvp7k+k+rKGWnLjS/oQozoIZzWfCewSUeBmnJkIb+CNr7Zc/EyRnnA==
"@vueuse/shared@14.1.0":
version "14.1.0"
resolved "https://registry.yarnpkg.com/@vueuse/shared/-/shared-14.1.0.tgz#49b2face86a9c0c52e20eaf4c732a0223276c11f"
integrity sha512-EcKxtYvn6gx1F8z9J5/rsg3+lTQnvOruQd8fUecW99DCK04BkWD7z5KQ/wTAx+DazyoEE9dJt/zV8OIEQbM6kw==
"@xmldom/xmldom@^0.8.3": "@xmldom/xmldom@^0.8.3":
version "0.8.6" version "0.8.6"
resolved "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.6.tgz#8a1524eb5bd5e965c1e3735476f0262469f71440" resolved "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.6.tgz#8a1524eb5bd5e965c1e3735476f0262469f71440"