vue 模拟商城 首页动态渲染

新建vue项目

 main.js

import Vue from 'vue'
import App from './App.vue'
import store from "@/store";
import router from "@/router";
import '@/utils/vant-ui';
import '@/styles/common.css'

Vue.config.productionTip = false;

new Vue({
  render: h => h(App),
  router,
  store,
}).$mount('#app');

app.vue

<template>
  <div id="app">
    <router-view></router-view>
  </div>
</template>

<script>

export default {
  name: 'App'
}
</script>

<style>

</style>

search list.vue

<template>
    <div>我是List</div>
</template>

<script>
    export default {
        name: "ListIndex"
    }
</script>

<style scoped>

</style>

search index.vue

<template>
    <div>我是Search</div>
</template>

<script>
    export default {
        name: "SearchIndex"
    }
</script>

<style scoped>

</style>

productDetails index.vue

<template>
    <div>我是ProductDetails</div>
</template>

<script>
    export default {
        name: "ProductDetailsIndex"
    }
</script>

<style scoped>

</style>

pay index.vue

<template>
    <div>我是Pay</div>
</template>

<script>
    export default {
        name: "PayIndex"
    }
</script>

<style scoped>

</style>

myorder index.vue

<template>
    <div>我是MyOrder</div>
</template>

<script>
    export default {
        name: "MyOrderIndex"
    }
</script>

<style scoped>

</style>

login index.vue

<template>
    <div class="login">
        <!-- 头部       -->
        <van-nav-bar
            title="会员登录"
            left-arrow
            @click-left="$router.go(-1)"></van-nav-bar>
        <!--  底部      -->
        <div class="container">
            <div class="title">
                <h3>手机号登录</h3>
                <p>未注册的手机号登录后自动注册</p>
            </div>

            <div class="form">
                <div class="form-item">
                    <input type="text" v-model="mobile" class="inp" placeholder="请输入手机号码" maxlength="11">
                </div>
                <div class="form-item">
                    <input type="text" v-model="picCode" class="inp" placeholder="请输入图形验证码" max="5">
                    <img v-if="picUrl" :src="picUrl" alt="" @click="getPicCode">
                </div>
                <div class="form-item">
                    <input type="text" v-model="smsCode" class="inp" placeholder="请输入短信验证码" >
                    <button @click="handleCode">
                        {{ currentSecond === totalSecond ? '获取验证码' : currentSecond + '秒之后重新发送'}}
                    </button>
                </div>
            </div>

            <div class="login-btn" @click="handleLogin">登录</div>
        </div>
    </div>
</template>

