|
<template> |
|
<div |
|
class="thumbnails" |
|
@mousedown="() => setThumbnailsFocus(true)" |
|
v-click-outside="() => setThumbnailsFocus(false)" |
|
v-contextmenu="contextmenusThumbnails" |
|
> |
|
<div class="add-slide"> |
|
<div class="btn" @click="createSlide()"><IconPlus class="icon" />添加幻灯片</div> |
|
<Popover trigger="click" placement="bottom-start" v-model:value="presetLayoutPopoverVisible" center> |
|
<template #content> |
|
<Templates |
|
@select="slide => { createSlideByTemplate(slide); presetLayoutPopoverVisible = false }" |
|
@selectAll="slides => { insertAllTemplates(slides); presetLayoutPopoverVisible = false }" |
|
/> |
|
</template> |
|
<div class="select-btn"><IconDown /></div> |
|
</Popover> |
|
</div> |
|
|
|
<Draggable |
|
class="thumbnail-list" |
|
ref="thumbnailsRef" |
|
:modelValue="slides" |
|
:animation="200" |
|
:scroll="true" |
|
:scrollSensitivity="50" |
|
:disabled="editingSectionId" |
|
@end="handleDragEnd" |
|
itemKey="id" |
|
> |
|
<template #item="{ element, index }"> |
|
<div class="thumbnail-container"> |
|
<div class="section-title" |
|
:data-section-id="element?.sectionTag?.id || ''" |
|
v-if="element.sectionTag || (hasSection && index === 0)" |
|
v-contextmenu="contextmenusSection" |
|
> |
|
<input |
|
:id="`section-title-input-${element?.sectionTag?.id || 'default'}`" |
|
type="text" |
|
:value="element?.sectionTag?.title || ''" |
|
placeholder="输入节名称" |
|
@blur="$event => saveSection($event)" |
|
@keydown.enter.stop="$event => saveSection($event)" |
|
v-if="editingSectionId === element?.sectionTag?.id || (index === 0 && editingSectionId === 'default')" |
|
> |
|
<span class="text" v-else> |
|
<div class="text-content">{{ element?.sectionTag ? (element?.sectionTag?.title || '无标题节') : '默认节' }}</div> |
|
</span> |
|
</div> |
|
<div |
|
class="thumbnail-item" |
|
:class="{ |
|
'active': slideIndex === index, |
|
'selected': selectedSlidesIndex.includes(index), |
|
}" |
|
@mousedown="$event => handleClickSlideThumbnail($event, index)" |
|
@dblclick="enterScreening()" |
|
v-contextmenu="contextmenusThumbnailItem" |
|
> |
|
<div class="label" :class="{ 'offset-left': index >= 99 }">{{ fillDigit(index + 1, 2) }}</div> |
|
<ThumbnailSlide class="thumbnail" :slide="element" :size="120" :visible="index < slidesLoadLimit" /> |
|
|
|
|
|
<div class="share-btn" @click.stop="handleShareSlide(index)" v-tooltip="'分享本页'"> |
|
<IconShare class="icon" /> |
|
</div> |
|
|
|
<div class="note-flag" v-if="element.notes && element.notes.length" @click="openNotesPanel()">{{ element.notes.length }}</div> |
|
</div> |
|
</div> |
|
</template> |
|
</Draggable> |
|
|
|
<div class="page-number">幻灯片 {{slideIndex + 1}} / {{slides.length}}</div> |
|
|
|
|
|
<Modal |
|
v-model:visible="shareModalVisible" |
|
:width="600" |
|
title="分享幻灯片" |
|
:footer="null" |
|
> |
|
<div class="share-modal-content"> |
|
<div class="share-slide-info"> |
|
<h4>第 {{ shareSlideIndex + 1 }} 页幻灯片</h4> |
|
<div class="slide-preview"> |
|
<ThumbnailSlide |
|
v-if="shareSlideIndex >= 0 && slides[shareSlideIndex]" |
|
:slide="slides[shareSlideIndex]" |
|
:size="200" |
|
/> |
|
</div> |
|
</div> |
|
|
|
<div class="share-links"> |
|
<div class="link-group"> |
|
<label>单页查看链接:</label> |
|
<div class="link-input-group"> |
|
<input |
|
ref="slideUrlRef" |
|
:value="shareLinks.slideUrl || '正在生成...'" |
|
readonly |
|
class="link-input" |
|
/> |
|
<button @click="copyToClipboard(shareLinks.slideUrl, '单页链接')" class="copy-btn">复制</button> |
|
</div> |
|
</div> |
|
|
|
<div class="link-group"> |
|
<label>完整PPT链接:</label> |
|
<div class="link-input-group"> |
|
<input |
|
ref="pptUrlRef" |
|
:value="shareLinks.pptUrl || '正在生成...'" |
|
readonly |
|
class="link-input" |
|
/> |
|
<button @click="copyToClipboard(shareLinks.pptUrl, 'PPT链接')" class="copy-btn">复制</button> |
|
</div> |
|
</div> |
|
|
|
<div class="link-group"> |
|
<label>图片链接:</label> |
|
<div class="link-input-group"> |
|
<input |
|
ref="screenshotUrlRef" |
|
:value="shareLinks.screenshotUrl || '正在生成...'" |
|
readonly |
|
class="link-input" |
|
/> |
|
<button @click="copyToClipboard(shareLinks.screenshotUrl, '图片链接')" class="copy-btn">复制</button> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<div class="share-tips"> |
|
<p><strong>说明:</strong></p> |
|
<ul> |
|
<li><strong>单页查看链接</strong>:只显示当前选中的幻灯片页面</li> |
|
<li><strong>完整PPT链接</strong>:可以查看整个PPT演示文稿</li> |
|
<li><strong>图片链接</strong>:直接返回幻灯片的JPEG图片</li> |
|
</ul> |
|
<p class="note">💡 这些链接是固定的,可以直接分享给他人访问</p> |
|
</div> |
|
</div> |
|
</Modal> |
|
</div> |
|
</template> |
|
|
|
<script lang="ts" setup> |
|
import { computed, nextTick, ref, watch } from 'vue' |
|
import { storeToRefs } from 'pinia' |
|
import { useMainStore, useSlidesStore, useKeyboardStore, useAuthStore } from '@/store' |
|
import { fillDigit } from '@/utils/common' |
|
import { isElementInViewport } from '@/utils/element' |
|
import type { ContextmenuItem } from '@/components/Contextmenu/types' |
|
import useSlideHandler from '@/hooks/useSlideHandler' |
|
import useSectionHandler from '@/hooks/useSectionHandler' |
|
import useScreening from '@/hooks/useScreening' |
|
import useLoadSlides from '@/hooks/useLoadSlides' |
|
import useAddSlidesOrElements from '@/hooks/useAddSlidesOrElements' |
|
import usePPTManager from '@/hooks/usePPTManager' |
|
import message from '@/utils/message' |
|
import type { Slide } from '@/types/slides' |
|
|
|
import ThumbnailSlide from '@/views/components/ThumbnailSlide/index.vue' |
|
import Templates from './Templates.vue' |
|
import Popover from '@/components/Popover.vue' |
|
import Modal from '@/components/Modal.vue' |
|
import Draggable from 'vuedraggable' |
|
|
|
const mainStore = useMainStore() |
|
const slidesStore = useSlidesStore() |
|
const keyboardStore = useKeyboardStore() |
|
const { selectedSlidesIndex: _selectedSlidesIndex, thumbnailsFocus } = storeToRefs(mainStore) |
|
const { slides, slideIndex, currentSlide } = storeToRefs(slidesStore) |
|
const { ctrlKeyState, shiftKeyState } = storeToRefs(keyboardStore) |
|
|
|
const { slidesLoadLimit } = useLoadSlides() |
|
|
|
const selectedSlidesIndex = computed(() => [..._selectedSlidesIndex.value, slideIndex.value]) |
|
|
|
const presetLayoutPopoverVisible = ref(false) |
|
|
|
const hasSection = computed(() => { |
|
return slides.value.some(item => item.sectionTag) |
|
}) |
|
|
|
const { addSlidesFromData } = useAddSlidesOrElements() |
|
|
|
const { |
|
copySlide, |
|
pasteSlide, |
|
createSlide, |
|
createSlideByTemplate, |
|
copyAndPasteSlide, |
|
deleteSlide, |
|
cutSlide, |
|
selectAllSlide, |
|
sortSlides, |
|
isEmptySlide, |
|
} = useSlideHandler() |
|
|
|
const { |
|
createSection, |
|
removeSection, |
|
removeAllSection, |
|
removeSectionSlides, |
|
updateSectionTitle, |
|
} = useSectionHandler() |
|
|
|
|
|
const thumbnailsRef = ref<InstanceType<typeof Draggable>>() |
|
watch(() => slideIndex.value, () => { |
|
|
|
|
|
if (selectedSlidesIndex.value.length) { |
|
mainStore.updateSelectedSlidesIndex([]) |
|
} |
|
|
|
|
|
nextTick(() => { |
|
const activeThumbnailRef: HTMLElement = thumbnailsRef.value?.$el?.querySelector('.thumbnail-item.active') |
|
if (thumbnailsRef.value && activeThumbnailRef && !isElementInViewport(activeThumbnailRef, thumbnailsRef.value.$el)) { |
|
setTimeout(() => { |
|
activeThumbnailRef.scrollIntoView({ behavior: 'smooth' }) |
|
}, 100) |
|
} |
|
}) |
|
}, { immediate: true }) |
|
|
|
|
|
const changeSlideIndex = (index: number) => { |
|
mainStore.setActiveElementIdList([]) |
|
|
|
if (slideIndex.value === index) return |
|
slidesStore.updateSlideIndex(index) |
|
} |
|
|
|
|
|
const handleClickSlideThumbnail = (e: MouseEvent, index: number) => { |
|
if (editingSectionId.value) return |
|
|
|
const isMultiSelected = selectedSlidesIndex.value.length > 1 |
|
|
|
if (isMultiSelected && selectedSlidesIndex.value.includes(index) && e.button !== 0) return |
|
|
|
|
|
|
|
if (ctrlKeyState.value) { |
|
if (slideIndex.value === index) { |
|
if (!isMultiSelected) return |
|
|
|
const newSelectedSlidesIndex = selectedSlidesIndex.value.filter(item => item !== index) |
|
mainStore.updateSelectedSlidesIndex(newSelectedSlidesIndex) |
|
changeSlideIndex(selectedSlidesIndex.value[0]) |
|
} |
|
else { |
|
if (selectedSlidesIndex.value.includes(index)) { |
|
const newSelectedSlidesIndex = selectedSlidesIndex.value.filter(item => item !== index) |
|
mainStore.updateSelectedSlidesIndex(newSelectedSlidesIndex) |
|
} |
|
else { |
|
const newSelectedSlidesIndex = [...selectedSlidesIndex.value, index] |
|
mainStore.updateSelectedSlidesIndex(newSelectedSlidesIndex) |
|
} |
|
} |
|
} |
|
|
|
else if (shiftKeyState.value) { |
|
if (slideIndex.value === index && !isMultiSelected) return |
|
|
|
let minIndex = Math.min(...selectedSlidesIndex.value) |
|
let maxIndex = index |
|
|
|
if (index < minIndex) { |
|
maxIndex = Math.max(...selectedSlidesIndex.value) |
|
minIndex = index |
|
} |
|
|
|
const newSelectedSlidesIndex = [] |
|
for (let i = minIndex; i <= maxIndex; i++) newSelectedSlidesIndex.push(i) |
|
mainStore.updateSelectedSlidesIndex(newSelectedSlidesIndex) |
|
} |
|
|
|
else { |
|
mainStore.updateSelectedSlidesIndex([]) |
|
changeSlideIndex(index) |
|
} |
|
} |
|
|
|
|
|
const setThumbnailsFocus = (focus: boolean) => { |
|
if (thumbnailsFocus.value === focus) return |
|
mainStore.setThumbnailsFocus(focus) |
|
|
|
if (!focus) mainStore.updateSelectedSlidesIndex([]) |
|
} |
|
|
|
|
|
const handleDragEnd = (eventData: { newIndex: number; oldIndex: number }) => { |
|
const { newIndex, oldIndex } = eventData |
|
if (newIndex === undefined || oldIndex === undefined || newIndex === oldIndex) return |
|
sortSlides(newIndex, oldIndex) |
|
} |
|
|
|
|
|
const openNotesPanel = () => { |
|
mainStore.setNotesPanelState(true) |
|
} |
|
|
|
const editingSectionId = ref('') |
|
|
|
const editSection = (id: string) => { |
|
mainStore.setDisableHotkeysState(true) |
|
editingSectionId.value = id || 'default' |
|
|
|
nextTick(() => { |
|
const inputRef = document.querySelector(`#section-title-input-${id || 'default'}`) as HTMLInputElement |
|
inputRef.focus() |
|
}) |
|
} |
|
|
|
const saveSection = (e: FocusEvent | KeyboardEvent) => { |
|
const title = (e.target as HTMLInputElement).value |
|
updateSectionTitle(editingSectionId.value, title) |
|
|
|
editingSectionId.value = '' |
|
mainStore.setDisableHotkeysState(false) |
|
} |
|
|
|
const insertAllTemplates = (slides: Slide[]) => { |
|
if (isEmptySlide.value) slidesStore.setSlides(slides) |
|
else addSlidesFromData(slides) |
|
} |
|
|
|
const contextmenusSection = (el: HTMLElement): ContextmenuItem[] => { |
|
const sectionId = el.dataset.sectionId! |
|
|
|
return [ |
|
{ |
|
text: '删除节', |
|
handler: () => removeSection(sectionId), |
|
}, |
|
{ |
|
text: '删除节和幻灯片', |
|
handler: () => { |
|
mainStore.setActiveElementIdList([]) |
|
removeSectionSlides(sectionId) |
|
}, |
|
}, |
|
{ |
|
text: '删除所有节', |
|
handler: removeAllSection, |
|
}, |
|
{ |
|
text: '重命名节', |
|
handler: () => editSection(sectionId), |
|
}, |
|
] |
|
} |
|
|
|
const { enterScreening, enterScreeningFromStart } = useScreening() |
|
|
|
const contextmenusThumbnails = (): ContextmenuItem[] => { |
|
return [ |
|
{ |
|
text: '粘贴', |
|
subText: 'Ctrl + V', |
|
handler: pasteSlide, |
|
}, |
|
{ |
|
text: '全选', |
|
subText: 'Ctrl + A', |
|
handler: selectAllSlide, |
|
}, |
|
{ |
|
text: '新建页面', |
|
subText: 'Enter', |
|
handler: createSlide, |
|
}, |
|
{ |
|
text: '幻灯片放映', |
|
subText: 'F5', |
|
handler: enterScreeningFromStart, |
|
}, |
|
] |
|
} |
|
|
|
const contextmenusThumbnailItem = (): ContextmenuItem[] => { |
|
return [ |
|
{ |
|
text: '剪切', |
|
subText: 'Ctrl + X', |
|
handler: cutSlide, |
|
}, |
|
{ |
|
text: '复制', |
|
subText: 'Ctrl + C', |
|
handler: copySlide, |
|
}, |
|
{ |
|
text: '粘贴', |
|
subText: 'Ctrl + V', |
|
handler: pasteSlide, |
|
}, |
|
{ |
|
text: '全选', |
|
subText: 'Ctrl + A', |
|
handler: selectAllSlide, |
|
}, |
|
{ divider: true }, |
|
{ |
|
text: '新建页面', |
|
subText: 'Enter', |
|
handler: createSlide, |
|
}, |
|
{ |
|
text: '复制页面', |
|
subText: 'Ctrl + D', |
|
handler: copyAndPasteSlide, |
|
}, |
|
{ |
|
text: '删除页面', |
|
subText: 'Delete', |
|
handler: () => deleteSlide(), |
|
}, |
|
{ |
|
text: '增加节', |
|
handler: createSection, |
|
disable: !!currentSlide.value.sectionTag, |
|
}, |
|
{ divider: true }, |
|
{ |
|
text: '从当前放映', |
|
subText: 'Shift + F5', |
|
handler: enterScreening, |
|
}, |
|
] |
|
} |
|
|
|
|
|
const authStore = useAuthStore() |
|
const { generateShareLinks } = usePPTManager() |
|
|
|
const shareModalVisible = ref(false) |
|
const shareSlideIndex = ref(-1) |
|
const shareLinks = ref({ |
|
slideUrl: '', |
|
pptUrl: '', |
|
screenshotUrl: '', |
|
}) |
|
|
|
|
|
const isGeneratingLinks = ref(false) |
|
const lastGenerateTime = ref(0) |
|
const generateDebounceTime = 2000 |
|
|
|
|
|
const handleShareSlide = async (index: number) => { |
|
if (!authStore.isLoggedIn) { |
|
message.error('请先登录后再分享') |
|
return |
|
} |
|
|
|
|
|
const now = Date.now() |
|
if (isGeneratingLinks.value || (now - lastGenerateTime.value < generateDebounceTime)) { |
|
message.warning('请稍后再试,避免频繁请求') |
|
return |
|
} |
|
|
|
shareSlideIndex.value = index |
|
shareModalVisible.value = true |
|
isGeneratingLinks.value = true |
|
lastGenerateTime.value = now |
|
|
|
|
|
shareLinks.value = { |
|
slideUrl: '', |
|
pptUrl: '', |
|
screenshotUrl: '', |
|
} |
|
|
|
try { |
|
|
|
const links = await generateShareLinks(index) |
|
shareLinks.value = { |
|
slideUrl: links.slideUrl, |
|
pptUrl: links.pptUrl, |
|
screenshotUrl: links.screenshotUrl, |
|
} |
|
} |
|
catch (error) { |
|
message.error('生成分享链接失败') |
|
|
|
} |
|
finally { |
|
|
|
setTimeout(() => { |
|
isGeneratingLinks.value = false |
|
}, 1000) |
|
} |
|
} |
|
|
|
|
|
const copyToClipboard = async (text: string, label: string) => { |
|
if (!text || text === '正在生成...') { |
|
message.error('链接还未生成完成') |
|
return |
|
} |
|
|
|
try { |
|
await navigator.clipboard.writeText(text) |
|
message.success(`${label}已复制到剪贴板`) |
|
} |
|
catch (error) { |
|
|
|
const textArea = document.createElement('textarea') |
|
textArea.value = text |
|
document.body.appendChild(textArea) |
|
textArea.select() |
|
try { |
|
document.execCommand('copy') |
|
message.success(`${label}已复制到剪贴板`) |
|
} |
|
catch (fallbackError) { |
|
message.error(`复制${label}失败`) |
|
} |
|
document.body.removeChild(textArea) |
|
} |
|
} |
|
</script> |
|
|
|
<style lang="scss" scoped> |
|
.thumbnails { |
|
border-right: solid 1px $borderColor; |
|
background-color: #fff; |
|
display: flex; |
|
flex-direction: column; |
|
user-select: none; |
|
} |
|
.add-slide { |
|
height: 40px; |
|
font-size: 12px; |
|
display: flex; |
|
flex-shrink: 0; |
|
border-bottom: 1px solid $borderColor; |
|
cursor: pointer; |
|
|
|
.btn { |
|
flex: 1; |
|
display: flex; |
|
justify-content: center; |
|
align-items: center; |
|
|
|
&:hover { |
|
background-color: $lightGray; |
|
} |
|
} |
|
.select-btn { |
|
width: 30px; |
|
height: 100%; |
|
display: flex; |
|
justify-content: center; |
|
align-items: center; |
|
border-left: 1px solid $borderColor; |
|
|
|
&:hover { |
|
background-color: $lightGray; |
|
} |
|
} |
|
|
|
.icon { |
|
margin-right: 3px; |
|
font-size: 14px; |
|
} |
|
} |
|
.thumbnail-list { |
|
padding: 5px 0; |
|
flex: 1; |
|
overflow: auto; |
|
} |
|
.thumbnail-item { |
|
display: flex; |
|
justify-content: center; |
|
align-items: center; |
|
padding: 5px 0; |
|
position: relative; |
|
|
|
.thumbnail { |
|
border-radius: $borderRadius; |
|
outline: 2px solid rgba($color: $themeColor, $alpha: .15); |
|
} |
|
|
|
&.active { |
|
.label { |
|
color: $themeColor; |
|
} |
|
.thumbnail { |
|
outline-color: $themeColor; |
|
} |
|
} |
|
&.selected { |
|
.thumbnail { |
|
outline-color: $themeColor; |
|
} |
|
.note-flag { |
|
background-color: $themeColor; |
|
|
|
&::after { |
|
border-top-color: $themeColor; |
|
} |
|
} |
|
} |
|
|
|
.note-flag { |
|
width: 16px; |
|
height: 12px; |
|
border-radius: 1px; |
|
position: absolute; |
|
left: 8px; |
|
top: 13px; |
|
font-size: 8px; |
|
background-color: rgba($color: $themeColor, $alpha: .75); |
|
color: #fff; |
|
text-align: center; |
|
line-height: 12px; |
|
cursor: pointer; |
|
|
|
&::after { |
|
content: ''; |
|
width: 0; |
|
height: 0; |
|
position: absolute; |
|
top: 10px; |
|
left: 4px; |
|
border: 4px solid transparent; |
|
border-top-color: rgba($color: $themeColor, $alpha: .75); |
|
} |
|
} |
|
|
|
.share-btn { |
|
position: absolute; |
|
top: 5px; |
|
right: 5px; |
|
width: 20px; |
|
height: 20px; |
|
background: rgba(255, 255, 255, 0.9); |
|
border-radius: 50%; |
|
display: flex; |
|
align-items: center; |
|
justify-content: center; |
|
cursor: pointer; |
|
opacity: 0; |
|
transition: opacity 0.2s ease; |
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); |
|
|
|
.icon { |
|
font-size: 12px; |
|
color: $themeColor; |
|
} |
|
|
|
&:hover { |
|
background: rgba(255, 255, 255, 1); |
|
transform: scale(1.1); |
|
|
|
.icon { |
|
color: darken($themeColor, 10%); |
|
} |
|
} |
|
} |
|
|
|
// 鼠标悬停时显示分享按钮 |
|
&:hover .share-btn { |
|
opacity: 1; |
|
} |
|
|
|
// 当前激活页面始终显示分享按钮 |
|
&.active .share-btn { |
|
opacity: 1; |
|
} |
|
} |
|
.label { |
|
font-size: 12px; |
|
color: #999; |
|
width: 20px; |
|
cursor: grab; |
|
|
|
&.offset-left { |
|
position: relative; |
|
left: -4px; |
|
} |
|
|
|
&:active { |
|
cursor: grabbing; |
|
} |
|
} |
|
.page-number { |
|
height: 40px; |
|
font-size: 12px; |
|
border-top: 1px solid $borderColor; |
|
line-height: 40px; |
|
text-align: center; |
|
color: #666; |
|
} |
|
.section-title { |
|
height: 26px; |
|
font-size: 12px; |
|
padding: 6px 8px 2px 18px; |
|
color: #555; |
|
|
|
&.contextmenu-active { |
|
color: $themeColor; |
|
|
|
.text::before { |
|
border-bottom-color: $themeColor; |
|
border-right-color: $themeColor; |
|
} |
|
} |
|
|
|
.text { |
|
display: flex; |
|
align-items: center; |
|
position: relative; |
|
|
|
&::before { |
|
content: ''; |
|
width: 0; |
|
height: 0; |
|
border-top: 3px solid transparent; |
|
border-left: 3px solid transparent; |
|
border-bottom: 3px solid #555; |
|
border-right: 3px solid #555; |
|
margin-right: 5px; |
|
} |
|
|
|
.text-content { |
|
display: inline-block; |
|
@include ellipsis-oneline(); |
|
} |
|
} |
|
|
|
input { |
|
width: 100%; |
|
border: 0; |
|
outline: 0; |
|
padding: 0; |
|
font-size: 12px; |
|
} |
|
} |
|
.share-modal-content { |
|
display: flex; |
|
flex-direction: column; |
|
gap: 20px; |
|
|
|
.share-slide-info { |
|
display: flex; |
|
flex-direction: column; |
|
align-items: center; |
|
|
|
h4 { |
|
margin-bottom: 10px; |
|
} |
|
|
|
.slide-preview { |
|
display: flex; |
|
justify-content: center; |
|
} |
|
} |
|
|
|
.share-links { |
|
display: flex; |
|
flex-direction: column; |
|
gap: 10px; |
|
|
|
.link-group { |
|
display: flex; |
|
flex-direction: column; |
|
|
|
label { |
|
margin-bottom: 5px; |
|
} |
|
|
|
.link-input-group { |
|
display: flex; |
|
align-items: center; |
|
gap: 10px; |
|
|
|
.link-input { |
|
flex: 1; |
|
padding: 5px; |
|
border: 1px solid $borderColor; |
|
border-radius: $borderRadius; |
|
} |
|
|
|
.copy-btn { |
|
padding: 5px 10px; |
|
background-color: $themeColor; |
|
color: #fff; |
|
border: none; |
|
border-radius: $borderRadius; |
|
cursor: pointer; |
|
|
|
&:hover { |
|
background-color: darken($themeColor, 10%); |
|
} |
|
} |
|
} |
|
} |
|
} |
|
|
|
.share-tips { |
|
p { |
|
margin-bottom: 5px; |
|
} |
|
|
|
ul { |
|
margin-left: 20px; |
|
list-style: disc; |
|
|
|
li { |
|
margin-bottom: 5px; |
|
} |
|
} |
|
|
|
.note { |
|
margin-top: 10px; |
|
color: $themeColor; |
|
} |
|
} |
|
} |
|
</style> |