@firerian/fireui 1.0.0

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 (63) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +74 -0
  3. package/dist/fireui.cjs +2 -0
  4. package/dist/fireui.cjs.map +1 -0
  5. package/dist/fireui.css +1 -0
  6. package/dist/fireui.es.mjs +3867 -0
  7. package/dist/fireui.es.mjs.map +1 -0
  8. package/dist/fireui.umd.js +2 -0
  9. package/dist/fireui.umd.js.map +1 -0
  10. package/dist/types/index.d.ts +1590 -0
  11. package/package.json +132 -0
  12. package/src/components/button/button.test.ts +357 -0
  13. package/src/components/button/button.vue +366 -0
  14. package/src/components/button/index.ts +17 -0
  15. package/src/components/button/types.ts +76 -0
  16. package/src/components/form/form-item.vue +136 -0
  17. package/src/components/form/form.vue +76 -0
  18. package/src/components/form/index.ts +16 -0
  19. package/src/components/grid/col.vue +99 -0
  20. package/src/components/grid/index.ts +16 -0
  21. package/src/components/grid/row.vue +85 -0
  22. package/src/components/grid/types.ts +66 -0
  23. package/src/components/index.ts +36 -0
  24. package/src/components/input/index.ts +8 -0
  25. package/src/components/input/input.test.ts +129 -0
  26. package/src/components/input/input.vue +256 -0
  27. package/src/components/input/types.ts +100 -0
  28. package/src/components/layout/aside.vue +89 -0
  29. package/src/components/layout/container.vue +53 -0
  30. package/src/components/layout/footer.vue +57 -0
  31. package/src/components/layout/header.vue +56 -0
  32. package/src/components/layout/index.ts +28 -0
  33. package/src/components/layout/main.vue +36 -0
  34. package/src/components/layout/types.ts +74 -0
  35. package/src/components/table/index.ts +16 -0
  36. package/src/components/table/table-column.vue +69 -0
  37. package/src/components/table/table.vue +354 -0
  38. package/src/components/tips/index.ts +12 -0
  39. package/src/components/tips/tips.test.ts +96 -0
  40. package/src/components/tips/tips.vue +206 -0
  41. package/src/components/tips/types.ts +56 -0
  42. package/src/components/tooltip/index.ts +8 -0
  43. package/src/components/tooltip/tooltip.test.ts +187 -0
  44. package/src/components/tooltip/tooltip.vue +261 -0
  45. package/src/components/tooltip/types.ts +60 -0
  46. package/src/hooks/useForm.ts +233 -0
  47. package/src/hooks/useTable.ts +153 -0
  48. package/src/index.ts +48 -0
  49. package/src/styles/main.scss +6 -0
  50. package/src/styles/mixins.scss +48 -0
  51. package/src/styles/reset.scss +49 -0
  52. package/src/styles/variables.scss +137 -0
  53. package/src/types/component.ts +9 -0
  54. package/src/types/form.ts +149 -0
  55. package/src/types/global.d.ts +49 -0
  56. package/src/types/grid.ts +76 -0
  57. package/src/types/index.ts +23 -0
  58. package/src/types/table.ts +181 -0
  59. package/src/types/tooltip.ts +44 -0
  60. package/src/utils/auto-import.ts +41 -0
  61. package/src/utils/index.ts +2 -0
  62. package/src/utils/install.ts +20 -0
  63. package/src/utils/useNamespace.ts +29 -0
