检测到重复键:'X'。这可能会导致更新错误。在 VueJS 数据表搜索中

Duplicate keys detected: 'X'. This may cause an update error. in VueJS datatable search

在我的 laravel vue 应用程序中,我在 vue 组件 (department-user-list.vue) 中有以下数据表和一些过滤器。

<template>
    <div>
        <cs-card
            :cardButton="false"
            :title="`Team members`"
        >
            <template slot="header-action">
                <div class="inline-block mr-4" direction-from="top">
                    <open-overlay identifier="corporateInviteEmployeeToDepartmentModal">
                        <cs-button size="small" variant="secondary">
                            Invite team member
                        </cs-button>
                    </open-overlay>
                </div>
                <div class="inline-block" direction-from="top">
                    <cs-button @click="redirectToAssignation" size="small">
                        Assign team member
                    </cs-button>
                </div>
            </template>
            <Datatable
                v-model="selectedForAction"
                :data="dataset"
                :headers="headers"
                :is-loading="isLoading"
                :has-action-bar-column="true"
                :key-id="`id`"
                :search-panel="{placeholder: 'Search team member...'}"
                @change="handlePaginationChange"
                @paginate="loadDepartmentEmployees"
            >
                <!--Filter Slot-->
                <template v-slot:filters>
                    <!--Nationality filter-->
                    <div class="w-2/12 pl-4 h-auto">
                        <cs-multiple-select
                            v-model="nationalitiesFilter"
                            :options="countries"
                            key-id="id"
                            label="name"
                            name="nationality"
                            placeholder="Nationality"
                        >
                        </cs-multiple-select>
                    </div>
                    <!--Certificate Status filter-->
                    <div class="w-6/12 pl-4 h-auto">
                        <cs-multiple-select
                            v-model="certificateStatusFilter"
                            :options="certificateStatus"
                            name="certificateStatusFilter"
                            placeholder="Qualification status"
                            @input="loadDepartmentEmployees"
                        />
                    </div>
                    <!--Matrix Status filter-->
                    <div class="w-4/12 pl-4 h-auto">
                        <cs-multiple-select
                            v-model="matrixStatusFilter"
                            :options="matrixStatus"
                            key-id="value"
                            label="label"
                            name="matrixStatusFilter"
                            placeholder="Matrix status"
                            @input="loadDepartmentEmployees"
                        />
                    </div>
                     <!--Employee Type  filter-->
                        <div class="w-4/12 pl-4 h-auto">
                            <cs-multiple-select
                                v-model="selectedEmployeeTypes"
                                :options="employeeType"
                                key-id="value"
                                label="label"
                                name="selectedEmployeeTypes"
                                placeholder="Employee type"
                                @input="loadDepartmentEmployees"
                            >
                            </cs-multiple-select>
                        </div>
                </template>

                <!--Table Header-->
                <template v-slot:header.country="{ header}">
                    <span class="material-icons-outlined">language</span>
                </template>

                <!--Table Body-->
                <template v-slot:item.name="{ item }">
                    <div class="flex items-center cursor-pointer">
                        <div class="rounded-full w-8 h-8 mr-4 overflow-hidden">
                            <img :src="item.profile_image[0].url" alt=""
                                 class="w-full h-full object-cover">
                        </div>
                        <a :href="employeeDetailRoute(item)">
                            <span class="text-certstyle-titles font-bold leading-loose">{{ item.name }}</span>
                        </a>
                        <span
                            v-if="item.is_subcontractor && item.company_name"
                            :title="item.company_name"
                            class="text-certstyle-text-light bg-certstyle-background flex font-semibold cursor-help  text-xs rounded mx-2 py-1 px-2"
                        >
                            {{ item.company_name.substring(0,10) }}
                             <span v-if="item.company_name.length > 10">...</span>
                        </span>
                    </div>
                </template>
                <template v-slot:item.job_title="{ item }">
                    {{ item.current_jobtitle || item.department_jobtitle }}
                </template>
                <template v-slot:item.country="{ item }">
                <span v-if="item.country" class="font-normal">
                    <country-flag width="w-5" :country-code="item.country.country_code"></country-flag>
                </span>
                </template>
                <template v-slot:item.certificate_status="{ item }">
                    <div class="status--summary--component inline-block mr-2 relative"
                         @click="getValidityStatus(item.certificate_matrix) !== 'all valid' &&
                            getValidityStatus(item.certificate_matrix) !== '-'
                             ? openCertificatesModal(item) : null
                         ">
                        <label :class="getValidityStatusClass(item.certificate_matrix)" class="badge">
                            {{ getValidityStatus(item.certificate_matrix) }}
                        </label>
                    </div>
                </template>
                <template v-slot:item.matrix_status="{ item }">
                    <div class="status--summary--component inline-block mr-2 relative"
                         @click="getMatrixStatus(item.certificate_matrix) === 'non compliant'
                            ? openCertificatesModal(item)
                            : null
                         ">
                        <label :class="getMatrixStatusClass(item.certificate_matrix)" class="badge">
                            {{ getMatrixStatus(item.certificate_matrix) }}
                        </label>
                    </div>
                </template>
                <template v-slot:actionItems="slotProps">
                    <DatatableActionbarItem
                        :slot-props="slotProps"
                        identifier="removeConfirmationModal"
                        label="Remove"
                        variant="danger"
                    />
                </template>
                <template v-slot:rowActionItems="slotProps">
                    <DatatableActionbarItem
                        icon=""
                        label="Contact details"
                        @click.native="openContactDetailsModal(slotProps.item)"
                        :slot-props="slotProps"
                    />
                </template>
            </Datatable>


            <modal-md
                v-if="selectedEmployee !== null"
                :options="{ useDefaultContentStyling: false }"
                      :identifier="`statusSummaryComponent`">
                <template slot="modal_title">
                    Qualifications
                    <span v-if="selectedEmployee !== null && selectedEmployee !== undefined">
                            {{ selectedEmployee.name }}
                        </span>
                </template>


                <div class="bg-white" slot="modal_content">
                    <div
                        class="flex items-center justify-between text-certstyle-text-light bg-white border-certstyle-border border-b text-sm py-2 px-10">
                        <cs-dashboard-table-header-item-unstyled :item="`statusSummaryCertificateTitle`">
                            <p>Qualification</p>
                        </cs-dashboard-table-header-item-unstyled>

                        <cs-dashboard-table-header-item-unstyled :item="`statusSummaryCertificateStatus`">
                            <p>Validity Status</p>
                        </cs-dashboard-table-header-item-unstyled>

                        <cs-dashboard-table-header-item-unstyled :item="`statusSummaryMatrixStatus`">
                            <p>Matrix Status</p>
                        </cs-dashboard-table-header-item-unstyled>

                    </div>

                    <!--Certificates-->
                    <div class="text-certstyle-titles">
                        <div v-for="certificate in selectedEmployee.certificate_matrix"
                             v-if="selectedEmployee.certificate_matrix.length > 0"
                             class="table--row flex items-center justify-between border-certstyle-border last:border-b-0 border-b px-10 py-4">
                            <!-- Title -->
                            <cs-dashboard-table-item-unstyled
                                class="w-1/2"
                                :item="`statusSummaryCertificateTitle`">
                                <p :title="certificate.title" class=" ">
                                    {{ certificate.title | limitString(30, '...') }}
                                </p>
                            </cs-dashboard-table-item-unstyled>

                            <cs-dashboard-table-item-unstyled class="w-32" :item="`statusSummaryCertificateStatus`">
                                <div class="flex items-center justify-start w-full">
                                    <!--Expired styling-->
                                    <label v-if="certificate.matrix_status === 0"
                                           class="badge badge-danger">
                                        -
                                    </label>

                                    <!--Expires styling-->
                                    <label v-if="certificate.matrix_status &&  certificate.expired === 1"
                                           class="badge badge-danger">
                                        expired
                                    </label>
                                    <!--Expires styling-->
                                    <label v-if="certificate.matrix_status &&  certificate.expire_soon === 1"
                                           class="badge badge-danger">
                                        expires soon
                                    </label>

                                    <!--Active styling-->
                                    <label
                                        v-if="certificate.matrix_status &&  certificate.expire_soon === 0 && certificate.expired === 0"
                                        class="badge badge-success">
                                        active
                                    </label>
                                </div>
                            </cs-dashboard-table-item-unstyled>

                            <cs-dashboard-table-item-unstyled
                                class="w-32"
                                :item="`statusSummaryMatrixStatus`">
                                <!--Active styling-->
                                <label v-if="certificate.matrix_status"
                                       class="badge badge-success">
                                    compliant
                                </label>
                                <label v-else
                                       class="badge badge-danger">
                                    non-compliant
                                </label>
                            </cs-dashboard-table-item-unstyled>
                        </div>
                        <div v-else
                             class="table--row flex items-center justify-between border-certstyle-border last:border-b-0 border-b px-10 py-4">
                            <cs-dashboard-table-item-unstyled
                                class="w-1/2"
                                :item="`statusSummaryCertificateTitle`">
                                No certificates found
                            </cs-dashboard-table-item-unstyled>
                            <cs-dashboard-table-item-unstyled class="w-32"
                                                              :item="`statusSummaryCertificateStatus`">
                            </cs-dashboard-table-item-unstyled>
                            <cs-dashboard-table-item-unstyled
                                class="w-32"
                                :item="`statusSummaryMatrixStatus`">
                            </cs-dashboard-table-item-unstyled>
                        </div>
                    </div>
                </div>
            </modal-md>
        </cs-card>

        <contact-details
            v-if="userToShowContactDetails"
            :user="userToShowContactDetails"
        />

        <add-employee-modal
            :countries="countries"
            :identifier="`addEmployeeModal`"
            :invite-modal-title="`Invite to department`"
            :suggestion-url="userSuggestionApiURL"
            :title="`Assign to ${department ? department.name: ''}`"
            @inviteEmployees="handleInvitedEmployees"
            @selectEmployee="addEmployeeToDepartment"
            @removeEmployee="handleRemoveEmployee"
        />

        <cs-confirmation-modal
            v-if="checkRole(['admin', 'planner'])"
            @proceed="handleRemoveEmployee"
            identifier="removeConfirmationModal">
            <div class="text-certstyle-titles font-normal" slot="content">
                Removing employees from this department can not be undo.
            </div>

            <div slot="cancelButton"
                 class="cursor-pointer hover:bg-certstyle-background border border-certstyle-border rounded px-6 py-2 text-certstyle-text-light mr-4">
                Cancel
            </div>

            <cs-button slot="proceedButton">
                Remove
            </cs-button>
        </cs-confirmation-modal>
    </div>

