mirror of
https://github.com/Kuingsmile/PicList.git
synced 2026-06-01 14:19:43 +08:00
✨ Feature(custom): rewrite upload config and plugin page
This commit is contained in:
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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'
|
||||
|
||||
775
src/renderer/components/UnifiedConfigForm.vue
Normal file
775
src/renderer/components/UnifiedConfigForm.vue
Normal 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>
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user