VueJS mounted钩子TypeError: 无法读取undefined的id属性
Vue mounted阶段报错
Cannot read properties of undefined (reading 'id')排查解决 问题背景
在Vue项目中编写team-planning.vue组件(仅展示script部分逻辑),组件运行时控制台抛出mounted钩子执行错误;将相同逻辑复用到department-list.vue组件时可正常运行,无任何报错。
报错信息
Error in mounted hook: "TypeError: Cannot read properties of undefined (reading 'id')"
错误定位路径:resources/js/components/dashboard/corporate/department/department-team-planning/department-team-planning.vue
问题组件代码
<script> // Mixins import Actionable from '@/Mixins/Actionable.js'; import HasToastrMessage from "@/Mixins/HasToastrMessage"; import Authorization from "@/Mixins/Authorization"; import Pagination from "@/Mixins/Pagination"; // Helper import Helper from '@/Support/Helper.js'; import CsCard from "@/components/reusable/csCard"; import Datatable from "@/components/reusable/datatable/Datatable"; import CountryService from "@/Services/Country"; import CsMultipleSelect from "@/components/reusable/dropdowns/cs-multiple-select"; import Status from "@/components/dashboard/sharedComponents/status"; import ContactDetails from "@/components/dashboard/corporate/department/vo-departments/modals/contact-details"; import CsButton from "@/components/reusable/cs-button"; import InviteEmployeesToDepartment from "@/components/dashboard/corporate/department/vo-departments/modals/invite-employees-to-department"; import AddEmployeeModal from "@/components/dashboard/corporate/employee/project-employees/add-employee-modal"; import DatatableActionbarItem from "@/components/reusable/datatable/DatatableActionbarItem"; import OpenOverlay from "@/components/overlay/open-overlay"; import CsConfirmationModal from "@/components/dashboard/reusable-modals/cs-confirmation-modal"; export default { components: { CsConfirmationModal, OpenOverlay, DatatableActionbarItem, AddEmployeeModal, InviteEmployeesToDepartment, CsButton, ContactDetails, Status, CsMultipleSelect, Datatable, CsCard }, mixins: [Actionable, HasToastrMessage, Authorization, Pagination], props: ['companyUuid', 'department'], data() { return { data: collect([]), departmentEmployees: collect([]), certificateStatusses: collect([]), activeEmployeesFilterOptions: [{ active: false, label: 'Not part of department', }, { active: true, label: 'Part of department' }], initialSelectionOfEmployees: collect([]), headers: [ {text: 'Name', value: 'name', classList: 'w-2/6'}, {text: 'Job title', value: 'job_title', classList: 'w-1/6'}, {text: 'country', value: 'country'}, {text: 'Qualification Status', value: 'certificate_status'}, {text: 'Matrix Status', value: 'matrix_status'}, ], totalRecords: 0, certificateStatus: ['valid', 'expired', 'expire soon'], certificateStatusFilter: [], matrixStatus: [ {value: 'compliant', label: 'compliant'}, {value: 'non-compliant', label: 'non-compliant'} ], matrixStatusFilter: [], nationalitiesFilter: [], countries: collect([]), employeeType:[ {value: '0', label: 'Employee'}, {value: '1', label: 'Subcontractor'} ], selectedEmployeeTypes:[], selectedEmployee: null, userToShowContactDetails: null } }, watch: { dataset() { this.totalRecords = this.dataset.get('total'); }, companyEmployees() { this.employees = this.companyEmployees; if (Helper.isset(this.data) && Helper.isset(this.employees)) { this.extractNationality(this.employees); this.extractCertificateStatusses(this.data); this.noData = false; } }, }, mounted() { this.loadDepartmentEmployees(); eventsHub.$on('filtering:done', () => { eventsHub.$emit('pagination:reset'); }); eventsHub.$on('overlay:isMounted:departmentEmployeeContactDetails', () => { eventsHub.$emit('overlay:open:departmentEmployeeContactDetails'); }); eventsHub.$on('overlay:close:departmentEmployeeContactDetails', () => { this.userToShowContactDetails = null }); eventsHub.$on('overlay:isMounted:statusSummaryComponent', () => { eventsHub.$emit('overlay:open:statusSummaryComponent'); }); eventsHub.$on('overlay:close:statusSummaryComponent', () => { this.selectedEmployee = null }); this.getCountries(); }, computed: { dataset() { return this.$store.getters.getDepartmentEmployees; }, userMadeChangesToDepartment() { let itemWasAdded = this.selectedForAction.diff(this.initialSelectionOfEmployees).count() > 0; let itemWasRemoved = this.initialSelectionOfEmployees.diff(this.selectedForAction).count() > 0; return itemWasAdded || itemWasRemoved; }, userSuggestionApiURL() { console.log(this.department.id); return `json/companies/${this.companyUuid}/projects/0/departments/${this.department.id}/suggested-users`; }, }, methods: { async loadDepartmentEmployees() { this.isLoading = true; await this.$store.dispatch('LoadDepartmentEmployees', { companyUuid: this.companyUuid, departmentId: this.department.id, nationalities: this.nationalitiesFilter, matrixStatus: this.matrixStatusFilter, certificateStatus: this.certificateStatusFilter, employeeType: this.selectedEmployeeTypes, ...this.datatablePayload }).finally(() => this.isLoading = false); }, extractCertificateStatusses(employees) { this.certificateStatusses = employees.pluck('certificates').flatten(1).map((certificate) => { return certificate.pivot; }).unique('status').all(); }, getValidityStatus(certificates) { const {missing, expired, expire_soon, active} = this.getCertificateStatus(certificates); // Removed as FBE-1833 said // if (missing.count() > 0) // return `${missing.count()} missing`; if (certificates.length === 0) return `empty`; else if (expired.count() > 0) return `${expired.count()} expired`; else if (expire_soon.count() > 0) return `${expire_soon.count()} expire soon`; else if (active.count() === certificates.length && certificates.length > 0) return 'all valid'; return '-'; }, getValidityStatusClass(certificates) { const {missing, expired, expire_soon, active} = this.getCertificateStatus(certificates); if (missing.count() > 0 || expired.count() > 0) return "badge-danger"; if (expire_soon.count() > 0) return 'badge-warning' if (certificates.length === 0) return 'badge-secondary' if (active.count() === certificates.length && certificates.length > 0) return "badge-success"; if (missing.count() === 0) return "badge-success"; return "badge-secondary"; }, getCertificateStatus(certificates) { const missing = collect(certificates).filter(certificate => certificate.matrix_status === 0 && certificate.mandatory); const expired = collect(certificates).filter(certificate => certificate.matrix_status && certificate.expired); const expire_soon = collect(certificates).filter(certificate => certificate.matrix_status && certificate.expire_soon); const active = collect(certificates).filter(certificate => certificate.matrix_status && !certificate.expire_soon && !certificate.expired); return {missing, expired, expire_soon, active}; }, getMatrixStatus(certificates) { const {missing, active, expire_soon, expired} = this.getCertificateStatus(certificates); if (missing.count() > 0 || expire_soon.count() > 0 || expired.count() > 0) return 'non compliant'; if (active.count() === certificates.length && certificates.length > 0) return 'compliant'; if (missing.count() === 0) return 'compliant'; return '-'; }, getMatrixStatusClass(certificates) { const {missing, expired, expire_soon, active} = this.getCertificateStatus(certificates); if (missing.count() > 0 || expire_soon.count() > 0 || expired.count() > 0) return "badge-danger"; if (active.count() === certificates.length && certificates.length > 0) return "badge-success"; if (missing.count() === 0) return "badge-success"; return "badge-secondary"; }, extractNationality(employees) { this.nationalities = employees .pluck('country') .reject((country) => { return country === null; }) .unique('country_code'); }, setDisplayData(data) { this.displayData = data; }, addRoute() { return "#"; }, updateData(data) { this.employees = collect(data); }, queryEmployeesForNationality(employee, value) { if (!Helper.isset(employee.country_code)) { return false; } return employee.country_code === value; }, queryEmployeesForCertificateStatus(employee, value) { return collect(employee.certificates).map((certificate) => { return certificate.pivot; }).contains('status', value); }, queryEmployeesForActiveStatus(employee, employeeShouldBePartOfDepartment) { if (employeeShouldBePartOfDepartment === 'true') { return this.selectedForAction.contains(employee.id); } else if (employeeShouldBePartOfDepartment === 'false') { return !this.selectedForAction.contains(employee.id); } }, saveDepartment() { this.$store.dispatch('updateDepartment', { companyUuid: this.companyUuid, departmentId: this.department.id, users: this.selectedForAction.all(), }).then(() => { this.showToastrSuccessMessage("Department successfully updated."); }); }, handleRowClick(event, item) { let element = event.target; // Handle all exceptions here if (element.classList.contains('selectable--checkbox')) { return false; } // do nothing, the selectable handles the functionality if (element.classList.contains('status--summary--component')) { return false; } // do nothing, the selectable handles the functionality // The default action should be handled below. window.location.href = `/${Helper.getLocale()}/dashboard/companies/${this.companyUuid}/users/${item.unique_id}/certificates`; }, redirectToAssignation() { return eventsHub.$emit('overlay:open:addEmployeeModal'); }, async getCountries() { await CountryService.get().then(({data}) => this.countries = data); }, openCertificatesModal(item) { this.selectedEmployee = item; eventsHub.$emit(`overlay:open:statusSummaryComponent`); }, openContactDetailsModal(item) { this.userToShowContactDetails = item eventsHub.$emit(`overlay:open:departmentEmployeeContactDetails`); }, employeeDetailRoute(employee) { return `/${Helper.getLocale()}/dashboard/companies/${this.companyUuid}/employees/${employee.id}/certificates`; }, /** * Assign Employee to Department with Selected JobTitle * @returns {Promise<AxiosResponse<any>>} * @param user */ addEmployeeToDepartment(user) { const data = { company_uuid: this.companyUuid, users: user, job_title: this.selectedJob?.id } return axios.post(`/${Helper.getLocale()}/dashboard/companies/${this.companyUuid}/users/departments/${this.department.id}`, data) .then(({data}) => { if (Helper.isset(data.success)) { this.showToastrSuccessMessage(data.success); this.$emit('resetSuggestedUser', user.id); } } ); }, /** * Remove Assigned Employee from Department * @returns {Promise<AxiosResponse<any>>} */ handleRemoveEmployee() { const payload = {users: this.selectedForAction.all(), _method: 'delete'}; return axios .post(`/${Helper.getLocale()}/dashboard/companies/${this.companyUuid}/departments/${this.department.id}/users`, payload) .then(({data}) => { if (Helper.isset(data.success)) { this.loadDepartmentEmployees(); this.showToastrSuccessMessage(data.success); this.selectedForAction.each(user => this.$emit('resetSuggestedUser', user.id)); } } ); }, /** * * @param inviteEmails * @param loader * @param cb */ handleInvitedEmployees(inviteEmails, loader, cb) { //Processing loader.toggle(); const data = { company_uuid: this.companyUuid, department: {id: this.department.id}, employeesEmails: inviteEmails, job_title: this.selectedJob?.id } this.$store.dispatch('inviteEmployeesToDepartment', data).then((data) => { cb(data); }).catch((error) => { this.errorMessages = collect([]); this.$nextTick(() => { let responseErrors = collect(error.response.data.errors); this.errorMessages = responseErrors.flatten(); }); }).finally(() => loader.toggle()); }, } } </script>
根因定位
- 报错本质是读取
undefined值的id属性,mounted钩子执行时第一行调用loadDepartmentEmployees(),方法内部直接访问this.department.id;同时组件初始化阶段会执行计算属性userSuggestionApiURL,内部也直接读取this.department.id。 - 相同逻辑在
department-list.vue运行正常,说明组件内部逻辑无语法错误,差异点是父组件传参时机不同:当前组件被挂载渲染时,父组件传入的departmentprop还未完成异步赋值,初始值为undefined,直接读取属性触发报错;而department-list.vue渲染时已经拿到完整的department对象,因此无异常。 - 组件内
saveDepartment、addEmployeeToDepartment、handleRemoveEmployee、handleInvitedEmployees等方法也存在直接读取this.department.id的逻辑,如果不加防护,后续department未就绪时触发这些方法同样会报同类错误。
修复方案
可任选一种或组合使用,从根源避免异步传参导致的属性读取错误:
- 方案1:Props增加默认值与类型校验,从源头避免初始值为undefined
props: { companyUuid: { type: String, required: true }, department: { type: Object, // 初始化时给带默认id的空对象,避免读取id时报错 default: () => ({id: null}) } }, - 方案2:父组件增加渲染判断,等department数据加载完成后再渲染当前组件
<!-- 父组件调用处 --> <team-planning v-if="currentDepartment && currentDepartment.id" :company-uuid="companyUuid" :department="currentDepartment" /> - 方案3:组件内部所有读取
department.id的位置增加空值防护,数据未就绪时不执行业务逻辑
比如修改loadDepartmentEmployees方法:
同步修改async loadDepartmentEmployees() { // department数据未就绪时直接返回,不发起请求 if (!this.department?.id) return this.isLoading = true; await this.$store.dispatch('LoadDepartmentEmployees', { companyUuid: this.companyUuid, departmentId: this.department.id, nationalities: this.nationalitiesFilter, matrixStatus: this.matrixStatusFilter, certificateStatus: this.certificateStatusFilter, employeeType: this.selectedEmployeeTypes, ...this.datatablePayload }).finally(() => this.isLoading = false); }userSuggestionApiURL计算属性:userSuggestionApiURL() { if (!this.department?.id) return '' return `json/companies/${this.companyUuid}/projects/0/departments/${this.department.id}/suggest
相关产品推荐
相关产品推荐

