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 {
deleteUploaderConfig,
duplicateUploaderConfig,
getUploaderConfigList,
resetUploaderConfig,
selectUploaderConfig,
@@ -45,6 +46,15 @@ const picbedRoutes = [
},
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,
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_CONFIG_LIST: 'PICBED_GET_CONFIG_LIST',
PICBED_DELETE_CONFIG: 'PICBED_DELETE_CONFIG',
PICBED_DUPLICATE_CONFIG: 'PICBED_DUPLICATE_CONFIG',
UPLOADER_CHANGE_CURRENT: 'UPLOADER_CHANGE_CURRENT',
UPLOADER_SELECT: 'UPLOADER_SELECT',
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
*/

View File

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

View File

@@ -1,42 +1,41 @@
<template>
<div v-if="isOpen" class="messagebox-overlay" @click="onCancel">
<div class="messagebox-container" @click.stop>
<div class="messagebox-header">
<h3 class="messagebox-title">
{{ title }}
</h3>
<button v-if="showClose" class="messagebox-close" @click="onCancel">×</button>
</div>
<div class="messagebox-content">
<div v-if="type" class="messagebox-icon">
<component :is="iconComponent" :size="48" />
<Transition name="messagebox-fade">
<div v-if="isOpen" class="messagebox-overlay" @click="onCancel">
<Transition name="messagebox-scale">
<div v-if="isOpen" class="messagebox-container" @click.stop>
<button v-if="showClose" class="messagebox-close" @click="onCancel">
<XIcon :size="20" />
</button>
<div class="messagebox-body">
<div class="messagebox-main">
<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 class="messagebox-message">
<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>
</Transition>
</div>
</div>
</Transition>
</template>
<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'
interface Props {
@@ -109,6 +108,36 @@ export default {
</script>
<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 {
position: fixed;
inset: 0;
@@ -116,15 +145,19 @@ export default {
display: flex;
justify-content: 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 {
position: relative;
overflow: hidden;
border-radius: 0.75rem;
width: 90%;
max-width: 32rem;
max-height: 80vh;
border: 1px solid rgb(229 231 235);
border-radius: 1rem;
width: 100%;
max-width: 26rem;
background: white;
box-shadow:
0 20px 25px -5px rgb(0 0 0 / 10%),
@@ -133,46 +166,30 @@ export default {
:root.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);
}
.messagebox-header {
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);
}
/* Close Button */
.messagebox-close {
position: absolute;
top: 1rem;
right: 1rem;
z-index: 10;
display: flex;
justify-content: center;
align-items: center;
border: none;
border-radius: 0.25rem;
padding: 0;
width: 24px;
height: 24px;
font-size: 1.5rem;
border-radius: 0.5rem;
padding: 0.375rem;
color: rgb(107 114 128);
background: none;
background: transparent;
transition: all 0.15s ease;
cursor: pointer;
}
.messagebox-close:hover {
color: rgb(17 24 39);
color: rgb(75 85 99);
background: rgb(243 244 246);
}
@@ -183,120 +200,246 @@ export default {
:root.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);
}
.messagebox-content {
/* Body */
.messagebox-body {
padding: 1.75rem 2rem 1.5rem;
}
.messagebox-main {
display: flex;
align-items: flex-start;
padding: 1rem 1.5rem;
gap: 1rem;
}
.messagebox-icon {
/* Icon Wrapper */
.messagebox-icon-wrapper {
display: flex;
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);
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);
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);
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);
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 {
flex: 1;
}
.messagebox-message p {
margin: 0;
font-size: 0.9375rem;
line-height: 1.5;
color: rgb(107 114 128);
line-height: 1.6;
}
:root.dark .messagebox-message p,
:root.auto.dark .messagebox-message p {
:root.dark .messagebox-message,
:root.auto.dark .messagebox-message {
color: rgb(156 163 175);
}
/* Actions */
.messagebox-actions {
display: flex;
justify-content: flex-end;
padding: 0 1.5rem 1.5rem;
border-top: 1px solid rgb(243 244 246);
padding: 1rem 1.5rem;
gap: 0.75rem;
}
:root.dark .messagebox-actions,
:root.auto.dark .messagebox-actions {
border-top-color: rgb(55 65 81);
}
.messagebox-actions.center {
justify-content: center;
}
.messagebox-btn {
flex: 1;
border: none;
border-radius: 0.375rem;
padding: 0.5rem 1rem;
min-width: 4rem;
border-radius: 0.5rem;
padding: 0.625rem 1.25rem;
font-size: 0.875rem;
font-weight: 500;
transition: all 0.15s ease;
cursor: pointer;
}
.messagebox-btn:active {
transform: scale(0.98);
}
/* Cancel Button */
.cancel-btn {
border: 1px solid rgb(209 213 219);
border: 1px solid rgb(229 231 235);
color: rgb(75 85 99);
background: rgb(243 244 246);
background: white;
}
.cancel-btn:hover {
background: rgb(229 231 235);
border-color: rgb(209 213 219);
background: rgb(249 250 251);
}
:root.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);
background: rgb(55 65 81);
}
:root.dark .cancel-btn:hover,
:root.auto.dark .cancel-btn:hover {
border-color: rgb(75 85 99);
background: rgb(75 85 99);
}
.confirm-btn.primary {
/* Confirm Buttons */
.confirm-btn {
border: none;
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 {
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 {
color: white;
background: rgb(239 68 68);
background: linear-gradient(135deg, rgb(239 68 68) 0%, rgb(220 38 38) 100%);
}
.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 {
color: white;
background: rgb(34 197 94);
background: linear-gradient(135deg, rgb(34 197 94) 0%, rgb(22 163 74) 100%);
}
.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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -667,7 +667,7 @@
}
.file-actions-dropdown-content {
position: absolute;
position: absolute;
top: 100%;
right: 0;
z-index: 3000;
@@ -676,8 +676,10 @@
margin-top: 0.25rem;
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
min-width: 140px;
min-width: 120px;
max-width: 200px;
/* Ensure dropdown is never clipped */
max-height: 300px;
white-space: nowrap;
background: var(--color-surface);
@@ -728,12 +730,16 @@
}
.file-actions-dropdown-item {
display: flex;
align-items: center;
border-bottom: 1px solid var(--color-border-secondary);
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:last-child {
@@ -741,7 +747,7 @@
}
.file-actions-dropdown-item:hover {
color: var(--color-accent);
color: var(--color-blue-common);
background: var(--color-surface-elevated);
}
@@ -1138,55 +1144,6 @@ input:checked + .switch-slider::before {
}
/* 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) {
.file-actions-dropdown-content {
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 {
display: flex;

View File

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

View File

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

View File

@@ -37,6 +37,13 @@
>
<Edit :size="16" />
</button>
<button
class="action-btn duplicate-btn"
:title="t('pages.uploaderConfig.duplicate')"
@click.stop="() => duplicateConfig(item._id)"
>
<Copy :size="16" />
</button>
<button
class="action-btn delete-btn"
:class="curConfigList.length <= 1 ? 'disabled' : ''"
@@ -73,7 +80,7 @@
<script lang="ts" setup>
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 { useI18n } from 'vue-i18n'
import { onBeforeRouteUpdate, useRoute, useRouter } from 'vue-router'
@@ -82,7 +89,9 @@ import useConfirm from '@/hooks/useConfirm'
import useMessage from '@/hooks/useMessage'
import { useStore } from '@/hooks/useStore'
import { PICBEDS_PAGE, UPLOADER_CONFIG_PAGE } from '@/router/config'
import $bus from '@/utils/bus'
import { configPaths } from '@/utils/configPaths'
import { SHOW_INPUT_BOX, SHOW_INPUT_BOX_RESPONSE } from '@/utils/constant'
import { saveConfig } from '@/utils/dataSender'
import { IRPCActionType } from '@/utils/enum'
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')
}
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) {
const result = await confirm({
title: t('pages.uploaderConfig.deleteTitle'),

View File

@@ -160,6 +160,11 @@
color: var(--color-accent);
}
.duplicate-btn:hover {
border-color: var(--color-info);
color: var(--color-info);
}
.delete-btn:hover:not(.disabled) {
border-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_CONFIG_LIST: 'PICBED_GET_CONFIG_LIST',
PICBED_DELETE_CONFIG: 'PICBED_DELETE_CONFIG',
PICBED_DUPLICATE_CONFIG: 'PICBED_DUPLICATE_CONFIG',
UPLOADER_CHANGE_CURRENT: 'UPLOADER_CHANGE_CURRENT',
UPLOADER_SELECT: 'UPLOADER_SELECT',
UPLOADER_UPDATE_CONFIG: 'UPLOADER_UPDATE_CONFIG',