<script>
    //按需导入
    import { getRandomPicCode, getMsgCode, doLogin } from '@/api/login'
    //import { Toast } from 'vant'

    export default {
        name: "LoginIndex",
        data(){
          return {
              picKey:'', //请求后台时的图形验证码唯一标识
              picUrl:'', //存储请求渲染的图片地址,
              totalSecond: 60, //总秒数
              currentSecond: 60, //当前数秒
              timer: null, //定时器
              mobile:'', //手机号
              picCode:'', //用户输入的图形验证码
              smsCode:'' //短信验证码
          }
        },
        created(){
            //页面一加载,就请求验证码
            this.getPicCode()
        },
        methods:{
            //获取图形验证码
            async getPicCode(){
                //展开分解
                const { data:{ base64, key }} = await getRandomPicCode();
                this.picUrl = base64;  //存储地址
                this.picKey = key; // 存储唯一标识
                //Toast('获取图形验证码成功!');
                this.$toast('获取验证码成功');
            },
            //获取短信验证码
            async handleCode(){
                //校验没通过,不发送短信验证码
                if(!this.validFn()){
                    return;
                }

                //当前没有开启定时器,且totalSecond==CurrentSecond时,
                if (!this.timer && this.currentSecond === this.totalSecond){
                    //发送请求,
                    const res = await getMsgCode(this.picCode,this.picKey,this.mobile);
                    //console.log(res);
                    this.$toast('短信发送成功,请注意查收!' + res.message);

                    this.timer = setInterval( ()=>{
                        console.log('正在倒计时...');
                        //开始数秒
                        this.currentSecond--;
                        //如果当前秒 = 0时,关闭清除定时器,恢复当前秒
                        if(this.currentSecond <= 0){
                            clearInterval(this.timer);
                            this.timer = null;
                            this.currentSecond = this.totalSecond;
                        }
                    },1000);
                }
            },
            //校验 手机号和图形验证码是否合法
            validFn(){
                //手机号正则表达式
                if(!(/^1[3-9]\d{9}$/.test(this.mobile))){
                    this.$toast('请输入正确的手机号码');
                    return false;
                }
                //图形验证码正则表达式
                if(!(/^\w{4}$/.test(this.picCode))){
                    this.$toast('请输入正确的图形验证码');
                    return  false;
                }
                return true;
            },
            //登录
            async handleLogin(){
                if(!this.validFn()){
                    return;
                }

                if(!(/^\d{6}$/.test(this.smsCode))){
                    this.$toast('请输入正确的短信验证码');
                    return;
                }

                const res = await doLogin(this.mobile,this.smsCode);
                //console.log(res);
                if(res.status === 200){
                    //存入vuex
                    this.$store.commit('user/setUserInfo',res.data);
                    this.$toast(res.message);
                    await this.$router.push('/');
                }
            }
        },
        destroyed(){
            //离开页面,清除定时器
            clearInterval(this.timer);
        }

    }
</script>

