foggy-data-viewer 1.0.1-beta.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 (34) hide show
  1. package/README.md +273 -0
  2. package/dist/favicon.svg +4 -0
  3. package/dist/index.js +1531 -0
  4. package/dist/index.umd +1 -0
  5. package/dist/style.css +1 -0
  6. package/package.json +51 -0
  7. package/src/App.vue +469 -0
  8. package/src/api/viewer.ts +163 -0
  9. package/src/components/DataTable.test.ts +533 -0
  10. package/src/components/DataTable.vue +810 -0
  11. package/src/components/DataTableWithSearch.test.ts +628 -0
  12. package/src/components/DataTableWithSearch.vue +277 -0
  13. package/src/components/DataViewer.vue +310 -0
  14. package/src/components/SearchToolbar.test.ts +521 -0
  15. package/src/components/SearchToolbar.vue +406 -0
  16. package/src/components/composables/index.ts +2 -0
  17. package/src/components/composables/useTableSelection.test.ts +248 -0
  18. package/src/components/composables/useTableSelection.ts +44 -0
  19. package/src/components/composables/useTableSummary.test.ts +341 -0
  20. package/src/components/composables/useTableSummary.ts +129 -0
  21. package/src/components/filters/BoolFilter.vue +103 -0
  22. package/src/components/filters/DateRangeFilter.vue +194 -0
  23. package/src/components/filters/NumberRangeFilter.vue +160 -0
  24. package/src/components/filters/SelectFilter.vue +464 -0
  25. package/src/components/filters/TextFilter.vue +230 -0
  26. package/src/components/filters/index.ts +5 -0
  27. package/src/examples/EnhancedTableExample.vue +136 -0
  28. package/src/index.ts +32 -0
  29. package/src/main.ts +14 -0
  30. package/src/types/index.ts +159 -0
  31. package/src/utils/README.md +140 -0
  32. package/src/utils/schemaHelper.test.ts +215 -0
  33. package/src/utils/schemaHelper.ts +44 -0
  34. package/src/vite-env.d.ts +7 -0
