Browse Source

客户档案

lxf 1 năm trước cách đây
mục cha
commit
60e2481145

+ 32 - 1
src/lang/cn.js

@@ -1119,7 +1119,6 @@ export const lang = {
 		accountOpening: '银行账号',
 		accountOpeningMsg: '请输入银行账号',
 		interbankNumber: '联行号',
-		interbankNumberMsg: '请输入联行号',
 		foreignExchange: '外汇信息',
 		beneficiaryName: 'Beneficiary Name',
 		beneficiaryBank: 'Beneficiary Bank',
@@ -1134,5 +1133,37 @@ export const lang = {
 		accountBalance: '账户余额',
 		submitMsgOne:'请添加至少一条账户余额',
 		submitMsgTwo:'请勿重复添加货币余额',
+	},
+	customerFile: {
+		name: '客户档案',
+		customerName: '客户名称',
+		customerNameMsg: '请输入名称',
+		cityText: '所在城市',
+		customerStatus: '客户类型',
+		add: '添加客户',
+		address: '详细地址',
+		code: '客户代码',
+		source: '客户来源',
+		sourceMsg: '请选择客户来源',
+		status: '客户类型',
+		statusMsg: '请选择客户类型',
+		userId: '业务员',
+		tag: '客户标签',
+		cityMsg: '请选择城市',
+		emailMsg: '请输入邮箱',
+		submitMsg: '请添加联系人',
+		customerContact: '客户联系人',
+		contact: '联系人',
+		email: '电子邮箱',
+		detail: '客户详情',
+		followUpRecord: '添加跟进记录',
+		date: '跟进时间',
+		dateMsg: '请选择跟进时间',
+		customerName: '跟进人',
+		content: '跟进记录',
+		contentTwo: '跟进内容',
+		contentTwoMsg: '请输入跟进内容',
+		selectDate:'选择日期',
+		selectTime:'选择时间',
 	}
 }

+ 20 - 0
src/router/routerLXF.js

@@ -85,6 +85,26 @@ export function routesLXF() {
       name: "添加账户",
       component: () => import("../views/fund/account/add.vue"),
     },
+    {
+      path: "customerFile",
+      name: "客户档案",
+      component: () => import("../views/customer/file/index.vue"),
+    },
+    {
+      path: "customerFileAdd",
+      name: "添加客户",
+      component: () => import("../views/customer/file/add.vue"),
+    },
+    {
+      path: "customerFileDetail",
+      name: "客户详情",
+      component: () => import("../views/customer/file/detail.vue"),
+    },
+    {
+      path: "customerFileRecords",
+      name: "添加跟进记录",
+      component: () => import("../views/customer/file/addRecords.vue"),
+    },
   ];
   return routesLXF;
 }

+ 229 - 0
src/views/customer/file/add.vue

