Просмотр исходного кода

feat: 新增客户管理功能

xyh 3 лет назад
Родитель
Сommit
55cb1346bd

+ 37 - 2
src/apis/client.js

@@ -1,6 +1,41 @@
-import {get} from './request';
+import {del, get, post, put} from './request';
+
+const BASE_URL = '/client';
 
 /** 查询发货天数 */
 export function getDays(code) {
-  return get({url: '/client/getClient', data: {code}});
+  return get({url: BASE_URL + '/getClient', data: {code}});
+}
+
+/**
+ * 查询客户列表
+ *
+ * data: {name, code}
+ */
+export function getCustomerList(data) {
+  return get({url: BASE_URL + '/getClientList', data});
+}
+
+/**
+ *
+ * 添加客户信息
+ *
+ * data: {name, days, code}
+ */
+export function addCustomer(data) {
+  return post({url: BASE_URL + '/addClient', data});
+}
+
+/** 删除客户信息 */
+export function delCustomer(id) {
+  return del({url: BASE_URL + '/delClient', data: {id}});
+}
+
+/**
+ * 修改客户信息
+ *
+ * data: {code, days}
+ */
+export function editCustomer(data) {
+  return put({url: BASE_URL + '/updClient', data});
 }

+ 1 - 0
src/app.config.js

