|
<template> |
|
<div class="theme-styles-extract"> |
|
<Tabs |
|
:tabs="tabs" |
|
v-model:value="activeTab" |
|
:tabsStyle="{ marginBottom: '12px' }" |
|
:tabStyle="{ padding: '8px 12px' }" |
|
/> |
|
<div class="content"> |
|
<div class="config-item" v-if="themeStyles.fontNames.length"> |
|
<div class="label">字体:</div> |
|
<div class="values"> |
|
<div class="value-wrap" v-for="(item, index) in themeStyles.fontNames" :key="item"> |
|
<div class="value" :style="{ fontFamily: item }">{{ fontMap[item] || item }}</div> |
|
<div class="handler"> |
|
<div class="state" :class="{ 'active': selectedIndex.fontName === index }">√</div> |
|
<div class="config-btn" @click="selectedIndex.fontName = index">选择</div> |
|
<div class="config-btn" @click="updateTheme({ fontName: item }); selectedIndex.fontName = index">应用到主题</div> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
<div class="config-item" v-if="themeStyles.fontColors.length"> |
|
<div class="label">文字颜色:</div> |
|
<div class="values"> |
|
<div class="value-wrap" v-for="(item, index) in themeStyles.fontColors" :key="item"> |
|
<div class="value" :style="{ backgroundColor: item, color: getMostReadableColor(item) }">{{ getHexColor(item) }}</div> |
|
<div class="handler"> |
|
<div class="state" :class="{ 'active': selectedIndex.fontColor === index }">√</div> |
|
<div class="config-btn" @click="selectedIndex.fontColor = index">选择</div> |
|
<div class="config-btn" @click="updateTheme({ fontColor: item }); selectedIndex.fontColor = index">应用到主题</div> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
<div class="config-item" v-if="themeStyles.backgroundColors.length"> |
|
<div class="label">背景颜色:</div> |
|
<div class="values"> |
|
<div class="value-wrap" v-for="(item, index) in themeStyles.backgroundColors" :key="item"> |
|
<div class="value" :style="{ backgroundColor: item, color: getMostReadableColor(item) }">{{ getHexColor(item) }}</div> |
|
<div class="handler"> |
|
<div class="state" :class="{ 'active': selectedIndex.backgroundColor === index }">√</div> |
|
<div class="config-btn" @click="selectedIndex.backgroundColor = index">选择</div> |
|
<div class="config-btn" @click="updateTheme({ backgroundColor: item }); selectedIndex.backgroundColor = index">应用到主题</div> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
<div class="config-item" v-if="themeStyles.themeColors.length"> |
|
<div class="label">主题色:<span class="tip">(点击色块排除不要的颜色)</span></div> |
|
<div class="values inline"> |
|
<div class="value-wrap" v-for="(item, index) in themeStyles.themeColors" :key="item" @click="removeThemeColor(index)"> |
|
<div class="value" :class="{ 'disabled': !selectedIndex.themeColors.includes(index) }" :style="{ backgroundColor: item }"></div> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<div class="btns"> |
|
<Button class="btn" type="primary" @click="updateAllThemes()">将选中配置保存为主题</Button> |
|
</div> |
|
</div> |
|
</template> |
|
|
|
<script lang="ts" setup> |
|
import { onMounted, ref, watch } from 'vue' |
|
import { storeToRefs } from 'pinia' |
|
import tinycolor from 'tinycolor2' |
|
import { useSlidesStore } from '@/store' |
|
import { FONTS } from '@/configs/font' |
|
import useSlideTheme from '@/hooks/useSlideTheme' |
|
import Tabs from '@/components/Tabs.vue' |
|
import Button from '@/components/Button.vue' |
|
import type { SlideTheme } from '@/types/slides' |
|
import message from '@/utils/message' |
|
|
|
const emit = defineEmits<{ |
|
(event: 'close'): void |
|
}>() |
|
|
|
const slidesStore = useSlidesStore() |
|
const { slides, currentSlide } = storeToRefs(slidesStore) |
|
const { getSlidesThemeStyles } = useSlideTheme() |
|
|
|
interface TabItem { |
|
key: 'single' | 'all' |
|
label: string |
|
} |
|
|
|
const tabs: TabItem[] = [ |
|
{ key: 'single', label: '从当前页中提取' }, |
|
{ key: 'all', label: '从全部幻灯片提取' }, |
|
] |
|
const activeTab = ref<'single' | 'all'>('single') |
|
|
|
const fontMap = ref<{ [key: string]: string }>({}) |
|
onMounted(() => { |
|
const map: { [key: string]: string } = {} |
|
for (const item of FONTS) { |
|
map[item.value] = item.label |
|
} |
|
fontMap.value = map |
|
}) |
|
|
|
const themeStyles = ref<ReturnType<typeof getSlidesThemeStyles>>({ |
|
backgroundColors: [], |
|
themeColors: [], |
|
fontColors: [], |
|
fontNames: [], |
|
}) |
|
const selectedIndex = ref({ |
|
backgroundColor: 0, |
|
themeColors: [0], |
|
fontColor: 0, |
|
fontName: 0, |
|
}) |
|
|
|
watch(activeTab, () => { |
|
if (activeTab.value === 'single') themeStyles.value = getSlidesThemeStyles(currentSlide.value) |
|
else themeStyles.value = getSlidesThemeStyles(slides.value) |
|
|
|
const themeColors = [] |
|
for (let i = 0; i < Math.min(themeStyles.value.themeColors.length); i++) { |
|
themeColors.push(i) |
|
} |
|
|
|
selectedIndex.value = { |
|
backgroundColor: 0, |
|
fontColor: 0, |
|
fontName: 0, |
|
themeColors, |
|
} |
|
}, { immediate: true }) |
|
|
|
const updateTheme = (themeProps: Partial<SlideTheme>) => { |
|
slidesStore.setTheme(themeProps) |
|
} |
|
|
|
const updateAllThemes = () => { |
|
let themeColors = themeStyles.value.themeColors.filter((item, index) => selectedIndex.value.themeColors.includes(index)) |
|
if (themeColors.length > 6) { |
|
themeColors = themeColors.slice(0, 6) |
|
message.warning('主题色超出数量限制,已自动选取前6个') |
|
} |
|
|
|
const backgroundColor = themeStyles.value.backgroundColors[selectedIndex.value.backgroundColor] |
|
const fontColor = themeStyles.value.fontColors[selectedIndex.value.fontColor] |
|
const fontName = themeStyles.value.fontNames[selectedIndex.value.fontName] |
|
|
|
const data: Partial<SlideTheme> = {} |
|
if (backgroundColor) data.backgroundColor = backgroundColor |
|
if (fontColor) data.fontColor = fontColor |
|
if (fontName) data.fontName = fontName |
|
if (themeColors.length) data.themeColors = themeColors |
|
|
|
slidesStore.setTheme(data) |
|
emit('close') |
|
} |
|
|
|
const removeThemeColor = (index: number) => { |
|
if (selectedIndex.value.themeColors.includes(index)) { |
|
selectedIndex.value.themeColors = selectedIndex.value.themeColors.filter(i => i !== index) |
|
} |
|
else selectedIndex.value.themeColors.push(index) |
|
} |
|
|
|
const getMostReadableColor = (color: string) => { |
|
const colorList = ['#000', '#fff'] |
|
return tinycolor.mostReadable(color, colorList, { includeFallbackColors: true }).toRgbString() |
|
} |
|
const getHexColor = (color: string) => { |
|
const c = tinycolor(color) |
|
const alpha = c.getAlpha() |
|
if (alpha < 1) return c.toHex8String().toUpperCase() |
|
return c.toHexString().toUpperCase() |
|
} |
|
</script> |
|
|
|
<style lang="scss" scoped> |
|
.theme-styles-extract { |
|
height: 500px; |
|
display: flex; |
|
flex-direction: column; |
|
} |
|
.content { |
|
flex: 1; |
|
overflow: auto; |
|
padding-right: 20px; |
|
margin-right: -20px; |
|
} |
|
.config-item { |
|
padding: 12px 0 10px; |
|
border-bottom: 1px dashed #f5f5f5; |
|
font-size: 13px; |
|
} |
|
.label { |
|
margin-bottom: 5px; |
|
display: flex; |
|
align-items: center; |
|
|
|
.tip { |
|
font-size: 12px; |
|
color: #999; |
|
} |
|
} |
|
.values { |
|
&.inline { |
|
@include flex-grid-layout(); |
|
|
|
.value-wrap { |
|
@include flex-grid-layout-children(10, 9%); |
|
margin-top: 0 !important; |
|
cursor: pointer; |
|
} |
|
.value { |
|
height: 25px; |
|
padding: 0; |
|
|
|
&.disabled { |
|
opacity: 0.2; |
|
position: relative; |
|
|
|
&::after { |
|
content: ''; |
|
width: 24px; |
|
height: 2px; |
|
position: absolute; |
|
top: 11px; |
|
left: -1px; |
|
transform: rotate(-45deg); |
|
background-color: #000; |
|
} |
|
&::before { |
|
content: ''; |
|
width: 24px; |
|
height: 2px; |
|
position: absolute; |
|
top: 11px; |
|
left: -1px; |
|
transform: rotate(45deg); |
|
background-color: #000; |
|
} |
|
} |
|
} |
|
} |
|
|
|
.value-wrap { |
|
display: flex; |
|
align-items: center; |
|
justify-content: space-between; |
|
|
|
& + .value-wrap { |
|
margin-top: 3px; |
|
} |
|
} |
|
.handler { |
|
flex: 1; |
|
display: flex; |
|
align-items: center; |
|
justify-content: space-between; |
|
font-size: 12px; |
|
margin-left: 10px; |
|
|
|
.state { |
|
opacity: 0; |
|
|
|
&.active { |
|
opacity: 1; |
|
} |
|
} |
|
|
|
.config-btn { |
|
cursor: pointer; |
|
|
|
&:hover { |
|
color: $themeColor; |
|
} |
|
} |
|
} |
|
.value { |
|
width: 150px; |
|
height: 25px; |
|
line-height: 25px; |
|
text-align: center; |
|
border: 1px solid $borderColor; |
|
font-size: 12px; |
|
padding: 0 5px; |
|
border-radius: $borderRadius; |
|
@include ellipsis-oneline(); |
|
} |
|
} |
|
.btns { |
|
margin-top: 12px; |
|
|
|
.btn { |
|
width: 100%; |
|
} |
|
} |
|
</style> |
|
|