@@ -0,0 +1,229 @@
+<template>
+  <div class="form">
+    <van-nav-bar :title="$t('customerFile.' + route.query.type)" :left-text="$t('common.back')" left-arrow @click-left="onClickLeft"> </van-nav-bar>
+    <testForm v-model="formData.data" :formOption="formOption" :formConfig="formConfig" :rules="rules" @onSubmit="onSubmit" ref="formDom"> </testForm>
+  </div>
+</template>
+
+<script setup>
+import { ref, getCurrentInstance, onMounted, reactive } from "vue";
+import { showSuccessToast, showFailToast } from "vant";
+import { useRoute } from "vue-router";
+import { getUserInfo } from "@/utils/auth";
+import testForm from "@/components/testForm/index.vue";
+
+const proxy = getCurrentInstance().proxy;
+const onClickLeft = () => history.back();
+const route = useRoute();
+const getDict = () => {
+  let query = {
+    pageNum: 1,
+    pageSize: 999,
+    tenantId: getUserInfo().tenantId,
+  };
+  proxy.post("/dictTenantData/page", { ...query, dictCode: "customer_source" }).then((res) => {
+    if (res.data.rows && res.data.rows.length > 0) {
+      formConfig[4].data = res.data.rows.map((item) => {
+        return {
+          text: item.dictValue,
+          value: item.dictKey,
+        };
+      });
+    }
+  });
+  proxy.post("/dictTenantData/page", { ...query, dictCode: "customer_status" }).then((res) => {
+    if (res.data.rows && res.data.rows.length > 0) {
+      formConfig[5].data = res.data.rows.map((item) => {
+        return {
+          text: item.dictValue,
+          value: item.dictKey,
+        };
+      });
+    }
+  });
+  proxy.post("/dictTenantData/page", { ...query, dictCode: "customer_tag" }).then((res) => {
+    if (res.data.rows && res.data.rows.length > 0) {
+      formConfig[7].data = res.data.rows.map((item) => {
+        return {
+          text: item.dictValue,
+          value: item.dictKey,
+        };
+      });
+    }
+  });
+  proxy.get("/tenantUser/list", { pageNum: 1, pageSize: 10000, tenantId: getUserInfo().tenantId }).then((res) => {
+    if (res.rows && res.rows.length > 0) {
+      formConfig[6].data = res.rows.map((item) => {
+        return {
+          text: item.nickName,
+          value: item.userId,
+        };
+      });
+    }
+  });
+};
+getDict();
+const formData = reactive({
+  data: {
+    customerCode: null,
+    code: null,
+    countryId: null,
+    provinceId: null,
+    cityId: null,
+    address: null,
+    zipCode: null,
+    name: null,
+    status: null,
+    source: null,
+    userId: null,
+    customerUserList: [],
+  },
+});
+const formDom = ref(null);
+const formOption = reactive({
+  readonly: false, //用于控制整个表单是否只读
+  disabled: false,
+  labelAlign: "top",
+  scroll: true,
+  labelWidth: "62pk",
+  hiddenSubmitBtn: false,
+  btnConfig: {
+    isNeed: true,
+    prop: "customerUserList",
+    plain: true,
+    listTitle: proxy.t("customerFile.customerContact"),
+    listConfig: [
+      {
+        type: "input",
+        label: proxy.t("customerFile.contact"),
+        prop: "name",
+      },
+      {
+        type: "input",
+        label: proxy.t("customerFile.email"),
+        prop: "email",
+      },
+    ],
+    clickFn: () => {
+      if (formData.data.customerUserList && formData.data.customerUserList.length > 0) {
+        formData.data.customerUserList.push({
+          name: null,
+          email: null,
+        });
+      } else {
+        formData.data.customerUserList = [
+          {
+            name: null,
+            email: null,
+          },
+        ];
+      }
+    },
+  },
+});
+const formConfig = reactive([
+  {
+    type: "input",
+    label: proxy.t("customerFile.customerName"),
+    prop: "name",
+    itemType: "text",
+  },
+  {
+    type: "cascader",
+    label: proxy.t("customerFile.cityText"),
+    prop: "city",
+    itemType: "city",
+    showPicker: false,
+  },
+  {
+    type: "input",
+    label: proxy.t("customerFile.address"),
+    prop: "address",
+    itemType: "textarea",
+  },
+  {
+    type: "input",
+    label: proxy.t("customerFile.code"),
+    prop: "code",
+    itemType: "text",
+  },
+  {
+    type: "picker",
+    label: proxy.t("customerFile.source"),
+    prop: "source",
+    itemType: "onePicker",
+    showPicker: false,
+    fieldNames: {
+      text: "text",
+      value: "value",
+    },
+    data: [],
+  },
+  {
+    type: "picker",
+    label: proxy.t("customerFile.status"),
+    prop: "status",
+    itemType: "onePicker",
+    showPicker: false,
+    fieldNames: {
+      text: "text",
+      value: "value",
+    },
+    data: [],
+  },
+  {
+    type: "picker",
+    label: proxy.t("customerFile.userId"),
+    prop: "userId",
+    itemType: "onePicker",
+    showPicker: false,
+    fieldNames: {
+      text: "text",
+      value: "value",
+    },
+    data: [],
+  },
+  {
+    type: "multipleChoice",
+    label: proxy.t("customerFile.tag"),
+    prop: "tags",
+    itemType: "multiple",
+    showPicker: false,
+    fieldNames: {
+      text: "text",
+      value: "value",
+    },
+    data: [],
+  },
+]);
+const rules = {
+  name: [{ required: true, message: proxy.t("customerFile.customerNameMsg") }],
+  city: [{ required: true, message: proxy.t("customerFile.cityMsg") }],
+  source: [{ required: true, message: proxy.t("customerFile.sourceMsg") }],
+  status: [{ required: true, message: proxy.t("customerFile.statusMsg") }],
+  email: [{ required: true, message: proxy.t("customerFile.emailMsg") }],
+};
+const onSubmit = () => {
+  if (formData.data.customerUserList && formData.data.customerUserList.length > 0) {
+    if (formData.data.cityObj) {
+      formData.data.countryId = formData.data.cityObj.selectedOptions[0].value;
+      formData.data.provinceId = formData.data.cityObj.tabIndex === 2 ? formData.data.cityObj.selectedOptions[1].value : null;
+      formData.data.cityId =
+        formData.data.cityObj.tabIndex === 1 ? formData.data.cityObj.selectedOptions[1].value : formData.data.cityObj.selectedOptions[2].value;
+    }
+    if (formData.data.tags && formData.data.tags.length > 0) {
+      formData.data.tag = formData.data.tags.join(",");
+    } else {
+      formData.data.tag = "";
+    }
+    proxy.post("/customer/" + route.query.type, formData.data).then(() => {
+      showSuccessToast(proxy.t("common.addSuccess"));
+      setTimeout(() => {
+        history.back();
+      }, 500);
+    });
+  } else {
+    showFailToast(proxy.t("customerFile.submitMsg"));
+  }
+};
+</script>