@@ -1,5 +1,6 @@
 export default defineAppConfig({
   pages: [
+    'pages/customer/index',
     'pages/index/index',
     'pages/list/index',
     'pages/receive/index',

Разница между файлами не показана из-за своего большого размера
+ 1 - 0
src/assets/delete.svg


Разница между файлами не показана из-за своего большого размера
+ 1 - 0
src/assets/edit.svg


Разница между файлами не показана из-за своего большого размера
+ 1 - 0
src/assets/plus.svg


+ 8 - 3
src/components/refresh/index.jsx

@@ -18,6 +18,7 @@ const Refresh = function ({
   emptyMsg,
   emptyClassName,
   emptyChildren,
+  endClassName,
 }) {
   const [topRef, onScroll] = useScrollControl();
   const refreshState = useRefreshState(topRef, isRefreshing);
@@ -60,9 +61,13 @@ const Refresh = function ({
       </Text>
 
       <Text
-        className={classNames('hidden text-sm text-center py-4 text-dark-3', {
-          '!block': noMore && !isRefreshing && !isLoading && !empty,
-        })}
+        className={classNames(
+          'hidden text-sm text-center py-4 text-dark-3',
+          endClassName,
+          {
+            '!block': noMore && !isRefreshing && !isLoading && !empty,
+          },
+        )}
       >
         已经到底了~~
       </Text>

+ 17 - 0
src/pages/customer/field/index.jsx

@@ -0,0 +1,17 @@
+import {Input, Text, View} from '@tarojs/components';
+
+export default function Field({title, value, onChange, type, disabled}) {
+  return (
+    <View className='mt-2 first:mt-0'>
+      <Text className='text-lg font-semibold color-[#333] block'>{title}</Text>
+      <Input
+        className='border-0 border-b border-solid border-gray-200 mt-1 py-1'
+        placeholder={`请输入${title}`}
+        value={value}
+        onChange={e => onChange(e.detail.value)}
+        type={type}
+        disabled={disabled}
+      />
+    </View>
+  );
+}

+ 57 - 0
src/pages/customer/filter/index.jsx

@@ -0,0 +1,57 @@
+import {View} from '@tarojs/components';
+import {Popup, Button} from '@antmjs/vantui';
+import Field from '../field';
+
+export default function Filter({
+  onChange,
+  fields,
+  onReset,
+  onConfirm,
+  onClose,
+  visible,
+}) {
+  function reset() {
+    onReset();
+    setTimeout(function () {
+      onConfirm();
+      onClose();
+    }, 0);
+  }
+
+  function confirm() {
+    onConfirm();
+    onClose();
+  }
+
+  return (
+    <Popup
+      show={visible}
+      position='bottom'
+      round
+      className='pt-8 px-4 !pb-6'
+      onClose={onClose}
+    >
+      <Field title='客户名称' value={fields.name} onChange={onChange('name')} />
+      <Field title='客户代码' value={fields.code} onChange={onChange('code')} />
+
+      <View className='flex justify-around mt-8'>
+        <Button
+          round
+          className='border border-solid border-gray-200 w-28'
+          onClick={reset}
+        >
+          重置
+        </Button>
+        <Button
+          type='primary'
+          round
+          className='w-28'
+          color='#58C6EA'
+          onClick={confirm}
+        >
+          查询
+        </Button>
+      </View>
+    </Popup>
+  );
+}

+ 94 - 0
src/pages/customer/hooks.js

@@ -0,0 +1,94 @@
+import {addCustomer, delCustomer, editCustomer} from '@apis';
+import {useMutation} from '@tanstack/react-query';
+import {useBoolean} from 'ahooks';
+import {useState} from 'react';
+import {showLoading, hideLoading, showToast, showModal} from '@tarojs/taro';
+
+export function usePut(refresh) {
+  const [visible, {setTrue, setFalse}] = useBoolean();
+  const [id, setId] = useState(null);
+
+  function onAdd() {
+    setId(null);
+    setTrue();
+  }
+
+  function onEdit(id) {
+    return function () {
+      setId(id);
+      setTrue();
+    };
+  }
+
+  const {mutate: addMutate} = useMutation({
+    mutationFn: addCustomer,
+    onMutate() {
+      showLoading({title: '正在提交'});
+    },
+    onSettled() {
+      hideLoading({noConflict: true});
+    },
+    onSuccess({code}) {
+      if (code === '200') {
+        showToast({title: '新增成功', icon: 'success'});
+        setFalse();
+        refresh();
+      }
+    },
+  });
+
+  const {mutate: editMutate} = useMutation({
+    mutationFn: editCustomer,
+    onMutate() {
+      showLoading({title: '正在提交'});
+    },
+    onSettled() {
+      hideLoading({noConflict: true});
+    },
+    onSuccess({code}) {
+      if (code === '200') {
+        showToast({title: '修改成功', icon: 'success'});
+        setFalse();
+        refresh();
+      }
+    },
+  });
+
+  function onConfirm(params) {
+    id ? editMutate(params) : addMutate(params);
+  }
+
+  return [
+    {putVisible: visible, id},
+    {onAdd, onEdit, onPutClose: setFalse, onConfirm},
+  ];
+}
+
+export function useDelete(refresh) {
+  const {mutate} = useMutation({
+    mutationFn: delCustomer,
+    onMutate() {
+      showLoading({title: '正在删除', mask: true});
+    },
+    onSettled() {
+      hideLoading({noConflict: true});
+    },
+    onSuccess({code}) {
+      if (code === '200') {
+        showToast({icon: 'success', title: '删除成功'});
+        refresh();
+      }
+    },
+  });
+
+  return function (id, name) {
+    return function () {
+      showModal({
+        title: `你确定要删除${name}吗?`,
+        success({confirm}) {
+          confirm && mutate(id);
+        },
+      });
+    };
+  };
+}

+ 0 - 0
src/pages/customer/index.config.js


+ 106 - 0
src/pages/customer/index.jsx

@@ -0,0 +1,106 @@
+import {Refresh} from '@components';
+import Item from './item';
+import {View, Image} from '@tarojs/components';
+import classNames from 'classnames';
+import filterIcon from '@assets/filter.svg';
+import plusIcon from '@assets/plus.svg';
+import Filter from './filter';
+import Put from './put';
+import {useInfiniteFetch} from '@hooks';
+import {getCustomerList} from '@apis';
+import {useState} from 'react';
+import {useBoolean} from 'ahooks';
+import {useDelete, usePut} from './hooks';
+
+export default function Customer() {
+  const [params, setParams] = useState({name: '', code: ''});
+  function onChange(key) {
+    return function (val) {
+      setParams(prev => ({...prev, [key]: val}));
+    };
+  }
+  function onReset() {
+    setParams({name: '', code: ''});
+  }
+  const [filterVisible, {setTrue: showFilter, setFalse: closeFilter}] =
+    useBoolean();
+
+  const [
+    {data, isFetching, isFetchingNextPage, hasNextPage, isEmpty},
+    {fetchNextPage, refresh},
+  ] = useInfiniteFetch({
+    key: [getCustomerList.name],
+    fn: getCustomerList,
+    limit: '10',
+    params,
+  });
+
+  const [{putVisible, id}, {onPutClose, onAdd, onEdit, onConfirm}] =
+    usePut(refresh);
+  const onDelte = useDelete(refresh);
+
+  return (
+    <>
+      <Refresh
+        background='#eee'
+        className='bg-[#eee] h-screen'
+        isRefreshing={isFetching && !isFetchingNextPage}
+        onRefresh={refresh}
+        isLoading={isFetchingNextPage}
+        onLoading={fetchNextPage}
+        noMore={!hasNextPage}
+        empty={isEmpty}
+        endClassName='pb-16'
+      >
+        <View className='h-3' />
+        {(data?.pages ?? []).map(function (el) {
+          return (
+            <Item
+              key={el.id}
+              {...el}
+              onEdit={onEdit}
+              onDel={onDelte(el.id, el.name)}
+            />
+          );
+        })}
+        <View className='h-3' />
+      </Refresh>
+
+      <View
+        className={classNames(
+          'fixed right-3 bottom-8 flex items-center justify-center bg-white rounded-3xl',
+          'w-10 h-10 shadow-lg z-10',
+        )}
+        onClick={showFilter}
+      >
+        <Image src={filterIcon} mode='widthFix' className='w-5' />
+      </View>
+
+      <View
+        className={classNames(
+          'fixed right-3 bottom-20 flex items-center justify-center bg-white rounded-3xl',
+          'w-10 h-10 shadow-lg z-10',
+        )}
+        onClick={onAdd}
+      >
+        <Image src={plusIcon} mode='widthFix' className='w-5' />
+      </View>
+
+      <Filter
+        fields={params}
+        onChange={onChange}
+        onReset={onReset}
+        onConfirm={refresh}
+        visible={filterVisible}
+        onClose={closeFilter}
+      />
+
+      <Put
+        visible={putVisible}
+        onClose={onPutClose}
+        id={id}
+        onConfirm={onConfirm}
+      />
+    </>
+  );
+}

+ 35 - 0
src/pages/customer/item/index.jsx

@@ -0,0 +1,35 @@
+import {View, Text, Image} from '@tarojs/components';
+import classNames from 'classnames';
+import editIcon from '@assets/edit.svg';
+import delIcon from '@assets/delete.svg';
+
+export default function Item({name, days, code, onEdit, onDel}) {
+  return (
+    <View
+      className={classNames(
+        'px-4 py-3 rounded-md bg-white shadow-sm',
+        'to-white to-100%',
+        'mx-3 mb-3',
+      )}
+    >
+      <Text className='text-base text-[#333] font-semibold'>{name}</Text>
+
+      <View className='text-sm text-[#666] flex justify-between mt-2'>
+        <Text>代码: {code}</Text>
+        <Text>周期: {days}天</Text>
+      </View>
+
+      <View className='flex justify-end mt-4'>
+        <View className='flex items-center mr-3' onClick={onEdit(code)}>
+          <Image src={editIcon} mode='widthFix' className='w-4 mr-1' />
+          <Text className='text-sm text-primary'>修改</Text>
+        </View>
+
+        <View className='flex items-center' onClick={onDel}>
+          <Image src={delIcon} mode='widthFix' className='w-4 mr-1' />
+          <Text className='text-sm text-[#ff2121]'>删除</Text>
+        </View>
+      </View>
+    </View>
+  );
+}

+ 41 - 0
src/pages/customer/put/hooks.js

@@ -0,0 +1,41 @@
+import {getCustomerList} from '@apis';
+import {useQueryClient} from '@tanstack/react-query';
+import {useEffect, useState} from 'react';
+
+export function useField(id, visible) {
+  const [fields, setFields] = useState({name: '', code: '', days: ''});
+
+  function onChange(key) {
+    return function (val) {
+      setFields(prev => ({...prev, [key]: val}));
+    };
+  }
+
+  const client = useQueryClient();
+  useEffect(
+    function () {
+      if (!visible) return;
+
+      if (!id) {
+        setFields({name: '', code: '', days: ''});
+      } else {
+        const data = client.getQueryData([getCustomerList.name]);
+        const list = data.pages
+          .map(function (val) {
+            return val.data.list;
+          })
+          .filter(val => val.length > 0)
+          .flat(2);
+        const idx = list.findIndex(val => val.code === id);
+        setFields(function () {
+          const {name, code, days} = list[idx];
+
+          return {name, code, days: Number(days)};
+        });
+      }
+    },
+    [client, id, visible],
+  );
+
+  return [fields, onChange];
+}

+ 70 - 0
src/pages/customer/put/index.jsx

@@ -0,0 +1,70 @@
+import {View} from '@tarojs/components';
+import {Popup, Button} from '@antmjs/vantui';
+import Field from '../field';
+import {useField} from './hooks';
+import {showToast} from '@tarojs/taro';
+
+export default function Put({visible, id, onConfirm, onClose}) {
+  const [{name, code, days}, onChange] = useField(id, visible);
+
+  function confirm() {
+    if (!name.length)
+      return showToast({
+        title: '请输入客户姓名',
+        icon: 'none',
+      });
+    if (!code.length)
+      return showToast({
+        title: '请输入客户编号',
+        icon: 'none',
+      });
+    if (!days.length)
+      return showToast({
+        title: '请输入正确的发货周期',
+        icon: 'none',
+      });
+    onConfirm({name, code, days});
+  }
+
+  return (
+    <Popup
+      show={visible}
+      position='bottom'
+      round
+      className='pt-8 px-4 !pb-6'
+      onClose={onClose}
+    >
+      <Field
+        title='客户名称'
+        value={name}
+        onChange={onChange('name')}
+        disabled={Boolean(id)}
+      />
+      <Field
+        title='客户代码'
+        value={code}
+        onChange={onChange('code')}
+        disabled={Boolean(id)}
+      />
+      <Field
+        title='发货周期'
+        type='number'
+        value={days}
+        onChange={onChange('days')}
+      />
+
+      <View className='flex justify-around mt-8'>
+        <Button
+          type='primary'
+          round
+          block
+          className='w-28'
+          color='#58C6EA'
+          onClick={confirm}
+        >
+          确定
+        </Button>
+      </View>
+    </Popup>
+  );
+}

+ 2 - 2
src/pages/index/hooks.js

@@ -9,7 +9,7 @@ import {userStore} from '@stores';
 import {useEffect, useState} from 'react';
 
 export function useBtnList() {
-  const {power, token} = useStore(userStore);
+  const {power} = useStore(userStore);
 
   const [btnList, setBtnList] = useState([
     {
@@ -28,7 +28,7 @@ export function useBtnList() {
   useEffect(
     function () {
       const powerContext = Number(power),
-        sendContext = /**    */ 0x000001,
+        sendContext = /**       */ 0x000001,
         anomalyContext = /**    */ 0x000010,
         customerContext = /**   */ 0x000100;
 

+ 2 - 0
src/routes/name.js

@@ -4,3 +4,5 @@ export const DELIVER_GOODS_PATH = '/pages/deliver/index';
 export const RECEIVE_GOODS_PATH = '/pages/receive/index';
 /** 发货单列表 */
 export const ORDER_PATH = '/pages/list/index';
+/** 客户管理 */
+export const CUSTOMER_PATH = '/pages/customer/index';