create-jnrs-template-vue 1.1.14 → 1.1.15

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.
Files changed (57) hide show
  1. package/bin/create.mjs +55 -32
  2. package/jnrs-template-vue/auto-imports.d.ts +2 -0
  3. package/jnrs-template-vue/components.d.ts +1 -1
  4. package/jnrs-template-vue/package.json +4 -4
  5. package/jnrs-template-vue/src/api/common/index.ts +7 -3
  6. package/jnrs-template-vue/src/api/demos/index.ts +40 -25
  7. package/jnrs-template-vue/src/api/system/index.ts +3 -0
  8. package/jnrs-template-vue/src/assets/images/fileIcon/iconArchive.png +0 -0
  9. package/jnrs-template-vue/src/assets/images/fileIcon/iconAudio.png +0 -0
  10. package/jnrs-template-vue/src/assets/images/fileIcon/iconCode.png +0 -0
  11. package/jnrs-template-vue/src/assets/images/fileIcon/iconExcel.png +0 -0
  12. package/jnrs-template-vue/src/assets/images/fileIcon/iconFile.png +0 -0
  13. package/jnrs-template-vue/src/assets/images/fileIcon/iconFlash.png +0 -0
  14. package/jnrs-template-vue/src/assets/images/fileIcon/iconGif.png +0 -0
  15. package/jnrs-template-vue/src/assets/images/fileIcon/iconImage.png +0 -0
  16. package/jnrs-template-vue/src/assets/images/fileIcon/iconMac.png +0 -0
  17. package/jnrs-template-vue/src/assets/images/fileIcon/iconOfd.png +0 -0
  18. package/jnrs-template-vue/src/assets/images/fileIcon/iconPdf.png +0 -0
  19. package/jnrs-template-vue/src/assets/images/fileIcon/iconPpt.png +0 -0
  20. package/jnrs-template-vue/src/assets/images/fileIcon/iconText.png +0 -0
  21. package/jnrs-template-vue/src/assets/images/fileIcon/iconUnknown.png +0 -0
  22. package/jnrs-template-vue/src/assets/images/fileIcon/iconVideo.png +0 -0
  23. package/jnrs-template-vue/src/assets/images/fileIcon/iconWindows.png +0 -0
  24. package/jnrs-template-vue/src/assets/images/fileIcon/iconWord.png +0 -0
  25. package/jnrs-template-vue/src/assets/images/fileIcon/iconWps.png +0 -0
  26. package/jnrs-template-vue/src/components/base/ImageView.vue +117 -40
  27. package/jnrs-template-vue/src/components/base/JnFileUpload.vue +433 -0
  28. package/jnrs-template-vue/src/components/base/PdfView.vue +106 -0
  29. package/jnrs-template-vue/src/components/common/JnDatetime.vue +37 -0
  30. package/jnrs-template-vue/src/components/common/JnDictTag.vue +70 -0
  31. package/jnrs-template-vue/src/components/common/JnEdit.vue +68 -0
  32. package/jnrs-template-vue/src/layout/SideMenu.vue +1 -0
  33. package/jnrs-template-vue/src/layout/TopHeader.vue +17 -5
  34. package/jnrs-template-vue/src/types/index.ts +32 -8
  35. package/jnrs-template-vue/src/utils/packages.ts +12 -7
  36. package/jnrs-template-vue/src/views/demos/crud/index.vue +180 -24
  37. package/jnrs-template-vue/src/views/demos/unitTest/RequestPage.vue +12 -16
  38. package/jnrs-template-vue/src/views/login/index.vue +13 -18
  39. package/jnrs-template-vue/src/views/system/mine/baseInfo.vue +3 -8
  40. package/jnrs-template-vue/tsconfig.json +4 -1
  41. package/jnrs-template-vue/vite.config.ts +1 -1
  42. package/jnrs-template-vue/viteMockServe/file.ts +68 -0
  43. package/jnrs-template-vue/viteMockServe/fileSrc/mock-pdf.pdf +0 -0
  44. package/jnrs-template-vue/viteMockServe/fileSrc/mock-png-0.png +0 -0
  45. package/jnrs-template-vue/viteMockServe/fileSrc/mock-png-1.png +0 -0
  46. package/jnrs-template-vue/viteMockServe/index.ts +10 -8
  47. package/jnrs-template-vue/viteMockServe/json/dictRes.json +21 -0
  48. package/jnrs-template-vue/viteMockServe/json/loginRes_admin.json +157 -0
  49. package/jnrs-template-vue/viteMockServe/{loginRes_user.json → json/loginRes_user.json} +1 -1
  50. package/jnrs-template-vue/viteMockServe/{tableRes.json → json/tableRes.json} +143 -70
  51. package/jnrs-template-vue/viteMockServe/success.ts +8 -0
  52. package/package.json +1 -1
  53. package/jnrs-template-vue/viteMockServe/dictRes.json +0 -141
  54. package/jnrs-template-vue/viteMockServe/loginRes_admin.json +0 -713
  55. /package/jnrs-template-vue/viteMockServe/{detailsRes.json → json/detailsRes.json} +0 -0
  56. /package/jnrs-template-vue/viteMockServe/{dictItemRes.json → json/dictItemRes.json} +0 -0
  57. /package/jnrs-template-vue/viteMockServe/{roleRes.json → json/roleRes.json} +0 -0
