vue3 子组件不会加载计算数据 属性

vue3 child component won't load with computed data as property

这是我遇到的问题。我有一个潜在客户页面,这是我的 Leads.vue 模板。它加载我的线索,然后通过道具将线索数据传递给其他组件。

LeadSources 组件接收一个计算方法,因为它是 属性。 您可以看到在 Leads.vue 页面上,LeadSources 组件为其 属性 数据调用 getSourceData() 方法。

当我检查 setup()LeadSources.vue 的道具值时, chartData 的值最初是一个空数组。如果页面热重载,那么 LeadSources apexchart 将填充 :series 数据,否则我无法让它工作。

基本上它是这样工作的。

Leads.vuegetSourceData() 传递给 LeadsSources.vue 组件,该组件在 setup() 上将其设置为变量 series 并尝试加载 apexchart 用它。

如果我刷新我的页面它不会工作,但如果我在我的 IDE 中保存一些东西,热重载将加载更新的 apexchart 并显示数据。似乎道具值第一次没有在 setup() 函数中设置。我如何在架构上解决这个问题?设置它的正确方法是什么?我无法判断问题是出在 leads.vue 方面,还是出在 LeadSources.vue 组件的组合方式上。

如有任何帮助,我将不胜感激,我已经花了太长时间试图让它正常工作。

Leads.vue

<template>
  <!--begin::Leads-->
  <div class="row gy-5 g-xl-8 mb-8">
    <div class="col-xxl-12">
      <LeadTracker
        :lead-data="leadData"
        :key="componentKey"
      />
    </div>
  </div>

  <div class="row gy-5 g-xl-8 mb-8">
    <div class="col-xxl-12">
      <LeadSources
        chart-color="primary"
        chart-height="500"
        :chart-data="getSourceData"
        :chart-threshold="sourceThreshold"
        widget-classes="lead-sources"
      ></LeadSources>
    </div>
  </div>
  <!--end::Leads-->
</template>

<script lang="ts">
import { defineComponent, defineAsyncComponent, onMounted } from "vue";
import { setCurrentPageTitle } from "@/core/helpers/breadcrumb";
import LeadSources from "@/components/leads/sources/LeadSources.vue";
import LeadTracker from "@/components/leads/tracker/LeadTracker.vue";
import LeadService from "@/core/services/LeadService";
import ApiService from "@/core/services/ApiService";
import {Lead} from "@/core/interfaces/lead";

export default defineComponent({
  name: "leads",
  components: {
    LeadTracker,
    LeadSources
  },
  data() {
    return {
      leadData: [] as Lead[],
    }
  },
  beforeCreate: async function() {
    this.leadData = await new LeadService().getLeads()
  },
  setup() {
    onMounted(() => {
      setCurrentPageTitle("Lead Activity");
    });

    const sourceThreshold = 5;

    return {
      sourceThreshold,
      componentKey: 0
    };
  },
  computed: {
    getSourceData() {
      interface SingleSource {
        source: string;
        value: number;
      }
      const sourceData: Array<SingleSource> = [];

      // Make array for source names
      const sourceTypes = [];
      this.leadData.filter(lead => {
        if (!sourceTypes.includes(lead.source)) sourceTypes.push(lead.source);
      });

      // Create objects for each form by name, push to leadSourceData
      sourceTypes.filter(type => {
        let totalSourceLeads = 1;
        this.leadData.filter(form => {
          if (form.source == type) totalSourceLeads++;
        });

        const leadSourceData = {
          source: type,
          value: totalSourceLeads
        };
        sourceData.push(leadSourceData);
      });

      // Sort by source popularity
      sourceData.sort(function(a, b) {
        return a.value - b.value;
      });

      return sourceData;
    }
  }
});
</script>

LeadSources.vue

<template>
  <!--begin::Lead Sources Widget-->
  <div :class="widgetClasses" class="card card-footprint">
    <!--begin::Body-->
    <div
      class="card-body p-0 d-flex justify-content-between flex-column overflow-hidden"
    >
      <div class="d-lg-flex flex-stack flex-grow-1 px-9 py-6">
        <!--begin::Text-->
        <div class="d-flex flex-column text-start col-lg-10">
          <span class="card-title">Lead Sources</span>
          <span class="card-description">Where your leads are coming from.</span>

          <!--begin::Chart-->
          <div class="d-flex flex-column">
            <apexchart
              class="mixed-widget-10-chart lead-sources-donut"
              :options="chartOptions"
              :series="series"
              type="donut"
              :height="chartHeight"
              :threshold="chartThreshold"
            ></apexchart>
          </div>
          <!--end::Chart-->
        </div>

        <!--begin::Unused Data-->
        <div class="d-flex flex-row flex-lg-column lg-col-2 justify-content-between unused-data">
          <div class="alt-sources flex-fill">
            <div><span class="alt-header">Other Sources:</span></div>
            <div v-for="item in otherSources" :key="item.source">
              <span>{{ item.source }}</span>
              <span>{{ item.value }}%</span>
            </div>
          </div>
          <div class="alt-sources flex-fill">
            <div><span class="alt-header">Sources Not Utilized:</span></div>
            <div v-for="item in unusedSources" :key="item.source">
              <span>{{ item.source }}</span>
            </div>
          </div>
        </div>
        <!--end::Unused Data-->
      </div>
    </div>
  </div>
  <!--end::Lead Sources Widget-->
</template>

<script lang="ts">
import { defineComponent, ref } from "vue";

