mirror of
https://github.com/Kuingsmile/PicList.git
synced 2026-05-06 20:42:57 +08:00
✨ Feature(custom): new manage setting and login page
This commit is contained in:
@@ -3,19 +3,29 @@
|
||||
### 🚀 性能优化
|
||||
|
||||
- 减少了60-70%的闲置内存占用和20%的打开窗口时内存占用
|
||||
- 优化了多个页面的加载速度和浏览性能
|
||||
|
||||
### ✨ 新增功能
|
||||
|
||||
- windows下新增便携模式,无需安装即可运行,数据存储在程序目录下的`data`文件夹中,支持自动更新;Linux下新增`rpm`安装包
|
||||
- 新增自定义主题功能,主题仓库[PicList ThemeHub](https://github.com/Kuingsmile/PicList-ThemeHub)
|
||||
- 12个内置主题供选择,如bilibili、二次元、极夜紫等风格
|
||||
- 重构了几乎全部页面,优化了数十项UI细节问题
|
||||
- 相册页面多项优化,支持显示已选择图片数量,匹配的url列表和记忆过滤器打开状态
|
||||
- 插件页面现在可以浏览所有插件列表,查看详情和安装
|
||||
- 新增教学引导页面,首次运行时会自动弹出
|
||||
- 现在支持关闭GPU加速,解决部分兼容性问题
|
||||
- 新增高级动画设置,开启后可获得更好的UI体验
|
||||
- 优化了多个页面的加载速度和浏览性能
|
||||
- 新功能
|
||||
- 现在支持关闭GPU加速,解决部分兼容性问题
|
||||
- 新增高级动画设置,开启后可获得更好的UI体验
|
||||
- windows下新增便携模式,无需安装即可运行,数据存储在程序目录下的`data`文件夹中,支持自动更新
|
||||
- Linux下新增`rpm`安装包
|
||||
- 管理页面新增图床编辑卡片页面,避免了之前多配置切换时的混乱
|
||||
|
||||
- UI
|
||||
- 新增自定义主题功能,主题仓库[PicList ThemeHub](https://github.com/Kuingsmile/PicList-ThemeHub)
|
||||
- 12个内置主题供选择,如bilibili、二次元、极夜紫等风格
|
||||
- 重新设计了管理功能的全部页面
|
||||
- 重构了几乎全部页面,优化了数十项UI细节问题,整体风格更加统一
|
||||
- 相册页面多项优化,支持显示已选择图片数量,匹配的url列表和记忆过滤器打开状态
|
||||
- 插件页面现在可以浏览所有插件列表,查看详情和安装
|
||||
- 新增教学引导页面,首次运行时会自动弹出
|
||||
|
||||
- 其它
|
||||
- 原管理功能重命名为`云端`,更符合实际功能
|
||||
- 现在重置图床后不再自动返回上一页面
|
||||
|
||||
### 🐛 问题修复
|
||||
|
||||
@@ -23,3 +33,4 @@
|
||||
- 修复了暗色模式下任务页面的显示问题
|
||||
- 修复了图床设置页面设置为默认图床按钮状态没有及时更新的问题
|
||||
- 修复了预处理设置页面,图床水印独立设置的按钮状态没有及时更新的问题
|
||||
- 修复了部分页面底部元素被遮挡的问题
|
||||
|
||||
@@ -155,22 +155,6 @@ interface RadioOption {
|
||||
label: string
|
||||
}
|
||||
|
||||
const {
|
||||
mapField,
|
||||
defaultValue,
|
||||
globalValue = undefined,
|
||||
inputType,
|
||||
rangeMin = 0,
|
||||
rangeMax = 100,
|
||||
rangeStep = 1,
|
||||
rangeSuffix = '',
|
||||
numberMin = 0,
|
||||
numberMax = 1000,
|
||||
textPlaceholder = '',
|
||||
selectOptions = [],
|
||||
radioOptions = [],
|
||||
} = defineProps<Props>()
|
||||
|
||||
interface Props {
|
||||
mapField: Record<string, any> | undefined
|
||||
defaultValue: any
|
||||
@@ -188,6 +172,22 @@ interface Props {
|
||||
radioOptions?: RadioOption[]
|
||||
}
|
||||
|
||||
const {
|
||||
mapField,
|
||||
defaultValue,
|
||||
globalValue = undefined,
|
||||
inputType,
|
||||
rangeMin = 0,
|
||||
rangeMax = 100,
|
||||
rangeStep = 1,
|
||||
rangeSuffix = '',
|
||||
numberMin = 0,
|
||||
numberMax = 1000,
|
||||
textPlaceholder = '',
|
||||
selectOptions = [],
|
||||
radioOptions = [],
|
||||
} = defineProps<Props>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
mapChange: [picbedType: string, value: any]
|
||||
}>()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<template>
|
||||
<div id="config-form" class="no-scrollbar flex h-full w-full flex-1 overflow-auto">
|
||||
<SettingSection clas="h-full flex-1" only-one-row>
|
||||
<SettingSection class="h-full flex-1 border-none! shadow-none!" only-one-row>
|
||||
<SettingCard>
|
||||
<CustomInput
|
||||
v-model="ruleForm._configName"
|
||||
@@ -18,12 +18,7 @@
|
||||
</SettingCard>
|
||||
|
||||
<!-- Dynamic Config Fields -->
|
||||
<SettingCard
|
||||
v-for="(item, index) in configList"
|
||||
:key="item.name + index"
|
||||
:class="{ required: item.required }"
|
||||
:p1="item.type === 'confirm'"
|
||||
>
|
||||
<SettingCard v-for="(item, index) in configList" :key="item.name + index" :p1="item.type === 'confirm'">
|
||||
<CustomInput
|
||||
v-if="item.type === 'input' || item.type === 'password'"
|
||||
v-model="ruleForm[item.name]"
|
||||
@@ -31,11 +26,15 @@
|
||||
:placeholder="item.message || item.name"
|
||||
:class="{ 'border-error!': validationErrors[item.name] }"
|
||||
:title="item.alias || item.name"
|
||||
:required="item.required || false"
|
||||
@input="clearFieldError(item.name)"
|
||||
>
|
||||
<template #title-extra>
|
||||
<div v-if="showTooltips && item.tips" class="relative">
|
||||
<div class="info-icon" @click="toggleTooltip(item.name + index)">
|
||||
<div
|
||||
class="flex h-[20px] w-[20px] cursor-pointer items-center justify-center rounded-full p-[2px] text-secondary hover:bg-bg-secondary hover:text-accent"
|
||||
@click="toggleTooltip(item.name + index)"
|
||||
>
|
||||
<Info :size="15" />
|
||||
</div>
|
||||
<div
|
||||
@@ -53,6 +52,8 @@
|
||||
:description="item.message || ''"
|
||||
no-border
|
||||
small
|
||||
:required="item.required || false"
|
||||
:tips="item.tips"
|
||||
@change="clearFieldError(item.name)"
|
||||
>
|
||||
<template #switch-text>
|
||||
@@ -60,18 +61,6 @@
|
||||
{{ ruleForm[item.name] ? item.confirmText || 'Yes' : item.cancelText || 'No' }}
|
||||
</span>
|
||||
</template>
|
||||
<template #title-extra>
|
||||
<div v-if="showTooltips && item.tips" class="relative">
|
||||
<div class="info-icon" @click="toggleTooltip(item.name + index)">
|
||||
<Info :size="15" />
|
||||
</div>
|
||||
<div
|
||||
v-show="visibleTooltips[item.name + index]"
|
||||
class="absolute top-full left-0 z-1000 max-w-[300px] min-w-[200px] rounded-md border border-border bg-bg-secondary p-3 text-xs leading-[1.4] text-main shadow-lg max-md:max-w-[250px] max-md:min-w-[150px]"
|
||||
v-html="transformMarkdownToHTML(item.tips)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</CustomSwitch>
|
||||
<CustomSelect
|
||||
v-if="item.type === 'list' && item.choices"
|
||||
@@ -79,6 +68,7 @@
|
||||
:title="item.alias || item.name"
|
||||
:placeholder="item.message || item.name"
|
||||
:class="{ 'border-danger': validationErrors[item.name] }"
|
||||
:required="item.required || false"
|
||||
:select-list="
|
||||
item.choices.map(choice => ({
|
||||
value: choice.value || choice,
|
||||
@@ -100,6 +90,7 @@
|
||||
:title="item.alias || item.name"
|
||||
:zero-placeholder="item.message || item.name"
|
||||
:icon="null"
|
||||
:required="item.required || false"
|
||||
:all-list="
|
||||
item.choices.map(choice => ({
|
||||
type: choice.value || choice,
|
||||
|
||||
@@ -55,6 +55,9 @@ const containerRef = useTemplateRef('containerRef')
|
||||
const containerHeight = ref(0)
|
||||
const containerWidth = ref<number>(0)
|
||||
const parentScrollListeners = ref<HTMLElement[]>([])
|
||||
const lastScrollTime = ref(0)
|
||||
let ro: ResizeObserver | null = null
|
||||
|
||||
const sortedBreakpoints = computed<Breakpoint[]>(() => [...gridBreakpoints].sort((a, b) => a.min - b.min))
|
||||
|
||||
const effectiveCols = computed<number>(() => {
|
||||
@@ -123,9 +126,6 @@ function handlePageScroll() {
|
||||
}
|
||||
}
|
||||
|
||||
let ro: ResizeObserver | null = null
|
||||
const lastScrollTime = ref(0)
|
||||
|
||||
function updateContainerMetrics() {
|
||||
if (!containerRef.value) return
|
||||
const rect = containerRef.value.getBoundingClientRect()
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
@click="emit('click')"
|
||||
>
|
||||
<slot name="icon">
|
||||
<component :is="icon" v-if="icon" :size="iconSize" />
|
||||
<component :is="icon" v-if="icon" :size="iconSize" :class="iconClass" />
|
||||
</slot>
|
||||
<slot>
|
||||
<span
|
||||
@@ -31,13 +31,17 @@ const {
|
||||
icon = null,
|
||||
iconSize = 16,
|
||||
type = 'primary',
|
||||
iconClass = '',
|
||||
textClass = '',
|
||||
} = defineProps<{
|
||||
text: string
|
||||
icon?: any
|
||||
active?: boolean
|
||||
iconSize?: number
|
||||
disabled?: boolean
|
||||
type?: 'primary' | 'secondary' | 'tab'
|
||||
type?: string
|
||||
iconClass?: string
|
||||
textClass?: string
|
||||
}>()
|
||||
|
||||
const textClassVar = computed(() => {
|
||||
@@ -49,7 +53,7 @@ const textClassVar = computed(() => {
|
||||
case 'tab':
|
||||
return active ? 'text-white' : 'text-secondary'
|
||||
default:
|
||||
return ''
|
||||
return textClass || ''
|
||||
}
|
||||
})
|
||||
|
||||
@@ -58,9 +62,9 @@ const classVar = computed(() => {
|
||||
case 'primary':
|
||||
return 'bg-accent text-white not-disabled:hover:bg-accent-hover! not-disabled:hover:-translate-y-px'
|
||||
case 'secondary':
|
||||
return 'border border-border! bg-bg-secondary! text-main! not-disabled:hover:bg-surface-elevated! not-disabled:hover:-translate-y-px'
|
||||
return 'border border-border bg-bg-secondary text-main not-disabled:hover:bg-surface-elevated! not-disabled:hover:-translate-y-px'
|
||||
case 'tab':
|
||||
return 'flex-1 text-secondary not-disabled:data-[active=false]:hover:bg-accent/30! data-[active=true]:text-white data-[active=true]:bg-accent!'
|
||||
return 'flex-1 text-secondary not-disabled:data-[active=false]:hover:bg-accent/30 data-[active=true]:text-white data-[active=true]:bg-accent'
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div class="flex items-center gap-2">
|
||||
<label class="mb-2 text-sm font-semibold text-secondary"
|
||||
>{{ title }}
|
||||
<span v-if="required" class="ml-1 text-red-400">*</span>
|
||||
<span v-if="required" class="ml-1 text-danger">*</span>
|
||||
</label>
|
||||
<slot name="title-extra"></slot>
|
||||
</div>
|
||||
@@ -12,7 +12,7 @@
|
||||
v-model="modelValue"
|
||||
:type="type"
|
||||
v-bind="$attrs"
|
||||
class="box-border w-full rounded-md border border-border bg-bg-tertiary p-3 pr-10 text-sm text-main transition-all duration-200 ease-apple focus:border-accent focus:outline-none"
|
||||
class="box-border w-full rounded-md border border-border bg-bg-tertiary p-3 pr-10 text-sm text-main transition-all duration-200 ease-apple focus:border-accent focus:outline-none disabled:cursor-not-allowed disabled:opacity-50"
|
||||
:placeholder="placeholder"
|
||||
/>
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
<EyeIcon v-if="type === 'password'" class="text-accent" :size="16" />
|
||||
<EyeClosedIcon v-else class="text-accent" :size="16" />
|
||||
</button>
|
||||
<slot name="input-extra"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -33,7 +34,20 @@
|
||||
import { EyeClosedIcon, EyeIcon } from 'lucide-vue-next'
|
||||
import { onMounted, ref } from 'vue'
|
||||
|
||||
const modelValue = defineModel<any>()
|
||||
const [modelValue, modifiers] = defineModel<any>({
|
||||
set(value) {
|
||||
let result = value
|
||||
if (modifiers.trim && typeof result === 'string') {
|
||||
result = result.trim()
|
||||
}
|
||||
if (modifiers.number) {
|
||||
const n = parseFloat(result)
|
||||
result = isNaN(n) ? result : n
|
||||
}
|
||||
return result
|
||||
},
|
||||
})
|
||||
|
||||
const type = ref('text')
|
||||
|
||||
const {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
<component :is="icon" v-if="icon" :size="iconSize" class="text-accent" />
|
||||
</slot>
|
||||
<span class="text-[0.925rem] leading-[1.4] font-semibold text-secondary">{{ title }}</span>
|
||||
<span v-if="required" class="ml-1 text-danger">*</span>
|
||||
</div>
|
||||
<select
|
||||
v-model="modelValue"
|
||||
@@ -25,13 +26,15 @@ const modelValue = defineModel<string>()
|
||||
|
||||
const {
|
||||
title,
|
||||
icon,
|
||||
icon = null,
|
||||
iconSize = 18,
|
||||
selectList = [],
|
||||
required = false,
|
||||
} = defineProps<{
|
||||
title: string
|
||||
icon: any
|
||||
icon?: any
|
||||
selectList?: { value: string; label: string }[]
|
||||
iconSize?: number
|
||||
required?: boolean
|
||||
}>()
|
||||
</script>
|
||||
|
||||
@@ -1,39 +1,88 @@
|
||||
<template>
|
||||
<label
|
||||
class="flex cursor-pointer items-center gap-4 rounded-lg border border-border p-4 transition-all duration-200 ease-apple hover:border-accent hover:bg-surface hover:shadow-sm"
|
||||
:class="noBorder ? 'border-none' : ''"
|
||||
>
|
||||
<input v-model="modelValue" type="checkbox" class="peer hidden" />
|
||||
<span
|
||||
class="bg-linear-180-r relative shrink-0 rounded-full bg-gray-400/80 shadow-sm transition-all duration-medium ease-standard peer-checked:bg-accent before:absolute before:rounded-full before:bg-white before:shadow-sm before:transition-all before:duration-200 before:ease-apple before:content-[''] peer-checked:before:translate-x-[24px]"
|
||||
:class="
|
||||
small
|
||||
? 'h-[21px] w-[44px] before:top-[2px] before:left-[2px] before:h-[17px] before:w-[17px]'
|
||||
: 'h-[28px] w-[52px] before:top-[3px] before:left-[3px] before:h-[22px] before:w-[22px]'
|
||||
"
|
||||
/>
|
||||
<div class="flex flex-row items-center gap-1">
|
||||
<div class="flex flex-1 flex-col gap-1">
|
||||
<span class="text-[0.925rem] leading-[1.4] font-semibold text-secondary">{{ title }}</span>
|
||||
<span v-if="!!description" class="text-xs text-secondary/90">{{ description }}</span>
|
||||
<div class="flex items-center rounded-xl hover:bg-surface hover:shadow-sm">
|
||||
<label
|
||||
class="flex cursor-pointer items-center gap-4 rounded-lg border border-border p-4 transition-all duration-200 ease-apple hover:border-accent"
|
||||
:class="noBorder ? 'border-none' : ''"
|
||||
>
|
||||
<input v-model="modelValue" type="checkbox" class="peer hidden" />
|
||||
<span
|
||||
class="bg-linear-180-r relative shrink-0 rounded-full bg-gray-400/80 shadow-sm transition-all duration-medium ease-standard peer-checked:bg-accent peer-checked:shadow-[inset_0_1px_3px_rgba(0,0,0,0.1),0_2px_8px_color-mix(in_srgb,var(--color-accent),transparent_30%)] before:absolute before:rounded-full before:bg-white before:shadow-sm before:transition-all before:duration-200 before:ease-apple before:content-[''] peer-checked:before:translate-x-[24px]"
|
||||
:class="
|
||||
small
|
||||
? 'h-[21px] w-[44px] before:top-[2px] before:left-[2px] before:h-[17px] before:w-[17px]'
|
||||
: 'h-[28px] w-[52px] before:top-[3px] before:left-[3px] before:h-[22px] before:w-[22px]'
|
||||
"
|
||||
/>
|
||||
<div class="flex flex-row items-center gap-1">
|
||||
<slot name="custom-title"></slot>
|
||||
<div v-if="!!title" class="flex flex-1 flex-col gap-1">
|
||||
<div>
|
||||
<span class="text-[0.925rem] leading-[1.4] font-semibold text-secondary">{{ title }}</span>
|
||||
<span v-if="required" class="ml-1 text-danger">*</span>
|
||||
</div>
|
||||
<span v-if="!!description" class="text-xs text-secondary/90">{{ description }}</span>
|
||||
</div>
|
||||
<slot name="switch-text"></slot>
|
||||
</div>
|
||||
<slot name="switch-text"></slot>
|
||||
<slot name="title-extra"></slot>
|
||||
</label>
|
||||
<slot name="title-extra"></slot>
|
||||
<div v-if="showTooltips && tips !== ''" class="relative">
|
||||
<div
|
||||
class="flex h-[20px] w-[20px] cursor-pointer items-center justify-center rounded-full p-[2px] text-secondary hover:bg-bg-secondary hover:text-accent"
|
||||
@click="toggleTooltip()"
|
||||
>
|
||||
<Info :size="16" />
|
||||
</div>
|
||||
<div
|
||||
v-show="visibleTooltips"
|
||||
class="absolute top-full left-0 z-1000 max-w-[300px] min-w-[200px] rounded-md border border-border bg-bg-secondary p-3 text-xs leading-[1.4] text-main shadow-lg max-md:max-w-[250px] max-md:min-w-[150px]"
|
||||
v-html="transformMarkdownToHTML(tips)"
|
||||
/>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { Info } from 'lucide-vue-next'
|
||||
import { marked } from 'marked'
|
||||
import { onMounted, ref } from 'vue'
|
||||
|
||||
const visibleTooltips = ref(false)
|
||||
|
||||
const modelValue = defineModel<boolean>()
|
||||
const {
|
||||
title = '',
|
||||
description = '',
|
||||
noBorder = false,
|
||||
small = false,
|
||||
showTooltips = true,
|
||||
tips = '',
|
||||
required = false,
|
||||
} = defineProps<{
|
||||
noBorder?: boolean
|
||||
title?: string
|
||||
description?: string
|
||||
small?: boolean
|
||||
showTooltips?: boolean
|
||||
tips?: string
|
||||
required?: boolean
|
||||
}>()
|
||||
|
||||
function toggleTooltip() {
|
||||
visibleTooltips.value = !visibleTooltips.value
|
||||
}
|
||||
|
||||
function transformMarkdownToHTML(markdown: string) {
|
||||
try {
|
||||
return marked.parse(markdown)
|
||||
} catch (_e) {
|
||||
return markdown
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (typeof modelValue.value === 'string') {
|
||||
modelValue.value = modelValue.value === 'true'
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="mt-3 max-h-[400px] overflow-y-auto rounded-lg border border-border bg-bg-tertiary p-0 shadow-sm">
|
||||
<div class="mt-3 max-h-[450px] overflow-y-auto rounded-lg border border-border bg-bg-tertiary p-0 shadow-sm">
|
||||
<template v-for="key in Object.keys(list)" :key="key">
|
||||
<div class="border-b border-border last:border-0">
|
||||
<div
|
||||
|
||||
@@ -4,19 +4,22 @@
|
||||
<component :is="icon" v-if="icon" :size="iconSize" class="text-accent" />
|
||||
</slot>
|
||||
<span class="text-[0.925rem] leading-[1.4] font-semibold text-secondary">{{ title }}</span>
|
||||
<span v-if="required" class="ml-1 text-danger">*</span>
|
||||
</div>
|
||||
<div ref="dropdownRef" class="sort-dropdown relative">
|
||||
<button
|
||||
ref="triggerRef"
|
||||
class="flex h-[28px] w-full cursor-pointer items-center justify-between gap-1 rounded-md border border-border-secondary px-2 py-1.5 text-sm leading-[1.4] text-main transition-all duration-fast ease-apple hover:border-accent-hover focus:[.active]:border-accent-hover focus:[.active]:shadow-md"
|
||||
:class="{ active: dropDownOpen }"
|
||||
@click="toggleDropdown($event)"
|
||||
@click="toggleDropdown()"
|
||||
>
|
||||
<SortAscIcon v-if="fronticon" :size="14" />
|
||||
<component :is="customFrontIcon || SortAscIcon" v-if="fronticon" :size="14" />
|
||||
<span class="text-center text-xs font-semibold text-secondary">{{ placeholder || modelValue }}</span>
|
||||
<ChevronDownIcon :size="14" />
|
||||
</button>
|
||||
<div
|
||||
v-show="dropDownOpen"
|
||||
ref="optionsRef"
|
||||
class="sort-options fixed z-10 mt-[2px] min-w-[150px] overflow-hidden rounded-md border border-border-secondary bg-bg-tertiary shadow-lg"
|
||||
>
|
||||
<button
|
||||
@@ -30,6 +33,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onClickOutside } from '@vueuse/core'
|
||||
import { ChevronDownIcon, SortAscIcon } from 'lucide-vue-next'
|
||||
@@ -37,6 +41,8 @@ import { nextTick, ref } from 'vue'
|
||||
|
||||
const dropdownRef = ref(null)
|
||||
const modelValue = defineModel<string>()
|
||||
const triggerRef = ref<HTMLElement | null>(null)
|
||||
const optionsRef = ref<HTMLElement | null>(null)
|
||||
|
||||
function selectItem(key: string) {
|
||||
modelValue.value = key
|
||||
@@ -44,23 +50,49 @@ function selectItem(key: string) {
|
||||
}
|
||||
|
||||
const dropDownOpen = ref(false)
|
||||
function toggleDropdown(event?: Event) {
|
||||
|
||||
async function toggleDropdown() {
|
||||
dropDownOpen.value = !dropDownOpen.value
|
||||
|
||||
if (dropDownOpen.value && event) {
|
||||
nextTick(() => {
|
||||
const trigger = event.target as HTMLElement
|
||||
const dropdown = trigger.parentElement?.querySelector('.sort-options') as HTMLElement
|
||||
if (dropdown && trigger) {
|
||||
const rect = trigger.getBoundingClientRect()
|
||||
dropdown.style.top = `${rect.bottom + 2}px`
|
||||
dropdown.style.left = `${rect.left}px`
|
||||
dropdown.style.width = `${Math.max(rect.width, 160)}px`
|
||||
}
|
||||
})
|
||||
if (dropDownOpen.value) {
|
||||
await nextTick()
|
||||
updatePosition()
|
||||
}
|
||||
}
|
||||
|
||||
function updatePosition() {
|
||||
const trigger = triggerRef.value
|
||||
const dropdown = optionsRef.value
|
||||
if (!trigger || !dropdown) return
|
||||
|
||||
const rect = trigger.getBoundingClientRect()
|
||||
const dropdownHeight = dropdown.offsetHeight
|
||||
const dropdownWidth = Math.max(rect.width, 160)
|
||||
const viewportHeight = window.innerHeight
|
||||
const viewportWidth = window.innerWidth
|
||||
|
||||
// 1. 垂直位置计算:检查下方空间是否足够
|
||||
const spaceBelow = viewportHeight - rect.bottom
|
||||
const canFitBelow = spaceBelow > dropdownHeight + 10 // 预留10px间距
|
||||
|
||||
if (!canFitBelow && rect.top > dropdownHeight) {
|
||||
// 空间不足且上方放得下,向上翻转
|
||||
dropdown.style.top = `${rect.top - dropdownHeight - 4}px`
|
||||
} else {
|
||||
// 默认向下
|
||||
dropdown.style.top = `${rect.bottom + 2}px`
|
||||
}
|
||||
|
||||
// 2. 水平位置计算:防止右侧溢出
|
||||
let leftPos = rect.left
|
||||
if (leftPos + dropdownWidth > viewportWidth) {
|
||||
leftPos = viewportWidth - dropdownWidth - 10 // 靠右对齐并留点边距
|
||||
}
|
||||
|
||||
dropdown.style.left = `${leftPos}px`
|
||||
dropdown.style.width = `${dropdownWidth}px`
|
||||
}
|
||||
|
||||
onClickOutside(dropdownRef, () => {
|
||||
dropDownOpen.value = false
|
||||
})
|
||||
@@ -73,6 +105,8 @@ const {
|
||||
icon = null,
|
||||
tight = true,
|
||||
iconSize = 18,
|
||||
customFrontIcon = null,
|
||||
required = false,
|
||||
} = defineProps<{
|
||||
title: string
|
||||
icon?: any
|
||||
@@ -80,6 +114,8 @@ const {
|
||||
tight?: boolean
|
||||
placeholder?: string
|
||||
fronticon?: boolean
|
||||
customFrontIcon?: any
|
||||
keyList: string[]
|
||||
required?: boolean
|
||||
}>()
|
||||
</script>
|
||||
|
||||
@@ -1,15 +1,33 @@
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<div class="message-container">
|
||||
<TransitionGroup name="message" tag="div">
|
||||
<div v-for="message in messages" :key="message.id" class="message-toast" :class="getMessageClass(message.type)">
|
||||
<div class="message-icon">
|
||||
<component :is="getIconComponent(message.type)" :size="20" />
|
||||
<div class="pointer-events-none fixed top-[34px] right-[20px] z-10000">
|
||||
<TransitionGroup
|
||||
name="message"
|
||||
tag="div"
|
||||
enter-active-class="transition-all duration-300 ease-in-out"
|
||||
leave-active-class="transition-all duration-300 ease-in-out"
|
||||
enter-from-class="opacity-0 translate-x-[100%]"
|
||||
leave-to-class="opacity-0 translate-x-[100%]"
|
||||
>
|
||||
<div
|
||||
v-for="message in messages"
|
||||
:key="message.id"
|
||||
class="flex-start group pointer-events-auto mb-2 flex max-w-96 min-w-80 gap-3 rounded-sm border border-gray-300 bg-white px-4 py-3 wrap-break-word shadow-sm [.message-error]:border-l-4 [.message-error]:border-l-danger [.message-info]:border-l-4 [.message-info]:border-l-accent [.message-success]:border-l-4 [.message-success]:border-l-success [.message-warning]:border-l-4 [.message-warning]:border-l-warning"
|
||||
:class="getMessageClass(message.type)"
|
||||
>
|
||||
<div
|
||||
class="mt-0.5 shrink-0 group-[.message-error]:text-danger group-[.message-info]:text-accent group-[.message-success]:text-success group-[.message-warning]:text-warning"
|
||||
>
|
||||
<component :is="getIconComponent(message.type)" :size="16" />
|
||||
</div>
|
||||
<div class="message-content">
|
||||
<div class="min-w-0 flex-1 text-sm leading-[1.25] font-medium wrap-break-word hyphens-auto text-secondary">
|
||||
{{ message.message }}
|
||||
</div>
|
||||
<button v-if="message.showClose" class="message-close" @click="removeMessage(message.id)">
|
||||
<button
|
||||
v-if="message.showClose"
|
||||
class="mt-0.5 flex shrink-0 cursor-pointer items-center justify-center rounded-sm border-none bg-none p-1 text-secondary hover:bg-danger/10"
|
||||
@click="removeMessage(message.id)"
|
||||
>
|
||||
<X :size="16" />
|
||||
</button>
|
||||
</div>
|
||||
@@ -117,117 +135,3 @@ export default {
|
||||
name: 'MessageToast',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.message-container {
|
||||
position: fixed;
|
||||
top: 34px;
|
||||
right: 20px;
|
||||
z-index: 3000;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.message-toast {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 0.5rem;
|
||||
border: 1px solid rgb(229 231 235);
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.75rem 1rem;
|
||||
min-width: 20rem;
|
||||
max-width: 24rem;
|
||||
background: white;
|
||||
box-shadow:
|
||||
0 4px 6px -1px rgb(0 0 0 / 10%),
|
||||
0 2px 4px -1px rgb(0 0 0 / 6%);
|
||||
gap: 0.75rem;
|
||||
pointer-events: all;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.message-info {
|
||||
border-left: 4px solid rgb(59 130 246);
|
||||
}
|
||||
|
||||
.message-info .message-icon {
|
||||
color: rgb(59 130 246);
|
||||
}
|
||||
|
||||
.message-success {
|
||||
border-left: 4px solid rgb(34 197 94);
|
||||
}
|
||||
|
||||
.message-success .message-icon {
|
||||
color: rgb(34 197 94);
|
||||
}
|
||||
|
||||
.message-warning {
|
||||
border-left: 4px solid rgb(245 158 11);
|
||||
}
|
||||
|
||||
.message-warning .message-icon {
|
||||
color: rgb(245 158 11);
|
||||
}
|
||||
|
||||
.message-error {
|
||||
border-left: 4px solid rgb(239 68 68);
|
||||
}
|
||||
|
||||
.message-error .message-icon {
|
||||
color: rgb(239 68 68);
|
||||
}
|
||||
|
||||
.message-icon {
|
||||
flex-shrink: 0;
|
||||
margin-top: 0.125rem;
|
||||
}
|
||||
|
||||
.message-content {
|
||||
min-width: 0;
|
||||
font-size: 0.875rem;
|
||||
color: rgb(75 85 99);
|
||||
flex: 1;
|
||||
line-height: 1.25rem;
|
||||
overflow-wrap: break-word;
|
||||
hyphens: auto;
|
||||
}
|
||||
|
||||
.message-close {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-top: 0.125rem;
|
||||
border: none;
|
||||
border-radius: 0.25rem;
|
||||
padding: 0.25rem;
|
||||
color: rgb(107 114 128);
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.message-close:hover {
|
||||
color: rgb(75 85 99);
|
||||
background: rgb(243 244 246);
|
||||
}
|
||||
|
||||
/* Transition animations */
|
||||
.message-enter-active,
|
||||
.message-leave-active {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.message-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateX(100%);
|
||||
}
|
||||
|
||||
.message-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(100%);
|
||||
}
|
||||
|
||||
.message-move {
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
"copySuccess": "Copy Success",
|
||||
"expand": "Expand Sidebar",
|
||||
"gallery": "Gallery",
|
||||
"manage": "Manage",
|
||||
"manage": "Cloud",
|
||||
"moreOptions": "More Options",
|
||||
"picbed": "PicBed",
|
||||
"picBedQrCode": "PicBed QR Code",
|
||||
@@ -462,7 +462,7 @@
|
||||
"webPathTips": "Used to assemble the public URL"
|
||||
},
|
||||
"smms": {
|
||||
"explain": "For mainland China, please use the backup domain https://sm.ms and avoid sending too many requests in a short time",
|
||||
"explain": "For mainland China, please use the backup domain https://smms.app and avoid sending too many requests in a short time",
|
||||
"tokenDesc": "SM.MS Token, available in your SM.MS profile",
|
||||
"tokenPlaceholder": "Please enter SM.MS Token"
|
||||
},
|
||||
@@ -541,10 +541,10 @@
|
||||
"refresh": "Refresh",
|
||||
"reset": "Reset",
|
||||
"save": "Save",
|
||||
"savedConfigs": "Saved Configurations",
|
||||
"savedConfigs": "Enter Saved Cloud",
|
||||
"selectPlaceholder": "Please select",
|
||||
"tips": "Tips",
|
||||
"title": "Image Host Management",
|
||||
"title": "Cloud Management",
|
||||
"viewDetails": "View details"
|
||||
},
|
||||
"main": {
|
||||
@@ -641,10 +641,17 @@
|
||||
"preSignedUrlExpireDesc": "Adjust based on actual needs",
|
||||
"randomStringRenameTips": "20 random characters",
|
||||
"randomStringRenameTitle": "Random string rename on upload (medium priority)",
|
||||
"section": {
|
||||
"cache": "Cache Settings",
|
||||
"general": "General Settings",
|
||||
"naming": "Naming Settings",
|
||||
"up-down": "Upload/Download Settings"
|
||||
},
|
||||
"selectDownloadFolderTips": "Select download directory",
|
||||
"selectDownloadFolderTitle": "Choose download folder",
|
||||
"timestampRenameTips": "When enabled, uploaded files will be renamed to a timestamp",
|
||||
"timestampRenameTitle": "Timestamp rename on upload (highest priority)"
|
||||
"timestampRenameTitle": "Timestamp rename on upload (highest priority)",
|
||||
"title": "Manage General Settings"
|
||||
}
|
||||
},
|
||||
"picBedConfigs": {
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
"copySuccess": "复制成功",
|
||||
"expand": "展开侧边栏",
|
||||
"gallery": "相册",
|
||||
"manage": "管理",
|
||||
"manage": "云端",
|
||||
"moreOptions": "更多选项",
|
||||
"picbed": "图床",
|
||||
"picBedQrCode": "图床配置二维码",
|
||||
@@ -462,7 +462,7 @@
|
||||
"webPathTips": "用于拼接访问网址"
|
||||
},
|
||||
"smms": {
|
||||
"explain": "大陆地区请访问备用域名https://sm.ms,不要短时大量请求",
|
||||
"explain": "大陆地区请访问备用域名https://smms.app,不要短时大量请求",
|
||||
"tokenDesc": "SM.MS Token, 可在 SM.MS 个人中心获取",
|
||||
"tokenPlaceholder": "请输入 SM.MS Token"
|
||||
},
|
||||
@@ -541,10 +541,10 @@
|
||||
"refresh": "刷新",
|
||||
"reset": "重置",
|
||||
"save": "保存",
|
||||
"savedConfigs": "已保存配置",
|
||||
"savedConfigs": "进入已配置云端",
|
||||
"selectPlaceholder": "请选择",
|
||||
"tips": "提示",
|
||||
"title": "图床管理",
|
||||
"title": "云端管理",
|
||||
"viewDetails": "查看详情"
|
||||
},
|
||||
"main": {
|
||||
@@ -641,10 +641,17 @@
|
||||
"preSignedUrlExpireDesc": "建议根据实际需求调整",
|
||||
"randomStringRenameTips": "20位随机字符",
|
||||
"randomStringRenameTitle": "上传文件随机字符串重命名(中优先级)",
|
||||
"section": {
|
||||
"cache": "缓存设置",
|
||||
"general": "通用设置",
|
||||
"naming": "命名设置",
|
||||
"up-down": "上传/下载设置"
|
||||
},
|
||||
"selectDownloadFolderTips": "选择下载目录",
|
||||
"selectDownloadFolderTitle": "选择下载文件夹",
|
||||
"timestampRenameTips": "开启后,上传的文件将自动重命名为时间戳",
|
||||
"timestampRenameTitle": "上传文件时间戳重命名(最高优先级)"
|
||||
"timestampRenameTitle": "上传文件时间戳重命名(最高优先级)",
|
||||
"title": "管理通用设置"
|
||||
}
|
||||
},
|
||||
"picBedConfigs": {
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
"copySuccess": "複製成功",
|
||||
"expand": "展開側邊欄",
|
||||
"gallery": "相簿",
|
||||
"manage": "管理",
|
||||
"manage": "雲端",
|
||||
"moreOptions": "更多選項",
|
||||
"picbed": "圖床",
|
||||
"picBedQrCode": "圖床配置 QRCODE",
|
||||
@@ -462,7 +462,7 @@
|
||||
"webPathTips": "用於拼接對外存取網址"
|
||||
},
|
||||
"smms": {
|
||||
"explain": "中國大陸地區請訪問備用網域 https://sm.ms,請勿在短時間內大量請求",
|
||||
"explain": "中國大陸地區請訪問備用網域 https://smms.app,請勿在短時間內大量請求",
|
||||
"tokenDesc": "SM.MS Token,可在 SM.MS 個人中心取得",
|
||||
"tokenPlaceholder": "請輸入 SM.MS Token"
|
||||
},
|
||||
@@ -541,10 +541,10 @@
|
||||
"refresh": "重新整理",
|
||||
"reset": "重設",
|
||||
"save": "儲存",
|
||||
"savedConfigs": "已儲存設定",
|
||||
"savedConfigs": "進入已配置雲端",
|
||||
"selectPlaceholder": "請選擇",
|
||||
"tips": "提示",
|
||||
"title": "圖床管理",
|
||||
"title": "雲端存儲管理",
|
||||
"viewDetails": "查看詳情"
|
||||
},
|
||||
"main": {
|
||||
@@ -641,10 +641,17 @@
|
||||
"preSignedUrlExpireDesc": "建議依實際需求調整",
|
||||
"randomStringRenameTips": "20 位隨機字元",
|
||||
"randomStringRenameTitle": "上傳檔案隨機字串重新命名(中優先級)",
|
||||
"section": {
|
||||
"cache": "快取設置",
|
||||
"general": "通用設置",
|
||||
"naming": "命名設置",
|
||||
"up-down": "上傳/下載設置"
|
||||
},
|
||||
"selectDownloadFolderTips": "選擇下載目錄",
|
||||
"selectDownloadFolderTitle": "選擇下載資料夾",
|
||||
"timestampRenameTips": "啟用後,上傳的檔案將自動更名為時間戳",
|
||||
"timestampRenameTitle": "上傳檔案時間戳重新命名(最高優先級)"
|
||||
"timestampRenameTitle": "上傳檔案時間戳重新命名(最高優先級)",
|
||||
"title": "管理通用設置"
|
||||
}
|
||||
},
|
||||
"picBedConfigs": {
|
||||
|
||||
@@ -1,281 +1,248 @@
|
||||
<template>
|
||||
<div class="login-container">
|
||||
<!-- Header Card -->
|
||||
<div class="login-card header-card">
|
||||
<div class="card-header">
|
||||
<div class="header-content">
|
||||
<div class="header-icon">
|
||||
<DatabaseIcon :size="24" />
|
||||
</div>
|
||||
<div class="relative flex h-full w-full items-center justify-center">
|
||||
<div class="relative z-1 flex h-full w-full flex-col items-center justify-start gap-4 rounded-xl border-none p-4">
|
||||
<div
|
||||
class="flex w-full items-center justify-between gap-4 overflow-visible rounded-2xl border border-border-secondary px-6 py-2 shadow-md max-md:items-stretch max-md:p-5"
|
||||
>
|
||||
<div class="flex flex-1 flex-wrap items-center gap-4 p-1">
|
||||
<Cloud :size="24" class="text-accent" />
|
||||
<div>
|
||||
<h1>{{ t('pages.manage.login.title') }}</h1>
|
||||
<p>{{ sortedAllConfigAliasMap.length }} {{ t('pages.manage.login.savedConfigs') }}</p>
|
||||
<h1 class="m-0 text-2xl font-semibold tracking-tight text-main">{{ t('pages.manage.login.title') }}</h1>
|
||||
<p class="m-0 text-sm text-secondary">
|
||||
{{ sortedAllConfigAliasMap.length }} {{ t('pages.manage.login.savedConfigs') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<button class="action-button" @click="refreshConfigs">
|
||||
<RefreshCwIcon :size="16" />
|
||||
{{ t('pages.manage.login.refresh') }}
|
||||
</button>
|
||||
<div class="flex flex-wrap gap-3 overflow-visible">
|
||||
<CustomButton
|
||||
type="secondary"
|
||||
:icon="RefreshCwIcon"
|
||||
:text="t('pages.manage.login.refresh')"
|
||||
@click="refreshConfigs"
|
||||
/>
|
||||
<CustomButton type="secondary" :icon="BookOpen" :text="t('pages.settings.docs')" @click="goConfigPage" />
|
||||
<CustomButton
|
||||
type="primary"
|
||||
:icon="Settings2"
|
||||
:text="t('pages.manage.main.settings')"
|
||||
@click="openBucketPageSetting"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Navigation Tabs -->
|
||||
<div class="login-card tabs-card">
|
||||
<div class="tabs-container">
|
||||
<div class="tabs-nav-wrapper">
|
||||
<div class="tabs-nav">
|
||||
<!-- Navigation Tabs -->
|
||||
<div
|
||||
class="flex w-full items-center justify-between gap-2 rounded-2xl border border-border-secondary p-2 shadow-md max-md:items-stretch"
|
||||
>
|
||||
<div class="flex-1 overflow-hidden p-2">
|
||||
<div class="flex w-full flex-wrap items-center gap-2">
|
||||
<button
|
||||
v-for="item in tabItems"
|
||||
:key="item.key"
|
||||
class="tab-button"
|
||||
class="transition-al flex min-w-fit flex-none cursor-pointer items-center gap-2 rounded-md border border-border-secondary bg-bg-secondary px-4 py-2 text-sm font-semibold whitespace-nowrap text-secondary no-underline duration-200 ease-apple hover:border-border hover:bg-accent/10 hover:text-main [.active]:border-accent [.active]:bg-accent [.active]:text-white"
|
||||
:class="{ active: activeName === item.key }"
|
||||
@click="handleTabChange(item.key)"
|
||||
>
|
||||
<FolderIcon v-if="item.key === 'login'" :size="16" />
|
||||
<img v-else :src="`./assets/${item.key}.webp`" class="tab-icon" :alt="item.name" />
|
||||
<img
|
||||
v-else
|
||||
:src="`./assets/${item.key}.webp`"
|
||||
class="h-[16px] w-[16px] object-contain"
|
||||
:alt="item.name"
|
||||
/>
|
||||
<span>{{ item.name }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Content Area -->
|
||||
<div class="login-card content-card">
|
||||
<div class="tab-content">
|
||||
<!-- Main Config List Tab -->
|
||||
<div v-if="activeName === 'login'" class="config-list-container">
|
||||
<div v-if="sortedAllConfigAliasMap.length === 0" class="empty-state">
|
||||
<div class="empty-icon">
|
||||
<DatabaseIcon :size="48" />
|
||||
<!-- Content Area -->
|
||||
<div
|
||||
class="no-scrollbar flex min-h-[500px] w-full flex-1 flex-col flex-wrap items-center justify-center gap-2 overflow-auto rounded-2xl border border-border-secondary shadow-md"
|
||||
>
|
||||
<div class="no-scrollbar h-full w-full flex-1 overflow-auto rounded-2xl border-none">
|
||||
<!-- Main Config List Tab -->
|
||||
<div v-if="activeName === 'login'" class="h-full w-full p-4">
|
||||
<div
|
||||
v-if="sortedAllConfigAliasMap.length === 0"
|
||||
class="flex h-full w-full flex-col items-center justify-center p-4"
|
||||
>
|
||||
<div class="mb-2 text-accent/50">
|
||||
<DatabaseIcon :size="48" />
|
||||
</div>
|
||||
<h3 class="mb-2 text-lg font-semibold text-secondary">{{ t('pages.manage.login.noConfigs') }}</h3>
|
||||
<p class="text-sm font-semibold text-secondary">{{ t('pages.manage.login.noConfigsDesc') }}</p>
|
||||
</div>
|
||||
<h3>{{ t('pages.manage.login.noConfigs') }}</h3>
|
||||
<p>{{ t('pages.manage.login.noConfigsDesc') }}</p>
|
||||
</div>
|
||||
<div v-else class="config-grid">
|
||||
<div v-for="item in sortedAllConfigAliasMap" :key="item.alias" class="config-item">
|
||||
<div class="config-header">
|
||||
<img :src="`./assets/${item.picBedName}.webp`" class="config-icon" :alt="item.picBedName" />
|
||||
<div class="config-info">
|
||||
<h4 class="config-alias">
|
||||
{{ item.alias }}
|
||||
</h4>
|
||||
<p class="config-type">
|
||||
{{ supportedPicBedList[item.picBedName]?.name || item.picBedName }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="config-details">
|
||||
<button class="details-button" @click="toggleConfigDetails(item.alias)">
|
||||
<InfoIcon :size="14" />
|
||||
{{ t('pages.manage.login.viewDetails') }}
|
||||
<ChevronDownIcon :size="14" :class="{ rotated: expandedConfigs.includes(item.alias) }" />
|
||||
</button>
|
||||
|
||||
<div v-if="expandedConfigs.includes(item.alias)" class="config-table">
|
||||
<div
|
||||
v-for="tableItem in formObjToTableData(item.config)"
|
||||
:key="tableItem.key"
|
||||
class="table-row"
|
||||
@click="copyToClipboard(tableItem.value)"
|
||||
>
|
||||
<span class="table-key">{{ tableItem.key }}</span>
|
||||
<span class="table-value">{{ tableItem.value }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="config-actions">
|
||||
<button class="action-button primary" @click="handleConfigClick(item)">
|
||||
<PointerIcon :size="16" />
|
||||
{{ t('pages.manage.login.enter') }}
|
||||
</button>
|
||||
<button class="action-button danger" @click="handleConfigRemove(item.alias)">
|
||||
<TrashIcon :size="16" />
|
||||
{{ t('pages.manage.login.delete') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- PicBed Configuration Tabs -->
|
||||
<div v-else class="picbed-config-container">
|
||||
<div v-if="supportedPicBedList[activeName]" class="picbed-config">
|
||||
<!-- Info Section -->
|
||||
<div class="info-section">
|
||||
<div class="info-card primary">
|
||||
<InfoIcon :size="20" />
|
||||
<p>{{ supportedPicBedList[activeName].explain }}</p>
|
||||
</div>
|
||||
<div class="info-card reference">
|
||||
<LinkIcon :size="20" />
|
||||
<p>
|
||||
{{ supportedPicBedList[activeName].referenceText }}
|
||||
<button class="link-button" @click="handleReferenceClick(supportedPicBedList[activeName].refLink)">
|
||||
{{ supportedPicBedList[activeName].refLink }}
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Configuration Form -->
|
||||
<div class="config-form">
|
||||
<div
|
||||
v-else
|
||||
class="grid w-full grid-cols-[repeat(auto-fill,minmax(300px,1fr))] gap-5 border-none p-1 max-md:gap-4"
|
||||
>
|
||||
<div
|
||||
v-for="option in supportedPicBedList[activeName].options"
|
||||
:key="option"
|
||||
class="form-group"
|
||||
:class="{ 'has-error': formErrors[activeName + '.' + option] }"
|
||||
v-for="item in sortedAllConfigAliasMap"
|
||||
:key="item.alias"
|
||||
class="group relative flex cursor-pointer flex-row gap-6 overflow-visible rounded-xl border border-border-secondary p-4 shadow-md transition-all duration-fast ease-apple hover:border-2 hover:border-accent"
|
||||
>
|
||||
<label class="form-label">
|
||||
{{ supportedPicBedList[activeName].configOptions[option].description }}
|
||||
<span v-if="supportedPicBedList[activeName].configOptions[option].required" class="required-marker"
|
||||
>*</span
|
||||
>
|
||||
<button
|
||||
v-if="supportedPicBedList[activeName].configOptions[option].tooltip"
|
||||
class="tooltip-button"
|
||||
:title="supportedPicBedList[activeName].configOptions[option].tooltip"
|
||||
>
|
||||
<InfoIcon :size="14" />
|
||||
</button>
|
||||
</label>
|
||||
<div class="flex-1">
|
||||
<div class="mb-4 flex items-center gap-4">
|
||||
<img
|
||||
:src="`./assets/${item.picBedName}.webp`"
|
||||
class="h-[40px] w-[40px] object-contain"
|
||||
:alt="item.picBedName"
|
||||
/>
|
||||
<div>
|
||||
<h4 class="mb-1 text-base font-semibold text-main">
|
||||
{{ item.alias }}
|
||||
</h4>
|
||||
<p class="m-0 text-sm text-secondary">
|
||||
{{ supportedPicBedList[item.picBedName]?.name || item.picBedName }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- String Input -->
|
||||
<input
|
||||
v-if="supportedPicBedList[activeName].configOptions[option].type === 'string'"
|
||||
v-model.trim="configResult[activeName + '.' + option]"
|
||||
type="text"
|
||||
class="form-input"
|
||||
:class="{ error: formErrors[activeName + '.' + option] }"
|
||||
:placeholder="supportedPicBedList[activeName].configOptions[option].placeholder"
|
||||
:disabled="!!supportedPicBedList[activeName].configOptions[option].disabled"
|
||||
@blur="validateField(activeName, option)"
|
||||
@input="clearFieldError(activeName + '.' + option)"
|
||||
/>
|
||||
|
||||
<!-- Boolean Switch -->
|
||||
<label
|
||||
v-else-if="supportedPicBedList[activeName].configOptions[option].type === 'boolean'"
|
||||
class="custom-switch"
|
||||
>
|
||||
<input
|
||||
v-model="configResult[activeName + '.' + option]"
|
||||
type="checkbox"
|
||||
@change="validateField(activeName, option)"
|
||||
/>
|
||||
<span class="switch-slider" />
|
||||
</label>
|
||||
|
||||
<!-- Number Input -->
|
||||
<input
|
||||
v-else-if="supportedPicBedList[activeName].configOptions[option].type === 'number'"
|
||||
v-model.number="configResult[activeName + '.' + option]"
|
||||
type="number"
|
||||
class="form-input"
|
||||
:class="{ error: formErrors[activeName + '.' + option] }"
|
||||
:placeholder="supportedPicBedList[activeName].configOptions[option].placeholder"
|
||||
@blur="validateField(activeName, option)"
|
||||
@input="clearFieldError(activeName + '.' + option)"
|
||||
/>
|
||||
|
||||
<!-- Select Dropdown -->
|
||||
<div
|
||||
v-else-if="supportedPicBedList[activeName].configOptions[option].type === 'select'"
|
||||
class="custom-select"
|
||||
>
|
||||
<select
|
||||
v-model="configResult[activeName + '.' + option]"
|
||||
class="form-select"
|
||||
:class="{ error: formErrors[activeName + '.' + option] }"
|
||||
@change="validateField(activeName, option)"
|
||||
>
|
||||
<option value="">
|
||||
{{ t('pages.manage.login.selectPlaceholder') }}
|
||||
</option>
|
||||
<option
|
||||
v-for="[key, value] in Object.entries(
|
||||
supportedPicBedList[activeName].configOptions[option].selectOptions,
|
||||
)"
|
||||
:key="key"
|
||||
:value="key"
|
||||
>
|
||||
{{ value }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Error Message -->
|
||||
<div v-if="formErrors[activeName + '.' + option]" class="error-message">
|
||||
{{ formErrors[activeName + '.' + option] }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="action-section">
|
||||
<div class="import-section">
|
||||
<div v-if="currentAliasList.length > 0" class="dropdown-container">
|
||||
<button class="dropdown-trigger action-button secondary" @click="toggleImportDropdown">
|
||||
<DownloadIcon :size="16" />
|
||||
{{ t('pages.manage.login.import') }}
|
||||
<ChevronDownIcon :size="16" />
|
||||
</button>
|
||||
<div v-if="importDropdownOpen" class="dropdown-menu">
|
||||
<div class="relative">
|
||||
<button
|
||||
v-for="alias in currentAliasList"
|
||||
:key="alias"
|
||||
class="dropdown-item"
|
||||
@click="handleConfigImport(alias)"
|
||||
class="flex cursor-pointer items-center gap-2 rounded-xl border-none bg-accent/5 p-2 text-sm text-secondary hover:text-main"
|
||||
@click="toggleConfigDetails(item.alias)"
|
||||
>
|
||||
{{ alias }}
|
||||
<InfoIcon :size="14" />
|
||||
{{ t('pages.manage.login.viewDetails') }}
|
||||
<ChevronDownIcon :size="14" :class="{ 'rotate-180': expandedConfigs.includes(item.alias) }" />
|
||||
</button>
|
||||
<Teleport v-if="expandedConfigs.includes(item.alias)" to="body">
|
||||
<div
|
||||
class="fixed top-1/3 left-1/2 z-1000 h-auto max-h-[400px] w-auto max-w-[900px] min-w-[200px] -translate-x-1/2 overflow-auto rounded-xl border border-slate-200 bg-white shadow-xl ring-1 ring-black/5"
|
||||
>
|
||||
<div class="relative">
|
||||
<button
|
||||
class="absolute top-2 right-2 z-10000 flex h-8 w-8 cursor-pointer items-center justify-center rounded-full border border-border bg-surface-elevated text-secondary transition-all duration-fast ease-apple hover:scale-105 hover:border-danger hover:bg-danger hover:text-white focus-visible:focus-ring"
|
||||
@click="toggleConfigDetails(item.alias)"
|
||||
>
|
||||
<XIcon :size="20" />
|
||||
</button>
|
||||
<table class="relative w-full table-fixed border-collapse text-left text-[13px]">
|
||||
<thead class="sticky top-0 z-10 bg-slate-50 shadow-[0_1px_0_0_rgba(0,0,0,0.05)]">
|
||||
<tr>
|
||||
<th class="w-1/3 px-4 py-2.5 font-semibold text-slate-500">Name</th>
|
||||
<th class="w-2/3 px-4 py-2.5 font-semibold text-slate-500">Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-100">
|
||||
<tr
|
||||
v-for="tableItem in formObjToTableData(item.config)"
|
||||
:key="tableItem.key"
|
||||
class="group cursor-pointer hover:bg-indigo-50/50"
|
||||
@click="copyToClipboard(tableItem.value)"
|
||||
>
|
||||
<td class="px-4 py-2.5 font-medium text-slate-700">
|
||||
{{ tableItem.key }}
|
||||
</td>
|
||||
<td class="relative px-4 py-2.5 font-mono text-slate-500">
|
||||
<div class="wrap-break-word group-hover:pr-10" :title="tableItem.value">
|
||||
{{ tableItem.value }}
|
||||
</div>
|
||||
<div
|
||||
class="absolute top-1/2 right-2 -translate-y-1/2 opacity-0 transition-opacity group-hover:opacity-100"
|
||||
>
|
||||
<span class="rounded bg-indigo-100 px-1.5 py-0.5 text-[10px] text-accent">
|
||||
{{ t('pages.gallery.copy') }}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="main-actions">
|
||||
<button class="action-button primary" @click="handleConfigChange(activeName)">
|
||||
<SaveIcon :size="16" />
|
||||
{{ t('pages.manage.login.save') }}
|
||||
</button>
|
||||
<button class="action-button danger" @click="handleConfigReset(activeName)">
|
||||
<RotateCcwIcon :size="16" />
|
||||
{{ t('pages.manage.login.reset') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Existing Configurations Table -->
|
||||
<div v-if="dataForTable.length > 0" class="config-table-section">
|
||||
<h3>{{ t('pages.manage.login.configTabTitle') }}</h3>
|
||||
<div class="responsive-table">
|
||||
<table class="config-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th v-for="option in supportedPicBedList[activeName].options" :key="option">
|
||||
{{ supportedPicBedList[activeName].configOptions[option].description }}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(row, index) in dataForTable" :key="index">
|
||||
<td
|
||||
v-for="option in supportedPicBedList[activeName].options"
|
||||
:key="option"
|
||||
@click="copyToClipboard(row[option])"
|
||||
>
|
||||
{{ row[option] }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="flex flex-col items-center justify-end gap-4">
|
||||
<CustomButton
|
||||
type="primary"
|
||||
:icon="PointerIcon"
|
||||
:text="t('pages.manage.login.enter')"
|
||||
@click="handleConfigClick(item)"
|
||||
/>
|
||||
<CustomButton
|
||||
type="danger"
|
||||
class="border border-border bg-danger/70 opacity-0 transition-all duration-fast ease-apple group-hover:opacity-100 hover:bg-danger"
|
||||
icon-class="text-white "
|
||||
text-class="text-white font-semibold text-sm "
|
||||
:icon="TrashIcon"
|
||||
:text="t('pages.manage.login.delete')"
|
||||
@click="handleConfigRemove(item.alias)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="editMode === false"
|
||||
class="flex h-full w-full flex-1 items-center gap-4 overflow-hidden rounded-2xl border border-border-secondary px-4 py-6 shadow-md"
|
||||
>
|
||||
<div class="no-scrollbar h-full w-full overflow-auto rounded-sm">
|
||||
<div
|
||||
class="grid w-full grid-cols-[repeat(auto-fill,minmax(300px,1fr))] gap-5 border-none p-1 max-md:gap-4"
|
||||
>
|
||||
<div
|
||||
v-for="(item, index) in existingConfiguration"
|
||||
:key="item.alias + index"
|
||||
class="relative flex min-h-[180px] cursor-pointer flex-col gap-6 overflow-hidden rounded-xl border border-border p-5 shadow-md transition-all duration-fast ease-apple hover:border-2 hover:border-accent hover:shadow-md"
|
||||
>
|
||||
<!-- Card Header -->
|
||||
<div class="relative z-1 flex flex-1 items-start justify-between">
|
||||
<div
|
||||
class="peer flex h-[40px] w-[40px] items-center justify-center rounded-lg border border-border-secondary text-accent transition-all duration-fast ease-apple"
|
||||
>
|
||||
<Cloud :size="20" />
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-1.5 transition-all duration-fast ease-apple">
|
||||
<button
|
||||
class="flex h-[30px] w-[30px] cursor-pointer items-center justify-center rounded-md border border-accent/30 text-secondary transition-all duration-fast ease-standard hover:scale-105 hover:border-accent hover:text-accent"
|
||||
:title="t('pages.uploaderConfig.edit')"
|
||||
@click.stop="openEditPage(item.alias)"
|
||||
>
|
||||
<Pencil :size="14" />
|
||||
</button>
|
||||
<button
|
||||
class="flex h-[30px] w-[30px] cursor-pointer items-center justify-center rounded-md border border-border bg-danger/10 text-danger transition-all duration-fast ease-standard hover:scale-105 hover:border-danger hover:text-danger"
|
||||
:title="t('pages.uploaderConfig.delete')"
|
||||
@click.stop="() => handleConfigRemove(item.alias)"
|
||||
>
|
||||
<Trash2 :size="14" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Card Body -->
|
||||
<div class="relative z-1 flex-1">
|
||||
<h3 class="mx-0 mt-0 mb-2 text-base leading-[1.4] font-semibold tracking-tight text-main">
|
||||
{{ item.alias }}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
key="add-new"
|
||||
class="group/new relative flex min-h-[180px] cursor-pointer flex-col items-center justify-center gap-6 overflow-hidden rounded-xl border-2 border-dashed border-border p-5 shadow-sm transition-all duration-fast ease-apple hover:border-solid hover:border-accent hover:bg-surface hover:shadow-md"
|
||||
@click="openEditPage('')"
|
||||
>
|
||||
<div class="flex flex-col items-center gap-3 transition-all duration-fast ease-apple">
|
||||
<div
|
||||
class="flex h-[56px] w-[56px] items-center justify-center rounded-xl border-2 border-dashed border-border text-tertiary transition-all duration-fast ease-apple group-hover/new:scale-105 group-hover/new:border-solid group-hover/new:border-accent group-hover/new:bg-accent/5 group-hover/new:text-accent"
|
||||
>
|
||||
<Plus :size="24" />
|
||||
</div>
|
||||
<div class="flex flex-col items-center gap-1">
|
||||
<span class="text-base font-semibold text-secondary">{{ t('pages.uploaderConfig.addNew') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template v-else-if="editMode">
|
||||
<ManageEditPage v-model:edit-mode="editMode" :alias-name="editingAlias" :active-name="activeName" />
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -284,31 +251,37 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
BookOpen,
|
||||
ChevronDownIcon,
|
||||
Cloud,
|
||||
DatabaseIcon,
|
||||
DownloadIcon,
|
||||
FolderIcon,
|
||||
InfoIcon,
|
||||
LinkIcon,
|
||||
Pencil,
|
||||
Plus,
|
||||
PointerIcon,
|
||||
RefreshCwIcon,
|
||||
RotateCcwIcon,
|
||||
SaveIcon,
|
||||
Settings2,
|
||||
Trash2,
|
||||
TrashIcon,
|
||||
XIcon,
|
||||
} from 'lucide-vue-next'
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import CustomButton from '@/components/common/CustomButton.vue'
|
||||
import useConfirm from '@/hooks/useConfirm'
|
||||
import useMessage from '@/hooks/useMessage'
|
||||
import ManageEditPage from '@/manage/pages/ManageEditPage.vue'
|
||||
import { useManageStore } from '@/manage/store/manageStore'
|
||||
import { formObjToTableData } from '@/manage/utils/common'
|
||||
import { supportedPicBedList } from '@/manage/utils/constants'
|
||||
import { getConfig, removeConfig, saveConfig } from '@/manage/utils/dataSender'
|
||||
import { formatEndpoint } from '@/utils/common'
|
||||
import { configPaths } from '@/utils/configPaths'
|
||||
import { getConfig as getPicBedsConfig } from '@/utils/dataSender'
|
||||
import { IRPCActionType } from '@/utils/enum'
|
||||
import { II18nLanguage, IRPCActionType } from '@/utils/enum'
|
||||
|
||||
const { t } = useI18n()
|
||||
const manageStore = useManageStore()
|
||||
@@ -316,10 +289,10 @@ const router = useRouter()
|
||||
const message = useMessage()
|
||||
const { confirm } = useConfirm()
|
||||
|
||||
const editMode = ref(false)
|
||||
const editingAlias = ref('')
|
||||
const activeName = ref('login')
|
||||
const expandedConfigs = ref<string[]>([])
|
||||
const importDropdownOpen = ref(false)
|
||||
|
||||
const configResult: IStringKeyMap = reactive({})
|
||||
const existingConfiguration = reactive({} as IStringKeyMap)
|
||||
const dataForTable = reactive([] as any[])
|
||||
@@ -361,77 +334,6 @@ const notifyUser = (msg: string, type: 'success' | 'error' | 'warning' = 'succes
|
||||
message[type](`${msg}`)
|
||||
}
|
||||
|
||||
const validateField = (picBedName: string, optionKey: string) => {
|
||||
const fieldKey = `${picBedName}.${optionKey}`
|
||||
const configOption = supportedPicBedList[picBedName]?.configOptions?.[optionKey]
|
||||
const value = configResult[fieldKey]
|
||||
|
||||
if (!configOption) return
|
||||
|
||||
delete formErrors[fieldKey]
|
||||
|
||||
if (configOption.required) {
|
||||
if (configOption.type === 'boolean') {
|
||||
} else if (!value || value === '') {
|
||||
formErrors[fieldKey] = t('pages.manage.constant.pleaseInput', { name: configOption.description })
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (configOption.rule && Array.isArray(configOption.rule)) {
|
||||
for (const rule of configOption.rule) {
|
||||
if (rule.validator) {
|
||||
try {
|
||||
rule.validator(rule, value, (error: Error | null) => {
|
||||
if (error) {
|
||||
formErrors[fieldKey] = error.message
|
||||
}
|
||||
})
|
||||
} catch (e) {
|
||||
console.error('Validation error:', e)
|
||||
}
|
||||
} else if (rule.type === 'number' && value !== undefined && value !== '') {
|
||||
if (isNaN(Number(value))) {
|
||||
formErrors[fieldKey] = rule.message || t('pages.manage.constant.itemsPPBeNumber')
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (optionKey === 'alias' && value) {
|
||||
const reg = /^[\p{Unified_Ideograph}_a-zA-Z0-9-]+$/u
|
||||
if (!reg.test(value)) {
|
||||
formErrors[fieldKey] = t('pages.manage.login.aliasMsg')
|
||||
}
|
||||
}
|
||||
|
||||
if (optionKey === 'itemsPerPage' && value !== undefined && value !== '') {
|
||||
const numValue = Number(value)
|
||||
if (numValue < 20 || numValue > 1000) {
|
||||
formErrors[fieldKey] = t('pages.manage.login.itemsPerPageMsg')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const clearFieldError = (fieldKey: string) => {
|
||||
delete formErrors[fieldKey]
|
||||
}
|
||||
|
||||
const validateAllFields = (picBedName: string): boolean => {
|
||||
const options = supportedPicBedList[picBedName]?.options || []
|
||||
let isValid = true
|
||||
|
||||
for (const option of options) {
|
||||
validateField(picBedName, option)
|
||||
if (formErrors[`${picBedName}.${option}`]) {
|
||||
isValid = false
|
||||
}
|
||||
}
|
||||
|
||||
return isValid
|
||||
}
|
||||
|
||||
const initializeDefaultValues = (picBedName: string) => {
|
||||
if (!supportedPicBedList[picBedName]) return
|
||||
|
||||
@@ -486,88 +388,10 @@ async function getExistingConfig(name: string) {
|
||||
handleConfigImport(currentAliasList[0])
|
||||
}
|
||||
|
||||
function getAliasList() {
|
||||
return Object.values(existingConfiguration).map(item => item.alias)
|
||||
}
|
||||
|
||||
async function handleConfigChange(name: string) {
|
||||
if (!validateAllFields(name)) {
|
||||
notifyUser(t('pages.manage.login.noRequiredMsg'), 'error')
|
||||
return
|
||||
}
|
||||
|
||||
const aliasList = getAliasList()
|
||||
const allKeys = Object.keys(supportedPicBedList[name].configOptions)
|
||||
const resultMap: IStringKeyMap = {}
|
||||
|
||||
for (const key of allKeys) {
|
||||
const resultKey = name + '.' + key
|
||||
if (key === 'customUrl' && configResult[resultKey] !== undefined && configResult[resultKey] !== '') {
|
||||
if (name !== 'upyun') {
|
||||
configResult[resultKey] = formatEndpoint(configResult[resultKey], false)
|
||||
}
|
||||
}
|
||||
|
||||
if (supportedPicBedList[name].configOptions[key].default !== undefined && configResult[resultKey] === '') {
|
||||
resultMap[key] = supportedPicBedList[name].configOptions[key].default
|
||||
} else if (configResult[resultKey] === undefined) {
|
||||
if (supportedPicBedList[name].configOptions[key].default !== undefined) {
|
||||
resultMap[key] = supportedPicBedList[name].configOptions[key].default
|
||||
} else {
|
||||
resultMap[key] = ''
|
||||
}
|
||||
} else {
|
||||
resultMap[key] = configResult[resultKey]
|
||||
}
|
||||
}
|
||||
resultMap.picBedName = name
|
||||
if (resultMap.bucketName !== undefined) {
|
||||
resultMap.transformedConfig = {}
|
||||
const bucketName = resultMap.bucketName.split(',')
|
||||
const baseDir = resultMap.baseDir?.split(',')
|
||||
const area = resultMap.area?.split(',')
|
||||
const customUrl = resultMap.customUrl?.split(',')
|
||||
const operator = resultMap.operator?.split(',')
|
||||
const password = resultMap.password?.split(',')
|
||||
for (let i = 0; i < bucketName.length; i++) {
|
||||
if (bucketName[i]) {
|
||||
resultMap.transformedConfig[bucketName[i]] = {
|
||||
baseDir: baseDir?.[i] || '/',
|
||||
area: area?.[i] || '',
|
||||
customUrl: customUrl?.[i] || '',
|
||||
operator: operator?.[i] || '',
|
||||
password: password?.[i] || '',
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (resultMap.transformedConfig) {
|
||||
resultMap.transformedConfig = JSON.stringify(resultMap.transformedConfig)
|
||||
}
|
||||
saveConfig(`picBed.${resultMap.alias}`, resultMap)
|
||||
await manageStore.refreshConfig()
|
||||
await getExistingConfig(activeName.value)
|
||||
dataForTable.length = 0
|
||||
getDataForTable()
|
||||
if (aliasList.includes(resultMap.alias)) {
|
||||
notifyUser(`${t('pages.manage.login.configChangeMsg')}${resultMap.alias}`, 'warning')
|
||||
} else {
|
||||
notifyUser(`${t('pages.manage.login.configSaveMsg')}${resultMap.alias}`, 'success')
|
||||
}
|
||||
}
|
||||
|
||||
const handleConfigReset = (name: string) => {
|
||||
const keys = Object.keys(formErrors).filter(key => key.startsWith(name))
|
||||
keys.forEach(key => {
|
||||
delete formErrors[key]
|
||||
function openBucketPageSetting() {
|
||||
router.push({
|
||||
path: '/main-page/manage-setting-page',
|
||||
})
|
||||
|
||||
const configKeys = Object.keys(configResult).filter(key => key.startsWith(name))
|
||||
configKeys.forEach(key => {
|
||||
delete configResult[key]
|
||||
})
|
||||
|
||||
initializeDefaultValues(name)
|
||||
}
|
||||
|
||||
const handleConfigRemove = async (name: string) => {
|
||||
@@ -611,8 +435,6 @@ const copyToClipboard = (text: string) => {
|
||||
notifyUser(`${t('pages.manage.login.copySuccess', { text })}`, 'success')
|
||||
}
|
||||
|
||||
const handleReferenceClick = (url: string) => window.electron.sendRPC(IRPCActionType.OPEN_URL, url)
|
||||
|
||||
const handleConfigClick = async (item: any) => {
|
||||
const alias = item.alias
|
||||
const config = JSON.stringify(item.config)
|
||||
@@ -629,6 +451,11 @@ const handleConfigClick = async (item: any) => {
|
||||
})
|
||||
}
|
||||
|
||||
function openEditPage(alias: string) {
|
||||
editingAlias.value = alias
|
||||
editMode.value = true
|
||||
}
|
||||
|
||||
function handleConfigImport(alias: string) {
|
||||
const selectedConfig = existingConfiguration[alias]
|
||||
if (!selectedConfig) return
|
||||
@@ -641,6 +468,7 @@ function handleConfigImport(alias: string) {
|
||||
}
|
||||
|
||||
const handleTabChange = (tabName: string) => {
|
||||
editMode.value = false
|
||||
activeName.value = tabName
|
||||
getExistingConfig(tabName)
|
||||
|
||||
@@ -653,7 +481,7 @@ const handleTabChange = (tabName: string) => {
|
||||
}
|
||||
}
|
||||
|
||||
const toggleConfigDetails = (alias: string) => {
|
||||
const toggleConfigDetails = async (alias: string) => {
|
||||
const index = expandedConfigs.value.indexOf(alias)
|
||||
if (index > -1) {
|
||||
expandedConfigs.value.splice(index, 1)
|
||||
@@ -662,10 +490,6 @@ const toggleConfigDetails = (alias: string) => {
|
||||
}
|
||||
}
|
||||
|
||||
const toggleImportDropdown = () => {
|
||||
importDropdownOpen.value = !importDropdownOpen.value
|
||||
}
|
||||
|
||||
const refreshConfigs = () => {
|
||||
getAllConfigAliasArray()
|
||||
notifyUser(t('pages.manage.login.configurationRefreshMsg'), 'success')
|
||||
@@ -716,6 +540,12 @@ async function getCurrentConfigList() {
|
||||
await getAllConfigAliasArray()
|
||||
}
|
||||
|
||||
async function goConfigPage() {
|
||||
const lang = (await getConfig(configPaths.settings.language)) || II18nLanguage.ZH_CN
|
||||
const url = `https://piclist.cn/${lang === II18nLanguage.EN ? 'en/' : ''}manage.html`
|
||||
window.electron.sendRPC(IRPCActionType.OPEN_URL, url)
|
||||
}
|
||||
|
||||
function isImported(alias: string) {
|
||||
return Object.values(allConfigAliasMap).some(item => item.alias === alias)
|
||||
}
|
||||
@@ -967,5 +797,3 @@ onMounted(() => {
|
||||
getCurrentConfigList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped src="./css/LoginPage.css"></style>
|
||||
|
||||
429
src/renderer/manage/pages/ManageEditPage.vue
Normal file
429
src/renderer/manage/pages/ManageEditPage.vue
Normal file
@@ -0,0 +1,429 @@
|
||||
<template>
|
||||
<div class="no-scrollbar flex h-full w-full flex-col gap-4 overflow-auto p-4">
|
||||
<!-- Info Section -->
|
||||
<div
|
||||
class="flex items-center justify-center rounded-md border border-border-secondary bg-bg-secondary p-2 shadow-md"
|
||||
>
|
||||
<InfoIcon :size="20" />
|
||||
<p class="m-0 text-sm leading-[1.5] font-semibold text-secondary">
|
||||
{{ supportedPicBedList[activeName].explain }}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="flex items-center justify-center rounded-md border border-border-secondary bg-bg-secondary p-2 shadow-md"
|
||||
>
|
||||
<LinkIcon :size="20" />
|
||||
<p class="m-0 text-sm leading-[1.5] font-semibold text-secondary">
|
||||
{{ supportedPicBedList[activeName].referenceText }}
|
||||
<button class="link-button" @click="handleReferenceClick(supportedPicBedList[activeName].refLink)">
|
||||
{{ supportedPicBedList[activeName].refLink }}
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
<div class="border-none">
|
||||
<div class="grid w-full grid-cols-1 gap-3">
|
||||
<SettingCard>
|
||||
<CustomInput
|
||||
v-model.trim="configResult[activeName + '.alias']"
|
||||
type="text"
|
||||
:placeholder="supportedPicBedList[activeName].configOptions.alias.placeholder || ''"
|
||||
:title="supportedPicBedList[activeName].configOptions.alias.description"
|
||||
:required="supportedPicBedList[activeName].configOptions.alias.required"
|
||||
:class="{ 'border-danger': formErrors[activeName + '.' + 'alias'] }"
|
||||
@blur="validateField(activeName, 'alias')"
|
||||
@input="clearFieldError(activeName + '.alias')"
|
||||
/>
|
||||
<template v-if="formErrors[activeName + '.' + 'alias']" #extra>
|
||||
<div class="mt-1 text-xs text-danger">
|
||||
{{ formErrors[activeName + '.' + 'alias'] }}
|
||||
</div>
|
||||
</template>
|
||||
</SettingCard>
|
||||
<template v-for="option in supportedPicBedList[activeName].options" :key="option">
|
||||
<SettingCard
|
||||
v-if="supportedPicBedList[activeName].configOptions[option].type === 'string' && option !== 'alias'"
|
||||
>
|
||||
<CustomInput
|
||||
v-model.trim="configResult[activeName + '.' + option]"
|
||||
type="text"
|
||||
:placeholder="supportedPicBedList[activeName].configOptions[option].placeholder || ''"
|
||||
:class="{ 'border-danger': formErrors[activeName + '.' + option] }"
|
||||
:title="supportedPicBedList[activeName].configOptions[option].description"
|
||||
:required="supportedPicBedList[activeName].configOptions[option].required"
|
||||
:disabled="!!supportedPicBedList[activeName].configOptions[option].disabled"
|
||||
@blur="validateField(activeName, option)"
|
||||
@input="clearFieldError(activeName + '.' + option)"
|
||||
/>
|
||||
<template v-if="formErrors[activeName + '.' + option]" #extra>
|
||||
<div class="mt-1 text-xs text-danger">
|
||||
{{ formErrors[activeName + '.' + option] }}
|
||||
</div>
|
||||
</template>
|
||||
</SettingCard>
|
||||
</template>
|
||||
<template v-for="option in supportedPicBedList[activeName].options" :key="option">
|
||||
<SettingCard v-if="supportedPicBedList[activeName].configOptions[option].type === 'number'">
|
||||
<CustomInput
|
||||
v-model.number="configResult[activeName + '.' + option]"
|
||||
type="number"
|
||||
:placeholder="supportedPicBedList[activeName].configOptions[option].placeholder || ''"
|
||||
:class="{ 'border-danger': formErrors[activeName + '.' + option] }"
|
||||
:title="supportedPicBedList[activeName].configOptions[option].description"
|
||||
:required="supportedPicBedList[activeName].configOptions[option].required"
|
||||
@blur="validateField(activeName, option)"
|
||||
@input="clearFieldError(activeName + '.' + option)"
|
||||
/>
|
||||
<template v-if="formErrors[activeName + '.' + option]" #extra>
|
||||
<div class="mt-1 text-xs text-danger">
|
||||
{{ formErrors[activeName + '.' + option] }}
|
||||
</div>
|
||||
</template>
|
||||
</SettingCard>
|
||||
</template>
|
||||
<template v-for="option in supportedPicBedList[activeName].options" :key="option">
|
||||
<SettingCard v-if="supportedPicBedList[activeName].configOptions[option].type === 'boolean'" p1>
|
||||
<CustomSwitch
|
||||
v-model="configResult[activeName + '.' + option]"
|
||||
no-border
|
||||
small
|
||||
:required="supportedPicBedList[activeName].configOptions[option].required"
|
||||
:title="supportedPicBedList[activeName].configOptions[option].description"
|
||||
:tips="supportedPicBedList[activeName].configOptions[option].tooltip || ''"
|
||||
@update:model-value="validateField(activeName, option)"
|
||||
>
|
||||
</CustomSwitch>
|
||||
</SettingCard>
|
||||
</template>
|
||||
<template v-for="option in supportedPicBedList[activeName].options" :key="option">
|
||||
<SettingCard v-if="supportedPicBedList[activeName].configOptions[option].type === 'select'">
|
||||
<CustomSelect
|
||||
v-model="configResult[activeName + '.' + option]"
|
||||
:title="supportedPicBedList[activeName].configOptions[option].description"
|
||||
:required="supportedPicBedList[activeName].configOptions[option].required"
|
||||
:select-list="
|
||||
Object.entries(supportedPicBedList[activeName].configOptions[option].selectOptions || {}).map(
|
||||
([key, value]) => ({
|
||||
value: key,
|
||||
label: value as string,
|
||||
}),
|
||||
)
|
||||
"
|
||||
:class="{ 'border-danger': formErrors[activeName + '.' + option] }"
|
||||
@change="validateField(activeName, option)"
|
||||
>
|
||||
<template #pre-info>
|
||||
<option value="" disabled>
|
||||
{{ t('pages.manage.login.selectPlaceholder') }}
|
||||
</option>
|
||||
</template>
|
||||
</CustomSelect>
|
||||
<template v-if="formErrors[activeName + '.' + option]" #extra>
|
||||
<div class="mt-1 text-xs text-danger">
|
||||
{{ formErrors[activeName + '.' + option] }}
|
||||
</div>
|
||||
</template>
|
||||
</SettingCard>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Action Buttons -->
|
||||
<div class="flex flex-wrap items-center justify-end gap-4">
|
||||
<div class="flex gap-4">
|
||||
<div v-if="currentAliasList.length > 0" class="w-40">
|
||||
<SingleSelect
|
||||
v-model="selectedAlias"
|
||||
:title="t('pages.manage.login.import')"
|
||||
:key-list="currentAliasList"
|
||||
:custom-front-icon="DownloadIcon"
|
||||
/>
|
||||
</div>
|
||||
<CustomButton
|
||||
type="primary"
|
||||
:text="t('pages.manage.login.save')"
|
||||
:icon="SaveIcon"
|
||||
@click="handleConfigChange(activeName)"
|
||||
/>
|
||||
<CustomButton
|
||||
class="bg-danger/70"
|
||||
:text="t('pages.manage.login.reset')"
|
||||
:icon="RotateCcwIcon"
|
||||
@click="handleConfigReset(activeName)"
|
||||
/>
|
||||
<CustomButton class="bg-warning/70" :text="t('common.cancel')" :icon="XIcon" @click="cancelEditMode" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { DownloadIcon, InfoIcon, LinkIcon, RotateCcwIcon, SaveIcon, XIcon } from 'lucide-vue-next'
|
||||
import { onMounted, reactive, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import CustomButton from '@/components/common/CustomButton.vue'
|
||||
import CustomInput from '@/components/common/CustomInput.vue'
|
||||
import CustomSelect from '@/components/common/CustomSelect.vue'
|
||||
import CustomSwitch from '@/components/common/CustomSwitch.vue'
|
||||
import SettingCard from '@/components/common/SettingCard.vue'
|
||||
import SingleSelect from '@/components/common/SingleSelect.vue'
|
||||
import useMessage from '@/hooks/useMessage'
|
||||
import { useManageStore } from '@/manage/store/manageStore'
|
||||
import { supportedPicBedList } from '@/manage/utils/constants'
|
||||
import { getConfig, saveConfig } from '@/manage/utils/dataSender'
|
||||
import { formatEndpoint } from '@/utils/common'
|
||||
import { IRPCActionType } from '@/utils/enum'
|
||||
|
||||
const { t } = useI18n()
|
||||
const manageStore = useManageStore()
|
||||
const message = useMessage()
|
||||
const formErrors = reactive({} as IStringKeyMap)
|
||||
const configResult: IStringKeyMap = reactive({})
|
||||
const existingConfiguration = reactive({} as IStringKeyMap)
|
||||
const currentAliasList = reactive([] as string[])
|
||||
const dataForTable = reactive([] as any[])
|
||||
const editMode = defineModel<boolean>('editMode')
|
||||
const selectedAlias = ref('')
|
||||
|
||||
const { aliasName, activeName } = defineProps<{
|
||||
aliasName: string
|
||||
activeName: string
|
||||
}>()
|
||||
|
||||
watch(selectedAlias, newAlias => {
|
||||
if (newAlias) {
|
||||
handleConfigImport(newAlias)
|
||||
}
|
||||
})
|
||||
|
||||
const handleReferenceClick = (url: string) => window.electron.sendRPC(IRPCActionType.OPEN_URL, url)
|
||||
|
||||
const validateField = (picBedName: string, optionKey: string) => {
|
||||
const fieldKey = `${picBedName}.${optionKey}`
|
||||
const configOption = supportedPicBedList[picBedName]?.configOptions?.[optionKey]
|
||||
const value = configResult[fieldKey]
|
||||
|
||||
if (!configOption) return
|
||||
|
||||
delete formErrors[fieldKey]
|
||||
|
||||
if (configOption.required) {
|
||||
if (configOption.type === 'boolean') {
|
||||
} else if (!value || value === '') {
|
||||
formErrors[fieldKey] = t('pages.manage.constant.pleaseInput', { name: configOption.description })
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (configOption.rule && Array.isArray(configOption.rule)) {
|
||||
for (const rule of configOption.rule) {
|
||||
if (rule.validator) {
|
||||
try {
|
||||
rule.validator(rule, value, (error: Error | null) => {
|
||||
if (error) {
|
||||
formErrors[fieldKey] = error.message
|
||||
}
|
||||
})
|
||||
} catch (e) {
|
||||
console.error('Validation error:', e)
|
||||
}
|
||||
} else if (rule.type === 'number' && value !== undefined && value !== '') {
|
||||
if (isNaN(Number(value))) {
|
||||
formErrors[fieldKey] = rule.message || t('pages.manage.constant.itemsPPBeNumber')
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (optionKey === 'alias' && value) {
|
||||
const reg = /^[\p{Unified_Ideograph}_a-zA-Z0-9-]+$/u
|
||||
if (!reg.test(value)) {
|
||||
formErrors[fieldKey] = t('pages.manage.login.aliasMsg')
|
||||
}
|
||||
}
|
||||
|
||||
if (optionKey === 'itemsPerPage' && value !== undefined && value !== '') {
|
||||
const numValue = Number(value)
|
||||
if (numValue < 20 || numValue > 1000) {
|
||||
formErrors[fieldKey] = t('pages.manage.login.itemsPerPageMsg')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const clearFieldError = (fieldKey: string) => {
|
||||
delete formErrors[fieldKey]
|
||||
}
|
||||
|
||||
async function handleConfigChange(name: string) {
|
||||
if (!validateAllFields(name)) {
|
||||
notifyUser(t('pages.manage.login.noRequiredMsg'), 'error')
|
||||
return
|
||||
}
|
||||
|
||||
const aliasList = getAliasList()
|
||||
const allKeys = Object.keys(supportedPicBedList[name].configOptions)
|
||||
const resultMap: IStringKeyMap = {}
|
||||
|
||||
for (const key of allKeys) {
|
||||
const resultKey = name + '.' + key
|
||||
if (key === 'customUrl' && configResult[resultKey] !== undefined && configResult[resultKey] !== '') {
|
||||
if (name !== 'upyun') {
|
||||
configResult[resultKey] = formatEndpoint(configResult[resultKey], false)
|
||||
}
|
||||
}
|
||||
|
||||
if (supportedPicBedList[name].configOptions[key].default !== undefined && configResult[resultKey] === '') {
|
||||
resultMap[key] = supportedPicBedList[name].configOptions[key].default
|
||||
} else if (configResult[resultKey] === undefined) {
|
||||
if (supportedPicBedList[name].configOptions[key].default !== undefined) {
|
||||
resultMap[key] = supportedPicBedList[name].configOptions[key].default
|
||||
} else {
|
||||
resultMap[key] = ''
|
||||
}
|
||||
} else {
|
||||
resultMap[key] = configResult[resultKey]
|
||||
}
|
||||
}
|
||||
resultMap.picBedName = name
|
||||
if (resultMap.bucketName !== undefined) {
|
||||
resultMap.transformedConfig = {}
|
||||
const bucketName = resultMap.bucketName.split(',')
|
||||
const baseDir = resultMap.baseDir?.split(',')
|
||||
const area = resultMap.area?.split(',')
|
||||
const customUrl = resultMap.customUrl?.split(',')
|
||||
const operator = resultMap.operator?.split(',')
|
||||
const password = resultMap.password?.split(',')
|
||||
for (let i = 0; i < bucketName.length; i++) {
|
||||
if (bucketName[i]) {
|
||||
resultMap.transformedConfig[bucketName[i]] = {
|
||||
baseDir: baseDir?.[i] || '/',
|
||||
area: area?.[i] || '',
|
||||
customUrl: customUrl?.[i] || '',
|
||||
operator: operator?.[i] || '',
|
||||
password: password?.[i] || '',
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (resultMap.transformedConfig) {
|
||||
resultMap.transformedConfig = JSON.stringify(resultMap.transformedConfig)
|
||||
}
|
||||
saveConfig(`picBed.${resultMap.alias}`, resultMap)
|
||||
await manageStore.refreshConfig()
|
||||
await getExistingConfig(activeName)
|
||||
dataForTable.length = 0
|
||||
getDataForTable()
|
||||
if (aliasList.includes(resultMap.alias)) {
|
||||
notifyUser(`${t('pages.manage.login.configChangeMsg')}${resultMap.alias}`, 'warning')
|
||||
} else {
|
||||
notifyUser(`${t('pages.manage.login.configSaveMsg')}${resultMap.alias}`, 'success')
|
||||
}
|
||||
editMode.value = false
|
||||
}
|
||||
|
||||
const notifyUser = (msg: string, type: 'success' | 'error' | 'warning' = 'success') => {
|
||||
message[type](`${msg}`)
|
||||
}
|
||||
|
||||
function getDataForTable() {
|
||||
for (const key in existingConfiguration) {
|
||||
dataForTable.push({ ...(existingConfiguration[key] as IStringKeyMap) })
|
||||
}
|
||||
}
|
||||
|
||||
async function getExistingConfig(name: string) {
|
||||
currentAliasList.length = 0
|
||||
const result = await getConfig<any>('picBed')
|
||||
for (const key in existingConfiguration) {
|
||||
delete existingConfiguration[key]
|
||||
}
|
||||
if (!result || typeof result !== 'object' || Object.keys(result).length === 0) {
|
||||
existingConfiguration[name] = { fail: '暂无配置' }
|
||||
} else {
|
||||
for (const key in result) {
|
||||
if (result[key].picBedName === name) {
|
||||
existingConfiguration[key] = result[key]
|
||||
currentAliasList.push(result[key].alias)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dataForTable.length = 0
|
||||
getDataForTable()
|
||||
handleConfigImport(aliasName)
|
||||
}
|
||||
|
||||
function handleConfigImport(alias: string) {
|
||||
const selectedConfig = existingConfiguration[alias]
|
||||
if (!selectedConfig) return
|
||||
|
||||
supportedPicBedList[selectedConfig.picBedName].options.forEach((option: any) => {
|
||||
if (selectedConfig[option] !== undefined) {
|
||||
configResult[selectedConfig.picBedName + '.' + option] = selectedConfig[option]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const validateAllFields = (picBedName: string): boolean => {
|
||||
const options = supportedPicBedList[picBedName]?.options || []
|
||||
let isValid = true
|
||||
|
||||
for (const option of options) {
|
||||
validateField(picBedName, option)
|
||||
if (formErrors[`${picBedName}.${option}`]) {
|
||||
isValid = false
|
||||
}
|
||||
}
|
||||
|
||||
return isValid
|
||||
}
|
||||
|
||||
function getAliasList() {
|
||||
return Object.values(existingConfiguration).map(item => item.alias)
|
||||
}
|
||||
|
||||
const handleConfigReset = (name: string) => {
|
||||
const keys = Object.keys(formErrors).filter(key => key.startsWith(name))
|
||||
keys.forEach(key => {
|
||||
delete formErrors[key]
|
||||
})
|
||||
|
||||
const configKeys = Object.keys(configResult).filter(key => key.startsWith(name))
|
||||
configKeys.forEach(key => {
|
||||
delete configResult[key]
|
||||
})
|
||||
|
||||
initializeDefaultValues(name)
|
||||
}
|
||||
|
||||
const initializeDefaultValues = (picBedName: string) => {
|
||||
if (!supportedPicBedList[picBedName]) return
|
||||
|
||||
const options = supportedPicBedList[picBedName].options || []
|
||||
for (const option of options) {
|
||||
const fieldKey = `${picBedName}.${option}`
|
||||
const configOption = supportedPicBedList[picBedName].configOptions[option]
|
||||
|
||||
if (configResult[fieldKey] === undefined || configResult[fieldKey] === '') {
|
||||
if (configOption.default !== undefined) {
|
||||
configResult[fieldKey] = configOption.default
|
||||
} else if (configOption.type === 'boolean') {
|
||||
configResult[fieldKey] = false
|
||||
} else if (configOption.type === 'number') {
|
||||
configResult[fieldKey] = 0
|
||||
} else {
|
||||
configResult[fieldKey] = ''
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const cancelEditMode = () => {
|
||||
editMode.value = false
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
getExistingConfig(activeName)
|
||||
await manageStore.refreshConfig()
|
||||
})
|
||||
</script>
|
||||
@@ -99,7 +99,13 @@
|
||||
</div>
|
||||
|
||||
<!-- PicBed Switch Dialog -->
|
||||
<transition name="modal">
|
||||
<transition
|
||||
name="modal"
|
||||
enter-active-class="transition-all duration-200 ease-apple"
|
||||
leave-active-class="transition-all duration-200 ease-apple"
|
||||
enter-from-class="opacity-0"
|
||||
leave-to-class="opacity-0"
|
||||
>
|
||||
<div v-if="picBedSwitchDialogVisible" class="dialog-overlay" @click="picBedSwitchDialogVisible = false">
|
||||
<div class="dialog-container" @click.stop>
|
||||
<div class="dialog-header">
|
||||
|
||||
@@ -1,213 +1,133 @@
|
||||
<template>
|
||||
<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">
|
||||
<Trash2Icon :size="16" />
|
||||
{{
|
||||
t('pages.manage.setting.clearCache', {
|
||||
percent: dbSizeAvailableRate,
|
||||
size: formatFileSize(dbSize) || 0,
|
||||
})
|
||||
}}
|
||||
</button>
|
||||
</div>
|
||||
<div class="relative flex h-full w-full items-center justify-center">
|
||||
<div class="relative z-1 flex h-full w-full flex-col items-center justify-start gap-4 rounded-xl border-none p-4">
|
||||
<div
|
||||
class="flex w-full items-center justify-between gap-4 rounded-2xl border border-border-secondary px-6 py-2 shadow-md max-md:items-stretch max-md:p-5"
|
||||
>
|
||||
<div class="flex flex-1 flex-wrap items-center gap-4 p-2">
|
||||
<Settings :size="24" class="text-accent" />
|
||||
<div>
|
||||
<h1 class="m-0 text-2xl font-semibold tracking-tight text-main">{{ t('pages.manage.setting.title') }}</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="relative flex h-full w-full flex-1 items-center justify-center overflow-hidden rounded-2xl border border-border-secondary p-1 shadow-md"
|
||||
>
|
||||
<div class="border4 no-scrollbar flex h-full w-full flex-1 flex-col gap-6 overflow-auto p-4">
|
||||
<!-- Cache Info Card -->
|
||||
<SettingSection :title="t('pages.manage.setting.section.cache')" :icon="Trash2Icon" only-one-row>
|
||||
<CustomButton
|
||||
type="secondary"
|
||||
:icon="Trash2Icon"
|
||||
class="bg-warning/20 p-4!"
|
||||
:text="
|
||||
t('pages.manage.setting.clearCache', {
|
||||
percent: dbSizeAvailableRate,
|
||||
size: formatFileSize(dbSize) || 0,
|
||||
})
|
||||
"
|
||||
@click="handleConfirmClearDb"
|
||||
/>
|
||||
</SettingSection>
|
||||
|
||||
<!-- 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>
|
||||
<SettingSection :title="t('pages.manage.setting.section.general')" :icon="Settings">
|
||||
<SettingCard>
|
||||
<CustomSelect
|
||||
v-model="form.pasteFormat"
|
||||
:select-list="pasteFormatList"
|
||||
:title="t('pages.manage.setting.copyFormat.title')"
|
||||
:icon="Edit2Icon"
|
||||
/>
|
||||
</SettingCard>
|
||||
<SettingCard>
|
||||
<CustomInput
|
||||
v-model="form.customPasteFormat"
|
||||
:title="t('pages.manage.setting.copyFormat.customTitle')"
|
||||
:placeholder="t('pages.manage.setting.copyFormat.customTips')"
|
||||
/>
|
||||
</SettingCard>
|
||||
<SettingCard v-for="item in switchFieldsConfigList" :key="item.configName" class="mb-4" p1>
|
||||
<CustomSwitch v-model="form[item.configName]" small no-border :tips="item.tooltip">
|
||||
<template #custom-title>
|
||||
<span v-for="(segment, index) in item.segments" :key="index" :class="segment.class">
|
||||
{{ segment.text }}
|
||||
</span>
|
||||
</template>
|
||||
<template #switch-text>
|
||||
<span class="text-sm text-secondary">{{
|
||||
form[item.configName] ? item.activeText : item.inactiveText
|
||||
}}</span>
|
||||
</template>
|
||||
</CustomSwitch>
|
||||
</SettingCard>
|
||||
</SettingSection>
|
||||
|
||||
<!-- 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
|
||||
<SettingSection
|
||||
v-if="form.customRename"
|
||||
:title="t('pages.manage.setting.section.naming')"
|
||||
:icon="Edit2Icon"
|
||||
only-one-row
|
||||
>
|
||||
<CustomInput
|
||||
v-model="form.customRenameFormat"
|
||||
type="text"
|
||||
class="form-input"
|
||||
:title="t('pages.manage.setting.customRenameTablePlaceholder')"
|
||||
:placeholder="t('pages.manage.setting.customRenameTablePlaceholder')"
|
||||
/>
|
||||
</div>
|
||||
<placeholderTable :list="advancedRenameList" :title-list="advancedRenameTitleList" />
|
||||
</SettingSection>
|
||||
|
||||
<!-- 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">
|
||||
<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
|
||||
<SettingSection :icon="Download" :title="t('pages.manage.setting.section.up-down')">
|
||||
<SettingCard v-for="item in switchFieldsSpecialList" :key="item.configName" class="mb-4" p1>
|
||||
<CustomSwitch v-model="form[item.configName]" small no-border :tips="item.tooltip">
|
||||
<template #custom-title>
|
||||
<span v-for="(segment, index) in item.segments" :key="index" :class="segment.class">
|
||||
{{ segment.text }}
|
||||
</span>
|
||||
</template>
|
||||
</CustomSwitch>
|
||||
</SettingCard>
|
||||
<SettingCard>
|
||||
<CustomInput
|
||||
v-model.number="form.maxDownloadFileCount"
|
||||
type="number"
|
||||
class="form-input number-input"
|
||||
:title="t('pages.manage.setting.maxDownLoadFileLimit')"
|
||||
:placeholder="t('pages.manage.setting.maxDownLoadFileLimitDesc')"
|
||||
type="number"
|
||||
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
|
||||
</SettingCard>
|
||||
<SettingCard>
|
||||
<CustomInput
|
||||
v-model.number="form.PreSignedExpire"
|
||||
type="number"
|
||||
class="form-input number-input"
|
||||
:title="t('pages.manage.setting.preSignedUrlExpire')"
|
||||
:placeholder="t('pages.manage.setting.preSignedUrlExpireDesc')"
|
||||
type="number"
|
||||
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="`format-${item}`" class="radio-option">
|
||||
<input v-model="form.pasteFormat" type="radio" :value="item" class="radio-input" :name="'paste-format'" />
|
||||
<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
|
||||
</SettingCard>
|
||||
<SettingCard>
|
||||
<CustomInput
|
||||
v-model="form.downloadDir"
|
||||
type="text"
|
||||
class="form-input group-input"
|
||||
disabled
|
||||
:title="t('pages.manage.setting.selectDownloadFolderTitle')"
|
||||
: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>
|
||||
disabled
|
||||
>
|
||||
<template #input-extra>
|
||||
<button
|
||||
type="button"
|
||||
class="absolute top-0 right-0 flex w-[10%] min-w-[80px] cursor-pointer items-center gap-2 rounded-md bg-accent px-4 py-3 text-sm font-medium text-white"
|
||||
@click="handleDownloadDirClick"
|
||||
>
|
||||
<FolderIcon :size="16" />
|
||||
{{ t('pages.manage.setting.browse') }}
|
||||
</button>
|
||||
</template>
|
||||
</CustomInput>
|
||||
</SettingCard>
|
||||
</SettingSection>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -215,15 +135,21 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { FolderIcon, Trash2Icon } from 'lucide-vue-next'
|
||||
import { nextTick, onBeforeMount, ref, watch } from 'vue'
|
||||
import { Download, Edit2Icon, FolderIcon, Settings, Trash2Icon } from 'lucide-vue-next'
|
||||
import { computed, nextTick, onBeforeMount, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import CustomButton from '@/components/common/CustomButton.vue'
|
||||
import CustomInput from '@/components/common/CustomInput.vue'
|
||||
import CustomSelect from '@/components/common/CustomSelect.vue'
|
||||
import CustomSwitch from '@/components/common/CustomSwitch.vue'
|
||||
import PlaceholderTable from '@/components/common/PlaceholderTable.vue'
|
||||
import SettingCard from '@/components/common/SettingCard.vue'
|
||||
import SettingSection from '@/components/common/SettingSection.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 { formatFileSize } from '@/manage/utils/common'
|
||||
import { getConfig, saveConfig } from '@/manage/utils/dataSender'
|
||||
import { IRPCActionType } from '@/utils/enum'
|
||||
|
||||
@@ -250,23 +176,18 @@ const form = ref<IStringKeyMap>({
|
||||
maxDownloadFileCount: 5,
|
||||
customRenameFormat: '{filename}',
|
||||
})
|
||||
|
||||
const settingsKeys = Object.keys(form.value)
|
||||
|
||||
const dbSize = ref(0)
|
||||
const dbSizeAvailableRate = ref('0')
|
||||
|
||||
const pasteFormatList = ['markdown', 'markdown-with-link', 'rawurl', 'html', 'bbcode', 'custom']
|
||||
|
||||
settingsKeys.forEach(key => {
|
||||
watch(
|
||||
() => form.value[key],
|
||||
newValue => {
|
||||
saveConfig({ [`settings.${key}`]: newValue })
|
||||
},
|
||||
{ flush: 'post' },
|
||||
)
|
||||
})
|
||||
const settingsKeys = Object.keys(form.value)
|
||||
const pasteFormatList = [
|
||||
{ label: t('pages.manage.setting.copyFormat.markdown'), value: 'markdown' },
|
||||
{ label: t('pages.manage.setting.copyFormat.markdown-with-link'), value: 'markdown-with-link' },
|
||||
{ label: t('pages.manage.setting.copyFormat.rawurl'), value: 'rawurl' },
|
||||
{ label: t('pages.manage.setting.copyFormat.html'), value: 'html' },
|
||||
{ label: t('pages.manage.setting.copyFormat.bbcode'), value: 'bbcode' },
|
||||
{ label: t('pages.manage.setting.copyFormat.custom'), value: 'custom' },
|
||||
]
|
||||
|
||||
const switchFieldsList = [
|
||||
'isAutoRefresh',
|
||||
@@ -282,35 +203,33 @@ const switchFieldsList = [
|
||||
]
|
||||
const switchFieldsNoTipsList = ['isShowThumbnail', 'isUsePreSignedUrl']
|
||||
const switchFieldsHasActiveTextList = [] as string[]
|
||||
|
||||
const switchFieldsConfigList = switchFieldsList.map(item => ({
|
||||
configName: item,
|
||||
segments: [
|
||||
{
|
||||
text: t(`pages.manage.setting.${item}Title` as any),
|
||||
style: 'color: var(--color-text-primary);',
|
||||
class: 'text-secondary text-sm font-semibold',
|
||||
},
|
||||
],
|
||||
tooltip: switchFieldsNoTipsList.includes(item) ? undefined : t(`pages.manage.setting.${item}Tips` as any),
|
||||
activeText: switchFieldsHasActiveTextList.includes(item) ? t(`pages.manage.setting.${item}On` as any) : undefined,
|
||||
inactiveText: switchFieldsHasActiveTextList.includes(item) ? t(`pages.manage.setting.${item}Off` as any) : undefined,
|
||||
}))
|
||||
|
||||
const switchFieldsSpecialList = [
|
||||
{
|
||||
configName: 'isDownloadFileKeepDirStructure',
|
||||
segments: [
|
||||
{
|
||||
text: t('pages.manage.setting.download'),
|
||||
style: 'color: var(--color-text-primary);',
|
||||
class: 'text-secondary text-sm font-semibold',
|
||||
},
|
||||
{
|
||||
text: t('pages.manage.setting.file'),
|
||||
style: 'color: orange;',
|
||||
class: 'text-warning text-sm font-semibold',
|
||||
},
|
||||
{
|
||||
text: t('pages.manage.setting.keepDirStructure'),
|
||||
style: 'color: var(--color-text-primary);',
|
||||
class: 'text-secondary text-sm font-semibold',
|
||||
},
|
||||
],
|
||||
tooltip: t('pages.manage.setting.keepDirStructureDesc'),
|
||||
@@ -320,21 +239,62 @@ const switchFieldsSpecialList = [
|
||||
segments: [
|
||||
{
|
||||
text: t('pages.manage.setting.download'),
|
||||
style: 'color: var(--color-text-primary);',
|
||||
class: 'text-secondary text-sm font-semibold',
|
||||
},
|
||||
{
|
||||
text: t('pages.manage.setting.folder'),
|
||||
style: 'color: orange;',
|
||||
class: 'text-warning text-sm font-semibold',
|
||||
},
|
||||
{
|
||||
text: t('pages.manage.setting.keepDirStructure'),
|
||||
style: 'color: var(--color-text-primary);',
|
||||
class: 'text-secondary text-sm font-semibold',
|
||||
},
|
||||
],
|
||||
tooltip: t('pages.manage.setting.keepDirStructureDesc'),
|
||||
},
|
||||
]
|
||||
|
||||
settingsKeys.forEach(key => {
|
||||
watch(
|
||||
() => form.value[key],
|
||||
newValue => {
|
||||
saveConfig({ [`settings.${key}`]: newValue })
|
||||
},
|
||||
{ flush: 'post' },
|
||||
)
|
||||
})
|
||||
|
||||
const advancedRenameList = computed(() => ({
|
||||
categoryTime: [
|
||||
{ label: t('pages.settings.upload.placeholder.year4'), value: '{Y}' },
|
||||
{ label: t('pages.settings.upload.placeholder.year2'), value: '{y}' },
|
||||
{ label: t('pages.settings.upload.placeholder.month'), value: '{m}' },
|
||||
{ label: t('pages.settings.upload.placeholder.date'), value: '{d}' },
|
||||
{ label: t('pages.settings.upload.placeholder.hour'), value: '{h}' },
|
||||
{ label: t('pages.settings.upload.placeholder.minute'), value: '{i}' },
|
||||
{ label: t('pages.settings.upload.placeholder.second'), value: '{s}' },
|
||||
{ label: t('pages.settings.upload.placeholder.millisecond'), value: '{ms}' },
|
||||
{ label: t('pages.settings.upload.placeholder.timestamp'), value: '{timestamp}' },
|
||||
],
|
||||
categoryHash: [
|
||||
{ label: t('pages.settings.upload.placeholder.md5'), value: '{md5}' },
|
||||
{ label: t('pages.settings.upload.placeholder.md5-16'), value: '{md5-16}' },
|
||||
{ label: t('pages.settings.upload.placeholder.uuid'), value: '{uuid}' },
|
||||
{ label: t('pages.settings.upload.placeholder.sha256'), value: '{sha256}' },
|
||||
{ label: t('pages.settings.upload.placeholder.sha256-n'), value: '{sha256-n}' },
|
||||
],
|
||||
categoryFile: [
|
||||
{ label: t('pages.settings.upload.placeholder.filename'), value: '{filename}' },
|
||||
{ label: t('pages.settings.upload.placeholder.randomString'), value: '{str-number}' },
|
||||
],
|
||||
}))
|
||||
|
||||
const advancedRenameTitleList = computed(() => ({
|
||||
categoryTime: t('pages.settings.upload.placeholder.categoryTime'),
|
||||
categoryHash: t('pages.settings.upload.placeholder.categoryHash'),
|
||||
categoryFile: t('pages.settings.upload.placeholder.categoryFile'),
|
||||
}))
|
||||
|
||||
async function initData() {
|
||||
const config = (await getConfig()) as IStringKeyMap
|
||||
settingsKeys.forEach(key => {
|
||||
@@ -350,11 +310,6 @@ async function handleDownloadDirClick() {
|
||||
}
|
||||
}
|
||||
|
||||
const handleCellClick = (row: any, column: any) => {
|
||||
navigator.clipboard.writeText(row[column.property])
|
||||
message.success(`${t('pages.manage.setting.copySuccess', { name: row[column.property] })}`)
|
||||
}
|
||||
|
||||
function handleConfirmClearDb() {
|
||||
confirm({
|
||||
title: t('pages.manage.setting.notice'),
|
||||
@@ -394,5 +349,3 @@ onBeforeMount(() => {
|
||||
getIndexDbSize()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped src="./css/ManageSetting.css"></style>
|
||||
|
||||
@@ -1,747 +0,0 @@
|
||||
/* Container */
|
||||
.login-container {
|
||||
display: flex;
|
||||
overflow-y: auto;
|
||||
margin: 0;
|
||||
padding: 1rem;
|
||||
width: 100%;
|
||||
min-height: calc(100% - 32px);
|
||||
flex-direction: column;
|
||||
gap: 1.25rem;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Card Base */
|
||||
.login-card {
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--color-border-secondary);
|
||||
border-radius: var(--radius-xl);
|
||||
background: var(settings-section);
|
||||
box-shadow: var(--shadow-sm);
|
||||
transition: var(--transition-medium);
|
||||
}
|
||||
|
||||
.login-card:hover {
|
||||
border-color: var(--color-border);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
/* Header Card */
|
||||
.header-card .card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid var(--color-border-secondary);
|
||||
padding: 1rem 1.5rem;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.header-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.header-content h1 {
|
||||
margin: 0;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-primary);
|
||||
letter-spacing: -0.025em;
|
||||
}
|
||||
|
||||
.header-content p {
|
||||
margin: 0;
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* Action Button Base */
|
||||
.action-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
color: var(--color-text-primary);
|
||||
background: var(--color-background-secondary);
|
||||
transition: var(--transition-fast);
|
||||
gap: 0.5rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.action-button:hover {
|
||||
border-color: var(--color-border);
|
||||
background: var(--color-background-secondary);
|
||||
box-shadow: var(--shadow-sm);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.action-button:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.action-button.primary {
|
||||
border-color: var(--color-accent);
|
||||
color: white;
|
||||
background: var(--color-accent);
|
||||
}
|
||||
|
||||
.action-button.primary:hover {
|
||||
border-color: var(--color-accent);
|
||||
background: var(--color-accent);
|
||||
}
|
||||
|
||||
.action-button.secondary {
|
||||
color: var(--color-text-secondary);
|
||||
background: var(--color-background-secondary);
|
||||
}
|
||||
|
||||
.action-button.danger {
|
||||
border-color: var(--color-danger);
|
||||
color: white;
|
||||
background: var(--color-danger);
|
||||
}
|
||||
|
||||
.action-button.danger:hover {
|
||||
border-color: var(--color-danger);
|
||||
background: var(--color-danger);
|
||||
}
|
||||
|
||||
/* Tabs */
|
||||
.tabs-card {
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
|
||||
.tabs-container {
|
||||
overflow: hidden;
|
||||
padding: 0.75rem 1rem;
|
||||
}
|
||||
|
||||
.tabs-nav-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.tabs-nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.tab-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: 1px solid var(--color-border-secondary);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 0.5rem 1rem;
|
||||
min-width: fit-content;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
color: var(--color-text-secondary);
|
||||
background: var(--color-background-secondary);
|
||||
transition: var(--transition-fast);
|
||||
gap: 0.5rem;
|
||||
cursor: pointer;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.tab-button:hover {
|
||||
border-color: var(--color-border);
|
||||
color: var(--color-text-primary);
|
||||
background: var(--color-background-tertiary);
|
||||
}
|
||||
|
||||
.tab-button.active {
|
||||
border-color: var(--color-border-secondary);
|
||||
color: white;
|
||||
background: var(--color-accent);
|
||||
}
|
||||
|
||||
.tab-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
/* Content */
|
||||
.content-card {
|
||||
flex: 1;
|
||||
background: var(--color-background-secondary);
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
/* Config List */
|
||||
.config-list-container {
|
||||
width: 100%;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
padding: 3rem 1rem;
|
||||
text-align: center;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
margin-bottom: 1rem;
|
||||
color: var(--color-text-tertiary);
|
||||
}
|
||||
|
||||
.empty-state h3 {
|
||||
margin: 0 0 0.5rem;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
margin: 0;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.config-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.config-item {
|
||||
border: 1px solid var(--color-border-secondary);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 1.25rem;
|
||||
background: var( --color-background-secondary);
|
||||
transition: var(--transition-medium);
|
||||
}
|
||||
|
||||
.config-item:hover {
|
||||
border-color: var(--color-border);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.config-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.config-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.config-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.config-alias {
|
||||
margin: 0 0 0.25rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.config-type {
|
||||
margin: 0;
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.config-details {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.details-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: none;
|
||||
border-radius: var(--radius-md);
|
||||
padding: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-text-secondary);
|
||||
background: var(--color-background-secondary);
|
||||
transition: var(--transition-fast);
|
||||
gap: 0.5rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.details-button:hover {
|
||||
color: var(--color-text-primary);
|
||||
background: var(--color-background-secondary);
|
||||
}
|
||||
|
||||
.details-button .rotated {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.config-table {
|
||||
overflow: hidden;
|
||||
margin-top: 0.75rem;
|
||||
border: 1px solid var(--color-border-secondary);
|
||||
border-radius: var(--radius-md);
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.table-row {
|
||||
display: flex;
|
||||
border-bottom: 1px solid var(--color-border-secondary);
|
||||
cursor: pointer;
|
||||
transition: var(--transition-fast);
|
||||
}
|
||||
|
||||
.table-row:hover {
|
||||
background: var(--color-surface-elevated);
|
||||
}
|
||||
|
||||
.table-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.table-key,
|
||||
.table-value {
|
||||
padding: 0.5rem 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.table-key {
|
||||
width: 120px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-secondary);
|
||||
background: var(--color-background-secondary);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.table-value {
|
||||
flex: 1;
|
||||
color: var(--color-text-primary);
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.config-actions {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
/* PicBed Config */
|
||||
.picbed-config-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.picbed-config {
|
||||
margin: 0 auto;
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
.info-section {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.info-card {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 1rem;
|
||||
border-radius: var(--radius-md);
|
||||
padding: 1rem;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.info-card.primary {
|
||||
border: 1px solid rgb(99 102 241 / 20%);
|
||||
color: var(--color-accent);
|
||||
background: rgb(99 102 241 / 10%);
|
||||
}
|
||||
|
||||
.info-card.reference {
|
||||
border: 1px solid rgb(107 114 128 / 20%);
|
||||
color: var(--color-text-secondary);
|
||||
background: rgb(107 114 128 / 10%);
|
||||
}
|
||||
|
||||
.info-card p {
|
||||
margin: 0;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.link-button {
|
||||
border: none;
|
||||
padding: 0;
|
||||
font-size: inherit;
|
||||
text-decoration: underline;
|
||||
color: var(--color-primary);
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.link-button:hover {
|
||||
color: var(--color-primary-hover);
|
||||
}
|
||||
|
||||
/* Form */
|
||||
.config-form {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-primary);
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.tooltip-button {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border: none;
|
||||
border-radius: var(--radius-round);
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
color: var(--color-text-primary);
|
||||
background: var(--color-background-secondary);
|
||||
transition: var(--transition-fast);
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
.tooltip-button:hover {
|
||||
color: var(--color-text-secondary);
|
||||
background: var(--color-border);
|
||||
}
|
||||
|
||||
.form-input,
|
||||
.form-select {
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 0.75rem;
|
||||
width: 100%;
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-text-primary);
|
||||
background: var(--color-background-tertiary);
|
||||
transition: var(--transition-fast);
|
||||
}
|
||||
|
||||
.form-input:focus,
|
||||
.form-select:focus {
|
||||
border-color: var(--color-primary);
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 3px rgb(99 102 241 / 10%);
|
||||
}
|
||||
|
||||
.form-input:disabled {
|
||||
color: var(--color-text-tertiary);
|
||||
background: var(--color-background-secondary);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Custom Switch */
|
||||
.custom-switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 48px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.custom-switch input {
|
||||
width: 0;
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.switch-slider {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: var(--radius-xl);
|
||||
background: linear-gradient(180deg, #d0d3d9 0%, #c0c4cc 100%);
|
||||
box-shadow: inset 0 1px 3px rgb(0 0 0 / 15%);
|
||||
transition: all var(--transition-medium);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.switch-slider::before {
|
||||
position: absolute;
|
||||
bottom: 3px;
|
||||
left: 3px;
|
||||
border-radius: var(--radius-round);
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
background: linear-gradient(180deg, #ffffff 0%, #f5f5f5 100%);
|
||||
box-shadow:
|
||||
0 2px 6px rgb(0 0 0 / 20%),
|
||||
0 1px 2px rgb(0 0 0 / 10%);
|
||||
transition: all var(--transition-medium);
|
||||
content: "";
|
||||
}
|
||||
|
||||
input:checked + .switch-slider {
|
||||
background: var(--color-accent);
|
||||
box-shadow:
|
||||
inset 0 1px 3px rgb(0 0 0 / 10%),
|
||||
0 2px 8px rgb(64 158 255 / 30%);
|
||||
}
|
||||
|
||||
input:checked + .switch-slider::before {
|
||||
transform: translateX(24px);
|
||||
}
|
||||
|
||||
/* Action Section */
|
||||
.action-section {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.import-section {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.main-actions {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
/* Dropdown */
|
||||
.dropdown-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.dropdown-trigger {
|
||||
justify-content: space-between;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
right: 0;
|
||||
left: 0;
|
||||
z-index: 50;
|
||||
overflow-y: auto;
|
||||
margin-top: 0.25rem;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
max-height: 200px;
|
||||
background: var(--color-surface-elevated);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
display: block;
|
||||
border: none;
|
||||
padding: 0.75rem;
|
||||
width: 100%;
|
||||
font-size: 0.875rem;
|
||||
text-align: left;
|
||||
color: var(--color-text-primary);
|
||||
background: var(--color-background-tertiary);
|
||||
transition: var(--transition-fast);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dropdown-item:hover {
|
||||
background: var(--color-accent);
|
||||
}
|
||||
|
||||
/* Config Table Section */
|
||||
.config-table-section {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.config-table-section h3 {
|
||||
margin: 0 0 1rem;
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.responsive-table {
|
||||
overflow-x: auto;
|
||||
border: 1px solid var(--color-border-secondary);
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
|
||||
.config-table th {
|
||||
border-bottom: 1px solid var(--color-border-secondary);
|
||||
padding: 0.75rem;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
color: var(--color-text-secondary);
|
||||
background: var(--color-background-secondary);
|
||||
}
|
||||
|
||||
.config-table td {
|
||||
border-bottom: 1px solid var(--color-border-secondary);
|
||||
padding: 0.75rem;
|
||||
text-align: center;
|
||||
transition: var(--transition-fast);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.config-table td:hover {
|
||||
background: var(--color-surface-elevated);
|
||||
}
|
||||
|
||||
.config-table tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (width <= 768px) {
|
||||
.login-container {
|
||||
padding: 0.75rem;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.header-card .card-header {
|
||||
align-items: flex-start;
|
||||
padding: 0.75rem 1rem;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tabs-container {
|
||||
padding: 0.5rem 0.75rem;
|
||||
}
|
||||
|
||||
.tabs-nav {
|
||||
gap: 0.375rem;
|
||||
}
|
||||
|
||||
.tab-button {
|
||||
padding: 0.5rem 0.75rem;
|
||||
max-width: 150px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.tab-icon {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
.config-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.action-section {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.main-actions {
|
||||
justify-content: stretch;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.main-actions .action-button {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width <= 480px) {
|
||||
.login-container {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.tabs-container {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.tab-button {
|
||||
padding: 0.375rem 0.5rem;
|
||||
max-width: 120px;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.tab-button span {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tab-button.active span {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
|
||||
/* Form validation styles */
|
||||
.form-group.has-error .form-label {
|
||||
color: var(--color-error);
|
||||
}
|
||||
|
||||
.form-input.error,
|
||||
.form-select.error {
|
||||
border-color: var(--color-error);
|
||||
background-color: color-mix(in srgb, var(--color-error), transparent 80%);
|
||||
}
|
||||
|
||||
.form-input.error:focus,
|
||||
.form-select.error:focus {
|
||||
border-color: var(--color-error);
|
||||
box-shadow: 0 0 0 2px color-mix(in srgb, var(--color-error), transparent 80%);
|
||||
}
|
||||
|
||||
.required-marker {
|
||||
margin-left: 0.25rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-error);
|
||||
}
|
||||
|
||||
.error-message {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 0.25rem;
|
||||
font-size: 0.75rem;
|
||||
color: var(--color-error);
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.error-message::before {
|
||||
content: "⚠";
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
@@ -1,472 +0,0 @@
|
||||
/* Container */
|
||||
.manage-setting-container {
|
||||
display: flex;
|
||||
overflow-y: scroll;
|
||||
margin: 0;
|
||||
padding: 1rem;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
box-sizing: border-box;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
/* Card Base */
|
||||
.setting-card {
|
||||
border: 1px solid var(--color-border-secondary);
|
||||
border-radius: var(--radius-xl);
|
||||
background: var(--color-background-secondary);
|
||||
box-shadow: var(--shadow-sm);
|
||||
transition: var(--transition-medium);
|
||||
}
|
||||
|
||||
.setting-card:hover {
|
||||
border-color: var(--color-border);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
/* Action Button Base */
|
||||
.action-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
color: var(--color-text-primary);
|
||||
background: var(--color-surface-elevated);
|
||||
transition: var(--transition-fast);
|
||||
gap: 0.5rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.action-button:hover {
|
||||
border-color: var(--color-border);
|
||||
background: var(--color-background-secondary);
|
||||
box-shadow: var(--shadow-sm);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.action-button:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.action-button.primary {
|
||||
border-color: var(--color-accent);
|
||||
color: white;
|
||||
background: var(--color-accent);
|
||||
}
|
||||
|
||||
.action-button.primary:hover {
|
||||
border-color: var(--color-accent);
|
||||
background: var(--color-accent);
|
||||
}
|
||||
|
||||
.action-button.secondary {
|
||||
color: var(--color-text-secondary);
|
||||
background: var(--color-background-secondary);
|
||||
}
|
||||
|
||||
.action-button.warning {
|
||||
border-color: var(--color-warning);
|
||||
color: white;
|
||||
background: var(--color-warning);
|
||||
}
|
||||
|
||||
.action-button.warning:hover {
|
||||
border-color: var(--color-warning);
|
||||
background: 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 + .setting-section {
|
||||
margin-top: 1rem;
|
||||
border-top: 1px solid var(--color-border-secondary);
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
/* Form Groups */
|
||||
|
||||
.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 {
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 0.75rem;
|
||||
width: 100%;
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-text-primary);
|
||||
background: var(--color-background-secondary);
|
||||
transition: var(--transition-fast);
|
||||
}
|
||||
|
||||
.form-input:focus {
|
||||
border-color: var(--color-accent);
|
||||
background: var(--color-background-tertiary);
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 2px rgb(0 122 255 / 20%);
|
||||
}
|
||||
|
||||
.form-input:disabled {
|
||||
color: var(--color-text-tertiary);
|
||||
background: var(--color-background-secondary);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.number-input {
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
/* Cache Info */
|
||||
.cache-info {
|
||||
display: flex;
|
||||
margin-top: 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
color: var(--color-text-secondary);
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.cache-size {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Section Headers */
|
||||
.section-header {
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
/* Pattern Table */
|
||||
.pattern-table-container {
|
||||
overflow-x: auto;
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
|
||||
.pattern-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.pattern-table th {
|
||||
border: 1px solid var(--color-border);
|
||||
padding: 0.5rem;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
color: var(--color-text-primary);
|
||||
background: var(--color-background-secondary);
|
||||
}
|
||||
|
||||
.pattern-table td {
|
||||
border: 1px solid var(--color-border);
|
||||
padding: 0.5rem;
|
||||
text-align: center;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.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;
|
||||
width: 100%;
|
||||
min-height: fit-content;
|
||||
}
|
||||
|
||||
.radio-option {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 0.625rem;
|
||||
background: var(--color-background-t);
|
||||
transition: var(--transition-fast);
|
||||
gap: 0.75rem;
|
||||
cursor: pointer;
|
||||
min-height: 2.5rem;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.radio-option:hover {
|
||||
border-color: var(--color-accent);
|
||||
background: var(--color-surface);
|
||||
}
|
||||
|
||||
.radio-input {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.radio-custom {
|
||||
position: relative;
|
||||
border: 2px solid var(--color-border);
|
||||
border-radius: var(--radius-round);
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
background: var(--color-background-tiertiary);
|
||||
transition: var(--transition-fast);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.radio-custom::after {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
border-radius: var(--radius-round);
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: var(--color-accent);
|
||||
transition: var(--transition-fast);
|
||||
content: '';
|
||||
transform: translate(-50%, -50%) scale(0);
|
||||
}
|
||||
|
||||
.radio-input:checked + .radio-custom {
|
||||
border-color: var(--color-accent);
|
||||
}
|
||||
|
||||
.radio-input:checked + .radio-custom::after {
|
||||
transform: translate(-50%, -50%) scale(1);
|
||||
}
|
||||
|
||||
/* 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;
|
||||
border: none;
|
||||
padding: 0.75rem 1rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
width: 10%;
|
||||
min-width: 80px;
|
||||
color: white;
|
||||
background: var(--color-accent);
|
||||
transition: var(--transition-fast);
|
||||
gap: 0.5rem;
|
||||
border-top-right-radius: var(--radius-md);
|
||||
border-bottom-right-radius: var(--radius-md);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.input-append-button:hover {
|
||||
background: var(--color-accent-hover);
|
||||
}
|
||||
|
||||
/* Dialog Styles */
|
||||
.dialog-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 2000;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: rgb(0 0 0 / 50%);
|
||||
}
|
||||
|
||||
.dialog-container {
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
width: 90%;
|
||||
max-width: 400px;
|
||||
background: var(--color-surface);
|
||||
box-shadow: var(--shadow-xl);
|
||||
}
|
||||
|
||||
.dialog-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1.5rem 1.5rem 0;
|
||||
}
|
||||
|
||||
.dialog-title {
|
||||
margin: 0;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.dialog-close {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 0.25rem;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
color: var(--color-text-secondary);
|
||||
background: none;
|
||||
transition: var(--transition-fast);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dialog-close:hover {
|
||||
color: var(--color-text-primary);
|
||||
background: var(--color-surface-elevated);
|
||||
}
|
||||
|
||||
.close-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.dialog-content {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.confirm-message {
|
||||
margin: 0;
|
||||
color: var(--color-text-secondary);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.dialog-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: 0 1.5rem 1.5rem;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.setting-section {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-direction: column;
|
||||
min-height: fit-content;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.radio-text {
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-text-primary);
|
||||
line-height: 1.4;
|
||||
overflow-wrap: break-word;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.setting-card.content-card {
|
||||
min-height: fit-content;
|
||||
contain: layout style;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (width <= 768px) {
|
||||
.manage-setting-container {
|
||||
padding: 0.75rem;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.header-card .card-header {
|
||||
align-items: flex-start;
|
||||
padding: 1rem;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
justify-content: flex-start;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
@@ -205,54 +205,3 @@ export function customStrReplace(str: string, pattern: string, replacement: stri
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export const customRenameFormatTable = [
|
||||
{
|
||||
placeholder: '{Y}',
|
||||
description: '年份,4位数',
|
||||
placeholderB: '{y}',
|
||||
descriptionB: '年份,2位数',
|
||||
},
|
||||
{
|
||||
placeholder: '{m}',
|
||||
description: '月份(01-12)',
|
||||
placeholderB: '{d}',
|
||||
descriptionB: '日期(01-31)',
|
||||
},
|
||||
{
|
||||
placeholder: '{h}',
|
||||
description: '小时(00-23)',
|
||||
placeholderB: '{i}',
|
||||
descriptionB: '分钟(00-59)',
|
||||
},
|
||||
{
|
||||
placeholder: '{s}',
|
||||
description: '秒(00-59)',
|
||||
placeholderB: '{ms}',
|
||||
descriptionB: '毫秒(000-999)',
|
||||
},
|
||||
{
|
||||
placeholder: '{timestamp}',
|
||||
description: '时间戳(毫秒)',
|
||||
placeholderB: '{uuid}',
|
||||
descriptionB: 'uuid字符串',
|
||||
},
|
||||
{
|
||||
placeholder: '{md5}',
|
||||
description: 'md5',
|
||||
placeholderB: '{md5-16}',
|
||||
descriptionB: 'md5前16位',
|
||||
},
|
||||
{
|
||||
placeholder: '{str-number}',
|
||||
description: 'number位随机字符串',
|
||||
placeholderB: '{filename}',
|
||||
descriptionB: '原文件名',
|
||||
},
|
||||
{
|
||||
placeholder: '{sha256}',
|
||||
description: 'SHA256 哈希',
|
||||
placeholderB: '{sha256-n}',
|
||||
descriptionB: 'SHA256 哈希(前n位)',
|
||||
},
|
||||
]
|
||||
|
||||
@@ -573,17 +573,17 @@ import $$db from '@/utils/db'
|
||||
import { IPasteStyle, IRPCActionType } from '@/utils/enum'
|
||||
import { picBedsCanbeDeleted } from '@/utils/static'
|
||||
|
||||
const { t } = useI18n()
|
||||
const message = useMessage()
|
||||
const { confirm } = useConfirm()
|
||||
const { picBedG } = usePicBed()
|
||||
|
||||
type IResult<T> = T & {
|
||||
id: string
|
||||
createdAt: number
|
||||
updatedAt: number
|
||||
}
|
||||
|
||||
const { t } = useI18n()
|
||||
const message = useMessage()
|
||||
const { confirm } = useConfirm()
|
||||
const { picBedG } = usePicBed()
|
||||
|
||||
const images = ref<ImgInfo[]>([])
|
||||
const virtualScrollerRef = useTemplateRef('virtualScrollerRef')
|
||||
const previewImageRef = useTemplateRef('previewImageRef')
|
||||
@@ -609,10 +609,7 @@ const debouncedSearchText = ref<string>('')
|
||||
const debouncedSearchTextURL = ref<string>('')
|
||||
const handleBarActive = useStorage<boolean>('galleryHandleBarActive', true)
|
||||
const pasteStyle = ref<string>('')
|
||||
const pasteStyleList = ['markdown', 'HTML', 'URL', 'UBB', 'Custom']
|
||||
const useShortUrl = ref<string>('')
|
||||
const shortURLList = [t('pages.gallery.shortUrl'), t('pages.gallery.longUrl')]
|
||||
|
||||
const fileSortNameReverse = ref(false)
|
||||
const fileSortTimeReverse = ref(false)
|
||||
const fileSortExtReverse = ref(false)
|
||||
@@ -630,18 +627,6 @@ const viewMode = useStorage<'list' | 'grid'>('galleryViewMode', 'grid')
|
||||
const componentKey = ref(0)
|
||||
const currentSortField = ref<'name' | 'time' | 'ext' | 'check'>('name')
|
||||
const userGridColumns = useStorage<number>('galleryGridColumns', 4)
|
||||
|
||||
const effectiveGridBreakpoints = computed(() => {
|
||||
return [{ min: 0, cols: userGridColumns.value }]
|
||||
})
|
||||
|
||||
const filteredPicBedG = computed(() => {
|
||||
if (galleryPicBedFilterSetting.value.length === 0) {
|
||||
return picBedG.value
|
||||
}
|
||||
return picBedG.value.filter(item => galleryPicBedFilterSetting.value.includes(item.type))
|
||||
})
|
||||
|
||||
const imageLoadStates = reactive<Record<string, boolean>>({})
|
||||
const imageErrorStates = reactive<Record<string, boolean>>({})
|
||||
|
||||
@@ -659,6 +644,8 @@ const imagePreviewState = reactive({
|
||||
swipeThreshold: 100,
|
||||
})
|
||||
|
||||
const pasteStyleList = ['markdown', 'HTML', 'URL', 'UBB', 'Custom']
|
||||
const shortURLList = [t('pages.gallery.shortUrl'), t('pages.gallery.longUrl')]
|
||||
const advancedRenameList = {
|
||||
categoryTime: [
|
||||
{ label: t('pages.settings.upload.placeholder.year4'), value: '{Y}' },
|
||||
@@ -684,6 +671,19 @@ const advancedRenameList = {
|
||||
{ label: t('pages.settings.upload.placeholder.randomString'), value: '{str-n}' },
|
||||
],
|
||||
}
|
||||
let searchDebounceTimer: ReturnType<typeof setTimeout> | null = null
|
||||
let searchURLDebounceTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
const effectiveGridBreakpoints = computed(() => {
|
||||
return [{ min: 0, cols: userGridColumns.value }]
|
||||
})
|
||||
|
||||
const filteredPicBedG = computed(() => {
|
||||
if (galleryPicBedFilterSetting.value.length === 0) {
|
||||
return picBedG.value
|
||||
}
|
||||
return picBedG.value.filter(item => galleryPicBedFilterSetting.value.includes(item.type))
|
||||
})
|
||||
|
||||
const matchedCount = computed(() => {
|
||||
const matches = filterList.value.filter((item: any) => {
|
||||
@@ -692,6 +692,10 @@ const matchedCount = computed(() => {
|
||||
return matches.length
|
||||
})
|
||||
|
||||
const filterList = computed(() => {
|
||||
return getGallery()
|
||||
})
|
||||
|
||||
const matchedUrls = computed(() => {
|
||||
const matches = filterList.value.filter((item: any) => {
|
||||
return customStrMatch(item.imgUrl, batchRenameMatch.value)
|
||||
@@ -699,33 +703,6 @@ const matchedUrls = computed(() => {
|
||||
return matches.map((item: any) => item.imgUrl || '').filter(Boolean)
|
||||
})
|
||||
|
||||
const dateRange = computed({
|
||||
get: () => {
|
||||
if (dateRangeStart.value && dateRangeEnd.value) {
|
||||
return [dateRangeStart.value, dateRangeEnd.value]
|
||||
}
|
||||
return ''
|
||||
},
|
||||
set: (value: string | string[]) => {
|
||||
if (Array.isArray(value)) {
|
||||
dateRangeStart.value = value[0] || ''
|
||||
dateRangeEnd.value = value[1] || ''
|
||||
} else {
|
||||
dateRangeStart.value = ''
|
||||
dateRangeEnd.value = ''
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
function copyPlaceholder(placeholder: string) {
|
||||
window.electron.clipboard.writeText(String(placeholder))
|
||||
message.success(t('pages.settings.upload.copySuccess', { content: placeholder }))
|
||||
}
|
||||
|
||||
const filterList = computed(() => {
|
||||
return getGallery()
|
||||
})
|
||||
|
||||
const isAllSelected = computed(() => {
|
||||
return Object.values(choosedList).length > 0 && filterList.value.every(item => choosedList[item.id!])
|
||||
})
|
||||
@@ -773,6 +750,73 @@ const imageTransformStyle = computed(() => {
|
||||
}
|
||||
})
|
||||
|
||||
const dateRange = computed({
|
||||
get: () => {
|
||||
if (dateRangeStart.value && dateRangeEnd.value) {
|
||||
return [dateRangeStart.value, dateRangeEnd.value]
|
||||
}
|
||||
return ''
|
||||
},
|
||||
set: (value: string | string[]) => {
|
||||
if (Array.isArray(value)) {
|
||||
dateRangeStart.value = value[0] || ''
|
||||
dateRangeEnd.value = value[1] || ''
|
||||
} else {
|
||||
dateRangeStart.value = ''
|
||||
dateRangeEnd.value = ''
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
watch(pasteStyle, newVal => {
|
||||
saveConfig(configPaths.settings.pasteStyle, newVal)
|
||||
})
|
||||
|
||||
watch(useShortUrl, newVal => {
|
||||
saveConfig(configPaths.settings.useShortUrl, newVal === t('pages.gallery.shortUrl'))
|
||||
})
|
||||
|
||||
watch(currentSortField, () => {
|
||||
sortFile(currentSortField.value)
|
||||
})
|
||||
|
||||
watch(filterList, () => {
|
||||
clearChoosedList()
|
||||
})
|
||||
|
||||
watch(userGridColumns, _ => {
|
||||
nextTick(() => {
|
||||
if (virtualScrollerRef.value) {
|
||||
virtualScrollerRef.value.refresh()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
watch(searchText, newVal => {
|
||||
if (searchDebounceTimer) clearTimeout(searchDebounceTimer)
|
||||
searchDebounceTimer = setTimeout(() => {
|
||||
debouncedSearchText.value = newVal
|
||||
nextTick(() => {
|
||||
virtualScrollerRef.value?.scrollToTop()
|
||||
})
|
||||
}, 300)
|
||||
})
|
||||
|
||||
watch(searchTextURL, newVal => {
|
||||
if (searchURLDebounceTimer) clearTimeout(searchURLDebounceTimer)
|
||||
searchURLDebounceTimer = setTimeout(() => {
|
||||
debouncedSearchTextURL.value = newVal
|
||||
nextTick(() => {
|
||||
virtualScrollerRef.value?.scrollToTop()
|
||||
})
|
||||
}, 300)
|
||||
})
|
||||
|
||||
function copyPlaceholder(placeholder: string) {
|
||||
window.electron.clipboard.writeText(String(placeholder))
|
||||
message.success(t('pages.settings.upload.copySuccess', { content: placeholder }))
|
||||
}
|
||||
|
||||
function onImageLoad(id: string) {
|
||||
imageLoadStates[id] = true
|
||||
}
|
||||
@@ -1012,15 +1056,6 @@ function getViewModeLabel() {
|
||||
return t(`pages.gallery.${viewMode.value}View`)
|
||||
}
|
||||
|
||||
onBeforeRouteUpdate((to, from) => {
|
||||
if (from.name === 'gallery') {
|
||||
clearChoosedList()
|
||||
}
|
||||
if (to.name === 'gallery') {
|
||||
updateGallery()
|
||||
}
|
||||
})
|
||||
|
||||
async function initConf() {
|
||||
const settingConfig = await getConfig<any>('settings')
|
||||
pasteStyle.value = settingConfig.pasteStyle || IPasteStyle.MARKDOWN
|
||||
@@ -1139,41 +1174,6 @@ async function updateGallery() {
|
||||
})
|
||||
}
|
||||
|
||||
watch(filterList, () => {
|
||||
clearChoosedList()
|
||||
})
|
||||
|
||||
watch(userGridColumns, _ => {
|
||||
nextTick(() => {
|
||||
if (virtualScrollerRef.value) {
|
||||
virtualScrollerRef.value.refresh()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
let searchDebounceTimer: ReturnType<typeof setTimeout> | null = null
|
||||
let searchURLDebounceTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
watch(searchText, newVal => {
|
||||
if (searchDebounceTimer) clearTimeout(searchDebounceTimer)
|
||||
searchDebounceTimer = setTimeout(() => {
|
||||
debouncedSearchText.value = newVal
|
||||
nextTick(() => {
|
||||
virtualScrollerRef.value?.scrollToTop()
|
||||
})
|
||||
}, 300)
|
||||
})
|
||||
|
||||
watch(searchTextURL, newVal => {
|
||||
if (searchURLDebounceTimer) clearTimeout(searchURLDebounceTimer)
|
||||
searchURLDebounceTimer = setTimeout(() => {
|
||||
debouncedSearchTextURL.value = newVal
|
||||
nextTick(() => {
|
||||
virtualScrollerRef.value?.scrollToTop()
|
||||
})
|
||||
}, 300)
|
||||
})
|
||||
|
||||
function handleChooseImage(val: boolean, index: number) {
|
||||
const currentItem = filterList.value[index]
|
||||
if (currentItem && currentItem.id) {
|
||||
@@ -1425,18 +1425,6 @@ function toggleHandleBar() {
|
||||
handleBarActive.value = !handleBarActive.value
|
||||
}
|
||||
|
||||
watch(pasteStyle, newVal => {
|
||||
saveConfig(configPaths.settings.pasteStyle, newVal)
|
||||
})
|
||||
|
||||
watch(useShortUrl, newVal => {
|
||||
saveConfig(configPaths.settings.useShortUrl, newVal === t('pages.gallery.shortUrl'))
|
||||
})
|
||||
|
||||
watch(currentSortField, () => {
|
||||
sortFile(currentSortField.value)
|
||||
})
|
||||
|
||||
function sortFile(type: 'name' | 'time' | 'ext' | 'check') {
|
||||
switch (type) {
|
||||
case 'name':
|
||||
@@ -1555,6 +1543,26 @@ function handleBatchRename() {
|
||||
}
|
||||
}
|
||||
|
||||
onBeforeRouteUpdate((to, from) => {
|
||||
if (from.name === 'gallery') {
|
||||
clearChoosedList()
|
||||
}
|
||||
if (to.name === 'gallery') {
|
||||
updateGallery()
|
||||
}
|
||||
})
|
||||
|
||||
onActivated(async () => {
|
||||
await initConf()
|
||||
nextTick(() => {
|
||||
if (virtualScrollerRef.value && typeof virtualScrollerRef.value.refresh === 'function') {
|
||||
virtualScrollerRef.value.refresh()
|
||||
} else {
|
||||
componentKey.value++
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
onBeforeMount(async () => {
|
||||
window.electron.ipcRendererOn('updateGallery', updateGalleryHandler)
|
||||
updateGallery()
|
||||
@@ -1574,17 +1582,6 @@ onBeforeUnmount(async () => {
|
||||
if (searchURLDebounceTimer) clearTimeout(searchURLDebounceTimer)
|
||||
isAlwaysForceReload.value = (await getConfig(configPaths.settings.isAlwaysForceReload)) || false
|
||||
})
|
||||
|
||||
onActivated(async () => {
|
||||
await initConf()
|
||||
nextTick(() => {
|
||||
if (virtualScrollerRef.value && typeof virtualScrollerRef.value.refresh === 'function') {
|
||||
virtualScrollerRef.value.refresh()
|
||||
} else {
|
||||
componentKey.value++
|
||||
}
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
|
||||
@@ -214,11 +214,11 @@ import {
|
||||
import { useStorage } from '@vueuse/core'
|
||||
import { pick } from 'lodash-es'
|
||||
import {
|
||||
BriefcaseBusiness,
|
||||
CheckIcon,
|
||||
ChevronDownIcon,
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
Cloud,
|
||||
CopyIcon,
|
||||
DatabaseIcon,
|
||||
ImagesIcon,
|
||||
@@ -259,6 +259,20 @@ const guideRef = ref<InstanceType<typeof FirstTimeGuide> | null>(null)
|
||||
|
||||
let removeIpcListener: () => void = () => {}
|
||||
|
||||
const visiblePicBeds = computed(() => picBedG.value.filter(item => item.visible))
|
||||
|
||||
const navigationItems = computed(() => [
|
||||
{ name: t('navigation.upload'), path: '/main-page/upload', icon: UploadIcon },
|
||||
{ name: t('navigation.manage'), path: '/main-page/manage-login-page', icon: Cloud },
|
||||
{ name: t('navigation.gallery'), path: '/main-page/gallery', icon: ImagesIcon },
|
||||
{ name: t('navigation.settings'), path: '/main-page/settings', icon: Settings },
|
||||
{
|
||||
name: t('navigation.plugins'),
|
||||
path: '/main-page/plugins',
|
||||
icon: PlugIcon,
|
||||
},
|
||||
])
|
||||
|
||||
watch(
|
||||
() => choosedPicBedForQRCode,
|
||||
val => {
|
||||
@@ -273,8 +287,6 @@ watch(
|
||||
{ deep: true },
|
||||
)
|
||||
|
||||
const visiblePicBeds = computed(() => picBedG.value.filter(item => item.visible))
|
||||
|
||||
const qrCodeHandler = () => {
|
||||
qrcodeVisible.value = true
|
||||
}
|
||||
@@ -314,18 +326,6 @@ function isPicBedPathActive(type: string): boolean {
|
||||
return route.name === routerConfig.UPLOADER_CONFIG_PAGE && route.params.type === type
|
||||
}
|
||||
|
||||
const navigationItems = computed(() => [
|
||||
{ name: t('navigation.upload'), path: '/main-page/upload', icon: UploadIcon },
|
||||
{ name: t('navigation.manage'), path: '/main-page/manage-login-page', icon: BriefcaseBusiness },
|
||||
{ name: t('navigation.gallery'), path: '/main-page/gallery', icon: ImagesIcon },
|
||||
{ name: t('navigation.settings'), path: '/main-page/settings', icon: Settings },
|
||||
{
|
||||
name: t('navigation.plugins'),
|
||||
path: '/main-page/plugins',
|
||||
icon: PlugIcon,
|
||||
},
|
||||
])
|
||||
|
||||
function openGithubPage() {
|
||||
window.electron.sendRPC(IRPCActionType.OPEN_URL, 'https://github.com/Kuingsmile/PicList')
|
||||
}
|
||||
|
||||
@@ -144,15 +144,6 @@ const currentPicbedType = $route.params.type as string
|
||||
|
||||
type.value = $route.params.type as string
|
||||
|
||||
onBeforeMount(async () => {
|
||||
try {
|
||||
await getPicBeds()
|
||||
await getPicBedConfigList()
|
||||
} catch (error) {
|
||||
console.error('Initialization error:', error)
|
||||
}
|
||||
})
|
||||
|
||||
function toggleDropdown() {
|
||||
dropdownVisible.value = !dropdownVisible.value
|
||||
}
|
||||
@@ -231,7 +222,6 @@ const handleReset = async () => {
|
||||
try {
|
||||
await window.electron.triggerRPC<void>(IRPCActionType.UPLOADER_RESET_CONFIG, type.value, $route.params.configId)
|
||||
message.success(t('pages.picBedConfigs.resetSuccess'))
|
||||
$router.back()
|
||||
} catch (error) {
|
||||
console.error('Failed to reset configuration:', error)
|
||||
message.error(t('pages.picBedConfigs.resetFailed'))
|
||||
@@ -278,6 +268,15 @@ async function handleCopyApi() {
|
||||
message.error(t('pages.picBedConfigs.copyAPIFailed'))
|
||||
}
|
||||
}
|
||||
|
||||
onBeforeMount(async () => {
|
||||
try {
|
||||
await getPicBeds()
|
||||
await getPicBedConfigList()
|
||||
} catch (error) {
|
||||
console.error('Initialization error:', error)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
|
||||
@@ -461,11 +461,6 @@ const browseSearchText = ref('')
|
||||
const browsePlugins = ref<IPicGoPlugin[]>([])
|
||||
const loadingBrowse = ref(false)
|
||||
|
||||
function setSrc(e: Event) {
|
||||
const target = e.target as HTMLImageElement
|
||||
target.src = import.meta.env.BASE_URL + 'roundLogo.png'
|
||||
}
|
||||
|
||||
const npmSearchText = computed(() => {
|
||||
return searchText.value.match('picgo-plugin-')
|
||||
? searchText.value
|
||||
@@ -508,6 +503,11 @@ watch(showBrowseDialog, (val: boolean) => {
|
||||
}
|
||||
})
|
||||
|
||||
function setSrc(e: Event) {
|
||||
const target = e.target as HTMLImageElement
|
||||
target.src = import.meta.env.BASE_URL + 'roundLogo.png'
|
||||
}
|
||||
|
||||
async function getLatestVersionOfPlugIn(pluginName: string) {
|
||||
try {
|
||||
const res = await fetch(`https://registry.npmjs.com/${pluginName}`)
|
||||
@@ -518,11 +518,11 @@ async function getLatestVersionOfPlugIn(pluginName: string) {
|
||||
}
|
||||
}
|
||||
|
||||
const hideLoadingHandler = () => {
|
||||
function hideLoadingHandler() {
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
const picgoHandlePluginDoneHandler = (fullName: string) => {
|
||||
function picgoHandlePluginDoneHandler(fullName: string) {
|
||||
pluginList.value.forEach(item => {
|
||||
if (item.fullName === fullName || item.name === fullName) {
|
||||
item.ing = false
|
||||
@@ -531,7 +531,7 @@ const picgoHandlePluginDoneHandler = (fullName: string) => {
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
const pluginListHandler = (list: IPicGoPlugin[]) => {
|
||||
function pluginListHandler(list: IPicGoPlugin[]) {
|
||||
pluginList.value = list
|
||||
pluginNameList.value = list.map(item => item.fullName)
|
||||
for (const item of pluginList.value) {
|
||||
@@ -540,7 +540,7 @@ const pluginListHandler = (list: IPicGoPlugin[]) => {
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
const installPluginHandler = ({ success, body }: { success: boolean; body: string }) => {
|
||||
function installPluginHandler({ success, body }: { success: boolean; body: string }) {
|
||||
loading.value = false
|
||||
pluginList.value.forEach(item => {
|
||||
if (item.fullName === body) {
|
||||
@@ -557,7 +557,7 @@ const installPluginHandler = ({ success, body }: { success: boolean; body: strin
|
||||
})
|
||||
}
|
||||
|
||||
const updateSuccessHandler = (plugin: string) => {
|
||||
function updateSuccessHandler(plugin: string) {
|
||||
loading.value = false
|
||||
pluginList.value.forEach(item => {
|
||||
if (item.fullName === plugin) {
|
||||
@@ -570,7 +570,7 @@ const updateSuccessHandler = (plugin: string) => {
|
||||
getPluginList()
|
||||
}
|
||||
|
||||
const uninstallSuccessHandler = (plugin: string) => {
|
||||
function uninstallSuccessHandler(plugin: string) {
|
||||
loading.value = false
|
||||
pluginList.value = pluginList.value.filter(item => {
|
||||
if (item.fullName === plugin) {
|
||||
@@ -588,18 +588,18 @@ const uninstallSuccessHandler = (plugin: string) => {
|
||||
pluginNameList.value = pluginNameList.value.filter(item => item !== plugin)
|
||||
}
|
||||
|
||||
const picgoConfigPluginHandler = (
|
||||
function picgoConfigPluginHandler(
|
||||
_currentType: 'plugin' | 'transformer' | 'uploader',
|
||||
_configName: string,
|
||||
_config: any,
|
||||
) => {
|
||||
) {
|
||||
currentType.value = _currentType
|
||||
configName.value = _configName
|
||||
config.value = _config
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const picgoHandlePluginIngHandler = (fullName: string) => {
|
||||
function picgoHandlePluginIngHandler(fullName: string) {
|
||||
pluginList.value.forEach(item => {
|
||||
if (item.fullName === fullName || item.name === fullName) {
|
||||
item.ing = true
|
||||
|
||||
@@ -65,7 +65,7 @@ const form = reactive({
|
||||
originName: '',
|
||||
})
|
||||
|
||||
const handleFileName = (newName: string, _originName: string, _id: string) => {
|
||||
function handleFileName(newName: string, _originName: string, _id: string) {
|
||||
form.fileName = newName
|
||||
form.originName = _originName
|
||||
id.value = _id
|
||||
|
||||
@@ -138,16 +138,6 @@ const command = ref('')
|
||||
const shortKey = ref('')
|
||||
const currentIndex = ref(0)
|
||||
|
||||
onBeforeMount(async () => {
|
||||
const shortKeyConfig = (await getConfig<IShortKeyConfigs>(configPaths.settings.shortKey._path))!
|
||||
list.value = Object.keys(shortKeyConfig).map(item => {
|
||||
return {
|
||||
...shortKeyConfig[item],
|
||||
from: calcOrigin(item),
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
watch(keyBindingVisible, (val: boolean) => {
|
||||
window.electron.sendRPC(IRPCActionType.SHORTKEY_TOGGLE_SHORTKEY_MODIFIED_MODE, val)
|
||||
})
|
||||
@@ -194,6 +184,16 @@ async function confirmKeyBinding() {
|
||||
}
|
||||
}
|
||||
|
||||
onBeforeMount(async () => {
|
||||
const shortKeyConfig = (await getConfig<IShortKeyConfigs>(configPaths.settings.shortKey._path))!
|
||||
list.value = Object.keys(shortKeyConfig).map(item => {
|
||||
return {
|
||||
...shortKeyConfig[item],
|
||||
from: calcOrigin(item),
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.electron.sendRPC(IRPCActionType.SHORTKEY_TOGGLE_SHORTKEY_MODIFIED_MODE, false)
|
||||
})
|
||||
|
||||
@@ -58,6 +58,7 @@ import { IRPCActionType } from '@/utils/enum'
|
||||
const isShowprogress = ref(false)
|
||||
const progress = ref(0)
|
||||
const isAlwaysOnTop = ref(false)
|
||||
|
||||
const defaultLogo = computed(() => `${import.meta.env.BASE_URL}roundLogo.png`)
|
||||
|
||||
function setAlwaysOnTop() {
|
||||
@@ -69,7 +70,7 @@ const minimizeWindow = () => window.electron.sendRPC(IRPCActionType.MINIMIZE_WIN
|
||||
const openMiniWindow = () => window.electron.sendRPC(IRPCActionType.OPEN_MINI_WINDOW)
|
||||
const closeWindow = () => window.electron.sendRPC(IRPCActionType.CLOSE_WINDOW)
|
||||
|
||||
const uploadProcessHandler = (data: { progress: number }) => {
|
||||
function uploadProcessHandler(data: { progress: number }) {
|
||||
isShowprogress.value = data.progress !== 100 && data.progress !== 0
|
||||
progress.value = data.progress
|
||||
}
|
||||
|
||||
@@ -122,7 +122,6 @@ import { IRPCActionType, IToolboxItemCheckStatus, IToolboxItemType } from '@/uti
|
||||
const { t } = useI18n()
|
||||
const { confirm } = useConfirm()
|
||||
const activeTypes = ref<string[]>([])
|
||||
const defaultLogo = computed(() => `${import.meta.env.BASE_URL}roundLogo.png`)
|
||||
const fixList = reactive<IToolboxMap>({
|
||||
[IToolboxItemType.IS_CONFIG_FILE_BROKEN]: {
|
||||
title: t('pages.toolbox.checkConfigFileBroken'),
|
||||
@@ -151,6 +150,8 @@ const fixList = reactive<IToolboxMap>({
|
||||
},
|
||||
})
|
||||
|
||||
const defaultLogo = computed(() => `${import.meta.env.BASE_URL}roundLogo.png`)
|
||||
|
||||
const progress = computed(() => {
|
||||
const total = Object.keys(fixList).length
|
||||
const done = Object.keys(fixList).filter(key => {
|
||||
@@ -181,7 +182,7 @@ const canFixLength = computed(() => {
|
||||
}).length
|
||||
})
|
||||
|
||||
const toggleItem = (key: string) => {
|
||||
function toggleItem(key: string) {
|
||||
const index = activeTypes.value.indexOf(key)
|
||||
if (index > -1) {
|
||||
activeTypes.value.splice(index, 1)
|
||||
@@ -190,7 +191,7 @@ const toggleItem = (key: string) => {
|
||||
}
|
||||
}
|
||||
|
||||
const toolboxCheckResHandler = ({ type, msg = '', status, value = '' }: IToolboxCheckRes) => {
|
||||
function toolboxCheckResHandler({ type, msg = '', status, value = '' }: IToolboxCheckRes) {
|
||||
fixList[type].status = status
|
||||
fixList[type].msg = msg
|
||||
fixList[type].value = value
|
||||
@@ -199,9 +200,7 @@ const toolboxCheckResHandler = ({ type, msg = '', status, value = '' }: IToolbox
|
||||
}
|
||||
}
|
||||
|
||||
window.electron.ipcRendererOn(IRPCActionType.TOOLBOX_CHECK_RES, toolboxCheckResHandler)
|
||||
|
||||
const handleCheck = () => {
|
||||
function handleCheck() {
|
||||
activeTypes.value = []
|
||||
Object.keys(fixList).forEach(key => {
|
||||
fixList[key].status = IToolboxItemCheckStatus.LOADING
|
||||
@@ -211,7 +210,7 @@ const handleCheck = () => {
|
||||
window.electron.sendRPC(IRPCActionType.TOOLBOX_CHECK)
|
||||
}
|
||||
|
||||
const handleFix = async () => {
|
||||
async function handleFix() {
|
||||
const fixRes = await Promise.all(
|
||||
Object.keys(fixList)
|
||||
.filter(key => {
|
||||
@@ -246,6 +245,8 @@ const handleFix = async () => {
|
||||
})
|
||||
}
|
||||
|
||||
window.electron.ipcRendererOn(IRPCActionType.TOOLBOX_CHECK_RES, toolboxCheckResHandler)
|
||||
|
||||
onUnmounted(() => {
|
||||
window.electron.ipcRendererRemoveAllListeners(IRPCActionType.TOOLBOX_CHECK_RES)
|
||||
})
|
||||
|
||||
@@ -109,19 +109,19 @@ import { getConfig } from '@/utils/dataSender'
|
||||
import $$db from '@/utils/db'
|
||||
import { IPasteStyle, IRPCActionType, IWindowList } from '@/utils/enum'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
type IResult<T> = T & {
|
||||
id: string
|
||||
createdAt: number
|
||||
updatedAt: number
|
||||
}
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const files = ref<IResult<ImgInfo>[]>([])
|
||||
const notification = reactive({
|
||||
title: t('pages.tray.copySuccess'),
|
||||
body: '',
|
||||
})
|
||||
|
||||
const clipboardFiles = ref<ImgInfo[]>([])
|
||||
const uploadFlag = ref(false)
|
||||
|
||||
@@ -133,7 +133,7 @@ async function getData() {
|
||||
files.value = (await $$db.get<ImgInfo>({ orderBy: 'desc', limit: 10 }))!.data
|
||||
}
|
||||
|
||||
const formatCustomLink = (customLink: string, item: ImgInfo) => {
|
||||
function formatCustomLink(customLink: string, item: ImgInfo) {
|
||||
const fileName = item.fileName!.replace(new RegExp(`\\${item.extname}$`), '')
|
||||
const url = item.url || item.imgUrl
|
||||
const extName = item.extname
|
||||
@@ -222,7 +222,7 @@ function onImageError(event: Event) {
|
||||
img.src = './errorLoading.png'
|
||||
}
|
||||
|
||||
const dragFilesHandler = async (_files: string[]) => {
|
||||
async function dragFilesHandler(_files: string[]) {
|
||||
for (const file of _files) {
|
||||
await $$db.insert(file)
|
||||
}
|
||||
@@ -232,11 +232,11 @@ const dragFilesHandler = async (_files: string[]) => {
|
||||
}))!.data
|
||||
}
|
||||
|
||||
const clipboardFilesHandler = (files: ImgInfo[]) => {
|
||||
function clipboardFilesHandler(files: ImgInfo[]) {
|
||||
clipboardFiles.value = files
|
||||
}
|
||||
|
||||
const uploadFilesHandler = async () => {
|
||||
async function uploadFilesHandler() {
|
||||
files.value = (await $$db.get<ImgInfo>({
|
||||
orderBy: 'desc',
|
||||
limit: 5,
|
||||
@@ -244,7 +244,7 @@ const uploadFilesHandler = async () => {
|
||||
uploadFlag.value = false
|
||||
}
|
||||
|
||||
const updateFilesHandler = () => {
|
||||
function updateFilesHandler() {
|
||||
getData()
|
||||
}
|
||||
|
||||
|
||||
@@ -118,22 +118,22 @@ const updateInfo = ref<UpdateInfo>({
|
||||
const dontShowAgain = ref(false)
|
||||
const downloadProgress = ref<number | null>(null)
|
||||
|
||||
const handleUpdateInfo = (info: UpdateInfo) => {
|
||||
function handleUpdateInfo(info: UpdateInfo) {
|
||||
updateInfo.value = info
|
||||
if (info.type !== 'downloading') {
|
||||
downloadProgress.value = null
|
||||
}
|
||||
}
|
||||
|
||||
const handleUpdateProgress = (progress: { progress: number }) => {
|
||||
function handleUpdateProgress(progress: { progress: number }) {
|
||||
downloadProgress.value = progress.progress
|
||||
}
|
||||
|
||||
const renderMarkdown = (content: string) => {
|
||||
function renderMarkdown(content: string) {
|
||||
return marked(content, { breaks: true, gfm: true })
|
||||
}
|
||||
|
||||
const downloadUpdate = () => {
|
||||
function downloadUpdate() {
|
||||
updateInfo.value.type = 'downloading'
|
||||
downloadProgress.value = 0
|
||||
window.electron.sendRPC(IRPCActionType.DOWNLOAD_UPDATE)
|
||||
@@ -142,7 +142,7 @@ const downloadUpdate = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const goToDownloadPage = () => {
|
||||
function goToDownloadPage() {
|
||||
window.electron.sendRPC(IRPCActionType.GO_TO_DOWNLOAD_PAGE)
|
||||
if (dontShowAgain.value) {
|
||||
window.electron.sendRPC(IRPCActionType.SET_SHOW_UPDATE_TIP, false)
|
||||
@@ -150,11 +150,11 @@ const goToDownloadPage = () => {
|
||||
closeWindow()
|
||||
}
|
||||
|
||||
const installUpdate = () => {
|
||||
function installUpdate() {
|
||||
window.electron.sendRPC(IRPCActionType.INSTALL_UPDATE)
|
||||
}
|
||||
|
||||
const closeWindow = () => {
|
||||
function closeWindow() {
|
||||
if (dontShowAgain.value && updateInfo.value.type === 'update-available') {
|
||||
window.electron.sendRPC(IRPCActionType.SET_SHOW_UPDATE_TIP, false)
|
||||
}
|
||||
@@ -162,6 +162,7 @@ const closeWindow = () => {
|
||||
}
|
||||
|
||||
let unbindThemeListener: (() => void) | null = null
|
||||
|
||||
onMounted(() => {
|
||||
window.electron.ipcRendererOn(SHOW_UPDATE_INFO, handleUpdateInfo)
|
||||
window.electron.ipcRendererOn(UPDATE_PROGRESS, handleUpdateProgress)
|
||||
|
||||
@@ -749,26 +749,13 @@ const pasteStyle = ref(IPasteStyle.MARKDOWN)
|
||||
const PicBedId = ref('')
|
||||
const fileInput = useTemplateRef('fileInput')
|
||||
const uploadInterval = ref(1000)
|
||||
|
||||
const favoritePicbeds = useStorage<IFavoritePicbedItem[]>('favorite-picbeds', [])
|
||||
const MAX_FAVORITE_PICBEDS = 6
|
||||
const longPressedBadge = ref<string | null>(null)
|
||||
let longPressTimer: NodeJS.Timeout | null = null
|
||||
const LONG_PRESS_DURATION = 500
|
||||
const isCurrentPicBedInFavorites = computed(() => {
|
||||
const result = favoritePicbeds.value.some(item => item.id === defaultIdG.value)
|
||||
return result
|
||||
})
|
||||
|
||||
// New task queue settings
|
||||
const showTaskSettings = useStorage('upload-task-queue-show-settings', true)
|
||||
const taskSearchQuery = ref('')
|
||||
const taskFilter = ref<'all' | 'pending' | 'completed' | 'failed'>('all')
|
||||
const autoStart = ref(false)
|
||||
const pauseOnError = ref(false)
|
||||
const maxRetryCount = ref(3)
|
||||
|
||||
// Task queue status
|
||||
const favoritePicbeds = useStorage<IFavoritePicbedItem[]>('favorite-picbeds', [])
|
||||
const taskQueueStatus = reactive<IUploadTaskQueueStatus>({
|
||||
tasks: [],
|
||||
config: {
|
||||
@@ -792,8 +779,24 @@ const taskQueueStatus = reactive<IUploadTaskQueueStatus>({
|
||||
estimatedTimeMs: 0,
|
||||
},
|
||||
})
|
||||
const longPressedBadge = ref<string | null>(null)
|
||||
const pasteFormatList = ref<Record<string, string>>({
|
||||
[IPasteStyle.MARKDOWN]: '',
|
||||
[IPasteStyle.HTML]: '<img src="url"/>',
|
||||
[IPasteStyle.URL]: 'http://test.com/test.png',
|
||||
[IPasteStyle.UBB]: '[img]url[/img]',
|
||||
[IPasteStyle.CUSTOM]: '',
|
||||
})
|
||||
|
||||
const MAX_FAVORITE_PICBEDS = 6
|
||||
let longPressTimer: NodeJS.Timeout | null = null
|
||||
const LONG_PRESS_DURATION = 500
|
||||
|
||||
const isCurrentPicBedInFavorites = computed(() => {
|
||||
const result = favoritePicbeds.value.some(item => item.id === defaultIdG.value)
|
||||
return result
|
||||
})
|
||||
|
||||
// Computed properties
|
||||
const filteredTasks = computed(() => {
|
||||
let tasks = taskQueueStatus.tasks
|
||||
|
||||
@@ -824,18 +827,13 @@ const picBedName = computed(() => {
|
||||
return target ? target.name : defaultPicBedG.value
|
||||
})
|
||||
|
||||
const pasteFormatList = ref<Record<string, string>>({
|
||||
[IPasteStyle.MARKDOWN]: '',
|
||||
[IPasteStyle.HTML]: '<img src="url"/>',
|
||||
[IPasteStyle.URL]: 'http://test.com/test.png',
|
||||
[IPasteStyle.UBB]: '[img]url[/img]',
|
||||
[IPasteStyle.CUSTOM]: '',
|
||||
})
|
||||
|
||||
function syncPicBedHandler(): void {
|
||||
updatePicBeds()
|
||||
}
|
||||
|
||||
watch(progress, onProgressChange)
|
||||
watch(favoritePicbeds, valideFavoritePicbeds, { immediate: true })
|
||||
|
||||
let removeUploadProgressListenerCallback: () => void = () => {}
|
||||
let removeSyncPicBedListenerCallback: () => void = () => {}
|
||||
|
||||
@@ -849,18 +847,16 @@ function uploadProgressHandler(p: number): void {
|
||||
}
|
||||
}
|
||||
|
||||
const handleImageProcess = () => {
|
||||
function handleImageProcess() {
|
||||
PicBedId.value = ''
|
||||
imageProcessDialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleImageProcessSingle = () => {
|
||||
function handleImageProcessSingle() {
|
||||
PicBedId.value = defaultIdG.value
|
||||
imageProcessDialogVisible.value = true
|
||||
}
|
||||
|
||||
watch(progress, onProgressChange)
|
||||
|
||||
function onProgressChange(val: number) {
|
||||
if (val === 100) {
|
||||
setTimeout(() => {
|
||||
@@ -979,8 +975,6 @@ async function valideFavoritePicbeds() {
|
||||
}
|
||||
}
|
||||
|
||||
watch(favoritePicbeds, valideFavoritePicbeds, { immediate: true })
|
||||
|
||||
function addCurrentPicbedToFavorites() {
|
||||
favoritePicbeds.value.push({
|
||||
id: defaultIdG.value,
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
<div class="no-scrollbar h-full w-full overflow-auto rounded-sm">
|
||||
<div class="grid w-full grid-cols-[repeat(auto-fill,minmax(300px,1fr))] gap-5 border-none p-1 max-md:gap-4">
|
||||
<!-- Config Items -->
|
||||
|
||||
<div
|
||||
v-for="(item, index) in curConfigList"
|
||||
:key="item._id"
|
||||
@@ -162,7 +163,6 @@ const router = useRouter()
|
||||
const route = useRoute()
|
||||
const { defaultPicBedG, picBedG, updatePicBeds } = usePicBed()
|
||||
const favoritePicbeds = useStorage<IFavoritePicbedItem[]>('favorite-picbeds', [])
|
||||
|
||||
const type = ref('')
|
||||
const curConfigList = ref<IStringKeyMap[]>([])
|
||||
const defaultConfigId = ref('')
|
||||
@@ -203,19 +203,6 @@ async function selectItem(id: string) {
|
||||
defaultConfigId.value = id
|
||||
}
|
||||
|
||||
onBeforeRouteUpdate((to, _, next) => {
|
||||
if (to.params.type && to.name === UPLOADER_CONFIG_PAGE) {
|
||||
type.value = to.params.type as string
|
||||
getCurrentConfigList()
|
||||
}
|
||||
next()
|
||||
})
|
||||
|
||||
onBeforeMount(() => {
|
||||
type.value = route.params.type as string
|
||||
getCurrentConfigList()
|
||||
})
|
||||
|
||||
async function getCurrentConfigList() {
|
||||
const configList = await window.electron.triggerRPC<IUploaderConfigItem>(
|
||||
IRPCActionType.PICBED_GET_CONFIG_LIST,
|
||||
@@ -328,7 +315,21 @@ function setDefaultPicBed(type: string) {
|
||||
updatePicBeds()
|
||||
message.success(t('pages.uploaderConfig.setSuccess'))
|
||||
}
|
||||
|
||||
onBeforeRouteUpdate((to, _, next) => {
|
||||
if (to.params.type && to.name === UPLOADER_CONFIG_PAGE) {
|
||||
type.value = to.params.type as string
|
||||
getCurrentConfigList()
|
||||
}
|
||||
next()
|
||||
})
|
||||
|
||||
onBeforeMount(() => {
|
||||
type.value = route.params.type as string
|
||||
getCurrentConfigList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'UploaderConfigPage',
|
||||
|
||||
@@ -5,6 +5,7 @@ export const MANAGE_EMPTY_PAGE = 'ManageEmptyPage'
|
||||
export const MANAGE_LOGIN_PAGE = 'ManageLoginPage'
|
||||
export const MANAGE_MAIN_PAGE = 'ManageMainPage'
|
||||
export const MANAGE_SETTING_PAGE = 'ManageSettingPage'
|
||||
export const MANAGE_SETTING_PAGE_DIRECT = 'ManageSettingPageDirect'
|
||||
export const MAIN_PAGE = 'MainPage'
|
||||
export const MINI_PAGE = 'MiniPage'
|
||||
export const PICBEDS_PAGE = 'PicbedsPage'
|
||||
@@ -17,3 +18,4 @@ export const TRAY_PAGE = 'TrayPage'
|
||||
export const UPDATE_PAGE = 'UpdatePage'
|
||||
export const UPLOAD_PAGE = 'UploadPage'
|
||||
export const UPLOADER_CONFIG_PAGE = 'UploaderConfigPage'
|
||||
export const MANAGE_EDIT_PAGE = 'ManageEditPage'
|
||||
|
||||
@@ -48,6 +48,12 @@ export default createRouter({
|
||||
component: UploadPage,
|
||||
name: config.UPLOAD_PAGE,
|
||||
},
|
||||
{
|
||||
path: 'manage-setting-page',
|
||||
name: config.MANAGE_SETTING_PAGE_DIRECT,
|
||||
component: ManageSettingPage,
|
||||
},
|
||||
|
||||
{
|
||||
path: 'manage-main-page',
|
||||
name: config.MANAGE_MAIN_PAGE,
|
||||
|
||||
Reference in New Issue
Block a user