@@ -0,0 +1,433 @@
1
+ <!--
2
+ @Author : TanRui
3
+ @WeChat : Tan578853789
4
+ @File : JnFileUpload.vue
5
+ @Date : 2025/12/25
6
+ @Desc. : 通用上传 & 回显组件 上传类型为File类型 二进制
7
+ -->
8
+
9
+ <script setup lang="ts">
10
+ import type { UploadRawFile, UploadFile, FormInstance } from 'element-plus'
11
+ import type { Attachment } from '@/types'
12
+ import { ElMessage } from 'element-plus'
13
+ import { ref, computed, nextTick } from 'vue'
14
+ import { downloadFile } from '@/utils/packages'
15
+ import iconUnknown from '@/assets/images/fileIcon/iconUnknown.png'
16
+ import iconCode from '@/assets/images/fileIcon/iconCode.png'
17
+ import iconWord from '@/assets/images/fileIcon/iconWord.png'
18
+ import iconWps from '@/assets/images/fileIcon/iconWps.png'
19
+ import iconText from '@/assets/images/fileIcon/iconText.png'
20
+ import iconPpt from '@/assets/images/fileIcon/iconPpt.png'
21
+ import iconPdf from '@/assets/images/fileIcon/iconPdf.png'
22
+ import iconArchive from '@/assets/images/fileIcon/iconArchive.png'
23
+ import iconWindows from '@/assets/images/fileIcon/iconWindows.png'
24
+ import iconMac from '@/assets/images/fileIcon/iconMac.png'
25
+ import iconVideo from '@/assets/images/fileIcon/iconVideo.png'
26
+ import iconAudio from '@/assets/images/fileIcon/iconAudio.png'
27
+ import iconFlash from '@/assets/images/fileIcon/iconFlash.png'
28
+ import iconGif from '@/assets/images/fileIcon/iconGif.png'
29
+ import iconImage from '@/assets/images/fileIcon/iconImage.png'
30
+ import iconFile from '@/assets/images/fileIcon/iconFile.png'
31
+ import iconExcel from '@/assets/images/fileIcon/iconExcel.png'
32
+ import iconOfd from '@/assets/images/fileIcon/iconOfd.png'
33
+
34
+ type MixedUploadFile = Attachment | UploadRawFile
35
+
36
+ interface Props {
37
+ /**
38
+ * 绑定的模型值(文件列表),默认为空数组
39
+ */
40
+ modelValue: MixedUploadFile[]
41
+
42
+ /**
43
+ * 接受的文件类型(如 'image/*'),默认为空字符串
44
+ */
45
+ accept?: string
46
+
47
+ /**
48
+ * 是否启用拖拽上传,默认为 false
49
+ */
50
+ drag?: boolean
51
+
52
+ /**
53
+ * 单个文件大小限制(单位:MB),默认为 0(不限制)
54
+ */
55
+ fileSizeMb?: number
56
+
57
+ /**
58
+ * 最大上传文件数量,0 表示不限制,默认为 0
59
+ */
60
+ limit?: number
61
+
62
+ /**
63
+ * Element Plus 组件的尺寸,默认为 'small'
64
+ */
65
+ size?: '' | 'small' | 'default' | 'large'
66
+
67
+ /**
68
+ * 父级组件的表单引用(用于触发校验),默认为 null
69
+ */
70
+ formRef?: FormInstance
71
+
72
+ /**
73
+ * 父级表单中用于校验的字段名(需配合 formRef 使用),默认为空字符串
74
+ */
75
+ validateFieldName?: string
76
+ }
77
+
78
+ const {
79
+ modelValue,
80
+ accept = '',
81
+ drag = false,
82
+ fileSizeMb = 100,
83
+ limit = 0,
84
+ size = 'small',
85
+ formRef = null,
86
+ validateFieldName = ''
87
+ } = defineProps<Props>()
88
+
89
+ const emit = defineEmits(['update:modelValue'])
90
+
91
+ const loading = ref(false)
92
+ const uploadRef = ref()
93
+ const isDisable = computed(() => {
94
+ return limit && modelValue && modelValue.length >= limit
95
+ })
96
+
97
+ const fileSizeFmt = (value: number) => {
98
+ const units = ['B', 'KB', 'MB', 'GB']
99
+ let size = value
100
+ let unitIndex = 0
101
+
102
+ while (size >= 1024 && unitIndex < units.length - 1) {
103
+ size /= 1024
104
+ unitIndex++
105
+ }
106
+
107
+ const fixedDigits = unitIndex === 0 ? 0 : unitIndex === 1 ? 0 : 1
108
+ return size.toFixed(fixedDigits) + ' ' + units[unitIndex]
109
+ }
110
+
111
+ const getAccessoryIcon = (type: string) => {
112
+ let icon = iconUnknown
113
+ if (type.includes('code')) {
114
+ icon = iconCode
115
+ } else if (type.includes('text')) {
116
+ icon = iconText
117
+ } else if (type.includes('wps')) {
118
+ icon = iconWps
119
+ } else if (type.includes('word')) {
120
+ icon = iconWord
121
+ } else if (type.includes('ppt')) {
122
+ icon = iconPpt
123
+ } else if (type.includes('pdf')) {
124
+ icon = iconPdf
125
+ } else if (type.includes('archive')) {
126
+ icon = iconArchive
127
+ } else if (type.includes('x-msdownload')) {
128
+ icon = iconWindows
129
+ } else if (type.includes('x-apple-aspen-config')) {
130
+ icon = iconMac
131
+ } else if (type.includes('video')) {
132
+ icon = iconVideo
133
+ } else if (type.includes('audio')) {
134
+ icon = iconAudio
135
+ } else if (type.includes('flash')) {
136
+ icon = iconFlash
137
+ } else if (type.includes('gif')) {
138
+ icon = iconGif
139
+ } else if (type.includes('image')) {
140
+ icon = iconImage
141
+ } else if (type.includes('file')) {
142
+ icon = iconFile
143
+ } else if (type.includes('excel')) {
144
+ icon = iconExcel
145
+ } else if (type.includes('ofd')) {
146
+ icon = iconOfd
147
+ }
148
+ return icon
149
+ }
150
+
151
+ const loadingProgress = () => {
152
+ loading.value = true
153
+ }
154
+
155
+ const handleChange = (uploadFile: UploadFile) => {
156
+ const { status, raw } = uploadFile
157
+
158
+ if (status === 'ready' && raw) {
159
+ // 数量校验
160
+ if (limit && modelValue && modelValue.length >= limit) {
161
+ ElMessage({
162
+ message: `最多只能上传 ${limit} 个文件!`,
163
+ type: 'warning',
164
+ grouping: true,
165
+ duration: 3000
166
+ })
167
+ return false
168
+ }
169
+
170
+ // 文件类型校验
171
+ if (accept && accept.split(',').length > 0) {
172
+ let flag = false
173
+ accept.split(',').forEach((element) => {
174
+ if (raw.name.indexOf(element) > -1) {
175
+ flag = true
176
+ }
177
+ })
178
+ if (!flag) {
179
+ ElMessage({
180
+ message: `上传文件只能是 ${accept} 格式!`,
181
+ type: 'warning',
182
+ grouping: true,
183
+ duration: 3000
184
+ })
185
+ return false
186
+ }
187
+ }
188
+
189
+ // 文件大小校验
190
+ if (fileSizeMb && raw.size / 1024 / 1024 > fileSizeMb) {
191
+ ElMessage({
192
+ message: `上传的文件大小不能超过 ${fileSizeMb} MB!`,
193
+ type: 'warning',
194
+ grouping: true,
195
+ duration: 3000
196
+ })
197
+ return false
198
+ }
199
+
200
+ const temp = modelValue
201
+ temp.push(uploadFile.raw as UploadRawFile)
202
+ emit('update:modelValue', temp)
203
+
204
+ nextTick(() => {
205
+ if (formRef && validateFieldName) {
206
+ formRef.validateField(validateFieldName)
207
+ }
208
+ uploadRef.value?.clearFiles()
209
+ loading.value = false
210
+ })
211
+ }
212
+ }
213
+
214
+ const deleteFile = (file: MixedUploadFile) => {
215
+ const temp = modelValue
216
+ const index = modelValue.findIndex((item) => {
217
+ // Attachment 类型
218
+ if ('id' in file && 'id' in item) {
219
+ return item.id === file.id
220
+ }
221
+ // UploadRawFile 类型
222
+ if ('uid' in file && 'uid' in item) {
223
+ return item.uid === file.uid
224
+ }
225
+ })
226
+
227
+ if (index !== -1) {
228
+ temp.splice(index, 1)
229
+ }
230
+
231
+ emit('update:modelValue', temp)
232
+
233
+ nextTick(() => {
234
+ if (formRef && validateFieldName) {
235
+ formRef.validateField(validateFieldName)
236
+ }
237
+ })
238
+ }
239
+ </script>
240
+
241
+ <template>
242
+ <div class="jnFileUpload">
243
+ <el-upload
244
+ ref="uploadRef"
245
+ :http-request="() => {}"
246
+ :show-file-list="false"
247
+ :multiple="true"
248
+ :drag="drag"
249
+ :accept="accept"
250
+ :disabled="isDisable"
251
+ :on-progress="loadingProgress"
252
+ :on-change="handleChange"
253
+ >
254
+ <el-button type="primary" :size="size" :loading="loading" :disabled="isDisable">
255
+ 上传
256
+ <span v-if="limit" style="display: inline-block; margin-left: 5px">
257
+ ({{ modelValue ? modelValue.length : 0 }}/{{ limit }})
258
+ </span>
259
+ </el-button>
260
+ </el-upload>
261
+
262
+ <div class="uploadTips" :class="{ uploadTips_isDrag: drag }">
263
+ <span v-if="accept">
264
+ 文件类型为
265
+ <b>{{ accept }}</b>
266
+ </span>
267
+ <span v-if="accept">
268
+ 文件大小为
269
+ <b>{{ fileSizeMb }}MB</b>
270
+ 以内
271
+ </span>
272
+ </div>
273
+
274
+ <div class="fileList" v-show="modelValue && modelValue.length > 0" v-loading="loading">
275
+ <div class="fileLis_item" v-for="(item, index) in modelValue" :key="index">
276
+ <!-- Attachment 类型 -->
277
+ <template v-if="'id' in item">
278
+ <div class="fileLis_item_left">
279
+ <span class="fileLis_item_index">{{ index + 1 }}</span>
280
+ <img class="fileLis_item_icon" v-if="item.fileType" :src="getAccessoryIcon(item.fileType)" />
281
+ <span class="fileLis_item_name">{{ item.fileName }}</span>
282
+ <span class="fileLis_item_size">
283
+ {{ fileSizeFmt(item.fileSize) }}
284
+ </span>
285
+ </div>
286
+ <div class="fileLis_item_right">
287
+ <span class="fileLis_item_btn" @click="downloadFile(item.uniqueFileName, item.fileName)">
288
+ <el-icon><Download /></el-icon>
289
+ </span>
290
+ <span class="fileLis_item_btn fileLis_item_btn_danger" @click="deleteFile(item)">
291
+ <el-icon><Delete /></el-icon>
292
+ </span>
293
+ </div>
294
+ </template>
295
+
296
+ <!-- UploadRawFile 类型 -->
297
+ <template v-if="'uid' in item">
298
+ <div class="fileLis_item_left">
299
+ <span class="fileLis_item_index">{{ index + 1 }}</span>
300
+ <img class="fileLis_item_icon" v-if="item.type" :src="getAccessoryIcon(item.type)" />
301
+ <span class="fileLis_item_name">{{ item.name }}</span>
302
+ <span class="fileLis_item_size">
303
+ {{ fileSizeFmt(item.size) }}
304
+ </span>
305
+ </div>
306
+ <div class="fileLis_item_right">
307
+ <span class="fileLis_item_btn fileLis_item_btn_danger" @click="deleteFile(item)">
308
+ <el-icon><Delete /></el-icon>
309
+ </span>
310
+ </div>
311
+ </template>
312
+ </div>
313
+ </div>
314
+ </div>
315
+ </template>
316
+
317
+ <style lang="scss" scoped>
318
+ .jnFileUpload {
319
+ position: relative;
320
+ width: 100%;
321
+ color: var(--jnrs-font-primary-06);
322
+
323
+ :deep(.el-upload-dragger) {
324
+ padding: 30px 0 10px;
325
+ }
326
+
327
+ .uploadTips {
328
+ display: block;
329
+ width: 100%;
330
+ margin-top: 5px;
331
+ line-height: 1.2em;
332
+ span {
333
+ display: inline-block;
334
+ margin-right: 10px;
335
+ }
336
+ b {
337
+ color: var(--el-color-danger, #ff3300);
338
+ }
339
+ }
340
+
341
+ .uploadTips_isDrag {
342
+ position: absolute;
343
+ top: 4px;
344
+ left: 0;
345
+ text-align: center;
346
+ }
347
+
348
+ .fileList {
349
+ width: 100%;
350
+
351
+ .fileLis_item {
352
+ display: flex;
353
+ align-items: center;
354
+ justify-content: space-between;
355
+ width: 100%;
356
+ margin-top: 8px;
357
+ padding: 5px 0;
358
+ border: 1px solid var(--jnrs-font-primary-03);
359
+ background: var(--jnrs-background-primary);
360
+
361
+ .fileLis_item_left {
362
+ flex: 1;
363
+ position: relative;
364
+ display: flex;
365
+ align-items: center;
366
+ justify-content: flex-start;
367
+ padding-right: 50px;
368
+ line-height: 1.1em;
369
+ }
370
+
371
+ .fileLis_item_right {
372
+ flex-shrink: 0;
373
+ width: 60px;
374
+ display: flex;
375
+ align-items: center;
376
+ justify-content: flex-end;
377
+ padding-right: 4px;
378
+ }
379
+
380
+ .fileLis_item_index {
381
+ flex-shrink: 0;
382
+ width: 20px;
383
+ height: 20px;
384
+ line-height: 20px;
385
+ text-align: center;
386
+ }
387
+
388
+ .fileLis_item_icon {
389
+ flex-shrink: 0;
390
+ width: 20px;
391
+ height: 20px;
392
+ }
393
+
394
+ .fileLis_item_name {
395
+ padding: 0 4px;
396
+ word-break: break-all;
397
+ }
398
+
399
+ .fileLis_item_size {
400
+ position: absolute;
401
+ right: 0;
402
+ width: 50px;
403
+ color: #999;
404
+ white-space: nowrap;
405
+ text-align: right;
406
+ }
407
+
408
+ .fileLis_item_btn {
409
+ flex-shrink: 0;
410
+ display: flex;
411
+ align-items: center;
412
+ justify-content: center;
413
+ width: 20px;
414
+ height: 20px;
415
+ margin: 0 2px;
416
+ border-radius: 3px;
417
+ font-size: 16px;
418
+ color: var(--el-color-primary, #5887f7);
419
+ transition: all 0.3s ease;
420
+ cursor: pointer;
421
+
422
+ &:hover {
423
+ background-color: var(--jnrs-card-primary);
424
+ }
425
+ }
426
+
427
+ .fileLis_item_btn_danger {
428
+ color: var(--el-color-danger, #ff3300);
429
+ }
430
+ }
431
+ }
432
+ }
433
+ </style>
@@ -0,0 +1,106 @@
1
+ <script lang="ts" setup>
2
+ import type { FileItem } from '@jnrs/shared'
3
+ import type { Attachment } from '@/types'
4
+ import { ref, watch, onActivated, onDeactivated, onBeforeUnmount } from 'vue'
5
+ import { blobToUrl } from '@jnrs/shared'
6
+ import { FileApi } from '@/api/common'
7
+
8
+ import { JnPdfView } from '@jnrs/vue-core/components'
9
+
10
+ interface Props {
11
+ /**
12
+ * 要加载的文件列表 | 文件名唯一标识 uniqueFileName
13
+ */
14
+ loadKeys: Attachment[] | string | undefined
15
+ }
16
+
17
+ const { loadKeys } = defineProps<Props>()
18
+
19
+ // 存储每个 URL 对应的 URL 对象
20
+ const fileList = ref<FileItem[]>([])
21
+
22
+ // 存储每个 URL 对应的 revoke 函数用于副作用清理
23
+ const revokeFns = ref<(() => void)[]>([])
24
+
25
+ // 清理当前所有 Object URL
26
+ const clearUrls = () => {
27
+ revokeFns.value.forEach((revoke) => revoke())
28
+ revokeFns.value = []
29
+ fileList.value = []
30
+ }
31
+
32
+ const loadFiles = async (keys: Props['loadKeys']) => {
33
+ clearUrls()
34
+
35
+ // 如果是字符串
36
+ if (typeof keys === 'string') {
37
+ try {
38
+ const blob = await FileApi(keys)
39
+ const { url, revoke } = blobToUrl(blob)
40
+ fileList.value = [{ src: url, fileName: keys }]
41
+ revokeFns.value = [revoke]
42
+ } catch (err) {
43
+ console.warn(keys, err)
44
+ }
45
+ return
46
+ }
47
+
48
+ // 如果是对象
49
+ if (keys !== null && typeof keys === 'object' && !Array.isArray(keys)) {
50
+ const { uniqueFileName, fileName } = keys
51
+ try {
52
+ const blob = await FileApi(uniqueFileName)
53
+ const { url, revoke } = blobToUrl(blob)
54
+ fileList.value = [{ src: url, fileName: fileName || uniqueFileName }]
55
+ revokeFns.value = [revoke]
56
+ } catch (err) {
57
+ console.warn(uniqueFileName, err)
58
+ }
59
+ return
60
+ }
61
+
62
+ // 如果是数组
63
+ if (Array.isArray(keys) && keys.length > 0) {
64
+ const res = await Promise.all(
65
+ keys.map(async ({ uniqueFileName, fileName }) => {
66
+ try {
67
+ const blob = await FileApi(uniqueFileName)
68
+ const { url, revoke } = blobToUrl(blob)
69
+ return { url, fileName, revoke }
70
+ } catch (err) {
71
+ console.warn(uniqueFileName, err)
72
+ return { url: '', fileName: '', revoke: () => {} }
73
+ }
74
+ })
75
+ )
76
+ fileList.value = res.map(({ url, fileName }) => ({ src: url, fileName }))
77
+ revokeFns.value = res.map((d) => d.revoke)
78
+ }
79
+ }
80
+
81
+ watch(
82
+ () => loadKeys,
83
+ (nv) => {
84
+ loadFiles(nv)
85
+ },
86
+ { deep: true, immediate: true }
87
+ )
88
+
89
+ onActivated(() => {
90
+ loadFiles(loadKeys)
91
+ })
92
+
93
+ // 副作用清理
94
+ onDeactivated(() => {
95
+ clearUrls()
96
+ })
97
+
98
+ // 副作用清理
99
+ onBeforeUnmount(() => {
100
+ clearUrls()
101
+ })
102
+ </script>
103
+
104
+ <template>
105
+ <jn-pdf-view :fileList="fileList"></jn-pdf-view>
106
+ </template>
@@ -0,0 +1,37 @@
1
+ <!--
2
+ @Author : TanRui
3
+ @WeChat : Tan578853789
4
+ @File : JnDatetime.vue
5
+ @Date : 2025/12/25
6
+ @desc. : 显示时间格式化,第一行是日期,第二行是时间
7
+ -->
8
+
9
+ <script setup lang="ts">
10
+ import { computed } from 'vue'
11
+
12
+ interface Props {
13
+ value: string
14
+ color?: string
15
+ }
16
+ const { value = '', color = '' } = defineProps<Props>()
17
+
18
+ const datetime = computed(() => {
19
+ if (value && value.split('T').length === 2) {
20
+ return value.split('T')
21
+ }
22
+ if (value && value.split(' ').length === 2) {
23
+ return value.split(' ')
24
+ }
25
+ return false
26
+ })
27
+ </script>
28
+
29
+ <template>
30
+ <template v-if="datetime">
31
+ <div style="line-height: 1.2em" :style="{ color: color }">
32
+ <div>{{ datetime[0] }}</div>
33
+ <span style="opacity: 0.7; font-size: 0.9em">{{ datetime[1] }}</span>
34
+ </div>
35
+ </template>
36
+ <span v-else>{{ value }}</span>
37
+ </template>
@@ -0,0 +1,70 @@
1
+ <!--
2
+ @Author : TanRui
3
+ @WeChat : Tan578853789
4
+ @File : JnDictTag.vue
5
+ @Date : 2025/12/25
6
+ @Desc. : 带颜色的字典标签
7
+ -->
8
+
9
+ <script setup lang="ts">
10
+ import { computed } from 'vue'
11
+ import { getDictLabel, getDictColor } from '@/utils/packages'
12
+
13
+ interface Props {
14
+ dictName: string
15
+ value: string | number
16
+ /**
17
+ * 是否使用颜色
18
+ */
19
+ showColor?: boolean
20
+ /**
21
+ * 是否使用 popover
22
+ */
23
+ popover?: string
24
+ /**
25
+ * 消息放置位置
26
+ */
27
+ placement?: 'top' | 'top-left' | 'top-right' | 'bottom' | 'bottom-left' | 'bottom-right'
28
+ }
29
+
30
+ const { dictName = '', value = '', showColor = true, popover = '' } = defineProps<Props>()
31
+
32
+ const computedColor = computed(() => {
33
+ return showColor ? getDictColor(dictName, value) : 'unset'
34
+ })
35
+ </script>
36
+
37
+ <template>
38
+ <el-popover :content="popover" placement="top" :disabled="!popover">
39
+ <template #reference>
40
+ <span
41
+ class="dictTag"
42
+ :style="{
43
+ backgroundColor: computedColor
44
+ }"
45
+ >
46
+ <span
47
+ :class="{ dictTag_label_showColor: showColor }"
48
+ :style="{
49
+ color: computedColor
50
+ }"
51
+ >
52
+ {{ getDictLabel(dictName, value) }}
53
+ </span>
54
+ </span>
55
+ </template>
56
+ </el-popover>
57
+ </template>
58
+
59
+ <style lang="scss" scoped>
60
+ .dictTag {
61
+ padding: 2px 8px;
62
+ border-radius: 8px;
63
+ white-space: nowrap;
64
+
65
+ .dictTag_label_showColor {
66
+ font-size: 0.8em;
67
+ filter: invert(0.6) brightness(0.6);
68
+ }
69
+ }
70
+ </style>
@@ -0,0 +1,68 @@
1
+ <!--
2
+ @Author : TanRui
3
+ @WeChat : Tan578853789
4
+ @File : JnEdit.vue
5
+ @Date : 2025/12/25
6
+ @Desc. : 表格编辑组件
7
+ -->
8
+
9
+ <script setup lang="ts">
10
+ import { ref } from 'vue'
11
+
12
+ interface Props {
13
+ /**
14
+ * 对话框的宽度,默认值为 50%
15
+ */
16
+ title?: string
17
+ /**
18
+ * 对话框的宽度,默认值为 50%
19
+ */
20
+ width?: string | number
21
+ }
22
+
23
+ const { width = '50%', title = '' } = defineProps<Props>()
24
+
25
+ const emit = defineEmits<{
26
+ closed: []
27
+ }>()
28
+
29
+ const dialogVisible = ref(false)
30
+
31
+ const open = () => {
32
+ dialogVisible.value = true
33
+ }
34
+
35
+ const close = () => {
36
+ dialogVisible.value = false
37
+ }
38
+
39
+ const handleClosed = () => {
40
+ emit('closed')
41
+ }
42
+
43
+ defineExpose({
44
+ open,
45
+ close,
46
+ handleClosed
47
+ })
48
+ </script>
49
+
50
+ <template>
51
+ <el-dialog
52
+ v-model="dialogVisible"
53
+ :title="title"
54
+ :width="width"
55
+ :close-on-click-modal="true"
56
+ :close-on-press-escape="true"
57
+ draggable
58
+ overflow
59
+ @closed="handleClosed"
60
+ >
61
+ <slot></slot>
62
+ <template #footer>
63
+ <slot name="footer"></slot>
64
+ </template>
65
+ </el-dialog>
66
+ </template>
67
+
68
+ <style lang="scss" scoped></style>