export default defineComponent({
  name: "LeadSource",
  props: {
    widgetClasses: String,
    chartColor: String,
    chartHeight: String,
    chartLabels: Array,
    chartData: Array,
    chartThreshold: Number
  },
  setup(props) {
    const sum = (data) => {
      let total = 0;
      data?.map(function(v) {
        total += v;
      });
      return total;
    }

    const chartData = ref(props.chartData).value;

    const threshold = ref(props.chartThreshold).value;

    const usedSourcesLabel: string[] = [];
    const usedSourcesData: number[] = [];
    const otherSources: any = [];
    const unusedSources: any = [];

    const splitData = (data, max) => {
      // set used, other sources < 5%, unused sources
      data.filter((item) => {
        if (item.value > max) {
          usedSourcesLabel.push(item.source);
          usedSourcesData.push(item.value);
        } else if (item.value < max && item.value != 0 && item.value !== null) {
          otherSources.push(item);
        } else if (item.value == 0 || item.value === null) {
          unusedSources.push(item);
        }
      });
    };

    splitData(chartData, threshold);

    const chartOptions = {
      chart: {
        width: 380,
        type: "donut"
      },
      colors: [
        "#1C6767",
        "#CD2E3B",
        "#154D5D",
        "#F1D67E",
        "#4F9E82",
        "#EF8669",
        "#393939",
        "#30AEB4"
      ],
      plotOptions: {
        pie: {
          startAngle: -90,
          endAngle: 270
        }
      },
      dataLabels: {
        enabled: false
      },
      fill: {
        type: "gradient",
        gradient: {
          type: "horizontal",
          shadeIntensity: 0.5,
          opacityFrom: 1,
          opacityTo: 1,
          stops: [0, 100],
        }
      },
      legend: {
        show: true,
        position: "left",
        fontSize: "16px",
        height: 220,
        onItemClick: {
          toggleDataSeries: false
        },
        onItemHover: {
          highlightDataSeries: false
        },
        formatter: function (val, opts) {
          return val + " - " + opts.w.globals.series[opts.seriesIndex];
        }
      },
      title: {
        text: undefined
      },
      tooltip: {
        style: {
          fontSize: "14px"
        },
        marker: {
          show: false
        },
        y: {
          formatter: function(val) {
            return val + "%";
          },
          title: {
            formatter: (seriesName) => seriesName,
          },
        }
      },
      labels: usedSourcesLabel,
      annotations: {
        position: "front",
        yaxis: [{
          label: {
            text: "text annotation"
          }
        }],
        xaxis: [{
          label: {
            text: "text xaxis annotation"
          }
        }],
      },
      responsive: [{
        breakpoint: 480,
        options: {
          legend: {
            position: "bottom",
            horizontalAlign: "left"
          }
        }
      }]
    };
    const series = usedSourcesData;
    return {
      chartOptions,
      series,
      otherSources,
      unusedSources
    };
  }
});
</script>

已编辑

我会附上 LeadService.ts class 和 ApiService.ts class 这样你就可以看到数据来自哪里

LeadService.ts

import ApiService from "@/core/services/ApiService";
import {Lead} from "@/core/interfaces/lead";

export default class LeadService {

  getLeads() {

    const accountInfo = JSON.parse(localStorage.getItem('accountInfo') || '{}');

    ApiService.setHeader();

    return ApiService.query("/leads", {params: {client_id : accountInfo.client_id}})
      .then(({ data }) => {
        let leadData: Lead[] = data['Items'];
        return leadData;
      })
      .catch(({ response }) => {
        return response;
      });

  }
}

ApiService.ts

import { App } from "vue";
import axios from "axios";
import VueAxios from "vue-axios";
import JwtService from "@/core/services/JwtService";
import { AxiosResponse, AxiosRequestConfig } from "axios";
import auth from "@/core/helpers/auth";

/**
 * @description service to call HTTP request via Axios
 */
class ApiService {
  /**
   * @description property to share vue instance
   */
  public static vueInstance: App;

  /**
   * @description initialize vue axios
   */
  public static init(app: App<Element>) {
    ApiService.vueInstance = app;
    ApiService.vueInstance.use(VueAxios, axios);
    ApiService.vueInstance.axios.defaults.baseURL = "https://api.domain.com/";
  }

  /**
   * @description set the default HTTP request headers
   */
  public static setHeader(): void {
    ApiService.vueInstance.axios.defaults.headers.common[
      "Authorization"
    ] = `Bearer ${auth.getSignInUserSession().getIdToken().jwtToken}`;
    ApiService.vueInstance.axios.defaults.headers.common[
      "Content-Type"
    ] = "application/json application/vnd.api+json";
  } 

  /**
   * @description send the GET HTTP request
   * @param resource: string
   * @param params: AxiosRequestConfig
   * @returns Promise<AxiosResponse>
   */
  public static query(
    resource: string,
    params: AxiosRequestConfig
  ): Promise<AxiosResponse> {
    return ApiService.vueInstance.axios.get(resource, params).catch(error => {
      // @TODO log out and send home if response is 401 bad auth
      throw new Error(`[KT] ApiService ${error}`);
    });
  }
}

export default ApiService;

我认为问题是在您从 api 调用数据时引起的。 此代码:

beforeCreate: async function() {
    this.leadData = await new LeadService().getLeads()
},

我会重构为

async created () {
    const service = new LeadService()
    const value = await service.getLeads()
}

另外,如果能够看到您是如何获取数据的,那就太好了。 有时这段代码:const value = await axios.get('/api/getStuff').data 可能会因为异常问题而出现问题。这会导致您描述的相同问题,热重装工作正常,但不新鲜。我怀疑同样的问题依赖于像 => (await new LeadService()).getLeads() 这样的代码执行,你可能正在等待 class,而不是实际的异步代码。