CatPtain's picture
Upload 339 files
89ce340 verified
raw
history blame
8.77 kB
<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>