feat(hooks): 完善useEcharts

This commit is contained in:
Coffee-crocodile 2023-01-11 17:04:34 +08:00
parent c5705f7032
commit 6ef49cb047
7 changed files with 746 additions and 127 deletions

16
.gitattributes vendored Normal file
View File

@ -0,0 +1,16 @@
"*.vue" eol=lf
"*.js" eol=lf
"*.ts" eol=lf
"*.jsx" eol=lf
"*.tsx" eol=lf
"*.cjs" eol=lf
"*.cts" eol=lf
"*.mjs" eol=lf
"*.mts" eol=lf
"*.json" eol=lf
"*.html" eol=lf
"*.css" eol=lf
"*.less" eol=lf
"*.scss" eol=lf
"*.sass" eol=lf
"*.styl" eol=lf

View File

@ -1,9 +1,29 @@
<template>
<div class="flex-col-center h-800px">
<img v-if="type === '403'" src="@/assets/svg/error-403.svg" alt="" class="w-1/3" />
<img v-if="type === '404'" src="@/assets/svg/error-404.svg" alt="" class="w-1/3" />
<img v-if="type === '500'" src="@/assets/svg/error-500.svg" alt="" class="w-1/3" />
<n-button type="primary" @click="toRoot">回到首页</n-button>
<div class="flex-col-center h-full">
<img
v-if="type === '403'"
src="@/assets/svg/error-403.svg"
alt=""
class="w-1/3"
>
<img
v-if="type === '404'"
src="@/assets/svg/error-404.svg"
alt=""
class="w-1/3"
>
<img
v-if="type === '500'"
src="@/assets/svg/error-500.svg"
alt=""
class="w-1/3"
>
<n-button
type="primary"
@click="toRoot"
>
回到首页
</n-button>
</div>
</template>
@ -12,8 +32,8 @@ import { useAppRouter } from '@/hooks';
type TipType = '403' | '404' | '500';
defineProps<{
/** 异常类型 403 404 500 */
type: TipType;
/** 异常类型 403 404 500 */
type: TipType;
}>();
const { toRoot } = useAppRouter();
</script>

View File

