@hbdlzy/ui-core 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,327 @@
1
+ <template>
2
+ <div ref="containerRef" class="base-echart">
3
+ <div ref="chartRef" class="base-echart__inner"></div>
4
+ </div>
5
+ </template>
6
+
7
+ <script lang="ts" setup>
8
+ import { nextTick, onMounted, onUnmounted, ref, shallowRef, watch } from 'vue'
9
+ import { echarts } from '../../echarts'
10
+ import type {
11
+ EChartInitOptions,
12
+ EChartInstance,
13
+ EChartLoadingOptions,
14
+ EChartOption,
15
+ EChartResizeOptions,
16
+ EChartSetOptionOptions
17
+ } from '../../echarts'
18
+ import type { BaseEChartExpose, BaseEChartProps } from './BaseEChart.types'
19
+
20
+ const props = withDefaults(defineProps<BaseEChartProps>(), {
21
+ option: () => ({}),
22
+ initOptions: () => ({ renderer: 'canvas' } as EChartInitOptions),
23
+ setOptionOptions: () => ({ notMerge: false, lazyUpdate: true } as EChartSetOptionOptions),
24
+ loading: false,
25
+ autoResize: true,
26
+ useDefaultColors: true,
27
+ colorStorageKey: 'echartsColor',
28
+ defaultColors: () => [],
29
+ group: undefined
30
+ })
31
+
32
+ const emit = defineEmits<{
33
+ (event: 'ready', chart: EChartInstance): void
34
+ }>()
35
+
36
+ const containerRef = ref<HTMLElement | null>(null)
37
+ const chartRef = ref<HTMLElement | null>(null)
38
+ const chartInstance = shallowRef<EChartInstance | null>(null)
39
+
40
+ let resizeObserver: ResizeObserver | null = null
41
+ let resizeFrameId: number | null = null
42
+ let pendingResizeOptions: EChartResizeOptions | undefined
43
+
44
+ const cancelResizeFrame = () => {
45
+ if (resizeFrameId !== null) {
46
+ cancelAnimationFrame(resizeFrameId)
47
+ resizeFrameId = null
48
+ }
49
+ }
50
+
51
+ const getDefaultColors = () => {
52
+ if (!props.useDefaultColors || typeof window === 'undefined') {
53
+ return props.defaultColors
54
+ }
55
+
56
+ try {
57
+ const raw = window.sessionStorage.getItem(props.colorStorageKey)
58
+ const colors = raw ? (JSON.parse(raw) as string[]) : []
59
+ return colors.length > 0 ? colors : props.defaultColors
60
+ } catch {
61
+ return props.defaultColors
62
+ }
63
+ }
64
+
65
+ const normalizeOption = (option: EChartOption = {}) => {
66
+ if (option.color) {
67
+ return option
68
+ }
69
+
70
+ const colors = getDefaultColors()
71
+ if (!colors || colors.length === 0) {
72
+ return option
73
+ }
74
+
75
+ return {
76
+ ...option,
77
+ color: colors
78
+ }
79
+ }
80
+
81
+ const createChart = () => {
82
+ if (!chartRef.value) {
83
+ return null
84
+ }
85
+
86
+ const existingChart = echarts.getInstanceByDom(chartRef.value)
87
+ existingChart?.dispose()
88
+
89
+ const instance = echarts.init(chartRef.value, props.theme, props.initOptions)
90
+ if (props.group) {
91
+ instance.group = props.group
92
+ }
93
+
94
+ chartInstance.value = instance
95
+ emit('ready', instance)
96
+ return instance
97
+ }
98
+
99
+ const getInstance = () => chartInstance.value
100
+
101
+ const ensureChart = () => chartInstance.value ?? createChart()
102
+
103
+ const setOption = (
104
+ option: EChartOption,
105
+ setOptionOptions: boolean | EChartSetOptionOptions = props.setOptionOptions
106
+ ) => {
107
+ const chart = ensureChart()
108
+ if (!chart) {
109
+ return null
110
+ }
111
+
112
+ const nextOption = normalizeOption(option)
113
+ if (typeof setOptionOptions === 'boolean') {
114
+ chart.setOption(nextOption, setOptionOptions)
115
+ } else {
116
+ chart.setOption(nextOption, setOptionOptions)
117
+ }
118
+
119
+ return chart
120
+ }
121
+
122
+ const showLoading = (type = 'default', loadingOptions?: EChartLoadingOptions) => {
123
+ const chart = ensureChart()
124
+ if (!chart) {
125
+ return
126
+ }
127
+
128
+ chart.showLoading(type, loadingOptions)
129
+ }
130
+
131
+ const hideLoading = () => {
132
+ chartInstance.value?.hideLoading()
133
+ }
134
+
135
+ const syncLoading = () => {
136
+ if (props.loading) {
137
+ showLoading('default', props.loadingOptions)
138
+ return
139
+ }
140
+
141
+ hideLoading()
142
+ }
143
+
144
+ const resize = (resizeOptions?: EChartResizeOptions) => {
145
+ pendingResizeOptions = resizeOptions
146
+ cancelResizeFrame()
147
+
148
+ resizeFrameId = requestAnimationFrame(() => {
149
+ chartInstance.value?.resize(pendingResizeOptions)
150
+ pendingResizeOptions = undefined
151
+ resizeFrameId = null
152
+ })
153
+ }
154
+
155
+ const handleWindowResize = () => {
156
+ resize()
157
+ }
158
+
159
+ const stopAutoResize = () => {
160
+ resizeObserver?.disconnect()
161
+ resizeObserver = null
162
+ window.removeEventListener('resize', handleWindowResize)
163
+ }
164
+
165
+ const startAutoResize = () => {
166
+ stopAutoResize()
167
+
168
+ if (!props.autoResize || !containerRef.value) {
169
+ return
170
+ }
171
+
172
+ resizeObserver = new ResizeObserver(() => {
173
+ resize()
174
+ })
175
+ resizeObserver.observe(containerRef.value)
176
+ window.addEventListener('resize', handleWindowResize)
177
+ }
178
+
179
+ const clear = () => {
180
+ chartInstance.value?.clear()
181
+ }
182
+
183
+ const disposeChart = () => {
184
+ chartInstance.value?.dispose()
185
+ chartInstance.value = null
186
+ }
187
+
188
+ const dispose = () => {
189
+ disposeChart()
190
+ }
191
+
192
+ const dispatchAction = (payload: Record<string, unknown>) => {
193
+ chartInstance.value?.dispatchAction(payload as never)
194
+ }
195
+
196
+ const on = (eventName: string, handler: (...args: any[]) => void, context?: unknown) => {
197
+ const chart = ensureChart()
198
+ chart?.on(eventName, handler, context)
199
+ }
200
+
201
+ const off = (eventName?: string, handler?: (...args: any[]) => void) => {
202
+ if (!chartInstance.value) {
203
+ return
204
+ }
205
+
206
+ if (!eventName) {
207
+ chartInstance.value.off()
208
+ return
209
+ }
210
+
211
+ if (handler) {
212
+ chartInstance.value.off(eventName, handler)
213
+ return
214
+ }
215
+
216
+ chartInstance.value.off(eventName)
217
+ }
218
+
219
+ const reinitChart = async () => {
220
+ disposeChart()
221
+ await nextTick()
222
+ createChart()
223
+ setOption(props.option)
224
+ syncLoading()
225
+ resize()
226
+ }
227
+
228
+ watch(
229
+ () => props.option,
230
+ option => {
231
+ setOption(option)
232
+ },
233
+ { deep: true }
234
+ )
235
+
236
+ watch(
237
+ () => props.loading,
238
+ () => {
239
+ syncLoading()
240
+ }
241
+ )
242
+
243
+ watch(
244
+ () => props.loadingOptions,
245
+ () => {
246
+ if (props.loading) {
247
+ syncLoading()
248
+ }
249
+ },
250
+ { deep: true }
251
+ )
252
+
253
+ watch(
254
+ () => props.group,
255
+ group => {
256
+ if (!chartInstance.value) {
257
+ return
258
+ }
259
+
260
+ chartInstance.value.group = group ?? ''
261
+ }
262
+ )
263
+
264
+ watch(
265
+ () => props.autoResize,
266
+ async enabled => {
267
+ stopAutoResize()
268
+ if (enabled) {
269
+ await nextTick()
270
+ startAutoResize()
271
+ resize()
272
+ }
273
+ }
274
+ )
275
+
276
+ watch(
277
+ () => props.theme,
278
+ () => {
279
+ reinitChart()
280
+ },
281
+ { deep: true }
282
+ )
283
+
284
+ watch(
285
+ () => props.initOptions,
286
+ () => {
287
+ reinitChart()
288
+ },
289
+ { deep: true }
290
+ )
291
+
292
+ onMounted(async () => {
293
+ await nextTick()
294
+ createChart()
295
+ setOption(props.option)
296
+ syncLoading()
297
+ startAutoResize()
298
+ resize()
299
+ })
300
+
301
+ onUnmounted(() => {
302
+ stopAutoResize()
303
+ cancelResizeFrame()
304
+ disposeChart()
305
+ })
306
+
307
+ defineExpose<BaseEChartExpose>({
308
+ getInstance,
309
+ setOption,
310
+ resize,
311
+ dispatchAction,
312
+ showLoading,
313
+ hideLoading,
314
+ clear,
315
+ dispose,
316
+ on,
317
+ off
318
+ })
319
+ </script>
320
+
321
+ <style scoped>
322
+ .base-echart,
323
+ .base-echart__inner {
324
+ width: 100%;
325
+ height: 100%;
326
+ }
327
+ </style>
@@ -0,0 +1,136 @@
1
+ # BaseEChart
2
+
3
+ `BaseEChart` 用来统一封装项目里重复出现的 ECharts 初始化、更新、尺寸监听和实例销毁逻辑。
4
+
5
+ ## 解决的问题
6
+
7
+ - 页面只传 `option` 就能渲染图表
8
+ - 自动处理 `init`、`setOption`、`resize`、`dispose`
9
+ - 容器尺寸变化时自动 `resize`
10
+ - 不传 `color` 时自动读取 `sessionStorage.echartsColor`
11
+ - 需要手动控制时,可以直接通过组件实例调用 ECharts 方法
12
+
13
+ ## 适用场景
14
+
15
+ - 大部分常规图表页面
16
+ - 希望页面层只维护 `option`
17
+ - 需要统一收敛图表的生命周期代码
18
+ - 需要在页面层偶尔调用 `resize`、`dispatchAction`、`showLoading`
19
+
20
+ ## 不适用场景
21
+
22
+ - 需要完全接管图表 DOM 生命周期
23
+ - 需要自定义复杂容器交互且不适合组件封装
24
+ - 单个页面里要做强定制图表引擎适配
25
+
26
+ ## 基础用法
27
+
28
+ 父容器必须有明确高度。
29
+
30
+ ```vue
31
+ <template>
32
+ <div style="height: 320px">
33
+ <BaseEChart :option="option" />
34
+ </div>
35
+ </template>
36
+
37
+ <script setup lang="ts">
38
+ import { BaseEChart, echarts, type EChartOption } from '@hbdlzy/ui-core'
39
+
40
+ const option: EChartOption = {
41
+ tooltip: {
42
+ trigger: 'axis'
43
+ },
44
+ xAxis: {
45
+ type: 'category',
46
+ data: ['01', '02', '03']
47
+ },
48
+ yAxis: {
49
+ type: 'value'
50
+ },
51
+ series: [
52
+ {
53
+ type: 'line',
54
+ smooth: true,
55
+ data: [12, 18, 15],
56
+ areaStyle: {
57
+ color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
58
+ { offset: 0, color: 'rgba(24, 144, 255, 0.35)' },
59
+ { offset: 1, color: 'rgba(24, 144, 255, 0)' }
60
+ ])
61
+ }
62
+ }
63
+ ]
64
+ }
65
+ </script>
66
+ ```
67
+
68
+ ## 直接调用实例方法
69
+
70
+ ```vue
71
+ <script setup lang="ts">
72
+ import { ref } from 'vue'
73
+ import { BaseEChart, type BaseEChartExpose } from '@hbdlzy/ui-core'
74
+
75
+ const chartRef = ref<BaseEChartExpose | null>(null)
76
+
77
+ const refreshChart = () => {
78
+ chartRef.value?.resize()
79
+ chartRef.value?.dispatchAction({
80
+ type: 'hideTip'
81
+ })
82
+ }
83
+ </script>
84
+
85
+ <template>
86
+ <BaseEChart ref="chartRef" :option="option" />
87
+ </template>
88
+ ```
89
+
90
+ ## 直接调用 echarts 原生能力
91
+
92
+ ```ts
93
+ import { echarts } from '@hbdlzy/ui-core'
94
+
95
+ const gradient = new echarts.graphic.LinearGradient(0, 0, 0, 1, [
96
+ { offset: 0, color: '#2f88ff' },
97
+ { offset: 1, color: 'rgba(47, 136, 255, 0)' }
98
+ ])
99
+ ```
100
+
101
+ ## Props
102
+
103
+ - `option`: 图表配置,直接透传给 `setOption`
104
+ - `theme`: ECharts 主题
105
+ - `initOptions`: `echarts.init` 的第三个参数
106
+ - `setOptionOptions`: `setOption` 的第二个参数
107
+ - `loading`: 是否显示 loading
108
+ - `loadingOptions`: loading 配置
109
+ - `autoResize`: 是否自动监听尺寸变化并执行 `resize`
110
+ - `useDefaultColors`: 当 `option.color` 未传时,是否自动读取默认配色
111
+ - `colorStorageKey`: 默认配色的 `sessionStorage` key,默认 `echartsColor`
112
+ - `defaultColors`: 默认配色兜底值
113
+ - `group`: 图表联动分组
114
+
115
+ ## Events
116
+
117
+ - `ready`: 图表实例初始化完成时触发,返回 ECharts 实例
118
+
119
+ ## Expose
120
+
121
+ - `getInstance`: 获取当前 ECharts 实例
122
+ - `setOption`: 手动设置 option
123
+ - `resize`: 手动触发 resize
124
+ - `dispatchAction`: 调用原生 `dispatchAction`
125
+ - `showLoading`: 显示 loading
126
+ - `hideLoading`: 隐藏 loading
127
+ - `clear`: 清空图表
128
+ - `dispose`: 销毁图表实例
129
+ - `on`: 绑定 ECharts 事件
130
+ - `off`: 解绑 ECharts 事件
131
+
132
+ ## 推荐约定
133
+
134
+ - 页面层只维护 `option`,不要重复写 `echarts.init`
135
+ - 默认走 `BaseEChart + option`,只有确实需要时再通过 `ref` 调暴露方法
136
+ - 渐变色、图形工具优先从 `@hbdlzy/ui-core` 导出的 `echarts` 使用
@@ -0,0 +1,5 @@
1
+ import BaseEChart from './BaseEChart.vue'
2
+
3
+ export default BaseEChart
4
+
5
+ export type { BaseEChartExpose, BaseEChartProps } from './BaseEChart.types'
@@ -140,7 +140,7 @@ async function handleExport() {
140
140
 
141
141
  try {
142
142
  if (exportMode.value === 'excel' && props.excelOptions) {
143
- exportExcel(props.excelOptions)
143
+ await exportExcel(props.excelOptions)
144
144
 
145
145
  if (props.autoMessage) {
146
146
  ElMessage.success(props.successMessage)
@@ -1,25 +1,121 @@
1
1
  # BaseExportButton
2
2
 
3
- `BaseExportButton` 是一个基础导出按钮组件,用来统一项目里的导出入口。
3
+ `BaseExportButton` 用来统一项目中的导出入口,避免每个页面重复写按钮状态、导出前校验、成功失败提示和文件下载逻辑。
4
4
 
5
- 它支持两类场景:
5
+ ## 解决的问题
6
6
 
7
- - 纯前端 Excel 导出
8
- - 调接口返回 blob 文件下载
7
+ - 统一前端 Excel 导出按钮交互
8
+ - 统一后端 `blob` 文件下载按钮交互
9
+ - 自动处理 loading 状态
10
+ - 自动处理成功和失败消息提示
11
+ - 将导出前校验、导出成功回调收敛成统一接口
9
12
 
10
- ## 使用方式
13
+ ## 适用场景
14
+
15
+ - 页面需要导出表格数据为 Excel
16
+ - 页面需要调用后端接口导出文件
17
+ - 希望多个项目中的导出按钮交互保持一致
18
+
19
+ ## 不适用场景
20
+
21
+ - 页面需要复杂的导出配置弹窗
22
+ - 导出前需要强业务表单流程编排
23
+ - 不是按钮触发型的导出入口
24
+
25
+ ## 支持模式
26
+
27
+ - `excelOptions`: 纯前端 Excel 导出
28
+ - `requestHandler`: 调接口获取 `blob` 或二进制响应后下载
29
+
30
+ 这两个模式二选一,不能同时传。
31
+
32
+ ## 基础用法
11
33
 
12
34
  ```vue
35
+ <template>
36
+ <BaseExportButton :excel-options="excelOptions" />
37
+ </template>
38
+
13
39
  <script setup lang="ts">
14
- import { BaseExportButton } from '@hbdlzy/ui-core'
40
+ import { BaseExportButton, type ExcelExportOptions } from '@hbdlzy/ui-core'
41
+
42
+ interface UserRow {
43
+ name: string
44
+ department: string
45
+ }
46
+
47
+ const excelOptions: ExcelExportOptions<UserRow> = {
48
+ fileName: '用户列表',
49
+ columns: [
50
+ { label: '姓名', key: 'name' },
51
+ { label: '部门', key: 'department' }
52
+ ],
53
+ data: [
54
+ { name: '张三', department: '运维' },
55
+ { name: '李四', department: '调度' }
56
+ ]
57
+ }
15
58
  </script>
59
+ ```
60
+
61
+ ## 后端导出示例
16
62
 
63
+ ```vue
17
64
  <template>
18
- <BaseExportButton :excel-options="excelOptions" />
65
+ <BaseExportButton
66
+ file-name="运行报表.xlsx"
67
+ :request-handler="requestHandler"
68
+ :before-export="beforeExport"
69
+ />
19
70
  </template>
71
+
72
+ <script setup lang="ts">
73
+ import { BaseExportButton } from '@hbdlzy/ui-core'
74
+
75
+ const beforeExport = async () => {
76
+ return true
77
+ }
78
+
79
+ const requestHandler = async () => {
80
+ const response = await api.exportReport()
81
+
82
+ return {
83
+ data: response.data,
84
+ headers: response.headers
85
+ }
86
+ }
87
+ </script>
20
88
  ```
21
89
 
90
+ ## Props
91
+
92
+ - `label`: 按钮文案,默认 `导出`
93
+ - `type`: Element Plus 按钮类型,默认 `primary`
94
+ - `size`: 按钮尺寸,默认 `default`
95
+ - `plain`: 是否朴素按钮
96
+ - `link`: 是否链接按钮
97
+ - `text`: 是否文字按钮
98
+ - `disabled`: 是否禁用
99
+ - `fileName`: 后端下载时的兜底文件名
100
+ - `excelOptions`: 前端 Excel 导出配置
101
+ - `requestHandler`: 后端导出请求函数
102
+ - `beforeExport`: 导出前钩子,返回 `false` 时中断导出
103
+ - `successMessage`: 成功提示文案
104
+ - `errorMessage`: 失败提示文案
105
+ - `autoMessage`: 是否自动弹出成功失败提示
106
+
107
+ ## Events
108
+
109
+ - `start`: 点击并通过校验后开始导出
110
+ - `success`: 导出成功,返回 `{ mode, fileName }`
111
+ - `error`: 导出失败
112
+
22
113
  ## 约束
23
114
 
24
115
  - `excelOptions` 和 `requestHandler` 二选一
25
- - 组件只负责触发导出,不负责业务数据查询
116
+ - 组件只负责触发导出,不负责业务数据查询
117
+ - 如果使用前端导出,建议优先复用 `exportExcel`
118
+
119
+ ## 相关能力
120
+
121
+ - 工具函数:`@hbdlzy/ui-core` 中的 `exportExcel`