Commit 6e9d80c33d300675b7b519d3f415b6ab88c1ce41

Authored by 杜志良
2 parents 29701274 346196a5

Merge branch 'master' into d-cas

Showing 37 changed files with 1896 additions and 1067 deletions
afterbuild.js 0 → 100644
  1 +const fs = require("fs");
  2 +
  3 +const encoding = "UTF-8";
  4 +/**
  5 + * 写入version信息方便判断版本
  6 + */
  7 +function sendfile() {
  8 + try {
  9 + const pathFolder = "./dist/version.text";
  10 + deleteFolder(pathFolder);
  11 + fs.writeFileSync(pathFolder, new Date().getTime().toString(), { encoding });
  12 + } catch (e) {
  13 + console.error("sourcemap文件写入失败", e);
  14 + }
  15 +}
  16 +
  17 +/**删除文件或文件夹 */
  18 +function deleteFolder(path) {
  19 + let files = [];
  20 + if (fs.existsSync(path)) {
  21 + if (fs.statSync(path).isFile()) {
  22 + fs.unlinkSync(path); //删除文件
  23 + } else {
  24 + files = fs.readdirSync(path);
  25 + files.forEach((file, index) => {
  26 + let curPath = path + "/" + file;
  27 + if (fs.statSync(curPath).isDirectory()) {
  28 + // recurse
  29 + deleteFolder(curPath);
  30 + } else {
  31 + // delete file
  32 + fs.unlinkSync(curPath);
  33 + }
  34 + });
  35 + fs.rmdirSync(path);
  36 + }
  37 + }
  38 +}
  39 +
  40 +sendfile();
