@ithinkdt/ui 4.0.0-10 → 4.0.0-12

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/src/page.jsx ADDED
@@ -0,0 +1,626 @@
1
+ import { format } from 'date-fns'
2
+ import {
3
+ NButton, NCheckbox, NColorPicker, NDatePicker, NDrawer, NDrawerContent, NFlex, NInput,
4
+ NInputNumber, NModal, NScrollbar, NSelect, NText, NUpload,
5
+ useMessage,
6
+ } from 'ithinkdt-ui'
7
+ import { defineComponent, h, mergeProps, unref } from 'vue'
8
+
9
+ import { useDict, useDictMap } from '@ithinkdt/common/dict'
10
+
11
+ import { DataForm, DtDeptRender, DtUserDept, DtUserRender, NCheckboxes, NRadios } from './components'
12
+ import { useI18n } from './use-i18n.js'
13
+
14
+ const mapProps = (props) => {
15
+ return Object.fromEntries(Object.entries(props || {}).map(([prop, value]) => [prop, unref(value)]))
16
+ }
17
+ export function createPageFormHelper({
18
+ getUserGroups, getUsersByGroup, getUsersByDept, getUsersByUsername, uploadFile,
19
+ }) {
20
+ const SimpleUpload = defineComponent({
21
+ name: 'SimpleUpload',
22
+ props: {
23
+ type: { type: String, default: 'file' }, // file | image
24
+ multiple: { type: Boolean, default: false },
25
+ max: { type: Number, default: undefined },
26
+ accept: { type: String, default: undefined },
27
+ maxSize: { type: Number, default: undefined }, // MB
28
+ disabled: { type: Boolean, default: undefined },
29
+ fileList: { type: Array, default: () => [] },
30
+ onUpdateFileList: { type: [Array, Function] },
31
+ },
32
+ setup(props, { slots }) {
33
+ const { t } = useI18n()
34
+
35
+ const message = useMessage()
36
+ const customRequest = computed(() => props.customRequest || (
37
+ async ({ file, onProgress, onFinish, onError }) => {
38
+ uploadFile(file, percent => onProgress({ percent }))
39
+ .then((id) => {
40
+ file.file.fileId = id
41
+ onFinish()
42
+ }).catch((error) => {
43
+ message.success(error.message)
44
+ onError(error)
45
+ })
46
+ }
47
+ ))
48
+ return () => {
49
+ const { type, onUpdateFileList, ...props0 } = props
50
+ return (
51
+ <NUpload
52
+ {...props0}
53
+ onFinish={({ file, event }) => {
54
+ props0.onFinish?.({ file, event })
55
+ return {
56
+ ...file,
57
+ id: file.file.fileId,
58
+ }
59
+ }}
60
+ customRequest={customRequest.value}
61
+ listType={type === 'image' ? 'image-card' : 'text'}
62
+ accept={props0.accept ?? type === 'image' ? 'image/*' : undefined}
63
+ onUpdate:fileList={onUpdateFileList}
64
+ >
65
+ {{
66
+ default: () => <NButton disabled={props.disabled}>{t('common.page.form.selectFileText')}</NButton>,
67
+ ...slots,
68
+ }}
69
+ </NUpload>
70
+ )
71
+ }
72
+ },
73
+ })
74
+
75
+ return {
76
+ input: () => (
77
+ { slots, props },
78
+ { modelValue, 'onUpdate:modelValue': onUpdateValue, required, readonly, ...params },
79
+ ) => {
80
+ if (readonly) return <NText depth={2} style="line-height: 1.25">{modelValue ?? ''}</NText>
81
+
82
+ props = mapProps(props)
83
+ return h(NInput, {
84
+ 'clearable': true,
85
+ ...props,
86
+ ...params,
87
+ 'value': modelValue,
88
+ 'onUpdate:value': onUpdateValue,
89
+ }, slots)
90
+ },
91
+ number: () => (
92
+ { slots, props },
93
+ { modelValue, 'onUpdate:modelValue': onUpdateValue, required, readonly, ...params },
94
+ ) => {
95
+ if (readonly) return <NText depth={2} style="line-height: 1.25">{modelValue ?? ''}</NText>
96
+ props = mapProps(props)
97
+ return h(NInputNumber, {
98
+ 'clearable': true,
99
+ ...props,
100
+ ...params,
101
+ 'value': modelValue,
102
+ 'onUpdate:value': onUpdateValue,
103
+ }, slots)
104
+ },
105
+ select: () => {
106
+ let dictType0, options0
107
+ return (
108
+ { slots, props: props0 },
109
+ { modelValue, 'onUpdate:modelValue': onUpdateValue, required, readonly, ...params },
110
+ ) => {
111
+ const { dictType, options, ...props } = mapProps(props0)
112
+ if (options) {
113
+ options0 = options
114
+ } else {
115
+ if (dictType0 !== dictType) {
116
+ dictType0 = dictType
117
+ if (!options0 && dictType0) {
118
+ options0 = useDict(dictType0)
119
+ }
120
+ }
121
+ }
122
+ if (readonly) {
123
+ if (props.multiple) {
124
+ const items = modelValue ? options0?.filter(item => modelValue.includes(item[props.valueField || 'value'])) : []
125
+ return (
126
+ <NText depth={2} style="line-height: 1.25">
127
+ {
128
+ items.map((it, i, arr) => {
129
+ const vn = props.renderLabel ? props.renderLabel(it) ?? '' : it[props.labelField || 'label'] ?? ''
130
+ return (
131
+ <span key={it[props.valueField || 'value']}>
132
+ {vn}
133
+ {i < arr.length - 1 ? ', ' : ''}
134
+ </span>
135
+ )
136
+ })
137
+ }
138
+ </NText>
139
+ )
140
+ } else {
141
+ const it = options0?.find(item => item[props.valueField || 'value'] === modelValue)
142
+ return <NText depth={2} style="line-height: 1.25">{it ? props.renderLabel ? props.renderLabel(it) ?? '' : it[props.labelField || 'label'] ?? '' : ''}</NText>
143
+ }
144
+ }
145
+
146
+ return h(NSelect, {
147
+ 'clearable': true,
148
+ 'loading': options0?.loading,
149
+ ...props,
150
+ 'options': unref(options0),
151
+ ...params,
152
+ 'value': modelValue,
153
+ 'onUpdate:value': onUpdateValue,
154
+ }, slots)
155
+ }
156
+ },
157
+ checkbox: () => (
158
+ { slots, props },
159
+ { modelValue, 'onUpdate:modelValue': onUpdateValue, required, readonly, ...params },
160
+ ) => {
161
+ props = mapProps(props)
162
+ if (readonly) {
163
+ let content
164
+ if (modelValue === (props.checkedValue ?? true)) {
165
+ content = slots?.checked
166
+ ? slots.checked()
167
+ : <NCheckbox checked />
168
+ } else {
169
+ content = slots?.unchecked ? slots.unchecked() : <NCheckbox disabled />
170
+ }
171
+ return <NText depth={2} style="line-height: 1.25">{content}</NText>
172
+ }
173
+
174
+ return h(NCheckbox, {
175
+ ...props,
176
+ ...params,
177
+ 'checked': modelValue,
178
+ 'onUpdate:checked': onUpdateValue,
179
+ }, slots)
180
+ },
181
+ checkboxes: () => {
182
+ let dictType0, options0
183
+ return (
184
+ { props: props0 },
185
+ { modelValue, required, readonly, ...params },
186
+ ) => {
187
+ const { dictType, options, ...props } = mapProps(props0)
188
+ if (options) {
189
+ options0 = options
190
+ } else {
191
+ if (dictType0 !== dictType) {
192
+ dictType0 = dictType
193
+ if (!options0 && dictType0) {
194
+ options0 = useDict(dictType0)
195
+ }
196
+ }
197
+ }
198
+ if (readonly) {
199
+ const items = options0?.filter(item => modelValue.includes(item[props.valueField ?? 'value']))
200
+ return (
201
+ <NText depth={2} style="line-height: 2.4">
202
+ {
203
+ items.map((it, i, arr) => {
204
+ const vn = it[props.labelField ?? 'label'] ?? ''
205
+ return (
206
+ <span key={it[props.valueField ?? 'value']}>
207
+ {vn}
208
+ {i < arr.length - 1 ? ', ' : ''}
209
+ </span>
210
+ )
211
+ })
212
+ }
213
+ </NText>
214
+ )
215
+ }
216
+
217
+ return h(NCheckboxes,
218
+ {
219
+ ...props,
220
+ ...params,
221
+ modelValue,
222
+ },
223
+ )
224
+ }
225
+ },
226
+ radios: () => {
227
+ let dictType0, options0
228
+ return (
229
+ { props: props0 },
230
+ { modelValue, required, readonly, ...params },
231
+ ) => {
232
+ const { dictType, options, ...props } = mapProps(props0)
233
+ if (options) {
234
+ options0 = options
235
+ } else {
236
+ if (dictType0 !== dictType) {
237
+ dictType0 = dictType
238
+ if (!options0 && dictType0) {
239
+ options0 = useDict(dictType0)
240
+ }
241
+ }
242
+ }
243
+
244
+ if (readonly) {
245
+ const it = options0?.find(item => item[props.valueField ?? 'value'] === modelValue)
246
+ return <NText depth={2} style="line-height: 1.25">{it?.[props.labelField ?? 'label'] ?? ''}</NText>
247
+ }
248
+
249
+ return h(
250
+ NRadios,
251
+ {
252
+ ...props,
253
+ ...params,
254
+ modelValue,
255
+ },
256
+ )
257
+ }
258
+ },
259
+ datepicker: () => (
260
+ { slots, props },
261
+ { modelValue, 'onUpdate:modelValue': onUpdateValue, required, readonly, ...params },
262
+ ) => {
263
+ const { type = 'date', format: formatter = type.startsWith('datetime') ? 'yyyy-MM-dd HH:mm:ss' : 'yyyy-MM-dd', ...props2 } = mapProps(props)
264
+ if (readonly) {
265
+ if (type.endsWith('range')) {
266
+ return (
267
+ <NText depth={2} style="line-height: 1.25">
268
+ {modelValue
269
+ ? `${modelValue[0] ? format(modelValue[0], formatter) : ''
270
+ } ~ ${modelValue[1] ? format(modelValue[1], formatter) : ''}`
271
+ : ''}
272
+ </NText>
273
+ )
274
+ }
275
+ return (
276
+ <NText depth={2} style="line-height: 1.25">
277
+ {modelValue ? format(modelValue, formatter) : ''}
278
+ </NText>
279
+ )
280
+ }
281
+
282
+ return h(NDatePicker, {
283
+ 'clearable': true,
284
+ ...props2,
285
+ ...params,
286
+ type,
287
+ 'format': formatter,
288
+ 'value': modelValue,
289
+ 'onUpdate:value': onUpdateValue,
290
+ }, slots)
291
+ },
292
+ file: () => {
293
+ return (
294
+ { slots, props },
295
+ { modelValue, 'onUpdate:modelValue': onUpdateValue, required, readonly, ...params },
296
+ ) => {
297
+ props = mapProps(props)
298
+ if (readonly) {
299
+ if (modelValue.length === 0) return
300
+ return (
301
+ <NFlex gap="8" wrap>
302
+ {modelValue.map(it => (
303
+ <a
304
+ key={it.id}
305
+ href={it.url}
306
+ target="_blank"
307
+ rel="noreferrer"
308
+ style="max-width: 100px; text-overflow: ellipsis; overflow: hidden; white-space: nowrap; display: inline-block"
309
+ >
310
+ {it.name}
311
+ </a>
312
+ ))}
313
+ </NFlex>
314
+ )
315
+ }
316
+ return h(SimpleUpload, {
317
+ ...props,
318
+ ...params,
319
+ fileList: modelValue,
320
+ onUpdateFileList: onUpdateValue,
321
+ }, slots)
322
+ }
323
+ },
324
+ user: () => {
325
+ let users, groups, depts
326
+ return (
327
+ { slots, props: props0 },
328
+ { modelValue, 'onUpdate:modelValue': onUpdateValue, required, readonly, ...params },
329
+ ) => {
330
+ const props = mapProps(props0)
331
+ if (!users) {
332
+ users = shallowRef([])
333
+ getUsersByUsername().then((res) => {
334
+ users.value = res
335
+ })
336
+ getUserGroups().then((res) => {
337
+ groups.value = res
338
+ })
339
+ getDeptsByCode().then((res) => {
340
+ depts.value = res
341
+ })
342
+ }
343
+ if (readonly) {
344
+ return <DtUserRender value={modelValue} multiple={props.multiple} getUsersByUsername={getUsersByUsername} />
345
+ }
346
+
347
+ return h(DtUserDept, {
348
+ 'type': 'user',
349
+ 'users': users.value,
350
+ 'depts': depts.value,
351
+ 'groups': groups.value,
352
+ getUsersByDept,
353
+ getUsersByGroup,
354
+ ...props,
355
+ ...params,
356
+ modelValue,
357
+ 'onUpdate:modelValue': onUpdateValue,
358
+ }, slots)
359
+ }
360
+ },
361
+ }
362
+ }
363
+
364
+ export function createPageTableHelper({ getDeptsByCode, getUsersByUsername, getFileInfos, previewFileUrl }) {
365
+ const getFormatter = (defaultFormatter) => {
366
+ return () => (value, record, index, params) => {
367
+ if (value === undefined || value === null) return
368
+ return format(value, params.formatter ?? defaultFormatter)
369
+ }
370
+ }
371
+
372
+ const IDot = (props = {}) => (
373
+ <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 32 32" {...props}>
374
+ {/* Icon from Carbon by IBM - undefined */}
375
+ <circle cx="16" cy="16" r="8" fill="currentColor" />
376
+ </svg>
377
+ )
378
+
379
+ return {
380
+ date: getFormatter('yyyy-MM-dd'),
381
+ datetime: getFormatter('yyyy-MM-dd HH:mm:ss'),
382
+ dict: () => {
383
+ let dictType0, options0, optionMap
384
+ return (value, record, index, params) => {
385
+ if (value === undefined || value === null) return
386
+ const { dictType, options, multiple, statusMap } = mapProps(params)
387
+ if (options) {
388
+ if (options0 !== options) {
389
+ options0 = options
390
+ optionMap = new Map(options.map(op => [op[params.valueField || 'value'], op]))
391
+ }
392
+ } else {
393
+ if (dictType0 !== dictType) {
394
+ dictType0 = dictType
395
+ if (!optionMap && dictType0) {
396
+ optionMap = useDictMap(dictType0)
397
+ }
398
+ }
399
+ }
400
+ if (!multiple) {
401
+ value = value.toString()
402
+ if (statusMap && value in statusMap) {
403
+ let status = statusMap[value] ?? 'default'
404
+ if (['primary', 'success', 'warning', 'danger'].includes(status)) {
405
+ status = `var(--color-${status})`
406
+ }
407
+ return (
408
+ <span style="position: relative; padding-left: 1.25em">
409
+ <IDot style={`color: ${status}; position: absolute;left: 0; top: 1.5px`} />
410
+ <span>{optionMap.get(value?.toString())?.[params.labelField || 'label'] ?? ''}</span>
411
+ </span>
412
+ )
413
+ }
414
+ return optionMap.get(value)?.[params.labelField || 'label'] ?? ''
415
+ }
416
+ return value?.map(it => optionMap.get(it.toString())?.[params.labelField || 'label']) ?? ''
417
+ }
418
+ },
419
+ number: () => {
420
+ return (value, record, index, params) => {
421
+ if (value === undefined || value === null) return
422
+ if (params.percent) {
423
+ value = Number(value) * 100
424
+ }
425
+ switch ('number') {
426
+ case typeof params.fixed: {
427
+ value = value.toFixed(params.fixed)
428
+ break
429
+ }
430
+ case typeof params.round: {
431
+ value = value.toFixed(params.round).replace(/0+$/, '')
432
+ break
433
+ }
434
+ case typeof params.precision: {
435
+ value = value.toPrecision(params.precision)
436
+ break
437
+ }
438
+ }
439
+ return params?.separator ? Number(value).toLocaleString() : value
440
+ }
441
+ },
442
+ email: () => {
443
+ return (value) => {
444
+ return <a href={`mailto:${value}`}>{value}</a>
445
+ }
446
+ },
447
+ url: () => {
448
+ return (value) => {
449
+ return <a href={value}>{value}</a>
450
+ }
451
+ },
452
+ color: () => {
453
+ return (value) => {
454
+ return <NColorPicker value={value} disabled size="small" />
455
+ }
456
+ },
457
+ image: () => {
458
+ return (value, record, index, params) => {
459
+ if (!value) return
460
+ const urls = (Array.isArray(value) ? value : (params?.multiple ? value.split(',') : [value]))
461
+ .map(id => previewFileUrl(id))
462
+ return (
463
+ <NFlex gap="8" wrap>
464
+ {urls.map(url => (
465
+ <img key={url} src={url} style="max-height: 32px; max-width: 100px; object-fit: contain" />
466
+ ))}
467
+ </NFlex>
468
+ )
469
+ }
470
+ },
471
+ file: () => {
472
+ const key = nanoid()
473
+ return (value, record, index, params) => {
474
+ if (!value) return
475
+ record.__file_urls ??= {}
476
+ if (!record.__file_urls[key]) {
477
+ getFileInfos(Array.isArray(value) ? value : (params?.multiple ? value.split(',') : [value]))
478
+ .then((infos) => {
479
+ record.__file_urls[key] = infos
480
+ })
481
+ }
482
+ const infos = record.__file_urls[key] || []
483
+
484
+ return (
485
+ <NFlex gap="8" wrap>
486
+ {infos.map(it => (
487
+ <a
488
+ key={it.id}
489
+ href={it.url}
490
+ target="_blank"
491
+ rel="noreferrer"
492
+ style="max-width: 100px; text-overflow: ellipsis; overflow: hidden; white-space: nowrap; display: inline-block"
493
+ >
494
+ {it.name}
495
+ </a>
496
+ ))}
497
+ </NFlex>
498
+ )
499
+ }
500
+ },
501
+ dept: () => {
502
+ return (value, record, index, params) => {
503
+ if (!value) return
504
+ return <DtDeptRender value={value} multiple={params?.multiple} getDeptsByCode={getDeptsByCode} />
505
+ }
506
+ },
507
+ user: () => {
508
+ return (value, record, index, params) => {
509
+ if (!value) return
510
+ return <DtUserRender value={value} multiple={params?.multiple} getUsersByUsername={getUsersByUsername} />
511
+ }
512
+ },
513
+ }
514
+ }
515
+
516
+ export function createFormHelper() {
517
+ return () => ({ items, model, handleSubmit, reset, validation, readonly, inModal, showColon }) => {
518
+ return (
519
+ <DataForm
520
+ readonly={readonly}
521
+ model={model}
522
+ items={items}
523
+ validation={validation}
524
+ showColon={unref(showColon) ?? true}
525
+ showAction={false}
526
+ onSubmit={handleSubmit}
527
+ onRest={reset}
528
+ style={inModal ? 'padding-right: 2rem; padding-top: 1rem' : undefined}
529
+ />
530
+ )
531
+ }
532
+ }
533
+
534
+ export function createModalHelper() {
535
+ const formatLength = (length) => {
536
+ if (typeof length === 'number') {
537
+ return `${length}px`
538
+ }
539
+ return length
540
+ }
541
+ const contentStyle0 = {
542
+ maxHeight: 'calc(100vh - 130px)',
543
+ display: 'flex',
544
+ flexDirection: 'column',
545
+ }
546
+
547
+ const segmented = { content: true }
548
+ return () => ({
549
+ type = 'dialog', visible, title, content, footer, placement, resizable, draggable, closeOnEsc, closable,
550
+ showMask, maskClosable, width, height, style, onClose, onCancel, onConfirm, onAfterClose, onAfterOpen,
551
+ confirmText, confirmDisabled, confirmLoading, cancelText, cancelDisabled, cancelLoading, nativeScrollbar, ...options
552
+ }) => {
553
+ const footer0 = footer
554
+ ? () => footer
555
+ : footer === null
556
+ ? () => null
557
+ : () => (
558
+ <NFlex justify="end" gap="16">
559
+ {cancelText === null ? undefined : <NButton onClick={onCancel} disabled={cancelDisabled} loading={cancelLoading} style="min-width: 60px">{cancelText}</NButton>}
560
+ {confirmText === null ? undefined : <NButton type="primary" onClick={onConfirm} disabled={confirmDisabled} loading={confirmLoading} style="min-width: 60px">{confirmText}</NButton>}
561
+ </NFlex>
562
+ )
563
+ if (type === 'dialog') {
564
+ const { style: style0 } = mergeProps(
565
+ { style: { width: formatLength(width ?? 520), height: formatLength(height) } },
566
+ { style },
567
+ )
568
+ return (
569
+ <NModal
570
+ show={visible}
571
+ preset="card"
572
+ title={() => title}
573
+ maskClosable={maskClosable ?? false}
574
+ closeOnEsc={closeOnEsc}
575
+ closable={closable}
576
+ onClose={onClose}
577
+ segmented={segmented}
578
+ size="small"
579
+ onAfterEnter={onAfterOpen}
580
+ onAfterLeave={onAfterClose}
581
+ onUpdateShow={onClose}
582
+ draggable={draggable ?? true}
583
+ style={style0}
584
+ contentStyle={contentStyle0}
585
+ {...options}
586
+ >
587
+ {{
588
+ default: () => (
589
+ <NScrollbar abstract style="display: flex; flex-direction: column">
590
+ {content}
591
+ </NScrollbar>
592
+ ),
593
+ action: footer0,
594
+ }}
595
+ </NModal>
596
+ )
597
+ }
598
+
599
+ return (
600
+ <NDrawer
601
+ show={visible}
602
+ trapFocus={false}
603
+ closeOnEsc={closeOnEsc}
604
+ showMask={showMask}
605
+ maskClosable={maskClosable}
606
+ resizable={resizable}
607
+ placement={placement}
608
+ width={width ?? 440}
609
+ height={height}
610
+ onUpdateShow={onClose}
611
+ onAfterEnter={onAfterOpen}
612
+ onAfterLeave={onAfterClose}
613
+ style={style}
614
+ {...options}
615
+ >
616
+ <NDrawerContent closable={closable} nativeScrollbar={nativeScrollbar ?? false}>
617
+ {{
618
+ default: () => content,
619
+ header: () => title,
620
+ footer: footer0,
621
+ }}
622
+ </NDrawerContent>
623
+ </NDrawer>
624
+ )
625
+ }
626
+ }
@@ -0,0 +1,8 @@
1
+ import { inject } from 'vue'
2
+
3
+ export const UI_I18N_INJECTION = '__UI_I18N__'
4
+
5
+ export function useI18n() {
6
+ const i18n = inject(UI_I18N_INJECTION)
7
+ return i18n.value()
8
+ }
@@ -0,0 +1,58 @@
1
+ import { useMergedClsPrefix } from 'ithinkdt-ui/es/_mixins/use-config'
2
+ import _useStyle from 'ithinkdt-ui/es/_mixins/use-style'
3
+
4
+ export { c, cB, cE, cM } from 'ithinkdt-ui/es/_utils/cssr/index'
5
+
6
+ export { useMergedClsPrefix as useClsPrefix }
7
+
8
+ export default function useStyle(mountId, style, clsPrefix, styleIsolate) {
9
+ clsPrefix ??= useMergedClsPrefix()
10
+ _useStyle(mountId, style, clsPrefix, styleIsolate)
11
+ return clsPrefix
12
+ }
13
+
14
+ export const fullWidth = {
15
+ width: '100%',
16
+ }
17
+
18
+ export const fullHeight = {
19
+ height: '100%',
20
+ }
21
+
22
+ export const fullWH = {
23
+ ...fullWidth,
24
+ ...fullHeight,
25
+ }
26
+
27
+ export const flex = {
28
+ display: 'flex',
29
+ }
30
+
31
+ export const flexDirCol = {
32
+ ...flex,
33
+ flexDirection: 'column',
34
+ }
35
+
36
+ export const flexAlignCenter = {
37
+ ...flex,
38
+ alignItems: 'center',
39
+ }
40
+
41
+ export const flexJustifyCenter = {
42
+ ...flex,
43
+ justifyContent: 'center',
44
+ }
45
+
46
+ export const flexJustifySB = {
47
+ ...flex,
48
+ justifyContent: 'space-between',
49
+ }
50
+
51
+ export const flexCenter = {
52
+ ...flexAlignCenter,
53
+ ...flexJustifyCenter,
54
+ }
55
+
56
+ export const flexGap = (gap) => {
57
+ return { ...flex, gap }
58
+ }