</template>

这个列表和过滤器工作正常,但是当我尝试通过输入用户名来搜索用户时,它一直给我以下错误,

Duplicate keys detected: '1025'. This may cause an update error.

found in

---> at resources/js/components/reusable/datatable/Datatable.vue at resources/js/components/reusable/csCard.vue at resources/js/components/dashboard/communities/department/one-departments/department-user-list.vue

但是当我尝试通过其他一些参数过滤列表时,它们被过滤而没有任何错误。

我正在努力寻找我在这里做错了什么....

更新

以下是我针对单个用户的未过滤 json 数据

 {
      "id": 1038,
      "unique_id": "0a3938c1-07d5-3884-9df0-a8fe3201a3e5",
      "first_name": "Mango",
      "last_name": "F1",
      "job_title": null,
      "email": "mangoF1@gmail.com",
      "phone_number": null,
      "phone_country_calling_code": null,
      "country_id": null,
      "company_name": null,
      "is_subcontractor": 0,
      "current_jobtitle": "Deck Foreman",
      "department_jobtitle": "Rigging Engineers",
      "language": "en",
      "email_verified_at": "2022-04-12T12:47:55.000000Z",
      "gender": null,
      "state": null,
      "postal_code": null,
      "house_number": null,
      "street": null,
      "city": null,
      "date_of_birth": null,
      "city_of_birth": null,
      "emergency_number": null,
      "opt_in": null,
      "created_at": "2022-04-12T12:46:34.000000Z",
      "updated_at": "2022-04-12T12:47:55.000000Z",
      "emergency_number_country_calling_code": null,
      "deleted_at": null,
      "has_ip_restriction": 0,
      "address": null,
      "latitude": null,
      "longitude": null,
      "place_id": null,
      "bouwpas_worker_id": null,
      "bouwpas_certificate_id": null,
      "certificate_matrix": [
        {
          "id": 463,
          .....
        }
      ]
    },

