Prechádzať zdrojové kódy

feat: 新增crm系统客户管理模块

pengjq 4 dní pred
rodič
commit
01d43b04d8

+ 190 - 0
src/pages-crm/customer/components/search-form.vue

@@ -0,0 +1,190 @@
+<template>
+  <view class="search-sticky-wrapper">
+    <view class="tab-bar">
+      <view
+        v-for="item in tabs"
+        :key="item.value"
+        class="tab-item" :class="[{ active: activeTab === item.value }]"
+        @click="handleTabChange(item.value)"
+      >
+        {{ item.label }}
+      </view>
+    </view>
+    <view @click="visible = true">
+      <wd-search :placeholder="placeholder" hide-cancel disabled />
+    </view>
+
+    <wd-popup v-model="visible" position="top" @close="visible = false">
+      <view class="yd-search-form-container" :style="{ paddingTop: `${getNavbarHeight()}px` }">
+        <view class="yd-search-form-item">
+          <view class="yd-search-form-label">
+            客户名称
+          </view>
+          <wd-input
+            v-model="formData.name"
+            placeholder="请输入客户名称"
+            clearable
+          />
+        </view>
+        <view class="yd-search-form-item">
+          <view class="yd-search-form-label">
+            手机号
+          </view>
+          <wd-input
+            v-model="formData.mobile"
+            placeholder="请输入手机号"
+            clearable
+          />
+        </view>
+        <view class="yd-search-form-item">
+          <view class="yd-search-form-label">
+            所属行业
+          </view>
+          <wd-select-picker
+            v-model="formData.industryId"
+            :columns="industryDictOptions"
+            value-key="value"
+            label-key="label"
+            placeholder="请选择"
+          />
+        </view>
+        <view class="yd-search-form-item">
+          <view class="yd-search-form-label">
+            客户级别
+          </view>
+          <wd-select-picker
+            v-model="formData.level"
+            :columns="levelDictOptions"
+            value-key="value"
+            label-key="label"
+            placeholder="请选择"
+          />
+        </view>
+        <view class="yd-search-form-item">
+          <view class="yd-search-form-label">
+            客户来源
+          </view>
+          <wd-select-picker
+            v-model="formData.source"
+            :columns="sourceDictOptions"
+            value-key="value"
+            label-key="label"
+            placeholder="请选择"
+          />
+        </view>
+        <view class="yd-search-form-actions">
+          <wd-button class="flex-1" plain @click="handleReset">
+            重置
+          </wd-button>
+          <wd-button class="flex-1" type="primary" @click="handleSearch">
+            搜索
+          </wd-button>
+        </view>
+      </view>
+    </wd-popup>
+  </view>
+</template>
+
+<script lang="ts" setup>
+import { computed, reactive, ref } from 'vue'
+import { getIntDictOptions } from '@/hooks/useDict'
+import { getNavbarHeight } from '@/utils'
+import { DICT_TYPE } from '@/utils/constants'
+
+const emit = defineEmits<{
+  search: [data: Record<string, any>]
+  reset: []
+}>()
+
+const visible = ref(false)
+const activeTab = ref('1')
+const tabs = [
+  { label: '我负责的', value: '1' },
+  { label: '我参与的', value: '2' },
+  { label: '下属负责的', value: '3' },
+]
+const formData = reactive({
+  name: undefined as string | undefined,
+  mobile: undefined as string | undefined,
+  industryId: undefined as number | undefined,
+  level: undefined as number | undefined,
+  source: undefined as number | undefined,
+})
+
+const industryDictOptions = computed(() => getIntDictOptions(DICT_TYPE.CRM_CUSTOMER_INDUSTRY))
+const levelDictOptions = computed(() => getIntDictOptions(DICT_TYPE.CRM_CUSTOMER_LEVEL))
+const sourceDictOptions = computed(() => getIntDictOptions(DICT_TYPE.CRM_CUSTOMER_SOURCE))
+
+const placeholder = computed(() => {
+  const conditions: string[] = []
+  if (formData.name) {
+    conditions.push(`名称:${formData.name}`)
+  }
+  if (formData.mobile) {
+    conditions.push(`手机:${formData.mobile}`)
+  }
+  return conditions.length > 0 ? conditions.join(' | ') : '搜索客户'
+})
+
+function handleTabChange(value: string) {
+  activeTab.value = value
+  emit('search', { sceneType: value })
+}
+
+function handleSearch() {
+  visible.value = false
+  emit('search', { ...formData, sceneType: activeTab.value })
+}
+
+function handleReset() {
+  formData.name = undefined
+  formData.mobile = undefined
+  formData.industryId = undefined
+  formData.level = undefined
+  formData.source = undefined
+  visible.value = false
+  emit('reset')
+}
+</script>
+
+<style lang="scss" scoped>
+.search-sticky-wrapper {
+  position: sticky;
+  top: 0;
+  z-index: 100;
+  background-color: #fff;
+}
+
+.tab-bar {
+  display: flex;
+  padding: 16rpx 0;
+  border-bottom: 1rpx solid #f0f0f0;
+}
+
+.tab-item {
+  flex: 1;
+  text-align: center;
+  padding: 16rpx 0;
+  font-size: 28rpx;
+  color: #666;
+  position: relative;
+  transition: color 0.3s;
+
+  &.active {
+    color: #1890ff;
+    font-weight: 500;
+
+    &::after {
+      content: '';
+      position: absolute;
+      bottom: 0;
+      left: 50%;
+      transform: translateX(-50%);
+      width: 48rpx;
+      height: 4rpx;
+      background-color: #1890ff;
+      border-radius: 2rpx;
+    }
+  }
+}
+</style>

