You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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运行正常,说明组件内部逻辑无语法错误,差异点是父组件传参时机不同:当前组件被挂载渲染时,父组件传入的department prop还未完成异步赋值,初始值为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
相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.30 14:01:04