Forráskód Böngészése

feat: 个人标签新增checkbox模式

lanjianrong 5 éve
szülő
commit
0872a8e1c3

+ 1 - 0
package.json

@@ -95,6 +95,7 @@
     "detect-installer": "^1.0.1",
     "enzyme": "^3.11.0",
     "eslint": "^7.18.0",
+    "eslint-plugin-javascript": "^1.3.4",
     "eslint-plugin-jsx": "^0.1.0",
     "eslint-plugin-typescript": "^0.14.0",
     "express": "^4.17.1",

+ 5 - 0
src/basic/css/common.scss

@@ -29,6 +29,7 @@
   background-color: $zh-white;
 }
 
+
 /** 字体颜色 */
 .zh-gray {
   color: $zh-gray;
@@ -54,6 +55,10 @@
   color: $zh-muted;
 }
 
+.zh-primary {
+  color: $zh-primary;
+}
+
 /** 文本水平对齐方向 */
 .zh-text-center {
   text-align: center;

+ 1 - 1
src/basic/css/variable.scss

@@ -11,7 +11,7 @@ $zh-line-color: rgba(0, 0, 0, 0.125) !default;
 $zh-danger: #fd3995 !default;
 $zh-warning: #ffc241 !default;
 $zh-success: #1dc9b7 !default;
-
+$zh-primary: #886ab5 !default;
 // 实现0.5px的效果
 @mixin zh-border {
   position: relative;

+ 20 - 2
src/models/customer.js

@@ -1,10 +1,13 @@
 import consts from '@/consts'
 import { queryNatures } from '@/services/contact'
+import { queryTagList } from '@/services/customer'
 
 export default {
   namespace: 'customer',
   state: {
-    natures: [] // 客户性质列表
+    natures: [], // 客户性质列表
+    personTags: [], // 个人标签
+    teamTags: [] // 协作标签
   },
   effects: {
     *fetch({ payload }, { call, put }) {
@@ -13,6 +16,20 @@ export default {
         yield put({
           type: 'changeState',
           payload: {
+            key: 'natures',
+            data: response.data
+          }
+        })
+      }
+    },
+    *fetchTags({ payload }, { call, put }) {
+      const response = yield call(queryTagList, payload)
+      if (response?.code === consts.RET_CODE.SUCCESS) {
+        const { tagType } = payload
+        yield put({
+          type: 'changeState',
+          payload: {
+            key: tagType === 0 ? 'personTags' : 'teamTags',
             data: response.data
           }
         })
@@ -22,9 +39,10 @@ export default {
   reducers: {
     // 通用reducer
     changeState(state, action) {
+      console.log('action', action)
       return {
         ...state,
-        natures: action.payload.data
+        [action.payload.key]: action.payload.data
       }
     }
   }

+ 185 - 0
src/pages/Customer/Company/components/PersonLabel/index.jsx

@@ -0,0 +1,185 @@
+import { Button, Checkbox, Dropdown, Popover } from 'antd'
+import React, { useState } from 'react'
+import { connect } from 'dva'
+import styles from './index.scss'
+import { apiConnOrDelTag } from '@/services/customer'
+
+const DropdownTypeEnum = {
+  check: 0,
+  edit: 1
+}
+// 数据类型
+export const TagDataTypeEnum = {
+  CONTACT: 0, // 联系人
+  COMPANY: 1 // 客户
+}
+// 标签类型
+export const TagTypeEnum = {
+  PERSONTAG: 0, // 个人
+  TEAMTAG: 1 // 协作
+}
+export const LabelModeType = {
+  column: 0, // 列操作
+  toolbar: 1 // 常用于表格批量操作
+}
+
+export const LabelChangeType = {
+  conn: 0, // 关联标签
+  del: 1 // 删除标签
+}
+
+const PersonLabel = props => {
+  const {
+    children,
+    dataId,
+    tagIds = [],
+    tagType,
+    tagColumn,
+    modeType = 'column',
+    checkCallBack,
+    dispatch,
+    personTags
+  } = props
+  const [dropdownType, setDropdownType] = useState(DropdownTypeEnum.check)
+
+  if (!personTags?.length) {
+    dispatch({
+      type: 'customer/fetchTags',
+      payload: { tagType, tagColumn }
+    })
+  }
+
+  const menu = (
+    <div className="zh-mg-tb-7 zh-mg-lf-14">
+      <DropDownMenu
+        dataId={dataId}
+        personTags={personTags}
+        tagIds={tagIds}
+        menuType={dropdownType}
+        tagType={tagType}
+        tagColumn={tagColumn}
+        modeType={modeType}
+        checkCallBack={checkCallBack}
+      />
+      <div className="zh-mg-top-5">
+        {dropdownType === DropdownTypeEnum.check ? (
+          <span
+            onClick={() => setDropdownType(DropdownTypeEnum.edit)}
+            className="zh-primary zh-pointer">
+            编辑标签
+          </span>
+        ) : (
+          <>
+            <Button type="primary">确认</Button>
+            <Button type="default" className="zh-mg-left-5">
+              取消
+            </Button>
+          </>
+        )}
+      </div>
+    </div>
+  )
+  return (
+    <Popover content={menu} trigger="click" className={styles.dropdownMenu}>
+      {children}
+    </Popover>
+  )
+}
+
+const DropDownMenu = props => {
+  // 保存选中的checkbox ids
+  const {
+    tagIds,
+    dataId,
+    menuType,
+    tagType,
+    tagColumn,
+    personTags,
+    modeType,
+    checkCallBack
+  } = props
+  const [checkedIds, setCheckedIds] = useState(tagIds)
+
+  console.log('personTags', personTags)
+  const statusColors = {
+    [TagTypeEnum.PERSONTAG]: [
+      '#16A085',
+      '#2980B9',
+      '#8E44AD',
+      '#f90000',
+      '#B8651B',
+      '#2C3E50',
+      '#efd200'
+    ],
+    [TagTypeEnum.TEAMTAG]: [
+      '#F8AC59',
+      '#FF69B4',
+      '#999999',
+      '#7186ab',
+      '#778b72',
+      '#c292ca',
+      '#a14751'
+    ]
+  }
+
+  const handleOnchange = async (e, id) => {
+    const { checked } = e.target
+    if (modeType === LabelModeType.column) {
+      const payload = {
+        type: checked ? LabelChangeType.conn : LabelChangeType.del,
+        data: { tagId: id, dataId, dataType: tagColumn, tagType }
+      }
+      const { code = -1 } = await apiConnOrDelTag(payload)
+    } else {
+      const ids = checked ? [...checkedIds, id] : checkedIds.filter(item => item)
+      setCheckedIds(ids)
+      checkCallBack && checkCallBack(ids)
+    }
+  }
+  return (
+    <ul className={styles.labelContent}>
+      {menuType === DropdownTypeEnum.check
+        ? personTags.map((item, idx) => (
+            <li key={item.id} className={styles.menuItem}>
+              <div className={styles.labelText}>
+                <div
+                  className={[
+                    styles.statusIcon,
+                    tagType === TagTypeEnum.PERSONTAG ? styles.suqare : ''
+                  ].join(' ')}
+                  style={{ backgroundColor: statusColors[tagType][idx] }}
+                />
+                <span>{item.name}</span>
+              </div>
+              <div className={styles.checkSpan}>
+                <Checkbox
+                  checked={checkedIds.includes(item.id)}
+                  onChange={e => handleOnchange(e, item.id)}
+                />
+              </div>
+            </li>
+          ))
+        : null}
+      {menuType === DropdownTypeEnum.edit
+        ? personTags.map((item, idx) => (
+            <li key={item.id} className={styles.menuItem}>
+              <div>
+                <div
+                  className={[
+                    styles.statusIcon,
+                    tagType === TagTypeEnum.PERSONTAG ? styles.suqare : ''
+                  ].join(' ')}
+                  style={{ backgroundColor: statusColors[tagType][idx] }}
+                />
+                <span>{item.name}</span>
+              </div>
+            </li>
+          ))
+        : null}
+    </ul>
+  )
+}
+
+export default connect(({ customer }) => ({
+  personTags: customer.personTags
+}))(PersonLabel)

+ 51 - 0
src/pages/Customer/Company/components/PersonLabel/index.scss

@@ -0,0 +1,51 @@
+.labelContent {
+  margin-bottom: 0;
+  padding: 0;
+  list-style: none;
+  .menuItem {
+    display: flex;
+    flex-direction: row;
+    flex-wrap: nowrap;
+    margin-bottom: 0.25rem;
+    padding: 0;
+    color: #666666;
+    background-color: #ffffff;
+    border: 1px solid #e5e5e5;
+    box-shadow: 0 0.2rem 0.325rem rgb(0 0 0 / 4%);
+    &:last-of-type {
+      margin: 0;
+    }
+    .labelText {
+      display: flex;
+      align-items: center;
+      width: 80%;
+      max-width: 200px;
+      padding: 0.5rem 0.875rem;
+      overflow: hidden;
+      white-space: nowrap;
+      text-overflow: ellipsis;
+      border-right: 1px solid #e5e5e5;
+    }
+    .editLabel {
+      display: flex;
+      align-items: center;
+    }
+    .statusIcon {
+      width: 12px;
+      height: 12px;
+      margin-right: 0.5rem;
+      line-height: 12px;
+      &::after {
+        content: ' ';
+      }
+      &.suqare {
+        border-radius: 50%;
+      }
+    }
+    .checkSpan {
+      display: flex;
+      align-items: center;
+      padding: 0.5rem 0.875rem;
+    }
+  }
+}

+ 16 - 16
src/pages/Customer/Contact/index.jsx

@@ -97,7 +97,7 @@ const Contact = () => {
       dataIndex: 'department',
       key: 'department',
       ellipsis: true,
-      width: 100
+      width: 75
     },
     // {
     //   title: '电话',
@@ -109,38 +109,38 @@ const Contact = () => {
       title: '手机',
       dataIndex: 'telephone',
       key: 'telephone',
-      width: 150
+      width: 75
     },
     {
       title: 'QQ',
       dataIndex: 'qq',
       key: 'qq',
-      width: 100
+      width: 75
     },
     {
       title: '邮箱',
       dataIndex: 'email',
       key: 'email',
-      width: 180
+      width: 75
     },
     {
       title: '职务',
       dataIndex: 'position',
       key: 'position',
-      width: 100
+      width: 25
     },
     {
       title: '办公室',
       dataIndex: 'office',
       key: 'office',
-      width: 150,
+      width: 25,
       ellipsis: true
     },
     {
       title: '地区',
       dataIndex: 'local',
       key: 'local',
-      width: 200
+      width: 25
     },
     {
       title: '个人标签',
@@ -231,60 +231,60 @@ const Contact = () => {
       dataIndex: 'address',
       key: 'address',
       ellipsis: true,
-      width: 350
+      width: 50
     },
     {
       title: '乘车',
       dataIndex: 'ride',
       key: 'ride',
       ellipsis: true,
-      width: 350
+      width: 25
     },
     {
       title: '地标',
       dataIndex: 'landmarks',
       key: 'landmarks',
       ellipsis: true,
-      width: 150
+      width: 50
     },
     {
       title: '住宿',
       dataIndex: 'stay',
       key: 'stay',
       ellipsis: true,
-      width: 250
+      width: 50
     },
     {
       title: '备注',
       dataIndex: 'mark',
       key: 'mark',
       ellipsis: true,
-      width: 150
+      width: 50
     },
     {
       title: '软件锁',
       dataIndex: 'keynum',
       key: 'keynum',
-      width: 150,
+      width: 50,
       render: keynum => <a>{keynum}</a>
     },
     {
       title: '大司空',
       dataIndex: 'dasikong',
       key: 'dasikong',
-      width: 150
+      width: 50
     },
     {
       title: '养护云',
       dataIndex: 'yanghuyun',
       key: 'yanghuyun',
-      width: 150
+      width: 50
     },
     {
       title: '创建人',
       dataIndex: 'chuanpeople',
       key: 'chuanpeople',
-      width: 150
+      width: 50
     }
   ]
 

+ 12 - 6
src/pages/Customer/Test/index.jsx

@@ -5,6 +5,9 @@ import { useModal } from '@/components/Modal'
 import { connect } from 'dva'
 import SearchModal from '@/pages/Customer/Contact/components/CustomerModal'
 import ChangeCompanyInput from '../Contact/components/ChangeCompany'
+import PersonLabel from '../Company/components/PersonLabel'
+import { TagTypeEnum } from '../Company/components/PersonLabel'
+import { TagDataTypeEnum } from '../Company/components/PersonLabel'
 
 const { Option } = Select
 const Test = ({ natures, dispatch }) => {
@@ -86,12 +89,12 @@ const Test = ({ natures, dispatch }) => {
     !natures.length && initNatures()
   }, [])
 
-  const a = useMemo(() => {
-    const e = null
-    const b = 2
-    const c = e ?? b
-    console.log('c', c)
-  }, [state])
+  // const a = useMemo(() => {
+  //   const e = null
+  //   const b = 2
+  //   const c = e ?? b
+  //   console.log('c', c)
+  // }, [state])
   // const customOptions = useCallback(originNode => {
   //   return originNode
   // }, [])
@@ -134,6 +137,9 @@ const Test = ({ natures, dispatch }) => {
         options={options}
         style={{ width: '200px' }}
       /> */}
+      <PersonLabel tagType={TagTypeEnum.PERSONTAG} tagColumn={TagDataTypeEnum.CONTACT}>
+        <Button>2222</Button>
+      </PersonLabel>
     </Card>
   )
 }

+ 27 - 0
src/services/customer.js

@@ -1,5 +1,6 @@
 import request from '@/basic/utils/request'
 import consts from '@/consts'
+import { LabelChangeType } from '@/pages/Customer/Company/components/PersonLabel'
 
 /**
  * 获取客户列表
@@ -25,3 +26,29 @@ export async function createCustomer(payload) {
   })
   return data
 }
+
+/**
+ * 获取标签列表
+ * @param {*} params
+ */
+export async function queryTagList(params) {
+  const data = await request('/api/tag/list', { params })
+  return data
+}
+
+/**
+ * 标签关联/删除
+ * @param {*} type
+ * @param {*} payload
+ * @returns
+ */
+export async function apiConnOrDelTag({ type, data }) {
+  let url = ''
+  if (type === LabelChangeType.conn) {
+    url = '/api/tag/link'
+  } else {
+    url = '/api/tag/link/delete'
+  }
+  const res = await request(url, { data })
+  return res
+}