Kaynağa Gözat

客户管理页面迭代

lxf 1 yıl önce
ebeveyn
işleme
88f0a90130

+ 463 - 383
src/views/customer/file/index.vue

@@ -11,119 +11,110 @@
         :action-list="[
           {
             text: '添加客户',
-            action: () => openModal('add'),
+            action: () => openModal(),
           },
         ]"
-        @get-list="getList"
-      >
+        @get-list="getList">
         <template #address="{ item }">
           <span>{{ item.countryName }}</span>
           <span v-if="item.provinceName"> ,{{ item.provinceName }}</span>
           <span v-if="item.cityName"> ,{{ item.cityName }}</span>
         </template>
         <template #name="{ item }">
-          <div
-            style="cursor: pointer; color: #409eff"
-            @click="handleClickName(item)"
-          >
+          <div style="cursor: pointer; color: #409eff" @click="handleClickName(item)">
             {{ item.name }}
           </div>
         </template>
         <template #tags="{ item }">
-          <div>
-            <el-tag
-              style="margin-right: 10px"
-              type="success"
-              v-for="(tag, index) in item.tag"
-              :key="index"
-              >{{ dictValueLabel(tag, customerTag) }}</el-tag
-            >
+          <div style="width: 100%">
+            <el-tag style="margin-right: 8px" type="success" v-for="(tag, index) in item.tag" closable :key="index" @close="tagClose(tag, item)">
+              {{ dictValueLabel(tag, customerTag) }}
+            </el-tag>
+            <el-select
+              v-if="item.addTagShow"
+              v-model="addTag"
+              style="width: 100%"
+              @change="
+                (val) => {
+                  return changeTag(val, item);
+                }
+              ">
+              <el-option v-for="tag in customerTag" :key="tag.value" :label="tag.label" :value="tag.value" :disabled="judgeTagSelect(item.tag, tag.value)" />
+            </el-select>
+            <el-tag style="cursor: pointer" type="success" @click="showSelect(item)" v-else> + </el-tag>
           </div>
         </template>
         <template #follow="{ item }">
-          <div style="width: 100%; display: flex">
-            <div
-              v-for="(follow, index) in item.customerFollowRecordsList"
-              :key="index"
-              style="width: 190px; padding: 0 5px; border-right: 1px solid #ccc"
-            >
-              <div style="display: flex; align-items: center">
-                <span>{{ follow.date }}</span>
-                <el-icon
-                  style="margin: 0 10px; cursor: pointer"
-                  @click="updateFollow(follow)"
-                  ><Edit
-                /></el-icon>
-                <el-icon style="cursor: pointer" @click="deleteFollow(follow)"
-                  ><Delete
-                /></el-icon>
-              </div>
-              <div style="margin-top: 5px">
-                {{ follow.content }}
-              </div>
+          <div :class="'getWidth' + item.id" style="width: 100%">
+            <div style="width: 100%; display: flex">
+              <template v-if="item.customerFollowRecordsList && item.customerFollowRecordsList.length > 0">
+                <div
+                  :style="
+                    index > 2
+                      ? 'line-height: 32px; margin-right: 8px; padding: 0 8px; background-color: #eeeeee; border-radius: 4px; cursor: pointer; display: none'
+                      : 'line-height: 32px; margin-right: 8px; padding: 0 8px; background-color: #eeeeee; border-radius: 4px; cursor: pointer'
+                  "
+                  v-for="(record, index) in item.customerFollowRecordsList"
+                  :key="record.id">
+                  <el-popover placement="bottom" :width="300" trigger="hover" @show="recordShow(record)">
+                    <template #reference>
+                      <div>
+                        <span>{{ item.createTime.substr(0, 10) }}</span>
+                        <el-icon style="margin-left: 8px; transform: translateY(2px)" @click="deleteFollow(record)"><DeleteFilled /></el-icon>
+                      </div>
+                    </template>
+                    <template #default>
+                      <div style="width: 100%">
+                        <div style="color: #909399; margin: 8px 0">跟进时间: {{ record.createTime }}</div>
+                        <div style="word-wrap: break-word; margin: 8px 0" v-html="getStyle(record.content)" v-if="record.content"></div>
+                        <div v-else>跟进记录:</div>
+                        <div style="margin: 8px 0; display: flex" v-if="record.fileList && record.fileList.length > 0">
+                          <div style="width: 36px">附件:</div>
+                          <div style="width: calc(100% - 36px)">
+                            <div v-for="(file, index) in record.fileList" :key="index">
+                              <a style="color: #409eff; cursor: pointer" @click="openFile(file.fileUrl)">{{ file.fileName }}</a>
+                            </div>
+                          </div>
+                        </div>
+                      </div>
+                    </template>
+                  </el-popover>
+                </div>
+                <div
+                  style="line-height: 32px; margin-right: 8px; padding: 0 8px; background-color: #eeeeee; border-radius: 4px; cursor: pointer"
+                  @click="clickMore(item)"
+                  v-if="item.customerFollowRecordsList.length >= 3">
+                  更多
+                </div>
+              </template>
             </div>
           </div>
         </template>
       </byTable>
     </div>
 
-    <el-dialog
-      :title="modalType == 'add' ? '新增' : '编辑'"
-      v-if="dialogVisible"
-      v-model="dialogVisible"
-      width="800"
-      v-loading="loadingOperation"
-    >
-      <byForm
-        :formConfig="formConfig"
-        :formOption="formOption"
-        v-model="formData.data"
-        :rules="rules"
-        ref="submit"
-      >
+    <el-dialog :title="modalType == 'add' ? '新增' : '编辑'" v-if="dialogVisible" v-model="dialogVisible" width="800" v-loading="loadingOperation">
+      <byForm :formConfig="formConfig" :formOption="formOption" v-model="formData.data" :rules="rules" ref="submit">
         <template #address>
           <el-row style="width: 100%">
             <el-col :span="8">
               <el-form-item prop="countryId">
-                <el-select
-                  v-model="formData.data.countryId"
-                  placeholder="国家"
-                  @change="(val) => getCityData(val, '20', true)"
-                >
-                  <el-option
-                    v-for="item in countryData"
-                    :label="item.chineseName"
-                    :value="item.id"
-                  >
-                  </el-option>
+                <el-select v-model="formData.data.countryId" placeholder="国家" @change="(val) => getCityData(val, '20', true)">
+                  <el-option v-for="item in countryData" :label="item.chineseName" :value="item.id"> </el-option>
                 </el-select>
               </el-form-item>
             </el-col>
             <el-col :span="8">
               <el-form-item prop="provinceId">
-                <el-select
-                  v-model="formData.data.provinceId"
-                  placeholder="省/洲"
-                  @change="(val) => getCityData(val, '30', true)"
-                >
-                  <el-option
-                    v-for="item in provinceData"
-                    :label="item.name"
-                    :value="item.id"
-                  >
-                  </el-option>
+                <el-select v-model="formData.data.provinceId" placeholder="省/洲" @change="(val) => getCityData(val, '30', true)">
+                  <el-option v-for="item in provinceData" :label="item.name" :value="item.id"> </el-option>
                 </el-select>
               </el-form-item>
             </el-col>
             <el-col :span="8">
               <el-form-item prop="cityId">
                 <el-select v-model="formData.data.cityId" placeholder="城市">
-                  <el-option
-                    v-for="item in cityData"
-                    :label="item.name"
-                    :value="item.id"
-                  >
-                  </el-option>
+                  <el-option v-for="item in cityData" :label="item.name" :value="item.id"> </el-option>
                 </el-select>
               </el-form-item>
             </el-col>
@@ -131,119 +122,158 @@
           <el-row style="margin-top: 20px; width: 100%">
             <el-col :span="24">
               <el-form-item prop="address">
-                <el-input v-model="formData.data.address" type="textarea">
-                </el-input>
+                <el-input v-model="formData.data.address" type="textarea"> </el-input>
               </el-form-item>
             </el-col>
           </el-row>
         </template>
         <template #person>
-          <div>
-            <el-button type="primary" @click="clickAddPerson"> 添加 </el-button>
-            <byTable
-              :source="formData.data.customerUserList"
-              :config="configPerson"
-              hideSearch
-              hidePagination
-            >
-            </byTable>
+          <div style="width: 100%">
+            <el-button type="primary" @click="clickAddPerson">添 加</el-button>
+            <el-table :data="formData.data.customerUserList" style="width: 100%; margin-top: 16px">
+              <el-table-column label="联系人" width="160">
+                <template #default="{ row, $index }">
+                  <div style="width: 100%">
+                    <el-form-item :prop="'customerUserList.' + $index + '.name'" :rules="rules.name2" :inline-message="true">
+                      <el-input v-model="row.name" placeholder="请输入联系人" />
+                    </el-form-item>
+                  </div>
+                </template>
+              </el-table-column>
+              <el-table-column label="电子邮箱">
+                <template #default="{ row, $index }">
+                  <div style="width: 100%">
+                    <el-form-item :prop="'customerUserList.' + $index + '.email'" :rules="rules.email" :inline-message="true">
+                      <el-input v-model="row.email" placeholder="请输入电子邮箱" />
+                    </el-form-item>
+                  </div>
+                </template>
+              </el-table-column>
+              <el-table-column align="center" label="操作" width="120" fixed="right">
+                <template #default="{ row, $index }">
+                  <el-button type="primary" link @click="clickInformationMore(row, $index)">更多</el-button>
+                  <el-button type="primary" link @click="clickDelete($index)">删除</el-button>
+                </template>
+              </el-table-column>
+            </el-table>
           </div>
         </template>
       </byForm>
       <template #footer>
         <el-button @click="dialogVisible = false" size="large">取 消</el-button>
-        <el-button
-          type="primary"
-          @click="submitForm('submit')"
-          size="large"
-          :loading="submitLoading"
-        >
-          确 定
-        </el-button>
+        <el-button type="primary" @click="submitForm()" size="large" :loading="submitLoading">确 定</el-button>
       </template>
     </el-dialog>
 
-    <el-dialog title="添加联系人" v-model="openPerson" width="400">
-      <byForm
-        :formConfig="formConfigPerson"
-        :formOption="formOption"
-        v-model="formPerson.data"
-        :rules="rulesPerson"
-        ref="person"
-      >
-      </byForm>
+    <el-dialog title="更多联系方式" v-model="openPerson" width="700">
+      <el-form :label-position="'top'" :model="formPerson.data" :rules="rulesPerson" ref="person">
+        <el-form-item label="联系人" prop="name">
+          <el-input v-model="formPerson.data.name" />
+        </el-form-item>
+        <el-form-item label="电子邮箱" prop="email">
+          <el-input v-model="formPerson.data.email" />
+        </el-form-item>
+        <el-form-item label="更多联系方式">
+          <div style="width: 100%">
+            <el-button type="primary" @click="clickAddMoreInformation">添 加</el-button>
+            <el-table :data="formPerson.data.contact" style="width: 100%; margin-top: 16px">
+              <el-table-column label="类型" width="180">
+                <template #default="{ row, $index }">
+                  <div style="width: 100%">
+                    <el-form-item :prop="'contact.' + $index + '.type'" :rules="rulesPerson.type" :inline-message="true">
+                      <el-select v-model="row.type" placeholder="请选择类型" style="width: 100%">
+                        <el-option v-for="item in contactType" :key="item.value" :label="item.label" :value="item.value" />
+                      </el-select>
+                    </el-form-item>
+                  </div>
+                </template>
+              </el-table-column>
+              <el-table-column label="联系号码">
+                <template #default="{ row, $index }">
+                  <div style="width: 100%">
+                    <el-form-item :prop="'contact.' + $index + '.contactNo'" :rules="rulesPerson.contactNo" :inline-message="true">
+                      <el-input v-model="row.contactNo" placeholder="请输入联系号码" />
+                    </el-form-item>
+                  </div>
+                </template>
+              </el-table-column>
+              <el-table-column align="center" label="操作" width="120" fixed="right">
+                <template #default="{ $index }">
+                  <el-button type="primary" link @click="clickInformationDelete($index)">删除</el-button>
+                </template>
+              </el-table-column>
+            </el-table>
+          </div>
+        </el-form-item>
+      </el-form>
       <template #footer>
         <el-button @click="openPerson = false" size="large">取 消</el-button>
-        <el-button type="primary" @click="submitPerson('person')" size="large">
-          确 定
-        </el-button>
+        <el-button type="primary" @click="submitPerson()" size="large">确 定</el-button>
       </template>
     </el-dialog>
 
     <el-dialog title="分配" v-model="openAllocation" width="300">
