n20-common-lib 3.1.4 → 3.1.6

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.
@@ -0,0 +1,172 @@
1
+ <template>
2
+ <!-- 左侧报表清单 -->
3
+ <div class="pivot-sidebar-left" :class="{ collapsed: isCollapsed }">
4
+ <div class="sidebar-header">
5
+ <span class="sidebar-title">我的报表清单</span>
6
+ <div class="add-report-btn" @click="handleAddReport" title="新建">
7
+ <i class="v3-icon-plus"></i>
8
+ </div>
9
+ </div>
10
+
11
+ <div class="report-list">
12
+ <div
13
+ v-for="(report, index) in reportList"
14
+ :key="report.id"
15
+ class="report-item"
16
+ :class="{ active: currentIndex === index, editing: editingId === report.id }"
17
+ @click="handleSelectReport(index)"
18
+ @mouseenter="hoveredId = report.id"
19
+ @mouseleave="hoveredId = null"
20
+ >
21
+ <i :class="getChartTypeIcon(report.chartType)" class="report-icon"></i>
22
+ <!-- 报表名称显示/编辑 -->
23
+ <span v-if="editingId !== report.id" class="report-name">{{ report.name }}</span>
24
+ <input
25
+ v-else
26
+ v-model="editingName"
27
+ class="report-name-input"
28
+ type="text"
29
+ @click.stop
30
+ @blur="handleSaveRename"
31
+ @keyup.enter="handleSaveRename"
32
+ @keyup.esc="handleCancelRename"
33
+ />
34
+ <!-- 更多操作菜单 -->
35
+ <div
36
+ v-if="editingId !== report.id"
37
+ class="report-more"
38
+ :class="{ visible: hoveredId === report.id || activeMenuId === report.id }"
39
+ @click.stop="handleToggleMenu(report.id)"
40
+ >
41
+ <i class="v3-icon-more"></i>
42
+ <div v-if="activeMenuId === report.id" class="report-menu">
43
+ <div class="report-menu-item" @click.stop="handleStartRename(report)">
44
+ <i class="v3-icon-edit"></i>
45
+ <span>重命名</span>
46
+ </div>
47
+ <div class="report-menu-item delete" @click.stop="handleDeleteReport(report)">
48
+ <i class="v3-icon-delete"></i>
49
+ <span>删除</span>
50
+ </div>
51
+ </div>
52
+ </div>
53
+ </div>
54
+ </div>
55
+
56
+ <!-- 折叠/展开指示器 -->
57
+ <div
58
+ class="scroll-bar-indicator"
59
+ :class="{ collapsed: isCollapsed }"
60
+ @click="handleToggleCollapse"
61
+ >
62
+ <i :class="isCollapsed ? 'v3-icon-right' : 'v3-icon-left'"></i>
63
+ </div>
64
+ </div>
65
+ </template>
66
+
67
+ <script>
68
+ export default {
69
+ name: 'ReportSidebar',
70
+ props: {
71
+ // 报表列表
72
+ reportList: {
73
+ type: Array,
74
+ default: () => []
75
+ },
76
+ // 当前选中的报表索引
77
+ currentIndex: {
78
+ type: Number,
79
+ default: 0
80
+ },
81
+ // 是否折叠
82
+ isCollapsed: {
83
+ type: Boolean,
84
+ default: false
85
+ }
86
+ },
87
+ data() {
88
+ return {
89
+ // 报表项悬停ID
90
+ hoveredId: null,
91
+ // 当前显示菜单的报表ID
92
+ activeMenuId: null,
93
+ // 正在编辑的报表ID
94
+ editingId: null,
95
+ // 编辑中的报表名称
96
+ editingName: ''
97
+ }
98
+ },
99
+ methods: {
100
+ // 获取图表类型图标
101
+ getChartTypeIcon(type) {
102
+ const iconMap = {
103
+ bar: 'v3-icon-chart-histogram',
104
+ line: 'v3-icon-chart-line',
105
+ pie: 'v3-icon-chart-pie',
106
+ horizontalBar: 'v3-icon-histogram'
107
+ }
108
+ return iconMap[type] || 'v3-icon-chart-histogram'
109
+ },
110
+
111
+ // 选择报表
112
+ handleSelectReport(index) {
113
+ this.$emit('select', index)
114
+ },
115
+
116
+ // 添加报表
117
+ handleAddReport() {
118
+ this.$emit('add')
119
+ },
120
+
121
+ // 切换折叠状态
122
+ handleToggleCollapse() {
123
+ this.$emit('toggle-collapse')
124
+ },
125
+
126
+ // 切换报表菜单显示
127
+ handleToggleMenu(reportId) {
128
+ this.activeMenuId = this.activeMenuId === reportId ? null : reportId
129
+ },
130
+
131
+ // 开始重命名
132
+ handleStartRename(report) {
133
+ this.editingId = report.id
134
+ this.editingName = report.name
135
+ this.activeMenuId = null
136
+ this.$nextTick(() => {
137
+ const input = this.$el.querySelector('.report-name-input')
138
+ if (input) {
139
+ input.focus()
140
+ input.select()
141
+ }
142
+ })
143
+ },
144
+
145
+ // 保存重命名
146
+ handleSaveRename() {
147
+ if (this.editingId && this.editingName.trim()) {
148
+ this.$emit('rename', { id: this.editingId, name: this.editingName.trim() })
149
+ }
150
+ this.editingId = null
151
+ this.editingName = ''
152
+ },
153
+
154
+ // 取消重命名
155
+ handleCancelRename() {
156
+ this.editingId = null
157
+ this.editingName = ''
158
+ },
159
+
160
+ // 删除报表
161
+ handleDeleteReport(report) {
162
+ this.activeMenuId = null
163
+ this.$emit('delete', report)
164
+ },
165
+
166
+ // 关闭菜单(供外部调用)
167
+ closeMenu() {
168
+ this.activeMenuId = null
169
+ }
170
+ }
171
+ }
172
+ </script>
@@ -0,0 +1,341 @@
1
+ <template>
2
+ <!-- 表格视图 -->
3
+ <div class="table-view">
4
+ <div v-if="hasData" class="table-wrapper">
5
+ <!-- 数据概览头部 -->
6
+ <!-- <div class="table-header">
7
+ <div class="table-title">数据概览</div>
8
+ <div class="table-desc">
9
+ <template v-if="isCrossMode">
10
+ 按行维度「{{ rowDimensionLabel }}」、列维度「{{ columnDimensionLabel }}」交叉分析后的结果,共 {{ dataCount }} 条
11
+ </template>
12
+ <template v-else>
13
+ 按维度「{{ dimensionLabel }}」聚合后的结果,共 {{ dataCount }} 条
14
+ </template>
15
+ </div>
16
+ </div> -->
17
+
18
+ <!-- 表格容器 -->
19
+ <div class="table-container">
20
+ <vxe-table
21
+ ref="vxeTable"
22
+ :data="tableData"
23
+ :height="'100%'"
24
+ :scroll-y="{ enabled: true, gt: 0 }"
25
+ :scroll-x="{ enabled: true, gt: 0 }"
26
+ :row-config="{ isHover: true }"
27
+ :column-config="{ resizable: true }"
28
+ show-header-overflow
29
+ show-overflow
30
+ auto-resize
31
+ class="pivot-vxe-table"
32
+ >
33
+ <!-- 动态列渲染 - 支持多级表头 -->
34
+ <template v-for="col in vxeColumns">
35
+ <!-- 分组列(多级表头) -->
36
+ <vxe-colgroup
37
+ v-if="col.children && col.children.length > 0"
38
+ :key="col.key"
39
+ :title="col.title"
40
+ :header-align="col.headerAlign || 'center'"
41
+ >
42
+ <vxe-column
43
+ v-for="childCol in col.children"
44
+ :key="childCol.key"
45
+ :field="childCol.key"
46
+ :title="childCol.title"
47
+ :min-width="childCol.minWidth"
48
+ :align="childCol.align"
49
+ :header-align="childCol.headerAlign"
50
+ >
51
+ <template #default="{ row }">
52
+ <div class="metric-cell-content">
53
+ <span v-if="childCol.aggregateType === 'count'" class="metric-count">
54
+ {{ formatCount(row._groupCount) }} 笔
55
+ </span>
56
+ <span v-else class="metric-value">{{ formatNumber(row[childCol.key]) }}</span>
57
+ </div>
58
+ </template>
59
+ </vxe-column>
60
+ </vxe-colgroup>
61
+
62
+ <!-- 普通列 -->
63
+ <vxe-column
64
+ v-else
65
+ :key="col.field"
66
+ :field="col.field"
67
+ :title="col.title"
68
+ :min-width="col.minWidth"
69
+ :align="col.align"
70
+ :header-align="col.headerAlign"
71
+ >
72
+ <template #default="{ row }">
73
+ <!-- 维度列:直接显示值 -->
74
+ <template v-if="col.type === 'dimension'">
75
+ {{ row[col.field] }}
76
+ </template>
77
+ <!-- 指标列:根据聚合类型显示 -->
78
+ <template v-else-if="col.type === 'metric'">
79
+ <div class="metric-cell-content">
80
+ <span v-if="col.aggregateType === 'count'" class="metric-count">
81
+ {{ formatCount(row._groupCount) }} 笔
82
+ </span>
83
+ <span v-else class="metric-value">{{ formatNumber(row[col.field]) }}</span>
84
+ </div>
85
+ </template>
86
+ </template>
87
+ </vxe-column>
88
+ </template>
89
+
90
+ <!-- 空状态 -->
91
+ <template #empty>
92
+ <Empty type="noData2" :height="200" :width="200" content="暂无数据" />
93
+ </template>
94
+ </vxe-table>
95
+ </div>
96
+
97
+ <!-- 分页组件 -->
98
+ <div class="cross-pagination">
99
+ <div class="pagination-info">共{{ totalCount }}条</div>
100
+ <div class="pagination-controls">
101
+ <div class="pagination-btn" :class="{ disabled: currentPage <= 1 }" @click="handlePrevPage">
102
+ <i class="v3-icon-left"></i>
103
+ </div>
104
+ <div class="pagination-current">{{ currentPage }}</div>
105
+ <span class="pagination-separator">/</span>
106
+ <div class="pagination-total">{{ totalPages }}</div>
107
+ <div class="pagination-btn" :class="{ disabled: currentPage >= totalPages }" @click="handleNextPage">
108
+ <i class="v3-icon-right"></i>
109
+ </div>
110
+ </div>
111
+ <div class="pagination-size-select" @click="showPageSizeDropdown = !showPageSizeDropdown">
112
+ <span>{{ pageSize }}条/页</span>
113
+ <i class="v3-icon-down"></i>
114
+ <div v-if="showPageSizeDropdown" class="pagination-size-dropdown">
115
+ <div
116
+ v-for="size in pageSizeOptions"
117
+ :key="size"
118
+ class="pagination-size-option"
119
+ :class="{ active: pageSize === size }"
120
+ @click.stop="handlePageSizeChange(size)"
121
+ >
122
+ {{ size }}条/页
123
+ </div>
124
+ </div>
125
+ </div>
126
+ <div class="pagination-jump">
127
+ <span class="pagination-jump-label">前往</span>
128
+ <input type="number" class="pagination-jump-input" :value="currentPage" @change="handleJumpToPage" min="1" />
129
+ <span class="pagination-jump-label">页</span>
130
+ </div>
131
+ </div>
132
+ </div>
133
+ <div v-else class="empty-state">
134
+ <Empty type="noData2" :height="200" :width="200" content="暂无数据" />
135
+ </div>
136
+ </div>
137
+ </template>
138
+
139
+ <script>
140
+ import Empty from '../Empty/index.vue'
141
+
142
+ export default {
143
+ name: 'TableView',
144
+ components: {
145
+ Empty
146
+ },
147
+ props: {
148
+ // 是否有数据
149
+ hasData: {
150
+ type: Boolean,
151
+ default: false
152
+ },
153
+ // 数据条数
154
+ dataCount: {
155
+ type: Number,
156
+ default: 0
157
+ },
158
+ // 维度标签
159
+ dimensionLabel: {
160
+ type: String,
161
+ default: ''
162
+ },
163
+ // 是否交叉统计模式
164
+ isCrossMode: {
165
+ type: Boolean,
166
+ default: false
167
+ },
168
+ // 行维度标签(交叉统计用)
169
+ rowDimensionLabel: {
170
+ type: String,
171
+ default: ''
172
+ },
173
+ // 列维度标签(交叉统计用)
174
+ columnDimensionLabel: {
175
+ type: String,
176
+ default: ''
177
+ },
178
+ // 表格列定义
179
+ tableColumns: {
180
+ type: Array,
181
+ default: () => []
182
+ },
183
+ // 交叉表列定义
184
+ crossTableColumns: {
185
+ type: Array,
186
+ default: () => []
187
+ },
188
+ // 分页后的表格数据
189
+ tableData: {
190
+ type: Array,
191
+ default: () => []
192
+ },
193
+ // 总数据条数
194
+ totalCount: {
195
+ type: Number,
196
+ default: 0
197
+ },
198
+ // 当前页
199
+ currentPage: {
200
+ type: Number,
201
+ default: 1
202
+ },
203
+ // 每页条数
204
+ pageSize: {
205
+ type: Number,
206
+ default: 10
207
+ },
208
+ // 每页条数选项
209
+ pageSizeOptions: {
210
+ type: Array,
211
+ default: () => [10, 20, 50, 100]
212
+ },
213
+ // 总页数
214
+ totalPages: {
215
+ type: Number,
216
+ default: 1
217
+ }
218
+ },
219
+ data() {
220
+ return {
221
+ showPageSizeDropdown: false
222
+ }
223
+ },
224
+ computed: {
225
+ // 转换为 vxe-table 列格式(支持多级表头)
226
+ vxeColumns() {
227
+ // 计算列最小宽度的辅助函数
228
+ const calcMinWidth = (title, isDimension) => {
229
+ // 基础宽度:标题字符数 * 14px + 左右 padding 32px
230
+ const titleWidth = (title?.length || 0) * 14 + 32
231
+ // 维度列最小 120px,指标列最小 100px
232
+ const baseMin = isDimension ? 120 : 100
233
+ return Math.max(titleWidth, baseMin)
234
+ }
235
+
236
+ if (this.isCrossMode) {
237
+ return this.crossTableColumns.map((col) => {
238
+ // 分组列(多级表头)
239
+ if (col.type === 'group' && col.children) {
240
+ return {
241
+ key: col.key,
242
+ title: col.title,
243
+ type: 'group',
244
+ headerAlign: 'center',
245
+ // 递归处理子列
246
+ children: col.children.map((childCol) => ({
247
+ field: childCol.key,
248
+ title: childCol.title,
249
+ minWidth: calcMinWidth(childCol.title, false),
250
+ align: 'right',
251
+ headerAlign: 'right',
252
+ type: childCol.type,
253
+ aggregateType: childCol.aggregateType
254
+ }))
255
+ }
256
+ }
257
+ // 普通列
258
+ const isDimension = col.type === 'dimension'
259
+ return {
260
+ field: col.key,
261
+ title: col.title,
262
+ minWidth: calcMinWidth(col.title, isDimension),
263
+ align: isDimension ? 'left' : 'right',
264
+ headerAlign: isDimension ? 'left' : 'right',
265
+ type: col.type,
266
+ aggregateType: col.aggregateType
267
+ }
268
+ })
269
+ } else {
270
+ return this.tableColumns.map((col) => {
271
+ const isDimension = col.type === 'dimension'
272
+ return {
273
+ field: col.key,
274
+ title: col.title,
275
+ minWidth: calcMinWidth(col.title, isDimension),
276
+ align: isDimension ? 'left' : 'right',
277
+ headerAlign: isDimension ? 'left' : 'right',
278
+ type: col.type,
279
+ aggregateType: col.aggregateType
280
+ }
281
+ })
282
+ }
283
+ }
284
+ },
285
+ methods: {
286
+ // 格式化数值显示
287
+ formatNumber(val) {
288
+ if (val === null || val === undefined || val === '') return '--'
289
+ const num = parseFloat(val)
290
+ if (isNaN(num)) return '--'
291
+ return num.toLocaleString('en-US', { minimumFractionDigits: 2 })
292
+ },
293
+
294
+ // 格式化笔数显示(0 显示为 "-",其他显示数字)
295
+ formatCount(count) {
296
+ if (count === null || count === undefined || count === 0) return '-'
297
+ return count
298
+ },
299
+
300
+ // 上一页
301
+ handlePrevPage() {
302
+ if (this.currentPage > 1) {
303
+ this.$emit('prev-page')
304
+ }
305
+ },
306
+
307
+ // 下一页
308
+ handleNextPage() {
309
+ if (this.currentPage < this.totalPages) {
310
+ this.$emit('next-page')
311
+ }
312
+ },
313
+
314
+ // 跳转到指定页
315
+ handleJumpToPage(e) {
316
+ const page = parseInt(e.target.value)
317
+ if (page && page >= 1 && page <= this.totalPages) {
318
+ this.$emit('jump-page', page)
319
+ }
320
+ },
321
+
322
+ // 每页条数变更
323
+ handlePageSizeChange(size) {
324
+ this.showPageSizeDropdown = false
325
+ this.$emit('page-size-change', size)
326
+ },
327
+
328
+ // 关闭下拉菜单(供外部调用)
329
+ closeDropdown() {
330
+ this.showPageSizeDropdown = false
331
+ },
332
+
333
+ // 刷新表格(供外部调用)
334
+ refreshTable() {
335
+ if (this.$refs.vxeTable) {
336
+ this.$refs.vxeTable.refreshColumn()
337
+ }
338
+ }
339
+ }
340
+ }
341
+ </script>