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.vue
将 getSourceData()
传递给 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,而不是实际的异步代码。
这是我遇到的问题。我有一个潜在客户页面,这是我的 Leads.vue
模板。它加载我的线索,然后通过道具将线索数据传递给其他组件。
LeadSources
组件接收一个计算方法,因为它是 属性。
您可以看到在 Leads.vue
页面上,LeadSources
组件为其 属性 数据调用 getSourceData()
方法。
当我检查 setup()
中 LeadSources.vue
的道具值时, chartData
的值最初是一个空数组。如果页面热重载,那么 LeadSources
apexchart 将填充 :series
数据,否则我无法让它工作。
基本上它是这样工作的。
Leads.vue
将 getSourceData()
传递给 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,而不是实际的异步代码。