@ -1,87 +1,144 @@
import * as echarts from 'echarts/core';
import { nextTick, ref, onUnmounted, onMounted } from 'vue';
import { nextTick, ref, onUnmounted, watch } from 'vue';
import type { Ref } from 'vue';
import { BarChart, LineChart } from 'echarts/charts';
import { BarChart, LineChart, PieChart, RadarChart } from 'echarts/charts';
// 系列类型的定义后缀都为 SeriesOption
import type { BarSeriesOption, LineSeriesOption } from 'echarts/charts';
import type {
BarSeriesOption,
LineSeriesOption,
PieSeriesOption,
RadarSeriesOption,
} from 'echarts/charts';
import {
TitleComponent,
TooltipComponent,
GridComponent,
DatasetComponent, // 数据集组件
TransformComponent, // 内置数据转换器组件 (filter, sort)
} from 'echarts/components';
// 组件类型的定义后缀都为 ComponentOption
import type {
TitleComponentOption,
TooltipComponentOption,
GridComponentOption,
DatasetComponentOption,
TitleComponentOption,
TooltipComponentOption,
GridComponentOption,
LegendComponentOption,
DatasetComponentOption,
ToolboxComponentOption,
} from 'echarts/components';
import {
TitleComponent,
TooltipComponent,
GridComponent,
LegendComponent,
DatasetComponent, // 数据集组件
TransformComponent, // 内置数据转换器组件 (filter, sort)
ToolboxComponent,
} from 'echarts/components';
import { LabelLayout, UniversalTransition } from 'echarts/features';
import { CanvasRenderer } from 'echarts/renderers';
import { useAppStore } from '@/store';
import { useElementSize } from '@vueuse/core';
// 通过 ComposeOption 来组合出一个只有必须组件和图表的 Option 类型
export type ECOption = echarts.ComposeOption<
| BarSeriesOption
| LineSeriesOption
| TitleComponentOption
| TooltipComponentOption
| GridComponentOption
| DatasetComponentOption
| BarSeriesOption
| PieSeriesOption
| LineSeriesOption
| TitleComponentOption
| TooltipComponentOption
| GridComponentOption
| LegendComponentOption
| DatasetComponentOption
| ToolboxComponentOption
| RadarSeriesOption
>;
// 注册必须的组件
echarts.use([
TitleComponent,
TooltipComponent,
GridComponent,
DatasetComponent,
TransformComponent,
BarChart,
LineChart,
LabelLayout,
UniversalTransition,
CanvasRenderer,
TitleComponent,
TooltipComponent,
GridComponent,
LegendComponent,
DatasetComponent,
TransformComponent,
BarChart,
PieChart,
LineChart,
LabelLayout,
UniversalTransition,
CanvasRenderer,
ToolboxComponent,
RadarChart,
]);
/**
* Echarts hooks函数
* @param options -
* @description
*/
export function useEcharts(options: Ref<ECOption>) {
const domRef = ref<HTMLElement>();
const appStore = useAppStore();
let chart: echarts.ECharts | null = null;
const domRef = ref<HTMLElement>();
async function render() {
if (domRef.value) {
chart = echarts.init(domRef.value);
update(options.value);
}
}
function isRendered() {
return Boolean(domRef.value && chart);
}
function destroy() {
chart?.dispose();
chart = null;
}
let chart: echarts.ECharts | null = null;
function update(updateOptions: ECOption) {
if (isRendered()) {
chart!.setOption({ ...updateOptions, backgroundColor: 'transparent' });
}
}
const initialSize = { width: 0, height: 0 };
const { width, height } = useElementSize(domRef, initialSize);
onMounted(async () => {
await nextTick();
render();
});
onUnmounted(() => {
destroy();
});
function canRender() {
return initialSize.width > 0 && initialSize.height > 0;
}
return {
domRef,
};
function isRendered() {
return Boolean(domRef.value && chart);
}
async function render() {
const chartTheme = appStore.darkMode ? 'dark' : 'light';
await nextTick();
if (domRef.value) {
chart = echarts.init(domRef.value, chartTheme);
update(options.value);
}
}
function update(updateOptions: ECOption) {
if (isRendered()) {
chart!.setOption({ ...updateOptions, backgroundColor: 'transparent' });
}
}
function resize() {
chart?.resize();
}
function destroy() {
chart?.dispose();
chart = null;
}
const sizeWatch = watch([width, height], ([newWidth, newHeight]) => {
initialSize.width = newWidth;
initialSize.height = newHeight;
if (newWidth === 0 && newHeight === 0) {
// 节点被删除 将chart置为空
chart = null;
}
if (!canRender()) return;
if (isRendered()) {
resize();
} else {
render();
}
});
const OptionWatch = watch(options, (newValue) => {
console.log(newValue);
update(newValue);
});
onUnmounted(() => {
sizeWatch();
OptionWatch();
destroy();
});
return {
domRef,
};
}

View File