@@ -0,0 +1,194 @@
1
+ <script setup lang="ts">
2
+ import { ref, watch, computed } from 'vue'
3
+ import type { SliceRequestDef } from '@/types'
4
+
5
+ interface Props {
6
+ field: string
7
+ modelValue?: SliceRequestDef[] | null
8
+ format?: string
9
+ showTime?: boolean
10
+ }
11
+
12
+ const props = withDefaults(defineProps<Props>(), {
13
+ format: 'YYYY-MM-DD',
14
+ showTime: false
15
+ })
16
+
17
+ const emit = defineEmits<{
18
+ (e: 'update:modelValue', value: SliceRequestDef[] | null): void
19
+ }>()
20
+
21
+ // 日期范围值 [开始, 结束]
22
+ const dateRange = ref<[Date, Date] | null>(null)
23
+
24
+ // 是否显示时间选择
25
+ const isDatetime = computed(() => props.showTime || props.format?.includes('HH'))
26
+
27
+ // 日期格式
28
+ const dateFormat = computed(() => {
29
+ if (isDatetime.value) {
30
+ return 'YYYY-MM-DD HH:mm'
31
+ }
32
+ return 'YYYY-MM-DD'
33
+ })
34
+
35
+ // 从 modelValue 初始化
36
+ watch(() => props.modelValue, (slices) => {
37
+ if (!slices || slices.length === 0) {
38
+ dateRange.value = null
39
+ return
40
+ }
41
+
42
+ // 从 slice 中解析日期范围
43
+ // 支持 [) 操作符或两个 >= <= 条件
44
+ const rangeSlice = slices.find(s => s.op === '[)' || s.op === '[]')
45
+ if (rangeSlice && Array.isArray(rangeSlice.value)) {
46
+ const [start, end] = rangeSlice.value as [string, string]
47
+ dateRange.value = [
48
+ new Date(start.replace(' ', 'T')),
49
+ new Date(end.replace(' ', 'T'))
50
+ ]
51
+ } else {
52
+ // 尝试从 >= 和 <= 条件解析
53
+ const gteSlice = slices.find(s => s.op === '>=')
54
+ const lteSlice = slices.find(s => s.op === '<=' || s.op === '<')
55
+ if (gteSlice || lteSlice) {
56
+ const start = gteSlice ? new Date(String(gteSlice.value).replace(' ', 'T')) : null
57
+ const end = lteSlice ? new Date(String(lteSlice.value).replace(' ', 'T')) : null
58
+ if (start && end) {
59
+ dateRange.value = [start, end]
60
+ } else if (start) {
61
+ dateRange.value = [start, start]
62
+ } else if (end) {
63
+ dateRange.value = [end, end]
64
+ }
65
+ } else {
66
+ dateRange.value = null
67
+ }
68
+ }
69
+ }, { immediate: true })
70
+
71
+ function formatDate(date: Date): string {
72
+ const year = date.getFullYear()
73
+ const month = String(date.getMonth() + 1).padStart(2, '0')
74
+ const day = String(date.getDate()).padStart(2, '0')
75
+
76
+ if (isDatetime.value) {
77
+ const hours = String(date.getHours()).padStart(2, '0')
78
+ const minutes = String(date.getMinutes()).padStart(2, '0')
79
+ const seconds = String(date.getSeconds()).padStart(2, '0')
80
+ return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
81
+ }
82
+
83
+ return `${year}-${month}-${day}`
84
+ }
85
+
86
+ function handleChange(val: [Date, Date] | null) {
87
+ if (!val || val.length !== 2) {
88
+ emit('update:modelValue', null)
89
+ return
90
+ }
91
+
92
+ const [start, end] = val
93
+ const startVal = formatDate(start)
94
+ // 对于日期,结束日期使用 < 下一天来实现左闭右开
95
+ let endVal: string
96
+ if (isDatetime.value) {
97
+ endVal = formatDate(end)
98
+ } else {
99
+ // 日期范围:结束日期+1天,使用 [) 左闭右开
100
+ const nextDay = new Date(end)
101
+ nextDay.setDate(nextDay.getDate() + 1)
102
+ endVal = formatDate(nextDay)
103
+ }
104
+
105
+ // 生成 DSL slice: { field, op: "[)", value: [start, end] }
106
+ emit('update:modelValue', [{
107
+ field: props.field,
108
+ op: '[)',
109
+ value: [startVal, endVal]
110
+ }])
111
+ }
112
+ </script>
113
+
114
+ <template>
115
+ <div class="filter-date-range">
116
+ <el-date-picker
117
+ v-model="dateRange"
118
+ :type="isDatetime ? 'datetimerange' : 'daterange'"
119
+ :format="dateFormat"
120
+ range-separator="~"
121
+ start-placeholder="开始"
122
+ end-placeholder="结束"
123
+ size="small"
124
+ :clearable="true"
125
+ :editable="false"
126
+ @change="handleChange"
127
+ />
128
+ </div>
129
+ </template>
130
+
131
+ <style scoped>
132
+ .filter-date-range {
133
+ width: 100%;
134
+ }
135
+
136
+ .filter-date-range :deep(.el-date-editor) {
137
+ width: 100% !important;
138
+ max-width: 100%;
139
+ height: 26px;
140
+ }
141
+
142
+ .filter-date-range :deep(.el-range-input) {
143
+ font-size: 11px;
144
+ flex: 1;
145
+ min-width: 0;
146
+ }
147
+
148
+ .filter-date-range :deep(.el-range-separator) {
149
+ font-size: 11px;
150
+ padding: 0 2px;
151
+ line-height: 24px;
152
+ flex-shrink: 0;
153
+ }
154
+
155
+ .filter-date-range :deep(.el-input__wrapper) {
156
+ padding: 0 4px;
157
+ height: 26px;
158
+ box-sizing: border-box;
159
+ }
160
+
161
+ /* 控制图标大小 */
162
+ .filter-date-range :deep(.el-input__prefix),
163
+ .filter-date-range :deep(.el-input__suffix) {
164
+ display: flex;
165
+ align-items: center;
166
+ height: 26px;
167
+ }
168
+
169
+ .filter-date-range :deep(.el-input__prefix-inner),
170
+ .filter-date-range :deep(.el-input__suffix-inner) {
171
+ display: flex;
172
+ align-items: center;
173
+ height: 100%;
174
+ }
175
+
176
+ .filter-date-range :deep(.el-input__prefix .el-icon),
177
+ .filter-date-range :deep(.el-input__suffix .el-icon) {
178
+ font-size: 12px !important;
179
+ width: 12px;
180
+ height: 12px;
181
+ line-height: 12px;
182
+ }
183
+
184
+ .filter-date-range :deep(.el-icon svg) {
185
+ width: 12px;
186
+ height: 12px;
187
+ }
188
+
189
+ /* 确保不会换行 */
190
+ .filter-date-range :deep(.el-range-editor--small .el-range__close-icon) {
191
+ font-size: 12px;
192
+ line-height: 24px;
193
+ }
194
+ </style>
@@ -0,0 +1,160 @@
1
+ <script setup lang="ts">
2
+ import { ref, watch } from 'vue'
3
+ import type { SliceRequestDef } from '@/types'
4
+
5
+ interface Props {
6
+ field: string
7
+ modelValue?: SliceRequestDef[] | null
8
+ placeholderMin?: string
9
+ placeholderMax?: string
10
+ }
11
+
12
+ const props = withDefaults(defineProps<Props>(), {
13
+ placeholderMin: '最小',
14
+ placeholderMax: '最大'
15
+ })
16
+
17
+ const emit = defineEmits<{
18
+ (e: 'update:modelValue', value: SliceRequestDef[] | null): void
19
+ }>()
20
+
21
+ const minValue = ref<string>('')
22
+ const maxValue = ref<string>('')
23
+
24
+ // 从 modelValue 初始化
25
+ watch(() => props.modelValue, (slices) => {
26
+ if (!slices || slices.length === 0) {
27
+ minValue.value = ''
28
+ maxValue.value = ''
29
+ return
30
+ }
31
+
32
+ // 解析 [] 范围操作符
33
+ const rangeSlice = slices.find(s => s.op === '[]' || s.op === '[)')
34
+ if (rangeSlice && Array.isArray(rangeSlice.value)) {
35
+ const [min, max] = rangeSlice.value as [number, number]
36
+ minValue.value = min != null ? String(min) : ''
37
+ maxValue.value = max != null ? String(max) : ''
38
+ } else {
39
+ // 解析 >= 和 <= 条件
40
+ const gteSlice = slices.find(s => s.op === '>=')
41
+ const lteSlice = slices.find(s => s.op === '<=')
42
+ minValue.value = gteSlice?.value != null ? String(gteSlice.value) : ''
43
+ maxValue.value = lteSlice?.value != null ? String(lteSlice.value) : ''
44
+ }
45
+ }, { immediate: true })
46
+
47
+ function emitChange() {
48
+ const min = minValue.value.trim()
49
+ const max = maxValue.value.trim()
50
+
51
+ if (!min && !max) {
52
+ emit('update:modelValue', null)
53
+ return
54
+ }
55
+
56
+ const minNum = min ? parseFloat(min) : null
57
+ const maxNum = max ? parseFloat(max) : null
58
+
59
+ // 验证数字
60
+ if (min && isNaN(minNum!)) return
61
+ if (max && isNaN(maxNum!)) return
62
+
63
+ // 生成 DSL slice
64
+ if (minNum !== null && maxNum !== null) {
65
+ // 两个都有值:使用 [] 闭区间
66
+ emit('update:modelValue', [{
67
+ field: props.field,
68
+ op: '[]',
69
+ value: [minNum, maxNum]
70
+ }])
71
+ } else if (minNum !== null) {
72
+ // 只有最小值:>=
73
+ emit('update:modelValue', [{
74
+ field: props.field,
75
+ op: '>=',
76
+ value: minNum
77
+ }])
78
+ } else if (maxNum !== null) {
79
+ // 只有最大值:<=
80
+ emit('update:modelValue', [{
81
+ field: props.field,
82
+ op: '<=',
83
+ value: maxNum
84
+ }])
85
+ }
86
+ }
87
+
88
+ function clear() {
89
+ minValue.value = ''
90
+ maxValue.value = ''
91
+ emit('update:modelValue', null)
92
+ }
93
+ </script>
94
+
95
+ <template>
96
+ <div class="filter-number-range">
97
+ <input
98
+ v-model="minValue"
99
+ type="text"
100
+ :placeholder="placeholderMin"
101
+ @change="emitChange"
102
+ @keyup.enter="emitChange"
103
+ />
104
+ <span class="separator">-</span>
105
+ <input
106
+ v-model="maxValue"
107
+ type="text"
108
+ :placeholder="placeholderMax"
109
+ @change="emitChange"
110
+ @keyup.enter="emitChange"
111
+ />
112
+ <span v-if="minValue || maxValue" class="clear-btn" @click="clear">×</span>
113
+ </div>
114
+ </template>
115
+
116
+ <style scoped>
117
+ .filter-number-range {
118
+ display: flex;
119
+ align-items: center;
120
+ gap: 4px;
121
+ position: relative;
122
+ }
123
+
124
+ .filter-number-range input {
125
+ flex: 1;
126
+ min-width: 0;
127
+ width: 50px;
128
+ height: 26px;
129
+ padding: 0 6px;
130
+ border: 1px solid #dcdfe6;
131
+ border-radius: 4px;
132
+ font-size: 12px;
133
+ line-height: 26px;
134
+ outline: none;
135
+ text-align: right;
136
+ transition: border-color 0.2s;
137
+ }
138
+
139
+ .filter-number-range input:focus {
140
+ border-color: #409eff;
141
+ }
142
+
143
+ .separator {
144
+ color: #909399;
145
+ font-size: 12px;
146
+ flex-shrink: 0;
147
+ }
148
+
149
+ .clear-btn {
150
+ position: absolute;
151
+ right: -16px;
152
+ color: #c0c4cc;
153
+ cursor: pointer;
154
+ font-size: 14px;
155
+ }
156
+
157
+ .clear-btn:hover {
158
+ color: #909399;
159
+ }
160
+ </style>