Feature(custom): optimize UI of process page

This commit is contained in:
Kuingsmile
2026-01-21 12:58:04 +08:00
parent 258ed48a92
commit 0801bf063f
8 changed files with 333 additions and 947 deletions

View File

@@ -1,13 +1,16 @@
<template> <template>
<div class="image-process-settings"> <div class="no-scrollbar flex h-full flex-col gap-5 overflow-auto border-none p-3 text-main">
<!-- Tab Navigation --> <!-- Tab Navigation -->
<div class="tab-navigation"> <div class="relative flex flex-wrap rounded-xl border border-border-secondary/50 p-2 shadow-sm">
<div class="tab-indicator" :style="tabIndicatorStyle" /> <div
class="absolute z-0 rounded-lg bg-accent shadow-md transition-all duration-medium ease-bounce"
:style="tabIndicatorStyle"
/>
<button <button
v-for="tab in tabs" v-for="tab in tabs"
ref="tabRefs" ref="tabRefs"
:key="tab.id" :key="tab.id"
class="tab-button" class="relative z-1 flex flex-1 cursor-pointer items-center justify-center gap-2.5 rounded-lg bg-transparent px-5 py-3.5 text-sm font-semibold text-secondary transition-all duration-medium ease-in-out not-[.active]:hover:bg-accent/50 not-[.active]:hover:text-main [.active]:font-bold [.active]:text-white"
:class="{ active: activeTab === tab.id }" :class="{ active: activeTab === tab.id }"
@click="activeTab = tab.id" @click="activeTab = tab.id"
> >
@@ -17,10 +20,19 @@
</div> </div>
<!-- Settings Content --> <!-- Settings Content -->
<div class="settings-content"> <div
<transition name="fade-slide" mode="out-in"> class="no-scrollbar flex flex-1 flex-col overflow-auto rounded-lg border border-border-secondary/50 p-4 shadow-sm"
>
<transition
name="fade-slide"
enter-active-class="transition-all duration-medium ease-apple"
leave-active-class="transition-all duration-medium ease-apple"
enter-from-class="opacity-0 translate-y-[12px]"
leave-to-class="opacity-0 -translate-y-[12px]"
mode="out-in"
>
<!-- General Settings Tab --> <!-- General Settings Tab -->
<div v-if="activeTab === 'general'" key="general" class="tab-content"> <div v-if="activeTab === 'general'" key="general" class="flex flex-col gap-4">
<div class="settings-section"> <div class="settings-section">
<div class="section-header"> <div class="section-header">
<div class="section-icon"> <div class="section-icon">
@@ -33,13 +45,11 @@
<div class="form-grid"> <div class="form-grid">
<div class="form-group"> <div class="form-group">
<label class="switch-label"> <customSwitch
<input v-model="activeForm.compress.isRemoveExif" type="checkbox" class="switch-input" /> v-model="activeForm.compress.isRemoveExif"
<span class="switch-slider" /> :title="$t('pages.imageProcess.general.isRemoveExif')"
<div class="switch-content"> class="custom-switch"
<span class="switch-title">{{ $t('pages.imageProcess.general.isRemoveExif') }}</span> />
</div>
</label>
<PerPicbedSetting <PerPicbedSetting
v-if="!configId" v-if="!configId"
@@ -62,9 +72,14 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label>{{ $t('pages.imageProcess.general.quality') }}</label> <customRange
<input v-model.number="activeForm.compress.quality" type="range" min="1" max="100" class="form-range" /> v-model.number="activeForm.compress.quality"
<div class="range-value">{{ activeForm.compress.quality }}%</div> :title="$t('pages.imageProcess.general.quality')"
:min="1"
:max="100"
:step="1"
:show-value="`${activeForm.compress.quality}%`"
/>
<PerPicbedSetting <PerPicbedSetting
v-if="!configId" v-if="!configId"
@@ -97,13 +112,11 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="switch-label"> <customSwitch
<input v-model="activeForm.compress.isConvert" type="checkbox" class="switch-input" /> v-model="activeForm.compress.isConvert"
<span class="switch-slider" /> :title="$t('pages.imageProcess.general.isConvert')"
<div class="switch-content"> class="custom-switch"
<span class="switch-title">{{ $t('pages.imageProcess.general.isConvert') }}</span> />
</div>
</label>
<PerPicbedSetting <PerPicbedSetting
v-if="!configId" v-if="!configId"
@@ -121,7 +134,7 @@
<div v-if="activeForm.compress.isConvert" class="form-grid"> <div v-if="activeForm.compress.isConvert" class="form-grid">
<div class="form-group"> <div class="form-group">
<label>{{ $t('pages.imageProcess.general.destinationFormat') }}</label> <label class="title-text">{{ $t('pages.imageProcess.general.destinationFormat') }}</label>
<select v-model="activeForm.compress.convertFormat" class="form-input"> <select v-model="activeForm.compress.convertFormat" class="form-input">
<option v-for="format in availableFormat" :key="format" :value="format"> <option v-for="format in availableFormat" :key="format" :value="format">
{{ format.toUpperCase() }} {{ format.toUpperCase() }}
@@ -150,7 +163,7 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label>{{ $t('pages.imageProcess.general.specificFormatConversion') }}</label> <label class="title-text">{{ $t('pages.imageProcess.general.specificFormatConversion') }}</label>
<textarea <textarea
v-model="convertStr" v-model="convertStr"
class="form-textarea" class="form-textarea"
@@ -183,7 +196,7 @@
</div> </div>
<!-- Watermark Tab --> <!-- Watermark Tab -->
<div v-else-if="activeTab === 'watermark'" key="watermark" class="tab-content"> <div v-else-if="activeTab === 'watermark'" key="watermark" class="flex flex-col gap-4">
<div class="settings-section"> <div class="settings-section">
<div class="section-header"> <div class="section-header">
<div class="section-icon watermark-icon"> <div class="section-icon watermark-icon">
@@ -196,13 +209,11 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="switch-label"> <customSwitch
<input v-model="activeForm.watermark.isAddWatermark" type="checkbox" class="switch-input" /> v-model="activeForm.watermark.isAddWatermark"
<span class="switch-slider" /> :title="$t('pages.imageProcess.watermark.isAdd')"
<div class="switch-content"> class="custom-switch"
<span class="switch-title">{{ $t('pages.imageProcess.watermark.isAdd') }}</span> />
</div>
</label>
<PerPicbedSetting <PerPicbedSetting
v-if="!configId" v-if="!configId"
@@ -224,25 +235,23 @@
/> />
</div> </div>
<div v-if="activeForm.watermark.isAddWatermark" class="watermark-settings"> <div
v-if="activeForm.watermark.isAddWatermark"
class="mt-4 border-t border-t-border pt-3 transition-all duration-200 ease-apple"
>
<div class="form-group"> <div class="form-group">
<label>{{ $t('pages.imageProcess.watermark.type') }}</label> <label class="title-text">{{ $t('pages.imageProcess.watermark.type') }}</label>
<div class="radio-group"> <div class="flex flex-wrap gap-4">
<label class="radio-option"> <customRadioOption
<input v-model="activeForm.watermark.watermarkType" type="radio" value="text" class="radio-input" /> v-model="activeForm.watermark.watermarkType"
<span class="radio-indicator" /> value="text"
<span class="radio-label">{{ $t('pages.imageProcess.watermark.text') }}</span> :title="$t('pages.imageProcess.watermark.text')"
</label> />
<label class="radio-option"> <customRadioOption
<input v-model="activeForm.watermark.watermarkType"
v-model="activeForm.watermark.watermarkType" value="image"
type="radio" :title="$t('pages.imageProcess.watermark.image')"
value="image" />
class="radio-input"
/>
<span class="radio-indicator" />
<span class="radio-label">{{ $t('pages.imageProcess.watermark.image') }}</span>
</label>
</div> </div>
<PerPicbedSetting <PerPicbedSetting
@@ -271,13 +280,11 @@
<div class="form-grid"> <div class="form-grid">
<div class="form-group"> <div class="form-group">
<label class="switch-label"> <customSwitch
<input v-model="activeForm.watermark.isFullScreenWatermark" type="checkbox" class="switch-input" /> v-model="activeForm.watermark.isFullScreenWatermark"
<span class="switch-slider" /> :title="$t('pages.imageProcess.watermark.isFullScreen')"
<div class="switch-content"> class="custom-switch"
<span class="switch-title">{{ $t('pages.imageProcess.watermark.isFullScreen') }}</span> />
</div>
</label>
<PerPicbedSetting <PerPicbedSetting
v-if="!configId" v-if="!configId"
@@ -300,15 +307,14 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label>{{ $t('pages.imageProcess.watermark.degree') }}</label> <customRange
<input
v-model.number="activeForm.watermark.watermarkDegree" v-model.number="activeForm.watermark.watermarkDegree"
type="range" :title="$t('pages.imageProcess.watermark.degree')"
min="-360" :min="-360"
max="360" :max="360"
class="form-range" :step="1"
:show-value="`${activeForm.watermark.watermarkDegree}°`"
/> />
<div class="range-value">{{ activeForm.watermark.watermarkDegree }}°</div>
<PerPicbedSetting <PerPicbedSetting
v-if="!configId" v-if="!configId"
@@ -335,18 +341,14 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label>{{ $t('pages.imageProcess.watermark.scaleRatio') }}</label> <customRange
<input
v-model.number="activeForm.watermark.watermarkScaleRatio" v-model.number="activeForm.watermark.watermarkScaleRatio"
type="range" :title="$t('pages.imageProcess.watermark.scaleRatio')"
min="0" :min="0"
max="1" :max="1"
step="0.01" :step="0.01"
class="form-range" :show-value="`${Math.round((activeForm.watermark.watermarkScaleRatio || 0) * 100)}%`"
/> />
<div class="range-value">
{{ Math.round((activeForm.watermark.watermarkScaleRatio || 0) * 100) }}%
</div>
<PerPicbedSetting <PerPicbedSetting
v-if="!configId" v-if="!configId"
@@ -375,7 +377,7 @@
<div v-if="activeForm.watermark.watermarkType === 'text'" class="form-grid"> <div v-if="activeForm.watermark.watermarkType === 'text'" class="form-grid">
<div class="form-group"> <div class="form-group">
<label>{{ $t('pages.imageProcess.watermark.inputText') }}</label> <label class="title-text">{{ $t('pages.imageProcess.watermark.inputText') }}</label>
<input <input
v-model="activeForm.watermark.watermarkText" v-model="activeForm.watermark.watermarkText"
type="text" type="text"
@@ -406,7 +408,7 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label>{{ $t('pages.imageProcess.watermark.textFontPath') }}</label> <label class="title-text">{{ $t('pages.imageProcess.watermark.textFontPath') }}</label>
<input <input
v-model="activeForm.watermark.watermarkFontPath" v-model="activeForm.watermark.watermarkFontPath"
type="text" type="text"
@@ -436,13 +438,17 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label>{{ $t('pages.imageProcess.watermark.color') }}</label> <label class="title-text">{{ $t('pages.imageProcess.watermark.color') }}</label>
<div class="color-input-group"> <div class="flex flex-wrap items-center gap-2">
<input v-model="activeForm.watermark.watermarkColor" type="color" class="form-color" /> <input
v-model="activeForm.watermark.watermarkColor"
type="color"
class="h-[48px] w-[48px] cursor-pointer overflow-hidden rounded-lg border border-border bg-bg p-0.5 transition-all duration-200 ease-apple hover:border-accent hover:shadow-sm focus:border-accent focus:shadow-sm focus:outline-none"
/>
<input <input
v-model="activeForm.watermark.watermarkColor" v-model="activeForm.watermark.watermarkColor"
type="text" type="text"
class="form-input" class="form-input flex-1"
placeholder="#CCCCCC73" placeholder="#CCCCCC73"
/> />
</div> </div>
@@ -470,7 +476,7 @@
<!-- Image Watermark Settings --> <!-- Image Watermark Settings -->
<div v-if="activeForm.watermark.watermarkType === 'image'" class="form-group"> <div v-if="activeForm.watermark.watermarkType === 'image'" class="form-group">
<label>{{ $t('pages.imageProcess.watermark.imagePath') }}</label> <label class="title-text">{{ $t('pages.imageProcess.watermark.imagePath') }}</label>
<input <input
v-model="activeForm.watermark.watermarkImagePath" v-model="activeForm.watermark.watermarkImagePath"
type="text" type="text"
@@ -500,18 +506,14 @@
</div> </div>
<div v-if="activeForm.watermark.watermarkType === 'image'" class="form-group"> <div v-if="activeForm.watermark.watermarkType === 'image'" class="form-group">
<label>{{ $t('pages.imageProcess.watermark.imageOpacity') }}</label> <customRange
<input
v-model.number="activeForm.watermark.watermarkImageOpacity" v-model.number="activeForm.watermark.watermarkImageOpacity"
type="range" :title="$t('pages.imageProcess.watermark.imageOpacity')"
min="0" :min="0"
max="255" :max="255"
step="1" :step="1"
class="form-range" :show-value="`${activeForm.watermark.watermarkImageOpacity || 0}`"
/> />
<div class="range-value">
{{ activeForm.watermark.watermarkImageOpacity || 0 }}
</div>
<PerPicbedSetting <PerPicbedSetting
v-if="!configId" v-if="!configId"
@@ -537,13 +539,13 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label>{{ $t('pages.imageProcess.watermark.position') }}</label> <label class="title-text">{{ $t('pages.imageProcess.watermark.position') }}</label>
<div class="position-grid"> <div class="grid max-w-[320px] grid-cols-3 gap-2.5">
<button <button
v-for="[key, label] in waterMarkPositionMap" v-for="[key, label] in waterMarkPositionMap"
:key="key" :key="key"
type="button" type="button"
class="position-button" class="rounded-lg border border-border-secondary bg-bg p-3 text-center text-sm font-semibold text-secondary transition-all duration-200 ease-apple hover:border-accent hover:bg-accent/8 hover:text-main [.active]:border-accent/10 [.active]:bg-accent/20 [.active]:text-main"
:class="{ active: activeForm.watermark.watermarkPosition === key }" :class="{ active: activeForm.watermark.watermarkPosition === key }"
@click="activeForm.watermark.watermarkPosition = key as any" @click="activeForm.watermark.watermarkPosition = key as any"
> >
@@ -581,7 +583,7 @@
</div> </div>
<!-- Transform Tab --> <!-- Transform Tab -->
<div v-else-if="activeTab === 'transform'" key="transform" class="tab-content"> <div v-else-if="activeTab === 'transform'" key="transform" class="flex flex-col gap-4">
<div class="settings-section"> <div class="settings-section">
<div class="section-header"> <div class="section-header">
<div class="section-icon transform-icon"> <div class="section-icon transform-icon">
@@ -595,13 +597,11 @@
<div class="form-grid"> <div class="form-grid">
<div class="form-group"> <div class="form-group">
<label class="switch-label"> <customSwitch
<input v-model="activeForm.compress.isFlip" type="checkbox" class="switch-input" /> v-model="activeForm.compress.isFlip"
<span class="switch-slider" /> :title="$t('pages.imageProcess.transform.isFlip')"
<div class="switch-content"> class="custom-switch"
<span class="switch-title">{{ $t('pages.imageProcess.transform.isFlip') }}</span> />
</div>
</label>
<PerPicbedSetting <PerPicbedSetting
v-if="!configId" v-if="!configId"
@@ -618,13 +618,11 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="switch-label"> <customSwitch
<input v-model="activeForm.compress.isFlop" type="checkbox" class="switch-input" /> v-model="activeForm.compress.isFlop"
<span class="switch-slider" /> :title="$t('pages.imageProcess.transform.isFlop')"
<div class="switch-content"> class="custom-switch"
<span class="switch-title">{{ $t('pages.imageProcess.transform.isFlop') }}</span> />
</div>
</label>
<PerPicbedSetting <PerPicbedSetting
v-if="!configId" v-if="!configId"
@@ -654,13 +652,11 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="switch-label"> <customSwitch
<input v-model="activeForm.compress.isRotate" type="checkbox" class="switch-input" /> v-model="activeForm.compress.isRotate"
<span class="switch-slider" /> :title="$t('pages.imageProcess.transform.isRotate')"
<div class="switch-content"> class="custom-switch"
<span class="switch-title">{{ $t('pages.imageProcess.transform.isRotate') }}</span> />
</div>
</label>
<PerPicbedSetting <PerPicbedSetting
v-if="!configId" v-if="!configId"
@@ -677,15 +673,14 @@
</div> </div>
<div v-if="activeForm.compress.isRotate" class="form-group"> <div v-if="activeForm.compress.isRotate" class="form-group">
<label>{{ $t('pages.imageProcess.transform.rotationDegree') }}</label> <customRange
<input
v-model.number="activeForm.compress.rotateDegree" v-model.number="activeForm.compress.rotateDegree"
type="range" :title="$t('pages.imageProcess.transform.rotationDegree')"
min="-360" :min="-360"
max="360" :max="360"
class="form-range" :step="1"
:show-value="`${activeForm.compress.rotateDegree}°`"
/> />
<div class="range-value">{{ activeForm.compress.rotateDegree }}°</div>
<PerPicbedSetting <PerPicbedSetting
v-if="!configId" v-if="!configId"
@@ -724,13 +719,11 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="switch-label"> <customSwitch
<input v-model="activeForm.compress.isReSize" type="checkbox" class="switch-input" /> v-model="activeForm.compress.isReSize"
<span class="switch-slider" /> :title="$t('pages.imageProcess.transform.isResize')"
<div class="switch-content"> class="custom-switch"
<span class="switch-title">{{ $t('pages.imageProcess.transform.isResize') }}</span> />
</div>
</label>
<PerPicbedSetting <PerPicbedSetting
v-if="!configId" v-if="!configId"
@@ -746,10 +739,13 @@
/> />
</div> </div>
<div v-if="activeForm.compress.isReSize" class="resize-settings"> <div
v-if="activeForm.compress.isReSize"
class="mt-4 border-t border-t-border pt-3 transition-all duration-200 ease-apple"
>
<div class="form-grid"> <div class="form-grid">
<div class="form-group"> <div class="form-group">
<label>{{ $t('pages.imageProcess.transform.resizeWidth') }}</label> <label class="title-text">{{ $t('pages.imageProcess.transform.resizeWidth') }}</label>
<input v-model.number="activeForm.compress.reSizeWidth" type="number" min="0" class="form-input" /> <input v-model.number="activeForm.compress.reSizeWidth" type="number" min="0" class="form-input" />
<PerPicbedSetting <PerPicbedSetting
@@ -775,7 +771,7 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label>{{ $t('pages.imageProcess.transform.resizeHeight') }}</label> <label class="title-text">{{ $t('pages.imageProcess.transform.resizeHeight') }}</label>
<input v-model.number="activeForm.compress.reSizeHeight" type="number" min="0" class="form-input" /> <input v-model.number="activeForm.compress.reSizeHeight" type="number" min="0" class="form-input" />
<PerPicbedSetting <PerPicbedSetting
@@ -808,15 +804,11 @@
" "
class="form-group" class="form-group"
> >
<label class="switch-label"> <customSwitch
<input v-model="activeForm.compress.skipReSizeOfSmallImg" type="checkbox" class="switch-input" /> v-model="activeForm.compress.skipReSizeOfSmallImg"
<span class="switch-slider" /> :title="$t('pages.imageProcess.transform.skipResizeOfSmallImgHeight')"
<div class="switch-content"> class="custom-switch"
<span class="switch-title">{{ />
$t('pages.imageProcess.transform.skipResizeOfSmallImgHeight')
}}</span>
</div>
</label>
<PerPicbedSetting <PerPicbedSetting
v-if="!configId" v-if="!configId"
@@ -851,14 +843,12 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="switch-label"> <customSwitch
<input v-model="activeForm.compress.isReSizeByPercent" type="checkbox" class="switch-input" /> v-model="activeForm.compress.isReSizeByPercent"
<span class="switch-slider" /> :title="$t('pages.imageProcess.transform.isResizeByPercent')"
<div class="switch-content"> :description="$t('pages.imageProcess.transform.isResizeByPercentHint')"
<span class="switch-title">{{ $t('pages.imageProcess.transform.isResizeByPercent') }}</span> class="custom-switch"
<span class="switch-description">{{ $t('pages.imageProcess.transform.isResizeByPercentHint') }}</span> />
</div>
</label>
<PerPicbedSetting <PerPicbedSetting
v-if="!configId" v-if="!configId"
@@ -881,15 +871,14 @@
</div> </div>
<div v-if="activeForm.compress.isReSizeByPercent" class="form-group"> <div v-if="activeForm.compress.isReSizeByPercent" class="form-group">
<label>{{ $t('pages.imageProcess.transform.resizePercent') }}</label> <customRange
<input
v-model.number="activeForm.compress.reSizePercent" v-model.number="activeForm.compress.reSizePercent"
type="range" :title="$t('pages.imageProcess.transform.resizePercent')"
min="1" :min="1"
max="500" :max="500"
class="form-range" :step="1"
:show-value="`${activeForm.compress.reSizePercent}%`"
/> />
<div class="range-value">{{ activeForm.compress.reSizePercent }}%</div>
<PerPicbedSetting <PerPicbedSetting
v-if="!configId" v-if="!configId"
@@ -918,7 +907,7 @@
</div> </div>
<!-- Skip Process Tab --> <!-- Skip Process Tab -->
<div v-else-if="activeTab === 'skipProcess'" key="skipProcess" class="tab-content"> <div v-else-if="activeTab === 'skipProcess'" key="skipProcess" class="flex flex-col gap-4">
<div class="settings-section"> <div class="settings-section">
<div class="section-header"> <div class="section-header">
<div class="section-icon"> <div class="section-icon">
@@ -935,51 +924,47 @@
rows="3" rows="3"
:placeholder="'zip,rar,7z,tar,gz'" :placeholder="'zip,rar,7z,tar,gz'"
/> />
<small>{{ $t('pages.imageProcess.general.skipProcessExtListPlaceholder') }}</small> <small class="mt-2 block rounded-sm bg-bg-secondary px-3 py-2 text-xs leading-[1.5] text-tertiary">{{
$t('pages.imageProcess.general.skipProcessExtListPlaceholder')
}}</small>
</div> </div>
</div> </div>
</div> </div>
<!-- Rename Tab --> <!-- Rename Tab -->
<div v-else-if="activeTab === 'rename'" key="rename" class="tab-content"> <div v-else-if="activeTab === 'rename'" key="rename" class="flex flex-col gap-4">
<div class="settings-section"> <div class="settings-section">
<div class="form-grid"> <div class="form-grid">
<div class="form-group"> <div class="form-group">
<label class="switch-label"> <customSwitch
<input v-model="autoRenameComputed" type="checkbox" class="switch-input" /> v-model="autoRenameComputed"
<span class="switch-slider" /> :title="$t('pages.imageProcess.rename.renameTimestamp')"
<div class="switch-content"> description="YYYYMMDDHHmmssSSS"
<span class="switch-title">{{ $t('pages.imageProcess.rename.renameTimestamp') }}</span> class="custom-switch"
<span class="switch-description">YYYYMMDDHHmmssSSS</span> />
</div>
</label>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="switch-label"> <customSwitch
<input v-model="manualRenameComputed" type="checkbox" class="switch-input" /> v-model="manualRenameComputed"
<span class="switch-slider" /> :title="$t('pages.imageProcess.rename.manualRename')"
<div class="switch-content"> class="custom-switch"
<span class="switch-title">{{ $t('pages.imageProcess.rename.manualRename') }}</span> />
</div>
</label>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="switch-label"> <customSwitch
<input v-model="renameSettingsComputed.rename.enable" type="checkbox" class="switch-input" /> v-model="renameSettingsComputed.rename.enable"
<span class="switch-slider" /> :title="$t('pages.settings.upload.enableAdvancedRname')"
<div class="switch-content"> :description="$t('pages.settings.upload.enableAdvancedRnameDesc')"
<div class="switch-title">{{ $t('pages.settings.upload.enableAdvancedRname') }}</div> class="custom-switch"
<div class="switch-description">{{ $t('pages.settings.upload.enableAdvancedRnameDesc') }}</div> />
</div>
</label>
</div> </div>
<div class="form-group rename-format-field"> <div class="form-group rename-format-field">
<label> <label class="title-text mb-4 flex items-center gap-2">
<Edit :size="14" /> <Edit :size="14" class="text-accent" />
{{ $t('pages.settings.upload.advancedRnameFormat') }} {{ $t('pages.settings.upload.advancedRnameFormat') }}
</label> </label>
<input <input
@@ -991,59 +976,8 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label>{{ $t('pages.settings.upload.availablePlaceholders') }}</label> <label class="title-text">{{ $t('pages.settings.upload.availablePlaceholders') }}</label>
<div class="placeholder-help"> <placeholderTable :list="advancedRenameList" :title-list="advancedRenameTitleList" />
<div class="placeholder-category">
<div class="category-title">
{{ $t('pages.settings.upload.placeholder.categoryTime') }}
</div>
<div class="placeholder-grid">
<div
v-for="item in advancedRenameList.categoryTime"
:key="item.value"
class="placeholder-item"
@click="copyPlaceholder(item.value)"
>
<code>{{ item.value }}</code>
<span>{{ item.label }}</span>
</div>
</div>
</div>
<div class="placeholder-category">
<div class="category-title">
{{ $t('pages.settings.upload.placeholder.categoryHash') }}
</div>
<div class="placeholder-grid">
<div
v-for="item in advancedRenameList.categoryHash"
:key="item.value"
class="placeholder-item"
@click="copyPlaceholder(item.value)"
>
<code>{{ item.value }}</code>
<span>{{ item.label }}</span>
</div>
</div>
</div>
<div class="placeholder-category">
<div class="category-title">
{{ $t('pages.settings.upload.placeholder.categoryFile') }}
</div>
<div class="placeholder-grid">
<div
v-for="item in advancedRenameList.categoryFile"
:key="item.value"
class="placeholder-item"
@click="copyPlaceholder(item.value)"
>
<code>{{ item.value }}</code>
<span>{{ item.label }}</span>
</div>
</div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -1077,14 +1011,16 @@ import type {
import { computed, nextTick, onBeforeMount, onBeforeUnmount, onMounted, ref, toRaw, useTemplateRef, watch } from 'vue' import { computed, nextTick, onBeforeMount, onBeforeUnmount, onMounted, ref, toRaw, useTemplateRef, watch } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import customRadioOption from '@/components/common/customRadioOption.vue'
import customRange from '@/components/common/customRange.vue'
import customSwitch from '@/components/common/customSwitch.vue'
import placeholderTable from '@/components/common/placeholderTable.vue'
import PerPicbedSetting from '@/components/PerPicbedSetting.vue' import PerPicbedSetting from '@/components/PerPicbedSetting.vue'
import useMessage from '@/hooks/useMessage'
import { getRawData } from '@/utils/common' import { getRawData } from '@/utils/common'
import { configPaths } from '@/utils/configPaths' import { configPaths } from '@/utils/configPaths'
import { getConfig, saveConfig } from '@/utils/dataSender' import { getConfig, saveConfig } from '@/utils/dataSender'
const { t } = useI18n() const { t } = useI18n()
const message = useMessage()
const activeTab = useStorage<string>('image-process-setting-active-tab', 'general') const activeTab = useStorage<string>('image-process-setting-active-tab', 'general')
// Tab indicator animation // Tab indicator animation
@@ -1106,7 +1042,9 @@ function updateTabIndicator() {
const activeTabEl = tabRefs.value[activeIndex] const activeTabEl = tabRefs.value[activeIndex]
if (activeTabEl) { if (activeTabEl) {
tabIndicatorStyle.value = { tabIndicatorStyle.value = {
width: `${activeTabEl.offsetWidth}px`, top: `${activeTabEl.offsetTop}px`,
height: `${activeTabEl.offsetHeight}px`,
width: `${activeTabEl.offsetWidth - 12}px`,
transform: `translateX(${activeTabEl.offsetLeft}px)`, transform: `translateX(${activeTabEl.offsetLeft}px)`,
} }
} }
@@ -1166,10 +1104,11 @@ const advancedRenameList = computed(() => ({
], ],
})) }))
function copyPlaceholder(placeholder: string) { const advancedRenameTitleList = computed(() => ({
window.electron.clipboard.writeText(placeholder) categoryTime: t('pages.settings.upload.placeholder.categoryTime'),
message.success(t('pages.settings.upload.copySuccess', { content: placeholder })) categoryHash: t('pages.settings.upload.placeholder.categoryHash'),
} categoryFile: t('pages.settings.upload.placeholder.categoryFile'),
}))
const waterMarkPositionMap = new Map([ const waterMarkPositionMap = new Map([
['north', t('pages.imageProcess.watermark.positionOptions.top')], ['north', t('pages.imageProcess.watermark.positionOptions.top')],

View File

@@ -0,0 +1,18 @@
<template>
<label
class="flex cursor-pointer items-center gap-2.5 rounded-lg border border-border bg-bg px-4 py-3.5 transition-all duration-200 ease-apple hover:border-accent-hover hover:bg-accent/8"
>
<input v-model="modelValue" type="radio" :value="value" class="peer hidden" />
<span
class="relative h-[18px] w-[18px] rounded-full border border-border bg-bg transition-all duration-200 ease-apple peer-checked:border-accent peer-checked:after:absolute peer-checked:after:top-1/2 peer-checked:after:left-1/2 peer-checked:after:h-[10px] peer-checked:after:w-[10px] peer-checked:after:-translate-x-1/2 peer-checked:after:-translate-y-1/2 peer-checked:after:transform peer-checked:after:rounded-full peer-checked:after:bg-accent peer-checked:after:content-['']"
/>
<span class="text-sm font-medium text-main">{{ title }}</span>
</label>
</template>
<script lang="ts" setup>
const modelValue = defineModel<string>()
const { value = '', title = '' } = defineProps<{
value?: string
title?: string
}>()
</script>

View File

@@ -0,0 +1,34 @@
<template>
<label class="text-sm font-semibold text-main">{{ title }}</label>
<input
v-model.number="modelValue"
type="range"
:min="min"
:max="max"
:step="step"
class="my-3 h-[8px] w-full appearance-none rounded-sm bg-linear-to-l from-accent transition-colors duration-150 ease-apple outline-none [&::--moz-range-thumb]:shadow-sm [&::-moz-range-thumb]:h-[22px] [&::-moz-range-thumb]:w-[22px] [&::-moz-range-thumb]:cursor-pointer [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:border [&::-moz-range-thumb]:border-border [&::-moz-range-thumb]:bg-white [&::-webkit-slider-thumb]:h-[22px] [&::-webkit-slider-thumb]:w-[22px] [&::-webkit-slider-thumb]:cursor-pointer [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:border [&::-webkit-slider-thumb]:border-border [&::-webkit-slider-thumb]:bg-white [&::-webkit-slider-thumb]:shadow-sm [&::-webkit-slider-thumb]:transition-all [&::-webkit-slider-thumb]:duration-200 [&::-webkit-slider-thumb]:ease-apple [&::-webkit-slider-thumb:hover]:scale-105 [&::-webkit-slider-thumb:hover]:shadow-md"
/>
<div
class="mt-2 inline-flex min-w-14 items-center justify-center rounded-md bg-accent px-2 py-1.5 text-sm font-semibold text-white shadow-sm"
>
{{ showValue }}
</div>
</template>
<script lang="ts" setup>
const modelValue = defineModel<number>()
const {
title = '',
step = 1,
min = 1,
max = 100,
showValue = '',
} = defineProps<{
title?: string
min?: number
max?: number
step?: number
showValue?: string
}>()
</script>

View File

@@ -0,0 +1,22 @@
<template>
<label
class="flex cursor-pointer items-center gap-4 rounded-lg border border-border bg-bg p-4 transition-all duration-200 ease-apple hover:border-accent hover:bg-surface hover:shadow-sm"
>
<input v-model="modelValue" type="checkbox" class="peer hidden" />
<span
class="bg-linear-180-r relative h-[28px] w-[52px] shrink-0 rounded-full bg-gray-400/80 shadow-sm transition-all duration-medium ease-standard peer-checked:bg-accent before:absolute before:top-[3px] before:left-[3px] before:h-[22px] before:w-[22px] before:rounded-full before:bg-white before:shadow-sm before:transition-all before:duration-200 before:ease-apple before:content-[''] peer-checked:before:translate-x-[24px]"
/>
<div class="flex flex-1 flex-col gap-1">
<span class="text-[0.925rem] leading-[1.4] font-semibold text-secondary">{{ props.title }}</span>
<span class="text-xs text-secondary/90">{{ props.description }}</span>
</div>
</label>
</template>
<script lang="ts" setup>
const modelValue = defineModel<boolean>()
const props = defineProps<{
title?: string
description?: string
}>()
</script>

View File

@@ -0,0 +1,43 @@
<template>
<div class="mt-3 max-h-[400px] overflow-y-auto rounded-lg border border-border bg-bg-tertiary p-0 shadow-sm">
<template v-for="key in Object.keys(list)" :key="key">
<div class="border-b border-border last:border-0">
<div
class="bg-linear-150-r m-0 border-b border-border bg-accent/10 px-4 pt-3.5 pb-2 text-sm font-semibold tracking-wide text-secondary"
>
{{ $t(titleList[key]) }}
</div>
<div class="grid grid-cols-[repeat(auto-fill,minmax(240px,1fr))] gap-0 py-2">
<div
v-for="item in list[key]"
:key="item.value"
class="m-0 flex cursor-pointer items-center rounded-none px-4 py-2 text-sm leading-[1.4] hover:bg-accent/5"
@click="copyPlaceholder(item.value)"
>
<code
class="mr-3.5 min-w-[80px] shrink-0 rounded-md border border-white/20 bg-bg-secondary px-2 py-1 text-center font-['SF_Mono',Monaco,Menlo,'Ubuntu_Mono',monospace] text-base font-semibold text-main shadow-sm"
>{{ item.value }}</code
>
<span class="flex-1 font-medium text-main">{{ item.label }}</span>
</div>
</div>
</div>
</template>
</div>
</template>
<script lang="ts" setup>
import { useI18n } from 'vue-i18n'
import useMessage from '@/hooks/useMessage'
const { t } = useI18n()
const message = useMessage()
function copyPlaceholder(placeholder: string) {
window.electron.clipboard.writeText(placeholder)
message.success(t('pages.settings.upload.copySuccess', { content: placeholder }))
}
const { list, titleList } = defineProps<{
list: Record<string, { label: string; value: string }[]>
titleList: Record<string, string>
}>()
</script>

View File

@@ -1,715 +1,51 @@
/* ==================== Base & Layout ==================== */ @import 'tailwindcss' reference;
.image-process-settings { @import '../../assets/css/theme.css' reference;
overflow-y: auto; @import '../../assets/css/utilities.css' reference;
padding: 1rem;
height: 100%;
min-height: 100vh;
color: var(--color-text-primary);
}
/* ==================== Tab Navigation ==================== */
.tab-navigation {
position: relative;
display: flex;
margin-bottom: 2rem;
border: 1px solid var(--color-border);
border-radius: var(--radius-xl);
padding: 6px;
box-shadow:var(--shadow-sm);
}
.tab-indicator {
position: absolute;
top: 6px;
left: 6px;
z-index: 0;
border-radius: var(--radius-lg);
height: calc(100% - 12px);
background: var(--color-accent);
box-shadow: var(--shadow-md);
transition: all var(--transition-medium);
}
.tab-button {
position: relative;
z-index: 1;
display: flex;
justify-content: center;
align-items: center;
border: none;
border-radius: var(--radius-lg);
padding: 0.875rem 1.25rem;
font-size: 0.9rem;
font-weight: 600;
color: var(--color-text-secondary);
background: transparent;
transition: all 0.25s ease;
gap: 0.625rem;
cursor: pointer;
flex: 1;
}
.tab-button:hover:not(.active) {
color: var(--color-text-primary);
background: var(--color-background-secondary);
}
.tab-button.active {
color: white;
background: transparent;
}
/* ==================== Settings Content ==================== */
.settings-content {
display: flex;
flex-direction: column;
}
.tab-content {
display: flex;
flex-direction: column;
gap: 1.75rem;
}
/* Fade slide transition for tab content */
.fade-slide-enter-active,
.fade-slide-leave-active {
transition: all 0.3s ease;
}
.fade-slide-enter-from {
opacity: 0;
transform: translateY(12px);
}
.fade-slide-leave-to {
opacity: 0;
transform: translateY(-12px);
}
/* ==================== Settings Section ==================== */ /* ==================== Settings Section ==================== */
.settings-section { .settings-section {
border: 1px solid var(--color-border); @apply rounded-xl border border-border bg-bg p-5 shadow-sm transition-all duration-200 ease-apple hover:shadow-md;
border-radius: var(--radius-xl);
padding: 1.75rem;
background: var(--color-background-primary);
box-shadow: var(--shadow-sm);
transition: all 0.25s ease;
} }
.settings-section:hover {
box-shadow: var(--shadow-md);
}
/* Section Header with Icon */
.section-header { .section-header {
display: flex; @apply mb-6 flex items-center gap-1;
align-items: flex-start;
margin-bottom: 1.5rem;
gap: 1rem;
} }
.section-icon { .section-icon {
display: flex; @apply flex h-[30px] w-[30px] shrink-0 items-center justify-center rounded-lg bg-bg-secondary text-accent;
justify-content: center;
align-items: center;
border-radius: var(--radius-lg);
width: 30px;
height: 30px;
color: var(--color-accent);
background: var(--color-background-secondary);
flex-shrink: 0;
} }
.section-title-group h2 { .section-title-group {
margin: 0 0 0.25rem; @apply [&_h2]:mb-0 [&_h2]:text-lg [&_h2]:leading-0 [&_h2]:font-semibold [&_h2]:tracking-tight [&_h2]:text-main;
font-size: 1.125rem; @apply [&_p]:mt-6 [&_p]:text-sm [&_p]:leading-0 [&_p]:text-secondary;
font-weight: 600;
letter-spacing: -0.01em;
color: var(--color-text-primary);
}
.section-title-group p {
margin: 0;
font-size: 0.85rem;
color: var(--color-text-secondary);
line-height: 1.5;
}
.settings-section h2 {
margin: 0 0 0.5rem;
font-size: 1.125rem;
font-weight: 600;
color: var(--color-text-primary);
}
.settings-section p {
margin: 0 0 1.5rem;
font-size: 0.875rem;
color: var(--color-text-secondary);
} }
/* ==================== Form Elements ==================== */ /* ==================== Form Elements ==================== */
.form-grid {
@apply grid grid-cols-[repeat(auto-fit,minmax(280px,1fr))] gap-4;
}
.form-group { .form-group {
margin-bottom: 1.5rem; @apply mb-6;
} @apply last:mb-0;
@apply [&>:label:not(.custom-switch,.radio-option)]:mb-2.5
.form-group:last-child { [&>:label:not(.custom-switch,.radio-option)]:block
margin-bottom: 0; [&>:label:not(.custom-switch,.radio-option)]:text-sm
} [&>:label:not(.custom-switch,.radio-option)]:font-semibold
[&>:label:not(.custom-switch,.radio-option)]:text-main;
.form-group > label:not(.switch-label, .radio-option) {
display: block;
margin-bottom: 0.625rem;
font-size: 0.875rem;
font-weight: 600;
color: var(--color-text-primary);
} }
.form-input, .form-input,
.form-textarea { .form-textarea {
border: 1.5px solid var(--color-border); @apply box-border w-full rounded-lg border border-border bg-bg-tertiary px-4 py-3.5 text-sm text-main transition-all duration-200 ease-apple;
border-radius: var(--radius-lg); @apply hover:border-border-secondary;
padding: 0.875rem 1rem; @apply focus:border-accent focus:bg-surface-elevated focus:shadow-sm focus:outline-none;
width: 100%;
font-size: 0.9rem;
color: var(--color-text-primary);
background: var(--color-background-tertiary);
transition: all 0.2s ease;
box-sizing: border-box;
}
.form-input:hover,
.form-textarea:hover {
border-color: var(--color-border-secondary);
}
.form-input:focus,
.form-textarea:focus {
border-color: var(--color-accent);
outline: none;
box-shadow: var(--shadow-sm);
} }
.form-textarea { .form-textarea {
min-height: 90px; @apply min-h-[90px] resize-y font-[ui-monospace,SFMono-Regular,'SF_Mono',Menlo,Consolas,'Liberation_Mono',monospace];
font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace;
resize: vertical;
} }
/* ==================== Range Slider ==================== */ .title-text {
.form-range { @apply text-base font-semibold text-main;
margin: 0.75rem 0;
border-radius: 4px;
width: 100%;
height: 8px;
background: linear-gradient(90deg, var(--color-accent) 0%, #e4e7ed 0%);
outline: none;
appearance: none;
transition: background 0.15s ease;
} }
.form-range::-webkit-slider-thumb {
border: 2px solid var(--color-border);
border-radius: var(--radius-round);
width: 22px;
height: 22px;
background: var(--color-accent);
box-shadow: var(--shadow-sm);
appearance: none;
cursor: pointer;
transition: all 0.2s ease;
}
.form-range::-webkit-slider-thumb:hover {
transform: scale(1.1);
box-shadow: var(--shadow-md);
}
.form-range::-moz-range-thumb {
border: 2px solid var(--color-border);
border-radius: var(--radius-round);
width: 22px;
height: 22px;
background: var(--color-accent);
box-shadow: var(--shadow-sm);
cursor: pointer;
}
.range-value {
display: inline-flex;
justify-content: center;
align-items: center;
margin-top: 0.5rem;
border-radius: var(--radius-md);
padding: 0.375rem 0.75rem;
min-width: 3.5rem;
font-size: 0.8rem;
font-weight: 600;
text-align: center;
color: white;
background: var(--color-accent);
box-shadow: var(--shadow-sm);
}
/* ==================== Color Input ==================== */
.form-color {
overflow: hidden;
border: 2px solid var(--color-border);
border-radius: var(--radius-lg);
padding: 2px;
width: 48px;
height: 48px;
background: var(--color-background-primary);
transition: all 0.2s ease;
cursor: pointer;
}
.form-color:hover {
border-color: var(--color-accent);
box-shadow: var(--shadow-sm);
}
.form-color:focus {
border-color: var(--color-accent);
outline: none;
box-shadow: 0 0 0 3px rgb(64 158 255 / 20%);
}
.color-input-group {
display: flex;
gap: 1rem;
align-items: center;
}
.color-input-group .form-input {
flex: 1;
font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace;
}
/* ==================== Grid Layout ==================== */
.form-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 1.75rem;
}
/* ==================== Switch Component ==================== */
.switch-label {
display: flex;
align-items: center;
border: 1.5px solid var(--color-border);
border-radius: var(--radius-lg);
padding: 1.125rem 1.25rem;
background: var(--color-background-primary);
transition: all 0.25s ease;
gap: 1rem;
cursor: pointer;
}
.switch-label:hover {
border-color: var(--color-accent);
background: var(--color-surface);
box-shadow: 0 2px 8px rgb(64 158 255 / 10%);
}
.switch-input {
display: none;
}
.switch-slider {
position: relative;
border-radius: var(--radius-xl);
width: 52px;
height: 28px;
background: linear-gradient(180deg, #d0d3d9 0%, #c0c4cc 100%);
box-shadow: inset 0 1px 3px rgb(0 0 0 / 15%);
transition: all var(--transition-medium);
flex-shrink: 0;
}
.switch-slider::before {
position: absolute;
top: 3px;
left: 3px;
border-radius: var(--radius-round);
width: 22px;
height: 22px;
background: linear-gradient(180deg, #ffffff 0%, #f5f5f5 100%);
box-shadow:
0 2px 6px rgb(0 0 0 / 20%),
0 1px 2px rgb(0 0 0 / 10%);
transition: all var(--transition-medium);
content: '';
}
.switch-input:checked + .switch-slider {
background: var(--color-accent);
box-shadow:
inset 0 1px 3px rgb(0 0 0 / 10%),
0 2px 8px rgb(64 158 255 / 30%);
}
.switch-input:checked + .switch-slider::before {
transform: translateX(24px);
}
.switch-content {
display: flex;
flex-direction: column;
gap: 0.25rem;
flex: 1;
}
.switch-title {
font-size: 0.925rem;
font-weight: 600;
color: var(--color-text-primary);
line-height: 1.4;
}
.switch-description {
font-size: 0.75rem;
color: var(--color-text-secondary);
}
/* ==================== Radio Group ==================== */
.radio-group {
display: flex;
gap: 1rem;
flex-wrap: wrap;
}
.radio-option {
display: flex;
align-items: center;
border: 1.5px solid var(--color-border);
border-radius: var(--radius-lg);
padding: 0.75rem 1rem;
background: var(--color-background-primary);
transition: all 0.25s ease;
gap: 0.625rem;
cursor: pointer;
}
.radio-option:hover {
border-color: var(--color-accent);
background: rgb(64 158 255 / 8%);
}
.radio-input {
display: none;
}
.radio-indicator {
position: relative;
border: 2px solid var(--color-border);
border-radius: var(--radius-round);
width: 18px;
height: 18px;
background: var(--color-background-primary);
transition: all 0.25s ease;
}
.radio-input:checked + .radio-indicator {
border-color: var(--color-accent);
background: var(--color-background-primary);
}
.radio-input:checked + .radio-indicator::after {
position: absolute;
top: 50%;
left: 50%;
border-radius: var(--radius-round);
width: 10px;
height: 10px;
background: var(--color-accent);
content: '';
transform: translate(-50%, -50%);
}
.radio-label {
font-size: 0.9rem;
font-weight: 500;
color: var(--color-text-primary);
}
/* ==================== Position Grid ==================== */
.position-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.625rem;
max-width: 320px;
}
.position-button {
border: 1.5px solid var(--color-border);
border-radius: var(--radius-lg);
padding: 0.875rem;
font-size: 0.8rem;
font-weight: 600;
text-align: center;
color: var(--color-text-secondary);
background: var(--color-background-primary);
transition: all 0.25s ease;
cursor: pointer;
}
.position-button:hover {
border-color: var(--color-accent);
color: var(--color-text-primary);
background: rgb(64 158 255 / 8%);
transform: translateY(-1px);
}
.position-button.active {
border-color: var(--color-accent);
color: white;
background: var(--color-background-secondary);
box-shadow: 0 4px 12px rgb(64 158 255 / 35%);
transform: translateY(-1px);
}
/* ==================== Buttons ==================== */
.btn {
display: inline-flex;
align-items: center;
border: 1.5px solid var(--color-border);
border-radius: var(--radius-lg);
padding: 0.75rem 1.25rem;
font-size: 0.875rem;
font-weight: 600;
text-decoration: none;
color: var(--color-text-primary);
background: var(--color-background-primary);
transition: all 0.25s ease;
gap: 0.5rem;
cursor: pointer;
}
.btn:hover {
border-color: var(--color-accent);
background: var(--color-background-secondary);
transform: translateY(-1px);
}
/* ==================== Small Text / Hints ==================== */
small {
display: block;
margin-top: 0.5rem;
border-radius: var(--radius-sm);
padding: 0.5rem 0.75rem;
font-size: 0.8rem;
color: var(--color-text-tertiary);
background: var(--color-background-secondary);
line-height: 1.5;
}
/* ==================== Watermark and Resize settings groups ==================== */
.watermark-settings,
.resize-settings {
margin-top: 1.5rem;
border-top: 1px solid var(--color-border-secondary);
padding-top: 1.5rem;
animation: slide-down 0.3s ease;
}
@keyframes slide-down {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* ==================== Responsive Design ==================== */
@media (width <= 768px) {
.image-process-settings {
padding: 1rem;
}
.settings-header {
flex-direction: column;
gap: 1rem;
align-items: stretch;
padding: 1.25rem;
}
.header-icon-wrapper {
width: 48px;
height: 48px;
}
.header-text h1 {
font-size: 1.375rem;
}
.header-actions {
justify-content: stretch;
}
.header-actions .btn {
flex: 1;
justify-content: center;
}
.tab-navigation {
flex-direction: column;
}
.tab-indicator {
display: none;
}
.tab-button.active {
background: var(--color-accent);
}
.form-grid {
grid-template-columns: 1fr;
}
.radio-group {
flex-direction: column;
}
.color-input-group {
flex-direction: column;
align-items: stretch;
}
.position-grid {
max-width: 100%;
}
.section-header {
flex-direction: column;
gap: 0.75rem;
}
}
/* Placeholder Help Styles */
.placeholder-help {
overflow-y: auto;
margin-top: 0.75rem;
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
padding: 0;
max-height: 400px;
background: var(--color-background-tertiary);
box-shadow: 0 2px 8px rgb(0 0 0 / 6%);
}
.placeholder-category {
border-bottom: 1px solid var(--color-border);
}
.placeholder-category:last-child {
border-bottom: none;
}
.category-title {
margin: 0;
border-bottom: 1px solid var(--color-border);
padding: 0.875rem 1rem 0.5rem;
font-size: 0.875rem;
font-weight: 600;
color: var(--color-text-secondary);
background: linear-gradient(135deg, var(--color-background-secondary) 0%, var(--color-background-tertiary) 100%);
letter-spacing: 0.02em;
}
.placeholder-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 0;
padding: 0.5rem 0;
}
.placeholder-item {
display: flex;
align-items: center;
margin: 0;
border-radius: 0;
padding: 0.5rem 1rem;
font-size: 0.875rem;
line-height: 1.4;
cursor: pointer;
}
.placeholder-item:hover {
background: color-mix(in srgb, var(--color-accent), transparent 95%);
}
.placeholder-item code {
margin-right: 0.875rem;
border: 1px solid rgb(255 255 255 / 20%);
border-radius: var(--radius-md);
padding: 0.3rem 0.6rem;
min-width: 80px;
font-size: 1rem;
font-family: 'SF Mono', Monaco, Menlo, 'Ubuntu Mono', monospace;
font-weight: 600;
text-align: center;
color: var(--color-text-primary);
background: var(--color-background-secondary);
box-shadow: 0 1px 3px rgb(0 0 0 / 12%), 0 1px 2px rgb(0 0 0 / 24%);
letter-spacing: 0.02em;
flex-shrink: 0;
}
.placeholder-item span {
font-weight: 500;
color: var(--color-text-primary);
flex: 1;
}
/* Scrollbar styling for macOS feel */
.placeholder-help::-webkit-scrollbar {
width: 6px;
}
.placeholder-help::-webkit-scrollbar-track {
border-radius: var(--radius-lg);
background: transparent;
}
.placeholder-help::-webkit-scrollbar-thumb {
border-radius: var(--radius-lg);
background: var(--color-border);
transition: background 0.2s ease;
}
.placeholder-help::-webkit-scrollbar-thumb:hover {
background: var(--color-text-secondary);
}
/* Rename specific styles */
.rename-toggle-card {
margin-bottom: 1rem;
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
padding: 0.25rem;
background: var(--color-background-tertiary);
}
.rename-toggle-card .switch-label {
margin: 0;
border: none;
background: transparent;
}
.rename-format-field label {
display: flex;
align-items: center;
gap: 0.375rem;
}
.rename-format-field label svg {
color: var(--color-accent);
}

View File

@@ -247,12 +247,12 @@
<transition name="modal"> <transition name="modal">
<div <div
v-if="imageProcessDialogVisible" v-if="imageProcessDialogVisible"
class="fixed inset-0 z-1000 flex items-center justify-center overflow-y-auto bg-black/50 p-4 max-md:p-4" class="fixed inset-0 z-1000 flex items-center justify-center overflow-y-auto bg-black/30"
:class="{ 'advanced-animation': enableAdvancedAnimation }" :class="{ 'advanced-animation': enableAdvancedAnimation }"
@click.stop @click.stop
> >
<div <div
class="m-auto h-[85vh] max-h-[85vh] w-[90vw] max-w-[90vw] overflow-hidden rounded-2xl border border-border-secondary bg-bg-tertiary shadow-xl" class="m-auto flex h-[85vh] w-[90vw] flex-col overflow-hidden rounded-2xl border border-border-secondary bg-bg-tertiary shadow-xl"
@click.stop @click.stop
> >
<div <div
@@ -273,7 +273,7 @@
<XIcon :size="20" /> <XIcon :size="20" />
</button> </button>
</div> </div>
<div class="no-scrollbar max-h-[calc(90vh-90px)] overflow-y-auto max-md:p-4"> <div class="no-scrollbar h-[calc(90vh-90px)] flex-1 overflow-y-auto max-md:p-4">
<ImageProcessSetting :config-id="PicBedId" :current-picbed-name="defaultPicBedG" /> <ImageProcessSetting :config-id="PicBedId" :current-picbed-name="defaultPicBedG" />
</div> </div>
</div> </div>

View File

@@ -104,10 +104,7 @@
} }
.placeholder-grid { .placeholder-grid {
display: grid; @apply grid grid-cols-[repeat(auto-fit,minmax(150px,1fr))] gap-0 py-2;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 0;
padding: 0.5rem 0;
} }
.placeholder-item { .placeholder-item {
@@ -139,6 +136,3 @@
@apply m-0 border-b border-border pt-3.5 px-4 pb-2 text-sm font-semibold text-secondary bg-[linear-gradient(180deg,var(--color-background-secondary)_0%,var(--color-background-tertiary)_100%)]; @apply m-0 border-b border-border pt-3.5 px-4 pb-2 text-sm font-semibold text-secondary bg-[linear-gradient(180deg,var(--color-background-secondary)_0%,var(--color-background-tertiary)_100%)];
} }
.placeholder-item:hover {
background: color-mix(in srgb, var(--color-accent), transparent 95%);
}