以下是我的控制器函数,

public function index($locale, Company $company, Department $department, Request $request)
    {
        $this->authorize('viewUsers', [Department::class, $company, $department]);

        $users = $department->usersWithSubcontractors()
            //            ->notApiUser()
            ->select(
                'users.id',
                'unique_id',
                'first_name',
                'last_name',
                'job_title',
                'email',
                'phone_number',
                'unique_id',
                'phone_country_calling_code',
                'country_id',
                'company_name',
                'is_subcontractor',
            )->addSelect([
                'current_jobtitle' => JobTitle::selectRaw('IFNULL(project_specific_job_titles.title, job_titles.title)')
                    ->join('project_track_records', 'project_track_records.user_id', 'users.id', 'inner')
                    ->join('project_specific_job_titles', 'project_track_records.project_specific_job_title_id', 'project_specific_job_titles.id', 'inner')
                    ->whereColumn('job_titles.id', 'project_specific_job_titles.job_title_id')
                    ->whereDate('project_track_records.start_date', '<=', Carbon::now())
                    ->whereDate('project_track_records.end_date', '>=', Carbon::now())
                    ->limit(1),
                'department_jobtitle' => JobTitle::selectRaw('title')->whereColumn('job_titles.id', 'department_user.job_title_id'),
            ])->when(request('q'), function ($q) {
                $q->where(function ($q) {
                    $q->whereRaw("CONCAT_WS(' ', `first_name`, `last_name`) like '%" . request('q') . "%'");
                });
            })->when($request->get('employeeType'), function ($q) use ($request) {
                $q->whereIn('users.is_subcontractor', $request->get('employeeType'));
            })->paginate(\request('per_page', config('repository.pagination.limit')));

        $users->load('country');
        $users->append(['profile_image']);
        $users->makeVisible(['unique_id']);

        $users->map(fn ($item) => $item->certificate_matrix = (new GetCertificationMatrixQueryAction())->execute($item->id, $department->id));

        if ($request->filled('certificateStatus')) {
            $valid = in_array('valid', $request->get('certificateStatus'));
            $expire_soon = in_array('expire soon', $request->get('certificateStatus'));
            $expired = in_array('expired', $request->get('certificateStatus'));

            if ($valid) $users->setCollection($users->filter(fn ($item) => $item->certificate_matrix->filter(fn ($certificate) => $certificate->matrix_status && !$certificate->expire_soon && !$certificate->expired)->count() === $item->certificate_matrix->count())->values());
            if ($expire_soon && !$expired) $users->setCollection($users->filter(fn ($item) => $item->certificate_matrix->filter(fn ($certificate) => $certificate->matrix_status && $certificate->expire_soon)->count() > 0)->values());
            if ($expired && !$expire_soon) $users->setCollection($users->filter(fn ($item) => $item->certificate_matrix->filter(fn ($certificate) => $certificate->matrix_status && $certificate->expired)->count() > 0)->values());
        }

        if ($request->filled('matrixStatus')) {
            $compliant = in_array('compliant', $request->get('matrixStatus'));
            $non_compliant = in_array('non-compliant', $request->get('matrixStatus'));

            if ($non_compliant && !$compliant)
                $users->setCollection($users->filter(fn ($item) => $item->certificate_matrix->filter(fn ($certificate) => $certificate->matrix_status === 0 || $certificate->expire_soon || !$certificate->expired)->count() > 0)->values());

            if ($compliant && !$non_compliant)
                $users->setCollection($users->filter(fn ($item) => $item->certificate_matrix->filter(fn ($certificate) => $certificate->matrix_status && !$certificate->expire_soon && !$certificate->expired)->count() == $item->certificate_matrix->count())->values());
        }
        return response()->json($users);
    }

