From bbed3016e29e1b98b4d5fbf31c50e0798b03b815 Mon Sep 17 00:00:00 2001 From: Wesley <985189328@qq.com> Date: Sun, 24 Aug 2025 01:57:50 +0800 Subject: [PATCH 01/16] feat: new utils --- js/watermark/generateWatermark.ts | 141 ++++++++++++++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 js/watermark/generateWatermark.ts diff --git a/js/watermark/generateWatermark.ts b/js/watermark/generateWatermark.ts new file mode 100644 index 0000000000..d8c2f4ef10 --- /dev/null +++ b/js/watermark/generateWatermark.ts @@ -0,0 +1,141 @@ +import { WatermarkText, WatermarkImage } from './type'; + +export default function generateWatermark({ + width, + height, + gapX, + gapY, + offsetLeft, + offsetTop, + rotate, + alpha, + watermarkContent, + lineSpace, + fontColor = 'rgba(0,0,0,0.1)' +}: { + width: number, + height: number, + gapX: number, + gapY: number, + offsetLeft: number, + offsetTop: number, + rotate: number, + alpha: number, + watermarkContent: WatermarkText | WatermarkImage | Array, + lineSpace: number, + fontColor?: string +}, onFinish: (url: string) => void): void { + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + if (!ctx) { + console.warn('当前环境不支持Canvas, 无法绘制水印'); + onFinish(''); + return; + } + + const ratio = window.devicePixelRatio || 1; + const canvasWidth = (gapX + width) * ratio; + const canvasHeight = (gapY + height) * ratio; + + canvas.width = canvasWidth; + canvas.height = canvasHeight; + canvas.style.width = `${gapX + width}px`; + canvas.style.height = `${gapY + height}px`; + + ctx.globalAlpha = alpha; + ctx.fillStyle = 'transparent'; + ctx.fillRect(0, 0, canvasWidth, canvasHeight); + + const contents = Array.isArray(watermarkContent) ? watermarkContent : [watermarkContent]; + + // 绘制两个水印:左上角和右下角 + const positions = [ + { x: offsetLeft, y: offsetTop }, // 左上角 + { x: offsetLeft + gapX, y: offsetTop + gapY } // 右下角 + ]; + + let completedCount = 0; + const totalPositions = positions.length; + + positions.forEach((position, posIndex) => { + ctx.save(); + ctx.translate(position.x * ratio, position.y * ratio); + ctx.rotate((Math.PI / 180) * Number(rotate)); + + let top = 0; + let pendingImages = 0; + + contents.forEach((item: WatermarkText & WatermarkImage) => { + if (item.url) { + const { url, isGrayscale = false } = item; + const currentTop = top; + top += height; + pendingImages++; + + const img = new Image(); + img.crossOrigin = 'anonymous'; + img.referrerPolicy = 'no-referrer'; + img.src = url; + img.onload = () => { + ctx.drawImage(img, 0, currentTop * ratio, width * ratio, height * ratio); + if (isGrayscale) { + const imgData = ctx.getImageData(0, currentTop * ratio, width * ratio, height * ratio); + const pixels = imgData.data; + for (let i = 0; i < pixels.length; i += 4) { + const lightness = (pixels[i] + pixels[i + 1] + pixels[i + 2]) / 3; + pixels[i] = lightness; + pixels[i + 1] = lightness; + pixels[i + 2] = lightness; + } + ctx.putImageData(imgData, 0, currentTop * ratio); + } + + pendingImages--; + if (pendingImages === 0) { + completedCount++; + if (completedCount === totalPositions) { + onFinish(canvas.toDataURL()); + } + } + }; + img.onerror = () => { + pendingImages--; + if (pendingImages === 0) { + completedCount++; + if (completedCount === totalPositions) { + onFinish(canvas.toDataURL()); + } + } + }; + } else if (item.text) { + const { + text, + fontSize = 16, + fontFamily = undefined, + fontWeight = 'normal', + } = item; + const fillStyle = item?.fontColor || fontColor; + const currentTop = top; + top += lineSpace; + + const markSize = Number(fontSize) * ratio; + const markHeight = height * ratio; + ctx.font = `normal normal ${fontWeight} ${markSize}px/${markHeight}px ${fontFamily}`; + ctx.textAlign = 'start'; + ctx.textBaseline = 'top'; + ctx.fillStyle = fillStyle; + ctx.fillText(text, 0, currentTop * ratio); + } + }); + + ctx.restore(); + + // 如果没有图片需要加载,直接完成 + if (pendingImages === 0) { + completedCount++; + if (completedCount === totalPositions) { + onFinish(canvas.toDataURL()); + } + } + }); +} \ No newline at end of file From 3bb58c2f382bb34fe6e1b37fea8d033d2a007010 Mon Sep 17 00:00:00 2001 From: Wesley <985189328@qq.com> Date: Sun, 24 Aug 2025 05:07:53 +0800 Subject: [PATCH 02/16] chore: main fn --- js/watermark/generateWatermark.ts | 308 ++++++++++++++++++------------ js/watermark/type.ts | 2 + 2 files changed, 190 insertions(+), 120 deletions(-) diff --git a/js/watermark/generateWatermark.ts b/js/watermark/generateWatermark.ts index d8c2f4ef10..02ecf0d987 100644 --- a/js/watermark/generateWatermark.ts +++ b/js/watermark/generateWatermark.ts @@ -1,141 +1,209 @@ -import { WatermarkText, WatermarkImage } from './type'; - -export default function generateWatermark({ - width, - height, - gapX, - gapY, - offsetLeft, - offsetTop, - rotate, - alpha, - watermarkContent, - lineSpace, - fontColor = 'rgba(0,0,0,0.1)' -}: { - width: number, - height: number, - gapX: number, - gapY: number, - offsetLeft: number, - offsetTop: number, +// generateWatermark v2 + +import { WatermarkText, WatermarkImage, WatermarkLayout } from "./type"; + +const ratio = window.devicePixelRatio || 1; + +// 元素中心为旋转点执行旋转 +const drawRotate = ( + ctx: CanvasRenderingContext2D, + x: number, + y: number, rotate: number, - alpha: number, - watermarkContent: WatermarkText | WatermarkImage | Array, - lineSpace: number, - fontColor?: string -}, onFinish: (url: string) => void): void { - const canvas = document.createElement('canvas'); - const ctx = canvas.getContext('2d'); +) => { + ctx.translate(x, y); + ctx.rotate((Math.PI / 180) * Number(rotate)); + ctx.translate(-x, -y); +}; + +// 绘制文字 +const drawText = ( + ctx: CanvasRenderingContext2D, + x: number, + y: number, + markHeight: number, + text: string, + fontWeight: string, + fontSize: number, + fontFamily: string, + fillStyle: string +) => { + ctx.font = `normal normal ${fontWeight} ${ + fontSize * ratio + }px/${markHeight}px ${fontFamily}`; + ctx.fillStyle = fillStyle; + ctx.textAlign = "start"; + ctx.textBaseline = "top"; + + ctx.fillText(text, x, y); +}; + +export default function generateWatermark( + { + width, + height, + gapX, + gapY, + offsetLeft, + offsetTop, + rotate, + alpha, + watermarkContent, + lineSpace, + fontColor = "rgba(0,0,0,0.1)", + layout, + }: { + width: number; + height: number; + gapX: number; + gapY: number; + offsetLeft: number; + offsetTop: number; + rotate: number; + alpha: number; + watermarkContent: + | WatermarkText + | WatermarkImage + | Array; + lineSpace: number; + fontColor?: string; + layout?: WatermarkLayout; + }, + onFinish: (url: string) => void +): string { + const isHexagonal = layout === "hexagonal"; + + const canvas = document.createElement("canvas"); + const ctx = canvas.getContext("2d"); if (!ctx) { - console.warn('当前环境不支持Canvas, 无法绘制水印'); - onFinish(''); + console.warn("当前环境不支持Canvas, 无法绘制水印"); + onFinish(""); return; } - const ratio = window.devicePixelRatio || 1; + const canvasWidth = (gapX + width) * ratio; const canvasHeight = (gapY + height) * ratio; + /** Alternate drawing parameters */ + const alternateDrawX = (gapX + width) * ratio; + const alternateDrawY = (gapY + height) * ratio; + canvas.width = canvasWidth; canvas.height = canvasHeight; + canvas.style.width = `${gapX + width}px`; canvas.style.height = `${gapY + height}px`; + if (isHexagonal) { + canvas.style.width = `${canvasWidth * 2}px`; + canvas.style.height = `${canvasHeight * 2}px`; + canvas.width = canvasWidth * 2; + canvas.height = canvasHeight * 2; + } + + ctx.translate(offsetLeft * ratio, offsetTop * ratio); ctx.globalAlpha = alpha; - ctx.fillStyle = 'transparent'; - ctx.fillRect(0, 0, canvasWidth, canvasHeight); - - const contents = Array.isArray(watermarkContent) ? watermarkContent : [watermarkContent]; - - // 绘制两个水印:左上角和右下角 - const positions = [ - { x: offsetLeft, y: offsetTop }, // 左上角 - { x: offsetLeft + gapX, y: offsetTop + gapY } // 右下角 - ]; - - let completedCount = 0; - const totalPositions = positions.length; - - positions.forEach((position, posIndex) => { - ctx.save(); - ctx.translate(position.x * ratio, position.y * ratio); - ctx.rotate((Math.PI / 180) * Number(rotate)); - - let top = 0; - let pendingImages = 0; - - contents.forEach((item: WatermarkText & WatermarkImage) => { - if (item.url) { - const { url, isGrayscale = false } = item; - const currentTop = top; - top += height; - pendingImages++; - - const img = new Image(); - img.crossOrigin = 'anonymous'; - img.referrerPolicy = 'no-referrer'; - img.src = url; - img.onload = () => { - ctx.drawImage(img, 0, currentTop * ratio, width * ratio, height * ratio); - if (isGrayscale) { - const imgData = ctx.getImageData(0, currentTop * ratio, width * ratio, height * ratio); - const pixels = imgData.data; - for (let i = 0; i < pixels.length; i += 4) { - const lightness = (pixels[i] + pixels[i + 1] + pixels[i + 2]) / 3; - pixels[i] = lightness; - pixels[i + 1] = lightness; - pixels[i + 2] = lightness; - } - ctx.putImageData(imgData, 0, currentTop * ratio); - } - pendingImages--; - if (pendingImages === 0) { - completedCount++; - if (completedCount === totalPositions) { - onFinish(canvas.toDataURL()); - } - } - }; - img.onerror = () => { - pendingImages--; - if (pendingImages === 0) { - completedCount++; - if (completedCount === totalPositions) { - onFinish(canvas.toDataURL()); - } - } - }; - } else if (item.text) { - const { - text, - fontSize = 16, - fontFamily = undefined, - fontWeight = 'normal', - } = item; - const fillStyle = item?.fontColor || fontColor; - const currentTop = top; - top += lineSpace; - - const markSize = Number(fontSize) * ratio; - const markHeight = height * ratio; - ctx.font = `normal normal ${fontWeight} ${markSize}px/${markHeight}px ${fontFamily}`; - ctx.textAlign = 'start'; - ctx.textBaseline = 'top'; - ctx.fillStyle = fillStyle; - ctx.fillText(text, 0, currentTop * ratio); - } - }); + const markWidth = width * ratio; + const markHeight = height * ratio; + + ctx.fillStyle = "transparent"; + ctx.fillRect(0, 0, markWidth, markHeight); + + const alternateRotateX = canvasWidth; + const alternateRotateY = canvasHeight; + + ctx.save(); + drawRotate(ctx, 0, 0, rotate); - ctx.restore(); + const contents = Array.isArray(watermarkContent) + ? watermarkContent + : [{ ...watermarkContent }]; + let top = 0; + contents.forEach((item: WatermarkText & WatermarkImage & { top: number }) => { + if (item.url) { + const { url, isGrayscale = false } = item; + item.top = top; + top += height; + const img = new Image(); + img.crossOrigin = "anonymous"; + img.referrerPolicy = "no-referrer"; + img.src = url; + img.onload = () => { + ctx.drawImage(img, 0, item.top * ratio, width * ratio, height * ratio); + if (isHexagonal) { + ctx.restore(); + drawRotate(ctx, alternateRotateX, alternateRotateY, rotate); + ctx.drawImage( + img, + alternateDrawX, + alternateDrawY, + width * ratio, + height * ratio + ); + } - // 如果没有图片需要加载,直接完成 - if (pendingImages === 0) { - completedCount++; - if (completedCount === totalPositions) { + if (isGrayscale) { + const imgData = ctx.getImageData( + 0, + 0, + ctx.canvas.width, + ctx.canvas.height + ); + const pixels = imgData.data; + for (let i = 0; i < pixels.length; i += 4) { + const lightness = (pixels[i] + pixels[i + 1] + pixels[i + 2]) / 3; + pixels[i] = lightness; + pixels[i + 1] = lightness; + pixels[i + 2] = lightness; + } + ctx.putImageData(imgData, 0, 0); + } onFinish(canvas.toDataURL()); + }; + } else if (item.text) { + const { + text, + fontSize = 16, + fontFamily = "normal", + fontWeight = "normal", + } = item; + const fillStyle = item?.fontColor || fontColor; + + item.top = top; + top += lineSpace; + + drawText( + ctx, + 0, + item.top * ratio + item.top * ((fontSize * ratio + 3) * ratio), + markHeight, + text, + fontWeight, + fontSize, + fontFamily, + fillStyle + ); + + if (isHexagonal) { + ctx.restore(); + drawRotate(ctx, alternateRotateX, alternateRotateY, rotate); + + drawText( + ctx, + alternateDrawX, + alternateDrawY, + markHeight, + text, + fontWeight, + fontSize, + fontFamily, + fillStyle + ); } } }); -} \ No newline at end of file + + onFinish(canvas.toDataURL()); +} diff --git a/js/watermark/type.ts b/js/watermark/type.ts index de4b2aa95f..a75cbd30d6 100644 --- a/js/watermark/type.ts +++ b/js/watermark/type.ts @@ -38,3 +38,5 @@ export interface WatermarkImage { */ url?: string; } + +export type WatermarkLayout = "rectangular" | "hexagonal"; \ No newline at end of file From 8b92d0730d568f3235308501d80daa4e972c7f62 Mon Sep 17 00:00:00 2001 From: Wesley <985189328@qq.com> Date: Wed, 10 Sep 2025 21:52:00 +0800 Subject: [PATCH 03/16] chore: optimize --- js/watermark/generateWatermark.ts | 61 +++++++++++++++++++------------ 1 file changed, 37 insertions(+), 24 deletions(-) diff --git a/js/watermark/generateWatermark.ts b/js/watermark/generateWatermark.ts index 02ecf0d987..f88685c333 100644 --- a/js/watermark/generateWatermark.ts +++ b/js/watermark/generateWatermark.ts @@ -1,6 +1,6 @@ -// generateWatermark v2 +// generateWatermark-v2 支持layout生成不同样式的水印 -import { WatermarkText, WatermarkImage, WatermarkLayout } from "./type"; +import { WatermarkText, WatermarkImage,WatermarkLayout } from "./type"; const ratio = window.devicePixelRatio || 1; @@ -67,31 +67,40 @@ export default function generateWatermark( | Array; lineSpace: number; fontColor?: string; - layout?: WatermarkLayout; + layout: WatermarkLayout; }, - onFinish: (url: string) => void + onFinish: (url: string, backgroundSize?: { width: number }) => void ): string { const isHexagonal = layout === "hexagonal"; const canvas = document.createElement("canvas"); const ctx = canvas.getContext("2d"); + if (!ctx) { console.warn("当前环境不支持Canvas, 无法绘制水印"); onFinish(""); return; } + const ratio = window.devicePixelRatio || 1; + let actualBackgroundSize = { + width: gapX + width, + }; + const canvasWidth = (gapX + width) * ratio; const canvasHeight = (gapY + height) * ratio; - /** Alternate drawing parameters */ - const alternateDrawX = (gapX + width) * ratio; - const alternateDrawY = (gapY + height) * ratio; + const markWidth = width * ratio; + const markHeight = height * ratio; + + const dislocationRotateX = canvasWidth; + const dislocationRotateY = canvasHeight; + const dislocationDrawX = (gapX + width) * ratio; + const dislocationDrawY = (gapY + height) * ratio; canvas.width = canvasWidth; canvas.height = canvasHeight; - canvas.style.width = `${gapX + width}px`; canvas.style.height = `${gapY + height}px`; @@ -100,27 +109,28 @@ export default function generateWatermark( canvas.style.height = `${canvasHeight * 2}px`; canvas.width = canvasWidth * 2; canvas.height = canvasHeight * 2; + + // 两倍宽度+间距 + actualBackgroundSize = { + width: gapX + (width * 2) + width / 2, + }; } ctx.translate(offsetLeft * ratio, offsetTop * ratio); ctx.globalAlpha = alpha; - const markWidth = width * ratio; - const markHeight = height * ratio; - ctx.fillStyle = "transparent"; ctx.fillRect(0, 0, markWidth, markHeight); - const alternateRotateX = canvasWidth; - const alternateRotateY = canvasHeight; - ctx.save(); drawRotate(ctx, 0, 0, rotate); const contents = Array.isArray(watermarkContent) ? watermarkContent : [{ ...watermarkContent }]; + let top = 0; + contents.forEach((item: WatermarkText & WatermarkImage & { top: number }) => { if (item.url) { const { url, isGrayscale = false } = item; @@ -132,13 +142,15 @@ export default function generateWatermark( img.src = url; img.onload = () => { ctx.drawImage(img, 0, item.top * ratio, width * ratio, height * ratio); + + // 错位水印 if (isHexagonal) { ctx.restore(); - drawRotate(ctx, alternateRotateX, alternateRotateY, rotate); + drawRotate(ctx, dislocationRotateX, dislocationRotateY, rotate); ctx.drawImage( img, - alternateDrawX, - alternateDrawY, + dislocationDrawX, + dislocationDrawY, width * ratio, height * ratio ); @@ -160,7 +172,7 @@ export default function generateWatermark( } ctx.putImageData(imgData, 0, 0); } - onFinish(canvas.toDataURL()); + onFinish(canvas.toDataURL(), actualBackgroundSize); }; } else if (item.text) { const { @@ -186,14 +198,15 @@ export default function generateWatermark( fillStyle ); + // 错位水印 if (isHexagonal) { ctx.restore(); - drawRotate(ctx, alternateRotateX, alternateRotateY, rotate); + drawRotate(ctx, dislocationRotateX, dislocationRotateY, rotate); drawText( ctx, - alternateDrawX, - alternateDrawY, + dislocationDrawX, + dislocationDrawY, markHeight, text, fontWeight, @@ -204,6 +217,6 @@ export default function generateWatermark( } } }); - - onFinish(canvas.toDataURL()); -} + + onFinish(canvas.toDataURL(), actualBackgroundSize); +} \ No newline at end of file From 002381fcc5d667a448fcf0b0b7746ca950c1c01b Mon Sep 17 00:00:00 2001 From: Wesley <985189328@qq.com> Date: Wed, 10 Sep 2025 21:58:57 +0800 Subject: [PATCH 04/16] chore: docs --- docs/web/api/watermark.en-US.md | 4 ++++ docs/web/api/watermark.md | 5 ++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/web/api/watermark.en-US.md b/docs/web/api/watermark.en-US.md index a746466678..e2a9f524eb 100644 --- a/docs/web/api/watermark.en-US.md +++ b/docs/web/api/watermark.en-US.md @@ -29,3 +29,7 @@ spline: data ### Graylevel watermark {{ graylevel }} + +### Different Layout watermark + +{{ layout }} diff --git a/docs/web/api/watermark.md b/docs/web/api/watermark.md index f7eacc7704..1dfbd8828f 100644 --- a/docs/web/api/watermark.md +++ b/docs/web/api/watermark.md @@ -26,7 +26,10 @@ spline: data {{ movingImage }} - ### 图片灰阶水印 {{ graylevel }} + +### 不同布局的水印 + +{{ layout }} From 48d6b039f4e1d9e0b08ee97ccd9169974754b5fd Mon Sep 17 00:00:00 2001 From: Wesley <985189328@qq.com> Date: Wed, 10 Sep 2025 21:59:33 +0800 Subject: [PATCH 05/16] chore: lint --- js/watermark/generateWatermark.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/js/watermark/generateWatermark.ts b/js/watermark/generateWatermark.ts index f88685c333..e457682675 100644 --- a/js/watermark/generateWatermark.ts +++ b/js/watermark/generateWatermark.ts @@ -1,6 +1,6 @@ // generateWatermark-v2 支持layout生成不同样式的水印 -import { WatermarkText, WatermarkImage,WatermarkLayout } from "./type"; +import { WatermarkText, WatermarkImage, WatermarkLayout } from "./type"; const ratio = window.devicePixelRatio || 1; @@ -9,7 +9,7 @@ const drawRotate = ( ctx: CanvasRenderingContext2D, x: number, y: number, - rotate: number, + rotate: number ) => { ctx.translate(x, y); ctx.rotate((Math.PI / 180) * Number(rotate)); @@ -112,7 +112,7 @@ export default function generateWatermark( // 两倍宽度+间距 actualBackgroundSize = { - width: gapX + (width * 2) + width / 2, + width: gapX + width * 2 + width / 2, }; } @@ -219,4 +219,4 @@ export default function generateWatermark( }); onFinish(canvas.toDataURL(), actualBackgroundSize); -} \ No newline at end of file +} From 7bc5d4c1e7399788a58302788bc3a42c97faaf5c Mon Sep 17 00:00:00 2001 From: Wesley <985189328@qq.com> Date: Wed, 10 Sep 2025 22:00:57 +0800 Subject: [PATCH 06/16] chore: lint --- js/watermark/generateWatermark.ts | 3 +++ js/watermark/type.ts | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/js/watermark/generateWatermark.ts b/js/watermark/generateWatermark.ts index e457682675..2d1dd756ac 100644 --- a/js/watermark/generateWatermark.ts +++ b/js/watermark/generateWatermark.ts @@ -77,6 +77,7 @@ export default function generateWatermark( const ctx = canvas.getContext("2d"); if (!ctx) { + // eslint-disable-next-line no-console console.warn("当前环境不支持Canvas, 无法绘制水印"); onFinish(""); return; @@ -134,6 +135,7 @@ export default function generateWatermark( contents.forEach((item: WatermarkText & WatermarkImage & { top: number }) => { if (item.url) { const { url, isGrayscale = false } = item; + // eslint-disable-next-line no-param-reassign item.top = top; top += height; const img = new Image(); @@ -183,6 +185,7 @@ export default function generateWatermark( } = item; const fillStyle = item?.fontColor || fontColor; + // eslint-disable-next-line no-param-reassign item.top = top; top += lineSpace; diff --git a/js/watermark/type.ts b/js/watermark/type.ts index a75cbd30d6..860e33dad4 100644 --- a/js/watermark/type.ts +++ b/js/watermark/type.ts @@ -18,7 +18,7 @@ export interface WatermarkText { * 水印文本文字粗细 * @default normal */ - fontWeight?: 'normal' | 'lighter' | 'bold' | 'bolder'; + fontWeight?: "normal" | "lighter" | "bold" | "bolder"; /** * 水印文本内容 * @default '' @@ -39,4 +39,4 @@ export interface WatermarkImage { url?: string; } -export type WatermarkLayout = "rectangular" | "hexagonal"; \ No newline at end of file +export type WatermarkLayout = "rectangular" | "hexagonal"; From c7d67b1b13d8c0566a6c7a25d81562c965b6e800 Mon Sep 17 00:00:00 2001 From: Wesley <985189328@qq.com> Date: Wed, 10 Sep 2025 22:02:13 +0800 Subject: [PATCH 07/16] chore: merge develop --- docs/mobile/api/drawer.en-US.md | 4 ++ docs/mobile/api/drawer.md | 6 ++- js/date-picker/format.ts | 10 ++++- js/utils/getColorTokenColor.ts | 1 + style/mobile/components/step-item/_index.less | 9 +++- style/mobile/components/textarea/_index.less | 14 +++++- style/mobile/components/textarea/_var.less | 2 +- style/mobile/theme/_dark.less | 1 + style/mobile/theme/_light.less | 1 + style/web/components/image-viewer/_index.less | 43 +++++++++++++++++++ style/web/components/image-viewer/_var.less | 8 ++++ style/web/components/textarea/_index.less | 4 ++ style/web/theme/_dark.less | 1 + style/web/theme/_light.less | 1 + test/unit/date-picker/utils.test.js | 28 +++++++++++- 15 files changed, 126 insertions(+), 7 deletions(-) diff --git a/docs/mobile/api/drawer.en-US.md b/docs/mobile/api/drawer.en-US.md index 89108c8b3c..170f4ac82d 100644 --- a/docs/mobile/api/drawer.en-US.md +++ b/docs/mobile/api/drawer.en-US.md @@ -18,6 +18,10 @@ toc: false {{ title }} +### Drawer With Placement + +{{ placement }} + ### Drawer With Footer {{ footer }} diff --git a/docs/mobile/api/drawer.md b/docs/mobile/api/drawer.md index 3b4c70edec..fa4afc84f4 100644 --- a/docs/mobile/api/drawer.md +++ b/docs/mobile/api/drawer.md @@ -1,6 +1,6 @@ --- title: Drawer 抽屉 -description: 屏幕边缘滑出的浮层面板。 +description: 屏幕边缘滑出的浮层面板。 spline: base isComponent: true toc: false @@ -18,6 +18,10 @@ toc: false {{ title }} +### 抽屉方向 + +{{ placement }} + ### 带底部插槽抽屉 {{ footer }} diff --git a/js/date-picker/format.ts b/js/date-picker/format.ts index e849fc8d63..b9b78428e9 100644 --- a/js/date-picker/format.ts +++ b/js/date-picker/format.ts @@ -2,12 +2,15 @@ import { isString } from 'lodash-es'; import dayjs from 'dayjs'; import isoWeeksInYear from 'dayjs/plugin/isoWeeksInYear'; import isLeapYear from 'dayjs/plugin/isLeapYear'; +import customParseFormat from 'dayjs/plugin/customParseFormat'; + import log from '../log'; type DateValue = string | number | Date; dayjs.extend(isoWeeksInYear); dayjs.extend(isLeapYear); +dayjs.extend(customParseFormat); export const TIME_FORMAT = 'HH:mm:ss'; @@ -210,7 +213,6 @@ export function calcFormatTime(time: string, timeFormat: string) { } return time; } - // TODO 细化 value 类型 // 格式化时间 export function formatTime(value: any, format: string, timeFormat: string, defaultTime: string | string[]) { @@ -220,9 +222,13 @@ export function formatTime(value: any, format: string, timeFormat: string, defau defaultTime = Array.isArray(defaultTime) ? defaultTime : [defaultTime, defaultTime]; result = result.map((v, i) => { // string格式需要用format去解析,其他诸如Date、time-stamp格式则直接dayjs - if (v) return dayjs(v, typeof v === 'string' ? format : undefined).format(timeFormat); + if (v) { + const formattedResult = dayjs(v, typeof v === 'string' ? format : undefined).format(timeFormat); + return !dayjs(formattedResult, timeFormat).isValid() && defaultTime[i] ? defaultTime[i] : formattedResult; + } return calcFormatTime(defaultTime[i], timeFormat); }); + result = result.length ? result : defaultTime.map((t) => calcFormatTime(t, timeFormat)); // value是数组就输出数组,不是数组就输出第一个即可 return Array.isArray(value) ? result : result?.[0]; diff --git a/js/utils/getColorTokenColor.ts b/js/utils/getColorTokenColor.ts index a85bfeea72..04a0c39110 100644 --- a/js/utils/getColorTokenColor.ts +++ b/js/utils/getColorTokenColor.ts @@ -4,6 +4,7 @@ * @returns string */ export const getColorTokenColor = (token: string): string => { + if (typeof window === 'undefined') return ''; const targetElement = document?.documentElement; const styles = getComputedStyle(targetElement); return styles.getPropertyValue(token).trim() ?? ''; diff --git a/style/mobile/components/step-item/_index.less b/style/mobile/components/step-item/_index.less index b397410689..97ece2d6c5 100644 --- a/style/mobile/components/step-item/_index.less +++ b/style/mobile/components/step-item/_index.less @@ -44,6 +44,10 @@ align-items: center; } + &--vertical { + margin-bottom: 8px; + } + &__anchor { display: flex; align-items: center; @@ -128,10 +132,13 @@ display: flex; align-items: center; justify-content: space-between; - margin-bottom: 4px; } } + &__title + &__description:not(:empty) { + margin-top: 4px; + } + &__description { color: @step-item-description-color; line-height: 20px; diff --git a/style/mobile/components/textarea/_index.less b/style/mobile/components/textarea/_index.less index cbb5152579..f6333ea2ea 100644 --- a/style/mobile/components/textarea/_index.less +++ b/style/mobile/components/textarea/_index.less @@ -77,7 +77,19 @@ &--border { border-radius: @textarea-border-radius; - border: @textarea-border-width solid @textarea-border-color; + position: relative; + + &::after { + content: ""; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + border: @textarea-border-width solid @textarea-border-color; + border-radius: inherit; + pointer-events: none; + } } &--disabled { diff --git a/style/mobile/components/textarea/_var.less b/style/mobile/components/textarea/_var.less index f604a9d1ba..1cd810ddb1 100644 --- a/style/mobile/components/textarea/_var.less +++ b/style/mobile/components/textarea/_var.less @@ -25,6 +25,6 @@ // 文本框圆角大小 @textarea-border-radius: var(--td-textarea-border-radius, @radius-default); // 文本框边框颜色 -@textarea-border-color: var(--td-textarea-border-color, rgba(220, 220, 220, 1)); +@textarea-border-color: var(--td-textarea-border-color, @component-border); // 文本框禁用状态时的输入文本颜色 @textarea-disabled-text-color: var(--td-textarea-disabled-text-color, @text-color-disabled); diff --git a/style/mobile/theme/_dark.less b/style/mobile/theme/_dark.less index 1f68eae24d..aeb306da65 100644 --- a/style/mobile/theme/_dark.less +++ b/style/mobile/theme/_dark.less @@ -124,6 +124,7 @@ --td-text-color-anti: var(--td-font-white-1); // 色彩-文字-反色 --td-text-color-brand: var(--td-brand-color-8); // 色彩-文字-品牌 --td-text-color-link: var(--td-brand-color-8); // 色彩-文字-链接 + --td-text-color-watermark: rgba(255, 255, 255, 10%); // 色彩-文字-水印颜色 // 分割线 --td-border-level-1-color: var(--td-gray-color-11); diff --git a/style/mobile/theme/_light.less b/style/mobile/theme/_light.less index a45c0c8fa4..593505f695 100644 --- a/style/mobile/theme/_light.less +++ b/style/mobile/theme/_light.less @@ -127,6 +127,7 @@ --td-text-color-anti: var(--td-font-white-1); --td-text-color-brand: var(--td-brand-color); --td-text-color-link: var(--td-brand-color); + --td-text-color-watermark: rgba(0, 0, 0, 10%); // 色彩-文字-水印颜色 // 分割线 --td-border-level-1-color: var(--td-gray-color-3); diff --git a/style/web/components/image-viewer/_index.less b/style/web/components/image-viewer/_index.less index 30e04e33ca..c2a3883a02 100644 --- a/style/web/components/image-viewer/_index.less +++ b/style/web/components/image-viewer/_index.less @@ -493,3 +493,46 @@ } } } + +// 默认trigger样式 +.@{prefix}-image-viewer__trigger { + width: @image-viewer-trigger-size; + height: @image-viewer-trigger-size; + display: inline-flex; + position: relative; + justify-content: center; + align-items: center; + overflow: hidden; + + &-hover { + width: @image-viewer-trigger-size; + height: @image-viewer-trigger-size; + display: flex; + justify-content: center; + align-items: center; + position: absolute; + left: 0; + top: 0; + opacity: 0; + background-color: @image-viewer-trigger-hover-bg-color; + color: @image-viewer-trigger-text-color; + transition: @image-viewer-trigger-transition-duration; + cursor: pointer; + + &:hover { + opacity: 1; + cursor: pointer; + } + } + + &-img { + width: @image-viewer-trigger-size; + height: @image-viewer-trigger-size; + cursor: pointer; + position: absolute; + } + + &-icon { + margin-right: 4px; + } +} diff --git a/style/web/components/image-viewer/_var.less b/style/web/components/image-viewer/_var.less index 47ccf7ade2..14ce9ec686 100644 --- a/style/web/components/image-viewer/_var.less +++ b/style/web/components/image-viewer/_var.less @@ -37,3 +37,11 @@ @image-viewer-header-box-width: calc(40px / 9 * 16); @image-viewer-header-box-width-margin-left: calc(40px / 9 * 16 * 3 + 4px * 3); @image-viewer-header-all-box-width: calc(40px / 9 * 16 * 7 + 4px * 6); + +// 默认trigger相关变量 +@image-viewer-trigger-size: 100%; +@image-viewer-trigger-border-radius: @border-radius-medium; +@image-viewer-trigger-border-width: 4px; +@image-viewer-trigger-hover-bg-color: @mask-active; +@image-viewer-trigger-text-color: @text-color-anti; +@image-viewer-trigger-transition-duration: @anim-duration-base; diff --git a/style/web/components/textarea/_index.less b/style/web/components/textarea/_index.less index adf3762594..b789657ee2 100644 --- a/style/web/components/textarea/_index.less +++ b/style/web/components/textarea/_index.less @@ -83,6 +83,10 @@ .@{prefix}-resize-none { resize: none; } + + .@{prefix}-hide-scrollbar { + .hideScrollbar(); + } } .@{prefix}-textarea__tips { diff --git a/style/web/theme/_dark.less b/style/web/theme/_dark.less index 01b9d97769..621b147dd9 100644 --- a/style/web/theme/_dark.less +++ b/style/web/theme/_dark.less @@ -135,6 +135,7 @@ --td-text-color-anti: #fff; // 色彩-文字-反色 --td-text-color-brand: var(--td-brand-color-8); // 色彩-文字-品牌 --td-text-color-link: var(--td-brand-color-8); // 色彩-文字-链接 + --td-text-color-watermark: rgba(255, 255, 255, 10%); // 色彩-文字-水印颜色 // 分割线 --td-border-level-1-color: var(--td-gray-color-11); diff --git a/style/web/theme/_light.less b/style/web/theme/_light.less index dcd8809dd1..4c24498e16 100644 --- a/style/web/theme/_light.less +++ b/style/web/theme/_light.less @@ -136,6 +136,7 @@ --td-text-color-anti: #fff; // 色彩-文字-反色 --td-text-color-brand: var(--td-brand-color-7); // 色彩-文字-品牌 --td-text-color-link: var(--td-brand-color-8); // 色彩-文字-链接 + --td-text-color-watermark: rgba(0, 0, 0, 10%);// 色彩-文字-水印颜色 // 分割线 --td-border-level-1-color: var(--td-gray-color-3); diff --git a/test/unit/date-picker/utils.test.js b/test/unit/date-picker/utils.test.js index 64d653727c..6614d74cab 100644 --- a/test/unit/date-picker/utils.test.js +++ b/test/unit/date-picker/utils.test.js @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest'; -import { extractTimeFormat } from '../../../js/date-picker/format'; +import { extractTimeFormat, formatTime } from '../../../js/date-picker/format'; describe('utils', () => { describe(' extractTimeFormat', () => { @@ -18,4 +18,30 @@ describe('utils', () => { expect(res).toBe('HH时mm分ss秒SSS毫秒'); }); }); + describe('formatTime', () => { + it('valid date time value, return time value of datetime', () => { + const res = formatTime('2025-08-26 10:24:24', 'YYYY-MM-DD HH:mm:ss', 'HH:mm:ss'); + expect(res).toBe('10:24:24'); + }); + + it('valid date time value, format and defaultTime, return time value of datetime', () => { + const res = formatTime('2025-08-26 10:24:24', 'YYYY-MM-DD HH:mm:ss', 'HH:mm:ss', '00:00:00'); + expect(res).toBe('10:24:24'); + }); + + it('valid array type date time value and format, return time value of datetime', () => { + const res = formatTime(['2025-08-26 10:24:24', '2025-08-26 10:24:24'], 'YYYY-MM-DD HH:mm:ss', 'HH:mm:ss', ['00:00:00', '23:59:59']); + expect(res).toEqual(['10:24:24', '10:24:24']); + }); + + it('invalid date time value and defaultTime, return defaultTime', () => { + const res = formatTime('2025-08-26', 'YYYY-MM-DD HH:mm:ss', 'HH:mm:ss', '00:00:00'); + expect(res).toBe('00:00:00'); + }); + + it('invalid array type date time value, return time value of datetime', () => { + const res = formatTime(['2025-08-26', '2025-08-26'], 'YYYY-MM-DD HH:mm:ss', 'HH:mm:ss', ['00:00:00', '23:59:59']); + expect(res).toEqual(['00:00:00', '23:59:59']); + }); + }); }); From d81409b9ce4d01572b83cc446fa001de206714eb Mon Sep 17 00:00:00 2001 From: Wesley <985189328@qq.com> Date: Wed, 10 Sep 2025 22:03:32 +0800 Subject: [PATCH 08/16] chore: lint --- js/watermark/generateWatermark.ts | 28 ++++++++++++++-------------- js/watermark/type.ts | 4 ++-- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/js/watermark/generateWatermark.ts b/js/watermark/generateWatermark.ts index 2d1dd756ac..c5572fe3f2 100644 --- a/js/watermark/generateWatermark.ts +++ b/js/watermark/generateWatermark.ts @@ -1,6 +1,6 @@ // generateWatermark-v2 支持layout生成不同样式的水印 -import { WatermarkText, WatermarkImage, WatermarkLayout } from "./type"; +import { WatermarkText, WatermarkImage, WatermarkLayout } from './type'; const ratio = window.devicePixelRatio || 1; @@ -32,8 +32,8 @@ const drawText = ( fontSize * ratio }px/${markHeight}px ${fontFamily}`; ctx.fillStyle = fillStyle; - ctx.textAlign = "start"; - ctx.textBaseline = "top"; + ctx.textAlign = 'start'; + ctx.textBaseline = 'top'; ctx.fillText(text, x, y); }; @@ -50,7 +50,7 @@ export default function generateWatermark( alpha, watermarkContent, lineSpace, - fontColor = "rgba(0,0,0,0.1)", + fontColor = 'rgba(0,0,0,0.1)', layout, }: { width: number; @@ -71,15 +71,15 @@ export default function generateWatermark( }, onFinish: (url: string, backgroundSize?: { width: number }) => void ): string { - const isHexagonal = layout === "hexagonal"; + const isHexagonal = layout === 'hexagonal'; - const canvas = document.createElement("canvas"); - const ctx = canvas.getContext("2d"); + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); if (!ctx) { // eslint-disable-next-line no-console - console.warn("当前环境不支持Canvas, 无法绘制水印"); - onFinish(""); + console.warn('当前环境不支持Canvas, 无法绘制水印'); + onFinish(''); return; } @@ -120,7 +120,7 @@ export default function generateWatermark( ctx.translate(offsetLeft * ratio, offsetTop * ratio); ctx.globalAlpha = alpha; - ctx.fillStyle = "transparent"; + ctx.fillStyle = 'transparent'; ctx.fillRect(0, 0, markWidth, markHeight); ctx.save(); @@ -139,8 +139,8 @@ export default function generateWatermark( item.top = top; top += height; const img = new Image(); - img.crossOrigin = "anonymous"; - img.referrerPolicy = "no-referrer"; + img.crossOrigin = 'anonymous'; + img.referrerPolicy = 'no-referrer'; img.src = url; img.onload = () => { ctx.drawImage(img, 0, item.top * ratio, width * ratio, height * ratio); @@ -180,8 +180,8 @@ export default function generateWatermark( const { text, fontSize = 16, - fontFamily = "normal", - fontWeight = "normal", + fontFamily = 'normal', + fontWeight = 'normal', } = item; const fillStyle = item?.fontColor || fontColor; diff --git a/js/watermark/type.ts b/js/watermark/type.ts index 860e33dad4..417451c357 100644 --- a/js/watermark/type.ts +++ b/js/watermark/type.ts @@ -18,7 +18,7 @@ export interface WatermarkText { * 水印文本文字粗细 * @default normal */ - fontWeight?: "normal" | "lighter" | "bold" | "bolder"; + fontWeight?: 'normal' | 'lighter' | 'bold' | 'bolder'; /** * 水印文本内容 * @default '' @@ -39,4 +39,4 @@ export interface WatermarkImage { url?: string; } -export type WatermarkLayout = "rectangular" | "hexagonal"; +export type WatermarkLayout = 'rectangular' | 'hexagonal'; From 0b0a650c37368d2ea667c705db9f7b6cef1064e0 Mon Sep 17 00:00:00 2001 From: Wesley <985189328@qq.com> Date: Wed, 10 Sep 2025 22:09:46 +0800 Subject: [PATCH 09/16] chore: docs --- docs/web/api/watermark.en-US.md | 1 + docs/web/api/watermark.md | 1 + 2 files changed, 2 insertions(+) diff --git a/docs/web/api/watermark.en-US.md b/docs/web/api/watermark.en-US.md index e2a9f524eb..c1c7afa589 100644 --- a/docs/web/api/watermark.en-US.md +++ b/docs/web/api/watermark.en-US.md @@ -31,5 +31,6 @@ spline: data {{ graylevel }} ### Different Layout watermark +set layout to use different layout. {{ layout }} diff --git a/docs/web/api/watermark.md b/docs/web/api/watermark.md index 1dfbd8828f..2adeee4708 100644 --- a/docs/web/api/watermark.md +++ b/docs/web/api/watermark.md @@ -31,5 +31,6 @@ spline: data {{ graylevel }} ### 不同布局的水印 +通过设置 layout 使用不同的布局。 {{ layout }} From a182e68991d37ba7d3e7c1c2688e2b1135fc9c7a Mon Sep 17 00:00:00 2001 From: Wesley <985189328@qq.com> Date: Thu, 11 Sep 2025 11:56:29 +0800 Subject: [PATCH 10/16] chore: fix multi watermark --- js/watermark/generateWatermark.ts | 109 +++++++++++++++++------------- 1 file changed, 63 insertions(+), 46 deletions(-) diff --git a/js/watermark/generateWatermark.ts b/js/watermark/generateWatermark.ts index c5572fe3f2..7ae53bb3de 100644 --- a/js/watermark/generateWatermark.ts +++ b/js/watermark/generateWatermark.ts @@ -123,40 +123,51 @@ export default function generateWatermark( ctx.fillStyle = 'transparent'; ctx.fillRect(0, 0, markWidth, markHeight); - ctx.save(); - drawRotate(ctx, 0, 0, rotate); - const contents = Array.isArray(watermarkContent) ? watermarkContent : [{ ...watermarkContent }]; let top = 0; + let imageLoadCount = 0; + let totalImages = 0; + // 预处理 contents.forEach((item: WatermarkText & WatermarkImage & { top: number }) => { + // eslint-disable-next-line no-param-reassign + item.top = top; if (item.url) { - const { url, isGrayscale = false } = item; - // eslint-disable-next-line no-param-reassign - item.top = top; top += height; + totalImages += isHexagonal ? 2 : 1; // hexagonal布局需要绘制两次 + } else if (item.text) { + top += lineSpace; + } + }); + + // 绘制水印内容 + const renderWatermarkItem = ( + item: WatermarkText & WatermarkImage & { top: number }, + offsetX: number = 0, + offsetY: number = 0, + rotateX: number = 0, + rotateY: number = 0 + ) => { + if (item.url) { + const { url, isGrayscale = false } = item; const img = new Image(); img.crossOrigin = 'anonymous'; img.referrerPolicy = 'no-referrer'; img.src = url; img.onload = () => { - ctx.drawImage(img, 0, item.top * ratio, width * ratio, height * ratio); - - // 错位水印 - if (isHexagonal) { - ctx.restore(); - drawRotate(ctx, dislocationRotateX, dislocationRotateY, rotate); - ctx.drawImage( - img, - dislocationDrawX, - dislocationDrawY, - width * ratio, - height * ratio - ); - } + ctx.save(); + drawRotate(ctx, rotateX, rotateY, rotate); + ctx.drawImage( + img, + offsetX, + offsetY + item.top * ratio, + width * ratio, + height * ratio + ); + ctx.restore(); if (isGrayscale) { const imgData = ctx.getImageData( @@ -174,7 +185,11 @@ export default function generateWatermark( } ctx.putImageData(imgData, 0, 0); } - onFinish(canvas.toDataURL(), actualBackgroundSize); + + imageLoadCount++; + if (imageLoadCount === totalImages) { + onFinish(canvas.toDataURL(), actualBackgroundSize); + } }; } else if (item.text) { const { @@ -185,14 +200,12 @@ export default function generateWatermark( } = item; const fillStyle = item?.fontColor || fontColor; - // eslint-disable-next-line no-param-reassign - item.top = top; - top += lineSpace; - + ctx.save(); + drawRotate(ctx, rotateX, rotateY, rotate); drawText( ctx, - 0, - item.top * ratio + item.top * ((fontSize * ratio + 3) * ratio), + offsetX, + offsetY + item.top * ratio, markHeight, text, fontWeight, @@ -200,26 +213,30 @@ export default function generateWatermark( fontFamily, fillStyle ); - - // 错位水印 - if (isHexagonal) { - ctx.restore(); - drawRotate(ctx, dislocationRotateX, dislocationRotateY, rotate); - - drawText( - ctx, - dislocationDrawX, - dislocationDrawY, - markHeight, - text, - fontWeight, - fontSize, - fontFamily, - fillStyle - ); - } + ctx.restore(); } + }; + + // 矩形水印 + contents.forEach((item: WatermarkText & WatermarkImage & { top: number }) => { + renderWatermarkItem(item, 0, 0, 0, 0); }); - onFinish(canvas.toDataURL(), actualBackgroundSize); + // 六边形水印 + if (isHexagonal) { + contents.forEach((item: WatermarkText & WatermarkImage & { top: number }) => { + renderWatermarkItem( + item, + dislocationDrawX, + dislocationDrawY, + dislocationRotateX, + dislocationRotateY + ); + }); + } + + // 没有图片 + if (totalImages === 0) { + onFinish(canvas.toDataURL(), actualBackgroundSize); + } } From 975bda2e3d2237b99b823b771a15cb9018c5b95e Mon Sep 17 00:00:00 2001 From: Wesley <985189328@qq.com> Date: Thu, 11 Sep 2025 12:00:59 +0800 Subject: [PATCH 11/16] fix: lint --- js/watermark/generateWatermark.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/watermark/generateWatermark.ts b/js/watermark/generateWatermark.ts index 7ae53bb3de..fd843a891f 100644 --- a/js/watermark/generateWatermark.ts +++ b/js/watermark/generateWatermark.ts @@ -186,7 +186,7 @@ export default function generateWatermark( ctx.putImageData(imgData, 0, 0); } - imageLoadCount++; + imageLoadCount += 1; if (imageLoadCount === totalImages) { onFinish(canvas.toDataURL(), actualBackgroundSize); } From 75c36edd68d53d19a3c5f7f63997783aaa3f56c6 Mon Sep 17 00:00:00 2001 From: Wesley <985189328@qq.com> Date: Thu, 11 Sep 2025 12:22:23 +0800 Subject: [PATCH 12/16] fix: grayscale --- js/watermark/generateWatermark.ts | 43 ++++++++++++++++++++----------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/js/watermark/generateWatermark.ts b/js/watermark/generateWatermark.ts index fd843a891f..154f813896 100644 --- a/js/watermark/generateWatermark.ts +++ b/js/watermark/generateWatermark.ts @@ -160,22 +160,17 @@ export default function generateWatermark( img.onload = () => { ctx.save(); drawRotate(ctx, rotateX, rotateY, rotate); - ctx.drawImage( - img, - offsetX, - offsetY + item.top * ratio, - width * ratio, - height * ratio - ); - ctx.restore(); + // fix: 灰度效果只影响图片,不影响文字 if (isGrayscale) { - const imgData = ctx.getImageData( - 0, - 0, - ctx.canvas.width, - ctx.canvas.height - ); + const tempCanvas = document.createElement('canvas'); + const tempCtx = tempCanvas.getContext('2d'); + tempCanvas.width = width * ratio; + tempCanvas.height = height * ratio; + + tempCtx.drawImage(img, 0, 0, width * ratio, height * ratio); + + const imgData = tempCtx.getImageData(0, 0, width * ratio, height * ratio); const pixels = imgData.data; for (let i = 0; i < pixels.length; i += 4) { const lightness = (pixels[i] + pixels[i + 1] + pixels[i + 2]) / 3; @@ -183,9 +178,27 @@ export default function generateWatermark( pixels[i + 1] = lightness; pixels[i + 2] = lightness; } - ctx.putImageData(imgData, 0, 0); + tempCtx.putImageData(imgData, 0, 0); + + ctx.drawImage( + tempCanvas, + offsetX, + offsetY + item.top * ratio, + width * ratio, + height * ratio + ); + } else { + ctx.drawImage( + img, + offsetX, + offsetY + item.top * ratio, + width * ratio, + height * ratio + ); } + ctx.restore(); + imageLoadCount += 1; if (imageLoadCount === totalImages) { onFinish(canvas.toDataURL(), actualBackgroundSize); From ca63a113fa68115eca0ac804b4b653e04c1df76d Mon Sep 17 00:00:00 2001 From: Wesley <985189328@qq.com> Date: Tue, 23 Sep 2025 01:15:32 +0800 Subject: [PATCH 13/16] chore: cg old version --- js/watermark/generateBase64Url.ts | 215 ++++++++++++++++++++----- js/watermark/generateWatermark.ts | 255 ------------------------------ 2 files changed, 179 insertions(+), 291 deletions(-) delete mode 100644 js/watermark/generateWatermark.ts diff --git a/js/watermark/generateBase64Url.ts b/js/watermark/generateBase64Url.ts index 1f7170986a..dcb6ce97f9 100644 --- a/js/watermark/generateBase64Url.ts +++ b/js/watermark/generateBase64Url.ts @@ -1,4 +1,40 @@ -import { WatermarkText, WatermarkImage } from './type'; +import { WatermarkText, WatermarkImage, WatermarkLayout } from './type'; + +const ratio = window.devicePixelRatio || 1; + +// 元素中心为旋转点执行旋转 +const drawRotate = ( + ctx: CanvasRenderingContext2D, + x: number, + y: number, + rotate: number +) => { + ctx.translate(x, y); + ctx.rotate((Math.PI / 180) * Number(rotate)); + ctx.translate(-x, -y); +}; + +// 绘制文字 +const drawText = ( + ctx: CanvasRenderingContext2D, + x: number, + y: number, + markHeight: number, + text: string, + fontWeight: string, + fontSize: number, + fontFamily: string, + fillStyle: string +) => { + ctx.font = `normal normal ${fontWeight} ${ + fontSize * ratio + }px/${markHeight}px ${fontFamily}`; + ctx.fillStyle = fillStyle; + ctx.textAlign = 'start'; + ctx.textBaseline = 'top'; + + ctx.fillText(text, x, y); +}; export default function generateBase64Url({ width, @@ -11,64 +47,121 @@ export default function generateBase64Url({ alpha, watermarkContent, lineSpace, - fontColor = 'rgba(0,0,0,0.1)' + fontColor = 'rgba(0,0,0,0.1)', + layout, }: { width: number, height: number, - gapX:number, + gapX: number, gapY: number, - offsetLeft:number, - offsetTop:number, - rotate:number, - alpha:number, + offsetLeft: number, + offsetTop: number, + rotate: number, + alpha: number, watermarkContent: WatermarkText | WatermarkImage | Array, - lineSpace:number, - fontColor?:string -}, onFinish: (url: string) => void): string { + lineSpace: number, + fontColor?: string, + layout: WatermarkLayout, +}, onFinish: (url: string, backgroundSize?: { width: number }) => void +): string { + const isHexagonal = layout === 'hexagonal'; + const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); + if (!ctx) { // eslint-disable-next-line no-console console.warn('当前环境不支持Canvas, 无法绘制水印'); onFinish(''); return; } - const ratio = window.devicePixelRatio || 1; + + let actualBackgroundSize = { + width: gapX + width, + }; + const canvasWidth = (gapX + width) * ratio; const canvasHeight = (gapY + height) * ratio; + const markWidth = width * ratio; + const markHeight = height * ratio; + + const dislocationRotateX = canvasWidth; + const dislocationRotateY = canvasHeight; + const dislocationDrawX = (gapX + width) * ratio; + const dislocationDrawY = (gapY + height) * ratio; + canvas.width = canvasWidth; canvas.height = canvasHeight; canvas.style.width = `${gapX + width}px`; canvas.style.height = `${gapY + height}px`; + if (isHexagonal) { + canvas.style.width = `${canvasWidth * 2}px`; + canvas.style.height = `${canvasHeight * 2}px`; + canvas.width = canvasWidth * 2; + canvas.height = canvasHeight * 2; + + // 两倍宽度+间距 + actualBackgroundSize = { + width: gapX + width * 2 + width / 2, + }; + } + ctx.translate(offsetLeft * ratio, offsetTop * ratio); - ctx.rotate((Math.PI / 180) * Number(rotate)); ctx.globalAlpha = alpha; - const markWidth = width * ratio; - const markHeight = height * ratio; - ctx.fillStyle = 'transparent'; ctx.fillRect(0, 0, markWidth, markHeight); - const contents = Array.isArray(watermarkContent) ? watermarkContent : [{ ...watermarkContent }]; + const contents = Array.isArray(watermarkContent) + ? watermarkContent + : [{ ...watermarkContent }]; + let top = 0; + let imageLoadCount = 0; + let totalImages = 0; + + // 预处理 contents.forEach((item: WatermarkText & WatermarkImage & { top: number }) => { + // eslint-disable-next-line no-param-reassign + item.top = top; if (item.url) { - const { url, isGrayscale = false } = item; - // eslint-disable-next-line no-param-reassign - item.top = top; top += height; + totalImages += isHexagonal ? 2 : 1; // hexagonal布局需要绘制两次 + } else if (item.text) { + top += lineSpace; + } + }); + + // 绘制水印内容 + const renderWatermarkItem = ( + item: WatermarkText & WatermarkImage & { top: number }, + offsetX: number = 0, + offsetY: number = 0, + rotateX: number = 0, + rotateY: number = 0 + ) => { + if (item.url) { + const { url, isGrayscale = false } = item; const img = new Image(); img.crossOrigin = 'anonymous'; img.referrerPolicy = 'no-referrer'; img.src = url; img.onload = () => { - // ctx.filter = 'grayscale(1)'; - ctx.drawImage(img, 0, item.top * ratio, width * ratio, height * ratio); + ctx.save(); + drawRotate(ctx, rotateX, rotateY, rotate); + + // fix: 灰度效果只影响图片,不影响文字 if (isGrayscale) { - const imgData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height); + const tempCanvas = document.createElement('canvas'); + const tempCtx = tempCanvas.getContext('2d'); + tempCanvas.width = width * ratio; + tempCanvas.height = height * ratio; + + tempCtx.drawImage(img, 0, 0, width * ratio, height * ratio); + + const imgData = tempCtx.getImageData(0, 0, width * ratio, height * ratio); const pixels = imgData.data; for (let i = 0; i < pixels.length; i += 4) { const lightness = (pixels[i] + pixels[i + 1] + pixels[i + 2]) / 3; @@ -76,29 +169,79 @@ export default function generateBase64Url({ pixels[i + 1] = lightness; pixels[i + 2] = lightness; } - ctx.putImageData(imgData, 0, 0); + tempCtx.putImageData(imgData, 0, 0); + + ctx.drawImage( + tempCanvas, + offsetX, + offsetY + item.top * ratio, + width * ratio, + height * ratio + ); + } else { + ctx.drawImage( + img, + offsetX, + offsetY + item.top * ratio, + width * ratio, + height * ratio + ); + } + + ctx.restore(); + + // 图片加载完成再返回 + imageLoadCount += 1; + if (imageLoadCount === totalImages) { + onFinish(canvas.toDataURL(), actualBackgroundSize); } - onFinish(canvas.toDataURL()); }; } else if (item.text) { const { text, fontSize = 16, - fontFamily = undefined, + fontFamily = 'normal', fontWeight = 'normal', } = item; const fillStyle = item?.fontColor || fontColor; - // eslint-disable-next-line no-param-reassign - item.top = top; - top += lineSpace; - const markSize = Number(fontSize) * ratio; - // TODO 后续完善font 渲染控制 目前font-family 暂时为 undefined - ctx.font = `normal normal ${fontWeight} ${markSize}px/${markHeight}px ${fontFamily}`; - ctx.textAlign = 'start'; - ctx.textBaseline = 'top'; - ctx.fillStyle = fillStyle; - ctx.fillText(text, 0, item.top * ratio); + + ctx.save(); + drawRotate(ctx, rotateX, rotateY, rotate); + drawText( + ctx, + offsetX, + offsetY + item.top * ratio, + markHeight, + text, + fontWeight, + fontSize, + fontFamily, + fillStyle + ); + ctx.restore(); } + }; + + // 矩形水印 + contents.forEach((item: WatermarkText & WatermarkImage & { top: number }) => { + renderWatermarkItem(item, 0, 0, 0, 0); }); - onFinish(canvas.toDataURL()); + + // 六边形水印 + if (isHexagonal) { + contents.forEach((item: WatermarkText & WatermarkImage & { top: number }) => { + renderWatermarkItem( + item, + dislocationDrawX, + dislocationDrawY, + dislocationRotateX, + dislocationRotateY + ); + }); + } + + // 没有图片 + if (totalImages === 0) { + onFinish(canvas.toDataURL(), actualBackgroundSize); + } } diff --git a/js/watermark/generateWatermark.ts b/js/watermark/generateWatermark.ts deleted file mode 100644 index 154f813896..0000000000 --- a/js/watermark/generateWatermark.ts +++ /dev/null @@ -1,255 +0,0 @@ -// generateWatermark-v2 支持layout生成不同样式的水印 - -import { WatermarkText, WatermarkImage, WatermarkLayout } from './type'; - -const ratio = window.devicePixelRatio || 1; - -// 元素中心为旋转点执行旋转 -const drawRotate = ( - ctx: CanvasRenderingContext2D, - x: number, - y: number, - rotate: number -) => { - ctx.translate(x, y); - ctx.rotate((Math.PI / 180) * Number(rotate)); - ctx.translate(-x, -y); -}; - -// 绘制文字 -const drawText = ( - ctx: CanvasRenderingContext2D, - x: number, - y: number, - markHeight: number, - text: string, - fontWeight: string, - fontSize: number, - fontFamily: string, - fillStyle: string -) => { - ctx.font = `normal normal ${fontWeight} ${ - fontSize * ratio - }px/${markHeight}px ${fontFamily}`; - ctx.fillStyle = fillStyle; - ctx.textAlign = 'start'; - ctx.textBaseline = 'top'; - - ctx.fillText(text, x, y); -}; - -export default function generateWatermark( - { - width, - height, - gapX, - gapY, - offsetLeft, - offsetTop, - rotate, - alpha, - watermarkContent, - lineSpace, - fontColor = 'rgba(0,0,0,0.1)', - layout, - }: { - width: number; - height: number; - gapX: number; - gapY: number; - offsetLeft: number; - offsetTop: number; - rotate: number; - alpha: number; - watermarkContent: - | WatermarkText - | WatermarkImage - | Array; - lineSpace: number; - fontColor?: string; - layout: WatermarkLayout; - }, - onFinish: (url: string, backgroundSize?: { width: number }) => void -): string { - const isHexagonal = layout === 'hexagonal'; - - const canvas = document.createElement('canvas'); - const ctx = canvas.getContext('2d'); - - if (!ctx) { - // eslint-disable-next-line no-console - console.warn('当前环境不支持Canvas, 无法绘制水印'); - onFinish(''); - return; - } - - const ratio = window.devicePixelRatio || 1; - - let actualBackgroundSize = { - width: gapX + width, - }; - - const canvasWidth = (gapX + width) * ratio; - const canvasHeight = (gapY + height) * ratio; - - const markWidth = width * ratio; - const markHeight = height * ratio; - - const dislocationRotateX = canvasWidth; - const dislocationRotateY = canvasHeight; - const dislocationDrawX = (gapX + width) * ratio; - const dislocationDrawY = (gapY + height) * ratio; - - canvas.width = canvasWidth; - canvas.height = canvasHeight; - canvas.style.width = `${gapX + width}px`; - canvas.style.height = `${gapY + height}px`; - - if (isHexagonal) { - canvas.style.width = `${canvasWidth * 2}px`; - canvas.style.height = `${canvasHeight * 2}px`; - canvas.width = canvasWidth * 2; - canvas.height = canvasHeight * 2; - - // 两倍宽度+间距 - actualBackgroundSize = { - width: gapX + width * 2 + width / 2, - }; - } - - ctx.translate(offsetLeft * ratio, offsetTop * ratio); - ctx.globalAlpha = alpha; - - ctx.fillStyle = 'transparent'; - ctx.fillRect(0, 0, markWidth, markHeight); - - const contents = Array.isArray(watermarkContent) - ? watermarkContent - : [{ ...watermarkContent }]; - - let top = 0; - let imageLoadCount = 0; - let totalImages = 0; - - // 预处理 - contents.forEach((item: WatermarkText & WatermarkImage & { top: number }) => { - // eslint-disable-next-line no-param-reassign - item.top = top; - if (item.url) { - top += height; - totalImages += isHexagonal ? 2 : 1; // hexagonal布局需要绘制两次 - } else if (item.text) { - top += lineSpace; - } - }); - - // 绘制水印内容 - const renderWatermarkItem = ( - item: WatermarkText & WatermarkImage & { top: number }, - offsetX: number = 0, - offsetY: number = 0, - rotateX: number = 0, - rotateY: number = 0 - ) => { - if (item.url) { - const { url, isGrayscale = false } = item; - const img = new Image(); - img.crossOrigin = 'anonymous'; - img.referrerPolicy = 'no-referrer'; - img.src = url; - img.onload = () => { - ctx.save(); - drawRotate(ctx, rotateX, rotateY, rotate); - - // fix: 灰度效果只影响图片,不影响文字 - if (isGrayscale) { - const tempCanvas = document.createElement('canvas'); - const tempCtx = tempCanvas.getContext('2d'); - tempCanvas.width = width * ratio; - tempCanvas.height = height * ratio; - - tempCtx.drawImage(img, 0, 0, width * ratio, height * ratio); - - const imgData = tempCtx.getImageData(0, 0, width * ratio, height * ratio); - const pixels = imgData.data; - for (let i = 0; i < pixels.length; i += 4) { - const lightness = (pixels[i] + pixels[i + 1] + pixels[i + 2]) / 3; - pixels[i] = lightness; - pixels[i + 1] = lightness; - pixels[i + 2] = lightness; - } - tempCtx.putImageData(imgData, 0, 0); - - ctx.drawImage( - tempCanvas, - offsetX, - offsetY + item.top * ratio, - width * ratio, - height * ratio - ); - } else { - ctx.drawImage( - img, - offsetX, - offsetY + item.top * ratio, - width * ratio, - height * ratio - ); - } - - ctx.restore(); - - imageLoadCount += 1; - if (imageLoadCount === totalImages) { - onFinish(canvas.toDataURL(), actualBackgroundSize); - } - }; - } else if (item.text) { - const { - text, - fontSize = 16, - fontFamily = 'normal', - fontWeight = 'normal', - } = item; - const fillStyle = item?.fontColor || fontColor; - - ctx.save(); - drawRotate(ctx, rotateX, rotateY, rotate); - drawText( - ctx, - offsetX, - offsetY + item.top * ratio, - markHeight, - text, - fontWeight, - fontSize, - fontFamily, - fillStyle - ); - ctx.restore(); - } - }; - - // 矩形水印 - contents.forEach((item: WatermarkText & WatermarkImage & { top: number }) => { - renderWatermarkItem(item, 0, 0, 0, 0); - }); - - // 六边形水印 - if (isHexagonal) { - contents.forEach((item: WatermarkText & WatermarkImage & { top: number }) => { - renderWatermarkItem( - item, - dislocationDrawX, - dislocationDrawY, - dislocationRotateX, - dislocationRotateY - ); - }); - } - - // 没有图片 - if (totalImages === 0) { - onFinish(canvas.toDataURL(), actualBackgroundSize); - } -} From d29ea0a378a71baf870a2dd31b2da49e8dd66c12 Mon Sep 17 00:00:00 2001 From: Wesley <985189328@qq.com> Date: Tue, 23 Sep 2025 01:24:17 +0800 Subject: [PATCH 14/16] fix: lint --- js/watermark/generateBase64Url.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/js/watermark/generateBase64Url.ts b/js/watermark/generateBase64Url.ts index dcb6ce97f9..cef94ea622 100644 --- a/js/watermark/generateBase64Url.ts +++ b/js/watermark/generateBase64Url.ts @@ -62,8 +62,7 @@ export default function generateBase64Url({ lineSpace: number, fontColor?: string, layout: WatermarkLayout, -}, onFinish: (url: string, backgroundSize?: { width: number }) => void -): string { +}, onFinish: (url: string, backgroundSize?: { width: number }) => void): string { const isHexagonal = layout === 'hexagonal'; const canvas = document.createElement('canvas'); From 10d23bfd007415052736b39cea9fdf2dfd2fb4b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?w=C5=AB=20y=C4=81ng?= Date: Tue, 23 Sep 2025 21:26:17 +0800 Subject: [PATCH 15/16] chore: framework compability MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 部分框架未支持layout不影响使用 --- js/watermark/generateBase64Url.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/watermark/generateBase64Url.ts b/js/watermark/generateBase64Url.ts index cef94ea622..5618591030 100644 --- a/js/watermark/generateBase64Url.ts +++ b/js/watermark/generateBase64Url.ts @@ -61,7 +61,7 @@ export default function generateBase64Url({ watermarkContent: WatermarkText | WatermarkImage | Array, lineSpace: number, fontColor?: string, - layout: WatermarkLayout, + layout?: WatermarkLayout, }, onFinish: (url: string, backgroundSize?: { width: number }) => void): string { const isHexagonal = layout === 'hexagonal'; From ec89fab71dfc3e4d3c0d272478f84d4dc48626ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?w=C5=AB=20y=C4=81ng?= Date: Tue, 23 Sep 2025 21:40:10 +0800 Subject: [PATCH 16/16] chore: framework compatibility --- js/watermark/generateBase64Url.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/js/watermark/generateBase64Url.ts b/js/watermark/generateBase64Url.ts index 5618591030..00c1a1e441 100644 --- a/js/watermark/generateBase64Url.ts +++ b/js/watermark/generateBase64Url.ts @@ -148,7 +148,7 @@ export default function generateBase64Url({ img.referrerPolicy = 'no-referrer'; img.src = url; img.onload = () => { - ctx.save(); + ctx.save?.(); drawRotate(ctx, rotateX, rotateY, rotate); // fix: 灰度效果只影响图片,不影响文字 @@ -187,7 +187,7 @@ export default function generateBase64Url({ ); } - ctx.restore(); + ctx.restore?.(); // 图片加载完成再返回 imageLoadCount += 1; @@ -204,7 +204,7 @@ export default function generateBase64Url({ } = item; const fillStyle = item?.fontColor || fontColor; - ctx.save(); + ctx.save?.(); drawRotate(ctx, rotateX, rotateY, rotate); drawText( ctx, @@ -217,7 +217,7 @@ export default function generateBase64Url({ fontFamily, fillStyle ); - ctx.restore(); + ctx.restore?.(); } };