mirror of
https://github.com/pipipi-pikachu/PPTist.git
synced 2025-04-15 02:20:00 +08:00
feat: 支持多颜色预置主题
This commit is contained in:
parent
09661dc3ed
commit
00ee84bfb5
@ -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
180
src/hooks/useSlideTheme.ts
Normal 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,
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
Loading…
x
Reference in New Issue
Block a user