feat: 优化添加背景页面颜色选择体验
- 标题栏中间显示11个色阶(当前色居中,左浅右深) - 右侧栏顶部添加颜色选择器和输入框 - 右侧色调面板改为12个常用背景色(白色起始,由浅到深) - 优化交互:点击即时应用,活动状态清晰标识
This commit is contained in:
parent
0f30876371
commit
ae7ed98808
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user