wui-components-v2 1.1.70 → 1.1.72
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/api/feishu.ts +20 -20
- package/api/index.ts +4 -4
- package/api/login.ts +51 -51
- package/api/tree.ts +28 -28
- package/components/action-popup/action-popup.vue +116 -116
- package/components/add-address-list/add-address-list.vue +187 -187
- package/components/add-address-page/config.ts +297 -297
- package/components/audio-play/audio-play.vue +220 -220
- package/components/batch-upload-file/batch-upload-file.vue +75 -75
- package/components/card-botom-buttons/card-botom-buttons.vue +279 -279
- package/components/demo-card/demo-card.vue +44 -44
- package/components/fold-card/fold-card.vue +2 -2
- package/components/form-control/form-control.vue +9 -1
- package/components/global-loading/GlobalLoading.md +366 -366
- package/components/global-loading/global-loading.vue +55 -55
- package/components/global-message/GlobalMessage.md +570 -570
- package/components/global-toast/GlobalToast.md +261 -261
- package/components/global-toast/global-toast.vue +56 -56
- package/components/label-value/open-value-more.vue +56 -56
- package/components/loading/loading.vue +5 -5
- package/components/login-form-box/login-form-box.vue +65 -65
- package/components/phone-login-form/phone-login-form.vue +204 -204
- package/components/privacy-popup/privacy-popup.vue +191 -191
- package/components/scan-input/scan-input.vue +406 -406
- package/components/select-col-picker/select-col-picker.vue +168 -168
- package/components/tab-search/tab-search.vue +73 -73
- package/components/tree-select/components/TreeNode.vue +200 -200
- package/components/tree-select/components/index.vue +471 -471
- package/components/tree-select/components/tree-select.vue +56 -56
- package/components/tree-select/index.vue +93 -93
- package/components/user-choose/user-choose.vue +169 -151
- package/components/video-play/video-play.vue +56 -56
- package/components/wui-auto-update-component/wui-auto-update-component.vue +132 -132
- package/components/wui-default/wui-default.vue +49 -49
- package/components/wui-details-page/wui-details-page.vue +147 -147
- package/components/wui-edit-page/wui-edit-page-copy.vue +757 -757
- package/components/wui-edit-page/wui-edit-page.vue +793 -793
- package/components/wui-login/wui-login.vue +43 -43
- package/components/wui-login1/wui-login.vue +41 -41
- package/components/wui-menus/wui-menus-top.vue +137 -137
- package/components/wui-menus/wui-menus-top1.vue +8 -8
- package/components/wui-menus1/components/banner-carousel.vue +320 -320
- package/components/wui-menus1/components/quick-panel.vue +55 -55
- package/components/wui-menus1/components/search-bar.vue +49 -49
- package/components/wui-menus1/components/section-menus.vue +208 -208
- package/components/wui-menus1/wui-menus-top.vue +718 -718
- package/components/wui-menus1/wui-menus-top1.vue +398 -398
- package/components/wui-notify-info/notify-handle.vue +53 -53
- package/components/wui-scan-binding-sensor/wui-scan-binding-sensor.vue +75 -75
- package/components/wui-search-history-babbar/components/SearchBar.vue +70 -70
- package/components/wui-select-list/wui-select-list.vue +1 -9
- package/components/wui-select-popup/wui-select-popup.vue +1 -0
- package/components/wui-tree-page/wui-tree-page.vue +297 -297
- package/components/wui-update-component/wui-update-component.vue +221 -221
- package/composables/types/theme.ts +65 -65
- package/composables/types/user.ts +5 -5
- package/composables/useColorGenerator.ts +221 -221
- package/composables/useCompanyFieldFilter.ts +26 -24
- package/composables/useEnumes.ts +43 -43
- package/composables/useEventBus.ts +37 -37
- package/composables/useGlobalLoading.ts +47 -47
- package/composables/useGlobalMessage.ts +53 -53
- package/composables/useGlobalToast.ts +63 -63
- package/composables/useLocale.ts +54 -54
- package/composables/useManualTheme.ts +179 -179
- package/composables/useSectionMenus.ts +120 -120
- package/composables/useTabbar.ts +50 -50
- package/composables/useTheme.ts +74 -74
- package/composables/useUser.ts +24 -24
- package/index.d.ts +90 -90
- package/package.json +24 -24
- package/store/manualThemeStore.ts +143 -143
- package/store/persist.ts +41 -41
- package/store/themeStore.ts +74 -74
- package/store/userStore.ts +22 -22
- package/styles/dark-mode.scss +498 -498
- package/type.ts +154 -154
- package/utils/control-tree.ts +353 -353
- package/utils/eventBus.ts +36 -36
- package/utils/idata-scan.ts +99 -99
- package/utils/index.ts +979 -979
- package/utils/rsaEncrypt.ts +10 -10
package/utils/index.ts
CHANGED
|
@@ -1,979 +1,979 @@
|
|
|
1
|
-
import type { Comparator } from '../type'
|
|
2
|
-
|
|
3
|
-
// const filefxd = ['image', 'video', 'audio', 'file', 'wav']
|
|
4
|
-
const baseUrl = uni.getStorageSync('BASE_URL')
|
|
5
|
-
const token = uni.getStorageSync('TOKEN')
|
|
6
|
-
const hydrocarbonProgramToken = uni.getStorageSync('HYDROCARBON_PROGRAM_TOKEN')
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* 获取当前页面路径
|
|
10
|
-
* @returns 当前页面路径
|
|
11
|
-
*/
|
|
12
|
-
export function getCurrentPath() {
|
|
13
|
-
const pages = getCurrentPages()
|
|
14
|
-
const currentPage = pages[pages.length - 1]
|
|
15
|
-
return currentPage.route || ''
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* 格式化列表项展示数据
|
|
20
|
-
* @param data 数据值
|
|
21
|
-
* @param type 数据类型
|
|
22
|
-
* @returns 格式化后的数据
|
|
23
|
-
*/
|
|
24
|
-
export function formatItemData(data: any, type: string) {
|
|
25
|
-
// console.log('[Mock] formatItemData', data, type)
|
|
26
|
-
if (type === 'relselect' || type === 'tree-entity-select' || type === 'table-entity-select') {
|
|
27
|
-
// 判断data是否为数组
|
|
28
|
-
if (Array.isArray(data)) {
|
|
29
|
-
const dataArr = data.map(item => item.split('@R@')[1]).join(',')
|
|
30
|
-
return dataArr
|
|
31
|
-
}
|
|
32
|
-
// 字符串中是否包含@,@
|
|
33
|
-
if (typeof data === 'string' && data.includes('@,@')) {
|
|
34
|
-
return data.split('@,@').map((item: string) => item.split('@R@')[1]).join(',')
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// 字符串中是否包含@R@
|
|
38
|
-
if (typeof data === 'string' && data.includes('@R@')) {
|
|
39
|
-
return data.split('@R@')[1]
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
return data || ''
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
if (type === 'datetime') {
|
|
46
|
-
return data ? data.split('.')[0] : ''
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
if (type === 'file' || type === 'relfile' || type === 'video') {
|
|
50
|
-
if (data) {
|
|
51
|
-
// 判断是否为JSON文件
|
|
52
|
-
if (typeof data === 'string') {
|
|
53
|
-
const a = JSON.parse(data)
|
|
54
|
-
return [{
|
|
55
|
-
url: `${baseUrl}/v3/files${a.base.path}?@token=${token}&@programToken=${hydrocarbonProgramToken}&name=${a.base.fileName || ''}`,
|
|
56
|
-
name: a.base.fileName,
|
|
57
|
-
type: a.base.type,
|
|
58
|
-
}]
|
|
59
|
-
}
|
|
60
|
-
else if (Array.isArray(data)) {
|
|
61
|
-
return data.map((item) => {
|
|
62
|
-
if (item.includes('@R@')) {
|
|
63
|
-
const a = JSON.parse(item.split('@R@')[1])
|
|
64
|
-
return {
|
|
65
|
-
url: `${baseUrl}/v3/files${a.base.path}?@token=${token}&@programToken=${hydrocarbonProgramToken}&name=${a.base.fileName || ''}`,
|
|
66
|
-
name: a.base.fileName,
|
|
67
|
-
type: a.base.type,
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
else {
|
|
71
|
-
const a = JSON.parse(item)
|
|
72
|
-
return {
|
|
73
|
-
url: `${baseUrl}/v3/files${a.base.path}?@token=${token}&@programToken=${hydrocarbonProgramToken}&name=${a.base.fileName || ''}`,
|
|
74
|
-
name: a.base.fileName,
|
|
75
|
-
type: a.base.type,
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
})
|
|
79
|
-
}
|
|
80
|
-
else {
|
|
81
|
-
return [{
|
|
82
|
-
url: data.url,
|
|
83
|
-
name: data.name,
|
|
84
|
-
type: getAfterLastMatch(data.name, '.') || getAfterLastMatch(data.url, '.'),
|
|
85
|
-
}]
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
return [{
|
|
89
|
-
url: 'https://',
|
|
90
|
-
name: '',
|
|
91
|
-
type: 'img',
|
|
92
|
-
}]
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
return data
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* 高并发环境下也能保证唯一的ID生成器
|
|
100
|
-
* @returns 25字符的唯一字符串
|
|
101
|
-
*/
|
|
102
|
-
let counter = 0
|
|
103
|
-
export function generateHighResolutionID(): string {
|
|
104
|
-
const time = Date.now().toString(36)
|
|
105
|
-
const random = Math.random().toString(36).substring(2, 10)
|
|
106
|
-
counter = (counter + 1) % Number.MAX_SAFE_INTEGER
|
|
107
|
-
return `${time}-${random}-${counter.toString(36).padStart(6, '0')}`
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// 下载文件
|
|
111
|
-
export function downloadFile(url: string) {
|
|
112
|
-
console.log('下载', url)
|
|
113
|
-
uni.downloadFile({
|
|
114
|
-
url, // 文件的网络地址
|
|
115
|
-
success: (res) => {
|
|
116
|
-
if (res.statusCode === 200) {
|
|
117
|
-
console.log('下载成功', res.tempFilePath)
|
|
118
|
-
// 打开文件(如PDF、Word等)
|
|
119
|
-
uni.openDocument({
|
|
120
|
-
filePath: res.tempFilePath,
|
|
121
|
-
showMenu: true,
|
|
122
|
-
success: () => console.log('打开文件成功'),
|
|
123
|
-
})
|
|
124
|
-
}
|
|
125
|
-
else {
|
|
126
|
-
console.error('下载失败', res)
|
|
127
|
-
uni.showToast({
|
|
128
|
-
title: '下载失败',
|
|
129
|
-
icon: 'none',
|
|
130
|
-
})
|
|
131
|
-
}
|
|
132
|
-
},
|
|
133
|
-
fail: (err) => {
|
|
134
|
-
console.error('下载失败', err)
|
|
135
|
-
uni.showToast({
|
|
136
|
-
title: '下载失败',
|
|
137
|
-
icon: 'none',
|
|
138
|
-
})
|
|
139
|
-
},
|
|
140
|
-
})
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* 字符串截取匹配最后一项(不包含匹配字符)
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*/
|
|
148
|
-
function getAfterLastMatch(str: string, pattern: string) {
|
|
149
|
-
const lastIndex = str.lastIndexOf(pattern)
|
|
150
|
-
if (lastIndex === -1)
|
|
151
|
-
return null
|
|
152
|
-
return str.substring(lastIndex + pattern.length)
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/****
|
|
156
|
-
*
|
|
157
|
-
* 根据comparator、defaultValue、val
|
|
158
|
-
* 判断按钮是否满足显示
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*/
|
|
162
|
-
export function comparator(
|
|
163
|
-
comparator: Comparator,
|
|
164
|
-
val: string,
|
|
165
|
-
defaultValue: string,
|
|
166
|
-
): boolean {
|
|
167
|
-
let dvs: string[]
|
|
168
|
-
let value = val
|
|
169
|
-
if (val && val.indexOf && val.indexOf('@R@') > 0) {
|
|
170
|
-
const vs = val.split('@R@')
|
|
171
|
-
value = vs[1]
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
if (!comparator) {
|
|
175
|
-
comparator = 'equals'
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
if (comparator === 'none1') {
|
|
179
|
-
return !value || value === ''
|
|
180
|
-
}
|
|
181
|
-
else if (comparator === 'none1n') {
|
|
182
|
-
return !!value && value !== ''
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
if (!defaultValue) {
|
|
186
|
-
return true
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
if (!value && value !== '0') {
|
|
190
|
-
return false
|
|
191
|
-
}
|
|
192
|
-
let vals
|
|
193
|
-
switch (comparator) {
|
|
194
|
-
case 'contain':
|
|
195
|
-
return value.includes(defaultValue)
|
|
196
|
-
case 'equal':
|
|
197
|
-
case 'equals':
|
|
198
|
-
return isEqual(value, defaultValue)
|
|
199
|
-
case 'exclude':
|
|
200
|
-
dvs = toArray(defaultValue)
|
|
201
|
-
vals = toArray(value)
|
|
202
|
-
for (let i = 0; i < dvs.length; i++) {
|
|
203
|
-
for (let j = 0; j < vals.length; j++) {
|
|
204
|
-
if (dvs[i] === vals[j]) {
|
|
205
|
-
return false
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
return true
|
|
210
|
-
case 'includeOne':
|
|
211
|
-
case 'include_anyone':
|
|
212
|
-
dvs = toArray(defaultValue)
|
|
213
|
-
vals = toArray(value)
|
|
214
|
-
for (let i = 0; i < dvs.length; i++) {
|
|
215
|
-
for (let j = 0; j < vals.length; j++) {
|
|
216
|
-
if (dvs[i] === vals[j]) {
|
|
217
|
-
return true
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
return false
|
|
222
|
-
|
|
223
|
-
case 'include_all':
|
|
224
|
-
{
|
|
225
|
-
dvs = toArray(defaultValue)
|
|
226
|
-
vals = toArray(value)
|
|
227
|
-
let count = 0
|
|
228
|
-
const targetCount = dvs.length
|
|
229
|
-
for (let i = 0; i < dvs.length; i++) {
|
|
230
|
-
for (let j = 0; j < vals.length; j++) {
|
|
231
|
-
if (dvs[i] === vals[j]) {
|
|
232
|
-
count++
|
|
233
|
-
break
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
return targetCount <= count
|
|
238
|
-
}
|
|
239
|
-
case 'Greater':
|
|
240
|
-
case 'greater':
|
|
241
|
-
if (!Number.isNaN(Number(value)) && !Number.isNaN(Number(defaultValue))) {
|
|
242
|
-
return Number(value) > Number(defaultValue)
|
|
243
|
-
}
|
|
244
|
-
else {
|
|
245
|
-
return value > defaultValue
|
|
246
|
-
}
|
|
247
|
-
case 'Less':
|
|
248
|
-
case 'less':
|
|
249
|
-
if (!Number.isNaN(Number(value)) && !Number.isNaN(Number(defaultValue))) {
|
|
250
|
-
return Number(value) < Number(defaultValue)
|
|
251
|
-
}
|
|
252
|
-
else {
|
|
253
|
-
return value < defaultValue
|
|
254
|
-
}
|
|
255
|
-
case 'noGreater':
|
|
256
|
-
case 'nogreater':
|
|
257
|
-
if (!Number.isNaN(Number(value)) && !Number.isNaN(Number(defaultValue))) {
|
|
258
|
-
return Number(value) <= Number(defaultValue)
|
|
259
|
-
}
|
|
260
|
-
else {
|
|
261
|
-
return value <= defaultValue
|
|
262
|
-
}
|
|
263
|
-
case 'noLess':
|
|
264
|
-
case 'noless':
|
|
265
|
-
if (!Number.isNaN(Number(value)) && !Number.isNaN(Number(defaultValue))) {
|
|
266
|
-
return Number(value) >= Number(defaultValue)
|
|
267
|
-
}
|
|
268
|
-
else {
|
|
269
|
-
return value >= defaultValue
|
|
270
|
-
}
|
|
271
|
-
case 'unequal':
|
|
272
|
-
case 'unequals':
|
|
273
|
-
return !isEqual(value, defaultValue)
|
|
274
|
-
default:
|
|
275
|
-
console.log(`暂不支持的比较关系:${comparator};请联系管理员添加。`)
|
|
276
|
-
return false // 兜底返回值
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
function isEqual(value: string, defaultValue: string) {
|
|
281
|
-
if (defaultValue === '是' || defaultValue === '否' || defaultValue === 'true' || defaultValue === 'false') {
|
|
282
|
-
if (value === 'true' || value === '是') {
|
|
283
|
-
if (defaultValue === '是' || defaultValue === 'true') {
|
|
284
|
-
return true
|
|
285
|
-
}
|
|
286
|
-
else {
|
|
287
|
-
return false
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
else if (value === 'false' || value === '否') {
|
|
291
|
-
if (defaultValue === '否' || defaultValue === 'false') {
|
|
292
|
-
return true
|
|
293
|
-
}
|
|
294
|
-
else {
|
|
295
|
-
return false
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
else {
|
|
299
|
-
return false
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
else {
|
|
303
|
-
if (!Number.isNaN(Number(value)) && !Number.isNaN(Number(defaultValue))) {
|
|
304
|
-
return Number(value) === Number(defaultValue)
|
|
305
|
-
}
|
|
306
|
-
else {
|
|
307
|
-
return value === defaultValue
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
function toArray(value: string) {
|
|
313
|
-
let val
|
|
314
|
-
if (typeof value == 'string') {
|
|
315
|
-
val = value.split(',')
|
|
316
|
-
}
|
|
317
|
-
else if (Array.isArray(value)) {
|
|
318
|
-
val = value
|
|
319
|
-
}
|
|
320
|
-
else {
|
|
321
|
-
val = [value]
|
|
322
|
-
}
|
|
323
|
-
return val
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
/**
|
|
327
|
-
* 根据目标颜色生成渐变色字符串
|
|
328
|
-
* @param color - 目标颜色,可以是十六进制颜色或RGB颜色
|
|
329
|
-
* @param angle - 渐变角度,默认为180deg
|
|
330
|
-
* @param startOpacity - 起始透明度,默认为1(不透明)
|
|
331
|
-
* @param endOpacity - 结束透明度,默认为0.3
|
|
332
|
-
* @returns 渐变色字符串
|
|
333
|
-
*/
|
|
334
|
-
export function generateGradientColor(
|
|
335
|
-
color: string,
|
|
336
|
-
angle: number = 180,
|
|
337
|
-
startOpacity: number = 1,
|
|
338
|
-
endOpacity: number = 0.3,
|
|
339
|
-
): string {
|
|
340
|
-
// 处理十六进制颜色转换为RGB
|
|
341
|
-
const hexToRgb = (hex: string): { r: number, g: number, b: number } | null => {
|
|
342
|
-
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
|
|
343
|
-
return result
|
|
344
|
-
? {
|
|
345
|
-
r: Number.parseInt(result[1], 16),
|
|
346
|
-
g: Number.parseInt(result[2], 16),
|
|
347
|
-
b: Number.parseInt(result[3], 16),
|
|
348
|
-
}
|
|
349
|
-
: null
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
// 将RGB颜色转换为rgba格式
|
|
353
|
-
const toRgba = (color: string, opacity: number): string => {
|
|
354
|
-
if (color.startsWith('#')) {
|
|
355
|
-
const rgb = hexToRgb(color)
|
|
356
|
-
if (rgb) {
|
|
357
|
-
return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${opacity})`
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
else if (color.startsWith('rgb(')) {
|
|
361
|
-
// 处理rgb()格式
|
|
362
|
-
const rgbValues = color.match(/\d+/g)
|
|
363
|
-
if (rgbValues && rgbValues.length === 3) {
|
|
364
|
-
return `rgba(${rgbValues[0]}, ${rgbValues[1]}, ${rgbValues[2]}, ${opacity})`
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
// 如果已经是rgba或其他格式,直接返回带透明度的版本
|
|
368
|
-
return color.replace(/rgb\(/, 'rgba(').replace(/\)/, `, ${opacity})`)
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
// 生成渐变色字符串
|
|
372
|
-
const startColor = color.startsWith('rgba') ? color : toRgba(color, startOpacity)
|
|
373
|
-
const endColor = toRgba(color, endOpacity)
|
|
374
|
-
|
|
375
|
-
return `linear-gradient(${angle}deg, ${startColor} 0%, ${endColor} 100%)`
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
// 使用示例
|
|
379
|
-
// console.log(generateGradientColor('#2094FF'));
|
|
380
|
-
// 输出: linear-gradient(180deg, rgba(32,148,255,1) 0%, rgba(32,148,255,0.3) 100%)
|
|
381
|
-
|
|
382
|
-
// console.log(generateGradientColor('#2094FF', 90, 0.8, 0.1));
|
|
383
|
-
// 输出: linear-gradient(90deg, rgba(32,148,255,0.8) 0%, rgba(32,148,255,0.1) 100%)
|
|
384
|
-
|
|
385
|
-
/**
|
|
386
|
-
* 格式化日期显示
|
|
387
|
-
* @param date - 要格式化的日期,默认为当前日期
|
|
388
|
-
* @returns 格式化后的日期字符串,如"2025年9月25日 周四 下午"
|
|
389
|
-
*/
|
|
390
|
-
export function formatChineseDate(date: Date = new Date()): string {
|
|
391
|
-
// 年份
|
|
392
|
-
const year = date.getFullYear()
|
|
393
|
-
|
|
394
|
-
// 月份(月份从0开始,需要+1)
|
|
395
|
-
const month = date.getMonth() + 1
|
|
396
|
-
|
|
397
|
-
// 日期
|
|
398
|
-
const day = date.getDate()
|
|
399
|
-
|
|
400
|
-
// 星期几
|
|
401
|
-
const weekdays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
|
|
402
|
-
const weekday = weekdays[date.getDay()]
|
|
403
|
-
|
|
404
|
-
// 上午/下午判断
|
|
405
|
-
const hour = date.getHours()
|
|
406
|
-
const period = hour < 12 ? '上午' : '下午'
|
|
407
|
-
|
|
408
|
-
return `${year}年${month}月${day}日 ${weekday} ${period}`
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
// 使用示例
|
|
412
|
-
// console.log(formatChineseDate(new Date(2025, 8, 25)));
|
|
413
|
-
// 输出: 2025年9月25日 周四 下午
|
|
414
|
-
|
|
415
|
-
// console.log(formatChineseDate(new Date(2025, 8, 25, 10, 30)));
|
|
416
|
-
// 输出: 2025年9月25日 周四 上午
|
|
417
|
-
|
|
418
|
-
// console.log(formatChineseDate());
|
|
419
|
-
// 输出当前日期的格式化结果
|
|
420
|
-
|
|
421
|
-
/**
|
|
422
|
-
* 淡化颜色
|
|
423
|
-
* @param color - 目标颜色,可以是十六进制颜色或RGB颜色
|
|
424
|
-
* @returns 淡化后的颜色字符串
|
|
425
|
-
*/
|
|
426
|
-
export function desaturateColor(color: string, opacity: number = 0.04): string {
|
|
427
|
-
// 处理十六进制颜色转换为RGB
|
|
428
|
-
const hexToRgb = (hex: string): { r: number, g: number, b: number } | null => {
|
|
429
|
-
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
|
|
430
|
-
return result
|
|
431
|
-
? {
|
|
432
|
-
r: Number.parseInt(result[1], 16),
|
|
433
|
-
g: Number.parseInt(result[2], 16),
|
|
434
|
-
b: Number.parseInt(result[3], 16),
|
|
435
|
-
}
|
|
436
|
-
: null
|
|
437
|
-
}
|
|
438
|
-
// 将RGB颜色转换为rgba格式
|
|
439
|
-
const toRgba = (color: string, opacity: number): string => {
|
|
440
|
-
if (color.startsWith('#')) {
|
|
441
|
-
const rgb = hexToRgb(color)
|
|
442
|
-
if (rgb) {
|
|
443
|
-
return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${opacity})`
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
else if (color.startsWith('rgb(')) {
|
|
447
|
-
// 处理rgb()格式
|
|
448
|
-
const rgbValues = color.match(/\d+/g)
|
|
449
|
-
if (rgbValues && rgbValues.length === 3) {
|
|
450
|
-
return `rgba(${rgbValues[0]}, ${rgbValues[1]}, ${rgbValues[2]}, ${opacity})`
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
// 如果已经是rgba或其他格式,直接返回带透明度的版本
|
|
454
|
-
return color.replace(/rgb\(/, 'rgba(').replace(/\)/, `, ${opacity})`)
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
return toRgba(color, opacity)
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
/**
|
|
461
|
-
* 扩展版本:判断字符串中是否包含多个字符中的任意一个
|
|
462
|
-
*/
|
|
463
|
-
export function containsAnyCharacter(str: string, chars: string[]): boolean {
|
|
464
|
-
if (typeof str !== 'string' || !Array.isArray(chars)) {
|
|
465
|
-
return false
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
// 验证数组中的每个元素都是单个字符
|
|
469
|
-
for (const char of chars) {
|
|
470
|
-
if (typeof char !== 'string' || char.length !== 1) {
|
|
471
|
-
throw new Error('数组中的每个元素必须是单个字符')
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
return chars.some(char => str.includes(char))
|
|
476
|
-
}
|
|
477
|
-
// 测试用例
|
|
478
|
-
// console.log(containsAnyCharacter("hello world", ["a", "e", "i", "o", "u"])); // true (包含o)
|
|
479
|
-
|
|
480
|
-
/**
|
|
481
|
-
* 颜色转换和生成工具类 (针对十六进制颜色优化)
|
|
482
|
-
*/
|
|
483
|
-
|
|
484
|
-
/**
|
|
485
|
-
* 十六进制颜色转换为RGB对象
|
|
486
|
-
* @param {string} hex - 十六进制颜色值,如 #2164F3 或 2164F3
|
|
487
|
-
* @returns {object} RGB对象 {r, g, b}
|
|
488
|
-
*/
|
|
489
|
-
export function hexToRgb(hex: string) {
|
|
490
|
-
// 移除 # 号
|
|
491
|
-
hex = hex.replace(/^#/, '')
|
|
492
|
-
|
|
493
|
-
// 处理简写形式如 #FFF
|
|
494
|
-
if (hex.length === 3) {
|
|
495
|
-
hex = hex.split('').map(c => c + c).join('')
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
// 解析十六进制值
|
|
499
|
-
const bigint = Number.parseInt(hex, 16)
|
|
500
|
-
|
|
501
|
-
return {
|
|
502
|
-
r: (bigint >> 16) & 255,
|
|
503
|
-
g: (bigint >> 8) & 255,
|
|
504
|
-
b: bigint & 255,
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
/**
|
|
509
|
-
* RGB对象转换为十六进制颜色
|
|
510
|
-
* @param {object} rgb - RGB对象 {r, g, b}
|
|
511
|
-
* @returns {string} 十六进制颜色值
|
|
512
|
-
*/
|
|
513
|
-
export function rgbToHex(rgb: { r: any, g: any, b: any }) {
|
|
514
|
-
const toHex = (c: { toString: (arg0: number) => any }) => {
|
|
515
|
-
const hex = c.toString(16)
|
|
516
|
-
return hex.length === 1 ? `0${hex}` : hex
|
|
517
|
-
}
|
|
518
|
-
return `#${toHex(rgb.r)}${toHex(rgb.g)}${toHex(rgb.b)}`.toUpperCase()
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
/**
|
|
522
|
-
* 将 RGB 颜色转换为 HSL 颜色模型
|
|
523
|
-
* @param {number} r - 红色值 (0-255)
|
|
524
|
-
* @param {number} g - 绿色值 (0-255)
|
|
525
|
-
* @param {number} b - 蓝色值 (0-255)
|
|
526
|
-
* @returns {object} HSL对象 {h, s, l}
|
|
527
|
-
*/
|
|
528
|
-
export function rgbToHsl(r: number, g: number, b: number) {
|
|
529
|
-
r /= 255
|
|
530
|
-
g /= 255
|
|
531
|
-
b /= 255
|
|
532
|
-
|
|
533
|
-
const max = Math.max(r, g, b)
|
|
534
|
-
const min = Math.min(r, g, b)
|
|
535
|
-
let h
|
|
536
|
-
let s
|
|
537
|
-
const l = (max + min) / 2
|
|
538
|
-
|
|
539
|
-
if (max === min) {
|
|
540
|
-
h = s = 0
|
|
541
|
-
}
|
|
542
|
-
else {
|
|
543
|
-
const d = max - min
|
|
544
|
-
s = l > 0.5 ? d / (2 - max - min) : d / (max + min)
|
|
545
|
-
|
|
546
|
-
switch (max) {
|
|
547
|
-
case r: h = (g - b) / d + (g < b ? 6 : 0)
|
|
548
|
-
break
|
|
549
|
-
case g: h = (b - r) / d + 2
|
|
550
|
-
break
|
|
551
|
-
case b: h = (r - g) / d + 4
|
|
552
|
-
break
|
|
553
|
-
default: h = 0 // 添加默认情况,确保h被初始化
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
h /= 6
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
return {
|
|
560
|
-
h: Math.round(h * 360),
|
|
561
|
-
s: Math.round(s * 100),
|
|
562
|
-
l: Math.round(l * 100),
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
/**
|
|
567
|
-
* 将 HSL 颜色转换为 RGB 颜色
|
|
568
|
-
* @param {number} h - 色相 (0-360)
|
|
569
|
-
* @param {number} s - 饱和度 (0-100)
|
|
570
|
-
* @param {number} l - 亮度 (0-100)
|
|
571
|
-
* @returns {object} RGB对象 {r, g, b}
|
|
572
|
-
*/
|
|
573
|
-
export function hslToRgb(h: number, s: number = 50, l: number = 50): { r: number, g: number, b: number } {
|
|
574
|
-
h = h !== undefined ? h / 360 : 0
|
|
575
|
-
s = s !== undefined ? s / 100 : 0.5
|
|
576
|
-
l = l !== undefined ? l / 100 : 0.5
|
|
577
|
-
|
|
578
|
-
let r, g, b
|
|
579
|
-
|
|
580
|
-
if (s === 0) {
|
|
581
|
-
r = g = b = l
|
|
582
|
-
}
|
|
583
|
-
else {
|
|
584
|
-
const hue2rgb = (p: number, q: number, t: number) => {
|
|
585
|
-
if (t < 0)
|
|
586
|
-
t += 1
|
|
587
|
-
if (t > 1)
|
|
588
|
-
t -= 1
|
|
589
|
-
if (t < 1 / 6)
|
|
590
|
-
return p + (q - p) * 6 * t
|
|
591
|
-
if (t < 1 / 2)
|
|
592
|
-
return q
|
|
593
|
-
if (t < 2 / 3)
|
|
594
|
-
return p + (q - p) * (2 / 3 - t) * 6
|
|
595
|
-
return p
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
const q = l < 0.5 ? l * (1 + s) : l + s - l * s
|
|
599
|
-
const p = 2 * l - q
|
|
600
|
-
|
|
601
|
-
r = hue2rgb(p, q, h + 1 / 3)
|
|
602
|
-
g = hue2rgb(p, q, h)
|
|
603
|
-
b = hue2rgb(p, q, h - 1 / 3)
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
return {
|
|
607
|
-
r: Math.round(r * 255),
|
|
608
|
-
g: Math.round(g * 255),
|
|
609
|
-
b: Math.round(b * 255),
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
/**
|
|
614
|
-
* 基于主色调生成随机颜色
|
|
615
|
-
* @param {string} baseHex - 基础十六进制颜色,如 #2164F3
|
|
616
|
-
* @param {object} options - 配置选项
|
|
617
|
-
* @returns {object} 颜色对象
|
|
618
|
-
*/
|
|
619
|
-
export function generateRandomColorFromHex(baseHex: string, options = { hueRange: 15, saturationRange: 20, lightnessRange: 20 }) {
|
|
620
|
-
const {
|
|
621
|
-
hueRange = 15,
|
|
622
|
-
saturationRange = 20,
|
|
623
|
-
lightnessRange = 20,
|
|
624
|
-
} = options
|
|
625
|
-
|
|
626
|
-
// 转换为RGB
|
|
627
|
-
const baseRgb = hexToRgb(baseHex)
|
|
628
|
-
|
|
629
|
-
// 转换为HSL
|
|
630
|
-
const baseHsl = rgbToHsl(baseRgb.r, baseRgb.g, baseRgb.b)
|
|
631
|
-
|
|
632
|
-
// 在指定范围内随机调整HSL值
|
|
633
|
-
const randomH = baseHsl.h + (Math.random() * 2 - 1) * hueRange
|
|
634
|
-
const randomS = baseHsl.s + (Math.random() * 2 - 1) * saturationRange
|
|
635
|
-
const randomL = baseHsl.l + (Math.random() * 2 - 1) * lightnessRange
|
|
636
|
-
|
|
637
|
-
// 确保值在有效范围内
|
|
638
|
-
const h = Math.max(0, Math.min(360, randomH))
|
|
639
|
-
const s = Math.max(0, Math.min(100, randomS))
|
|
640
|
-
const l = Math.max(0, Math.min(100, randomL))
|
|
641
|
-
|
|
642
|
-
// 转换回RGB
|
|
643
|
-
const rgb = hslToRgb(h, s, l)
|
|
644
|
-
const hex = rgbToHex(rgb)
|
|
645
|
-
|
|
646
|
-
return {
|
|
647
|
-
hex,
|
|
648
|
-
rgb,
|
|
649
|
-
hsl: { h: Math.round(h), s: Math.round(s), l: Math.round(l) },
|
|
650
|
-
}
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
/**
|
|
654
|
-
* 批量生成协调的颜色
|
|
655
|
-
* @param {string} baseHex - 基础十六进制颜色
|
|
656
|
-
* @param {number} count - 需要生成的颜色数量
|
|
657
|
-
* @param {object} options - 配置选项
|
|
658
|
-
* @returns {Array} 颜色数组
|
|
659
|
-
*/
|
|
660
|
-
export function generateHarmoniousColors(baseHex: string, count: number, options = { hueRange: 15, saturationRange: 20, lightnessRange: 20, colorType: 'all' }) {
|
|
661
|
-
const colors = []
|
|
662
|
-
const baseRgb = hexToRgb(baseHex)
|
|
663
|
-
const baseHsl = rgbToHsl(baseRgb.r, baseRgb.g, baseRgb.b)
|
|
664
|
-
|
|
665
|
-
const {
|
|
666
|
-
hueRange = 15,
|
|
667
|
-
saturationRange = 20,
|
|
668
|
-
lightnessRange = 20,
|
|
669
|
-
// 新增:颜色类型,可选值:'all', 'light', 'dark', 'muted'
|
|
670
|
-
colorType = 'all',
|
|
671
|
-
} = options
|
|
672
|
-
|
|
673
|
-
// 使用let声明,允许重新赋值
|
|
674
|
-
let adjustedSaturationRange = saturationRange
|
|
675
|
-
|
|
676
|
-
// 根据颜色类型调整基础亮度
|
|
677
|
-
let baseLightness = baseHsl.l
|
|
678
|
-
if (colorType === 'light') {
|
|
679
|
-
baseLightness = Math.min(80, baseHsl.l + 20)
|
|
680
|
-
}
|
|
681
|
-
else if (colorType === 'dark') {
|
|
682
|
-
baseLightness = Math.max(20, baseHsl.l - 20)
|
|
683
|
-
}
|
|
684
|
-
else if (colorType === 'muted') {
|
|
685
|
-
adjustedSaturationRange = Math.min(adjustedSaturationRange, 15)
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
for (let i = 0; i < count; i++) {
|
|
689
|
-
// 在指定范围内随机调整HSL值
|
|
690
|
-
const randomH = baseHsl.h + (Math.random() * 2 - 1) * hueRange
|
|
691
|
-
const randomS = baseHsl.s + (Math.random() * 2 - 1) * adjustedSaturationRange
|
|
692
|
-
const randomL = baseLightness + (Math.random() * 2 - 1) * lightnessRange
|
|
693
|
-
|
|
694
|
-
// 确保值在有效范围内
|
|
695
|
-
const h = Math.max(0, Math.min(360, randomH))
|
|
696
|
-
const s = Math.max(0, Math.min(100, randomS))
|
|
697
|
-
const l = Math.max(0, Math.min(100, randomL))
|
|
698
|
-
|
|
699
|
-
// 转换回RGB
|
|
700
|
-
const rgb = hslToRgb(h, s, l)
|
|
701
|
-
const hex = rgbToHex(rgb)
|
|
702
|
-
|
|
703
|
-
colors.push({
|
|
704
|
-
hex,
|
|
705
|
-
rgb,
|
|
706
|
-
hsl: { h: Math.round(h), s: Math.round(s), l: Math.round(l) },
|
|
707
|
-
rgbString: `rgb(${rgb.r}, ${rgb.g}, ${rgb.b})`,
|
|
708
|
-
})
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
return colors
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
/**
|
|
715
|
-
* 生成一组完美的图标颜色(智能算法)
|
|
716
|
-
* @param {string} baseHex - 基础颜色
|
|
717
|
-
* @param {number} count - 图标数量
|
|
718
|
-
* @returns {Array} 优化后的颜色数组
|
|
719
|
-
*/
|
|
720
|
-
export function generateIconColors(baseHex: string, count: number) {
|
|
721
|
-
const baseRgb = hexToRgb(baseHex)
|
|
722
|
-
const baseHsl = rgbToHsl(baseRgb.r, baseRgb.g, baseRgb.b)
|
|
723
|
-
const colors = []
|
|
724
|
-
|
|
725
|
-
// 根据数量调整色相分布
|
|
726
|
-
const hueStep = 360 / (count + 1)
|
|
727
|
-
|
|
728
|
-
for (let i = 0; i < count; i++) {
|
|
729
|
-
// 创建更均匀的色相分布
|
|
730
|
-
let hue = (baseHsl.h + (i * hueStep)) % 360
|
|
731
|
-
|
|
732
|
-
// 保持主色调附近有一些颜色
|
|
733
|
-
if (i < 3) {
|
|
734
|
-
hue = baseHsl.h + (i - 1) * 10
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
// 智能调整饱和度和亮度
|
|
738
|
-
// 重要图标更鲜艳,次要图标更柔和
|
|
739
|
-
const saturation = Math.max(60, Math.min(90, baseHsl.s + (Math.random() * 20 - 10),
|
|
740
|
-
))
|
|
741
|
-
|
|
742
|
-
// 创建对比度:一些更亮,一些更暗
|
|
743
|
-
const lightness = i % 2 === 0
|
|
744
|
-
? Math.min(70, baseHsl.l + 15 + Math.random() * 10)
|
|
745
|
-
: Math.max(30, baseHsl.l - 10 + Math.random() * 10)
|
|
746
|
-
|
|
747
|
-
const rgb = hslToRgb(hue, saturation, lightness)
|
|
748
|
-
const hex = rgbToHex(rgb)
|
|
749
|
-
|
|
750
|
-
colors.push({
|
|
751
|
-
hex,
|
|
752
|
-
rgb,
|
|
753
|
-
hsl: { h: Math.round(hue), s: Math.round(saturation), l: Math.round(lightness) },
|
|
754
|
-
rgbString: `rgb(${rgb.r}, ${rgb.g}, ${rgb.b})`,
|
|
755
|
-
})
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
return colors
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
/**
|
|
762
|
-
* 检查颜色对比度是否足够(用于文本可读性)
|
|
763
|
-
* @param {string} hexColor - 十六进制颜色
|
|
764
|
-
* @param {string} backgroundColor - 背景十六进制颜色,默认白色
|
|
765
|
-
* @returns {number} 对比度比率
|
|
766
|
-
*/
|
|
767
|
-
export function getContrastRatio(hexColor: string, backgroundColor = '#FFFFFF') {
|
|
768
|
-
const rgb1 = hexToRgb(hexColor)
|
|
769
|
-
const rgb2 = hexToRgb(backgroundColor)
|
|
770
|
-
|
|
771
|
-
const luminance1 = getLuminance(rgb1)
|
|
772
|
-
const luminance2 = getLuminance(rgb2)
|
|
773
|
-
|
|
774
|
-
const lighter = Math.max(luminance1, luminance2)
|
|
775
|
-
const darker = Math.min(luminance1, luminance2)
|
|
776
|
-
|
|
777
|
-
return (lighter + 0.05) / (darker + 0.05)
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
/**
|
|
781
|
-
* 计算颜色亮度
|
|
782
|
-
* @param {object} rgb - RGB对象
|
|
783
|
-
* @returns {number} 亮度值
|
|
784
|
-
*/
|
|
785
|
-
function getLuminance(rgb: { r: number, g: number, b: number }) {
|
|
786
|
-
const [r, g, b] = [rgb.r, rgb.g, rgb.b].map((v) => {
|
|
787
|
-
v /= 255
|
|
788
|
-
return v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4
|
|
789
|
-
})
|
|
790
|
-
|
|
791
|
-
return 0.2126 * r + 0.7152 * g + 0.0722 * b
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
/**
|
|
795
|
-
* 生成颜色变体(浅色、深色等)
|
|
796
|
-
* @param {string} hex - 基础颜色
|
|
797
|
-
* @param {string} variant - 变体类型:'lighter', 'light', 'dark', 'darker', 'muted'
|
|
798
|
-
* @returns {string} 变体颜色的十六进制值
|
|
799
|
-
*/
|
|
800
|
-
export function getColorVariant(hex: string, variant = 'light') {
|
|
801
|
-
const rgb = hexToRgb(hex)
|
|
802
|
-
const hsl = rgbToHsl(rgb.r, rgb.g, rgb.b)
|
|
803
|
-
|
|
804
|
-
switch (variant) {
|
|
805
|
-
case 'lighter':
|
|
806
|
-
hsl.l = Math.min(95, hsl.l + 40)
|
|
807
|
-
hsl.s = Math.max(30, hsl.s - 10)
|
|
808
|
-
break
|
|
809
|
-
case 'light':
|
|
810
|
-
hsl.l = Math.min(85, hsl.l + 30)
|
|
811
|
-
hsl.s = Math.max(40, hsl.s - 15)
|
|
812
|
-
break
|
|
813
|
-
case 'dark':
|
|
814
|
-
hsl.l = Math.max(20, hsl.l - 20)
|
|
815
|
-
hsl.s = Math.min(80, hsl.s + 10)
|
|
816
|
-
break
|
|
817
|
-
case 'darker':
|
|
818
|
-
hsl.l = Math.max(10, hsl.l - 30)
|
|
819
|
-
hsl.s = Math.min(70, hsl.s + 20)
|
|
820
|
-
break
|
|
821
|
-
case 'muted':
|
|
822
|
-
hsl.s = Math.max(20, hsl.s - 30)
|
|
823
|
-
break
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
const variantRgb = hslToRgb(hsl.h, hsl.s, hsl.l)
|
|
827
|
-
return rgbToHex(variantRgb)
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
/**
|
|
831
|
-
*
|
|
832
|
-
*
|
|
833
|
-
* 获取字符串的第一个字符
|
|
834
|
-
* @param {string} str - 输入的字符串
|
|
835
|
-
* @returns {string} 字符串的第一个字符
|
|
836
|
-
*/
|
|
837
|
-
export function getFirstChar(str: string) {
|
|
838
|
-
return str.charAt(0)
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
// 生成七彩虹范围内的任意颜色
|
|
842
|
-
/**
|
|
843
|
-
* 七彩虹颜色范围(HSL表示)
|
|
844
|
-
* 每个颜色范围:色相(H)范围,饱和度(S)固定,亮度(L)固定
|
|
845
|
-
* 选择较高的饱和度(70-80%)和中等亮度(45-55%)以确保白色文字可见
|
|
846
|
-
*/
|
|
847
|
-
const COLOR_RANGES = [
|
|
848
|
-
{ name: '红', hueRange: [0, 10], saturation: 75, lightness: 50 },
|
|
849
|
-
{ name: '橙', hueRange: [20, 40], saturation: 75, lightness: 50 },
|
|
850
|
-
{ name: '黄', hueRange: [45, 60], saturation: 75, lightness: 45 }, // 黄色稍微暗一点以保证白色文字可见
|
|
851
|
-
{ name: '绿', hueRange: [90, 140], saturation: 70, lightness: 45 },
|
|
852
|
-
{ name: '青', hueRange: [160, 190], saturation: 70, lightness: 50 },
|
|
853
|
-
{ name: '蓝', hueRange: [200, 240], saturation: 75, lightness: 45 },
|
|
854
|
-
{ name: '紫', hueRange: [260, 300], saturation: 70, lightness: 50 },
|
|
855
|
-
{ name: '粉', hueRange: [310, 340], saturation: 70, lightness: 55 },
|
|
856
|
-
] as const
|
|
857
|
-
|
|
858
|
-
type ColorRange = typeof COLOR_RANGES[number]
|
|
859
|
-
|
|
860
|
-
/**
|
|
861
|
-
* 将HSL转换为十六进制颜色
|
|
862
|
-
*/
|
|
863
|
-
function hslToHex(h: number, s: number, l: number): string {
|
|
864
|
-
l /= 100
|
|
865
|
-
const a = s * Math.min(l, 1 - l) / 100
|
|
866
|
-
const f = (n: number) => {
|
|
867
|
-
const k = (n + h / 30) % 12
|
|
868
|
-
const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1)
|
|
869
|
-
return Math.round(255 * color).toString(16).padStart(2, '0')
|
|
870
|
-
}
|
|
871
|
-
return `#${f(0)}${f(8)}${f(4)}`
|
|
872
|
-
}
|
|
873
|
-
|
|
874
|
-
/**
|
|
875
|
-
* 检查颜色是否为暗色系
|
|
876
|
-
* 通过HSL的亮度判断,亮度低于30%的被认为是暗色
|
|
877
|
-
*/
|
|
878
|
-
function isDarkColor(h: number, s: number, l: number): boolean {
|
|
879
|
-
return l < 30
|
|
880
|
-
}
|
|
881
|
-
|
|
882
|
-
/**
|
|
883
|
-
* 七彩虹颜色生成器类
|
|
884
|
-
*/
|
|
885
|
-
export class RainbowColorGenerator {
|
|
886
|
-
private lastColorRange: ColorRange | null = null
|
|
887
|
-
private lastColorHex: string | null = null
|
|
888
|
-
|
|
889
|
-
/**
|
|
890
|
-
* 生成随机颜色值
|
|
891
|
-
*/
|
|
892
|
-
private generateRandomColorFromRange(range: ColorRange): string {
|
|
893
|
-
const { hueRange, saturation, lightness } = range
|
|
894
|
-
const hue = Math.floor(Math.random() * (hueRange[1] - hueRange[0] + 1)) + hueRange[0]
|
|
895
|
-
|
|
896
|
-
// 确保颜色不是暗色系
|
|
897
|
-
if (isDarkColor(hue, saturation, lightness)) {
|
|
898
|
-
// 如果亮度太低,增加亮度到安全范围
|
|
899
|
-
return hslToHex(hue, saturation, Math.max(lightness, 40))
|
|
900
|
-
}
|
|
901
|
-
|
|
902
|
-
return hslToHex(hue, saturation, lightness)
|
|
903
|
-
}
|
|
904
|
-
|
|
905
|
-
/**
|
|
906
|
-
* 检查两个颜色是否属于同一个色系
|
|
907
|
-
*/
|
|
908
|
-
private isSameColorRange(range1: ColorRange, range2: ColorRange): boolean {
|
|
909
|
-
return range1.name === range2.name
|
|
910
|
-
}
|
|
911
|
-
|
|
912
|
-
/**
|
|
913
|
-
* 获取可用的颜色范围(排除上一个使用的颜色范围)
|
|
914
|
-
*/
|
|
915
|
-
private getAvailableRanges(): ColorRange[] {
|
|
916
|
-
if (!this.lastColorRange) {
|
|
917
|
-
return [...COLOR_RANGES]
|
|
918
|
-
}
|
|
919
|
-
return COLOR_RANGES.filter(range => !this.isSameColorRange(range, this.lastColorRange!))
|
|
920
|
-
}
|
|
921
|
-
|
|
922
|
-
/**
|
|
923
|
-
* 获取下一个随机颜色
|
|
924
|
-
* @returns 十六进制颜色字符串
|
|
925
|
-
*/
|
|
926
|
-
getNextColor(): string {
|
|
927
|
-
const availableRanges = this.getAvailableRanges()
|
|
928
|
-
|
|
929
|
-
// 随机选择一个颜色范围
|
|
930
|
-
const randomIndex = Math.floor(Math.random() * availableRanges.length)
|
|
931
|
-
const selectedRange = availableRanges[randomIndex]
|
|
932
|
-
|
|
933
|
-
// 生成颜色
|
|
934
|
-
const color = this.generateRandomColorFromRange(selectedRange)
|
|
935
|
-
|
|
936
|
-
// 更新上一个颜色
|
|
937
|
-
this.lastColorRange = selectedRange
|
|
938
|
-
this.lastColorHex = color
|
|
939
|
-
|
|
940
|
-
return color
|
|
941
|
-
}
|
|
942
|
-
|
|
943
|
-
/**
|
|
944
|
-
* 重置生成器状态
|
|
945
|
-
*/
|
|
946
|
-
reset(): void {
|
|
947
|
-
this.lastColorRange = null
|
|
948
|
-
this.lastColorHex = null
|
|
949
|
-
}
|
|
950
|
-
|
|
951
|
-
/**
|
|
952
|
-
* 获取上一个生成的颜色
|
|
953
|
-
*/
|
|
954
|
-
getLastColor(): string | null {
|
|
955
|
-
return this.lastColorHex
|
|
956
|
-
}
|
|
957
|
-
}
|
|
958
|
-
/**
|
|
959
|
-
* 格式化手机号
|
|
960
|
-
* @param phone 手机号字符串
|
|
961
|
-
* @param mask 隐藏码,默认为****
|
|
962
|
-
* @returns 格式化后的手机号字符串
|
|
963
|
-
*/
|
|
964
|
-
export function formatPhone(phone: string, mask = '****') {
|
|
965
|
-
if (!phone) return '';
|
|
966
|
-
|
|
967
|
-
const cleanPhone = phone.replace(/\D/g, '');
|
|
968
|
-
const len = cleanPhone.length;
|
|
969
|
-
|
|
970
|
-
// 根据号码长度决定显示方式
|
|
971
|
-
if (len === 11) {
|
|
972
|
-
return cleanPhone.slice(0, 3) + mask + cleanPhone.slice(-4);
|
|
973
|
-
} else if (len === 8) {
|
|
974
|
-
// 座机号码等短号码
|
|
975
|
-
return cleanPhone.slice(0, 2) + mask + cleanPhone.slice(-2);
|
|
976
|
-
}
|
|
977
|
-
|
|
978
|
-
return phone;
|
|
979
|
-
}
|
|
1
|
+
import type { Comparator } from '../type'
|
|
2
|
+
|
|
3
|
+
// const filefxd = ['image', 'video', 'audio', 'file', 'wav']
|
|
4
|
+
const baseUrl = uni.getStorageSync('BASE_URL')
|
|
5
|
+
const token = uni.getStorageSync('TOKEN')
|
|
6
|
+
const hydrocarbonProgramToken = uni.getStorageSync('HYDROCARBON_PROGRAM_TOKEN')
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 获取当前页面路径
|
|
10
|
+
* @returns 当前页面路径
|
|
11
|
+
*/
|
|
12
|
+
export function getCurrentPath() {
|
|
13
|
+
const pages = getCurrentPages()
|
|
14
|
+
const currentPage = pages[pages.length - 1]
|
|
15
|
+
return currentPage.route || ''
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 格式化列表项展示数据
|
|
20
|
+
* @param data 数据值
|
|
21
|
+
* @param type 数据类型
|
|
22
|
+
* @returns 格式化后的数据
|
|
23
|
+
*/
|
|
24
|
+
export function formatItemData(data: any, type: string) {
|
|
25
|
+
// console.log('[Mock] formatItemData', data, type)
|
|
26
|
+
if (type === 'relselect' || type === 'tree-entity-select' || type === 'table-entity-select') {
|
|
27
|
+
// 判断data是否为数组
|
|
28
|
+
if (Array.isArray(data)) {
|
|
29
|
+
const dataArr = data.map(item => item.split('@R@')[1]).join(',')
|
|
30
|
+
return dataArr
|
|
31
|
+
}
|
|
32
|
+
// 字符串中是否包含@,@
|
|
33
|
+
if (typeof data === 'string' && data.includes('@,@')) {
|
|
34
|
+
return data.split('@,@').map((item: string) => item.split('@R@')[1]).join(',')
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// 字符串中是否包含@R@
|
|
38
|
+
if (typeof data === 'string' && data.includes('@R@')) {
|
|
39
|
+
return data.split('@R@')[1]
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return data || ''
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (type === 'datetime') {
|
|
46
|
+
return data ? data.split('.')[0] : ''
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (type === 'file' || type === 'relfile' || type === 'video') {
|
|
50
|
+
if (data) {
|
|
51
|
+
// 判断是否为JSON文件
|
|
52
|
+
if (typeof data === 'string') {
|
|
53
|
+
const a = JSON.parse(data)
|
|
54
|
+
return [{
|
|
55
|
+
url: `${baseUrl}/v3/files${a.base.path}?@token=${token}&@programToken=${hydrocarbonProgramToken}&name=${a.base.fileName || ''}`,
|
|
56
|
+
name: a.base.fileName,
|
|
57
|
+
type: a.base.type,
|
|
58
|
+
}]
|
|
59
|
+
}
|
|
60
|
+
else if (Array.isArray(data)) {
|
|
61
|
+
return data.map((item) => {
|
|
62
|
+
if (item.includes('@R@')) {
|
|
63
|
+
const a = JSON.parse(item.split('@R@')[1])
|
|
64
|
+
return {
|
|
65
|
+
url: `${baseUrl}/v3/files${a.base.path}?@token=${token}&@programToken=${hydrocarbonProgramToken}&name=${a.base.fileName || ''}`,
|
|
66
|
+
name: a.base.fileName,
|
|
67
|
+
type: a.base.type,
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
const a = JSON.parse(item)
|
|
72
|
+
return {
|
|
73
|
+
url: `${baseUrl}/v3/files${a.base.path}?@token=${token}&@programToken=${hydrocarbonProgramToken}&name=${a.base.fileName || ''}`,
|
|
74
|
+
name: a.base.fileName,
|
|
75
|
+
type: a.base.type,
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
return [{
|
|
82
|
+
url: data.url,
|
|
83
|
+
name: data.name,
|
|
84
|
+
type: getAfterLastMatch(data.name, '.') || getAfterLastMatch(data.url, '.'),
|
|
85
|
+
}]
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return [{
|
|
89
|
+
url: 'https://',
|
|
90
|
+
name: '',
|
|
91
|
+
type: 'img',
|
|
92
|
+
}]
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return data
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* 高并发环境下也能保证唯一的ID生成器
|
|
100
|
+
* @returns 25字符的唯一字符串
|
|
101
|
+
*/
|
|
102
|
+
let counter = 0
|
|
103
|
+
export function generateHighResolutionID(): string {
|
|
104
|
+
const time = Date.now().toString(36)
|
|
105
|
+
const random = Math.random().toString(36).substring(2, 10)
|
|
106
|
+
counter = (counter + 1) % Number.MAX_SAFE_INTEGER
|
|
107
|
+
return `${time}-${random}-${counter.toString(36).padStart(6, '0')}`
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// 下载文件
|
|
111
|
+
export function downloadFile(url: string) {
|
|
112
|
+
console.log('下载', url)
|
|
113
|
+
uni.downloadFile({
|
|
114
|
+
url, // 文件的网络地址
|
|
115
|
+
success: (res) => {
|
|
116
|
+
if (res.statusCode === 200) {
|
|
117
|
+
console.log('下载成功', res.tempFilePath)
|
|
118
|
+
// 打开文件(如PDF、Word等)
|
|
119
|
+
uni.openDocument({
|
|
120
|
+
filePath: res.tempFilePath,
|
|
121
|
+
showMenu: true,
|
|
122
|
+
success: () => console.log('打开文件成功'),
|
|
123
|
+
})
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
console.error('下载失败', res)
|
|
127
|
+
uni.showToast({
|
|
128
|
+
title: '下载失败',
|
|
129
|
+
icon: 'none',
|
|
130
|
+
})
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
fail: (err) => {
|
|
134
|
+
console.error('下载失败', err)
|
|
135
|
+
uni.showToast({
|
|
136
|
+
title: '下载失败',
|
|
137
|
+
icon: 'none',
|
|
138
|
+
})
|
|
139
|
+
},
|
|
140
|
+
})
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* 字符串截取匹配最后一项(不包含匹配字符)
|
|
145
|
+
*
|
|
146
|
+
*
|
|
147
|
+
*/
|
|
148
|
+
function getAfterLastMatch(str: string, pattern: string) {
|
|
149
|
+
const lastIndex = str.lastIndexOf(pattern)
|
|
150
|
+
if (lastIndex === -1)
|
|
151
|
+
return null
|
|
152
|
+
return str.substring(lastIndex + pattern.length)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/****
|
|
156
|
+
*
|
|
157
|
+
* 根据comparator、defaultValue、val
|
|
158
|
+
* 判断按钮是否满足显示
|
|
159
|
+
*
|
|
160
|
+
*
|
|
161
|
+
*/
|
|
162
|
+
export function comparator(
|
|
163
|
+
comparator: Comparator,
|
|
164
|
+
val: string,
|
|
165
|
+
defaultValue: string,
|
|
166
|
+
): boolean {
|
|
167
|
+
let dvs: string[]
|
|
168
|
+
let value = val
|
|
169
|
+
if (val && val.indexOf && val.indexOf('@R@') > 0) {
|
|
170
|
+
const vs = val.split('@R@')
|
|
171
|
+
value = vs[1]
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (!comparator) {
|
|
175
|
+
comparator = 'equals'
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (comparator === 'none1') {
|
|
179
|
+
return !value || value === ''
|
|
180
|
+
}
|
|
181
|
+
else if (comparator === 'none1n') {
|
|
182
|
+
return !!value && value !== ''
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (!defaultValue) {
|
|
186
|
+
return true
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (!value && value !== '0') {
|
|
190
|
+
return false
|
|
191
|
+
}
|
|
192
|
+
let vals
|
|
193
|
+
switch (comparator) {
|
|
194
|
+
case 'contain':
|
|
195
|
+
return value.includes(defaultValue)
|
|
196
|
+
case 'equal':
|
|
197
|
+
case 'equals':
|
|
198
|
+
return isEqual(value, defaultValue)
|
|
199
|
+
case 'exclude':
|
|
200
|
+
dvs = toArray(defaultValue)
|
|
201
|
+
vals = toArray(value)
|
|
202
|
+
for (let i = 0; i < dvs.length; i++) {
|
|
203
|
+
for (let j = 0; j < vals.length; j++) {
|
|
204
|
+
if (dvs[i] === vals[j]) {
|
|
205
|
+
return false
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return true
|
|
210
|
+
case 'includeOne':
|
|
211
|
+
case 'include_anyone':
|
|
212
|
+
dvs = toArray(defaultValue)
|
|
213
|
+
vals = toArray(value)
|
|
214
|
+
for (let i = 0; i < dvs.length; i++) {
|
|
215
|
+
for (let j = 0; j < vals.length; j++) {
|
|
216
|
+
if (dvs[i] === vals[j]) {
|
|
217
|
+
return true
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return false
|
|
222
|
+
|
|
223
|
+
case 'include_all':
|
|
224
|
+
{
|
|
225
|
+
dvs = toArray(defaultValue)
|
|
226
|
+
vals = toArray(value)
|
|
227
|
+
let count = 0
|
|
228
|
+
const targetCount = dvs.length
|
|
229
|
+
for (let i = 0; i < dvs.length; i++) {
|
|
230
|
+
for (let j = 0; j < vals.length; j++) {
|
|
231
|
+
if (dvs[i] === vals[j]) {
|
|
232
|
+
count++
|
|
233
|
+
break
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return targetCount <= count
|
|
238
|
+
}
|
|
239
|
+
case 'Greater':
|
|
240
|
+
case 'greater':
|
|
241
|
+
if (!Number.isNaN(Number(value)) && !Number.isNaN(Number(defaultValue))) {
|
|
242
|
+
return Number(value) > Number(defaultValue)
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
return value > defaultValue
|
|
246
|
+
}
|
|
247
|
+
case 'Less':
|
|
248
|
+
case 'less':
|
|
249
|
+
if (!Number.isNaN(Number(value)) && !Number.isNaN(Number(defaultValue))) {
|
|
250
|
+
return Number(value) < Number(defaultValue)
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
return value < defaultValue
|
|
254
|
+
}
|
|
255
|
+
case 'noGreater':
|
|
256
|
+
case 'nogreater':
|
|
257
|
+
if (!Number.isNaN(Number(value)) && !Number.isNaN(Number(defaultValue))) {
|
|
258
|
+
return Number(value) <= Number(defaultValue)
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
return value <= defaultValue
|
|
262
|
+
}
|
|
263
|
+
case 'noLess':
|
|
264
|
+
case 'noless':
|
|
265
|
+
if (!Number.isNaN(Number(value)) && !Number.isNaN(Number(defaultValue))) {
|
|
266
|
+
return Number(value) >= Number(defaultValue)
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
return value >= defaultValue
|
|
270
|
+
}
|
|
271
|
+
case 'unequal':
|
|
272
|
+
case 'unequals':
|
|
273
|
+
return !isEqual(value, defaultValue)
|
|
274
|
+
default:
|
|
275
|
+
console.log(`暂不支持的比较关系:${comparator};请联系管理员添加。`)
|
|
276
|
+
return false // 兜底返回值
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function isEqual(value: string, defaultValue: string) {
|
|
281
|
+
if (defaultValue === '是' || defaultValue === '否' || defaultValue === 'true' || defaultValue === 'false') {
|
|
282
|
+
if (value === 'true' || value === '是') {
|
|
283
|
+
if (defaultValue === '是' || defaultValue === 'true') {
|
|
284
|
+
return true
|
|
285
|
+
}
|
|
286
|
+
else {
|
|
287
|
+
return false
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
else if (value === 'false' || value === '否') {
|
|
291
|
+
if (defaultValue === '否' || defaultValue === 'false') {
|
|
292
|
+
return true
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
return false
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
return false
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
else {
|
|
303
|
+
if (!Number.isNaN(Number(value)) && !Number.isNaN(Number(defaultValue))) {
|
|
304
|
+
return Number(value) === Number(defaultValue)
|
|
305
|
+
}
|
|
306
|
+
else {
|
|
307
|
+
return value === defaultValue
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function toArray(value: string) {
|
|
313
|
+
let val
|
|
314
|
+
if (typeof value == 'string') {
|
|
315
|
+
val = value.split(',')
|
|
316
|
+
}
|
|
317
|
+
else if (Array.isArray(value)) {
|
|
318
|
+
val = value
|
|
319
|
+
}
|
|
320
|
+
else {
|
|
321
|
+
val = [value]
|
|
322
|
+
}
|
|
323
|
+
return val
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* 根据目标颜色生成渐变色字符串
|
|
328
|
+
* @param color - 目标颜色,可以是十六进制颜色或RGB颜色
|
|
329
|
+
* @param angle - 渐变角度,默认为180deg
|
|
330
|
+
* @param startOpacity - 起始透明度,默认为1(不透明)
|
|
331
|
+
* @param endOpacity - 结束透明度,默认为0.3
|
|
332
|
+
* @returns 渐变色字符串
|
|
333
|
+
*/
|
|
334
|
+
export function generateGradientColor(
|
|
335
|
+
color: string,
|
|
336
|
+
angle: number = 180,
|
|
337
|
+
startOpacity: number = 1,
|
|
338
|
+
endOpacity: number = 0.3,
|
|
339
|
+
): string {
|
|
340
|
+
// 处理十六进制颜色转换为RGB
|
|
341
|
+
const hexToRgb = (hex: string): { r: number, g: number, b: number } | null => {
|
|
342
|
+
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
|
|
343
|
+
return result
|
|
344
|
+
? {
|
|
345
|
+
r: Number.parseInt(result[1], 16),
|
|
346
|
+
g: Number.parseInt(result[2], 16),
|
|
347
|
+
b: Number.parseInt(result[3], 16),
|
|
348
|
+
}
|
|
349
|
+
: null
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// 将RGB颜色转换为rgba格式
|
|
353
|
+
const toRgba = (color: string, opacity: number): string => {
|
|
354
|
+
if (color.startsWith('#')) {
|
|
355
|
+
const rgb = hexToRgb(color)
|
|
356
|
+
if (rgb) {
|
|
357
|
+
return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${opacity})`
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
else if (color.startsWith('rgb(')) {
|
|
361
|
+
// 处理rgb()格式
|
|
362
|
+
const rgbValues = color.match(/\d+/g)
|
|
363
|
+
if (rgbValues && rgbValues.length === 3) {
|
|
364
|
+
return `rgba(${rgbValues[0]}, ${rgbValues[1]}, ${rgbValues[2]}, ${opacity})`
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
// 如果已经是rgba或其他格式,直接返回带透明度的版本
|
|
368
|
+
return color.replace(/rgb\(/, 'rgba(').replace(/\)/, `, ${opacity})`)
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// 生成渐变色字符串
|
|
372
|
+
const startColor = color.startsWith('rgba') ? color : toRgba(color, startOpacity)
|
|
373
|
+
const endColor = toRgba(color, endOpacity)
|
|
374
|
+
|
|
375
|
+
return `linear-gradient(${angle}deg, ${startColor} 0%, ${endColor} 100%)`
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// 使用示例
|
|
379
|
+
// console.log(generateGradientColor('#2094FF'));
|
|
380
|
+
// 输出: linear-gradient(180deg, rgba(32,148,255,1) 0%, rgba(32,148,255,0.3) 100%)
|
|
381
|
+
|
|
382
|
+
// console.log(generateGradientColor('#2094FF', 90, 0.8, 0.1));
|
|
383
|
+
// 输出: linear-gradient(90deg, rgba(32,148,255,0.8) 0%, rgba(32,148,255,0.1) 100%)
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* 格式化日期显示
|
|
387
|
+
* @param date - 要格式化的日期,默认为当前日期
|
|
388
|
+
* @returns 格式化后的日期字符串,如"2025年9月25日 周四 下午"
|
|
389
|
+
*/
|
|
390
|
+
export function formatChineseDate(date: Date = new Date()): string {
|
|
391
|
+
// 年份
|
|
392
|
+
const year = date.getFullYear()
|
|
393
|
+
|
|
394
|
+
// 月份(月份从0开始,需要+1)
|
|
395
|
+
const month = date.getMonth() + 1
|
|
396
|
+
|
|
397
|
+
// 日期
|
|
398
|
+
const day = date.getDate()
|
|
399
|
+
|
|
400
|
+
// 星期几
|
|
401
|
+
const weekdays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
|
|
402
|
+
const weekday = weekdays[date.getDay()]
|
|
403
|
+
|
|
404
|
+
// 上午/下午判断
|
|
405
|
+
const hour = date.getHours()
|
|
406
|
+
const period = hour < 12 ? '上午' : '下午'
|
|
407
|
+
|
|
408
|
+
return `${year}年${month}月${day}日 ${weekday} ${period}`
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// 使用示例
|
|
412
|
+
// console.log(formatChineseDate(new Date(2025, 8, 25)));
|
|
413
|
+
// 输出: 2025年9月25日 周四 下午
|
|
414
|
+
|
|
415
|
+
// console.log(formatChineseDate(new Date(2025, 8, 25, 10, 30)));
|
|
416
|
+
// 输出: 2025年9月25日 周四 上午
|
|
417
|
+
|
|
418
|
+
// console.log(formatChineseDate());
|
|
419
|
+
// 输出当前日期的格式化结果
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* 淡化颜色
|
|
423
|
+
* @param color - 目标颜色,可以是十六进制颜色或RGB颜色
|
|
424
|
+
* @returns 淡化后的颜色字符串
|
|
425
|
+
*/
|
|
426
|
+
export function desaturateColor(color: string, opacity: number = 0.04): string {
|
|
427
|
+
// 处理十六进制颜色转换为RGB
|
|
428
|
+
const hexToRgb = (hex: string): { r: number, g: number, b: number } | null => {
|
|
429
|
+
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
|
|
430
|
+
return result
|
|
431
|
+
? {
|
|
432
|
+
r: Number.parseInt(result[1], 16),
|
|
433
|
+
g: Number.parseInt(result[2], 16),
|
|
434
|
+
b: Number.parseInt(result[3], 16),
|
|
435
|
+
}
|
|
436
|
+
: null
|
|
437
|
+
}
|
|
438
|
+
// 将RGB颜色转换为rgba格式
|
|
439
|
+
const toRgba = (color: string, opacity: number): string => {
|
|
440
|
+
if (color.startsWith('#')) {
|
|
441
|
+
const rgb = hexToRgb(color)
|
|
442
|
+
if (rgb) {
|
|
443
|
+
return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${opacity})`
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
else if (color.startsWith('rgb(')) {
|
|
447
|
+
// 处理rgb()格式
|
|
448
|
+
const rgbValues = color.match(/\d+/g)
|
|
449
|
+
if (rgbValues && rgbValues.length === 3) {
|
|
450
|
+
return `rgba(${rgbValues[0]}, ${rgbValues[1]}, ${rgbValues[2]}, ${opacity})`
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
// 如果已经是rgba或其他格式,直接返回带透明度的版本
|
|
454
|
+
return color.replace(/rgb\(/, 'rgba(').replace(/\)/, `, ${opacity})`)
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
return toRgba(color, opacity)
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* 扩展版本:判断字符串中是否包含多个字符中的任意一个
|
|
462
|
+
*/
|
|
463
|
+
export function containsAnyCharacter(str: string, chars: string[]): boolean {
|
|
464
|
+
if (typeof str !== 'string' || !Array.isArray(chars)) {
|
|
465
|
+
return false
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// 验证数组中的每个元素都是单个字符
|
|
469
|
+
for (const char of chars) {
|
|
470
|
+
if (typeof char !== 'string' || char.length !== 1) {
|
|
471
|
+
throw new Error('数组中的每个元素必须是单个字符')
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
return chars.some(char => str.includes(char))
|
|
476
|
+
}
|
|
477
|
+
// 测试用例
|
|
478
|
+
// console.log(containsAnyCharacter("hello world", ["a", "e", "i", "o", "u"])); // true (包含o)
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* 颜色转换和生成工具类 (针对十六进制颜色优化)
|
|
482
|
+
*/
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* 十六进制颜色转换为RGB对象
|
|
486
|
+
* @param {string} hex - 十六进制颜色值,如 #2164F3 或 2164F3
|
|
487
|
+
* @returns {object} RGB对象 {r, g, b}
|
|
488
|
+
*/
|
|
489
|
+
export function hexToRgb(hex: string) {
|
|
490
|
+
// 移除 # 号
|
|
491
|
+
hex = hex.replace(/^#/, '')
|
|
492
|
+
|
|
493
|
+
// 处理简写形式如 #FFF
|
|
494
|
+
if (hex.length === 3) {
|
|
495
|
+
hex = hex.split('').map(c => c + c).join('')
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// 解析十六进制值
|
|
499
|
+
const bigint = Number.parseInt(hex, 16)
|
|
500
|
+
|
|
501
|
+
return {
|
|
502
|
+
r: (bigint >> 16) & 255,
|
|
503
|
+
g: (bigint >> 8) & 255,
|
|
504
|
+
b: bigint & 255,
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* RGB对象转换为十六进制颜色
|
|
510
|
+
* @param {object} rgb - RGB对象 {r, g, b}
|
|
511
|
+
* @returns {string} 十六进制颜色值
|
|
512
|
+
*/
|
|
513
|
+
export function rgbToHex(rgb: { r: any, g: any, b: any }) {
|
|
514
|
+
const toHex = (c: { toString: (arg0: number) => any }) => {
|
|
515
|
+
const hex = c.toString(16)
|
|
516
|
+
return hex.length === 1 ? `0${hex}` : hex
|
|
517
|
+
}
|
|
518
|
+
return `#${toHex(rgb.r)}${toHex(rgb.g)}${toHex(rgb.b)}`.toUpperCase()
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* 将 RGB 颜色转换为 HSL 颜色模型
|
|
523
|
+
* @param {number} r - 红色值 (0-255)
|
|
524
|
+
* @param {number} g - 绿色值 (0-255)
|
|
525
|
+
* @param {number} b - 蓝色值 (0-255)
|
|
526
|
+
* @returns {object} HSL对象 {h, s, l}
|
|
527
|
+
*/
|
|
528
|
+
export function rgbToHsl(r: number, g: number, b: number) {
|
|
529
|
+
r /= 255
|
|
530
|
+
g /= 255
|
|
531
|
+
b /= 255
|
|
532
|
+
|
|
533
|
+
const max = Math.max(r, g, b)
|
|
534
|
+
const min = Math.min(r, g, b)
|
|
535
|
+
let h
|
|
536
|
+
let s
|
|
537
|
+
const l = (max + min) / 2
|
|
538
|
+
|
|
539
|
+
if (max === min) {
|
|
540
|
+
h = s = 0
|
|
541
|
+
}
|
|
542
|
+
else {
|
|
543
|
+
const d = max - min
|
|
544
|
+
s = l > 0.5 ? d / (2 - max - min) : d / (max + min)
|
|
545
|
+
|
|
546
|
+
switch (max) {
|
|
547
|
+
case r: h = (g - b) / d + (g < b ? 6 : 0)
|
|
548
|
+
break
|
|
549
|
+
case g: h = (b - r) / d + 2
|
|
550
|
+
break
|
|
551
|
+
case b: h = (r - g) / d + 4
|
|
552
|
+
break
|
|
553
|
+
default: h = 0 // 添加默认情况,确保h被初始化
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
h /= 6
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
return {
|
|
560
|
+
h: Math.round(h * 360),
|
|
561
|
+
s: Math.round(s * 100),
|
|
562
|
+
l: Math.round(l * 100),
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* 将 HSL 颜色转换为 RGB 颜色
|
|
568
|
+
* @param {number} h - 色相 (0-360)
|
|
569
|
+
* @param {number} s - 饱和度 (0-100)
|
|
570
|
+
* @param {number} l - 亮度 (0-100)
|
|
571
|
+
* @returns {object} RGB对象 {r, g, b}
|
|
572
|
+
*/
|
|
573
|
+
export function hslToRgb(h: number, s: number = 50, l: number = 50): { r: number, g: number, b: number } {
|
|
574
|
+
h = h !== undefined ? h / 360 : 0
|
|
575
|
+
s = s !== undefined ? s / 100 : 0.5
|
|
576
|
+
l = l !== undefined ? l / 100 : 0.5
|
|
577
|
+
|
|
578
|
+
let r, g, b
|
|
579
|
+
|
|
580
|
+
if (s === 0) {
|
|
581
|
+
r = g = b = l
|
|
582
|
+
}
|
|
583
|
+
else {
|
|
584
|
+
const hue2rgb = (p: number, q: number, t: number) => {
|
|
585
|
+
if (t < 0)
|
|
586
|
+
t += 1
|
|
587
|
+
if (t > 1)
|
|
588
|
+
t -= 1
|
|
589
|
+
if (t < 1 / 6)
|
|
590
|
+
return p + (q - p) * 6 * t
|
|
591
|
+
if (t < 1 / 2)
|
|
592
|
+
return q
|
|
593
|
+
if (t < 2 / 3)
|
|
594
|
+
return p + (q - p) * (2 / 3 - t) * 6
|
|
595
|
+
return p
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
const q = l < 0.5 ? l * (1 + s) : l + s - l * s
|
|
599
|
+
const p = 2 * l - q
|
|
600
|
+
|
|
601
|
+
r = hue2rgb(p, q, h + 1 / 3)
|
|
602
|
+
g = hue2rgb(p, q, h)
|
|
603
|
+
b = hue2rgb(p, q, h - 1 / 3)
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
return {
|
|
607
|
+
r: Math.round(r * 255),
|
|
608
|
+
g: Math.round(g * 255),
|
|
609
|
+
b: Math.round(b * 255),
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* 基于主色调生成随机颜色
|
|
615
|
+
* @param {string} baseHex - 基础十六进制颜色,如 #2164F3
|
|
616
|
+
* @param {object} options - 配置选项
|
|
617
|
+
* @returns {object} 颜色对象
|
|
618
|
+
*/
|
|
619
|
+
export function generateRandomColorFromHex(baseHex: string, options = { hueRange: 15, saturationRange: 20, lightnessRange: 20 }) {
|
|
620
|
+
const {
|
|
621
|
+
hueRange = 15,
|
|
622
|
+
saturationRange = 20,
|
|
623
|
+
lightnessRange = 20,
|
|
624
|
+
} = options
|
|
625
|
+
|
|
626
|
+
// 转换为RGB
|
|
627
|
+
const baseRgb = hexToRgb(baseHex)
|
|
628
|
+
|
|
629
|
+
// 转换为HSL
|
|
630
|
+
const baseHsl = rgbToHsl(baseRgb.r, baseRgb.g, baseRgb.b)
|
|
631
|
+
|
|
632
|
+
// 在指定范围内随机调整HSL值
|
|
633
|
+
const randomH = baseHsl.h + (Math.random() * 2 - 1) * hueRange
|
|
634
|
+
const randomS = baseHsl.s + (Math.random() * 2 - 1) * saturationRange
|
|
635
|
+
const randomL = baseHsl.l + (Math.random() * 2 - 1) * lightnessRange
|
|
636
|
+
|
|
637
|
+
// 确保值在有效范围内
|
|
638
|
+
const h = Math.max(0, Math.min(360, randomH))
|
|
639
|
+
const s = Math.max(0, Math.min(100, randomS))
|
|
640
|
+
const l = Math.max(0, Math.min(100, randomL))
|
|
641
|
+
|
|
642
|
+
// 转换回RGB
|
|
643
|
+
const rgb = hslToRgb(h, s, l)
|
|
644
|
+
const hex = rgbToHex(rgb)
|
|
645
|
+
|
|
646
|
+
return {
|
|
647
|
+
hex,
|
|
648
|
+
rgb,
|
|
649
|
+
hsl: { h: Math.round(h), s: Math.round(s), l: Math.round(l) },
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* 批量生成协调的颜色
|
|
655
|
+
* @param {string} baseHex - 基础十六进制颜色
|
|
656
|
+
* @param {number} count - 需要生成的颜色数量
|
|
657
|
+
* @param {object} options - 配置选项
|
|
658
|
+
* @returns {Array} 颜色数组
|
|
659
|
+
*/
|
|
660
|
+
export function generateHarmoniousColors(baseHex: string, count: number, options = { hueRange: 15, saturationRange: 20, lightnessRange: 20, colorType: 'all' }) {
|
|
661
|
+
const colors = []
|
|
662
|
+
const baseRgb = hexToRgb(baseHex)
|
|
663
|
+
const baseHsl = rgbToHsl(baseRgb.r, baseRgb.g, baseRgb.b)
|
|
664
|
+
|
|
665
|
+
const {
|
|
666
|
+
hueRange = 15,
|
|
667
|
+
saturationRange = 20,
|
|
668
|
+
lightnessRange = 20,
|
|
669
|
+
// 新增:颜色类型,可选值:'all', 'light', 'dark', 'muted'
|
|
670
|
+
colorType = 'all',
|
|
671
|
+
} = options
|
|
672
|
+
|
|
673
|
+
// 使用let声明,允许重新赋值
|
|
674
|
+
let adjustedSaturationRange = saturationRange
|
|
675
|
+
|
|
676
|
+
// 根据颜色类型调整基础亮度
|
|
677
|
+
let baseLightness = baseHsl.l
|
|
678
|
+
if (colorType === 'light') {
|
|
679
|
+
baseLightness = Math.min(80, baseHsl.l + 20)
|
|
680
|
+
}
|
|
681
|
+
else if (colorType === 'dark') {
|
|
682
|
+
baseLightness = Math.max(20, baseHsl.l - 20)
|
|
683
|
+
}
|
|
684
|
+
else if (colorType === 'muted') {
|
|
685
|
+
adjustedSaturationRange = Math.min(adjustedSaturationRange, 15)
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
for (let i = 0; i < count; i++) {
|
|
689
|
+
// 在指定范围内随机调整HSL值
|
|
690
|
+
const randomH = baseHsl.h + (Math.random() * 2 - 1) * hueRange
|
|
691
|
+
const randomS = baseHsl.s + (Math.random() * 2 - 1) * adjustedSaturationRange
|
|
692
|
+
const randomL = baseLightness + (Math.random() * 2 - 1) * lightnessRange
|
|
693
|
+
|
|
694
|
+
// 确保值在有效范围内
|
|
695
|
+
const h = Math.max(0, Math.min(360, randomH))
|
|
696
|
+
const s = Math.max(0, Math.min(100, randomS))
|
|
697
|
+
const l = Math.max(0, Math.min(100, randomL))
|
|
698
|
+
|
|
699
|
+
// 转换回RGB
|
|
700
|
+
const rgb = hslToRgb(h, s, l)
|
|
701
|
+
const hex = rgbToHex(rgb)
|
|
702
|
+
|
|
703
|
+
colors.push({
|
|
704
|
+
hex,
|
|
705
|
+
rgb,
|
|
706
|
+
hsl: { h: Math.round(h), s: Math.round(s), l: Math.round(l) },
|
|
707
|
+
rgbString: `rgb(${rgb.r}, ${rgb.g}, ${rgb.b})`,
|
|
708
|
+
})
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
return colors
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/**
|
|
715
|
+
* 生成一组完美的图标颜色(智能算法)
|
|
716
|
+
* @param {string} baseHex - 基础颜色
|
|
717
|
+
* @param {number} count - 图标数量
|
|
718
|
+
* @returns {Array} 优化后的颜色数组
|
|
719
|
+
*/
|
|
720
|
+
export function generateIconColors(baseHex: string, count: number) {
|
|
721
|
+
const baseRgb = hexToRgb(baseHex)
|
|
722
|
+
const baseHsl = rgbToHsl(baseRgb.r, baseRgb.g, baseRgb.b)
|
|
723
|
+
const colors = []
|
|
724
|
+
|
|
725
|
+
// 根据数量调整色相分布
|
|
726
|
+
const hueStep = 360 / (count + 1)
|
|
727
|
+
|
|
728
|
+
for (let i = 0; i < count; i++) {
|
|
729
|
+
// 创建更均匀的色相分布
|
|
730
|
+
let hue = (baseHsl.h + (i * hueStep)) % 360
|
|
731
|
+
|
|
732
|
+
// 保持主色调附近有一些颜色
|
|
733
|
+
if (i < 3) {
|
|
734
|
+
hue = baseHsl.h + (i - 1) * 10
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// 智能调整饱和度和亮度
|
|
738
|
+
// 重要图标更鲜艳,次要图标更柔和
|
|
739
|
+
const saturation = Math.max(60, Math.min(90, baseHsl.s + (Math.random() * 20 - 10),
|
|
740
|
+
))
|
|
741
|
+
|
|
742
|
+
// 创建对比度:一些更亮,一些更暗
|
|
743
|
+
const lightness = i % 2 === 0
|
|
744
|
+
? Math.min(70, baseHsl.l + 15 + Math.random() * 10)
|
|
745
|
+
: Math.max(30, baseHsl.l - 10 + Math.random() * 10)
|
|
746
|
+
|
|
747
|
+
const rgb = hslToRgb(hue, saturation, lightness)
|
|
748
|
+
const hex = rgbToHex(rgb)
|
|
749
|
+
|
|
750
|
+
colors.push({
|
|
751
|
+
hex,
|
|
752
|
+
rgb,
|
|
753
|
+
hsl: { h: Math.round(hue), s: Math.round(saturation), l: Math.round(lightness) },
|
|
754
|
+
rgbString: `rgb(${rgb.r}, ${rgb.g}, ${rgb.b})`,
|
|
755
|
+
})
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
return colors
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
/**
|
|
762
|
+
* 检查颜色对比度是否足够(用于文本可读性)
|
|
763
|
+
* @param {string} hexColor - 十六进制颜色
|
|
764
|
+
* @param {string} backgroundColor - 背景十六进制颜色,默认白色
|
|
765
|
+
* @returns {number} 对比度比率
|
|
766
|
+
*/
|
|
767
|
+
export function getContrastRatio(hexColor: string, backgroundColor = '#FFFFFF') {
|
|
768
|
+
const rgb1 = hexToRgb(hexColor)
|
|
769
|
+
const rgb2 = hexToRgb(backgroundColor)
|
|
770
|
+
|
|
771
|
+
const luminance1 = getLuminance(rgb1)
|
|
772
|
+
const luminance2 = getLuminance(rgb2)
|
|
773
|
+
|
|
774
|
+
const lighter = Math.max(luminance1, luminance2)
|
|
775
|
+
const darker = Math.min(luminance1, luminance2)
|
|
776
|
+
|
|
777
|
+
return (lighter + 0.05) / (darker + 0.05)
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
/**
|
|
781
|
+
* 计算颜色亮度
|
|
782
|
+
* @param {object} rgb - RGB对象
|
|
783
|
+
* @returns {number} 亮度值
|
|
784
|
+
*/
|
|
785
|
+
function getLuminance(rgb: { r: number, g: number, b: number }) {
|
|
786
|
+
const [r, g, b] = [rgb.r, rgb.g, rgb.b].map((v) => {
|
|
787
|
+
v /= 255
|
|
788
|
+
return v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4
|
|
789
|
+
})
|
|
790
|
+
|
|
791
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
/**
|
|
795
|
+
* 生成颜色变体(浅色、深色等)
|
|
796
|
+
* @param {string} hex - 基础颜色
|
|
797
|
+
* @param {string} variant - 变体类型:'lighter', 'light', 'dark', 'darker', 'muted'
|
|
798
|
+
* @returns {string} 变体颜色的十六进制值
|
|
799
|
+
*/
|
|
800
|
+
export function getColorVariant(hex: string, variant = 'light') {
|
|
801
|
+
const rgb = hexToRgb(hex)
|
|
802
|
+
const hsl = rgbToHsl(rgb.r, rgb.g, rgb.b)
|
|
803
|
+
|
|
804
|
+
switch (variant) {
|
|
805
|
+
case 'lighter':
|
|
806
|
+
hsl.l = Math.min(95, hsl.l + 40)
|
|
807
|
+
hsl.s = Math.max(30, hsl.s - 10)
|
|
808
|
+
break
|
|
809
|
+
case 'light':
|
|
810
|
+
hsl.l = Math.min(85, hsl.l + 30)
|
|
811
|
+
hsl.s = Math.max(40, hsl.s - 15)
|
|
812
|
+
break
|
|
813
|
+
case 'dark':
|
|
814
|
+
hsl.l = Math.max(20, hsl.l - 20)
|
|
815
|
+
hsl.s = Math.min(80, hsl.s + 10)
|
|
816
|
+
break
|
|
817
|
+
case 'darker':
|
|
818
|
+
hsl.l = Math.max(10, hsl.l - 30)
|
|
819
|
+
hsl.s = Math.min(70, hsl.s + 20)
|
|
820
|
+
break
|
|
821
|
+
case 'muted':
|
|
822
|
+
hsl.s = Math.max(20, hsl.s - 30)
|
|
823
|
+
break
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
const variantRgb = hslToRgb(hsl.h, hsl.s, hsl.l)
|
|
827
|
+
return rgbToHex(variantRgb)
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
/**
|
|
831
|
+
*
|
|
832
|
+
*
|
|
833
|
+
* 获取字符串的第一个字符
|
|
834
|
+
* @param {string} str - 输入的字符串
|
|
835
|
+
* @returns {string} 字符串的第一个字符
|
|
836
|
+
*/
|
|
837
|
+
export function getFirstChar(str: string) {
|
|
838
|
+
return str.charAt(0)
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
// 生成七彩虹范围内的任意颜色
|
|
842
|
+
/**
|
|
843
|
+
* 七彩虹颜色范围(HSL表示)
|
|
844
|
+
* 每个颜色范围:色相(H)范围,饱和度(S)固定,亮度(L)固定
|
|
845
|
+
* 选择较高的饱和度(70-80%)和中等亮度(45-55%)以确保白色文字可见
|
|
846
|
+
*/
|
|
847
|
+
const COLOR_RANGES = [
|
|
848
|
+
{ name: '红', hueRange: [0, 10], saturation: 75, lightness: 50 },
|
|
849
|
+
{ name: '橙', hueRange: [20, 40], saturation: 75, lightness: 50 },
|
|
850
|
+
{ name: '黄', hueRange: [45, 60], saturation: 75, lightness: 45 }, // 黄色稍微暗一点以保证白色文字可见
|
|
851
|
+
{ name: '绿', hueRange: [90, 140], saturation: 70, lightness: 45 },
|
|
852
|
+
{ name: '青', hueRange: [160, 190], saturation: 70, lightness: 50 },
|
|
853
|
+
{ name: '蓝', hueRange: [200, 240], saturation: 75, lightness: 45 },
|
|
854
|
+
{ name: '紫', hueRange: [260, 300], saturation: 70, lightness: 50 },
|
|
855
|
+
{ name: '粉', hueRange: [310, 340], saturation: 70, lightness: 55 },
|
|
856
|
+
] as const
|
|
857
|
+
|
|
858
|
+
type ColorRange = typeof COLOR_RANGES[number]
|
|
859
|
+
|
|
860
|
+
/**
|
|
861
|
+
* 将HSL转换为十六进制颜色
|
|
862
|
+
*/
|
|
863
|
+
function hslToHex(h: number, s: number, l: number): string {
|
|
864
|
+
l /= 100
|
|
865
|
+
const a = s * Math.min(l, 1 - l) / 100
|
|
866
|
+
const f = (n: number) => {
|
|
867
|
+
const k = (n + h / 30) % 12
|
|
868
|
+
const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1)
|
|
869
|
+
return Math.round(255 * color).toString(16).padStart(2, '0')
|
|
870
|
+
}
|
|
871
|
+
return `#${f(0)}${f(8)}${f(4)}`
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
/**
|
|
875
|
+
* 检查颜色是否为暗色系
|
|
876
|
+
* 通过HSL的亮度判断,亮度低于30%的被认为是暗色
|
|
877
|
+
*/
|
|
878
|
+
function isDarkColor(h: number, s: number, l: number): boolean {
|
|
879
|
+
return l < 30
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
/**
|
|
883
|
+
* 七彩虹颜色生成器类
|
|
884
|
+
*/
|
|
885
|
+
export class RainbowColorGenerator {
|
|
886
|
+
private lastColorRange: ColorRange | null = null
|
|
887
|
+
private lastColorHex: string | null = null
|
|
888
|
+
|
|
889
|
+
/**
|
|
890
|
+
* 生成随机颜色值
|
|
891
|
+
*/
|
|
892
|
+
private generateRandomColorFromRange(range: ColorRange): string {
|
|
893
|
+
const { hueRange, saturation, lightness } = range
|
|
894
|
+
const hue = Math.floor(Math.random() * (hueRange[1] - hueRange[0] + 1)) + hueRange[0]
|
|
895
|
+
|
|
896
|
+
// 确保颜色不是暗色系
|
|
897
|
+
if (isDarkColor(hue, saturation, lightness)) {
|
|
898
|
+
// 如果亮度太低,增加亮度到安全范围
|
|
899
|
+
return hslToHex(hue, saturation, Math.max(lightness, 40))
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
return hslToHex(hue, saturation, lightness)
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
/**
|
|
906
|
+
* 检查两个颜色是否属于同一个色系
|
|
907
|
+
*/
|
|
908
|
+
private isSameColorRange(range1: ColorRange, range2: ColorRange): boolean {
|
|
909
|
+
return range1.name === range2.name
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
/**
|
|
913
|
+
* 获取可用的颜色范围(排除上一个使用的颜色范围)
|
|
914
|
+
*/
|
|
915
|
+
private getAvailableRanges(): ColorRange[] {
|
|
916
|
+
if (!this.lastColorRange) {
|
|
917
|
+
return [...COLOR_RANGES]
|
|
918
|
+
}
|
|
919
|
+
return COLOR_RANGES.filter(range => !this.isSameColorRange(range, this.lastColorRange!))
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
/**
|
|
923
|
+
* 获取下一个随机颜色
|
|
924
|
+
* @returns 十六进制颜色字符串
|
|
925
|
+
*/
|
|
926
|
+
getNextColor(): string {
|
|
927
|
+
const availableRanges = this.getAvailableRanges()
|
|
928
|
+
|
|
929
|
+
// 随机选择一个颜色范围
|
|
930
|
+
const randomIndex = Math.floor(Math.random() * availableRanges.length)
|
|
931
|
+
const selectedRange = availableRanges[randomIndex]
|
|
932
|
+
|
|
933
|
+
// 生成颜色
|
|
934
|
+
const color = this.generateRandomColorFromRange(selectedRange)
|
|
935
|
+
|
|
936
|
+
// 更新上一个颜色
|
|
937
|
+
this.lastColorRange = selectedRange
|
|
938
|
+
this.lastColorHex = color
|
|
939
|
+
|
|
940
|
+
return color
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
/**
|
|
944
|
+
* 重置生成器状态
|
|
945
|
+
*/
|
|
946
|
+
reset(): void {
|
|
947
|
+
this.lastColorRange = null
|
|
948
|
+
this.lastColorHex = null
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
/**
|
|
952
|
+
* 获取上一个生成的颜色
|
|
953
|
+
*/
|
|
954
|
+
getLastColor(): string | null {
|
|
955
|
+
return this.lastColorHex
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
/**
|
|
959
|
+
* 格式化手机号
|
|
960
|
+
* @param phone 手机号字符串
|
|
961
|
+
* @param mask 隐藏码,默认为****
|
|
962
|
+
* @returns 格式化后的手机号字符串
|
|
963
|
+
*/
|
|
964
|
+
export function formatPhone(phone: string, mask = '****') {
|
|
965
|
+
if (!phone) return '';
|
|
966
|
+
|
|
967
|
+
const cleanPhone = phone.replace(/\D/g, '');
|
|
968
|
+
const len = cleanPhone.length;
|
|
969
|
+
|
|
970
|
+
// 根据号码长度决定显示方式
|
|
971
|
+
if (len === 11) {
|
|
972
|
+
return cleanPhone.slice(0, 3) + mask + cleanPhone.slice(-4);
|
|
973
|
+
} else if (len === 8) {
|
|
974
|
+
// 座机号码等短号码
|
|
975
|
+
return cleanPhone.slice(0, 2) + mask + cleanPhone.slice(-2);
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
return phone;
|
|
979
|
+
}
|