+ 149 - 0
src/pages-crm/customer/detail/index.vue

@@ -0,0 +1,149 @@
+<template>
+  <view class="yd-page-container">
+    <wd-navbar
+      title="客户详情"
+      left-arrow placeholder safe-area-inset-top fixed
+      @click-left="handleBack"
+    />
+
+    <view class="pb-140rpx">
+      <wd-cell-group border>
+        <wd-cell title="客户名称" :value="formData?.name || '-'" />
+        <wd-cell title="客户来源">
+          <dict-tag :type="DICT_TYPE.CRM_CUSTOMER_SOURCE" :value="formData?.source" />
+        </wd-cell>
+        <wd-cell title="手机号" :value="formData?.mobile || '-'" />
+        <wd-cell title="固定电话" :value="formData?.telephone || '-'" />
+        <wd-cell title="QQ" :value="formData?.qq || '-'" />
+        <wd-cell title="微信" :value="formData?.wechat || '-'" />
+        <wd-cell title="邮箱" :value="formData?.email || '-'" />
+        <wd-cell title="客户级别">
+          <dict-tag :type="DICT_TYPE.CRM_CUSTOMER_LEVEL" :value="formData?.level" />
+        </wd-cell>
+        <wd-cell title="客户行业">
+          <dict-tag :type="DICT_TYPE.CRM_CUSTOMER_INDUSTRY" :value="formData?.industryId" />
+        </wd-cell>
+        <wd-cell title="所在地" :value="formData?.areaName || '-'" />
+        <wd-cell title="详细地址" :value="formData?.detailAddress || '-'" />
+        <wd-cell title="负责人" :value="formData?.ownerUserName || '-'" />
+        <wd-cell title="负责人部门" :value="formData?.ownerUserDept || '-'" />
+        <wd-cell title="锁定状态">
+          <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="formData?.lockStatus" />
+        </wd-cell>
+        <wd-cell title="成交状态">
+          <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="formData?.dealStatus" />
+        </wd-cell>
+        <wd-cell title="跟进状态">
+          <dict-tag :type="DICT_TYPE.CRM_FOLLOWUP_STATUS" :value="formData?.followUpStatus" />
+        </wd-cell>
+        <wd-cell title="最后跟进时间" :value="formatDateTime(formData?.contactLastTime) || '-'" />
+        <wd-cell title="最后跟进内容" :value="formData?.contactLastContent || '-'" />
+        <wd-cell title="下次联系时间" :value="formatDateTime(formData?.contactNextTime) || '-'" />
+        <wd-cell title="创建人" :value="formData?.creatorName || '-'" />
+        <wd-cell title="创建时间" :value="formatDateTime(formData?.createTime) || '-'" />
+        <wd-cell title="更新时间" :value="formatDateTime(formData?.updateTime) || '-'" />
+        <wd-cell title="备注" :value="formData?.remark || '-'" />
+      </wd-cell-group>
+    </view>
+
+    <view class="yd-detail-footer">
+      <view class="yd-detail-footer-actions">
+        <wd-button
+          v-if="hasAccessByCodes(['crm:customer:update'])"
+          class="flex-1" type="warning" @click="handleEdit"
+        >
+          编辑
+        </wd-button>
+        <wd-button
+          v-if="hasAccessByCodes(['crm:customer:delete'])"
+          class="flex-1" type="error" :loading="deleting" @click="handleDelete"
+        >
+          删除
+        </wd-button>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script lang="ts" setup>
+import type { CustomerVO } from '@/api/crm/customer'
+import { onShow } from '@dcloudio/uni-app'
+import { onMounted, ref } from 'vue'
+import { useToast } from 'wot-design-uni'
+import { deleteCustomer, getCustomer } from '@/api/crm/customer'
+import { useAccess } from '@/hooks/useAccess'
+import { navigateBackPlus } from '@/utils'
+import { DICT_TYPE } from '@/utils/constants'
+import { formatDateTime } from '@/utils/date'
+
+const props = defineProps<{
+  id?: number | any
+}>()
+
+definePage({
+  style: {
+    navigationBarTitleText: '',
+    navigationStyle: 'custom',
+  },
+})
+
+const { hasAccessByCodes } = useAccess()
+const toast = useToast()
+const formData = ref<CustomerVO>()
+const deleting = ref(false)
+
+function handleBack() {
+  navigateBackPlus('/pages-crm/customer/index')
+}
+
+async function getDetail() {
+  if (!props.id) {
+    return
+  }
+  try {
+    toast.loading('加载中...')
+    formData.value = await getCustomer(props.id)
+  } finally {
+    toast.close()
+  }
+}
+
+function handleEdit() {
+  uni.navigateTo({
+    url: `/pages-crm/customer/form/index?id=${props.id}`,
+  })
+}
+
+function handleDelete() {
+  if (!props.id) {
+    return
+  }
+  uni.showModal({
+    title: '提示',
+    content: '确定要删除该客户吗?',
+    success: async (res) => {
+      if (!res.confirm) {
+        return
+      }
+      deleting.value = true
+      try {
+        await deleteCustomer(Number(props.id))
+        toast.success('删除成功')
+        setTimeout(() => {
+          handleBack()
+        }, 500)
+      } finally {
+        deleting.value = false
+      }
+    },
+  })
+}
+
+onMounted(() => {
+  getDetail()
+})
+
+onShow(() => {
+  getDetail()
+})
+</script>

