mirror of
https://github.com/Kuingsmile/PicList.git
synced 2026-05-07 05:02:43 +08:00
776 lines
18 KiB
Vue
776 lines
18 KiB
Vue
<!-- 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>
|