@@ -0,0 +1,354 @@
1
+ <template>
2
+ <div
3
+ :class="[
4
+ ns.b,
5
+ {
6
+ [ns.m('border')]: border,
7
+ [ns.m('stripe')]: stripe
8
+ }
9
+ ]"
10
+ >
11
+ <table class="fire-table__inner">
12
+ <thead>
13
+ <tr>
14
+ <th
15
+ v-for="column in columns"
16
+ :key="column.prop || column.label"
17
+ :class="[
18
+ 'fire-table__column',
19
+ {
20
+ [`fire-table__column--fixed-${column.fixed}`]: column.fixed,
21
+ [`fire-table__column--sortable`]: column.sortable
22
+ }
23
+ ]"
24
+ :style="{ width: column.width }"
25
+ @click="handleColumnClick(column)"
26
+ >
27
+ <div class="fire-table__column-header">
28
+ <slot :name="`header-${column.prop}`" :column="column">
29
+ <template v-if="column.$slots.header">
30
+ <slot :name="'header'" :column="column" />
31
+ </template>
32
+ <span v-else>{{ column.label }}</span>
33
+ </slot>
34
+ <div v-if="column.sortable" class="fire-table__column-sorter">
35
+ <span
36
+ class="fire-table__column-sorter-icon"
37
+ :class="{
38
+ 'fire-table__column-sorter-icon--ascending': column.sortOrder === 'ascending',
39
+ 'fire-table__column-sorter-icon--descending': column.sortOrder === 'descending'
40
+ }"
41
+ >
42
+ <ChevronUp :size="12" />
43
+ <ChevronDown :size="12" />
44
+ </span>
45
+ </div>
46
+ </div>
47
+ </th>
48
+ </tr>
49
+ </thead>
50
+ <tbody>
51
+ <tr
52
+ v-for="(row, index) in data"
53
+ :key="row.key || index"
54
+ :class="{
55
+ 'fire-table__row--current': highlightCurrentRow && row === currentRow
56
+ }"
57
+ @click="handleRowClick(row, $event)"
58
+ >
59
+ <td
60
+ v-for="column in columns"
61
+ :key="column.prop || column.label"
62
+ :class="[
63
+ 'fire-table__cell',
64
+ {
65
+ [`fire-table__cell--fixed-${column.fixed}`]: column.fixed
66
+ }
67
+ ]"
68
+ :style="{ width: column.width }"
69
+ >
70
+ <slot :name="`default-${column.prop}`" :row="row" :column="column" :$index="index">
71
+ <template v-if="column.$slots.default">
72
+ <slot :name="'default'" :row="row" :column="column" :$index="index" />
73
+ </template>
74
+ <span v-else>{{ row[column.prop as keyof typeof row] }}</span>
75
+ </slot>
76
+ </td>
77
+ </tr>
78
+ <tr v-if="data.length === 0" class="fire-table__empty">
79
+ <td :colspan="columns.length" class="fire-table__empty-cell">
80
+ <slot name="empty">
81
+ <div class="fire-table__empty-content">
82
+ <div style="font-size: 48px; margin-bottom: 16px;">📦</div>
83
+ <p>暂无数据</p>
84
+ </div>
85
+ </slot>
86
+ </td>
87
+ </tr>
88
+ </tbody>
89
+ </table>
90
+ </div>
91
+ </template>
92
+
93
+ <script setup lang="ts">
94
+ import { ref, watch } from 'vue'
95
+ import { provideTable } from '../../hooks/useTable'
96
+ import { useNamespace } from '../../utils'
97
+ import { ChevronUp, ChevronDown } from 'lucide-vue-next'
98
+ import type { TableProps, TableEmits } from '../../types/table'
99
+
100
+ const ns = useNamespace('table')
101
+
102
+ const props = withDefaults(defineProps<TableProps>(), {
103
+ data: () => [],
104
+ border: false,
105
+ stripe: false,
106
+ highlightCurrentRow: false,
107
+ currentRowKey: ''
108
+ })
109
+
110
+ const emit = defineEmits<TableEmits>()
111
+
112
+ const { tableContext, currentRow } = provideTable(props)
113
+
114
+ const columns = ref<any[]>([])
115
+
116
+ // 监听 data 变化
117
+ watch(() => props.data, (newData) => {
118
+ tableContext.data = newData
119
+ })
120
+
121
+ // 监听 highlightCurrentRow 变化
122
+ watch(() => props.highlightCurrentRow, (value) => {
123
+ tableContext.highlightCurrentRow = value
124
+ })
125
+
126
+ // 监听 currentRowKey 变化
127
+ watch(() => props.currentRowKey, (key) => {
128
+ tableContext.currentRowKey = key
129
+ if (key && props.data.length > 0) {
130
+ const row = props.data.find(item => item.key === key || item.id === key)
131
+ if (row) {
132
+ currentRow.value = row
133
+ }
134
+ }
135
+ })
136
+
137
+ // 监听 currentRow 变化
138
+ watch(currentRow, (newRow, oldRow) => {
139
+ emit('current-change', newRow, oldRow)
140
+ })
141
+
142
+ const handleRowClick = (row: any, event: Event) => {
143
+ if (props.highlightCurrentRow) {
144
+ currentRow.value = row
145
+ }
146
+ emit('row-click', row, event)
147
+ }
148
+
149
+ const handleColumnClick = (column: any) => {
150
+ if (column.sortable) {
151
+ // 循环切换排序状态
152
+ if (column.sortOrder === 'ascending') {
153
+ column.sortOrder = 'descending'
154
+ } else if (column.sortOrder === 'descending') {
155
+ column.sortOrder = null
156
+ } else {
157
+ column.sortOrder = 'ascending'
158
+ }
159
+
160
+ // 清除其他列的排序状态
161
+ columns.value.forEach(col => {
162
+ if (col !== column) {
163
+ col.sortOrder = null
164
+ }
165
+ })
166
+
167
+ emit('sort-change', column, column.sortOrder)
168
+ }
169
+ }
170
+
171
+ // 注册列
172
+ const registerColumn = (column: any) => {
173
+ columns.value.push(column)
174
+ }
175
+
176
+ const unregisterColumn = (column: any) => {
177
+ const index = columns.value.indexOf(column)
178
+ if (index > -1) {
179
+ columns.value.splice(index, 1)
180
+ }
181
+ }
182
+
183
+ // 暴露注册方法
184
+ defineExpose({
185
+ registerColumn,
186
+ unregisterColumn
187
+ })
188
+ </script>
189
+
190
+ <style scoped lang="scss">
191
+ @import '../../styles/variables.scss';
192
+
193
+ .#{$namespace}-table {
194
+ width: 100%;
195
+ border-collapse: collapse;
196
+ font-size: 14px;
197
+
198
+ &--border {
199
+ border: 1px solid $border-color;
200
+
201
+ .#{$namespace}-table__inner {
202
+ border: none;
203
+ }
204
+
205
+ .#{$namespace}-table__column {
206
+ border-right: 1px solid $border-color;
207
+ border-bottom: 1px solid $border-color;
208
+ }
209
+
210
+ .#{$namespace}-table__cell {
211
+ border-right: 1px solid $border-color;
212
+ border-bottom: 1px solid $border-color;
213
+ }
214
+
215
+ .#{$namespace}-table__column:last-child,
216
+ .#{$namespace}-table__cell:last-child {
217
+ border-right: none;
218
+ }
219
+ }
220
+
221
+ &--stripe {
222
+ .#{$namespace}-table__row:nth-child(even) {
223
+ background-color: $bg-color-light;
224
+ }
225
+ }
226
+
227
+ &__inner {
228
+ width: 100%;
229
+ border-collapse: collapse;
230
+ }
231
+
232
+ &__column {
233
+ padding: 12px;
234
+ text-align: left;
235
+ font-weight: 600;
236
+ color: $text-color;
237
+ background-color: $bg-color;
238
+ border-bottom: 1px solid $border-color;
239
+ white-space: nowrap;
240
+ user-select: none;
241
+
242
+ &--fixed-left {
243
+ position: sticky;
244
+ left: 0;
245
+ background-color: $bg-color;
246
+ z-index: 1;
247
+ }
248
+
249
+ &--fixed-right {
250
+ position: sticky;
251
+ right: 0;
252
+ background-color: $bg-color;
253
+ z-index: 1;
254
+ }
255
+
256
+ &--sortable {
257
+ cursor: pointer;
258
+
259
+ &:hover {
260
+ background-color: $bg-color-light;
261
+ }
262
+ }
263
+ }
264
+
265
+ &__column-header {
266
+ display: flex;
267
+ align-items: center;
268
+ justify-content: space-between;
269
+ }
270
+
271
+ &__column-sorter {
272
+ margin-left: 8px;
273
+
274
+ &-icon {
275
+ display: flex;
276
+ flex-direction: column;
277
+ align-items: center;
278
+ cursor: pointer;
279
+ color: $text-color-secondary;
280
+
281
+ &--ascending {
282
+ svg:first-child {
283
+ color: $primary-color;
284
+ }
285
+ }
286
+
287
+ &--descending {
288
+ svg:last-child {
289
+ color: $primary-color;
290
+ }
291
+ }
292
+
293
+ svg {
294
+ margin: -2px 0;
295
+ transition: color 0.2s ease;
296
+
297
+ &:hover {
298
+ color: $primary-color;
299
+ }
300
+ }
301
+ }
302
+ }
303
+
304
+ &__row {
305
+ &--current {
306
+ background-color: rgba($primary-color, 0.1) !important;
307
+ }
308
+ }
309
+
310
+ &__cell {
311
+ padding: 12px;
312
+ color: $text-color;
313
+ border-bottom: 1px solid $border-color;
314
+ white-space: nowrap;
315
+
316
+ &--fixed-left {
317
+ position: sticky;
318
+ left: 0;
319
+ background-color: #fff;
320
+ z-index: 1;
321
+ }
322
+
323
+ &--fixed-right {
324
+ position: sticky;
325
+ right: 0;
326
+ background-color: #fff;
327
+ z-index: 1;
328
+ }
329
+ }
330
+
331
+ &__empty {
332
+ height: 200px;
333
+ }
334
+
335
+ &__empty-cell {
336
+ text-align: center;
337
+ vertical-align: middle;
338
+ }
339
+
340
+ &__empty-content {
341
+ display: flex;
342
+ flex-direction: column;
343
+ align-items: center;
344
+ justify-content: center;
345
+ padding: 40px 0;
346
+
347
+ p {
348
+ margin-top: 16px;
349
+ color: $text-color-secondary;
350
+ font-size: 14px;
351
+ }
352
+ }
353
+ }
354
+ </style>
@@ -0,0 +1,12 @@
1
+ import Tips from './tips.vue'
2
+ import { withInstall } from '../../utils'
3
+
4
+ const FireTips = withInstall(Tips)
5
+
6
+ export {
7
+ FireTips
8
+ }
9
+
10
+ export default {
11
+ FireTips
12
+ }
@@ -0,0 +1,96 @@
1
+ import { describe, it, expect, vi } from 'vitest'
2
+ import { mount } from '@vue/test-utils'
3
+ import { FireTips } from './index'
4
+
5
+ describe('FireTips', () => {
6
+ it('should render correctly with default type', () => {
7
+ const wrapper = mount(FireTips, {
8
+ props: {
9
+ title: 'Info Title',
10
+ description: 'Info description'
11
+ }
12
+ })
13
+
14
+ expect(wrapper.exists()).toBe(true)
15
+ expect(wrapper.find('.fire-tips-title').text()).toBe('Info Title')
16
+ expect(wrapper.find('.fire-tips-description').text()).toBe('Info description')
17
+ expect(wrapper.classes()).toContain('fire-tips--info')
18
+ })
19
+
20
+ it('should render different types', () => {
21
+ const types = ['success', 'warning', 'error']
22
+
23
+ types.forEach((type) => {
24
+ const wrapper = mount(FireTips, {
25
+ props: {
26
+ type: type as 'success' | 'warning' | 'error',
27
+ title: `${type} Title`
28
+ }
29
+ })
30
+
31
+ expect(wrapper.classes()).toContain(`fire-tips--${type}`)
32
+ })
33
+ })
34
+
35
+ it('should be closable and emit close event', async () => {
36
+ const closeSpy = vi.fn()
37
+
38
+ const wrapper = mount(FireTips, {
39
+ props: {
40
+ title: 'Closable Tips',
41
+ closable: true
42
+ },
43
+ listeners: {
44
+ close: closeSpy
45
+ }
46
+ })
47
+
48
+ expect(wrapper.find('.fire-tips-close').exists()).toBe(true)
49
+
50
+ await wrapper.find('.fire-tips-close').trigger('click')
51
+ expect(closeSpy).toHaveBeenCalled()
52
+ })
53
+
54
+ it('should not show close button when closable is false', () => {
55
+ const wrapper = mount(FireTips, {
56
+ props: {
57
+ title: 'Not Closable Tips',
58
+ closable: false
59
+ }
60
+ })
61
+
62
+ expect(wrapper.find('.fire-tips-close').exists()).toBe(false)
63
+ })
64
+
65
+ it('should use default slot content', () => {
66
+ const wrapper = mount(FireTips, {
67
+ slots: {
68
+ default: '<div>Default slot content</div>'
69
+ }
70
+ })
71
+
72
+ expect(wrapper.text()).toContain('Default slot content')
73
+ })
74
+
75
+ it('should use title and description slots', () => {
76
+ const wrapper = mount(FireTips, {
77
+ slots: {
78
+ title: '<div>Custom Title</div>',
79
+ description: '<div>Custom Description</div>'
80
+ }
81
+ })
82
+
83
+ expect(wrapper.find('.fire-tips-title').text()).toBe('Custom Title')
84
+ expect(wrapper.find('.fire-tips-description').text()).toBe('Custom Description')
85
+ })
86
+
87
+ it('should use custom icon', () => {
88
+ const wrapper = mount(FireTips, {
89
+ slots: {
90
+ icon: '<div class="custom-icon">Icon</div>'
91
+ }
92
+ })
93
+
94
+ expect(wrapper.find('.custom-icon').exists()).toBe(true)
95
+ })
96
+ })
@@ -0,0 +1,206 @@
1
+ <template>
2
+ <div
3
+ :class="[
4
+ ns.b,
5
+ ns.m(type),
6
+ {
7
+ [ns.m('closable')]: closable
8
+ }
9
+ ]"
10
+ >
11
+ <div class="fire-tips-icon">
12
+ <slot name="icon">
13
+ <component :is="iconComponent" v-if="!icon" />
14
+ <component :is="icon" v-else />
15
+ </slot>
16
+ </div>
17
+ <div class="fire-tips-content">
18
+ <div class="fire-tips-title" v-if="title || $slots.title">
19
+ <slot name="title">{{ title }}</slot>
20
+ </div>
21
+ <div class="fire-tips-description" v-if="description || $slots.description">
22
+ <slot name="description">{{ description }}</slot>
23
+ </div>
24
+ <slot></slot>
25
+ </div>
26
+ <button
27
+ v-if="closable"
28
+ class="fire-tips-close"
29
+ @click="handleClose"
30
+ aria-label="Close"
31
+ >
32
+ <X :size="16" />
33
+ </button>
34
+ </div>
35
+ </template>
36
+
37
+ <script setup lang="ts">
38
+ import { computed } from 'vue'
39
+ import { Info, CheckCircle, AlertTriangle, XCircle, X } from 'lucide-vue-next'
40
+ import { useNamespace } from '../../utils'
41
+
42
+ const ns = useNamespace('tips')
43
+
44
+ const props = withDefaults(defineProps<{
45
+ /**
46
+ * 提示类型
47
+ */
48
+ type?: 'info' | 'success' | 'warning' | 'error'
49
+ /**
50
+ * 是否可关闭
51
+ */
52
+ closable?: boolean
53
+ /**
54
+ * 自定义图标
55
+ */
56
+ icon?: string | object
57
+ /**
58
+ * 标题
59
+ */
60
+ title?: string
61
+ /**
62
+ * 描述
63
+ */
64
+ description?: string
65
+ /**
66
+ * 自定义类名
67
+ */
68
+ class?: string
69
+ /**
70
+ * 自定义样式
71
+ */
72
+ style?: string | object
73
+ }>(), {
74
+ type: 'info',
75
+ closable: false,
76
+ icon: undefined,
77
+ title: '',
78
+ description: ''
79
+ })
80
+
81
+ const emit = defineEmits<{
82
+ /**
83
+ * 关闭事件
84
+ */
85
+ (e: 'close'): void
86
+ }>()
87
+
88
+ const iconComponent = computed(() => {
89
+ switch (props.type) {
90
+ case 'success':
91
+ return CheckCircle
92
+ case 'warning':
93
+ return AlertTriangle
94
+ case 'error':
95
+ return XCircle
96
+ default:
97
+ return Info
98
+ }
99
+ })
100
+
101
+ const handleClose = () => {
102
+ emit('close')
103
+ }
104
+ </script>
105
+
106
+ <style scoped lang="scss">
107
+ @import '../../styles/variables.scss';
108
+
109
+ .#{$namespace}-tips {
110
+ display: flex;
111
+ align-items: flex-start;
112
+ padding: 16px;
113
+ border-radius: $border-radius-base;
114
+ border: 1px solid transparent;
115
+ margin-bottom: 16px;
116
+ transition: all 0.2s ease;
117
+
118
+ &-icon {
119
+ flex-shrink: 0;
120
+ margin-right: 12px;
121
+ font-size: 20px;
122
+ line-height: 1;
123
+ margin-top: 2px;
124
+ }
125
+
126
+ &-content {
127
+ flex: 1;
128
+ min-width: 0;
129
+ }
130
+
131
+ &-title {
132
+ font-size: 14px;
133
+ font-weight: 600;
134
+ margin-bottom: 4px;
135
+ }
136
+
137
+ &-description {
138
+ font-size: 14px;
139
+ line-height: 1.4;
140
+ }
141
+
142
+ &-close {
143
+ flex-shrink: 0;
144
+ background: transparent;
145
+ border: none;
146
+ cursor: pointer;
147
+ padding: 0;
148
+ margin-left: 12px;
149
+ font-size: 16px;
150
+ line-height: 1;
151
+ color: inherit;
152
+ opacity: 0.6;
153
+ transition: opacity 0.2s ease;
154
+
155
+ &:hover {
156
+ opacity: 1;
157
+ }
158
+ }
159
+
160
+ // 类型样式
161
+ &--info {
162
+ background-color: rgba(59, 130, 246, 0.1);
163
+ border-color: rgba(59, 130, 246, 0.3);
164
+ color: $primary-color;
165
+
166
+ .#{$namespace}-tips-icon {
167
+ color: $primary-color;
168
+ }
169
+ }
170
+
171
+ &--success {
172
+ background-color: rgba(34, 197, 94, 0.1);
173
+ border-color: rgba(34, 197, 94, 0.3);
174
+ color: $success-color;
175
+
176
+ .#{$namespace}-tips-icon {
177
+ color: $success-color;
178
+ }
179
+ }
180
+
181
+ &--warning {
182
+ background-color: rgba(245, 158, 11, 0.1);
183
+ border-color: rgba(245, 158, 11, 0.3);
184
+ color: $warning-color;
185
+
186
+ .#{$namespace}-tips-icon {
187
+ color: $warning-color;
188
+ }
189
+ }
190
+
191
+ &--error {
192
+ background-color: rgba(239, 68, 68, 0.1);
193
+ border-color: rgba(239, 68, 68, 0.3);
194
+ color: $danger-color;
195
+
196
+ .#{$namespace}-tips-icon {
197
+ color: $danger-color;
198
+ }
199
+ }
200
+
201
+ // 可关闭样式
202
+ &--closable {
203
+ padding-right: 8px;
204
+ }
205
+ }
206
+ </style>
@@ -0,0 +1,56 @@
1
+ export interface TipsProps {
2
+ /**
3
+ * 提示类型
4
+ */
5
+ type?: 'info' | 'success' | 'warning' | 'error'
6
+ /**
7
+ * 是否可关闭
8
+ */
9
+ closable?: boolean
10
+ /**
11
+ * 自定义图标
12
+ */
13
+ icon?: string | object
14
+ /**
15
+ * 标题
16
+ */
17
+ title?: string
18
+ /**
19
+ * 描述
20
+ */
21
+ description?: string
22
+ /**
23
+ * 自定义类名
24
+ */
25
+ class?: string
26
+ /**
27
+ * 自定义样式
28
+ */
29
+ style?: string | object
30
+ }
31
+
32
+ export interface TipsEmits {
33
+ /**
34
+ * 关闭事件
35
+ */
36
+ (e: 'close'): void
37
+ }
38
+
39
+ export interface TipsSlots {
40
+ /**
41
+ * 默认内容
42
+ */
43
+ default: () => any
44
+ /**
45
+ * 图标
46
+ */
47
+ icon?: () => any
48
+ /**
49
+ * 标题
50
+ */
51
+ title?: () => any
52
+ /**
53
+ * 描述
54
+ */
55
+ description?: () => any
56
+ }