Feature(custom): support duplicate config and the UI of confirm/input box is optimized

This commit is contained in:
Kuingsmile
2025-12-30 17:06:29 +08:00
parent a7691234b5
commit c7fd139ec9
16 changed files with 613 additions and 279 deletions

View File

@@ -7,6 +7,7 @@ import deleteRoutes from '~/events/rpc/routes/picbed/delete'
import { IRPCActionType, IRPCType } from '~/utils/enum' import { IRPCActionType, IRPCType } from '~/utils/enum'
import { import {
deleteUploaderConfig, deleteUploaderConfig,
duplicateUploaderConfig,
getUploaderConfigList, getUploaderConfigList,
resetUploaderConfig, resetUploaderConfig,
selectUploaderConfig, selectUploaderConfig,
@@ -45,6 +46,15 @@ const picbedRoutes = [
}, },
type: IRPCType.INVOKE, type: IRPCType.INVOKE,
}, },
{
action: IRPCActionType.PICBED_DUPLICATE_CONFIG,
handler: async (_: IIPCEvent, args: [type: string, id: string, newName: string]) => {
const [type, id, newName] = args
const config = duplicateUploaderConfig(type, id, newName)
return config
},
type: IRPCType.INVOKE,
},
{ {
action: IRPCActionType.UPLOADER_SELECT, action: IRPCActionType.UPLOADER_SELECT,
handler: async (_: IIPCEvent, args: [type: string, id: string]) => { handler: async (_: IIPCEvent, args: [type: string, id: string]) => {

View File

@@ -92,6 +92,7 @@ export const IRPCActionType = {
PICBED_GET_PICBED_CONFIG: 'PICBED_GET_PICBED_CONFIG', PICBED_GET_PICBED_CONFIG: 'PICBED_GET_PICBED_CONFIG',
PICBED_GET_CONFIG_LIST: 'PICBED_GET_CONFIG_LIST', PICBED_GET_CONFIG_LIST: 'PICBED_GET_CONFIG_LIST',
PICBED_DELETE_CONFIG: 'PICBED_DELETE_CONFIG', PICBED_DELETE_CONFIG: 'PICBED_DELETE_CONFIG',
PICBED_DUPLICATE_CONFIG: 'PICBED_DUPLICATE_CONFIG',
UPLOADER_CHANGE_CURRENT: 'UPLOADER_CHANGE_CURRENT', UPLOADER_CHANGE_CURRENT: 'UPLOADER_CHANGE_CURRENT',
UPLOADER_SELECT: 'UPLOADER_SELECT', UPLOADER_SELECT: 'UPLOADER_SELECT',
UPLOADER_UPDATE_CONFIG: 'UPLOADER_UPDATE_CONFIG', UPLOADER_UPDATE_CONFIG: 'UPLOADER_UPDATE_CONFIG',

View File

@@ -151,6 +151,34 @@ export const deleteUploaderConfig = (type: string, id: string): IUploaderConfigI
} }
} }
export const duplicateUploaderConfig = (type: string, id: string, newName: string): IUploaderConfigItem | void => {
const { configList, defaultId } = getUploaderConfigList(type)
const originalConfig = configList.find((item: IStringKeyMap) => item._id === id)
if (!originalConfig) {
return
}
const duplicatedConfig: IUploaderConfigListItem = {
...originalConfig,
_configName: newName,
_id: uuid(),
_createdAt: Date.now(),
_updatedAt: Date.now(),
}
const updatedConfigList = [...configList, duplicatedConfig]
console.log('updatedConfigList', updatedConfigList)
picgo.saveConfig({
[`uploader.${type}.configList`]: updatedConfigList,
})
return {
configList: updatedConfigList,
defaultId,
}
}
/** /**
* upgrade old uploader config to new format * upgrade old uploader config to new format
*/ */

View File

@@ -1,51 +1,61 @@
<template> <template>
<Teleport to="body"> <Teleport to="body">
<div v-if="showInputBoxVisible" class="inputbox-overlay"> <Transition name="inputbox-fade">
<div class="inputbox-container" @click.stop> <div v-if="showInputBoxVisible" class="inputbox-overlay" @click="handleInputBoxCancel">
<div class="inputbox-header"> <Transition name="inputbox-scale">
<h3 class="inputbox-title"> <div v-if="showInputBoxVisible" class="inputbox-container" @click.stop>
{{ inputBoxOptions.title || t('pages.inputBox.title') }} <button class="inputbox-close" @click="handleInputBoxCancel">
</h3> <X :size="20" />
<button class="inputbox-close" @click="handleInputBoxCancel"> </button>
<X :size="20" />
</button> <div class="inputbox-body">
</div> <h3 class="inputbox-title">
<div class="inputbox-content"> {{ inputBoxOptions.title || t('pages.inputBox.title') }}
<textarea </h3>
v-if="inputBoxOptions.multiLine"
v-model="inputBoxValue" <div class="inputbox-content">
:placeholder="inputBoxOptions.placeholder" <textarea
class="inputbox-textarea" v-if="inputBoxOptions.multiLine"
rows="4" ref="textareaRef"
@keyup.ctrl.enter="handleInputBoxConfirm" v-model="inputBoxValue"
@keyup.escape="handleInputBoxCancel" :placeholder="inputBoxOptions.placeholder"
/> class="inputbox-textarea"
<input rows="4"
v-else @keyup.ctrl.enter="handleInputBoxConfirm"
v-model="inputBoxValue" @keyup.meta.enter="handleInputBoxConfirm"
:placeholder="inputBoxOptions.placeholder" @keyup.escape="handleInputBoxCancel"
class="inputbox-input" />
type="text" <input
@keyup.enter="handleInputBoxConfirm" v-else
@keyup.escape="handleInputBoxCancel" ref="inputRef"
/> v-model="inputBoxValue"
</div> :placeholder="inputBoxOptions.placeholder"
<div class="inputbox-actions"> class="inputbox-input"
<button class="inputbox-btn cancel-btn" @click="handleInputBoxCancel"> type="text"
{{ t('common.cancel') }} @keyup.enter="handleInputBoxConfirm"
</button> @keyup.escape="handleInputBoxCancel"
<button class="inputbox-btn confirm-btn primary" @click="handleInputBoxConfirm"> />
{{ t('common.confirm') }} </div>
</button> </div>
</div>
<div class="inputbox-actions">
<button class="inputbox-btn cancel-btn" @click="handleInputBoxCancel">
{{ t('common.cancel') }}
</button>
<button class="inputbox-btn confirm-btn" :disabled="!inputBoxValue.trim()" @click="handleInputBoxConfirm">
{{ t('common.confirm') }}
</button>
</div>
</div>
</Transition>
</div> </div>
</div> </Transition>
</Teleport> </Teleport>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { X } from 'lucide-vue-next' import { X } from 'lucide-vue-next'
import { onBeforeMount, onBeforeUnmount, reactive, ref } from 'vue' import { nextTick, onBeforeMount, onBeforeUnmount, reactive, ref } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import $bus from '@/utils/bus' import $bus from '@/utils/bus'
@@ -55,6 +65,8 @@ import type { IShowInputBoxOption } from '#/types/types'
const { t } = useI18n() const { t } = useI18n()
const inputBoxValue = ref('') const inputBoxValue = ref('')
const showInputBoxVisible = ref(false) const showInputBoxVisible = ref(false)
const inputRef = ref<HTMLInputElement>()
const textareaRef = ref<HTMLTextAreaElement>()
const inputBoxOptions = reactive({ const inputBoxOptions = reactive({
title: '', title: '',
placeholder: '', placeholder: '',
@@ -67,12 +79,21 @@ function handleIpcInputBoxEvent(options: IShowInputBoxOption) {
initInputBoxValue(options) initInputBoxValue(options)
} }
function initInputBoxValue(options: IShowInputBoxOption) { async function initInputBoxValue(options: IShowInputBoxOption) {
inputBoxValue.value = options.value || '' inputBoxValue.value = options.value || ''
inputBoxOptions.title = options.title || '' inputBoxOptions.title = options.title || ''
inputBoxOptions.placeholder = options.placeholder || '' inputBoxOptions.placeholder = options.placeholder || ''
inputBoxOptions.multiLine = options.multiLine || false inputBoxOptions.multiLine = options.multiLine || false
showInputBoxVisible.value = true showInputBoxVisible.value = true
await nextTick()
if (inputBoxOptions.multiLine) {
textareaRef.value?.focus()
textareaRef.value?.select()
} else {
inputRef.value?.focus()
inputRef.value?.select()
}
} }
function handleInputBoxCancel() { function handleInputBoxCancel() {
@@ -106,6 +127,36 @@ export default {
</script> </script>
<style scoped> <style scoped>
/* Transitions */
.inputbox-fade-enter-active,
.inputbox-fade-leave-active {
transition: opacity 0.2s ease;
}
.inputbox-fade-enter-from,
.inputbox-fade-leave-to {
opacity: 0;
}
.inputbox-scale-enter-active {
transition: all 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.inputbox-scale-leave-active {
transition: all 0.2s ease;
}
.inputbox-scale-enter-from {
opacity: 0;
transform: scale(0.9) translateY(-10px);
}
.inputbox-scale-leave-to {
opacity: 0;
transform: scale(0.95);
}
/* Overlay */
.inputbox-overlay { .inputbox-overlay {
position: fixed; position: fixed;
inset: 0; inset: 0;
@@ -113,15 +164,19 @@ export default {
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
background: rgb(0 0 0 / 50%); padding: 1rem;
background: rgb(0 0 0 / 40%);
backdrop-filter: blur(4px);
} }
/* Container */
.inputbox-container { .inputbox-container {
position: relative;
overflow: hidden; overflow: hidden;
border-radius: 0.75rem; border: 1px solid rgb(229 231 235);
width: 90%; border-radius: 1rem;
max-width: 32rem; width: 100%;
max-height: 80vh; max-width: 28rem;
background: white; background: white;
box-shadow: box-shadow:
0 20px 25px -5px rgb(0 0 0 / 10%), 0 20px 25px -5px rgb(0 0 0 / 10%),
@@ -130,45 +185,30 @@ export default {
:root.dark .inputbox-container, :root.dark .inputbox-container,
:root.auto.dark .inputbox-container { :root.auto.dark .inputbox-container {
border: 1px solid rgb(55 65 81); border-color: rgb(55 65 81);
background: rgb(31 41 55); background: rgb(31 41 55);
} }
.inputbox-header { /* Close Button */
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.5rem 1.5rem 0;
}
.inputbox-title {
margin: 0;
font-size: 1.25rem;
font-weight: 600;
color: rgb(17 24 39);
}
:root.dark .inputbox-title,
:root.auto.dark .inputbox-title {
color: rgb(243 244 246);
}
.inputbox-close { .inputbox-close {
position: absolute;
top: 1rem;
right: 1rem;
z-index: 10;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
border: none; border: none;
border-radius: 0.25rem; border-radius: 0.5rem;
padding: 0.25rem; padding: 0.375rem;
width: 24px;
height: 24px;
color: rgb(107 114 128); color: rgb(107 114 128);
background: none; background: transparent;
transition: all 0.15s ease;
cursor: pointer; cursor: pointer;
} }
.inputbox-close:hover { .inputbox-close:hover {
color: rgb(17 24 39); color: rgb(75 85 99);
background: rgb(243 244 246); background: rgb(243 244 246);
} }
@@ -179,29 +219,55 @@ export default {
:root.dark .inputbox-close:hover, :root.dark .inputbox-close:hover,
:root.auto.dark .inputbox-close:hover { :root.auto.dark .inputbox-close:hover {
color: rgb(243 244 246); color: rgb(209 213 219);
background: rgb(55 65 81); background: rgb(55 65 81);
} }
.inputbox-content { /* Body */
padding: 1rem 1.5rem; .inputbox-body {
padding: 2rem 2rem 1.5rem;
} }
.inputbox-title {
margin: 0 0 1.25rem;
padding-right: 2rem;
font-size: 1.125rem;
font-weight: 600;
line-height: 1.4;
color: rgb(17 24 39);
}
:root.dark .inputbox-title,
:root.auto.dark .inputbox-title {
color: rgb(243 244 246);
}
.inputbox-content {
position: relative;
}
/* Input */
.inputbox-input { .inputbox-input {
border: 1px solid rgb(209 213 219); border: 1.5px solid rgb(229 231 235);
border-radius: 0.5rem; border-radius: 0.625rem;
padding: 0.75rem 1rem; padding: 0.75rem 1rem;
width: 100%; width: 100%;
font-size: 0.875rem; font-size: 0.9375rem;
font-family: inherit; font-family: inherit;
color: rgb(17 24 39); color: rgb(17 24 39);
background: white; background: rgb(249 250 251);
outline: none; outline: none;
transition: all 0.2s ease; transition: all 0.2s ease;
} }
.inputbox-input:hover {
border-color: rgb(209 213 219);
background: white;
}
.inputbox-input:focus { .inputbox-input:focus {
border-color: rgb(59 130 246); border-color: rgb(59 130 246);
background: white;
box-shadow: 0 0 0 3px rgb(59 130 246 / 10%); box-shadow: 0 0 0 3px rgb(59 130 246 / 10%);
} }
@@ -209,23 +275,31 @@ export default {
color: rgb(156 163 175); color: rgb(156 163 175);
} }
/* Textarea */
.inputbox-textarea { .inputbox-textarea {
border: 1px solid rgb(209 213 219); border: 1.5px solid rgb(229 231 235);
border-radius: 0.375rem; border-radius: 0.625rem;
padding: 0.5rem 0.75rem; padding: 0.75rem 1rem;
width: 100%; width: 100%;
min-height: 4rem; min-height: 6rem;
font-size: 0.875rem; font-size: 0.9375rem;
font-family: inherit; font-family: inherit;
line-height: 1.6;
color: rgb(17 24 39); color: rgb(17 24 39);
background: white; background: rgb(249 250 251);
outline: none; outline: none;
resize: vertical; resize: vertical;
transition: all 0.2s ease; transition: all 0.2s ease;
} }
.inputbox-textarea:hover {
border-color: rgb(209 213 219);
background: white;
}
.inputbox-textarea:focus { .inputbox-textarea:focus {
border-color: rgb(59 130 246); border-color: rgb(59 130 246);
background: white;
box-shadow: 0 0 0 3px rgb(59 130 246 / 10%); box-shadow: 0 0 0 3px rgb(59 130 246 / 10%);
} }
@@ -233,16 +307,24 @@ export default {
color: rgb(156 163 175); color: rgb(156 163 175);
} }
/* Dark Mode - Input */
:root.dark .inputbox-input, :root.dark .inputbox-input,
:root.auto.dark .inputbox-input { :root.auto.dark .inputbox-input {
border-color: rgb(75 85 99); border-color: rgb(55 65 81);
color: rgb(243 244 246); color: rgb(243 244 246);
background: rgb(55 65 81); background: rgb(55 65 81);
} }
:root.dark .inputbox-input:hover,
:root.auto.dark .inputbox-input:hover {
border-color: rgb(75 85 99);
background: rgb(55 65 81);
}
:root.dark .inputbox-input:focus, :root.dark .inputbox-input:focus,
:root.auto.dark .inputbox-input:focus { :root.auto.dark .inputbox-input:focus {
border-color: rgb(59 130 246); border-color: rgb(59 130 246);
background: rgb(55 65 81);
box-shadow: 0 0 0 3px rgb(59 130 246 / 10%); box-shadow: 0 0 0 3px rgb(59 130 246 / 10%);
} }
@@ -251,16 +333,24 @@ export default {
color: rgb(107 114 128); color: rgb(107 114 128);
} }
/* Dark Mode - Textarea */
:root.dark .inputbox-textarea, :root.dark .inputbox-textarea,
:root.auto.dark .inputbox-textarea { :root.auto.dark .inputbox-textarea {
border-color: rgb(75 85 99); border-color: rgb(55 65 81);
color: rgb(243 244 246); color: rgb(243 244 246);
background: rgb(55 65 81); background: rgb(55 65 81);
} }
:root.dark .inputbox-textarea:hover,
:root.auto.dark .inputbox-textarea:hover {
border-color: rgb(75 85 99);
background: rgb(55 65 81);
}
:root.dark .inputbox-textarea:focus, :root.dark .inputbox-textarea:focus,
:root.auto.dark .inputbox-textarea:focus { :root.auto.dark .inputbox-textarea:focus {
border-color: rgb(59 130 246); border-color: rgb(59 130 246);
background: rgb(55 65 81);
box-shadow: 0 0 0 3px rgb(59 130 246 / 10%); box-shadow: 0 0 0 3px rgb(59 130 246 / 10%);
} }
@@ -269,51 +359,103 @@ export default {
color: rgb(107 114 128); color: rgb(107 114 128);
} }
/* Actions */
.inputbox-actions { .inputbox-actions {
display: flex; display: flex;
justify-content: flex-end; border-top: 1px solid rgb(243 244 246);
padding: 0 1.5rem 1.5rem; padding: 1rem 1.5rem;
gap: 0.75rem; gap: 0.75rem;
} }
:root.dark .inputbox-actions,
:root.auto.dark .inputbox-actions {
border-top-color: rgb(55 65 81);
}
.inputbox-btn { .inputbox-btn {
flex: 1;
border: none; border: none;
border-radius: 0.375rem; border-radius: 0.5rem;
padding: 0.5rem 1rem; padding: 0.625rem 1.25rem;
min-width: 4rem;
font-size: 0.875rem; font-size: 0.875rem;
font-weight: 500; font-weight: 500;
transition: all 0.15s ease;
cursor: pointer; cursor: pointer;
} }
.inputbox-btn:active {
transform: scale(0.98);
}
.inputbox-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.inputbox-btn:disabled:active {
transform: none;
}
/* Cancel Button */
.cancel-btn { .cancel-btn {
border: 1px solid rgb(209 213 219); border: 1px solid rgb(229 231 235);
color: rgb(75 85 99); color: rgb(75 85 99);
background: rgb(243 244 246); background: white;
} }
.cancel-btn:hover { .cancel-btn:hover {
background: rgb(229 231 235); border-color: rgb(209 213 219);
background: rgb(249 250 251);
} }
:root.dark .cancel-btn, :root.dark .cancel-btn,
:root.auto.dark .cancel-btn { :root.auto.dark .cancel-btn {
border-color: rgb(75 85 99); border-color: rgb(55 65 81);
color: rgb(209 213 219); color: rgb(209 213 219);
background: rgb(55 65 81); background: rgb(55 65 81);
} }
:root.dark .cancel-btn:hover, :root.dark .cancel-btn:hover,
:root.auto.dark .cancel-btn:hover { :root.auto.dark .cancel-btn:hover {
border-color: rgb(75 85 99);
background: rgb(75 85 99); background: rgb(75 85 99);
} }
.confirm-btn.primary { /* Confirm Button */
.confirm-btn {
border: none;
color: white; color: white;
background: rgb(59 130 246); background: linear-gradient(135deg, rgb(59 130 246) 0%, rgb(37 99 235) 100%);
box-shadow: 0 1px 2px 0 rgb(0 0 0 / 5%);
} }
.confirm-btn.primary:hover { .confirm-btn:hover:not(:disabled) {
background: rgb(37 99 235); background: linear-gradient(135deg, rgb(37 99 235) 0%, rgb(29 78 216) 100%);
box-shadow: 0 4px 12px rgb(59 130 246 / 40%);
}
/* Responsive */
@media (width <= 640px) {
.inputbox-overlay {
align-items: flex-end;
padding: 0;
}
.inputbox-container {
border-radius: 1rem 1rem 0 0;
max-width: 100%;
}
.inputbox-body {
padding: 1.75rem 1.5rem 1.25rem;
}
.inputbox-actions {
flex-direction: column-reverse;
}
.inputbox-btn {
width: 100%;
}
} }
</style> </style>

View File

@@ -1,42 +1,41 @@
<template> <template>
<div v-if="isOpen" class="messagebox-overlay" @click="onCancel"> <Transition name="messagebox-fade">
<div class="messagebox-container" @click.stop> <div v-if="isOpen" class="messagebox-overlay" @click="onCancel">
<div class="messagebox-header"> <Transition name="messagebox-scale">
<h3 class="messagebox-title"> <div v-if="isOpen" class="messagebox-container" @click.stop>
{{ title }} <button v-if="showClose" class="messagebox-close" @click="onCancel">
</h3> <XIcon :size="20" />
<button v-if="showClose" class="messagebox-close" @click="onCancel">×</button> </button>
</div>
<div class="messagebox-content"> <div class="messagebox-body">
<div v-if="type" class="messagebox-icon"> <div class="messagebox-main">
<component :is="iconComponent" :size="48" /> <div v-if="type" class="messagebox-icon-wrapper" :class="`messagebox-icon-${type}`">
<component :is="iconComponent" :size="24" :stroke-width="2.5" />
</div>
<div class="messagebox-content">
<h3 class="messagebox-title">{{ title }}</h3>
<p class="messagebox-message">{{ message }}</p>
</div>
</div>
</div>
<div class="messagebox-actions" :class="{ center }">
<button class="messagebox-btn cancel-btn" @click="onCancel">
{{ cancelButtonText }}
</button>
<button class="messagebox-btn confirm-btn" :class="confirmButtonClass" @click="onConfirm">
{{ confirmButtonText }}
</button>
</div>
</div> </div>
<div class="messagebox-message"> </Transition>
<p>{{ message }}</p>
</div>
</div>
<div v-if="center" class="messagebox-actions center">
<button class="messagebox-btn cancel-btn" @click="onCancel">
{{ cancelButtonText }}
</button>
<button class="messagebox-btn confirm-btn" :class="confirmButtonClass" @click="onConfirm">
{{ confirmButtonText }}
</button>
</div>
<div v-else class="messagebox-actions">
<button class="messagebox-btn confirm-btn" :class="confirmButtonClass" @click="onConfirm">
{{ confirmButtonText }}
</button>
<button class="messagebox-btn cancel-btn" @click="onCancel">
{{ cancelButtonText }}
</button>
</div>
</div> </div>
</div> </Transition>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { AlertTriangle, CheckCircle, Info, XCircle } from 'lucide-vue-next' import { AlertTriangle, CheckCircle, Info, X as XIcon, XCircle } from 'lucide-vue-next'
import { computed } from 'vue' import { computed } from 'vue'
interface Props { interface Props {
@@ -109,6 +108,36 @@ export default {
</script> </script>
<style scoped> <style scoped>
/* Transitions */
.messagebox-fade-enter-active,
.messagebox-fade-leave-active {
transition: opacity 0.2s ease;
}
.messagebox-fade-enter-from,
.messagebox-fade-leave-to {
opacity: 0;
}
.messagebox-scale-enter-active {
transition: all 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.messagebox-scale-leave-active {
transition: all 0.2s ease;
}
.messagebox-scale-enter-from {
opacity: 0;
transform: scale(0.9) translateY(-10px);
}
.messagebox-scale-leave-to {
opacity: 0;
transform: scale(0.95);
}
/* Overlay */
.messagebox-overlay { .messagebox-overlay {
position: fixed; position: fixed;
inset: 0; inset: 0;
@@ -116,15 +145,19 @@ export default {
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
background: rgb(0 0 0 / 50%); padding: 1rem;
background: rgb(0 0 0 / 40%);
backdrop-filter: blur(4px);
} }
/* Container */
.messagebox-container { .messagebox-container {
position: relative;
overflow: hidden; overflow: hidden;
border-radius: 0.75rem; border: 1px solid rgb(229 231 235);
width: 90%; border-radius: 1rem;
max-width: 32rem; width: 100%;
max-height: 80vh; max-width: 26rem;
background: white; background: white;
box-shadow: box-shadow:
0 20px 25px -5px rgb(0 0 0 / 10%), 0 20px 25px -5px rgb(0 0 0 / 10%),
@@ -133,46 +166,30 @@ export default {
:root.dark .messagebox-container, :root.dark .messagebox-container,
:root.auto.dark .messagebox-container { :root.auto.dark .messagebox-container {
border: 1px solid rgb(55 65 81); border-color: rgb(55 65 81);
background: rgb(31 41 55); background: rgb(31 41 55);
} }
.messagebox-header { /* Close Button */
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.5rem 1.5rem 0;
}
.messagebox-title {
margin: 0;
font-size: 1.25rem;
font-weight: 600;
color: rgb(17 24 39);
}
:root.dark .messagebox-title,
:root.auto.dark .messagebox-title {
color: rgb(243 244 246);
}
.messagebox-close { .messagebox-close {
position: absolute;
top: 1rem;
right: 1rem;
z-index: 10;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
border: none; border: none;
border-radius: 0.25rem; border-radius: 0.5rem;
padding: 0; padding: 0.375rem;
width: 24px;
height: 24px;
font-size: 1.5rem;
color: rgb(107 114 128); color: rgb(107 114 128);
background: none; background: transparent;
transition: all 0.15s ease;
cursor: pointer; cursor: pointer;
} }
.messagebox-close:hover { .messagebox-close:hover {
color: rgb(17 24 39); color: rgb(75 85 99);
background: rgb(243 244 246); background: rgb(243 244 246);
} }
@@ -183,120 +200,246 @@ export default {
:root.dark .messagebox-close:hover, :root.dark .messagebox-close:hover,
:root.auto.dark .messagebox-close:hover { :root.auto.dark .messagebox-close:hover {
color: rgb(243 244 246); color: rgb(209 213 219);
background: rgb(55 65 81); background: rgb(55 65 81);
} }
.messagebox-content { /* Body */
.messagebox-body {
padding: 1.75rem 2rem 1.5rem;
}
.messagebox-main {
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
padding: 1rem 1.5rem;
gap: 1rem; gap: 1rem;
} }
.messagebox-icon { /* Icon Wrapper */
.messagebox-icon-wrapper {
display: flex;
flex-shrink: 0; flex-shrink: 0;
color: rgb(107 114 128); justify-content: center;
align-items: center;
border-radius: 0.625rem;
width: 3rem;
height: 3rem;
animation: icon-pop 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
} }
.messagebox-icon svg[data-lucide='alert-triangle'] { @keyframes icon-pop {
0% {
opacity: 0;
transform: scale(0);
}
50% {
transform: scale(1.1);
}
100% {
opacity: 1;
transform: scale(1);
}
}
.messagebox-icon-warning {
color: rgb(245 158 11); color: rgb(245 158 11);
background: rgb(254 243 199);
} }
.messagebox-icon svg[data-lucide='info'] { :root.dark .messagebox-icon-warning,
:root.auto.dark .messagebox-icon-warning {
color: rgb(251 191 36);
background: rgb(120 53 15 / 30%);
}
.messagebox-icon-info {
color: rgb(59 130 246); color: rgb(59 130 246);
background: rgb(219 234 254);
} }
.messagebox-icon svg[data-lucide='check-circle'] { :root.dark .messagebox-icon-info,
:root.auto.dark .messagebox-icon-info {
color: rgb(96 165 250);
background: rgb(30 58 138 / 30%);
}
.messagebox-icon-success {
color: rgb(34 197 94); color: rgb(34 197 94);
background: rgb(220 252 231);
} }
.messagebox-icon svg[data-lucide='x-circle'] { :root.dark .messagebox-icon-success,
:root.auto.dark .messagebox-icon-success {
color: rgb(74 222 128);
background: rgb(20 83 45 / 30%);
}
.messagebox-icon-error {
color: rgb(239 68 68); color: rgb(239 68 68);
background: rgb(254 226 226);
}
:root.dark .messagebox-icon-error,
:root.auto.dark .messagebox-icon-error {
color: rgb(248 113 113);
background: rgb(127 29 29 / 30%);
}
/* Content */
.messagebox-content {
flex: 1;
min-width: 0;
}
.messagebox-title {
margin: 0 0 0.375rem;
font-size: 1.0625rem;
font-weight: 600;
line-height: 1.4;
color: rgb(17 24 39);
}
:root.dark .messagebox-title,
:root.auto.dark .messagebox-title {
color: rgb(243 244 246);
} }
.messagebox-message { .messagebox-message {
flex: 1;
}
.messagebox-message p {
margin: 0; margin: 0;
font-size: 0.9375rem;
line-height: 1.5;
color: rgb(107 114 128); color: rgb(107 114 128);
line-height: 1.6;
} }
:root.dark .messagebox-message p, :root.dark .messagebox-message,
:root.auto.dark .messagebox-message p { :root.auto.dark .messagebox-message {
color: rgb(156 163 175); color: rgb(156 163 175);
} }
/* Actions */
.messagebox-actions { .messagebox-actions {
display: flex; display: flex;
justify-content: flex-end; border-top: 1px solid rgb(243 244 246);
padding: 0 1.5rem 1.5rem; padding: 1rem 1.5rem;
gap: 0.75rem; gap: 0.75rem;
} }
:root.dark .messagebox-actions,
:root.auto.dark .messagebox-actions {
border-top-color: rgb(55 65 81);
}
.messagebox-actions.center { .messagebox-actions.center {
justify-content: center; justify-content: center;
} }
.messagebox-btn { .messagebox-btn {
flex: 1;
border: none; border: none;
border-radius: 0.375rem; border-radius: 0.5rem;
padding: 0.5rem 1rem; padding: 0.625rem 1.25rem;
min-width: 4rem;
font-size: 0.875rem; font-size: 0.875rem;
font-weight: 500; font-weight: 500;
transition: all 0.15s ease;
cursor: pointer; cursor: pointer;
} }
.messagebox-btn:active {
transform: scale(0.98);
}
/* Cancel Button */
.cancel-btn { .cancel-btn {
border: 1px solid rgb(209 213 219); border: 1px solid rgb(229 231 235);
color: rgb(75 85 99); color: rgb(75 85 99);
background: rgb(243 244 246); background: white;
} }
.cancel-btn:hover { .cancel-btn:hover {
background: rgb(229 231 235); border-color: rgb(209 213 219);
background: rgb(249 250 251);
} }
:root.dark .cancel-btn, :root.dark .cancel-btn,
:root.auto.dark .cancel-btn { :root.auto.dark .cancel-btn {
border-color: rgb(75 85 99); border-color: rgb(55 65 81);
color: rgb(209 213 219); color: rgb(209 213 219);
background: rgb(55 65 81); background: rgb(55 65 81);
} }
:root.dark .cancel-btn:hover, :root.dark .cancel-btn:hover,
:root.auto.dark .cancel-btn:hover { :root.auto.dark .cancel-btn:hover {
border-color: rgb(75 85 99);
background: rgb(75 85 99); background: rgb(75 85 99);
} }
.confirm-btn.primary { /* Confirm Buttons */
.confirm-btn {
border: none;
color: white; color: white;
background: rgb(59 130 246); box-shadow: 0 1px 2px 0 rgb(0 0 0 / 5%);
}
.confirm-btn.primary {
background: linear-gradient(135deg, rgb(59 130 246) 0%, rgb(37 99 235) 100%);
} }
.confirm-btn.primary:hover { .confirm-btn.primary:hover {
background: rgb(37 99 235); background: linear-gradient(135deg, rgb(37 99 235) 0%, rgb(29 78 216) 100%);
box-shadow: 0 4px 12px rgb(59 130 246 / 40%);
} }
.confirm-btn.danger { .confirm-btn.danger {
color: white; background: linear-gradient(135deg, rgb(239 68 68) 0%, rgb(220 38 38) 100%);
background: rgb(239 68 68);
} }
.confirm-btn.danger:hover { .confirm-btn.danger:hover {
background: rgb(220 38 38); background: linear-gradient(135deg, rgb(220 38 38) 0%, rgb(185 28 28) 100%);
box-shadow: 0 4px 12px rgb(239 68 68 / 40%);
} }
.confirm-btn.success { .confirm-btn.success {
color: white; background: linear-gradient(135deg, rgb(34 197 94) 0%, rgb(22 163 74) 100%);
background: rgb(34 197 94);
} }
.confirm-btn.success:hover { .confirm-btn.success:hover {
background: rgb(22 163 74); background: linear-gradient(135deg, rgb(22 163 74) 0%, rgb(21 128 61) 100%);
box-shadow: 0 4px 12px rgb(34 197 94 / 40%);
}
/* Responsive */
@media (width <= 640px) {
.messagebox-overlay {
align-items: flex-end;
padding: 0;
}
.messagebox-container {
border-radius: 1rem 1rem 0 0;
max-width: 100%;
}
.messagebox-body {
padding: 1.5rem 1.5rem 1.25rem;
}
.messagebox-main {
gap: 0.875rem;
}
.messagebox-icon-wrapper {
width: 2.75rem;
height: 2.75rem;
}
.messagebox-actions {
flex-direction: column-reverse;
}
.messagebox-btn {
width: 100%;
}
} }
</style> </style>

View File

@@ -194,7 +194,6 @@ export default {
color: rgb(75 85 99); color: rgb(75 85 99);
flex: 1; flex: 1;
line-height: 1.25rem; line-height: 1.25rem;
word-break: break-word;
overflow-wrap: break-word; overflow-wrap: break-word;
hyphens: auto; hyphens: auto;
} }

View File

@@ -918,10 +918,16 @@
}, },
"uploaderConfig": { "uploaderConfig": {
"addNew": "Add New", "addNew": "Add New",
"copy": "Copy",
"delete": "Delete", "delete": "Delete",
"deleteConfirm": "Are you sure you want to delete this PicBed config?", "deleteConfirm": "Are you sure you want to delete this PicBed config?",
"deleteSuccess": "Delete Success", "deleteSuccess": "Delete Success",
"deleteTitle": "Notification", "deleteTitle": "Notification",
"duplicate": "Duplicate",
"duplicateError": "Duplicate Failed",
"duplicatePlaceholder": "Enter new configuration name",
"duplicateSuccess": "Duplicate Success",
"duplicateTitle": "Duplicate Configuration",
"edit": "Edit", "edit": "Edit",
"selected": "Selected", "selected": "Selected",
"setAsDefault": "Set as Default PicBed", "setAsDefault": "Set as Default PicBed",

View File

@@ -913,10 +913,16 @@
}, },
"uploaderConfig": { "uploaderConfig": {
"addNew": "新增", "addNew": "新增",
"copy": "副本",
"delete": "删除", "delete": "删除",
"deleteConfirm": "确认删除图床配置吗?", "deleteConfirm": "确认删除图床配置吗?",
"deleteSuccess": "删除成功", "deleteSuccess": "删除成功",
"deleteTitle": "通知", "deleteTitle": "通知",
"duplicate": "拷贝",
"duplicateError": "拷贝失败",
"duplicatePlaceholder": "请输入新配置名称",
"duplicateSuccess": "拷贝成功",
"duplicateTitle": "拷贝配置",
"edit": "编辑", "edit": "编辑",
"selected": "已选中", "selected": "已选中",
"setAsDefault": "设为默认图床", "setAsDefault": "设为默认图床",

View File

@@ -913,10 +913,16 @@
}, },
"uploaderConfig": { "uploaderConfig": {
"addNew": "新增", "addNew": "新增",
"copy": "副本",
"delete": "刪除", "delete": "刪除",
"deleteConfirm": "確認刪除圖床配置嗎?", "deleteConfirm": "確認刪除圖床配置嗎?",
"deleteSuccess": "刪除成功", "deleteSuccess": "刪除成功",
"deleteTitle": "通知", "deleteTitle": "通知",
"duplicate": "拷贝",
"duplicateError": "拷贝失败",
"duplicatePlaceholder": "請輸入新配置名稱",
"duplicateSuccess": "拷贝成功",
"duplicateTitle": "拷贝配置",
"edit": "編輯", "edit": "編輯",
"selected": "已選取", "selected": "已選取",
"setAsDefault": "設為預設圖床", "setAsDefault": "設為預設圖床",

View File

@@ -107,10 +107,7 @@ export default { name: 'MainPage' }
--color-primary-hover: #818cf8; --color-primary-hover: #818cf8;
--color-accent: #0a84ff; --color-accent: #0a84ff;
--color-accent-hover: #409cff; --color-accent-hover: #409cff;
}
:root.dark,
:root.auto.dark {
h1, h1,
h2, h2,
h3, h3,
@@ -203,7 +200,6 @@ body {
.content-container { .content-container {
margin: 0; margin: 0;
padding: 0.3 rem;
max-width: none; max-width: none;
height: 100%; height: 100%;
} }

View File

@@ -667,7 +667,7 @@
} }
.file-actions-dropdown-content { .file-actions-dropdown-content {
position: absolute; position: absolute;
top: 100%; top: 100%;
right: 0; right: 0;
z-index: 3000; z-index: 3000;
@@ -676,8 +676,10 @@
margin-top: 0.25rem; margin-top: 0.25rem;
border: 1px solid var(--color-border); border: 1px solid var(--color-border);
border-radius: var(--radius-md); border-radius: var(--radius-md);
min-width: 140px; min-width: 120px;
max-width: 200px; max-width: 200px;
/* Ensure dropdown is never clipped */
max-height: 300px; max-height: 300px;
white-space: nowrap; white-space: nowrap;
background: var(--color-surface); background: var(--color-surface);
@@ -728,12 +730,16 @@
} }
.file-actions-dropdown-item { .file-actions-dropdown-item {
display: flex;
align-items: center;
border-bottom: 1px solid var(--color-border-secondary); border-bottom: 1px solid var(--color-border-secondary);
padding: 0.5rem 0.75rem; padding: 0.5rem 0.75rem;
font-size: 0.875rem; font-size: 0.875rem;
color: var(--color-text-primary); color: var(--color-text-primary);
background: var(--color-background-primary);
transition: var(--transition-fast); transition: var(--transition-fast);
cursor: pointer; cursor: pointer;
gap: 0.5rem;
} }
.file-actions-dropdown-item:last-child { .file-actions-dropdown-item:last-child {
@@ -741,7 +747,7 @@
} }
.file-actions-dropdown-item:hover { .file-actions-dropdown-item:hover {
color: var(--color-accent); color: var(--color-blue-common);
background: var(--color-surface-elevated); background: var(--color-surface-elevated);
} }
@@ -1138,55 +1144,6 @@ input:checked + .switch-slider::before {
} }
/* File Actions Dropdown */ /* File Actions Dropdown */
.file-actions-dropdown {
position: relative;
z-index: 100;
}
.file-actions-dropdown-content {
position: absolute;
top: 100%;
right: 0;
z-index: 3000;
overflow: visible;
overflow-y: auto;
margin-top: 0.25rem;
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
min-width: 120px;
max-width: 200px;
/* Ensure dropdown is never clipped */
max-height: 300px;
white-space: nowrap;
background: var(--color-surface);
box-shadow: var(--shadow-lg);
transform-origin: top right;
animation: dropdown-appear 0.15s ease-out;
}
.file-actions-dropdown-content.position-left {
right: auto;
left: 0;
transform-origin: top left;
}
.file-actions-dropdown-content.position-up {
top: auto;
bottom: 100%;
margin-top: 0;
margin-bottom: 0.25rem;
transform-origin: bottom right;
}
.file-actions-dropdown-content.position-up.position-left {
transform-origin: bottom left;
}
.content-fullscreen .file-actions-dropdown-content {
z-index: 4000;
}
@media (width <= 768px) { @media (width <= 768px) {
.file-actions-dropdown-content { .file-actions-dropdown-content {
min-width: 100px; min-width: 100px;
@@ -1194,22 +1151,6 @@ input:checked + .switch-slider::before {
} }
} }
.file-actions-dropdown-item {
display: flex;
align-items: center;
padding: 0.5rem 0.75rem;
font-size: 0.875rem;
color: var(--color-text-primary);
background: var(--color-background-primary);
transition: var(--transition-fast);
cursor: pointer;
gap: 0.5rem;
}
.file-actions-dropdown-item:hover {
color: var(--color-blue-common);
}
/* Empty State */ /* Empty State */
.empty-state { .empty-state {
display: flex; display: flex;

View File

@@ -308,6 +308,9 @@
margin-top: 0.75rem; margin-top: 0.75rem;
border: 1px solid var(--color-border-secondary); border: 1px solid var(--color-border-secondary);
border-radius: var(--radius-md); border-radius: var(--radius-md);
width: 100%;
border-collapse: collapse;
font-size: 0.875rem;
} }
.table-row { .table-row {
@@ -590,11 +593,6 @@ input:checked + .switch-slider::before {
border-radius: var(--radius-md); border-radius: var(--radius-md);
} }
.config-table {
width: 100%;
border-collapse: collapse;
font-size: 0.875rem;
}
.config-table th { .config-table th {
border-bottom: 1px solid var(--color-border-secondary); border-bottom: 1px solid var(--color-border-secondary);

View File

@@ -212,7 +212,7 @@
min-width: 0; min-width: 0;
font-weight: 500; font-weight: 500;
line-height: 1.2; line-height: 1.2;
word-break: break-word; overflow-wrap: break-word;
flex: 1; flex: 1;
} }

View File

@@ -37,6 +37,13 @@
> >
<Edit :size="16" /> <Edit :size="16" />
</button> </button>
<button
class="action-btn duplicate-btn"
:title="t('pages.uploaderConfig.duplicate')"
@click.stop="() => duplicateConfig(item._id)"
>
<Copy :size="16" />
</button>
<button <button
class="action-btn delete-btn" class="action-btn delete-btn"
:class="curConfigList.length <= 1 ? 'disabled' : ''" :class="curConfigList.length <= 1 ? 'disabled' : ''"
@@ -73,7 +80,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { DatabaseIcon, Edit, Plus, Trash2 } from 'lucide-vue-next' import { Copy, DatabaseIcon, Edit, Plus, Trash2 } from 'lucide-vue-next'
import { onBeforeMount, ref } from 'vue' import { onBeforeMount, ref } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { onBeforeRouteUpdate, useRoute, useRouter } from 'vue-router' import { onBeforeRouteUpdate, useRoute, useRouter } from 'vue-router'
@@ -82,7 +89,9 @@ import useConfirm from '@/hooks/useConfirm'
import useMessage from '@/hooks/useMessage' import useMessage from '@/hooks/useMessage'
import { useStore } from '@/hooks/useStore' import { useStore } from '@/hooks/useStore'
import { PICBEDS_PAGE, UPLOADER_CONFIG_PAGE } from '@/router/config' import { PICBEDS_PAGE, UPLOADER_CONFIG_PAGE } from '@/router/config'
import $bus from '@/utils/bus'
import { configPaths } from '@/utils/configPaths' import { configPaths } from '@/utils/configPaths'
import { SHOW_INPUT_BOX, SHOW_INPUT_BOX_RESPONSE } from '@/utils/constant'
import { saveConfig } from '@/utils/dataSender' import { saveConfig } from '@/utils/dataSender'
import { IRPCActionType } from '@/utils/enum' import { IRPCActionType } from '@/utils/enum'
import type { IStringKeyMap, IUploaderConfigItem } from '#/types/types' import type { IStringKeyMap, IUploaderConfigItem } from '#/types/types'
@@ -148,6 +157,49 @@ function formatTime(time: number): string {
return dayjs(time).format('YYYY-MM-DD HH:mm') return dayjs(time).format('YYYY-MM-DD HH:mm')
} }
async function duplicateConfig(id: string) {
const originalConfig = curConfigList.value.find(item => item._id === id)
if (!originalConfig) return
return new Promise<void>(resolve => {
$bus.emit(SHOW_INPUT_BOX, {
title: t('pages.uploaderConfig.duplicateTitle'),
placeholder: t('pages.uploaderConfig.duplicatePlaceholder'),
value: `${originalConfig._configName} - ${t('pages.uploaderConfig.copy')}`,
})
const handleResponse = async (newName: string) => {
$bus.off(SHOW_INPUT_BOX_RESPONSE, handleResponse)
if (!newName) {
resolve()
return
}
try {
const res = await window.electron.triggerRPC<IUploaderConfigItem>(
IRPCActionType.PICBED_DUPLICATE_CONFIG,
type.value,
id,
newName,
)
if (!res) {
resolve()
return
}
curConfigList.value = res.configList
defaultConfigId.value = res.defaultId
message.success(t('pages.uploaderConfig.duplicateSuccess'))
} catch (error) {
message.error(t('pages.uploaderConfig.duplicateError'))
}
resolve()
}
$bus.on(SHOW_INPUT_BOX_RESPONSE, handleResponse)
})
}
async function deleteConfig(id: string) { async function deleteConfig(id: string) {
const result = await confirm({ const result = await confirm({
title: t('pages.uploaderConfig.deleteTitle'), title: t('pages.uploaderConfig.deleteTitle'),

View File

@@ -160,6 +160,11 @@
color: var(--color-accent); color: var(--color-accent);
} }
.duplicate-btn:hover {
border-color: var(--color-info);
color: var(--color-info);
}
.delete-btn:hover:not(.disabled) { .delete-btn:hover:not(.disabled) {
border-color: var(--color-danger); border-color: var(--color-danger);
color: var(--color-danger); color: var(--color-danger);

View File

@@ -39,6 +39,7 @@ export const IRPCActionType = {
PICBED_GET_PICBED_CONFIG: 'PICBED_GET_PICBED_CONFIG', PICBED_GET_PICBED_CONFIG: 'PICBED_GET_PICBED_CONFIG',
PICBED_GET_CONFIG_LIST: 'PICBED_GET_CONFIG_LIST', PICBED_GET_CONFIG_LIST: 'PICBED_GET_CONFIG_LIST',
PICBED_DELETE_CONFIG: 'PICBED_DELETE_CONFIG', PICBED_DELETE_CONFIG: 'PICBED_DELETE_CONFIG',
PICBED_DUPLICATE_CONFIG: 'PICBED_DUPLICATE_CONFIG',
UPLOADER_CHANGE_CURRENT: 'UPLOADER_CHANGE_CURRENT', UPLOADER_CHANGE_CURRENT: 'UPLOADER_CHANGE_CURRENT',
UPLOADER_SELECT: 'UPLOADER_SELECT', UPLOADER_SELECT: 'UPLOADER_SELECT',
UPLOADER_UPDATE_CONFIG: 'UPLOADER_UPDATE_CONFIG', UPLOADER_UPDATE_CONFIG: 'UPLOADER_UPDATE_CONFIG',