+ 94 - 0
src/views/customer/file/addRecords.vue

@@ -0,0 +1,94 @@
+<template>
+  <div class="form">
+    <van-nav-bar :title="$t('customerFile.followUpRecord')" :left-text="$t('common.back')" left-arrow @click-left="onClickLeft"> </van-nav-bar>
+    <testForm v-model="formData.data" :formOption="formOption" :formConfig="formConfig" :rules="rules" @onSubmit="onSubmit" ref="formDom">
+      <template #date>
+        <div style="width: 100%">
+          <van-cell-group inset>
+            <van-field
+              v-model="formData.data.date"
+              is-link
+              readonly
+              :label="$t('customerFile.date')"
+              :placeholder="$t('common.pleaseSelect')"
+              style="padding: 0 !important"
+              :required="true"
+              @click="clickDate" />
+            <van-popup v-model:show="showPicker" round position="bottom">
+              <van-picker-group :title="$t('customerFile.date')" :tabs="[$t('customerFile.selectDate'), $t('customerFile.selectTime')]" @confirm="onConfirm" @cancel="onCancel">
+                <van-date-picker v-model="currentDate" />
+                <van-time-picker v-model="currentTime" :columns-type="columnsType" />
+              </van-picker-group>
+            </van-popup>
+          </van-cell-group>
+        </div>
+      </template>
+    </testForm>
+  </div>
+</template>
+
+<script setup>
+import { ref, getCurrentInstance, reactive } from "vue";
+import { showSuccessToast } from "vant";
+import testForm from "@/components/testForm/index.vue";
+import { formatDate } from "@/utils/auth";
+import { useRoute } from "vue-router";
+
+const proxy = getCurrentInstance().proxy;
+const onClickLeft = () => history.back();
+const route = useRoute();
+const currentDate = ref([]);
+const currentTime = ref([]);
+const columnsType = ["hour", "minute", "second"];
+const onConfirm = () => {
+  formData.data.date = currentDate.value.join("-") + " " + currentTime.value.join(":");
+  showPicker.value = false;
+};
+const onCancel = () => {
+  showPicker.value = false;
+};
+const clickDate = () => {
+  currentDate.value = formatDate(new Date(formData.data.date), "yyyy-MM-dd").split("-");
+  currentTime.value = formatDate(new Date(formData.data.date), "hh:mm:ss").split(":");
+  showPicker.value = true;
+};
+const formData = reactive({
+  data: {
+    date: formatDate(new Date(), "yyyy-MM-dd hh:mm:ss"),
+    content: null,
+  },
+});
+const formDom = ref(null);
+const showPicker = ref(false);
+const formOption = reactive({
+  readonly: false, //用于控制整个表单是否只读
+  disabled: false,
+  labelAlign: "top",
+  scroll: true,
+  labelWidth: "62pk",
+});
+const formConfig = reactive([
+  {
+    type: "slot",
+    slotName: "date",
+  },
+  {
+    type: "input",
+    label: proxy.t("customerFile.contentTwo"),
+    prop: "content",
+    itemType: "textarea",
+  },
+]);
+const rules = {
+  date: [{ required: true, message: proxy.t("customerFile.dateMsg") }],
+  content: [{ required: true, message: proxy.t("customerFile.contentTwoMsg") }],
+};
+const onSubmit = () => {
+  proxy.post("/customerFollowRecords/add", { ...formData.data, customerId: route.query.id }).then(() => {
+    showSuccessToast(proxy.t("common.addSuccess"));
+    setTimeout(() => {
+      history.back();
+    }, 500);
+  });
+};
+</script>

