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,571 @@
1
+ <template>
2
+ <div :class="['jh-pro-list', { 'jh-pro-list-card': cardBordered, 'jh-pro-list-ghost': ghost }]">
3
+ <!-- 标题区 -->
4
+ <div v-if="headerTitle || $slots['header-title'] || $slots['toolbar-actions'] || $slots['toolbar-extra']" class="jh-pro-list-header">
5
+ <div class="jh-pro-list-header-left">
6
+ <div v-if="headerTitle || $slots['header-title']" class="jh-pro-list-title">
7
+ <slot name="header-title">
8
+ <span class="jh-pro-list-title-text">{{ headerTitle }}</span>
9
+ <v-tooltip v-if="tooltip" bottom>
10
+ <template v-slot:activator="{ on, attrs }">
11
+ <v-icon small class="ml-1" v-bind="attrs" v-on="on">mdi-help-circle-outline</v-icon>
12
+ </template>
13
+ <span>{{ tooltip }}</span>
14
+ </v-tooltip>
15
+ </slot>
16
+ </div>
17
+ <div v-if="$slots['toolbar-actions']" class="jh-pro-list-header-actions">
18
+ <slot name="toolbar-actions"></slot>
19
+ </div>
20
+ </div>
21
+ <div class="jh-pro-list-header-right">
22
+ <slot name="toolbar-extra"></slot>
23
+ </div>
24
+ </div>
25
+
26
+ <!-- 工具栏 -->
27
+ <v-row v-if="toolbar !== false" class="jh-pro-list-toolbar ma-0 pb-3 pa-3 pa-md-0" align="center">
28
+ <v-spacer></v-spacer>
29
+ <div class="flex items-center gap-2 justify-end flex-wrap">
30
+ <v-text-field
31
+ v-if="toolbarConfig.search"
32
+ v-model="searchInputInternal"
33
+ :prefix="isMobile ? '' : '筛选'"
34
+ :placeholder="isMobile ? '筛选' : ''"
35
+ class="jh-v-input jh-toolbar-search border"
36
+ dense
37
+ filled
38
+ single-line
39
+ hide-details
40
+ clearable
41
+ ></v-text-field>
42
+
43
+ <v-btn v-if="toolbarConfig.refresh" icon small @click="handleRefresh" title="刷新">
44
+ <v-icon>mdi-refresh</v-icon>
45
+ </v-btn>
46
+
47
+ <v-menu v-if="toolbarConfig.layout" offset-y>
48
+ <template v-slot:activator="{ on, attrs }">
49
+ <v-btn icon small v-bind="attrs" v-on="on" title="布局">
50
+ <v-icon>{{ layoutIcon }}</v-icon>
51
+ </v-btn>
52
+ </template>
53
+ <v-list dense>
54
+ <v-list-item @click="currentLayout = 'list'">
55
+ <v-list-item-icon><v-icon small>mdi-view-list</v-icon></v-list-item-icon>
56
+ <v-list-item-title>列表</v-list-item-title>
57
+ </v-list-item>
58
+ <v-list-item @click="currentLayout = 'grid'">
59
+ <v-list-item-icon><v-icon small>mdi-view-grid</v-icon></v-list-item-icon>
60
+ <v-list-item-title>网格</v-list-item-title>
61
+ </v-list-item>
62
+ </v-list>
63
+ </v-menu>
64
+
65
+ <v-menu v-if="toolbarConfig.size" offset-y>
66
+ <template v-slot:activator="{ on, attrs }">
67
+ <v-btn icon small v-bind="attrs" v-on="on" title="尺寸">
68
+ <v-icon>mdi-format-line-spacing</v-icon>
69
+ </v-btn>
70
+ </template>
71
+ <v-list dense>
72
+ <v-list-item @click="currentSize = 'large'">
73
+ <v-list-item-title>
74
+ <v-icon v-if="currentSize === 'large'" small left>mdi-check</v-icon>大
75
+ </v-list-item-title>
76
+ </v-list-item>
77
+ <v-list-item @click="currentSize = 'default'">
78
+ <v-list-item-title>
79
+ <v-icon v-if="currentSize === 'default'" small left>mdi-check</v-icon>默认
80
+ </v-list-item-title>
81
+ </v-list-item>
82
+ <v-list-item @click="currentSize = 'small'">
83
+ <v-list-item-title>
84
+ <v-icon v-if="currentSize === 'small'" small left>mdi-check</v-icon>小
85
+ </v-list-item-title>
86
+ </v-list-item>
87
+ </v-list>
88
+ </v-menu>
89
+ </div>
90
+ </v-row>
91
+
92
+ <!-- 批量操作提示栏 -->
93
+ <div v-if="rowSelection && selectedItems.length > 0" class="jh-pro-list-alert">
94
+ <div class="jh-pro-list-alert-info">
95
+ <slot name="alert" :selected-row-keys="selectedRowKeys" :selected-rows="selectedItems">
96
+ <v-icon small class="mr-2" color="primary">mdi-checkbox-marked-circle</v-icon>
97
+ <span>已选择 <strong class="primary--text">{{ selectedItems.length }}</strong> 项</span>
98
+ <v-btn text x-small class="ml-2" @click="clearSelection">清空</v-btn>
99
+ </slot>
100
+ </div>
101
+ <div class="jh-pro-list-alert-actions">
102
+ <slot name="alert-actions" :selected-row-keys="selectedRowKeys" :selected-rows="selectedItems"></slot>
103
+ </div>
104
+ </div>
105
+
106
+ <!-- 加载状态 -->
107
+ <div v-if="currentLoading && currentItems.length === 0" class="jh-pro-list-loading">
108
+ <v-progress-circular indeterminate color="primary"></v-progress-circular>
109
+ <div class="mt-2">数据加载中...</div>
110
+ </div>
111
+
112
+ <!-- 空状态 -->
113
+ <div v-else-if="currentItems.length === 0" class="jh-pro-list-empty">
114
+ <v-icon large color="grey lighten-1">mdi-inbox-outline</v-icon>
115
+ <div class="mt-2 text-body-2 grey--text">暂无数据</div>
116
+ </div>
117
+
118
+ <!-- 列表内容 -->
119
+ <div v-else>
120
+ <!-- 网格布局 -->
121
+ <v-row v-if="currentLayout === 'grid' && grid" class="jh-pro-list-grid ma-0">
122
+ <v-col
123
+ v-for="(item, index) in currentItems"
124
+ :key="getRowKey(item, index)"
125
+ :cols="grid.column || 12"
126
+ :sm="grid.sm"
127
+ :md="grid.md"
128
+ :lg="grid.lg"
129
+ :xl="grid.xl"
130
+ class="pa-2"
131
+ >
132
+ <div :class="itemClasses(item)" @click="handleItemClick(item, index)">
133
+ <div v-if="rowSelection" class="jh-pro-list-item-checkbox" @click.stop="toggleSelection(item)">
134
+ <v-checkbox :input-value="isItemSelected(item)" hide-details dense :color="checkboxColor"></v-checkbox>
135
+ </div>
136
+ <slot name="renderItem" :item="item" :index="index">
137
+ <div class="jh-pro-list-item-content">
138
+ <div v-if="getMeta('avatar', item)" class="jh-pro-list-item-avatar">
139
+ <v-avatar :size="avatarSize">
140
+ <img v-if="typeof getMeta('avatar', item) === 'string'" :src="getMeta('avatar', item)" :alt="getMeta('title', item)" />
141
+ <span v-else>{{ getMeta('avatar', item) }}</span>
142
+ </v-avatar>
143
+ </div>
144
+ <div class="jh-pro-list-item-main">
145
+ <div v-if="getMeta('title', item)" class="jh-pro-list-item-title">
146
+ <slot name="title" :item="item">{{ getMeta('title', item) }}</slot>
147
+ </div>
148
+ <div v-if="getMeta('subTitle', item)" class="jh-pro-list-item-subtitle">
149
+ <slot name="subTitle" :item="item">{{ getMeta('subTitle', item) }}</slot>
150
+ </div>
151
+ <div v-if="getMeta('description', item)" class="jh-pro-list-item-description">
152
+ <slot name="description" :item="item">{{ getMeta('description', item) }}</slot>
153
+ </div>
154
+ <div v-if="getMeta('content', item)" class="jh-pro-list-item-content-area">
155
+ <slot name="content" :item="item">{{ getMeta('content', item) }}</slot>
156
+ </div>
157
+ </div>
158
+ <div v-if="getMeta('extra', item)" class="jh-pro-list-item-extra">
159
+ <slot name="extra" :item="item">{{ getMeta('extra', item) }}</slot>
160
+ </div>
161
+ </div>
162
+ <div v-if="getMeta('actions', item)" class="jh-pro-list-item-actions">
163
+ <slot name="actions" :item="item">
164
+ <v-btn
165
+ v-for="(action, idx) in getMeta('actions', item)"
166
+ :key="idx"
167
+ text
168
+ x-small
169
+ :color="action.color || 'primary'"
170
+ @click.stop="handleActionClick(action, item)"
171
+ >
172
+ <v-icon v-if="action.icon" small left>{{ action.icon }}</v-icon>
173
+ {{ action.text }}
174
+ </v-btn>
175
+ </slot>
176
+ </div>
177
+ </slot>
178
+ </div>
179
+ </v-col>
180
+ </v-row>
181
+
182
+ <!-- 列表布局 -->
183
+ <div v-else class="jh-pro-list-items">
184
+ <div
185
+ v-for="(item, index) in currentItems"
186
+ :key="getRowKey(item, index)"
187
+ :class="itemClasses(item)"
188
+ @click="handleItemClick(item, index)"
189
+ >
190
+ <div v-if="rowSelection" class="jh-pro-list-item-checkbox" @click.stop="toggleSelection(item)">
191
+ <v-checkbox :input-value="isItemSelected(item)" hide-details dense :color="checkboxColor"></v-checkbox>
192
+ </div>
193
+ <slot name="renderItem" :item="item" :index="index">
194
+ <div class="jh-pro-list-item-content">
195
+ <div v-if="getMeta('avatar', item)" class="jh-pro-list-item-avatar">
196
+ <v-avatar :size="avatarSize">
197
+ <img v-if="typeof getMeta('avatar', item) === 'string'" :src="getMeta('avatar', item)" :alt="getMeta('title', item)" />
198
+ <span v-else>{{ getMeta('avatar', item) }}</span>
199
+ </v-avatar>
200
+ </div>
201
+ <div class="jh-pro-list-item-main">
202
+ <div v-if="getMeta('title', item)" class="jh-pro-list-item-title">
203
+ <slot name="title" :item="item">{{ getMeta('title', item) }}</slot>
204
+ </div>
205
+ <div v-if="getMeta('subTitle', item)" class="jh-pro-list-item-subtitle">
206
+ <slot name="subTitle" :item="item">{{ getMeta('subTitle', item) }}</slot>
207
+ </div>
208
+ <div v-if="getMeta('description', item)" class="jh-pro-list-item-description">
209
+ <slot name="description" :item="item">{{ getMeta('description', item) }}</slot>
210
+ </div>
211
+ <div v-if="getMeta('content', item)" class="jh-pro-list-item-content-area">
212
+ <slot name="content" :item="item">{{ getMeta('content', item) }}</slot>
213
+ </div>
214
+ </div>
215
+ <div v-if="getMeta('extra', item)" class="jh-pro-list-item-extra">
216
+ <slot name="extra" :item="item">{{ getMeta('extra', item) }}</slot>
217
+ </div>
218
+ </div>
219
+ <div v-if="getMeta('actions', item)" class="jh-pro-list-item-actions">
220
+ <slot name="actions" :item="item">
221
+ <v-btn
222
+ v-for="(action, idx) in getMeta('actions', item)"
223
+ :key="idx"
224
+ text
225
+ x-small
226
+ :color="action.color || 'primary'"
227
+ @click.stop="handleActionClick(action, item)"
228
+ >
229
+ <v-icon v-if="action.icon" small left>{{ action.icon }}</v-icon>
230
+ {{ action.text }}
231
+ </v-btn>
232
+ </slot>
233
+ </div>
234
+ </slot>
235
+ <div v-if="expandable && expandedKeys.includes(getRowKey(item, index))" class="jh-pro-list-item-expand">
236
+ <slot name="expandedRowRender" :item="item" :index="index"></slot>
237
+ </div>
238
+ </div>
239
+ </div>
240
+ </div>
241
+
242
+ <!-- 分页 -->
243
+ <div v-if="pagination !== false && currentItems.length > 0" class="jh-pro-list-pagination">
244
+ <v-pagination v-model="currentPage" :length="totalPages" :total-visible="7" @input="handlePageChange"></v-pagination>
245
+ <div class="jh-pro-list-pagination-info">
246
+ <span>共 {{ totalCount }} 条</span>
247
+ <v-select
248
+ v-model="currentPageSize"
249
+ :items="pageSizeOptions"
250
+ dense
251
+ outlined
252
+ hide-details
253
+ class="ml-2"
254
+ style="max-width: 100px;"
255
+ @change="handlePageSizeChange"
256
+ >
257
+ <template v-slot:selection="{ item }">{{ item }} 条/页</template>
258
+ <template v-slot:item="{ item }">{{ item }} 条/页</template>
259
+ </v-select>
260
+ </div>
261
+ </div>
262
+ </div>
263
+ </template>
264
+
265
+ <script>
266
+ export default {
267
+ name: 'JhList',
268
+ props: {
269
+ // 静态数据源(非 request 模式)
270
+ dataSource: {
271
+ type: Array,
272
+ default: () => []
273
+ },
274
+ // 远程请求函数(返回 Promise)
275
+ request: {
276
+ type: Function,
277
+ default: null
278
+ },
279
+ // 行唯一键,可为字段名或函数
280
+ rowKey: {
281
+ type: [String, Function],
282
+ default: 'id'
283
+ },
284
+ // 字段映射配置(标题、描述等)
285
+ metas: {
286
+ type: Object,
287
+ default: () => ({})
288
+ },
289
+ // 展示布局 list | grid | card
290
+ layout: {
291
+ type: String,
292
+ default: 'list',
293
+ validator: (v) => ['list', 'grid', 'card'].includes(v)
294
+ },
295
+ // Grid 布局参数(cols、gutter 等)
296
+ grid: {
297
+ type: Object,
298
+ default: null
299
+ },
300
+ // 尺寸 small | default | large
301
+ size: {
302
+ type: String,
303
+ default: 'default',
304
+ validator: (v) => ['small', 'default', 'large'].includes(v)
305
+ },
306
+ // 是否展示分割线
307
+ split: {
308
+ type: Boolean,
309
+ default: true
310
+ },
311
+ // 分页配置(false 关闭分页)
312
+ pagination: {
313
+ type: [Object, Boolean],
314
+ default: () => ({ current: 1, pageSize: 10, total: 0 })
315
+ },
316
+ // 行选择配置
317
+ rowSelection: {
318
+ type: [Object, Boolean],
319
+ default: null
320
+ },
321
+ // 复选框颜色
322
+ checkboxColor: {
323
+ type: String,
324
+ default: 'primary'
325
+ },
326
+ // 行展开配置
327
+ expandable: {
328
+ type: [Object, Boolean],
329
+ default: null
330
+ },
331
+ // 标题文本
332
+ headerTitle: {
333
+ type: String,
334
+ default: ''
335
+ },
336
+ // 标题提示
337
+ tooltip: {
338
+ type: String,
339
+ default: ''
340
+ },
341
+ // 是否展示卡片边框
342
+ cardBordered: {
343
+ type: Boolean,
344
+ default: true
345
+ },
346
+ // 幽灵模式(无背景)
347
+ ghost: {
348
+ type: Boolean,
349
+ default: false
350
+ },
351
+ // 工具栏配置或开关
352
+ toolbar: {
353
+ type: [Object, Boolean],
354
+ default: () => ({ search: true, refresh: true, layout: true, size: true })
355
+ },
356
+ // 顶部搜索配置(false 关闭)
357
+ search: {
358
+ type: [Object, Boolean],
359
+ default: false
360
+ },
361
+ // 全局加载态
362
+ loading: {
363
+ type: Boolean,
364
+ default: false
365
+ },
366
+ // 轮询刷新间隔(ms,0 关闭)
367
+ polling: {
368
+ type: Number,
369
+ default: 0
370
+ },
371
+ // 搜索输入防抖时间
372
+ debounceTime: {
373
+ type: Number,
374
+ default: 300
375
+ }
376
+ },
377
+ data() {
378
+ return {
379
+ isMobile: window.innerWidth < 600,
380
+ searchInputInternal: '',
381
+ currentLoading: this.loading,
382
+ currentItems: [],
383
+ currentPage: this.pagination ? this.pagination.current || 1 : 1,
384
+ currentPageSize: this.pagination ? this.pagination.pageSize || 10 : 10,
385
+ totalCount: 0,
386
+ currentLayout: this.layout,
387
+ currentSize: this.size,
388
+ selectedItems: [],
389
+ expandedKeys: [],
390
+ pollingTimer: null,
391
+ searchDebounceTimer: null
392
+ };
393
+ },
394
+ computed: {
395
+ toolbarConfig() {
396
+ if (this.toolbar === false) return {};
397
+ return typeof this.toolbar === 'object' ? { search: true, refresh: true, layout: true, size: true, ...this.toolbar } : { search: true, refresh: true, layout: true, size: true };
398
+ },
399
+ layoutIcon() {
400
+ return { list: 'mdi-view-list', grid: 'mdi-view-grid', card: 'mdi-view-module' }[this.currentLayout] || 'mdi-view-list';
401
+ },
402
+ avatarSize() {
403
+ return { small: 32, default: 40, large: 48 }[this.currentSize];
404
+ },
405
+ selectedRowKeys() {
406
+ return this.selectedItems.map(item => this.getRowKey(item));
407
+ },
408
+ totalPages() {
409
+ return Math.ceil(this.totalCount / this.currentPageSize);
410
+ },
411
+ pageSizeOptions() {
412
+ return [10, 20, 50, 100];
413
+ }
414
+ },
415
+ watch: {
416
+ dataSource: { immediate: true, handler(val) { if (!this.request) { this.currentItems = val || []; this.totalCount = this.currentItems.length; } } },
417
+ loading(val) { this.currentLoading = val; },
418
+ searchInputInternal(val) {
419
+ if (this.request) {
420
+ if (this.searchDebounceTimer) clearTimeout(this.searchDebounceTimer);
421
+ this.searchDebounceTimer = setTimeout(() => this.reload(), this.debounceTime);
422
+ } else this.filterItems();
423
+ },
424
+ layout(val) { this.currentLayout = val; },
425
+ size(val) { this.currentSize = val; }
426
+ },
427
+ mounted() {
428
+ window.addEventListener('resize', this.handleResize);
429
+ if (this.request) this.reload();
430
+ this.startPolling();
431
+ },
432
+ beforeDestroy() {
433
+ window.removeEventListener('resize', this.handleResize);
434
+ this.stopPolling();
435
+ if (this.searchDebounceTimer) clearTimeout(this.searchDebounceTimer);
436
+ },
437
+ methods: {
438
+ getRowKey(item, index) {
439
+ return typeof this.rowKey === 'function' ? this.rowKey(item, index) : item[this.rowKey] || index;
440
+ },
441
+ getMeta(metaKey, item) {
442
+ const meta = this.metas[metaKey];
443
+ if (!meta) return null;
444
+ if (typeof meta === 'string') return item[meta];
445
+ if (typeof meta === 'function') return meta(item);
446
+ if (meta.dataIndex) return item[meta.dataIndex];
447
+ if (meta.render) return meta.render(item);
448
+ return null;
449
+ },
450
+ itemClasses(item) {
451
+ return ['jh-pro-list-item', `jh-pro-list-item--${this.currentSize}`, { 'jh-pro-list-item--selected': this.isItemSelected(item) }];
452
+ },
453
+ isItemSelected(item) {
454
+ const key = this.getRowKey(item);
455
+ return this.selectedItems.some(selectedItem => this.getRowKey(selectedItem) === key);
456
+ },
457
+ toggleSelection(item) {
458
+ const key = this.getRowKey(item);
459
+ const index = this.selectedItems.findIndex(selectedItem => this.getRowKey(selectedItem) === key);
460
+ if (index > -1) this.selectedItems.splice(index, 1);
461
+ else this.selectedItems.push(item);
462
+ if (this.rowSelection && this.rowSelection.onChange) this.rowSelection.onChange(this.selectedRowKeys, this.selectedItems);
463
+ this.$emit('selection-change', { selectedRowKeys: this.selectedRowKeys, selectedRows: this.selectedItems });
464
+ },
465
+ clearSelection() {
466
+ this.selectedItems = [];
467
+ if (this.rowSelection && this.rowSelection.onChange) this.rowSelection.onChange([], []);
468
+ this.$emit('selection-change', { selectedRowKeys: [], selectedRows: [] });
469
+ },
470
+ handleItemClick(item, index) { this.$emit('item-click', item, index); },
471
+ handleActionClick(action, item) {
472
+ if (action.onClick) action.onClick(item);
473
+ this.$emit('action-click', action, item);
474
+ },
475
+ async handleRefresh() {
476
+ this.$emit('refresh');
477
+ if (this.request) await this.reload();
478
+ },
479
+ handlePageChange(page) {
480
+ this.currentPage = page;
481
+ if (this.request) this.reload();
482
+ else this.filterItems();
483
+ this.$emit('page-change', page);
484
+ },
485
+ handlePageSizeChange(pageSize) {
486
+ this.currentPageSize = pageSize;
487
+ this.currentPage = 1;
488
+ if (this.request) this.reload();
489
+ else this.filterItems();
490
+ this.$emit('page-size-change', pageSize);
491
+ },
492
+ filterItems() {
493
+ let items = [...this.dataSource];
494
+ if (this.searchInputInternal) {
495
+ const searchLower = this.searchInputInternal.toLowerCase();
496
+ items = items.filter(item => Object.values(item).some(val => String(val).toLowerCase().includes(searchLower)));
497
+ }
498
+ this.totalCount = items.length;
499
+ if (this.pagination !== false) {
500
+ const start = (this.currentPage - 1) * this.currentPageSize;
501
+ this.currentItems = items.slice(start, start + this.currentPageSize);
502
+ } else this.currentItems = items;
503
+ },
504
+ async reload() {
505
+ if (!this.request) { this.filterItems(); return; }
506
+ this.currentLoading = true;
507
+ try {
508
+ const response = await this.request({ current: this.currentPage, pageSize: this.currentPageSize, search: this.searchInputInternal });
509
+ if (response && response.success !== false) {
510
+ this.currentItems = response.data || response.list || [];
511
+ this.totalCount = response.total || response.totalCount || this.currentItems.length;
512
+ }
513
+ } catch (error) {
514
+ console.error('Failed to load data:', error);
515
+ this.$emit('request-error', error);
516
+ } finally {
517
+ this.currentLoading = false;
518
+ }
519
+ },
520
+ handleResize() { this.isMobile = window.innerWidth < 600; },
521
+ startPolling() {
522
+ if (this.polling > 0 && this.request) this.pollingTimer = setInterval(() => this.reload(), this.polling);
523
+ },
524
+ stopPolling() {
525
+ if (this.pollingTimer) { clearInterval(this.pollingTimer); this.pollingTimer = null; }
526
+ }
527
+ }
528
+ };
529
+ </script>
530
+
531
+ <style scoped>
532
+ .jh-pro-list { border-radius: 8px; position: relative; }
533
+ .jh-pro-list-card { background: #ffffff; border: 1px solid #f0f0f0; padding: 16px; }
534
+ .jh-pro-list-ghost { background: transparent; border: none; }
535
+ .jh-pro-list-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; padding: 16px 0; }
536
+ .jh-pro-list-header-left { display: flex; align-items: center; gap: 16px; }
537
+ .jh-pro-list-header-right { display: flex; align-items: center; gap: 8px; }
538
+ .jh-pro-list-title { display: flex; align-items: center; }
539
+ .jh-pro-list-title-text { font-size: 18px; font-weight: 500; color: rgba(0, 0, 0, 0.85); }
540
+ .jh-pro-list-toolbar { padding: 8px 0; }
541
+ .jh-pro-list-alert { display: flex; justify-content: space-between; align-items: center; padding: 12px 16px; background: #e6f7ff; border: 1px solid #91d5ff; border-radius: 4px; margin-bottom: 16px; }
542
+ .jh-pro-list-alert-info { display: flex; align-items: center; }
543
+ .jh-pro-list-alert-actions { display: flex; gap: 8px; }
544
+ .jh-pro-list-loading, .jh-pro-list-empty { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 48px 24px; color: rgba(0, 0, 0, 0.45); }
545
+ .jh-pro-list-items { display: flex; flex-direction: column; }
546
+ .jh-pro-list-item { position: relative; display: flex; padding: 16px; border-bottom: 1px solid #f0f0f0; transition: all 0.3s; cursor: pointer; background: #ffffff; }
547
+ .jh-pro-list-item:hover { background: #fafafa; }
548
+ .jh-pro-list-item--selected { background: #e6f7ff; }
549
+ .jh-pro-list-item--small { padding: 12px; }
550
+ .jh-pro-list-item--large { padding: 20px; }
551
+ .jh-pro-list-item-checkbox { margin-right: 12px; display: flex; align-items: flex-start; padding-top: 4px; }
552
+ .jh-pro-list-item-content { flex: 1; display: flex; gap: 16px; }
553
+ .jh-pro-list-item-avatar { flex-shrink: 0; }
554
+ .jh-pro-list-item-main { flex: 1; min-width: 0; }
555
+ .jh-pro-list-item-title { font-size: 16px; font-weight: 500; color: rgba(0, 0, 0, 0.85); margin-bottom: 4px; }
556
+ .jh-pro-list-item-subtitle { font-size: 14px; color: rgba(0, 0, 0, 0.65); margin-bottom: 4px; }
557
+ .jh-pro-list-item-description { font-size: 14px; color: rgba(0, 0, 0, 0.45); line-height: 1.5; }
558
+ .jh-pro-list-item-content-area { margin-top: 8px; }
559
+ .jh-pro-list-item-extra { flex-shrink: 0; margin-left: 16px; }
560
+ .jh-pro-list-item-actions { display: flex; gap: 8px; margin-top: 12px; padding-top: 12px; border-top: 1px solid #f0f0f0; }
561
+ .jh-pro-list-item-expand { margin-top: 12px; padding-top: 12px; border-top: 1px solid #f0f0f0; }
562
+ .jh-pro-list-grid .jh-pro-list-item { border: 1px solid #f0f0f0; border-radius: 4px; flex-direction: column; height: 100%; }
563
+ .jh-pro-list-pagination { display: flex; justify-content: space-between; align-items: center; margin-top: 24px; padding-top: 16px; border-top: 1px solid #f0f0f0; }
564
+ .jh-pro-list-pagination-info { display: flex; align-items: center; gap: 8px; }
565
+ @media (max-width: 768px) {
566
+ .jh-pro-list-header { flex-direction: column; align-items: flex-start; gap: 12px; }
567
+ .jh-pro-list-item-content { flex-direction: column; }
568
+ .jh-pro-list-item-extra { margin-left: 0; margin-top: 12px; }
569
+ .jh-pro-list-pagination { flex-direction: column; gap: 12px; }
570
+ }
571
+ </style>
@@ -0,0 +1,56 @@
1
+ # JhMarkdownEditor - Markdown 编辑器
2
+
3
+ JhMarkdownEditor 基于 Editor.md 封装,提供全功能 Markdown 编辑、预览与 SEO HTML 输出能力。
4
+
5
+ ## 功能特性
6
+
7
+ - ✍️ **所写即所得**:支持 watch/preview 模式,实时查看渲染结果
8
+ - 🧰 **工具栏可配置**:`toolbarIcons` 控制显示的按钮,满足精简或全量需求
9
+ - 📏 **尺寸可调**:通过 `height/width` 自由适配弹窗、页面等不同场景
10
+ - 🔒 **只读模式**:`readonly` 用于详情页展示,禁用编辑能力
11
+ - 📤 **内容输出**:`content-for-seo` 事件提供编译后的 HTML,可用于摘要
12
+
13
+ ## 基础用法
14
+
15
+ ```vue
16
+ <template>
17
+ <jh-markdown-editor
18
+ v-model="content"
19
+ height="520px"
20
+ placeholder="请输入文章内容"
21
+ @content-for-seo="storeHtml"
22
+ />
23
+ </template>
24
+ ```
25
+
26
+ ## API
27
+
28
+ ### Props
29
+
30
+ | 参数 | 说明 | 类型 | 默认值 |
31
+ | --- | --- | --- | --- |
32
+ | value | 双向绑定的 Markdown 文本 | string | `''` |
33
+ | placeholder | 输入提示 | string | `请输入内容...` |
34
+ | height | 编辑器高度 | string | `calc(100vh - 400px)` |
35
+ | width | 编辑器宽度百分比 | number | 100 |
36
+ | editorPath | Editor.md 静态资源路径 | string | `/public/plugins/editor.md/lib/` |
37
+ | toolbarIcons | 工具栏图标数组 | string[] | 预置常用图标 |
38
+ | readonly | 是否只读 | boolean | false |
39
+
40
+ ### Events
41
+
42
+ | 事件名 | 说明 | 回调参数 |
43
+ | --- | --- | --- |
44
+ | input | `v-model` 更新 | (value: string) |
45
+ | change | markdown 内容变化 | (value: string) |
46
+ | content-for-seo | 返回编译后的 HTML 片段 | (html: string) |
47
+
48
+ ### Slots
49
+
50
+ 组件无插槽。
51
+
52
+ ## 使用建议
53
+
54
+ - 需在页面提前加载 Editor.md 依赖 (`window.editormd`),否则组件会给出告警
55
+ - 如果需要插入模板内容,可通过 `ref` 调用 `insertValue`
56
+ - watch `value` 时注意避免循环:组件内部已通过 `isMDChange` 做了同步保护