feat: 支持多颜色预置主题

This commit is contained in:
pipipi-pikachu 2022-07-24 22:23:11 +08:00
parent 09661dc3ed
commit 00ee84bfb5
3 changed files with 339 additions and 122 deletions

View File

@ -1,30 +1,93 @@
export const PRESET_THEMES = [
{ color: '#d14424', background: '#ffffff', text: '#333' },
{ color: '#42464b', background: '#ffffff', text: '#333' },
{ color: '#5d82ba', background: '#ffffff', text: '#333' },
{ color: '#005a6f', background: '#ffffff', text: '#333' },
{ color: '#d0614c', background: '#dfb044', text: '#333' },
{ color: '#86a1ad', background: '#dfdbd4', text: '#333' },
{ color: '#697586', background: '#d5c4a4', text: '#333' },
{ color: '#333333', background: '#7acfa6', text: '#333' },
{ color: '#42464b', background: '#415065', text: '#fff' },
{ color: '#0c5999', background: '#35a2cd', text: '#fff' },
{ color: '#c49a41', background: '#8c4357', text: '#fff' },
{ color: '#dfaa00', background: '#2e4e7d', text: '#fff' },
{ color: '#d1ad88', background: '#f99070', text: '#fff' },
{ color: '#464d52', background: '#657984', text: '#fff' },
{ color: '#ffcfb6', background: '#1e4c6f', text: '#fff' },
{ color: '#c3a043', background: '#43292a', text: '#fff' },
{ color: '#464d52', background: '#60546f', text: '#fff' },
{ color: '#df9636', background: '#5b89a0', text: '#fff' },
{ color: '#b898a4', background: '#93716b', text: '#fff' },
{ color: '#c47a11', background: '#187db1', text: '#fff' },
{ color: '#333333', background: '#759564', text: '#fff' },
{ color: '#355b5e', background: '#424b50', text: '#fff' },
{ color: '#d29090', background: '#942a32', text: '#fff' },
{ color: '#00cfdf', background: '#3b434d', text: '#fff' },
{ color: '#424246', background: '#c70042', text: '#fff' },
{ color: '#2e4155', background: '#b35d44', text: '#fff' },
{ color: '#11bfce', background: '#8f98aa', text: '#fff' },
{ color: '#333333', background: '#549688', text: '#fff' },
export interface PresetTheme {
background: string
fontColor: string
fontname: string
colors: string[]
}
export const PRESET_THEMES: PresetTheme[] = [
{
background: '#ffffff',
fontColor: '#333333',
fontname: 'Microsoft Yahei',
colors: ['#5b9bd5', '#ed7d31', '#a5a5a5', '#ffc000', '#4472c4', '#70ad47'],
},
{
background: '#ffffff',
fontColor: '#333333',
fontname: 'Microsoft Yahei',
colors: ['#83992a', '#3c9670', '#44709d', '#a23b32', '#d87728', '#deb340'],
},
{
background: '#ffffff',
fontColor: '#333333',
fontname: 'Microsoft Yahei',
colors: ['#e48312', '#bd582c', '#865640', '#9b8357', '#c2bc80', '#94a088'],
},
{
background: '#ffffff',
fontColor: '#333333',
fontname: 'Microsoft Yahei',
colors: ['#bdc8df', '#003fa9', '#f5ba00', '#ff7567', '#7676d9', '#923ffc'],
},
{
background: '#ffffff',
fontColor: '#333333',
fontname: 'Microsoft Yahei',
colors: ['#90c225', '#54a121', '#e6b91e', '#e86618', '#c42f19', '#918756'],
},
{
background: '#ffffff',
fontColor: '#333333',
fontname: 'Microsoft Yahei',
colors: ['#1cade4', '#2683c6', '#27ced7', '#42ba97', '#3e8853', '#62a39f'],
},
{
background: '#e9efd6',
fontColor: '#333333',
fontname: 'Microsoft Yahei',
colors: ['#a5300f', '#de7e18', '#9f8351', '#728653', '#92aa4c', '#6aac91'],
},
{
background: '#17444e',
fontColor: '#ffffff',
fontname: 'Microsoft Yahei',
colors: ['#b01513', '#ea6312', '#e6b729', '#6bab90', '#55839a', '#9e5d9d'],
},
{
background: '#36234d',
fontColor: '#ffffff',
fontname: 'Microsoft Yahei',
colors: ['#b31166', '#e33d6f', '#e45f3c', '#e9943a', '#9b6bf2', '#d63cd0'],
},
{
background: '#247fad',
fontColor: '#ffffff',
fontname: 'Microsoft Yahei',
colors: ['#052f61', '#a50e82', '#14967c', '#6a9e1f', '#e87d37', '#c62324'],
},
{
background: '#103f55',
fontColor: '#ffffff',
fontname: 'Microsoft Yahei',
colors: ['#40aebd', '#97e8d5', '#a1cf49', '#628f3e', '#f2df3a', '#fcb01c'],
},
{
background: '#242367',
fontColor: '#ffffff',
fontname: 'Microsoft Yahei',
colors: ['#ac3ec1', '#477bd1', '#46b298', '#90ba4c', '#dd9d31', '#e25345'],
},
{
background: '#e4b75e',
fontColor: '#333333',
fontname: 'Microsoft Yahei',
colors: ['#f0a22e', '#a5644e', '#b58b80', '#c3986d', '#a19574', '#c17529'],
},
{
background: '#333333',
fontColor: '#ffffff',
fontname: 'Microsoft Yahei',
colors: ['#bdc8df', '#003fa9', '#f5ba00', '#ff7567', '#7676d9', '#923ffc'],
},
]

180
src/hooks/useSlideTheme.ts Normal file
View File

@ -0,0 +1,180 @@
import tinycolor from 'tinycolor2'
import { storeToRefs } from 'pinia'
import { useSlidesStore } from '@/store'
import { Slide } from '@/types/slides'
import { PresetTheme } from '@/configs/theme'
import useHistorySnapshot from '@/hooks/useHistorySnapshot'
export default () => {
const slidesStore = useSlidesStore()
const { slides, currentSlide, theme } = storeToRefs(slidesStore)
const { addHistorySnapshot } = useHistorySnapshot()
// 获取指定幻灯片内所有颜色(主要的)
const getSlideAllColors = (slide: Slide) => {
const colors: string[] = []
for (const el of slide.elements) {
if (el.type === 'shape' && tinycolor(el.fill).getAlpha() !== 0) {
const color = tinycolor(el.fill).toRgbString()
if (!colors.includes(color)) colors.push(color)
}
if (el.type === 'text' && el.fill && tinycolor(el.fill).getAlpha() !== 0) {
const color = tinycolor(el.fill).toRgbString()
if (!colors.includes(color)) colors.push(color)
}
if (el.type === 'table' && el.theme && tinycolor(el.theme.color).getAlpha() !== 0) {
const color = tinycolor(el.theme.color).toRgbString()
if (!colors.includes(color)) colors.push(color)
}
if (el.type === 'chart' && el.fill && tinycolor(el.fill).getAlpha() !== 0) {
const color = tinycolor(el.fill).toRgbString()
if (!colors.includes(color)) colors.push(color)
}
if (el.type === 'line' && tinycolor(el.color).getAlpha() !== 0) {
const color = tinycolor(el.color).toRgbString()
if (!colors.includes(color)) colors.push(color)
}
if (el.type === 'audio' && tinycolor(el.color).getAlpha() !== 0) {
const color = tinycolor(el.color).toRgbString()
if (!colors.includes(color)) colors.push(color)
}
}
return colors
}
// 创建原颜色与新颜色的对应关系表
const createSlideThemeColorMap = (slide: Slide, newColors: string[]): { [key: string]: string } => {
const oldColors = getSlideAllColors(slide)
const themeColorMap = {}
if (oldColors.length > newColors.length) {
const analogous = tinycolor(newColors[0]).analogous(oldColors.length - newColors.length + 10)
const otherColors = analogous.map(item => item.toHexString()).slice(1)
newColors.push(...otherColors)
}
for (let i = 0; i < oldColors.length; i++) {
themeColorMap[oldColors[i]] = newColors[i]
}
return themeColorMap
}
// 设置幻灯片主题
const setSlideTheme = (slide: Slide, theme: PresetTheme) => {
const colorMap = createSlideThemeColorMap(slide, theme.colors)
if (!slide.background || slide.background.type !== 'image') {
slide.background = {
type: 'solid',
color: theme.background,
}
}
for (const el of slide.elements) {
if (el.type === 'shape') {
el.fill = colorMap[tinycolor(el.fill).toRgbString()] || el.fill
if (el.gradient) delete el.gradient
}
if (el.type === 'text') {
if (el.fill) el.fill = colorMap[tinycolor(el.fill).toRgbString()] || el.fill
el.defaultColor = theme.fontColor
el.defaultFontName = theme.fontname
}
if (el.type === 'table') {
if (el.theme) el.theme.color = colorMap[tinycolor(el.theme.color).toRgbString()] || el.theme.color
for (const rowCells of el.data) {
for (const cell of rowCells) {
if (cell.style) {
cell.style.color = theme.fontColor
cell.style.fontname = theme.fontname
}
}
}
}
if (el.type === 'chart') {
el.themeColor = [colorMap[tinycolor(el.themeColor[0]).toRgbString()]] || el.themeColor
el.gridColor = theme.fontColor
}
if (el.type === 'line') el.color = colorMap[tinycolor(el.color).toRgbString()] || el.color
if (el.type === 'audio') el.color = colorMap[tinycolor(el.color).toRgbString()] || el.color
if (el.type === 'latex') el.color = theme.fontColor
}
}
// 应用预置主题(单页)
const applyPresetThemeToSingleSlide = (theme: PresetTheme) => {
const newSlide: Slide = JSON.parse(JSON.stringify(currentSlide.value))
setSlideTheme(newSlide, theme)
slidesStore.updateSlide({
background: newSlide.background,
elements: newSlide.elements,
})
addHistorySnapshot()
}
// 应用预置主题(全部)
const applyPresetThemeToAllSlides = (theme: PresetTheme) => {
const newSlides: Slide[] = JSON.parse(JSON.stringify(slides.value))
for (const slide of newSlides) {
setSlideTheme(slide, theme)
}
slidesStore.setTheme({
backgroundColor: theme.background,
themeColor: theme.colors[0],
fontColor: theme.fontColor,
fontName: theme.fontname,
})
slidesStore.setSlides(newSlides)
addHistorySnapshot()
}
// 将当前主题配置应用到全部页面
const applyThemeToAllSlides = () => {
const newSlides: Slide[] = JSON.parse(JSON.stringify(slides.value))
const { themeColor, backgroundColor, fontColor, fontName } = theme.value
for (const slide of newSlides) {
if (!slide.background || slide.background.type !== 'image') {
slide.background = {
type: 'solid',
color: backgroundColor
}
}
for (const el of slide.elements) {
if (el.type === 'shape') el.fill = themeColor
else if (el.type === 'line') el.color = themeColor
else if (el.type === 'text') {
el.defaultColor = fontColor
el.defaultFontName = fontName
if (el.fill) el.fill = themeColor
}
else if (el.type === 'table') {
if (el.theme) el.theme.color = themeColor
for (const rowCells of el.data) {
for (const cell of rowCells) {
if (cell.style) {
cell.style.color = fontColor
cell.style.fontname = fontName
}
}
}
}
else if (el.type === 'chart') {
el.themeColor = [themeColor]
el.gridColor = fontColor
}
else if (el.type === 'latex') el.color = fontColor
else if (el.type === 'audio') el.color = themeColor
}
}
slidesStore.setSlides(newSlides)
addHistorySnapshot()
}
return {
applyPresetThemeToSingleSlide,
applyPresetThemeToAllSlides,
applyThemeToAllSlides,
}
}

View File

@ -165,29 +165,34 @@
</Popover>
</div>
<div class="title dropdown" :class="{ 'active': showPresetThemes }" @click="togglePresetThemesVisible()" style="margin-top: 20px;">
预置主题 <IconDown class="icon" />
</div>
<div class="theme-list" v-if="showPresetThemes">
<div class="row"><Button style="flex: 1;" @click="applyThemeToAllSlides()">应用主题到全部</Button></div>
<Divider />
<div class="title">预置主题</div>
<div class="theme-list">
<div
class="theme-item"
v-for="(item, index) in PRESET_THEMES"
:key="index"
:style="{ backgroundColor: item.background }"
@click="updateTheme({
fontColor: item.text,
:style="{
backgroundColor: item.background,
themeColor: item.color,
})"
fontFamily: item.fontname,
}"
>
<div class="theme-item-content">
<div class="text" :style="{ color: item.text }">Aa</div>
<div class="color-block" :style="{ backgroundColor: item.color }"></div>
<div class="text" :style="{ color: item.fontColor }">文字 Aa</div>
<div class="colors">
<div class="color-block" v-for="(color, index) in item.colors" :key="index" :style="{ backgroundColor: color}"></div>
</div>
<div class="btns">
<div class="btn" @click="applyPresetThemeToSingleSlide(item)">应用</div>
<div class="btn" @click="applyPresetThemeToAllSlides(item)">应用全局</div>
</div>
</div>
</div>
</div>
<div class="row"><Button style="flex: 1;" @click="applyThemeAllSlide()">应用主题到全部</Button></div>
</div>
</template>
@ -195,10 +200,11 @@
import { computed, ref } from 'vue'
import { storeToRefs } from 'pinia'
import { useMainStore, useSlidesStore } from '@/store'
import { Slide, SlideBackground, SlideTheme } from '@/types/slides'
import { SlideBackground, SlideTheme } from '@/types/slides'
import { PRESET_THEMES } from '@/configs/theme'
import { WEB_FONTS } from '@/configs/font'
import useHistorySnapshot from '@/hooks/useHistorySnapshot'
import useSlideTheme from '@/hooks/useSlideTheme'
import ColorButton from './common/ColorButton.vue'
import { getImageDataURL } from '@/utils/image'
@ -218,6 +224,11 @@ const background = computed(() => {
})
const { addHistorySnapshot } = useHistorySnapshot()
const {
applyPresetThemeToSingleSlide,
applyPresetThemeToAllSlides,
applyThemeToAllSlides,
} = useSlideTheme()
//
const updateBackgroundType = (type: 'solid' | 'image' | 'gradient') => {
@ -281,58 +292,6 @@ const updateTheme = (themeProps: Partial<SlideTheme>) => {
slidesStore.setTheme(themeProps)
}
//
const applyThemeAllSlide = () => {
const newSlides: Slide[] = JSON.parse(JSON.stringify(slides.value))
const { themeColor, backgroundColor, fontColor, fontName } = theme.value
for (const slide of newSlides) {
if (!slide.background || slide.background.type !== 'image') {
slide.background = {
...slide.background,
type: 'solid',
color: backgroundColor
}
}
const elements = slide.elements
for (const el of elements) {
if (el.type === 'shape') el.fill = themeColor
else if (el.type === 'line') el.color = themeColor
else if (el.type === 'text') {
el.defaultColor = fontColor
el.defaultFontName = fontName
if (el.fill) el.fill = themeColor
}
else if (el.type === 'table') {
if (el.theme) el.theme.color = themeColor
for (const rowCells of el.data) {
for (const cell of rowCells) {
if (cell.style) {
cell.style.color = fontColor
cell.style.fontname = fontName
}
}
}
}
else if (el.type === 'chart') {
el.themeColor = [themeColor]
el.gridColor = fontColor
}
else if (el.type === 'latex') el.color = fontColor
else if (el.type === 'audio') el.color = themeColor
}
}
slidesStore.setSlides(newSlides)
addHistorySnapshot()
}
//
const showPresetThemes = ref(true)
const togglePresetThemesVisible = () => {
showPresetThemes.value = !showPresetThemes.value
}
//
const updateViewportRatio = (value: number) => {
slidesStore.setViewportRatio(value)
@ -351,21 +310,6 @@ const updateViewportRatio = (value: number) => {
}
.title {
margin-bottom: 10px;
&.dropdown {
display: flex;
align-items: center;
cursor: pointer;
.icon {
margin-left: 5px;
transition: transform $transitionDelayFast;
}
&:not(.active) .icon {
transform: rotate(-90deg);
}
}
}
.background-image-wrapper {
margin-bottom: 10px;
@ -400,9 +344,9 @@ const updateViewportRatio = (value: number) => {
@include flex-grid-layout();
}
.theme-item {
@include flex-grid-layout-children(4, 22%);
@include flex-grid-layout-children(2, 48%);
padding-bottom: 22%;
padding-bottom: 30%;
border-radius: $borderRadius;
position: relative;
cursor: pointer;
@ -413,22 +357,52 @@ const updateViewportRatio = (value: number) => {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
transition: box-shadow $transitionDelay;
&:hover {
box-shadow: 0 0 4px #888;
}
padding: 8px;
border: 1px solid $borderColor;
}
.text {
font-size: 16px;
}
.colors {
display: flex;
}
.color-block {
width: 28px;
height: 10px;
margin-top: 5px;
margin-top: 8px;
width: 12px;
height: 12px;
margin-right: 2px;
}
&:hover .btns {
display: flex;
}
.btns {
@include absolute-0();
flex-direction: column;
justify-content: center;
align-items: center;
display: none;
background-color: rgba($color: #000, $alpha: .25);
}
.btn {
width: 72px;
padding: 5px 0;
text-align: center;
background-color: $themeColor;
color: #fff;
font-size: 12px;
border-radius: $borderRadius;
&:hover {
background-color: #c42f19;
}
& + .btn {
margin-top: 5px;
}
}
}
.slider {