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">
<h2>{{ t('Add Background') }}</h2>
<!-- Background Color Selection in Header Center -->
<div v-if="uploadedImage" class="header-color-controls">
<label class="header-color-label">{{ t('Background Color') }}</label>
<div class="header-color-picker-container">
<input
v-model="backgroundColor"
type="color"
class="header-color-picker"
@input="onColorChange"
/>
<input
v-model="backgroundColor"
type="text"
class="header-hex-input"
placeholder="#FFFFFF"
@input="onHexInputChange"
@blur="onHexInputBlur"
/>
<!-- Color Shades in Header Center -->
<div v-if="uploadedImage" class="header-color-shades">
<div
v-for="(shade, index) in colorShades"
:key="index"
class="shade-item"
:class="{ 'active': shade === backgroundColor }"
:style="{ backgroundColor: shade }"
@click="selectShade(shade)"
:title="shade"
>
</div>
</div>
<div v-if="uploadedImage" class="toolbar-actions">
@ -164,19 +157,39 @@
</div>
</div>
<!-- Right Sidebar for Color Tones -->
<!-- Right Sidebar for Common Colors and Color Picker -->
<div v-if="uploadedImage" class="right-sidebar">
<div class="sidebar-content">
<div class="palette-section">
<h4 class="palette-title">{{ t('Color Tones') }}</h4>
<div class="palette-grid">
<div class="color-picker-section">
<h4 class="section-title">{{ t('Background Color') }}</h4>
<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
v-for="(color, index) in colorPalette"
v-for="(color, index) in commonColors"
:key="index"
class="palette-color"
class="common-color-item"
:class="{ 'active': color === backgroundColor }"
:style="{ backgroundColor: color }"
@click="selectPaletteColor(color)"
@click="selectCommonColor(color)"
:title="color"
>
</div>
@ -233,11 +246,27 @@ const isColorValid = ref<boolean>(true)
let colorChangeDebounceTimer: ReturnType<typeof setTimeout> | null = null
const colorThief = new ColorThief()
// Generate color palette based on HSL color space
const colorPalette = computed(() => {
return generateColorPalette(backgroundColor.value)
// Generate 11 color shades based on current background color (current color in middle)
const colorShades = computed(() => {
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
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)
@ -277,24 +306,27 @@ const hslToHex = (h: number, s: number, l: number): string => {
return `#${f(0)}${f(8)}${f(4)}`.toUpperCase()
}
const generateColorPalette = (baseColor: string): string[] => {
const { h, s } = hexToHsl(baseColor)
const palette: string[] = []
// Generate 11 color shades with current color in the middle (6th position)
const generateColorShades = (baseColor: string): string[] => {
const { h, s, l } = hexToHsl(baseColor)
const shades: string[] = []
// Generate 12 colors with same hue, varying saturation and lightness
const lightnessLevels = [95, 85, 75, 65, 55, 45, 35, 25, 15, 10, 5]
const saturationVariants = [s, Math.min(100, s + 15), Math.max(0, s - 15)]
// Main tones (same saturation, different lightness)
for (const l of lightnessLevels) {
palette.push(hslToHex(h, s, l))
// Generate 5 lighter shades before current color
for (let i = 5; i > 0; i--) {
const lighterL = Math.min(100, l + (i * 8))
shades.push(hslToHex(h, s, lighterL))
}
// Add some saturation variants
palette.push(hslToHex(h, Math.min(100, s + 20), 50))
palette.push(hslToHex(h, Math.max(20, s - 20), 50))
// Add current color in the middle (6th position)
shades.push(baseColor)
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(() => {
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
applyBackgroundSilent()
}
@ -1280,7 +1317,7 @@ const removeHistoryItem = (index: number) => {
width: 100%;
padding: 12px 36px;
gap: 16px;
padding-right: 316px; /* Make space for right sidebar + padding */
padding-right: 316px;
}
.page-header h2 {
@ -1291,66 +1328,47 @@ const removeHistoryItem = (index: number) => {
flex-shrink: 0;
}
/* Header Color Controls */
.header-color-controls {
/* Header Color Shades */
.header-color-shades {
display: flex;
align-items: center;
gap: 12px;
gap: 8px;
flex: 1;
justify-content: center;
}
.header-color-label {
font-size: 14px;
font-weight: 600;
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;
.shade-item {
width: 40px;
height: 40px;
border-radius: 8px;
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 {
padding: 0;
&:hover {
transform: scale(1.1);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
z-index: 1;
}
&::-webkit-color-swatch {
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 {
&.active {
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);
box-shadow: 0 0 0 3px rgba(31, 199, 111, 0.3);
transform: scale(1.15);
&::after {
content: '';
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;
}
/* Right Sidebar - Naive UI Style */
/* Right Sidebar */
.right-sidebar {
position: fixed;
right: 0;
top: 64px; /* Below app header */
top: 64px;
bottom: 0;
width: 280px;
background: white;
@ -1669,15 +1687,19 @@ const removeHistoryItem = (index: number) => {
flex: 1;
overflow-y: auto;
padding: 20px;
display: flex;
flex-direction: column;
gap: 24px;
}
.palette-section {
.color-picker-section,
.common-colors-section {
display: flex;
flex-direction: column;
gap: 16px;
}
.palette-title {
.section-title {
font-size: 14px;
font-weight: 600;
color: #1f2937;
@ -1686,13 +1708,61 @@ const removeHistoryItem = (index: number) => {
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;
grid-template-columns: repeat(4, 1fr);
grid-template-columns: repeat(3, 1fr);
gap: 10px;
}
.palette-color {
.common-color-item {
aspect-ratio: 1;
border-radius: 8px;
cursor: pointer;
@ -1939,10 +2009,16 @@ const removeHistoryItem = (index: number) => {
width: 100%;
}
.header-color-controls {
.header-color-shades {
width: 100%;
justify-content: flex-start;
order: 2;
gap: 6px;
}
.shade-item {
width: 32px;
height: 32px;
}
.toolbar-actions {
@ -2016,8 +2092,8 @@ const removeHistoryItem = (index: number) => {
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.05);
}
.palette-grid {
grid-template-columns: repeat(6, 1fr);
.common-colors-grid {
grid-template-columns: repeat(4, 1fr);
}
.history-bar {
@ -2061,7 +2137,7 @@ const removeHistoryItem = (index: number) => {
width: 240px;
}
.palette-grid {
.common-colors-grid {
grid-template-columns: repeat(3, 1fr);
}
}