feat: 优化添加背景页面颜色选择体验

- 标题栏中间显示11个色阶(当前色居中,左浅右深)
- 右侧栏顶部添加颜色选择器和输入框
- 右侧色调面板改为12个常用背景色(白色起始,由浅到深)
- 优化交互:点击即时应用,活动状态清晰标识
This commit is contained in:
jingrow 2026-01-21 15:47:04 +08:00
parent 0f30876371
commit ae7ed98808

View File

@ -21,24 +21,17 @@
<div class="page-header"> <div class="page-header">
<h2>{{ t('Add Background') }}</h2> <h2>{{ t('Add Background') }}</h2>
<!-- Background Color Selection in Header Center --> <!-- Color Shades in Header Center -->
<div v-if="uploadedImage" class="header-color-controls"> <div v-if="uploadedImage" class="header-color-shades">
<label class="header-color-label">{{ t('Background Color') }}</label> <div
<div class="header-color-picker-container"> v-for="(shade, index) in colorShades"
<input :key="index"
v-model="backgroundColor" class="shade-item"
type="color" :class="{ 'active': shade === backgroundColor }"
class="header-color-picker" :style="{ backgroundColor: shade }"
@input="onColorChange" @click="selectShade(shade)"
/> :title="shade"
<input >
v-model="backgroundColor"
type="text"
class="header-hex-input"
placeholder="#FFFFFF"
@input="onHexInputChange"
@blur="onHexInputBlur"
/>
</div> </div>
</div> </div>
<div v-if="uploadedImage" class="toolbar-actions"> <div v-if="uploadedImage" class="toolbar-actions">
@ -164,19 +157,39 @@
</div> </div>
</div> </div>
<!-- Right Sidebar for Color Tones --> <!-- Right Sidebar for Common Colors and Color Picker -->
<div v-if="uploadedImage" class="right-sidebar"> <div v-if="uploadedImage" class="right-sidebar">
<div class="sidebar-content"> <div class="sidebar-content">
<div class="palette-section"> <div class="color-picker-section">
<h4 class="palette-title">{{ t('Color Tones') }}</h4> <h4 class="section-title">{{ t('Background Color') }}</h4>
<div class="palette-grid"> <div class="color-picker-container">
<input
v-model="backgroundColor"
type="color"
class="sidebar-color-picker"
@input="onColorChange"
/>
<input
v-model="backgroundColor"
type="text"
class="sidebar-hex-input"
placeholder="#FFFFFF"
@input="onHexInputChange"
@blur="onHexInputBlur"
/>
</div>
</div>
<div class="common-colors-section">
<h4 class="section-title">{{ t('Common Colors') }}</h4>
<div class="common-colors-grid">
<div <div
v-for="(color, index) in colorPalette" v-for="(color, index) in commonColors"
:key="index" :key="index"
class="palette-color" class="common-color-item"
:class="{ 'active': color === backgroundColor }" :class="{ 'active': color === backgroundColor }"
:style="{ backgroundColor: color }" :style="{ backgroundColor: color }"
@click="selectPaletteColor(color)" @click="selectCommonColor(color)"
:title="color" :title="color"
> >
</div> </div>
@ -233,11 +246,27 @@ const isColorValid = ref<boolean>(true)
let colorChangeDebounceTimer: ReturnType<typeof setTimeout> | null = null let colorChangeDebounceTimer: ReturnType<typeof setTimeout> | null = null
const colorThief = new ColorThief() const colorThief = new ColorThief()
// Generate color palette based on HSL color space // Generate 11 color shades based on current background color (current color in middle)
const colorPalette = computed(() => { const colorShades = computed(() => {
return generateColorPalette(backgroundColor.value) return generateColorShades(backgroundColor.value)
}) })
// 12 common background colors - light to dark
const commonColors = ref([
'#FFFFFF', // White
'#F5F5F5', // Very Light Gray
'#E8E8E8', // Light Gray
'#D4D4D4', // Gray
'#FFE4E1', // Light Pink
'#FFD7BE', // Light Peach
'#FFF4CC', // Light Yellow
'#D4E4BC', // Light Green
'#C8E6F5', // Light Blue
'#E6D7FF', // Light Purple
'#BEBEBE', // Medium Gray
'#A0A0A0' // Dark Gray
])
// HSL color utilities - efficient and modern approach // HSL color utilities - efficient and modern approach
const hexToHsl = (hex: string): { h: number; s: number; l: number } => { const hexToHsl = (hex: string): { h: number; s: number; l: number } => {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex) const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
@ -277,24 +306,27 @@ const hslToHex = (h: number, s: number, l: number): string => {
return `#${f(0)}${f(8)}${f(4)}`.toUpperCase() return `#${f(0)}${f(8)}${f(4)}`.toUpperCase()
} }
const generateColorPalette = (baseColor: string): string[] => { // Generate 11 color shades with current color in the middle (6th position)
const { h, s } = hexToHsl(baseColor) const generateColorShades = (baseColor: string): string[] => {
const palette: string[] = [] const { h, s, l } = hexToHsl(baseColor)
const shades: string[] = []
// Generate 12 colors with same hue, varying saturation and lightness // Generate 5 lighter shades before current color
const lightnessLevels = [95, 85, 75, 65, 55, 45, 35, 25, 15, 10, 5] for (let i = 5; i > 0; i--) {
const saturationVariants = [s, Math.min(100, s + 15), Math.max(0, s - 15)] const lighterL = Math.min(100, l + (i * 8))
shades.push(hslToHex(h, s, lighterL))
// Main tones (same saturation, different lightness)
for (const l of lightnessLevels) {
palette.push(hslToHex(h, s, l))
} }
// Add some saturation variants // Add current color in the middle (6th position)
palette.push(hslToHex(h, Math.min(100, s + 20), 50)) shades.push(baseColor)
palette.push(hslToHex(h, Math.max(20, s - 20), 50))
return palette // Generate 5 darker shades after current color
for (let i = 1; i <= 5; i++) {
const darkerL = Math.max(0, l - (i * 8))
shades.push(hslToHex(h, s, darkerL))
}
return shades
} }
const resultImageUrl = computed(() => { const resultImageUrl = computed(() => {
if (!resultImage.value) return '' if (!resultImage.value) return ''
@ -857,7 +889,12 @@ const onHexInputBlur = () => {
} }
} }
const selectPaletteColor = (color: string) => { const selectShade = (color: string) => {
backgroundColor.value = color
applyBackgroundSilent()
}
const selectCommonColor = (color: string) => {
backgroundColor.value = color backgroundColor.value = color
applyBackgroundSilent() applyBackgroundSilent()
} }
@ -1280,7 +1317,7 @@ const removeHistoryItem = (index: number) => {
width: 100%; width: 100%;
padding: 12px 36px; padding: 12px 36px;
gap: 16px; gap: 16px;
padding-right: 316px; /* Make space for right sidebar + padding */ padding-right: 316px;
} }
.page-header h2 { .page-header h2 {
@ -1291,66 +1328,47 @@ const removeHistoryItem = (index: number) => {
flex-shrink: 0; flex-shrink: 0;
} }
/* Header Color Controls */ /* Header Color Shades */
.header-color-controls { .header-color-shades {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 12px; gap: 8px;
flex: 1; flex: 1;
justify-content: center; justify-content: center;
} }
.header-color-label { .shade-item {
font-size: 14px; width: 40px;
font-weight: 600; height: 40px;
color: #64748b;
white-space: nowrap;
}
.header-color-picker-container {
display: flex;
align-items: center;
gap: 12px;
}
.header-color-picker {
width: 48px;
height: 48px;
border: 2px solid #e5e7eb;
border-radius: 8px; border-radius: 8px;
cursor: pointer; cursor: pointer;
background: none; border: 2px solid transparent;
transition: all 0.15s ease;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
&::-webkit-color-swatch-wrapper { &:hover {
padding: 0; transform: scale(1.1);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
z-index: 1;
} }
&::-webkit-color-swatch { &.active {
border: none;
border-radius: 6px;
}
}
.header-hex-input {
width: 120px;
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 10px 12px;
font-size: 14px;
font-family: monospace;
color: #1f2937;
background: white;
outline: none;
transition: all 0.2s ease;
&:focus {
border-color: #1fc76f; border-color: #1fc76f;
box-shadow: 0 0 0 3px rgba(31, 199, 111, 0.1); box-shadow: 0 0 0 3px rgba(31, 199, 111, 0.3);
} transform: scale(1.15);
&.invalid { &::after {
border-color: #ef4444; content: '';
box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.1); position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 8px;
height: 8px;
background: white;
border-radius: 50%;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
} }
} }
@ -1649,11 +1667,11 @@ const removeHistoryItem = (index: number) => {
image-rendering: crisp-edges; image-rendering: crisp-edges;
} }
/* Right Sidebar - Naive UI Style */ /* Right Sidebar */
.right-sidebar { .right-sidebar {
position: fixed; position: fixed;
right: 0; right: 0;
top: 64px; /* Below app header */ top: 64px;
bottom: 0; bottom: 0;
width: 280px; width: 280px;
background: white; background: white;
@ -1669,15 +1687,19 @@ const removeHistoryItem = (index: number) => {
flex: 1; flex: 1;
overflow-y: auto; overflow-y: auto;
padding: 20px; padding: 20px;
display: flex;
flex-direction: column;
gap: 24px;
} }
.palette-section { .color-picker-section,
.common-colors-section {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 16px; gap: 16px;
} }
.palette-title { .section-title {
font-size: 14px; font-size: 14px;
font-weight: 600; font-weight: 600;
color: #1f2937; color: #1f2937;
@ -1686,13 +1708,61 @@ const removeHistoryItem = (index: number) => {
border-bottom: 1px solid #e5e7eb; border-bottom: 1px solid #e5e7eb;
} }
.palette-grid { .color-picker-container {
display: flex;
flex-direction: column;
gap: 12px;
}
.sidebar-color-picker {
width: 100%;
height: 60px;
border: 2px solid #e5e7eb;
border-radius: 8px;
cursor: pointer;
background: none;
&::-webkit-color-swatch-wrapper {
padding: 0;
}
&::-webkit-color-swatch {
border: none;
border-radius: 6px;
}
}
.sidebar-hex-input {
width: 100%;
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 12px;
font-size: 14px;
font-family: monospace;
color: #1f2937;
background: white;
outline: none;
transition: all 0.2s ease;
text-align: center;
&:focus {
border-color: #1fc76f;
box-shadow: 0 0 0 3px rgba(31, 199, 111, 0.1);
}
&.invalid {
border-color: #ef4444;
box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.1);
}
}
.common-colors-grid {
display: grid; display: grid;
grid-template-columns: repeat(4, 1fr); grid-template-columns: repeat(3, 1fr);
gap: 10px; gap: 10px;
} }
.palette-color { .common-color-item {
aspect-ratio: 1; aspect-ratio: 1;
border-radius: 8px; border-radius: 8px;
cursor: pointer; cursor: pointer;
@ -1939,10 +2009,16 @@ const removeHistoryItem = (index: number) => {
width: 100%; width: 100%;
} }
.header-color-controls { .header-color-shades {
width: 100%; width: 100%;
justify-content: flex-start; justify-content: flex-start;
order: 2; order: 2;
gap: 6px;
}
.shade-item {
width: 32px;
height: 32px;
} }
.toolbar-actions { .toolbar-actions {
@ -2016,8 +2092,8 @@ const removeHistoryItem = (index: number) => {
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.05); box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.05);
} }
.palette-grid { .common-colors-grid {
grid-template-columns: repeat(6, 1fr); grid-template-columns: repeat(4, 1fr);
} }
.history-bar { .history-bar {
@ -2061,7 +2137,7 @@ const removeHistoryItem = (index: number) => {
width: 240px; width: 240px;
} }
.palette-grid { .common-colors-grid {
grid-template-columns: repeat(3, 1fr); grid-template-columns: repeat(3, 1fr);
} }
} }