+ 267 - 0
src/views/customer/file/detail.vue

@@ -0,0 +1,267 @@
+<template>
+  <div class="form">
+    <van-nav-bar :title="$t('customerFile.' + route.query.type)" :left-text="$t('common.back')" left-arrow @click-left="onClickLeft"> </van-nav-bar>
+    <testForm v-model="formData.data" :formOption="formOption" :formConfig="formConfig"> </testForm>
+    <testForm v-model="formData.data" :formOption="formOptionTwo" :formConfig="formConfigTwo" @onSubmit="onSubmit"> </testForm>
+  </div>
+</template>
+
+<script setup>
+import { ref, getCurrentInstance, onMounted, reactive } from "vue";
+import { useRoute } from "vue-router";
+import { getUserInfo } from "@/utils/auth";
+import testForm from "@/components/testForm/index.vue";
+
+const proxy = getCurrentInstance().proxy;
+const onClickLeft = () => history.back();
+const route = useRoute();
+const customerSource = ref([]);
+const customerStatus = ref([]);
+const customerTag = ref([]);
+const userList = ref([]);
+const formData = reactive({
+  data: {
+    customerCode: null,
+    code: null,
+    countryId: null,
+    provinceId: null,
+    cityId: null,
+    address: null,
+    zipCode: null,
+    name: null,
+    status: null,
+    source: null,
+    userId: null,
+    customerUserList: [],
+  },
+});
+const formOption = reactive({
+  readonly: true, //用于控制整个表单是否只读
+  disabled: false,
+  labelAlign: "top",
+  scroll: true,
+  labelWidth: "62pk",
+  hiddenSubmitBtn: true,
+  btnConfig: {
+    isNeed: false,
+    prop: "customerUserList",
+    plain: true,
+    listTitle: proxy.t("customerFile.customerContact"),
+    listConfig: [
+      {
+        type: "input",
+        label: proxy.t("customerFile.contact"),
+        prop: "name",
+      },
+      {
+        type: "input",
+        label: proxy.t("customerFile.email"),
+        prop: "email",
+      },
+    ],
+  },
+});
+const formConfig = reactive([
+  {
+    type: "input",
+    label: proxy.t("customerFile.customerName"),
+    prop: "name",
+    itemType: "text",
+  },
+  {
+    type: "input",
+    label: proxy.t("customerFile.cityText"),
+    prop: "cityText",
+    itemType: "text",
+  },
+  {
+    type: "input",
+    label: proxy.t("customerFile.address"),
+    prop: "address",
+    itemType: "textarea",
+  },
+  {
+    type: "input",
+    label: proxy.t("customerFile.code"),
+    prop: "code",
+    itemType: "text",
+  },
+  {
+    type: "input",
+    label: proxy.t("customerFile.source"),
+    prop: "sourceText",
+    itemType: "text",
+  },
+  {
+    type: "input",
+    label: proxy.t("customerFile.status"),
+    prop: "statusText",
+    itemType: "text",
+  },
+  {
+    type: "input",
+    label: proxy.t("customerFile.userId"),
+    prop: "userText",
+    itemType: "text",
+  },
+  {
+    type: "input",
+    label: proxy.t("customerFile.tag"),
+    prop: "tagText",
+    itemType: "text",
+  },
+]);
+const formOptionTwo = reactive({
+  readonly: true, //用于控制整个表单是否只读
+  disabled: false,
+  labelAlign: "top",
+  scroll: true,
+  labelWidth: "62pk",
+  hiddenSubmitBtn: false,
+  submitBtnText: proxy.t("customerFile.followUpRecord"),
+  btnConfig: {
+    isNeed: false,
+    prop: "recordsList",
+    plain: true,
+    listTitle: proxy.t("customerFile.content"),
+    listConfig: [
+      {
+        type: "input",
+        label: proxy.t("customerFile.date"),
+        prop: "date",
+      },
+      {
+        type: "input",
+        label: proxy.t("customerFile.customerName"),
+        prop: "customerName",
+      },
+      {
+        type: "input",
+        label: proxy.t("customerFile.content"),
+        prop: "content",
+      },
+    ],
+  },
+});
+const formConfigTwo = reactive([]);
+const onSubmit = () => {
+  proxy.$router.push({
+    path: "customerFileRecords",
+    query: {
+      id: route.query.id,
+    },
+  });
+};
+const getUser = () => {
+  return proxy.get("/tenantUser/list", { pageNum: 1, pageSize: 10000, tenantId: getUserInfo().tenantId }).then((res) => {
+    if (res.rows && res.rows.length > 0) {
+      userList.value = res.rows.map((item) => {
+        return {
+          text: item.nickName,
+          value: item.userId,
+        };
+      });
+    }
+  });
+};
+const getSource = () => {
+  return proxy
+    .post("/dictTenantData/page", {
+      pageNum: 1,
+      pageSize: 999,
+      tenantId: getUserInfo().tenantId,
+      dictCode: "customer_source",
+    })
+    .then((res) => {
+      customerSource.value = res.data.rows;
+    });
+};
+const getStatus = () => {
+  return proxy
+    .post("/dictTenantData/page", {
+      pageNum: 1,
+      pageSize: 999,
+      tenantId: getUserInfo().tenantId,
+      dictCode: "customer_status",
+    })
+    .then((res) => {
+      customerStatus.value = res.data.rows;
+    });
+};
+const getTag = () => {
+  return proxy
+    .post("/dictTenantData/page", {
+      pageNum: 1,
+      pageSize: 999,
+      tenantId: getUserInfo().tenantId,
+      dictCode: "customer_tag",
+    })
+    .then((res) => {
+      customerTag.value = res.data.rows;
+    });
+};
+onMounted(() => {
+  Promise.all([getUser(), getSource(), getStatus(), getTag()]).then(() => {
+    if (route.query.id) {
+      proxy.post("/customer/detail", { id: route.query.id }).then((res) => {
+        formData.data = res.data;
+        let cityText = "";
+        if (formData.data.countryName) {
+          cityText = formData.data.countryName;
+          if (formData.data.provinceName) {
+            cityText = cityText + "," + formData.data.provinceName;
+            if (formData.data.cityName) {
+              cityText = cityText + "," + formData.data.cityName;
+            }
+          }
+        }
+        formData.data.cityText = cityText;
+        let sourceText = "";
+        if (formData.data.source && customerSource.value && customerSource.value.length > 0) {
+          let list = customerSource.value.filter((item) => item.dictKey == formData.data.source);
+          if (list && list.length > 0) {
+            sourceText = list[0].dictValue;
+          }
+        }
+        formData.data.sourceText = sourceText;
+        let statusText = "";
+        if (formData.data.status && customerStatus.value && customerStatus.value.length > 0) {
+          let list = customerStatus.value.filter((item) => item.dictKey == formData.data.status);
+          if (list && list.length > 0) {
+            statusText = list[0].dictValue;
+          }
+        }
+        formData.data.statusText = statusText;
+        let userText = "";
+        if (formData.data.userId && userList.value && userList.value.length > 0) {
+          let list = userList.value.filter((item) => item.value == formData.data.userId);
+          if (list && list.length > 0) {
+            userText = list[0].text;
+          }
+        }
+        formData.data.userText = userText;
+        if (formData.data.tag) {
+          let tagText = "";
+          let tags = formData.data.tag.split(",");
+          if (tags && tags.length > 0) {
+            for (let i = 0; i < tags.length; i++) {
+              let list = customerTag.value.filter((item) => item.dictKey == tags[i]);
+              if (list && list.length > 0) {
+                if (i === 0) {
+                  tagText = list[0].dictValue;
+                } else {
+                  tagText = tagText + "," + list[0].dictValue;
+                }
+              }
+            }
+          }
+          formData.data.tagText = tagText;
+        }
+        proxy.post("/customerFollowRecords/page", { pageNum: 1, pageSize: 999, customerId: route.query.id }).then((resRecords) => {
+          formData.data.recordsList = resRecords.data.rows;
+        });
+      });
+    }
+  });
+});
+</script>