-      <byForm
-        :formConfig="formConfigAllocation"
-        :formOption="formOption"
-        v-model="formAllocation.data"
-        ref="allocation"
-      >
-      </byForm>
+      <byForm :formConfig="formConfigAllocation" :formOption="formOption" v-model="formAllocation.data" ref="allocation"> </byForm>
       <template #footer>
-        <el-button @click="openAllocation = false" size="large"
-          >取 消</el-button
-        >
-        <el-button
-          type="primary"
-          @click="submitAllocation('allocation')"
-          size="large"
-        >
-          确 定
-        </el-button>
+        <el-button @click="openAllocation = false" size="large">取 消</el-button>
+        <el-button type="primary" @click="submitAllocation()" size="large">确 定</el-button>
       </template>
     </el-dialog>
 
-    <el-dialog title="跟进" v-model="openFollow" width="500" destroy-on-close>
-      <byForm
-        :formConfig="formConfigAFollow"
-        :formOption="formOption"
-        v-model="formFollow.data"
-        :rules="rulesFollow"
-        ref="follow"
-      >
+    <el-dialog title="添加跟进记录" v-if="openFollow" v-model="openFollow" width="500" destroy-on-close>
+      <byForm :formConfig="formConfigAFollow" :formOption="formOption" v-model="formFollow.data" :rules="rulesFollow" ref="follow">
         <template #fileSlot>
-          <div>
+          <div style="width: 100%">
             <el-upload
               v-model:fileList="fileList"
-              :show-file-list="false"
-              class="upload-demo"
               action="https://winfaster.obs.cn-south-1.myhuaweicloud.com"
               :data="uploadData"
-              :before-upload="handleBeforeUpload"
-            >
+              multiple
+              :before-upload="uploadFile"
+              :on-preview="onPreviewFile">
               <el-button type="primary">文件上传</el-button>
             </el-upload>
-            <div>
-              <div style="margin-top: 15px">
-                <el-tag
-                  style="margin-right: 10px"
-                  class="ml-2"
-                  type="info"
-                  v-for="(item, index) in fileListCopy"
-                  :key="index"
-                  closable
-                  @close="handleClose(index)"
-                  >{{ item.fileName }}</el-tag
-                >
-              </div>
-            </div>
           </div>
         </template>
       </byForm>
       <template #footer>
         <el-button @click="openFollow = false" size="large">取 消</el-button>
-        <el-button type="primary" @click="submitFollow('follow')" size="large">
-          确 定
-        </el-button>
+        <el-button type="primary" @click="submitFollow()" size="large">确 定</el-button>
+      </template>
+    </el-dialog>
+
+    <el-dialog title="跟进记录" v-if="openRecordMore" v-model="openRecordMore" width="800" destroy-on-close>
+      <div>
+        <div style="padding: 8px 0">
+          <el-button type="primary" @click="clickFollowUp(rowData)" plain>添加跟进记录</el-button>
+        </div>
+        <div style="padding-top: 16px">
+          <div v-infinite-scroll="infiniteScroll" class="infinite-scroll" :infinite-scroll-disabled="judgeTotal()">
+            <el-timeline>
+              <el-timeline-item v-for="(record, index) in recordList" :key="index" :timestamp="record.createTime" hide-timestamp>
+                <div>
+                  <div style="padding: 0 0 8px 0; display: flex; justify-content: space-between">
+                    <span>{{ dictValueLabel(record.createUser, userList) }}</span>
+                    <span>{{ record.createTime }}</span>
+                  </div>
+                  <div style="word-wrap: break-word; margin: 8px 0" v-html="getStyle(record.content)" v-if="record.content"></div>
+                  <div style="margin: 8px 0" v-else>跟进记录:</div>
+                  <div style="margin: 8px 0; display: flex" v-if="record.fileList && record.fileList.length > 0">
+                    <div style="width: 36px">附件:</div>
+                    <div style="width: calc(100% - 36px)">
+                      <div v-for="(file, index) in record.fileList" :key="index">
+                        <a style="color: #409eff; cursor: pointer" @click="openFile(file.fileUrl)">{{ file.fileName }}</a>
+                      </div>
+                    </div>
+                  </div>
+                </div>
+              </el-timeline-item>
+            </el-timeline>
+          </div>
+        </div>
+      </div>
+      <template #footer>
+        <el-button @click="openRecordMore = false" size="large">关 闭</el-button>
       </template>
     </el-dialog>
   </div>
@@ -265,9 +295,9 @@ const openAllocation = ref(false);
 const customerTag = ref([]);
 const customerSource = ref([]);
 const customerStatus = ref([]);
+const contactType = ref([]);
 const userList = ref([]);
 const fileList = ref([]);
