jianghu-ui 1.0.1

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 (112) hide show
  1. package/README.md +376 -0
  2. package/dist/jianghu-ui.css +2318 -0
  3. package/dist/jianghu-ui.js +2 -0
  4. package/dist/jianghu-ui.js.LICENSE.txt +1 -0
  5. package/package.json +56 -0
  6. package/src/Design.stories.mdx +195 -0
  7. package/src/Introduction.stories.mdx +148 -0
  8. package/src/components/JhAddressSelect/JhAddressSelect.md +250 -0
  9. package/src/components/JhAddressSelect/JhAddressSelect.stories.js +282 -0
  10. package/src/components/JhAddressSelect/JhAddressSelect.vue +261 -0
  11. package/src/components/JhCard/JhCard.md +246 -0
  12. package/src/components/JhCard/JhCard.stories.js +688 -0
  13. package/src/components/JhCard/JhCard.vue +604 -0
  14. package/src/components/JhCheckCard/JhCheckCard.md +245 -0
  15. package/src/components/JhCheckCard/JhCheckCard.stories.js +750 -0
  16. package/src/components/JhCheckCard/JhCheckCard.vue +476 -0
  17. package/src/components/JhConfirmDialog/JhConfirmDialog.md +70 -0
  18. package/src/components/JhConfirmDialog/JhConfirmDialog.stories.js +550 -0
  19. package/src/components/JhConfirmDialog/JhConfirmDialog.vue +181 -0
  20. package/src/components/JhDateRangePicker/JhDateRangePicker.md +56 -0
  21. package/src/components/JhDateRangePicker/JhDateRangePicker.stories.js +320 -0
  22. package/src/components/JhDateRangePicker/JhDateRangePicker.vue +307 -0
  23. package/src/components/JhDescriptions/JhDescriptions.md +724 -0
  24. package/src/components/JhDescriptions/JhDescriptions.stories.js +858 -0
  25. package/src/components/JhDescriptions/JhDescriptions.vue +933 -0
  26. package/src/components/JhDraggable/JhDraggable.md +66 -0
  27. package/src/components/JhDraggable/JhDraggable.stories.js +161 -0
  28. package/src/components/JhDraggable/JhDraggable.vue +254 -0
  29. package/src/components/JhDrawer/JhDrawer.md +68 -0
  30. package/src/components/JhDrawer/JhDrawer.stories.js +478 -0
  31. package/src/components/JhDrawer/JhDrawer.vue +281 -0
  32. package/src/components/JhDrawerForm/JhDrawerForm.md +69 -0
  33. package/src/components/JhDrawerForm/JhDrawerForm.stories.js +492 -0
  34. package/src/components/JhDrawerForm/JhDrawerForm.vue +297 -0
  35. package/src/components/JhEditableTable/JhEditableTable.md +507 -0
  36. package/src/components/JhEditableTable/JhEditableTable.stories.js +615 -0
  37. package/src/components/JhEditableTable/JhEditableTable.vue +685 -0
  38. package/src/components/JhFileInput/JhFileInput.md +56 -0
  39. package/src/components/JhFileInput/JhFileInput.stories.js +103 -0
  40. package/src/components/JhFileInput/JhFileInput.vue +253 -0
  41. package/src/components/JhForm/JhForm.md +676 -0
  42. package/src/components/JhForm/JhForm.stories.js +1375 -0
  43. package/src/components/JhForm/JhForm.vue +657 -0
  44. package/src/components/JhFormField/JhFormField.stories.js +217 -0
  45. package/src/components/JhFormField/JhFormField.vue +439 -0
  46. package/src/components/JhFormFields/JhFormFields.md +647 -0
  47. package/src/components/JhFormFields/JhFormFields.stories.js +922 -0
  48. package/src/components/JhFormFields/JhFormFields.vue +998 -0
  49. package/src/components/JhFormList/JhFormList.md +303 -0
  50. package/src/components/JhFormList/JhFormList.stories.js +661 -0
  51. package/src/components/JhFormList/JhFormList.vue +1127 -0
  52. package/src/components/JhJsonEditor/JhJsonEditor.md +54 -0
  53. package/src/components/JhJsonEditor/JhJsonEditor.stories.js +157 -0
  54. package/src/components/JhJsonEditor/JhJsonEditor.vue +178 -0
  55. package/src/components/JhLayout/JhLayout.md +580 -0
  56. package/src/components/JhLayout/JhLayout.stories.js +414 -0
  57. package/src/components/JhLayout/JhLayout.vue +387 -0
  58. package/src/components/JhList/JhList.md +441 -0
  59. package/src/components/JhList/JhList.stories.js +524 -0
  60. package/src/components/JhList/JhList.vue +571 -0
  61. package/src/components/JhMarkdownEditor/JhMarkdownEditor.md +56 -0
  62. package/src/components/JhMarkdownEditor/JhMarkdownEditor.stories.js +191 -0
  63. package/src/components/JhMarkdownEditor/JhMarkdownEditor.vue +188 -0
  64. package/src/components/JhMask/JhMask.md +62 -0
  65. package/src/components/JhMask/JhMask.stories.js +270 -0
  66. package/src/components/JhMask/JhMask.vue +123 -0
  67. package/src/components/JhMenu/JhMenu.md +85 -0
  68. package/src/components/JhMenu/JhMenu.stories.js +384 -0
  69. package/src/components/JhMenu/JhMenu.vue +545 -0
  70. package/src/components/JhModal/JhModal.md +68 -0
  71. package/src/components/JhModal/JhModal.stories.js +562 -0
  72. package/src/components/JhModal/JhModal.vue +235 -0
  73. package/src/components/JhModalForm/JhModalForm.md +69 -0
  74. package/src/components/JhModalForm/JhModalForm.stories.js +592 -0
  75. package/src/components/JhModalForm/JhModalForm.vue +298 -0
  76. package/src/components/JhPageContainer/JhPageContainer.md +409 -0
  77. package/src/components/JhPageContainer/JhPageContainer.stories.js +209 -0
  78. package/src/components/JhPageContainer/JhPageContainer.vue +72 -0
  79. package/src/components/JhQueryFilter/JhQueryFilter.md +77 -0
  80. package/src/components/JhQueryFilter/JhQueryFilter.stories.js +684 -0
  81. package/src/components/JhQueryFilter/JhQueryFilter.vue +429 -0
  82. package/src/components/JhScene/JhScene.md +64 -0
  83. package/src/components/JhScene/JhScene.stories.js +317 -0
  84. package/src/components/JhScene/JhScene.vue +376 -0
  85. package/src/components/JhStatisticCard/JhStatisticCard.md +363 -0
  86. package/src/components/JhStatisticCard/JhStatisticCard.stories.js +847 -0
  87. package/src/components/JhStatisticCard/JhStatisticCard.vue +459 -0
  88. package/src/components/JhStepsForm/JhStepsForm.md +666 -0
  89. package/src/components/JhStepsForm/JhStepsForm.stories.js +1224 -0
  90. package/src/components/JhStepsForm/JhStepsForm.vue +749 -0
  91. package/src/components/JhTable/JhTable.md +730 -0
  92. package/src/components/JhTable/JhTable.stories.js +1444 -0
  93. package/src/components/JhTable/JhTable.vue +2298 -0
  94. package/src/components/JhTableAttachment/JhTableAttachment.md +70 -0
  95. package/src/components/JhTableAttachment/JhTableAttachment.stories.js +198 -0
  96. package/src/components/JhTableAttachment/JhTableAttachment.vue +264 -0
  97. package/src/components/JhToast/JhToast.md +67 -0
  98. package/src/components/JhToast/JhToast.stories.js +386 -0
  99. package/src/components/JhToast/JhToast.vue +239 -0
  100. package/src/components/JhTreeSelect/JhTreeSelect.md +82 -0
  101. package/src/components/JhTreeSelect/JhTreeSelect.stories.js +391 -0
  102. package/src/components/JhTreeSelect/JhTreeSelect.vue +727 -0
  103. package/src/components/JhWaterMark/JhWaterMark.md +190 -0
  104. package/src/components/JhWaterMark/JhWaterMark.stories.js +675 -0
  105. package/src/components/JhWaterMark/JhWaterMark.vue +351 -0
  106. package/src/components/README.md +52 -0
  107. package/src/index.js +135 -0
  108. package/src/style/globalCSSJHV4.css +348 -0
  109. package/src/style/globalCSSVuetifyV4.css +637 -0
  110. package/src/style/storybook.css +4 -0
  111. package/src/tailwind.css +3 -0
  112. package/src/utils/vuetify.js +31 -0