错误消息告诉您有重复项,当用户的 id 是唯一的(可能是 primary key)时,您对收到此类错误消息的原因感到困惑。但是,重要的是要注意,您为用户加载了部门,如果一个用户有多个部门,那么您将多次获得该用户。您可以对结果进行分组以解决问题。

单个用户的示例数据在 certificate_matrix 数组中有重复的 id 值(第 2 条和第 11 条记录的 ID 均为 443)

这可能会导致 v-for 循环出现问题(实际上您没有任何 :key

<!--Certificates-->
<div class="text-certstyle-titles">
    <div v-for="certificate in selectedEmployee.certificate_matrix"
         v-if="selectedEmployee.certificate_matrix.length > 0"
         class="table--row flex items-center justify-between border-certstyle-border last:border-b-0 border-b px-10 py-4">

或者在 certificate_matrix 记录被循环的任何其他地方

使用组合键结合两个字段的值生成唯一键

尝试确保前端唯一键的一种方法是将组合(有点像复合键索引)作为 ${id}-${matrix_status},如果该组合要保持唯一的话。

使用循环迭代索引作为复合后缀生成唯一键

如果没有其他字段值可以组合以确保 v-for 中子节点的 唯一 键,则 ${id}-${index} 可用于生成唯一键 - 例如:

<div v-for="(certificate, index) in selectedEmployee.certificate_matrix"
     :key="`${certificate.id}-${index}`" //To ensure uniqueness of key
     v-if="selectedEmployee.certificate_matrix.length > 0"
     class="table--row flex items-center justify-between border-certstyle-border last:border-b-0 border-b px-10 py-4">
使用 uuid 作为唯一键

另一种方法是在数据库表上添加一个 uuid 列,然后使用 uuid 作为 <DataTable /> 组件标记中的键,包括 :key:key-id 被使用。

Laravel 可以很容易地生成 Uuid v4 - uuids via (string) Str::orderedUuid(); 并且还有 $table->uuid('uid')->index() 迁移文件的列类型。

甚至可以在新模型记录 created/persisted

时存储 uid
// In any model class which has a uuid column
public static function booted()
{
    static::creating(function ($model) {
        $model->uid = Str::orderedUuid()->toString();
    });
}

Laravel Docs - Helpers - Str Ordered Uuid

Laravel Docs - Migrations - Uuid Column Type

Laravel Docs - Uuid - Validation Rule

更新

基于评论反馈

Thank you for your answer, I have updated the question with the correct backend controller. The reason for id duplication is one user can have multiple departments. Because of that my eloquent gives me results with multiple user id's for some results. When I tried to group the results by user.id, it gives me an 500 error, and I need to pass the user.id as the key rather than passing uuid since the id has used for other functions, like user deletion and all... Any way to group the result from the backend to get only unique user ids?

为了确保只有 唯一 用户记录通过控制器的响应发送,通过 unique() 方法过滤掉 $users 集合的重复记录Collection

//...method code
$users->setCollection($users->unique('id')->values());

return response()->json($users);