@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/package.json +5 -8
- package/src/components/Checkboxes.jsx +128 -0
- package/src/components/DataActions.jsx +107 -0
- package/src/components/DataCustom.jsx +172 -0
- package/src/components/DataFilter.jsx +113 -0
- package/src/components/DataForm.jsx +264 -0
- package/src/components/DataLocaleInput.jsx +121 -0
- package/src/components/DataPagination.jsx +62 -0
- package/src/components/DataSelection.jsx +57 -0
- package/src/components/DataTable.jsx +243 -0
- package/src/components/Radios.jsx +120 -0
- package/src/components/UserDept.jsx +643 -0
- package/src/components/assets.jsx +274 -0
- package/src/components/index.js +11 -0
- package/src/design/Account.jsx +152 -0
- package/src/design/Appearance.jsx +89 -0
- package/src/design/Breadcrumb.jsx +67 -0
- package/src/design/Fullscreen.jsx +65 -0
- package/src/design/Language.jsx +45 -0
- package/src/design/Layout.jsx +241 -0
- package/src/design/Logo.jsx +91 -0
- package/src/design/Menu.jsx +133 -0
- package/src/design/MultiTabs.jsx +501 -0
- package/src/design/Notification.jsx +322 -0
- package/src/design/common.jsx +19 -0
- package/src/design/index.js +10 -0
- package/src/directives/index.js +2 -0
- package/src/directives/spin.js +181 -0
- package/src/directives/tooltip.jsx +121 -0
- package/src/index.js +18 -1361
- package/src/page.jsx +626 -0
- package/src/use-i18n.js +8 -0
- package/src/use-style.js +58 -0
- package/src/utils.js +20 -0
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import { format } from 'date-fns'
|
|
2
|
+
import {
|
|
3
|
+
NBadge,
|
|
4
|
+
NButton,
|
|
5
|
+
NDrawer,
|
|
6
|
+
NDrawerContent,
|
|
7
|
+
NEmpty,
|
|
8
|
+
NIcon,
|
|
9
|
+
NList,
|
|
10
|
+
NListItem,
|
|
11
|
+
NPagination,
|
|
12
|
+
NThing,
|
|
13
|
+
} from 'ithinkdt-ui'
|
|
14
|
+
import { defineComponent, reactive, withDirectives } from 'vue'
|
|
15
|
+
|
|
16
|
+
import { useAtomicBroadcast } from '@ithinkdt/common/composables'
|
|
17
|
+
|
|
18
|
+
import { vSpin } from '../directives/spin.js'
|
|
19
|
+
import { vTooltip } from '../directives/tooltip.jsx'
|
|
20
|
+
import { useI18n } from '../use-i18n.js'
|
|
21
|
+
|
|
22
|
+
export const AppNotification = /* @__PURE__ */ defineComponent({
|
|
23
|
+
name: 'AppNotification',
|
|
24
|
+
inheritAttrs: false,
|
|
25
|
+
props: {
|
|
26
|
+
getUnreadCount: Function,
|
|
27
|
+
getPage: Function,
|
|
28
|
+
markRead: Function,
|
|
29
|
+
markDelete: Function,
|
|
30
|
+
interval: { type: Number, default: 30_000 },
|
|
31
|
+
},
|
|
32
|
+
setup(props, { slots, attrs }) {
|
|
33
|
+
const { t } = useI18n()
|
|
34
|
+
|
|
35
|
+
const show = ref(false)
|
|
36
|
+
const currentTab = ref('unread')
|
|
37
|
+
const data = reactive({
|
|
38
|
+
page: 1,
|
|
39
|
+
size: 10,
|
|
40
|
+
loading: false,
|
|
41
|
+
records: [],
|
|
42
|
+
total: 0,
|
|
43
|
+
unread: 0,
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
const { sync, post } = useAtomicBroadcast({
|
|
47
|
+
channel: '__ithinkdt_mc_shared_channel',
|
|
48
|
+
onMsg: (msg) => {
|
|
49
|
+
data.unread = msg
|
|
50
|
+
},
|
|
51
|
+
getMsg: () => {
|
|
52
|
+
if (show.value) {
|
|
53
|
+
data.loading = true
|
|
54
|
+
Promise.resolve(props.getPage?.(currentTab.value, data.page, data.size) ?? [])
|
|
55
|
+
.then(({ records, total }) => {
|
|
56
|
+
data.records = records
|
|
57
|
+
if (currentTab.value === 'unread') {
|
|
58
|
+
data.unread = total
|
|
59
|
+
post(total)
|
|
60
|
+
} else {
|
|
61
|
+
data.total = total
|
|
62
|
+
}
|
|
63
|
+
})
|
|
64
|
+
.finally(() => {
|
|
65
|
+
data.loading = false
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
return props.getUnreadCount?.() ?? 0
|
|
69
|
+
},
|
|
70
|
+
timeout: () => props.interval,
|
|
71
|
+
immediate: true,
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
return () => {
|
|
75
|
+
const trigger = withDirectives(
|
|
76
|
+
<NButton
|
|
77
|
+
{...attrs}
|
|
78
|
+
quaternary
|
|
79
|
+
style="--n-padding: 0 12px"
|
|
80
|
+
onClick={() => {
|
|
81
|
+
show.value = true
|
|
82
|
+
currentTab.value = data.unread > 0 ? 'unread' : 'all'
|
|
83
|
+
setTimeout(sync, 300)
|
|
84
|
+
}}
|
|
85
|
+
>
|
|
86
|
+
{slots.icon
|
|
87
|
+
? (
|
|
88
|
+
slots.icon()
|
|
89
|
+
)
|
|
90
|
+
: (
|
|
91
|
+
<NBadge show={data.unread > 0} dot offset={[-2, 5]} processing>
|
|
92
|
+
<NIcon size="18">
|
|
93
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
|
|
94
|
+
<g
|
|
95
|
+
fill="none"
|
|
96
|
+
stroke="currentColor"
|
|
97
|
+
stroke-linecap="round"
|
|
98
|
+
stroke-linejoin="round"
|
|
99
|
+
stroke-width="1.5"
|
|
100
|
+
color="currentColor"
|
|
101
|
+
>
|
|
102
|
+
<path d="M2.53 14.77c-.213 1.394.738 2.361 1.902 2.843c4.463 1.85 10.673 1.85 15.136 0c1.164-.482 2.115-1.45 1.902-2.843c-.13-.857-.777-1.57-1.256-2.267c-.627-.924-.689-1.931-.69-3.003C19.525 5.358 16.157 2 12 2S4.475 5.358 4.475 9.5c0 1.072-.062 2.08-.69 3.003c-.478.697-1.124 1.41-1.255 2.267" />
|
|
103
|
+
<path d="M8 19c.458 1.725 2.076 3 4 3c1.925 0 3.541-1.275 4-3" />
|
|
104
|
+
</g>
|
|
105
|
+
</svg>
|
|
106
|
+
</NIcon>
|
|
107
|
+
</NBadge>
|
|
108
|
+
)}
|
|
109
|
+
</NButton>,
|
|
110
|
+
[[vTooltip, t('common.notification.tip', { count: data.unread }), undefined, { bottom: true }]],
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
const header = () => (
|
|
114
|
+
<div style="display: flex; align-items: baseline; gap: 20px">
|
|
115
|
+
<div style="padding-right: 12px">{t('common.notification.title') }</div>
|
|
116
|
+
<NButton
|
|
117
|
+
text
|
|
118
|
+
type={currentTab.value === 'all' ? 'primary' : 'default'}
|
|
119
|
+
onClick={() => {
|
|
120
|
+
currentTab.value = 'all'
|
|
121
|
+
sync()
|
|
122
|
+
}}
|
|
123
|
+
>
|
|
124
|
+
{t('common.notification.all') }
|
|
125
|
+
</NButton>
|
|
126
|
+
<NButton
|
|
127
|
+
text
|
|
128
|
+
type={currentTab.value === 'unread' ? 'primary' : 'default'}
|
|
129
|
+
onClick={() => {
|
|
130
|
+
currentTab.value = 'unread'
|
|
131
|
+
sync()
|
|
132
|
+
}}
|
|
133
|
+
>
|
|
134
|
+
{t('common.notification.unread', { count: data.unread > 99 ? '99+' : data.unread.toString() }) }
|
|
135
|
+
</NButton>
|
|
136
|
+
</div>
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
const renderItem = msg => (
|
|
140
|
+
<NListItem
|
|
141
|
+
key={msg.key}
|
|
142
|
+
onClick={() => {
|
|
143
|
+
if (msg.status === 'unread') {
|
|
144
|
+
props.markRead?.([msg.key]).then(() => {
|
|
145
|
+
msg.status = 'read'
|
|
146
|
+
})
|
|
147
|
+
data.unread--
|
|
148
|
+
}
|
|
149
|
+
if (msg.link) {
|
|
150
|
+
window.open(msg.link, '_target')
|
|
151
|
+
}
|
|
152
|
+
}}
|
|
153
|
+
>
|
|
154
|
+
<NThing
|
|
155
|
+
bordered={false}
|
|
156
|
+
closable
|
|
157
|
+
contentIndented
|
|
158
|
+
size="small"
|
|
159
|
+
onMouseenter={() => (msg.hover = true)}
|
|
160
|
+
onMouseleave={() => (msg.hover = false)}
|
|
161
|
+
style="padding: 0 4px"
|
|
162
|
+
>
|
|
163
|
+
{{
|
|
164
|
+
'avatar': () => (
|
|
165
|
+
<NIcon size={22} color={msg.status === 'unread' ? 'var(--color-primary)' : undefined}>
|
|
166
|
+
<svg
|
|
167
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
168
|
+
width="1em"
|
|
169
|
+
height="1em"
|
|
170
|
+
viewBox="0 0 24 24"
|
|
171
|
+
>
|
|
172
|
+
<path
|
|
173
|
+
fill="currentColor"
|
|
174
|
+
d="M12.3 7.29c.2-.18.44-.29.7-.29c.27 0 .5.11.71.29c.19.21.29.45.29.71c0 .27-.1.5-.29.71c-.21.19-.44.29-.71.29c-.26 0-.5-.1-.7-.29c-.19-.21-.3-.44-.3-.71c0-.26.11-.5.3-.71m-2.5 4.68s2.17-1.72 2.96-1.79c.74-.06.59.79.52 1.23l-.01.06c-.14.53-.31 1.17-.48 1.78c-.38 1.39-.75 2.75-.66 3c.1.34.72-.09 1.17-.39c.06-.04.11-.08.16-.11c0 0 .08-.08.16.03c.02.03.04.06.06.08c.09.14.14.19.02.27l-.04.02c-.22.15-1.16.81-1.54 1.05c-.41.27-1.98 1.17-1.74-.58c.21-1.23.49-2.29.71-3.12c.41-1.5.59-2.18-.33-1.59c-.37.22-.59.36-.72.45c-.11.08-.12.08-.19-.05l-.03-.06l-.05-.08c-.07-.1-.07-.11.03-.2M22 12c0 5.5-4.5 10-10 10S2 17.5 2 12S6.5 2 12 2s10 4.5 10 10m-2 0c0-4.42-3.58-8-8-8s-8 3.58-8 8s3.58 8 8 8s8-3.58 8-8"
|
|
175
|
+
/>
|
|
176
|
+
</svg>
|
|
177
|
+
</NIcon>
|
|
178
|
+
),
|
|
179
|
+
'header': () => msg.title,
|
|
180
|
+
'header-extra': () =>
|
|
181
|
+
msg.hover
|
|
182
|
+
? (
|
|
183
|
+
<div
|
|
184
|
+
style="display: flex; justify-content: end; gap: 12px"
|
|
185
|
+
onClick={(e) => {
|
|
186
|
+
e.preventDefault()
|
|
187
|
+
e.stopPropagation()
|
|
188
|
+
}}
|
|
189
|
+
>
|
|
190
|
+
{msg.status === 'unread'
|
|
191
|
+
? (
|
|
192
|
+
<NButton
|
|
193
|
+
text
|
|
194
|
+
type="primary"
|
|
195
|
+
onClick={() => {
|
|
196
|
+
props.markRead?.([msg.key]).then(() => {
|
|
197
|
+
msg.status = 'read'
|
|
198
|
+
data.unread--
|
|
199
|
+
})
|
|
200
|
+
}}
|
|
201
|
+
>
|
|
202
|
+
{t('common.notification.markRead') }
|
|
203
|
+
</NButton>
|
|
204
|
+
)
|
|
205
|
+
: undefined}
|
|
206
|
+
|
|
207
|
+
<NButton
|
|
208
|
+
text
|
|
209
|
+
type="error"
|
|
210
|
+
onClick={() => {
|
|
211
|
+
if (data.records === 1) {
|
|
212
|
+
data.records = []
|
|
213
|
+
sync()
|
|
214
|
+
} else {
|
|
215
|
+
data.records.splice(
|
|
216
|
+
data.records.findIndex(it => it.key == msg.key),
|
|
217
|
+
1,
|
|
218
|
+
)
|
|
219
|
+
}
|
|
220
|
+
props.markDelete?.([msg.key])
|
|
221
|
+
}}
|
|
222
|
+
>
|
|
223
|
+
{t('common.notification.markDelete') }
|
|
224
|
+
</NButton>
|
|
225
|
+
</div>
|
|
226
|
+
)
|
|
227
|
+
: undefined,
|
|
228
|
+
'default': msg.content,
|
|
229
|
+
'footer': () => (
|
|
230
|
+
<span style="color: gray">
|
|
231
|
+
{msg.hover ? format(msg.date, t('common.notification.time')) : t('common.timeago', { time: msg.date }) }
|
|
232
|
+
</span>
|
|
233
|
+
),
|
|
234
|
+
}}
|
|
235
|
+
</NThing>
|
|
236
|
+
</NListItem>
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
const content = () =>
|
|
240
|
+
withDirectives(
|
|
241
|
+
data.records.length > 0
|
|
242
|
+
? (
|
|
243
|
+
<NList clickable hoverable style="min-height: 50vh">
|
|
244
|
+
{data.records.map(msg => renderItem(msg))}
|
|
245
|
+
</NList>
|
|
246
|
+
)
|
|
247
|
+
: (
|
|
248
|
+
<NEmpty style="margin-top: 30vh" />
|
|
249
|
+
),
|
|
250
|
+
[[vSpin, data.loading]],
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
const footer = () => (
|
|
254
|
+
<div style="display: flex; justify-content: space-between; width: 100%">
|
|
255
|
+
{currentTab.value === 'unread'
|
|
256
|
+
? (
|
|
257
|
+
<NButton
|
|
258
|
+
text
|
|
259
|
+
type="primary"
|
|
260
|
+
disabled={data.records.length === 0}
|
|
261
|
+
onClick={() => {
|
|
262
|
+
const keys = data.records.filter(it => it.status === 'unread').map(it => it.key)
|
|
263
|
+
props.markRead(keys).then(() => {
|
|
264
|
+
while (data.unread - keys.length <= data.size * (data.page - 1)) {
|
|
265
|
+
data.page--
|
|
266
|
+
}
|
|
267
|
+
sync()
|
|
268
|
+
})
|
|
269
|
+
}}
|
|
270
|
+
>
|
|
271
|
+
{{
|
|
272
|
+
icon: () => (
|
|
273
|
+
<NIcon size="20">
|
|
274
|
+
<svg
|
|
275
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
276
|
+
width="1em"
|
|
277
|
+
height="1em"
|
|
278
|
+
viewBox="0 0 24 24"
|
|
279
|
+
>
|
|
280
|
+
<path
|
|
281
|
+
fill="currentColor"
|
|
282
|
+
d="M.41 13.41L6 19l1.41-1.42L1.83 12m20.41-6.42L11.66 16.17L7.5 12l-1.43 1.41L11.66 19l12-12M18 7l-1.41-1.42l-6.35 6.35l1.42 1.41z"
|
|
283
|
+
/>
|
|
284
|
+
</svg>
|
|
285
|
+
</NIcon>
|
|
286
|
+
),
|
|
287
|
+
default: () => t('common.notification.markPageRead'),
|
|
288
|
+
}}
|
|
289
|
+
</NButton>
|
|
290
|
+
)
|
|
291
|
+
: (
|
|
292
|
+
<span />
|
|
293
|
+
)}
|
|
294
|
+
<NPagination
|
|
295
|
+
simple
|
|
296
|
+
pageSize={data.size}
|
|
297
|
+
page={data.page}
|
|
298
|
+
itemCount={currentTab.value === 'unread' ? data.unread : data.total}
|
|
299
|
+
onUpdatePage={(page) => {
|
|
300
|
+
data.page = page
|
|
301
|
+
sync()
|
|
302
|
+
}}
|
|
303
|
+
/>
|
|
304
|
+
</div>
|
|
305
|
+
)
|
|
306
|
+
return (
|
|
307
|
+
<>
|
|
308
|
+
{trigger}
|
|
309
|
+
<NDrawer v-model:show={show.value} showMask={false} width={400}>
|
|
310
|
+
<NDrawerContent title={t('common.notification.title')} closable nativeScrollbar={false} bodyContentStyle={{ padding: '0' }}>
|
|
311
|
+
{{
|
|
312
|
+
header,
|
|
313
|
+
default: content,
|
|
314
|
+
footer,
|
|
315
|
+
}}
|
|
316
|
+
</NDrawerContent>
|
|
317
|
+
</NDrawer>
|
|
318
|
+
</>
|
|
319
|
+
)
|
|
320
|
+
}
|
|
321
|
+
},
|
|
322
|
+
})
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export const clsPrefix = 'app'
|
|
2
|
+
|
|
3
|
+
export const COLLAPSED_INJECTION_KEY = 'APP_SIDER_COLLAPSED'
|
|
4
|
+
|
|
5
|
+
export const IBookmark = () => (
|
|
6
|
+
<svg
|
|
7
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
8
|
+
xmlns:xlink="http://www.w3.org/1999/xlink"
|
|
9
|
+
role="img"
|
|
10
|
+
width="1em"
|
|
11
|
+
height="1em"
|
|
12
|
+
viewBox="0 0 24 24"
|
|
13
|
+
>
|
|
14
|
+
<path fill="currentColor" d="m17 18l-5-2.18L7 18V5h10m0-2H7a2 2 0 0 0-2 2v16l7-3l7 3V5a2 2 0 0 0-2-2Z" />
|
|
15
|
+
</svg>
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
export const CTX_INJECTION_KEY = '__APP_CTX__'
|
|
19
|
+
export const getCtx = () => inject(CTX_INJECTION_KEY)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { AppContent, AppFooter, AppHeader, AppLayout, AppSider } from './Layout.jsx'
|
|
2
|
+
export { AppLogo } from './Logo.jsx'
|
|
3
|
+
export { AppMenu } from './Menu.jsx'
|
|
4
|
+
export { AppBreadcrumb } from './Breadcrumb.jsx'
|
|
5
|
+
export { AppAccount } from './Account.jsx'
|
|
6
|
+
export { AppFullscreen } from './Fullscreen.jsx'
|
|
7
|
+
export { AppAppearance } from './Appearance.jsx'
|
|
8
|
+
export { AppLanguage } from './Language.jsx'
|
|
9
|
+
export { AppNotification } from './Notification.jsx'
|
|
10
|
+
export { AppMultiTabs } from './MultiTabs.jsx'
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { Fragment, defineComponent, h, nextTick, ref, toRef, watch } from 'vue'
|
|
2
|
+
|
|
3
|
+
import { useElementIntersectionRect } from '@ithinkdt/common/composables'
|
|
4
|
+
import { string2dom } from '@ithinkdt/common/dom'
|
|
5
|
+
import { debounce } from '@ithinkdt/common/fn'
|
|
6
|
+
|
|
7
|
+
import useStyle, { c, cB, cE, cM } from '../use-style.js'
|
|
8
|
+
|
|
9
|
+
let clsPrefix
|
|
10
|
+
let isDark
|
|
11
|
+
|
|
12
|
+
const style = /* @__PURE__ */ c([
|
|
13
|
+
c(({ props }) => `:where(${props.bPrefix}spin-host)`, {
|
|
14
|
+
position: 'relative',
|
|
15
|
+
}),
|
|
16
|
+
cB(
|
|
17
|
+
'spin-directive',
|
|
18
|
+
{
|
|
19
|
+
zIndex: '999999',
|
|
20
|
+
position: 'absolute',
|
|
21
|
+
color: 'var(--color-primary)',
|
|
22
|
+
display: 'flex',
|
|
23
|
+
flexDirection: 'column',
|
|
24
|
+
justifyContent: 'center',
|
|
25
|
+
alignItems: 'center',
|
|
26
|
+
gap: '4px',
|
|
27
|
+
willChange: 'background-color',
|
|
28
|
+
backgroundColor: 'rgb(var(--host-bg, 255 255 255) / 0)',
|
|
29
|
+
transition: 'background-color 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
|
|
30
|
+
},
|
|
31
|
+
[
|
|
32
|
+
cE('tip, icon', {
|
|
33
|
+
willChange: 'opacity',
|
|
34
|
+
opacity: '0',
|
|
35
|
+
transition: 'opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
|
|
36
|
+
}),
|
|
37
|
+
cE('tip:empty', { display: 'none' }),
|
|
38
|
+
cM(
|
|
39
|
+
'spining',
|
|
40
|
+
{
|
|
41
|
+
backgroundColor: 'rgb(var(--host-bg, 255 255 255) / 0.5)',
|
|
42
|
+
},
|
|
43
|
+
[cE('tip, icon', { opacity: '1' })],
|
|
44
|
+
),
|
|
45
|
+
],
|
|
46
|
+
),
|
|
47
|
+
])
|
|
48
|
+
|
|
49
|
+
export const SpinDirectiveProvider = /* @__PURE__ */ defineComponent({
|
|
50
|
+
name: 'SpinDirectiveProvider',
|
|
51
|
+
props: {
|
|
52
|
+
dark: Boolean,
|
|
53
|
+
},
|
|
54
|
+
setup(props) {
|
|
55
|
+
clsPrefix = useStyle('-spin-directive', style)
|
|
56
|
+
isDark = toRef(props, 'dark')
|
|
57
|
+
return () => h(Fragment)
|
|
58
|
+
},
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
const Spin = /* @__PURE__ */ Symbol('spin-dir')
|
|
62
|
+
|
|
63
|
+
const updateLoading = (el, { value, modifiers }) => {
|
|
64
|
+
el[Spin].loading.value = !!value
|
|
65
|
+
if (!el[Spin].loading.value) return
|
|
66
|
+
const dark = modifiers.dark || (!modifiers.light && isDark.value)
|
|
67
|
+
el[Spin].spinEl.style.setProperty(
|
|
68
|
+
'--host-bg',
|
|
69
|
+
dark ? '0 0 0' : '255 255 255',
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export const vSpin = {
|
|
74
|
+
beforeMount(el) {
|
|
75
|
+
el.classList.add(`${clsPrefix.value}-spin-host`)
|
|
76
|
+
|
|
77
|
+
if (el[Spin]) return
|
|
78
|
+
|
|
79
|
+
const loading = ref(false)
|
|
80
|
+
|
|
81
|
+
const spinEl = string2dom(
|
|
82
|
+
`
|
|
83
|
+
<div class="${clsPrefix.value}-spin-directive">
|
|
84
|
+
<svg class="${clsPrefix.value}-spin-directive__icon" width="32" height="32" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
|
|
85
|
+
<g>
|
|
86
|
+
<animateTransform attributeName="transform" type="rotate" values="0 100 100;270 100 100" begin="0s" dur="1.6s" fill="freeze" repeatCount="indefinite"></animateTransform>
|
|
87
|
+
<circle fill="none" stroke="currentColor" stroke-width="18" stroke-linecap="round" cx="100" cy="100" r="92" stroke-dasharray="567" stroke-dashoffset="1848">
|
|
88
|
+
<animateTransform attributeName="transform" type="rotate" values="0 100 100;135 100 100;450 100 100" begin="0s" dur="1.6s" fill="freeze" repeatCount="indefinite"></animateTransform>
|
|
89
|
+
<animate attributeName="stroke-dashoffset" values="567;142;567" begin="0s" dur="1.6s" fill="freeze" repeatCount="indefinite"></animate>
|
|
90
|
+
</circle>
|
|
91
|
+
</g>
|
|
92
|
+
</svg>
|
|
93
|
+
<div class="${clsPrefix.value}-spin-directive__tip"></div>
|
|
94
|
+
</div>
|
|
95
|
+
`.trim(),
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
let rect
|
|
99
|
+
const { stop: stopEl, update } = useElementIntersectionRect(el, (_rect) => {
|
|
100
|
+
rect = _rect
|
|
101
|
+
Object.assign(spinEl.style, {
|
|
102
|
+
left: 0,
|
|
103
|
+
top: 0,
|
|
104
|
+
width: rect.width + 'px',
|
|
105
|
+
height: rect.height + 'px',
|
|
106
|
+
})
|
|
107
|
+
}, { interval: 1000 / 30 })
|
|
108
|
+
|
|
109
|
+
let timer
|
|
110
|
+
const stopSpin = watch(
|
|
111
|
+
loading,
|
|
112
|
+
debounce(() => {
|
|
113
|
+
if (loading.value) {
|
|
114
|
+
if (timer === undefined) {
|
|
115
|
+
clearTimeout(timer)
|
|
116
|
+
timer = undefined
|
|
117
|
+
}
|
|
118
|
+
update()
|
|
119
|
+
el.append(spinEl)
|
|
120
|
+
requestAnimationFrame(() => {
|
|
121
|
+
if (loading.value) spinEl.classList.add(`${clsPrefix.value}-spin-directive--spining`)
|
|
122
|
+
})
|
|
123
|
+
} else {
|
|
124
|
+
spinEl.classList.remove(`${clsPrefix.value}-spin-directive--spining`)
|
|
125
|
+
const listener = () => {
|
|
126
|
+
spinEl.remove()
|
|
127
|
+
if (timer) clearTimeout(timer)
|
|
128
|
+
timer = null
|
|
129
|
+
spinEl.removeEventListener('transitioncancel', listener)
|
|
130
|
+
spinEl.removeEventListener('transitionend', listener)
|
|
131
|
+
}
|
|
132
|
+
spinEl.addEventListener('transitioncancel', listener)
|
|
133
|
+
spinEl.addEventListener('transitionend', listener)
|
|
134
|
+
let timer = setTimeout(listener, 300)
|
|
135
|
+
}
|
|
136
|
+
}, 30),
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
el[Spin] = {
|
|
140
|
+
loading,
|
|
141
|
+
spinEl,
|
|
142
|
+
updateTip: () => {
|
|
143
|
+
const tipEl = spinEl.querySelector(`.${clsPrefix.value}-spin-directive__tip`)
|
|
144
|
+
tipEl.textContent = el.dataset.spinTip
|
|
145
|
+
},
|
|
146
|
+
stop: () => {
|
|
147
|
+
stopEl()
|
|
148
|
+
stopSpin()
|
|
149
|
+
},
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
mounted(el, binding) {
|
|
154
|
+
updateLoading(el, binding)
|
|
155
|
+
el[Spin].updateTip()
|
|
156
|
+
if (el[Spin].observer) return
|
|
157
|
+
// 创建 MutationObserver 对象
|
|
158
|
+
nextTick().then(() => {
|
|
159
|
+
const observer = (el[Spin].observer = new MutationObserver((mutationsList) => {
|
|
160
|
+
for (const mutation of mutationsList) {
|
|
161
|
+
if (mutation.type === 'attributes' && mutation.attributeName === 'data-spin-tip') {
|
|
162
|
+
el[Spin].updateTip()
|
|
163
|
+
return
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}))
|
|
167
|
+
|
|
168
|
+
// 开始观察目标元素
|
|
169
|
+
observer.observe(el, { attributes: true, attributeFilter: ['data-spin-tip'] })
|
|
170
|
+
})
|
|
171
|
+
},
|
|
172
|
+
|
|
173
|
+
updated: updateLoading,
|
|
174
|
+
|
|
175
|
+
beforeUnmount(el) {
|
|
176
|
+
el[Spin].loading.value = false
|
|
177
|
+
el[Spin].stop()
|
|
178
|
+
el[Spin].observer?.disconnect()
|
|
179
|
+
delete el[Spin]
|
|
180
|
+
},
|
|
181
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { promiseTimeout, useEventListener } from '@vueuse/core'
|
|
2
|
+
import { NTooltip } from 'ithinkdt-ui'
|
|
3
|
+
import { defineComponent, reactive, shallowRef } from 'vue'
|
|
4
|
+
|
|
5
|
+
const Tooltip = /* @__PURE__ */ Symbol('tooltip-dir')
|
|
6
|
+
const current = /* @__PURE__ */ shallowRef()
|
|
7
|
+
|
|
8
|
+
let _update
|
|
9
|
+
export const TooltipDirectiveProvider = /* @__PURE__ */ defineComponent({
|
|
10
|
+
name: 'TooltipDirectiveProvider',
|
|
11
|
+
props: {
|
|
12
|
+
delay: { type: Number, default: 180 },
|
|
13
|
+
},
|
|
14
|
+
setup(props) {
|
|
15
|
+
const tip = shallowRef()
|
|
16
|
+
const data = reactive({ show: false, tip: undefined, placement: 'top', x: 0, y: 0 })
|
|
17
|
+
|
|
18
|
+
_update = async () => {
|
|
19
|
+
if (!current.value) return
|
|
20
|
+
if (props.delay > 0) await promiseTimeout(props.delay)
|
|
21
|
+
const el = current.value
|
|
22
|
+
const tooltip = el?.[Tooltip]
|
|
23
|
+
if (!tooltip) return
|
|
24
|
+
const _tip = tooltip.tip
|
|
25
|
+
tip.value
|
|
26
|
+
= typeof _tip === 'function' ? _tip : () => _tip || el.attributes.getNamedItem('title') || el.textContent
|
|
27
|
+
|
|
28
|
+
const { auto } = tooltip.binding.modifiers
|
|
29
|
+
if (
|
|
30
|
+
!auto
|
|
31
|
+
|| el.firstChild.offsetWidth > el.clientWidth
|
|
32
|
+
|| el.firstChild.offsetHeight > el.clientHeight
|
|
33
|
+
) {
|
|
34
|
+
data.show = true
|
|
35
|
+
|
|
36
|
+
const rect = el.getBoundingClientRect()
|
|
37
|
+
data.placement
|
|
38
|
+
= Object.keys(tooltip.binding.modifiers).find(m =>
|
|
39
|
+
['top', 'right', 'left', 'bottom'].find(k => m.startsWith(k)),
|
|
40
|
+
) ?? 'top'
|
|
41
|
+
switch (data.placement.split('-')[0]) {
|
|
42
|
+
case 'top': {
|
|
43
|
+
data.x = rect.left + rect.width / 2
|
|
44
|
+
data.y = rect.top
|
|
45
|
+
break
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
case 'right': {
|
|
49
|
+
data.x = rect.left + rect.width
|
|
50
|
+
data.y = rect.top + rect.height / 2
|
|
51
|
+
break
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
case 'left': {
|
|
55
|
+
data.x = rect.left
|
|
56
|
+
data.y = rect.top + rect.height / 2
|
|
57
|
+
break
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
case 'bottom': {
|
|
61
|
+
data.x = rect.left + rect.width / 2
|
|
62
|
+
data.y = rect.bottom
|
|
63
|
+
break
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
} else if (data.show) {
|
|
67
|
+
data.show = false
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
useEventListener(
|
|
72
|
+
'mouseover',
|
|
73
|
+
(ev) => {
|
|
74
|
+
const el = ev.target
|
|
75
|
+
if (!current.value || current.value?.contains(el)) return
|
|
76
|
+
data.show = false
|
|
77
|
+
current.value = undefined
|
|
78
|
+
},
|
|
79
|
+
{ capture: true },
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
return () => (
|
|
83
|
+
<NTooltip
|
|
84
|
+
trigger="manual"
|
|
85
|
+
show={!!current.value && data.show}
|
|
86
|
+
x={data.x}
|
|
87
|
+
y={data.y}
|
|
88
|
+
placement={data.placement}
|
|
89
|
+
keepAliveOnHover
|
|
90
|
+
>
|
|
91
|
+
{tip.value?.()}
|
|
92
|
+
</NTooltip>
|
|
93
|
+
)
|
|
94
|
+
},
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
const _listener = (ev) => {
|
|
98
|
+
current.value = ev.target
|
|
99
|
+
_update?.()
|
|
100
|
+
}
|
|
101
|
+
export const vTooltip = {
|
|
102
|
+
mounted(el, binding) {
|
|
103
|
+
el[Tooltip] = {
|
|
104
|
+
binding,
|
|
105
|
+
tip: binding.value,
|
|
106
|
+
}
|
|
107
|
+
el.addEventListener('mouseenter', _listener)
|
|
108
|
+
},
|
|
109
|
+
updated(el, binding) {
|
|
110
|
+
el[Tooltip].binding = binding
|
|
111
|
+
el[Tooltip].tip = binding.value
|
|
112
|
+
_update()
|
|
113
|
+
},
|
|
114
|
+
beforeUnmount(el) {
|
|
115
|
+
if (el[Tooltip]) el.removeEventListener('mouseenter', _listener)
|
|
116
|
+
if (current.value === el) {
|
|
117
|
+
current.value = undefined
|
|
118
|
+
}
|
|
119
|
+
delete el[Tooltip]
|
|
120
|
+
},
|
|
121
|
+
}
|