{
+ @override
+ Widget build(BuildContext context) {
+ final theme = TTheme.of(context);
+ final textColor = widget.textColor ?? theme.textColorPlaceholder;
+
+ return Stack(
+ children: [
+ // 子组件
+ if (widget.child != null) widget.child!,
+
+ // 水印层
+ Positioned.fill(
+ child: IgnorePointer(
+ ignoring: true,
+ child: CustomPaint(
+ painter: _WatermarkPainter(
+ text: widget.text,
+ type: widget.type,
+ layout: widget.layout,
+ textColor: textColor.withOpacity(widget.opacity),
+ textSize: widget.textSize,
+ fontWeight: widget.fontWeight,
+ rotate: widget.rotate,
+ gapX: widget.gapX,
+ gapY: widget.gapY,
+ offsetX: widget.offsetX,
+ offsetY: widget.offsetY,
+ ),
+ ),
+ ),
+ ),
+ ],
+ );
+ }
+}
+
+/// 水印绘制器
+class _WatermarkPainter extends CustomPainter {
+ final String text;
+ final TWatermarkType type;
+ final TWatermarkLayout layout;
+ final Color textColor;
+ final double textSize;
+ final FontWeight fontWeight;
+ final double rotate;
+ final double gapX;
+ final double gapY;
+ final double offsetX;
+ final double offsetY;
+
+ _WatermarkPainter({
+ required this.text,
+ required this.type,
+ required this.layout,
+ required this.textColor,
+ required this.textSize,
+ required this.fontWeight,
+ required this.rotate,
+ required this.gapX,
+ required this.gapY,
+ required this.offsetX,
+ required this.offsetY,
+ });
+
+ @override
+ void paint(Canvas canvas, Size size) {
+ final paint = Paint()
+ ..color = textColor
+ ..isAntiAlias = true;
+
+ // 裁剪到容器边界,确保水印不超出容器
+ canvas.clipRect(Rect.fromLTWH(0, 0, size.width, size.height));
+
+ final textPainter = TextPainter(
+ text: TextSpan(
+ text: text,
+ style: TextStyle(
+ color: textColor,
+ fontSize: textSize,
+ fontWeight: fontWeight,
+ ),
+ ),
+ textDirection: TextDirection.ltr,
+ );
+
+ textPainter.layout();
+
+ final textWidth = textPainter.width;
+ final textHeight = textPainter.height;
+
+ // 根据布局方式绘制水印
+ switch (layout) {
+ case TWatermarkLayout.horizontal:
+ _drawHorizontal(canvas, textPainter, size, textWidth, textHeight);
+ break;
+ case TWatermarkLayout.vertical:
+ _drawVertical(canvas, textPainter, size, textWidth, textHeight);
+ break;
+ case TWatermarkLayout.grid:
+ _drawGrid(canvas, textPainter, size, textWidth, textHeight);
+ break;
+ }
+ }
+
+ /// 水平排列
+ void _drawHorizontal(
+ Canvas canvas,
+ TextPainter textPainter,
+ Size size,
+ double textWidth,
+ double textHeight,
+ ) {
+ final centerY = size.height / 2 + offsetY;
+ var currentX = offsetX;
+
+ // 向左延伸绘制,确保旋转后能覆盖左边缘
+ while (currentX > -textWidth) {
+ currentX -= textWidth + gapX;
+ }
+
+ while (currentX < size.width) {
+ _drawTextWithRotation(
+ canvas,
+ textPainter,
+ Offset(currentX, centerY - textHeight / 2),
+ );
+ currentX += textWidth + gapX;
+ }
+ }
+
+ /// 垂直排列
+ void _drawVertical(
+ Canvas canvas,
+ TextPainter textPainter,
+ Size size,
+ double textWidth,
+ double textHeight,
+ ) {
+ final centerX = size.width / 2 + offsetX;
+ var currentY = offsetY;
+
+ // 向上延伸绘制,确保旋转后能覆盖上边缘
+ while (currentY > -textHeight) {
+ currentY -= textHeight + gapY;
+ }
+
+ while (currentY < size.height) {
+ _drawTextWithRotation(
+ canvas,
+ textPainter,
+ Offset(centerX - textWidth / 2, currentY),
+ );
+ currentY += textHeight + gapY;
+ }
+ }
+
+ /// 网格排列
+ void _drawGrid(
+ Canvas canvas,
+ TextPainter textPainter,
+ Size size,
+ double textWidth,
+ double textHeight,
+ ) {
+ // 从原点开始绘制,配合clipRect确保水印在容器内
+ var startY = offsetY;
+ var startX = offsetX;
+
+ // 绘制网格,向右下扩展到容器边界
+ // 扩展一些以确保旋转后的水印也能覆盖边缘
+ var currentY = startY;
+ while (currentY < size.height + textHeight) {
+ var currentX = startX;
+ while (currentX < size.width + textWidth) {
+ _drawTextWithRotation(
+ canvas,
+ textPainter,
+ Offset(currentX, currentY),
+ );
+ currentX += textWidth + gapX;
+ }
+ currentY += textHeight + gapY;
+ }
+ }
+
+ /// 绘制带旋转的文本
+ void _drawTextWithRotation(
+ Canvas canvas,
+ TextPainter textPainter,
+ Offset position,
+ ) {
+ canvas.save();
+
+ // 移动到文本中心点
+ final center = Offset(
+ position.dx + textPainter.width / 2,
+ position.dy + textPainter.height / 2,
+ );
+
+ canvas.translate(center.dx, center.dy);
+ // 旋转
+ canvas.rotate(rotate * 3.1415926535897932 / 180);
+ canvas.translate(-center.dx, -center.dy);
+
+ // 绘制文本
+ textPainter.paint(canvas, position);
+
+ canvas.restore();
+ }
+
+ @override
+ bool shouldRepaint(covariant _WatermarkPainter oldDelegate) {
+ return oldDelegate.text != text ||
+ oldDelegate.type != type ||
+ oldDelegate.layout != layout ||
+ oldDelegate.textColor != textColor ||
+ oldDelegate.textSize != textSize ||
+ oldDelegate.fontWeight != fontWeight ||
+ oldDelegate.rotate != rotate ||
+ oldDelegate.gapX != gapX ||
+ oldDelegate.gapY != gapY ||
+ oldDelegate.offsetX != offsetX ||
+ oldDelegate.offsetY != offsetY;
+ }
+}
diff --git a/tdesign-component/lib/tdesign_flutter.dart b/tdesign-component/lib/tdesign_flutter.dart
index 675a6500d..cf1cc5bb0 100644
--- a/tdesign-component/lib/tdesign_flutter.dart
+++ b/tdesign-component/lib/tdesign_flutter.dart
@@ -94,6 +94,7 @@ export 'src/components/time_counter/t_time_counter_style.dart';
export 'src/components/toast/t_toast.dart';
export 'src/components/tree/t_tree_select.dart';
export 'src/components/upload/t_upload.dart';
+export 'src/components/watermark/t_watermark.dart';
export 'src/theme/basic.dart';
export 'src/theme/resource_delegate.dart';
export 'src/theme/t_colors.dart';
diff --git a/tdesign-component/test/t_watermark_test.dart b/tdesign-component/test/t_watermark_test.dart
new file mode 100644
index 000000000..87f3ad9b5
--- /dev/null
+++ b/tdesign-component/test/t_watermark_test.dart
@@ -0,0 +1,166 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:tdesign_flutter/tdesign_flutter.dart';
+
+void main() {
+ group('TWatermark', () {
+ testWidgets('水印组件基本渲染测试', (WidgetTester tester) async {
+ await tester.pumpWidget(
+ MaterialApp(
+ home: Scaffold(
+ body: TWatermark(
+ text: '测试水印',
+ ),
+ ),
+ ),
+ );
+
+ expect(find.byType(TWatermark), findsOneWidget);
+ });
+
+ testWidgets('水印组件带子组件测试', (WidgetTester tester) async {
+ await tester.pumpWidget(
+ MaterialApp(
+ home: Scaffold(
+ body: TWatermark(
+ text: '测试水印',
+ child: const Text('子组件内容'),
+ ),
+ ),
+ ),
+ );
+
+ expect(find.byType(TWatermark), findsOneWidget);
+ expect(find.text('子组件内容'), findsOneWidget);
+ });
+
+ testWidgets('水印组件单行类型测试', (WidgetTester tester) async {
+ await tester.pumpWidget(
+ MaterialApp(
+ home: Scaffold(
+ body: TWatermark(
+ text: '单行水印',
+ type: TWatermarkType.singleLine,
+ ),
+ ),
+ ),
+ );
+
+ expect(find.byType(TWatermark), findsOneWidget);
+ });
+
+ testWidgets('水印组件多行类型测试', (WidgetTester tester) async {
+ await tester.pumpWidget(
+ MaterialApp(
+ home: Scaffold(
+ body: TWatermark(
+ text: '多行\n水印',
+ type: TWatermarkType.multiLine,
+ ),
+ ),
+ ),
+ );
+
+ expect(find.byType(TWatermark), findsOneWidget);
+ });
+
+ testWidgets('水印组件水平布局测试', (WidgetTester tester) async {
+ await tester.pumpWidget(
+ MaterialApp(
+ home: Scaffold(
+ body: SizedBox(
+ height: 200,
+ child: TWatermark(
+ text: '水平',
+ layout: TWatermarkLayout.horizontal,
+ ),
+ ),
+ ),
+ ),
+ );
+
+ expect(find.byType(TWatermark), findsOneWidget);
+ });
+
+ testWidgets('水印组件垂直布局测试', (WidgetTester tester) async {
+ await tester.pumpWidget(
+ MaterialApp(
+ home: Scaffold(
+ body: SizedBox(
+ height: 200,
+ child: TWatermark(
+ text: '垂直',
+ layout: TWatermarkLayout.vertical,
+ ),
+ ),
+ ),
+ ),
+ );
+
+ expect(find.byType(TWatermark), findsOneWidget);
+ });
+
+ testWidgets('水印组件网格布局测试', (WidgetTester tester) async {
+ await tester.pumpWidget(
+ MaterialApp(
+ home: Scaffold(
+ body: SizedBox(
+ height: 200,
+ child: TWatermark(
+ text: '网格',
+ layout: TWatermarkLayout.grid,
+ ),
+ ),
+ ),
+ ),
+ );
+
+ expect(find.byType(TWatermark), findsOneWidget);
+ });
+
+ testWidgets('水印组件自定义样式测试', (WidgetTester tester) async {
+ await tester.pumpWidget(
+ MaterialApp(
+ home: Scaffold(
+ body: TWatermark(
+ text: '自定义',
+ textColor: Colors.red,
+ textSize: 20,
+ opacity: 0.5,
+ rotate: -45,
+ gapX: 100,
+ gapY: 80,
+ ),
+ ),
+ ),
+ );
+
+ expect(find.byType(TWatermark), findsOneWidget);
+ });
+
+ testWidgets('水印组件忽略指针测试', (WidgetTester tester) async {
+ var buttonClicked = false;
+
+ await tester.pumpWidget(
+ MaterialApp(
+ home: Scaffold(
+ body: TWatermark(
+ text: '测试',
+ child: ElevatedButton(
+ onPressed: () {
+ buttonClicked = true;
+ },
+ child: const Text('点击我'),
+ ),
+ ),
+ ),
+ ),
+ );
+
+ await tester.tap(find.text('点击我'));
+ await tester.pump();
+
+ expect(buttonClicked, true);
+ });
+ });
+}
diff --git a/tdesign-site/site/site.config.mjs b/tdesign-site/site/site.config.mjs
index 75d15c202..6bb1b3b29 100644
--- a/tdesign-site/site/site.config.mjs
+++ b/tdesign-site/site/site.config.mjs
@@ -426,6 +426,13 @@ export default {
path: '/flutter/components/tag',
component: () => import('@/tag/README.md'),
},
+ {
+ title: 'Watermark 水印',
+ name: 'watermark',
+ meta: { docType: 'data' },
+ path: '/flutter/components/watermark',
+ component: () => import('@/watermark/README.md'),
+ },
],
},
{
diff --git a/tdesign-site/src/watermark/README.md b/tdesign-site/src/watermark/README.md
new file mode 100644
index 000000000..be16828f3
--- /dev/null
+++ b/tdesign-site/src/watermark/README.md
@@ -0,0 +1,390 @@
+---
+title: Watermark 水印
+description:
+spline: base
+isComponent: true
+---
+
+


+## 引入
+
+在tdesign_flutter/tdesign_flutter.dart中有所有组件的路径。
+
+```dart
+import 'package:tdesign_flutter/tdesign_flutter.dart';
+```
+
+## 代码演示
+
+[td_watermark_page.dart](https://github.com/Tencent/tdesign-flutter/blob/main/tdesign-component/example/lib/page/td_watermark_page.dart)
+
+### 1 基础用法
+
+单行文本水印:
+
+
+
+
+ Widget _buildSingleLine(BuildContext context) {
+ return Container(
+ height: 200,
+ decoration: BoxDecoration(
+ color: TTheme.of(context).bgColorContainer,
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: TWatermark(
+ text: 'TDesign Flutter',
+ type: TWatermarkType.singleLine,
+ ),
+ );
+ }
+
+
+
+
+多行文本水印:
+
+
+
+
+ Widget _buildMultiLine(BuildContext context) {
+ return Container(
+ height: 200,
+ decoration: BoxDecoration(
+ color: TTheme.of(context).bgColorContainer,
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: TWatermark(
+ text: 'TDesign\nFlutter',
+ type: TWatermarkType.multiLine,
+ ),
+ );
+ }
+
+
+
+### 1 排列方式
+
+水平排列:
+
+
+
+
+ Widget _buildHorizontalLayout(BuildContext context) {
+ return Container(
+ height: 200,
+ decoration: BoxDecoration(
+ color: TTheme.of(context).bgColorContainer,
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: TWatermark(
+ text: '内部资料',
+ layout: TWatermarkLayout.horizontal,
+ gapX: 150,
+ ),
+ );
+ }
+
+
+
+
+垂直排列:
+
+
+
+
+ Widget _buildVerticalLayout(BuildContext context) {
+ return Container(
+ height: 200,
+ decoration: BoxDecoration(
+ color: TTheme.of(context).bgColorContainer,
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: TWatermark(
+ text: '机密',
+ layout: TWatermarkLayout.vertical,
+ gapY: 80,
+ ),
+ );
+ }
+
+
+
+
+网格排列:
+
+
+
+
+ Widget _buildGridLayout(BuildContext context) {
+ return Container(
+ height: 200,
+ decoration: BoxDecoration(
+ color: TTheme.of(context).bgColorContainer,
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: TWatermark(
+ text: 'TDesign',
+ layout: TWatermarkLayout.grid,
+ gapX: 120,
+ gapY: 80,
+ ),
+ );
+ }
+
+
+
+### 1 自定义样式
+
+自定义颜色和透明度:
+
+
+
+
+ Widget _buildCustomColor(BuildContext context) {
+ return Container(
+ height: 200,
+ decoration: BoxDecoration(
+ color: TTheme.of(context).bgColorContainer,
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: TWatermark(
+ text: '保密文档',
+ textColor: TTheme.of(context).errorNormalColor,
+ opacity: 0.2,
+ ),
+ );
+ }
+
+
+
+
+自定义字体大小:
+
+
+
+
+ Widget _buildCustomSize(BuildContext context) {
+ return Container(
+ height: 200,
+ decoration: BoxDecoration(
+ color: TTheme.of(context).bgColorContainer,
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: TWatermark(
+ text: '大字体水印',
+ textSize: 24,
+ fontWeight: FontWeight.bold,
+ ),
+ );
+ }
+
+
+
+
+自定义旋转角度:
+
+
+
+
+ Widget _buildCustomRotate(BuildContext context) {
+ return Container(
+ height: 200,
+ decoration: BoxDecoration(
+ color: TTheme.of(context).bgColorContainer,
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: TWatermark(
+ text: '旋转45度',
+ rotate: -45,
+ gapX: 150,
+ gapY: 100,
+ ),
+ );
+ }
+
+
+
+
+自定义间距:
+
+
+
+
+ Widget _buildCustomGap(BuildContext context) {
+ return Container(
+ height: 200,
+ decoration: BoxDecoration(
+ color: TTheme.of(context).bgColorContainer,
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: TWatermark(
+ text: '密集',
+ gapX: 60,
+ gapY: 40,
+ textSize: 12,
+ ),
+ );
+ }
+
+
+
+### 1 带内容的水印
+
+图片上的水印:
+
+
+
+
+ Widget _buildImageWatermark(BuildContext context) {
+ return Container(
+ height: 300,
+ decoration: BoxDecoration(
+ color: TTheme.of(context).bgColorContainer,
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: TWatermark(
+ text: '仅供查看',
+ child: Center(
+ child: Container(
+ width: 200,
+ height: 200,
+ decoration: BoxDecoration(
+ color: TTheme.of(context).brandFocusColor,
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: Icon(
+ TIcons.image,
+ size: 80,
+ color: TTheme.of(context).brandNormalColor,
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+
+
+
+
+列表上的水印:
+
+
+
+
+ Widget _buildListWatermark(BuildContext context) {
+ return Container(
+ height: 300,
+ decoration: BoxDecoration(
+ color: TTheme.of(context).bgColorContainer,
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: TWatermark(
+ text: '内部数据',
+ opacity: 0.1,
+ child: ListView.builder(
+ itemCount: 10,
+ itemBuilder: (context, index) {
+ return Container(
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ border: Border(
+ bottom: BorderSide(
+ color: TTheme.of(context).componentStrokeColor,
+ width: 0.5,
+ ),
+ ),
+ ),
+ child: Row(
+ children: [
+ CircleAvatar(
+ backgroundColor: TTheme.of(context).brandLightColor,
+ child: Text('${index + 1}'),
+ ),
+ const SizedBox(width: 12),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ TText(
+ '列表项 ${index + 1}',
+ font: TTheme.of(context).fontBodyMedium,
+ ),
+ const SizedBox(height: 4),
+ TText(
+ '这是第 ${index + 1} 条数据的描述信息',
+ font: TTheme.of(context).fontBodySmall,
+ textColor: TTheme.of(context).textColorSecondary,
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ );
+ },
+ ),
+ ),
+ );
+ }
+
+
+
+
+表单上的水印:
+
+
+
+
+ Widget _buildFormWatermark(BuildContext context) {
+ return Container(
+ height: 350,
+ decoration: BoxDecoration(
+ color: TTheme.of(context).bgColorContainer,
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: TWatermark(
+ text: '草稿',
+ opacity: 0.08,
+ textSize: 48,
+ child: Padding(
+ padding: const EdgeInsets.all(16),
+ child: Column(
+ children: [
+ TInput(
+ leftLabel: '姓名',
+ hintText: '请输入姓名',
+ ),
+ const SizedBox(height: 16),
+ TInput(
+ leftLabel: '邮箱',
+ hintText: '请输入邮箱',
+ ),
+ const SizedBox(height: 16),
+ TInput(
+ leftLabel: '电话',
+ hintText: '请输入电话号码',
+ ),
+ const SizedBox(height: 16),
+ TButton(
+ text: '提交',
+ theme: TButtonTheme.primary,
+ onTap: () {},
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+
+
+
+
+
+## API
+
+暂无对应api
+
+
+
\ No newline at end of file