@mixd-id/web-scaffold 0.2.250801014 → 0.2.260801002
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.
- package/README.md +25 -1
- package/docs/components/AIDashboard.md +117 -32
- package/docs/components/Alert.md +3 -1
- package/docs/components/Card.md +5 -0
- package/docs/components/PivotList.md +207 -0
- package/docs/components/Textbox.md +20 -1
- package/docs/components/Tooltip.md +148 -0
- package/package.json +17 -61
- package/src/components/AIChat.vue +14 -5
- package/src/components/AIChatItem.vue +34 -3
- package/src/components/AIDashboard.vue +144 -98
- package/src/components/Alert.vue +58 -35
- package/src/components/Card.vue +55 -6
- package/src/components/Carousel.vue +1 -1
- package/src/{widgets → components}/CartSetting.vue +1 -0
- package/src/components/Chart.vue +2 -2
- package/src/components/Checkbox.vue +22 -24
- package/src/components/CodeEditor.vue +2 -0
- package/src/{widgets → components}/CollapsiblePanelSetting.vue +1 -0
- package/src/components/ColorPicker.vue +2 -2
- package/src/{widgets → components}/ComponentSetting2.vue +1 -1
- package/src/components/Confirm.vue +2 -0
- package/src/{widgets → components}/ContactForm.vue +1 -1
- package/src/components/CopyToClipboard.vue +0 -1
- package/src/components/Datepicker.vue +2 -0
- package/src/components/Datetimepicker.vue +1 -1
- package/src/components/Dropdown.vue +1 -1
- package/src/{widgets → components}/FeatureList.vue +1 -1
- package/src/components/Gmaps.vue +1 -1
- package/src/{widgets → components}/IconList.vue +1 -1
- package/src/components/Image.vue +1 -1
- package/src/components/ImagePreview.vue +32 -51
- package/src/components/List.vue +45 -8
- package/src/components/MarkdownEdit.vue +1 -1
- package/src/components/MarkdownPreview.vue +4 -1
- package/src/{widgets → components}/MenuItem1Setting.vue +1 -0
- package/src/components/Metric.vue +30 -7
- package/src/components/MultiDropdown.vue +3 -1
- package/src/{widgets → components}/MultiValueSetting.vue +2 -2
- package/src/{widgets → components}/MultiValueSetting2.vue +2 -2
- package/src/components/OTPField.vue +3 -1
- package/src/components/PivotList.vue +1239 -0
- package/src/{widgets → components}/PresetBar.vue +1 -1
- package/src/components/PresetSelectorFilterItem.vue +1 -1
- package/src/components/Radio.vue +1 -1
- package/src/{widgets → components}/Share.vue +1 -1
- package/src/components/SvgEditor.vue +1 -1
- package/src/components/Switch.vue +1 -1
- package/src/components/TextWithTag.vue +2 -2
- package/src/components/Textbox.vue +63 -5
- package/src/components/Toast.vue +2 -1
- package/src/components/Tooltip.vue +626 -0
- package/src/components/VirtualGrid.vue +2 -2
- package/src/components/VirtualTable.vue +4 -3
- package/src/{widgets → components}/WebDatasourceSelector.vue +2 -0
- package/src/{widgets → components}/WebLayoutSelector.vue +1 -1
- package/src/directives/tooltip.js +131 -0
- package/src/index.js +11 -235
- package/src/themes/default/index.js +16 -0
- package/src/utils/ai-dashboard.js +163 -38
- package/src/utils/helpers.mjs +52 -5
- package/src/utils/list.mjs +4 -1
- package/src/utils/pivot-formula.mjs +358 -0
- package/src/utils/pivot-list.mjs +557 -0
- package/src/utils/tooltip.mjs +243 -0
- package/src/utils/wss.js +26 -6
- package/src/utils/wss.mjs +29 -2
- /package/src/{widgets → components}/AhrefSetting.vue +0 -0
- /package/src/{widgets → components}/ArticleSetting.vue +0 -0
- /package/src/{widgets → components}/BackgroundColorSetting.vue +0 -0
- /package/src/{widgets → components}/BlockSetting.vue +0 -0
- /package/src/{widgets → components}/BorderColorSetting.vue +0 -0
- /package/src/{widgets → components}/ButtonSetting.vue +0 -0
- /package/src/{widgets → components}/CarouselSetting.vue +0 -0
- /package/src/{widgets → components}/ColumnSelector.vue +0 -0
- /package/src/{widgets → components}/ComponentSetting.vue +0 -0
- /package/src/{widgets → components}/ContactFormSetting.vue +0 -0
- /package/src/{widgets → components}/CountdownSetting.vue +0 -0
- /package/src/{widgets → components}/DataListSetting.vue +0 -0
- /package/src/{widgets → components}/EmbeddedVideo.vue +0 -0
- /package/src/{widgets → components}/EmbeddedVideoSetting.vue +0 -0
- /package/src/{widgets → components}/FAQ.vue +0 -0
- /package/src/{widgets → components}/FAQSetting.vue +0 -0
- /package/src/{widgets → components}/FeatureListSetting.vue +0 -0
- /package/src/{widgets → components}/FlexSetting.vue +0 -0
- /package/src/{widgets → components}/GridSetting.vue +0 -0
- /package/src/{widgets → components}/Header2.vue +0 -0
- /package/src/{widgets → components}/Header2Setting.vue +0 -0
- /package/src/{widgets → components}/IconListSetting.vue +0 -0
- /package/src/{widgets → components}/ImageSetting.vue +0 -0
- /package/src/{widgets → components}/LLMChat.vue +0 -0
- /package/src/{widgets → components}/LinkSetting.vue +0 -0
- /package/src/{widgets → components}/LinkSettingModal.vue +0 -0
- /package/src/{widgets → components}/ModalSetting.vue +0 -0
- /package/src/{widgets → components}/OGSettingModal.vue +0 -0
- /package/src/{widgets → components}/ParagraphSetting.vue +0 -0
- /package/src/{widgets → components}/PositionSetting.vue +0 -0
- /package/src/{widgets → components}/PresetBarPivot.vue +0 -0
- /package/src/{widgets → components}/PresetBarPivotColumnEdit.vue +0 -0
- /package/src/{widgets → components}/PresetBarPivotValueEdit.vue +0 -0
- /package/src/{widgets → components}/PresetSelector.vue +0 -0
- /package/src/{widgets → components}/Review.vue +0 -0
- /package/src/{widgets → components}/ReviewSetting.vue +0 -0
- /package/src/{widgets → components}/SearchModalSetting.vue +0 -0
- /package/src/{widgets → components}/ShareSetting.vue +0 -0
- /package/src/{widgets → components}/SvgSetting.vue +0 -0
- /package/src/{widgets → components}/TableSetting.vue +0 -0
- /package/src/{widgets → components}/TestimonialSetting.vue +0 -0
- /package/src/{widgets → components}/TextBlockSetting.vue +0 -0
- /package/src/{widgets → components}/ThumbnailSetting.vue +0 -0
- /package/src/{widgets → components}/WebComponentSelector.vue +0 -0
- /package/src/{widgets → components}/WebPageBuilder.vue +0 -0
- /package/src/{widgets → components}/YoutubeVideoSetting.vue +0 -0
|
@@ -0,0 +1,1239 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="flex flex-col bg-base-400 md:bg-transparent"
|
|
3
|
+
v-intersect="onVisibleChange">
|
|
4
|
+
|
|
5
|
+
<div v-if="readyState >= 1"
|
|
6
|
+
class="flex-1 flex flex-row">
|
|
7
|
+
|
|
8
|
+
<div v-if="computedPresetMode === 'sidebar' && sidebar.open"
|
|
9
|
+
class="relative flex flex-col border-r-[1px] border-border-50 bg-base-400"
|
|
10
|
+
:style="sidebarStyle">
|
|
11
|
+
|
|
12
|
+
<div class="flex-1 flex flex-col relative">
|
|
13
|
+
<PresetBar :config="cConfig"
|
|
14
|
+
class="flex-1"
|
|
15
|
+
:enumCache="enumCache"
|
|
16
|
+
:controller="controller"
|
|
17
|
+
:preset-key="presetKey"
|
|
18
|
+
@apply="load">
|
|
19
|
+
|
|
20
|
+
<template #toolbar>
|
|
21
|
+
<button type="button" class="p-1" @click="togglePreset">
|
|
22
|
+
<svg width="16" height="16" class="fill-text-300 hover:fill-red-500" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><!--! Font Awesome Pro 6.0.0-alpha3 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) --><path d="M310.6 361.4c12.5 12.5 12.5 32.75 0 45.25C304.4 412.9 296.2 416 288 416s-16.38-3.125-22.62-9.375L160 301.3L54.63 406.6C48.38 412.9 40.19 416 32 416S15.63 412.9 9.375 406.6c-12.5-12.5-12.5-32.75 0-45.25l105.4-105.4L9.375 150.6c-12.5-12.5-12.5-32.75 0-45.25s32.75-12.5 45.25 0L160 210.8l105.4-105.4c12.5-12.5 32.75-12.5 45.25 0s12.5 32.75 0 45.25l-105.4 105.4L310.6 361.4z"/></svg>
|
|
23
|
+
</button>
|
|
24
|
+
</template>
|
|
25
|
+
|
|
26
|
+
</PresetBar>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<div v-if="sidebar.open"
|
|
30
|
+
:class="$style.resize1"
|
|
31
|
+
@mousedown="(e) => $util.dragResize(e, resize1)"></div>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<div v-if="preset" class="flex-1 flex flex-col relative">
|
|
35
|
+
|
|
36
|
+
<div class="flex-1 flex flex-col" :class="containerClass">
|
|
37
|
+
<slot name="head"
|
|
38
|
+
:preset="preset"
|
|
39
|
+
:presetSelector="$refs.presetSelector"
|
|
40
|
+
:compPrefix="compPrefix">
|
|
41
|
+
<div class="flex flex-col md:flex-row gap-3 md:items-end overflow-hidden p-3 pb-1 md:p-0"
|
|
42
|
+
:class="headerClass">
|
|
43
|
+
<div class="flex md:flex-1 flex-row items-end md:gap-3 py-1 md:py-0" @click="log(cConfig)">
|
|
44
|
+
<slot name="left"></slot>
|
|
45
|
+
<div class="flex flex-row gap-2 md:gap-3 items-center px-3" ref="title">
|
|
46
|
+
<div class="flex flex-col whitespace-nowrap text-ellipsis overflow-hidden">
|
|
47
|
+
<div class="cursor-pointer group leading-4" @click="$refs.contextMenu.open($refs.title)">
|
|
48
|
+
<small class="text-text-400 text-xs">{{ title ?? 'Untitled' }}</small>
|
|
49
|
+
<div class="flex flex-row items-baseline gap-1">
|
|
50
|
+
<h3 class="whitespace-nowrap relative text-ellipsis overflow-hidden group-hover:text-primary">
|
|
51
|
+
{{ preset.name ?? 'Preset Name' }}
|
|
52
|
+
</h3>
|
|
53
|
+
<svg width="13" height="13" class="fill-text group-hover:fill-primary relative top-[-1px]" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><!--! Font Awesome Pro 6.0.0-alpha3 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) --><path d="M310.6 246.6l-127.1 128C176.4 380.9 168.2 384 160 384s-16.38-3.125-22.63-9.375l-127.1-128C.2244 237.5-2.516 223.7 2.438 211.8S19.07 192 32 192h255.1c12.94 0 24.62 7.781 29.58 19.75S319.8 237.5 310.6 246.6z"/></svg>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
<ContextMenu ref="contextMenu" class="bg-base-300 mt-4">
|
|
57
|
+
<div class="flex flex-col min-w-[280px] max-w-[400px] p-1">
|
|
58
|
+
|
|
59
|
+
<div class="flex flex-col gap-1 bg-base-300">
|
|
60
|
+
<button v-for="(preset, idx) in configPresets"
|
|
61
|
+
class="p-2 px-5 text-left flex flex-row items-center rounded-md"
|
|
62
|
+
:class="configParams.presetIdx === preset.uid ? 'bg-primary-200 text-white' : 'hover:bg-primary-100'"
|
|
63
|
+
type="button"
|
|
64
|
+
@click="selectPreset(idx)">
|
|
65
|
+
<label class="flex-1 pr-12 text-ellipsis-nowrap">
|
|
66
|
+
{{ preset.name }}
|
|
67
|
+
</label>
|
|
68
|
+
<div class="p-1">
|
|
69
|
+
<div v-if="idx < 10"
|
|
70
|
+
class="border-[1px] border-text-300 px-2 text-mono text-xs text-text-400 rounded-lg">
|
|
71
|
+
Alt {{ idx + 1 }}
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
</button>
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
<div class="my-2 h-[1px] bg-border-50"></div>
|
|
78
|
+
|
|
79
|
+
<div class="px-3">
|
|
80
|
+
<button type="button" class="text-primary p-3 text-left w-full" @click="openPreset">Edit</button>
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
</div>
|
|
84
|
+
</ContextMenu>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
<slot name="left-toolbar"></slot>
|
|
89
|
+
|
|
90
|
+
<div class="flex-1"></div>
|
|
91
|
+
|
|
92
|
+
<slot name="toolbar">
|
|
93
|
+
</slot>
|
|
94
|
+
|
|
95
|
+
<slot name="right-toolbar">
|
|
96
|
+
<div class="hidden md:flex flex-row gap-2" v-if="Boolean(toolbar)">
|
|
97
|
+
<Textbox v-if="canSearch"
|
|
98
|
+
class="flex-1 md:w-[240px]"
|
|
99
|
+
placeholder="Search..."
|
|
100
|
+
clearable="1"
|
|
101
|
+
maxlength="100"
|
|
102
|
+
v-model="preset.search"
|
|
103
|
+
@clear="delete preset.search; load()"
|
|
104
|
+
@keyup.enter="load">
|
|
105
|
+
<template #start>
|
|
106
|
+
<div class="p-2 pr-0">
|
|
107
|
+
<svg width="14" height="14" class="fill-text-300" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Pro 6.0.0-alpha3 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) --><path d="M504.1 471l-134-134C399.1 301.5 415.1 256.8 415.1 208c0-114.9-93.13-208-208-208S-.0002 93.13-.0002 208S93.12 416 207.1 416c48.79 0 93.55-16.91 129-45.04l134 134C475.7 509.7 481.9 512 488 512s12.28-2.344 16.97-7.031C514.3 495.6 514.3 480.4 504.1 471zM48 208c0-88.22 71.78-160 160-160s160 71.78 160 160s-71.78 160-160 160S48 296.2 48 208z"/></svg>
|
|
108
|
+
</div>
|
|
109
|
+
</template>
|
|
110
|
+
</Textbox>
|
|
111
|
+
</div>
|
|
112
|
+
</slot>
|
|
113
|
+
</div>
|
|
114
|
+
|
|
115
|
+
</div>
|
|
116
|
+
</slot>
|
|
117
|
+
|
|
118
|
+
<div class="md:hidden">
|
|
119
|
+
<slot name="mobile-toolbar">
|
|
120
|
+
<div class="p-3">
|
|
121
|
+
<Textbox v-if="canSearch"
|
|
122
|
+
placeholder="Search..."
|
|
123
|
+
class="bg-base-300"
|
|
124
|
+
clearable="1"
|
|
125
|
+
maxlength="100"
|
|
126
|
+
v-model="preset.search"
|
|
127
|
+
@clear="delete preset.search; load()"
|
|
128
|
+
@keyup.enter="load">
|
|
129
|
+
<template #start>
|
|
130
|
+
<div class="p-2 pr-0">
|
|
131
|
+
<svg width="14" height="14" class="fill-text-300" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Pro 6.0.0-alpha3 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) --><path d="M504.1 471l-134-134C399.1 301.5 415.1 256.8 415.1 208c0-114.9-93.13-208-208-208S-.0002 93.13-.0002 208S93.12 416 207.1 416c48.79 0 93.55-16.91 129-45.04l134 134C475.7 509.7 481.9 512 488 512s12.28-2.344 16.97-7.031C514.3 495.6 514.3 480.4 504.1 471zM48 208c0-88.22 71.78-160 160-160s160 71.78 160 160s-71.78 160-160 160S48 296.2 48 208z"/></svg>
|
|
132
|
+
</div>
|
|
133
|
+
</template>
|
|
134
|
+
</Textbox>
|
|
135
|
+
</div>
|
|
136
|
+
</slot>
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
<!-- Formula bar — spreadsheet-style editor for client-side virtual
|
|
140
|
+
columns. Formulas reference pivot result columns as [key] and
|
|
141
|
+
are recomputed locally, no server round-trip. -->
|
|
142
|
+
<div v-if="pivotReady && Boolean(toolbar)" class="flex flex-col gap-1 px-3 md:px-0">
|
|
143
|
+
<div class="flex flex-row items-center gap-2 bg-base-300 border-[1px] border-border-50 rounded-lg px-3 py-1">
|
|
144
|
+
<em class="font-serif italic font-bold text-text-300 select-none" title="Virtual columns — calculated client-side">fx</em>
|
|
145
|
+
|
|
146
|
+
<select class="appearance-none bg-transparent outline-none max-w-[160px] text-ellipsis border-r-[1px] border-border-50 pr-2"
|
|
147
|
+
:value="formulaBar.key"
|
|
148
|
+
@change="e => selectFormula(e.target.value)">
|
|
149
|
+
<option value="">+ New column</option>
|
|
150
|
+
<option v-for="vc in virtualColumns" :key="vc.key" :value="vc.key">{{ vc.label || vc.key }}</option>
|
|
151
|
+
</select>
|
|
152
|
+
|
|
153
|
+
<input class="w-[140px] bg-transparent outline-none border-r-[1px] border-border-50 pr-2"
|
|
154
|
+
placeholder="Column name"
|
|
155
|
+
v-model="formulaBar.name"
|
|
156
|
+
@keyup.enter="applyFormula" />
|
|
157
|
+
|
|
158
|
+
<input class="flex-1 bg-transparent outline-none font-mono text-sm"
|
|
159
|
+
placeholder='=([_amount-sum] - [_cost-sum]) / [_amount-sum]'
|
|
160
|
+
v-model="formulaBar.formula"
|
|
161
|
+
@focus="formulaBar.focused = true"
|
|
162
|
+
@blur="formulaBar.focused = false"
|
|
163
|
+
@keyup.enter="applyFormula" />
|
|
164
|
+
|
|
165
|
+
<button type="button" class="text-primary px-1" title="Apply formula (Enter)" @click="applyFormula">
|
|
166
|
+
<svg width="14" height="14" class="fill-current" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M173.898 439.404l-166.4-166.4c-9.997-9.997-9.997-26.206 0-36.204l36.203-36.204c9.997-9.998 26.207-9.998 36.204 0L192 312.69 432.095 72.596c9.997-9.997 26.207-9.997 36.204 0l36.203 36.204c9.997 9.997 9.997 26.206 0 36.204l-294.4 294.401c-9.998 9.997-26.207 9.997-36.204-.001z"/></svg>
|
|
167
|
+
</button>
|
|
168
|
+
<button v-if="formulaBar.key" type="button" class="px-1" title="Remove this virtual column" @click="removeFormula">
|
|
169
|
+
<svg width="14" height="14" class="fill-text-300 hover:fill-red-500" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><path d="M207.6 256l107.72-107.72c6.23-6.23 6.23-16.34 0-22.58l-25.03-25.03c-6.23-6.23-16.34-6.23-22.58 0L160 208.4 52.28 100.68c-6.23-6.23-16.34-6.23-22.58 0L4.68 125.7c-6.23 6.23-6.23 16.34 0 22.58L112.4 256 4.68 363.72c-6.23 6.23-6.23 16.34 0 22.58l25.03 25.03c6.23 6.23 16.34 6.23 22.58 0L160 303.6l107.72 107.72c6.23 6.23 16.34 6.23 22.58 0l25.03-25.03c6.23-6.23 6.23-16.34 0-22.58L207.6 256z"/></svg>
|
|
170
|
+
</button>
|
|
171
|
+
</div>
|
|
172
|
+
|
|
173
|
+
<div v-if="formulaBar.error" class="text-red-500 text-sm px-2">
|
|
174
|
+
{{ formulaBar.error }}
|
|
175
|
+
</div>
|
|
176
|
+
|
|
177
|
+
<div v-if="formulaBar.focused && formulaRefs.length > 0"
|
|
178
|
+
class="flex flex-row flex-wrap gap-1 px-1">
|
|
179
|
+
<button v-for="key in formulaRefs" :key="key"
|
|
180
|
+
type="button"
|
|
181
|
+
class="px-2 rounded-md bg-base-300 border-[1px] border-border-50 text-xs text-text-400 hover:text-primary hover:border-primary font-mono"
|
|
182
|
+
@mousedown.prevent="insertRef(key)">
|
|
183
|
+
[{{ key }}]
|
|
184
|
+
</button>
|
|
185
|
+
</div>
|
|
186
|
+
</div>
|
|
187
|
+
|
|
188
|
+
<div class="flex-1 flex" ref="content">
|
|
189
|
+
<div v-if="readyState === 2" class="flex-1 flex items-center justify-center">
|
|
190
|
+
<svg class="animate-spin" width="36" height="36" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>
|
|
191
|
+
</div>
|
|
192
|
+
|
|
193
|
+
<div v-else-if="!pivotReady" class="flex-1 flex flex-col items-center justify-center gap-3 text-text-300">
|
|
194
|
+
<p>Add at least one group or aggregate to build the pivot.</p>
|
|
195
|
+
<button type="button" class="text-primary" @click="openPreset">Open preset editor</button>
|
|
196
|
+
</div>
|
|
197
|
+
|
|
198
|
+
<div v-else class="flex-1 flex flex-col">
|
|
199
|
+
<VirtualTable
|
|
200
|
+
ref="table"
|
|
201
|
+
:columns="columns"
|
|
202
|
+
class="flex-1 bg-base-300"
|
|
203
|
+
:items="dataItems"
|
|
204
|
+
:enumCache="enumCache"
|
|
205
|
+
:freeze-left="freezeLeft"
|
|
206
|
+
:item-class="itemClass"
|
|
207
|
+
@freeze="freeze"
|
|
208
|
+
@unfreeze="unfreeze"
|
|
209
|
+
@item-click="onTableItemClick"
|
|
210
|
+
@dragover="(...args) => $emit('dragover', ...args)">
|
|
211
|
+
|
|
212
|
+
<template v-for="(_, slot) in headerSlots" #[slot]="{ item, index }">
|
|
213
|
+
<div :class="getHeader(slot.replace('col-', ''))">
|
|
214
|
+
<slot :name="slot" :item="item" :index="index"></slot>
|
|
215
|
+
</div>
|
|
216
|
+
</template>
|
|
217
|
+
|
|
218
|
+
<template v-for="(_, slot) in contentSlots" #[slot]="{ column, item, index, selected }">
|
|
219
|
+
<slot :name="slot" :item="item" :index="index" :selected="selected" :column="column"></slot>
|
|
220
|
+
</template>
|
|
221
|
+
|
|
222
|
+
</VirtualTable>
|
|
223
|
+
</div>
|
|
224
|
+
|
|
225
|
+
<PresetSelector ref="presetSelector" :config="cConfig" @select="load" />
|
|
226
|
+
</div>
|
|
227
|
+
</div>
|
|
228
|
+
|
|
229
|
+
</div>
|
|
230
|
+
|
|
231
|
+
</div>
|
|
232
|
+
|
|
233
|
+
</div>
|
|
234
|
+
</template>
|
|
235
|
+
|
|
236
|
+
<script>
|
|
237
|
+
|
|
238
|
+
import VirtualTable from "./VirtualTable.vue";
|
|
239
|
+
import PresetSelector from "./PresetSelector.vue";
|
|
240
|
+
import PresetBar from "./PresetBar.vue";
|
|
241
|
+
import {generatePivotColumns, generateTotalColumns, parsePivotKey, setupConfig} from "../utils/preset-selector.mjs";
|
|
242
|
+
import {invokeAfterIdle} from "../utils/helpers.mjs";
|
|
243
|
+
import {compileFormula} from "../utils/pivot-formula.mjs";
|
|
244
|
+
|
|
245
|
+
export default{
|
|
246
|
+
|
|
247
|
+
emits: [ 'dragover', 'after-load', 'open-preset', 'signal', 'pivot-item-click', 'item-click' ],
|
|
248
|
+
|
|
249
|
+
inject: [ 'useSocket', 'toast' ],
|
|
250
|
+
|
|
251
|
+
props: {
|
|
252
|
+
config: Object,
|
|
253
|
+
defaultConfig: Function,
|
|
254
|
+
presetKey: String,
|
|
255
|
+
subscribeKey: String,
|
|
256
|
+
controller: String,
|
|
257
|
+
title: String,
|
|
258
|
+
updateInterval: {
|
|
259
|
+
type: Number,
|
|
260
|
+
default: 1200
|
|
261
|
+
},
|
|
262
|
+
|
|
263
|
+
src: undefined,
|
|
264
|
+
headerClass: String,
|
|
265
|
+
presetMode: {
|
|
266
|
+
type: String,
|
|
267
|
+
default: "sidebar"
|
|
268
|
+
},
|
|
269
|
+
containerClass: {
|
|
270
|
+
type: String,
|
|
271
|
+
default: "md:p-8 md:gap-5"
|
|
272
|
+
},
|
|
273
|
+
itemClass: String,
|
|
274
|
+
toolbar: {
|
|
275
|
+
type: [ String, Boolean ],
|
|
276
|
+
default: true
|
|
277
|
+
},
|
|
278
|
+
itemsFn: Function,
|
|
279
|
+
searchable: {
|
|
280
|
+
type: [ Boolean, String ],
|
|
281
|
+
default: true
|
|
282
|
+
}
|
|
283
|
+
},
|
|
284
|
+
|
|
285
|
+
methods: {
|
|
286
|
+
|
|
287
|
+
// Merge server (model-derived) column types into every column def that
|
|
288
|
+
// doesn't declare one — includes dotted relationship keys resolved by the
|
|
289
|
+
// pivot-list mixin, so date/number/boolean pivot behaviors kick in without
|
|
290
|
+
// declaring types by hand.
|
|
291
|
+
applyColumnTypes(types){
|
|
292
|
+
if(!types || typeof types !== 'object') return
|
|
293
|
+
const fill = (columns) => (columns ?? []).forEach(column => {
|
|
294
|
+
if(column.key && !column.type && types[column.key] && types[column.key] !== 'string'){
|
|
295
|
+
column.type = types[column.key]
|
|
296
|
+
}
|
|
297
|
+
})
|
|
298
|
+
fill(this.cConfig.columns)
|
|
299
|
+
;(this.cConfig.presets ?? []).forEach(preset => fill(preset.columns))
|
|
300
|
+
;(this.cConfig.sharedPresets ?? []).forEach(preset => fill(preset.columns))
|
|
301
|
+
},
|
|
302
|
+
|
|
303
|
+
getEnumText(key, value){
|
|
304
|
+
|
|
305
|
+
let enumText
|
|
306
|
+
|
|
307
|
+
const column = this.cConfig.columns.find(_ => _.key === key)
|
|
308
|
+
|
|
309
|
+
if(Array.isArray(column?.typeParams)){
|
|
310
|
+
const typeParam = (column.typeParams ?? []).filter((_) => _.value === value).pop()
|
|
311
|
+
enumText = typeParam && typeParam.text ? typeParam.text : enumText
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if(this.enumCache && this.enumCache[key] && this.enumCache[key][value]){
|
|
315
|
+
enumText = this.enumCache[key][value].text ?? enumText
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return enumText ? enumText : value
|
|
319
|
+
},
|
|
320
|
+
|
|
321
|
+
getHeader(column){
|
|
322
|
+
|
|
323
|
+
return [
|
|
324
|
+
this.$style.header,
|
|
325
|
+
(this.preset.filters ?? []).findIndex((_) => _.key === column.key && _.enabled) >= 0 ?
|
|
326
|
+
this.$style.headerSelected : ''
|
|
327
|
+
]
|
|
328
|
+
.join(' ')
|
|
329
|
+
},
|
|
330
|
+
|
|
331
|
+
load(){
|
|
332
|
+
if(!this.computedSrc || !this.preset) return Promise.resolve()
|
|
333
|
+
|
|
334
|
+
if(!this.preset.columns){
|
|
335
|
+
this.preset.columns = JSON.parse(JSON.stringify(this.cConfig.columns))
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if(!this.pivotReady){
|
|
339
|
+
this.data.items = []
|
|
340
|
+
return Promise.resolve()
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
this.readyState = 2
|
|
344
|
+
return this.useSocket().send(this.computedSrc, {
|
|
345
|
+
...this.preset,
|
|
346
|
+
id: undefined,
|
|
347
|
+
uid: undefined,
|
|
348
|
+
})
|
|
349
|
+
.then(data => {
|
|
350
|
+
if(data?.items){
|
|
351
|
+
this.applyColumnTypes(data.columnTypes)
|
|
352
|
+
generatePivotColumns(this.preset, data.items)
|
|
353
|
+
generateTotalColumns(this.preset, data.items)
|
|
354
|
+
Object.assign(this.data, data ?? {})
|
|
355
|
+
this.applyVirtualColumns()
|
|
356
|
+
this.loadEnums(this.data.items)
|
|
357
|
+
this.$emit('after-load')
|
|
358
|
+
}
|
|
359
|
+
})
|
|
360
|
+
.catch(err => this.toast(err))
|
|
361
|
+
.finally(_ => this.readyState = 1)
|
|
362
|
+
},
|
|
363
|
+
|
|
364
|
+
async loadPreset(){
|
|
365
|
+
if(!Object.keys(this.$route.query).map(_ => _.toLowerCase()).includes('reset')){
|
|
366
|
+
if(this.presetKey){
|
|
367
|
+
const config = await this.useSocket().send(this.presetSrc, { key:this.presetKey })
|
|
368
|
+
if(config && (config.presets ?? []).length > 0){
|
|
369
|
+
Object.assign(this.cConfig, config)
|
|
370
|
+
|
|
371
|
+
if(this.$route.query?.search)
|
|
372
|
+
this.preset.search = this.$route.query.search
|
|
373
|
+
}
|
|
374
|
+
else{
|
|
375
|
+
const defaultConfig = setupConfig(typeof this.defaultConfig === 'function' ? await this.defaultConfig() : {})
|
|
376
|
+
Object.assign(this.cConfig, defaultConfig)
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
else{
|
|
380
|
+
const defaultConfig = setupConfig(typeof this.defaultConfig === 'function' ? await this.defaultConfig() : {})
|
|
381
|
+
Object.assign(this.cConfig, defaultConfig)
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
else{
|
|
385
|
+
if(this.defaultConfig){
|
|
386
|
+
const defaultConfig = setupConfig(await this.defaultConfig())
|
|
387
|
+
Object.assign(this.cConfig, defaultConfig)
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const query = {}
|
|
391
|
+
for(let key in this.$route.query){
|
|
392
|
+
if(key.toLowerCase() !== 'reset')
|
|
393
|
+
query[key] = this.$route.query[key]
|
|
394
|
+
}
|
|
395
|
+
this.$router.replace({
|
|
396
|
+
...this.$route,
|
|
397
|
+
query
|
|
398
|
+
})
|
|
399
|
+
}
|
|
400
|
+
},
|
|
401
|
+
|
|
402
|
+
async loadEnums(items){
|
|
403
|
+
if(!items) return
|
|
404
|
+
|
|
405
|
+
return Promise.all((this.preset.columns ?? []).map(async column => {
|
|
406
|
+
|
|
407
|
+
if(column.enumSrc) {
|
|
408
|
+
|
|
409
|
+
if(!this.enumCache[column.key])
|
|
410
|
+
this.enumCache[column.key] = {}
|
|
411
|
+
|
|
412
|
+
for(let item of items){
|
|
413
|
+
if(item[column.key] && !this.enumCache[column.key][item[column.key]]){
|
|
414
|
+
this.enumCache[column.key][item[column.key]] = { value:item[column.key] }
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const reqItems = Object.values(this.enumCache[column.key]).filter(_ => !_.text).map(_ => _.value)
|
|
419
|
+
|
|
420
|
+
if(reqItems.length > 0 && reqItems !== this.lastEnumItems){
|
|
421
|
+
|
|
422
|
+
const [ src, key, attr ] = column.enumSrc.split(':')
|
|
423
|
+
const res = await this.useSocket().send(src, {
|
|
424
|
+
columns: [
|
|
425
|
+
{ key:attr, visible:true }
|
|
426
|
+
],
|
|
427
|
+
filters: [
|
|
428
|
+
{
|
|
429
|
+
key:key, value: [ { operator:'in', value:reqItems } ]
|
|
430
|
+
}
|
|
431
|
+
]
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
(res.items ?? []).forEach(_ => {
|
|
435
|
+
this.enumCache[column.key][_[key]] = {
|
|
436
|
+
text: _[attr] ?? _[key],
|
|
437
|
+
value: _[key],
|
|
438
|
+
}
|
|
439
|
+
})
|
|
440
|
+
|
|
441
|
+
for(let key in this.enumCache[column.key]){
|
|
442
|
+
if(!this.enumCache[column.key][key].text)
|
|
443
|
+
this.enumCache[column.key][key].text = key
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
this.lastEnumItems = reqItems
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
}))
|
|
451
|
+
},
|
|
452
|
+
|
|
453
|
+
// ------------------------------------------------ virtual columns
|
|
454
|
+
|
|
455
|
+
// Recompute every virtual column over the loaded pivot rows (total row
|
|
456
|
+
// included — the formula runs on the totals like a spreadsheet would) and
|
|
457
|
+
// sync their column defs into pivot.columns.
|
|
458
|
+
applyVirtualColumns(){
|
|
459
|
+
const pivot = (this.preset ?? {}).pivot
|
|
460
|
+
if(!pivot) return
|
|
461
|
+
|
|
462
|
+
const virtuals = this.virtualColumns
|
|
463
|
+
const items = this.data.items ?? []
|
|
464
|
+
|
|
465
|
+
// drop defs whose virtual column no longer exists
|
|
466
|
+
if(Array.isArray(pivot.columns)){
|
|
467
|
+
for(let i = pivot.columns.length - 1 ; i >= 0 ; i--){
|
|
468
|
+
if(pivot.columns[i].virtual && !virtuals.find(_ => _.key === pivot.columns[i].key)){
|
|
469
|
+
pivot.columns.splice(i, 1)
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
for(const vc of virtuals){
|
|
475
|
+
let compiled
|
|
476
|
+
try{
|
|
477
|
+
compiled = compileFormula(vc.formula)
|
|
478
|
+
}
|
|
479
|
+
catch(err){
|
|
480
|
+
compiled = { evaluate: () => err.code ?? '#ERROR!' }
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
let isString = false
|
|
484
|
+
for(const item of items){
|
|
485
|
+
const value = compiled.evaluate(item)
|
|
486
|
+
item[vc.key] = value
|
|
487
|
+
if(typeof value === 'string' && !value.startsWith('#')) isString = true
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const type = vc.type ?? (isString ? 'string' : 'number')
|
|
491
|
+
const def = (pivot.columns ?? []).find(_ => _.key === vc.key)
|
|
492
|
+
if(def){
|
|
493
|
+
Object.assign(def, { label: vc.label || vc.key, type, virtual: true })
|
|
494
|
+
}
|
|
495
|
+
else{
|
|
496
|
+
(pivot.columns = pivot.columns ?? []).push({
|
|
497
|
+
key: vc.key,
|
|
498
|
+
label: vc.label || 'Formula',
|
|
499
|
+
tooltip: vc.formula,
|
|
500
|
+
type,
|
|
501
|
+
virtual: true
|
|
502
|
+
})
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
},
|
|
506
|
+
|
|
507
|
+
selectFormula(key){
|
|
508
|
+
const vc = this.virtualColumns.find(_ => _.key === key)
|
|
509
|
+
Object.assign(this.formulaBar, vc ?
|
|
510
|
+
{ key, name: vc.label, formula: vc.formula, error: '' } :
|
|
511
|
+
{ key: '', name: '', formula: '', error: '' })
|
|
512
|
+
},
|
|
513
|
+
|
|
514
|
+
applyFormula(){
|
|
515
|
+
const { key, name, formula } = this.formulaBar
|
|
516
|
+
if(!`${formula ?? ''}`.replace(/^\s*=/, '').trim()) return
|
|
517
|
+
|
|
518
|
+
try{
|
|
519
|
+
compileFormula(formula)
|
|
520
|
+
}
|
|
521
|
+
catch(err){
|
|
522
|
+
this.formulaBar.error = err.message
|
|
523
|
+
return
|
|
524
|
+
}
|
|
525
|
+
this.formulaBar.error = ''
|
|
526
|
+
|
|
527
|
+
let vc = this.virtualColumns.find(_ => _.key === key)
|
|
528
|
+
if(!vc){
|
|
529
|
+
vc = { key: `_vc_${Math.random().toString(36).slice(2, 8)}` }
|
|
530
|
+
this.virtualColumns.push(vc)
|
|
531
|
+
this.formulaBar.key = vc.key
|
|
532
|
+
}
|
|
533
|
+
vc.label = name || vc.label || 'Formula'
|
|
534
|
+
vc.formula = formula
|
|
535
|
+
|
|
536
|
+
this.applyVirtualColumns()
|
|
537
|
+
},
|
|
538
|
+
|
|
539
|
+
removeFormula(){
|
|
540
|
+
const idx = this.virtualColumns.findIndex(_ => _.key === this.formulaBar.key)
|
|
541
|
+
if(idx >= 0) this.virtualColumns.splice(idx, 1)
|
|
542
|
+
this.selectFormula('')
|
|
543
|
+
this.applyVirtualColumns()
|
|
544
|
+
},
|
|
545
|
+
|
|
546
|
+
insertRef(key){
|
|
547
|
+
this.formulaBar.formula = `${this.formulaBar.formula ?? ''}[${key}]`
|
|
548
|
+
},
|
|
549
|
+
|
|
550
|
+
// ------------------------------------------------ presets
|
|
551
|
+
|
|
552
|
+
resize1(w){
|
|
553
|
+
if(this.sidebar.width + w >= 200 && this.sidebar.width + w <= 600){
|
|
554
|
+
this.sidebar.width += w
|
|
555
|
+
}
|
|
556
|
+
},
|
|
557
|
+
|
|
558
|
+
selectPreset(idx){
|
|
559
|
+
if(this.configPresets[idx]){
|
|
560
|
+
this.configParams.presetIdx = this.configPresets[idx].uid
|
|
561
|
+
this.selectFormula('')
|
|
562
|
+
this.load()
|
|
563
|
+
this.$refs.presetSelector?.close()
|
|
564
|
+
this.$refs.contextMenu?.close()
|
|
565
|
+
}
|
|
566
|
+
},
|
|
567
|
+
|
|
568
|
+
savePreset: invokeAfterIdle(function() {
|
|
569
|
+
if(this.presetKey) {
|
|
570
|
+
this.useSocket().send(this.presetSrc,
|
|
571
|
+
{key: this.presetKey, config: this.cConfig})
|
|
572
|
+
}
|
|
573
|
+
}),
|
|
574
|
+
|
|
575
|
+
openPreset(){
|
|
576
|
+
|
|
577
|
+
switch(this.computedPresetMode){
|
|
578
|
+
|
|
579
|
+
case 'external':
|
|
580
|
+
this.$emit('open-preset')
|
|
581
|
+
break
|
|
582
|
+
|
|
583
|
+
case 'sidebar':
|
|
584
|
+
this.sidebar.open = true
|
|
585
|
+
break
|
|
586
|
+
|
|
587
|
+
default:
|
|
588
|
+
this.$refs.presetSelector.open()
|
|
589
|
+
break
|
|
590
|
+
|
|
591
|
+
}
|
|
592
|
+
},
|
|
593
|
+
|
|
594
|
+
togglePreset(){
|
|
595
|
+
switch(this.computedPresetMode){
|
|
596
|
+
|
|
597
|
+
case 'sidebar':
|
|
598
|
+
this.sidebar.open = !this.sidebar.open
|
|
599
|
+
break
|
|
600
|
+
|
|
601
|
+
case 'popup':
|
|
602
|
+
this.$refs.presetSelector.open()
|
|
603
|
+
break
|
|
604
|
+
}
|
|
605
|
+
},
|
|
606
|
+
|
|
607
|
+
// Pivot rows are aggregates — realtime signals can't be patched in, so
|
|
608
|
+
// re-run the pivot query (debounced: bursts collapse into one reload).
|
|
609
|
+
onSignal(event, items){
|
|
610
|
+
if(!this._pivotReload){
|
|
611
|
+
this._pivotReload = invokeAfterIdle(() => this.load(), { delay: this.updateInterval, flushOnExit: false })
|
|
612
|
+
}
|
|
613
|
+
this._pivotReload()
|
|
614
|
+
this.$emit('signal', event, items)
|
|
615
|
+
},
|
|
616
|
+
|
|
617
|
+
// Clicking an aggregate cell emits `pivot-item-click` with the equivalent
|
|
618
|
+
// row filters — data-type aware (booleans compare as booleans, date
|
|
619
|
+
// buckets become format_* fn filters). The host decides what to do with
|
|
620
|
+
// them (navigate to a List, open a modal, …) — PivotList itself renders
|
|
621
|
+
// only the single pivot table.
|
|
622
|
+
onTableItemClick(item, column){
|
|
623
|
+
if(item._type === 'totalRow') return
|
|
624
|
+
|
|
625
|
+
if(!(column.key ?? '').startsWith('_') || column.virtual){
|
|
626
|
+
this.$emit('item-click', item, column)
|
|
627
|
+
return
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
const filters = []
|
|
631
|
+
|
|
632
|
+
for(let idx in this.preset.pivot.rows){
|
|
633
|
+
const pivotRow = this.preset.pivot.rows[idx]
|
|
634
|
+
const key = pivotRow.key
|
|
635
|
+
const presetColumn = this.preset.columns.find(_ => _.key === key) ?? pivotRow
|
|
636
|
+
let value = item[pivotRow.key]
|
|
637
|
+
switch(presetColumn.type){
|
|
638
|
+
|
|
639
|
+
case 'bool':
|
|
640
|
+
case 'boolean':
|
|
641
|
+
filters.push({
|
|
642
|
+
key,
|
|
643
|
+
value: [{
|
|
644
|
+
operator: '=',
|
|
645
|
+
value: value === 'true' || value === true || value === 1
|
|
646
|
+
}]
|
|
647
|
+
})
|
|
648
|
+
break
|
|
649
|
+
|
|
650
|
+
case 'date':
|
|
651
|
+
switch(pivotRow.aggregate ?? pivotRow.aggregrate){
|
|
652
|
+
|
|
653
|
+
case 'hour':
|
|
654
|
+
value = value.split('-')
|
|
655
|
+
filters.push({
|
|
656
|
+
key,
|
|
657
|
+
value: [{
|
|
658
|
+
operator: '=',
|
|
659
|
+
value: value[0].split(':')[0],
|
|
660
|
+
fn: 'format_hour'
|
|
661
|
+
}]
|
|
662
|
+
})
|
|
663
|
+
break
|
|
664
|
+
|
|
665
|
+
case 'date':
|
|
666
|
+
case 'week':
|
|
667
|
+
case 'month':
|
|
668
|
+
case 'quarter':
|
|
669
|
+
case 'year': {
|
|
670
|
+
const bucket = pivotRow.aggregate ?? pivotRow.aggregrate
|
|
671
|
+
filters.push({
|
|
672
|
+
key,
|
|
673
|
+
value: [{
|
|
674
|
+
operator: '=',
|
|
675
|
+
value: value,
|
|
676
|
+
fn: `format_${bucket}`
|
|
677
|
+
}]
|
|
678
|
+
})
|
|
679
|
+
break
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
}
|
|
683
|
+
break
|
|
684
|
+
|
|
685
|
+
default:
|
|
686
|
+
filters.push({
|
|
687
|
+
key,
|
|
688
|
+
value: [{
|
|
689
|
+
operator: '=',
|
|
690
|
+
value
|
|
691
|
+
}]
|
|
692
|
+
})
|
|
693
|
+
break
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// The clicked value column adds the drill-down filter:
|
|
699
|
+
// spread columns -> spread field = the clicked group value
|
|
700
|
+
// plain sum -> measure > 0
|
|
701
|
+
const parsed = column.key === '_rowTotal' ? null : parsePivotKey(column.key, this.preset.pivot)
|
|
702
|
+
|
|
703
|
+
if(parsed && parsed.spread != null){
|
|
704
|
+
const key = parsed.spreadKey
|
|
705
|
+
const presetColumn = this.preset.columns.find(_ => _.key === key)
|
|
706
|
+
?? (this.cConfig.columns ?? []).find(_ => _.key === key)
|
|
707
|
+
let value = parsed.spread
|
|
708
|
+
switch(presetColumn?.type){
|
|
709
|
+
|
|
710
|
+
case 'bool':
|
|
711
|
+
case 'boolean':
|
|
712
|
+
value = value === 'true'
|
|
713
|
+
break
|
|
714
|
+
|
|
715
|
+
default:
|
|
716
|
+
value = value === '(empty)' ? '' :
|
|
717
|
+
value === '(null)' ? null :
|
|
718
|
+
value.replaceAll('_', ' ')
|
|
719
|
+
break
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
filters.push({
|
|
723
|
+
key,
|
|
724
|
+
value: [{
|
|
725
|
+
operator: '=',
|
|
726
|
+
value
|
|
727
|
+
}]
|
|
728
|
+
})
|
|
729
|
+
}
|
|
730
|
+
else if(parsed && parsed.aggregate === 'sum'){
|
|
731
|
+
filters.push({
|
|
732
|
+
key: parsed.field,
|
|
733
|
+
value: [{
|
|
734
|
+
operator: '>',
|
|
735
|
+
value: 0
|
|
736
|
+
}]
|
|
737
|
+
})
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
this.$emit('pivot-item-click', { item, column, filters })
|
|
741
|
+
},
|
|
742
|
+
|
|
743
|
+
resize(){
|
|
744
|
+
this.$refs.table ? this.$refs.table.resize() : null
|
|
745
|
+
},
|
|
746
|
+
|
|
747
|
+
onConnect(reconnect){
|
|
748
|
+
if(reconnect){
|
|
749
|
+
this.load()
|
|
750
|
+
}
|
|
751
|
+
},
|
|
752
|
+
|
|
753
|
+
onKeyDown(e){
|
|
754
|
+
|
|
755
|
+
if(e.altKey){
|
|
756
|
+
|
|
757
|
+
switch(e.code){
|
|
758
|
+
|
|
759
|
+
case 'Backquote':
|
|
760
|
+
this.togglePreset()
|
|
761
|
+
break
|
|
762
|
+
|
|
763
|
+
default:
|
|
764
|
+
if(e.code.startsWith('Digit')){
|
|
765
|
+
const idx = parseInt(e.code.substring(5)) - 1
|
|
766
|
+
if(this.configPresets[idx]) {
|
|
767
|
+
this.selectPreset(idx)
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
break
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
}
|
|
774
|
+
},
|
|
775
|
+
|
|
776
|
+
freeze(key){
|
|
777
|
+
const freezed = this.columns.find(_ => _.freeze)
|
|
778
|
+
if(freezed){
|
|
779
|
+
delete freezed.freeze
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
const column = this.columns.find(_ => _.key === key)
|
|
783
|
+
if(column){
|
|
784
|
+
column.freeze = true
|
|
785
|
+
}
|
|
786
|
+
},
|
|
787
|
+
|
|
788
|
+
unfreeze(key){
|
|
789
|
+
|
|
790
|
+
const column = this.columns.find(_ => _.key === key)
|
|
791
|
+
if(column){
|
|
792
|
+
delete column.freeze
|
|
793
|
+
}
|
|
794
|
+
},
|
|
795
|
+
|
|
796
|
+
onVisibleChange({ inViewport }){
|
|
797
|
+
if(inViewport){
|
|
798
|
+
this.$addResizeListener(this.$el, () => this.compPrefix = this.$util.calculateMediaPrefix(this.$el.clientWidth))
|
|
799
|
+
|
|
800
|
+
this.loadPreset()
|
|
801
|
+
.finally(() => {
|
|
802
|
+
this.readyState = 1
|
|
803
|
+
this.$nextTick(() => {
|
|
804
|
+
this.load()
|
|
805
|
+
})
|
|
806
|
+
})
|
|
807
|
+
|
|
808
|
+
if(this.subscribeKey){
|
|
809
|
+
this.useSocket().send('user.subscribe', { name:this.subscribeKey })
|
|
810
|
+
this.useSocket().on(this.subscribeKey, this.onSignal)
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
window.addEventListener('keydown', this.onKeyDown)
|
|
814
|
+
|
|
815
|
+
this.useSocket().on('connect', this.onConnect)
|
|
816
|
+
}
|
|
817
|
+
},
|
|
818
|
+
|
|
819
|
+
},
|
|
820
|
+
|
|
821
|
+
components: {PresetBar, PresetSelector, VirtualTable},
|
|
822
|
+
|
|
823
|
+
unmounted() {
|
|
824
|
+
this.$removeResizeListener(this.$el)
|
|
825
|
+
window.removeEventListener('keydown', this.onKeyDown)
|
|
826
|
+
|
|
827
|
+
if(this.subscribeKey) {
|
|
828
|
+
this.useSocket().send('user.unsubscribe', {name: this.subscribeKey})
|
|
829
|
+
this.useSocket().off(this.subscribeKey, this.onSignal)
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
this.useSocket().off('connect', this.onConnect)
|
|
833
|
+
},
|
|
834
|
+
|
|
835
|
+
computed: {
|
|
836
|
+
|
|
837
|
+
columns(){
|
|
838
|
+
return ((this.preset ?? {}).pivot ?? {}).columns ?? []
|
|
839
|
+
},
|
|
840
|
+
|
|
841
|
+
cConfig(){
|
|
842
|
+
return setupConfig(this.config ?? this._config)
|
|
843
|
+
},
|
|
844
|
+
|
|
845
|
+
contentSlots(){
|
|
846
|
+
const slots = {}
|
|
847
|
+
for(let key in this.$slots){
|
|
848
|
+
if(!key.startsWith('col-'))
|
|
849
|
+
slots[key] = this.$slots[key]
|
|
850
|
+
}
|
|
851
|
+
return slots
|
|
852
|
+
},
|
|
853
|
+
|
|
854
|
+
headerSlots(){
|
|
855
|
+
const slots = {}
|
|
856
|
+
for(let key in this.$slots){
|
|
857
|
+
if(key.startsWith('col-'))
|
|
858
|
+
slots[key] = this.$slots[key]
|
|
859
|
+
}
|
|
860
|
+
return slots
|
|
861
|
+
},
|
|
862
|
+
|
|
863
|
+
computedPresetMode(){
|
|
864
|
+
return this.$device.type === 'mobile' ? 'popup' : this.presetMode
|
|
865
|
+
},
|
|
866
|
+
|
|
867
|
+
dataItems(){
|
|
868
|
+
// pivot results arrive ordered by the server (pivot.sorts falling back
|
|
869
|
+
// to preset.sorts) — no client-side re-sorting
|
|
870
|
+
if(typeof this.itemsFn === 'function'){
|
|
871
|
+
return this.itemsFn(this.data.items)
|
|
872
|
+
}
|
|
873
|
+
return this.data.items
|
|
874
|
+
},
|
|
875
|
+
|
|
876
|
+
// A PivotList preset is ALWAYS in pivot mode — that's the component's
|
|
877
|
+
// contract with the pivot-list server mixin.
|
|
878
|
+
preset(){
|
|
879
|
+
const preset = this.configPresets.find(_ => _.uid === this.configParams.presetIdx) ??
|
|
880
|
+
this.configSharedPresets.find(_ => _.uid === this.configParams.presetIdx)
|
|
881
|
+
|
|
882
|
+
if(preset){
|
|
883
|
+
if(!preset.pivot || typeof preset.pivot !== 'object' || Array.isArray(preset.pivot)){
|
|
884
|
+
preset.pivot = { rows:[], values:[], columns:[] }
|
|
885
|
+
}
|
|
886
|
+
preset.pivot.enabled = true
|
|
887
|
+
|
|
888
|
+
if(this.$route.query?.search)
|
|
889
|
+
preset.search = this.$route.query.search
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
return preset
|
|
893
|
+
},
|
|
894
|
+
|
|
895
|
+
pivotReady(){
|
|
896
|
+
const pivot = (this.preset ?? {}).pivot ?? {}
|
|
897
|
+
return (pivot.rows ?? []).length > 0 || (pivot.values ?? []).length > 0
|
|
898
|
+
},
|
|
899
|
+
|
|
900
|
+
virtualColumns(){
|
|
901
|
+
const pivot = (this.preset ?? {}).pivot
|
|
902
|
+
if(!pivot) return []
|
|
903
|
+
if(!Array.isArray(pivot.virtualColumns))
|
|
904
|
+
pivot.virtualColumns = []
|
|
905
|
+
return pivot.virtualColumns
|
|
906
|
+
},
|
|
907
|
+
|
|
908
|
+
// Insertable [refs] for the formula bar: every pivot result column that
|
|
909
|
+
// isn't itself a formula.
|
|
910
|
+
formulaRefs(){
|
|
911
|
+
return this.columns.filter(_ => _.key && !_.virtual).map(_ => _.key)
|
|
912
|
+
},
|
|
913
|
+
|
|
914
|
+
presetSrc(){
|
|
915
|
+
return this.controller ?
|
|
916
|
+
`${this.controller}.preset` :
|
|
917
|
+
'user.preset'
|
|
918
|
+
},
|
|
919
|
+
|
|
920
|
+
configParams(){
|
|
921
|
+
if(!this.cConfig.params)
|
|
922
|
+
this.cConfig.params = {}
|
|
923
|
+
|
|
924
|
+
if(this.configPresets.length > 0 &&
|
|
925
|
+
(!this.cConfig.params.presetIdx ||
|
|
926
|
+
!this.configPresets.find(_ => _.uid === this.cConfig.params.presetIdx)))
|
|
927
|
+
this.cConfig.params.presetIdx = this.configPresets[0].uid
|
|
928
|
+
|
|
929
|
+
return this.cConfig.params
|
|
930
|
+
},
|
|
931
|
+
|
|
932
|
+
configPresets(){
|
|
933
|
+
return this.cConfig.presets ?? []
|
|
934
|
+
},
|
|
935
|
+
|
|
936
|
+
configSharedPresets(){
|
|
937
|
+
return this.cConfig.sharedPresets ?? []
|
|
938
|
+
},
|
|
939
|
+
|
|
940
|
+
canSearch(){
|
|
941
|
+
return [ 1, true, 'true' ].includes(this.searchable)
|
|
942
|
+
},
|
|
943
|
+
|
|
944
|
+
sidebar(){
|
|
945
|
+
|
|
946
|
+
if(!this.configParams.sidebar || !('open' in this.configParams.sidebar))
|
|
947
|
+
this.configParams.sidebar = {
|
|
948
|
+
open: false,
|
|
949
|
+
width: 360
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
return this.configParams.sidebar
|
|
953
|
+
},
|
|
954
|
+
|
|
955
|
+
sidebarStyle(){
|
|
956
|
+
return {
|
|
957
|
+
width: !this.sidebar.open ? 0 : this.sidebar.width + 'px'
|
|
958
|
+
}
|
|
959
|
+
},
|
|
960
|
+
|
|
961
|
+
computedSrc(){
|
|
962
|
+
return this.src ?? (this.controller ? `${this.controller}.pivot-load` : null)
|
|
963
|
+
},
|
|
964
|
+
|
|
965
|
+
freezeLeft(){
|
|
966
|
+
return this.columns.findIndex(_ => _.freeze) + 1
|
|
967
|
+
},
|
|
968
|
+
|
|
969
|
+
},
|
|
970
|
+
|
|
971
|
+
data(){
|
|
972
|
+
return {
|
|
973
|
+
readyState: 0,
|
|
974
|
+
data: {
|
|
975
|
+
items: []
|
|
976
|
+
},
|
|
977
|
+
compPrefix: '',
|
|
978
|
+
enumCache: {},
|
|
979
|
+
lastEnumItems: null,
|
|
980
|
+
formulaBar: {
|
|
981
|
+
key: '',
|
|
982
|
+
name: '',
|
|
983
|
+
formula: '',
|
|
984
|
+
error: '',
|
|
985
|
+
focused: false
|
|
986
|
+
},
|
|
987
|
+
_config: {
|
|
988
|
+
columns:[],
|
|
989
|
+
presets:[]
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
},
|
|
993
|
+
|
|
994
|
+
provide(){
|
|
995
|
+
return {
|
|
996
|
+
listStyle: this.$style,
|
|
997
|
+
load: this.load
|
|
998
|
+
}
|
|
999
|
+
},
|
|
1000
|
+
|
|
1001
|
+
watch: {
|
|
1002
|
+
|
|
1003
|
+
cConfig: {
|
|
1004
|
+
deep: true,
|
|
1005
|
+
handler(){
|
|
1006
|
+
this.savePreset()
|
|
1007
|
+
}
|
|
1008
|
+
},
|
|
1009
|
+
|
|
1010
|
+
},
|
|
1011
|
+
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
</script>
|
|
1015
|
+
|
|
1016
|
+
<style module>
|
|
1017
|
+
|
|
1018
|
+
.resize1{
|
|
1019
|
+
@apply w-[3px] cursor-ew-resize absolute top-0 right-0 bottom-0;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
.pv-green1, .pv-green2, .pv-green3, .pv-green4, .pv-green5{
|
|
1023
|
+
@apply rounded-lg text-right text-text cursor-pointer;
|
|
1024
|
+
}
|
|
1025
|
+
.pv-green1{
|
|
1026
|
+
background-color: #d3f3df;
|
|
1027
|
+
}
|
|
1028
|
+
.pv-green2{
|
|
1029
|
+
background-color: #bdeecf;
|
|
1030
|
+
}
|
|
1031
|
+
.pv-green3{
|
|
1032
|
+
background-color: #a7e8bf;
|
|
1033
|
+
}
|
|
1034
|
+
.pv-green4{
|
|
1035
|
+
background-color: #91e2af;
|
|
1036
|
+
}
|
|
1037
|
+
.pv-green5{
|
|
1038
|
+
background-color: #7adc9e;
|
|
1039
|
+
}
|
|
1040
|
+
html[data-theme="dark"] .pv-green1{
|
|
1041
|
+
@apply text-text;
|
|
1042
|
+
background-color: #031409;
|
|
1043
|
+
}
|
|
1044
|
+
html[data-theme="dark"] .pv-green2{
|
|
1045
|
+
@apply text-text;
|
|
1046
|
+
background-color: #072713;
|
|
1047
|
+
}
|
|
1048
|
+
html[data-theme="dark"] .pv-green3{
|
|
1049
|
+
@apply text-text;
|
|
1050
|
+
background-color: #0a3b1c;
|
|
1051
|
+
}
|
|
1052
|
+
html[data-theme="dark"] .pv-green4{
|
|
1053
|
+
@apply text-text;
|
|
1054
|
+
background-color: #0e4f26;
|
|
1055
|
+
}
|
|
1056
|
+
html[data-theme="dark"] .pv-green5{
|
|
1057
|
+
@apply text-text;
|
|
1058
|
+
background-color: #11632f;
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
.pv-red1, .pv-red2, .pv-red3, .pv-red4, .pv-red5{
|
|
1062
|
+
@apply rounded-lg text-right text-text cursor-pointer;
|
|
1063
|
+
}
|
|
1064
|
+
.pv-red1{
|
|
1065
|
+
background-color: #fce9e9;
|
|
1066
|
+
}
|
|
1067
|
+
.pv-red2{
|
|
1068
|
+
background-color: #f8d4d4;
|
|
1069
|
+
}
|
|
1070
|
+
.pv-red3{
|
|
1071
|
+
background-color: #f5bebe;
|
|
1072
|
+
}
|
|
1073
|
+
.pv-red4{
|
|
1074
|
+
background-color: #f1a8a8;
|
|
1075
|
+
}
|
|
1076
|
+
.pv-red5{
|
|
1077
|
+
background-color: #ee9393;
|
|
1078
|
+
}
|
|
1079
|
+
html[data-theme="dark"] .pv-red1{
|
|
1080
|
+
@apply text-text;
|
|
1081
|
+
background-color: #160404;
|
|
1082
|
+
}
|
|
1083
|
+
html[data-theme="dark"] .pv-red2{
|
|
1084
|
+
@apply text-text;
|
|
1085
|
+
background-color: #2c0808;
|
|
1086
|
+
}
|
|
1087
|
+
html[data-theme="dark"] .pv-red3{
|
|
1088
|
+
@apply text-text;
|
|
1089
|
+
background-color: #420b0b;
|
|
1090
|
+
}
|
|
1091
|
+
html[data-theme="dark"] .pv-red4{
|
|
1092
|
+
@apply text-text;
|
|
1093
|
+
background-color: #580f0f;
|
|
1094
|
+
}
|
|
1095
|
+
html[data-theme="dark"] .pv-red5{
|
|
1096
|
+
@apply text-text;
|
|
1097
|
+
background-color: #6e1313;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
.pv-blue1, .pv-blue2, .pv-blue3, .pv-blue4, .pv-blue5{
|
|
1101
|
+
@apply rounded-lg text-right text-text cursor-pointer;
|
|
1102
|
+
}
|
|
1103
|
+
.pv-blue1{
|
|
1104
|
+
background-color: #e9effd;
|
|
1105
|
+
}
|
|
1106
|
+
.pv-blue2{
|
|
1107
|
+
background-color: #d3e0fb;
|
|
1108
|
+
}
|
|
1109
|
+
.pv-blue3{
|
|
1110
|
+
background-color: #bed0f9;
|
|
1111
|
+
}
|
|
1112
|
+
.pv-blue4{
|
|
1113
|
+
background-color: #a8c1f7;
|
|
1114
|
+
}
|
|
1115
|
+
.pv-blue5{
|
|
1116
|
+
background-color: #92b1f5;
|
|
1117
|
+
}
|
|
1118
|
+
html[data-theme="dark"] .pv-blue1{
|
|
1119
|
+
@apply text-text;
|
|
1120
|
+
background-color: #040a17;
|
|
1121
|
+
}
|
|
1122
|
+
html[data-theme="dark"] .pv-blue2{
|
|
1123
|
+
@apply text-text;
|
|
1124
|
+
background-color: #07142f;
|
|
1125
|
+
}
|
|
1126
|
+
html[data-theme="dark"] .pv-blue3{
|
|
1127
|
+
@apply text-text;
|
|
1128
|
+
background-color: #0b1e46;
|
|
1129
|
+
}
|
|
1130
|
+
html[data-theme="dark"] .pv-blue4{
|
|
1131
|
+
@apply text-text;
|
|
1132
|
+
background-color: #0f285e;
|
|
1133
|
+
}
|
|
1134
|
+
html[data-theme="dark"] .pv-blue5{
|
|
1135
|
+
@apply text-text;
|
|
1136
|
+
background-color: #133276;
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
.pv-yellow1, .pv-yellow2, .pv-yellow3, .pv-yellow4, .pv-yellow5{
|
|
1140
|
+
@apply rounded-lg text-right text-text cursor-pointer;
|
|
1141
|
+
}
|
|
1142
|
+
.pv-yellow1{
|
|
1143
|
+
background-color: #fdf7e6;
|
|
1144
|
+
}
|
|
1145
|
+
.pv-yellow2{
|
|
1146
|
+
background-color: #fbf0ce;
|
|
1147
|
+
}
|
|
1148
|
+
.pv-yellow3{
|
|
1149
|
+
background-color: #f9e8b5;
|
|
1150
|
+
}
|
|
1151
|
+
.pv-yellow4{
|
|
1152
|
+
background-color: #f7e19c;
|
|
1153
|
+
}
|
|
1154
|
+
.pv-yellow5{
|
|
1155
|
+
background-color: #f5d984;
|
|
1156
|
+
}
|
|
1157
|
+
html[data-theme="dark"] .pv-yellow1{
|
|
1158
|
+
@apply text-text;
|
|
1159
|
+
background-color: #171201;
|
|
1160
|
+
}
|
|
1161
|
+
html[data-theme="dark"] .pv-yellow2{
|
|
1162
|
+
@apply text-text;
|
|
1163
|
+
background-color: #2f2402;
|
|
1164
|
+
}
|
|
1165
|
+
html[data-theme="dark"] .pv-yellow3{
|
|
1166
|
+
@apply text-text;
|
|
1167
|
+
background-color: #463602;
|
|
1168
|
+
}
|
|
1169
|
+
html[data-theme="dark"] .pv-yellow4{
|
|
1170
|
+
@apply text-text;
|
|
1171
|
+
background-color: #5e4803;
|
|
1172
|
+
}
|
|
1173
|
+
html[data-theme="dark"] .pv-yellow5{
|
|
1174
|
+
@apply text-text;
|
|
1175
|
+
background-color: #755a04;
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
.pv-fuchsia1, .pv-fuchsia2, .pv-fuchsia3, .pv-fuchsia4, .pv-fuchsia5{
|
|
1179
|
+
@apply rounded-lg text-right text-text cursor-pointer;
|
|
1180
|
+
}
|
|
1181
|
+
.pv-fuchsia1{
|
|
1182
|
+
background-color: #f6e8f7;
|
|
1183
|
+
}
|
|
1184
|
+
.pv-fuchsia2{
|
|
1185
|
+
background-color: #edd2ef;
|
|
1186
|
+
}
|
|
1187
|
+
.pv-fuchsia3{
|
|
1188
|
+
background-color: #e3bbe7;
|
|
1189
|
+
}
|
|
1190
|
+
.pv-fuchsia4{
|
|
1191
|
+
background-color: #daa4df;
|
|
1192
|
+
}
|
|
1193
|
+
.pv-fuchsia5{
|
|
1194
|
+
background-color: #d18ed7;
|
|
1195
|
+
}
|
|
1196
|
+
html[data-theme="dark"] .pv-fuchsia1{
|
|
1197
|
+
@apply text-text;
|
|
1198
|
+
background-color: #100311;
|
|
1199
|
+
}
|
|
1200
|
+
html[data-theme="dark"] .pv-fuchsia2{
|
|
1201
|
+
@apply text-text;
|
|
1202
|
+
background-color: #210623;
|
|
1203
|
+
}
|
|
1204
|
+
html[data-theme="dark"] .pv-fuchsia3{
|
|
1205
|
+
@apply text-text;
|
|
1206
|
+
background-color: #310834;
|
|
1207
|
+
}
|
|
1208
|
+
html[data-theme="dark"] .pv-fuchsia4{
|
|
1209
|
+
@apply text-text;
|
|
1210
|
+
background-color: #410b46;
|
|
1211
|
+
}
|
|
1212
|
+
html[data-theme="dark"] .pv-fuchsia5{
|
|
1213
|
+
@apply text-text;
|
|
1214
|
+
background-color: #520e58;
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
.pv-gradient1, .pv-gradient2, .pv-gradient3, .pv-gradient4, .pv-gradient5, .pv-default{
|
|
1218
|
+
@apply rounded-lg text-right text-text cursor-pointer;
|
|
1219
|
+
}
|
|
1220
|
+
.pv-gradient1{
|
|
1221
|
+
@apply bg-gradient-to-r from-cyan-500 to-blue-500;
|
|
1222
|
+
}
|
|
1223
|
+
.pv-gradient2{
|
|
1224
|
+
@apply bg-gradient-to-r from-sky-500 to-indigo-500;
|
|
1225
|
+
}
|
|
1226
|
+
.pv-gradient3{
|
|
1227
|
+
@apply bg-gradient-to-r from-violet-500 to-fuchsia-500;
|
|
1228
|
+
}
|
|
1229
|
+
.pv-gradient4{
|
|
1230
|
+
@apply bg-gradient-to-r from-purple-500 to-pink-500;
|
|
1231
|
+
}
|
|
1232
|
+
.pv-gradient5{
|
|
1233
|
+
@apply bg-gradient-to-r from-pink-500 to-teal-500;
|
|
1234
|
+
}
|
|
1235
|
+
.pv-default{
|
|
1236
|
+
@apply text-primary;
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
</style>
|