-const fileListCopy = ref([]);
 const uploadData = ref({});
 const sourceList = ref({
   data: [],
@@ -316,57 +346,48 @@ const config = computed(() => {
         prop: "name",
         slot: "name",
         fixed: "left",
+        width: 160,
       },
     },
     {
       attrs: {
         label: "所在城市",
         slot: "address",
-        width: 200,
+        width: 160,
       },
     },
     {
       attrs: {
         label: "客户代码",
         prop: "customerCode",
-        width: 140,
+        width: 120,
       },
     },
     {
       attrs: {
         label: "客户来源",
         prop: "source",
-        width: 140,
+        width: 120,
       },
       render(type) {
-        let data = customerSource.value.filter((item) => item.value == type);
-        if (data && data.length > 0) {
-          return data[0].label;
-        } else {
-          return "";
-        }
+        return proxy.dictValueLabel(type, customerSource.value);
       },
     },
     {
       attrs: {
         label: "客户类型",
         prop: "status",
-        width: 140,
+        width: 120,
       },
       render(type) {
-        let data = customerStatus.value.filter((item) => item.value == type);
-        if (data && data.length > 0) {
-          return data[0].label;
-        } else {
-          return "";
-        }
+        return proxy.dictValueLabel(type, customerStatus.value);
       },
     },
     {
       attrs: {
         label: "客户标签",
         slot: "tags",
-        width: 200,
+        width: 180,
       },
     },
     {
@@ -388,13 +409,13 @@ const config = computed(() => {
       attrs: {
         label: "跟进",
         slot: "follow",
-        width: 700,
+        "min-width": 440,
       },
     },
     {
       attrs: {
         label: "操作",
-        width: "200",
+        width: 190,
         align: "center",
         fixed: "right",
       },
@@ -410,6 +431,7 @@ const config = computed(() => {
             click() {
               formAllocation.data = {
                 id: row.id,
+                userId: row.userId,
               };
               openAllocation.value = true;
             },
@@ -422,13 +444,7 @@ const config = computed(() => {
             },
             el: "button",
             click() {
-              formFollow.data = {
-                customerId: row.id,
-              };
-              fileList.value = [];
-              fileListCopy.value = [];
-              modalType.value = "add";
-              openFollow.value = true;
+              clickFollowUp(row);
             },
           },
           {
@@ -450,15 +466,11 @@ const config = computed(() => {
             },
             el: "button",
             click() {
-              ElMessageBox.confirm(
-                "此操作将永久删除该数据, 是否继续?",
-                "提示",
-                {
-                  confirmButtonText: "确定",
-                  cancelButtonText: "取消",
-                  type: "warning",
-                }
-              ).then(() => {
+              ElMessageBox.confirm("此操作将永久删除该数据, 是否继续?", "提示", {
+                confirmButtonText: "确定",
+                cancelButtonText: "取消",
+                type: "warning",
+              }).then(() => {
                 proxy
                   .post("/customer/delete", {
                     id: row.id,
@@ -549,7 +561,7 @@ const formConfig = computed(() => {
     {
       type: "select",
       label: "客户标签",
-      prop: "tag",
+      prop: "tags",
       itemWidth: 100,
       multiple: true,
       data: customerTag.value,
@@ -557,7 +569,6 @@ const formConfig = computed(() => {
         width: "100%",
       },
     },
-
     {
       type: "slot",
       slotName: "person",
@@ -567,6 +578,8 @@ const formConfig = computed(() => {
 });
 let rules = ref({
   name: [{ required: true, message: "请输入客户名称", trigger: "blur" }],
+  name2: [{ required: true, message: "请输入联系人", trigger: "blur" }],
+  email: [{ required: true, message: "请输入电子邮箱", trigger: "blur" }],
   countryId: [{ required: true, message: "请选择国家", trigger: "change" }],
   provinceId: [{ required: true, message: "请选择省/州", trigger: "change" }],
   cityId: [{ required: true, message: "请选择城市", trigger: "change" }],
@@ -584,46 +597,6 @@ const formConfigAllocation = computed(() => {
     },
   ];
 });
-const configPerson = computed(() => {
-  return [
-    {
-      attrs: {
-        label: "联系人名称",
-        prop: "name",
-        width: 150,
-      },
-    },
-    {
-      attrs: {
-        label: "联系人电话",
-        prop: "phone",
-        width: 440,
-      },
-    },
-    {
-      attrs: {
-        label: "操作",
-        width: "100",
-        align: "center",
-      },
-      renderHTML(row) {
-        return [
-          {
-            attrs: {
-              label: "删除",
-              type: "primary",
-              text: true,
-            },
-            el: "button",
-            click() {
-              formData.data.customerUserList.splice(row.$index, 1);
-            },
-          },
-        ];
-      },
-    },
-  ];
-});
 const formConfigAFollow = computed(() => {
   return [
     {
@@ -648,47 +621,30 @@ const formConfigAFollow = computed(() => {
     },
   ];
 });
-const formConfigPerson = computed(() => {
-  return [
-    {
-      type: "input",
-      prop: "name",
-      label: "联系人名称",
-      required: true,
-      itemWidth: 100,
-      itemType: "text",
-    },
-    {
-      type: "input",
-      prop: "phone",
-      label: "联系人电话",
-      required: true,
-      itemWidth: 100,
-      itemType: "text",
-    },
-  ];
-});
 let rulesPerson = ref({
-  name: [{ required: true, message: "请输入联系人名称", trigger: "blur" }],
-  phone: [{ required: true, message: "请输入联系人电话", trigger: "blur" }],
+  name: [{ required: true, message: "请输入联系人", trigger: "blur" }],
+  email: [{ required: true, message: "请输入电子邮箱", trigger: "blur" }],
+  type: [{ required: true, message: "请选择类型", trigger: "change" }],
+  contactNo: [{ required: true, message: "请输入联系号码", trigger: "blur" }],
 });
 let rulesFollow = ref({
   date: [{ required: true, message: "请选择跟进时间", trigger: "change" }],
   content: [{ required: true, message: "请输入跟进内容", trigger: "blur" }],
 });
-
 const submit = ref(null);
 const person = ref(null);
 const allocation = ref(null);
 const follow = ref(null);
-
 const getList = async (req) => {
   sourceList.value.pagination = { ...sourceList.value.pagination, ...req };
   loading.value = true;
   proxy.post("/customer/page", sourceList.value.pagination).then((res) => {
     res.rows.forEach((x) => {
+      x.addTagShow = false;
       if (x.tag) {
         x.tag = x.tag.split(",");
+      } else {
+        x.tag = [];
       }
     });
     sourceList.value.data = res.rows;
@@ -702,7 +658,7 @@ const openModal = () => {
   modalType.value = "add";
   formData.data = {
     countryId: "China",
-    tag: [],
+    tags: [],
   };
   getCityData(formData.data.countryId, "20");
   loadingOperation.value = false;
@@ -712,17 +668,10 @@ const update = (row) => {
   modalType.value = "edit";
   loadingOperation.value = true;
   proxy.post("/customer/detail", { id: row.id }).then((res) => {
-    res.customerUserList = res.customerUserList.map((item) => {
-      return {
-        name: item.name,
-        phone: item.phone,
-        customerId: item.customerId,
-      };
-    });
     if (res.tag) {
-      res.tag = res.tag.split(",");
+      res.tags = res.tag.split(",");
     } else {
-      res.tag = [];
+      res.tags = [];
     }
     formData.data = res;
     getCityData(formData.data.countryId, "20");
@@ -754,8 +703,19 @@ const getCityData = (id, type, isChange) => {
 };
 getCityData("0");
 const clickAddPerson = () => {
-  formPerson.data = {};
-  openPerson.value = true;
+  if (formData.data.customerUserList && formData.data.customerUserList.length > 0) {
+    formData.data.customerUserList.push({
+      name: "",
+      email: "",
+    });
+  } else {
+    formData.data.customerUserList = [
+      {
+        name: "",
+        email: "",
+      },
+    ];
+  }
 };
 const submitAllocation = () => {
   allocation.value.handleSubmit(() => {
@@ -769,60 +729,38 @@ const submitAllocation = () => {
     });
   });
 };
-
 const submitFollow = () => {
   follow.value.handleSubmit(() => {
-    formFollow.data.fileList =
-      fileListCopy.value.map((x) => ({
-        id: x.id,
-        fileName: x.fileName,
-      })) || [];
-    proxy
-      .post("/customerFollowRecords/" + modalType.value, formFollow.data)
-      .then(
-        () => {
-          ElMessage({
-            message: modalType.value == "add" ? "添加成功" : "编辑成功",
-            type: "success",
-          });
-          openFollow.value = false;
-          getList();
-        },
-        (err) => {
-          console.log(err);
-        }
-      );
-  });
-};
-
-const submitPerson = () => {
-  person.value.handleSubmit(() => {
-    if (
-      formData.data.customerUserList &&
-      formData.data.customerUserList.length > 0
-    ) {
-      formData.data.customerUserList.push({
-        name: formPerson.data.name,
-        phone: formPerson.data.phone,
+    if (fileList.value && fileList.value.length > 0) {
+      formFollow.data.fileList = fileList.value.map((item) => {
+        return {
+          id: item.raw.id,
+          fileName: item.raw.fileName,
+          fileUrl: item.raw.fileUrl,
+        };
       });
     } else {
-      formData.data.customerUserList = [
-        {
-          name: formPerson.data.name,
-          phone: formPerson.data.phone,
-        },
-      ];
+      formFollow.data.fileList = [];
     }
-    openPerson.value = false;
+    proxy.post("/customerFollowRecords/" + modalType.value, formFollow.data).then(
+      () => {
+        ElMessage({
+          message: modalType.value == "add" ? "添加成功" : "编辑成功",
+          type: "success",
+        });
+        openFollow.value = false;
+        getList();
+      },
+      (err) => {
+        console.log(err);
+      }
+    );
   });
 };
 const submitForm = () => {
   submit.value.handleSubmit(() => {
-    if (
-      formData.data.customerUserList &&
-      formData.data.customerUserList.length > 0
-    ) {
-      formData.data.tag = formData.data.tag.join(",");
+    if (formData.data.customerUserList && formData.data.customerUserList.length > 0) {
+      formData.data.tag = formData.data.tags.join(",");
       submitLoading.value = true;
       proxy.post("/customer/" + modalType.value, formData.data).then(
         () => {
@@ -844,38 +782,25 @@ const submitForm = () => {
     }
   });
 };
-
 const getDict = () => {
-  proxy
-    .post("/dictTenantData/page", {
-      pageNum: 1,
-      pageSize: 999,
-      dictCode: "customer_source",
-      tenantId: useUserStore().user.tenantId,
-    })
-    .then((res) => {
-      customerSource.value = res.rows.map((item) => {
-        return {
-          label: item.dictValue,
-          value: item.dictKey,
-        };
-      });
-    });
-  proxy
-    .post("/dictTenantData/page", {
-      pageNum: 1,
-      pageSize: 999,
-      dictCode: "customer_status",
-      tenantId: useUserStore().user.tenantId,
-    })
-    .then((res) => {
-      customerStatus.value = res.rows.map((item) => {
-        return {
-          label: item.dictValue,
-          value: item.dictKey,
-        };
-      });
-    });
+  proxy.getDictOne(["customer_tag", "customer_source", "customer_status", "contact_type"]).then((res) => {
+    customerTag.value = res["customer_tag"].map((x) => ({
+      label: x.dictValue,
+      value: x.dictKey,
+    }));
+    customerSource.value = res["customer_source"].map((x) => ({
+      label: x.dictValue,
+      value: x.dictKey,
+    }));
+    customerStatus.value = res["customer_status"].map((x) => ({
+      label: x.dictValue,
+      value: x.dictKey,
+    }));
+    contactType.value = res["contact_type"].map((x) => ({
+      label: x.dictValue,
+      value: x.dictKey,
+    }));
+  });
   proxy
     .get("/tenantUser/list", {
       pageNum: 1,
@@ -890,16 +815,9 @@ const getDict = () => {
         };
       });
     });
-  proxy.getDictOne(["customer_tag"]).then((res) => {
-    customerTag.value = res["customer_tag"].map((x) => ({
-      label: x.dictValue,
-      value: x.dictKey,
-    }));
-  });
 };
 getDict();
 getList();
-
 const handleClickName = (row) => {
   proxy.$router.push({
     path: "/customer/customer/portrait",
@@ -908,37 +826,6 @@ const handleClickName = (row) => {
     },
   });
 };
-
-const handleBeforeUpload = async (file) => {
-  const res = await proxy.post("/fileInfo/getSing", { fileName: file.name });
-  uploadData.value = res.uploadBody;
-  fileListCopy.value.push({
-    id: res.id,
-    fileName: res.fileName,
-    path: res.fileUrl,
-    url: res.fileUrl,
-    uid: file.uid,
-  });
-};
-
-const handleClose = (index) => {
-  fileList.value.splice(index, 1);
-  fileListCopy.value.splice(index, 1);
-};
-
-const updateFollow = (data) => {
-  modalType.value = "edit";
-  formFollow.data = {
-    ...data,
-  };
-  proxy
-    .post("/fileInfo/getList", { businessIdList: [data.id] })
-    .then((fileObj) => {
-      fileList.value = fileObj[data.id] || [];
-      fileListCopy.value = fileObj[data.id] || [];
-    });
-  openFollow.value = true;
-};
 const deleteFollow = (data) => {
   ElMessageBox.confirm("是否确认删除该跟进?", "提示", {
     confirmButtonText: "确定",
@@ -958,10 +845,203 @@ const deleteFollow = (data) => {
       });
   });
 };
+const addTag = ref("");
+const judgeTagSelect = (data, val) => {
+  if (data && data.length > 0) {
+    if (data.includes(val)) {
+      return true;
+    }
+  }
+  return false;
+};
+const changeTag = (val, item) => {
+  let data = {
+    id: item.id,
+    tag: JSON.parse(JSON.stringify(item.tag)),
+  };
+  data.tag.push(val);
+  data.tag = data.tag.join(",");
+  proxy.post("/customer/editTag", data).then(() => {
+    ElMessage({
+      message: "添加成功",
+      type: "success",
+    });
+    item.addTagShow = false;
+    addTag.value = "";
+    getList();
+  });
+};
+const tagClose = (val, item) => {
+  let data = {
+    id: item.id,
+    tag: JSON.parse(JSON.stringify(item.tag)),
+  };
+  data.tag = data.tag.filter((row) => row !== val);
+  if (data.tag && data.tag.length > 0) {
+    data.tag = data.tag.join(",");
+  } else {
+    data.tag = "";
+  }
+  proxy.post("/customer/editTag", data).then(() => {
+    ElMessage({
+      message: "添加成功",
+      type: "success",
+    });
+    item.addTagShow = false;
+    addTag.value = "";
+    getList();
+  });
+};
+const showSelect = (item) => {
+  item.addTagShow = true;
+};
+const clickFollowUp = (item) => {
+  formFollow.data = {
+    customerId: item.id,
+    fileList: [],
+  };
+  fileList.value = [];
+  modalType.value = "add";
+  openFollow.value = true;
+  openRecordMore.value = false;
+};
+const uploadFile = async (file) => {
+  const res = await proxy.post("/fileInfo/getSing", { fileName: file.name });
+  uploadData.value = res.uploadBody;
+  file.id = res.id;
+  file.fileName = res.fileName;
+  file.fileUrl = res.fileUrl;
+  return true;
+};
+const onPreviewFile = (file) => {
+  window.open(file.raw.fileUrl, "_blank");
+};
+const getStyle = (val) => {
+  if (val) {
+    return "跟进记录: " + val.replace(/\n|\r\n/g, "<br>");
+  } else {
+    return "";
+  }
+};
+const recordShow = (item) => {
+  if (!(item.fileList && item.fileList.length > 0)) {
+    proxy.post("/fileInfo/getList", { businessIdList: [item.id] }).then((fileObj) => {
+      item.fileList = fileObj[item.id] || [];
+    });
+  }
+};
+const openFile = (path) => {
+  window.open(path, "_blank");
+};
+const judgeWidth = (item, index) => {
+  if (item && item.id) {
+    let dom = document.querySelector(".getWidth" + item.id);
+    if (dom) {
+      let width = getComputedStyle(dom).width.replace("px", "");
+      let num = parseInt(width / 118);
+      let num2 = parseInt((width - 52) / 118);
+      if (num2 === num) {
+        if (index + 1 > num) {
+          return "display: none";
+        }
+      } else {
+        if (index + 1 >= num) {
+          return "display: none";
+        }
+      }
+    }
+  }
+  return "";
+};
+const recordList = ref([]);
+const rowData = ref({});
+const openRecordMore = ref(false);
+const queryParams = ref({
+  total: 0,
+  pageNum: 1,
+  pageSize: 10,
+  customerId: "",
+});
+const clickMore = (item) => {
+  if (openRecordMore.value === false) {
+    recordList.value = [];
+    rowData.value = item;
+    queryParams.value.customerId = item.id;
+  }
+  proxy.post("/customerFollowRecords/page", queryParams.value).then((res) => {
+    recordList.value = recordList.value.concat(res.rows);
+    queryParams.value.total = res.total;
+    proxy.post("/fileInfo/getList", { businessIdList: res.rows.map((rows) => rows.id) }).then((fileObj) => {
+      for (let i = 0; i < res.rows.length; i++) {
+        recordList.value[parseInt(i + (queryParams.value.pageNum - 1) * queryParams.value.pageSize)].fileList =
+          fileObj[recordList.value[parseInt(i + (queryParams.value.pageNum - 1) * queryParams.value.pageSize)].id] || [];
+      }
+    });
+  });
+  openRecordMore.value = true;
+};
+const infiniteScroll = () => {
+  queryParams.value.pageNum++;
+  clickMore();
+};
+const judgeTotal = () => {
+  if (queryParams.value.pageNum * queryParams.value.pageSize >= queryParams.value.total) {
+    return true;
+  }
+  return false;
+};
+const moreIndex = ref(0);
+const clickInformationMore = (item, index) => {
+  moreIndex.value = index;
+  if (item.contactJson) {
+    item.contact = JSON.parse(item.contactJson);
+  } else {
+    item.contact = [];
+  }
+  formPerson.data = JSON.parse(JSON.stringify(item));
+  openPerson.value = true;
+};
+const clickDelete = (index) => {
+  formData.data.customerUserList.splice(index, 1);
+};
+const clickAddMoreInformation = () => {
+  if (formPerson.data.contact && formPerson.data.contact.length > 0) {
+    formPerson.data.contact.push({
+      type: "",
+      contactNo: "",
+    });
+  } else {
+    formPerson.data.contact = [
+      {
+        type: "",
+        contactNo: "",
+      },
+    ];
+  }
+};
+const clickInformationDelete = (index) => {
+  formPerson.data.contact.splice(index, 1);
+};
+const submitPerson = () => {
+  person.value.validate((valid) => {
+    if (valid) {
+      formPerson.data.contactJson = JSON.stringify(formPerson.data.contact);
+      formData.data.customerUserList[moreIndex.value] = formPerson.data;
+      openPerson.value = false;
+    }
+  });
+};
 </script>
 
 <style lang="scss" scoped>
 .tenant {
   padding: 20px;
 }
+.infinite-scroll {
+  max-height: calc(89vh - 94px - 70px - 58px - 16px);
+  overflow-y: auto;
+  &::-webkit-scrollbar {
+    width: 0px;
+  }
+}
 </style>

+ 637 - 118
src/views/customer/highseas/index.vue

@@ -11,7 +11,7 @@
         :action-list="[
           {
             text: '添加客户',
-            action: () => openModal('add'),
+            action: () => openModal(),
           },
         ]"
         @get-list="getList">
@@ -20,13 +20,83 @@
           <span v-if="item.provinceName"> ,{{ item.provinceName }}</span>
           <span v-if="item.cityName"> ,{{ item.cityName }}</span>
         </template>
+        <template #name="{ item }">
+          <div style="cursor: pointer; color: #409eff" @click="handleClickName(item)">
+            {{ item.name }}
+          </div>
+        </template>
+        <template #tags="{ item }">
+          <div style="width: 100%">
+            <el-tag style="margin-right: 8px" type="success" v-for="(tag, index) in item.tag" closable :key="index" @close="tagClose(tag, item)">
+              {{ dictValueLabel(tag, customerTag) }}
+            </el-tag>
+            <el-select
+              v-if="item.addTagShow"
+              v-model="addTag"
+              style="width: 100%"
+              @change="
+                (val) => {
+                  return changeTag(val, item);
+                }
+              ">
+              <el-option v-for="tag in customerTag" :key="tag.value" :label="tag.label" :value="tag.value" :disabled="judgeTagSelect(item.tag, tag.value)" />
+            </el-select>
+            <el-tag style="cursor: pointer" type="success" @click="showSelect(item)" v-else> + </el-tag>
+          </div>
+        </template>
+        <template #follow="{ item }">
+          <div :class="'getWidth' + item.id" style="width: 100%">
+            <div style="width: 100%; display: flex">
+              <template v-if="item.customerFollowRecordsList && item.customerFollowRecordsList.length > 0">
+                <div
+                  :style="
+                    index > 2
+                      ? 'line-height: 32px; margin-right: 8px; padding: 0 8px; background-color: #eeeeee; border-radius: 4px; cursor: pointer; display: none'
+                      : 'line-height: 32px; margin-right: 8px; padding: 0 8px; background-color: #eeeeee; border-radius: 4px; cursor: pointer'
+                  "
+                  v-for="(record, index) in item.customerFollowRecordsList"
+                  :key="record.id">
+                  <el-popover placement="bottom" :width="300" trigger="hover" @show="recordShow(record)">
+                    <template #reference>
+                      <div>
+                        <span>{{ item.createTime.substr(0, 10) }}</span>
+                        <el-icon style="margin-left: 8px; transform: translateY(2px)" @click="deleteFollow(record)"><DeleteFilled /></el-icon>
+                      </div>
+                    </template>
+                    <template #default>
+                      <div style="width: 100%">
+                        <div style="color: #909399; margin: 8px 0">跟进时间: {{ record.createTime }}</div>
+                        <div style="word-wrap: break-word; margin: 8px 0" v-html="getStyle(record.content)" v-if="record.content"></div>
+                        <div v-else>跟进记录:</div>
+                        <div style="margin: 8px 0; display: flex" v-if="record.fileList && record.fileList.length > 0">
+                          <div style="width: 36px">附件:</div>
+                          <div style="width: calc(100% - 36px)">
+                            <div v-for="(file, index) in record.fileList" :key="index">
+                              <a style="color: #409eff; cursor: pointer" @click="openFile(file.fileUrl)">{{ file.fileName }}</a>
+                            </div>
+                          </div>
+                        </div>
+                      </div>
+                    </template>
+                  </el-popover>
+                </div>
+                <div
+                  style="line-height: 32px; margin-right: 8px; padding: 0 8px; background-color: #eeeeee; border-radius: 4px; cursor: pointer"
+                  @click="clickMore(item)"
+                  v-if="item.customerFollowRecordsList.length >= 3">
+                  更多
+                </div>
+              </template>
+            </div>
+          </div>
+        </template>
       </byTable>
     </div>
 
-    <el-dialog :title="modalType == 'add' ? '新增' : '编辑'" v-model="dialogVisible" width="800" v-loading="loadingOperation">
+    <el-dialog :title="modalType == 'add' ? '新增' : '编辑'" v-if="dialogVisible" v-model="dialogVisible" width="800" v-loading="loadingOperation">
       <byForm :formConfig="formConfig" :formOption="formOption" v-model="formData.data" :rules="rules" ref="submit">
         <template #address>
-          <el-row :gutter="10" style="width: 100%; margin-left: -15px">
+          <el-row style="width: 100%">
             <el-col :span="8">
               <el-form-item prop="countryId">
                 <el-select v-model="formData.data.countryId" placeholder="国家" @change="(val) => getCityData(val, '20', true)">
@@ -49,7 +119,7 @@
               </el-form-item>
             </el-col>
           </el-row>
-          <el-row style="margin-top: 20px; width: 100%; margin-left: -10px">
+          <el-row style="margin-top: 20px; width: 100%">
             <el-col :span="24">
               <el-form-item prop="address">
                 <el-input v-model="formData.data.address" type="textarea"> </el-input>
@@ -57,25 +127,153 @@
             </el-col>
           </el-row>
         </template>
-
         <template #person>
-          <div>
-            <el-button type="primary" @click="clickAddPerson"> 添加 </el-button>
-            <byTable :source="formData.data.customerUserList" :config="configPerson" hideSearch hidePagination> </byTable>
+          <div style="width: 100%">
+            <el-button type="primary" @click="clickAddPerson">添 加</el-button>
+            <el-table :data="formData.data.customerUserList" style="width: 100%; margin-top: 16px">
+              <el-table-column label="联系人" width="160">
+                <template #default="{ row, $index }">
+                  <div style="width: 100%">
+                    <el-form-item :prop="'customerUserList.' + $index + '.name'" :rules="rules.name2" :inline-message="true">
+                      <el-input v-model="row.name" placeholder="请输入联系人" />
+                    </el-form-item>
+                  </div>
+                </template>
+              </el-table-column>
+              <el-table-column label="电子邮箱">
+                <template #default="{ row, $index }">
+                  <div style="width: 100%">
+                    <el-form-item :prop="'customerUserList.' + $index + '.email'" :rules="rules.email" :inline-message="true">
+                      <el-input v-model="row.email" placeholder="请输入电子邮箱" />
+                    </el-form-item>
+                  </div>
+                </template>
+              </el-table-column>
+              <el-table-column align="center" label="操作" width="120" fixed="right">
+                <template #default="{ row, $index }">
+                  <el-button type="primary" link @click="clickInformationMore(row, $index)">更多</el-button>
+                  <el-button type="primary" link @click="clickDelete($index)">删除</el-button>
+                </template>
+              </el-table-column>
+            </el-table>
           </div>
         </template>
       </byForm>
       <template #footer>
         <el-button @click="dialogVisible = false" size="large">取 消</el-button>
-        <el-button type="primary" @click="submitForm('submit')" size="large" :loading="submitLoading">确 定</el-button>
+        <el-button type="primary" @click="submitForm()" size="large" :loading="submitLoading">确 定</el-button>
       </template>
     </el-dialog>
 
-    <el-dialog title="添加联系人" v-model="openPerson" width="400" v-loading="openPerson">
-      <byForm :formConfig="formConfigPerson" :formOption="formOption" v-model="formPerson.data" :rules="rulesPerson" ref="person"> </byForm>
+    <el-dialog title="更多联系方式" v-model="openPerson" width="700">
+      <el-form :label-position="'top'" :model="formPerson.data" :rules="rulesPerson" ref="person">
+        <el-form-item label="联系人" prop="name">
+          <el-input v-model="formPerson.data.name" />
+        </el-form-item>
+        <el-form-item label="电子邮箱" prop="email">
+          <el-input v-model="formPerson.data.email" />
+        </el-form-item>
+        <el-form-item label="更多联系方式">
+          <div style="width: 100%">
+            <el-button type="primary" @click="clickAddMoreInformation">添 加</el-button>
+            <el-table :data="formPerson.data.contact" style="width: 100%; margin-top: 16px">
+              <el-table-column label="类型" width="180">
+                <template #default="{ row, $index }">
+                  <div style="width: 100%">
+                    <el-form-item :prop="'contact.' + $index + '.type'" :rules="rulesPerson.type" :inline-message="true">
+                      <el-select v-model="row.type" placeholder="请选择类型" style="width: 100%">
+                        <el-option v-for="item in contactType" :key="item.value" :label="item.label" :value="item.value" />
+                      </el-select>
+                    </el-form-item>
+                  </div>
+                </template>
+              </el-table-column>
+              <el-table-column label="联系号码">
+                <template #default="{ row, $index }">
+                  <div style="width: 100%">
+                    <el-form-item :prop="'contact.' + $index + '.contactNo'" :rules="rulesPerson.contactNo" :inline-message="true">
+                      <el-input v-model="row.contactNo" placeholder="请输入联系号码" />
+                    </el-form-item>
+                  </div>
+                </template>
+              </el-table-column>
+              <el-table-column align="center" label="操作" width="120" fixed="right">
+                <template #default="{ $index }">
+                  <el-button type="primary" link @click="clickInformationDelete($index)">删除</el-button>
+                </template>
+              </el-table-column>
+            </el-table>
+          </div>
+        </el-form-item>
+      </el-form>
       <template #footer>
         <el-button @click="openPerson = false" size="large">取 消</el-button>
-        <el-button type="primary" @click="submitPerson('person')" size="large">确 定</el-button>
+        <el-button type="primary" @click="submitPerson()" size="large">确 定</el-button>
+      </template>
+    </el-dialog>
+
+    <el-dialog title="分配" v-model="openAllocation" width="300">
+      <byForm :formConfig="formConfigAllocation" :formOption="formOption" v-model="formAllocation.data" ref="allocation"> </byForm>
+      <template #footer>
+        <el-button @click="openAllocation = false" size="large">取 消</el-button>
+        <el-button type="primary" @click="submitAllocation()" size="large">确 定</el-button>
+      </template>
+    </el-dialog>
+
+    <el-dialog title="添加跟进记录" v-if="openFollow" v-model="openFollow" width="500" destroy-on-close>
+      <byForm :formConfig="formConfigAFollow" :formOption="formOption" v-model="formFollow.data" :rules="rulesFollow" ref="follow">
+        <template #fileSlot>
+          <div style="width: 100%">
+            <el-upload
+              v-model:fileList="fileList"
+              action="https://winfaster.obs.cn-south-1.myhuaweicloud.com"
+              :data="uploadData"
+              multiple
+              :before-upload="uploadFile"
+              :on-preview="onPreviewFile">
+              <el-button type="primary">文件上传</el-button>
+            </el-upload>
+          </div>
+        </template>
+      </byForm>
+      <template #footer>
+        <el-button @click="openFollow = false" size="large">取 消</el-button>
+        <el-button type="primary" @click="submitFollow()" size="large">确 定</el-button>
+      </template>
+    </el-dialog>
+
+    <el-dialog title="跟进记录" v-if="openRecordMore" v-model="openRecordMore" width="800" destroy-on-close>
+      <div>
+        <div style="padding: 8px 0">
+          <el-button type="primary" @click="clickFollowUp(rowData)" plain>添加跟进记录</el-button>
+        </div>
+        <div style="padding-top: 16px">
+          <div v-infinite-scroll="infiniteScroll" class="infinite-scroll" :infinite-scroll-disabled="judgeTotal()">
+            <el-timeline>
+              <el-timeline-item v-for="(record, index) in recordList" :key="index" :timestamp="record.createTime" hide-timestamp>
+                <div>
+                  <div style="padding: 0 0 8px 0; display: flex; justify-content: space-between">
+                    <span>{{ dictValueLabel(record.createUser, userList) }}</span>
+                    <span>{{ record.createTime }}</span>
+                  </div>
+                  <div style="word-wrap: break-word; margin: 8px 0" v-html="getStyle(record.content)" v-if="record.content"></div>
+                  <div style="margin: 8px 0" v-else>跟进记录:</div>
+                  <div style="margin: 8px 0; display: flex" v-if="record.fileList && record.fileList.length > 0">
+                    <div style="width: 36px">附件:</div>
+                    <div style="width: calc(100% - 36px)">
+                      <div v-for="(file, index) in record.fileList" :key="index">
+                        <a style="color: #409eff; cursor: pointer" @click="openFile(file.fileUrl)">{{ file.fileName }}</a>
+                      </div>
+                    </div>
+                  </div>
+                </div>
+              </el-timeline-item>
+            </el-timeline>
+          </div>
+        </div>
+      </div>
+      <template #footer>
+        <el-button @click="openRecordMore = false" size="large">关 闭</el-button>
       </template>
     </el-dialog>
   </div>
@@ -93,9 +291,14 @@ const loading = ref(false);
 const loadingOperation = ref(false);
 const submitLoading = ref(false);
 const openPerson = ref(false);
+const openAllocation = ref(false);
+const customerTag = ref([]);
 const customerSource = ref([]);
 const customerStatus = ref([]);
+const contactType = ref([]);
 const userList = ref([]);
+const fileList = ref([]);
+const uploadData = ref({});
 const sourceList = ref({
   data: [],
   pagination: {
@@ -110,6 +313,20 @@ const sourceList = ref({
 const selectConfig = computed(() => {
   return [
     {
+      label: "客户状态",
+      prop: "type",
+      data: [
+        {
+          label: "公海",
+          value: "0",
+        },
+        {
+          label: "私海",
+          value: "1",
+        },
+      ],
+    },
+    {
       label: "客户来源",
       prop: "source",
       data: customerSource.value,
@@ -127,46 +344,60 @@ const config = computed(() => {
       attrs: {
         label: "客户名称",
         prop: "name",
+        slot: "name",
+        fixed: "left",
+        width: 160,
       },
     },
-
     {
       attrs: {
         label: "所在城市",
         slot: "address",
-        width: 200,
+        width: 160,
       },
     },
     {
       attrs: {
         label: "客户代码",
         prop: "customerCode",
-        width: 140,
+        width: 120,
       },
     },
     {
       attrs: {
         label: "客户来源",
         prop: "source",
-        width: 140,
+        width: 120,
       },
       render(type) {
-        let data = customerSource.value.filter((item) => item.value == type);
-        if (data && data.length > 0) {
-          return data[0].label;
-        } else {
-          return "";
-        }
+        return proxy.dictValueLabel(type, customerSource.value);
       },
     },
     {
       attrs: {
         label: "客户类型",
         prop: "status",
+        width: 120,
+      },
+      render(type) {
+        return proxy.dictValueLabel(type, customerStatus.value);
+      },
+    },
+    {
+      attrs: {
+        label: "客户标签",
+        slot: "tags",
+        width: 180,
+      },
+    },
+    {
+      attrs: {
+        label: "业务员",
+        prop: "userId",
         width: 140,
       },
       render(type) {
-        let data = customerStatus.value.filter((item) => item.value == type);
+        let data = userList.value.filter((item) => item.value == type);
         if (data && data.length > 0) {
           return data[0].label;
         } else {
@@ -176,14 +407,33 @@ const config = computed(() => {
     },
     {
       attrs: {
+        label: "跟进",
+        slot: "follow",
+        "min-width": 440,
+      },
+    },
+    {
+      attrs: {
         label: "操作",
-        width: "100",
+        width: 140,
         align: "center",
+        fixed: "right",
       },
       renderHTML(row) {
         return [
           {
             attrs: {
+              label: "跟进",
+              type: "primary",
+              text: true,
+            },
+            el: "button",
+            click() {
+              clickFollowUp(row);
+            },
+          },
+          {
+            attrs: {
               label: "认领客户",
               type: "primary",
               text: true,
@@ -217,6 +467,7 @@ const config = computed(() => {
 });
 let modalType = ref("add");
 let dialogVisible = ref(false);
+let openFollow = ref(false);
 let formData = reactive({
   data: {
     countryId: "China",
@@ -225,6 +476,12 @@ let formData = reactive({
 let formPerson = reactive({
   data: {},
 });
+let formAllocation = reactive({
+  data: {},
+});
+let formFollow = reactive({
+  data: {},
+});
 const formOption = reactive({
   inline: true,
   labelWidth: 100,
@@ -244,8 +501,8 @@ const formConfig = computed(() => {
     {
       type: "slot",
       slotName: "address",
-      label: "详细地址",
       prop: "countryId",
+      label: "详细地址",
     },
     {
       type: "input",
@@ -277,6 +534,17 @@ const formConfig = computed(() => {
       data: userList.value,
     },
     {
+      type: "select",
+      label: "客户标签",
+      prop: "tags",
+      itemWidth: 100,
+      multiple: true,
+      data: customerTag.value,
+      style: {
+        width: "100%",
+      },
+    },
+    {
       type: "slot",
       slotName: "person",
       label: "客户联系人",
@@ -285,83 +553,75 @@ const formConfig = computed(() => {
 });
 let rules = ref({
   name: [{ required: true, message: "请输入客户名称", trigger: "blur" }],
+  name2: [{ required: true, message: "请输入联系人", trigger: "blur" }],
+  email: [{ required: true, message: "请输入电子邮箱", trigger: "blur" }],
   countryId: [{ required: true, message: "请选择国家", trigger: "change" }],
   provinceId: [{ required: true, message: "请选择省/州", trigger: "change" }],
   cityId: [{ required: true, message: "请选择城市", trigger: "change" }],
   source: [{ required: true, message: "请选择客户来源", trigger: "change" }],
   status: [{ required: true, message: "请选择类型", trigger: "change" }],
 });
-const formConfigPerson = computed(() => {
+const formConfigAllocation = computed(() => {
   return [
     {
-      type: "input",
-      prop: "name",
-      label: "联系人名称",
-      required: true,
-      itemWidth: 100,
-      itemType: "text",
-    },
-    {
-      type: "input",
-      prop: "phone",
-      label: "联系人电话",
-      required: true,
+      type: "select",
+      label: "业务员",
+      prop: "userId",
       itemWidth: 100,
-      itemType: "text",
+      data: userList.value,
     },
   ];
 });
-const configPerson = computed(() => {
+const formConfigAFollow = computed(() => {
   return [
     {
-      attrs: {
-        label: "联系人名称",
-        prop: "name",
-        width: 150,
-      },
+      type: "date",
+      itemType: "datetime",
+      label: "跟进时间",
+      prop: "date",
+      itemWidth: 100,
     },
     {
-      attrs: {
-        label: "联系人电话",
-        prop: "phone",
-        width: 440,
-      },
+      type: "input",
+      itemType: "textarea",
+      label: "跟进内容",
+      prop: "content",
+      itemWidth: 100,
     },
     {
-      attrs: {
-        label: "操作",
-        width: "100",
-        align: "center",
-      },
-      renderHTML(row) {
-        return [
-          {
-            attrs: {
-              label: "删除",
-              type: "primary",
-              text: true,
-            },
-            el: "button",
-            click() {
-              formData.data.customerUserList.splice(row.$index, 1);
-            },
-          },
-        ];
-      },
+      type: "slot",
+      label: "上传附件",
+      prop: "fileList",
+      slotName: "fileSlot",
     },
   ];
 });
-
 let rulesPerson = ref({
-  name: [{ required: true, message: "请输入联系人名称", trigger: "blur" }],
-  phone: [{ required: true, message: "请输入联系人电话", trigger: "blur" }],
+  name: [{ required: true, message: "请输入联系人", trigger: "blur" }],
+  email: [{ required: true, message: "请输入电子邮箱", trigger: "blur" }],
+  type: [{ required: true, message: "请选择类型", trigger: "change" }],
+  contactNo: [{ required: true, message: "请输入联系号码", trigger: "blur" }],
+});
+let rulesFollow = ref({
+  date: [{ required: true, message: "请选择跟进时间", trigger: "change" }],
+  content: [{ required: true, message: "请输入跟进内容", trigger: "blur" }],
 });
 const submit = ref(null);
 const person = ref(null);
+const allocation = ref(null);
+const follow = ref(null);
 const getList = async (req) => {
   sourceList.value.pagination = { ...sourceList.value.pagination, ...req };
   loading.value = true;
   proxy.post("/customer/page", sourceList.value.pagination).then((res) => {
+    res.rows.forEach((x) => {
+      x.addTagShow = false;
+      if (x.tag) {
+        x.tag = x.tag.split(",");
+      } else {
+        x.tag = [];
+      }
+    });
     sourceList.value.data = res.rows;
     sourceList.value.pagination.total = res.total;
     setTimeout(() => {
@@ -373,11 +633,28 @@ const openModal = () => {
   modalType.value = "add";
   formData.data = {
     countryId: "China",
+    tags: [],
   };
   getCityData(formData.data.countryId, "20");
   loadingOperation.value = false;
   dialogVisible.value = true;
 };
+const update = (row) => {
+  modalType.value = "edit";
+  loadingOperation.value = true;
+  proxy.post("/customer/detail", { id: row.id }).then((res) => {
+    if (res.tag) {
+      res.tags = res.tag.split(",");
+    } else {
+      res.tags = [];
+    }
+    formData.data = res;
+    getCityData(formData.data.countryId, "20");
+    getCityData(formData.data.provinceId, "30");
+    loadingOperation.value = false;
+  });
+  dialogVisible.value = true;
+};
 const countryData = ref([]);
 const provinceData = ref([]);
 const cityData = ref([]);
@@ -401,30 +678,64 @@ const getCityData = (id, type, isChange) => {
 };
 getCityData("0");
 const clickAddPerson = () => {
-  formPerson.data = {};
-  openPerson.value = true;
+  if (formData.data.customerUserList && formData.data.customerUserList.length > 0) {
+    formData.data.customerUserList.push({
+      name: "",
+      email: "",
+    });
+  } else {
+    formData.data.customerUserList = [
+      {
+        name: "",
+        email: "",
+      },
+    ];
+  }
 };
-const submitPerson = () => {
-  person.value.handleSubmit(() => {
-    if (formData.data.customerUserList && formData.data.customerUserList.length > 0) {
-      formData.data.customerUserList.push({
-        name: formPerson.data.name,
-        phone: formPerson.data.phone,
+const submitAllocation = () => {
+  allocation.value.handleSubmit(() => {
+    proxy.post("/customer/CustomerAllocation", formAllocation.data).then(() => {
+      ElMessage({
+        message: "分配成功",
+        type: "success",
+      });
+      openAllocation.value = false;
+      getList();
+    });
+  });
+};
+const submitFollow = () => {
+  follow.value.handleSubmit(() => {
+    if (fileList.value && fileList.value.length > 0) {
+      formFollow.data.fileList = fileList.value.map((item) => {
+        return {
+          id: item.raw.id,
+          fileName: item.raw.fileName,
+          fileUrl: item.raw.fileUrl,
+        };
       });
     } else {
-      formData.data.customerUserList = [
-        {
-          name: formPerson.data.name,
-          phone: formPerson.data.phone,
-        },
-      ];
+      formFollow.data.fileList = [];
     }
-    openPerson.value = false;
+    proxy.post("/customerFollowRecords/" + modalType.value, formFollow.data).then(
+      () => {
+        ElMessage({
+          message: modalType.value == "add" ? "添加成功" : "编辑成功",
+          type: "success",
+        });
+        openFollow.value = false;
+        getList();
+      },
+      (err) => {
+        console.log(err);
+      }
+    );
   });
 };
 const submitForm = () => {
   submit.value.handleSubmit(() => {
     if (formData.data.customerUserList && formData.data.customerUserList.length > 0) {
+      formData.data.tag = formData.data.tags.join(",");
       submitLoading.value = true;
       proxy.post("/customer/" + modalType.value, formData.data).then(
         () => {
@@ -447,36 +758,24 @@ const submitForm = () => {
   });
 };
 const getDict = () => {
-  proxy
-    .post("/dictTenantData/page", {
-      pageNum: 1,
-      pageSize: 999,
-      dictCode: "customer_source",
-      tenantId: useUserStore().user.tenantId,
-    })
-    .then((res) => {
-      customerSource.value = res.rows.map((item) => {
-        return {
-          label: item.dictValue,
-          value: item.dictKey,
-        };
-      });
-    });
-  proxy
-    .post("/dictTenantData/page", {
-      pageNum: 1,
-      pageSize: 999,
-      dictCode: "customer_status",
-      tenantId: useUserStore().user.tenantId,
-    })
-    .then((res) => {
-      customerStatus.value = res.rows.map((item) => {
-        return {
-          label: item.dictValue,
-          value: item.dictKey,
-        };
-      });
-    });
+  proxy.getDictOne(["customer_tag", "customer_source", "customer_status", "contact_type"]).then((res) => {
+    customerTag.value = res["customer_tag"].map((x) => ({
+      label: x.dictValue,
+      value: x.dictKey,
+    }));
+    customerSource.value = res["customer_source"].map((x) => ({
+      label: x.dictValue,
+      value: x.dictKey,
+    }));
+    customerStatus.value = res["customer_status"].map((x) => ({
+      label: x.dictValue,
+      value: x.dictKey,
+    }));
+    contactType.value = res["contact_type"].map((x) => ({
+      label: x.dictValue,
+      value: x.dictKey,
+    }));
+  });
   proxy
     .get("/tenantUser/list", {
       pageNum: 1,
@@ -494,10 +793,230 @@ const getDict = () => {
 };
 getDict();
 getList();
+const handleClickName = (row) => {
+  proxy.$router.push({
+    path: "/customer/customer/portrait",
+    query: {
+      id: row.id,
+    },
+  });
+};
+const deleteFollow = (data) => {
+  ElMessageBox.confirm("是否确认删除该跟进?", "提示", {
+    confirmButtonText: "确定",
+    cancelButtonText: "取消",
+    type: "warning",
+  }).then(() => {
+    proxy
+      .post("/customerFollowRecords/delete", {
+        id: data.id,
+      })
+      .then(() => {
+        ElMessage({
+          message: "删除成功",
+          type: "success",
+        });
+        getList();
+      });
+  });
+};
+const addTag = ref("");
+const judgeTagSelect = (data, val) => {
+  if (data && data.length > 0) {
+    if (data.includes(val)) {
+      return true;
+    }
+  }
+  return false;
+};
+const changeTag = (val, item) => {
+  let data = {
+    id: item.id,
+    tag: JSON.parse(JSON.stringify(item.tag)),
+  };
+  data.tag.push(val);
+  data.tag = data.tag.join(",");
+  proxy.post("/customer/editTag", data).then(() => {
+    ElMessage({
+      message: "添加成功",
+      type: "success",
+    });
+    item.addTagShow = false;
+    addTag.value = "";
+    getList();
+  });
+};
+const tagClose = (val, item) => {
+  let data = {
+    id: item.id,
+    tag: JSON.parse(JSON.stringify(item.tag)),
+  };
+  data.tag = data.tag.filter((row) => row !== val);
+  if (data.tag && data.tag.length > 0) {
+    data.tag = data.tag.join(",");
+  } else {
+    data.tag = "";
+  }
+  proxy.post("/customer/editTag", data).then(() => {
+    ElMessage({
+      message: "添加成功",
+      type: "success",
+    });
+    item.addTagShow = false;
+    addTag.value = "";
+    getList();
+  });
+};
+const showSelect = (item) => {
+  item.addTagShow = true;
+};
+const clickFollowUp = (item) => {
+  formFollow.data = {
+    customerId: item.id,
+    fileList: [],
+  };
+  fileList.value = [];
+  modalType.value = "add";
+  openFollow.value = true;
+  openRecordMore.value = false;
+};
+const uploadFile = async (file) => {
+  const res = await proxy.post("/fileInfo/getSing", { fileName: file.name });
+  uploadData.value = res.uploadBody;
+  file.id = res.id;
+  file.fileName = res.fileName;
+  file.fileUrl = res.fileUrl;
+  return true;
+};
+const onPreviewFile = (file) => {
+  window.open(file.raw.fileUrl, "_blank");
+};
+const getStyle = (val) => {
+  if (val) {
+    return "跟进记录: " + val.replace(/\n|\r\n/g, "<br>");
+  } else {
+    return "";
+  }
+};
+const recordShow = (item) => {
+  if (!(item.fileList && item.fileList.length > 0)) {
+    proxy.post("/fileInfo/getList", { businessIdList: [item.id] }).then((fileObj) => {
+      item.fileList = fileObj[item.id] || [];
+    });
+  }
+};
+const openFile = (path) => {
+  window.open(path, "_blank");
+};
+const judgeWidth = (item, index) => {
+  if (item && item.id) {
+    let dom = document.querySelector(".getWidth" + item.id);
+    if (dom) {
+      let width = getComputedStyle(dom).width.replace("px", "");
+      let num = parseInt(width / 118);
+      let num2 = parseInt((width - 52) / 118);
+      if (num2 === num) {
+        if (index + 1 > num) {
+          return "display: none";
+        }
+      } else {
+        if (index + 1 >= num) {
+          return "display: none";
+        }
+      }
+    }
+  }
+  return "";
+};
+const recordList = ref([]);
+const rowData = ref({});
+const openRecordMore = ref(false);
+const queryParams = ref({
+  total: 0,
+  pageNum: 1,
+  pageSize: 10,
+  customerId: "",
+});
+const clickMore = (item) => {
+  if (openRecordMore.value === false) {
+    recordList.value = [];
+    rowData.value = item;
+    queryParams.value.customerId = item.id;
+  }
+  proxy.post("/customerFollowRecords/page", queryParams.value).then((res) => {
+    recordList.value = recordList.value.concat(res.rows);
+    queryParams.value.total = res.total;
+    proxy.post("/fileInfo/getList", { businessIdList: res.rows.map((rows) => rows.id) }).then((fileObj) => {
+      for (let i = 0; i < res.rows.length; i++) {
+        recordList.value[parseInt(i + (queryParams.value.pageNum - 1) * queryParams.value.pageSize)].fileList =
+          fileObj[recordList.value[parseInt(i + (queryParams.value.pageNum - 1) * queryParams.value.pageSize)].id] || [];
+      }
+    });
+  });
+  openRecordMore.value = true;
+};
+const infiniteScroll = () => {
+  queryParams.value.pageNum++;
+  clickMore();
+};
+const judgeTotal = () => {
+  if (queryParams.value.pageNum * queryParams.value.pageSize >= queryParams.value.total) {
+    return true;
+  }
+  return false;
+};
+const moreIndex = ref(0);
+const clickInformationMore = (item, index) => {
+  moreIndex.value = index;
+  if (item.contactJson) {
+    item.contact = JSON.parse(item.contactJson);
+  } else {
+    item.contact = [];
+  }
+  formPerson.data = JSON.parse(JSON.stringify(item));
+  openPerson.value = true;
+};
+const clickDelete = (index) => {
+  formData.data.customerUserList.splice(index, 1);
+};
+const clickAddMoreInformation = () => {
+  if (formPerson.data.contact && formPerson.data.contact.length > 0) {
+    formPerson.data.contact.push({
+      type: "",
+      contactNo: "",
+    });
+  } else {
+    formPerson.data.contact = [
+      {
+        type: "",
+        contactNo: "",
+      },
+    ];
+  }
+};
+const clickInformationDelete = (index) => {
+  formPerson.data.contact.splice(index, 1);
+};
+const submitPerson = () => {
+  person.value.validate((valid) => {
+    if (valid) {
+      formPerson.data.contactJson = JSON.stringify(formPerson.data.contact);
+      formData.data.customerUserList[moreIndex.value] = formPerson.data;
+      openPerson.value = false;
+    }
+  });
+};
 </script>
 
 <style lang="scss" scoped>
 .tenant {
   padding: 20px;
 }
+.infinite-scroll {
+  max-height: calc(89vh - 94px - 70px - 58px - 16px);
+  overflow-y: auto;
+  &::-webkit-scrollbar {
+    width: 0px;
+  }
+}
 </style>

+ 576 - 140
src/views/customer/privatesea/index.vue

@@ -11,7 +11,7 @@
         :action-list="[
           {
             text: '添加客户',
-            action: () => openModal('add'),
+            action: () => openModal(),
           },
         ]"
         @get-list="getList">
@@ -20,13 +20,83 @@
           <span v-if="item.provinceName"> ,{{ item.provinceName }}</span>
           <span v-if="item.cityName"> ,{{ item.cityName }}</span>
         </template>
+        <template #name="{ item }">
+          <div style="cursor: pointer; color: #409eff" @click="handleClickName(item)">
+            {{ item.name }}
+          </div>
+        </template>
+        <template #tags="{ item }">
+          <div style="width: 100%">
+            <el-tag style="margin-right: 8px" type="success" v-for="(tag, index) in item.tag" closable :key="index" @close="tagClose(tag, item)">
+              {{ dictValueLabel(tag, customerTag) }}
+            </el-tag>
+            <el-select
+              v-if="item.addTagShow"
+              v-model="addTag"
+              style="width: 100%"
+              @change="
+                (val) => {
+                  return changeTag(val, item);
+                }
+              ">
+              <el-option v-for="tag in customerTag" :key="tag.value" :label="tag.label" :value="tag.value" :disabled="judgeTagSelect(item.tag, tag.value)" />
+            </el-select>
+            <el-tag style="cursor: pointer" type="success" @click="showSelect(item)" v-else> + </el-tag>
+          </div>
+        </template>
+        <template #follow="{ item }">
+          <div :class="'getWidth' + item.id" style="width: 100%">
+            <div style="width: 100%; display: flex">
+              <template v-if="item.customerFollowRecordsList && item.customerFollowRecordsList.length > 0">
+                <div
+                  :style="
+                    index > 2
+                      ? 'line-height: 32px; margin-right: 8px; padding: 0 8px; background-color: #eeeeee; border-radius: 4px; cursor: pointer; display: none'
+                      : 'line-height: 32px; margin-right: 8px; padding: 0 8px; background-color: #eeeeee; border-radius: 4px; cursor: pointer'
+                  "
+                  v-for="(record, index) in item.customerFollowRecordsList"
+                  :key="record.id">
+                  <el-popover placement="bottom" :width="300" trigger="hover" @show="recordShow(record)">
+                    <template #reference>
+                      <div>
+                        <span>{{ item.createTime.substr(0, 10) }}</span>
+                        <el-icon style="margin-left: 8px; transform: translateY(2px)" @click="deleteFollow(record)"><DeleteFilled /></el-icon>
+                      </div>
+                    </template>
+                    <template #default>
+                      <div style="width: 100%">
+                        <div style="color: #909399; margin: 8px 0">跟进时间: {{ record.createTime }}</div>
+                        <div style="word-wrap: break-word; margin: 8px 0" v-html="getStyle(record.content)" v-if="record.content"></div>
+                        <div v-else>跟进记录:</div>
+                        <div style="margin: 8px 0; display: flex" v-if="record.fileList && record.fileList.length > 0">
+                          <div style="width: 36px">附件:</div>
+                          <div style="width: calc(100% - 36px)">
+                            <div v-for="(file, index) in record.fileList" :key="index">
+                              <a style="color: #409eff; cursor: pointer" @click="openFile(file.fileUrl)">{{ file.fileName }}</a>
+                            </div>
+                          </div>
+                        </div>
+                      </div>
+                    </template>
+                  </el-popover>
+                </div>
+                <div
+                  style="line-height: 32px; margin-right: 8px; padding: 0 8px; background-color: #eeeeee; border-radius: 4px; cursor: pointer"
+                  @click="clickMore(item)"
+                  v-if="item.customerFollowRecordsList.length >= 3">
+                  更多
+                </div>
+              </template>
+            </div>
+          </div>
+        </template>
       </byTable>
     </div>
 
-    <el-dialog :title="modalType == 'add' ? '新增' : '编辑'" v-model="dialogVisible" width="800" v-loading="loadingOperation">
+    <el-dialog :title="modalType == 'add' ? '新增' : '编辑'" v-if="dialogVisible" v-model="dialogVisible" width="800" v-loading="loadingOperation">
       <byForm :formConfig="formConfig" :formOption="formOption" v-model="formData.data" :rules="rules" ref="submit">
         <template #address>
-          <el-row :gutter="10" style="width: 100%; margin-left: -15px">
+          <el-row style="width: 100%">
             <el-col :span="8">
               <el-form-item prop="countryId">
                 <el-select v-model="formData.data.countryId" placeholder="国家" @change="(val) => getCityData(val, '20', true)">
@@ -49,7 +119,7 @@
               </el-form-item>
             </el-col>
           </el-row>
-          <el-row style="margin-top: 20px; width: 100%; margin-left: -10px">
+          <el-row style="margin-top: 20px; width: 100%">
             <el-col :span="24">
               <el-form-item prop="address">
                 <el-input v-model="formData.data.address" type="textarea"> </el-input>
@@ -57,25 +127,145 @@
             </el-col>
           </el-row>
         </template>
-
         <template #person>
-          <div>
-            <el-button type="primary" @click="clickAddPerson"> 添加 </el-button>
-            <byTable :source="formData.data.customerUserList" :config="configPerson" hideSearch hidePagination> </byTable>
+          <div style="width: 100%">
+            <el-button type="primary" @click="clickAddPerson">添 加</el-button>
+            <el-table :data="formData.data.customerUserList" style="width: 100%; margin-top: 16px">
+              <el-table-column label="联系人" width="160">
+                <template #default="{ row, $index }">
+                  <div style="width: 100%">
+                    <el-form-item :prop="'customerUserList.' + $index + '.name'" :rules="rules.name2" :inline-message="true">
+                      <el-input v-model="row.name" placeholder="请输入联系人" />
+                    </el-form-item>
+                  </div>
+                </template>
+              </el-table-column>
+              <el-table-column label="电子邮箱">
+                <template #default="{ row, $index }">
+                  <div style="width: 100%">
+                    <el-form-item :prop="'customerUserList.' + $index + '.email'" :rules="rules.email" :inline-message="true">
+                      <el-input v-model="row.email" placeholder="请输入电子邮箱" />
+                    </el-form-item>
+                  </div>
+                </template>
+              </el-table-column>
+              <el-table-column align="center" label="操作" width="120" fixed="right">
+                <template #default="{ row, $index }">
+                  <el-button type="primary" link @click="clickInformationMore(row, $index)">更多</el-button>
+                  <el-button type="primary" link @click="clickDelete($index)">删除</el-button>
+                </template>
+              </el-table-column>
+            </el-table>
           </div>
         </template>
       </byForm>
       <template #footer>
         <el-button @click="dialogVisible = false" size="large">取 消</el-button>
-        <el-button type="primary" @click="submitForm('submit')" size="large" :loading="submitLoading">确 定</el-button>
+        <el-button type="primary" @click="submitForm()" size="large" :loading="submitLoading">确 定</el-button>
       </template>
     </el-dialog>
 
-    <el-dialog title="添加联系人" v-model="openPerson" width="400" v-loading="openPerson">
-      <byForm :formConfig="formConfigPerson" :formOption="formOption" v-model="formPerson.data" :rules="rulesPerson" ref="person"> </byForm>
+    <el-dialog title="更多联系方式" v-model="openPerson" width="700">
+      <el-form :label-position="'top'" :model="formPerson.data" :rules="rulesPerson" ref="person">
+        <el-form-item label="联系人" prop="name">
+          <el-input v-model="formPerson.data.name" />
+        </el-form-item>
+        <el-form-item label="电子邮箱" prop="email">
+          <el-input v-model="formPerson.data.email" />
+        </el-form-item>
+        <el-form-item label="更多联系方式">
+          <div style="width: 100%">
+            <el-button type="primary" @click="clickAddMoreInformation">添 加</el-button>
+            <el-table :data="formPerson.data.contact" style="width: 100%; margin-top: 16px">
+              <el-table-column label="类型" width="180">
+                <template #default="{ row, $index }">
+                  <div style="width: 100%">
+                    <el-form-item :prop="'contact.' + $index + '.type'" :rules="rulesPerson.type" :inline-message="true">
+                      <el-select v-model="row.type" placeholder="请选择类型" style="width: 100%">
+                        <el-option v-for="item in contactType" :key="item.value" :label="item.label" :value="item.value" />
+                      </el-select>
+                    </el-form-item>
+                  </div>
+                </template>
+              </el-table-column>
+              <el-table-column label="联系号码">
+                <template #default="{ row, $index }">
+                  <div style="width: 100%">
+                    <el-form-item :prop="'contact.' + $index + '.contactNo'" :rules="rulesPerson.contactNo" :inline-message="true">
+                      <el-input v-model="row.contactNo" placeholder="请输入联系号码" />
+                    </el-form-item>
+                  </div>
+                </template>
+              </el-table-column>
+              <el-table-column align="center" label="操作" width="120" fixed="right">
+                <template #default="{ $index }">
+                  <el-button type="primary" link @click="clickInformationDelete($index)">删除</el-button>
+                </template>
+              </el-table-column>
+            </el-table>
+          </div>
+        </el-form-item>
+      </el-form>
       <template #footer>
         <el-button @click="openPerson = false" size="large">取 消</el-button>
-        <el-button type="primary" @click="submitPerson('person')" size="large">确 定</el-button>
+        <el-button type="primary" @click="submitPerson()" size="large">确 定</el-button>
+      </template>
+    </el-dialog>
+
+    <el-dialog title="添加跟进记录" v-if="openFollow" v-model="openFollow" width="500" destroy-on-close>
+      <byForm :formConfig="formConfigAFollow" :formOption="formOption" v-model="formFollow.data" :rules="rulesFollow" ref="follow">
+        <template #fileSlot>
+          <div style="width: 100%">
+            <el-upload
+              v-model:fileList="fileList"
+              action="https://winfaster.obs.cn-south-1.myhuaweicloud.com"
+              :data="uploadData"
+              multiple
+              :before-upload="uploadFile"
+              :on-preview="onPreviewFile">
+              <el-button type="primary">文件上传</el-button>
+            </el-upload>
+          </div>
+        </template>
+      </byForm>
+      <template #footer>
+        <el-button @click="openFollow = false" size="large">取 消</el-button>
+        <el-button type="primary" @click="submitFollow()" size="large">确 定</el-button>
+      </template>
+    </el-dialog>
+
+    <el-dialog title="跟进记录" v-if="openRecordMore" v-model="openRecordMore" width="800" destroy-on-close>
+      <div>
+        <div style="padding: 8px 0">
+          <el-button type="primary" @click="clickFollowUp(rowData)" plain>添加跟进记录</el-button>
+        </div>
+        <div style="padding-top: 16px">
+          <div v-infinite-scroll="infiniteScroll" class="infinite-scroll" :infinite-scroll-disabled="judgeTotal()">
+            <el-timeline>
+              <el-timeline-item v-for="(record, index) in recordList" :key="index" :timestamp="record.createTime" hide-timestamp>
+                <div>
+                  <div style="padding: 0 0 8px 0; display: flex; justify-content: space-between">
+                    <span>{{ dictValueLabel(record.createUser, userList) }}</span>
+                    <span>{{ record.createTime }}</span>
+                  </div>
+                  <div style="word-wrap: break-word; margin: 8px 0" v-html="getStyle(record.content)" v-if="record.content"></div>
+                  <div style="margin: 8px 0" v-else>跟进记录:</div>
+                  <div style="margin: 8px 0; display: flex" v-if="record.fileList && record.fileList.length > 0">
+                    <div style="width: 36px">附件:</div>
+                    <div style="width: calc(100% - 36px)">
+                      <div v-for="(file, index) in record.fileList" :key="index">
+                        <a style="color: #409eff; cursor: pointer" @click="openFile(file.fileUrl)">{{ file.fileName }}</a>
+                      </div>
+                    </div>
+                  </div>
+                </div>
+              </el-timeline-item>
+            </el-timeline>
+          </div>
+        </div>
+      </div>
+      <template #footer>
+        <el-button @click="openRecordMore = false" size="large">关 闭</el-button>
       </template>
     </el-dialog>
   </div>
@@ -93,9 +283,13 @@ const loading = ref(false);
 const loadingOperation = ref(false);
 const submitLoading = ref(false);
 const openPerson = ref(false);
+const customerTag = ref([]);
 const customerSource = ref([]);
 const customerStatus = ref([]);
+const contactType = ref([]);
 const userList = ref([]);
+const fileList = ref([]);
+const uploadData = ref({});
 const sourceList = ref({
   data: [],
   pagination: {
@@ -104,11 +298,26 @@ const sourceList = ref({
     pageSize: 10,
     status: "",
     source: "",
+    type: "",
   },
 });
 const selectConfig = computed(() => {
   return [
     {
+      label: "客户状态",
+      prop: "type",
+      data: [
+        {
+          label: "公海",
+          value: "0",
+        },
+        {
+          label: "私海",
+          value: "1",
+        },
+      ],
+    },
+    {
       label: "客户来源",
       prop: "source",
       data: customerSource.value,
@@ -126,46 +335,60 @@ const config = computed(() => {
       attrs: {
         label: "客户名称",
         prop: "name",
+        slot: "name",
+        fixed: "left",
+        width: 160,
       },
     },
-
     {
       attrs: {
         label: "所在城市",
         slot: "address",
-        width: 200,
+        width: 160,
       },
     },
     {
       attrs: {
         label: "客户代码",
         prop: "customerCode",
-        width: 140,
+        width: 120,
       },
     },
     {
       attrs: {
         label: "客户来源",
         prop: "source",
-        width: 140,
+        width: 120,
       },
       render(type) {
-        let data = customerSource.value.filter((item) => item.value == type);
-        if (data && data.length > 0) {
-          return data[0].label;
-        } else {
-          return "";
-        }
+        return proxy.dictValueLabel(type, customerSource.value);
       },
     },
     {
       attrs: {
         label: "客户类型",
         prop: "status",
+        width: 120,
+      },
+      render(type) {
+        return proxy.dictValueLabel(type, customerStatus.value);
+      },
+    },
+    {
+      attrs: {
+        label: "客户标签",
+        slot: "tags",
+        width: 180,
+      },
+    },
+    {
+      attrs: {
+        label: "业务员",
+        prop: "userId",
         width: 140,
       },
       render(type) {
-        let data = customerStatus.value.filter((item) => item.value == type);
+        let data = userList.value.filter((item) => item.value == type);
         if (data && data.length > 0) {
           return data[0].label;
         } else {
@@ -175,14 +398,33 @@ const config = computed(() => {
     },
     {
       attrs: {
+        label: "跟进",
+        slot: "follow",
+        "min-width": 440,
+      },
+    },
+    {
+      attrs: {
         label: "操作",
-        width: "100",
+        width: 140,
         align: "center",
+        fixed: "right",
       },
       renderHTML(row) {
         return [
           {
             attrs: {
+              label: "跟进",
+              type: "primary",
+              text: true,
+            },
+            el: "button",
+            click() {
+              clickFollowUp(row);
+            },
+          },
+          {
+            attrs: {
               label: "退回公海",
               type: "primary",
               text: true,
@@ -216,6 +458,7 @@ const config = computed(() => {
 });
 let modalType = ref("add");
 let dialogVisible = ref(false);
+let openFollow = ref(false);
 let formData = reactive({
   data: {
     countryId: "China",
@@ -224,6 +467,12 @@ let formData = reactive({
 let formPerson = reactive({
   data: {},
 });
+let formAllocation = reactive({
+  data: {},
+});
+let formFollow = reactive({
+  data: {},
+});
 const formOption = reactive({
   inline: true,
   labelWidth: 100,
@@ -243,8 +492,8 @@ const formConfig = computed(() => {
     {
       type: "slot",
       slotName: "address",
-      label: "详细地址",
       prop: "countryId",
+      label: "详细地址",
     },
     {
       type: "input",
@@ -274,6 +523,18 @@ const formConfig = computed(() => {
       prop: "userId",
       itemWidth: 100,
       data: userList.value,
+      disabled: true,
+    },
+    {
+      type: "select",
+      label: "客户标签",
+      prop: "tags",
+      itemWidth: 100,
+      multiple: true,
+      data: customerTag.value,
+      style: {
+        width: "100%",
+      },
     },
     {
       type: "slot",
@@ -284,83 +545,64 @@ const formConfig = computed(() => {
 });
 let rules = ref({
   name: [{ required: true, message: "请输入客户名称", trigger: "blur" }],
+  name2: [{ required: true, message: "请输入联系人", trigger: "blur" }],
+  email: [{ required: true, message: "请输入电子邮箱", trigger: "blur" }],
   countryId: [{ required: true, message: "请选择国家", trigger: "change" }],
   provinceId: [{ required: true, message: "请选择省/州", trigger: "change" }],
   cityId: [{ required: true, message: "请选择城市", trigger: "change" }],
   source: [{ required: true, message: "请选择客户来源", trigger: "change" }],
   status: [{ required: true, message: "请选择类型", trigger: "change" }],
 });
-const formConfigPerson = computed(() => {
+const formConfigAFollow = computed(() => {
   return [
     {
-      type: "input",
-      prop: "name",
-      label: "联系人名称",
-      required: true,
+      type: "date",
+      itemType: "datetime",
+      label: "跟进时间",
+      prop: "date",
       itemWidth: 100,
-      itemType: "text",
     },
     {
       type: "input",
-      prop: "phone",
-      label: "联系人电话",
-      required: true,
+      itemType: "textarea",
+      label: "跟进内容",
+      prop: "content",
       itemWidth: 100,
-      itemType: "text",
-    },
-  ];
-});
-const configPerson = computed(() => {
-  return [
-    {
-      attrs: {
-        label: "联系人名称",
-        prop: "name",
-        width: 150,
-      },
     },
     {
-      attrs: {
-        label: "联系人电话",
-        prop: "phone",
-        width: 440,
-      },
-    },
-    {
-      attrs: {
-        label: "操作",
-        width: "100",
-        align: "center",
-      },
-      renderHTML(row) {
-        return [
-          {
-            attrs: {
-              label: "删除",
-              type: "primary",
-              text: true,
-            },
-            el: "button",
-            click() {
-              formData.data.customerUserList.splice(row.$index, 1);
-            },
-          },
-        ];
-      },
+      type: "slot",
+      label: "上传附件",
+      prop: "fileList",
+      slotName: "fileSlot",
     },
   ];
 });
-
 let rulesPerson = ref({
-  name: [{ required: true, message: "请输入联系人名称", trigger: "blur" }],
-  phone: [{ required: true, message: "请输入联系人电话", trigger: "blur" }],
+  name: [{ required: true, message: "请输入联系人", trigger: "blur" }],
+  email: [{ required: true, message: "请输入电子邮箱", trigger: "blur" }],
+  type: [{ required: true, message: "请选择类型", trigger: "change" }],
+  contactNo: [{ required: true, message: "请输入联系号码", trigger: "blur" }],
+});
+let rulesFollow = ref({
+  date: [{ required: true, message: "请选择跟进时间", trigger: "change" }],
+  content: [{ required: true, message: "请输入跟进内容", trigger: "blur" }],
 });
 const submit = ref(null);
 const person = ref(null);
+const allocation = ref(null);
+const follow = ref(null);
 const getList = async (req) => {
   sourceList.value.pagination = { ...sourceList.value.pagination, ...req };
   loading.value = true;
-  proxy.post("/customer/privateSeaPage", sourceList.value.pagination).then((res) => {
+  proxy.post("customer/privateSeaPage", sourceList.value.pagination).then((res) => {
+    res.rows.forEach((x) => {
+      x.addTagShow = false;
+      if (x.tag) {
+        x.tag = x.tag.split(",");
+      } else {
+        x.tag = [];
+      }
+    });
     sourceList.value.data = res.rows;
     sourceList.value.pagination.total = res.total;
     setTimeout(() => {
@@ -372,29 +614,13 @@ const openModal = () => {
   modalType.value = "add";
   formData.data = {
     countryId: "China",
+    userId: useUserStore().user.userId,
+    tags: [],
   };
   getCityData(formData.data.countryId, "20");
   loadingOperation.value = false;
   dialogVisible.value = true;
 };
-const update = (row) => {
-  modalType.value = "edit";
-  loadingOperation.value = true;
-  proxy.post("/customer/detail", { id: row.id }).then((res) => {
-    res.customerUserList = res.customerUserList.map((item) => {
-      return {
-        name: item.name,
-        phone: item.phone,
-        customerId: item.customerId,
-      };
-    });
-    formData.data = res;
-    getCityData(formData.data.countryId, "20");
-    getCityData(formData.data.provinceId, "30");
-    loadingOperation.value = false;
-  });
-  dialogVisible.value = true;
-};
 const countryData = ref([]);
 const provinceData = ref([]);
 const cityData = ref([]);
@@ -418,30 +644,52 @@ const getCityData = (id, type, isChange) => {
 };
 getCityData("0");
 const clickAddPerson = () => {
-  formPerson.data = {};
-  openPerson.value = true;
+  if (formData.data.customerUserList && formData.data.customerUserList.length > 0) {
+    formData.data.customerUserList.push({
+      name: "",
+      email: "",
+    });
+  } else {
+    formData.data.customerUserList = [
+      {
+        name: "",
+        email: "",
+      },
+    ];
+  }
 };
-const submitPerson = () => {
-  person.value.handleSubmit(() => {
-    if (formData.data.customerUserList && formData.data.customerUserList.length > 0) {
-      formData.data.customerUserList.push({
-        name: formPerson.data.name,
-        phone: formPerson.data.phone,
+const submitFollow = () => {
+  follow.value.handleSubmit(() => {
+    if (fileList.value && fileList.value.length > 0) {
+      formFollow.data.fileList = fileList.value.map((item) => {
+        return {
+          id: item.raw.id,
+          fileName: item.raw.fileName,
+          fileUrl: item.raw.fileUrl,
+        };
       });
     } else {
-      formData.data.customerUserList = [
-        {
-          name: formPerson.data.name,
-          phone: formPerson.data.phone,
-        },
-      ];
+      formFollow.data.fileList = [];
     }
-    openPerson.value = false;
+    proxy.post("/customerFollowRecords/" + modalType.value, formFollow.data).then(
+      () => {
+        ElMessage({
+          message: modalType.value == "add" ? "添加成功" : "编辑成功",
+          type: "success",
+        });
+        openFollow.value = false;
+        getList();
+      },
+      (err) => {
+        console.log(err);
+      }
+    );
   });
 };
 const submitForm = () => {
   submit.value.handleSubmit(() => {
     if (formData.data.customerUserList && formData.data.customerUserList.length > 0) {
+      formData.data.tag = formData.data.tags.join(",");
       submitLoading.value = true;
       proxy.post("/customer/" + modalType.value, formData.data).then(
         () => {
@@ -464,36 +712,24 @@ const submitForm = () => {
   });
 };
 const getDict = () => {
-  proxy
-    .post("/dictTenantData/page", {
-      pageNum: 1,
-      pageSize: 999,
-      dictCode: "customer_source",
-      tenantId: useUserStore().user.tenantId,
-    })
-    .then((res) => {
-      customerSource.value = res.rows.map((item) => {
-        return {
-          label: item.dictValue,
-          value: item.dictKey,
-        };
-      });
-    });
-  proxy
-    .post("/dictTenantData/page", {
-      pageNum: 1,
-      pageSize: 999,
-      dictCode: "customer_status",
-      tenantId: useUserStore().user.tenantId,
-    })
-    .then((res) => {
-      customerStatus.value = res.rows.map((item) => {
-        return {
-          label: item.dictValue,
-          value: item.dictKey,
-        };
-      });
-    });
+  proxy.getDictOne(["customer_tag", "customer_source", "customer_status", "contact_type"]).then((res) => {
+    customerTag.value = res["customer_tag"].map((x) => ({
+      label: x.dictValue,
+      value: x.dictKey,
+    }));
+    customerSource.value = res["customer_source"].map((x) => ({
+      label: x.dictValue,
+      value: x.dictKey,
+    }));
+    customerStatus.value = res["customer_status"].map((x) => ({
+      label: x.dictValue,
+      value: x.dictKey,
+    }));
+    contactType.value = res["contact_type"].map((x) => ({
+      label: x.dictValue,
+      value: x.dictKey,
+    }));
+  });
   proxy
     .get("/tenantUser/list", {
       pageNum: 1,
@@ -511,10 +747,210 @@ const getDict = () => {
 };
 getDict();
 getList();
+const handleClickName = (row) => {
+  proxy.$router.push({
+    path: "/customer/customer/portrait",
+    query: {
+      id: row.id,
+    },
+  });
+};
+const deleteFollow = (data) => {
+  ElMessageBox.confirm("是否确认删除该跟进?", "提示", {
+    confirmButtonText: "确定",
+    cancelButtonText: "取消",
+    type: "warning",
+  }).then(() => {
+    proxy
+      .post("/customerFollowRecords/delete", {
+        id: data.id,
+      })
+      .then(() => {
+        ElMessage({
+          message: "删除成功",
+          type: "success",
+        });
+        getList();
+      });
+  });
+};
+const addTag = ref("");
+const judgeTagSelect = (data, val) => {
+  if (data && data.length > 0) {
+    if (data.includes(val)) {
+      return true;
+    }
+  }
+  return false;
+};
+const changeTag = (val, item) => {
+  let data = {
+    id: item.id,
+    tag: JSON.parse(JSON.stringify(item.tag)),
+  };
+  data.tag.push(val);
+  data.tag = data.tag.join(",");
+  proxy.post("/customer/editTag", data).then(() => {
+    ElMessage({
+      message: "添加成功",
+      type: "success",
+    });
+    item.addTagShow = false;
+    addTag.value = "";
+    getList();
+  });
+};
+const tagClose = (val, item) => {
+  let data = {
+    id: item.id,
+    tag: JSON.parse(JSON.stringify(item.tag)),
+  };
+  data.tag = data.tag.filter((row) => row !== val);
+  if (data.tag && data.tag.length > 0) {
+    data.tag = data.tag.join(",");
+  } else {
+    data.tag = "";
+  }
+  proxy.post("/customer/editTag", data).then(() => {
+    ElMessage({
+      message: "添加成功",
+      type: "success",
+    });
+    item.addTagShow = false;
+    addTag.value = "";
+    getList();
+  });
+};
+const showSelect = (item) => {
+  item.addTagShow = true;
+};
+const clickFollowUp = (item) => {
+  formFollow.data = {
+    customerId: item.id,
+    fileList: [],
+  };
+  fileList.value = [];
+  modalType.value = "add";
+  openFollow.value = true;
+  openRecordMore.value = false;
+};
+const uploadFile = async (file) => {
+  const res = await proxy.post("/fileInfo/getSing", { fileName: file.name });
+  uploadData.value = res.uploadBody;
+  file.id = res.id;
+  file.fileName = res.fileName;
+  file.fileUrl = res.fileUrl;
+  return true;
+};
+const onPreviewFile = (file) => {
+  window.open(file.raw.fileUrl, "_blank");
+};
+const getStyle = (val) => {
+  if (val) {
+    return "跟进记录: " + val.replace(/\n|\r\n/g, "<br>");
+  } else {
+    return "";
+  }
+};
+const recordShow = (item) => {
+  if (!(item.fileList && item.fileList.length > 0)) {
+    proxy.post("/fileInfo/getList", { businessIdList: [item.id] }).then((fileObj) => {
+      item.fileList = fileObj[item.id] || [];
+    });
+  }
+};
+const openFile = (path) => {
+  window.open(path, "_blank");
+};
+const recordList = ref([]);
+const rowData = ref({});
+const openRecordMore = ref(false);
+const queryParams = ref({
+  total: 0,
+  pageNum: 1,
+  pageSize: 10,
+  customerId: "",
+});
+const clickMore = (item) => {
+  if (openRecordMore.value === false) {
+    recordList.value = [];
+    rowData.value = item;
+    queryParams.value.customerId = item.id;
+  }
+  proxy.post("/customerFollowRecords/page", queryParams.value).then((res) => {
+    recordList.value = recordList.value.concat(res.rows);
+    queryParams.value.total = res.total;
+    proxy.post("/fileInfo/getList", { businessIdList: res.rows.map((rows) => rows.id) }).then((fileObj) => {
+      for (let i = 0; i < res.rows.length; i++) {
+        recordList.value[parseInt(i + (queryParams.value.pageNum - 1) * queryParams.value.pageSize)].fileList =
+          fileObj[recordList.value[parseInt(i + (queryParams.value.pageNum - 1) * queryParams.value.pageSize)].id] || [];
+      }
+    });
+  });
+  openRecordMore.value = true;
+};
+const infiniteScroll = () => {
+  queryParams.value.pageNum++;
+  clickMore();
+};
+const judgeTotal = () => {
+  if (queryParams.value.pageNum * queryParams.value.pageSize >= queryParams.value.total) {
+    return true;
+  }
+  return false;
+};
+const moreIndex = ref(0);
+const clickInformationMore = (item, index) => {
+  moreIndex.value = index;
+  if (item.contactJson) {
+    item.contact = JSON.parse(item.contactJson);
+  } else {
+    item.contact = [];
+  }
+  formPerson.data = JSON.parse(JSON.stringify(item));
+  openPerson.value = true;
+};
+const clickDelete = (index) => {
+  formData.data.customerUserList.splice(index, 1);
+};
+const clickAddMoreInformation = () => {
+  if (formPerson.data.contact && formPerson.data.contact.length > 0) {
+    formPerson.data.contact.push({
+      type: "",
+      contactNo: "",
+    });
+  } else {
+    formPerson.data.contact = [
+      {
+        type: "",
+        contactNo: "",
+      },
+    ];
+  }
+};
+const clickInformationDelete = (index) => {
+  formPerson.data.contact.splice(index, 1);
+};
+const submitPerson = () => {
+  person.value.validate((valid) => {
+    if (valid) {
+      formPerson.data.contactJson = JSON.stringify(formPerson.data.contact);
+      formData.data.customerUserList[moreIndex.value] = formPerson.data;
+      openPerson.value = false;
+    }
+  });
+};
 </script>
 
 <style lang="scss" scoped>
 .tenant {
   padding: 20px;
 }
+.infinite-scroll {
+  max-height: calc(89vh - 94px - 70px - 58px - 16px);
+  overflow-y: auto;
+  &::-webkit-scrollbar {
+    width: 0px;
+  }
+}
 </style>