Feature(custom): rewrite upload config and plugin page

This commit is contained in:
Kuingsmile
2025-08-07 14:52:46 +08:00
parent ca95b7aa49
commit a9c7cecd00
29 changed files with 5198 additions and 1801 deletions

View File

@@ -1,257 +0,0 @@
<!-- eslint-disable vue/no-v-html -->
<template>
<div
id="config-form"
:class="props.colorMode === 'white' ? 'white' : ''"
>
<el-form
ref="$form"
label-position="left"
label-width="50%"
:model="ruleForm"
size="small"
>
<el-form-item
:label="$t('UPLOADER_CONFIG_NAME')"
required
prop="_configName"
>
<el-input
v-model="ruleForm._configName"
type="input"
:placeholder="$t('UPLOADER_CONFIG_PLACEHOLDER')"
/>
</el-form-item>
<!-- dynamic config -->
<el-form-item
v-for="(item, index) in configList"
:key="item.name + index"
:required="item.required"
:prop="item.name"
>
<template #label>
<el-row align="middle">
{{ item.alias || item.name }}
<template v-if="item.tips">
<el-tooltip
class="item"
effect="dark"
placement="right"
:persistent="false"
teleported
>
<template #content>
<span
class="config-form-common-tips"
v-html="transformMarkdownToHTML(item.tips)"
/>
</template>
<el-icon class="ml-[4px] cursor-pointer hover:text-blue">
<InfoFilled />
</el-icon>
</el-tooltip>
</template>
</el-row>
</template>
<el-input
v-if="item.type === 'input' || item.type === 'password'"
v-model="ruleForm[item.name]"
type="input"
:placeholder="item.message || item.name"
/>
<el-select
v-else-if="item.type === 'list' && item.choices"
v-model="ruleForm[item.name]"
:placeholder="item.message || item.name"
:persistent="false"
teleported
>
<el-option
v-for="choice in item.choices"
:key="choice.name || choice.value || choice"
:label="choice.name || choice.value || choice"
:value="choice.value || choice"
/>
</el-select>
<el-select
v-else-if="item.type === 'checkbox' && item.choices"
v-model="ruleForm[item.name]"
:placeholder="item.message || item.name"
multiple
collapse-tags
:persistent="false"
teleported
>
<el-option
v-for="choice in item.choices"
:key="choice.value || choice"
:label="choice.name || choice.value || choice"
:value="choice.value || choice"
/>
</el-select>
<el-switch
v-else-if="item.type === 'confirm'"
v-model="ruleForm[item.name]"
:active-text="item.confirmText || 'yes'"
:inactive-text="item.cancelText || 'no'"
/>
</el-form-item>
<slot />
</el-form>
</div>
</template>
<script lang="ts" setup>
import { InfoFilled } from '@element-plus/icons-vue'
import type { FormInstance } from 'element-plus'
import { cloneDeep, union } from 'lodash-es'
import { marked } from 'marked'
import { reactive, ref, toRefs, watch } from 'vue'
import { useRoute } from 'vue-router'
import { getConfig } from '@/utils/dataSender'
import type { IPicGoPluginConfig, IStringKeyMap } from '#/types/types'
interface IProps {
config: any[]
type: 'uploader' | 'transformer' | 'plugin'
id: string
colorMode?: 'white' | 'dark'
}
const props = defineProps<IProps>()
const $route = useRoute()
const $form = ref<FormInstance>()
const configList = ref<IPicGoPluginConfig[]>([])
const ruleForm = reactive<IStringKeyMap>({})
watch(
toRefs(props.config),
(val: IPicGoPluginConfig[]) => {
handleConfigChange(val)
},
{
deep: true,
immediate: true
}
)
function handleConfigChange (val: any) {
handleConfig(val)
}
async function validate (): Promise<IStringKeyMap | false> {
return new Promise(resolve => {
$form.value?.validate((valid: boolean) => {
if (valid) {
resolve(ruleForm)
} else {
resolve(false)
}
})
})
}
function transformMarkdownToHTML (markdown: string) {
try {
return marked.parse(markdown)
} catch (e) {
return markdown
}
}
function getConfigType () {
switch (props.type) {
case 'plugin': {
return props.id
}
case 'uploader': {
return `picBed.${props.id}`
}
case 'transformer': {
return `transformer.${props.id}`
}
default:
return 'unknown'
}
}
async function handleConfig (val: IPicGoPluginConfig[]) {
const config = await getCurConfigFormData()
const configId = $route.params.configId
Object.assign(ruleForm, config)
if (val.length > 0) {
configList.value = cloneDeep(val).map(item => {
if (!configId) return item
let defaultValue = item.default !== undefined ? item.default : item.type === 'checkbox' ? [] : null
if (item.type === 'checkbox') {
const defaults =
item.choices
?.filter((i: any) => {
return i.checked
})
.map((i: any) => i.value) || []
defaultValue = union(defaultValue, defaults)
}
if (config && config[item.name] !== undefined) {
defaultValue = config[item.name]
}
ruleForm[item.name] = defaultValue
return item
})
}
}
async function getCurConfigFormData () {
const configId = $route.params.configId
const curTypeConfigList = (await getConfig<IStringKeyMap[]>(`uploader.${props.id}.configList`)) || []
return curTypeConfigList.find(i => i._id === configId) || {}
}
function updateRuleForm (key: string, value: any) {
try {
ruleForm[key] = value
} catch (e) {
console.log(e)
}
}
defineExpose({
updateRuleForm,
validate,
getConfigType
})
</script>
<style lang="stylus">
.config-form-common-tips
a
color #409EFF
text-decoration none
#config-form
.el-form
label
line-height 22px
padding-bottom 0
&-item
display: flex
justify-content space-between
border-bottom 1px solid darken(#eee, 50%)
padding-bottom 16px
&:last-child
border-bottom none
&__content
justify-content flex-end
.el-button-group
width 100%
.el-button
width 50%
.el-radio-group
margin-left 25px
.el-switch__label
&.is-active
color #409EFF
&.white
.el-form-item
border-bottom 1px solid #ddd
</style>

View File

@@ -1,213 +0,0 @@
<template>
<div
id="config-form"
:class="props.colorMode === 'white' ? 'white' : ''"
>
<el-form
ref="$form"
label-position="left"
label-width="50%"
:model="ruleForm"
size="small"
>
<el-form-item
:label="$t('UPLOADER_CONFIG_NAME')"
required
prop="_configName"
>
<el-input
v-model="ruleForm._configName"
type="input"
:placeholder="$t('UPLOADER_CONFIG_PLACEHOLDER')"
/>
</el-form-item>
<!-- dynamic config -->
<el-form-item
v-for="(item, index) in configList"
:key="item.name + index"
:label="item.alias || item.name"
:required="item.required"
:prop="item.name"
>
<el-input
v-if="item.type === 'input' || item.type === 'password'"
v-model="ruleForm[item.name]"
type="input"
:placeholder="item.message || item.name"
/>
<el-select
v-else-if="item.type === 'list' && item.choices"
v-model="ruleForm[item.name]"
:placeholder="item.message || item.name"
:persistent="false"
teleported
>
<el-option
v-for="choice in item.choices"
:key="choice.name || choice.value || choice"
:label="choice.name || choice.value || choice"
:value="choice.value || choice"
/>
</el-select>
<el-select
v-else-if="item.type === 'checkbox' && item.choices"
v-model="ruleForm[item.name]"
:placeholder="item.message || item.name"
multiple
collapse-tags
:persistent="false"
teleported
>
<el-option
v-for="choice in item.choices"
:key="choice.value || choice"
:label="choice.name || choice.value || choice"
:value="choice.value || choice"
/>
</el-select>
<el-switch
v-else-if="item.type === 'confirm'"
v-model="ruleForm[item.name]"
active-text="yes"
inactive-text="no"
/>
</el-form-item>
<slot />
</el-form>
</div>
</template>
<script lang="ts" setup>
import type { FormInstance } from 'element-plus'
import { cloneDeep, union } from 'lodash-es'
import { reactive, ref, watch } from 'vue'
import { getConfig } from '@/utils/dataSender'
import type { IPicGoPluginConfig, IStringKeyMap } from '#/types/types'
interface IProps {
config: any[]
type: 'uploader' | 'transformer' | 'plugin'
id: string
colorMode?: 'white' | 'dark'
}
const props = defineProps<IProps>()
const $form = ref<FormInstance>()
const configList = ref<IPicGoPluginConfig[]>([])
const ruleForm = reactive<IStringKeyMap>({})
watch(
() => props.config,
(val: IPicGoPluginConfig[]) => {
handleConfigChange(val)
},
{
deep: true,
immediate: true
}
)
function handleConfigChange (val: any) {
handleConfig(val)
}
async function validate (): Promise<IStringKeyMap | false> {
return new Promise(resolve => {
$form.value?.validate((valid: boolean) => {
if (valid) {
resolve(ruleForm)
} else {
resolve(false)
}
})
})
}
function getConfigType () {
switch (props.type) {
case 'plugin': {
return props.id
}
case 'uploader': {
return `picBed.${props.id}`
}
case 'transformer': {
return `transformer.${props.id}`
}
default:
return 'unknown'
}
}
async function handleConfig (val: IPicGoPluginConfig[]) {
const config = await getCurConfigFormData()
Object.assign(ruleForm, config)
if (val.length > 0) {
configList.value = cloneDeep(val).map(item => {
let defaultValue = item.default !== undefined ? item.default : item.type === 'checkbox' ? [] : null
if (item.type === 'checkbox') {
const defaults =
item.choices
?.filter((i: any) => {
return i.checked
})
.map((i: any) => i.value) || []
defaultValue = union(defaultValue, defaults)
}
if (config && config[item.name] !== undefined) {
defaultValue = config[item.name]
}
ruleForm[item.name] = defaultValue
return item
})
}
}
async function getCurConfigFormData () {
return (await getConfig<IStringKeyMap>(`${props.id}`)) || {}
}
function updateRuleForm (key: string, value: any) {
try {
ruleForm[key] = value
} catch (e) {
console.log(e)
}
}
defineExpose({
updateRuleForm,
validate,
getConfigType
})
</script>
<style lang="stylus">
#config-form
.el-form
label
line-height 22px
padding-bottom 0
&-item
display: flex
justify-content space-between
border-bottom 1px solid darken(#eee, 50%)
padding-bottom 16px
&:last-child
border-bottom none
&__content
justify-content flex-end
.el-button-group
width 100%
.el-button
width 50%
.el-radio-group
margin-left 25px
.el-switch__label
&.is-active
color #409EFF
&.white
.el-form-item
border-bottom 1px solid #ddd
</style>

View File

@@ -1,40 +1,65 @@
<template>
<el-dialog
v-model="showInputBoxVisible"
:title="inputBoxOptions.title || $t('INPUT')"
:modal-append-to-body="false"
append-to-body
>
<el-input
v-model="inputBoxValue"
:placeholder="inputBoxOptions.placeholder"
/>
<template #footer>
<el-button
round
@click="handleInputBoxCancel"
<Teleport to="body">
<div
v-if="showInputBoxVisible"
class="inputbox-overlay"
@click="handleInputBoxCancel"
>
<div
class="inputbox-container"
@click.stop
>
{{ $t('CANCEL') }}
</el-button>
<el-button
type="primary"
round
@click="handleInputBoxConfirm"
>
{{ $t('CONFIRM') }}
</el-button>
</template>
</el-dialog>
<div class="inputbox-header">
<h3 class="inputbox-title">
{{ inputBoxOptions.title || t('pages.inputBox.title') }}
</h3>
<button
class="inputbox-close"
@click="handleInputBoxCancel"
>
<X :size="20" />
</button>
</div>
<div class="inputbox-content">
<input
v-model="inputBoxValue"
:placeholder="inputBoxOptions.placeholder"
class="inputbox-input"
type="text"
@keyup.enter="handleInputBoxConfirm"
@keyup.escape="handleInputBoxCancel"
>
</div>
<div class="inputbox-actions">
<button
class="inputbox-btn cancel-btn"
@click="handleInputBoxCancel"
>
{{ t('common.cancel') }}
</button>
<button
class="inputbox-btn confirm-btn primary"
@click="handleInputBoxConfirm"
>
{{ t('common.confirm') }}
</button>
</div>
</div>
</div>
</Teleport>
</template>
<script lang="ts" setup>
import type { IpcRendererEvent } from 'electron'
import { X } from 'lucide-vue-next'
import { onBeforeMount, onBeforeUnmount, reactive, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import $bus from '@/utils/bus'
import { SHOW_INPUT_BOX, SHOW_INPUT_BOX_RESPONSE } from '@/utils/constant'
import type { IShowInputBoxOption } from '#/types/types'
const { t } = useI18n()
const inputBoxValue = ref('')
const showInputBoxVisible = ref(false)
const inputBoxOptions = reactive({
@@ -81,4 +106,174 @@ export default {
name: 'InputBoxDialog'
}
</script>
<style lang="stylus"></style>
<style scoped>
.inputbox-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 2000;
}
.inputbox-container {
background: white;
border-radius: 0.75rem;
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
max-width: 32rem;
width: 90%;
max-height: 80vh;
overflow: hidden;
}
:root.dark .inputbox-container,
:root.auto.dark .inputbox-container {
background: rgb(31 41 55);
border: 1px solid rgb(55 65 81);
}
.inputbox-header {
padding: 1.5rem 1.5rem 0 1.5rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.inputbox-title {
font-size: 1.25rem;
font-weight: 600;
color: rgb(17 24 39);
margin: 0;
}
:root.dark .inputbox-title,
:root.auto.dark .inputbox-title {
color: rgb(243 244 246);
}
.inputbox-close {
background: none;
border: none;
color: rgb(107 114 128);
cursor: pointer;
padding: 0.25rem;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 0.25rem;
}
.inputbox-close:hover {
background: rgb(243 244 246);
color: rgb(17 24 39);
}
:root.dark .inputbox-close,
:root.auto.dark .inputbox-close {
color: rgb(156 163 175);
}
:root.dark .inputbox-close:hover,
:root.auto.dark .inputbox-close:hover {
background: rgb(55 65 81);
color: rgb(243 244 246);
}
.inputbox-content {
padding: 1rem 1.5rem;
}
.inputbox-input {
width: 100%;
padding: 0.75rem 1rem;
border: 1px solid rgb(209 213 219);
border-radius: 0.5rem;
background: white;
color: rgb(17 24 39);
font-size: 0.875rem;
font-family: inherit;
transition: all 0.2s ease;
outline: none;
}
.inputbox-input:focus {
border-color: rgb(59 130 246);
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.inputbox-input::placeholder {
color: rgb(156 163 175);
}
:root.dark .inputbox-input,
:root.auto.dark .inputbox-input {
background: rgb(55 65 81);
border-color: rgb(75 85 99);
color: rgb(243 244 246);
}
:root.dark .inputbox-input:focus,
:root.auto.dark .inputbox-input:focus {
border-color: rgb(59 130 246);
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
:root.dark .inputbox-input::placeholder,
:root.auto.dark .inputbox-input::placeholder {
color: rgb(107 114 128);
}
.inputbox-actions {
display: flex;
gap: 0.75rem;
padding: 0 1.5rem 1.5rem 1.5rem;
justify-content: flex-end;
}
.inputbox-btn {
padding: 0.5rem 1rem;
border-radius: 0.375rem;
font-size: 0.875rem;
font-weight: 500;
border: none;
cursor: pointer;
min-width: 4rem;
}
.cancel-btn {
background: rgb(243 244 246);
color: rgb(75 85 99);
border: 1px solid rgb(209 213 219);
}
.cancel-btn:hover {
background: rgb(229 231 235);
}
:root.dark .cancel-btn,
:root.auto.dark .cancel-btn {
background: rgb(55 65 81);
color: rgb(209 213 219);
border-color: rgb(75 85 99);
}
:root.dark .cancel-btn:hover,
:root.auto.dark .cancel-btn:hover {
background: rgb(75 85 99);
}
.confirm-btn.primary {
background: rgb(59 130 246);
color: white;
}
.confirm-btn.primary:hover {
background: rgb(37 99 235);
}
</style>

View File

@@ -65,7 +65,7 @@
:title="$t('navigation.moreOptions')"
@click="openMenu"
>
<BadgeInfoIcon :size="20" />
<Info :size="20" />
</button>
</div>
</nav>
@@ -215,7 +215,7 @@ import {
} from '@headlessui/vue'
import { ElMessage as $message } from 'element-plus'
import { pick } from 'lodash-es'
import { BadgeInfoIcon, CheckIcon, ChevronDownIcon, CopyIcon, DatabaseIcon, FolderIcon, PieChartIcon, PlugIcon, Settings, UploadIcon } from 'lucide-vue-next'
import { CheckIcon, ChevronDownIcon, CopyIcon, DatabaseIcon, FolderIcon, Info, PieChartIcon, PlugIcon, Settings, UploadIcon } from 'lucide-vue-next'
import QrcodeVue from 'qrcode.vue'
import pkg from 'root/package.json'
import { computed, nextTick, onBeforeMount, reactive, Ref, ref, watch } from 'vue'

View File

@@ -0,0 +1,775 @@
<!-- eslint-disable vue/no-v-html -->
<template>
<div
id="config-form"
:class="[{ white: props.colorMode === 'white' }]"
>
<form
ref="$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"
>
{{ validationErrors._configName }}
</div>
</div>
</div>
<!-- Dynamic Config Fields -->
<div
v-for="(item, index) in configList"
:key="item.name + index"
class="form-group"
:class="{ required: item.required }"
>
<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" />
</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">
{{ ruleForm[item.name] ? (item.confirmText || 'Yes') : (item.cancelText || 'No') }}
</span>
</label>
<!-- Validation Error -->
<div
v-if="validationErrors[item.name]"
class="error-message"
>
{{ validationErrors[item.name] }}
</div>
</div>
</div>
<slot />
</form>
</div>
</template>
<script lang="ts" setup>
import { cloneDeep, union } from 'lodash-es'
import { ChevronDownIcon, Info } from 'lucide-vue-next'
import { marked } from 'marked'
import { reactive, ref, toRefs, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRoute } from 'vue-router'
import { getConfig } from '@/utils/dataSender'
import type { IPicGoPluginConfig, IStringKeyMap } from '#/types/types'
interface IProps {
config: any[]
type: 'uploader' | 'transformer' | 'plugin'
id: string
colorMode?: 'white' | 'dark'
mode?: 'picbed' | 'plugin'
showTooltips?: boolean
}
const props = withDefaults(defineProps<IProps>(), {
colorMode: undefined,
mode: 'picbed',
showTooltips: true
})
const $route = useRoute()
const $form = ref<HTMLFormElement>()
const { t } = useI18n()
const configList = ref<IPicGoPluginConfig[]>([])
const ruleForm = reactive<IStringKeyMap>({})
const validationErrors = reactive<IStringKeyMap>({})
const visibleTooltips = reactive<{ [key: string]: boolean }>({})
// Watch for config changes
watch(
toRefs(props.config),
(val: IPicGoPluginConfig[]) => {
handleConfigChange(val)
},
{
deep: true,
immediate: true
}
)
function handleConfigChange (val: any) {
handleConfig(val)
}
function validateField (fieldName: string, value: any, config?: IPicGoPluginConfig): string | null {
if (fieldName === '_configName') {
if (!value || value.trim() === '') {
return 'Configuration name is required'
}
return null
}
if (config?.required && (!value || (Array.isArray(value) && value.length === 0))) {
return `${config.alias || config.name} is required`
}
return null
}
function validateForm (): boolean {
const errors: IStringKeyMap = {}
const configNameError = validateField('_configName', ruleForm._configName)
if (configNameError) {
errors._configName = configNameError
}
configList.value.forEach(config => {
const error = validateField(config.name, ruleForm[config.name], config)
if (error) {
errors[config.name] = error
}
})
Object.keys(validationErrors).forEach(key => {
delete validationErrors[key]
})
Object.assign(validationErrors, errors)
return Object.keys(errors).length === 0
}
function clearFieldError (fieldName: string) {
if (validationErrors[fieldName]) {
delete validationErrors[fieldName]
}
}
function toggleTooltip (key: string) {
visibleTooltips[key] = !visibleTooltips[key]
Object.keys(visibleTooltips).forEach(otherKey => {
if (otherKey !== key) {
visibleTooltips[otherKey] = false
}
})
}
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()
if (isValid) {
resolve(ruleForm)
} else {
resolve(false)
}
})
}
function transformMarkdownToHTML (markdown: string) {
try {
return marked.parse(markdown)
} catch (e) {
return markdown
}
}
function getConfigType () {
switch (props.type) {
case 'plugin': {
return props.id
}
case 'uploader': {
return `picBed.${props.id}`
}
case 'transformer': {
return `transformer.${props.id}`
}
default:
return 'unknown'
}
}
async function handleConfig (val: IPicGoPluginConfig[]) {
const config = await getCurConfigFormData()
const configId = props.mode === 'picbed' ? $route.params.configId : null
Object.assign(ruleForm, config)
if (val.length > 0) {
configList.value = cloneDeep(val).map(item => {
// For plugin mode, don't check configId
if (props.mode === 'plugin' || !configId) {
let defaultValue = item.default !== undefined ? item.default : item.type === 'checkbox' ? [] : null
if (item.type === 'checkbox') {
const defaults =
item.choices
?.filter((i: any) => i.checked)
.map((i: any) => i.value) || []
defaultValue = union(defaultValue, defaults)
}
if (config && config[item.name] !== undefined) {
defaultValue = config[item.name]
}
ruleForm[item.name] = defaultValue
return item
}
let defaultValue = item.default !== undefined ? item.default : item.type === 'checkbox' ? [] : null
if (item.type === 'checkbox') {
const defaults =
item.choices
?.filter((i: any) => i.checked)
.map((i: any) => i.value) || []
defaultValue = union(defaultValue, defaults)
}
if (config && config[item.name] !== undefined) {
defaultValue = config[item.name]
}
ruleForm[item.name] = defaultValue
return item
})
}
}
async function getCurConfigFormData () {
if (props.mode === 'plugin') {
return (await getConfig<IStringKeyMap>(`${props.id}`)) || {}
} else {
const configId = $route.params.configId
const curTypeConfigList = (await getConfig<IStringKeyMap[]>(`uploader.${props.id}.configList`)) || []
return curTypeConfigList.find(i => i._id === configId) || {}
}
}
function updateRuleForm (key: string, value: any) {
try {
ruleForm[key] = value
clearFieldError(key)
} catch (e) {
console.log(e)
}
}
defineExpose({
updateRuleForm,
validate,
getConfigType
})
</script>
<style scoped>
#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, #ef4444);
}
.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;
align-items: center;
justify-content: center;
cursor: pointer;
color: var(--color-text-secondary);
transition: var(--transition-fast);
border-radius: 50%;
padding: 2px;
}
.info-icon:hover {
color: var(--color-accent);
background: rgba(0, 122, 255, 0.1);
}
.tooltip-content {
position: absolute;
top: 100%;
left: 0;
z-index: 1000;
min-width: 200px;
max-width: 300px;
padding: 0.75rem;
background: var(--color-surface-elevated);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
box-shadow: var(--shadow-lg);
font-size: 0.75rem;
line-height: 1.4;
color: var(--color-text-primary);
}
/* Input Styles */
.form-input {
width: 100%;
padding: 0.75rem 1rem;
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
background: var(--color-surface-elevated);
color: var(--color-text-primary);
font-size: 0.875rem;
font-family: inherit;
transition: var(--transition-fast);
}
.form-input:focus {
outline: none;
border-color: var(--color-accent);
box-shadow: 0 0 0 2px rgba(0, 122, 255, 0.2);
}
.form-input::placeholder {
color: var(--color-text-secondary);
}
.form-input.error {
border-color: var(--color-error, #ef4444);
}
.form-input.error:focus {
box-shadow: 0 0 0 2px rgba(239, 68, 68, 0.2);
}
/* Select Styles */
.select-wrapper {
position: relative;
}
.form-select {
width: 100%;
padding: 0.75rem 2.5rem 0.75rem 1rem;
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
background: var(--color-surface-elevated);
color: var(--color-text-primary);
font-size: 0.875rem;
font-family: inherit;
transition: var(--transition-fast);
appearance: none;
cursor: pointer;
}
.form-select:focus {
outline: none;
border-color: var(--color-accent);
box-shadow: 0 0 0 2px rgba(0, 122, 255, 0.2);
}
.form-select.error {
border-color: var(--color-error, #ef4444);
}
.form-select.error:focus {
box-shadow: 0 0 0 2px rgba(239, 68, 68, 0.2);
}
.select-arrow {
position: absolute;
right: 1rem;
top: 50%;
transform: translateY(-50%);
color: var(--color-text-secondary);
pointer-events: none;
transition: var(--transition-fast);
}
.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;
width: 1.25rem;
height: 1.25rem;
border: 2px solid var(--color-border);
border-radius: var(--radius-sm);
background: var(--color-surface-elevated);
transition: var(--transition-fast);
flex-shrink: 0;
}
.checkbox-custom::after {
content: '';
position: absolute;
left: 3px;
top: 0px;
width: 6px;
height: 10px;
border: solid white;
border-width: 0 2px 2px 0;
transform: rotate(45deg);
opacity: 0;
transition: var(--transition-fast);
}
.checkbox-input:checked + .checkbox-custom {
background: var(--color-accent);
border-color: var(--color-accent);
}
.checkbox-input:checked + .checkbox-custom::after {
opacity: 1;
}
.checkbox-input:focus + .checkbox-custom {
box-shadow: 0 0 0 2px rgba(0, 122, 255, 0.2);
}
.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;
width: 3rem;
height: 1.5rem;
background: var(--color-border);
border-radius: 0.75rem;
transition: var(--transition-fast);
flex-shrink: 0;
}
.switch-button {
position: absolute;
top: 2px;
left: 2px;
width: 1.25rem;
height: 1.25rem;
background: white;
border-radius: 50%;
transition: var(--transition-fast);
box-shadow: var(--shadow-sm);
}
.switch-input:checked + .switch-slider {
background: var(--color-accent);
}
.switch-input:checked + .switch-slider .switch-button {
transform: translateX(1.5rem);
}
.switch-input:focus + .switch-slider {
box-shadow: 0 0 0 2px rgba(0, 122, 255, 0.2);
}
.switch-text {
font-weight: 500;
color: var(--color-text-secondary);
}
.switch-input:checked ~ .switch-text {
color: var(--color-accent);
}
/* Error Message */
.error-message {
font-size: 0.75rem;
color: var(--color-error, #ef4444);
margin-top: 0.25rem;
}
/* White theme adjustments */
.white .form-input,
.white .form-select {
background: white;
border-color: #ddd;
}
.white .form-input:focus,
.white .form-select:focus {
border-color: var(--color-accent);
}
.white .checkbox-custom {
background: white;
border-color: #ddd;
}
.white .switch-slider {
background: #ddd;
}
.white .tooltip-content {
background: white;
border-color: #ddd;
}
/* Responsive Design */
@media (max-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;
}
}
/* Dark mode adjustments */
:root.dark .form-input,
:root.auto.dark .form-input,
:root.dark .form-select,
:root.auto.dark .form-select {
background: var(--color-surface-elevated);
border-color: var(--color-border);
}
:root.dark .checkbox-custom,
:root.auto.dark .checkbox-custom {
background: var(--color-surface-elevated);
border-color: var(--color-border);
}
:root.dark .switch-slider,
:root.auto.dark .switch-slider {
background: var(--color-border);
}
:root.dark .tooltip-content,
:root.auto.dark .tooltip-content {
background: var(--color-surface-elevated);
border-color: var(--color-border);
}
/* 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;
}
</style>

View File

@@ -144,16 +144,18 @@ export default {
.message-toast {
display: flex;
align-items: center;
align-items: flex-start;
gap: 0.75rem;
padding: 0.75rem 1rem;
margin-bottom: 0.5rem;
border-radius: 0.5rem;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
max-width: 24rem;
min-width: 20rem;
pointer-events: all;
background: white;
border: 1px solid rgb(229 231 235);
word-wrap: break-word;
}
:root.dark .message-toast,
@@ -196,6 +198,7 @@ export default {
.message-icon {
flex-shrink: 0;
margin-top: 0.125rem;
}
.message-content {
@@ -203,6 +206,11 @@ export default {
color: rgb(75 85 99);
font-size: 0.875rem;
line-height: 1.25rem;
word-wrap: break-word;
word-break: break-word;
overflow-wrap: break-word;
hyphens: auto;
min-width: 0;
}
:root.dark .message-content,
@@ -220,6 +228,8 @@ export default {
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
margin-top: 0.125rem;
}
.message-close:hover {