🚧 WIP(custom): rewrite manage page

This commit is contained in:
Kuingsmile
2025-08-09 23:26:00 +08:00
parent 3cb17aaf1d
commit a0cfd2a408
14 changed files with 5300 additions and 3313 deletions

View File

@@ -1185,7 +1185,7 @@ small {
/* Dark mode adjustments */
:root.dark .image-process-settings,
:root.auto.dark .image-process-settings {
background: var(--color-background-primary);
background: var(--color-background-secondary);
}
:root.dark .settings-header,

View File

@@ -63,6 +63,7 @@ const props = withDefaults(defineProps<{
const containerRef = ref<HTMLElement | null>(null)
const containerHeight = ref<number>(props.pageMode ? 0 : props.height)
const containerWidth = ref<number>(0)
const parentScrollListeners = ref<HTMLElement[]>([])
const itemsRef = ref<Item[]>(props.items)
watch(() => props.items, v => { itemsRef.value = v })
@@ -124,7 +125,7 @@ const viewportStyle = computed(() => {
const itemStyle = computed(() =>
isGridMode.value
? {}
: { height: `${props.itemHeight}px`, padding: `${props.itemPadding}px` }
: { height: `${props.itemHeight}px` }
)
function handleScroll () {
@@ -133,7 +134,34 @@ function handleScroll () {
updateScrollTop(c.scrollTop)
}
function handlePageScroll () {
if (!props.pageMode) return
// Throttle the scroll handler for better performance
const now = Date.now()
if (now - lastScrollTime.value < 16) return // ~60fps
lastScrollTime.value = now
updateContainerMetrics()
// When in page mode, recalculate visible items based on viewport intersection
const el = containerRef.value
if (!el) return
const rect = el.getBoundingClientRect()
const viewportHeight = window.innerHeight
// Calculate the intersection with the viewport
const intersectionTop = Math.max(0, -rect.top)
const intersectionBottom = Math.min(rect.height, viewportHeight - rect.top)
const intersectionHeight = Math.max(0, intersectionBottom - intersectionTop)
if (intersectionHeight > 0) {
// Update the virtual scroll position based on the intersection
updateScrollTop(intersectionTop)
}
}
let ro: ResizeObserver | null = null
const lastScrollTime = ref(0)
function updateContainerMetrics () {
const el = containerRef.value
@@ -152,7 +180,20 @@ onMounted(() => {
if (!el) return
ro = new ResizeObserver(updateContainerMetrics)
ro.observe(el)
if (props.pageMode) ro.observe(document.documentElement)
if (props.pageMode) {
ro.observe(document.documentElement)
// Listen to scroll events on the window for page mode
window.addEventListener('scroll', handlePageScroll, { passive: true })
// Also listen to scroll events on potential scroll containers
let parent = el.parentElement
while (parent) {
if (parent.scrollHeight > parent.clientHeight) {
parent.addEventListener('scroll', handlePageScroll, { passive: true })
parentScrollListeners.value.push(parent)
}
parent = parent.parentElement
}
}
updateContainerMetrics()
if (props.pageMode) {
window.addEventListener('resize', updateContainerMetrics, { passive: true })
@@ -162,6 +203,14 @@ onMounted(() => {
onBeforeUnmount(() => {
if (ro) ro.disconnect()
window.removeEventListener('resize', updateContainerMetrics)
if (props.pageMode) {
window.removeEventListener('scroll', handlePageScroll)
// Clean up parent scroll listeners
parentScrollListeners.value.forEach(parent => {
parent.removeEventListener('scroll', handlePageScroll)
})
parentScrollListeners.value = []
}
})
function scrollTo (index: number) { scrollToItem(index) }
@@ -178,6 +227,10 @@ function refresh () {
if (containerRef.value) {
updateScrollTop(containerRef.value.scrollTop)
}
// Also trigger page scroll calculation in page mode
if (props.pageMode) {
handlePageScroll()
}
}
defineExpose({ scrollTo, scrollToTop, scrollToBottom, setViewMode, toggleViewMode, refresh })
@@ -203,9 +256,11 @@ defineExpose({ scrollTo, scrollToTop, scrollToBottom, setViewMode, toggleViewMod
inset: 0 auto auto 0;
will-change: transform;
backface-visibility: hidden;
width: 100%;
}
.virtual-scroller-viewport.is-grid {
width: 100%;
display: grid;
grid-template-columns: repeat(var(--items-per-row, 1), minmax(0, 1fr));
grid-auto-rows: var(--row-height, 1px);

View File

@@ -1,7 +1,14 @@
{
"app": { "title": "PicList" },
"titleBar": { "alwaysOnTop": "Always On Top", "close": "Close", "minimize": "Minimize", "miniWindow": "Mini Window" },
"common": { "confirm": "Confirm", "cancel": "Cancel", "close": "Close", "reset": "Reset", "import": "Import" },
"common": {
"confirm": "Confirm",
"cancel": "Cancel",
"close": "Close",
"reset": "Reset",
"import": "Import",
"submit": "Submit"
},
"navigation": {
"upload": "Upload",
"manage": "Manage",
@@ -695,74 +702,6 @@
}
}
},
"MANAGE_SETTING_TITLE": "Manage Setting",
"MANAGE_SETTING_ISAUTOREFRESH_TITLE": "Auto refresh file list when entering new directory",
"MANAGE_SETTING_ISAUTOREFRESH_TIPS": "Only applies to non-paginated mode, data is cached to indexdb to speed up loading speed",
"MANAGE_SETTING_CLEAR_CACHE_TITLE": "Clear file list cache database, currently in use:",
"MANAGE_SETTING_CLEAR_CACHE_FREE_TITLE": "Available:",
"MANAGE_SETTING_CLEAR_CACHE_TIPS": "After clearing, the file list will be reloaded when entering a new directory next time",
"MANAGE_SETTING_CLEAR_CACHE_PROMPT": "Are you sure you want to clear the file list cache database?",
"MANAGE_SETTING_CLEAR_CACHE_BUTTON": "Clear",
"MANAGE_SETTING_ISSHOWTHUMBNAIL_TITLE": "Display the original image instead of format icon (requires public access permissions)",
"MANAGE_SETTING_ISUSEPRESIGNEDURL_TITLE": "Use presigned URL for image display",
"MANAGE_SETTING_ISSHOWLIST_TITLE": "Default display mode for the file list",
"MANAGE_SETTING_ISSHOWLIST_ON": "List",
"MANAGE_SETTING_ISSHOWLIST_OFF": "Card",
"MANAGE_SETTING_ISFORCECUSTOMURLHTTPS_TITLE": "Force custom URL to use HTTPS",
"MANAGE_SETTING_ISFORCECUSTOMURLHTTPS_TIPS": "After enabling, all operations will automatically add the https prefix to custom domains",
"MANAGE_SETTING_ISUPLOADKEEPDIRSTRUCTURE_TITLE": "Preserve directory structure when uploading",
"MANAGE_SETTING_ISUPLOADKEEPDIRSTRUCTURE_TIPS": "After disabling, all files will be expanded to the specified directory",
"MANAGE_SETTING_ISDOWNLOADFILEKEEPDIRSTRUCTURE_TITLE_A": "Download",
"MANAGE_SETTING_ISDOWNLOADFILEKEEPDIRSTRUCTURE_TITLE_B": " File ",
"MANAGE_SETTING_ISDOWNLOADFILEKEEPDIRSTRUCTURE_TITLE_C": "will preserve the directory structure",
"MANAGE_SETTING_ISDOWNLOADFOLDERKEEPDIRSTRUCTURE_TITLE_D": " Folder ",
"MANAGE_SETTING_ISDOWNLOADFILEKEEPDIRSTRUCTURE_TIPS": "After enabling, the original directory structure will be preserved",
"MANAGE_SETTING_MAX_DOWNLOAD_FILE_SIZE_TITLE": "Maximum number of files to download simultaneously (1-9999)",
"MANAGE_SETTING_MAX_DOWNLOAD_FILE_SIZE_TIPS": "Not work on Tencent",
"MANAGE_SETTING_MAX_DOWNLOAD_FILE_SIZE_INPUT_TIPS": "Please enter the maximum number of files to download simultaneously",
"MANAGE_SETTING_ISIGNORECASE_TITLE": "Should file search be case-insensitive",
"MANAGE_SETTING_ISIGNORECASE_TIPS": "After enabling, the search will be case-insensitive",
"MANAGE_SETTING_TIMESTAMPRENAME_TITLE": "Rename uploaded files with timestamp - (highest priority)",
"MANAGE_SETTING_TIMESTAMPRENAME_TIPS": "After enabling, the uploaded file will be renamed with the timestamp",
"MANAGE_SETTING_RANDOMSTRINGRENAME_TITLE": "Rename uploaded files with random strings - (medium priority)",
"MANAGE_SETTING_RANDOMSTRINGRENAME_TIPS": "Random string length is 20",
"MANAGE_SETTING_CUSTOMRENAME_TITLE": "Rename uploaded files with custom names - (lowest priority)",
"MANAGE_SETTING_CUSTOMRENAME_TIPS": "After enabling, the uploaded file will be renamed with the custom pattern",
"MANAGE_SETTING_CUSTOM_PATTERN_TITLE": "Custom rename format, placeholders can be freely combined, please refer to the table below",
"MANAGE_SETTING_CUSTOM_PATTERN_TIPS": "Please enter the custom rename format",
"MANAGE_SETTING_CUSTOM_PATTERN_TABLE_TITLE": "Placeholder",
"MANAGE_SETTING_CUSTOM_PATTERN_TABLE_TIPS": "Description",
"MANAGE_SETTING_PRESIGNED_URL_EXPIRE_TITLE": "Presigned URL expiration time (seconds)",
"MANAGE_SETTING_PRESIGNED_URL_EXPIRE_TIPS": "Please enter the presigned URL expiration time",
"MANAGE_SETTING_CHOOSE_COPY_FORMAT_TITLE": "Select default link format for copying",
"MANAGE_SETTING_CHOOSE_COPY_FORMAT_MARKDOWN": "Markdown",
"MANAGE_SETTING_CHOOSE_COPY_FORMAT_MARKDOWN_WITH_LINK": "Markdown with link",
"MANAGE_SETTING_CHOOSE_COPY_FORMAT_RAWURL": "Raw URL",
"MANAGE_SETTING_CHOOSE_COPY_FORMAT_HTML": "HTML",
"MANAGE_SETTING_CHOOSE_COPY_FORMAT_BBCODE": "BBCode",
"MANAGE_SETTING_CHOOSE_COPY_FORMAT_CUSTOM": "Custom",
"MANAGE_SETTING_CUSTOM_COPY_FORMAT_TITLE": "Custom link format($url -> raw url, $fileName -> raw fileName)",
"MANAGE_SETTING_CUSTOM_COPY_FORMAT_TIPS": "Please enter the custom link format",
"MANAGE_SETTING_CHOOSE_DOWNLOAD_FOLDER_TITLE": "Choose default download folder",
"MANAGE_SETTING_CHOOSE_DOWNLOAD_FOLDER_TIPS": "System default download directory",
"MANAGE_SETTING_CHOOSE_DOWNLOAD_FOLDER_BUTTON": "Choose folder",
"MANAGE_SETTING_COPY_MESSAGE": "Copied",
"MANAGE_SETTING_CLEAR_CACHE_SUCCESS": "Cleared successfully",
"MANAGE_SETTING_CLEAR_CACHE_FAILED": "Clear failed",
"MANAGE_SETTING_ISENCODEURL_TITLE": "Encode URL when copy",
"MANAGE_SETTING_ISENCODEURL_TIPS": "After enabling, the URL will be encoded when copying",
"MANAGE_NO_DATA": "No data",
"MANAGE_MAIN_PAGE_NEW_BUCKET": "New Bucket",
"MANAGE_MAIN_PAGE_BACK_TO_HOME": "Home",
"MANAGE_MAIN_PAGE_SWITCH_PICBED": "Switch",
"MANAGE_MAIN_PAGE_SETTING": "Setting",
"MANAGE_MAIN_PAGE_SUBMIT": "Submit",
"MANAGE_MAIN_PAGE_TIPS": "Tips",
"MANAGE_MAIN_PAGE_TIPS_SUCCESS": "Created successfully",
"MANAGE_MAIN_PAGE_TIPS_FAILED": "Create failed",
"MANAGE_MAIN_PAGE_BUCKET": "Bucket",
"MANAGE_MAIN_PAGE_GALLERY": "Album",
"MANAGE_MAIN_PAGE_REPOSITORY": "Repo",
"MANAGE_BUCKET_PAGE_LOADING_TEXT": "Loading...",
"MANAGE_BUCKET_PAGE_CUSTOM_URL_SELECT_PLACEHOLDER": "Please select a custom domain",
"MANAGE_BUCKET_PAGE_CUSTOM_URL_INPUT_PLACEHOLDER": "Please enter a custom domain",
@@ -774,6 +713,8 @@
"MANAGE_BUCKET_PAGE_BATCH_COPY_URL_TOOLTIP": "Batch copy URL",
"MANAGE_BUCKET_PAGE_COPY_FILE_INFO_TOOLTIP": "Copy file information",
"MANAGE_BUCKET_PAGE_FORCE_REFRESH_TOOLTIP": "Force refresh file list",
"MANAGE_BUCKET_PAGE_FULLSCREEN_TOOLTIP": "Enter fullscreen mode (F11)",
"MANAGE_BUCKET_PAGE_EXIT_FULLSCREEN_TOOLTIP": "Exit fullscreen mode (F11)",
"MANAGE_BUCKET_PAGE_SEARCH_PLACEHOLDER": "Search files",
"MANAGE_BUCKET_PAGE_ROOT_FOLDER": "Root folder",
"MANAGE_BUCKET_PAGE_FILE_NUMBER": "Number of files: ",

View File

@@ -1,7 +1,14 @@
{
"app": { "title": "PicList" },
"titleBar": { "alwaysOnTop": "置顶", "close": "关闭", "minimize": "最小化", "miniWindow": "迷你窗口" },
"common": { "confirm": "确认", "cancel": "取消", "close": "关闭", "reset": "重置", "import": "导入" },
"common": {
"confirm": "确认",
"cancel": "取消",
"close": "关闭",
"reset": "重置",
"import": "导入",
"submit": "提交"
},
"navigation": {
"upload": "上传",
"manage": "管理",
@@ -481,6 +488,77 @@
"copySuccess": "复制成功"
},
"manage": {
"main": {
"openPicBedUrl": "打开图床官网",
"newBucket": "新建存储桶",
"loading": "加载中...",
"backToHome": "首页",
"switchPicBed": "切换",
"settings": "设置",
"bucket": "存储桶",
"gallery": "相册",
"repo": "仓库",
"createSuccess": "创建成功",
"createFailed": "创建失败"
},
"empty": {
"noData": "暂无数据",
"noDataDesc": "请先创建存储桶或上传图片"
},
"setting": {
"clearCache": "清空文件列表缓存数据库,已使用 {size} 可用 {percent}%",
"clearCacheMsg": "确定要清空缓存吗?",
"isAutoRefreshTitle": "每次进入新目录时,是否自动刷新文件列表",
"isAutoRefreshTips": "仅对不分页模式有效,默认在加载过一次后自动缓存到数据库来加快下次加载速度",
"isShowThumbnailTitle": "图片显示为原图而非默认文件格式图标(需要存储桶可公开访问)",
"isUsePreSignedUrlTitle": "是否使用预签名URL预览图片",
"isForceCustomUrlHttpsTitle": "为自定义域名开启强制HTTPS",
"isForceCustomUrlHttpsTips": "开启后, 复制链接等操作将会自动为自定义域名添加https前缀",
"isEncodeUrlTitle": "复制链接时进行URL编码",
"isEncodeUrlTips": "根据平台选择是否开启",
"isUploadKeepDirStructureTitle": "上传时保持目录结构",
"isUploadKeepDirStructureTips": "关闭后会将所有文件展开到指定目录下",
"isIgnoreCaseTitle": "文件搜索时,是否忽略大小写",
"isIgnoreCaseTips": "建议开启",
"timestampRenameTitle": "上传文件时间戳重命名(最高优先级)",
"timestampRenameTips": "开启后,上传的文件将自动重命名为时间戳",
"randomStringRenameTitle": "上传文件随机字符串重命名(中优先级)",
"randomStringRenameTips": "20位随机字符",
"customRenameTitle": "上传文件自定义重命名(低优先级)",
"customRenameTips": "开启后填写命名格式",
"customRenameTableTitle": "自定义重命名格式参考表",
"customRenameTablePlaceholder": "请输入自定义重命名格式",
"placeholder": "占位符",
"description": "描述",
"copySuccess": "已复制 {name}",
"download": "下载",
"file": "文件",
"folder": "文件夹",
"keepDirStructure": "保持目录结构",
"keepDirStructureDesc": "开启后,下载时会保持原始目录结构",
"clearSuccess": "清空缓存成功",
"clearFailed": "清空缓存失败",
"notice": "通知",
"maxDownLoadFileLimit": "最大并行下载文件数",
"maxDownLoadFileLimitDesc": "建议根据网络情况调整",
"preSignedUrlExpire": "预签名URL过期时间(单位: 秒)",
"preSignedUrlExpireDesc": "建议根据实际需求调整",
"copyFormat": {
"title": "复制格式",
"markdown": "Markdown",
"rawurl": "原始URL",
"markdown-with-link": "Markdown带链接",
"html": "HTML格式",
"bbcode": "BBCode格式",
"custom": "自定义格式",
"customTitle": "自定义链接格式($url为链接,$fileName为文件名)",
"customTips": "请根据实际需求填写自定义格式"
},
"selectDownloadFolderTitle": "选择下载文件夹",
"selectDownloadFolderTips": "选择下载目录",
"defaultDownloadFolder": "系统默认下载文件夹",
"browse": "浏览"
},
"login": {
"title": "图床管理",
"savedConfigs": "已保存配置",
@@ -690,74 +768,6 @@
}
}
},
"MANAGE_SETTING_TITLE": "管理页面设置",
"MANAGE_SETTING_ISAUTOREFRESH_TITLE": "每次进入新目录时,是否自动刷新文件列表",
"MANAGE_SETTING_ISAUTOREFRESH_TIPS": "仅对不分页模式有效,默认在加载过一次后自动缓存到数据库来加快下次加载速度",
"MANAGE_SETTING_CLEAR_CACHE_TITLE": "清空文件列表缓存数据库 已占用:",
"MANAGE_SETTING_CLEAR_CACHE_FREE_TITLE": "剩余可用:",
"MANAGE_SETTING_CLEAR_CACHE_TIPS": "清空后下次进入新目录时将会重新加载文件列表",
"MANAGE_SETTING_CLEAR_CACHE_PROMPT": "确定要清空文件列表缓存数据库吗?",
"MANAGE_SETTING_CLEAR_CACHE_BUTTON": "清空",
"MANAGE_SETTING_ISSHOWTHUMBNAIL_TITLE": "图片显示为原图而非默认文件格式图标(需要存储桶可公开访问)",
"MANAGE_SETTING_ISUSEPRESIGNEDURL_TITLE": "使用预签名URL预览图片",
"MANAGE_SETTING_ISSHOWLIST_TITLE": "文件列表默认显示方式",
"MANAGE_SETTING_ISSHOWLIST_ON": "列表",
"MANAGE_SETTING_ISSHOWLIST_OFF": "卡片",
"MANAGE_SETTING_ISFORCECUSTOMURLHTTPS_TITLE": "为自定义域名开启强制HTTPS",
"MANAGE_SETTING_ISFORCECUSTOMURLHTTPS_TIPS": "开启后, 复制链接等操作将会自动为自定义域名添加https前缀",
"MANAGE_SETTING_ISUPLOADKEEPDIRSTRUCTURE_TITLE": "上传时保留目录结构",
"MANAGE_SETTING_ISUPLOADKEEPDIRSTRUCTURE_TIPS": "关闭后会将所有文件展开到指定目录下",
"MANAGE_SETTING_ISDOWNLOADFILEKEEPDIRSTRUCTURE_TITLE_A": "下载",
"MANAGE_SETTING_ISDOWNLOADFILEKEEPDIRSTRUCTURE_TITLE_B": "文件",
"MANAGE_SETTING_ISDOWNLOADFILEKEEPDIRSTRUCTURE_TITLE_C": "时保留目录结构",
"MANAGE_SETTING_ISDOWNLOADFOLDERKEEPDIRSTRUCTURE_TITLE_D": "文件夹",
"MANAGE_SETTING_ISDOWNLOADFILEKEEPDIRSTRUCTURE_TIPS": "开启后,下载时会保留原始目录结构",
"MANAGE_SETTING_MAX_DOWNLOAD_FILE_SIZE_TITLE": "最大同时下载文件数(1-9999)",
"MANAGE_SETTING_MAX_DOWNLOAD_FILE_SIZE_TIPS": "腾讯云由于后端实现不同,该设置不生效",
"MANAGE_SETTING_MAX_DOWNLOAD_FILE_SIZE_INPUT_TIPS": "请输入最大同时下载文件数",
"MANAGE_SETTING_ISIGNORECASE_TITLE": "文件搜索时,是否忽略大小写",
"MANAGE_SETTING_ISIGNORECASE_TIPS": "开启后,搜索时会忽略大小写",
"MANAGE_SETTING_TIMESTAMPRENAME_TITLE": "上传文件时间戳重命名--(优先级最高)",
"MANAGE_SETTING_TIMESTAMPRENAME_TIPS": "开启后,上传文件时会自动重命名为时间戳",
"MANAGE_SETTING_RANDOMSTRINGRENAME_TITLE": "上传文件随机字符串重命名--(优先级中)",
"MANAGE_SETTING_RANDOMSTRINGRENAME_TIPS": "随机字符串长度为20",
"MANAGE_SETTING_CUSTOMRENAME_TITLE": "上传文件自定义重命名--(优先级最低)",
"MANAGE_SETTING_CUSTOMRENAME_TIPS": "请填写自定义重命名格式",
"MANAGE_SETTING_CUSTOM_PATTERN_TITLE": "自定义重命名格式,占位符请参考下表,可自由组合",
"MANAGE_SETTING_CUSTOM_PATTERN_TIPS": "请填写自定义重命名格式",
"MANAGE_SETTING_CUSTOM_PATTERN_TABLE_TITLE": "占位符",
"MANAGE_SETTING_CUSTOM_PATTERN_TABLE_TIPS": "描述",
"MANAGE_SETTING_PRESIGNED_URL_EXPIRE_TITLE": "预签名URL过期时间(单位:秒)",
"MANAGE_SETTING_PRESIGNED_URL_EXPIRE_TIPS": "请填写预签名URL过期时间",
"MANAGE_SETTING_CHOOSE_COPY_FORMAT_TITLE": "选择默认复制的链接格式",
"MANAGE_SETTING_CHOOSE_COPY_FORMAT_MARKDOWN": "Markdown",
"MANAGE_SETTING_CHOOSE_COPY_FORMAT_MARKDOWN_WITH_LINK": "Markdown(带链接)",
"MANAGE_SETTING_CHOOSE_COPY_FORMAT_RAWURL": "原始链接",
"MANAGE_SETTING_CHOOSE_COPY_FORMAT_HTML": "HTML格式",
"MANAGE_SETTING_CHOOSE_COPY_FORMAT_BBCODE": "BBCode格式",
"MANAGE_SETTING_CHOOSE_COPY_FORMAT_CUSTOM": "自定义格式",
"MANAGE_SETTING_CUSTOM_COPY_FORMAT_TITLE": "自定义链接格式($url为链接,$fileName为文件名)",
"MANAGE_SETTING_CUSTOM_COPY_FORMAT_TIPS": "请填写自定义链接格式",
"MANAGE_SETTING_CHOOSE_DOWNLOAD_FOLDER_TITLE": "选择下载目录",
"MANAGE_SETTING_CHOOSE_DOWNLOAD_FOLDER_TIPS": "系统默认下载目录",
"MANAGE_SETTING_CHOOSE_DOWNLOAD_FOLDER_BUTTON": "选择目录",
"MANAGE_SETTING_COPY_MESSAGE": "已复制",
"MANAGE_SETTING_CLEAR_CACHE_SUCCESS": "清除成功",
"MANAGE_SETTING_CLEAR_CACHE_FAILED": "清除失败",
"MANAGE_SETTING_ISENCODEURL_TITLE": "复制链接时进行URL编码",
"MANAGE_SETTING_ISENCODEURL_TIPS": "根据平台选择是否开启",
"MANAGE_NO_DATA": "暂无数据",
"MANAGE_MAIN_PAGE_NEW_BUCKET": "新建存储桶",
"MANAGE_MAIN_PAGE_BACK_TO_HOME": "返回首页",
"MANAGE_MAIN_PAGE_SWITCH_PICBED": "切换图床",
"MANAGE_MAIN_PAGE_SETTING": "设置",
"MANAGE_MAIN_PAGE_SUBMIT": "提交",
"MANAGE_MAIN_PAGE_TIPS": "提示",
"MANAGE_MAIN_PAGE_TIPS_SUCCESS": "创建成功",
"MANAGE_MAIN_PAGE_TIPS_FAILED": "创建失败",
"MANAGE_MAIN_PAGE_BUCKET": "存储桶",
"MANAGE_MAIN_PAGE_GALLERY": "相册",
"MANAGE_MAIN_PAGE_REPOSITORY": "仓库",
"MANAGE_BUCKET_PAGE_LOADING_TEXT": "加载文件中...",
"MANAGE_BUCKET_PAGE_CUSTOM_URL_SELECT_PLACEHOLDER": "请选择自定义域名",
"MANAGE_BUCKET_PAGE_CUSTOM_URL_INPUT_PLACEHOLDER": "请输入自定义域名",
@@ -769,6 +779,8 @@
"MANAGE_BUCKET_PAGE_BATCH_COPY_URL_TOOLTIP": "批量复制URL",
"MANAGE_BUCKET_PAGE_COPY_FILE_INFO_TOOLTIP": "复制文件信息",
"MANAGE_BUCKET_PAGE_FORCE_REFRESH_TOOLTIP": "强制刷新文件列表",
"MANAGE_BUCKET_PAGE_FULLSCREEN_TOOLTIP": "进入全屏模式 (F11)",
"MANAGE_BUCKET_PAGE_EXIT_FULLSCREEN_TOOLTIP": "退出全屏模式 (F11)",
"MANAGE_BUCKET_PAGE_SEARCH_PLACEHOLDER": "搜索文件",
"MANAGE_BUCKET_PAGE_ROOT_FOLDER": "根目录",
"MANAGE_BUCKET_PAGE_FILE_NUMBER": "文件数: ",

View File

@@ -1,7 +1,14 @@
{
"app": { "title": "PicList" },
"titleBar": { "alwaysOnTop": "置頂", "close": "關閉", "minimize": "最小化", "miniWindow": "迷你視窗" },
"common": { "confirm": "確認", "cancel": "取消", "close": "關閉", "reset": "重置", "import": "匯入" },
"common": {
"confirm": "確認",
"cancel": "取消",
"close": "關閉",
"reset": "重置",
"import": "匯入",
"submit": "提交"
},
"navigation": {
"upload": "上傳",
"manage": "管理",
@@ -690,74 +697,6 @@
}
}
},
"MANAGE_SETTING_TITLE": "管理設定",
"MANAGE_SETTING_ISAUTOREFRESH_TITLE": "每次進入新目錄時,是否自動重新整理檔案列表",
"MANAGE_SETTING_ISAUTOREFRESH_TIPS": "僅對不分頁模式有效,預設會在載入後自動快取至資料庫以提升下次載入速度",
"MANAGE_SETTING_CLEAR_CACHE_TITLE": "清空檔案列表快取資料庫 已佔用:",
"MANAGE_SETTING_CLEAR_CACHE_FREE_TITLE": "剩餘可用:",
"MANAGE_SETTING_CLEAR_CACHE_TIPS": "清空後下次進入新目錄時將會重新載入檔案列表",
"MANAGE_SETTING_CLEAR_CACHE_PROMPT": "確定要清空檔案列表快取資料庫嗎?",
"MANAGE_SETTING_CLEAR_CACHE_BUTTON": "清空",
"MANAGE_SETTING_ISSHOWTHUMBNAIL_TITLE": "顯示圖片的原始圖像而非預設的檔案格式圖示(需要存儲桶公開訪問權限)",
"MANAGE_SETTING_ISUSEPRESIGNEDURL_TITLE": "使用預簽名URL预览圖片",
"MANAGE_SETTING_ISSHOWLIST_TITLE": "檔案列表預設顯示方式",
"MANAGE_SETTING_ISSHOWLIST_ON": "列表",
"MANAGE_SETTING_ISSHOWLIST_OFF": "卡片",
"MANAGE_SETTING_ISFORCECUSTOMURLHTTPS_TITLE": "自定義域名啟用強制 HTTPS",
"MANAGE_SETTING_ISFORCECUSTOMURLHTTPS_TIPS": "開啟後,複製鏈結等操作將會自動為自定義域名添加 HTTPS 前綴",
"MANAGE_SETTING_ISUPLOADKEEPDIRSTRUCTURE_TITLE": "保留上傳時的目錄結構",
"MANAGE_SETTING_ISUPLOADKEEPDIRSTRUCTURE_TIPS": "停用後,所有文件將會展開到指定目錄下",
"MANAGE_SETTING_ISDOWNLOADFILEKEEPDIRSTRUCTURE_TITLE_A": "下載",
"MANAGE_SETTING_ISDOWNLOADFILEKEEPDIRSTRUCTURE_TITLE_B": "文件",
"MANAGE_SETTING_ISDOWNLOADFILEKEEPDIRSTRUCTURE_TITLE_C": "時保留目錄結構",
"MANAGE_SETTING_ISDOWNLOADFOLDERKEEPDIRSTRUCTURE_TITLE_D": "目錄",
"MANAGE_SETTING_ISDOWNLOADFILEKEEPDIRSTRUCTURE_TIPS": "啟用後,下載時會保留原始目錄結構",
"MANAGE_SETTING_MAX_DOWNLOAD_FILE_SIZE_TITLE": "最大同時下載檔案數量(1-9999)",
"MANAGE_SETTING_MAX_DOWNLOAD_FILE_SIZE_TIPS": "由於後端實現方式不同,此設定在腾讯云上不生效",
"MANAGE_SETTING_MAX_DOWNLOAD_FILE_SIZE_INPUT_TIPS": "請輸入最大同時下載檔案數量",
"MANAGE_SETTING_ISIGNORECASE_TITLE": "搜尋檔案時,是否忽略大小寫",
"MANAGE_SETTING_ISIGNORECASE_TIPS": "啟用後,搜尋時將會忽略大小寫",
"MANAGE_SETTING_TIMESTAMPRENAME_TITLE": "上傳檔案時間戳重新命名--(最高優先級)",
"MANAGE_SETTING_TIMESTAMPRENAME_TIPS": "啟用後,上傳檔案時將會使用時間戳重新命名",
"MANAGE_SETTING_RANDOMSTRINGRENAME_TITLE": "上傳檔案隨機字符串重新命名--(中優先級)",
"MANAGE_SETTING_RANDOMSTRINGRENAME_TIPS": "隨機字符串長度為20",
"MANAGE_SETTING_CUSTOMRENAME_TITLE": "上傳檔案自定義重新命名--(最低優先級)",
"MANAGE_SETTING_CUSTOMRENAME_TIPS": "啟用後,上傳檔案時將會使用自定義重新命名",
"MANAGE_SETTING_CUSTOM_PATTERN_TITLE": "自訂重新命名格式,占位符請參考下表,可自由組合",
"MANAGE_SETTING_CUSTOM_PATTERN_TIPS": "請輸入自訂重新命名格式",
"MANAGE_SETTING_CUSTOM_PATTERN_TABLE_TITLE": "占位符",
"MANAGE_SETTING_CUSTOM_PATTERN_TABLE_TIPS": "說明",
"MANAGE_SETTING_PRESIGNED_URL_EXPIRE_TITLE": "預設下載鏈結有效期(秒)",
"MANAGE_SETTING_PRESIGNED_URL_EXPIRE_TIPS": "請輸入下載鏈結有效期",
"MANAGE_SETTING_CHOOSE_COPY_FORMAT_TITLE": "選擇預設複製的連結格式",
"MANAGE_SETTING_CHOOSE_COPY_FORMAT_MARKDOWN": "Markdown",
"MANAGE_SETTING_CHOOSE_COPY_FORMAT_MARKDOWN_WITH_LINK": "Markdown(帶連結)",
"MANAGE_SETTING_CHOOSE_COPY_FORMAT_RAWURL": "原始鏈結",
"MANAGE_SETTING_CHOOSE_COPY_FORMAT_HTML": "HTML格式",
"MANAGE_SETTING_CHOOSE_COPY_FORMAT_BBCODE": "BBCode格式",
"MANAGE_SETTING_CHOOSE_COPY_FORMAT_CUSTOM": "自定義格式",
"MANAGE_SETTING_CUSTOM_COPY_FORMAT_TITLE": "自定義鏈結格式($url為原始鏈結,$fileName為檔案名稱)",
"MANAGE_SETTING_CUSTOM_COPY_FORMAT_TIPS": "請輸入自定義鏈結格式",
"MANAGE_SETTING_CHOOSE_DOWNLOAD_FOLDER_TITLE": "選擇下載目錄",
"MANAGE_SETTING_CHOOSE_DOWNLOAD_FOLDER_TIPS": "系統預設下載目錄",
"MANAGE_SETTING_CHOOSE_DOWNLOAD_FOLDER_BUTTON": "選擇目錄",
"MANAGE_SETTING_COPY_MESSAGE": "已複製",
"MANAGE_SETTING_CLEAR_CACHE_SUCCESS": "清除成功",
"MANAGE_SETTING_CLEAR_CACHE_FAILED": "清除失敗",
"MANAGE_SETTING_ISENCODEURL_TITLE": "複製鏈結時編碼",
"MANAGE_SETTING_ISENCODEURL_TIPS": "啟用後,複製鏈結時將會編碼",
"MANAGE_NO_DATA": "暫無數據",
"MANAGE_MAIN_PAGE_NEW_BUCKET": "新建存儲桶",
"MANAGE_MAIN_PAGE_BACK_TO_HOME": "返回首頁",
"MANAGE_MAIN_PAGE_SWITCH_PICBED": "切換圖床",
"MANAGE_MAIN_PAGE_SETTING": "設定",
"MANAGE_MAIN_PAGE_SUBMIT": "提交",
"MANAGE_MAIN_PAGE_TIPS": "提示",
"MANAGE_MAIN_PAGE_TIPS_SUCCESS": "創建成功",
"MANAGE_MAIN_PAGE_TIPS_FAILED": "創建失敗",
"MANAGE_MAIN_PAGE_BUCKET": "存儲桶",
"MANAGE_MAIN_PAGE_GALLERY": "圖庫",
"MANAGE_MAIN_PAGE_REPOSITORY": "倉庫",
"MANAGE_BUCKET_PAGE_LOADING_TEXT": "載入檔案中...",
"MANAGE_BUCKET_PAGE_CUSTOM_URL_SELECT_PLACEHOLDER": "請選擇自訂域名",
"MANAGE_BUCKET_PAGE_CUSTOM_URL_INPUT_PLACEHOLDER": "請輸入自訂域名",
@@ -769,6 +708,8 @@
"MANAGE_BUCKET_PAGE_BATCH_COPY_URL_TOOLTIP": "批次複製 URL",
"MANAGE_BUCKET_PAGE_COPY_FILE_INFO_TOOLTIP": "複製檔案資訊",
"MANAGE_BUCKET_PAGE_FORCE_REFRESH_TOOLTIP": "強制重新整理檔案列表",
"MANAGE_BUCKET_PAGE_FULLSCREEN_TOOLTIP": "進入全螢幕模式 (F11)",
"MANAGE_BUCKET_PAGE_EXIT_FULLSCREEN_TOOLTIP": "退出全螢幕模式 (F11)",
"MANAGE_BUCKET_PAGE_SEARCH_PLACEHOLDER": "搜尋檔案",
"MANAGE_BUCKET_PAGE_ROOT_FOLDER": "根目錄",
"MANAGE_BUCKET_PAGE_FILE_NUMBER": "檔案數:",

View File

@@ -0,0 +1,226 @@
<template>
<div class="switch-container">
<div class="switch-label-wrapper">
<span class="switch-label-text">
<span
v-for="(segment, index) in segments"
:key="index"
:style="segment.style"
>
{{ segment.text }}
</span>
<div
v-if="tooltip"
class="tooltip-wrapper"
>
<div
class="info-icon"
@click="toggleTooltip"
>
<svg
viewBox="0 0 20 20"
fill="currentColor"
class="info-svg"
>
<path
fill-rule="evenodd"
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z"
clip-rule="evenodd"
/>
</svg>
</div>
<div
v-show="showTooltip"
class="tooltip-content"
>
{{ tooltip }}
</div>
</div>
</span>
</div>
<div class="switch-control">
<label class="switch">
<input
v-model="value"
type="checkbox"
class="switch-input"
>
<span class="switch-slider">
<span class="switch-button" />
</span>
</label>
<div
v-if="activeText || inactiveText"
class="switch-text"
>
{{ value ? activeText : inactiveText }}
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
defineProps<{
tooltip?: string
activeText?: string
inactiveText?: string
segments?: { text: string; style: string }[]
}>()
const value = defineModel<boolean>()
const showTooltip = ref(false)
const toggleTooltip = () => {
showTooltip.value = !showTooltip.value
}
</script>
<style scoped>
.switch-container {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 1.5rem;
padding: 1rem;
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
background: var(--color-surface-elevated);
}
.switch-label-wrapper {
flex: 1;
}
.switch-label-text {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.875rem;
font-weight: 500;
color: var(--color-text-primary);
}
.tooltip-wrapper {
position: relative;
}
.info-icon {
display: flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
cursor: pointer;
color: var(--color-text-secondary);
transition: var(--transition-fast);
border-radius: 50%;
padding: 2px;
}
.info-icon:hover {
color: var(--color-accent);
background: rgba(0, 122, 255, 0.1);
}
.info-svg {
width: 16px;
height: 16px;
}
.tooltip-content {
position: absolute;
top: 100%;
left: 0;
z-index: 1000;
min-width: 200px;
max-width: 300px;
padding: 0.75rem;
background: var(--color-surface-elevated);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
box-shadow: var(--shadow-lg);
font-size: 0.75rem;
line-height: 1.4;
color: var(--color-text-primary);
}
.switch-control {
display: flex;
align-items: center;
gap: 0.75rem;
}
.switch {
position: relative;
display: inline-block;
width: 3rem;
height: 1.5rem;
}
.switch-input {
opacity: 0;
width: 0;
height: 0;
}
.switch-slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: var(--color-border);
border-radius: 0.75rem;
transition: var(--transition-fast);
}
.switch-button {
position: absolute;
top: 2px;
left: 2px;
width: 1.25rem;
height: 1.25rem;
background: white;
border-radius: 50%;
transition: var(--transition-fast);
box-shadow: var(--shadow-sm);
}
.switch-input:checked + .switch-slider {
background: var(--color-accent);
}
.switch-input:checked + .switch-slider .switch-button {
transform: translateX(1.5rem);
}
.switch-input:focus + .switch-slider {
box-shadow: 0 0 0 2px rgba(0, 122, 255, 0.2);
}
.switch-text {
font-size: 0.875rem;
font-weight: 500;
color: var(--color-text-secondary);
min-width: 50px;
}
.switch-input:checked ~ .switch-text {
color: var(--color-accent);
}
/* Dark mode adjustments */
:root.dark .switch-slider,
:root.auto.dark .switch-slider {
background: var(--color-border);
}
:root.dark .tooltip-content,
:root.auto.dark .tooltip-content {
background: var(--color-surface-elevated);
border-color: var(--color-border);
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,61 @@
<template>
<el-empty :description="$t('MANAGE_NO_DATA')" />
<div class="empty-page">
<div class="empty-container">
<div class="empty-icon">
<FolderOpenIcon class="icon" />
</div>
<div class="empty-content">
<h3 class="empty-title">
{{ t('pages.manage.empty.noData') }}
</h3>
<p class="empty-description">
{{ t('pages.manage.empty.noDataDesc') }}
</p>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { FolderOpenIcon } from 'lucide-vue-next'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
</script>
<style lang="stylus" scoped>
.empty-page
height 100%
display flex
align-items center
justify-content center
padding 2rem
.empty-container
display flex
flex-direction column
align-items center
text-align center
max-width 400px
.empty-icon
margin-bottom 1.5rem
.icon
width 64px
height 64px
color var(--color-text-secondary)
.empty-content
.empty-title
font-size 1.25rem
font-weight 600
color var(--color-text-primary)
margin 0 0 0.5rem 0
.empty-description
font-size 0.875rem
color var(--color-text-secondary)
margin 0
line-height 1.5
</style>

View File

@@ -1,288 +1,347 @@
<template>
<div class="layout">
<div class="layout__menu">
<div class="layout__menu__button">
<span
class="layout__menu__button__item"
@click="openPicBedUrl"
>
<img
:src="`/assets/${currentPagePicBedConfig.picBedName}.webp`"
class="layout__menu__button__item__icon"
<div class="manage-container">
<!-- Header Card -->
<div class="manage-card header-card">
<div class="card-header">
<div class="header-content">
<div class="header-icon">
<img
:src="`/assets/${currentPagePicBedConfig.picBedName}.webp`"
class="header-icon-img"
>
</div>
<div class="header-text">
<h2 class="header-title">
{{ supportedPicBedList[currentPagePicBedConfig.picBedName].name }}
</h2>
<p class="header-subtitle">
{{ menuTitleMap[currentPicBedName] }}
</p>
</div>
</div>
<div class="header-actions">
<button
class="action-button secondary"
@click="openPicBedUrl"
>
{{ supportedPicBedList[currentPagePicBedConfig.picBedName].name }}
</span>
</div>
<el-divider
content-position="left"
class="layout__menu__button__divider"
border-style="none"
>
<span style="font-size: 14px; color: #909399">
{{ menuTitleMap[currentPicBedName] }}
<el-tooltip
<ExternalLinkIcon class="button-icon" />
{{ t('pages.manage.main.openPicBedUrl') }}
</button>
<button
v-if="showNewIconList.includes(currentPicBedName)"
effect="dark"
:content="$t('MANAGE_MAIN_PAGE_NEW_BUCKET')"
placement="right"
:persistent="false"
teleported
popper-class="layout__menu__button__divider__tooltip"
class="action-button primary"
@click="openNewBucketDrawer"
>
<el-icon
class="layout__menu__button__divider__icon"
color="red"
style="top: 2px"
@click="openNewBucketDrawer()"
>
<CirclePlus />
</el-icon>
</el-tooltip>
</span>
</el-divider>
<div />
<el-menu
v-loading="isLoadingBucketList"
class="layout__menu__list"
:default-active="getCurrentActiveBucket"
style="width: 120px"
active-text-color="#409EFF"
@select="handleSelectMenu"
>
<el-menu-item
v-for="item of bucketNameList"
:key="item"
:index="item"
>
<span
class="layout__menu__list__item"
:style="{
color: item === currentSelectedBucket ? '#409EFF' : '#606266'
}"
>
<el-icon
v-if="currentSelectedBucket === item && currentPicBedName !== 'github'"
class="layout__menu__list__item__icon"
color="#409EFF"
style="top: 2px"
>
<FolderOpened />
</el-icon>
<el-icon
v-else-if="currentPicBedName !== 'github'"
class="layout__menu__list__item__icon"
color="#606266"
style="top: 2px"
>
<Folder />
</el-icon>
{{
currentPicBedName === 'tcyun'
? item.slice(0, item.length - 11)
: currentPicBedName === 'github'
? item.length > 10
? `${item.slice(0, 5)}..${item.slice(-5)}`
: item
: item
}}
</span>
</el-menu-item>
</el-menu>
<el-menu
class="layout__menu__setting"
style="width: 120px"
>
<el-menu-item
index="changePicBed"
style="height: 40px"
@click="switchPicBed('main')"
>
<span class="layout__menu__setting__item">
<el-icon class="layout__menu__setting__item__icon">
<HomeFilled />
</el-icon>
{{ $t('MANAGE_MAIN_PAGE_BACK_TO_HOME') }}
</span>
</el-menu-item>
<el-menu-item
index="changePicBed"
style="height: 40px"
@click="changePicBed"
>
<span class="layout__menu__setting__item">
<el-icon class="layout__menu__setting__item__icon">
<Switch />
</el-icon>
{{ $t('MANAGE_MAIN_PAGE_SWITCH_PICBED') }}
</span>
</el-menu-item>
<el-menu-item
index="bucketPageSetting"
style="height: 40px"
@click="openBucketPageSetting"
>
<span class="layout__menu__setting__item">
<el-icon class="layout__menu__setting__item__icon">
<Tools />
</el-icon>
{{ $t('MANAGE_MAIN_PAGE_SETTING') }}
</span>
</el-menu-item>
</el-menu>
<PlusIcon class="button-icon" />
{{ t('pages.manage.main.newBucket') }}
</button>
</div>
</div>
</div>
<!-- Main Content Card -->
<div class="manage-card main-card">
<div class="main-layout">
<div class="sidebar">
<div class="sidebar-header">
<h3 class="sidebar-title">
{{ menuTitleMap[currentPicBedName] }}
</h3>
</div>
<div class="sidebar-content">
<div
v-if="isLoadingBucketList"
class="loading-container"
>
<div class="loading-spinner" />
<span class="loading-text">{{ t('pages.manage.main.loading') }}</span>
</div>
<div
v-else
class="menu-list"
>
<div
v-for="item in bucketNameList"
:key="item"
class="menu-item"
:class="{ active: item === currentSelectedBucket }"
@click="handleSelectMenu(item)"
>
<FolderIcon
v-if="currentSelectedBucket === item && currentPicBedName !== 'github'"
class="menu-icon active"
/>
<FolderIcon
v-else-if="currentPicBedName !== 'github'"
class="menu-icon"
/>
<GitBranchIcon
v-else-if="currentPicBedName === 'github'"
class="menu-icon"
/>
<span class="menu-text">
{{
currentPicBedName === 'tcyun'
? item.slice(0, item.length - 11)
: currentPicBedName === 'github'
? item.length > 10
? `${item.slice(0, 5)}..${item.slice(-5)}`
: item
: item
}}
</span>
</div>
</div>
</div>
<div class="sidebar-footer">
<div class="footer-actions">
<button
class="footer-action-item"
@click="switchPicBed('main')"
>
<HomeIcon class="action-icon" />
<span class="action-text">{{ t('pages.manage.main.backToHome') }}</span>
</button>
<button
class="footer-action-item"
@click="changePicBed"
>
<ArrowLeftRightIcon class="action-icon" />
<span class="action-text">{{ t('pages.manage.main.switchPicBed') }}</span>
</button>
<button
class="footer-action-item"
@click="openBucketPageSetting"
>
<SettingsIcon class="action-icon" />
<span class="action-text">{{ t('pages.manage.main.settings') }}</span>
</button>
</div>
</div>
</div>
<div class="content-area">
<router-view />
</div>
</div>
</div>
<!-- PicBed Switch Dialog -->
<div
class="layout__content"
style="height: 100%; background-color: transparent; flex: 1; width: 0"
>
<router-view />
</div>
<el-dialog
v-model="picBedSwitchDialogVisible"
top="30vh"
append-to-body
v-if="picBedSwitchDialogVisible"
class="dialog-overlay"
@click="picBedSwitchDialogVisible = false"
>
<div
class="choice-cos"
style="display: flex; flex-direction: row; flex-wrap: wrap; justify-content: space-around"
class="dialog-container"
@click.stop
>
<el-card shadow="hover">
<div
style="text-align: center; display: flex; flex-direction: column"
@click="switchPicBed('main')"
<div class="dialog-header">
<h3 class="dialog-title">
{{ t('pages.manage.main.switchPicBed') }}
</h3>
<button
class="dialog-close"
@click="picBedSwitchDialogVisible = false"
>
<el-icon
color="red"
size="25px"
style="margin: 0 auto"
<XIcon class="close-icon" />
</button>
</div>
<div class="dialog-content">
<div class="choice-cos">
<!-- Back to main card -->
<div
class="picbed-card main-card"
@click="switchPicBed('main')"
>
<ChromeFilled />
</el-icon>
<span style="font-size: 13px; margin-top: 5px; color: red">
{{ $t('MANAGE_MAIN_PAGE_BACK_TO_HOME') }}
</span>
<div class="card-icon">
<HomeIcon class="main-icon" />
</div>
<div class="card-content">
<div class="card-title main-title">
{{ $t('pages.manage.main.backToHome') }}
</div>
</div>
</div>
<!-- PicBed cards -->
<div
v-for="(config, alias) in allPicBedConfigure"
:key="String(alias)"
class="picbed-card"
:class="{ active: String(alias) === currentAlias }"
@click="switchPicBed(String(alias))"
>
<div class="card-icon">
<img
:src="`/assets/${config.picBedName}.webp`"
class="picbed-icon"
>
</div>
<div class="card-content">
<div class="card-title">
{{ config.alias }}
</div>
</div>
<div
v-if="String(alias) === currentAlias"
class="check-icon"
>
<CheckIcon />
</div>
</div>
</div>
</el-card>
<el-card
v-for="item in allPicBedConfigure"
:key="item"
shadow="hover"
>
<div
style="text-align: center; display: flex; flex-direction: column"
@click="switchPicBed(item.alias)"
>
<el-image
:src="`/assets/${item.picBedName}.webp`"
class="layout__addNewBucket__icon"
style="width: 25px; height: 25px; margin: 0 auto"
/>
<span style="font-size: 13px; margin-top: 5px; color: cornflowerblue">
{{ item.alias }}
</span>
</div>
</el-card>
</div>
</div>
</el-dialog>
<el-drawer
v-model="nweBucketDrawerVisible"
class="layout__addNewBucket"
append-to-body
</div>
<!-- New Bucket Drawer -->
<div
v-if="nweBucketDrawerVisible"
class="drawer-overlay"
@click="nweBucketDrawerVisible = false"
>
<el-form
label-position="top"
require-asterisk-position="right"
label-width="10vw"
size="default"
:model="newBucketConfigResult"
:rules="rules"
<div
class="drawer-container"
@click.stop
>
<div style="position: relative; height: 10vh; width: 100%">
<el-image
:src="`/assets/${currentPicBedName}.webp`"
class="layout__addNewBucket__icon"
style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%)"
/>
<div class="drawer-header">
<h3 class="drawer-title">
{{ t('pages.manage.main.newBucket') }}
</h3>
<button
class="drawer-close"
@click="nweBucketDrawerVisible = false"
>
<XIcon class="close-icon" />
</button>
</div>
<el-divider border-style="none" />
<el-form-item
v-for="option in newBucketConfig[currentPicBedName].options"
:key="option"
:prop="currentPicBedName + '.' + option"
:label="newBucketConfig[currentPicBedName].configOptions[option].description"
>
<el-input
v-if="
newBucketConfig[currentPicBedName].configOptions[option].component === 'input' &&
currentPicBedName !== 'tcyun'
"
v-model.trim="newBucketConfigResult[currentPicBedName + '.' + option]"
:placeholder="newBucketConfig[currentPicBedName].configOptions[option].placeholder"
/>
<el-input
v-if="
currentPicBedName === 'tcyun' &&
newBucketConfig[currentPicBedName].configOptions[option].component === 'input'
"
v-model.trim="newBucketConfigResult[currentPicBedName + '.' + option]"
:placeholder="newBucketConfig[currentPicBedName].configOptions[option].placeholder"
>
<template #append>
{{ '-' + currentPagePicBedConfig.appId }}
</template>
</el-input>
<el-select
v-if="newBucketConfig[currentPicBedName].configOptions[option].component === 'select'"
v-model="newBucketConfigResult[currentPicBedName + '.' + option]"
size="large"
:persistent="false"
teleported
>
<el-option
v-for="item in Object.keys(newBucketConfig[currentPicBedName].configOptions[option].options)"
:key="item"
:label="newBucketConfig[currentPicBedName].configOptions[option].options[item]"
:value="item"
/>
</el-select>
<el-switch
v-if="newBucketConfig[currentPicBedName].configOptions[option].component === 'switch'"
v-model="newBucketConfigResult[currentPicBedName + '.' + option]"
:active-value="true"
:inactive-value="false"
/>
</el-form-item>
<div style="position: relative; height: 10vh; width: 100%; z-index: 1">
<el-button
:icon="SuccessFilled"
type="primary"
style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%)"
@click="createNewBucket(currentPicBedName)"
>
{{ $t('MANAGE_MAIN_PAGE_SUBMIT') }}
</el-button>
<div class="drawer-content">
<form @submit.prevent="createNewBucket(currentPicBedName)">
<div class="form-header">
<div class="form-icon">
<img
:src="`/assets/${currentPicBedName}.webp`"
class="picbed-form-icon"
>
</div>
</div>
<div class="form-divider" />
<div
v-for="option in newBucketConfig[currentPicBedName].options"
:key="option"
class="form-group"
>
<label class="form-label">
{{ newBucketConfig[currentPicBedName].configOptions[option].description }}
</label>
<!-- Input field -->
<input
v-if="newBucketConfig[currentPicBedName].configOptions[option].component === 'input' && currentPicBedName !== 'tcyun'"
v-model.trim="newBucketConfigResult[currentPicBedName + '.' + option]"
type="text"
class="form-input"
:placeholder="newBucketConfig[currentPicBedName].configOptions[option].placeholder"
>
<!-- TCyun special input with append -->
<div
v-if="currentPicBedName === 'tcyun' && newBucketConfig[currentPicBedName].configOptions[option].component === 'input'"
class="input-group"
>
<input
v-model.trim="newBucketConfigResult[currentPicBedName + '.' + option]"
type="text"
class="form-input group-input"
:placeholder="newBucketConfig[currentPicBedName].configOptions[option].placeholder"
>
<span class="input-append">{{ '-' + currentPagePicBedConfig.appId }}</span>
</div>
<!-- Select field -->
<div
v-if="newBucketConfig[currentPicBedName].configOptions[option].component === 'select'"
class="select-wrapper"
>
<select
v-model="newBucketConfigResult[currentPicBedName + '.' + option]"
class="form-select"
>
<option
v-for="(label, value) in newBucketConfig[currentPicBedName].configOptions[option].options"
:key="value"
:value="value"
>
{{ label }}
</option>
</select>
<ChevronDownIcon class="select-arrow" />
</div>
<!-- Switch field -->
<label
v-if="newBucketConfig[currentPicBedName].configOptions[option].component === 'switch'"
class="switch-label"
>
<input
v-model="newBucketConfigResult[currentPicBedName + '.' + option]"
type="checkbox"
class="switch-input"
:true-value="true"
:false-value="false"
>
<span class="switch-slider">
<span class="switch-button" />
</span>
</label>
</div>
<div class="form-actions">
<button
type="button"
class="action-button secondary"
@click="nweBucketDrawerVisible = false"
>
{{ $t('common.cancel') }}
</button>
<button
type="submit"
class="action-button primary"
>
<CheckIcon class="button-icon" />
{{ t('common.submit') }}
</button>
</div>
</form>
</div>
</el-form>
</el-drawer>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import {
ChromeFilled,
CirclePlus,
Folder,
FolderOpened,
HomeFilled,
SuccessFilled,
Switch,
Tools
} from '@element-plus/icons-vue'
import { ElNotification } from 'element-plus'
import { computed, onBeforeMount, reactive, ref, watch } from 'vue'
ArrowLeftRightIcon,
CheckIcon,
ChevronDownIcon,
ExternalLinkIcon,
FolderIcon,
GitBranchIcon,
HomeIcon,
PlusIcon,
SettingsIcon,
XIcon
} from 'lucide-vue-next'
import { onBeforeMount, reactive, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRoute, useRouter } from 'vue-router'
import useMessage from '@/hooks/useMessage'
import { useManageStore } from '@/manage/store/manageStore'
import { supportedPicBedList } from '@/manage/utils/constants'
import { newBucketConfig } from '@/manage/utils/newBucketConfig'
@@ -293,6 +352,7 @@ const { t } = useI18n()
const manageStore = useManageStore() as any
const route = useRoute()
const router = useRouter()
const message = useMessage()
const currentAlias = ref(route.query.alias as string)
const currentPicBedName = ref(route.query.picBedName as string)
@@ -309,17 +369,16 @@ const isLoadingBucketList = ref(false)
const nweBucketDrawerVisible = ref(false)
const picBedSwitchDialogVisible = ref(false)
watch(route, async newRoute => {
watch(route, async (newRoute) => {
if (newRoute.fullPath.split('?')[0] === '/main-page/manage-main-page') {
console.log('route changed')
currentAlias.value = newRoute.query.alias as string
currentPicBedName.value = newRoute.query.picBedName as string
allPicBedConfigure = JSON.parse(newRoute.query.allPicBedConfigure as string)
currentPagePicBedConfig = reactive(JSON.parse(newRoute.query.config as string))
await getBucketList()
}
})
const getCurrentActiveBucket = computed(() => (bucketNameList.value.length === 0 ? '' : bucketNameList.value[0]))
}, { deep: true })
const urlMap: IStringKeyMap = {
aliyun: 'https://oss.console.aliyun.com',
@@ -337,9 +396,9 @@ const urlMap: IStringKeyMap = {
const showNewIconList = ['aliyun', 'qiniu', 'tcyun', 's3plist']
const bucketT = t('MANAGE_MAIN_PAGE_BUCKET')
const galleryT = t('MANAGE_MAIN_PAGE_GALLERY')
const repositoryT = t('MANAGE_MAIN_PAGE_REPOSITORY')
const bucketT = t('pages.manage.main.bucket')
const galleryT = t('pages.manage.main.gallery')
const repositoryT = t('pages.manage.main.repo')
const menuTitleMap: IStringKeyMap = {
aliyun: bucketT,
@@ -355,26 +414,8 @@ const menuTitleMap: IStringKeyMap = {
local: ''
}
const rules = ruleMap(newBucketConfig)
const openPicBedUrl = () => window.electron.sendRPC(IRPCActionType.OPEN_URL, urlMap[currentPagePicBedConfig.picBedName])
function ruleMap (options: IStringKeyMap) {
return Object.keys(options).reduce((result, key) => {
options[key].options.forEach((option: string) => {
const keyName = `${key}.${option}`
const configOption = options[key].configOptions[option]
if (configOption.rule) {
result[keyName] = configOption.rule
}
if (configOption.default) {
newBucketConfigResult[keyName] = configOption.default
}
})
return result
}, {} as IStringKeyMap)
}
function openNewBucketDrawer () {
nweBucketDrawerVisible.value = true
}
@@ -390,7 +431,7 @@ function createNewBucket (picBedName: string) {
resultValue === '' && defaultValue !== undefined
? defaultValue
: resultValue === undefined
? defaultValue ?? ''
? ''
: resultValue
return result
@@ -399,23 +440,17 @@ function createNewBucket (picBedName: string) {
resultMap.BucketName = `${resultMap.BucketName}-${currentPagePicBedConfig.appId}`
}
resultMap.endpoint = currentPagePicBedConfig.endpoint
window.electron.triggerRPC(IRPCActionType.MANAGE_CREATE_BUCKET, currentAlias, resultMap).then((result: any) => {
window.electron.triggerRPC(IRPCActionType.MANAGE_CREATE_BUCKET, currentAlias.value, resultMap).then((result: any) => {
if (result) {
ElNotification({
title: t('MANAGE_MAIN_PAGE_TIPS'),
message: t('MANAGE_MAIN_PAGE_TIPS_SUCCESS'),
type: 'success'
})
// Show success notification
message.success(t('pages.manage.main.createSuccess'))
nweBucketDrawerVisible.value = false
setTimeout(() => {
getBucketList()
}, 2000)
} else {
ElNotification({
title: t('MANAGE_MAIN_PAGE_TIPS'),
message: t('MANAGE_MAIN_PAGE_TIPS_FAILED'),
type: 'error'
})
// Show error notification
message.error(t('pages.manage.main.createFailed'))
}
})
}
@@ -427,7 +462,6 @@ async function getBucketList () {
const result = await window.electron.triggerRPC<any>(IRPCActionType.MANAGE_GET_BUCKET_LIST, currentAlias.value)
isLoadingBucketList.value = false
if (result.length > 0) {
result.forEach((item: any) => {
bucketList.value[item.Name] = item
@@ -474,7 +508,11 @@ function handleSelectMenu (bucketName: string) {
router.push({
path: '/main-page/manage-main-page/manage-bucket-page',
query: {
configMap: JSON.stringify(configMap)
configMap: JSON.stringify(configMap),
alias: currentAlias.value,
picBedName: currentPicBedName.value,
config: JSON.stringify(currentPagePicBedConfig),
allPicBedConfigure: JSON.stringify(allPicBedConfigure)
}
})
}
@@ -515,8 +553,15 @@ function changePicBed () {
}
function openBucketPageSetting () {
console.log('Open Bucket Page Setting')
router.push({
path: '/main-page/manage-main-page/manage-setting-page'
path: '/main-page/manage-main-page/manage-setting-page',
query: {
alias: currentAlias.value,
picBedName: currentPicBedName.value,
config: JSON.stringify(currentPagePicBedConfig),
allPicBedConfigure: JSON.stringify(allPicBedConfigure)
}
})
}
@@ -525,74 +570,4 @@ onBeforeMount(() => {
})
</script>
<style lang="stylus">
.layout
height 100%
display flex
flex-direction row
&__menu
background: #fff
color: #fff
display: flex
flex-direction: column
border-bottom-right-radius: 4px
z-index 1
width: 130px
position: relative
&__button
font-weight: bold;
text-align: left;
padding-left: 0px;
padding-top: 10px;
&__item
color:#2d8cf0
width: 100%
height: 100%
display: flex
align-items: center
justify-content: center
&:hover
cursor: pointer
color: orange
&__icon
width: 25px
height: 25px
&__divider
&__icon
&:hover
cursor: pointer
color: orange
&__list
flex: 1
overflow-y: auto
&__item
width: 100%
height: 100%
display: flex
color: #2d8cf0
align-items: center
justify-content: center
&:hover
cursor: pointer
color: orange
&__icon
width: 25px
height: 25px
&__setting
position relative
overflow hidden
&__item
width: 100%
display: flex
align-items: center
color: #000
justify-content: center
font-size: 12px
font-family: Arial, Helvetica, sans-serif
&:hover
cursor: pointer
color: orange
&__icon
width: 25px
height: 25px
</style>
<style src="./css/ManageMain.css" scoped></style>

View File

@@ -1,234 +1,253 @@
<template>
<div id="manage-setting">
<el-row
class="view-title"
align="middle"
justify="center"
style="font-size: 20px; color: black"
>
{{ $t('MANAGE_SETTING_TITLE') }}
</el-row>
<el-row class="setting-list">
<el-col
:span="20"
:offset="2"
>
<el-row style="width: 100%">
<el-form
label-position="left"
label-width="50%"
size="default"
style="position: relative; width: 100%"
>
<el-form-item>
<template #label>
<span style="position: absolute; left: 0">
<span>{{ $t('MANAGE_SETTING_CLEAR_CACHE_TITLE') }} </span>
<span style="color: #ff4949">{{ formatFileSize(dbSize) === '' ? 0 : formatFileSize(dbSize) }} </span>
<span> &nbsp;{{ $t('MANAGE_SETTING_CLEAR_CACHE_FREE_TITLE') }} </span>
<span style="color: #ff4949">{{ dbSizeAvailableRate }} %</span>
<el-tooltip
effect="dark"
:content="$t('MANAGE_SETTING_CLEAR_CACHE_TIPS')"
placement="right"
:persistent="false"
teleported
>
<el-icon>
<InfoFilled />
</el-icon>
</el-tooltip>
</span>
</template>
<el-popconfirm
:title="$t('MANAGE_SETTING_CLEAR_CACHE_PROMPT')"
:confirm-button-text="$t('CONFIRM')"
:cancel-button-text="$t('CANCEL')"
hide-icon
:persistent="false"
teleported
@confirm="handleClearDb"
<div class="manage-setting-container">
<!-- Cache Info Card -->
<div class="setting-card content-card">
<div class="card-content">
<div class="setting-section">
<div class="form-group">
<div class="form-control">
<button
type="button"
class="action-button warning"
@click="handleConfirmClearDb"
>
<template #reference>
<el-button
type="primary"
plain
style="position: absolute; right: 0"
>
{{ $t('MANAGE_SETTING_CLEAR_CACHE_BUTTON') }}
</el-button>
</template>
</el-popconfirm>
</el-form-item>
<DynamicSwitch
v-for="item in switchFieldsConfigList"
:key="item.configName"
v-model="form[item.configName]"
:segments="item.segments"
:tooltip="item.tooltip"
:config-name="item.configName"
:active-text="item.activeText"
:inactive-text="item.inactiveText"
/>
<el-link
v-if="form.customRename"
style="margin-top: 10px; margin-bottom: 10px; color: #409eff"
:underline="false"
>
{{ $t('MANAGE_SETTING_CUSTOM_PATTERN_TITLE') }}
</el-link>
<el-input
v-if="form.customRename"
v-model="form.customRenameFormat"
:placeholder="$t('MANAGE_SETTING_CUSTOM_PATTERN_TIPS')"
style="width: 100%"
/>
<el-table
v-if="form.customRename"
:data="customRenameFormatTable"
style="width: 100%; margin-top: 10px; margin-left: 10%"
:header-cell-style="{ 'text-align': 'center' }"
:cell-style="{ 'text-align': 'center' }"
@cell-click="handleCellClick"
>
<el-table-column
v-for="prop in ['placeholder', 'description', 'placeholderB', 'descriptionB']"
:key="prop"
:prop="prop"
:label="$t('MANAGE_SETTING_CUSTOM_PATTERN_TABLE_TITLE' as any)"
width="150"
/>
</el-table>
<br v-if="form.customRename">
<DynamicSwitch
v-for="item in switchFieldsSpecialList"
:key="item.configName"
v-model="form[item.configName]"
:segments="item.segments"
:tooltip="item.tooltip"
:config-name="item.configName"
/>
<el-form-item>
<template #label>
<span style="position: absolute; left: 0">
{{ $t('MANAGE_SETTING_MAX_DOWNLOAD_FILE_SIZE_TITLE') }}
<el-tooltip
effect="dark"
:content="$t('MANAGE_SETTING_MAX_DOWNLOAD_FILE_SIZE_TIPS')"
placement="right"
:persistent="false"
teleported
>
<el-icon>
<InfoFilled />
</el-icon>
</el-tooltip>
</span>
</template>
<el-input-number
v-model="form.maxDownloadFileCount"
style="position: absolute; right: 0"
:placeholder="$t('MANAGE_SETTING_MAX_DOWNLOAD_FILE_SIZE_INPUT_TIPS')"
:min="1"
:max="9999"
:step="1"
/>
</el-form-item>
<el-form-item>
<template #label>
<span style="position: absolute; left: 0">
{{ $t('MANAGE_SETTING_PRESIGNED_URL_EXPIRE_TITLE') }}
<el-tooltip
effect="dark"
:content="$t('MANAGE_SETTING_PRESIGNED_URL_EXPIRE_TIPS')"
placement="right"
:persistent="false"
teleported
>
<el-icon>
<InfoFilled />
</el-icon>
</el-tooltip>
</span>
</template>
<el-input-number
v-model="form.PreSignedExpire"
style="position: absolute; right: 0"
:placeholder="$t('MANAGE_SETTING_PRESIGNED_URL_EXPIRE_TIPS')"
:min="1"
:step="1"
/>
</el-form-item>
<el-link
style="margin-top: 10px; margin-bottom: 10px; color: #409eff"
:underline="false"
>
{{ $t('MANAGE_SETTING_CHOOSE_COPY_FORMAT_TITLE') }}
</el-link>
<br>
<el-radio-group v-model="form.pasteFormat">
<el-radio
v-for="item in pasteFormatList"
:key="item"
:value="item"
>
{{ $t(`MANAGE_SETTING_CHOOSE_COPY_FORMAT_${item.toUpperCase().replace(/-/g, '_')}` as any) }}
</el-radio>
</el-radio-group>
<el-link
v-if="form.pasteFormat === 'custom'"
style="margin-top: 10px; margin-bottom: 10px; color: #409eff"
:underline="false"
>
{{ $t('MANAGE_SETTING_CUSTOM_COPY_FORMAT_TITLE') }}
</el-link>
<el-input
v-if="form.pasteFormat === 'custom'"
v-model="form.customPasteFormat"
:placeholder="$t('MANAGE_SETTING_CUSTOM_COPY_FORMAT_TIPS')"
style="width: 100%"
/>
<div>
<el-link
style="margin-top: 10px; margin-bottom: 10px; color: #409eff"
:underline="false"
>
{{ $t('MANAGE_SETTING_CHOOSE_DOWNLOAD_FOLDER_TITLE') }}
</el-link>
<Trash2Icon :size="16" />
{{ t('pages.manage.setting.clearCache', { percent: dbSizeAvailableRate, size: formatFileSize(dbSize) || 0 }) }}
</button>
</div>
<el-input
v-model="form.downloadDir"
disabled
:placeholder="$t('MANAGE_SETTING_CHOOSE_DOWNLOAD_FOLDER_TIPS')"
style="width: 100%; margin-top: 10px"
</div>
</div>
</div>
</div>
<!-- General Settings Card -->
<div class="setting-card content-card">
<div class="card-content">
<div class="setting-section">
<CustomSwitch
v-for="item in switchFieldsConfigList"
:key="item.configName"
v-model="form[item.configName]"
:segments="item.segments"
:tooltip="item.tooltip"
:active-text="item.activeText"
:inactive-text="item.inactiveText"
/>
</div>
</div>
</div>
<!-- Custom Rename Pattern Card -->
<div
v-if="form.customRename"
class="setting-card content-card"
>
<div class="card-content">
<div class="setting-section">
<div class="section-header">
<h4 class="section-title">
{{ t('pages.manage.setting.customRenameTableTitle') }}
</h4>
</div>
<div class="form-group">
<input
v-model="form.customRenameFormat"
type="text"
class="form-input"
:placeholder="t('pages.manage.setting.customRenameTablePlaceholder')"
>
<template #append>
<el-button
type="primary"
@click="handleDownloadDirClick"
</div>
<!-- Pattern Reference Table -->
<div class="pattern-table-container">
<table class="pattern-table">
<thead>
<tr>
<th>{{ t('pages.manage.setting.placeholder') }}</th>
<th>{{ t('pages.manage.setting.description') }}</th>
<th>{{ t('pages.manage.setting.placeholder') }}</th>
<th>{{ t('pages.manage.setting.description') }}</th>
</tr>
</thead>
<tbody>
<tr
v-for="(row, index) in customRenameFormatTable"
:key="index"
>
<el-icon>
<Folder />
</el-icon>
{{ $t('MANAGE_SETTING_CHOOSE_DOWNLOAD_FOLDER_BUTTON') }}
</el-button>
</template>
</el-input>
</el-form>
<el-divider border-style="none" />
</el-row>
</el-col>
</el-row>
<td
class="clickable"
@click="handleCellClick(row, { property: 'placeholder' })"
>
{{ row.placeholder }}
</td>
<td>{{ row.description }}</td>
<td
class="clickable"
@click="handleCellClick(row, { property: 'placeholderB' })"
>
{{ row.placeholderB }}
</td>
<td>{{ row.descriptionB }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Special Settings Card -->
<div class="setting-card content-card">
<div class="card-content">
<div class="setting-section">
<!-- Special Switch Fields -->
<CustomSwitch
v-for="item in switchFieldsSpecialList"
:key="item.configName"
v-model="form[item.configName]"
:segments="item.segments"
:tooltip="item.tooltip"
/>
</div>
</div>
</div>
<!-- Download Settings Card -->
<div class="setting-card content-card">
<div class="card-content">
<div class="setting-section">
<!-- Max Download File Count -->
<div class="form-group">
<div class="form-label-wrapper">
<span class="form-label">
{{ t('pages.manage.setting.maxDownLoadFileLimit') }}
</span>
</div>
<div class="form-control">
<input
v-model.number="form.maxDownloadFileCount"
type="number"
class="form-input number-input"
:placeholder="t('pages.manage.setting.maxDownLoadFileLimitDesc')"
min="1"
max="9999"
step="1"
>
</div>
</div>
<!-- PreSigned URL Expire -->
<div class="form-group">
<div class="form-label-wrapper">
<span class="form-label">
{{ t('pages.manage.setting.preSignedUrlExpire') }}
</span>
</div>
<div class="form-control">
<input
v-model.number="form.PreSignedExpire"
type="number"
class="form-input number-input"
:placeholder="t('pages.manage.setting.preSignedUrlExpireDesc')"
min="1"
step="1"
>
</div>
</div>
</div>
</div>
</div>
<!-- Copy Format Card -->
<div class="setting-card content-card">
<div class="card-content">
<div class="setting-section">
<div class="section-header">
<h4 class="section-title">
{{ t('pages.manage.setting.copyFormat.title') }}
</h4>
</div>
<div class="radio-group">
<label
v-for="item in pasteFormatList"
:key="item"
class="radio-option"
>
<input
v-model="form.pasteFormat"
type="radio"
:value="item"
class="radio-input"
>
<span class="radio-custom" />
<span class="radio-text">
{{ t(`pages.manage.setting.copyFormat.${item}`) }}
</span>
</label>
</div>
<!-- Custom Copy Format -->
<div
class="form-group"
>
<div class="form-label-wrapper">
<span class="form-label">
{{ t('pages.manage.setting.copyFormat.customTitle') }}
</span>
</div>
<input
v-model="form.customPasteFormat"
type="text"
class="form-input"
:placeholder="t('pages.manage.setting.copyFormat.customTips')"
>
</div>
</div>
</div>
</div>
<!-- Download Folder Card -->
<div class="setting-card content-card">
<div class="card-content">
<div class="setting-section">
<div class="section-header">
<h4 class="section-title">
{{ t('pages.manage.setting.selectDownloadFolderTitle') }}
</h4>
</div>
<div class="form-group">
<div class="input-group">
<input
v-model="form.downloadDir"
type="text"
class="form-input group-input"
disabled
:placeholder="t('pages.manage.setting.defaultDownloadFolder')"
>
<button
type="button"
class="input-append-button"
@click="handleDownloadDirClick"
>
<FolderIcon :size="16" />
{{ t('pages.manage.setting.browse') }}
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { Folder, InfoFilled } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import { FolderIcon, Trash2Icon } from 'lucide-vue-next'
import { onBeforeMount, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import DynamicSwitch from '@/manage/components/DynamicSwitch.vue'
import useConfirm from '@/hooks/useConfirm'
import useMessage from '@/hooks/useMessage'
import CustomSwitch from '@/manage/components/CustomSwitch.vue'
import { fileCacheDbInstance } from '@/manage/store/bucketFileDb'
import { customRenameFormatTable, formatFileSize } from '@/manage/utils/common'
import { getConfig, saveConfig } from '@/manage/utils/dataSender'
@@ -236,13 +255,14 @@ import { IRPCActionType } from '@/utils/enum'
import type { IStringKeyMap } from '#/types/types'
const { t } = useI18n()
const message = useMessage()
const { confirm } = useConfirm()
const form = ref<IStringKeyMap>({
timestampRename: false,
randomStringRename: false,
customRename: false,
isAutoRefresh: false,
isShowThumbnail: false,
isShowList: false,
isUsePreSignedUrl: false,
isIgnoreCase: false,
isForceCustomUrlHttps: false,
@@ -275,7 +295,6 @@ settingsKeys.forEach(key => {
const switchFieldsList = [
'isAutoRefresh',
'isShowThumbnail',
'isShowList',
'isUsePreSignedUrl',
'isForceCustomUrlHttps',
'isEncodeUrl',
@@ -285,23 +304,23 @@ const switchFieldsList = [
'randomStringRename',
'customRename'
]
const switchFieldsNoTipsList = ['isShowThumbnail', 'isShowList', 'isUsePreSignedUrl']
const switchFieldsHasActiveTextList = ['isShowList']
const switchFieldsNoTipsList = ['isShowThumbnail', 'isUsePreSignedUrl']
const switchFieldsHasActiveTextList = [] as string[]
const switchFieldsConfigList = switchFieldsList.map(item => ({
configName: item,
segments: [
{
text: t(`MANAGE_SETTING_${item.toUpperCase()}_TITLE` as any),
style: 'color: black;'
text: t(`pages.manage.setting.${item}Title` as any),
style: 'color: var(--color-text-primary);'
}
],
tooltip: switchFieldsNoTipsList.includes(item) ? undefined : t(`MANAGE_SETTING_${item.toUpperCase()}_TIPS` as any),
tooltip: switchFieldsNoTipsList.includes(item) ? undefined : t(`pages.manage.setting.${item}Tips` as any),
activeText: switchFieldsHasActiveTextList.includes(item)
? t(`MANAGE_SETTING_${item.toUpperCase()}_ON` as any)
? t(`pages.manage.setting.${item}On` as any)
: undefined,
inactiveText: switchFieldsHasActiveTextList.includes(item)
? t(`MANAGE_SETTING_${item.toUpperCase()}_OFF` as any)
? t(`pages.manage.setting.${item}Off` as any)
: undefined
}))
@@ -310,37 +329,37 @@ const switchFieldsSpecialList = [
configName: 'isDownloadFileKeepDirStructure',
segments: [
{
text: t('MANAGE_SETTING_ISDOWNLOADFILEKEEPDIRSTRUCTURE_TITLE_A'),
style: 'color: black;'
text: t('pages.manage.setting.download'),
style: 'color: var(--color-text-primary);'
},
{
text: t('MANAGE_SETTING_ISDOWNLOADFILEKEEPDIRSTRUCTURE_TITLE_B'),
text: t('pages.manage.setting.file'),
style: 'color: orange;'
},
{
text: t('MANAGE_SETTING_ISDOWNLOADFILEKEEPDIRSTRUCTURE_TITLE_C'),
style: 'color: black;'
text: t('pages.manage.setting.keepDirStructure'),
style: 'color: var(--color-text-primary);'
}
],
tooltip: t('MANAGE_SETTING_ISDOWNLOADFILEKEEPDIRSTRUCTURE_TIPS')
tooltip: t('pages.manage.setting.keepDirStructureDesc')
},
{
configName: 'isDownloadFolderKeepDirStructure',
segments: [
{
text: t('MANAGE_SETTING_ISDOWNLOADFILEKEEPDIRSTRUCTURE_TITLE_A'),
style: 'color: black;'
text: t('pages.manage.setting.download'),
style: 'color: var(--color-text-primary);'
},
{
text: t('MANAGE_SETTING_ISDOWNLOADFOLDERKEEPDIRSTRUCTURE_TITLE_D'),
style: 'color: coral;'
text: t('pages.manage.setting.folder'),
style: 'color: orange;'
},
{
text: t('MANAGE_SETTING_ISDOWNLOADFILEKEEPDIRSTRUCTURE_TITLE_C'),
style: 'color: black;'
text: t('pages.manage.setting.keepDirStructure'),
style: 'color: var(--color-text-primary);'
}
],
tooltip: t('MANAGE_SETTING_ISDOWNLOADFILEKEEPDIRSTRUCTURE_TIPS')
tooltip: t('pages.manage.setting.keepDirStructureDesc')
}
]
@@ -360,18 +379,33 @@ async function handleDownloadDirClick () {
const handleCellClick = (row: any, column: any) => {
navigator.clipboard.writeText(row[column.property])
ElMessage.success(`${t('MANAGE_SETTING_COPY_MESSAGE')}${row[column.property]}`)
message.success(`${t('pages.manage.setting.copySuccess', { name: row[column.property] })}`)
}
function handleClearDb () {
function handleConfirmClearDb () {
confirm({
title: t('pages.manage.setting.notice'),
message: t('pages.manage.setting.clearCacheMsg'),
type: 'warning',
confirmButtonText: t('common.confirm'),
cancelButtonText: t('common.cancel'),
center: true
}).then(result => {
if (result) {
confirmClearDb()
}
})
}
function confirmClearDb () {
fileCacheDbInstance
.delete()
.then(() => {
getIndexDbSize()
ElMessage.success(t('MANAGE_SETTING_CLEAR_CACHE_SUCCESS'))
message.success(t('pages.manage.setting.clearSuccess'))
})
.catch(() => {
ElMessage.error(t('MANAGE_SETTING_CLEAR_CACHE_FAILED'))
message.error(t('pages.manage.setting.clearFailed'))
})
}
@@ -388,8 +422,4 @@ onBeforeMount(() => {
})
</script>
<style lang="stylus">
#manage-setting
height 100%
overflow-y auto
</style>
<style scoped src="./css/ManageSetting.css"></style>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,647 @@
/* ManageMain Page Styles */
.manage-container {
height: 97%;
display: flex;
flex-direction: column;
padding: 0.75rem;
}
.manage-card {
background: var(--color-background-secondary);
border-radius: var(--radius-lg);
border: 1px solid var(--color-border);
box-shadow: var(--shadow-sm);
}
.header-card {
padding: 0.5rem;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
}
.header-content {
display: flex;
align-items: center;
gap: 1rem;
}
.header-icon {
width: 68px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
}
.header-icon-img {
width: 40px;
height: 40px;
object-fit: contain;
}
.header-text .header-title {
font-size: 1.25rem;
font-weight: 600;
color: var(--color-text-primary);
margin: 0 0 0.01rem 0;
}
.header-text .header-subtitle {
font-size: 0.875rem;
color: var(--color-text-secondary);
margin: 0;
}
.header-actions {
display: flex;
gap: 1rem;
}
.main-card {
flex: 1;
overflow: hidden;
min-height: 0; /* Fix for flex overflow */
}
.main-layout {
display: flex;
height:99%;
}
.sidebar {
width: 160px;
background: var(--color-surface-secondary);
border-right: 1px solid var(--color-border);
display: flex;
flex-direction: column;
min-height: 0; /* Fix for flex overflow */
}
.sidebar-header {
padding: 1rem;
border-bottom: 1px solid var(--color-border);
flex-shrink: 0;
}
.sidebar-title {
font-size: 1rem;
font-weight: 600;
color: var(--color-text-primary);
margin: 0;
}
.sidebar-content {
flex: 1;
overflow-y: auto;
padding: 0.5rem;
min-height: 0;
}
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 2rem;
gap: 0.5rem;
}
.loading-spinner {
width: 20px;
height: 20px;
border: 2px solid var(--color-border);
border-top: 2px solid var(--color-accent);
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.loading-text {
font-size: 0.75rem;
color: var(--color-text-secondary);
}
.menu-list {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.menu-item {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem;
border-radius: var(--radius-md);
cursor: pointer;
transition: var(--transition-fast);
font-size: 0.875rem;
}
.menu-item:hover {
background: var(--color-surface);
}
.menu-item.active {
background: var(--color-accent);
color: white;
}
.menu-item.active .menu-icon {
color: white;
}
.menu-icon {
width: 16px;
height: 16px;
color: var(--color-text-secondary);
flex-shrink: 0;
}
.menu-icon.active {
color: var(--color-accent);
}
.menu-text {
font-weight: 500;
line-height: 1.2;
word-break: break-word;
}
.sidebar-footer {
border-top: 1px solid var(--color-border);
padding: 0.5rem;
flex-shrink: 0;
}
.footer-actions {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.footer-action-item {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem;
border-radius: var(--radius-md);
cursor: pointer;
transition: var(--transition-fast);
font-size: 0.875rem;
background: none;
border: none;
color: var(--color-text-primary);
text-align: left;
width: 100%;
}
.footer-action-item:hover {
background: var(--color-surface);
}
.action-icon {
width: 16px;
height: 16px;
color: var(--color-text-secondary);
flex-shrink: 0;
}
.action-text {
font-weight: 500;
}
.content-area {
flex: 1;
padding: 1.5rem;
overflow-y: auto;
min-height: 0;
}
.action-button {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.75rem 1.5rem;
border: none;
border-radius: var(--radius-lg);
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: var(--transition-fast);
font-family: inherit;
}
.action-button.primary {
background: var(--color-accent);
color: white;
}
.action-button.primary:hover {
background: var(--color-accent-hover);
transform: translateY(-1px);
box-shadow: var(--shadow-md);
}
.action-button.secondary {
background: var(--color-surface-elevated);
color: var(--color-text-primary);
border: 1px solid var(--color-border);
}
.action-button.secondary:hover {
background: var(--color-surface);
border-color: var(--color-accent);
color: var(--color-accent);
}
.button-icon {
width: 16px;
height: 16px;
}
/* Dialog styles */
.dialog-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 2000;
}
.dialog-container {
background: var(--color-surface);
border-radius: var(--radius-lg);
border: 1px solid var(--color-border);
box-shadow: var(--shadow-xl);
max-width: 600px;
width: 90%;
max-height: 80vh;
overflow: hidden;
}
.dialog-header {
padding: 1.5rem 1.5rem 0;
display: flex;
justify-content: space-between;
align-items: center;
}
.dialog-title {
font-size: 1.25rem;
font-weight: 600;
color: var(--color-text-primary);
margin: 0;
}
.dialog-close {
background: none;
border: none;
color: var(--color-text-secondary);
cursor: pointer;
padding: 0.25rem;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
border-radius: var(--radius-sm);
transition: var(--transition-fast);
}
.dialog-close:hover {
background: var(--color-surface-elevated);
color: var(--color-text-primary);
}
.close-icon {
width: 16px;
height: 16px;
}
.dialog-content {
padding: 1.5rem;
}
.choice-cos {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
}
.picbed-card {
display: flex;
flex-direction: column;
align-items: center;
padding: 1.5rem;
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
cursor: pointer;
transition: var(--transition-fast);
position: relative;
background: var(--color-surface-elevated);
}
.picbed-card:hover {
border-color: var(--color-accent);
transform: translateY(-2px);
box-shadow: var(--shadow-md);
}
.picbed-card.active {
border-color: var(--color-accent);
background-color: rgba(64, 158, 255, 0.1);
}
.picbed-card.main-card {
border-color: var(--color-error);
}
.picbed-card.main-card:hover {
border-color: var(--color-error);
background-color: rgba(255, 59, 48, 0.1);
}
.card-icon {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 0.75rem;
}
.picbed-icon {
width: 32px;
height: 32px;
object-fit: contain;
}
.main-icon {
width: 24px;
height: 24px;
color: var(--color-error);
}
.card-content {
text-align: center;
}
.card-title {
font-size: 0.875rem;
font-weight: 500;
color: var(--color-text-primary);
}
.main-title {
color: var(--color-error);
}
.check-icon {
position: absolute;
top: 0.5rem;
right: 0.5rem;
width: 20px;
height: 20px;
color: var(--color-accent);
}
/* Drawer styles */
.drawer-overlay {
position: fixed;
top: 32px;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: flex-end;
z-index: 2000;
}
.drawer-container {
background: var(--color-surface);
height: 100vh;
width: 400px;
max-width: 90vw;
overflow-y: auto;
box-shadow: var(--shadow-xl);
}
.drawer-header {
padding: 1.5rem;
border-bottom: 1px solid var(--color-border);
display: flex;
justify-content: space-between;
align-items: center;
}
.drawer-title {
font-size: 1.25rem;
font-weight: 600;
color: var(--color-text-primary);
margin: 0;
}
.drawer-close {
background: none;
border: none;
color: var(--color-text-secondary);
cursor: pointer;
padding: 0.25rem;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
border-radius: var(--radius-sm);
transition: var(--transition-fast);
}
.drawer-close:hover {
background: var(--color-surface-elevated);
color: var(--color-text-primary);
}
.drawer-content {
padding: 1.5rem;
}
.form-header {
display: flex;
justify-content: center;
padding: 2rem 0;
}
.form-icon {
width: 60px;
height: 60px;
display: flex;
align-items: center;
justify-content: center;
}
.picbed-form-icon {
width: 48px;
height: 48px;
object-fit: contain;
}
.form-divider {
height: 1px;
background: var(--color-border);
margin: 1.5rem 0;
}
.form-group {
margin-bottom: 1.5rem;
}
.form-label {
display: block;
margin-bottom: 0.5rem;
font-size: 0.875rem;
font-weight: 500;
color: var(--color-text-primary);
}
.form-input {
width: 100%;
padding: 0.75rem;
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
background: var(--color-surface-elevated);
color: var(--color-text-primary);
font-size: 0.875rem;
transition: var(--transition-fast);
box-sizing: border-box;
}
.form-input:focus {
outline: none;
border-color: var(--color-accent);
box-shadow: 0 0 0 2px rgba(0, 122, 255, 0.2);
}
.input-group {
display: flex;
}
.group-input {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
border-right: none;
}
.input-append {
padding: 0.75rem;
background: var(--color-background-secondary);
border: 1px solid var(--color-border);
border-left: none;
border-top-right-radius: var(--radius-md);
border-bottom-right-radius: var(--radius-md);
font-size: 0.875rem;
color: var(--color-text-secondary);
}
.select-wrapper {
position: relative;
}
.form-select {
width: 100%;
padding: 0.75rem 2.5rem 0.75rem 0.75rem;
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
background: var(--color-surface-elevated);
color: var(--color-text-primary);
font-size: 0.875rem;
appearance: none;
cursor: pointer;
transition: var(--transition-fast);
}
.form-select:focus {
outline: none;
border-color: var(--color-accent);
box-shadow: 0 0 0 2px rgba(0, 122, 255, 0.2);
}
.select-arrow {
position: absolute;
right: 0.75rem;
top: 50%;
transform: translateY(-50%);
width: 16px;
height: 16px;
color: var(--color-text-secondary);
pointer-events: none;
}
.switch-label {
display: flex;
align-items: center;
cursor: pointer;
}
.switch-input {
position: absolute;
opacity: 0;
cursor: pointer;
}
.switch-slider {
position: relative;
width: 3rem;
height: 1.5rem;
background: var(--color-border);
border-radius: 0.75rem;
transition: var(--transition-fast);
}
.switch-button {
position: absolute;
top: 2px;
left: 2px;
width: 1.25rem;
height: 1.25rem;
background: white;
border-radius: 50%;
transition: var(--transition-fast);
box-shadow: var(--shadow-sm);
}
.switch-input:checked + .switch-slider {
background: var(--color-accent);
}
.switch-input:checked + .switch-slider .switch-button {
transform: translateX(1.5rem);
}
.form-actions {
display: flex;
gap: 1rem;
margin-top: 2rem;
justify-content: flex-end;
}

View File

@@ -0,0 +1,515 @@
/* Container */
.manage-setting-container {
padding: 1rem;
width: 100%;
margin: 0;
display: flex;
flex-direction: column;
gap: 1rem;
min-height: 100vh;
box-sizing: border-box;
overflow-y: auto;
}
/* Card Base */
.setting-card {
background: var(--color-surface);
border: 1px solid var(--color-border-secondary);
border-radius: var(--radius-xl);
overflow: hidden;
transition: var(--transition-medium);
box-shadow: var(--shadow-sm);
}
.setting-card:hover {
box-shadow: var(--shadow-md);
border-color: var(--color-border);
}
/* Header Card */
.header-card .card-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem 1.5rem;
border-bottom: 1px solid var(--color-border-secondary);
flex-wrap: wrap;
gap: 1rem;
}
.header-content {
display: flex;
align-items: center;
gap: 1rem;
flex: 1;
}
.header-icon {
color: var(--color-blue-common);
display: flex;
align-items: center;
}
.header-content h1 {
font-size: 1.5rem;
font-weight: 600;
color: var(--color-text-primary);
margin: 0;
letter-spacing: -0.025em;
}
.header-content p {
font-size: 0.875rem;
color: var(--color-text-secondary);
margin: 0;
}
.header-actions {
display: flex;
align-items: center;
gap: 0.75rem;
flex-wrap: wrap;
}
/* Action Button Base */
.action-button {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
background: var(--color-surface-elevated);
color: var(--color-text-primary);
font-size: 0.875rem;
font-weight: 500;
text-decoration: none;
transition: var(--transition-fast);
cursor: pointer;
white-space: nowrap;
}
.action-button:hover {
background: var(--color-background-secondary);
border-color: var(--color-border-darker);
transform: translateY(-1px);
box-shadow: var(--shadow-sm);
}
.action-button:active {
transform: translateY(0);
box-shadow: none;
}
.action-button.primary {
background: var(--color-blue-common);
color: white;
border-color: var(--color-blue-common);
}
.action-button.primary:hover {
background: var(--color-accent);
border-color: var(--color-accent);
}
.action-button.secondary {
background: var(--color-background-secondary);
color: var(--color-text-secondary);
}
.action-button.warning {
background: var(--color-warning);
color: white;
border-color: var(--color-warning);
}
.action-button.warning:hover {
background: var(--color-warning);
border-color: var(--color-warning);
opacity: 0.9;
}
.action-button .button-icon {
width: 16px;
height: 16px;
}
/* Content Cards */
.content-card {
flex: 1;
}
.card-content {
padding: 1.25rem;
}
/* Setting sections with reduced spacing */
.setting-section {
display: flex;
gap: 0.5rem;
flex-direction: column;
}
.setting-section + .setting-section {
margin-top: 1rem;
padding-top: 1rem;
border-top: 1px solid var(--color-border-secondary);
}
/* Form Groups */
.form-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.form-label-wrapper {
display: flex;
align-items: center;
gap: 0.5rem;
}
.form-label {
font-size: 0.875rem;
font-weight: 500;
color: var(--color-text-primary);
}
.form-control {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.form-input {
width: 100%;
padding: 0.75rem;
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
background: var(--color-surface-elevated);
color: var(--color-text-primary);
font-size: 0.875rem;
transition: var(--transition-fast);
}
.form-input:focus {
outline: none;
border-color: var(--color-accent);
box-shadow: 0 0 0 2px rgba(0, 122, 255, 0.2);
}
.form-input:disabled {
background: var(--color-background-secondary);
color: var(--color-text-tertiary);
cursor: not-allowed;
}
.number-input {
max-width: 200px;
}
/* Cache Info */
.cache-info {
display: flex;
gap: 1rem;
font-size: 0.75rem;
color: var(--color-text-secondary);
margin-top: 0.5rem;
}
.cache-size {
font-weight: 500;
}
/* Section Headers */
.section-header {
margin-bottom: 0.75rem;
}
.section-title {
font-size: 1rem;
font-weight: 500;
color: var(--color-accent);
margin: 0;
}
/* Pattern Table */
.pattern-table-container {
margin-top: 0.75rem;
overflow-x: auto;
}
.pattern-table {
width: 100%;
border-collapse: collapse;
font-size: 0.75rem;
}
.pattern-table th {
background: var(--color-background-secondary);
padding: 0.5rem;
border: 1px solid var(--color-border);
font-weight: 500;
color: var(--color-text-primary);
text-align: center;
}
.pattern-table td {
padding: 0.5rem;
border: 1px solid var(--color-border);
color: var(--color-text-secondary);
text-align: center;
}
.pattern-table td.clickable {
cursor: pointer;
color: var(--color-accent);
transition: var(--transition-fast);
}
.pattern-table td.clickable:hover {
background: var(--color-surface-elevated);
}
/* Radio Groups */
.radio-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.radio-option {
display: flex;
align-items: center;
gap: 0.75rem;
cursor: pointer;
padding: 0.625rem;
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
transition: var(--transition-fast);
background: var(--color-surface-elevated);
}
.radio-option:hover {
border-color: var(--color-accent);
background: var(--color-surface);
}
.radio-input {
position: absolute;
opacity: 0;
cursor: pointer;
}
.radio-custom {
position: relative;
width: 1.25rem;
height: 1.25rem;
border: 2px solid var(--color-border);
border-radius: 50%;
background: var(--color-surface-elevated);
transition: var(--transition-fast);
flex-shrink: 0;
}
.radio-custom::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--color-accent);
transform: translate(-50%, -50%) scale(0);
transition: var(--transition-fast);
}
.radio-input:checked + .radio-custom {
border-color: var(--color-accent);
}
.radio-input:checked + .radio-custom::after {
transform: translate(-50%, -50%) scale(1);
}
.radio-text {
font-size: 0.875rem;
color: var(--color-text-primary);
line-height: 1.4;
}
/* Input Groups */
.input-group {
display: flex;
}
.group-input {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
border-right: none;
}
.input-append-button {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.75rem 1rem;
background: var(--color-accent);
color: white;
border: none;
border-top-right-radius: var(--radius-md);
border-bottom-right-radius: var(--radius-md);
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: var(--transition-fast);
}
.input-append-button:hover {
background: var(--color-accent-hover);
}
/* Dialog Styles */
.dialog-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 2000;
}
.dialog-container {
background: var(--color-surface);
border-radius: var(--radius-lg);
border: 1px solid var(--color-border);
box-shadow: var(--shadow-xl);
max-width: 400px;
width: 90%;
overflow: hidden;
}
.dialog-header {
padding: 1.5rem 1.5rem 0;
display: flex;
justify-content: space-between;
align-items: center;
}
.dialog-title {
font-size: 1.25rem;
font-weight: 600;
color: var(--color-text-primary);
margin: 0;
}
.dialog-close {
background: none;
border: none;
color: var(--color-text-secondary);
cursor: pointer;
padding: 0.25rem;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
border-radius: var(--radius-sm);
transition: var(--transition-fast);
}
.dialog-close:hover {
background: var(--color-surface-elevated);
color: var(--color-text-primary);
}
.close-icon {
width: 16px;
height: 16px;
}
.dialog-content {
padding: 1.5rem;
}
.confirm-message {
color: var(--color-text-secondary);
line-height: 1.5;
margin: 0;
}
.dialog-actions {
display: flex;
gap: 1rem;
padding: 0 1.5rem 1.5rem;
justify-content: flex-end;
}
/* Dark mode adjustments */
:root.dark .manage-setting-container,
:root.auto.dark .manage-setting-container {
background: var(--color-background-secondary);
}
:root.dark .form-input,
:root.auto.dark .form-input {
background: var(--color-surface-elevated);
border-color: var(--color-border);
}
:root.dark .radio-custom,
:root.auto.dark .radio-custom {
background: var(--color-surface-elevated);
border-color: var(--color-border);
}
:root.dark .pattern-table th,
:root.auto.dark .pattern-table th {
background: var(--color-surface-elevated);
}
/* Responsive Design */
@media (max-width: 768px) {
.manage-setting-container {
padding: 0.75rem;
gap: 0.75rem;
}
.card-content {
padding: 1rem;
}
.header-card .card-header {
padding: 1rem;
flex-direction: column;
align-items: flex-start;
gap: 0.75rem;
}
.header-actions {
width: 100%;
justify-content: flex-start;
}
.pattern-table {
font-size: 0.7rem;
}
.pattern-table th,
.pattern-table td {
padding: 0.375rem;
}
.radio-option {
padding: 0.5rem;
}
.form-input {
padding: 0.625rem;
}
}

View File

@@ -613,7 +613,7 @@ small {
/* Dark mode adjustments */
:root.dark .piclist-settings,
:root.auto.dark .piclist-settings {
background: var(--color-background-primary);
background: var(--color-background-secondary);
}
:root.dark .settings-header,