+ 93 - 0
src/views/customer/file/index.vue

@@ -0,0 +1,93 @@
+<template>
+  <van-nav-bar :title="$t('customerFile.name')" left-text="" left-arrow @click-left="onClickLeft" @click-right="onClickRight">
+    <template #right>{{ $t("common.add") }}</template>
+  </van-nav-bar>
+  <van-search v-model="req.keyword" :placeholder="$t('common.pleaseEnterKeywords')" @search="onRefresh" />
+  <van-pull-refresh v-model="loading" @refresh="onRefresh">
+    <div class="list">
+      <van-list v-model:loading="loading" :finished="finished" :finished-text="$t('common.noMore')" @load="getList" style="margin-bottom: 60px">
+        <commonList :data="listData" @onClick="toDtl" :config="listConfig"></commonList>
+      </van-list>
+    </div>
+  </van-pull-refresh>
+</template>
+<script setup>
+import { ref, getCurrentInstance } from "vue";
+import commonList from "@/components/common-list.vue";
+
+const proxy = getCurrentInstance().proxy;
+const onClickLeft = () => proxy.$router.push("/main/working");
+const onClickRight = () => {
+  proxy.$router.push({
+    path: "customerFileAdd",
+    query: {
+      type: "add",
+    },
+  });
+};
+const req = ref({
+  pageNum: 1,
+  keyword: null,
+});
+const finished = ref(false);
+const onRefresh = () => {
+  req.value.pageNum = 1;
+  finished.value = false;
+  getList("refresh");
+};
+const loading = ref(false);
+const listData = ref([]);
+const getList = (type) => {
+  loading.value = true;
+  proxy
+    .post("/customer/page", req.value)
+    .then((res) => {
+      if (res.data.rows && res.data.rows.length > 0) {
+        res.data.rows = res.data.rows.map((item) => {
+          return {
+            ...item,
+            cityText: item.countryName + "," + item.provinceName + "," + item.cityName,
+          };
+        });
+      }
+      listData.value = type === "refresh" ? res.data.rows : listData.value.concat(res.data.rows);
+      if (req.value.pageNum * 10 >= res.data.total) {
+        finished.value = true;
+      }
+      req.value.pageNum++;
+      loading.value = false;
+    })
+    .catch(() => {
+      loading.value = false;
+    });
+};
+const toDtl = (row) => {
+  proxy.$router.push({
+    path: "customerFileDetail",
+    query: {
+      id: row.id,
+      type: "detail",
+    },
+  });
+};
+const listConfig = ref([
+  {
+    label: proxy.t("customerFile.customerName"),
+    prop: "name",
+  },
+  {
+    label: proxy.t("customerFile.cityText"),
+    prop: "cityText",
+  },
+  {
+    label: proxy.t("customerFile.customerStatus"),
+    prop: "statusVal",
+  },
+]);
+</script>
+
+<style lang="scss" scoped>
+.list {
+  min-height: 70vh;
+}
+</style>