+ 280 - 0
src/pages-crm/customer/form/index.vue

@@ -0,0 +1,280 @@
+<template>
+  <view class="yd-page-container">
+    <wd-navbar
+      :title="getTitle"
+      left-arrow placeholder safe-area-inset-top fixed
+      @click-left="handleBack"
+    />
+
+    <view>
+      <wd-form ref="formRef" :model="formData" :rules="formRules">
+        <wd-cell-group border>
+          <wd-input
+            v-model="formData.name"
+            label="客户名称"
+            label-width="180rpx"
+            prop="name"
+            clearable
+            placeholder="请输入客户名称"
+          />
+          <wd-select-picker
+            v-model="formData.source"
+            label="客户来源"
+            label-width="180rpx"
+            :columns="sourceDictOptions"
+            value-key="value"
+            label-key="label"
+            type="radio"
+            placeholder="请选择"
+          />
+          <UserPicker v-model="formData.ownerUserId" label="负责人" type="radio" />
+          <wd-input
+            v-model="formData.mobile"
+            label="手机号"
+            label-width="180rpx"
+            prop="mobile"
+            clearable
+            placeholder="请输入手机号"
+          />
+          <wd-input
+            v-model="formData.telephone"
+            label="固定电话"
+            label-width="180rpx"
+            clearable
+            placeholder="请输入固定电话"
+          />
+          <wd-input
+            v-model="formData.email"
+            label="邮箱"
+            label-width="180rpx"
+            prop="email"
+            clearable
+            placeholder="请输入邮箱"
+          />
+          <wd-input
+            v-model="formData.wechat"
+            label="微信"
+            label-width="180rpx"
+            clearable
+            placeholder="请输入微信"
+          />
+          <wd-input
+            v-model="formData.qq"
+            label="QQ"
+            label-width="180rpx"
+            clearable
+            placeholder="请输入QQ"
+          />
+          <wd-select-picker
+            v-model="formData.industryId"
+            label="客户行业"
+            label-width="180rpx"
+            :columns="industryDictOptions"
+            value-key="value"
+            label-key="label"
+            type="radio"
+            placeholder="请选择"
+          />
+          <wd-select-picker
+            v-model="formData.level"
+            label="客户级别"
+            label-width="180rpx"
+            :columns="levelDictOptions"
+            value-key="value"
+            label-key="label"
+            type="radio"
+            placeholder="请选择"
+          />
+          <AreaPicker v-model="formData.areaId" label="地址" />
+          <wd-textarea
+            v-model="formData.detailAddress"
+            label="详细地址"
+            label-width="180rpx"
+            placeholder="请输入详细地址"
+            :maxlength="200"
+            show-word-limit
+            clearable
+          />
+          <wd-cell title="下次联系时间" title-width="180rpx" center>
+            <wd-datetime-picker
+              v-model="contactNextTimeValue"
+              type="datetime"
+              :min-date="minDate"
+              @confirm="handleDateTimeConfirm"
+            />
+          </wd-cell>
+          <wd-textarea
+            v-model="formData.remark"
+            label="备注"
+            label-width="180rpx"
+            placeholder="请输入备注"
+            :maxlength="200"
+            show-word-limit
+            clearable
+          />
+        </wd-cell-group>
+      </wd-form>
+    </view>
+
+    <view class="yd-detail-footer">
+      <wd-button
+        type="primary"
+        block
+        :loading="formLoading"
+        @click="handleSubmit"
+      >
+        保存
+      </wd-button>
+    </view>
+  </view>
+</template>
+
+<script lang="ts" setup>
+import type { FormInstance } from 'wot-design-uni/components/wd-form/types'
+import type { CustomerVO } from '@/api/crm/customer'
+import { computed, onMounted, ref } from 'vue'
+import { useToast } from 'wot-design-uni'
+import { createCustomer, getCustomer, updateCustomer } from '@/api/crm/customer'
+import { AreaPicker, UserPicker } from '@/components/system-select'
+import { getIntDictOptions } from '@/hooks/useDict'
+import { useUserStore } from '@/store/user'
+import { navigateBackPlus } from '@/utils'
+import { DICT_TYPE } from '@/utils/constants'
+import { isEmail, isMobile } from '@/utils/validator'
+
+const props = defineProps<{
+  id?: number | any
+}>()
+
+definePage({
+  style: {
+    navigationBarTitleText: '',
+    navigationStyle: 'custom',
+  },
+})
+
+const toast = useToast()
+const userStore = useUserStore()
+const getTitle = computed(() => props.id ? '编辑客户' : '新增客户')
+const formLoading = ref(false)
+const minDate = new Date()
+
+const formData = ref<Partial<CustomerVO>>({
+  id: undefined,
+  name: undefined,
+  contactNextTime: undefined,
+  ownerUserId: undefined,
+  mobile: undefined,
+  telephone: undefined,
+  qq: undefined,
+  wechat: undefined,
+  email: undefined,
+  areaId: undefined,
+  detailAddress: undefined,
+  industryId: undefined,
+  level: undefined,
+  source: undefined,
+  remark: undefined,
+})
+
+const contactNextTimeValue = ref<number | null>(null)
+
+const formRules = {
+  name: [{ required: true, message: '客户名称不能为空' }],
+  ownerUserId: [{ required: true, message: '负责人不能为空' }],
+  mobile: [
+    { required: true, message: '手机号不能为空' },
+    { validator: (value: string) => isMobile(value), message: '请输入正确的手机号' },
+  ],
+  email: [{ required: false, validator: (value: string) => !value || isEmail(value), message: '请输入正确的邮箱地址' }],
+}
+const formRef = ref<FormInstance>()
+
+const sourceDictOptions = computed(() => getIntDictOptions(DICT_TYPE.CRM_CUSTOMER_SOURCE))
+const industryDictOptions = computed(() => getIntDictOptions(DICT_TYPE.CRM_CUSTOMER_INDUSTRY))
+const levelDictOptions = computed(() => getIntDictOptions(DICT_TYPE.CRM_CUSTOMER_LEVEL))
+
+function handleDateTimeConfirm({ value }: { value: number | null }) {
+  formData.value.contactNextTime = value ? new Date(value) : undefined
+}
+
+function handleBack() {
+  navigateBackPlus('/pages-crm/customer/index')
+}
+
+async function getDetail() {
+  if (!props.id) {
+    formData.value.ownerUserId = userStore.userInfo?.id
+    return
+  }
+  const data = await getCustomer(Number(props.id))
+  formData.value = {
+    id: data.id,
+    name: data.name,
+    contactNextTime: data.contactNextTime ? new Date(data.contactNextTime) : undefined,
+    ownerUserId: data.ownerUserId,
+    mobile: data.mobile,
+    telephone: data.telephone,
+    qq: data.qq,
+    wechat: data.wechat,
+    email: data.email,
+    areaId: data.areaId,
+    detailAddress: data.detailAddress,
+    industryId: data.industryId,
+    level: data.level,
+    source: data.source,
+    remark: data.remark,
+  }
+  if (data.contactNextTime) {
+    contactNextTimeValue.value = new Date(data.contactNextTime).getTime()
+  }
+}
+
+function prepareFormData(): Partial<CustomerVO> {
+  const data = { ...formData.value }
+  if (Array.isArray(data.source)) {
+    data.source = data.source[0]
+  }
+  if (Array.isArray(data.industryId)) {
+    data.industryId = data.industryId[0]
+  }
+  if (Array.isArray(data.level)) {
+    data.level = data.level[0]
+  }
+  if (Array.isArray(data.ownerUserId)) {
+    data.ownerUserId = data.ownerUserId[0]
+  }
+  if (Array.isArray(data.areaId)) {
+    data.areaId = data.areaId[0]
+  }
+  return data
+}
+
+async function handleSubmit() {
+  const { valid } = await formRef.value?.validate()
+  if (!valid) {
+    return
+  }
+
+  formLoading.value = true
+  try {
+    const submitData = prepareFormData()
+    if (props.id) {
+      await updateCustomer(submitData as CustomerVO)
+      toast.success('修改成功')
+    } else {
+      await createCustomer(submitData as CustomerVO)
+      toast.success('新增成功')
+    }
+    setTimeout(() => {
+      handleBack()
+    }, 500)
+  } finally {
+    formLoading.value = false
+  }
+}
+
+onMounted(() => {
+  getDetail()
+})
+</script>

