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%的打开窗口时内存占用
|
- 减少了60-70%的闲置内存占用和20%的打开窗口时内存占用
|
||||||
|
- 优化了多个页面的加载速度和浏览性能
|
||||||
|
|
||||||
### ✨ 新增功能
|
### ✨ 新增功能
|
||||||
|
|
||||||
- windows下新增便携模式,无需安装即可运行,数据存储在程序目录下的`data`文件夹中,支持自动更新;Linux下新增`rpm`安装包
|
- 新功能
|
||||||
- 新增自定义主题功能,主题仓库[PicList ThemeHub](https://github.com/Kuingsmile/PicList-ThemeHub)
|
- 现在支持关闭GPU加速,解决部分兼容性问题
|
||||||
- 12个内置主题供选择,如bilibili、二次元、极夜紫等风格
|
- 新增高级动画设置,开启后可获得更好的UI体验
|
||||||
- 重构了几乎全部页面,优化了数十项UI细节问题
|
- windows下新增便携模式,无需安装即可运行,数据存储在程序目录下的`data`文件夹中,支持自动更新
|
||||||
- 相册页面多项优化,支持显示已选择图片数量,匹配的url列表和记忆过滤器打开状态
|
- Linux下新增`rpm`安装包
|
||||||
- 插件页面现在可以浏览所有插件列表,查看详情和安装
|
- 管理页面新增图床编辑卡片页面,避免了之前多配置切换时的混乱
|
||||||
- 新增教学引导页面,首次运行时会自动弹出
|
|
||||||
- 现在支持关闭GPU加速,解决部分兼容性问题
|
- UI
|
||||||
- 新增高级动画设置,开启后可获得更好的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
|
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 {
|
interface Props {
|
||||||
mapField: Record<string, any> | undefined
|
mapField: Record<string, any> | undefined
|
||||||
defaultValue: any
|
defaultValue: any
|
||||||
@@ -188,6 +172,22 @@ interface Props {
|
|||||||
radioOptions?: RadioOption[]
|
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<{
|
const emit = defineEmits<{
|
||||||
mapChange: [picbedType: string, value: any]
|
mapChange: [picbedType: string, value: any]
|
||||||
}>()
|
}>()
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<!-- eslint-disable vue/no-v-html -->
|
<!-- eslint-disable vue/no-v-html -->
|
||||||
<template>
|
<template>
|
||||||
<div id="config-form" class="no-scrollbar flex h-full w-full flex-1 overflow-auto">
|
<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>
|
<SettingCard>
|
||||||
<CustomInput
|
<CustomInput
|
||||||
v-model="ruleForm._configName"
|
v-model="ruleForm._configName"
|
||||||
@@ -18,12 +18,7 @@
|
|||||||
</SettingCard>
|
</SettingCard>
|
||||||
|
|
||||||
<!-- Dynamic Config Fields -->
|
<!-- Dynamic Config Fields -->
|
||||||
<SettingCard
|
<SettingCard v-for="(item, index) in configList" :key="item.name + index" :p1="item.type === 'confirm'">
|
||||||
v-for="(item, index) in configList"
|
|
||||||
:key="item.name + index"
|
|
||||||
:class="{ required: item.required }"
|
|
||||||
:p1="item.type === 'confirm'"
|
|
||||||
>
|
|
||||||
<CustomInput
|
<CustomInput
|
||||||
v-if="item.type === 'input' || item.type === 'password'"
|
v-if="item.type === 'input' || item.type === 'password'"
|
||||||
v-model="ruleForm[item.name]"
|
v-model="ruleForm[item.name]"
|
||||||
@@ -31,11 +26,15 @@
|
|||||||
:placeholder="item.message || item.name"
|
:placeholder="item.message || item.name"
|
||||||
:class="{ 'border-error!': validationErrors[item.name] }"
|
:class="{ 'border-error!': validationErrors[item.name] }"
|
||||||
:title="item.alias || item.name"
|
:title="item.alias || item.name"
|
||||||
|
:required="item.required || false"
|
||||||
@input="clearFieldError(item.name)"
|
@input="clearFieldError(item.name)"
|
||||||
>
|
>
|
||||||
<template #title-extra>
|
<template #title-extra>
|
||||||
<div v-if="showTooltips && item.tips" class="relative">
|
<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" />
|
<Info :size="15" />
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -53,6 +52,8 @@
|
|||||||
:description="item.message || ''"
|
:description="item.message || ''"
|
||||||
no-border
|
no-border
|
||||||
small
|
small
|
||||||
|
:required="item.required || false"
|
||||||
|
:tips="item.tips"
|
||||||
@change="clearFieldError(item.name)"
|
@change="clearFieldError(item.name)"
|
||||||
>
|
>
|
||||||
<template #switch-text>
|
<template #switch-text>
|
||||||
@@ -60,18 +61,6 @@
|
|||||||
{{ ruleForm[item.name] ? item.confirmText || 'Yes' : item.cancelText || 'No' }}
|
{{ ruleForm[item.name] ? item.confirmText || 'Yes' : item.cancelText || 'No' }}
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</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>
|
</CustomSwitch>
|
||||||
<CustomSelect
|
<CustomSelect
|
||||||
v-if="item.type === 'list' && item.choices"
|
v-if="item.type === 'list' && item.choices"
|
||||||
@@ -79,6 +68,7 @@
|
|||||||
:title="item.alias || item.name"
|
:title="item.alias || item.name"
|
||||||
:placeholder="item.message || item.name"
|
:placeholder="item.message || item.name"
|
||||||
:class="{ 'border-danger': validationErrors[item.name] }"
|
:class="{ 'border-danger': validationErrors[item.name] }"
|
||||||
|
:required="item.required || false"
|
||||||
:select-list="
|
:select-list="
|
||||||
item.choices.map(choice => ({
|
item.choices.map(choice => ({
|
||||||
value: choice.value || choice,
|
value: choice.value || choice,
|
||||||
@@ -100,6 +90,7 @@
|
|||||||
:title="item.alias || item.name"
|
:title="item.alias || item.name"
|
||||||
:zero-placeholder="item.message || item.name"
|
:zero-placeholder="item.message || item.name"
|
||||||
:icon="null"
|
:icon="null"
|
||||||
|
:required="item.required || false"
|
||||||
:all-list="
|
:all-list="
|
||||||
item.choices.map(choice => ({
|
item.choices.map(choice => ({
|
||||||
type: choice.value || choice,
|
type: choice.value || choice,
|
||||||
|
|||||||
@@ -55,6 +55,9 @@ const containerRef = useTemplateRef('containerRef')
|
|||||||
const containerHeight = ref(0)
|
const containerHeight = ref(0)
|
||||||
const containerWidth = ref<number>(0)
|
const containerWidth = ref<number>(0)
|
||||||
const parentScrollListeners = ref<HTMLElement[]>([])
|
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 sortedBreakpoints = computed<Breakpoint[]>(() => [...gridBreakpoints].sort((a, b) => a.min - b.min))
|
||||||
|
|
||||||
const effectiveCols = computed<number>(() => {
|
const effectiveCols = computed<number>(() => {
|
||||||
@@ -123,9 +126,6 @@ function handlePageScroll() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let ro: ResizeObserver | null = null
|
|
||||||
const lastScrollTime = ref(0)
|
|
||||||
|
|
||||||
function updateContainerMetrics() {
|
function updateContainerMetrics() {
|
||||||
if (!containerRef.value) return
|
if (!containerRef.value) return
|
||||||
const rect = containerRef.value.getBoundingClientRect()
|
const rect = containerRef.value.getBoundingClientRect()
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
@click="emit('click')"
|
@click="emit('click')"
|
||||||
>
|
>
|
||||||
<slot name="icon">
|
<slot name="icon">
|
||||||
<component :is="icon" v-if="icon" :size="iconSize" />
|
<component :is="icon" v-if="icon" :size="iconSize" :class="iconClass" />
|
||||||
</slot>
|
</slot>
|
||||||
<slot>
|
<slot>
|
||||||
<span
|
<span
|
||||||
@@ -31,13 +31,17 @@ const {
|
|||||||
icon = null,
|
icon = null,
|
||||||
iconSize = 16,
|
iconSize = 16,
|
||||||
type = 'primary',
|
type = 'primary',
|
||||||
|
iconClass = '',
|
||||||
|
textClass = '',
|
||||||
} = defineProps<{
|
} = defineProps<{
|
||||||
text: string
|
text: string
|
||||||
icon?: any
|
icon?: any
|
||||||
active?: boolean
|
active?: boolean
|
||||||
iconSize?: number
|
iconSize?: number
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
type?: 'primary' | 'secondary' | 'tab'
|
type?: string
|
||||||
|
iconClass?: string
|
||||||
|
textClass?: string
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const textClassVar = computed(() => {
|
const textClassVar = computed(() => {
|
||||||
@@ -49,7 +53,7 @@ const textClassVar = computed(() => {
|
|||||||
case 'tab':
|
case 'tab':
|
||||||
return active ? 'text-white' : 'text-secondary'
|
return active ? 'text-white' : 'text-secondary'
|
||||||
default:
|
default:
|
||||||
return ''
|
return textClass || ''
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -58,9 +62,9 @@ const classVar = computed(() => {
|
|||||||
case 'primary':
|
case 'primary':
|
||||||
return 'bg-accent text-white not-disabled:hover:bg-accent-hover! not-disabled:hover:-translate-y-px'
|
return 'bg-accent text-white not-disabled:hover:bg-accent-hover! not-disabled:hover:-translate-y-px'
|
||||||
case 'secondary':
|
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':
|
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:
|
default:
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<label class="mb-2 text-sm font-semibold text-secondary"
|
<label class="mb-2 text-sm font-semibold text-secondary"
|
||||||
>{{ title }}
|
>{{ title }}
|
||||||
<span v-if="required" class="ml-1 text-red-400">*</span>
|
<span v-if="required" class="ml-1 text-danger">*</span>
|
||||||
</label>
|
</label>
|
||||||
<slot name="title-extra"></slot>
|
<slot name="title-extra"></slot>
|
||||||
</div>
|
</div>
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
v-model="modelValue"
|
v-model="modelValue"
|
||||||
:type="type"
|
:type="type"
|
||||||
v-bind="$attrs"
|
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"
|
:placeholder="placeholder"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -25,6 +25,7 @@
|
|||||||
<EyeIcon v-if="type === 'password'" class="text-accent" :size="16" />
|
<EyeIcon v-if="type === 'password'" class="text-accent" :size="16" />
|
||||||
<EyeClosedIcon v-else class="text-accent" :size="16" />
|
<EyeClosedIcon v-else class="text-accent" :size="16" />
|
||||||
</button>
|
</button>
|
||||||
|
<slot name="input-extra"></slot>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -33,7 +34,20 @@
|
|||||||
import { EyeClosedIcon, EyeIcon } from 'lucide-vue-next'
|
import { EyeClosedIcon, EyeIcon } from 'lucide-vue-next'
|
||||||
import { onMounted, ref } from 'vue'
|
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 type = ref('text')
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
<component :is="icon" v-if="icon" :size="iconSize" class="text-accent" />
|
<component :is="icon" v-if="icon" :size="iconSize" class="text-accent" />
|
||||||
</slot>
|
</slot>
|
||||||
<span class="text-[0.925rem] leading-[1.4] font-semibold text-secondary">{{ title }}</span>
|
<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>
|
||||||
<select
|
<select
|
||||||
v-model="modelValue"
|
v-model="modelValue"
|
||||||
@@ -25,13 +26,15 @@ const modelValue = defineModel<string>()
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
title,
|
title,
|
||||||
icon,
|
icon = null,
|
||||||
iconSize = 18,
|
iconSize = 18,
|
||||||
selectList = [],
|
selectList = [],
|
||||||
|
required = false,
|
||||||
} = defineProps<{
|
} = defineProps<{
|
||||||
title: string
|
title: string
|
||||||
icon: any
|
icon?: any
|
||||||
selectList?: { value: string; label: string }[]
|
selectList?: { value: string; label: string }[]
|
||||||
iconSize?: number
|
iconSize?: number
|
||||||
|
required?: boolean
|
||||||
}>()
|
}>()
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,39 +1,88 @@
|
|||||||
<template>
|
<template>
|
||||||
<label
|
<div class="flex items-center rounded-xl hover:bg-surface hover:shadow-sm">
|
||||||
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"
|
<label
|
||||||
:class="noBorder ? 'border-none' : ''"
|
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
|
<input v-model="modelValue" type="checkbox" class="peer hidden" />
|
||||||
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]"
|
<span
|
||||||
:class="
|
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]"
|
||||||
small
|
:class="
|
||||||
? 'h-[21px] w-[44px] before:top-[2px] before:left-[2px] before:h-[17px] before:w-[17px]'
|
small
|
||||||
: 'h-[28px] w-[52px] before:top-[3px] before:left-[3px] before:h-[22px] before:w-[22px]'
|
? '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">
|
<div class="flex flex-row items-center gap-1">
|
||||||
<span class="text-[0.925rem] leading-[1.4] font-semibold text-secondary">{{ title }}</span>
|
<slot name="custom-title"></slot>
|
||||||
<span v-if="!!description" class="text-xs text-secondary/90">{{ description }}</span>
|
<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>
|
</div>
|
||||||
<slot name="switch-text"></slot>
|
</label>
|
||||||
<slot name="title-extra"></slot>
|
<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>
|
</div>
|
||||||
</label>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<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 modelValue = defineModel<boolean>()
|
||||||
const {
|
const {
|
||||||
title = '',
|
title = '',
|
||||||
description = '',
|
description = '',
|
||||||
noBorder = false,
|
noBorder = false,
|
||||||
small = false,
|
small = false,
|
||||||
|
showTooltips = true,
|
||||||
|
tips = '',
|
||||||
|
required = false,
|
||||||
} = defineProps<{
|
} = defineProps<{
|
||||||
noBorder?: boolean
|
noBorder?: boolean
|
||||||
title?: string
|
title?: string
|
||||||
description?: string
|
description?: string
|
||||||
small?: boolean
|
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>
|
</script>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<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">
|
<template v-for="key in Object.keys(list)" :key="key">
|
||||||
<div class="border-b border-border last:border-0">
|
<div class="border-b border-border last:border-0">
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -4,19 +4,22 @@
|
|||||||
<component :is="icon" v-if="icon" :size="iconSize" class="text-accent" />
|
<component :is="icon" v-if="icon" :size="iconSize" class="text-accent" />
|
||||||
</slot>
|
</slot>
|
||||||
<span class="text-[0.925rem] leading-[1.4] font-semibold text-secondary">{{ title }}</span>
|
<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>
|
||||||
<div ref="dropdownRef" class="sort-dropdown relative">
|
<div ref="dropdownRef" class="sort-dropdown relative">
|
||||||
<button
|
<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="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 }"
|
: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>
|
<span class="text-center text-xs font-semibold text-secondary">{{ placeholder || modelValue }}</span>
|
||||||
<ChevronDownIcon :size="14" />
|
<ChevronDownIcon :size="14" />
|
||||||
</button>
|
</button>
|
||||||
<div
|
<div
|
||||||
v-show="dropDownOpen"
|
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"
|
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
|
<button
|
||||||
@@ -30,6 +33,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onClickOutside } from '@vueuse/core'
|
import { onClickOutside } from '@vueuse/core'
|
||||||
import { ChevronDownIcon, SortAscIcon } from 'lucide-vue-next'
|
import { ChevronDownIcon, SortAscIcon } from 'lucide-vue-next'
|
||||||
@@ -37,6 +41,8 @@ import { nextTick, ref } from 'vue'
|
|||||||
|
|
||||||
const dropdownRef = ref(null)
|
const dropdownRef = ref(null)
|
||||||
const modelValue = defineModel<string>()
|
const modelValue = defineModel<string>()
|
||||||
|
const triggerRef = ref<HTMLElement | null>(null)
|
||||||
|
const optionsRef = ref<HTMLElement | null>(null)
|
||||||
|
|
||||||
function selectItem(key: string) {
|
function selectItem(key: string) {
|
||||||
modelValue.value = key
|
modelValue.value = key
|
||||||
@@ -44,23 +50,49 @@ function selectItem(key: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const dropDownOpen = ref(false)
|
const dropDownOpen = ref(false)
|
||||||
function toggleDropdown(event?: Event) {
|
|
||||||
|
async function toggleDropdown() {
|
||||||
dropDownOpen.value = !dropDownOpen.value
|
dropDownOpen.value = !dropDownOpen.value
|
||||||
|
|
||||||
if (dropDownOpen.value && event) {
|
if (dropDownOpen.value) {
|
||||||
nextTick(() => {
|
await nextTick()
|
||||||
const trigger = event.target as HTMLElement
|
updatePosition()
|
||||||
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`
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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, () => {
|
onClickOutside(dropdownRef, () => {
|
||||||
dropDownOpen.value = false
|
dropDownOpen.value = false
|
||||||
})
|
})
|
||||||
@@ -73,6 +105,8 @@ const {
|
|||||||
icon = null,
|
icon = null,
|
||||||
tight = true,
|
tight = true,
|
||||||
iconSize = 18,
|
iconSize = 18,
|
||||||
|
customFrontIcon = null,
|
||||||
|
required = false,
|
||||||
} = defineProps<{
|
} = defineProps<{
|
||||||
title: string
|
title: string
|
||||||
icon?: any
|
icon?: any
|
||||||
@@ -80,6 +114,8 @@ const {
|
|||||||
tight?: boolean
|
tight?: boolean
|
||||||
placeholder?: string
|
placeholder?: string
|
||||||
fronticon?: boolean
|
fronticon?: boolean
|
||||||
|
customFrontIcon?: any
|
||||||
keyList: string[]
|
keyList: string[]
|
||||||
|
required?: boolean
|
||||||
}>()
|
}>()
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,15 +1,33 @@
|
|||||||
<template>
|
<template>
|
||||||
<Teleport to="body">
|
<Teleport to="body">
|
||||||
<div class="message-container">
|
<div class="pointer-events-none fixed top-[34px] right-[20px] z-10000">
|
||||||
<TransitionGroup name="message" tag="div">
|
<TransitionGroup
|
||||||
<div v-for="message in messages" :key="message.id" class="message-toast" :class="getMessageClass(message.type)">
|
name="message"
|
||||||
<div class="message-icon">
|
tag="div"
|
||||||
<component :is="getIconComponent(message.type)" :size="20" />
|
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>
|
||||||
<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 }}
|
{{ message.message }}
|
||||||
</div>
|
</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" />
|
<X :size="16" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -117,117 +135,3 @@ export default {
|
|||||||
name: 'MessageToast',
|
name: 'MessageToast',
|
||||||
}
|
}
|
||||||
</script>
|
</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",
|
"copySuccess": "Copy Success",
|
||||||
"expand": "Expand Sidebar",
|
"expand": "Expand Sidebar",
|
||||||
"gallery": "Gallery",
|
"gallery": "Gallery",
|
||||||
"manage": "Manage",
|
"manage": "Cloud",
|
||||||
"moreOptions": "More Options",
|
"moreOptions": "More Options",
|
||||||
"picbed": "PicBed",
|
"picbed": "PicBed",
|
||||||
"picBedQrCode": "PicBed QR Code",
|
"picBedQrCode": "PicBed QR Code",
|
||||||
@@ -462,7 +462,7 @@
|
|||||||
"webPathTips": "Used to assemble the public URL"
|
"webPathTips": "Used to assemble the public URL"
|
||||||
},
|
},
|
||||||
"smms": {
|
"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",
|
"tokenDesc": "SM.MS Token, available in your SM.MS profile",
|
||||||
"tokenPlaceholder": "Please enter SM.MS Token"
|
"tokenPlaceholder": "Please enter SM.MS Token"
|
||||||
},
|
},
|
||||||
@@ -541,10 +541,10 @@
|
|||||||
"refresh": "Refresh",
|
"refresh": "Refresh",
|
||||||
"reset": "Reset",
|
"reset": "Reset",
|
||||||
"save": "Save",
|
"save": "Save",
|
||||||
"savedConfigs": "Saved Configurations",
|
"savedConfigs": "Enter Saved Cloud",
|
||||||
"selectPlaceholder": "Please select",
|
"selectPlaceholder": "Please select",
|
||||||
"tips": "Tips",
|
"tips": "Tips",
|
||||||
"title": "Image Host Management",
|
"title": "Cloud Management",
|
||||||
"viewDetails": "View details"
|
"viewDetails": "View details"
|
||||||
},
|
},
|
||||||
"main": {
|
"main": {
|
||||||
@@ -641,10 +641,17 @@
|
|||||||
"preSignedUrlExpireDesc": "Adjust based on actual needs",
|
"preSignedUrlExpireDesc": "Adjust based on actual needs",
|
||||||
"randomStringRenameTips": "20 random characters",
|
"randomStringRenameTips": "20 random characters",
|
||||||
"randomStringRenameTitle": "Random string rename on upload (medium priority)",
|
"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",
|
"selectDownloadFolderTips": "Select download directory",
|
||||||
"selectDownloadFolderTitle": "Choose download folder",
|
"selectDownloadFolderTitle": "Choose download folder",
|
||||||
"timestampRenameTips": "When enabled, uploaded files will be renamed to a timestamp",
|
"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": {
|
"picBedConfigs": {
|
||||||
|
|||||||
@@ -62,7 +62,7 @@
|
|||||||
"copySuccess": "复制成功",
|
"copySuccess": "复制成功",
|
||||||
"expand": "展开侧边栏",
|
"expand": "展开侧边栏",
|
||||||
"gallery": "相册",
|
"gallery": "相册",
|
||||||
"manage": "管理",
|
"manage": "云端",
|
||||||
"moreOptions": "更多选项",
|
"moreOptions": "更多选项",
|
||||||
"picbed": "图床",
|
"picbed": "图床",
|
||||||
"picBedQrCode": "图床配置二维码",
|
"picBedQrCode": "图床配置二维码",
|
||||||
@@ -462,7 +462,7 @@
|
|||||||
"webPathTips": "用于拼接访问网址"
|
"webPathTips": "用于拼接访问网址"
|
||||||
},
|
},
|
||||||
"smms": {
|
"smms": {
|
||||||
"explain": "大陆地区请访问备用域名https://sm.ms,不要短时大量请求",
|
"explain": "大陆地区请访问备用域名https://smms.app,不要短时大量请求",
|
||||||
"tokenDesc": "SM.MS Token, 可在 SM.MS 个人中心获取",
|
"tokenDesc": "SM.MS Token, 可在 SM.MS 个人中心获取",
|
||||||
"tokenPlaceholder": "请输入 SM.MS Token"
|
"tokenPlaceholder": "请输入 SM.MS Token"
|
||||||
},
|
},
|
||||||
@@ -541,10 +541,10 @@
|
|||||||
"refresh": "刷新",
|
"refresh": "刷新",
|
||||||
"reset": "重置",
|
"reset": "重置",
|
||||||
"save": "保存",
|
"save": "保存",
|
||||||
"savedConfigs": "已保存配置",
|
"savedConfigs": "进入已配置云端",
|
||||||
"selectPlaceholder": "请选择",
|
"selectPlaceholder": "请选择",
|
||||||
"tips": "提示",
|
"tips": "提示",
|
||||||
"title": "图床管理",
|
"title": "云端管理",
|
||||||
"viewDetails": "查看详情"
|
"viewDetails": "查看详情"
|
||||||
},
|
},
|
||||||
"main": {
|
"main": {
|
||||||
@@ -641,10 +641,17 @@
|
|||||||
"preSignedUrlExpireDesc": "建议根据实际需求调整",
|
"preSignedUrlExpireDesc": "建议根据实际需求调整",
|
||||||
"randomStringRenameTips": "20位随机字符",
|
"randomStringRenameTips": "20位随机字符",
|
||||||
"randomStringRenameTitle": "上传文件随机字符串重命名(中优先级)",
|
"randomStringRenameTitle": "上传文件随机字符串重命名(中优先级)",
|
||||||
|
"section": {
|
||||||
|
"cache": "缓存设置",
|
||||||
|
"general": "通用设置",
|
||||||
|
"naming": "命名设置",
|
||||||
|
"up-down": "上传/下载设置"
|
||||||
|
},
|
||||||
"selectDownloadFolderTips": "选择下载目录",
|
"selectDownloadFolderTips": "选择下载目录",
|
||||||
"selectDownloadFolderTitle": "选择下载文件夹",
|
"selectDownloadFolderTitle": "选择下载文件夹",
|
||||||
"timestampRenameTips": "开启后,上传的文件将自动重命名为时间戳",
|
"timestampRenameTips": "开启后,上传的文件将自动重命名为时间戳",
|
||||||
"timestampRenameTitle": "上传文件时间戳重命名(最高优先级)"
|
"timestampRenameTitle": "上传文件时间戳重命名(最高优先级)",
|
||||||
|
"title": "管理通用设置"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"picBedConfigs": {
|
"picBedConfigs": {
|
||||||
|
|||||||
@@ -62,7 +62,7 @@
|
|||||||
"copySuccess": "複製成功",
|
"copySuccess": "複製成功",
|
||||||
"expand": "展開側邊欄",
|
"expand": "展開側邊欄",
|
||||||
"gallery": "相簿",
|
"gallery": "相簿",
|
||||||
"manage": "管理",
|
"manage": "雲端",
|
||||||
"moreOptions": "更多選項",
|
"moreOptions": "更多選項",
|
||||||
"picbed": "圖床",
|
"picbed": "圖床",
|
||||||
"picBedQrCode": "圖床配置 QRCODE",
|
"picBedQrCode": "圖床配置 QRCODE",
|
||||||
@@ -462,7 +462,7 @@
|
|||||||
"webPathTips": "用於拼接對外存取網址"
|
"webPathTips": "用於拼接對外存取網址"
|
||||||
},
|
},
|
||||||
"smms": {
|
"smms": {
|
||||||
"explain": "中國大陸地區請訪問備用網域 https://sm.ms,請勿在短時間內大量請求",
|
"explain": "中國大陸地區請訪問備用網域 https://smms.app,請勿在短時間內大量請求",
|
||||||
"tokenDesc": "SM.MS Token,可在 SM.MS 個人中心取得",
|
"tokenDesc": "SM.MS Token,可在 SM.MS 個人中心取得",
|
||||||
"tokenPlaceholder": "請輸入 SM.MS Token"
|
"tokenPlaceholder": "請輸入 SM.MS Token"
|
||||||
},
|
},
|
||||||
@@ -541,10 +541,10 @@
|
|||||||
"refresh": "重新整理",
|
"refresh": "重新整理",
|
||||||
"reset": "重設",
|
"reset": "重設",
|
||||||
"save": "儲存",
|
"save": "儲存",
|
||||||
"savedConfigs": "已儲存設定",
|
"savedConfigs": "進入已配置雲端",
|
||||||
"selectPlaceholder": "請選擇",
|
"selectPlaceholder": "請選擇",
|
||||||
"tips": "提示",
|
"tips": "提示",
|
||||||
"title": "圖床管理",
|
"title": "雲端存儲管理",
|
||||||
"viewDetails": "查看詳情"
|
"viewDetails": "查看詳情"
|
||||||
},
|
},
|
||||||
"main": {
|
"main": {
|
||||||
@@ -641,10 +641,17 @@
|
|||||||
"preSignedUrlExpireDesc": "建議依實際需求調整",
|
"preSignedUrlExpireDesc": "建議依實際需求調整",
|
||||||
"randomStringRenameTips": "20 位隨機字元",
|
"randomStringRenameTips": "20 位隨機字元",
|
||||||
"randomStringRenameTitle": "上傳檔案隨機字串重新命名(中優先級)",
|
"randomStringRenameTitle": "上傳檔案隨機字串重新命名(中優先級)",
|
||||||
|
"section": {
|
||||||
|
"cache": "快取設置",
|
||||||
|
"general": "通用設置",
|
||||||
|
"naming": "命名設置",
|
||||||
|
"up-down": "上傳/下載設置"
|
||||||
|
},
|
||||||
"selectDownloadFolderTips": "選擇下載目錄",
|
"selectDownloadFolderTips": "選擇下載目錄",
|
||||||
"selectDownloadFolderTitle": "選擇下載資料夾",
|
"selectDownloadFolderTitle": "選擇下載資料夾",
|
||||||
"timestampRenameTips": "啟用後,上傳的檔案將自動更名為時間戳",
|
"timestampRenameTips": "啟用後,上傳的檔案將自動更名為時間戳",
|
||||||
"timestampRenameTitle": "上傳檔案時間戳重新命名(最高優先級)"
|
"timestampRenameTitle": "上傳檔案時間戳重新命名(最高優先級)",
|
||||||
|
"title": "管理通用設置"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"picBedConfigs": {
|
"picBedConfigs": {
|
||||||
|
|||||||
@@ -1,281 +1,248 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="login-container">
|
<div class="relative flex h-full w-full items-center justify-center">
|
||||||
<!-- Header Card -->
|
<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="login-card header-card">
|
<div
|
||||||
<div class="card-header">
|
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="header-content">
|
>
|
||||||
<div class="header-icon">
|
<div class="flex flex-1 flex-wrap items-center gap-4 p-1">
|
||||||
<DatabaseIcon :size="24" />
|
<Cloud :size="24" class="text-accent" />
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
<h1>{{ t('pages.manage.login.title') }}</h1>
|
<h1 class="m-0 text-2xl font-semibold tracking-tight text-main">{{ t('pages.manage.login.title') }}</h1>
|
||||||
<p>{{ sortedAllConfigAliasMap.length }} {{ t('pages.manage.login.savedConfigs') }}</p>
|
<p class="m-0 text-sm text-secondary">
|
||||||
|
{{ sortedAllConfigAliasMap.length }} {{ t('pages.manage.login.savedConfigs') }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-actions">
|
<div class="flex flex-wrap gap-3 overflow-visible">
|
||||||
<button class="action-button" @click="refreshConfigs">
|
<CustomButton
|
||||||
<RefreshCwIcon :size="16" />
|
type="secondary"
|
||||||
{{ t('pages.manage.login.refresh') }}
|
:icon="RefreshCwIcon"
|
||||||
</button>
|
: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>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Navigation Tabs -->
|
<!-- Navigation Tabs -->
|
||||||
<div class="login-card tabs-card">
|
<div
|
||||||
<div class="tabs-container">
|
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="tabs-nav-wrapper">
|
>
|
||||||
<div class="tabs-nav">
|
<div class="flex-1 overflow-hidden p-2">
|
||||||
|
<div class="flex w-full flex-wrap items-center gap-2">
|
||||||
<button
|
<button
|
||||||
v-for="item in tabItems"
|
v-for="item in tabItems"
|
||||||
:key="item.key"
|
: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 }"
|
:class="{ active: activeName === item.key }"
|
||||||
@click="handleTabChange(item.key)"
|
@click="handleTabChange(item.key)"
|
||||||
>
|
>
|
||||||
<FolderIcon v-if="item.key === 'login'" :size="16" />
|
<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>
|
<span>{{ item.name }}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Content Area -->
|
<!-- Content Area -->
|
||||||
<div class="login-card content-card">
|
<div
|
||||||
<div class="tab-content">
|
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"
|
||||||
<!-- Main Config List Tab -->
|
>
|
||||||
<div v-if="activeName === 'login'" class="config-list-container">
|
<div class="no-scrollbar h-full w-full flex-1 overflow-auto rounded-2xl border-none">
|
||||||
<div v-if="sortedAllConfigAliasMap.length === 0" class="empty-state">
|
<!-- Main Config List Tab -->
|
||||||
<div class="empty-icon">
|
<div v-if="activeName === 'login'" class="h-full w-full p-4">
|
||||||
<DatabaseIcon :size="48" />
|
<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>
|
</div>
|
||||||
<h3>{{ t('pages.manage.login.noConfigs') }}</h3>
|
<div
|
||||||
<p>{{ t('pages.manage.login.noConfigsDesc') }}</p>
|
v-else
|
||||||
</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-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
|
<div
|
||||||
v-for="option in supportedPicBedList[activeName].options"
|
v-for="item in sortedAllConfigAliasMap"
|
||||||
:key="option"
|
:key="item.alias"
|
||||||
class="form-group"
|
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"
|
||||||
:class="{ 'has-error': formErrors[activeName + '.' + option] }"
|
|
||||||
>
|
>
|
||||||
<label class="form-label">
|
<div class="flex-1">
|
||||||
{{ supportedPicBedList[activeName].configOptions[option].description }}
|
<div class="mb-4 flex items-center gap-4">
|
||||||
<span v-if="supportedPicBedList[activeName].configOptions[option].required" class="required-marker"
|
<img
|
||||||
>*</span
|
:src="`./assets/${item.picBedName}.webp`"
|
||||||
>
|
class="h-[40px] w-[40px] object-contain"
|
||||||
<button
|
:alt="item.picBedName"
|
||||||
v-if="supportedPicBedList[activeName].configOptions[option].tooltip"
|
/>
|
||||||
class="tooltip-button"
|
<div>
|
||||||
:title="supportedPicBedList[activeName].configOptions[option].tooltip"
|
<h4 class="mb-1 text-base font-semibold text-main">
|
||||||
>
|
{{ item.alias }}
|
||||||
<InfoIcon :size="14" />
|
</h4>
|
||||||
</button>
|
<p class="m-0 text-sm text-secondary">
|
||||||
</label>
|
{{ supportedPicBedList[item.picBedName]?.name || item.picBedName }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- String Input -->
|
<div class="relative">
|
||||||
<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">
|
|
||||||
<button
|
<button
|
||||||
v-for="alias in currentAliasList"
|
class="flex cursor-pointer items-center gap-2 rounded-xl border-none bg-accent/5 p-2 text-sm text-secondary hover:text-main"
|
||||||
:key="alias"
|
@click="toggleConfigDetails(item.alias)"
|
||||||
class="dropdown-item"
|
|
||||||
@click="handleConfigImport(alias)"
|
|
||||||
>
|
>
|
||||||
{{ alias }}
|
<InfoIcon :size="14" />
|
||||||
|
{{ t('pages.manage.login.viewDetails') }}
|
||||||
|
<ChevronDownIcon :size="14" :class="{ 'rotate-180': expandedConfigs.includes(item.alias) }" />
|
||||||
</button>
|
</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>
|
||||||
</div>
|
<div class="flex flex-col items-center justify-end gap-4">
|
||||||
|
<CustomButton
|
||||||
<div class="main-actions">
|
type="primary"
|
||||||
<button class="action-button primary" @click="handleConfigChange(activeName)">
|
:icon="PointerIcon"
|
||||||
<SaveIcon :size="16" />
|
:text="t('pages.manage.login.enter')"
|
||||||
{{ t('pages.manage.login.save') }}
|
@click="handleConfigClick(item)"
|
||||||
</button>
|
/>
|
||||||
<button class="action-button danger" @click="handleConfigReset(activeName)">
|
<CustomButton
|
||||||
<RotateCcwIcon :size="16" />
|
type="danger"
|
||||||
{{ t('pages.manage.login.reset') }}
|
class="border border-border bg-danger/70 opacity-0 transition-all duration-fast ease-apple group-hover:opacity-100 hover:bg-danger"
|
||||||
</button>
|
icon-class="text-white "
|
||||||
</div>
|
text-class="text-white font-semibold text-sm "
|
||||||
</div>
|
:icon="TrashIcon"
|
||||||
|
:text="t('pages.manage.login.delete')"
|
||||||
<!-- Existing Configurations Table -->
|
@click="handleConfigRemove(item.alias)"
|
||||||
<div v-if="dataForTable.length > 0" class="config-table-section">
|
/>
|
||||||
<h3>{{ t('pages.manage.login.configTabTitle') }}</h3>
|
</div>
|
||||||
<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>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -284,31 +251,37 @@
|
|||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import {
|
import {
|
||||||
|
BookOpen,
|
||||||
ChevronDownIcon,
|
ChevronDownIcon,
|
||||||
|
Cloud,
|
||||||
DatabaseIcon,
|
DatabaseIcon,
|
||||||
DownloadIcon,
|
|
||||||
FolderIcon,
|
FolderIcon,
|
||||||
InfoIcon,
|
InfoIcon,
|
||||||
LinkIcon,
|
Pencil,
|
||||||
|
Plus,
|
||||||
PointerIcon,
|
PointerIcon,
|
||||||
RefreshCwIcon,
|
RefreshCwIcon,
|
||||||
RotateCcwIcon,
|
Settings2,
|
||||||
SaveIcon,
|
Trash2,
|
||||||
TrashIcon,
|
TrashIcon,
|
||||||
|
XIcon,
|
||||||
} from 'lucide-vue-next'
|
} from 'lucide-vue-next'
|
||||||
import { computed, onMounted, reactive, ref } from 'vue'
|
import { computed, onMounted, reactive, ref } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
import CustomButton from '@/components/common/CustomButton.vue'
|
||||||
import useConfirm from '@/hooks/useConfirm'
|
import useConfirm from '@/hooks/useConfirm'
|
||||||
import useMessage from '@/hooks/useMessage'
|
import useMessage from '@/hooks/useMessage'
|
||||||
|
import ManageEditPage from '@/manage/pages/ManageEditPage.vue'
|
||||||
import { useManageStore } from '@/manage/store/manageStore'
|
import { useManageStore } from '@/manage/store/manageStore'
|
||||||
import { formObjToTableData } from '@/manage/utils/common'
|
import { formObjToTableData } from '@/manage/utils/common'
|
||||||
import { supportedPicBedList } from '@/manage/utils/constants'
|
import { supportedPicBedList } from '@/manage/utils/constants'
|
||||||
import { getConfig, removeConfig, saveConfig } from '@/manage/utils/dataSender'
|
import { getConfig, removeConfig, saveConfig } from '@/manage/utils/dataSender'
|
||||||
import { formatEndpoint } from '@/utils/common'
|
import { formatEndpoint } from '@/utils/common'
|
||||||
|
import { configPaths } from '@/utils/configPaths'
|
||||||
import { getConfig as getPicBedsConfig } from '@/utils/dataSender'
|
import { getConfig as getPicBedsConfig } from '@/utils/dataSender'
|
||||||
import { IRPCActionType } from '@/utils/enum'
|
import { II18nLanguage, IRPCActionType } from '@/utils/enum'
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const manageStore = useManageStore()
|
const manageStore = useManageStore()
|
||||||
@@ -316,10 +289,10 @@ const router = useRouter()
|
|||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
const { confirm } = useConfirm()
|
const { confirm } = useConfirm()
|
||||||
|
|
||||||
|
const editMode = ref(false)
|
||||||
|
const editingAlias = ref('')
|
||||||
const activeName = ref('login')
|
const activeName = ref('login')
|
||||||
const expandedConfigs = ref<string[]>([])
|
const expandedConfigs = ref<string[]>([])
|
||||||
const importDropdownOpen = ref(false)
|
|
||||||
|
|
||||||
const configResult: IStringKeyMap = reactive({})
|
const configResult: IStringKeyMap = reactive({})
|
||||||
const existingConfiguration = reactive({} as IStringKeyMap)
|
const existingConfiguration = reactive({} as IStringKeyMap)
|
||||||
const dataForTable = reactive([] as any[])
|
const dataForTable = reactive([] as any[])
|
||||||
@@ -361,77 +334,6 @@ const notifyUser = (msg: string, type: 'success' | 'error' | 'warning' = 'succes
|
|||||||
message[type](`${msg}`)
|
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) => {
|
const initializeDefaultValues = (picBedName: string) => {
|
||||||
if (!supportedPicBedList[picBedName]) return
|
if (!supportedPicBedList[picBedName]) return
|
||||||
|
|
||||||
@@ -486,88 +388,10 @@ async function getExistingConfig(name: string) {
|
|||||||
handleConfigImport(currentAliasList[0])
|
handleConfigImport(currentAliasList[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAliasList() {
|
function openBucketPageSetting() {
|
||||||
return Object.values(existingConfiguration).map(item => item.alias)
|
router.push({
|
||||||
}
|
path: '/main-page/manage-setting-page',
|
||||||
|
|
||||||
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]
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const configKeys = Object.keys(configResult).filter(key => key.startsWith(name))
|
|
||||||
configKeys.forEach(key => {
|
|
||||||
delete configResult[key]
|
|
||||||
})
|
|
||||||
|
|
||||||
initializeDefaultValues(name)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleConfigRemove = async (name: string) => {
|
const handleConfigRemove = async (name: string) => {
|
||||||
@@ -611,8 +435,6 @@ const copyToClipboard = (text: string) => {
|
|||||||
notifyUser(`${t('pages.manage.login.copySuccess', { text })}`, 'success')
|
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 handleConfigClick = async (item: any) => {
|
||||||
const alias = item.alias
|
const alias = item.alias
|
||||||
const config = JSON.stringify(item.config)
|
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) {
|
function handleConfigImport(alias: string) {
|
||||||
const selectedConfig = existingConfiguration[alias]
|
const selectedConfig = existingConfiguration[alias]
|
||||||
if (!selectedConfig) return
|
if (!selectedConfig) return
|
||||||
@@ -641,6 +468,7 @@ function handleConfigImport(alias: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleTabChange = (tabName: string) => {
|
const handleTabChange = (tabName: string) => {
|
||||||
|
editMode.value = false
|
||||||
activeName.value = tabName
|
activeName.value = tabName
|
||||||
getExistingConfig(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)
|
const index = expandedConfigs.value.indexOf(alias)
|
||||||
if (index > -1) {
|
if (index > -1) {
|
||||||
expandedConfigs.value.splice(index, 1)
|
expandedConfigs.value.splice(index, 1)
|
||||||
@@ -662,10 +490,6 @@ const toggleConfigDetails = (alias: string) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggleImportDropdown = () => {
|
|
||||||
importDropdownOpen.value = !importDropdownOpen.value
|
|
||||||
}
|
|
||||||
|
|
||||||
const refreshConfigs = () => {
|
const refreshConfigs = () => {
|
||||||
getAllConfigAliasArray()
|
getAllConfigAliasArray()
|
||||||
notifyUser(t('pages.manage.login.configurationRefreshMsg'), 'success')
|
notifyUser(t('pages.manage.login.configurationRefreshMsg'), 'success')
|
||||||
@@ -716,6 +540,12 @@ async function getCurrentConfigList() {
|
|||||||
await getAllConfigAliasArray()
|
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) {
|
function isImported(alias: string) {
|
||||||
return Object.values(allConfigAliasMap).some(item => item.alias === alias)
|
return Object.values(allConfigAliasMap).some(item => item.alias === alias)
|
||||||
}
|
}
|
||||||
@@ -967,5 +797,3 @@ onMounted(() => {
|
|||||||
getCurrentConfigList()
|
getCurrentConfigList()
|
||||||
})
|
})
|
||||||
</script>
|
</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>
|
</div>
|
||||||
|
|
||||||
<!-- PicBed Switch Dialog -->
|
<!-- 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 v-if="picBedSwitchDialogVisible" class="dialog-overlay" @click="picBedSwitchDialogVisible = false">
|
||||||
<div class="dialog-container" @click.stop>
|
<div class="dialog-container" @click.stop>
|
||||||
<div class="dialog-header">
|
<div class="dialog-header">
|
||||||
|
|||||||
@@ -1,213 +1,133 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="manage-setting-container">
|
<div class="relative flex h-full w-full items-center justify-center">
|
||||||
<!-- Cache Info Card -->
|
<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="setting-card content-card">
|
<div
|
||||||
<div class="card-content">
|
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="setting-section">
|
>
|
||||||
<div class="form-group">
|
<div class="flex flex-1 flex-wrap items-center gap-4 p-2">
|
||||||
<div class="form-control">
|
<Settings :size="24" class="text-accent" />
|
||||||
<button type="button" class="action-button warning" @click="handleConfirmClearDb">
|
<div>
|
||||||
<Trash2Icon :size="16" />
|
<h1 class="m-0 text-2xl font-semibold tracking-tight text-main">{{ t('pages.manage.setting.title') }}</h1>
|
||||||
{{
|
|
||||||
t('pages.manage.setting.clearCache', {
|
|
||||||
percent: dbSizeAvailableRate,
|
|
||||||
size: formatFileSize(dbSize) || 0,
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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 -->
|
<SettingSection :title="t('pages.manage.setting.section.general')" :icon="Settings">
|
||||||
<div class="setting-card content-card">
|
<SettingCard>
|
||||||
<div class="card-content">
|
<CustomSelect
|
||||||
<div class="setting-section">
|
v-model="form.pasteFormat"
|
||||||
<CustomSwitch
|
:select-list="pasteFormatList"
|
||||||
v-for="item in switchFieldsConfigList"
|
:title="t('pages.manage.setting.copyFormat.title')"
|
||||||
:key="item.configName"
|
:icon="Edit2Icon"
|
||||||
v-model="form[item.configName]"
|
/>
|
||||||
:segments="item.segments"
|
</SettingCard>
|
||||||
:tooltip="item.tooltip"
|
<SettingCard>
|
||||||
:active-text="item.activeText"
|
<CustomInput
|
||||||
:inactive-text="item.inactiveText"
|
v-model="form.customPasteFormat"
|
||||||
/>
|
:title="t('pages.manage.setting.copyFormat.customTitle')"
|
||||||
</div>
|
:placeholder="t('pages.manage.setting.copyFormat.customTips')"
|
||||||
</div>
|
/>
|
||||||
</div>
|
</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 -->
|
<SettingSection
|
||||||
<div v-if="form.customRename" class="setting-card content-card">
|
v-if="form.customRename"
|
||||||
<div class="card-content">
|
:title="t('pages.manage.setting.section.naming')"
|
||||||
<div class="setting-section">
|
:icon="Edit2Icon"
|
||||||
<div class="section-header">
|
only-one-row
|
||||||
<h4 class="section-title">
|
>
|
||||||
{{ t('pages.manage.setting.customRenameTableTitle') }}
|
<CustomInput
|
||||||
</h4>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<input
|
|
||||||
v-model="form.customRenameFormat"
|
v-model="form.customRenameFormat"
|
||||||
type="text"
|
:title="t('pages.manage.setting.customRenameTablePlaceholder')"
|
||||||
class="form-input"
|
|
||||||
:placeholder="t('pages.manage.setting.customRenameTablePlaceholder')"
|
:placeholder="t('pages.manage.setting.customRenameTablePlaceholder')"
|
||||||
/>
|
/>
|
||||||
</div>
|
<placeholderTable :list="advancedRenameList" :title-list="advancedRenameTitleList" />
|
||||||
|
</SettingSection>
|
||||||
|
|
||||||
<!-- Pattern Reference Table -->
|
<SettingSection :icon="Download" :title="t('pages.manage.setting.section.up-down')">
|
||||||
<div class="pattern-table-container">
|
<SettingCard v-for="item in switchFieldsSpecialList" :key="item.configName" class="mb-4" p1>
|
||||||
<table class="pattern-table">
|
<CustomSwitch v-model="form[item.configName]" small no-border :tips="item.tooltip">
|
||||||
<thead>
|
<template #custom-title>
|
||||||
<tr>
|
<span v-for="(segment, index) in item.segments" :key="index" :class="segment.class">
|
||||||
<th>{{ t('pages.manage.setting.placeholder') }}</th>
|
{{ segment.text }}
|
||||||
<th>{{ t('pages.manage.setting.description') }}</th>
|
</span>
|
||||||
<th>{{ t('pages.manage.setting.placeholder') }}</th>
|
</template>
|
||||||
<th>{{ t('pages.manage.setting.description') }}</th>
|
</CustomSwitch>
|
||||||
</tr>
|
</SettingCard>
|
||||||
</thead>
|
<SettingCard>
|
||||||
<tbody>
|
<CustomInput
|
||||||
<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
|
|
||||||
v-model.number="form.maxDownloadFileCount"
|
v-model.number="form.maxDownloadFileCount"
|
||||||
type="number"
|
:title="t('pages.manage.setting.maxDownLoadFileLimit')"
|
||||||
class="form-input number-input"
|
|
||||||
:placeholder="t('pages.manage.setting.maxDownLoadFileLimitDesc')"
|
:placeholder="t('pages.manage.setting.maxDownLoadFileLimitDesc')"
|
||||||
|
type="number"
|
||||||
min="1"
|
min="1"
|
||||||
max="9999"
|
max="9999"
|
||||||
step="1"
|
step="1"
|
||||||
/>
|
/>
|
||||||
</div>
|
</SettingCard>
|
||||||
</div>
|
<SettingCard>
|
||||||
|
<CustomInput
|
||||||
<!-- PreSigned URL Expire -->
|
|
||||||
<div class="form-group">
|
|
||||||
<div class="form-label-wrapper">
|
|
||||||
<span class="form-label">
|
|
||||||
{{ t('pages.manage.setting.preSignedUrlExpire') }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="form-control">
|
|
||||||
<input
|
|
||||||
v-model.number="form.PreSignedExpire"
|
v-model.number="form.PreSignedExpire"
|
||||||
type="number"
|
:title="t('pages.manage.setting.preSignedUrlExpire')"
|
||||||
class="form-input number-input"
|
|
||||||
:placeholder="t('pages.manage.setting.preSignedUrlExpireDesc')"
|
:placeholder="t('pages.manage.setting.preSignedUrlExpireDesc')"
|
||||||
|
type="number"
|
||||||
min="1"
|
min="1"
|
||||||
step="1"
|
step="1"
|
||||||
/>
|
/>
|
||||||
</div>
|
</SettingCard>
|
||||||
</div>
|
<SettingCard>
|
||||||
</div>
|
<CustomInput
|
||||||
</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
|
|
||||||
v-model="form.downloadDir"
|
v-model="form.downloadDir"
|
||||||
type="text"
|
:title="t('pages.manage.setting.selectDownloadFolderTitle')"
|
||||||
class="form-input group-input"
|
|
||||||
disabled
|
|
||||||
:placeholder="t('pages.manage.setting.defaultDownloadFolder')"
|
:placeholder="t('pages.manage.setting.defaultDownloadFolder')"
|
||||||
/>
|
disabled
|
||||||
<button type="button" class="input-append-button" @click="handleDownloadDirClick">
|
>
|
||||||
<FolderIcon :size="16" />
|
<template #input-extra>
|
||||||
{{ t('pages.manage.setting.browse') }}
|
<button
|
||||||
</button>
|
type="button"
|
||||||
</div>
|
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"
|
||||||
</div>
|
@click="handleDownloadDirClick"
|
||||||
|
>
|
||||||
|
<FolderIcon :size="16" />
|
||||||
|
{{ t('pages.manage.setting.browse') }}
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
</CustomInput>
|
||||||
|
</SettingCard>
|
||||||
|
</SettingSection>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -215,15 +135,21 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { FolderIcon, Trash2Icon } from 'lucide-vue-next'
|
import { Download, Edit2Icon, FolderIcon, Settings, Trash2Icon } from 'lucide-vue-next'
|
||||||
import { nextTick, onBeforeMount, ref, watch } from 'vue'
|
import { computed, nextTick, onBeforeMount, ref, watch } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
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 useConfirm from '@/hooks/useConfirm'
|
||||||
import useMessage from '@/hooks/useMessage'
|
import useMessage from '@/hooks/useMessage'
|
||||||
import CustomSwitch from '@/manage/components/CustomSwitch.vue'
|
|
||||||
import { fileCacheDbInstance } from '@/manage/store/bucketFileDb'
|
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 { getConfig, saveConfig } from '@/manage/utils/dataSender'
|
||||||
import { IRPCActionType } from '@/utils/enum'
|
import { IRPCActionType } from '@/utils/enum'
|
||||||
|
|
||||||
@@ -250,23 +176,18 @@ const form = ref<IStringKeyMap>({
|
|||||||
maxDownloadFileCount: 5,
|
maxDownloadFileCount: 5,
|
||||||
customRenameFormat: '{filename}',
|
customRenameFormat: '{filename}',
|
||||||
})
|
})
|
||||||
|
|
||||||
const settingsKeys = Object.keys(form.value)
|
|
||||||
|
|
||||||
const dbSize = ref(0)
|
const dbSize = ref(0)
|
||||||
const dbSizeAvailableRate = ref('0')
|
const dbSizeAvailableRate = ref('0')
|
||||||
|
|
||||||
const pasteFormatList = ['markdown', 'markdown-with-link', 'rawurl', 'html', 'bbcode', 'custom']
|
const settingsKeys = Object.keys(form.value)
|
||||||
|
const pasteFormatList = [
|
||||||
settingsKeys.forEach(key => {
|
{ label: t('pages.manage.setting.copyFormat.markdown'), value: 'markdown' },
|
||||||
watch(
|
{ label: t('pages.manage.setting.copyFormat.markdown-with-link'), value: 'markdown-with-link' },
|
||||||
() => form.value[key],
|
{ label: t('pages.manage.setting.copyFormat.rawurl'), value: 'rawurl' },
|
||||||
newValue => {
|
{ label: t('pages.manage.setting.copyFormat.html'), value: 'html' },
|
||||||
saveConfig({ [`settings.${key}`]: newValue })
|
{ label: t('pages.manage.setting.copyFormat.bbcode'), value: 'bbcode' },
|
||||||
},
|
{ label: t('pages.manage.setting.copyFormat.custom'), value: 'custom' },
|
||||||
{ flush: 'post' },
|
]
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
const switchFieldsList = [
|
const switchFieldsList = [
|
||||||
'isAutoRefresh',
|
'isAutoRefresh',
|
||||||
@@ -282,35 +203,33 @@ const switchFieldsList = [
|
|||||||
]
|
]
|
||||||
const switchFieldsNoTipsList = ['isShowThumbnail', 'isUsePreSignedUrl']
|
const switchFieldsNoTipsList = ['isShowThumbnail', 'isUsePreSignedUrl']
|
||||||
const switchFieldsHasActiveTextList = [] as string[]
|
const switchFieldsHasActiveTextList = [] as string[]
|
||||||
|
|
||||||
const switchFieldsConfigList = switchFieldsList.map(item => ({
|
const switchFieldsConfigList = switchFieldsList.map(item => ({
|
||||||
configName: item,
|
configName: item,
|
||||||
segments: [
|
segments: [
|
||||||
{
|
{
|
||||||
text: t(`pages.manage.setting.${item}Title` as any),
|
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),
|
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,
|
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,
|
inactiveText: switchFieldsHasActiveTextList.includes(item) ? t(`pages.manage.setting.${item}Off` as any) : undefined,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const switchFieldsSpecialList = [
|
const switchFieldsSpecialList = [
|
||||||
{
|
{
|
||||||
configName: 'isDownloadFileKeepDirStructure',
|
configName: 'isDownloadFileKeepDirStructure',
|
||||||
segments: [
|
segments: [
|
||||||
{
|
{
|
||||||
text: t('pages.manage.setting.download'),
|
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'),
|
text: t('pages.manage.setting.file'),
|
||||||
style: 'color: orange;',
|
class: 'text-warning text-sm font-semibold',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: t('pages.manage.setting.keepDirStructure'),
|
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'),
|
tooltip: t('pages.manage.setting.keepDirStructureDesc'),
|
||||||
@@ -320,21 +239,62 @@ const switchFieldsSpecialList = [
|
|||||||
segments: [
|
segments: [
|
||||||
{
|
{
|
||||||
text: t('pages.manage.setting.download'),
|
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'),
|
text: t('pages.manage.setting.folder'),
|
||||||
style: 'color: orange;',
|
class: 'text-warning text-sm font-semibold',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: t('pages.manage.setting.keepDirStructure'),
|
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'),
|
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() {
|
async function initData() {
|
||||||
const config = (await getConfig()) as IStringKeyMap
|
const config = (await getConfig()) as IStringKeyMap
|
||||||
settingsKeys.forEach(key => {
|
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() {
|
function handleConfirmClearDb() {
|
||||||
confirm({
|
confirm({
|
||||||
title: t('pages.manage.setting.notice'),
|
title: t('pages.manage.setting.notice'),
|
||||||
@@ -394,5 +349,3 @@ onBeforeMount(() => {
|
|||||||
getIndexDbSize()
|
getIndexDbSize()
|
||||||
})
|
})
|
||||||
</script>
|
</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
|
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 { IPasteStyle, IRPCActionType } from '@/utils/enum'
|
||||||
import { picBedsCanbeDeleted } from '@/utils/static'
|
import { picBedsCanbeDeleted } from '@/utils/static'
|
||||||
|
|
||||||
const { t } = useI18n()
|
|
||||||
const message = useMessage()
|
|
||||||
const { confirm } = useConfirm()
|
|
||||||
const { picBedG } = usePicBed()
|
|
||||||
|
|
||||||
type IResult<T> = T & {
|
type IResult<T> = T & {
|
||||||
id: string
|
id: string
|
||||||
createdAt: number
|
createdAt: number
|
||||||
updatedAt: number
|
updatedAt: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
const message = useMessage()
|
||||||
|
const { confirm } = useConfirm()
|
||||||
|
const { picBedG } = usePicBed()
|
||||||
|
|
||||||
const images = ref<ImgInfo[]>([])
|
const images = ref<ImgInfo[]>([])
|
||||||
const virtualScrollerRef = useTemplateRef('virtualScrollerRef')
|
const virtualScrollerRef = useTemplateRef('virtualScrollerRef')
|
||||||
const previewImageRef = useTemplateRef('previewImageRef')
|
const previewImageRef = useTemplateRef('previewImageRef')
|
||||||
@@ -609,10 +609,7 @@ const debouncedSearchText = ref<string>('')
|
|||||||
const debouncedSearchTextURL = ref<string>('')
|
const debouncedSearchTextURL = ref<string>('')
|
||||||
const handleBarActive = useStorage<boolean>('galleryHandleBarActive', true)
|
const handleBarActive = useStorage<boolean>('galleryHandleBarActive', true)
|
||||||
const pasteStyle = ref<string>('')
|
const pasteStyle = ref<string>('')
|
||||||
const pasteStyleList = ['markdown', 'HTML', 'URL', 'UBB', 'Custom']
|
|
||||||
const useShortUrl = ref<string>('')
|
const useShortUrl = ref<string>('')
|
||||||
const shortURLList = [t('pages.gallery.shortUrl'), t('pages.gallery.longUrl')]
|
|
||||||
|
|
||||||
const fileSortNameReverse = ref(false)
|
const fileSortNameReverse = ref(false)
|
||||||
const fileSortTimeReverse = ref(false)
|
const fileSortTimeReverse = ref(false)
|
||||||
const fileSortExtReverse = ref(false)
|
const fileSortExtReverse = ref(false)
|
||||||
@@ -630,18 +627,6 @@ const viewMode = useStorage<'list' | 'grid'>('galleryViewMode', 'grid')
|
|||||||
const componentKey = ref(0)
|
const componentKey = ref(0)
|
||||||
const currentSortField = ref<'name' | 'time' | 'ext' | 'check'>('name')
|
const currentSortField = ref<'name' | 'time' | 'ext' | 'check'>('name')
|
||||||
const userGridColumns = useStorage<number>('galleryGridColumns', 4)
|
const 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 imageLoadStates = reactive<Record<string, boolean>>({})
|
||||||
const imageErrorStates = reactive<Record<string, boolean>>({})
|
const imageErrorStates = reactive<Record<string, boolean>>({})
|
||||||
|
|
||||||
@@ -659,6 +644,8 @@ const imagePreviewState = reactive({
|
|||||||
swipeThreshold: 100,
|
swipeThreshold: 100,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const pasteStyleList = ['markdown', 'HTML', 'URL', 'UBB', 'Custom']
|
||||||
|
const shortURLList = [t('pages.gallery.shortUrl'), t('pages.gallery.longUrl')]
|
||||||
const advancedRenameList = {
|
const advancedRenameList = {
|
||||||
categoryTime: [
|
categoryTime: [
|
||||||
{ label: t('pages.settings.upload.placeholder.year4'), value: '{Y}' },
|
{ 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}' },
|
{ 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 matchedCount = computed(() => {
|
||||||
const matches = filterList.value.filter((item: any) => {
|
const matches = filterList.value.filter((item: any) => {
|
||||||
@@ -692,6 +692,10 @@ const matchedCount = computed(() => {
|
|||||||
return matches.length
|
return matches.length
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const filterList = computed(() => {
|
||||||
|
return getGallery()
|
||||||
|
})
|
||||||
|
|
||||||
const matchedUrls = computed(() => {
|
const matchedUrls = computed(() => {
|
||||||
const matches = filterList.value.filter((item: any) => {
|
const matches = filterList.value.filter((item: any) => {
|
||||||
return customStrMatch(item.imgUrl, batchRenameMatch.value)
|
return customStrMatch(item.imgUrl, batchRenameMatch.value)
|
||||||
@@ -699,33 +703,6 @@ const matchedUrls = computed(() => {
|
|||||||
return matches.map((item: any) => item.imgUrl || '').filter(Boolean)
|
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(() => {
|
const isAllSelected = computed(() => {
|
||||||
return Object.values(choosedList).length > 0 && filterList.value.every(item => choosedList[item.id!])
|
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) {
|
function onImageLoad(id: string) {
|
||||||
imageLoadStates[id] = true
|
imageLoadStates[id] = true
|
||||||
}
|
}
|
||||||
@@ -1012,15 +1056,6 @@ function getViewModeLabel() {
|
|||||||
return t(`pages.gallery.${viewMode.value}View`)
|
return t(`pages.gallery.${viewMode.value}View`)
|
||||||
}
|
}
|
||||||
|
|
||||||
onBeforeRouteUpdate((to, from) => {
|
|
||||||
if (from.name === 'gallery') {
|
|
||||||
clearChoosedList()
|
|
||||||
}
|
|
||||||
if (to.name === 'gallery') {
|
|
||||||
updateGallery()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
async function initConf() {
|
async function initConf() {
|
||||||
const settingConfig = await getConfig<any>('settings')
|
const settingConfig = await getConfig<any>('settings')
|
||||||
pasteStyle.value = settingConfig.pasteStyle || IPasteStyle.MARKDOWN
|
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) {
|
function handleChooseImage(val: boolean, index: number) {
|
||||||
const currentItem = filterList.value[index]
|
const currentItem = filterList.value[index]
|
||||||
if (currentItem && currentItem.id) {
|
if (currentItem && currentItem.id) {
|
||||||
@@ -1425,18 +1425,6 @@ function toggleHandleBar() {
|
|||||||
handleBarActive.value = !handleBarActive.value
|
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') {
|
function sortFile(type: 'name' | 'time' | 'ext' | 'check') {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'name':
|
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 () => {
|
onBeforeMount(async () => {
|
||||||
window.electron.ipcRendererOn('updateGallery', updateGalleryHandler)
|
window.electron.ipcRendererOn('updateGallery', updateGalleryHandler)
|
||||||
updateGallery()
|
updateGallery()
|
||||||
@@ -1574,17 +1582,6 @@ onBeforeUnmount(async () => {
|
|||||||
if (searchURLDebounceTimer) clearTimeout(searchURLDebounceTimer)
|
if (searchURLDebounceTimer) clearTimeout(searchURLDebounceTimer)
|
||||||
isAlwaysForceReload.value = (await getConfig(configPaths.settings.isAlwaysForceReload)) || false
|
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>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
|||||||
@@ -214,11 +214,11 @@ import {
|
|||||||
import { useStorage } from '@vueuse/core'
|
import { useStorage } from '@vueuse/core'
|
||||||
import { pick } from 'lodash-es'
|
import { pick } from 'lodash-es'
|
||||||
import {
|
import {
|
||||||
BriefcaseBusiness,
|
|
||||||
CheckIcon,
|
CheckIcon,
|
||||||
ChevronDownIcon,
|
ChevronDownIcon,
|
||||||
ChevronLeftIcon,
|
ChevronLeftIcon,
|
||||||
ChevronRightIcon,
|
ChevronRightIcon,
|
||||||
|
Cloud,
|
||||||
CopyIcon,
|
CopyIcon,
|
||||||
DatabaseIcon,
|
DatabaseIcon,
|
||||||
ImagesIcon,
|
ImagesIcon,
|
||||||
@@ -259,6 +259,20 @@ const guideRef = ref<InstanceType<typeof FirstTimeGuide> | null>(null)
|
|||||||
|
|
||||||
let removeIpcListener: () => void = () => {}
|
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(
|
watch(
|
||||||
() => choosedPicBedForQRCode,
|
() => choosedPicBedForQRCode,
|
||||||
val => {
|
val => {
|
||||||
@@ -273,8 +287,6 @@ watch(
|
|||||||
{ deep: true },
|
{ deep: true },
|
||||||
)
|
)
|
||||||
|
|
||||||
const visiblePicBeds = computed(() => picBedG.value.filter(item => item.visible))
|
|
||||||
|
|
||||||
const qrCodeHandler = () => {
|
const qrCodeHandler = () => {
|
||||||
qrcodeVisible.value = true
|
qrcodeVisible.value = true
|
||||||
}
|
}
|
||||||
@@ -314,18 +326,6 @@ function isPicBedPathActive(type: string): boolean {
|
|||||||
return route.name === routerConfig.UPLOADER_CONFIG_PAGE && route.params.type === type
|
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() {
|
function openGithubPage() {
|
||||||
window.electron.sendRPC(IRPCActionType.OPEN_URL, 'https://github.com/Kuingsmile/PicList')
|
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
|
type.value = $route.params.type as string
|
||||||
|
|
||||||
onBeforeMount(async () => {
|
|
||||||
try {
|
|
||||||
await getPicBeds()
|
|
||||||
await getPicBedConfigList()
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Initialization error:', error)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
function toggleDropdown() {
|
function toggleDropdown() {
|
||||||
dropdownVisible.value = !dropdownVisible.value
|
dropdownVisible.value = !dropdownVisible.value
|
||||||
}
|
}
|
||||||
@@ -231,7 +222,6 @@ const handleReset = async () => {
|
|||||||
try {
|
try {
|
||||||
await window.electron.triggerRPC<void>(IRPCActionType.UPLOADER_RESET_CONFIG, type.value, $route.params.configId)
|
await window.electron.triggerRPC<void>(IRPCActionType.UPLOADER_RESET_CONFIG, type.value, $route.params.configId)
|
||||||
message.success(t('pages.picBedConfigs.resetSuccess'))
|
message.success(t('pages.picBedConfigs.resetSuccess'))
|
||||||
$router.back()
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to reset configuration:', error)
|
console.error('Failed to reset configuration:', error)
|
||||||
message.error(t('pages.picBedConfigs.resetFailed'))
|
message.error(t('pages.picBedConfigs.resetFailed'))
|
||||||
@@ -278,6 +268,15 @@ async function handleCopyApi() {
|
|||||||
message.error(t('pages.picBedConfigs.copyAPIFailed'))
|
message.error(t('pages.picBedConfigs.copyAPIFailed'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onBeforeMount(async () => {
|
||||||
|
try {
|
||||||
|
await getPicBeds()
|
||||||
|
await getPicBedConfigList()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Initialization error:', error)
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
|||||||
@@ -461,11 +461,6 @@ const browseSearchText = ref('')
|
|||||||
const browsePlugins = ref<IPicGoPlugin[]>([])
|
const browsePlugins = ref<IPicGoPlugin[]>([])
|
||||||
const loadingBrowse = ref(false)
|
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(() => {
|
const npmSearchText = computed(() => {
|
||||||
return searchText.value.match('picgo-plugin-')
|
return searchText.value.match('picgo-plugin-')
|
||||||
? searchText.value
|
? 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) {
|
async function getLatestVersionOfPlugIn(pluginName: string) {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`https://registry.npmjs.com/${pluginName}`)
|
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
|
loading.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
const picgoHandlePluginDoneHandler = (fullName: string) => {
|
function picgoHandlePluginDoneHandler(fullName: string) {
|
||||||
pluginList.value.forEach(item => {
|
pluginList.value.forEach(item => {
|
||||||
if (item.fullName === fullName || item.name === fullName) {
|
if (item.fullName === fullName || item.name === fullName) {
|
||||||
item.ing = false
|
item.ing = false
|
||||||
@@ -531,7 +531,7 @@ const picgoHandlePluginDoneHandler = (fullName: string) => {
|
|||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
const pluginListHandler = (list: IPicGoPlugin[]) => {
|
function pluginListHandler(list: IPicGoPlugin[]) {
|
||||||
pluginList.value = list
|
pluginList.value = list
|
||||||
pluginNameList.value = list.map(item => item.fullName)
|
pluginNameList.value = list.map(item => item.fullName)
|
||||||
for (const item of pluginList.value) {
|
for (const item of pluginList.value) {
|
||||||
@@ -540,7 +540,7 @@ const pluginListHandler = (list: IPicGoPlugin[]) => {
|
|||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
const installPluginHandler = ({ success, body }: { success: boolean; body: string }) => {
|
function installPluginHandler({ success, body }: { success: boolean; body: string }) {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
pluginList.value.forEach(item => {
|
pluginList.value.forEach(item => {
|
||||||
if (item.fullName === body) {
|
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
|
loading.value = false
|
||||||
pluginList.value.forEach(item => {
|
pluginList.value.forEach(item => {
|
||||||
if (item.fullName === plugin) {
|
if (item.fullName === plugin) {
|
||||||
@@ -570,7 +570,7 @@ const updateSuccessHandler = (plugin: string) => {
|
|||||||
getPluginList()
|
getPluginList()
|
||||||
}
|
}
|
||||||
|
|
||||||
const uninstallSuccessHandler = (plugin: string) => {
|
function uninstallSuccessHandler(plugin: string) {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
pluginList.value = pluginList.value.filter(item => {
|
pluginList.value = pluginList.value.filter(item => {
|
||||||
if (item.fullName === plugin) {
|
if (item.fullName === plugin) {
|
||||||
@@ -588,18 +588,18 @@ const uninstallSuccessHandler = (plugin: string) => {
|
|||||||
pluginNameList.value = pluginNameList.value.filter(item => item !== plugin)
|
pluginNameList.value = pluginNameList.value.filter(item => item !== plugin)
|
||||||
}
|
}
|
||||||
|
|
||||||
const picgoConfigPluginHandler = (
|
function picgoConfigPluginHandler(
|
||||||
_currentType: 'plugin' | 'transformer' | 'uploader',
|
_currentType: 'plugin' | 'transformer' | 'uploader',
|
||||||
_configName: string,
|
_configName: string,
|
||||||
_config: any,
|
_config: any,
|
||||||
) => {
|
) {
|
||||||
currentType.value = _currentType
|
currentType.value = _currentType
|
||||||
configName.value = _configName
|
configName.value = _configName
|
||||||
config.value = _config
|
config.value = _config
|
||||||
dialogVisible.value = true
|
dialogVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
const picgoHandlePluginIngHandler = (fullName: string) => {
|
function picgoHandlePluginIngHandler(fullName: string) {
|
||||||
pluginList.value.forEach(item => {
|
pluginList.value.forEach(item => {
|
||||||
if (item.fullName === fullName || item.name === fullName) {
|
if (item.fullName === fullName || item.name === fullName) {
|
||||||
item.ing = true
|
item.ing = true
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ const form = reactive({
|
|||||||
originName: '',
|
originName: '',
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleFileName = (newName: string, _originName: string, _id: string) => {
|
function handleFileName(newName: string, _originName: string, _id: string) {
|
||||||
form.fileName = newName
|
form.fileName = newName
|
||||||
form.originName = _originName
|
form.originName = _originName
|
||||||
id.value = _id
|
id.value = _id
|
||||||
|
|||||||
@@ -138,16 +138,6 @@ const command = ref('')
|
|||||||
const shortKey = ref('')
|
const shortKey = ref('')
|
||||||
const currentIndex = ref(0)
|
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) => {
|
watch(keyBindingVisible, (val: boolean) => {
|
||||||
window.electron.sendRPC(IRPCActionType.SHORTKEY_TOGGLE_SHORTKEY_MODIFIED_MODE, val)
|
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(() => {
|
onBeforeUnmount(() => {
|
||||||
window.electron.sendRPC(IRPCActionType.SHORTKEY_TOGGLE_SHORTKEY_MODIFIED_MODE, false)
|
window.electron.sendRPC(IRPCActionType.SHORTKEY_TOGGLE_SHORTKEY_MODIFIED_MODE, false)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ import { IRPCActionType } from '@/utils/enum'
|
|||||||
const isShowprogress = ref(false)
|
const isShowprogress = ref(false)
|
||||||
const progress = ref(0)
|
const progress = ref(0)
|
||||||
const isAlwaysOnTop = ref(false)
|
const isAlwaysOnTop = ref(false)
|
||||||
|
|
||||||
const defaultLogo = computed(() => `${import.meta.env.BASE_URL}roundLogo.png`)
|
const defaultLogo = computed(() => `${import.meta.env.BASE_URL}roundLogo.png`)
|
||||||
|
|
||||||
function setAlwaysOnTop() {
|
function setAlwaysOnTop() {
|
||||||
@@ -69,7 +70,7 @@ const minimizeWindow = () => window.electron.sendRPC(IRPCActionType.MINIMIZE_WIN
|
|||||||
const openMiniWindow = () => window.electron.sendRPC(IRPCActionType.OPEN_MINI_WINDOW)
|
const openMiniWindow = () => window.electron.sendRPC(IRPCActionType.OPEN_MINI_WINDOW)
|
||||||
const closeWindow = () => window.electron.sendRPC(IRPCActionType.CLOSE_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
|
isShowprogress.value = data.progress !== 100 && data.progress !== 0
|
||||||
progress.value = data.progress
|
progress.value = data.progress
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -122,7 +122,6 @@ import { IRPCActionType, IToolboxItemCheckStatus, IToolboxItemType } from '@/uti
|
|||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const { confirm } = useConfirm()
|
const { confirm } = useConfirm()
|
||||||
const activeTypes = ref<string[]>([])
|
const activeTypes = ref<string[]>([])
|
||||||
const defaultLogo = computed(() => `${import.meta.env.BASE_URL}roundLogo.png`)
|
|
||||||
const fixList = reactive<IToolboxMap>({
|
const fixList = reactive<IToolboxMap>({
|
||||||
[IToolboxItemType.IS_CONFIG_FILE_BROKEN]: {
|
[IToolboxItemType.IS_CONFIG_FILE_BROKEN]: {
|
||||||
title: t('pages.toolbox.checkConfigFileBroken'),
|
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 progress = computed(() => {
|
||||||
const total = Object.keys(fixList).length
|
const total = Object.keys(fixList).length
|
||||||
const done = Object.keys(fixList).filter(key => {
|
const done = Object.keys(fixList).filter(key => {
|
||||||
@@ -181,7 +182,7 @@ const canFixLength = computed(() => {
|
|||||||
}).length
|
}).length
|
||||||
})
|
})
|
||||||
|
|
||||||
const toggleItem = (key: string) => {
|
function toggleItem(key: string) {
|
||||||
const index = activeTypes.value.indexOf(key)
|
const index = activeTypes.value.indexOf(key)
|
||||||
if (index > -1) {
|
if (index > -1) {
|
||||||
activeTypes.value.splice(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].status = status
|
||||||
fixList[type].msg = msg
|
fixList[type].msg = msg
|
||||||
fixList[type].value = value
|
fixList[type].value = value
|
||||||
@@ -199,9 +200,7 @@ const toolboxCheckResHandler = ({ type, msg = '', status, value = '' }: IToolbox
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
window.electron.ipcRendererOn(IRPCActionType.TOOLBOX_CHECK_RES, toolboxCheckResHandler)
|
function handleCheck() {
|
||||||
|
|
||||||
const handleCheck = () => {
|
|
||||||
activeTypes.value = []
|
activeTypes.value = []
|
||||||
Object.keys(fixList).forEach(key => {
|
Object.keys(fixList).forEach(key => {
|
||||||
fixList[key].status = IToolboxItemCheckStatus.LOADING
|
fixList[key].status = IToolboxItemCheckStatus.LOADING
|
||||||
@@ -211,7 +210,7 @@ const handleCheck = () => {
|
|||||||
window.electron.sendRPC(IRPCActionType.TOOLBOX_CHECK)
|
window.electron.sendRPC(IRPCActionType.TOOLBOX_CHECK)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleFix = async () => {
|
async function handleFix() {
|
||||||
const fixRes = await Promise.all(
|
const fixRes = await Promise.all(
|
||||||
Object.keys(fixList)
|
Object.keys(fixList)
|
||||||
.filter(key => {
|
.filter(key => {
|
||||||
@@ -246,6 +245,8 @@ const handleFix = async () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
window.electron.ipcRendererOn(IRPCActionType.TOOLBOX_CHECK_RES, toolboxCheckResHandler)
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
window.electron.ipcRendererRemoveAllListeners(IRPCActionType.TOOLBOX_CHECK_RES)
|
window.electron.ipcRendererRemoveAllListeners(IRPCActionType.TOOLBOX_CHECK_RES)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -109,19 +109,19 @@ import { getConfig } from '@/utils/dataSender'
|
|||||||
import $$db from '@/utils/db'
|
import $$db from '@/utils/db'
|
||||||
import { IPasteStyle, IRPCActionType, IWindowList } from '@/utils/enum'
|
import { IPasteStyle, IRPCActionType, IWindowList } from '@/utils/enum'
|
||||||
|
|
||||||
const { t } = useI18n()
|
|
||||||
|
|
||||||
type IResult<T> = T & {
|
type IResult<T> = T & {
|
||||||
id: string
|
id: string
|
||||||
createdAt: number
|
createdAt: number
|
||||||
updatedAt: number
|
updatedAt: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
const files = ref<IResult<ImgInfo>[]>([])
|
const files = ref<IResult<ImgInfo>[]>([])
|
||||||
const notification = reactive({
|
const notification = reactive({
|
||||||
title: t('pages.tray.copySuccess'),
|
title: t('pages.tray.copySuccess'),
|
||||||
body: '',
|
body: '',
|
||||||
})
|
})
|
||||||
|
|
||||||
const clipboardFiles = ref<ImgInfo[]>([])
|
const clipboardFiles = ref<ImgInfo[]>([])
|
||||||
const uploadFlag = ref(false)
|
const uploadFlag = ref(false)
|
||||||
|
|
||||||
@@ -133,7 +133,7 @@ async function getData() {
|
|||||||
files.value = (await $$db.get<ImgInfo>({ orderBy: 'desc', limit: 10 }))!.data
|
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 fileName = item.fileName!.replace(new RegExp(`\\${item.extname}$`), '')
|
||||||
const url = item.url || item.imgUrl
|
const url = item.url || item.imgUrl
|
||||||
const extName = item.extname
|
const extName = item.extname
|
||||||
@@ -222,7 +222,7 @@ function onImageError(event: Event) {
|
|||||||
img.src = './errorLoading.png'
|
img.src = './errorLoading.png'
|
||||||
}
|
}
|
||||||
|
|
||||||
const dragFilesHandler = async (_files: string[]) => {
|
async function dragFilesHandler(_files: string[]) {
|
||||||
for (const file of _files) {
|
for (const file of _files) {
|
||||||
await $$db.insert(file)
|
await $$db.insert(file)
|
||||||
}
|
}
|
||||||
@@ -232,11 +232,11 @@ const dragFilesHandler = async (_files: string[]) => {
|
|||||||
}))!.data
|
}))!.data
|
||||||
}
|
}
|
||||||
|
|
||||||
const clipboardFilesHandler = (files: ImgInfo[]) => {
|
function clipboardFilesHandler(files: ImgInfo[]) {
|
||||||
clipboardFiles.value = files
|
clipboardFiles.value = files
|
||||||
}
|
}
|
||||||
|
|
||||||
const uploadFilesHandler = async () => {
|
async function uploadFilesHandler() {
|
||||||
files.value = (await $$db.get<ImgInfo>({
|
files.value = (await $$db.get<ImgInfo>({
|
||||||
orderBy: 'desc',
|
orderBy: 'desc',
|
||||||
limit: 5,
|
limit: 5,
|
||||||
@@ -244,7 +244,7 @@ const uploadFilesHandler = async () => {
|
|||||||
uploadFlag.value = false
|
uploadFlag.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateFilesHandler = () => {
|
function updateFilesHandler() {
|
||||||
getData()
|
getData()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -118,22 +118,22 @@ const updateInfo = ref<UpdateInfo>({
|
|||||||
const dontShowAgain = ref(false)
|
const dontShowAgain = ref(false)
|
||||||
const downloadProgress = ref<number | null>(null)
|
const downloadProgress = ref<number | null>(null)
|
||||||
|
|
||||||
const handleUpdateInfo = (info: UpdateInfo) => {
|
function handleUpdateInfo(info: UpdateInfo) {
|
||||||
updateInfo.value = info
|
updateInfo.value = info
|
||||||
if (info.type !== 'downloading') {
|
if (info.type !== 'downloading') {
|
||||||
downloadProgress.value = null
|
downloadProgress.value = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleUpdateProgress = (progress: { progress: number }) => {
|
function handleUpdateProgress(progress: { progress: number }) {
|
||||||
downloadProgress.value = progress.progress
|
downloadProgress.value = progress.progress
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderMarkdown = (content: string) => {
|
function renderMarkdown(content: string) {
|
||||||
return marked(content, { breaks: true, gfm: true })
|
return marked(content, { breaks: true, gfm: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
const downloadUpdate = () => {
|
function downloadUpdate() {
|
||||||
updateInfo.value.type = 'downloading'
|
updateInfo.value.type = 'downloading'
|
||||||
downloadProgress.value = 0
|
downloadProgress.value = 0
|
||||||
window.electron.sendRPC(IRPCActionType.DOWNLOAD_UPDATE)
|
window.electron.sendRPC(IRPCActionType.DOWNLOAD_UPDATE)
|
||||||
@@ -142,7 +142,7 @@ const downloadUpdate = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const goToDownloadPage = () => {
|
function goToDownloadPage() {
|
||||||
window.electron.sendRPC(IRPCActionType.GO_TO_DOWNLOAD_PAGE)
|
window.electron.sendRPC(IRPCActionType.GO_TO_DOWNLOAD_PAGE)
|
||||||
if (dontShowAgain.value) {
|
if (dontShowAgain.value) {
|
||||||
window.electron.sendRPC(IRPCActionType.SET_SHOW_UPDATE_TIP, false)
|
window.electron.sendRPC(IRPCActionType.SET_SHOW_UPDATE_TIP, false)
|
||||||
@@ -150,11 +150,11 @@ const goToDownloadPage = () => {
|
|||||||
closeWindow()
|
closeWindow()
|
||||||
}
|
}
|
||||||
|
|
||||||
const installUpdate = () => {
|
function installUpdate() {
|
||||||
window.electron.sendRPC(IRPCActionType.INSTALL_UPDATE)
|
window.electron.sendRPC(IRPCActionType.INSTALL_UPDATE)
|
||||||
}
|
}
|
||||||
|
|
||||||
const closeWindow = () => {
|
function closeWindow() {
|
||||||
if (dontShowAgain.value && updateInfo.value.type === 'update-available') {
|
if (dontShowAgain.value && updateInfo.value.type === 'update-available') {
|
||||||
window.electron.sendRPC(IRPCActionType.SET_SHOW_UPDATE_TIP, false)
|
window.electron.sendRPC(IRPCActionType.SET_SHOW_UPDATE_TIP, false)
|
||||||
}
|
}
|
||||||
@@ -162,6 +162,7 @@ const closeWindow = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let unbindThemeListener: (() => void) | null = null
|
let unbindThemeListener: (() => void) | null = null
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
window.electron.ipcRendererOn(SHOW_UPDATE_INFO, handleUpdateInfo)
|
window.electron.ipcRendererOn(SHOW_UPDATE_INFO, handleUpdateInfo)
|
||||||
window.electron.ipcRendererOn(UPDATE_PROGRESS, handleUpdateProgress)
|
window.electron.ipcRendererOn(UPDATE_PROGRESS, handleUpdateProgress)
|
||||||
|
|||||||
@@ -749,26 +749,13 @@ const pasteStyle = ref(IPasteStyle.MARKDOWN)
|
|||||||
const PicBedId = ref('')
|
const PicBedId = ref('')
|
||||||
const fileInput = useTemplateRef('fileInput')
|
const fileInput = useTemplateRef('fileInput')
|
||||||
const uploadInterval = ref(1000)
|
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 showTaskSettings = useStorage('upload-task-queue-show-settings', true)
|
||||||
const taskSearchQuery = ref('')
|
const taskSearchQuery = ref('')
|
||||||
const taskFilter = ref<'all' | 'pending' | 'completed' | 'failed'>('all')
|
const taskFilter = ref<'all' | 'pending' | 'completed' | 'failed'>('all')
|
||||||
const autoStart = ref(false)
|
const autoStart = ref(false)
|
||||||
const pauseOnError = ref(false)
|
const pauseOnError = ref(false)
|
||||||
const maxRetryCount = ref(3)
|
const maxRetryCount = ref(3)
|
||||||
|
const favoritePicbeds = useStorage<IFavoritePicbedItem[]>('favorite-picbeds', [])
|
||||||
// Task queue status
|
|
||||||
const taskQueueStatus = reactive<IUploadTaskQueueStatus>({
|
const taskQueueStatus = reactive<IUploadTaskQueueStatus>({
|
||||||
tasks: [],
|
tasks: [],
|
||||||
config: {
|
config: {
|
||||||
@@ -792,8 +779,24 @@ const taskQueueStatus = reactive<IUploadTaskQueueStatus>({
|
|||||||
estimatedTimeMs: 0,
|
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(() => {
|
const filteredTasks = computed(() => {
|
||||||
let tasks = taskQueueStatus.tasks
|
let tasks = taskQueueStatus.tasks
|
||||||
|
|
||||||
@@ -824,18 +827,13 @@ const picBedName = computed(() => {
|
|||||||
return target ? target.name : defaultPicBedG.value
|
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 {
|
function syncPicBedHandler(): void {
|
||||||
updatePicBeds()
|
updatePicBeds()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
watch(progress, onProgressChange)
|
||||||
|
watch(favoritePicbeds, valideFavoritePicbeds, { immediate: true })
|
||||||
|
|
||||||
let removeUploadProgressListenerCallback: () => void = () => {}
|
let removeUploadProgressListenerCallback: () => void = () => {}
|
||||||
let removeSyncPicBedListenerCallback: () => void = () => {}
|
let removeSyncPicBedListenerCallback: () => void = () => {}
|
||||||
|
|
||||||
@@ -849,18 +847,16 @@ function uploadProgressHandler(p: number): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleImageProcess = () => {
|
function handleImageProcess() {
|
||||||
PicBedId.value = ''
|
PicBedId.value = ''
|
||||||
imageProcessDialogVisible.value = true
|
imageProcessDialogVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleImageProcessSingle = () => {
|
function handleImageProcessSingle() {
|
||||||
PicBedId.value = defaultIdG.value
|
PicBedId.value = defaultIdG.value
|
||||||
imageProcessDialogVisible.value = true
|
imageProcessDialogVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(progress, onProgressChange)
|
|
||||||
|
|
||||||
function onProgressChange(val: number) {
|
function onProgressChange(val: number) {
|
||||||
if (val === 100) {
|
if (val === 100) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -979,8 +975,6 @@ async function valideFavoritePicbeds() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(favoritePicbeds, valideFavoritePicbeds, { immediate: true })
|
|
||||||
|
|
||||||
function addCurrentPicbedToFavorites() {
|
function addCurrentPicbedToFavorites() {
|
||||||
favoritePicbeds.value.push({
|
favoritePicbeds.value.push({
|
||||||
id: defaultIdG.value,
|
id: defaultIdG.value,
|
||||||
|
|||||||
@@ -35,6 +35,7 @@
|
|||||||
<div class="no-scrollbar h-full w-full overflow-auto rounded-sm">
|
<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 class="grid w-full grid-cols-[repeat(auto-fill,minmax(300px,1fr))] gap-5 border-none p-1 max-md:gap-4">
|
||||||
<!-- Config Items -->
|
<!-- Config Items -->
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-for="(item, index) in curConfigList"
|
v-for="(item, index) in curConfigList"
|
||||||
:key="item._id"
|
:key="item._id"
|
||||||
@@ -162,7 +163,6 @@ const router = useRouter()
|
|||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const { defaultPicBedG, picBedG, updatePicBeds } = usePicBed()
|
const { defaultPicBedG, picBedG, updatePicBeds } = usePicBed()
|
||||||
const favoritePicbeds = useStorage<IFavoritePicbedItem[]>('favorite-picbeds', [])
|
const favoritePicbeds = useStorage<IFavoritePicbedItem[]>('favorite-picbeds', [])
|
||||||
|
|
||||||
const type = ref('')
|
const type = ref('')
|
||||||
const curConfigList = ref<IStringKeyMap[]>([])
|
const curConfigList = ref<IStringKeyMap[]>([])
|
||||||
const defaultConfigId = ref('')
|
const defaultConfigId = ref('')
|
||||||
@@ -203,19 +203,6 @@ async function selectItem(id: string) {
|
|||||||
defaultConfigId.value = id
|
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() {
|
async function getCurrentConfigList() {
|
||||||
const configList = await window.electron.triggerRPC<IUploaderConfigItem>(
|
const configList = await window.electron.triggerRPC<IUploaderConfigItem>(
|
||||||
IRPCActionType.PICBED_GET_CONFIG_LIST,
|
IRPCActionType.PICBED_GET_CONFIG_LIST,
|
||||||
@@ -328,7 +315,21 @@ function setDefaultPicBed(type: string) {
|
|||||||
updatePicBeds()
|
updatePicBeds()
|
||||||
message.success(t('pages.uploaderConfig.setSuccess'))
|
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>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export default {
|
export default {
|
||||||
name: 'UploaderConfigPage',
|
name: 'UploaderConfigPage',
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export const MANAGE_EMPTY_PAGE = 'ManageEmptyPage'
|
|||||||
export const MANAGE_LOGIN_PAGE = 'ManageLoginPage'
|
export const MANAGE_LOGIN_PAGE = 'ManageLoginPage'
|
||||||
export const MANAGE_MAIN_PAGE = 'ManageMainPage'
|
export const MANAGE_MAIN_PAGE = 'ManageMainPage'
|
||||||
export const MANAGE_SETTING_PAGE = 'ManageSettingPage'
|
export const MANAGE_SETTING_PAGE = 'ManageSettingPage'
|
||||||
|
export const MANAGE_SETTING_PAGE_DIRECT = 'ManageSettingPageDirect'
|
||||||
export const MAIN_PAGE = 'MainPage'
|
export const MAIN_PAGE = 'MainPage'
|
||||||
export const MINI_PAGE = 'MiniPage'
|
export const MINI_PAGE = 'MiniPage'
|
||||||
export const PICBEDS_PAGE = 'PicbedsPage'
|
export const PICBEDS_PAGE = 'PicbedsPage'
|
||||||
@@ -17,3 +18,4 @@ export const TRAY_PAGE = 'TrayPage'
|
|||||||
export const UPDATE_PAGE = 'UpdatePage'
|
export const UPDATE_PAGE = 'UpdatePage'
|
||||||
export const UPLOAD_PAGE = 'UploadPage'
|
export const UPLOAD_PAGE = 'UploadPage'
|
||||||
export const UPLOADER_CONFIG_PAGE = 'UploaderConfigPage'
|
export const UPLOADER_CONFIG_PAGE = 'UploaderConfigPage'
|
||||||
|
export const MANAGE_EDIT_PAGE = 'ManageEditPage'
|
||||||
|
|||||||
@@ -48,6 +48,12 @@ export default createRouter({
|
|||||||
component: UploadPage,
|
component: UploadPage,
|
||||||
name: config.UPLOAD_PAGE,
|
name: config.UPLOAD_PAGE,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'manage-setting-page',
|
||||||
|
name: config.MANAGE_SETTING_PAGE_DIRECT,
|
||||||
|
component: ManageSettingPage,
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
path: 'manage-main-page',
|
path: 'manage-main-page',
|
||||||
name: config.MANAGE_MAIN_PAGE,
|
name: config.MANAGE_MAIN_PAGE,
|
||||||
|
|||||||
Reference in New Issue
Block a user