... ...
config/routers/pms.ts
... ... @@ -44,6 +44,10 @@ export default [
44 44 component: './pms/storage/partShop'
45 45 },
46 46 {
  47 + path: '/pms/storage/fwStockPartShop', // 服务站配件(霏微(库存))
  48 + component: './pms/storage/partShop'
  49 + },
  50 + {
47 51 path: '/pms/storage/areaStorage', // 区域库设置
48 52 component: './pms/storage/areaStorageSetting'
49 53 },
... ...
package.json
... ... @@ -5,8 +5,8 @@
5 5 "description": "霏微汽车云平台",
6 6 "scripts": {
7 7 "analyze": "cross-env ANALYZE=1 umi build",
8   - "build:prod": "cross-env REACT_APP_ENV=prod umi build",
9   - "build:unset": "cross-env REACT_APP_ENV=dev umi build",
  8 + "build:prod": "cross-env REACT_APP_ENV=prod umi build && node afterbuild.js",
  9 + "build:unset": "cross-env REACT_APP_ENV=dev umi build && node afterbuild.js",
10 10 "lint": "npm run lint:js && npm run lint:style && npm run lint:prettier",
11 11 "lint-staged": "lint-staged",
12 12 "lint-staged:js": "eslint --ext .js,.jsx,.ts,.tsx ",
... ... @@ -14,16 +14,16 @@
14 14 "lint:js": "eslint --cache --ext .js,.jsx,.ts,.tsx --format=pretty ./",
15 15 "lint:prettier": "check-prettier lint",
16 16 "lint:style": "stylelint --fix \"src/**/*.less\" --syntax less",
  17 + "package": "chmod +x ./build.sh && ./build.sh",
17 18 "prettier": "prettier -c --write \"**/*\"",
18 19 "site": "cross-env npm run fetch:blocks && npm run build && npm run functions:build",
19 20 "start": "cross-env HOST=devlocal.feewee.cn umi dev",
20 21 "start:dev": "cross-env REACT_APP_ENV=dev MOCK=none UMI_UI=none HOST=devlocal.feewee.cn umi dev",
21   - "start:test": "cross-env REACT_APP_ENV=test MOCK=none UMI_UI=none HOST=testlocal.feewee.cn umi dev",
22 22 "start:prod": "cross-env REACT_APP_ENV=prod MOCK=none UMI_UI=none HOST=local.feewee.cn umi dev",
  23 + "start:test": "cross-env REACT_APP_ENV=test MOCK=none UMI_UI=none HOST=testlocal.feewee.cn umi dev",
23 24 "test": "umi test",
24 25 "test:all": "node ./tests/run-tests.js",
25   - "test:component": "umi test ./src/components",
26   - "package": "chmod +x ./build.sh && ./build.sh"
  26 + "test:component": "umi test ./src/components"
27 27 },
28 28 "husky": {
29 29 "hooks": {}
... ...
src/components/GlobalFooter/index.tsx
1   -import React from 'react';
2   -import { CopyrightOutlined } from '@ant-design/icons';
3   -import { Layout } from 'antd';
4   -import classNames from 'classnames';
5   -import styles from './index.less';
  1 +import React, { useCallback, useEffect, useRef } from "react";
  2 +import { CopyrightOutlined } from "@ant-design/icons";
  3 +import { Button, Layout, Space, notification } from "antd";
  4 +import classNames from "classnames";
  5 +import styles from "./index.less";
  6 +import { useIntl } from "umi";
6 7  
7 8 export interface IGlobalFooterProps {
8 9 links?: Array<{
... ... @@ -17,25 +18,74 @@ const { Footer } = Layout;
17 18 const COPYRIGHT = `${new Date().getFullYear()} 重庆霏微科技有限公司 渝ICP备17016156号-3`;
18 19  
19 20 const GlobalFooter = ({ links }: IGlobalFooterProps) => {
  21 + const notifiShowRef = useRef<boolean>(false);
  22 +
  23 + const intl = useIntl();
  24 +
  25 + useEffect(() => {
  26 + document.getElementById("fw_updater")?.addEventListener("click", showNotification);
  27 + return () => {
  28 + document.getElementById("fw_updater")?.removeEventListener("click", showNotification);
  29 + };
  30 + }, []);
  31 +
  32 + const showNotification = useCallback(() => {
  33 + if (notifiShowRef?.current) {
  34 + return;
  35 + }
  36 + const key = `open${Date.now()}`;
  37 + const btn = (
  38 + <Space>
  39 + <Button
  40 + size="small"
  41 + onClick={() => {
  42 + notification.close(key);
  43 + notifiShowRef.current = false;
  44 + }}
  45 + >
  46 + {intl.formatMessage({ id: "app.update.tip.close" })}
  47 + </Button>
  48 + <Button type="primary" size="small" onClick={() => window.location.reload()}>
  49 + {intl.formatMessage({ id: "app.update.tip.confirm" })}
  50 + </Button>
  51 + </Space>
  52 + );
  53 + notification.open({
  54 + message: intl.formatMessage({ id: "app.update.tip.title" }),
  55 + description: intl.formatMessage({ id: "app.update.tip.message" }),
  56 + btn,
  57 + placement: "bottomRight",
  58 + key,
  59 + duration: 0,
  60 + onClose: () => {
  61 + notifiShowRef.current = false;
  62 + },
  63 + });
  64 + notifiShowRef.current = true;
  65 + }, []);
  66 +
20 67 const clsString = classNames(styles.globalFooter);
21 68 return (
22 69 <Footer style={{ padding: 0 }}>
23 70 <div className={clsString}>
24 71 {links && (
25 72 <div className={styles.links}>
26   - {links.map(link => (
  73 + {links.map((link) => (
27 74 <a
28 75 key={link.key}
29 76 title={link.key}
30   - target={link.blankTarget ? '_blank' : '_self'}
  77 + target={link.blankTarget ? "_blank" : "_self"}
31 78 href={link.href}
  79 + rel="noreferrer"
32 80 >
33 81 {link.title}
34 82 </a>
35 83 ))}
36 84 </div>
37 85 )}
38   - <div className={styles.copyright}>Copyright <CopyrightOutlined /> {COPYRIGHT}</div>
  86 + <div className={styles.copyright}>
  87 + Copyright <CopyrightOutlined /> {COPYRIGHT}
  88 + </div>
39 89 </div>
40 90 </Footer>
41 91 );
... ...
src/global.tsx
... ... @@ -82,7 +82,6 @@ if (pwa) {
82 82 }
83 83 }
84 84  
85   -
86 85 //@ts-ignore
87 86 if (!window.Number.prototype._toFixed) {
88 87 window.Number.prototype._toFixed = window.Number.prototype.toFixed;
... ...
src/locales/en-US.ts
... ... @@ -6,11 +6,16 @@ import settingDrawer from &#39;./en-US/settingDrawer&#39;;
6 6 import settings from './en-US/settings';
7 7  
8 8 export default {
9   - 'navBar.lang': 'Languages',
10   - 'layout.user.link.help': 'Help',
11   - 'layout.user.link.privacy': 'Privacy',
12   - 'layout.user.link.terms': 'Terms',
13   - 'app.preview.down.block': 'Download this page to your local project',
  9 + "navBar.lang": "Languages",
  10 + "layout.user.link.help": "Help",
  11 + "layout.user.link.privacy": "Privacy",
  12 + "layout.user.link.terms": "Terms",
  13 + "app.preview.down.block": "Download this page to your local project",
  14 + "app.update.tip.title": "Update Tips",
  15 + "app.update.tip.message":
  16 + "It is detected that the content of the website has been updated. Do you want to refresh the page to load the latest version?",
  17 + "app.update.tip.confirm": "Confirm",
  18 + "app.update.tip.close": "Close",
14 19 ...globalHeader,
15 20 ...menu,
16 21 ...settingDrawer,
... ...
src/locales/pt-BR.ts
... ... @@ -6,11 +6,16 @@ import settingDrawer from &#39;./pt-BR/settingDrawer&#39;;
6 6 import settings from './pt-BR/settings';
7 7  
8 8 export default {
9   - 'navBar.lang': 'Idiomas',
10   - 'layout.user.link.help': 'ajuda',
11   - 'layout.user.link.privacy': 'política de privacidade',
12   - 'layout.user.link.terms': 'termos de serviços',
13   - 'app.preview.down.block': 'Download this page to your local project',
  9 + "navBar.lang": "Idiomas",
  10 + "layout.user.link.help": "ajuda",
  11 + "layout.user.link.privacy": "política de privacidade",
  12 + "layout.user.link.terms": "termos de serviços",
  13 + "app.preview.down.block": "Download this page to your local project",
  14 + "app.update.tip.title": "Update Tips",
  15 + "app.update.tip.message":
  16 + "It is detected that the content of the website has been updated. Do you want to refresh the page to load the latest version?",
  17 + "app.update.tip.confirm": "Confirm",
  18 + "app.update.tip.close": "Close",
14 19 ...globalHeader,
15 20 ...menu,
16 21 ...settingDrawer,
... ...
src/locales/zh-CN.ts
... ... @@ -6,11 +6,15 @@ import settingDrawer from &#39;./zh-CN/settingDrawer&#39;;
6 6 import settings from './zh-CN/settings';
7 7  
8 8 export default {
9   - 'navBar.lang': '语言',
10   - 'layout.user.link.help': '帮助',
11   - 'layout.user.link.privacy': '隐私',
12   - 'layout.user.link.terms': '条款',
13   - 'app.preview.down.block': '下载此页面到本地项目',
  9 + "navBar.lang": "语言",
  10 + "layout.user.link.help": "帮助",
  11 + "layout.user.link.privacy": "隐私",
  12 + "layout.user.link.terms": "条款",
  13 + "app.preview.down.block": "下载此页面到本地项目",
  14 + "app.update.tip.title": "更新提示",
  15 + "app.update.tip.message": "检测到网站内容有更新,是否刷新页面加载最新版本?",
  16 + "app.update.tip.confirm": "确认",
  17 + "app.update.tip.close": "关闭",
14 18 ...globalHeader,
15 19 ...menu,
16 20 ...settingDrawer,
... ...
src/locales/zh-TW.ts
... ... @@ -6,11 +6,15 @@ import settingDrawer from &#39;./zh-TW/settingDrawer&#39;;
6 6 import settings from './zh-TW/settings';
7 7  
8 8 export default {
9   - 'navBar.lang': '語言',
10   - 'layout.user.link.help': '幫助',
11   - 'layout.user.link.privacy': '隱私',
12   - 'layout.user.link.terms': '條款',
13   - 'app.preview.down.block': '下載此頁面到本地項目',
  9 + "navBar.lang": "語言",
  10 + "layout.user.link.help": "幫助",
  11 + "layout.user.link.privacy": "隱私",
  12 + "layout.user.link.terms": "條款",
  13 + "app.preview.down.block": "下載此頁面到本地項目",
  14 + "app.update.tip.title": "更新提示",
  15 + "app.update.tip.message": "檢測到網站內容有更新,是否刷新頁面加載最新版本?",
  16 + "app.update.tip.confirm": "確認",
  17 + "app.update.tip.close": "關閉",
14 18 ...globalHeader,
15 19 ...menu,
16 20 ...settingDrawer,
... ...
src/pages/document.ejs
... ... @@ -3,19 +3,16 @@
3 3 <head>
4 4 <meta charset="UTF-8" />
5 5 <meta http-equiv="X-UA-Compatible" content="IE=edge" />
6   - <meta
7   - name="viewport"
8   - content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"
9   - />
  6 + <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0" />
10 7 <title>霏微汽车服务平台</title>
11 8 <script src="https://gw.alipayobjects.com/os/antv/pkg/_antv.data-set-0.9.6/dist/data-set.min.js"></script>
12 9 <!--加载鼠标绘制工具-->
13 10 <link rel="icon" href="/favicon.png" type="image/x-icon" />
14 11 <!-- 高德地图 -->
15 12 <script type="text/javascript">
16   - window._AMapSecurityConfig = {
17   - securityJsCode: '235e8683072929e38b792ded30736d2d',
18   - }
  13 + window._AMapSecurityConfig = {
  14 + securityJsCode: "235e8683072929e38b792ded30736d2d",
  15 + };
19 16 </script>
20 17 </head>
21 18 <body>
... ... @@ -23,34 +20,34 @@
23 20 <div id="root">
24 21 <style>
25 22 .page-loading-warp {
26   - padding: 120px;
27 23 display: flex;
28   - justify-content: center;
29 24 align-items: center;
  25 + justify-content: center;
  26 + padding: 120px;
30 27 }
31 28 .ant-spin {
  29 + position: absolute;
  30 + display: none;
32 31 -webkit-box-sizing: border-box;
33 32 box-sizing: border-box;
34 33 margin: 0;
35 34 padding: 0;
36 35 color: rgba(0, 0, 0, 0.65);
  36 + color: #1890ff;
37 37 font-size: 14px;
38 38 font-variant: tabular-nums;
39 39 line-height: 1.5;
40   - list-style: none;
41   - -webkit-font-feature-settings: 'tnum';
42   - font-feature-settings: 'tnum';
43   - position: absolute;
44   - display: none;
45   - color: #1890ff;
46 40 text-align: center;
47 41 vertical-align: middle;
  42 + list-style: none;
48 43 opacity: 0;
49 44 -webkit-transition: -webkit-transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
50 45 transition: -webkit-transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
51 46 transition: transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
52 47 transition: transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86),
53 48 -webkit-transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
  49 + -webkit-font-feature-settings: "tnum";
  50 + font-feature-settings: "tnum";
54 51 }
55 52  
56 53 .ant-spin-spinning {
... ... @@ -62,9 +59,9 @@
62 59 .ant-spin-dot {
63 60 position: relative;
64 61 display: inline-block;
65   - font-size: 20px;
66 62 width: 20px;
67 63 height: 20px;
  64 + font-size: 20px;
68 65 }
69 66  
70 67 .ant-spin-dot-item {
... ... @@ -120,9 +117,9 @@
120 117 }
121 118  
122 119 .ant-spin-lg .ant-spin-dot {
123   - font-size: 32px;
124 120 width: 32px;
125 121 height: 32px;
  122 + font-size: 32px;
126 123 }
127 124  
128 125 .ant-spin-lg .ant-spin-dot i {
... ... @@ -166,11 +163,12 @@
166 163 <div class="page-loading-warp">
167 164 <div class="ant-spin ant-spin-lg ant-spin-spinning">
168 165 <span class="ant-spin-dot ant-spin-dot-spin"
169   - ><i class="ant-spin-dot-item"></i><i class="ant-spin-dot-item"></i
170   - ><i class="ant-spin-dot-item"></i><i class="ant-spin-dot-item"></i
  166 + ><i class="ant-spin-dot-item"></i><i class="ant-spin-dot-item"></i><i class="ant-spin-dot-item"></i
  167 + ><i class="ant-spin-dot-item"></i
171 168 ></span>
172 169 </div>
173 170 </div>
174 171 </div>
  172 + <button id="fw_updater" style="display: none"></button>
175 173 </body>
176 174 </html>
... ...
src/pages/order3/SaleTask/api.ts
... ... @@ -25,6 +25,7 @@ export interface GetSaleTaskApiRes {
25 25 testDriveTaskCount: number;
26 26 seriesTaskCount: number;
27 27 vehicleGrossProfitTask: number;
  28 + totalGrossProfitTask: number;
28 29 }
29 30  
30 31 export interface SeriesTaskItem {
... ... @@ -71,6 +72,8 @@ export interface ShopTaskItem {
71 72 seriesTaskCount: number;
72 73 vehicleGrossProfitTask: number;
73 74 taskId?: number;
  75 + orderTaskApplyId?: number;
  76 + orderShopTaskId?: number;
74 77 }
75 78  
76 79 /** 月度零售任务列表 */
... ... @@ -81,8 +84,8 @@ export function getSaleTaskApi(
81 84 }
82 85  
83 86 export interface GetShopSaleTaskReq {
84   - shopId: number;
85   - taskDate: number;
  87 + shopId?: number;
  88 + taskDate?: number;
86 89 }
87 90  
88 91 /** 门店零售任务详情 */
... ... @@ -109,6 +112,54 @@ export function submitSaleTask(params: { id: number }): PromiseResp&lt;boolean&gt; {
109 112 return request.post(`${ORDER3_HOST}/erp/sales/task/submit`, params);
110 113 }
111 114  
  115 +export interface AutoAssignItem {
  116 + shopId: number;
  117 + taskCount: number;
  118 + newEnergyTaskCount: number;
  119 + fuelVehicleTaskCount: number;
  120 + tackCarTaskCount: number;
  121 +}
  122 +
  123 +export interface AutoAssignSaleTaskReq {
  124 + id: number;
  125 + assignTask: boolean;
  126 + shopTaskList: AutoAssignItem[];
  127 +}
  128 +
  129 +/** 自动分配零售任务 */
  130 +export function autoAssignSaleTask(
  131 + params: AutoAssignSaleTaskReq
  132 +): PromiseResp<boolean> {
  133 + return request.post(`${ORDER3_HOST}/erp/sales/task/auto/assign`, params);
  134 +}
  135 +
  136 +interface BatchSetSaleTaskItem {
  137 + shopIdList: number[];
  138 + taskAims: number;
  139 +}
  140 +
  141 +export interface BatchSetSaleTaskReq {
  142 + grossProfitTaskList?: BatchSetSaleTaskItem[];
  143 + tackCarTaskList?: BatchSetSaleTaskItem[];
  144 + testDriveTaskList?: BatchSetSaleTaskItem[];
  145 + assignTask: boolean;
  146 + orderTaskApplyId: number;
  147 +}
  148 +
  149 +/** 批量设置零售任务 */
  150 +export function batchSetSaleTask(
  151 + params: BatchSetSaleTaskReq
  152 +): PromiseResp<boolean> {
  153 + return request.post(`${ORDER3_HOST}/erp/sales/task/batch/shop/set`, params);
  154 +}
  155 +
  156 +/** 自动分配单个门店的零售任务 */
  157 +export function autoAssignOneShop(
  158 + params: ShopTaskItem
  159 +): PromiseResp<boolean> {
  160 + return request.post(`${ORDER3_HOST}/erp/sales/task/auto/assign/shop`, params);
  161 +}
  162 +
112 163 export interface BrandItem {
113 164 id: number;
114 165 initial: string;
... ... @@ -148,9 +199,10 @@ export interface PreviewTaskReq {
148 199 secondManageId?: number;
149 200 thirdManageId?: number;
150 201 taskId?: number;
151   - orderTaskApprovalType: number;
152   - id: number;
  202 + orderTaskApprovalType?: number;
  203 + id?: number;
153 204 token?: string;
  205 + keywords?: string;
154 206 }
155 207  
156 208 export interface TaskListItem {
... ... @@ -188,7 +240,9 @@ export interface PreviewTaskRes {
188 240 }
189 241  
190 242 /** 预览任务 */
191   -export function previewTask(params: PreviewTaskReq): PromiseResp<PreviewTaskRes> {
  243 +export function previewTask(
  244 + params: PreviewTaskReq
  245 +): PromiseResp<PreviewTaskRes> {
192 246 return request.get(`${ORDER3_HOST}/erp/sales/task/approve/info`, {
193 247 params,
194 248 });
... ...
src/pages/order3/SaleTask/components/AdviserTaskPreview.tsx
... ... @@ -4,8 +4,7 @@ import { observer } from &quot;mobx-react-lite&quot;;
4 4 import * as API from "../api";
5 5 import useInitial from "@/hooks/useInitail";
6 6 import ModifiedTableCell from "./ModifiedTableCell";
7   -
8   -const { Column } = Table;
  7 +import { ColumnsType } from "antd/es/table";
9 8  
10 9 // 查看销顾任务弹框
11 10 interface AdviserTaskPreviewProps {
... ... @@ -31,73 +30,103 @@ const AdviserTaskPreview = ({
31 30 showSeriesModal(record);
32 31 };
33 32  
  33 + const columns: ColumnsType<API.TaskListItem> = [
  34 + {
  35 + title: "姓名",
  36 + width: 100,
  37 + dataIndex: "dataName",
  38 + filterSearch: true,
  39 + onFilter: (
  40 + value: string | number | boolean,
  41 + record: API.TaskListItem
  42 + ) => {
  43 + return record.dataName.startsWith(value.toString());
  44 + },
  45 + },
  46 + {
  47 + title: "零售任务(台)",
  48 + children: [
  49 + {
  50 + title: "合计",
  51 + dataIndex: "taskCount",
  52 + key: "taskCount",
  53 + render: (text: string, record: API.TaskListItem) => {
  54 + return ModifiedTableCell(record, "taskCount");
  55 + },
  56 + },
  57 + {
  58 + title: "新能源车",
  59 + dataIndex: "newEnergyTaskCount",
  60 + key: "newEnergyTaskCount",
  61 + render: (text: string, record: API.TaskListItem) => {
  62 + return ModifiedTableCell(record, "newEnergyTaskCount");
  63 + },
  64 + },
  65 + {
  66 + title: "传统燃油车",
  67 + dataIndex: "fuelVehicleTaskCount",
  68 + key: "fuelVehicleTaskCount",
  69 + render: (text: string, record: API.TaskListItem) => {
  70 + return ModifiedTableCell(record, "fuelVehicleTaskCount");
  71 + },
  72 + },
  73 + ],
  74 + },
  75 + {
  76 + title: "单车毛利任务(元)",
  77 + dataIndex: "vehicleGrossProfitTask",
  78 + render: (text: string, record: API.TaskListItem) => {
  79 + if (record.dataId === -999) {
  80 + return <div style={{ textAlign: "center" }}>-</div>;
  81 + }
  82 + return ModifiedTableCell(record, "vehicleGrossProfitTask");
  83 + },
  84 + },
  85 + {
  86 + title: "线索到店成交(台)",
  87 + width: 100,
  88 + dataIndex: "clueDealTaskCount",
  89 + render: (text: string, record: API.TaskListItem) => {
  90 + return ModifiedTableCell(record, "clueDealTaskCount");
  91 + },
  92 + },
  93 + {
  94 + title: "首客试驾成交(台)",
  95 + width: 100,
  96 + dataIndex: "testDriveTaskCount",
  97 + render: (text: string, record: API.TaskListItem) => {
  98 + return ModifiedTableCell(record, "testDriveTaskCount");
  99 + },
  100 + },
  101 + {
  102 + title: "攻坚车任务(台)",
  103 + width: 100,
  104 + dataIndex: "tackCarTaskCount",
  105 + render: (text: string, record: API.TaskListItem) => {
  106 + return ModifiedTableCell(record, "tackCarTaskCount");
  107 + },
  108 + },
  109 + {
  110 + title: "车系任务(台)",
  111 + width: 100,
  112 + dataIndex: "seriesTaskCount",
  113 + render: (text: string, record: API.TaskListItem) => {
  114 + if (record.dataId === -999) return text;
  115 + return <a onClick={() => handlePreviewSeriesTask(record)}>{text}</a>;
  116 + },
  117 + },
  118 + ];
  119 +
34 120 return (
35 121 <Table
  122 + bordered
36 123 rowKey="dataId"
37 124 loading={loading}
  125 + columns={columns}
38 126 dataSource={data.taskList}
39 127 pagination={false}
40 128 scroll={{ y: 450 }}
41   - >
42   - <Column title="姓名" dataIndex="dataName" />
43   - <Column
44   - title="零售任务(台)"
45   - dataIndex="taskCount"
46   - render={(text: string, record: API.TaskListItem) => {
47   - return ModifiedTableCell(record, "taskCount");
48   - }}
49   - />
50   - <Column
51   - title="新能源车任务(台)"
52   - dataIndex="newEnergyTaskCount"
53   - render={(text: string, record: API.TaskListItem) => {
54   - return ModifiedTableCell(record, "newEnergyTaskCount");
55   - }}
56   - />
57   - <Column
58   - title="传统燃油车任务(台)"
59   - dataIndex="fuelVehicleTaskCount"
60   - render={(text: string, record: API.TaskListItem) => {
61   - return ModifiedTableCell(record, "fuelVehicleTaskCount");
62   - }}
63   - />
64   - <Column
65   - title="车辆毛利任务(元)"
66   - dataIndex="vehicleGrossProfitTask"
67   - render={(text: string, record: API.TaskListItem) => {
68   - return ModifiedTableCell(record, "vehicleGrossProfitTask");
69   - }}
70   - />
71   - <Column
72   - title="线索到店零售台数(台)"
73   - dataIndex="clueDealTaskCount"
74   - render={(text: string, record: API.TaskListItem) => {
75   - return ModifiedTableCell(record, "clueDealTaskCount");
76   - }}
77   - />
78   - <Column
79   - title="首客试驾成交任务数(台)"
80   - dataIndex="testDriveTaskCount"
81   - render={(text: string, record: API.TaskListItem) => {
82   - return ModifiedTableCell(record, "testDriveTaskCount");
83   - }}
84   - />
85   - <Column
86   - title="攻坚车任务数(台)"
87   - dataIndex="tackCarTaskCount"
88   - render={(text: string, record: API.TaskListItem) => {
89   - return ModifiedTableCell(record, "tackCarTaskCount");
90   - }}
91   - />
92   - <Column
93   - title="车系任务数(台)"
94   - dataIndex="seriesTaskCount"
95   - render={(text: string, record: API.TaskListItem) => {
96   - if (record.dataId === -999) return text;
97   - return <a onClick={() => handlePreviewSeriesTask(record)}>{text}</a>;
98   - }}
99   - />
100   - </Table>
  129 + />
101 130 );
102 131 };
103 132  
... ...
src/pages/order3/SaleTask/components/EntryTaskPreview.tsx
1 1 import React, { useState } from "react";
2   -import { Card, Radio, RadioChangeEvent, Row, Table } from "antd";
  2 +import {
  3 + Card,
  4 + DatePicker,
  5 + Input,
  6 + Radio,
  7 + RadioChangeEvent,
  8 + Row,
  9 + Table,
  10 +} from "antd";
3 11 import { observer } from "mobx-react-lite";
4 12 import * as API from "../api";
5 13 import { OrderTaskApprovalType } from "../entity";
6 14 import useInitial from "@/hooks/useInitail";
7 15 import ModifiedTableCell from "./ModifiedTableCell";
  16 +import { ColumnsType } from "antd/es/table";
  17 +import { Moment } from "moment";
8 18  
9   -const { Column } = Table;
10 19 const RadioButton = Radio.Button;
11 20 const RadioGroup = Radio.Group;
12 21  
13 22 // 预览任务入口弹框
14 23 interface EntryTaskPreviewProps {
  24 + month: Moment;
15 25 params: any; // API.PreviewTaskReq
16 26 showAdviserModal: (
17 27 record: API.TaskListItem,
... ... @@ -24,11 +34,13 @@ interface EntryTaskPreviewProps {
24 34 }
25 35  
26 36 const EntryTaskPreview = ({
  37 + month,
27 38 params,
28 39 showAdviserModal,
29 40 showSeriesModal,
30 41 }: EntryTaskPreviewProps) => {
31 42 const [type, setType] = useState(OrderTaskApprovalType.门店维度);
  43 + const [keywords, setKeywords] = useState("");
32 44  
33 45 const { data, loading, setParams } = useInitial<
34 46 API.PreviewTaskRes,
... ... @@ -40,15 +52,13 @@ const EntryTaskPreview = ({
40 52 if (value === 99) {
41 53 setType(OrderTaskApprovalType.新车一级管理维度);
42 54 setParams(
43   - // @ts-ignore
44 55 { orderTaskApprovalType: OrderTaskApprovalType.新车一级管理维度 },
45 56 true
46 57 );
47 58 return;
48 59 }
49 60 setType(value);
50   - // @ts-ignore
51   - setParams({ orderTaskApprovalType: value }, true);
  61 + setParams({ orderTaskApprovalType: value, keywords }, true);
52 62 };
53 63  
54 64 // 查看顾问任务
... ... @@ -67,130 +77,174 @@ const EntryTaskPreview = ({
67 77 showSeriesModal(record, type);
68 78 };
69 79  
70   - return (
71   - <Card
72   - title={
73   - <Row align="middle" justify="start">
74   - <RadioGroup onChange={handleChangeType} value={type}>
75   - <RadioButton value={OrderTaskApprovalType.门店维度}>
76   - 门店
77   - </RadioButton>
78   - <RadioButton value={OrderTaskApprovalType.销售顾问维度}>
79   - 销顾
80   - </RadioButton>
81   - <RadioButton value={99}>销售管理</RadioButton>
82   - </RadioGroup>
83   - {type !== OrderTaskApprovalType.门店维度 &&
84   - type !== OrderTaskApprovalType.销售顾问维度 && (
85   - <RadioGroup
86   - onChange={handleChangeType}
87   - value={type}
88   - style={{ marginLeft: 20 }}
89   - >
90   - <RadioButton value={OrderTaskApprovalType.新车一级管理维度}>
91   - 销售一级管理
92   - </RadioButton>
93   - <RadioButton value={OrderTaskApprovalType.新车二级管理维度}>
94   - 销售二级管理
95   - </RadioButton>
96   - <RadioButton value={OrderTaskApprovalType.新车三级管理维度}>
97   - 销售三级管理
98   - </RadioButton>
99   - </RadioGroup>
100   - )}
101   - </Row>
102   - }
103   - >
104   - <Table
105   - rowKey="dataId"
106   - loading={loading}
107   - dataSource={data.taskList}
108   - pagination={false}
109   - scroll={{ y: 450 }}
110   - >
111   - <Column
112   - title={type === OrderTaskApprovalType.门店维度 ? "门店" : "姓名"}
113   - dataIndex="dataName"
114   - filterSearch
115   - onFilter={(
116   - value: string | number | boolean,
117   - record: API.TaskListItem
118   - ) => {
119   - return record.dataName.startsWith(value.toString());
120   - }}
121   - />
122   - <Column
123   - title="零售任务(台)"
124   - dataIndex="taskCount"
125   - render={(text: string, record: API.TaskListItem) => {
  80 + const columns: ColumnsType<API.TaskListItem> = [
  81 + {
  82 + title: type === OrderTaskApprovalType.门店维度 ? "门店" : "姓名",
  83 + width: type === OrderTaskApprovalType.门店维度 ? 150 : 100,
  84 + dataIndex: "dataName",
  85 + filterSearch: true,
  86 + onFilter: (
  87 + value: string | number | boolean,
  88 + record: API.TaskListItem
  89 + ) => {
  90 + return record.dataName.startsWith(value.toString());
  91 + },
  92 + },
  93 + {
  94 + title: "零售任务(台)",
  95 + children: [
  96 + {
  97 + title: "合计",
  98 + dataIndex: "taskCount",
  99 + key: "taskCount",
  100 + render: (text: string, record: API.TaskListItem) => {
126 101 return ModifiedTableCell(record, "taskCount");
127   - }}
128   - />
129   - <Column
130   - title="新能源车任务(台)"
131   - dataIndex="newEnergyTaskCount"
132   - render={(text: string, record: API.TaskListItem) => {
  102 + },
  103 + },
  104 + {
  105 + title: "新能源车",
  106 + dataIndex: "newEnergyTaskCount",
  107 + key: "newEnergyTaskCount",
  108 + render: (text: string, record: API.TaskListItem) => {
133 109 return ModifiedTableCell(record, "newEnergyTaskCount");
134   - }}
135   - />
136   - <Column
137   - title="传统燃油车任务(台)"
138   - dataIndex="fuelVehicleTaskCount"
139   - render={(text: string, record: API.TaskListItem) => {
  110 + },
  111 + },
  112 + {
  113 + title: "传统燃油车",
  114 + dataIndex: "fuelVehicleTaskCount",
  115 + key: "fuelVehicleTaskCount",
  116 + render: (text: string, record: API.TaskListItem) => {
140 117 return ModifiedTableCell(record, "fuelVehicleTaskCount");
141   - }}
142   - />
143   - <Column
144   - title="车辆毛利任务(元)"
145   - dataIndex="vehicleGrossProfitTask"
146   - render={(text: string, record: API.TaskListItem) => {
147   - return ModifiedTableCell(record, "vehicleGrossProfitTask");
148   - }}
149   - />
150   - <Column
151   - title="线索到店零售台数(台)"
152   - dataIndex="clueDealTaskCount"
153   - render={(text: string, record: API.TaskListItem) => {
154   - return ModifiedTableCell(record, "clueDealTaskCount");
155   - }}
156   - />
157   - <Column
158   - title="首客试驾成交任务数(台)"
159   - dataIndex="testDriveTaskCount"
160   - render={(text: string, record: API.TaskListItem) => {
161   - return ModifiedTableCell(record, "testDriveTaskCount");
162   - }}
  118 + },
  119 + },
  120 + ],
  121 + },
  122 + {
  123 + title: "单车毛利任务(元)",
  124 + dataIndex: "vehicleGrossProfitTask",
  125 + render: (text: string, record: API.TaskListItem) => {
  126 + if (record.dataId === -999) {
  127 + return <div style={{ textAlign: "center" }}>-</div>;
  128 + }
  129 + return ModifiedTableCell(record, "vehicleGrossProfitTask");
  130 + },
  131 + },
  132 + {
  133 + title: "线索到店成交(台)",
  134 + width: 100,
  135 + dataIndex: "clueDealTaskCount",
  136 + render: (text: string, record: API.TaskListItem) => {
  137 + return ModifiedTableCell(record, "clueDealTaskCount");
  138 + },
  139 + },
  140 + {
  141 + title: "首客试驾成交(台)",
  142 + width: 100,
  143 + dataIndex: "testDriveTaskCount",
  144 + render: (text: string, record: API.TaskListItem) => {
  145 + return ModifiedTableCell(record, "testDriveTaskCount");
  146 + },
  147 + },
  148 + {
  149 + title: "攻坚车任务(台)",
  150 + width: 100,
  151 + dataIndex: "tackCarTaskCount",
  152 + render: (text: string, record: API.TaskListItem) => {
  153 + return ModifiedTableCell(record, "tackCarTaskCount");
  154 + },
  155 + },
  156 + {
  157 + title: "车系任务(台)",
  158 + width: 100,
  159 + dataIndex: "seriesTaskCount",
  160 + render: (text: string, record: API.TaskListItem) => {
  161 + if (record.dataId === -999) return text;
  162 + return <a onClick={() => handlePreviewSeriesTask(record)}>{text}</a>;
  163 + },
  164 + },
  165 + ];
  166 +
  167 + const extraColumns: ColumnsType<API.TaskListItem> = [
  168 + {
  169 + title: "销顾任务",
  170 + render: (text: string, record: API.TaskListItem) => {
  171 + if (record.dataId === -999) {
  172 + return "-";
  173 + }
  174 + return <a onClick={() => handlePreviewAdviserTask(record)}>查看</a>;
  175 + },
  176 + },
  177 + ];
  178 +
  179 + return (
  180 + <>
  181 + <Row align="middle" justify="start" style={{ marginBottom: 20 }}>
  182 + <DatePicker
  183 + style={{ width: 245 }}
  184 + picker="month"
  185 + value={month}
  186 + allowClear={false}
  187 + disabled
163 188 />
164   - <Column
165   - title="攻坚车任务数(台)"
166   - dataIndex="tackCarTaskCount"
167   - render={(text: string, record: API.TaskListItem) => {
168   - return ModifiedTableCell(record, "tackCarTaskCount");
  189 + <Input.Search
  190 + allowClear
  191 + placeholder="搜索门店或顾问"
  192 + style={{ width: 263, marginLeft: 20 }}
  193 + value={keywords}
  194 + onChange={(e) => setKeywords(e.target.value)}
  195 + onSearch={(v) => {
  196 + setParams({ keywords: v }, true);
169 197 }}
170 198 />
171   - <Column
172   - title="车系任务数(台)"
173   - dataIndex="seriesTaskCount"
174   - render={(text: string, record: API.TaskListItem) => {
175   - if (record.dataId === -999) return text;
176   - return (
177   - <a onClick={() => handlePreviewSeriesTask(record)}>{text}</a>
178   - );
179   - }}
  199 + </Row>
  200 + <Card
  201 + title={
  202 + <Row align="middle" justify="start">
  203 + <RadioGroup onChange={handleChangeType} value={type}>
  204 + <RadioButton value={OrderTaskApprovalType.门店维度}>
  205 + 门店
  206 + </RadioButton>
  207 + <RadioButton value={OrderTaskApprovalType.销售顾问维度}>
  208 + 销顾
  209 + </RadioButton>
  210 + <RadioButton value={99}>销售管理层</RadioButton>
  211 + </RadioGroup>
  212 + {type !== OrderTaskApprovalType.门店维度 &&
  213 + type !== OrderTaskApprovalType.销售顾问维度 && (
  214 + <RadioGroup
  215 + onChange={handleChangeType}
  216 + value={type}
  217 + style={{ marginLeft: 20 }}
  218 + >
  219 + <RadioButton value={OrderTaskApprovalType.新车一级管理维度}>
  220 + 销售一级管理
  221 + </RadioButton>
  222 + <RadioButton value={OrderTaskApprovalType.新车二级管理维度}>
  223 + 销售二级管理
  224 + </RadioButton>
  225 + <RadioButton value={OrderTaskApprovalType.新车三级管理维度}>
  226 + 销售三级管理
  227 + </RadioButton>
  228 + </RadioGroup>
  229 + )}
  230 + </Row>
  231 + }
  232 + >
  233 + <Table
  234 + bordered
  235 + rowKey="dataId"
  236 + columns={
  237 + type === OrderTaskApprovalType.门店维度
  238 + ? columns.concat(extraColumns)
  239 + : columns
  240 + }
  241 + loading={loading}
  242 + dataSource={data.taskList}
  243 + pagination={false}
  244 + scroll={{ y: 450 }}
180 245 />
181   - {type === OrderTaskApprovalType.门店维度 && (
182   - <Column
183   - title="销顾"
184   - render={(text: string, record: API.TaskListItem) => {
185   - if (record.dataId === -999) return "-";
186   - return (
187   - <a onClick={() => handlePreviewAdviserTask(record)}>查看</a>
188   - );
189   - }}
190   - />
191   - )}
192   - </Table>
193   - </Card>
  246 + </Card>
  247 + </>
194 248 );
195 249 };
196 250  
... ...
src/pages/order3/SaleTask/components/SaleTaskAutoAssign.tsx 0 → 100644
  1 +import React, { useContext, useEffect, useRef, useState } from "react";
  2 +import {
  3 + Table,
  4 + Form,
  5 + InputRef,
  6 + Row,
  7 + Button,
  8 + message,
  9 + Modal,
  10 + InputNumber,
  11 +} from "antd";
  12 +import type { FormInstance } from "antd/es/form";
  13 +import * as API from "../api";
  14 +import "./index.less";
  15 +import { MAX_NUM } from "../entity";
  16 +
  17 +type EditableTableProps = Parameters<typeof Table>[0];
  18 +type ColumnTypes = Exclude<EditableTableProps["columns"], undefined>;
  19 +interface Item {
  20 + id: string;
  21 + shopName: string;
  22 + taskCount: number;
  23 + newEnergyTaskCount: number;
  24 + fuelVehicleTaskCount: number;
  25 + tackCarTaskCount: number;
  26 +}
  27 +interface EditableRowProps {
  28 + index: number;
  29 +}
  30 +interface EditableCellProps {
  31 + title: React.ReactNode;
  32 + editable: boolean;
  33 + children: React.ReactNode;
  34 + dataIndex: keyof Item;
  35 + record: Item;
  36 + handleSave: (record: Item) => void;
  37 +}
  38 +
  39 +const defaultColumns: (ColumnTypes[number] & {
  40 + editable?: boolean;
  41 + dataIndex: string;
  42 +})[] = [
  43 + {
  44 + title: "门店",
  45 + dataIndex: "shopName",
  46 + editable: false,
  47 + },
  48 + {
  49 + title: "零售任务(台)",
  50 + dataIndex: "taskCount",
  51 + editable: true,
  52 + },
  53 + {
  54 + title: "新能源车任务(台)",
  55 + dataIndex: "newEnergyTaskCount",
  56 + editable: true,
  57 + },
  58 + {
  59 + title: "传统燃油车任务(台)",
  60 + dataIndex: "fuelVehicleTaskCount",
  61 + editable: false,
  62 + },
  63 + {
  64 + title: "攻坚车任务(台)",
  65 + dataIndex: "tackCarTaskCount",
  66 + editable: true,
  67 + },
  68 +];
  69 +
  70 +interface SaleTaskAutoAssignProps {
  71 + id: number;
  72 + value?: API.ShopTaskItem[];
  73 + onCancel: () => void;
  74 + onRefresh: () => void;
  75 +}
  76 +
  77 +export default function SaleTaskAutoAssign(props: SaleTaskAutoAssignProps) {
  78 + const EditableContext = React.createContext<FormInstance<any> | null>(null);
  79 + const [dataSource, setDataSource] = useState<API.ShopTaskItem[]>([]);
  80 +
  81 + useEffect(() => {
  82 + setDataSource(props.value ? [...props.value] : []);
  83 + }, [props.value]);
  84 +
  85 + const EditableRow: React.FC<EditableRowProps> = ({ index, ...props }) => {
  86 + const [form] = Form.useForm();
  87 + return (
  88 + <Form form={form} component={false}>
  89 + <EditableContext.Provider value={form}>
  90 + <tr {...props} />
  91 + </EditableContext.Provider>
  92 + </Form>
  93 + );
  94 + };
  95 +
  96 + const EditableCell: React.FC<EditableCellProps> = ({
  97 + title,
  98 + editable,
  99 + children,
  100 + dataIndex,
  101 + record,
  102 + handleSave,
  103 + ...restProps
  104 + }) => {
  105 + const [editing, setEditing] = useState(false);
  106 + const inputRef = useRef<InputRef>(null);
  107 + const form = useContext(EditableContext)!;
  108 +
  109 + useEffect(() => {
  110 + if (editing) {
  111 + inputRef.current!.focus();
  112 + }
  113 + }, [editing]);
  114 +
  115 + const toggleEdit = () => {
  116 + setEditing(!editing);
  117 + form.setFieldsValue({ [dataIndex]: record[dataIndex] });
  118 + };
  119 +
  120 + const save = async () => {
  121 + try {
  122 + const values = await form.validateFields();
  123 + toggleEdit();
  124 + handleSave({ ...record, ...values });
  125 + } catch (errInfo) {
  126 + console.log("Save failed:", errInfo);
  127 + }
  128 + };
  129 +
  130 + let childNode = children;
  131 +
  132 + if (editable) {
  133 + childNode = editing ? (
  134 + <Form.Item
  135 + noStyle
  136 + name={dataIndex}
  137 + rules={[
  138 + {
  139 + required: true,
  140 + message: `请输入${title}`,
  141 + },
  142 + ]}
  143 + >
  144 + <InputNumber
  145 + ref={inputRef}
  146 + min={0}
  147 + max={MAX_NUM}
  148 + style={{ width: "80px" }}
  149 + onPressEnter={save}
  150 + onBlur={save}
  151 + />
  152 + </Form.Item>
  153 + ) : (
  154 + <div className="editable-cell-value-wrap" onClick={toggleEdit}>
  155 + {children}
  156 + </div>
  157 + );
  158 + }
  159 +
  160 + return <td {...restProps}>{childNode}</td>;
  161 + };
  162 +
  163 + const handleSave = (row: API.ShopTaskItem) => {
  164 + const newData = [...dataSource];
  165 + const index = newData.findIndex((item) => row.id === item.id);
  166 + const item = newData[index];
  167 + if (row.taskCount !== 0 && row.newEnergyTaskCount > row.taskCount) {
  168 + message.warn("新能源车任务台数不得超过零售任务台数");
  169 + return;
  170 + }
  171 + const newRow = {
  172 + ...item,
  173 + ...row,
  174 + fuelVehicleTaskCount: row.taskCount - row.newEnergyTaskCount,
  175 + };
  176 + if (row.taskCount === 0) {
  177 + newRow.taskCount = 0;
  178 + newRow.newEnergyTaskCount = 0;
  179 + newRow.fuelVehicleTaskCount = 0;
  180 + }
  181 + newData.splice(index, 1, newRow);
  182 + setDataSource(newData);
  183 + };
  184 +
  185 + const autoAssignSaleTask = (isAssignToAdviser: boolean) => {
  186 + Modal.confirm({
  187 + title: isAssignToAdviser ? (
  188 + <span>
  189 + 确认分配到
  190 + <span className="tip">全部门店和顾问</span>
  191 + 吗?
  192 + </span>
  193 + ) : (
  194 + <span>
  195 + 确认分配到
  196 + <span className="tip">全部门店</span>
  197 + 吗?
  198 + </span>
  199 + ),
  200 + zIndex: 1002,
  201 + onOk: async () => {
  202 + const hide = message.loading("分配中,请稍候", 0);
  203 + API.autoAssignSaleTask({
  204 + id: props.id,
  205 + shopTaskList: dataSource.map((item) => ({
  206 + shopId: item.shopId,
  207 + taskCount: item.taskCount,
  208 + newEnergyTaskCount: item.newEnergyTaskCount,
  209 + fuelVehicleTaskCount: item.fuelVehicleTaskCount,
  210 + tackCarTaskCount: item.tackCarTaskCount,
  211 + })),
  212 + assignTask: isAssignToAdviser,
  213 + })
  214 + .then((res) => {
  215 + message.success("分配成功");
  216 + props.onRefresh();
  217 + })
  218 + .catch((error: any) => {
  219 + message.error(error.message ?? "请求失败");
  220 + })
  221 + .finally(() => {
  222 + hide();
  223 + });
  224 + },
  225 + });
  226 + };
  227 +
  228 + const components = {
  229 + body: {
  230 + row: EditableRow,
  231 + cell: EditableCell,
  232 + },
  233 + };
  234 +
  235 + const columns = defaultColumns.map((col) => {
  236 + if (!col.editable) {
  237 + return col;
  238 + }
  239 + return {
  240 + ...col,
  241 + onCell: (record: API.ShopTaskItem) => ({
  242 + record,
  243 + editable: col.editable,
  244 + dataIndex: col.dataIndex,
  245 + title: col.title,
  246 + handleSave,
  247 + }),
  248 + };
  249 + });
  250 +
  251 + return (
  252 + <>
  253 + <Table
  254 + components={components}
  255 + rowClassName={() => "editable-row"}
  256 + bordered
  257 + rowKey="id"
  258 + dataSource={dataSource}
  259 + columns={columns as ColumnTypes}
  260 + />
  261 + <Row align="middle" justify="center" style={{ marginTop: 20 }}>
  262 + <Button onClick={props.onCancel}>取消</Button>
  263 + <Button
  264 + type="primary"
  265 + style={{ marginLeft: 10 }}
  266 + onClick={() => autoAssignSaleTask(false)}
  267 + >
  268 + 分配到门店
  269 + </Button>
  270 + <Button
  271 + type="primary"
  272 + style={{ marginLeft: 10 }}
  273 + onClick={() => autoAssignSaleTask(true)}
  274 + >
  275 + 分配到门店和顾问
  276 + </Button>
  277 + </Row>
  278 + </>
  279 + );
  280 +}
... ...
src/pages/order3/SaleTask/components/SaleTaskBatchSet.tsx 0 → 100644
  1 +import { PlusOutlined } from "@ant-design/icons";
  2 +import {
  3 + Button,
  4 + Card,
  5 + Col,
  6 + Form,
  7 + InputNumber,
  8 + Modal,
  9 + Row,
  10 + message,
  11 +} from "antd";
  12 +import "./index.less";
  13 +import React, { useState } from "react";
  14 +import { MAX_NUM } from "../entity";
  15 +import * as API from "../api";
  16 +import ShopSelectNew from "@/components/ShopSelectNew";
  17 +import { isArray } from "lodash";
  18 +
  19 +interface SaleTaskBatchSetProps {
  20 + id: number;
  21 + onCancel: () => void;
  22 + onRefresh: () => void;
  23 +}
  24 +
  25 +export default function SaleTaskBatchSet(props: SaleTaskBatchSetProps) {
  26 + const [form] = Form.useForm();
  27 + // 过滤各项已经选择的门店
  28 + // const [selectedShopIds, setSelectedShopIds] = useState({
  29 + // grossProfit: [],
  30 + // tackCar: [],
  31 + // testDrive: [],
  32 + // });
  33 +
  34 + const batchSetSaleTask = async (isAssignToAdviser: boolean) => {
  35 + await form.validateFields();
  36 + const values = form.getFieldsValue();
  37 + if (
  38 + Object.values(values).every(
  39 + (item) => !item || (isArray(item) && item.length === 0)
  40 + )
  41 + ) {
  42 + message.warn("请设置任务后再进行分配");
  43 + return;
  44 + }
  45 + const newValues = {};
  46 + Array.from(Object.keys(values)).forEach((valueKey: any) => {
  47 + if (values[valueKey]) {
  48 + newValues[valueKey] = values[valueKey].map((valueItem: any) => ({
  49 + taskAims: valueItem.taskAims,
  50 + shopIdList: valueItem.shopIdList.map(
  51 + (shopItem: any) => shopItem.shopId
  52 + ),
  53 + }));
  54 + }
  55 + });
  56 + Modal.confirm({
  57 + title: isAssignToAdviser ? (
  58 + <span>
  59 + 确认分配到
  60 + <span className="tip">全部门店和顾问</span>
  61 + 吗?
  62 + </span>
  63 + ) : (
  64 + <span>
  65 + 确认分配到
  66 + <span className="tip">全部门店</span>
  67 + 吗?
  68 + </span>
  69 + ),
  70 + zIndex: 1002,
  71 + onOk: async () => {
  72 + const hide = message.loading("分配中,请稍候", 0);
  73 + API.batchSetSaleTask({
  74 + assignTask: isAssignToAdviser,
  75 + orderTaskApplyId: props.id,
  76 + ...newValues,
  77 + })
  78 + .then((res) => {
  79 + message.success("分配成功");
  80 + props.onRefresh();
  81 + })
  82 + .catch((error: any) => {
  83 + message.error(error.message ?? "请求失败");
  84 + })
  85 + .finally(() => {
  86 + hide();
  87 + });
  88 + },
  89 + });
  90 + };
  91 +
  92 + // const handleFormChange = (changedValues: any) => {
  93 + // const labelKey: any = Object.keys(changedValues)[0];
  94 + // const list: any = Object.values(changedValues)[0];
  95 + // console.log(list);
  96 + // if (!list[0]) return;
  97 + // if (Object.keys(list[0])[0] === "shopIdList") {
  98 + // const newSelectedIds = { ...selectedShopIds };
  99 + // newSelectedIds[labelKey] = Object.values(list[0])[0];
  100 + // setSelectedShopIds(newSelectedIds);
  101 + // }
  102 + // };
  103 +
  104 + return (
  105 + <Form
  106 + form={form}
  107 + name="sale-task-batch-set-form"
  108 + autoComplete="off"
  109 + // onValuesChange={handleFormChange}
  110 + >
  111 + <Form.List name="grossProfitTaskList">
  112 + {(fields, { add, remove }) => (
  113 + <Card>
  114 + <Row
  115 + align="middle"
  116 + justify="space-between"
  117 + style={{ marginBottom: 15 }}
  118 + >
  119 + <h4 className="title">单车毛利任务</h4>
  120 + <Button type="link" onClick={() => add()} icon={<PlusOutlined />}>
  121 + 新增
  122 + </Button>
  123 + </Row>
  124 + {fields.map(({ key, name, ...restField }) => (
  125 + <Row gutter={16} key={key}>
  126 + <Col className="gutter-row" span={6}>
  127 + <Form.Item
  128 + {...restField}
  129 + name={[name, "taskAims"]}
  130 + rules={[{ required: true, message: "请填写单车毛利任务" }]}
  131 + >
  132 + <InputNumber
  133 + formatter={(value) => `${value}元`}
  134 + parser={(value: any) => value.replace("元", "")}
  135 + min={0}
  136 + max={MAX_NUM}
  137 + style={{ width: "100%" }}
  138 + precision={2}
  139 + placeholder="请填写单车毛利任务"
  140 + />
  141 + </Form.Item>
  142 + </Col>
  143 + <Col className="gutter-row" span={15}>
  144 + <Form.Item
  145 + {...restField}
  146 + name={[name, "shopIdList"]}
  147 + rules={[{ required: true, message: "请选择适用门店" }]}
  148 + >
  149 + <ShopSelectNew
  150 + multiple
  151 + defaultOptions={{ bizTypes: "1" }}
  152 + placeholder="请选择适用门店"
  153 + // disabledShopIds={selectedShopIds.grossProfit}
  154 + />
  155 + </Form.Item>
  156 + </Col>
  157 + <Col className="gutter-row" span={3}>
  158 + <Button
  159 + type="link"
  160 + style={{ color: "#999" }}
  161 + onClick={() => remove(name)}
  162 + >
  163 + 删除
  164 + </Button>
  165 + </Col>
  166 + </Row>
  167 + ))}
  168 + </Card>
  169 + )}
  170 + </Form.List>
  171 + <Form.List name="testDriveTaskList">
  172 + {(fields, { add, remove }) => (
  173 + <Card style={{ marginTop: 20 }}>
  174 + <Row
  175 + align="middle"
  176 + justify="space-between"
  177 + style={{ marginBottom: 15 }}
  178 + >
  179 + <h4 className="title">首客试驾成交</h4>
  180 + <Button type="link" onClick={() => add()} icon={<PlusOutlined />}>
  181 + 新增
  182 + </Button>
  183 + </Row>
  184 + {fields.map(({ key, name, ...restField }) => (
  185 + <Row gutter={16} key={key}>
  186 + <Col className="gutter-row" span={6}>
  187 + <Form.Item
  188 + {...restField}
  189 + name={[name, "taskAims"]}
  190 + rules={[{ required: true, message: "请填写首客试驾成交" }]}
  191 + >
  192 + <InputNumber
  193 + formatter={(value) => `${value}台`}
  194 + parser={(value: any) => value.replace("台", "")}
  195 + min={0}
  196 + max={MAX_NUM}
  197 + style={{ width: "100%" }}
  198 + precision={0}
  199 + placeholder="请填写首客试驾成交"
  200 + />
  201 + </Form.Item>
  202 + </Col>
  203 + <Col className="gutter-row" span={15}>
  204 + <Form.Item
  205 + {...restField}
  206 + name={[name, "shopIdList"]}
  207 + rules={[{ required: true, message: "请选择适用门店" }]}
  208 + >
  209 + <ShopSelectNew
  210 + multiple
  211 + defaultOptions={{ bizTypes: "1" }}
  212 + placeholder="请选择适用门店"
  213 + // disabledShopIds={selectedShopIds.testDrive}
  214 + />
  215 + </Form.Item>
  216 + </Col>
  217 + <Col className="gutter-row" span={3}>
  218 + <Button
  219 + type="link"
  220 + style={{ color: "#999" }}
  221 + onClick={() => remove(name)}
  222 + >
  223 + 删除
  224 + </Button>
  225 + </Col>
  226 + </Row>
  227 + ))}
  228 + </Card>
  229 + )}
  230 + </Form.List>
  231 + <Form.List name="tackCarTaskList">
  232 + {(fields, { add, remove }) => (
  233 + <Card style={{ marginTop: 20 }}>
  234 + <Row
  235 + align="middle"
  236 + justify="space-between"
  237 + style={{ marginBottom: 15 }}
  238 + >
  239 + <h4 className="title">攻坚车任务</h4>
  240 + <Button type="link" onClick={() => add()} icon={<PlusOutlined />}>
  241 + 新增
  242 + </Button>
  243 + </Row>
  244 + {fields.map(({ key, name, ...restField }) => (
  245 + <Row gutter={16} key={key}>
  246 + <Col className="gutter-row" span={6}>
  247 + <Form.Item
  248 + {...restField}
  249 + name={[name, "taskAims"]}
  250 + rules={[{ required: true, message: "请填写攻坚车任务" }]}
  251 + >
  252 + <InputNumber
  253 + formatter={(value) => `${value}台`}
  254 + parser={(value: any) => value.replace("台", "")}
  255 + min={0}
  256 + max={MAX_NUM}
  257 + style={{ width: "100%" }}
  258 + precision={0}
  259 + placeholder="请填写攻坚车任务"
  260 + />
  261 + </Form.Item>
  262 + </Col>
  263 + <Col className="gutter-row" span={15}>
  264 + <Form.Item
  265 + {...restField}
  266 + name={[name, "shopIdList"]}
  267 + rules={[{ required: true, message: "请选择适用门店" }]}
  268 + >
  269 + <ShopSelectNew
  270 + multiple
  271 + defaultOptions={{ bizTypes: "1" }}
  272 + placeholder="请选择适用门店"
  273 + // disabledShopIds={selectedShopIds.tackCar}
  274 + />
  275 + </Form.Item>
  276 + </Col>
  277 + <Col className="gutter-row" span={3}>
  278 + <Button
  279 + type="link"
  280 + style={{ color: "#999" }}
  281 + onClick={() => remove(name)}
  282 + >
  283 + 删除
  284 + </Button>
  285 + </Col>
  286 + </Row>
  287 + ))}
  288 + </Card>
  289 + )}
  290 + </Form.List>
  291 + <Row align="middle" justify="center" style={{ marginTop: 20 }}>
  292 + <Button onClick={props.onCancel}>取消</Button>
  293 + <Button
  294 + type="primary"
  295 + style={{ marginLeft: 10 }}
  296 + onClick={() => batchSetSaleTask(false)}
  297 + >
  298 + 分配到门店
  299 + </Button>
  300 + <Button
  301 + type="primary"
  302 + style={{ marginLeft: 10 }}
  303 + onClick={() => batchSetSaleTask(true)}
  304 + >
  305 + 分配到门店和顾问
  306 + </Button>
  307 + </Row>
  308 + </Form>
  309 + );
  310 +}
... ...
src/pages/order3/SaleTask/components/index.less 0 → 100644
  1 +.editable-row:hover .editable-cell-value-wrap {
  2 + border: 1px solid #d9d9d9;
  3 +}
  4 +.editable-cell-value-wrap {
  5 + border: 1px solid transparent;
  6 + cursor: pointer;
  7 +}
  8 +.tip {
  9 + color: #ff4d4f;
  10 + font-weight: bold;
  11 +}
... ...
src/pages/order3/SaleTask/index.tsx
... ... @@ -15,14 +15,15 @@ import { history } from &quot;umi&quot;;
15 15 import moment, { Moment } from "moment";
16 16 import useInitial from "@/hooks/useInitail";
17 17 import { Provider, useStore } from "./store";
18   -import { default as ApprovalProgressModal } from "@/pages/stock/AdvanceProgress/components/ApproveModal";
  18 +import ApprovalProgressModal from "@/pages/stock/AdvanceProgress/components/ApproveModal";
19 19 import EntryTaskPreview from "./components/EntryTaskPreview";
20 20 import { OrderTaskApprovalType } from "./entity";
21 21 import AdviserTaskPreview from "./components/AdviserTaskPreview";
22 22 import SeriesTaskPreview from "./components/SeriesTaskPreview";
23 23 import ApproveModal from "@/pages/order3/Common/ApproveModal";
24   -
25   -const { Column } = Table;
  24 +import SaleTaskAutoAssign from "./components/SaleTaskAutoAssign";
  25 +import SaleTaskBatchSet from "./components/SaleTaskBatchSet";
  26 +import { ColumnsType } from "antd/es/table";
26 27  
27 28 export default () => (
28 29 <Provider>
... ... @@ -42,6 +43,9 @@ function SaleTaskList() {
42 43 const [stpVisible, setStpVisible] = useState(false);
43 44 const [seriesTaskParams, setSeriesTaskParams] = useState({});
44 45  
  46 + const [autoVisible, setAutoVisible] = useState(false);
  47 + const [batchVisible, setBatchVisible] = useState(false);
  48 +
45 49 const { data, loading, setParams } = useInitial(
46 50 API.getSaleTaskApi,
47 51 {} as API.GetSaleTaskApiRes,
... ... @@ -71,6 +75,19 @@ function SaleTaskList() {
71 75 });
72 76 };
73 77  
  78 + // 查看销顾任务
  79 + const goToAdviserPage = (record: API.ShopTaskItem) => {
  80 + history.push({
  81 + pathname: "/order3/saleTask/edit",
  82 + query: {
  83 + readOnly: isReadOnly ? "1" : "0",
  84 + shopId: String(record.shopId),
  85 + taskDate: String(targetMonth.valueOf()),
  86 + currTab: "2",
  87 + },
  88 + });
  89 + };
  90 +
74 91 // 查看流程进度
75 92 const viewProcess = () => {
76 93 setApprove({
... ... @@ -128,30 +145,242 @@ function SaleTaskList() {
128 145 setEtpVisible(true);
129 146 };
130 147  
  148 + // 销顾任务
  149 + const showAdviserModal = (
  150 + record: API.TaskListItem,
  151 + type: OrderTaskApprovalType
  152 + ) => {
  153 + const params: any = {
  154 + id: data.id,
  155 + taskId: record.id,
  156 + orderTaskApprovalType: OrderTaskApprovalType.门店维度, // 只有门店有查看销顾任务
  157 + };
  158 + switch (type) {
  159 + case OrderTaskApprovalType.门店维度:
  160 + params.shopId = record.dataId;
  161 + break;
  162 + case OrderTaskApprovalType.销售顾问维度:
  163 + params.staffId = record.dataId;
  164 + break;
  165 + case OrderTaskApprovalType.新车一级管理维度:
  166 + params.firstManageId = record.dataId;
  167 + break;
  168 + case OrderTaskApprovalType.新车二级管理维度:
  169 + params.secondManageId = record.dataId;
  170 + break;
  171 + case OrderTaskApprovalType.新车三级管理维度:
  172 + params.thirdManageId = record.dataId;
  173 + break;
  174 + default:
  175 + break;
  176 + }
  177 + setAdviserTaskParams(params);
  178 + setAtpVisible(true);
  179 + };
  180 +
  181 + // 销顾任务--查看车系任务
  182 + const showSeriesModalByAdviser = (record: API.TaskListItem) => {
  183 + const params = { ...adviserTaskParams } as any;
  184 + params.taskId = record.id;
  185 + params.orderTaskApprovalType = OrderTaskApprovalType.车系;
  186 + params.staffId = record.dataId;
  187 + setSeriesTaskParams(params);
  188 + setStpVisible(true);
  189 + };
  190 +
  191 + // 车系任务
  192 + const showSeriesModal = (
  193 + record: API.TaskListItem,
  194 + type: OrderTaskApprovalType
  195 + ) => {
  196 + const params: any = {
  197 + id: data.id,
  198 + taskId: record.id,
  199 + orderTaskApprovalType: OrderTaskApprovalType.车系,
  200 + };
  201 + switch (type) {
  202 + case OrderTaskApprovalType.门店维度:
  203 + params.shopId = record.dataId;
  204 + break;
  205 + case OrderTaskApprovalType.销售顾问维度:
  206 + params.staffId = record.dataId;
  207 + break;
  208 + case OrderTaskApprovalType.新车一级管理维度:
  209 + params.firstManageId = record.dataId;
  210 + break;
  211 + case OrderTaskApprovalType.新车二级管理维度:
  212 + params.secondManageId = record.dataId;
  213 + break;
  214 + case OrderTaskApprovalType.新车三级管理维度:
  215 + params.thirdManageId = record.dataId;
  216 + break;