+ 178 - 0
src/pages-crm/customer/index.vue

@@ -0,0 +1,178 @@
+<template>
+  <view class="yd-page-container">
+    <wd-navbar
+      title="客户管理"
+      left-arrow placeholder safe-area-inset-top fixed
+      @click-left="handleBack"
+    />
+
+    <view class="search-form-wrapper" :style="{ top: `${getNavbarHeight()}px` }">
+      <SearchForm @search="handleQuery" @reset="handleReset" />
+    </view>
+
+    <view class="p-24rpx" :style="{ paddingTop: `${getNavbarHeight() + 80}px` }">
+      <view
+        v-for="item in list"
+        :key="item.id"
+        class="mb-24rpx rounded-12rpx bg-white"
+        @click="handleDetail(item)"
+      >
+        <view class="p-24rpx">
+          <view class="mb-16rpx flex items-center justify-between">
+            <view class="text-32rpx text-[#333] font-semibold">
+              {{ item.name }}
+            </view>
+            <view class="flex gap-8rpx">
+              <dict-tag v-if="item.lockStatus" :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="true" label="已锁定" />
+              <dict-tag :type="DICT_TYPE.CRM_CUSTOMER_LEVEL" :value="item.level" />
+            </view>
+          </view>
+          <view class="mb-12rpx flex items-center gap-32rpx text-26rpx text-[#999]">
+            <view class="flex items-center gap-8rpx">
+              <wd-icon name="phone" size="24rpx" />
+              <text>{{ item.mobile }}</text>
+            </view>
+            <view v-if="item.ownerUserName" class="flex items-center gap-8rpx">
+              <wd-icon name="user" size="24rpx" />
+              <text>{{ item.ownerUserName }}</text>
+            </view>
+          </view>
+          <view class="flex items-center justify-between text-24rpx text-[#666]">
+            <view class="flex items-center gap-8rpx">
+              <wd-icon name="map" size="22rpx" />
+              <text>{{ item.areaName || '-' }}</text>
+            </view>
+            <text>{{ formatDate(item.createTime) }}</text>
+          </view>
+          <view v-if="item.contactLastContent" class="mt-16rpx border-t border-[#f0f0f0] pt-16rpx">
+            <view class="mb-8rpx text-24rpx text-[#999]">
+              最后跟进:
+            </view>
+            <view class="text-26rpx text-[#666]">
+              {{ item.contactLastContent }}
+            </view>
+          </view>
+        </view>
+      </view>
+
+      <view v-if="loadMoreState !== 'loading' && list.length === 0" class="py-100rpx text-center">
+        <wd-status-tip image="content" tip="暂无客户数据" />
+      </view>
+      <wd-loadmore
+        v-if="list.length > 0"
+        :state="loadMoreState"
+        @reload="loadMore"
+      />
+    </view>
+
+    <wd-fab
+      v-if="hasAccessByCodes(['crm:customer:create'])"
+      position="right-bottom"
+      type="primary"
+      :expandable="false"
+      @click="handleAdd"
+    />
+  </view>
+</template>
+
+<script lang="ts" setup>
+import type { CustomerVO } from '@/api/crm/customer'
+import type { LoadMoreState } from '@/http/types'
+import { onReachBottom, onShow } from '@dcloudio/uni-app'
+import { ref } from 'vue'
+import { getCustomerPage } from '@/api/crm/customer'
+import { useAccess } from '@/hooks/useAccess'
+import { getNavbarHeight, navigateBackPlus } from '@/utils'
+import { DICT_TYPE } from '@/utils/constants'
+import { formatDate } from '@/utils/date'
+import SearchForm from './components/search-form.vue'
+
+definePage({
+  style: {
+    navigationBarTitleText: '',
+    navigationStyle: 'custom',
+  },
+})
+
+const { hasAccessByCodes } = useAccess()
+const total = ref(0)
+const list = ref<CustomerVO[]>([])
+const loadMoreState = ref<LoadMoreState>('loading')
+const queryParams = ref({
+  pageNo: 1,
+  pageSize: 10,
+  sceneType: '1',
+})
+
+function handleBack() {
+  navigateBackPlus()
+}
+
+async function getList() {
+  loadMoreState.value = 'loading'
+  try {
+    const data = await getCustomerPage(queryParams.value)
+    list.value = [...list.value, ...data.list]
+    total.value = data.total
+    loadMoreState.value = list.value.length >= total.value ? 'finished' : 'loading'
+  } catch {
+    queryParams.value.pageNo = queryParams.value.pageNo > 1 ? queryParams.value.pageNo - 1 : 1
+    loadMoreState.value = 'error'
+  }
+}
+
+function handleQuery(data?: Record<string, any>) {
+  queryParams.value = {
+    ...data,
+    pageNo: 1,
+    pageSize: queryParams.value.pageSize,
+    sceneType: data?.sceneType || queryParams.value.sceneType,
+  }
+  list.value = []
+  getList()
+}
+
+function handleReset() {
+  handleQuery()
+}
+
+function loadMore() {
+  if (loadMoreState.value === 'finished') {
+    return
+  }
+  queryParams.value.pageNo++
+  getList()
+}
+
+function handleAdd() {
+  uni.navigateTo({
+    url: '/pages-crm/customer/form/index',
+  })
+}
+
+function handleDetail(item: CustomerVO) {
+  uni.navigateTo({
+    url: `/pages-crm/customer/detail/index?id=${item.id}`,
+  })
+}
+
+onReachBottom(() => {
+  loadMore()
+})
+
+onShow(() => {
+  queryParams.value.pageNo = 1
+  list.value = []
+  getList()
+})
+</script>
+
+<style lang="scss" scoped>
+.search-form-wrapper {
+  position: fixed;
+  left: 0;
+  right: 0;
+  z-index: 100;
+  background-color: #fff;
+}
+</style>

+ 8 - 0
src/pages/index/index.ts

@@ -30,6 +30,14 @@ const menuGroupsData: MenuGroup[] = [
     key: 'crm',
     name: '客户管理',
     menus: [
+      {
+        key: 'crmCustomer',
+        name: '客户管理',
+        icon: 'user-friends',
+        url: '/pages-crm/customer/index',
+        iconColor: '#ff7875',
+        permission: 'crm:customer:query',
+      },
       {
         key: 'crmContact',
         name: '联系人管理',