useWatermark.js 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314
  1. import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
  2. import { getWatermarkConfig } from '@/api/config'
  3. import { WHITE_LIST } from '@/config/whitelist.config.js'
  4. import { useAuthStore, useUserStore } from '@/store'
  5. /**
  6. * 水印配置
  7. */
  8. const defaultConfig = {
  9. enable: false,
  10. content: '系统水印',
  11. contentType: 'text', // text: 固定文本, dict: 字典配置
  12. opacity: 0.1,
  13. fontSize: 16,
  14. fontColor: '#cccccc',
  15. rotate: -20,
  16. gapX: 200,
  17. gapY: 200,
  18. offsetX: 0,
  19. offsetY: 0,
  20. zIndex: 1000,
  21. showTimestamp: false,
  22. timestampFormat: 'yyyy-MM-dd HH:mm:ss',
  23. }
  24. /**
  25. * 水印内容解析器映射
  26. * key: 字典code, value: 解析函数,返回显示文本
  27. */
  28. const contentParsers = {
  29. // 姓名+手机号
  30. name_phone: (userStore) => {
  31. const name = userStore.realName || userStore.username || ''
  32. const phone = userStore.phone || ''
  33. return phone ? `${name} ${phone}` : name
  34. },
  35. // 姓名
  36. name: (userStore) => {
  37. return userStore.realName || userStore.username || ''
  38. },
  39. // 用户名
  40. username: (userStore) => {
  41. return userStore.username || ''
  42. },
  43. // 手机号
  44. phone: (userStore) => {
  45. return userStore.phone || ''
  46. },
  47. // 邮箱
  48. email: (userStore) => {
  49. return userStore.email || ''
  50. },
  51. // 用户ID
  52. user_id: (userStore) => {
  53. return String(userStore.userId || '')
  54. },
  55. }
  56. /**
  57. * 格式化时间戳
  58. */
  59. function formatDate(date, format) {
  60. const year = date.getFullYear()
  61. const month = String(date.getMonth() + 1).padStart(2, '0')
  62. const day = String(date.getDate()).padStart(2, '0')
  63. const hours = String(date.getHours()).padStart(2, '0')
  64. const minutes = String(date.getMinutes()).padStart(2, '0')
  65. const seconds = String(date.getSeconds()).padStart(2, '0')
  66. return format
  67. .replace('yyyy', year)
  68. .replace('MM', month)
  69. .replace('dd', day)
  70. .replace('HH', hours)
  71. .replace('mm', minutes)
  72. .replace('ss', seconds)
  73. }
  74. /**
  75. * 创建水印
  76. */
  77. function createWatermarkCanvas(config) {
  78. const canvas = document.createElement('canvas')
  79. const ctx = canvas.getContext('2d')
  80. // 计算文本尺寸
  81. const font = `${config.fontSize}px Arial`
  82. ctx.font = font
  83. const textMetrics = ctx.measureText(config.content)
  84. const textWidth = textMetrics.width
  85. const textHeight = config.fontSize
  86. // 计算旋转后的画布尺寸
  87. const angle = (config.rotate * Math.PI) / 180
  88. const sin = Math.abs(Math.sin(angle))
  89. const cos = Math.abs(Math.cos(angle))
  90. // 画布尺寸 = 文本尺寸 + 间距
  91. const canvasWidth = textWidth * cos + textHeight * sin + config.gapX
  92. const canvasHeight = textWidth * sin + textHeight * cos + config.gapY
  93. canvas.width = canvasWidth
  94. canvas.height = canvasHeight
  95. // 绘制水印文本
  96. ctx.font = font
  97. ctx.fillStyle = config.fontColor
  98. ctx.globalAlpha = config.opacity
  99. ctx.textAlign = 'center'
  100. ctx.textBaseline = 'middle'
  101. // 保存上下文状态
  102. ctx.save()
  103. // 移动到画布中心并旋转
  104. const centerX = (textWidth * cos + textHeight * sin) / 2 + config.offsetX
  105. const centerY = (textWidth * sin + textHeight * cos) / 2 + config.offsetY
  106. ctx.translate(centerX, centerY)
  107. ctx.rotate(angle)
  108. // 绘制文本
  109. let displayContent = config.content
  110. if (config.showTimestamp) {
  111. const timestamp = formatDate(new Date(), config.timestampFormat)
  112. displayContent += `\n${timestamp}`
  113. }
  114. const lines = displayContent.split('\n')
  115. lines.forEach((line, index) => {
  116. const yOffset = (index - (lines.length - 1) / 2) * config.fontSize * 1.2
  117. ctx.fillText(line, 0, yOffset)
  118. })
  119. ctx.restore()
  120. return canvas.toDataURL('image/png')
  121. }
  122. /**
  123. * 获取水印显示内容
  124. */
  125. function getWatermarkDisplayContent(config, userStore) {
  126. // 如果内容类型是字典配置
  127. if (config.contentType === 'dict' || contentParsers[config.content]) {
  128. const parser = contentParsers[config.content]
  129. if (parser && userStore.userInfo) {
  130. return parser(userStore)
  131. }
  132. // 如果没有对应的解析器或用户未登录,返回空字符串
  133. return ''
  134. }
  135. // 固定文本
  136. return config.content || ''
  137. }
  138. /**
  139. * 水印 composable
  140. */
  141. export function useWatermark() {
  142. const userStore = useUserStore()
  143. const authStore = useAuthStore()
  144. const watermarkConfig = ref({ ...defaultConfig })
  145. const watermarkUrl = ref('')
  146. let refreshTimer = null
  147. // 计算实际显示的水印内容
  148. const displayContent = computed(() => {
  149. return getWatermarkDisplayContent(watermarkConfig.value, userStore)
  150. })
  151. /**
  152. * 加载水印配置
  153. */
  154. const loadWatermarkConfig = async () => {
  155. // 检查是否需要加载水印配置
  156. // 1. 必须有 token
  157. const hasToken = authStore.accessToken
  158. if (!hasToken) {
  159. return
  160. }
  161. // 2. 当前路由不在白名单中
  162. const currentPath = window.location.pathname
  163. if (WHITE_LIST.some(path => currentPath.startsWith(path))) {
  164. return
  165. }
  166. try {
  167. const res = await getWatermarkConfig()
  168. if (res.code === 200 && res.data) {
  169. watermarkConfig.value = { ...defaultConfig, ...res.data }
  170. updateWatermark()
  171. }
  172. }
  173. catch (error) {
  174. console.error('加载水印配置失败:', error)
  175. }
  176. }
  177. /**
  178. * 更新水印
  179. */
  180. const updateWatermark = () => {
  181. if (!watermarkConfig.value.enable) {
  182. watermarkUrl.value = ''
  183. return
  184. }
  185. // 如果没有显示内容,不显示水印
  186. const content = displayContent.value
  187. if (!content) {
  188. watermarkUrl.value = ''
  189. return
  190. }
  191. // 创建带实际显示内容的水印配置
  192. const configWithContent = {
  193. ...watermarkConfig.value,
  194. content,
  195. }
  196. const dataUrl = createWatermarkCanvas(configWithContent)
  197. watermarkUrl.value = dataUrl
  198. }
  199. /**
  200. * 启动定时刷新(用于更新时间戳)
  201. */
  202. const startRefreshTimer = () => {
  203. stopRefreshTimer()
  204. if (watermarkConfig.value.enable && watermarkConfig.value.showTimestamp) {
  205. refreshTimer = setInterval(() => {
  206. updateWatermark()
  207. }, 1000) // 每秒刷新一次
  208. }
  209. }
  210. /**
  211. * 停止定时刷新
  212. */
  213. const stopRefreshTimer = () => {
  214. if (refreshTimer) {
  215. clearInterval(refreshTimer)
  216. refreshTimer = null
  217. }
  218. }
  219. /**
  220. * 获取水印样式
  221. */
  222. const getWatermarkStyle = () => {
  223. if (!watermarkConfig.value.enable || !watermarkUrl.value) {
  224. return {}
  225. }
  226. return {
  227. position: 'fixed',
  228. top: 0,
  229. left: 0,
  230. width: '100%',
  231. height: '100%',
  232. pointerEvents: 'none',
  233. zIndex: watermarkConfig.value.zIndex,
  234. backgroundImage: `url(${watermarkUrl.value})`,
  235. backgroundRepeat: 'repeat',
  236. }
  237. }
  238. // 监听配置变化
  239. watch(
  240. () => watermarkConfig.value,
  241. () => {
  242. updateWatermark()
  243. startRefreshTimer()
  244. },
  245. { deep: true },
  246. )
  247. // 监听用户登录状态变化,更新水印内容
  248. watch(
  249. () => userStore.userInfo,
  250. () => {
  251. if (watermarkConfig.value.enable) {
  252. updateWatermark()
  253. }
  254. },
  255. { deep: true },
  256. )
  257. // 组件挂载时加载配置
  258. onMounted(() => {
  259. loadWatermarkConfig()
  260. })
  261. // 组件卸载时清理定时器
  262. onUnmounted(() => {
  263. stopRefreshTimer()
  264. })
  265. return {
  266. watermarkConfig,
  267. watermarkUrl,
  268. displayContent,
  269. loadWatermarkConfig,
  270. updateWatermark,
  271. getWatermarkStyle,
  272. // 注册自定义内容解析器
  273. registerContentParser: (code, parser) => {
  274. contentParsers[code] = parser
  275. },
  276. }
  277. }
  278. export default useWatermark