Feature(custom): optimize picbed config page

This commit is contained in:
Kuingsmile
2026-01-22 22:22:32 +08:00
parent 547588c670
commit 80523e0c1e
18 changed files with 297 additions and 603 deletions

View File

@@ -1,153 +1,151 @@
<!-- eslint-disable vue/no-v-html -->
<template>
<div id="config-form" :class="[{ white: colorMode === 'white' }]">
<form class="config-form" @submit.prevent>
<!-- Config Name Field -->
<div class="form-group required">
<label class="form-label">{{ t('pages.configForm.configName') }}</label>
<div class="form-control">
<input
v-model="ruleForm._configName"
type="text"
class="form-input"
:placeholder="t('pages.configForm.configNamePlaceholder')"
:class="{ error: validationErrors._configName }"
@input="clearFieldError('_configName')"
/>
<div v-if="validationErrors._configName" class="error-message">
<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>
<SettingCard>
<CustomInput
v-model="ruleForm._configName"
:title="t('pages.configForm.configName')"
:placeholder="t('pages.configForm.configNamePlaceholder')"
required
@input="clearFieldError('_configName')"
/>
<template v-if="validationErrors._configName" #extra>
<div class="mt-1 text-xs text-error">
{{ validationErrors._configName }}
</div>
</div>
</div>
</template>
</SettingCard>
<!-- Dynamic Config Fields -->
<div
<SettingCard
v-for="(item, index) in configList"
:key="item.name + index"
class="form-group"
:class="{ required: item.required }"
:p1="item.type === 'confirm'"
>
<div class="form-label-wrapper">
<label class="form-label">{{ item.alias || item.name }}</label>
<div v-if="showTooltips && item.tips" class="tooltip-wrapper">
<div class="info-icon" @click="toggleTooltip(item.name + index)">
<Info :size="20" />
<CustomInput
v-if="item.type === 'input' || item.type === 'password'"
v-model="ruleForm[item.name]"
type="text"
:placeholder="item.message || item.name"
:class="{ 'border-error!': validationErrors[item.name] }"
:title="item.alias || item.name"
@input="clearFieldError(item.name)"
>
<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>
<div
v-show="visibleTooltips[item.name + index]"
class="tooltip-content"
v-html="transformMarkdownToHTML(item.tips)"
/>
</div>
</div>
<div class="form-control">
<!-- Text/Password Input -->
<input
v-if="item.type === 'input' || item.type === 'password'"
v-model="ruleForm[item.name]"
type="text"
class="form-input"
:placeholder="item.message || item.name"
:class="{ error: validationErrors[item.name] }"
@input="clearFieldError(item.name)"
/>
<!-- Select (Single) -->
<div v-else-if="item.type === 'list' && item.choices" class="select-wrapper">
<select
v-model="ruleForm[item.name]"
class="form-select"
:class="{ error: validationErrors[item.name] }"
@change="clearFieldError(item.name)"
>
<option value="" disabled>
{{ item.message || item.name }}
</option>
<option
v-for="choice in item.choices"
:key="choice.name || choice.value || choice"
:value="choice.value || choice"
>
{{ choice.name || choice.value || choice }}
</option>
</select>
<div class="select-arrow">
<ChevronDownIcon :size="20" />
</div>
</div>
<!-- Multi-Select (Checkbox style) -->
<div v-else-if="item.type === 'checkbox' && item.choices" class="checkbox-group">
<div v-for="choice in item.choices" :key="choice.value || choice" class="checkbox-item">
<label class="checkbox-label">
<input
type="checkbox"
:value="choice.value || choice"
:checked="Array.isArray(ruleForm[item.name]) && ruleForm[item.name].includes(choice.value || choice)"
class="checkbox-input"
@change="handleCheckboxChange(item.name, choice.value || choice, $event)"
/>
<span class="checkbox-custom" />
<span class="checkbox-text">{{ choice.name || choice.value || choice }}</span>
</label>
</div>
</div>
<!-- Switch/Toggle -->
<label v-else-if="item.type === 'confirm'" class="switch-label">
<input
v-model="ruleForm[item.name]"
type="checkbox"
class="switch-input"
@change="clearFieldError(item.name)"
/>
<span class="switch-slider">
<span class="switch-button" />
</span>
<span class="switch-text">
</template>
</CustomInput>
<CustomSwitch
v-if="item.type === 'confirm'"
v-model="ruleForm[item.name]"
:title="item.alias || item.name"
:description="item.message || ''"
no-border
small
@change="clearFieldError(item.name)"
>
<template #switch-text>
<span class="text-[0.925rem] font-semibold text-secondary">
{{ ruleForm[item.name] ? item.confirmText || 'Yes' : item.cancelText || 'No' }}
</span>
</label>
</template>
<template #title-extra>
<div v-if="showTooltips && item.tips" class="relative">
<div class="info-icon" @click="toggleTooltip(item.name + index)">
<Info :size="15" />
</div>
<div
v-show="visibleTooltips[item.name + index]"
class="absolute top-full left-0 z-1000 max-w-[300px] min-w-[200px] rounded-md border border-border bg-bg-secondary p-3 text-xs leading-[1.4] text-main shadow-lg max-md:max-w-[250px] max-md:min-w-[150px]"
v-html="transformMarkdownToHTML(item.tips)"
/>
</div>
</template>
</CustomSwitch>
<CustomSelect
v-if="item.type === 'list' && item.choices"
v-model="ruleForm[item.name]"
:title="item.alias || item.name"
:placeholder="item.message || item.name"
:class="{ 'border-danger': validationErrors[item.name] }"
:select-list="
item.choices.map(choice => ({
value: choice.value || choice,
label: choice.name || choice.value || choice,
}))
"
:icon="null"
@change="clearFieldError(item.name)"
>
<template #pre-info>
<option value="" disabled>
{{ item.message || item.name }}
</option>
</template>
</CustomSelect>
<MultiSelect
v-if="item.type === 'checkbox' && item.choices"
v-model:choosed="ruleForm[item.name]"
:title="item.alias || item.name"
:zero-placeholder="item.message || item.name"
:icon="null"
:all-list="
item.choices.map(choice => ({
type: choice.value || choice,
name: choice.name || choice.value || choice,
}))
"
@change="clearFieldError(item.name)"
/>
<!-- Validation Error -->
<div v-if="validationErrors[item.name]" class="error-message">
<!-- Validation Error -->
<template v-if="validationErrors[item.name]" #extra>
<div class="mt-1 text-xs text-error">
{{ validationErrors[item.name] }}
</div>
</div>
</div>
</template>
</SettingCard>
<slot />
</form>
</SettingSection>
</div>
</template>
<script lang="ts" setup>
import { cloneDeep, union } from 'lodash-es'
import { ChevronDownIcon, Info } from 'lucide-vue-next'
import { Info } from 'lucide-vue-next'
import { marked } from 'marked'
import { reactive, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRoute } from 'vue-router'
import CustomInput from '@/components/common/CustomInput.vue'
import CustomSelect from '@/components/common/CustomSelect.vue'
import CustomSwitch from '@/components/common/CustomSwitch.vue'
import MultiSelect from '@/components/common/MultiSelect.vue'
import SettingCard from '@/components/common/SettingCard.vue'
import SettingSection from '@/components/common/SettingSection.vue'
import { getConfig } from '@/utils/dataSender'
interface IProps {
config: IPicGoPluginConfig[]
type: 'uploader' | 'transformer' | 'plugin'
id: string
colorMode?: 'white' | 'dark'
mode?: 'picbed' | 'plugin'
showTooltips?: boolean
}
const {
config: configProp,
type,
id,
colorMode = undefined,
mode = 'picbed',
showTooltips = true,
} = defineProps<IProps>()
const { config: configProp, type, id, mode = 'picbed', showTooltips = true } = defineProps<IProps>()
const $route = useRoute()
const { t } = useI18n()
@@ -172,7 +170,7 @@ watch(
function validateField(fieldName: string, value: any, _?: IPicGoPluginConfig): string | null {
if (fieldName === '_configName') {
if (!value || value.trim() === '') {
return 'Configuration name is required'
return t('pages.configForm.configNameRequired')
}
return null
}
@@ -202,6 +200,7 @@ function validateForm(): boolean {
}
function clearFieldError(fieldName: string) {
console.log('Clearing error for field:', fieldName)
if (validationErrors[fieldName]) {
delete validationErrors[fieldName]
}
@@ -217,25 +216,6 @@ function toggleTooltip(key: string) {
})
}
function handleCheckboxChange(fieldName: string, value: any, event: Event) {
const target = event.target as HTMLInputElement
const currentValues = Array.isArray(ruleForm[fieldName]) ? [...ruleForm[fieldName]] : []
if (target.checked) {
if (!currentValues.includes(value)) {
currentValues.push(value)
}
} else {
const index = currentValues.indexOf(value)
if (index > -1) {
currentValues.splice(index, 1)
}
}
ruleForm[fieldName] = currentValues
clearFieldError(fieldName)
}
async function validate(): Promise<IStringKeyMap | false> {
return new Promise(resolve => {
const isValid = validateForm()
@@ -338,5 +318,3 @@ defineExpose({
getConfigType,
})
</script>
<style scoped src="./css/UnifiedConfigForm.css"></style>

View File

@@ -1,7 +1,12 @@
<template>
<div class="flex flex-col">
<label class="mb-2 text-sm font-semibold text-secondary">{{ title }}</label>
<div class="flex items-center gap-2">
<label class="mb-2 text-sm font-semibold text-secondary"
>{{ title }}
<span v-if="required" class="ml-1 text-red-400">*</span>
</label>
<slot name="title-extra"></slot>
</div>
<div class="relative w-full">
<input
v-model="modelValue"
@@ -27,20 +32,28 @@
<script setup lang="ts">
import { EyeClosedIcon, EyeIcon } from 'lucide-vue-next'
import { onMounted, ref } from 'vue'
const modelValue = defineModel<any>()
const type = ref('text')
const {
isPassword = false,
title,
inputType = 'text',
placeholder,
required = false,
} = defineProps<{
isPassword?: boolean
title: string
inputType?: string
placeholder: string
required?: boolean
}>()
defineOptions({
inheritAttrs: false,
})
onMounted(() => {
if (isPassword) {
type.value = 'password'

View File

@@ -8,7 +8,9 @@
<select
v-model="modelValue"
class="border-box w-full rounded-md border border-border bg-bg-tertiary p-3 text-sm text-main transition-all duration-200 ease-apple focus:border-accent focus:outline-none"
v-bind="$attrs"
>
<slot name="pre-info"></slot>
<template v-if="selectList.length > 0">
<option v-for="item in selectList" :key="item.value" :value="item.value">
{{ item.label }}

View File

@@ -12,9 +12,13 @@
: 'h-[28px] w-[52px] before:top-[3px] before:left-[3px] before:h-[22px] before:w-[22px]'
"
/>
<div class="flex flex-1 flex-col gap-1">
<span class="text-[0.925rem] leading-[1.4] font-semibold text-secondary">{{ title }}</span>
<span class="text-xs text-secondary/90">{{ description }}</span>
<div class="flex flex-row items-center gap-1">
<div class="flex flex-1 flex-col gap-1">
<span class="text-[0.925rem] leading-[1.4] font-semibold text-secondary">{{ title }}</span>
<span v-if="!!description" class="text-xs text-secondary/90">{{ description }}</span>
</div>
<slot name="switch-text"></slot>
<slot name="title-extra"></slot>
</div>
</label>
</template>

View File

@@ -28,7 +28,7 @@
:key="item.type"
class="flex min-h-[unset] cursor-pointer items-center justify-between px-2 py-1 text-sm leading-[1.4] transition-all duration-fast ease-apple hover:bg-accent/50"
>
<input v-model="choosed" type="checkbox" :value="item.type" class="m-0" />
<input v-bind="$attrs" v-model="choosed" type="checkbox" :value="item.type" class="m-0" />
{{ item.name }}
</label>
</div>
@@ -38,7 +38,7 @@
<script setup lang="ts">
import { onClickOutside } from '@vueuse/core'
import { ChevronDownIcon } from 'lucide-vue-next'
import { nextTick, ref } from 'vue'
import { nextTick, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
const choosed = defineModel<string[]>('choosed')
@@ -81,4 +81,10 @@ const {
zeroPlaceholder: string
allList: any
}>()
onMounted(() => {
if (!Array.isArray(choosed.value)) {
choosed.value = []
}
})
</script>

View File

@@ -1,6 +1,7 @@
<template>
<div class="relative rounded-lg border border-border bg-bg-secondary shadow-sm" :class="p1 ? 'p-1' : 'p-4'">
<slot></slot>
<slot name="extra"></slot>
</div>
</template>

View File

@@ -1,334 +0,0 @@
#config-form {
width: 100%;
}
.config-form {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
/* Form Groups */
.form-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.form-group.required .form-label::after {
content: ' *';
color: var(--color-error);
}
.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);
line-height: 1.25;
}
.form-control {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
/* Tooltip Styles */
.tooltip-wrapper {
position: relative;
}
.info-icon {
display: flex;
justify-content: center;
align-items: center;
border-radius: var(--radius-round);
padding: 2px;
color: var(--color-text-secondary);
transition: var(--transition-fast);
cursor: pointer;
}
.info-icon:hover {
color: var(--color-accent);
background: rgb(0 122 255 / 10%);
}
.tooltip-content {
position: absolute;
top: 100%;
left: 0;
z-index: 1000;
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
padding: 0.75rem;
min-width: 200px;
max-width: 300px;
font-size: 0.75rem;
color: var(--color-text-primary);
background: var(--color-background-secondary);
box-shadow: var(--shadow-lg);
line-height: 1.4;
}
/* Input Styles */
.form-input {
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
padding: 0.75rem 1rem;
width: 100%;
font-size: 0.875rem;
font-family: inherit;
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-surface);
outline: none;
box-shadow: 0 0 0 2px rgb(0 122 255 / 20%);
}
.form-input::placeholder {
color: var(--color-text-secondary);
}
.form-input.error {
border-color: var(--color-error);
}
.form-input.error:focus {
box-shadow: 0 0 0 2px rgb(239 68 68 / 20%);
}
/* Select Styles */
.select-wrapper {
position: relative;
}
.form-select {
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
padding: 0.75rem 2.5rem 0.75rem 1rem;
width: 100%;
font-size: 0.875rem;
font-family: inherit;
color: var(--color-text-primary);
background: var(--color-surface-elevated);
transition: var(--transition-fast);
appearance: none;
cursor: pointer;
}
.form-select:focus {
border-color: var(--color-accent);
outline: none;
box-shadow: 0 0 0 2px rgb(0 122 255 / 20%);
}
.form-select.error {
border-color: var(--color-error);
}
.form-select.error:focus {
box-shadow: 0 0 0 2px rgb(239 68 68 / 20%);
}
.select-arrow {
position: absolute;
top: 50%;
right: 1rem;
color: var(--color-text-secondary);
transition: var(--transition-fast);
transform: translateY(-50%);
pointer-events: none;
}
.select-wrapper:hover .select-arrow,
.form-select:focus + .select-arrow {
color: var(--color-accent);
}
/* Checkbox Group Styles */
.checkbox-group {
display: flex;
flex-direction: column;
gap: 0.75rem;
padding: 0.5rem 0;
}
.checkbox-item {
display: flex;
align-items: center;
}
.checkbox-label {
display: flex;
align-items: center;
gap: 0.75rem;
cursor: pointer;
font-size: 0.875rem;
color: var(--color-text-primary);
transition: var(--transition-fast);
}
.checkbox-label:hover {
color: var(--color-accent);
}
.checkbox-input {
position: absolute;
opacity: 0;
cursor: pointer;
}
.checkbox-custom {
position: relative;
border: 2px solid var(--color-border);
border-radius: var(--radius-sm);
width: 1.25rem;
height: 1.25rem;
background: var(--color-surface-elevated);
transition: var(--transition-fast);
flex-shrink: 0;
}
.checkbox-custom::after {
position: absolute;
top: 0;
left: 3px;
border: solid white;
border-width: 0 2px 2px 0;
width: 6px;
height: 10px;
opacity: 0;
transition: var(--transition-fast);
content: '';
transform: rotate(45deg);
}
.checkbox-input:checked + .checkbox-custom {
border-color: var(--color-accent);
background: var(--color-accent);
}
.checkbox-input:checked + .checkbox-custom::after {
opacity: 1;
}
.checkbox-input:focus + .checkbox-custom {
box-shadow: 0 0 0 2px rgb(0 122 255 / 20%);
}
.checkbox-text {
flex: 1;
}
/* Switch Styles */
.switch-label {
display: flex;
align-items: center;
gap: 1rem;
cursor: pointer;
font-size: 0.875rem;
color: var(--color-text-primary);
}
.switch-input {
position: absolute;
opacity: 0;
cursor: pointer;
}
.switch-slider {
position: relative;
border-radius: 0.75rem;
width: 3rem;
height: 1.5rem;
background: linear-gradient(180deg, #d0d3d9 0%, #c0c4cc 100%);
transition: var(--transition-fast);
transition: all var(--transition-medium);
flex-shrink: 0;
}
.switch-button {
position: absolute;
top: 2px;
left: 2px;
border-radius: var(--radius-round);
width: 1.25rem;
height: 1.25rem;
background: linear-gradient(180deg, #ffffff 0%, #f5f5f5 100%);
box-shadow: var(--shadow-sm);
transition: var(--transition-fast);
}
.switch-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%);
}
.switch-input:checked + .switch-slider .switch-button {
transform: translateX(1.5rem);
}
.switch-input:focus + .switch-slider {
box-shadow: 0 0 0 2px rgb(0 122 255 / 20%);
}
.switch-text {
font-weight: 500;
color: var(--color-text-secondary);
}
.switch-input:checked ~ .switch-text {
color: var(--color-accent);
}
/* Error Message */
.error-message {
margin-top: 0.25rem;
font-size: 0.75rem;
color: var(--color-error);
}
/* Responsive Design */
@media (width <= 768px) {
.config-form {
gap: 1.25rem;
}
.form-input,
.form-select {
padding: 0.625rem 0.875rem;
}
.form-select {
padding-right: 2.25rem;
}
.tooltip-content {
min-width: 150px;
max-width: 250px;
}
}
/* Focus styles for accessibility */
.form-input:focus-visible,
.form-select:focus-visible,
.checkbox-input:focus-visible + .checkbox-custom,
.switch-input:focus-visible + .switch-slider,
.info-icon:focus-visible {
outline: 2px solid var(--color-accent);
outline-offset: 2px;
}

View File

@@ -75,7 +75,8 @@
"pages": {
"configForm": {
"configName": "Config Name",
"configNamePlaceholder": "Please enter config name"
"configNamePlaceholder": "Please enter config name",
"configNameRequired": "Configure name is required"
},
"gallery": {
"batchEditUrl": "Batch Edit URL",

View File

@@ -75,7 +75,8 @@
"pages": {
"configForm": {
"configName": "配置名",
"configNamePlaceholder": "请输入配置名称"
"configNamePlaceholder": "请输入配置名称",
"configNameRequired": "配置名称为必填项"
},
"gallery": {
"batchEditUrl": "批量修改URL",

View File

@@ -75,7 +75,8 @@
"pages": {
"configForm": {
"configName": "配置名稱",
"configNamePlaceholder": "請輸入配置名稱"
"configNamePlaceholder": "請輸入配置名稱",
"configNameRequired": "配置名稱為必填項"
},
"gallery": {
"batchEditUrl": "批量修改URL",

View File

@@ -7,7 +7,7 @@
<div
class="flex w-full items-center justify-between gap-4 rounded-2xl border border-border-secondary px-6 py-2 shadow-md max-md:items-stretch max-md:p-5"
>
<div class="flex flex-1 items-center gap-4">
<div class="flex flex-1 items-center gap-4 p-1">
<ImagesIcon :size="24" class="text-accent" />
<div>
<h1 class="m-0 text-2xl font-semibold tracking-tight text-main">{{ t('pages.gallery.title') }}</h1>

View File

@@ -5,7 +5,7 @@
<div
class="flex w-full items-center justify-between gap-4 rounded-2xl border border-border-secondary px-6 py-2 shadow-md max-md:items-stretch max-md:p-5"
>
<div class="flex flex-1 flex-wrap items-center gap-4">
<div class="flex flex-1 flex-wrap items-center gap-4 p-2">
<Settings :size="24" class="text-accent" />
<div>
<h1 class="m-0 text-2xl font-semibold tracking-tight text-main">{{ t('pages.settings.title') }}</h1>

View File

@@ -4,7 +4,7 @@
<div
class="flex w-full items-center justify-between gap-4 overflow-visible rounded-2xl border border-border-secondary px-6 py-2 shadow-md max-md:items-stretch max-md:p-5"
>
<div class="flex flex-1 flex-wrap items-center gap-4">
<div class="flex flex-1 flex-wrap items-center gap-4 p-1">
<DatabaseIcon :size="24" class="text-accent" />
<div>
<h1 class="m-0 text-2xl font-semibold tracking-tight text-main">{{ t('pages.plugin.title') }}</h1>
@@ -113,7 +113,7 @@
<div
v-for="item in pluginList"
:key="item.fullName"
class="relative flex h-auto min-h-[200px] flex-col rounded-xl border-2 border-border-secondary p-6 shadow-md transition-all duration-200 ease-apple hover:border-accent hover:shadow-xl [.disabled]:opacity-70"
class="relative flex h-auto flex-col rounded-xl border-2 border-border-secondary p-6 shadow-md transition-all duration-200 ease-apple hover:border-accent hover:shadow-xl [.disabled]:opacity-70"
:class="{ disabled: !item.enabled && !searchText }"
>
<!-- Plugin Badge -->
@@ -275,7 +275,6 @@
ref="$configForm"
:config="config"
:type="currentType"
color-mode="white"
mode="plugin"
:show-tooltips="false"
/>
@@ -324,7 +323,7 @@
<div
v-for="item in filteredBrowsePlugins"
:key="item.fullName"
class="relative flex h-auto min-h-[200px] flex-col rounded-xl border-2 border-border-secondary p-6 shadow-md transition-all duration-200 ease-apple hover:border-accent hover:shadow-xl [.disabled]:opacity-70"
class="relative flex h-auto flex-col rounded-xl border-2 border-border-secondary p-6 shadow-md transition-all duration-200 ease-apple hover:border-accent hover:shadow-xl [.disabled]:opacity-70"
>
<div class="mb-4 flex items-start gap-4">
<img

View File

@@ -5,14 +5,10 @@
<div
class="flex w-full items-center justify-between gap-4 rounded-2xl border border-border-secondary px-6 py-2 shadow-md"
>
<div class="flex flex-wrap items-center gap-4 max-md:justify-center max-md:text-center">
<div
class="border-xl flex h-[56px] w-[56px] items-center justify-center rounded-2xl border-none text-accent shadow-sm max-md:h-[35px] max-md:w-[35px]"
>
<Settings2 :size="28" />
</div>
<div class="flex flex-wrap items-center gap-4 p-2 max-md:justify-center max-md:text-center">
<Settings2 :size="24" class="text-accent" />
<div class="flex flex-col gap-1 max-md:text-center">
<h1 class="m-0 text-2xl font-bold tracking-tight text-main">
<h1 class="m-0 text-xl font-bold tracking-tight text-main">
{{ `${picBedName || type} ${t('pages.uploaderConfig.title')}` }}
</h1>
<p class="m-0 text-sm text-secondary">

View File

@@ -1,72 +1,54 @@
<template>
<div class="picbeds-page">
<div class="page-container">
<header class="page-header">
<div class="header-content">
<div class="header-icon">
<Cloud :size="38" :stroke-width="1.5" />
</div>
<div class="header-text">
<div class="title-row">
<h1 class="page-title">
{{ picBedName }}
</h1>
<button class="doc-link-btn" :title="t('pages.picBedConfigs.viewDoc')" @click="handleNameClick">
<ExternalLink :size="16" />
<span>{{ t('pages.picBedConfigs.viewDoc') }}</span>
</button>
</div>
</div>
<div class="relative flex h-full w-full items-center justify-center">
<div class="relative z-1 flex h-full w-full flex-col items-center justify-start gap-4 rounded-xl border-none p-4">
<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="flex flex-1 flex-wrap items-center gap-4 p-2">
<Cloud :size="24" class="text-accent" />
<h1 class="m-0 text-2xl font-semibold tracking-tight text-main">
{{ picBedName }}
</h1>
<CustomButton
type="secondary"
:icon="ExternalLink"
:text="t('pages.picBedConfigs.viewDoc')"
@click="handleNameClick"
/>
</div>
<div class="header-actions">
<button class="btn btn-secondary btn-glow" @click="imageProcessDialogVisible = true">
<Settings :size="16" />
<span>{{ t('pages.upload.imageProcessNameSingle') }}</span>
</button>
<button class="btn btn-secondary btn-glow" @click="handleCopyApi">
<Copy :size="16" />
<span>{{ t('pages.picBedConfigs.copyAPI') }}</span>
</button>
<div class="flex flex-wrap gap-3 overflow-visible">
<CustomButton
type="secondary"
:icon="Settings"
:text="t('pages.upload.imageProcessNameSingle')"
@click="imageProcessDialogVisible = true"
/>
<CustomButton type="primary" :icon="Copy" :text="t('pages.picBedConfigs.copyAPI')" @click="handleCopyApi" />
</div>
</header>
<!-- Main Content Card -->
<main class="main-content">
<div v-if="config.length > 0" class="config-card">
<!-- Card Header -->
<div class="card-header">
<div class="card-header-icon">
<Settings :size="18" />
</div>
<h2 class="card-title">{{ t('pages.picBedConfigs.configSettings') }}</h2>
</div>
<!-- Config Form -->
<div class="card-body">
<config-form :id="type" ref="$configForm" :config="config" type="uploader" color-mode="white">
<div
class="relative flex h-full w-full flex-1 items-center justify-center overflow-hidden rounded-2xl border border-border-secondary shadow-md"
>
<div class="no-scrollbar flex h-full w-full flex-1 overflow-auto rounded-sm">
<div v-if="config.length > 0" class="flex h-full w-full">
<!-- Config Form -->
<config-form :id="type" ref="$configForm" :config="config" type="uploader">
<!-- Action Buttons -->
<div class="action-buttons">
<button class="btn btn-outline" type="button" @click="handleReset">
<RotateCcw :size="16" />
<span>{{ t('common.reset') }}</span>
</button>
<button class="btn btn-success btn-glow" type="button" @click="handleConfirm">
<Check :size="16" />
<span>{{ t('common.confirm') }}</span>
</button>
<div class="mb-4 flex flex-wrap gap-3 rounded-xl border border-border bg-accent/10 p-4">
<CustomButton type="secondary" :icon="RotateCcw" :text="t('common.reset')" @click="handleReset" />
<CustomButton type="primary" :icon="Check" :text="t('common.confirm')" @click="handleConfirm" />
<div v-if="picBedConfigList.length > 0" class="dropdown-wrapper">
<button
class="btn btn-warning btn-glow dropdown-trigger"
type="button"
<CustomButton
type="primary"
:icon="Import"
:text="t('common.import')"
class="bg-warning!"
@click="toggleDropdown"
@blur="handleDropdownBlur"
>
<Import :size="16" />
<span>{{ t('common.import') }}</span>
<ChevronDown :size="14" class="dropdown-chevron" :class="{ rotated: dropdownVisible }" />
</button>
/>
<Transition name="dropdown">
<div v-show="dropdownVisible" class="dropdown-menu">
@@ -90,19 +72,19 @@
</div>
</config-form>
</div>
</div>
<!-- Empty State -->
<div v-else class="empty-state-card">
<div class="empty-state">
<div class="empty-icon-wrapper">
<FolderOpen :size="48" />
<!-- Empty State -->
<div v-else class="empty-state-card">
<div class="empty-state">
<div class="empty-icon-wrapper">
<FolderOpen :size="48" />
</div>
<h3 class="empty-title">{{ t('pages.picBedConfigs.noConfigOptions') }}</h3>
<p class="empty-description">{{ t('pages.picBedConfigs.noConfigOptionsDesc') }}</p>
</div>
<h3 class="empty-title">{{ t('pages.picBedConfigs.noConfigOptions') }}</h3>
<p class="empty-description">{{ t('pages.picBedConfigs.noConfigOptionsDesc') }}</p>
</div>
</div>
</main>
</div>
</div>
<!-- Loading Overlay -->
@@ -119,48 +101,28 @@
</Transition>
<transition name="modal">
<div v-if="imageProcessDialogVisible" class="modal-overlay" :class="advancedAnimation" @click.stop>
<div class="modal-container" @click.stop>
<div class="modal-header">
<h3 class="modal-title">
{{ t('pages.imageProcess.title') }}
</h3>
<span class="modal-subtitle">
{{ t('pages.imageProcess.subtitle-PerPicbed') }}
</span>
<button class="modal-close" @click="imageProcessDialogVisible = false">
<XIcon :size="20" />
</button>
</div>
<div class="modal-content">
<ImageProcessSetting :config-id="uuidValue" :current-picbed-name="currentPicbedType" />
</div>
</div>
</div>
<CustomModal
v-if="imageProcessDialogVisible"
v-model:visible="imageProcessDialogVisible"
:title="t('pages.imageProcess.title')"
:description="t('pages.imageProcess.subtitle-PerPicbed')"
>
<ImageProcessSetting :config-id="uuidValue" :current-picbed-name="currentPicbedType" />
</CustomModal>
</transition>
</div>
</template>
<script lang="ts" setup>
import dayjs from 'dayjs'
import {
Check,
ChevronDown,
Cloud,
Copy,
ExternalLink,
FileJson,
FolderOpen,
Import,
RotateCcw,
Settings,
XIcon,
} from 'lucide-vue-next'
import { Check, Cloud, Copy, ExternalLink, FileJson, FolderOpen, Import, RotateCcw, Settings } from 'lucide-vue-next'
import { v4 as uuid } from 'uuid'
import { computed, onBeforeMount, ref, useTemplateRef } from 'vue'
import { onBeforeMount, ref, useTemplateRef } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRoute, useRouter } from 'vue-router'
import CustomButton from '@/components/common/CustomButton.vue'
import CustomModal from '@/components/common/CustomModal.vue'
import ImageProcessSetting from '@/components/ImageProcessSetting.vue'
import ConfigForm from '@/components/UnifiedConfigForm.vue'
import useMessage from '@/hooks/useMessage'
@@ -180,7 +142,6 @@ const picBedName = ref('')
const loading = ref(false)
const dropdownVisible = ref(false)
const imageProcessDialogVisible = ref(false)
const enableAdvancedAnimation = ref(false)
const $route = useRoute()
const $router = useRouter()
const $configForm = useTemplateRef('$configForm')
@@ -189,18 +150,9 @@ const currentPicbedType = $route.params.type as string
type.value = $route.params.type as string
async function initConf() {
enableAdvancedAnimation.value = (await getConfig<boolean>(configPaths.settings.isCustomMiniIcon)) || false
}
const advancedAnimation = computed(() => ({
advancedAnimation: enableAdvancedAnimation.value,
}))
onBeforeMount(async () => {
loading.value = true
try {
initConf()
await getPicBeds()
await getPicBedConfigList()
} finally {
@@ -208,7 +160,7 @@ onBeforeMount(async () => {
}
})
function toggleDropdown(_: Event) {
function toggleDropdown() {
dropdownVisible.value = !dropdownVisible.value
}