<style scoped>

    .container{
        padding: 49px 29px;
    }

    .container .title{
        margin-bottom: 20px;

    }

    .title h3{
        font-size: 26px;
        font-weight: normal;
    }

    .title p{
        line-height: 40px;
        font-size: 14px;
        color: #b8b8b8;
    }

    .form-item{
        border-bottom: 1px solid #f3f1f2;
        padding: 8px;
        margin-bottom: 14px;
        display: flex;
        align-items: center;

    }

    .form-item .inp{
        display: block;
        border:none;
        outline: none;
        height: 32px;
        font-size: 14px;
        flex: 1;
    }

    .form-item img{
        width: 94px;
        height: 31px;
    }

    .form-item button{
        height: 31px;
        border: none;
        font-size: 13px;
        color: #cea26a;
        background-color: transparent;
        padding-right: 9px;
    }

    .login-btn{
        width: 100%;
        height: 42px;
        margin-top: 39px;
        background: linear-gradient(90deg,#ecb53c,#ff9211,#ff9211);
        color: #ffffff;
        border-radius: 39px;
        box-shadow: 0 10px 20px 0 rgba(0,0,0,.1);
        letter-spacing: 2px;
        display: flex;
        justify-content: center;
        align-items: center;
    }
</style>

layout user.vue

<template>
    <div>我是user</div>
</template>

<script>
    export default {
        name: "UserPage"
    }
</script>

<style scoped>

</style>

layout index.vue

<template>
    <div class="layout-index">
        <!-- 二级路由出口,二级组件展示的位置   -->
        <router-view></router-view>

        <van-tabbar route active-color="#ee0a24" inactive-color="#000">
            <van-tabbar-item to="/home" icon="wap-home-o">首页</van-tabbar-item>
            <van-tabbar-item to="/category" icon="apps-o">分类页</van-tabbar-item>
            <van-tabbar-item to="/cart" icon="shopping-cart-o">购物车</van-tabbar-item>
            <van-tabbar-item to="/user" icon="user-o">我的</van-tabbar-item>
        </van-tabbar>
    </div>
</template>

<script>
    export default {
        name: "LayoutIndex"
    }
</script>

<style scoped>

</style>

layout home.vue

<template>
    <div class="home">
        <!-- 导航条       -->
        <van-nav-bar title="模拟商城" fixed/>


        <!-- 搜索框       -->
        <van-search readonly shape="round" background="#f1f1f2"
                    placeholder="请在此输入搜索关键词" @click="$router.push('/search')" />


        <!-- 轮播图       -->
        <van-swipe class="my-swipe" :autoplay="3000" indicator-color="white">
            <van-swipe-item v-for="item in bannerList" :key="item.imgUrl">
                <img :src="item.imgUrl" alt="">
            </van-swipe-item>
        </van-swipe>

        <!--导航        -->
        <van-grid column-num="5" icon-size="40">
            <van-grid-item v-for="item in navList" :key="item.imgUrl"
            :icon = "item.imgUrl"
            :text="item.text"
            @click="$router.push('/category')"></van-grid-item>
        </van-grid>

        <!-- 主会场       -->
        <div class="main">
            <img src="" alt="">
        </div>

        <!--  猜你喜欢      -->
        <div class="guess">
            <p class="guess-title">--猜你喜欢--</p>
            <div class="goods-list">
                <GoodsItem v-for="item in productList" :key="item.goods_id" :item="item"></GoodsItem>
            </div>
        </div>


    </div>
</template>

<script>
    import GoodsItem from "@/components/GoodsItem";
    import { getHomeData } from '@/api/home'
    export default {
        name: "HomePage",
        data(){
          return{
              bannerList:[], //轮播图
              navList:[], // 导航
              productList:[], //商品
          }
        },
        components:{
            GoodsItem
        },
        async created(){
            const { data:{ pageData }} = await getHomeData();
            console.log(pageData);
            this.bannerList = pageData.items[1].data;
            this.navList = pageData.items[3].data;
            this.productList = pageData.items[6].data;

        }
    }
</script>

<style scoped>

    .home {
        padding-top: 100px;
        padding-bottom: 50px;

    }

    .van-nav-bar{
        z-index: 999;
        background-color: #c21401;

    }

    .home .van-nav-bar__title{
        color: #ffffff;
    }

    .van-search{
        position: fixed;
        width: 100%;
        top: 46px;
        z-index: 999;
    }

    .my-swipe .van-swipe-item{
        height: 185px;
        color: #ffffff;
        font-size: 20px;
        text-align: center;
        background-color: #39a9ed;
    }

    .my-swipe .van-swipe-item img{
        width: 100%;
        height: 185px;
    }


    /*主会场*/
    .main img{
        display: block;
        width: 100%;
    }

    /*猜你喜欢*/
    .guess .guess-title{
        height: 40px;
        line-height: 40px;
        text-align: center;
    }

    /*商品样式*/
    .goods-list{
        background-color: #f6f6f6;
    }
</style>

layout category.vue

<template>
    <div>我是category</div>
</template>

<script>
    export default {
        name: "CategoryPage"
    }
</script>

<style scoped>

</style>

layout cart.vue

<template>
    <div>我是cart</div>
</template>

<script>
    export default {
        name: "CartPage"
    }
</script>

<style scoped>

</style>

vant-ui.js

import Vue from 'vue'

//Vant组件库,全部导入方式
// npm i vant@latest-v2 -s
// import Vant from 'vant'
// import 'vant/lib/index.css'
// Vue.use(Vant);

//按需导入
import {
    Button,
    Tabbar,
    TabbarItem,
    NavBar,
    Toast,
    Search,
    Swipe,
    SwipeItem,
    Grid,
    GridItem
} from 'vant'

//注册使用
Vue.use(Button);
//底部菜单栏
Vue.use(Tabbar);
Vue.use(TabbarItem);
//登录页顶部导航栏
Vue.use(NavBar);
//轻提示
Vue.use(Toast);
//搜索框
Vue.use(Search);
//轮播图
Vue.use(Swipe);
Vue.use(SwipeItem);
//网格
Vue.use(Grid);
Vue.use(GridItem);

storage.js

//约定一个通用的键名
const INFO_KEY = 'xx_shopping_info';
//获取个人登录信息
export const getInfo = () => {
    const defaultObj = { token:'',userId:''};
    const result = localStorage.getItem(INFO_KEY);
    return result ? JSON.parse(result) : defaultObj;
};

//设置个人信息
export const setInfo = (obj) => {
    localStorage.setItem(INFO_KEY,JSON.stringify(obj));
};

//删除个人信息
export const removeInfo = () => {
    localStorage.removeItem(INFO_KEY);
};

request.js

import axios from "axios";
import { Toast } from 'vant'
//创建 axios 实例,将来对创建出来的实例,进行自定义配置
//好处,不会污染原始的axios
const instance = axios.create({
   baseURL:'http://cba.itlike.com/public/index.php?s=/api/',
   timeout:5000
});

//自定义配置 请求-响应 拦截器
//添加请求拦截器
instance.interceptors.request.use(function (config) {
   //在发送请求之前做些什么
    // 开启loading,禁止背景点击,节流处理,防止用户多次无效触发
    Toast.loading({
       message:'加载中...',
       forbidClick:true, //是否禁止点击背景功能
       loadingType: "spinner", //配置loading图标
       duration: 0, //图标不会自动消失
    });
   return config;
},function (error) {
   //对请求错误做些什么
   return Promise.reject(error);
});

//添加响应拦截器
instance.interceptors.response.use(function (response) {
    //2xx 范围内的状态码都会触发该函数
    //对响应数据做点什么
    const res = response.data;
    if(res.status !== 200){
        //提示
        Toast(res.message);
        //抛出一个错误的Promise
        return Promise.reject(res.message);
    }
    else{
        //正确情况下,清除loading效果
        Toast.clear();
    }

    return res;
},function (error) {
    //超出2xx 范围的状态码都会触发该函数
    //对响应错误做点什么
    return Promise.reject(error);
});


//导出配置好的实例
export default instance;

common.js

/*重置默认样式*/
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

/*文字溢出省略号*/
.text-ellipsis-2{
    overflow: hidden;
    -webkit-line-clamp:2;
    text-overflow: ellipsis;
    display: -webkit-box;
    -webkit-box-orient:vertical;
}

/*
添加导航的通用样式
 */
.van-nav-bar .van-nav-bar__arrow{
    color: #333333;
}

store index.js

import Vue from 'vue'
import Vuex from 'vuex'
import user from "@/store/modules/user";

Vue.use(Vuex);

const store = new Vuex.Store({
    getters:{
        //获取tosken
        token(state){
            return state.user.userInfo.token;
        }
    },
    modules:{
        user
    }
});

export default store;

store user.js

import { getInfo, setInfo } from '@/utils/storage'

export default {
    //开启命名空间
    namespaced:true,
    state(){
        return {
            //个人权证相关
            userInfo: getInfo()
        }
    },
    mutations:{
        //所有mutation的参数都是state
        setUserInfo(state,obj){
            state.userInfo = obj;
            setInfo(obj);
        }
    },
    actions:{

    },
    getters:{

    }
}

router index.js

import Vue from 'vue'
import VueRouter from 'vue-router'
import Login from '@/views/login'
import Layout from '@/views/layout'
import MyOrder from '@/views/myorder'
import Pay from '@/views/pay'
import ProductDetails from '@/views/productdetails'
import Search from '@/views/search'
import SearchList from '@/views/search/list'
import Home from "@/views/layout/home";
import Cart from "@/views/layout/cart";
import Category from "@/views/layout/category";
import User from "@/views/layout/user";
import store from "@/store";

Vue.use(VueRouter);

const router = new VueRouter({
    routes:[
        { path:'/login',component:Login},
        {
            path:'/',
            component: Layout,
            redirect:'/home',
            children:[
                { path:'/home',component:Home},
                { path:'/category',component:Category},
                { path:'/cart',component: Cart},
                { path:'/user',component:User}
            ]
        },
        { path:'/search',component:Search},
        { path:'/searchList',component: SearchList},
        { path:'/productDetails/:id',component:ProductDetails},
        { path:'/pay',component:Pay},
        { path:'/myOrder',component:MyOrder}
    ]
});

//全局前置导航守卫
// 所有的路径在其被真正访问之前,解析渲染对应的组件页面前,
// 都会完整经过全部前置守卫,只有全局前置守卫放行了,
//才会到达对应的页面
// to 到哪里去
// from 从哪里来得
// next 是否放行 next()直接放行,next(路径),进行拦截
//定义一个数组,专用存放用户需要拦截的页面路径
const authUrl = ['/pay','/myOrder'];

router.beforeEach((to, from, next) => {
    //看to.path 是否在authUrl中出现
    if(!authUrl.includes(to.path)){
        //非权限页面,无须登录,直接放行
        next();
        return;
    }
    //是权限页面,需要判断token
    const token = store.getters.token;
    if(token){
        next();
    }
    else{
        next('/login');
    }
});

export default router;import Vue from 'vue'
import VueRouter from 'vue-router'
import Login from '@/views/login'
import Layout from '@/views/layout'
import MyOrder from '@/views/myorder'
import Pay from '@/views/pay'
import ProductDetails from '@/views/productdetails'
import Search from '@/views/search'
import SearchList from '@/views/search/list'
import Home from "@/views/layout/home";
import Cart from "@/views/layout/cart";
import Category from "@/views/layout/category";
import User from "@/views/layout/user";
import store from "@/store";

Vue.use(VueRouter);

const router = new VueRouter({
    routes:[
        { path:'/login',component:Login},
        {
            path:'/',
            component: Layout,
            redirect:'/home',
            children:[
                { path:'/home',component:Home},
                { path:'/category',component:Category},
                { path:'/cart',component: Cart},
                { path:'/user',component:User}
            ]
        },
        { path:'/search',component:Search},
        { path:'/searchList',component: SearchList},
        { path:'/productDetails/:id',component:ProductDetails},
        { path:'/pay',component:Pay},
        { path:'/myOrder',component:MyOrder}
    ]
});

//全局前置导航守卫
// 所有的路径在其被真正访问之前,解析渲染对应的组件页面前,
// 都会完整经过全部前置守卫,只有全局前置守卫放行了,
//才会到达对应的页面
// to 到哪里去
// from 从哪里来得
// next 是否放行 next()直接放行,next(路径),进行拦截
//定义一个数组,专用存放用户需要拦截的页面路径
const authUrl = ['/pay','/myOrder'];

router.beforeEach((to, from, next) => {
    //看to.path 是否在authUrl中出现
    if(!authUrl.includes(to.path)){
        //非权限页面,无须登录,直接放行
        next();
        return;
    }
    //是权限页面,需要判断token
    const token = store.getters.token;
    if(token){
        next();
    }
    else{
        next('/login');
    }
});

export default router;

goodsitem.vue

<template>

    <div class="goods-item" v-if="item.goods_id" @click="$router.push(`/productDetails/${item.goods_id}`)">
        <div class="left">
            <img :src="item.goods_image" alt="">
        </div>
        <div class="right">
            <p class="tit text-ellipsis-2">
                {{ item.goods_name }}
            </p>
            <p class="count">{{ item.goods_sales }}</p>
            <p class="price">
                <span class="new">¥{{ item.goods_price_min }}</span>
                <span class="old">¥{{ item.goods_price_max }}</span>
            </p>
        </div>
    </div>
</template>

<script>
    export default {
        name: "GoodsItem",
        props:{
            item:{
                type: Object,
                //默认值
                default: () => {
                    return {}
                }
            }
        }
    }
</script>

<style scoped>
    .goods-item{
        height: 148px;
        margin-bottom: 6px;
        padding: 10px;
        background-color: #ffffff;
        display: flex;
    }

    .goods-item .left{
        width: 127px;

    }

    .left img{
        display: block;
        width: 100%;
    }

    .goods-item .right{
        flex: 1;
        font-size: 14px;
        line-height: 1.3;
        padding: 10px;
        display: flex;
        flex-direction: column;
        justify-content: space-evenly;
    }

    .right .count{
        color: #999999;
        font-size: 12px;
    }

    .right .price{
        color: #999999;
        font-size: 16px;
    }

    .right .new{
        color: #f03c3c;
        margin-right: 10px;
    }

    .right .old{
        text-decoration: line-through;
        font-size: 12px;
    }
</style>

api login.js

//用于存放所有登录有关的请求接口
import request from '@/utils/request'

//1 获取图形验证码
 export const getRandomPicCode = () =>{
    return request.get('/captcha/image');
 };

//2 获取短信验证码
export const getMsgCode = (captchaCode,captchaKey,mobile) => {
  return request.post('/captcha/sendSmsCaptcha',{
      form:{
          captchaCode, //输入的验证码
          captchaKey, //
          mobile //手机号
      }
  });
};

//3. 登录接口
export const doLogin = (mobile,smsCode) => {
    return request.post('/passport/login',{
        form:{
            isParty:false, //是否第三方登录
            partyData:{}, //第三方的登录信息
            mobile, //手机号
            smsCode // 短信验证码
        }
    })
};

api home.js

import  request from '@/utils/request'

//获取首页数据
export const getHomeData = () => {
    return request.get('/page/detail',{
        params:{
            pageId:0
        }
    })
};


<think>好的,我现在需要帮助用户解决在Vue3中获取数据并动态渲染ECharts图表的问题。首先,我得回忆一下Vue3和ECharts结合使用的基本步骤,同时结合用户提供的引用内容来确保答案的准确性。 用户的问题是关于动态加载数据和渲染图表,所以首先应该考虑如何在Vue3组件中获取数据,通常是异步获取,比如从API获取数据。然后,数据获取之后如何更新ECharts的配置项,并重新渲染图表。同时,用户提供的引用中有几个关键点需要注意: 引用[3]提到在mounted钩子中使用定时器延迟初始化ECharts,可能是为了解决DOM未渲染完成的问题。但更好的做法可能是在nextTick中处理,或者确保DOM已经挂载后再初始化。 引用[5]指出在动态渲染地图时,this的指向问题,需要使用that来保存this的引用,避免在嵌套函数中丢失上下文。这在Vue3的setup语法中使用可能有所不同,因为Vue3的组合式API更推荐使用ref和reactive来管理状态,可能不需要用that,但需要注意作用域。 引用[4]提到了全局引入和按需引入ECharts的方式,用户可能需要按需引入以减少包体积,这里需要给出两种方式的示例,但用户的问题更关注动态渲染,所以可能需要侧重于数据获取后的配置更新。 接下来,步骤应该是: 1. 安装ECharts:用户需要先安装ECharts包,可能还需要安装Vue相关的集成库,但根据引用中的示例,用户可能更倾向于直接使用echarts.init方法。 2.Vue3组件中引入ECharts:按需引入还是全局引入,这里可以引用[4]中的方法,给出两种方式的示例,但用户的问题可能更适合按需引入,因为动态加载数据通常希望保持轻量。 3. 组件的挂载和初始化:在onMounted生命周期钩子中初始化图表,但需要注意DOM元素的引用是否正确。例如,使用ref来获取DOM节点,可能需要使用Vue3的ref和模板引用来获取DOM元素。 4. 获取数据并更新图表:使用异步方法(如axios或fetch)获取数据,然后在数据返回后更新图表的配置项,并调用setOption方法重新渲染图表。这里需要注意在数据加载完成前可能显示空白或加载状态,以及可能的错误处理。 5. 响应式更新:如果数据变化需要重新渲染图表,可能需要使用watch或watchEffect来监听数据变化,并触发图表的重新渲染。此时需要注意销毁旧实例,避免内存泄漏,或者在更新时正确清除旧图表。 6. 销毁图表实例:在组件卸载时,使用dispose方法销毁ECharts实例,防止内存泄漏。 此外,引用[5]提到动态渲染地图时可能需要处理SVG地图数据,这里可能需要额外的步骤来注册地图数据,例如使用echarts.registerMap方法。但用户的问题可能更关注通用图表,如柱状图或折线图,所以可能需要分情况说明。 另外,引用[3]中的定时器方法可能不是最佳实践,应该推荐使用nextTick或在确保DOM可用的情况下初始化图表,避免不可预测的延迟问题。 现在需要将这些步骤整理成一个清晰的示例,包括代码结构和关键点说明。例如,使用setup语法,在onMounted中初始化图表,使用async/await获取数据,然后更新配置。同时处理窗口resize事件,以确保图表自适应大小。 还需要考虑错误处理,比如数据获取失败时的反馈,以及ECharts实例是否已经创建的判断,避免重复初始化。 最后,生成相关问题,可能需要涉及性能优化、响应式数据处理、图表类型选择等。</think>### 在 Vue3 中获取数据并动态渲染 ECharts 图表的实现步骤 #### 1. 安装 ECharts 依赖 ```bash npm install echarts --save ``` #### 2. 创建基础组件结构 ```vue <template> <div ref="chartRef" style="width: 600px; height: 400px"></div> </template> <script setup> import { ref, onMounted, onBeforeUnmount } from 'vue'; import * as echarts from 'echarts'; const chartRef = ref(null); // 获取 DOM 引用 let chartInstance = null; // 存储 ECharts 实例 // 初始化图表 const initChart = async () => { if (!chartRef.value) return; // 异步获取数据 const data = await fetchData(); // 动态配置项 const option = { xAxis: { type: 'category', data: data.categories }, yAxis: { type: 'value' }, series: [{ type: 'bar', data: data.values }] }; // 首次渲染或更新 if (!chartInstance) { chartInstance = echarts.init(chartRef.value); } chartInstance.setOption(option); }; // 模拟 API 请求 const fetchData = () => { return new Promise(resolve => { setTimeout(() => { resolve({ categories: ['A', 'B', 'C', 'D'], values: [120, 200, 150, 80] }); }, 1000); }); }; // 窗口自适应 const handleResize = () => { chartInstance?.resize(); }; onMounted(() => { initChart(); window.addEventListener('resize', handleResize); }); onBeforeUnmount(() => { window.removeEventListener('resize', handleResize); chartInstance?.dispose(); // 销毁实例[^3] }); </script> ``` #### 关键实现细节: 1. **DOM 引用处理** - 使用 `ref` 绑定容器元素,确保 DOM 已渲染后初始化图表[^3] - 避免直接使用 `setTimeout` 延迟,推荐在 `onMounted` 生命周期操作 2. **数据动态更新** - 通过 `async/await` 实现异步数据加载 - 数据变化时调用 `setOption` 更新图表(可结合 `watch` 监听数据变化) 3. **性能优化** - 添加窗口 `resize` 监听实现自适应[^3] - 组件卸载时销毁实例避免内存泄漏[^3] 4. **错误处理扩展** ```javascript const initChart = async () => { try { // ...原有逻辑 } catch (error) { console.error('图表初始化失败:', error); } }; ``` #### 示例效果 - 初始显示空白容器 - 1秒后动态渲染带数据的柱状图 - 窗口缩放时自动调整图表尺寸
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

虾米大王

有你的支持,我会更有动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值