tango-ui-cw 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.css +1 -1
- package/dist/index.js +10 -10
- package/dist/index.mjs +889 -757
- package/package.json +1 -1
- package/src/component/CSSFab/useTangoStyle.jsx +182 -182
- package/src/component/MaterialButton/{MaterialButton.css → MaterialButton.module.css} +64 -64
- package/src/component/MaterialButton/index.jsx +69 -58
- package/src/component/MaterialInput/{MaterialInput.css → MaterialInput.module.css} +37 -33
- package/src/component/MaterialInput/index.jsx +34 -29
- package/src/component/TButton/TButton.module.css +184 -0
- package/src/component/TButton/index.jsx +81 -74
- package/src/component/TColorPicker/{TColorPicker.css → TColorPicker.module.css} +24 -24
- package/src/component/TColorPicker/index.jsx +107 -106
- package/src/component/TDate/index.jsx +146 -148
- package/src/component/TDatePicker/TDatePicker.module.css +12 -0
- package/src/component/TDatePicker/index.jsx +72 -72
- package/src/component/TDrawer/{TDrawer.css → TDrawer.module.css} +203 -202
- package/src/component/TDrawer/index.jsx +80 -74
- package/src/component/TInput/{TInput.css → TInput.module.css} +67 -80
- package/src/component/TInput/index.jsx +95 -102
- package/src/component/TLayout/TLayout.css +88 -88
- package/src/component/TLayout/index.jsx +77 -77
- package/src/component/TLine/TLine.module.css +52 -0
- package/src/component/TLine/index.jsx +48 -57
- package/src/component/TMark/{TMark.css → TMark.module.css} +6 -6
- package/src/component/TMark/index.jsx +69 -78
- package/src/component/TModal/{TModal.css → TModal.module.css} +109 -108
- package/src/component/TModal/index.jsx +75 -69
- package/src/component/TNotice/{TNotice.css → TNotice.module.css} +50 -52
- package/src/component/TNotice/index.jsx +37 -38
- package/src/component/TNotice/useNotice.jsx +54 -54
- package/src/component/TSearch/{TSearch.css → TSearch.module.css} +80 -90
- package/src/component/TSearch/index.jsx +86 -100
- package/src/component/TSpace/TSpace.module.css +43 -0
- package/src/component/TSpace/index.jsx +60 -60
- package/src/component/TTable/{TTable.css → TTable.module.css} +26 -26
- package/src/component/TTable/index.jsx +73 -77
- package/src/component/TTooltip/TTooltip.module.css +66 -0
- package/src/component/TTooltip/index.jsx +33 -25
- package/src/component/Tango/store.js +105 -105
- package/src/component/Tools/WaterMark/WaterMark.jsx +78 -78
- package/src/component/TButton/TButton.css +0 -270
- package/src/component/TDate/TDate.css +0 -0
- package/src/component/TDatePicker/TDatePicker.css +0 -13
- package/src/component/TLine/TLine.css +0 -54
- package/src/component/TSpace/TSpace.css +0 -43
- package/src/component/TTooltip/TTooltip.css +0 -105
@@ -1,77 +1,73 @@
|
|
1
|
-
import React from "react";
|
2
|
-
import PropTypes from "prop-types";
|
3
|
-
import { useTangoStyle } from "../CSSFab/useTangoStyle";
|
4
|
-
import "./TTable.css";
|
5
|
-
|
6
|
-
export default function Table({
|
7
|
-
dataSource,
|
8
|
-
columns,
|
9
|
-
sx = {},
|
10
|
-
style = {},
|
11
|
-
className = "",
|
12
|
-
}) {
|
13
|
-
|
14
|
-
const combinedStyle = { ...sxStyle, ...style };
|
15
|
-
|
16
|
-
return (
|
17
|
-
<div
|
18
|
-
className={
|
19
|
-
style={{
|
20
|
-
...combinedStyle,
|
21
|
-
borderRadius: combinedStyle.borderRadius || 0,
|
22
|
-
overflow: "hidden",
|
23
|
-
border: "1px solid #ddd",
|
24
|
-
}}
|
25
|
-
>
|
26
|
-
<table className=
|
27
|
-
<thead>
|
28
|
-
<tr>
|
29
|
-
{columns?.map((col) => (
|
30
|
-
<th key={col.key || col.dataIndex}>{col.title}</th>
|
31
|
-
))}
|
32
|
-
</tr>
|
33
|
-
</thead>
|
34
|
-
<tbody>
|
35
|
-
{dataSource.map((row) => (
|
36
|
-
<tr key={row.key}>
|
37
|
-
{columns.map((col) => (
|
38
|
-
<td key={col.key || col.dataIndex}>
|
39
|
-
{col.render
|
40
|
-
? col.render(
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
}
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
style: {},
|
75
|
-
className: "",
|
76
|
-
};
|
77
|
-
|
1
|
+
import React from "react";
|
2
|
+
import PropTypes from "prop-types";
|
3
|
+
import { useTangoStyle } from "../CSSFab/useTangoStyle";
|
4
|
+
import styles from "./TTable.module.css";
|
5
|
+
|
6
|
+
export default function Table({
|
7
|
+
dataSource,
|
8
|
+
columns,
|
9
|
+
sx = {},
|
10
|
+
style = {},
|
11
|
+
className = "",
|
12
|
+
}) {
|
13
|
+
const sxStyle = useTangoStyle(sx);
|
14
|
+
const combinedStyle = { ...sxStyle, ...style };
|
15
|
+
|
16
|
+
return (
|
17
|
+
<div
|
18
|
+
className={`${styles.wrapper} ${className}`}
|
19
|
+
style={{
|
20
|
+
...combinedStyle,
|
21
|
+
borderRadius: combinedStyle.borderRadius || 0,
|
22
|
+
overflow: "hidden",
|
23
|
+
border: "1px solid #ddd",
|
24
|
+
}}
|
25
|
+
>
|
26
|
+
<table className={styles.table}>
|
27
|
+
<thead className={styles.thead}>
|
28
|
+
<tr>
|
29
|
+
{columns?.map((col) => (
|
30
|
+
<th key={col.key || col.dataIndex}>{col.title}</th>
|
31
|
+
))}
|
32
|
+
</tr>
|
33
|
+
</thead>
|
34
|
+
<tbody>
|
35
|
+
{dataSource.map((row) => (
|
36
|
+
<tr key={row.key}>
|
37
|
+
{columns.map((col) => (
|
38
|
+
<td key={col.key || col.dataIndex}>
|
39
|
+
{col.render
|
40
|
+
? col.render(row[col.dataIndex], row, dataSource.indexOf(row))
|
41
|
+
: row[col.dataIndex]}
|
42
|
+
</td>
|
43
|
+
))}
|
44
|
+
</tr>
|
45
|
+
))}
|
46
|
+
</tbody>
|
47
|
+
</table>
|
48
|
+
</div>
|
49
|
+
);
|
50
|
+
};
|
51
|
+
|
52
|
+
TTable.propTypes = {
|
53
|
+
dataSource: PropTypes.array.isRequired,
|
54
|
+
columns: PropTypes.arrayOf(
|
55
|
+
PropTypes.shape({
|
56
|
+
title: PropTypes.string.isRequired,
|
57
|
+
dataIndex: PropTypes.string.isRequired,
|
58
|
+
key: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
59
|
+
render: PropTypes.func,
|
60
|
+
})
|
61
|
+
).isRequired,
|
62
|
+
sx: PropTypes.object,
|
63
|
+
style: PropTypes.object,
|
64
|
+
className: PropTypes.string,
|
65
|
+
};
|
66
|
+
|
67
|
+
TTable.defaultProps = {
|
68
|
+
dataSource: [],
|
69
|
+
sx: {},
|
70
|
+
style: {},
|
71
|
+
className: "",
|
72
|
+
};
|
73
|
+
|
@@ -0,0 +1,66 @@
|
|
1
|
+
.tooltipWrapper {
|
2
|
+
position: relative;
|
3
|
+
display: inline-block;
|
4
|
+
}
|
5
|
+
|
6
|
+
.tooltip {
|
7
|
+
position: absolute;
|
8
|
+
padding: 8px 15px;
|
9
|
+
font-size: 16px;
|
10
|
+
color: white;
|
11
|
+
background-color: black;
|
12
|
+
border-radius: 8px;
|
13
|
+
white-space: nowrap;
|
14
|
+
z-index: 999;
|
15
|
+
box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.1);
|
16
|
+
}
|
17
|
+
|
18
|
+
/* 添加 Tooltip 的倒三角 */
|
19
|
+
.tooltip::after {
|
20
|
+
content: "";
|
21
|
+
position: absolute;
|
22
|
+
border-width: 8px;
|
23
|
+
border-style: solid;
|
24
|
+
}
|
25
|
+
|
26
|
+
/* Placement 样式:top */
|
27
|
+
.tooltipTop .tooltip {
|
28
|
+
bottom: 130%;
|
29
|
+
left: 50%;
|
30
|
+
transform: translateX(-50%);
|
31
|
+
margin-bottom: 8px;
|
32
|
+
}
|
33
|
+
.tooltipTop .tooltip::after {
|
34
|
+
top: 100%;
|
35
|
+
left: 50%;
|
36
|
+
transform: translateX(-50%);
|
37
|
+
border-color: black transparent transparent transparent;
|
38
|
+
}
|
39
|
+
|
40
|
+
/* Placement 样式:bottom */
|
41
|
+
.tooltipBottom .tooltip {
|
42
|
+
top: 130%;
|
43
|
+
left: 50%;
|
44
|
+
transform: translateX(-50%);
|
45
|
+
margin-top: 8px;
|
46
|
+
}
|
47
|
+
.tooltipBottom .tooltip::after {
|
48
|
+
bottom: 100%;
|
49
|
+
left: 50%;
|
50
|
+
transform: translateX(-50%);
|
51
|
+
border-color: transparent transparent black transparent;
|
52
|
+
}
|
53
|
+
|
54
|
+
/* Placement 样式:right */
|
55
|
+
.tooltipRight .tooltip {
|
56
|
+
top: 50%;
|
57
|
+
left: 110%;
|
58
|
+
transform: translateY(-50%);
|
59
|
+
margin-left: 8px;
|
60
|
+
}
|
61
|
+
.tooltipRight .tooltip::after {
|
62
|
+
top: 50%;
|
63
|
+
left: -8px;
|
64
|
+
transform: translateY(-50%);
|
65
|
+
border-color: transparent black transparent transparent;
|
66
|
+
}
|
@@ -1,25 +1,33 @@
|
|
1
|
-
import React from "react";
|
2
|
-
import PropTypes from "prop-types";
|
3
|
-
import "./TTooltip.css";
|
4
|
-
|
5
|
-
export default function Tooltip({ children, tooltipText, placement = "top" }) {
|
6
|
-
|
7
|
-
|
8
|
-
return (
|
9
|
-
<div
|
10
|
-
{
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
}
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
1
|
+
import React, { useState } from "react";
|
2
|
+
import PropTypes from "prop-types";
|
3
|
+
import styles from "./TTooltip.module.css";
|
4
|
+
|
5
|
+
export default function Tooltip({ children, tooltipText, placement = "top" }) {
|
6
|
+
const [hovered, setHovered] = useState(false);
|
7
|
+
|
8
|
+
return (
|
9
|
+
<div
|
10
|
+
className={`${styles.tooltipWrapper} ${styles[`tooltip${placement[0].toUpperCase() + placement.slice(1)}`]}`}
|
11
|
+
onMouseEnter={() => setHovered(true)}
|
12
|
+
onMouseLeave={() => setHovered(false)}
|
13
|
+
>
|
14
|
+
{children}
|
15
|
+
{hovered && (
|
16
|
+
<div className={styles.tooltip}>
|
17
|
+
{tooltipText}
|
18
|
+
</div>
|
19
|
+
)}
|
20
|
+
</div>
|
21
|
+
);
|
22
|
+
};
|
23
|
+
|
24
|
+
TTooltip.propTypes = {
|
25
|
+
children: PropTypes.node.isRequired,
|
26
|
+
tooltipText: PropTypes.string.isRequired,
|
27
|
+
placement: PropTypes.oneOf(["top", "bottom", "right"]),
|
28
|
+
};
|
29
|
+
|
30
|
+
TTooltip.defaultProps = {
|
31
|
+
placement: "top",
|
32
|
+
};
|
33
|
+
|
@@ -1,105 +1,105 @@
|
|
1
|
-
import { useState, useEffect } from 'react';
|
2
|
-
|
3
|
-
class TangoStore extends EventTarget {
|
4
|
-
constructor(initialState, options = {}, persistentFields = []) {
|
5
|
-
super();
|
6
|
-
this.state = { ...initialState };
|
7
|
-
this.storageKey = options.storageKey || 'tango-store-state'; // 存储键名
|
8
|
-
this.Eternity = options.Eternity || false; // 是否启用持久化
|
9
|
-
this.storageType = options.Storage || 'local'; // 默认为 'local', 可以选择 'session'
|
10
|
-
this.persistentFields = new Set(persistentFields); // 将数组转换为 Set,便于查找
|
11
|
-
|
12
|
-
// 根据选择的存储类型来决定使用哪种存储
|
13
|
-
this.storage = this.storageType === 'session' ? sessionStorage : localStorage;
|
14
|
-
|
15
|
-
// 加载本地存储的状态
|
16
|
-
if (this.Eternity) {
|
17
|
-
const storedState = this.storage.getItem(this.storageKey);
|
18
|
-
if (storedState) {
|
19
|
-
try {
|
20
|
-
const parsedState = JSON.parse(storedState);
|
21
|
-
this.state = { ...this.state, ...parsedState };
|
22
|
-
} catch (e) {
|
23
|
-
console.error('Failed to load persisted state:', e);
|
24
|
-
}
|
25
|
-
}
|
26
|
-
}
|
27
|
-
}
|
28
|
-
|
29
|
-
setState(newState) {
|
30
|
-
const hasChanged = Object.keys(newState).some(
|
31
|
-
(key) => this.state[key] !== newState[key]
|
32
|
-
);
|
33
|
-
|
34
|
-
if (!hasChanged) {
|
35
|
-
return; // 状态未改变,不派发事件
|
36
|
-
}
|
37
|
-
|
38
|
-
this.state = { ...this.state, ...newState };
|
39
|
-
|
40
|
-
if (this.Eternity) {
|
41
|
-
// 如果启用了持久化
|
42
|
-
if (this.persistentFields.size > 0) {
|
43
|
-
// 仅持久化指定字段
|
44
|
-
const filteredState = Object.keys(this.state).reduce((acc, key) => {
|
45
|
-
if (this.persistentFields.has(key)) {
|
46
|
-
acc[key] = this.state[key];
|
47
|
-
}
|
48
|
-
return acc;
|
49
|
-
}, {});
|
50
|
-
try {
|
51
|
-
this.storage.setItem(this.storageKey, JSON.stringify(filteredState));
|
52
|
-
} catch (e) {
|
53
|
-
console.error('Failed to persist state:', e);
|
54
|
-
}
|
55
|
-
} else {
|
56
|
-
// 默认持久化所有字段
|
57
|
-
try {
|
58
|
-
this.storage.setItem(this.storageKey, JSON.stringify(this.state));
|
59
|
-
} catch (e) {
|
60
|
-
console.error('Failed to persist state:', e);
|
61
|
-
}
|
62
|
-
}
|
63
|
-
}
|
64
|
-
|
65
|
-
// 通知订阅者状态变化
|
66
|
-
this.dispatchEvent(new CustomEvent('change', { detail: this.state }));
|
67
|
-
}
|
68
|
-
|
69
|
-
getState() {
|
70
|
-
return this.state;
|
71
|
-
}
|
72
|
-
|
73
|
-
subscribe(callback) {
|
74
|
-
const listener = (e) => callback(e.detail);
|
75
|
-
this.addEventListener('change', listener);
|
76
|
-
return () => this.removeEventListener('change', listener); // 取消订阅
|
77
|
-
}
|
78
|
-
}
|
79
|
-
|
80
|
-
// 提供创建 store 的方法
|
81
|
-
export const createTangoStore = (initialState = {}, options = {}, persistentFields = []) => {
|
82
|
-
return new TangoStore(initialState, options, persistentFields);
|
83
|
-
};
|
84
|
-
|
85
|
-
// 提供 `useTango` Hook,供用户直接使用
|
86
|
-
export const useTango = (store, key, defaultValue = undefined) => {
|
87
|
-
// 增强安全机制,确保 key 不存在时返回默认值
|
88
|
-
const [state, setState] = useState(() => {
|
89
|
-
const currentState = store.getState();
|
90
|
-
return currentState.hasOwnProperty(key) ? currentState[key] : defaultValue;
|
91
|
-
});
|
92
|
-
|
93
|
-
useEffect(() => {
|
94
|
-
const unsubscribe = store.subscribe((newState) => {
|
95
|
-
if (newState.hasOwnProperty(key)) {
|
96
|
-
setState(newState[key]);
|
97
|
-
} else {
|
98
|
-
setState(defaultValue); // key 不存在时返回默认值
|
99
|
-
}
|
100
|
-
});
|
101
|
-
return () => unsubscribe();
|
102
|
-
}, [store, key, defaultValue]);
|
103
|
-
|
104
|
-
return state;
|
105
|
-
};
|
1
|
+
import { useState, useEffect } from 'react';
|
2
|
+
|
3
|
+
class TangoStore extends EventTarget {
|
4
|
+
constructor(initialState, options = {}, persistentFields = []) {
|
5
|
+
super();
|
6
|
+
this.state = { ...initialState };
|
7
|
+
this.storageKey = options.storageKey || 'tango-store-state'; // 存储键名
|
8
|
+
this.Eternity = options.Eternity || false; // 是否启用持久化
|
9
|
+
this.storageType = options.Storage || 'local'; // 默认为 'local', 可以选择 'session'
|
10
|
+
this.persistentFields = new Set(persistentFields); // 将数组转换为 Set,便于查找
|
11
|
+
|
12
|
+
// 根据选择的存储类型来决定使用哪种存储
|
13
|
+
this.storage = this.storageType === 'session' ? sessionStorage : localStorage;
|
14
|
+
|
15
|
+
// 加载本地存储的状态
|
16
|
+
if (this.Eternity) {
|
17
|
+
const storedState = this.storage.getItem(this.storageKey);
|
18
|
+
if (storedState) {
|
19
|
+
try {
|
20
|
+
const parsedState = JSON.parse(storedState);
|
21
|
+
this.state = { ...this.state, ...parsedState };
|
22
|
+
} catch (e) {
|
23
|
+
console.error('Failed to load persisted state:', e);
|
24
|
+
}
|
25
|
+
}
|
26
|
+
}
|
27
|
+
}
|
28
|
+
|
29
|
+
setState(newState) {
|
30
|
+
const hasChanged = Object.keys(newState).some(
|
31
|
+
(key) => this.state[key] !== newState[key]
|
32
|
+
);
|
33
|
+
|
34
|
+
if (!hasChanged) {
|
35
|
+
return; // 状态未改变,不派发事件
|
36
|
+
}
|
37
|
+
|
38
|
+
this.state = { ...this.state, ...newState };
|
39
|
+
|
40
|
+
if (this.Eternity) {
|
41
|
+
// 如果启用了持久化
|
42
|
+
if (this.persistentFields.size > 0) {
|
43
|
+
// 仅持久化指定字段
|
44
|
+
const filteredState = Object.keys(this.state).reduce((acc, key) => {
|
45
|
+
if (this.persistentFields.has(key)) {
|
46
|
+
acc[key] = this.state[key];
|
47
|
+
}
|
48
|
+
return acc;
|
49
|
+
}, {});
|
50
|
+
try {
|
51
|
+
this.storage.setItem(this.storageKey, JSON.stringify(filteredState));
|
52
|
+
} catch (e) {
|
53
|
+
console.error('Failed to persist state:', e);
|
54
|
+
}
|
55
|
+
} else {
|
56
|
+
// 默认持久化所有字段
|
57
|
+
try {
|
58
|
+
this.storage.setItem(this.storageKey, JSON.stringify(this.state));
|
59
|
+
} catch (e) {
|
60
|
+
console.error('Failed to persist state:', e);
|
61
|
+
}
|
62
|
+
}
|
63
|
+
}
|
64
|
+
|
65
|
+
// 通知订阅者状态变化
|
66
|
+
this.dispatchEvent(new CustomEvent('change', { detail: this.state }));
|
67
|
+
}
|
68
|
+
|
69
|
+
getState() {
|
70
|
+
return this.state;
|
71
|
+
}
|
72
|
+
|
73
|
+
subscribe(callback) {
|
74
|
+
const listener = (e) => callback(e.detail);
|
75
|
+
this.addEventListener('change', listener);
|
76
|
+
return () => this.removeEventListener('change', listener); // 取消订阅
|
77
|
+
}
|
78
|
+
}
|
79
|
+
|
80
|
+
// 提供创建 store 的方法
|
81
|
+
export const createTangoStore = (initialState = {}, options = {}, persistentFields = []) => {
|
82
|
+
return new TangoStore(initialState, options, persistentFields);
|
83
|
+
};
|
84
|
+
|
85
|
+
// 提供 `useTango` Hook,供用户直接使用
|
86
|
+
export const useTango = (store, key, defaultValue = undefined) => {
|
87
|
+
// 增强安全机制,确保 key 不存在时返回默认值
|
88
|
+
const [state, setState] = useState(() => {
|
89
|
+
const currentState = store.getState();
|
90
|
+
return currentState.hasOwnProperty(key) ? currentState[key] : defaultValue;
|
91
|
+
});
|
92
|
+
|
93
|
+
useEffect(() => {
|
94
|
+
const unsubscribe = store.subscribe((newState) => {
|
95
|
+
if (newState.hasOwnProperty(key)) {
|
96
|
+
setState(newState[key]);
|
97
|
+
} else {
|
98
|
+
setState(defaultValue); // key 不存在时返回默认值
|
99
|
+
}
|
100
|
+
});
|
101
|
+
return () => unsubscribe();
|
102
|
+
}, [store, key, defaultValue]);
|
103
|
+
|
104
|
+
return state;
|
105
|
+
};
|
@@ -1,78 +1,78 @@
|
|
1
|
-
// 加水印组件
|
2
|
-
/**
|
3
|
-
* 为图片添加水印并返回处理后的图片URL
|
4
|
-
* @param {string} imageUrl - 图片的URL
|
5
|
-
* @param {Object} watermarkConfig - 水印配置对象
|
6
|
-
* @param {string} watermarkConfig.text - 水印内容
|
7
|
-
* @param {number} [watermarkConfig.fontSize=30] - 水印文字大小
|
8
|
-
* @param {string} [watermarkConfig.color='rgba(255, 255, 255, 0.5)'] - 水印文字颜色
|
9
|
-
* @param {number} [watermarkConfig.rotate=30] - 水印文字旋转度数
|
10
|
-
* @param {number} [watermarkConfig.spacing=100] - 水印文字间距
|
11
|
-
* @param {number} [watermarkConfig.lineHeight=30] - 水印文字行高
|
12
|
-
* @param {number} [watermarkConfig.opacity=0.5] - 水印文字透明度
|
13
|
-
* @returns {Promise<string>} - 返回处理后的图片URL
|
14
|
-
*/
|
15
|
-
export const addWatermarkToImage = (imageUrl, watermarkConfig) => {
|
16
|
-
return new Promise((resolve, reject) => {
|
17
|
-
const img = new Image();
|
18
|
-
img.crossOrigin = "anonymous"; // 解决跨域问题
|
19
|
-
img.src = imageUrl;
|
20
|
-
|
21
|
-
img.onload = () => {
|
22
|
-
const canvas = document.createElement("canvas");
|
23
|
-
const ctx = canvas.getContext("2d");
|
24
|
-
|
25
|
-
canvas.width = img.width;
|
26
|
-
canvas.height = img.height;
|
27
|
-
|
28
|
-
ctx.drawImage(img, 0, 0);
|
29
|
-
|
30
|
-
// 设置水印样式
|
31
|
-
ctx.font = `${watermarkConfig.fontSize || 30}px Arial`;
|
32
|
-
ctx.fillStyle = watermarkConfig.color || "rgba(255, 255, 255, 0.5)";
|
33
|
-
ctx.globalAlpha = watermarkConfig.opacity || 0.5;
|
34
|
-
|
35
|
-
const textWidth = ctx.measureText(watermarkConfig.text).width;
|
36
|
-
const textHeight = watermarkConfig.lineHeight || 30;
|
37
|
-
const spacing = watermarkConfig.spacing || 100;
|
38
|
-
const angle = ((watermarkConfig.rotate || 30) * Math.PI) / 180; // 将角度转换为弧度
|
39
|
-
|
40
|
-
// 保存初始状态
|
41
|
-
ctx.save();
|
42
|
-
// 将画布中心移到原点
|
43
|
-
ctx.translate(canvas.width / 2, canvas.height / 2);
|
44
|
-
// 旋转画布
|
45
|
-
ctx.rotate(angle);
|
46
|
-
// 将画布移回左上角
|
47
|
-
ctx.translate(-canvas.width / 2, -canvas.height / 2);
|
48
|
-
|
49
|
-
// 计算覆盖整个画布所需的起始点和步长
|
50
|
-
const diagonal = Math.sqrt(
|
51
|
-
canvas.width * canvas.width + canvas.height * canvas.height
|
52
|
-
);
|
53
|
-
const startX = -diagonal;
|
54
|
-
const startY = -diagonal;
|
55
|
-
const stepX = textWidth + spacing;
|
56
|
-
const stepY = textHeight + spacing;
|
57
|
-
|
58
|
-
for (let y = startY; y < canvas.height + diagonal; y += stepY) {
|
59
|
-
for (let x = startX; x < canvas.width + diagonal; x += stepX) {
|
60
|
-
ctx.fillText(watermarkConfig.text, x, y);
|
61
|
-
}
|
62
|
-
}
|
63
|
-
|
64
|
-
// 恢复画布到初始状态
|
65
|
-
ctx.restore();
|
66
|
-
|
67
|
-
// 根据图片URL的扩展名确定输出格式
|
68
|
-
const format = imageUrl.toLowerCase().endsWith(".png")
|
69
|
-
? "image/png"
|
70
|
-
: "image/jpeg";
|
71
|
-
resolve(canvas.toDataURL(format));
|
72
|
-
};
|
73
|
-
|
74
|
-
img.onerror = (error) => {
|
75
|
-
reject(new Error("图片加载失败", error));
|
76
|
-
};
|
77
|
-
});
|
78
|
-
};
|
1
|
+
// 加水印组件
|
2
|
+
/**
|
3
|
+
* 为图片添加水印并返回处理后的图片URL
|
4
|
+
* @param {string} imageUrl - 图片的URL
|
5
|
+
* @param {Object} watermarkConfig - 水印配置对象
|
6
|
+
* @param {string} watermarkConfig.text - 水印内容
|
7
|
+
* @param {number} [watermarkConfig.fontSize=30] - 水印文字大小
|
8
|
+
* @param {string} [watermarkConfig.color='rgba(255, 255, 255, 0.5)'] - 水印文字颜色
|
9
|
+
* @param {number} [watermarkConfig.rotate=30] - 水印文字旋转度数
|
10
|
+
* @param {number} [watermarkConfig.spacing=100] - 水印文字间距
|
11
|
+
* @param {number} [watermarkConfig.lineHeight=30] - 水印文字行高
|
12
|
+
* @param {number} [watermarkConfig.opacity=0.5] - 水印文字透明度
|
13
|
+
* @returns {Promise<string>} - 返回处理后的图片URL
|
14
|
+
*/
|
15
|
+
export const addWatermarkToImage = (imageUrl, watermarkConfig) => {
|
16
|
+
return new Promise((resolve, reject) => {
|
17
|
+
const img = new Image();
|
18
|
+
img.crossOrigin = "anonymous"; // 解决跨域问题
|
19
|
+
img.src = imageUrl;
|
20
|
+
|
21
|
+
img.onload = () => {
|
22
|
+
const canvas = document.createElement("canvas");
|
23
|
+
const ctx = canvas.getContext("2d");
|
24
|
+
|
25
|
+
canvas.width = img.width;
|
26
|
+
canvas.height = img.height;
|
27
|
+
|
28
|
+
ctx.drawImage(img, 0, 0);
|
29
|
+
|
30
|
+
// 设置水印样式
|
31
|
+
ctx.font = `${watermarkConfig.fontSize || 30}px Arial`;
|
32
|
+
ctx.fillStyle = watermarkConfig.color || "rgba(255, 255, 255, 0.5)";
|
33
|
+
ctx.globalAlpha = watermarkConfig.opacity || 0.5;
|
34
|
+
|
35
|
+
const textWidth = ctx.measureText(watermarkConfig.text).width;
|
36
|
+
const textHeight = watermarkConfig.lineHeight || 30;
|
37
|
+
const spacing = watermarkConfig.spacing || 100;
|
38
|
+
const angle = ((watermarkConfig.rotate || 30) * Math.PI) / 180; // 将角度转换为弧度
|
39
|
+
|
40
|
+
// 保存初始状态
|
41
|
+
ctx.save();
|
42
|
+
// 将画布中心移到原点
|
43
|
+
ctx.translate(canvas.width / 2, canvas.height / 2);
|
44
|
+
// 旋转画布
|
45
|
+
ctx.rotate(angle);
|
46
|
+
// 将画布移回左上角
|
47
|
+
ctx.translate(-canvas.width / 2, -canvas.height / 2);
|
48
|
+
|
49
|
+
// 计算覆盖整个画布所需的起始点和步长
|
50
|
+
const diagonal = Math.sqrt(
|
51
|
+
canvas.width * canvas.width + canvas.height * canvas.height
|
52
|
+
);
|
53
|
+
const startX = -diagonal;
|
54
|
+
const startY = -diagonal;
|
55
|
+
const stepX = textWidth + spacing;
|
56
|
+
const stepY = textHeight + spacing;
|
57
|
+
|
58
|
+
for (let y = startY; y < canvas.height + diagonal; y += stepY) {
|
59
|
+
for (let x = startX; x < canvas.width + diagonal; x += stepX) {
|
60
|
+
ctx.fillText(watermarkConfig.text, x, y);
|
61
|
+
}
|
62
|
+
}
|
63
|
+
|
64
|
+
// 恢复画布到初始状态
|
65
|
+
ctx.restore();
|
66
|
+
|
67
|
+
// 根据图片URL的扩展名确定输出格式
|
68
|
+
const format = imageUrl.toLowerCase().endsWith(".png")
|
69
|
+
? "image/png"
|
70
|
+
: "image/jpeg";
|
71
|
+
resolve(canvas.toDataURL(format));
|
72
|
+
};
|
73
|
+
|
74
|
+
img.onerror = (error) => {
|
75
|
+
reject(new Error("图片加载失败", error));
|
76
|
+
};
|
77
|
+
});
|
78
|
+
};
|