@ -11,6 +11,7 @@ async function setupApp() {
// 载入全局loading加载状态
const appLoading = createApp(AppLoading);
appLoading.mount('#appLoading');
// 创建vue实例
const app = createApp(App);
// 安装pinia全局状态库
@ -19,5 +20,7 @@ async function setupApp() {
await setupRouter(app);
// 挂载
await app.mount('#app');
// 卸载载入动画
appLoading.unmount();
}
setupApp();

View File

@ -1,24 +1,54 @@
<template>
<div class="flex-center wh-full">
<n-carousel autoplay trigger="hover" dot-type="line" effect="fade">
<img v-for="(item, index) in swiperList" :key="index" class="wh-full object-cover" :src="item" />
<n-carousel
autoplay
trigger="hover"
dot-type="line"
effect="fade"
>
<img
v-for="(item, index) in swiperList"
:key="index"
class="wh-full object-cover"
:src="item"
>
</n-carousel>
<div class="h-xl py-10 px-6xl rounded-lg backdrop-blur-xl shadow-md fixed bg-hex-ffffffd0 op-90">
<n-el
tag="div"
style="background: var(--modal-color); box-shadow: var(--box-shadow-3)"
class="h-xl py-10 px-6xl rounded-lg backdrop-blur-xl fixed op-90"
>
<n-h1 class="c-blue">
<i-icon-park-outline-plastic-surgery />
Ench Admin
</n-h1>
<n-p depth="3">高效简约可能对你有点帮助</n-p>
<n-form ref="formRef" :rules="rules" :model="formValue" :show-label="false" size="large">
<n-p depth="3">
高效简约可能对你有点帮助
</n-p>
<n-form
ref="formRef"
:rules="rules"
:model="formValue"
:show-label="false"
size="large"
>
<n-form-item path="account">
<n-input v-model:value="formValue.account" placeholder="输入账号">
<n-input
v-model:value="formValue.account"
placeholder="输入账号"
>
<template #prefix>
<i-icon-park-outline-woman />
</template>
</n-input>
</n-form-item>
<n-form-item path="pwd">
<n-input v-model:value="formValue.pwd" type="password" placeholder="输入密码" show-password-on="click">
<n-input
v-model:value="formValue.pwd"
type="password"
placeholder="输入密码"
show-password-on="click"
>
<template #prefix>
<i-icon-park-outline-lock />
</template>
@ -32,7 +62,10 @@
</n-form-item>
<n-form-item path="code">
<n-space align="center">
<n-input v-model:value="formValue.code" placeholder="输入验证码">
<n-input
v-model:value="formValue.code"
placeholder="输入验证码"
>
<template #prefix>
<i-icon-park-outline-message />
</template>
@ -40,12 +73,23 @@
<span>验证码什么的</span>
</n-space>
</n-form-item>
<n-space vertical :size="24">
<n-space
vertical
:size="24"
>
<div class="flex-y-center justify-between">
<n-checkbox>记住我</n-checkbox>
<n-button :text="true">忘记密码</n-button>
<n-button :text="true">
忘记密码
</n-button>
</div>
<n-button w-full type="primary" size="large" :loading="authStore.loginLoading" @click="handleLogin">
<n-button
w-full
type="primary"
size="large"
:loading="authStore.loginLoading"
@click="handleLogin"
>
<template #icon>
<n-icon>
<i-icon-park-outline-six-points />
@ -56,7 +100,7 @@
</n-space>
</n-form>
<n-divider><span op-80>其他登录</span></n-divider>
</div>
</n-el>
</div>
</template>
@ -67,44 +111,44 @@ import { ref } from 'vue';
const authStore = useAuthStore();
const swiperList = [
'https://images.unsplash.com/photo-1546414809-22c82b5e2ad4?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1470&q=80',
'https://images.unsplash.com/photo-1659533982925-09cb4f3f7876?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1473&q=80',
'https://images.unsplash.com/photo-1630771077377-674b39a13f58?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1470&q=80',
'https://images.unsplash.com/photo-1543782248-03e2c5a93e18?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1471&q=80',
'https://images.unsplash.com/photo-1546414809-22c82b5e2ad4?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1470&q=80',
'https://images.unsplash.com/photo-1659533982925-09cb4f3f7876?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1473&q=80',
'https://images.unsplash.com/photo-1630771077377-674b39a13f58?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1470&q=80',
'https://images.unsplash.com/photo-1543782248-03e2c5a93e18?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1471&q=80',
];
const formValue = ref({
account: 'admin',
pwd: '123456',
code: '1234',
account: 'admin',
pwd: '123456',
code: '1234',
});
const rules = {
account: {
required: true,
trigger: 'blur',
message: '请输入账户',
},
pwd: {
required: true,
trigger: 'blur',
message: '请输入密码',
},
code: {
required: true,
trigger: 'blur',
min: 4,
message: '最短长度为 4',
},
account: {
required: true,
trigger: 'blur',
message: '请输入账户',
},
pwd: {
required: true,
trigger: 'blur',
message: '请输入密码',
},
code: {
required: true,
trigger: 'blur',
min: 4,
message: '最短长度为 4',
},
};
const formRef = ref<FormInst | null>(null);
const handleLogin = () => {
formRef.value?.validate((errors) => {
if (errors) return console.error(errors);
formRef.value?.validate((errors) => {
if (errors) return console.error(errors);
const { account, pwd } = formValue.value;
authStore.login(account, pwd);
});
const { account, pwd } = formValue.value;
authStore.login(account, pwd);
});
};
</script>

View File

@ -1,10 +1,31 @@
<template>
<n-space :vertical="true" :size="16">
<n-space
:vertical="true"
:size="16"
>
<n-card>
<div ref="pieRef" class="h-400px"></div>
<div
ref="pieRef"
class="h-400px"
/>
</n-card>
<n-card>
<div ref="lineRef" class="h-400px"></div>
<div
ref="lineRef"
class="h-400px"
/>
</n-card>
<n-card>
<div
ref="barRef"
class="h-400px"
/>
</n-card>
<n-card>
<div
ref="radarRef"
class="h-400px"
/>
</n-card>
</n-space>
</template>
@ -13,26 +34,484 @@
import { ref } from 'vue';
import type { Ref } from 'vue';
import { type ECOption, useEcharts } from '@/hooks';
import { graphic } from 'echarts';
//
const pieOptions = ref<ECOption>({
title: {
text: 'ECharts 入门示例2',
},
tooltip: {},
xAxis: {
data: ['衬衫', '羊毛衫', '雪纺衫', '裤子', '高跟鞋', '袜子'],
},
yAxis: {},
series: [
{
name: '销量',
type: 'bar',
data: [5, 20, 36, 10, 10, 20],
},
],
title: {
text: '饼图',
},
color: [
'#37a2da',
'#32c5e9',
'#9fe6b8',
'#ffdb5c',
'#ff9f7f',
'#fb7293',
'#e7bcf3',
'#8378ea',
],
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b} : {c} ({d}%)',
},
toolbox: {
show: true,
feature: {
mark: { show: true },
dataView: { show: true, readOnly: false },
magicType: {
show: true,
type: ['line'],
},
restore: { show: true },
saveAsImage: { show: true },
},
},
calculable: true,
series: [
{
type: 'pie',
name: '增值电信业务统计表',
radius: [40, 150],
roseType: 'area',
data: [
{ value: 10, name: 'rose1' },
{ value: 5, name: 'rose2' },
{ value: 15, name: 'rose3' },
{ value: 25, name: 'rose4' },
{ value: 20, name: 'rose5' },
{ value: 35, name: 'rose6' },
{ value: 30, name: 'rose7' },
{ value: 40, name: 'rose8' },
],
},
],
}) as Ref<ECOption>;
const { domRef: pieRef } = useEcharts(pieOptions);
const { domRef: lineRef } = useEcharts(pieOptions);
//线
const lineOptions = ref<ECOption>({
title: {
text: '折线图',
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross',
label: {
backgroundColor: '#6a7985',
},
},
},
legend: {
data: ['Email', 'Union Ads', 'Video Ads', 'Direct', 'Search Engine'],
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true,
},
toolbox: {
feature: {
saveAsImage: {},
},
},
xAxis: {
type: 'category',
boundaryGap: false,
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
},
yAxis: {
type: 'value',
},
series: [
{
color: '#37a2da',
name: 'Email',
type: 'line',
smooth: true,
stack: 'Total',
areaStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{
offset: 0.25,
color: '#37a2da',
},
{
offset: 1,
color: '#fff',
},
],
},
},
emphasis: {
focus: 'series',
},
data: [120, 132, 101, 134, 90, 230, 210],
},
{
color: '#9fe6b8',
name: 'Union Ads',
type: 'line',
smooth: true,
stack: 'Total',
areaStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{
offset: 0.25,
color: '#9fe6b8',
},
{
offset: 1,
color: '#fff',
},
],
},
},
emphasis: {
focus: 'series',
},
data: [220, 182, 191, 234, 290, 330, 310],
},
{
color: '#fedb5c',
name: 'Video Ads',
type: 'line',
smooth: true,
stack: 'Total',
areaStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{
offset: 0.25,
color: '#fedb5c',
},
{
offset: 1,
color: '#fff',
},
],
},
},
emphasis: {
focus: 'series',
},
data: [150, 232, 201, 154, 190, 330, 410],
},
{
color: '#fb7293',
name: 'Direct',
type: 'line',
smooth: true,
stack: 'Total',
areaStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{
offset: 0.25,
color: '#fb7293',
},
{
offset: 1,
color: '#fff',
},
],
},
},
emphasis: {
focus: 'series',
},
data: [320, 332, 301, 334, 390, 330, 320],
},
{
color: '#e7bcf3',
name: 'Search Engine',
type: 'line',
smooth: true,
stack: 'Total',
areaStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{
offset: 0.25,
color: '#e7bcf3',
},
{
offset: 1,
color: '#fff',
},
],
},
},
emphasis: {
focus: 'series',
},
data: [820, 932, 901, 934, 1290, 1330, 1320],
},
],
}) as Ref<ECOption>;
const { domRef: lineRef } = useEcharts(lineOptions);
//
const barOptions = ref<ECOption>({
title: {
text: '柱状图',
},
tooltip: {},
grid: {
top: '8%',
left: '1%',
right: '1%',
bottom: '8%',
containLabel: true,
},
legend: {
itemGap: 50,
data: ['注册总量', '最新注册量'],
},
xAxis: [
{
type: 'category',
boundaryGap: true,
axisLine: {
//线x
show: true,
lineStyle: {
color: '#f9f9f9',
},
},
axisLabel: {
//
color: '#d1e6eb',
margin: 15,
},
axisTick: {
show: false,
},
data: ['1月', '2月', '3月', '4月', '5月', '6月', '7月'],
},
],
yAxis: [
{
type: 'value',
min: 0,
// max: 140,
splitNumber: 7,
splitLine: {
show: true,
lineStyle: {
color: '#0a3256',
},
},
axisLine: {
show: false,
},
axisLabel: {
margin: 20,
color: '#d1e6eb',
},
axisTick: {
show: false,
},
},
],
series: [
{
name: '注册总量',
type: 'line',
showAllSymbol: true,
symbol: 'emptyCircle',
symbolSize: 6,
itemStyle: {
color: '#28ffb3', // 线
borderColor: '#f0f',
},
label: {
show: true,
position: 'top',
color: '#f9f9f9',
borderColor: '#fff',
},
tooltip: {
show: false,
},
areaStyle: {
//
//线4x0,y0,x2,y2(0~1);true
color: new graphic.LinearGradient(
0,
0,
0,
1,
[
{
offset: 0,
color: 'rgba(0,154,120,1)',
},
{
offset: 1,
color: 'rgba(0,0,0, 0)',
},
],
false
),
shadowColor: 'rgba(53,142,215, 0.9)', //
shadowBlur: 20, //shadowBlurshadowColor,shadowOffsetX/Y,
},
data: [393, 438, 485, 631, 689, 824, 987],
},
{
name: '最新注册量',
type: 'bar',
barWidth: 20,
tooltip: {
show: false,
},
label: {
show: true,
position: 'top',
color: '#fff',
},
itemStyle: {
color: function (params) {
const colorList = [
'#0ec1ff',
'#10cdff',
'#12daff',
'#15ebff',
'#17f8ff',
'#1cfffb',
'#1dfff1',
];
return colorList[params.dataIndex];
},
},
data: [200, 382, 102, 267, 186, 315, 316],
},
],
}) as Ref<ECOption>;
const { domRef: barRef } = useEcharts(barOptions);
//
const radarOptions = ref<ECOption>({
title: {
text: 'Multiple Radar',
},
tooltip: {},
legend: {
left: 'center',
},
radar: [
{
indicator: [
{ name: '萧塘', max: 100 },
{ name: '环城东路', max: 100 },
{ name: '望园路', max: 100 },
{ name: '奉贤新城', max: 100 },
{ name: '奉浦大道', max: 100 },
{ name: '金海湖', max: 100 },
],
},
],
series: [
{
type: 'radar',
data: [
{
name: '进站',
value: [43, 90, 80, 53, 78, 89, 77, 50],
areaStyle: {
color: {
type: 'radial',
x: 0.5,
y: 0.5,
r: 0.5,
colorStops: [
{
offset: 0,
color: 'rgba(46,203,255, 0.14)', // 0%
},
{
offset: 0.15,
color: 'rgba(46,203,255, 0.14)', // 100%
},
{
offset: 0.75,
color: '#057FB3', // 100%
},
{
offset: 1,
color: '#078DC6', // 100%
},
],
global: false, // false
},
},
},
{
name: '出站',
value: [50, 44, 56, 69, 43, 77, 90, 20],
areaStyle: {
color: {
type: 'radial',
x: 0.5,
y: 0.5,
r: 0.5,
colorStops: [
{
offset: 0,
color: 'rgba(255, 127,0, 0.14)', // 0%
},
{
offset: 0.15,
color: 'rgba(255, 127,0, 0.14)', // 100%
},
{
offset: 0.75,
color: 'rgba(2255, 127,0, 0.4)', // 100%
},
{
offset: 1,
color: 'rgba(255, 127,0, 0.5)', // 100%
},
],
global: false, // false
},
},
},
],
},
],
}) as Ref<ECOption>;
const { domRef: radarRef } = useEcharts(radarOptions);
</script>
<style scoped></style>

View File

@ -30,7 +30,7 @@ export default defineConfig(({ command, mode }: ConfigEnv) => {
server: {
host: '0.0.0.0',
port: 3000,
open: false,
open: true,
proxy: createViteProxy(isOpenProxy, envConfig),
},
preview: {