feat(theme): 新增 文章水印 功能
This commit is contained in:
parent
c6a2b8cab2
commit
80b3da3e58
@ -11,6 +11,7 @@ import PageFooter from './PageFooter.vue'
|
||||
import PageMeta from './PageMeta.vue'
|
||||
import EncryptPage from './EncryptPage.vue'
|
||||
import TransitionFadeSlideY from './TransitionFadeSlideY.vue'
|
||||
import Watermark from './Watermark.vue'
|
||||
|
||||
const { hasSidebar, hasAside } = useSidebar()
|
||||
const isDark = useDarkMode()
|
||||
@ -56,8 +57,10 @@ onContentUpdated(() => zoom?.refresh())
|
||||
<PageMeta />
|
||||
<EncryptPage v-if="!isPageDecrypted" />
|
||||
<template v-else>
|
||||
<Content class="plume-content" />
|
||||
|
||||
<div style="position: relative;">
|
||||
<Content class="plume-content" />
|
||||
<Watermark />
|
||||
</div>
|
||||
<PageFooter />
|
||||
<PageComment v-if="hasComments" :darkmode="isDark" />
|
||||
</template>
|
||||
|
||||
105
theme/src/client/components/Watermark.vue
Normal file
105
theme/src/client/components/Watermark.vue
Normal file
@ -0,0 +1,105 @@
|
||||
<script setup lang="ts">
|
||||
import { useWaterMark } from '../composables/index.js'
|
||||
|
||||
const {
|
||||
enableWatermark,
|
||||
isFullPage,
|
||||
svgElRef,
|
||||
svgRect,
|
||||
imageUrl,
|
||||
content,
|
||||
textColor,
|
||||
rotateStyle,
|
||||
imageBase64,
|
||||
watermarkUrl,
|
||||
onlyPrint,
|
||||
} = useWaterMark()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-if="enableWatermark"
|
||||
class="watermark-wrapper"
|
||||
|
||||
:class="{ full: isFullPage, print: onlyPrint }"
|
||||
:style="{ backgroundImage: `url(${watermarkUrl})` }"
|
||||
>
|
||||
<div ref="svgElRef" class="watermark">
|
||||
<svg
|
||||
:viewBox="`0 0 ${svgRect.svgWidth} ${svgRect.svgHeight}`"
|
||||
:width="svgRect.svgWidth"
|
||||
:height="svgRect.svgHeight"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
:style="{
|
||||
padding: `0 ${svgRect.gapX}px ${svgRect.gapY}px 0`,
|
||||
opacity: svgRect.opacity,
|
||||
}"
|
||||
>
|
||||
<image
|
||||
v-if="imageUrl"
|
||||
:href="imageBase64"
|
||||
:xlink:href="imageBase64"
|
||||
x="0"
|
||||
y="0"
|
||||
:width="svgRect.width"
|
||||
:height="svgRect.height"
|
||||
:style="rotateStyle"
|
||||
/>
|
||||
<foreignObject
|
||||
v-else
|
||||
x="0"
|
||||
y="0"
|
||||
:width="svgRect.width"
|
||||
:height="svgRect.height"
|
||||
>
|
||||
<div
|
||||
xmlns="http://www.w3.org/1999/xhtml"
|
||||
:style="rotateStyle"
|
||||
>
|
||||
<p class="watermark-content" :style="{ color: textColor }" v-html="content" />
|
||||
</div>
|
||||
</foreignObject>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.watermark-wrapper {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 19;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
background-color: transparent;
|
||||
background-repeat: repeat;
|
||||
}
|
||||
|
||||
.watermark-wrapper .watermark {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.watermark-wrapper.full {
|
||||
position: fixed;
|
||||
top: var(--vp-nav-height);
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.watermark-wrapper .watermark-content {
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.watermark-wrapper.print {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media print {
|
||||
.watermark-wrapper.print {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
191
theme/src/client/composables/waterMark.ts
Normal file
191
theme/src/client/composables/waterMark.ts
Normal file
@ -0,0 +1,191 @@
|
||||
import { computed, nextTick, onMounted, onUnmounted, ref, watch, watchEffect } from 'vue'
|
||||
import { isLinkHttp } from 'vuepress/shared'
|
||||
import { usePageData, usePageFrontmatter, useRoute, useSiteLocaleData, withBase } from 'vuepress/client'
|
||||
import type { PlumeThemePageData, PlumeThemePageFrontmatter, WatermarkOptions } from '../../shared/index.js'
|
||||
import { toArray } from '../utils/base.js'
|
||||
import { useDarkMode } from './darkMode.js'
|
||||
import { useThemeLocaleData } from './themeData.js'
|
||||
|
||||
const defaultWatermarkOptions: WatermarkOptions = {
|
||||
global: true,
|
||||
matches: [],
|
||||
width: 150,
|
||||
height: 100,
|
||||
rotate: -22,
|
||||
fullPage: true,
|
||||
gapX: 20,
|
||||
gapY: 20,
|
||||
opacity: 0.1,
|
||||
onlyPrint: false,
|
||||
}
|
||||
|
||||
export function useWaterMark() {
|
||||
const isDark = useDarkMode()
|
||||
const site = useSiteLocaleData()
|
||||
const theme = useThemeLocaleData()
|
||||
const page = usePageData<PlumeThemePageData>()
|
||||
const frontmatter = usePageFrontmatter<PlumeThemePageFrontmatter>()
|
||||
const route = useRoute()
|
||||
|
||||
const watermark = computed<WatermarkOptions>(() => {
|
||||
if (!theme.value.watermark)
|
||||
return {}
|
||||
|
||||
const pageWatermark = typeof frontmatter.value.watermark === 'object' ? frontmatter.value.watermark : {}
|
||||
const content = site.value.title || theme.value.avatar?.name
|
||||
|
||||
return {
|
||||
content,
|
||||
...defaultWatermarkOptions,
|
||||
...theme.value.watermark === true ? {} : theme.value.watermark,
|
||||
...pageWatermark,
|
||||
}
|
||||
})
|
||||
|
||||
const enableWatermark = computed(() => {
|
||||
if (!theme.value.watermark)
|
||||
return false
|
||||
|
||||
const pageWatermark = frontmatter.value.watermark
|
||||
if (watermark.value.global)
|
||||
return pageWatermark !== false
|
||||
|
||||
if (pageWatermark)
|
||||
return true
|
||||
|
||||
const matches = toArray(watermark.value.matches!)
|
||||
return matches.some(toMatch)
|
||||
})
|
||||
|
||||
function toMatch(match: string) {
|
||||
const relativePath = page.value.filePathRelative || ''
|
||||
if (match[0] === '^') {
|
||||
const regex = new RegExp(match)
|
||||
return regex.test(route.path) || (relativePath && regex.test(relativePath))
|
||||
}
|
||||
if (match.endsWith('.md'))
|
||||
return !!relativePath && relativePath.endsWith(match)
|
||||
|
||||
return route.path.startsWith(match) || relativePath.startsWith(match)
|
||||
}
|
||||
|
||||
const isFullPage = computed(() => !!watermark.value.fullPage)
|
||||
const onlyPrint = computed(() => !!watermark.value.onlyPrint)
|
||||
|
||||
const svgRect = computed(() => ({
|
||||
width: watermark.value.width!,
|
||||
height: watermark.value.height!,
|
||||
gapX: watermark.value.gapX!,
|
||||
gapY: watermark.value.gapY!,
|
||||
svgWidth: watermark.value.width! + watermark.value.gapX!,
|
||||
svgHeight: watermark.value.height! + watermark.value.gapY!,
|
||||
opacity: watermark.value.opacity,
|
||||
}))
|
||||
|
||||
const rotateStyle = computed(() => ({
|
||||
transformOrigin: 'center',
|
||||
transform: `rotate(${watermark.value.rotate}deg)`,
|
||||
}))
|
||||
|
||||
const imageUrl = computed(() => {
|
||||
if (!enableWatermark)
|
||||
return ''
|
||||
const image = watermark.value.image || ''
|
||||
const source = typeof image === 'string' ? image : image[isDark.value ? 'dark' : 'light']
|
||||
return !source ? '' : isLinkHttp(source) ? source : withBase(source)
|
||||
})
|
||||
|
||||
const svgElRef = ref<HTMLDivElement>()
|
||||
const watermarkUrl = ref('')
|
||||
const imageBase64 = ref('')
|
||||
const defaultTextColor = ref('')
|
||||
|
||||
const content = computed(() => watermark.value.content)
|
||||
const textColor = computed(() => {
|
||||
if (!enableWatermark)
|
||||
return ''
|
||||
const textColor = watermark.value.textColor || defaultTextColor.value
|
||||
return typeof textColor === 'string' ? textColor : textColor[isDark.value ? 'dark' : 'light']
|
||||
})
|
||||
|
||||
const makeImageToBase64 = (url: string) => {
|
||||
const canvas = document.createElement('canvas')
|
||||
const image = new Image()
|
||||
image.crossOrigin = 'anonymous'
|
||||
image.referrerPolicy = 'no-referrer'
|
||||
image.onload = () => {
|
||||
canvas.width = image.naturalWidth
|
||||
canvas.height = image.naturalHeight
|
||||
const ctx = canvas.getContext('2d')
|
||||
ctx?.drawImage(image, 0, 0)
|
||||
imageBase64.value = canvas.toDataURL()
|
||||
}
|
||||
image.src = url
|
||||
}
|
||||
|
||||
const makeSvgToBlobUrl = (svgStr: string) => {
|
||||
// svg MIME type: https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
|
||||
const svgBlob = new Blob([svgStr], {
|
||||
type: 'image/svg+xml',
|
||||
})
|
||||
return URL.createObjectURL(svgBlob)
|
||||
}
|
||||
|
||||
const getDefaultTextColor = () => {
|
||||
const color = typeof document !== 'undefined' && typeof window !== 'undefined'
|
||||
? window.getComputedStyle(document.documentElement).getPropertyValue('--vp-c-text-1')
|
||||
: ''
|
||||
defaultTextColor.value = color
|
||||
}
|
||||
|
||||
watch(() => isDark.value, () => nextTick(getDefaultTextColor))
|
||||
|
||||
onMounted(getDefaultTextColor)
|
||||
|
||||
watchEffect(() => {
|
||||
if (imageUrl.value && enableWatermark.value)
|
||||
makeImageToBase64(imageUrl.value)
|
||||
})
|
||||
|
||||
watch(
|
||||
() => [
|
||||
watermark.value,
|
||||
imageBase64.value,
|
||||
enableWatermark.value,
|
||||
textColor.value,
|
||||
],
|
||||
() => {
|
||||
if (!enableWatermark.value)
|
||||
return
|
||||
|
||||
nextTick(() => {
|
||||
if (svgElRef.value) {
|
||||
if (watermarkUrl.value)
|
||||
URL.revokeObjectURL(watermarkUrl.value)
|
||||
|
||||
watermarkUrl.value = makeSvgToBlobUrl(svgElRef.value.innerHTML)
|
||||
}
|
||||
})
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
onUnmounted(() => {
|
||||
if (watermarkUrl.value)
|
||||
URL.revokeObjectURL(watermarkUrl.value)
|
||||
})
|
||||
|
||||
return {
|
||||
enableWatermark,
|
||||
isFullPage,
|
||||
imageUrl,
|
||||
content,
|
||||
textColor,
|
||||
svgElRef,
|
||||
svgRect,
|
||||
rotateStyle,
|
||||
imageBase64,
|
||||
watermarkUrl,
|
||||
onlyPrint,
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
import type { NavItemWithLink, PlumeThemeImage } from '.'
|
||||
import type { NavItemWithLink, PlumeThemeImage, WatermarkOptions } from '.'
|
||||
|
||||
/* =============================== Home begin ==================================== */
|
||||
export interface PlumeThemeHomeFrontmatter extends Omit<PlumeThemeHomeBanner, 'type'> {
|
||||
@ -110,6 +110,7 @@ export interface PlumeThemePageFrontmatter {
|
||||
backToTop?: boolean
|
||||
externalLink?: boolean
|
||||
readingTime?: boolean
|
||||
watermark?: boolean | Omit<WatermarkOptions, 'global' | 'matches'>
|
||||
}
|
||||
|
||||
export interface PlumeThemePostFrontmatter extends PlumeThemePageFrontmatter {
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import type { LocaleData } from 'vuepress/core'
|
||||
import type { NotesDataOptions } from '@vuepress-plume/plugin-notes-data'
|
||||
import type { PlumeThemeImage } from '../base.js'
|
||||
import type { NavItem } from './navbar.js'
|
||||
|
||||
export interface PlumeThemeAvatar {
|
||||
@ -170,6 +171,73 @@ export interface LastUpdatedOptions {
|
||||
formatOptions?: Intl.DateTimeFormatOptions & { forceLocale?: boolean }
|
||||
}
|
||||
|
||||
export interface WatermarkOptions {
|
||||
/**
|
||||
* 是否全局启用, 在不全局启用时,可以通过 `frontmatter.watermark` 为当前页面启用
|
||||
* @default false
|
||||
*/
|
||||
global?: boolean
|
||||
/**
|
||||
* 非全局启用时,可以通过该字段根据文件路径或页面链接 进行匹配, 来启用水印。
|
||||
* 以 `^` 的将被认为为类似于正则表达式进行匹配。
|
||||
*/
|
||||
matches?: string | string[]
|
||||
/**
|
||||
* 水印之间的水平间隔
|
||||
* @default 0
|
||||
*/
|
||||
gapX?: number
|
||||
/**
|
||||
* 水印之间的垂直间隔
|
||||
* @default 0
|
||||
*/
|
||||
gapY?: number
|
||||
|
||||
/**
|
||||
* 图片水印的内容,如果与 content 同时传入,优先使用图片水印
|
||||
*/
|
||||
image?: PlumeThemeImage
|
||||
/**
|
||||
* 水印宽度
|
||||
* @default 100
|
||||
*/
|
||||
width?: number
|
||||
/**
|
||||
* 水印高度
|
||||
* @default 100
|
||||
*/
|
||||
height?: number
|
||||
/**
|
||||
* 水印旋转角度
|
||||
* @default -22
|
||||
*/
|
||||
rotate?: number
|
||||
/**
|
||||
* 水印的内容,如果与 image 同时传入,优先使用图片水印
|
||||
*/
|
||||
content?: string
|
||||
/**
|
||||
* 水印是否全屏显示
|
||||
*/
|
||||
fullPage?: boolean
|
||||
/**
|
||||
* 水印透明度
|
||||
* @default 0.1
|
||||
*/
|
||||
opacity?: number
|
||||
|
||||
/**
|
||||
* 水印的文本颜色
|
||||
*/
|
||||
textColor?: string | { dark: string, light: string }
|
||||
|
||||
/**
|
||||
* 是否只在打印时显示
|
||||
* @default false
|
||||
*/
|
||||
onlyPrint?: boolean
|
||||
}
|
||||
|
||||
export interface PlumeThemeLocaleData extends LocaleData {
|
||||
/**
|
||||
* 网站站点首页
|
||||
@ -376,6 +444,11 @@ export interface PlumeThemeLocaleData extends LocaleData {
|
||||
linkLabel?: string
|
||||
linkText?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否开启水印
|
||||
*/
|
||||
watermark?: boolean | WatermarkOptions
|
||||
/**
|
||||
* 加密
|
||||
*/
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user