@@ -0,0 +1,56 @@
1
+ # JhFileInput - 文件上传输入框
2
+
3
+ JhFileInput 封装常见图片/附件上传场景,支持单选、多选、预览、删除、数量限制等能力。
4
+
5
+ ## 功能特性
6
+
7
+ - 🖼️ **实时预览**:自动识别图片并展示缩略图,非图片显示图标
8
+ - ♻️ **多源值支持**:兼容字符串路径、字符串数组、File 对象数组
9
+ - 🚦 **数量/类型限制**:`maxFiles`、`accept` 控制上传数量与格式
10
+ - 📤 **校验规则**:通过 `rules` 注入函数即可实现自定义校验
11
+ - 🚫 **只读模式**:设置 `readonly` 即可禁止上传与删除
12
+
13
+ ## 基础用法
14
+
15
+ ```vue
16
+ <template>
17
+ <jh-file-input
18
+ v-model="files"
19
+ multiple
20
+ :max-files="5"
21
+ accept="image/*"
22
+ :rules="[(val) => val.length > 0 || '请至少上传一张图片']"
23
+ />
24
+ </template>
25
+ ```
26
+
27
+ ## API
28
+
29
+ ### Props
30
+
31
+ | 参数 | 说明 | 类型 | 默认值 |
32
+ | --- | --- | --- | --- |
33
+ | value | `v-model`,可为字符串或 File 数组 | string \| Array | [] |
34
+ | maxFiles | 最大上传数量,0 表示不限制 | number | 0 |
35
+ | accept | input accept 属性 | string | `*/*` |
36
+ | multiple | 是否多选 | boolean | false |
37
+ | rules | 额外验证函数数组,返回 `true` 或错误消息 | Function[] | [] |
38
+ | readonly | 是否只读 | boolean | false |
39
+ | uploadPrefix | 拼接在字符串路径前的前缀 | string | `/upload/` |
40
+
41
+ ### Events
42
+
43
+ | 事件名 | 说明 | 回调参数 |
44
+ | --- | --- | --- |
45
+ | input | `v-model` 更新 | (files: Array \| string) |
46
+ | change | 同步触发,便于监听 | (files: Array \| string) |
47
+
48
+ ### Slots
49
+
50
+ 组件无自定义插槽。
51
+
52
+ ## 使用建议
53
+
54
+ - 需要与后端路径解耦时,可以在回显时传入字符串数组,上传时转换为 `FormData`
55
+ - 对于大文件建议结合外部上传接口,`rules` 中可校验文件大小/类型
56
+ - 读写 File 对象时注意浏览器无法直接序列化,必要时把 meta 信息保存到其他字段
@@ -0,0 +1,103 @@
1
+ import JhFileInput from './JhFileInput.vue';
2
+
3
+ export default {
4
+ title: '基础组件/JhFileInput - 文件上传',
5
+ component: JhFileInput,
6
+ tags: ['autodocs'],
7
+ argTypes: {
8
+ value: {
9
+ control: 'object',
10
+ description: '文件值,可以是数组、字符串或文件对象',
11
+ },
12
+ maxFiles: {
13
+ control: 'number',
14
+ description: '最多可上传的文件数量,0 表示不限制',
15
+ },
16
+ accept: {
17
+ control: 'text',
18
+ description: '接受的文件类型,如 "image/*" 或 ".jpg,.png"',
19
+ },
20
+ multiple: {
21
+ control: 'boolean',
22
+ description: '是否允许多文件上传',
23
+ },
24
+ readonly: {
25
+ control: 'boolean',
26
+ description: '是否只读模式',
27
+ },
28
+ uploadPrefix: {
29
+ control: 'text',
30
+ description: '上传文件的 URL 前缀',
31
+ },
32
+ },
33
+ };
34
+
35
+ export const 基础示例 = {
36
+ args: {
37
+ value: [],
38
+ maxFiles: 0,
39
+ accept: '*/*',
40
+ multiple: false,
41
+ readonly: false,
42
+ uploadPrefix: '/upload/',
43
+ },
44
+ };
45
+
46
+ export const 单图上传 = {
47
+ args: {
48
+ value: [],
49
+ maxFiles: 1,
50
+ accept: 'image/*',
51
+ multiple: false,
52
+ readonly: false,
53
+ uploadPrefix: '/upload/',
54
+ },
55
+ };
56
+
57
+ export const 多图上传 = {
58
+ args: {
59
+ value: [],
60
+ maxFiles: 5,
61
+ accept: 'image/*',
62
+ multiple: true,
63
+ readonly: false,
64
+ uploadPrefix: '/upload/',
65
+ },
66
+ };
67
+
68
+ export const 已有文件 = {
69
+ args: {
70
+ value: ['images/photo1.jpg', 'images/photo2.png', 'documents/file.pdf'],
71
+ maxFiles: 5,
72
+ accept: '*/*',
73
+ multiple: true,
74
+ readonly: false,
75
+ uploadPrefix: '/upload/',
76
+ },
77
+ };
78
+
79
+ export const 只读模式 = {
80
+ args: {
81
+ value: ['images/photo1.jpg', 'images/photo2.png'],
82
+ maxFiles: 0,
83
+ accept: '*/*',
84
+ multiple: true,
85
+ readonly: true,
86
+ uploadPrefix: '/upload/',
87
+ },
88
+ };
89
+
90
+ export const 带验证 = {
91
+ args: {
92
+ value: [],
93
+ maxFiles: 3,
94
+ accept: 'image/*',
95
+ multiple: true,
96
+ readonly: false,
97
+ uploadPrefix: '/upload/',
98
+ rules: [
99
+ (v) => (v && v.length > 0) || '请至少上传一个文件',
100
+ (v) => (v && v.length <= 3) || '最多只能上传3个文件',
101
+ ],
102
+ },
103
+ };
@@ -0,0 +1,253 @@
1
+ <template>
2
+ <v-app>
3
+ <v-container fluid class="pa-0">
4
+ <div class="w-full">
5
+ <!-- 文件上传区域 -->
6
+ <div class="grid grid-cols-[repeat(auto-fill,minmax(100px,1fr))] gap-4">
7
+ <!-- 已上传的文件 -->
8
+ <div v-if="value && isString(value)" class="relative aspect-square rounded overflow-hidden">
9
+ <div class="w-full h-full flex flex-col items-center justify-center p-2 bg-gray-100">
10
+ <img v-if="isImage(value)" :src="getImageUrl(value)" class="max-w-full max-h-full object-contain">
11
+ <v-icon v-else size="40" class="text-gray-500">mdi-file-document-outline</v-icon>
12
+ </div>
13
+ <!-- 删除按钮 -->
14
+ <v-btn
15
+ v-if="!readonly"
16
+ icon
17
+ x-small
18
+ class="absolute top-1 right-1 !bg-black/50"
19
+ @click="handleRemoveSingleFile"
20
+ >
21
+ <v-icon small color="white">mdi-close</v-icon>
22
+ </v-btn>
23
+ </div>
24
+ <div v-else-if="value && Array.isArray(value)" v-for="(file, index) in value" :key="index" class="relative aspect-square border border-gray-200 rounded overflow-hidden">
25
+ <!-- 预览图 -->
26
+ <div v-if="isString(file)" class="w-full h-full flex flex-col items-center justify-center p-2 bg-gray-100">
27
+ <img v-if="isImage(file)" :src="getImageUrl(file)" class="max-w-full max-h-full object-contain">
28
+ <v-icon v-else size="40" class="text-gray-500">mdi-file-document-outline</v-icon>
29
+ </div>
30
+ <div v-else class="w-full h-full flex flex-col items-center justify-center p-2 bg-gray-100">
31
+ <img v-if="isImage(file)" :src="getObjectUrl(file)" class="max-w-full max-h-full object-contain">
32
+ <v-icon v-else size="40" class="text-gray-500">mdi-file-document-outline</v-icon>
33
+ </div>
34
+ <!-- 删除按钮 -->
35
+ <v-btn
36
+ v-if="!readonly"
37
+ icon
38
+ x-small
39
+ class="absolute top-1 right-1 !bg-black/50"
40
+ @click="removeFile(index)"
41
+ >
42
+ <v-icon small color="white">mdi-close</v-icon>
43
+ </v-btn>
44
+ <!-- 类型 -->
45
+ <div class="absolute top-0 left-2 text-xs bg-green-500 text-white px-1 rounded-b-sm">
46
+ {{ getFileExtension(file) }}
47
+ </div>
48
+ </div>
49
+
50
+ <!-- 上传按钮 -->
51
+ <div class="aspect-square border-2 border-dashed border-gray-200 rounded flex items-center justify-center cursor-pointer transition-all hover:border-blue-500 hover:bg-blue-50" @click="triggerFileInput" v-if="isCanUpload">
52
+ <input
53
+ type="file"
54
+ ref="fileInput"
55
+ :multiple="multiple"
56
+ :accept="accept"
57
+ @change="handleFileChange"
58
+ class="hidden"
59
+ >
60
+ <v-icon size="40" class="text-gray-500 transition-all group-hover:text-blue-500">mdi-plus</v-icon>
61
+ </div>
62
+ </div>
63
+
64
+ <!-- 错误提示 -->
65
+ <div v-if="errorMessage" class="text-red-500 text-xs mt-2 pl-0.5">
66
+ {{ errorMessage }}
67
+ </div>
68
+
69
+ <!-- 上传限制提示 -->
70
+ <div v-if="isShowUploadLimit" class="text-gray-500 text-xs mt-2 pl-0.5">
71
+ 最多可上传 {{ maxFiles }} 个文件 (已上传 {{ isString(value) ? 1 : (value || []).length }} 个)
72
+ </div>
73
+ </div>
74
+ </v-container>
75
+ </v-app>
76
+ </template>
77
+
78
+ <script>
79
+ export default {
80
+ name: 'JhFileInput',
81
+ props: {
82
+ // 已选文件集合,可为 File[]、字符串路径数组或单个字符串
83
+ value: {
84
+ type: [Array, String],
85
+ default: () => []
86
+ },
87
+ // 限制最大可上传文件数,0 表示不限制
88
+ maxFiles: {
89
+ type: Number,
90
+ default: 0 // 0 表示不限制
91
+ },
92
+ // input accept 属性,控制允许的文件类型
93
+ accept: {
94
+ type: String,
95
+ default: '*/*' // 默认接受所有文件类型
96
+ },
97
+ // 是否允许多选
98
+ multiple: {
99
+ type: Boolean,
100
+ default: false
101
+ },
102
+ // 额外的校验规则
103
+ rules: {
104
+ type: Array,
105
+ default: () => []
106
+ },
107
+ // 只读模式(禁用上传和删除)
108
+ readonly: {
109
+ type: Boolean,
110
+ default: false
111
+ },
112
+ // 上传接口需要拼接的路径前缀
113
+ uploadPrefix: {
114
+ type: String,
115
+ default: '/upload/'
116
+ }
117
+ },
118
+ data() {
119
+ return {
120
+ errorMessage: ''
121
+ }
122
+ },
123
+ watch: {
124
+ value: {
125
+ handler() {
126
+ this.validate();
127
+ },
128
+ deep: true
129
+ }
130
+ },
131
+ computed: {
132
+ isCanUpload() {
133
+ if (this.readonly) return false;
134
+
135
+ if (this.multiple) {
136
+ return !this.maxFiles || ((this.value || []).length < this.maxFiles);
137
+ } else {
138
+ return (this.value || []).length < 1;
139
+ }
140
+ },
141
+ isShowUploadLimit() {
142
+ return !this.readonly && this.multiple && this.maxFiles > 0;
143
+ }
144
+ },
145
+ methods: {
146
+ isString(val) {
147
+ return typeof val === 'string';
148
+ },
149
+ triggerFileInput() {
150
+ this.$refs.fileInput.click();
151
+ },
152
+
153
+ async handleFileChange(event) {
154
+ const files = Array.from(event.target.files);
155
+
156
+ // 检查文件数量限制
157
+ if (this.maxFiles && (this.value || []).length + files.length > this.maxFiles) {
158
+ this.errorMessage = `最多只能上传 ${this.maxFiles} 个文件`;
159
+ return;
160
+ }
161
+
162
+ let newFiles = [...(this.value || [])];
163
+ for (const file of files) {
164
+ try {
165
+ // 检查是否已存在相同文件
166
+ const isDuplicate = newFiles.some(existingFile =>
167
+ existingFile.name === file.name &&
168
+ existingFile.size === file.size
169
+ );
170
+
171
+ if (isDuplicate) {
172
+ this.errorMessage = `文件 ${file.name} 已存在`;
173
+ continue;
174
+ }
175
+
176
+ newFiles.push(file);
177
+ } catch (error) {
178
+ console.error('Upload error:', error);
179
+ this.errorMessage = `上传文件 ${file.name} 失败`;
180
+ }
181
+ }
182
+
183
+ this.$emit('input', newFiles);
184
+ this.$emit('change', newFiles);
185
+ if (!this.errorMessage) {
186
+ this.errorMessage = '';
187
+ }
188
+
189
+ // 清空 input,以便重复选择同一文件
190
+ event.target.value = '';
191
+ },
192
+
193
+ handleRemoveSingleFile() {
194
+ this.$emit('input', []);
195
+ this.$emit('change', []);
196
+ },
197
+
198
+ removeFile(index) {
199
+ const files = [...this.value];
200
+ files.splice(index, 1);
201
+ this.$emit('input', files);
202
+ this.$emit('change', files);
203
+ },
204
+
205
+ isImage(file) {
206
+ if (this.isString(file)) {
207
+ return ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'].includes(file.split('.').pop().toLowerCase());
208
+ }
209
+ return file.type ? file.type.startsWith('image/') : false;
210
+ },
211
+
212
+ getImageUrl(path) {
213
+ return `${this.uploadPrefix}${path}`;
214
+ },
215
+
216
+ getObjectUrl(file) {
217
+ return URL.createObjectURL(file);
218
+ },
219
+
220
+ getFileExtension(file) {
221
+ if (this.isString(file)) {
222
+ return file.split('.').pop();
223
+ }
224
+ return file.name.split('.').pop();
225
+ },
226
+
227
+ validate() {
228
+ this.errorMessage = '';
229
+
230
+ // 执行验证规则
231
+ for (const rule of this.rules) {
232
+ const result = rule(this.value);
233
+ if (result !== true) {
234
+ this.errorMessage = result;
235
+ return false;
236
+ }
237
+ }
238
+
239
+ return true;
240
+ }
241
+ }
242
+ }
243
+ </script>
244
+
245
+ <style scoped>
246
+ .aspect-square {
247
+ aspect-ratio: 1 / 1;
248
+ }
249
+
250
+ .grid {
251
+ display: grid;
252
+ }
253
+ </style>