From b0db30fd2d415a9694c2370370607bf36b588511 Mon Sep 17 00:00:00 2001 From: Lei OT Date: Wed, 12 Nov 2025 17:17:53 +0800 Subject: [PATCH 1/8] =?UTF-8?q?feat:=20=E5=95=86=E6=97=85:=20=E5=B8=82?= =?UTF-8?q?=E5=9C=BA-=E8=AE=A2=E5=8D=95=E6=95=B0=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 50 ++++- package.json | 3 +- src/App.jsx | 32 +-- src/libs/ht.js | 2 +- src/views/biz/BizOrder.jsx | 412 +++++++++++++++++++++++++++++++++++++ src/zustand/BizOrder.js | 170 +++++++++++++++ 6 files changed, 646 insertions(+), 23 deletions(-) create mode 100644 src/views/biz/BizOrder.jsx create mode 100644 src/zustand/BizOrder.js diff --git a/package-lock.json b/package-lock.json index 99e66d5..0e4daec 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,8 @@ "react-router-dom": "^6.3.0", "react-scripts": "^5.0.1", "web-vitals": "^2.1.4", - "xlsx": "https://cdn.sheetjs.com/xlsx-0.18.11/xlsx-0.18.11.tgz" + "xlsx": "https://cdn.sheetjs.com/xlsx-0.18.11/xlsx-0.18.11.tgz", + "zustand": "^5.0.8" }, "devDependencies": { "@types/react": "^18.2.20", @@ -5477,7 +5478,7 @@ "version": "15.7.5", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==", - "dev": true + "devOptional": true }, "node_modules/@types/q": { "version": "1.5.5", @@ -5498,7 +5499,7 @@ "version": "18.2.20", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.20.tgz", "integrity": "sha512-WKNtmsLWJM/3D5mG4U84cysVY31ivmyw85dE84fOCk5Hx78wezB/XEjVPWl2JTZ5FkEeaTJf+VgUAUn3PE7Isw==", - "dev": true, + "devOptional": true, "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -5536,7 +5537,7 @@ "version": "0.16.3", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz", "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==", - "dev": true + "devOptional": true }, "node_modules/@types/semver": { "version": "7.3.13", @@ -21314,6 +21315,35 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zustand": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.8.tgz", + "integrity": "sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } } }, "dependencies": { @@ -25409,7 +25439,7 @@ "version": "15.7.5", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==", - "dev": true + "devOptional": true }, "@types/q": { "version": "1.5.5", @@ -25430,7 +25460,7 @@ "version": "18.2.20", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.20.tgz", "integrity": "sha512-WKNtmsLWJM/3D5mG4U84cysVY31ivmyw85dE84fOCk5Hx78wezB/XEjVPWl2JTZ5FkEeaTJf+VgUAUn3PE7Isw==", - "dev": true, + "devOptional": true, "requires": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -25468,7 +25498,7 @@ "version": "0.16.3", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz", "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==", - "dev": true + "devOptional": true }, "@types/semver": { "version": "7.3.13", @@ -37049,6 +37079,12 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==" + }, + "zustand": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.8.tgz", + "integrity": "sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw==", + "requires": {} } } } diff --git a/package.json b/package.json index 2e46f5a..36097d2 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,8 @@ "react-router-dom": "^6.3.0", "react-scripts": "^5.0.1", "web-vitals": "^2.1.4", - "xlsx": "https://cdn.sheetjs.com/xlsx-0.18.11/xlsx-0.18.11.tgz" + "xlsx": "https://cdn.sheetjs.com/xlsx-0.18.11/xlsx-0.18.11.tgz", + "zustand": "^5.0.8" }, "scripts": { "start": "react-scripts start", diff --git a/src/App.jsx b/src/App.jsx index d6c2687..31721b2 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,28 +1,24 @@ import './App.css'; -import React, { useContext, useState } from 'react'; -import Icon, { - HomeOutlined, - TeamOutlined, +import { useContext, useState } from 'react'; +import { DashboardOutlined, - FileProtectOutlined, - CustomerServiceTwoTone, SnippetsTwoTone, DollarOutlined, AreaChartOutlined, - WechatOutlined, UserOutlined, FlagOutlined, - PieChartOutlined, - BarChartOutlined, CoffeeOutlined, DesktopOutlined, - WhatsAppOutlined + WhatsAppOutlined, + ShoppingCartOutlined, + GiftOutlined } from '@ant-design/icons'; -import { Layout, Menu, Image, Badge, Button } from 'antd'; +import { Layout, Menu, Button } from 'antd'; import { BrowserRouter, Route, Routes, NavLink } from 'react-router-dom'; import Home from './views/Home'; import Dashboard from './views/Dashboard'; import Orders from './views/Orders'; import Orders_sub from './views/Orders_sub'; +import BizOrder from './views/biz/BizOrder'; import ProtectedRoute from './views/ProtectedRoute'; import Customer_care_inchina from './charts/Customer_care_inchina'; import Customer_care_potential from './charts/Customer_care_potential'; @@ -81,7 +77,7 @@ const App = () => { key: 'meeting-2024-GH', label: GH区域数据, // GH例会数据-2024 }, - ] + ], }, { key: 2, @@ -104,6 +100,12 @@ const App = () => { }, ], }, + { + key: 20, label: '商旅市场', icon: , + children: [ + { key: 201, label: 订单数据 }, + ] + }, { key: 5, label: '销售', @@ -118,7 +120,7 @@ const App = () => { { key: 3, label: '客运', - icon: , + icon: , children: [ { key: 31, @@ -151,7 +153,7 @@ const App = () => { { key: 6, label: '客服', - icon: , + icon: , children: [ { key: 61, @@ -253,6 +255,8 @@ const App = () => { } /> } /> } /> + } /> + ToDo} /> }> } /> diff --git a/src/libs/ht.js b/src/libs/ht.js index bca157c..a31a071 100644 --- a/src/libs/ht.js +++ b/src/libs/ht.js @@ -39,7 +39,7 @@ export const deptUnits = [ export const groups = [ { value: '1,2,28,7,33', key: '1,2,28,7,33', label: 'GH事业部', code: 'GH', children: [1, 2, 28, 7, 33] }, { value: '8,9,11,12,20,21', key: '8,9,11,12,20,21', label: '国际事业部', code: 'INT', children: [8, 9, 11, 12, 20, 21] }, - { value: '10,18,16,30', key: '10,18,16,30', label: '孵化学院', code: '', children: [10, 18, 16, 30] }, + { value: '10,18,16,30', key: '10,18,16,30', label: '孵化学院', code: 'CTX', children: [10, 18, 16, 30] }, { value: '1', key: '1', label: 'CH直销', code: '', children: [] }, { value: '2', key: '2', label: 'CH大客户', code: '', children: [] }, { value: '28', key: '28', label: 'HT项目组(前AH)', code: 'HT', children: [] }, diff --git a/src/views/biz/BizOrder.jsx b/src/views/biz/BizOrder.jsx new file mode 100644 index 0000000..263f2e7 --- /dev/null +++ b/src/views/biz/BizOrder.jsx @@ -0,0 +1,412 @@ +import { useContext } from 'react'; +import { Row, Col, Tabs, Table, Divider, Spin } from 'antd'; +import { + ContainerOutlined, + BlockOutlined, + SmileOutlined, + MobileOutlined, +} from '@ant-design/icons'; +import { Line } from '@ant-design/charts'; +import { NavLink } from 'react-router-dom'; +import * as comm from '../../utils/commons'; +import DateGroupRadio from '../../components/DateGroupRadio'; +import SearchForm from '../../components/search/SearchForm'; +import { TableExportBtn } from '../../components/Data'; + +import { observer } from 'mobx-react'; +import { stores_Context } from '../../config'; +import { useShallow } from 'zustand/shallow'; +import useBizOrderStore, { orderCountDataMapper, orderCountDataFieldMapper } from '../../zustand/BizOrder'; + +const BizOrder = observer(() => { + const { date_picker_store: searchFormStore } = useContext(stores_Context); + + const [searchValues, setSearchValues] = useBizOrderStore(useShallow((state) => [state.searchValues, state.setSearchValues])); + const [activeTab, setActiveTab] = useBizOrderStore(useShallow((state) => [state.activeTab, state.setActiveTab])); + const [loading, onTabChange, ] = useBizOrderStore(useShallow((state) => [state.loading, state.onTabChange,])); + + const orderCountDataRaw = useBizOrderStore((state) => state.orderCountDataRaw); + const [orderCountDataLines, avgLineValue ] = useBizOrderStore(useShallow((state) => [state.orderCountDataLines, state.avgLineValue])); + const [onChangeDateGroup, activeDateGroupRadio ] = useBizOrderStore(useShallow((state) => [state.onChangeDateGroup, state.activeDateGroupRadio])); + + const orderCountDataByType = useBizOrderStore((state) => state.orderCountDataByType); + const result = orderCountDataByType[activeTab] || {}; + + const getBizOrderCount = useBizOrderStore((state) => state.getBizOrderCount); + + const showDiff = !comm.isEmpty(searchFormStore.start_date_cp); + + const avg_line_y = Math.round(avgLineValue); + const config = { + data: orderCountDataLines, + padding: "auto", + xField: "xField", + yField: "yField", + seriesField: "seriesField", + // xAxis: { + // type: "timeCat", + // }, + point: { + size: 4, + shape: "cicle", + }, + annotations: [ + { + type: "text", + position: ["start", avg_line_y], + content: avg_line_y, + offsetX: -15, + style: { + fill: "#F4664A", + textBaseline: "bottom", + }, + }, + { + type: "line", + start: [-10, avg_line_y], + end: ["max", avg_line_y], + style: { + stroke: "#F4664A", + lineDash: [2, 2], + }, + }, + ], + label: {}, // 显示标签 + legend: { + itemValue: { + formatter: (text, item) => { + const items = orderCountDataLines.filter(d => d.seriesField === item.value); // 按站点筛选 + return items.length ? items.reduce((a, b) => a + b.yField, 0) : ""; // 计算总数 + }, + }, + }, + tooltip: { + customItems: (originalItems) => { + // process originalItems, + return originalItems.map(ele => ({...ele, name: ele.data?.seriesField || ele.data?.xField})); + }, + title: (title, datum) => { + let ret = title; + switch (activeDateGroupRadio) { + case 'day': + ret = `${title} ${comm.getWeek(datum.xField)}`; // 显示周几 + break; + + default: + break; + } + return ret; + }, + }, + // smooth: true, + }; + + const tableProps = { + dataSource: result?.ordercount1 || [], // table_data.dataSource, + columns: [ + { + title: '#', + fixed: 'left', + children: [ + { + title: ( + +
+ {result.ordercountTotal1?.groups} +
+ {showDiff ? ( +
+ {result.ordercountTotal2?.groups} +
+ ) : null} +
+ ), + titleX: + `${searchFormStore.start_date.format(config.DATE_FORMAT)}~${searchFormStore.end_date.format(config.DATE_FORMAT)}` + + (showDiff ? ` vs ${searchFormStore.start_date_cp.format(config.DATE_FORMAT)}~${searchFormStore.end_date_cp.format(config.DATE_FORMAT)}` : ''), + dataIndex: 'OrderType', + fixed: 'left', + render: (text, record) => {text}, + }, + ], + }, + { + title: '数量', + children: [ + { + title: !showDiff + ? result.ordercountTotal1?.OrderCount + : comm.show_vs_tag(result.ordercountTotal1?.OrderCount_vs, result.ordercountTotal1?.OrderCount_diff, result.ordercountTotal1?.OrderCount, result.ordercountTotal2?.OrderCount), + titleX: [result.ordercountTotal1?.OrderCount, result.ordercountTotal2?.OrderCount].join(' vs '), + dataIndex: 'OrderCount', + render: (text, r) => (!showDiff ? text : comm.show_vs_tag(r.OrderCount_vs, r.OrderCount_diff, r.OrderCount, r.diff?.OrderCount)), + }, + ], + }, + { + title: '成交数', + children: [ + { + title: !showDiff + ? result.ordercountTotal1?.CJCount + : comm.show_vs_tag(result.ordercountTotal1?.CJCount_vs, result.ordercountTotal1?.CJCount_diff, result.ordercountTotal1?.CJCount, result.ordercountTotal2?.CJCount), + titleX: [result.ordercountTotal1?.CJCount, result.ordercountTotal2?.CJCount].join(' vs '), + dataIndex: 'CJCount', + render: (text, r) => (!showDiff ? text : comm.show_vs_tag(r.CJCount_vs, r.CJCount_diff, r.CJCount, r.diff?.CJCount)), + }, + ], + }, + { + title: '成交人数', + children: [ + { + title: !showDiff + ? result.ordercountTotal1?.CJPersonNum + : comm.show_vs_tag(result.ordercountTotal1?.CJPersonNum_vs, result.ordercountTotal1?.CJPersonNum_diff, result.ordercountTotal1?.CJPersonNum, result.ordercountTotal2?.CJPersonNum), + titleX: [result.ordercountTotal1?.CJPersonNum, result.ordercountTotal2?.CJPersonNum].join(' vs '), + dataIndex: 'CJPersonNum', + render: (text, r) => (!showDiff ? text : comm.show_vs_tag(r.CJPersonNum_vs, r.CJPersonNum_diff, r.CJPersonNum, r.diff?.CJPersonNum)), + }, + ], + }, + { + title: '成交率', + children: [ + { + title: !showDiff + ? result.ordercountTotal1?.CJrate + : comm.show_vs_tag(result.ordercountTotal1?.CJrate_vs, result.ordercountTotal1?.CJrate_diff, result.ordercountTotal1?.CJrate, result.ordercountTotal2?.CJrate), + titleX: [result.ordercountTotal1?.CJrate, result.ordercountTotal2?.CJrate].join(' vs '), + dataIndex: 'CJrate', + render: (text, r) => (!showDiff ? text : comm.show_vs_tag(r.CJrate_vs, r.CJrate_diff, r.CJrate, r.diff?.CJrate)), + }, + ], + }, + { + title: '成交毛利(预计)', + children: [ + { + title: !showDiff + ? result.ordercountTotal1?.YJLY + : comm.show_vs_tag(result.ordercountTotal1?.YJLY_vs, result.ordercountTotal1?.YJLY_diff, result.ordercountTotal1?.YJLY, result.ordercountTotal2?.YJLY), + titleX: [result.ordercountTotal1?.YJLY, result.ordercountTotal2?.YJLY].join(' vs '), + dataIndex: 'YJLY', + render: (text, r) => (!showDiff ? text : comm.show_vs_tag(r.YJLY_vs, r.YJLY_diff, r.YJLY, r.diff?.YJLY)), + }, + ], + }, + + { + title: '单个订单价值', + children: [ + { + title: !showDiff + ? result.ordercountTotal1?.Ordervalue + : comm.show_vs_tag(result.ordercountTotal1?.Ordervalue_vs, result.ordercountTotal1?.Ordervalue_diff, result.ordercountTotal1?.Ordervalue, result.ordercountTotal2?.Ordervalue), + titleX: [result.ordercountTotal1?.Ordervalue, result.ordercountTotal2?.Ordervalue].join(' vs '), + dataIndex: 'Ordervalue', + render: (text, r) => (!showDiff ? text : comm.show_vs_tag(r.Ordervalue_vs, r.Ordervalue_diff, r.Ordervalue, r.diff?.Ordervalue)), + }, + ], + }, + ], + size: 'small', + pagination: false, + scroll: { x: 100 * 7 }, + loading, + }; + + return ( + <> +
+ + + { + setSearchValues(obj, form); + getBizOrderCount(obj); + onTabChange(activeTab); + }} + /> + + + + + + + + + + + + + + onTabChange(active_key)} + items={[ + { + key: 'Form', + label: ( + + + 来源类型 + + ), + }, + // { + // key: 'Product', + // label: ( + // + // + // 产品类型 + // + // ), + // }, + { + key: 'Country', + label: ( + + + 国籍 + + ), + }, + // { + // key: 'line', + // label: ( + // + // + // 线路 + // + // ), + // }, + // { + // key: 'city', + // label: ( + // + // + // 目的地 + // + // ), + // }, + { + key: 'LineClass', + label: ( + + + 页面类型 + + ), + }, + { + key: 'ordersource', + label: ( + + + 来源设备 + + ), + }, + // { + // key: 'GuestGroupType', + // label: ( + // + // + // 客群类别 + // + // ), + // }, + // { + // key: 'TravelMotivation', + // label: ( + // + // + // 出行动机 + // + // ), + // }, + // { + // key: 'ToB', + // label: ( + // + // + // 客运类别 + // + // ), + // }, + // { + // key: 'FoodRequirement', + // label: ( + // + // + // 饮食要求 + // + // ), + // }, + // { + // key: 'hobbies', + // label: ( + // + // + // 兴趣爱好 + // + // ), + // }, + // { + // key: 'ages', + // label: ( + // + // + // 年龄段 + // + // ), + // }, + ].map((ele) => { + return { + ...ele, + children: ( + <> + + + + + + ), + }; + })} + /> + + + {/* */} + + + {/* */} + + + + + + + ); +}); +export default BizOrder; diff --git a/src/zustand/BizOrder.js b/src/zustand/BizOrder.js new file mode 100644 index 0000000..4f8ed6f --- /dev/null +++ b/src/zustand/BizOrder.js @@ -0,0 +1,170 @@ +import { create } from 'zustand'; +import { devtools } from 'zustand/middleware'; +import { groupsMappedByCode } from '../libs/ht'; +import { fetchJSON } from '../utils/request'; +import { HT_HOST } from '../config'; +import { resultDataCb } from '../components/DateGroupRadio/date'; + +export const fetchBizOrderCount = async (params) => { + const { errcode, errmsg, ...result } = await fetchJSON(HT_HOST + '/service-web/QueryData/GetOrderCount_biz', { + ...params, + COLI_ApplyDate1: params.Date1, + COLI_ApplyDate2: params.Date2, + COLI_ApplyDateOld1: params.DateDiff1 || '', + COLI_ApplyDateOld2: params.DateDiff2 || '', + }); + return errcode !== 0 ? {} : result || {}; +}; + +const _typeRes = { + 'ordercount1': [], + 'ordercount2': [], + 'ordercountTotal1': { + 'OrderType': '合计', + 'OrderCount': 0, + 'CJCount': 0, + 'CJPersonNum': 0, + 'YJLY': '', + 'CJrate': '0%', + 'Ordervalue': '', + 'groups': '', + 'key': 1, + }, + 'ordercountTotal2': {}, +}; +export const fetchBizOrderCountByType = async (type, params) => { + const { errcode, errmsg, ...result } = await fetchJSON(HT_HOST + '/service-web/QueryData/GetOrderCountByType_biz', { + ...params, + COLI_ApplyDate1: params.Date1, + COLI_ApplyDate2: params.Date2, + COLI_ApplyDateOld1: params.DateDiff1 || '', + COLI_ApplyDateOld2: params.DateDiff2 || '', + OrderType: type, + }); + const res = errcode !== 0 ? _typeRes : result || _typeRes; + const rows1Map = res.ordercount1.reduce((a, row1) => ({ ...a, [row1.OrderTypeSN]: row1 }), {}); + const rows2Map = res.ordercount2.reduce((a, row2) => ({ ...a, [row2.OrderTypeSN]: row2 }), {}); + + const mixRow1 = res.ordercount1.map((row1) => ({ ...row1, diff: rows2Map[row1.OrderTypeSN] || {} })); + // Diff: elements in rows2 but not in rows1 + const diff = [...new Set(Object.keys(rows2Map).filter((x) => !new Set(Object.keys(rows1Map)).has(x)))]; + mixRow1.push(...diff.map((sn) => ({ diff: rows2Map[sn], OrderType: rows2Map[sn].OrderType, OrderTypeSN: rows2Map[sn].OrderTypeSN }))); + + return { ...res, ordercount1: mixRow1 }; +}; + +/** + * -------------------------------------------------------------------------------------------------------- + */ +const initialState = { + loading: false, + typeLoading: false, + searchValues: { + DateType: { key: 'applyDate', label: '提交日期' }, + WebCode: { key: 'all', label: '所有来源' }, + IncludeTickets: { key: '1', label: '含门票' }, + DepartmentList: groupsMappedByCode.CTX, // { key: 'All', label: '所有来源' }, // + }, + searchValuesToSub: { + DateType: 'applyDate', + WebCode: 'all', + IncludeTickets: '1', + DepartmentList: -1, // -1: All + }, + + activeTab: 'Form', + activeDateGroupRadio: 'day', + + orderCountDataRaw: {}, + orderCountDataLines: [], + avgLineValue: 0, + + orderCountDataByType: {}, +}; + +export const orderCountDataMapper = { 'data1': 'ordercount1', data2: 'ordercount2' }; +export const orderCountDataFieldMapper = { 'dateKey': 'ApplyDate', 'valueKey': 'orderCount', 'seriesKey': 'WebCode', _f: 'sum' }; + +const useBizOrderStore = create( + devtools( + (set, get) => ({ + ...initialState, + reset: () => set(initialState), + + setLoading: (loading) => set({ loading }), + setTypeLoading: (typeLoading) => set({ typeLoading }), + + setSearchValues: (obj, values) => set((state) => ({ searchValues: values, searchValuesToSub: obj })), + setSearchValuesToSub: (values) => set((state) => ({ searchValuesToSub: values })), + setActiveTab: (tab) => set({ activeTab: tab }), + + setOrderCountDataLines: (data) => set({ orderCountDataLines: data }), + setOrderCountDataByType: (type, data) => set({ orderCountDataByType: { ...get().orderCountDataByType, [type]: data } }), + + // data parser + onChangeDateGroup: (value, data, avg1) => { + const { setOrderCountDataLines } = get(); + const groupByDate = data.reduce((r, v) => { + (r[v.ApplyDate] || (r[v.ApplyDate] = [])).push(v); + return r; + }, {}); + const _data = Object.keys(groupByDate) + .reduce((r, _d) => { + const xAxisGroup = groupByDate[_d].reduce((a, v) => { + (a[v.groups] || (a[v.groups] = [])).push(v); + return a; + }, {}); + Object.keys(xAxisGroup).map((_group) => { + const summaryVal = xAxisGroup[_group].reduce((rows, row) => rows + row.orderCount, 0); + r.push({ ...xAxisGroup[_group][0], orderCount: summaryVal }); + return _group; + }); + return r; + }, []) + .map((row) => ({ xField: row.ApplyDate, yField: row.orderCount, seriesField: row.groups })); + setOrderCountDataLines(_data); + set({ activeDateGroupRadio: value, avgLineValue: avg1 }); + }, + + parseOrderCount: (orderCountData, dateGroup) => { + const { onChangeDateGroup } = get(); + resultDataCb(orderCountData, dateGroup, orderCountDataMapper, orderCountDataFieldMapper, onChangeDateGroup); + }, + + // site effects + getBizOrderCount: async (params) => { + const { setLoading, parseOrderCount } = get(); + setLoading(true); + try { + const res = await fetchBizOrderCount(params); + set({ orderCountDataRaw: res }); + // 第一次得到数据 + parseOrderCount(res, 'day'); + } catch (error) { + } finally { + setLoading(false); + } + }, + + getBizOrderCount_type: async (type) => { + const { setTypeLoading, searchValuesToSub, setOrderCountDataByType } = get(); + setTypeLoading(true); + try { + const res = await fetchBizOrderCountByType(type, searchValuesToSub); + setOrderCountDataByType(type, res); + } catch (error) { + } finally { + setTypeLoading(false); + } + }, + + onTabChange: async (tab) => { + const { setActiveTab, getBizOrderCount_type } = get(); + setActiveTab(tab); + await getBizOrderCount_type(tab); + }, + }), + { name: 'bizOrder' } + ) +); +export default useBizOrderStore; From 9b576144761d5343fd4f4aa03132be95ac775d47 Mon Sep 17 00:00:00 2001 From: Lei OT Date: Fri, 14 Nov 2025 13:43:29 +0800 Subject: [PATCH 2/8] =?UTF-8?q?feat:=20=E5=95=86=E6=97=85:=20=E5=B8=82?= =?UTF-8?q?=E5=9C=BA-=E8=AE=A2=E5=8D=95=E6=95=B0=E6=8D=AE:=20=E4=BA=8C?= =?UTF-8?q?=E7=BA=A7=E9=A1=B5=E9=9D=A2:=20=E6=8C=87=E5=AE=9A=E7=B1=BB?= =?UTF-8?q?=E7=9B=AE=E4=B8=8B=E7=9A=84=E5=80=BC=E7=9A=84=E8=AE=A2=E5=8D=95?= =?UTF-8?q?=E6=98=8E=E7=BB=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 50 ++++- package.json | 1 + src/App.jsx | 3 +- src/components/DateGroupRadio/date.js | 2 +- src/views/biz/BizOrder.jsx | 145 +++++++------ src/views/biz/BizOrderSub.jsx | 279 ++++++++++++++++++++++++++ src/zustand/BizOrder.js | 222 ++++++++++++-------- 7 files changed, 535 insertions(+), 167 deletions(-) create mode 100644 src/views/biz/BizOrderSub.jsx diff --git a/package-lock.json b/package-lock.json index 0e4daec..ab23224 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@ant-design/pro-components": "^2.6.16", "antd": "^4.22.6", "dingtalk-jsapi": "^3.0.9", + "immer": "^10.2.0", "insert-css": "^2.0.0", "mobx": "^6.6.1", "mobx-react": "^7.5.2", @@ -1522,6 +1523,16 @@ "react-dom": "^16.8.0 || ^17.0.0" } }, + "node_modules/@antv/xflow-core/node_modules/immer": { + "version": "9.0.21", + "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", + "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/@antv/xflow-extension": { "version": "1.0.51", "resolved": "https://registry.npmmirror.com/@antv/xflow-extension/-/xflow-extension-1.0.51.tgz", @@ -11458,9 +11469,14 @@ } }, "node_modules/immer": { - "version": "9.0.15", - "resolved": "https://registry.npmmirror.com/immer/-/immer-9.0.15.tgz", - "integrity": "sha512-2eB/sswms9AEUSkOm4SbV5Y7Vmt/bKRwByd52jfLkW4OLYeaTP3EEiJ9agqU0O/tq6Dk62Zfj+TJSqfm1rLVGQ==" + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", + "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } }, "node_modules/import-fresh": { "version": "3.3.0", @@ -17827,6 +17843,16 @@ "node": ">=8" } }, + "node_modules/react-dev-utils/node_modules/immer": { + "version": "9.0.21", + "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", + "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/react-dev-utils/node_modules/loader-utils": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.1.tgz", @@ -22682,6 +22708,13 @@ "mana-syringe": "^0.2.2", "reflect-metadata": "^0.1.13", "rxjs": "^6.6.7" + }, + "dependencies": { + "immer": { + "version": "9.0.21", + "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", + "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==" + } } }, "@antv/xflow-extension": { @@ -29931,9 +29964,9 @@ "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==" }, "immer": { - "version": "9.0.15", - "resolved": "https://registry.npmmirror.com/immer/-/immer-9.0.15.tgz", - "integrity": "sha512-2eB/sswms9AEUSkOm4SbV5Y7Vmt/bKRwByd52jfLkW4OLYeaTP3EEiJ9agqU0O/tq6Dk62Zfj+TJSqfm1rLVGQ==" + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", + "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==" }, "import-fresh": { "version": "3.3.0", @@ -34429,6 +34462,11 @@ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" }, + "immer": { + "version": "9.0.21", + "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", + "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==" + }, "loader-utils": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.1.tgz", diff --git a/package.json b/package.json index 36097d2..82c8302 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "@ant-design/pro-components": "^2.6.16", "antd": "^4.22.6", "dingtalk-jsapi": "^3.0.9", + "immer": "^10.2.0", "insert-css": "^2.0.0", "mobx": "^6.6.1", "mobx-react": "^7.5.2", diff --git a/src/App.jsx b/src/App.jsx index 31721b2..d7c7a03 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -19,6 +19,7 @@ import Dashboard from './views/Dashboard'; import Orders from './views/Orders'; import Orders_sub from './views/Orders_sub'; import BizOrder from './views/biz/BizOrder'; +import BizOrderSub from './views/biz/BizOrderSub'; import ProtectedRoute from './views/ProtectedRoute'; import Customer_care_inchina from './charts/Customer_care_inchina'; import Customer_care_potential from './charts/Customer_care_potential'; @@ -256,7 +257,7 @@ const App = () => { } /> } /> } /> - ToDo} /> + } /> }> } /> diff --git a/src/components/DateGroupRadio/date.js b/src/components/DateGroupRadio/date.js index 9bd9d15..be91eab 100644 --- a/src/components/DateGroupRadio/date.js +++ b/src/components/DateGroupRadio/date.js @@ -161,5 +161,5 @@ export const resultDataCb = (dataRaw, dateGroup, { data1, data2 }, fieldMapper, // '\ndata1KeyMappedStr', data1KeyMappedStr, // '\ndata2KeyMappedStr', data2KeyMappedStr // ); - cb(dateGroup, retData, avg1, parse2.avgVal); + return cb(dateGroup, retData, avg1, parse2.avgVal); }; diff --git a/src/views/biz/BizOrder.jsx b/src/views/biz/BizOrder.jsx index 263f2e7..f7f7a32 100644 --- a/src/views/biz/BizOrder.jsx +++ b/src/views/biz/BizOrder.jsx @@ -1,11 +1,6 @@ import { useContext } from 'react'; import { Row, Col, Tabs, Table, Divider, Spin } from 'antd'; -import { - ContainerOutlined, - BlockOutlined, - SmileOutlined, - MobileOutlined, -} from '@ant-design/icons'; +import { ContainerOutlined, BlockOutlined, SmileOutlined, MobileOutlined } from '@ant-design/icons'; import { Line } from '@ant-design/charts'; import { NavLink } from 'react-router-dom'; import * as comm from '../../utils/commons'; @@ -23,11 +18,11 @@ const BizOrder = observer(() => { const [searchValues, setSearchValues] = useBizOrderStore(useShallow((state) => [state.searchValues, state.setSearchValues])); const [activeTab, setActiveTab] = useBizOrderStore(useShallow((state) => [state.activeTab, state.setActiveTab])); - const [loading, onTabChange, ] = useBizOrderStore(useShallow((state) => [state.loading, state.onTabChange,])); + const [loading, onTabChange] = useBizOrderStore(useShallow((state) => [state.loading, state.onTabChange])); const orderCountDataRaw = useBizOrderStore((state) => state.orderCountDataRaw); - const [orderCountDataLines, avgLineValue ] = useBizOrderStore(useShallow((state) => [state.orderCountDataLines, state.avgLineValue])); - const [onChangeDateGroup, activeDateGroupRadio ] = useBizOrderStore(useShallow((state) => [state.onChangeDateGroup, state.activeDateGroupRadio])); + const [orderCountDataLines, avgLineValue] = useBizOrderStore(useShallow((state) => [state.orderCountDataLines, state.avgLineValue])); + const [onChangeDateGroup, activeDateGroupRadio] = useBizOrderStore(useShallow((state) => [state.onChangeDateGroup, state.activeDateGroupRadio])); const orderCountDataByType = useBizOrderStore((state) => state.orderCountDataByType); const result = orderCountDataByType[activeTab] || {}; @@ -36,70 +31,70 @@ const BizOrder = observer(() => { const showDiff = !comm.isEmpty(searchFormStore.start_date_cp); - const avg_line_y = Math.round(avgLineValue); - const config = { - data: orderCountDataLines, - padding: "auto", - xField: "xField", - yField: "yField", - seriesField: "seriesField", - // xAxis: { - // type: "timeCat", - // }, - point: { - size: 4, - shape: "cicle", - }, - annotations: [ - { - type: "text", - position: ["start", avg_line_y], - content: avg_line_y, - offsetX: -15, - style: { - fill: "#F4664A", - textBaseline: "bottom", - }, - }, - { - type: "line", - start: [-10, avg_line_y], - end: ["max", avg_line_y], - style: { - stroke: "#F4664A", - lineDash: [2, 2], - }, + const avg_line_y = Math.round(avgLineValue); + const lineConfig = { + data: orderCountDataLines, + padding: 'auto', + xField: 'xField', + yField: 'yField', + seriesField: 'seriesField', + // xAxis: { + // type: "timeCat", + // }, + point: { + size: 4, + shape: 'cicle', + }, + annotations: [ + { + type: 'text', + position: ['start', avg_line_y], + content: avg_line_y, + offsetX: -15, + style: { + fill: '#F4664A', + textBaseline: 'bottom', }, - ], - label: {}, // 显示标签 - legend: { - itemValue: { - formatter: (text, item) => { - const items = orderCountDataLines.filter(d => d.seriesField === item.value); // 按站点筛选 - return items.length ? items.reduce((a, b) => a + b.yField, 0) : ""; // 计算总数 - }, + }, + { + type: 'line', + start: [-10, avg_line_y], + end: ['max', avg_line_y], + style: { + stroke: '#F4664A', + lineDash: [2, 2], }, }, - tooltip: { - customItems: (originalItems) => { - // process originalItems, - return originalItems.map(ele => ({...ele, name: ele.data?.seriesField || ele.data?.xField})); + ], + label: {}, // 显示标签 + legend: { + itemValue: { + formatter: (text, item) => { + const items = orderCountDataLines.filter((d) => d.seriesField === item.value); // 按站点筛选 + return items.length ? items.reduce((a, b) => a + b.yField, 0) : ''; // 计算总数 }, - title: (title, datum) => { - let ret = title; - switch (activeDateGroupRadio) { - case 'day': - ret = `${title} ${comm.getWeek(datum.xField)}`; // 显示周几 - break; + }, + }, + tooltip: { + customItems: (originalItems) => { + // process originalItems, + return originalItems.map((ele) => ({ ...ele, name: ele.data?.seriesField || ele.data?.xField })); + }, + title: (title, datum) => { + let ret = title; + switch (activeDateGroupRadio) { + case 'day': + ret = `${title} ${comm.getWeek(datum.xField)}`; // 显示周几 + break; - default: - break; - } - return ret; - }, + default: + break; + } + return ret; }, - // smooth: true, - }; + }, + smooth: true, + }; const tableProps = { dataSource: result?.ordercount1 || [], // table_data.dataSource, @@ -111,19 +106,13 @@ const BizOrder = observer(() => { { title: ( -
- {result.ordercountTotal1?.groups} -
- {showDiff ? ( -
- {result.ordercountTotal2?.groups} -
- ) : null} +
{result.ordercountTotal1?.groups}
+ {showDiff ?
{result.ordercountTotal2?.groups}
: null}
), titleX: - `${searchFormStore.start_date.format(config.DATE_FORMAT)}~${searchFormStore.end_date.format(config.DATE_FORMAT)}` + - (showDiff ? ` vs ${searchFormStore.start_date_cp.format(config.DATE_FORMAT)}~${searchFormStore.end_date_cp.format(config.DATE_FORMAT)}` : ''), + `${searchFormStore.start_date.format(lineConfig.DATE_FORMAT)}~${searchFormStore.end_date.format(lineConfig.DATE_FORMAT)}` + + (showDiff ? ` vs ${searchFormStore.start_date_cp.format(lineConfig.DATE_FORMAT)}~${searchFormStore.end_date_cp.format(lineConfig.DATE_FORMAT)}` : ''), dataIndex: 'OrderType', fixed: 'left', render: (text, record) => {text}, @@ -255,7 +244,7 @@ const BizOrder = observer(() => {
- + diff --git a/src/views/biz/BizOrderSub.jsx b/src/views/biz/BizOrderSub.jsx new file mode 100644 index 0000000..b7d896e --- /dev/null +++ b/src/views/biz/BizOrderSub.jsx @@ -0,0 +1,279 @@ +import { useContext, useEffect } from 'react'; +import { Row, Col, Tabs, Table, Divider, Spin, Space } from 'antd'; +import { ContainerOutlined, BlockOutlined, SmileOutlined, MobileOutlined } from '@ant-design/icons'; +import { Line } from '@ant-design/charts'; +import { NavLink, useParams } from 'react-router-dom'; +import * as comm from '../../utils/commons'; +import DateGroupRadio from '../../components/DateGroupRadio'; +import SearchForm from '../../components/search/SearchForm'; +import { TableExportBtn } from '../../components/Data'; + +import { observer } from 'mobx-react'; +import { stores_Context } from '../../config'; +import { useShallow } from 'zustand/shallow'; +import useBizOrderStore, { orderCountDataMapper, orderCountDataFieldMapper } from '../../zustand/BizOrder'; + +// 在逗号和分号处自动换行的函数 +const addLineBreaksAtCommas = (text) => { + if (!text) return ''; + return text.replace(/&/g, '&').replace(/,/g, ',\n').replace(/,/g, ',\n').replace(/;/g, ';\n').replace(/;/g, ';\n'); +}; + +const OrderDetailTable = ({ caption, dataSource, loading, ...props }) => { + const columns = [ + { + title: '订单号', + dataIndex: 'COLI_ID', + key: 'COLI_ID', + }, + { + title: '网站', + dataIndex: 'COLI_WebCode', + key: 'COLI_WebCode', + }, + { + title: '成行', + dataIndex: 'COLI_Success', + key: 'COLI_Success', + render: (text, record) => {text == 1 ? '是' : '否'}, + sorter: (a, b) => b.COLI_Success - a.COLI_Success, + }, + // { + // title: "人数(成/童/婴)", + // dataIndex: "COLI_PersonNum", + // key: "COLI_PersonNum", + // render: (text, record) => ( + // + // {record.COLI_PersonNum}/{record.COLI_ChildNum}/{record.COLI_BabyNum} + // + // ), + // }, + { + title: '预计利润', + dataIndex: 'CGI_YJLY', + key: 'CGI_YJLY', + }, + { + title: '预定时间', + dataIndex: 'COLI_ApplyDate', + key: 'COLI_ApplyDate', + }, + { + title: '出发日期', + dataIndex: 'CGI_ArriveDate', + key: 'CGI_ArriveDate', + }, + // { + // title: '客人需求', + // dataIndex: 'COLI_CustomerRequest', + // key: 'COLI_CustomerRequest', + // ellipsis: true, + // }, + { + title: '订单内容', + dataIndex: 'COLI_OrderDetailText', + key: 'COLI_OrderDetailText', + ellipsis: true, + }, + Table.EXPAND_COLUMN, + ]; + return ( +
+ +
+
{caption}
+ +
+
+
record.key} + expandable={{ + expandedRowRender: (record) => ( +
+              {/* 
+                客户需求
+              
+              {record.COLI_CustomerRequest} */}
+              
+                订单内容
+              
+              {record.COLI_OrderDetailText}
+            
+ ), + }} + /> + + ); +}; + +const BizOrderSub = observer(({ ...props }) => { + const { ordertype, ordertype_sub, ordertype_title } = useParams(); + const { date_picker_store: searchFormStore } = useContext(stores_Context); + + const [searchValues, setSearchValues] = useBizOrderStore(useShallow((state) => [state.searchValues, state.setSearchValues])); + const [searchValuesToSub] = useBizOrderStore(useShallow((state) => [state.searchValuesToSub])); + + const [loading, typeLoading] = useBizOrderStore(useShallow((state) => [state.loading, state.typeLoading])); + const orderCountDataRawSub = useBizOrderStore((state) => state.orderCountDataRawSub); + const [orderCountDataLinesSub, avgLineValueSub] = useBizOrderStore(useShallow((state) => [state.orderCountDataLinesSub, state.avgLineValueSub])); + const [onChangeDateGroupSub, activeDateGroupRadioSub] = useBizOrderStore(useShallow((state) => [state.onChangeDateGroupSub, state.activeDateGroupRadioSub])); + + const orderDetails = useBizOrderStore((state) => state.orderDetails); + + const getBizOrderCount = useBizOrderStore((state) => state.getBizOrderCount); + const getBizOrderDetailByType = useBizOrderStore((state) => state.getBizOrderDetailByType); + + useEffect(() => { + getBizOrderCount(searchValuesToSub, ordertype, ordertype_sub); + getBizOrderDetailByType(searchValuesToSub, ordertype, ordertype_sub); + }, []); + + const avg_line_y = Math.round(avgLineValueSub); + const lineConfig = { + data: orderCountDataLinesSub, + padding: 'auto', + xField: 'xField', + yField: 'yField', + seriesField: 'seriesField', + // xAxis: { + // type: "timeCat", + // }, + point: { + size: 4, + shape: 'cicle', + }, + annotations: [ + { + type: 'text', + position: ['start', avg_line_y], + content: avg_line_y, + offsetX: -15, + style: { + fill: '#F4664A', + textBaseline: 'bottom', + }, + }, + { + type: 'line', + start: [-10, avg_line_y], + end: ['max', avg_line_y], + style: { + stroke: '#F4664A', + lineDash: [2, 2], + }, + }, + ], + label: {}, // 显示标签 + legend: { + itemValue: { + formatter: (text, item) => { + const items = orderCountDataLinesSub.filter((d) => d.seriesField === item.value); // 按站点筛选 + return items.length ? items.reduce((a, b) => a + b.yField, 0) : ''; // 计算总数 + }, + }, + }, + tooltip: { + customItems: (originalItems) => { + // process originalItems, + return originalItems.map((ele) => ({ ...ele, name: ele.data?.seriesField || ele.data?.xField })); + }, + title: (title, datum) => { + let ret = title; + switch (activeDateGroupRadioSub) { + case 'day': + ret = `${title} ${comm.getWeek(datum.xField)}`; // 显示周几 + break; + + default: + break; + } + return ret; + }, + }, + smooth: true, + }; + + const tab_items = [ + { + key: 'detail', + label: ( + + + 订单内容 + + ), + title: '订单内容', + children: ( + <> + + + + + + ), + }, + ]; + return ( +
+ +
+ 返回 + + + { + setSearchValues(obj, form); + getBizOrderCount(obj, ordertype, ordertype_sub); + getBizOrderDetailByType(obj, ordertype, ordertype_sub); + }} + /> + + + + + + + + + + + + + + + + + + + ); +}); +export default BizOrderSub; diff --git a/src/zustand/BizOrder.js b/src/zustand/BizOrder.js index 4f8ed6f..ac2285c 100644 --- a/src/zustand/BizOrder.js +++ b/src/zustand/BizOrder.js @@ -1,17 +1,21 @@ import { create } from 'zustand'; import { devtools } from 'zustand/middleware'; +import { immer } from 'zustand/middleware/immer'; import { groupsMappedByCode } from '../libs/ht'; import { fetchJSON } from '../utils/request'; import { HT_HOST } from '../config'; import { resultDataCb } from '../components/DateGroupRadio/date'; +import { isEmpty } from '../utils/commons'; -export const fetchBizOrderCount = async (params) => { +export const fetchBizOrderCount = async (params, type='', typeVal='') => { const { errcode, errmsg, ...result } = await fetchJSON(HT_HOST + '/service-web/QueryData/GetOrderCount_biz', { ...params, COLI_ApplyDate1: params.Date1, COLI_ApplyDate2: params.Date2, COLI_ApplyDateOld1: params.DateDiff1 || '', COLI_ApplyDateOld2: params.DateDiff2 || '', + OrderType: type, + OrderType_val: typeVal, }); return errcode !== 0 ? {} : result || {}; }; @@ -53,6 +57,50 @@ export const fetchBizOrderCountByType = async (type, params) => { return { ...res, ordercount1: mixRow1 }; }; +const _detailRes = { ordercount1: [], ordercount2: [] }; +export const fetchBizOrderDetailByType = async (params, type='', typeVal='', orderContent='detail') => { + const { errcode, errmsg, ...result } = await fetchJSON(HT_HOST + '/service-web/QueryData/GetOrderCountByType_Sub_biz', { + SubOrderType: orderContent, + ...params, + COLI_ApplyDate1: params.Date1, + COLI_ApplyDate2: params.Date2, + COLI_ApplyDateOld1: params.DateDiff1 || '', + COLI_ApplyDateOld2: params.DateDiff2 || '', + OrderType: type, + OrderType_val: typeVal, + }); + const res = errcode !== 0 ? _detailRes : (result || _detailRes); + const dateStr = [params.Date1, params.Date2].map(d => d.substring(0, 10)).join('~'); + const dateDiffStr = isEmpty(params.DateDiff1) ? '' : [params.DateDiff1, params.DateDiff2].map(d => d.substring(0, 10)).join('~'); + const ret = [{ dateRangeStr: dateStr, data: res.ordercount1 }, { dateRangeStr: dateDiffStr, data: res.ordercount2 }]; + return ret; +}; + +const calculateLineData = (value, data, avg1) => { + const groupByDate = data.reduce((r, v) => { + (r[v.ApplyDate] || (r[v.ApplyDate] = [])).push(v); + return r; + }, {}); + const _data = Object.keys(groupByDate) + .reduce((r, _d) => { + const xAxisGroup = groupByDate[_d].reduce((a, v) => { + (a[v.groups] || (a[v.groups] = [])).push(v); + return a; + }, {}); + Object.keys(xAxisGroup).map((_group) => { + const summaryVal = xAxisGroup[_group].reduce((rows, row) => rows + row.orderCount, 0); + r.push({ ...xAxisGroup[_group][0], orderCount: summaryVal }); + return _group; + }); + return r; + }, []) + .map((row) => ({ xField: row.ApplyDate, yField: row.orderCount, seriesField: row.groups })); + return { lines: _data, dateRadioValue: value, avgLineValue: avg1 }; +}; + +export const orderCountDataMapper = { 'data1': 'ordercount1', data2: 'ordercount2' }; +export const orderCountDataFieldMapper = { 'dateKey': 'ApplyDate', 'valueKey': 'orderCount', 'seriesKey': 'WebCode', _f: 'sum' }; + /** * -------------------------------------------------------------------------------------------------------- */ @@ -80,91 +128,103 @@ const initialState = { avgLineValue: 0, orderCountDataByType: {}, + + // 二级页面 + orderCountDataRawSub: {}, + orderCountDataLinesSub: [], + avgLineValueSub: 0, + activeDateGroupRadioSub: 'day', + orderDetails: [], }; -export const orderCountDataMapper = { 'data1': 'ordercount1', data2: 'ordercount2' }; -export const orderCountDataFieldMapper = { 'dateKey': 'ApplyDate', 'valueKey': 'orderCount', 'seriesKey': 'WebCode', _f: 'sum' }; const useBizOrderStore = create( devtools( - (set, get) => ({ - ...initialState, - reset: () => set(initialState), - - setLoading: (loading) => set({ loading }), - setTypeLoading: (typeLoading) => set({ typeLoading }), - - setSearchValues: (obj, values) => set((state) => ({ searchValues: values, searchValuesToSub: obj })), - setSearchValuesToSub: (values) => set((state) => ({ searchValuesToSub: values })), - setActiveTab: (tab) => set({ activeTab: tab }), - - setOrderCountDataLines: (data) => set({ orderCountDataLines: data }), - setOrderCountDataByType: (type, data) => set({ orderCountDataByType: { ...get().orderCountDataByType, [type]: data } }), - - // data parser - onChangeDateGroup: (value, data, avg1) => { - const { setOrderCountDataLines } = get(); - const groupByDate = data.reduce((r, v) => { - (r[v.ApplyDate] || (r[v.ApplyDate] = [])).push(v); - return r; - }, {}); - const _data = Object.keys(groupByDate) - .reduce((r, _d) => { - const xAxisGroup = groupByDate[_d].reduce((a, v) => { - (a[v.groups] || (a[v.groups] = [])).push(v); - return a; - }, {}); - Object.keys(xAxisGroup).map((_group) => { - const summaryVal = xAxisGroup[_group].reduce((rows, row) => rows + row.orderCount, 0); - r.push({ ...xAxisGroup[_group][0], orderCount: summaryVal }); - return _group; - }); - return r; - }, []) - .map((row) => ({ xField: row.ApplyDate, yField: row.orderCount, seriesField: row.groups })); - setOrderCountDataLines(_data); - set({ activeDateGroupRadio: value, avgLineValue: avg1 }); - }, - - parseOrderCount: (orderCountData, dateGroup) => { - const { onChangeDateGroup } = get(); - resultDataCb(orderCountData, dateGroup, orderCountDataMapper, orderCountDataFieldMapper, onChangeDateGroup); - }, - - // site effects - getBizOrderCount: async (params) => { - const { setLoading, parseOrderCount } = get(); - setLoading(true); - try { - const res = await fetchBizOrderCount(params); - set({ orderCountDataRaw: res }); - // 第一次得到数据 - parseOrderCount(res, 'day'); - } catch (error) { - } finally { - setLoading(false); - } - }, - - getBizOrderCount_type: async (type) => { - const { setTypeLoading, searchValuesToSub, setOrderCountDataByType } = get(); - setTypeLoading(true); - try { - const res = await fetchBizOrderCountByType(type, searchValuesToSub); - setOrderCountDataByType(type, res); - } catch (error) { - } finally { - setTypeLoading(false); - } - }, - - onTabChange: async (tab) => { - const { setActiveTab, getBizOrderCount_type } = get(); - setActiveTab(tab); - await getBizOrderCount_type(tab); - }, - }), - { name: 'bizOrder' } + immer( + (set, get) => ({ + ...initialState, + reset: () => set(initialState), + + setLoading: (loading) => set({ loading }), + setTypeLoading: (typeLoading) => set({ typeLoading }), + + setSearchValues: (obj, values) => set((state) => ({ searchValues: values, searchValuesToSub: obj })), + setSearchValuesToSub: (values) => set((state) => ({ searchValuesToSub: values })), + setActiveTab: (tab) => set({ activeTab: tab }), + + setOrderCountDataLines: (data) => set({ orderCountDataLines: data }), + setOrderCountDataByType: (type, data) => + set((state) => { + state.orderCountDataByType[type] = data; + }), + + // data ---- + onChangeDateGroup: (value, data, avg1) => { + const { lines, dateRadioValue, avgLineValue } = calculateLineData(value, data, avg1); + set({ orderCountDataLines: lines, avgLineValue, activeDateGroupRadio: dateRadioValue }); + }, + onChangeDateGroupSub: (value, data, avg1) => { + const { lines, dateRadioValue, avgLineValue } = calculateLineData(value, data, avg1); + set({ orderCountDataLinesSub: lines, avgLineValueSub: avgLineValue, activeDateGroupRadioSub: dateRadioValue }); + }, + + + // site effects + getBizOrderCount: async (params, type, typeVal) => { + const { setLoading, } = get(); + setLoading(true); + try { + const res = await fetchBizOrderCount(params, type, typeVal); + // 第一次得到数据 + const { lines, dateRadioValue, avgLineValue } = resultDataCb(res, 'day', orderCountDataMapper, orderCountDataFieldMapper, calculateLineData); + if (isEmpty(type)) { + // index page + set({ orderCountDataRaw: res }); + set({ orderCountDataLines: lines, avgLineValue, activeDateGroupRadio: dateRadioValue }); + } else { + // sub page + set({ orderCountDataRawSub: res }); + set({ orderCountDataLinesSub: lines, avgLineValueSub: avgLineValue, activeDateGroupRadioSub: dateRadioValue }); + } + } catch (error) { + } finally { + setLoading(false); + } + }, + + getBizOrderCount_type: async (type) => { + const { setTypeLoading, searchValuesToSub, setOrderCountDataByType } = get(); + setTypeLoading(true); + try { + const res = await fetchBizOrderCountByType(type, searchValuesToSub); + setOrderCountDataByType(type, res); + } catch (error) { + } finally { + setTypeLoading(false); + } + }, + + onTabChange: async (tab) => { + const { setActiveTab, getBizOrderCount_type } = get(); + setActiveTab(tab); + await getBizOrderCount_type(tab); + }, + + // sub + getBizOrderDetailByType: async (params, type, typeVal) => { + const { setTypeLoading, } = get(); + try { + setTypeLoading(true); + const res = await fetchBizOrderDetailByType(params, type, typeVal, 'detail'); + set({ orderDetails: res }); + } catch (error) { + } finally { + setTypeLoading(false); + } + }, + }), + { name: 'bizOrder' } + ) ) ); export default useBizOrderStore; From 190b210f4b52a174d9824acd9ad072090631210e Mon Sep 17 00:00:00 2001 From: Lei OT Date: Fri, 14 Nov 2025 15:56:27 +0800 Subject: [PATCH 3/8] trains --- src/App.jsx | 3 + src/views/biz/reports/TrainsUpsell.jsx | 81 +++++++++++++ src/zustand/BizOrder.js | 4 +- src/zustand/Trains.js | 161 +++++++++++++++++++++++++ 4 files changed, 247 insertions(+), 2 deletions(-) create mode 100644 src/views/biz/reports/TrainsUpsell.jsx create mode 100644 src/zustand/Trains.js diff --git a/src/App.jsx b/src/App.jsx index d7c7a03..c6945ab 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -55,6 +55,7 @@ import OPRisk from './views/sales-crm/Risk'; import Cruise from './views/Cruise'; import Hotel from './views/Hotel'; import HostCaseCount from './views/HostCaseCount'; +import TrainsUpsell from './views/biz/reports/TrainsUpsell'; const App = () => { const { Content, Footer, Sider, } = Layout; @@ -105,6 +106,7 @@ const App = () => { key: 20, label: '商旅市场', icon: , children: [ { key: 201, label: 订单数据 }, + { key: 202, label: Trains }, ] }, { @@ -258,6 +260,7 @@ const App = () => { } /> } /> } /> + } /> }> } /> diff --git a/src/views/biz/reports/TrainsUpsell.jsx b/src/views/biz/reports/TrainsUpsell.jsx new file mode 100644 index 0000000..4c2287e --- /dev/null +++ b/src/views/biz/reports/TrainsUpsell.jsx @@ -0,0 +1,81 @@ +import { createContext, useContext, useEffect, useState } from 'react'; +import { Row, Col, Tabs, Table, Divider, Spin } from 'antd'; +import { ContainerOutlined, BlockOutlined, SmileOutlined, MobileOutlined } from '@ant-design/icons'; +import { NavLink } from 'react-router-dom'; +import { Funnel } from '@ant-design/charts'; + +import {} from '../../../utils/commons'; +import SearchForm from '../../../components/search/SearchForm'; +import { TableExportBtn } from '../../../components/Data'; + +import { observer } from 'mobx-react'; +import { stores_Context } from '../../../config'; +import { useShallow } from 'zustand/shallow'; +import useTrainsStore from '../../../zustand/Trains'; + +const TrainsUpsell = observer(({ ...props }) => { + const { date_picker_store: searchFormStore } = useContext(stores_Context); + const [searchValues, setSearchValues] = useTrainsStore(useShallow((state) => [state.searchValues, state.setSearchValues])); + const data = [ + { + stage: '火车票预定', + number: 253, + }, + { + stage: '成行出票', + number: 200, + }, + { + stage: '火车票upsell', + number: 153, + }, + { + stage: '成行', + number: 113, + }, + ]; + const chartConfig = { + data: data, + xField: 'stage', + yField: 'number', + legend: false, + shape: 'pyramid', + }; + return ( + <> +
+ +
+ { + setSearchValues(obj, form); + // getBizOrderCount(obj); + // onTabChange(activeTab); + }} + /> + + + + + + + + + + + + ); +}); +export default TrainsUpsell; diff --git a/src/zustand/BizOrder.js b/src/zustand/BizOrder.js index ac2285c..0304b0a 100644 --- a/src/zustand/BizOrder.js +++ b/src/zustand/BizOrder.js @@ -17,7 +17,7 @@ export const fetchBizOrderCount = async (params, type='', typeVal='') => { OrderType: type, OrderType_val: typeVal, }); - return errcode !== 0 ? {} : result || {}; + return errcode !== 0 ? {} : (result || {}); }; const _typeRes = { @@ -45,7 +45,7 @@ export const fetchBizOrderCountByType = async (type, params) => { COLI_ApplyDateOld2: params.DateDiff2 || '', OrderType: type, }); - const res = errcode !== 0 ? _typeRes : result || _typeRes; + const res = errcode !== 0 ? _typeRes : (result || _typeRes); const rows1Map = res.ordercount1.reduce((a, row1) => ({ ...a, [row1.OrderTypeSN]: row1 }), {}); const rows2Map = res.ordercount2.reduce((a, row2) => ({ ...a, [row2.OrderTypeSN]: row2 }), {}); diff --git a/src/zustand/Trains.js b/src/zustand/Trains.js new file mode 100644 index 0000000..9ff6eb3 --- /dev/null +++ b/src/zustand/Trains.js @@ -0,0 +1,161 @@ +import { create } from 'zustand'; +import { devtools } from 'zustand/middleware'; +import { immer } from 'zustand/middleware/immer'; +import { groupsMappedByCode } from '../libs/ht'; +import { fetchJSON } from '../utils/request'; +import { HT_HOST } from '../config'; +import { resultDataCb } from '../components/DateGroupRadio/date'; +import { isEmpty } from '../utils/commons'; + +const FORM_TRAINSBOOKING = 32024; // 火车票预定 +const FORM_TRAINSUPSELL = 32214; // 火车票Upsell + +const _res = { ordercount1: [], ordercount2: [] }; +export const fetchBizTrainsOrderSummaryByType = async (type, params) => { + const { errcode, errmsg, ...result } = await fetchJSON(HT_HOST + '/service-web/QueryData/GetOrderCountByType_biz', { + ...params, + COLI_ApplyDate1: params.Date1, + COLI_ApplyDate2: params.Date2, + COLI_ApplyDateOld1: params.DateDiff1 || '', + COLI_ApplyDateOld2: params.DateDiff2 || '', + OrderType: type, + }); + const res = errcode !== 0 ? _res : (result || _res); + const dateStr = [params.Date1, params.Date2].map(d => d.substring(0, 10)).join('~'); + const dateDiffStr = isEmpty(params.DateDiff1) ? '' : [params.DateDiff1, params.DateDiff2].map(d => d.substring(0, 10)).join('~'); + const ret = [ + { dateRangeStr: dateStr, data: res.ordercount1.find((row) => row.OrderTypeSN === FORM_TRAINSBOOKING) }, + { dateRangeStr: dateDiffStr, data: res.ordercount2.find((row) => row.OrderTypeSN === FORM_TRAINSBOOKING) }, + ]; + return ret; +}; + + +/** + * -------------------------------------------------------------------------------------------------------- + */ +const initialState = { + loading: false, + typeLoading: false, + searchValues: { + DateType: { key: 'applyDate', label: '提交日期' }, + WebCode: { key: 'all', label: '所有来源' }, + IncludeTickets: { key: '1', label: '含门票' }, + DepartmentList: groupsMappedByCode.CTX, // { key: 'All', label: '所有来源' }, // + }, + searchValuesToSub: { + DateType: 'applyDate', + WebCode: 'all', + IncludeTickets: '1', + DepartmentList: -1, // -1: All + }, + + activeTab: 'Form', + activeDateGroupRadio: 'day', + + orderCountDataRaw: {}, + orderCountDataLines: [], + avgLineValue: 0, + + orderCountDataByType: {}, + + // 二级页面 + orderCountDataRawSub: {}, + orderCountDataLinesSub: [], + avgLineValueSub: 0, + activeDateGroupRadioSub: 'day', + orderDetails: [], +}; + + +const useTrainsStore = create( + devtools( + immer( + (set, get) => ({ + ...initialState, + reset: () => set(initialState), + + setLoading: (loading) => set({ loading }), + setTypeLoading: (typeLoading) => set({ typeLoading }), + + setSearchValues: (obj, values) => set((state) => ({ searchValues: values, searchValuesToSub: obj })), + setSearchValuesToSub: (values) => set((state) => ({ searchValuesToSub: values })), + setActiveTab: (tab) => set({ activeTab: tab }), + + setOrderCountDataLines: (data) => set({ orderCountDataLines: data }), + setOrderCountDataByType: (type, data) => + set((state) => { + state.orderCountDataByType[type] = data; + }), + + // data ---- + onChangeDateGroup: (value, data, avg1) => { + // const { lines, dateRadioValue, avgLineValue } = calculateLineData(value, data, avg1); + // set({ orderCountDataLines: lines, avgLineValue, activeDateGroupRadio: dateRadioValue }); + }, + onChangeDateGroupSub: (value, data, avg1) => { + // const { lines, dateRadioValue, avgLineValue } = calculateLineData(value, data, avg1); + // set({ orderCountDataLinesSub: lines, avgLineValueSub: avgLineValue, activeDateGroupRadioSub: dateRadioValue }); + }, + + + // site effects + getBizOrderCount: async (params, type, typeVal) => { + const { setLoading, } = get(); + setLoading(true); + try { + // const res = await fetchBizOrderCount(params, type, typeVal); + // 第一次得到数据 + // const { lines, dateRadioValue, avgLineValue } = resultDataCb(res, 'day', orderCountDataMapper, orderCountDataFieldMapper, calculateLineData); + // if (isEmpty(type)) { + // // index page + // set({ orderCountDataRaw: res }); + // set({ orderCountDataLines: lines, avgLineValue, activeDateGroupRadio: dateRadioValue }); + // } else { + // // sub page + // set({ orderCountDataRawSub: res }); + // set({ orderCountDataLinesSub: lines, avgLineValueSub: avgLineValue, activeDateGroupRadioSub: dateRadioValue }); + // } + } catch (error) { + } finally { + setLoading(false); + } + }, + + getBizOrderCount_type: async (type) => { + const { setTypeLoading, searchValuesToSub, setOrderCountDataByType } = get(); + setTypeLoading(true); + try { + // const res = await fetchBizOrderCountByType(type, searchValuesToSub); + // setOrderCountDataByType(type, res); + } catch (error) { + } finally { + setTypeLoading(false); + } + }, + + onTabChange: async (tab) => { + const { setActiveTab, getBizOrderCount_type } = get(); + setActiveTab(tab); + await getBizOrderCount_type(tab); + }, + + // sub + getBizOrderDetailByType: async (params, type, typeVal) => { + const { setTypeLoading, } = get(); + try { + setTypeLoading(true); + // const res = await fetchBizOrderDetailByType(params, type, typeVal, 'detail'); + // console.log('💥111', res); + // set({ orderDetails: res }); + } catch (error) { + } finally { + setTypeLoading(false); + } + }, + }), + { name: 'bizOrder' } + ) + ) +); +export default useTrainsStore; From 7912260e273a5e2999f0d8ed2a9faba6de1be01b Mon Sep 17 00:00:00 2001 From: Lei OT Date: Mon, 17 Nov 2025 10:42:33 +0800 Subject: [PATCH 4/8] =?UTF-8?q?perf:=20=E5=95=86=E5=8A=A1=E6=95=B0?= =?UTF-8?q?=E6=8D=AE:=20+IncludeInternal?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/search/SearchForm.jsx | 15 +++++++++++++++ src/views/biz/BizOrder.jsx | 14 ++++++++++++-- src/views/biz/BizOrderSub.jsx | 3 ++- src/zustand/BizOrder.js | 5 +++++ 4 files changed, 34 insertions(+), 3 deletions(-) diff --git a/src/components/search/SearchForm.jsx b/src/components/search/SearchForm.jsx index b6ee01a..232a3ad 100644 --- a/src/components/search/SearchForm.jsx +++ b/src/components/search/SearchForm.jsx @@ -379,6 +379,21 @@ function getFields(props) { , 3 ), + item( + 'IncludeInternal', + 99, + + + , + 3 + ), item( 'countryArea', 99, diff --git a/src/views/biz/BizOrder.jsx b/src/views/biz/BizOrder.jsx index f7f7a32..b5a99b8 100644 --- a/src/views/biz/BizOrder.jsx +++ b/src/views/biz/BizOrder.jsx @@ -1,6 +1,6 @@ import { useContext } from 'react'; import { Row, Col, Tabs, Table, Divider, Spin } from 'antd'; -import { ContainerOutlined, BlockOutlined, SmileOutlined, MobileOutlined } from '@ant-design/icons'; +import { ContainerOutlined, BlockOutlined, SmileOutlined, MobileOutlined, CustomerServiceOutlined } from '@ant-design/icons'; import { Line } from '@ant-design/charts'; import { NavLink } from 'react-router-dom'; import * as comm from '../../utils/commons'; @@ -216,7 +216,8 @@ const BizOrder = observer(() => { ...searchFormStore.formValues, ...searchValues, }, - shows: ['DateType', 'DepartmentList', 'WebCode', 'IncludeTickets', 'dates'], + // 'WebCode','IncludeTickets', + shows: ['DateType', 'DepartmentList', 'IncludeInternal', 'dates'], fieldProps: { DepartmentList: { show_all: false, mode: 'multiple' }, WebCode: { show_all: false, mode: 'multiple' }, @@ -253,6 +254,15 @@ const BizOrder = observer(() => { activeKey={activeTab} onChange={(active_key) => onTabChange(active_key)} items={[ + { + key: 'servicetype', + label: ( + + + 服务类型 + + ), + }, { key: 'Form', label: ( diff --git a/src/views/biz/BizOrderSub.jsx b/src/views/biz/BizOrderSub.jsx index b7d896e..40df6ea 100644 --- a/src/views/biz/BizOrderSub.jsx +++ b/src/views/biz/BizOrderSub.jsx @@ -232,7 +232,8 @@ const BizOrderSub = observer(({ ...props }) => { ...searchFormStore.formValues, ...searchValues, }, - shows: ['DateType', 'DepartmentList', 'WebCode', 'IncludeTickets', 'dates'], + // 'WebCode', 'IncludeTickets', + shows: ['DateType', 'DepartmentList', 'IncludeInternal', 'dates'], fieldProps: { DepartmentList: { show_all: false, mode: 'multiple' }, WebCode: { show_all: false, mode: 'multiple' }, diff --git a/src/zustand/BizOrder.js b/src/zustand/BizOrder.js index 0304b0a..5a837f8 100644 --- a/src/zustand/BizOrder.js +++ b/src/zustand/BizOrder.js @@ -7,8 +7,11 @@ import { HT_HOST } from '../config'; import { resultDataCb } from '../components/DateGroupRadio/date'; import { isEmpty } from '../utils/commons'; +const defaultParams = { WebCode: 'all', IncludeTickets: 1, IncludeInternal: 1, }; + export const fetchBizOrderCount = async (params, type='', typeVal='') => { const { errcode, errmsg, ...result } = await fetchJSON(HT_HOST + '/service-web/QueryData/GetOrderCount_biz', { + ...defaultParams, ...params, COLI_ApplyDate1: params.Date1, COLI_ApplyDate2: params.Date2, @@ -38,6 +41,7 @@ const _typeRes = { }; export const fetchBizOrderCountByType = async (type, params) => { const { errcode, errmsg, ...result } = await fetchJSON(HT_HOST + '/service-web/QueryData/GetOrderCountByType_biz', { + ...defaultParams, ...params, COLI_ApplyDate1: params.Date1, COLI_ApplyDate2: params.Date2, @@ -60,6 +64,7 @@ export const fetchBizOrderCountByType = async (type, params) => { const _detailRes = { ordercount1: [], ordercount2: [] }; export const fetchBizOrderDetailByType = async (params, type='', typeVal='', orderContent='detail') => { const { errcode, errmsg, ...result } = await fetchJSON(HT_HOST + '/service-web/QueryData/GetOrderCountByType_Sub_biz', { + ...defaultParams, SubOrderType: orderContent, ...params, COLI_ApplyDate1: params.Date1, From ac2980e603dd2e00c8576a8c5ba9e69c89e0b001 Mon Sep 17 00:00:00 2001 From: Lei OT Date: Tue, 18 Nov 2025 10:34:26 +0800 Subject: [PATCH 5/8] =?UTF-8?q?feat:=20TrainsUpsell=0Bfix:=20Mobx=20immer?= =?UTF-8?q?=20=E5=86=B2=E7=AA=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.jsx | 2 +- src/views/biz/BizOrder.jsx | 9 +- src/views/biz/reports/TrainsUpsell.jsx | 148 ++++++++++++++++++------- src/zustand/Trains.js | 92 ++++++++++++--- 4 files changed, 196 insertions(+), 55 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index c6945ab..ddbe4ca 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -106,7 +106,7 @@ const App = () => { key: 20, label: '商旅市场', icon: , children: [ { key: 201, label: 订单数据 }, - { key: 202, label: Trains }, + { key: 202, label: TrainsUpsell }, ] }, { diff --git a/src/views/biz/BizOrder.jsx b/src/views/biz/BizOrder.jsx index b5a99b8..f3fd429 100644 --- a/src/views/biz/BizOrder.jsx +++ b/src/views/biz/BizOrder.jsx @@ -9,6 +9,7 @@ import SearchForm from '../../components/search/SearchForm'; import { TableExportBtn } from '../../components/Data'; import { observer } from 'mobx-react'; +import { toJS } from 'mobx'; import { stores_Context } from '../../config'; import { useShallow } from 'zustand/shallow'; import useBizOrderStore, { orderCountDataMapper, orderCountDataFieldMapper } from '../../zustand/BizOrder'; @@ -111,8 +112,8 @@ const BizOrder = observer(() => { ), titleX: - `${searchFormStore.start_date.format(lineConfig.DATE_FORMAT)}~${searchFormStore.end_date.format(lineConfig.DATE_FORMAT)}` + - (showDiff ? ` vs ${searchFormStore.start_date_cp.format(lineConfig.DATE_FORMAT)}~${searchFormStore.end_date_cp.format(lineConfig.DATE_FORMAT)}` : ''), + `${result.ordercountTotal1?.groups}` + + (showDiff ? ` vs ${result.ordercountTotal2?.groups}` : ''), dataIndex: 'OrderType', fixed: 'left', render: (text, record) => {text}, @@ -208,12 +209,12 @@ const BizOrder = observer(() => { return ( <>
- +
{ + // const key = 'OrderCount'; + const data01 = [ + { stage: '火车票预定', number: data1?.data?.[0]?.OrderCount, dateRange: data1?.dateRangeStr }, + { stage: '成行出票', number: data1?.data?.[0]?.CJCount, dateRange: data1?.dateRangeStr }, + { stage: '火车票upsell', number: (data1?.data?.[1]?.OrderCount || 0) + 0, dateRange: data1?.dateRangeStr }, + { stage: '成行', number: data1?.data?.[1]?.CJCount, dateRange: data1?.dateRangeStr }, + ]; + const data02 = !comm.isEmpty(data2) + ? [ + { stage: '火车票预定', number: data2?.data?.[0]?.OrderCount, dateRange: data2?.dateRangeStr }, + { stage: '成行出票', number: data2?.data?.[0]?.CJCount, dateRange: data2?.dateRangeStr }, + { stage: '火车票upsell', number: data2?.data?.[1]?.OrderCount, dateRange: data2?.dateRangeStr }, + { stage: '成行', number: data2?.data?.[1]?.CJCount, dateRange: data2?.dateRangeStr }, + ] + : []; + return data01.concat(data02); +}; + + const TrainsUpsell = observer(({ ...props }) => { const { date_picker_store: searchFormStore } = useContext(stores_Context); const [searchValues, setSearchValues] = useTrainsStore(useShallow((state) => [state.searchValues, state.setSearchValues])); - const data = [ - { - stage: '火车票预定', - number: 253, - }, - { - stage: '成行出票', - number: 200, - }, - { - stage: '火车票upsell', - number: 153, - }, - { - stage: '成行', - number: 113, - }, - ]; + + const [loading] = useTrainsStore(useShallow((state) => [state.loading])); + const [trainsOrdersSummary, compareData] = useTrainsStore((state) => state.trainsOrdersSummary); + // const showDiff = !comm.isEmpty(searchValuesToSub.DateDiff2); + const showDiff = !comm.isEmpty(compareData); + + const getBizOrderCount = useTrainsStore((state) => state.getBizOrderCount); + + const data0 = buildFunnelData(trainsOrdersSummary, compareData); const chartConfig = { - data: data, + data: data0, xField: 'stage', yField: 'number', + compareField: showDiff ? 'dateRange' : false, + // isTransposed: true, legend: false, shape: 'pyramid', + theme: { + colors10: [ + // '#b5d8f3', + '#95bbda', + // '#739ec1', + '#5a89ad', + // '#3d759b', + '#2f668a', + // '#1f5373', + '#0F405D', + '#002c45', + ], + }, + }; + const tableProps = { + title: () => `${trainsOrdersSummary?.dateRangeStr || ''}` + (showDiff ? ` vs ${compareData?.dateRangeStr}` : ''), + dataSource: trainsOrdersSummary?.data || [], + columns: [ + { + title: '#', + fixed: 'left', + dataIndex: 'OrderType', + }, + { + title: '数量', + dataIndex: 'OrderCount', + render: (text, r) => (!showDiff ? text : comm.show_vs_tag(r.OrderCount_vs, r.OrderCount_diff, r.OrderCount, r.diff?.OrderCount)), + }, + { + title: '成交数', + dataIndex: 'CJCount', + render: (text, r) => (!showDiff ? text : comm.show_vs_tag(r.CJCount_vs, r.CJCount_diff, r.CJCount, r.diff?.CJCount)), + }, + { + title: '成交人数', + dataIndex: 'CJPersonNum', + render: (text, r) => (!showDiff ? text : comm.show_vs_tag(r.CJPersonNum_vs, r.CJPersonNum_diff, r.CJPersonNum, r.diff?.CJPersonNum)), + }, + { + title: '成交率', + dataIndex: 'CJrate', + render: (text, r) => (!showDiff ? text : comm.show_vs_tag(r.CJrate_vs, r.CJrate_diff, r.CJrate, r.diff?.CJrate)), + }, + { + title: '成交毛利(预计)', + dataIndex: 'YJLY', + render: (text, r) => (!showDiff ? text : comm.show_vs_tag(r.YJLY_vs, r.YJLY_diff, r.YJLY, r.diff?.YJLY)), + }, + + { + title: '单个订单价值', + dataIndex: 'Ordervalue', + render: (text, r) => (!showDiff ? text : comm.show_vs_tag(r.Ordervalue_vs, r.Ordervalue_diff, r.Ordervalue, r.diff?.Ordervalue)), + }, + ], + size: 'small', + pagination: false, + scroll: { x: 100 * 7 }, + loading, }; return ( <>
- +
{ }} onSubmit={(_err, obj, form, str) => { setSearchValues(obj, form); - // getBizOrderCount(obj); - // onTabChange(activeTab); + getBizOrderCount(obj); }} /> - - - - - - - + +
+ + + + +

订单数量

+ +
+ + + ); diff --git a/src/zustand/Trains.js b/src/zustand/Trains.js index 9ff6eb3..af8ae85 100644 --- a/src/zustand/Trains.js +++ b/src/zustand/Trains.js @@ -5,31 +5,96 @@ import { groupsMappedByCode } from '../libs/ht'; import { fetchJSON } from '../utils/request'; import { HT_HOST } from '../config'; import { resultDataCb } from '../components/DateGroupRadio/date'; -import { isEmpty } from '../utils/commons'; +import { groupBy, isEmpty } from '../utils/commons'; +const SERVICETYPE_TRAINSBOOKING = 2; // 火车票服务 const FORM_TRAINSBOOKING = 32024; // 火车票预定 const FORM_TRAINSUPSELL = 32214; // 火车票Upsell +const defaultParams = { WebCode: 'all', IncludeTickets: 1, IncludeInternal: 1 }; const _res = { ordercount1: [], ordercount2: [] }; -export const fetchBizTrainsOrderSummaryByType = async (type, params) => { + +export const fetchBizTrainsOrderSummaryByType = async (params) => { const { errcode, errmsg, ...result } = await fetchJSON(HT_HOST + '/service-web/QueryData/GetOrderCountByType_biz', { + ...defaultParams, ...params, COLI_ApplyDate1: params.Date1, COLI_ApplyDate2: params.Date2, COLI_ApplyDateOld1: params.DateDiff1 || '', COLI_ApplyDateOld2: params.DateDiff2 || '', - OrderType: type, + OrderType: 'Form', }); - const res = errcode !== 0 ? _res : (result || _res); - const dateStr = [params.Date1, params.Date2].map(d => d.substring(0, 10)).join('~'); - const dateDiffStr = isEmpty(params.DateDiff1) ? '' : [params.DateDiff1, params.DateDiff2].map(d => d.substring(0, 10)).join('~'); + const res = errcode !== 0 ? _res : result || _res; + const dateStr = [params.Date1, params.Date2].map((d) => d.substring(0, 10)).join('~'); const ret = [ { dateRangeStr: dateStr, data: res.ordercount1.find((row) => row.OrderTypeSN === FORM_TRAINSBOOKING) }, - { dateRangeStr: dateDiffStr, data: res.ordercount2.find((row) => row.OrderTypeSN === FORM_TRAINSBOOKING) }, + // { dateRangeStr: dateDiffStr, data: res.ordercount2.find((row) => row.OrderTypeSN === FORM_TRAINSBOOKING) }, + ]; + const dateDiffStr = isEmpty(params.DateDiff1) ? '' : [params.DateDiff1, params.DateDiff2].map((d) => d.substring(0, 10)).join('~'); + if (!isEmpty(dateDiffStr)) { + ret.push({ dateRangeStr: dateDiffStr, data: res.ordercount2.find((row) => row.OrderTypeSN === FORM_TRAINSBOOKING) }); + } + return ret; +}; + +/** + * 从传统订单中获取upsell数据 + */ +export const fetchTrainsUpsellTSummaryByType = async (params) => { + const { errcode, errmsg, ...result } = await fetchJSON(HT_HOST + '/service-web/QueryData/GetOrderCountByType', { + ...defaultParams, + ...params, + COLI_ApplyDate1: params.Date1, + COLI_ApplyDate2: params.Date2, + COLI_ApplyDateOld1: params.DateDiff1 || '', + COLI_ApplyDateOld2: params.DateDiff2 || '', + OrderType: 'Form', + }); + const res = errcode !== 0 ? _res : result || _res; + const dateStr = [params.Date1, params.Date2].map((d) => d.substring(0, 10)).join('~'); + const ret = [ + { dateRangeStr: dateStr, data: res.ordercount1.find((row) => row.OrderTypeSN === FORM_TRAINSUPSELL) }, + // { dateRangeStr: dateDiffStr, data: res.ordercount2.find((row) => row.OrderTypeSN === FORM_TRAINSUPSELL) }, ]; + const dateDiffStr = isEmpty(params.DateDiff1) ? '' : [params.DateDiff1, params.DateDiff2].map((d) => d.substring(0, 10)).join('~'); + if (!isEmpty(dateDiffStr)) { + ret.push({ dateRangeStr: dateDiffStr, data: res.ordercount2.find((row) => row.OrderTypeSN === FORM_TRAINSUPSELL) }); + } return ret; }; +export const fetchTrainsWithUpsellSummary = async (params) => { + const [trains, upsell] = await Promise.all([ + fetchBizTrainsOrderSummaryByType(params), // todo: 换成servicetype=2 + fetchTrainsUpsellTSummaryByType(params) + ]); + if (!isEmpty(trains[1])) { + trains[0].data.diff = trains[1].data; + upsell[0].data.diff = upsell[1].data; + } + const mergedMap = [...trains, ...upsell].reduce((acc, currentItem, currentIndex) => { + const dateRange = currentItem.dateRangeStr; + + if (acc[dateRange]) { + acc[dateRange].data.push(currentItem.data); + + for (const key in currentItem) { + if (key !== 'dateRange' && key !== 'data') { + acc[dateRange][key] = currentItem[key]; + } + } + } else { + acc[dateRange] = { + ...currentItem, + data: [currentItem.data], + }; + } + + return acc; + }, {}); + return Object.values(mergedMap); + // return groupBy([...trains, ...upsell], 'dateRangeStr'); +}; /** * -------------------------------------------------------------------------------------------------------- @@ -50,6 +115,8 @@ const initialState = { DepartmentList: -1, // -1: All }, + trainsOrdersSummary: [], + activeTab: 'Form', activeDateGroupRadio: 'day', @@ -67,7 +134,6 @@ const initialState = { orderDetails: [], }; - const useTrainsStore = create( devtools( immer( @@ -98,18 +164,18 @@ const useTrainsStore = create( // set({ orderCountDataLinesSub: lines, avgLineValueSub: avgLineValue, activeDateGroupRadioSub: dateRadioValue }); }, - // site effects getBizOrderCount: async (params, type, typeVal) => { - const { setLoading, } = get(); + const { setLoading } = get(); setLoading(true); + set({ trainsOrdersSummary: [] }); try { - // const res = await fetchBizOrderCount(params, type, typeVal); + const res = await fetchTrainsWithUpsellSummary(params); // 第一次得到数据 // const { lines, dateRadioValue, avgLineValue } = resultDataCb(res, 'day', orderCountDataMapper, orderCountDataFieldMapper, calculateLineData); // if (isEmpty(type)) { // // index page - // set({ orderCountDataRaw: res }); + set({ trainsOrdersSummary: res }); // set({ orderCountDataLines: lines, avgLineValue, activeDateGroupRadio: dateRadioValue }); // } else { // // sub page @@ -142,7 +208,7 @@ const useTrainsStore = create( // sub getBizOrderDetailByType: async (params, type, typeVal) => { - const { setTypeLoading, } = get(); + const { setTypeLoading } = get(); try { setTypeLoading(true); // const res = await fetchBizOrderDetailByType(params, type, typeVal, 'detail'); From 046e48810ee5d50c892246af107c8bee78a6ab25 Mon Sep 17 00:00:00 2001 From: Lei OT Date: Wed, 19 Nov 2025 10:09:48 +0800 Subject: [PATCH 6/8] =?UTF-8?q?perf:=20=E5=95=86=E5=8A=A1:=20=E9=BB=98?= =?UTF-8?q?=E8=AE=A4=E5=8F=82=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.jsx | 5 ++- src/libs/ht.js | 5 +++ src/views/biz/BizOrder.jsx | 56 +++++++++++++++++++------- src/views/biz/reports/TrainsUpsell.jsx | 21 ++++++++-- src/zustand/Trains.js | 4 +- 5 files changed, 69 insertions(+), 22 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index ddbe4ca..fd78c86 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -10,7 +10,8 @@ import { CoffeeOutlined, DesktopOutlined, WhatsAppOutlined, ShoppingCartOutlined, - GiftOutlined + GiftOutlined, + RocketOutlined } from '@ant-design/icons'; import { Layout, Menu, Button } from 'antd'; import { BrowserRouter, Route, Routes, NavLink } from 'react-router-dom'; @@ -103,7 +104,7 @@ const App = () => { ], }, { - key: 20, label: '商旅市场', icon: , + key: 20, label: '商务市场', icon: , children: [ { key: 201, label: 订单数据 }, { key: 202, label: TrainsUpsell }, diff --git a/src/libs/ht.js b/src/libs/ht.js index a31a071..d584d37 100644 --- a/src/libs/ht.js +++ b/src/libs/ht.js @@ -61,6 +61,11 @@ export const groupsMappedByCode = groups.reduce((a, c) => ({ ...a, [String(c.cod export const groupsMappedByKey = groups.reduce((a, c) => ({ ...a, [String(c.key)]: c }), {}); export const leafGroup = groups.slice(3); export const overviewGroup = groups.slice(0, 3); // todo: 花梨鹰 APP Trippest +export const groupsCTplus = [ + // { value: '10,18', key: '10,18', label: 'CT商旅', code: 'CT+', children: [10, 18,] }; + { value: '18', key: '18', label: 'CT', code: 'CT', children: [] }, + { value: '10', key: '10', label: '商旅', code: '', children: [] }, +]; /** * 来源 */ diff --git a/src/views/biz/BizOrder.jsx b/src/views/biz/BizOrder.jsx index f3fd429..8c194a1 100644 --- a/src/views/biz/BizOrder.jsx +++ b/src/views/biz/BizOrder.jsx @@ -1,7 +1,7 @@ import { useContext } from 'react'; -import { Row, Col, Tabs, Table, Divider, Spin } from 'antd'; +import { Row, Col, Tabs, Table, Divider, Spin, Checkbox, Space } from 'antd'; import { ContainerOutlined, BlockOutlined, SmileOutlined, MobileOutlined, CustomerServiceOutlined } from '@ant-design/icons'; -import { Line } from '@ant-design/charts'; +import { Line, Pie } from '@ant-design/charts'; import { NavLink } from 'react-router-dom'; import * as comm from '../../utils/commons'; import DateGroupRadio from '../../components/DateGroupRadio'; @@ -94,7 +94,27 @@ const BizOrder = observer(() => { return ret; }, }, - smooth: true, + // smooth: true, + }; + const pieConfig = { + appendPadding: 10, + data: [], + angleField: 'OrderCount', + colorField: 'OrderType', + radius: 0.8, + label: { + type: 'outer', + content: '{name} {value} \n {percentage}', + }, + legend: false, // 不显示图例 + interactions: [ + { + type: 'element-selected', + }, + { + type: 'element-active', + }, + ], }; const tableProps = { @@ -111,9 +131,7 @@ const BizOrder = observer(() => { {showDiff ?
{result.ordercountTotal2?.groups}
: null} ), - titleX: - `${result.ordercountTotal1?.groups}` + - (showDiff ? ` vs ${result.ordercountTotal2?.groups}` : ''), + titleX: `${result.ordercountTotal1?.groups}` + (showDiff ? ` vs ${result.ordercountTotal2?.groups}` : ''), dataIndex: 'OrderType', fixed: 'left', render: (text, record) => {text}, @@ -395,14 +413,24 @@ const BizOrder = observer(() => { }; })} /> - -
- {/* */} - - - {/* */} - - + + + +
+

各项占比

+ {/* setIsShowEmpty(e.target.checked)} + > + 包含空值 + */} +
+ +
+ + + + diff --git a/src/views/biz/reports/TrainsUpsell.jsx b/src/views/biz/reports/TrainsUpsell.jsx index 7716df7..a22402a 100644 --- a/src/views/biz/reports/TrainsUpsell.jsx +++ b/src/views/biz/reports/TrainsUpsell.jsx @@ -16,18 +16,19 @@ const buildFunnelData = (data1, data2) => { const data01 = [ { stage: '火车票预定', number: data1?.data?.[0]?.OrderCount, dateRange: data1?.dateRangeStr }, { stage: '成行出票', number: data1?.data?.[0]?.CJCount, dateRange: data1?.dateRangeStr }, - { stage: '火车票upsell', number: (data1?.data?.[1]?.OrderCount || 0) + 0, dateRange: data1?.dateRangeStr }, - { stage: '成行', number: data1?.data?.[1]?.CJCount, dateRange: data1?.dateRangeStr }, + { stage: '火车票Upsell', number: (data1?.data?.[1]?.OrderCount || 0) + 0, dateRange: data1?.dateRangeStr }, + { stage: 'Upsell成行', number: data1?.data?.[1]?.CJCount, dateRange: data1?.dateRangeStr }, ]; const data02 = !comm.isEmpty(data2) ? [ { stage: '火车票预定', number: data2?.data?.[0]?.OrderCount, dateRange: data2?.dateRangeStr }, { stage: '成行出票', number: data2?.data?.[0]?.CJCount, dateRange: data2?.dateRangeStr }, - { stage: '火车票upsell', number: data2?.data?.[1]?.OrderCount, dateRange: data2?.dateRangeStr }, - { stage: '成行', number: data2?.data?.[1]?.CJCount, dateRange: data2?.dateRangeStr }, + { stage: '火车票Upsell', number: data2?.data?.[1]?.OrderCount, dateRange: data2?.dateRangeStr }, + { stage: 'Upsell成行', number: data2?.data?.[1]?.CJCount, dateRange: data2?.dateRangeStr }, ] : []; return data01.concat(data02); + // return data02.concat(data01); }; @@ -50,6 +51,18 @@ const TrainsUpsell = observer(({ ...props }) => { compareField: showDiff ? 'dateRange' : false, // isTransposed: true, legend: false, + // tooltip: { + // shared: true, + // showMarkers: false, + // showTitle: false, + // }, + label: { + position: 'right', + // offsetX: 10, + style: { + fill: '#002c45', + }, + }, shape: 'pyramid', theme: { colors10: [ diff --git a/src/zustand/Trains.js b/src/zustand/Trains.js index af8ae85..356b4f8 100644 --- a/src/zustand/Trains.js +++ b/src/zustand/Trains.js @@ -1,7 +1,7 @@ import { create } from 'zustand'; import { devtools } from 'zustand/middleware'; import { immer } from 'zustand/middleware/immer'; -import { groupsMappedByCode } from '../libs/ht'; +import { groupsCTplus, groupsMappedByCode } from '../libs/ht'; import { fetchJSON } from '../utils/request'; import { HT_HOST } from '../config'; import { resultDataCb } from '../components/DateGroupRadio/date'; @@ -106,7 +106,7 @@ const initialState = { DateType: { key: 'applyDate', label: '提交日期' }, WebCode: { key: 'all', label: '所有来源' }, IncludeTickets: { key: '1', label: '含门票' }, - DepartmentList: groupsMappedByCode.CTX, // { key: 'All', label: '所有来源' }, // + DepartmentList: groupsCTplus, // groupsMappedByCode.CTX, // { key: 'All', label: '所有来源' }, // }, searchValuesToSub: { DateType: 'applyDate', From 6de33fd75b060933e4b69353f360ad35915ddbd1 Mon Sep 17 00:00:00 2001 From: Lei OT Date: Fri, 21 Nov 2025 14:27:00 +0800 Subject: [PATCH 7/8] =?UTF-8?q?perf:=20=E5=95=86=E5=8A=A1=E6=95=B0?= =?UTF-8?q?=E6=8D=AE:=20=E7=81=AB=E8=BD=A6=E7=A5=A8Upsell=E4=BD=BF?= =?UTF-8?q?=E7=94=A8=20`=E6=9C=8D=E5=8A=A1=E7=B1=BB=E5=9E=8B`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/search/SearchForm.jsx | 5 + src/views/biz/BizOrder.jsx | 2 + src/views/biz/reports/TrainsUpsell.jsx | 102 +++++++++++--- src/zustand/BizOrder.js | 181 ++++++++++++------------- src/zustand/Trains.js | 149 +++++++------------- 5 files changed, 233 insertions(+), 206 deletions(-) diff --git a/src/components/search/SearchForm.jsx b/src/components/search/SearchForm.jsx index 232a3ad..3f94fcc 100644 --- a/src/components/search/SearchForm.jsx +++ b/src/components/search/SearchForm.jsx @@ -92,6 +92,11 @@ export default observer((props) => { transform: (value) => value?.key || '', default: '', }, + 'IncludeInternal': { + key: 'IncludeInternal', + transform: (value) => value?.key || '', + default: '', + }, 'operator': { key: 'operator', // transform: (value) => value?.key || '', diff --git a/src/views/biz/BizOrder.jsx b/src/views/biz/BizOrder.jsx index 8c194a1..f3b7078 100644 --- a/src/views/biz/BizOrder.jsx +++ b/src/views/biz/BizOrder.jsx @@ -425,6 +425,7 @@ const BizOrder = observer(() => { 包含空值 */} + @@ -433,6 +434,7 @@ const BizOrder = observer(() => { + ); diff --git a/src/views/biz/reports/TrainsUpsell.jsx b/src/views/biz/reports/TrainsUpsell.jsx index a22402a..47d61a3 100644 --- a/src/views/biz/reports/TrainsUpsell.jsx +++ b/src/views/biz/reports/TrainsUpsell.jsx @@ -1,6 +1,6 @@ import { useContext } from 'react'; -import { Row, Col, Table, Spin, Space } from 'antd'; -import { Funnel } from '@ant-design/charts'; +import { Row, Col, Table, Spin, Space, Divider } from 'antd'; +import { Funnel, Pie } from '@ant-design/charts'; import * as comm from '../../../utils/commons'; import SearchForm from '../../../components/search/SearchForm'; @@ -14,14 +14,14 @@ import useTrainsStore from '../../../zustand/Trains'; const buildFunnelData = (data1, data2) => { // const key = 'OrderCount'; const data01 = [ - { stage: '火车票预定', number: data1?.data?.[0]?.OrderCount, dateRange: data1?.dateRangeStr }, + { stage: '火车票服务', number: data1?.data?.[0]?.OrderCount, dateRange: data1?.dateRangeStr }, { stage: '成行出票', number: data1?.data?.[0]?.CJCount, dateRange: data1?.dateRangeStr }, { stage: '火车票Upsell', number: (data1?.data?.[1]?.OrderCount || 0) + 0, dateRange: data1?.dateRangeStr }, { stage: 'Upsell成行', number: data1?.data?.[1]?.CJCount, dateRange: data1?.dateRangeStr }, ]; const data02 = !comm.isEmpty(data2) ? [ - { stage: '火车票预定', number: data2?.data?.[0]?.OrderCount, dateRange: data2?.dateRangeStr }, + { stage: '火车票服务', number: data2?.data?.[0]?.OrderCount, dateRange: data2?.dateRangeStr }, { stage: '成行出票', number: data2?.data?.[0]?.CJCount, dateRange: data2?.dateRangeStr }, { stage: '火车票Upsell', number: data2?.data?.[1]?.OrderCount, dateRange: data2?.dateRangeStr }, { stage: 'Upsell成行', number: data2?.data?.[1]?.CJCount, dateRange: data2?.dateRangeStr }, @@ -31,6 +31,21 @@ const buildFunnelData = (data1, data2) => { // return data02.concat(data01); }; +const buildPieData = (data1, data2) => { + const data01 = !comm.isEmpty(data1) + ? [ + { stage: '火车票出票', number: Number((data1?.data?.[0]?.YJLY || '0').replaceAll(',', '')), dateRange: data1?.dateRangeStr }, + { stage: 'Upsell成行', number: Number((data1?.data?.[1]?.YJLY || '0').replaceAll(',', '')), dateRange: data1?.dateRangeStr }, + ] + : []; + const data02 = !comm.isEmpty(data2) + ? [ + { stage: '火车票出票', number: Number((data2?.data?.[0]?.YJLY || '0').replaceAll(',', '')), dateRange: data2?.dateRangeStr }, + { stage: 'Upsell成行', number: Number((data2?.data?.[1]?.YJLY || '0').replaceAll(',', '')), dateRange: data2?.dateRangeStr }, + ] + : []; + return data01.concat(data02); +}; const TrainsUpsell = observer(({ ...props }) => { const { date_picker_store: searchFormStore } = useContext(stores_Context); @@ -41,7 +56,7 @@ const TrainsUpsell = observer(({ ...props }) => { // const showDiff = !comm.isEmpty(searchValuesToSub.DateDiff2); const showDiff = !comm.isEmpty(compareData); - const getBizOrderCount = useTrainsStore((state) => state.getBizOrderCount); + const getTrainsWithUpsell = useTrainsStore((state) => state.getTrainsWithUpsell); const data0 = buildFunnelData(trainsOrdersSummary, compareData); const chartConfig = { @@ -67,17 +82,47 @@ const TrainsUpsell = observer(({ ...props }) => { theme: { colors10: [ // '#b5d8f3', - '#95bbda', + '#c4e1ff', // '#739ec1', - '#5a89ad', + '#7ebdff', // '#3d759b', - '#2f668a', + '#5b90f9', // '#1f5373', - '#0F405D', + '#526bd1', '#002c45', ], }, }; + + const pieData = buildPieData(trainsOrdersSummary); + const pieData2 = buildPieData(compareData); + // console.log('🟠', pieData, pieData2); + const pieConfig = { + appendPadding: 0, + // data: pieData, + angleField: 'number', + colorField: 'stage', + radius: 0.8, + innerRadius: 0.6, + // startAngle: Math.PI , + // endAngle: Math.PI * 1.5, + label: { + type: 'spider', + // content: '{name} {value} \n {percentage}', + content: '{name}\n{percentage}', + }, + statistic: false, + legend: false, // 不显示图例 + interactions: [ + { + type: 'element-selected', + }, + { + type: 'element-active', + }, + ], + }; + const tableProps = { title: () => `${trainsOrdersSummary?.dateRangeStr || ''}` + (showDiff ? ` vs ${compareData?.dateRangeStr}` : ''), dataSource: trainsOrdersSummary?.data || [], @@ -144,7 +189,7 @@ const TrainsUpsell = observer(({ ...props }) => { }} onSubmit={(_err, obj, form, str) => { setSearchValues(obj, form); - getBizOrderCount(obj); + getTrainsWithUpsell(obj); }} /> @@ -152,14 +197,39 @@ const TrainsUpsell = observer(({ ...props }) => {
- - - + + +

订单数量

- - - + + +

预计利润

+
+ + {showDiff && ( + + )} +
+ + + diff --git a/src/zustand/BizOrder.js b/src/zustand/BizOrder.js index 5a837f8..433a593 100644 --- a/src/zustand/BizOrder.js +++ b/src/zustand/BizOrder.js @@ -7,9 +7,9 @@ import { HT_HOST } from '../config'; import { resultDataCb } from '../components/DateGroupRadio/date'; import { isEmpty } from '../utils/commons'; -const defaultParams = { WebCode: 'all', IncludeTickets: 1, IncludeInternal: 1, }; +const defaultParams = { WebCode: 'all', IncludeTickets: 1, IncludeInternal: 1 }; -export const fetchBizOrderCount = async (params, type='', typeVal='') => { +export const fetchBizOrderCount = async (params, type = '', typeVal = '') => { const { errcode, errmsg, ...result } = await fetchJSON(HT_HOST + '/service-web/QueryData/GetOrderCount_biz', { ...defaultParams, ...params, @@ -62,7 +62,7 @@ export const fetchBizOrderCountByType = async (type, params) => { }; const _detailRes = { ordercount1: [], ordercount2: [] }; -export const fetchBizOrderDetailByType = async (params, type='', typeVal='', orderContent='detail') => { +export const fetchBizOrderDetailByType = async (params, type = '', typeVal = '', orderContent = 'detail') => { const { errcode, errmsg, ...result } = await fetchJSON(HT_HOST + '/service-web/QueryData/GetOrderCountByType_Sub_biz', { ...defaultParams, SubOrderType: orderContent, @@ -75,9 +75,12 @@ export const fetchBizOrderDetailByType = async (params, type='', typeVal='', ord OrderType_val: typeVal, }); const res = errcode !== 0 ? _detailRes : (result || _detailRes); - const dateStr = [params.Date1, params.Date2].map(d => d.substring(0, 10)).join('~'); - const dateDiffStr = isEmpty(params.DateDiff1) ? '' : [params.DateDiff1, params.DateDiff2].map(d => d.substring(0, 10)).join('~'); - const ret = [{ dateRangeStr: dateStr, data: res.ordercount1 }, { dateRangeStr: dateDiffStr, data: res.ordercount2 }]; + const dateStr = [params.Date1, params.Date2].map((d) => d.substring(0, 10)).join('~'); + const dateDiffStr = isEmpty(params.DateDiff1) ? '' : [params.DateDiff1, params.DateDiff2].map((d) => d.substring(0, 10)).join('~'); + const ret = [ + { dateRangeStr: dateStr, data: res.ordercount1 }, + { dateRangeStr: dateDiffStr, data: res.ordercount2 }, + ]; return ret; }; @@ -125,7 +128,7 @@ const initialState = { DepartmentList: -1, // -1: All }, - activeTab: 'Form', + activeTab: 'servicetype', activeDateGroupRadio: 'day', orderCountDataRaw: {}, @@ -142,94 +145,90 @@ const initialState = { orderDetails: [], }; - const useBizOrderStore = create( devtools( - immer( - (set, get) => ({ - ...initialState, - reset: () => set(initialState), - - setLoading: (loading) => set({ loading }), - setTypeLoading: (typeLoading) => set({ typeLoading }), - - setSearchValues: (obj, values) => set((state) => ({ searchValues: values, searchValuesToSub: obj })), - setSearchValuesToSub: (values) => set((state) => ({ searchValuesToSub: values })), - setActiveTab: (tab) => set({ activeTab: tab }), - - setOrderCountDataLines: (data) => set({ orderCountDataLines: data }), - setOrderCountDataByType: (type, data) => - set((state) => { - state.orderCountDataByType[type] = data; - }), - - // data ---- - onChangeDateGroup: (value, data, avg1) => { - const { lines, dateRadioValue, avgLineValue } = calculateLineData(value, data, avg1); - set({ orderCountDataLines: lines, avgLineValue, activeDateGroupRadio: dateRadioValue }); - }, - onChangeDateGroupSub: (value, data, avg1) => { - const { lines, dateRadioValue, avgLineValue } = calculateLineData(value, data, avg1); - set({ orderCountDataLinesSub: lines, avgLineValueSub: avgLineValue, activeDateGroupRadioSub: dateRadioValue }); - }, - - - // site effects - getBizOrderCount: async (params, type, typeVal) => { - const { setLoading, } = get(); - setLoading(true); - try { - const res = await fetchBizOrderCount(params, type, typeVal); - // 第一次得到数据 - const { lines, dateRadioValue, avgLineValue } = resultDataCb(res, 'day', orderCountDataMapper, orderCountDataFieldMapper, calculateLineData); - if (isEmpty(type)) { - // index page - set({ orderCountDataRaw: res }); - set({ orderCountDataLines: lines, avgLineValue, activeDateGroupRadio: dateRadioValue }); - } else { - // sub page - set({ orderCountDataRawSub: res }); - set({ orderCountDataLinesSub: lines, avgLineValueSub: avgLineValue, activeDateGroupRadioSub: dateRadioValue }); - } - } catch (error) { - } finally { - setLoading(false); + immer((set, get) => ({ + ...initialState, + reset: () => set(initialState), + + setLoading: (loading) => set({ loading }), + setTypeLoading: (typeLoading) => set({ typeLoading }), + + setSearchValues: (obj, values) => set((state) => ({ searchValues: values, searchValuesToSub: obj })), + setSearchValuesToSub: (values) => set((state) => ({ searchValuesToSub: values })), + setActiveTab: (tab) => set({ activeTab: tab }), + + setOrderCountDataLines: (data) => set({ orderCountDataLines: data }), + setOrderCountDataByType: (type, data) => + set((state) => { + state.orderCountDataByType[type] = data; + }), + + // data ---- + onChangeDateGroup: (value, data, avg1) => { + const { lines, dateRadioValue, avgLineValue } = calculateLineData(value, data, avg1); + set({ orderCountDataLines: lines, avgLineValue, activeDateGroupRadio: dateRadioValue }); + }, + onChangeDateGroupSub: (value, data, avg1) => { + const { lines, dateRadioValue, avgLineValue } = calculateLineData(value, data, avg1); + set({ orderCountDataLinesSub: lines, avgLineValueSub: avgLineValue, activeDateGroupRadioSub: dateRadioValue }); + }, + + // site effects + getBizOrderCount: async (params, type, typeVal) => { + const { setLoading } = get(); + setLoading(true); + try { + const res = await fetchBizOrderCount(params, type, typeVal); + // 第一次得到数据 + const { lines, dateRadioValue, avgLineValue } = resultDataCb(res, 'day', orderCountDataMapper, orderCountDataFieldMapper, calculateLineData); + if (isEmpty(type)) { + // index page + set({ orderCountDataRaw: res }); + set({ orderCountDataLines: lines, avgLineValue, activeDateGroupRadio: dateRadioValue }); + } else { + // sub page + set({ orderCountDataRawSub: res }); + set({ orderCountDataLinesSub: lines, avgLineValueSub: avgLineValue, activeDateGroupRadioSub: dateRadioValue }); } - }, - - getBizOrderCount_type: async (type) => { - const { setTypeLoading, searchValuesToSub, setOrderCountDataByType } = get(); + } catch (error) { + } finally { + setLoading(false); + } + }, + + getBizOrderCount_type: async (type) => { + const { setTypeLoading, searchValuesToSub, setOrderCountDataByType } = get(); + setTypeLoading(true); + try { + const res = await fetchBizOrderCountByType(type, searchValuesToSub); + setOrderCountDataByType(type, res); + } catch (error) { + } finally { + setTypeLoading(false); + } + }, + + onTabChange: async (tab) => { + const { setActiveTab, getBizOrderCount_type } = get(); + setActiveTab(tab); + await getBizOrderCount_type(tab); + }, + + // sub + getBizOrderDetailByType: async (params, type, typeVal) => { + const { setTypeLoading } = get(); + try { setTypeLoading(true); - try { - const res = await fetchBizOrderCountByType(type, searchValuesToSub); - setOrderCountDataByType(type, res); - } catch (error) { - } finally { - setTypeLoading(false); - } - }, - - onTabChange: async (tab) => { - const { setActiveTab, getBizOrderCount_type } = get(); - setActiveTab(tab); - await getBizOrderCount_type(tab); - }, - - // sub - getBizOrderDetailByType: async (params, type, typeVal) => { - const { setTypeLoading, } = get(); - try { - setTypeLoading(true); - const res = await fetchBizOrderDetailByType(params, type, typeVal, 'detail'); - set({ orderDetails: res }); - } catch (error) { - } finally { - setTypeLoading(false); - } - }, - }), - { name: 'bizOrder' } - ) + const res = await fetchBizOrderDetailByType(params, type, typeVal, 'detail'); + set({ orderDetails: res }); + } catch (error) { + } finally { + setTypeLoading(false); + } + }, + })), + { name: 'bizOrder' } ) ); export default useBizOrderStore; diff --git a/src/zustand/Trains.js b/src/zustand/Trains.js index 356b4f8..9c05fa1 100644 --- a/src/zustand/Trains.js +++ b/src/zustand/Trains.js @@ -7,14 +7,14 @@ import { HT_HOST } from '../config'; import { resultDataCb } from '../components/DateGroupRadio/date'; import { groupBy, isEmpty } from '../utils/commons'; -const SERVICETYPE_TRAINSBOOKING = 2; // 火车票服务 +const SERVICETYPE_TRAINSBOOKING = '2'; // 火车票服务 const FORM_TRAINSBOOKING = 32024; // 火车票预定 const FORM_TRAINSUPSELL = 32214; // 火车票Upsell const defaultParams = { WebCode: 'all', IncludeTickets: 1, IncludeInternal: 1 }; const _res = { ordercount1: [], ordercount2: [] }; -export const fetchBizTrainsOrderSummaryByType = async (params) => { +export const fetchBizTrainsOrderSummaryByType = async (params, type = 'Form', typeVal = '') => { const { errcode, errmsg, ...result } = await fetchJSON(HT_HOST + '/service-web/QueryData/GetOrderCountByType_biz', { ...defaultParams, ...params, @@ -22,17 +22,17 @@ export const fetchBizTrainsOrderSummaryByType = async (params) => { COLI_ApplyDate2: params.Date2, COLI_ApplyDateOld1: params.DateDiff1 || '', COLI_ApplyDateOld2: params.DateDiff2 || '', - OrderType: 'Form', + OrderType: type, }); const res = errcode !== 0 ? _res : result || _res; const dateStr = [params.Date1, params.Date2].map((d) => d.substring(0, 10)).join('~'); const ret = [ - { dateRangeStr: dateStr, data: res.ordercount1.find((row) => row.OrderTypeSN === FORM_TRAINSBOOKING) }, - // { dateRangeStr: dateDiffStr, data: res.ordercount2.find((row) => row.OrderTypeSN === FORM_TRAINSBOOKING) }, + { dateRangeStr: dateStr, data: res.ordercount1.find((row) => row.OrderTypeSN === typeVal) }, + // { dateRangeStr: dateDiffStr, data: res.ordercount2.find((row) => row.OrderTypeSN === typeVal) }, ]; const dateDiffStr = isEmpty(params.DateDiff1) ? '' : [params.DateDiff1, params.DateDiff2].map((d) => d.substring(0, 10)).join('~'); if (!isEmpty(dateDiffStr)) { - ret.push({ dateRangeStr: dateDiffStr, data: res.ordercount2.find((row) => row.OrderTypeSN === FORM_TRAINSBOOKING) }); + ret.push({ dateRangeStr: dateDiffStr, data: res.ordercount2.find((row) => row.OrderTypeSN === typeVal) }); } return ret; }; @@ -40,7 +40,7 @@ export const fetchBizTrainsOrderSummaryByType = async (params) => { /** * 从传统订单中获取upsell数据 */ -export const fetchTrainsUpsellTSummaryByType = async (params) => { +export const fetchTrainsUpsellTSummaryByType = async (params, type = 'Form', typeVal = '') => { const { errcode, errmsg, ...result } = await fetchJSON(HT_HOST + '/service-web/QueryData/GetOrderCountByType', { ...defaultParams, ...params, @@ -48,25 +48,26 @@ export const fetchTrainsUpsellTSummaryByType = async (params) => { COLI_ApplyDate2: params.Date2, COLI_ApplyDateOld1: params.DateDiff1 || '', COLI_ApplyDateOld2: params.DateDiff2 || '', - OrderType: 'Form', + OrderType: type, }); const res = errcode !== 0 ? _res : result || _res; const dateStr = [params.Date1, params.Date2].map((d) => d.substring(0, 10)).join('~'); const ret = [ - { dateRangeStr: dateStr, data: res.ordercount1.find((row) => row.OrderTypeSN === FORM_TRAINSUPSELL) }, - // { dateRangeStr: dateDiffStr, data: res.ordercount2.find((row) => row.OrderTypeSN === FORM_TRAINSUPSELL) }, + { dateRangeStr: dateStr, data: res.ordercount1.find((row) => row.OrderTypeSN === typeVal) }, + // { dateRangeStr: dateDiffStr, data: res.ordercount2.find((row) => row.OrderTypeSN === typeVal) }, ]; const dateDiffStr = isEmpty(params.DateDiff1) ? '' : [params.DateDiff1, params.DateDiff2].map((d) => d.substring(0, 10)).join('~'); if (!isEmpty(dateDiffStr)) { - ret.push({ dateRangeStr: dateDiffStr, data: res.ordercount2.find((row) => row.OrderTypeSN === FORM_TRAINSUPSELL) }); + ret.push({ dateRangeStr: dateDiffStr, data: res.ordercount2.find((row) => row.OrderTypeSN === typeVal) }); } return ret; }; export const fetchTrainsWithUpsellSummary = async (params) => { const [trains, upsell] = await Promise.all([ - fetchBizTrainsOrderSummaryByType(params), // todo: 换成servicetype=2 - fetchTrainsUpsellTSummaryByType(params) + // fetchBizTrainsOrderSummaryByType(params, 'Form', FORM_TRAINSBOOKING), // todo: 换成servicetype=2 + fetchBizTrainsOrderSummaryByType(params, 'servicetype', SERVICETYPE_TRAINSBOOKING), + fetchTrainsUpsellTSummaryByType(params, 'Form', FORM_TRAINSUPSELL), ]); if (!isEmpty(trains[1])) { trains[0].data.diff = trains[1].data; @@ -136,92 +137,42 @@ const initialState = { const useTrainsStore = create( devtools( - immer( - (set, get) => ({ - ...initialState, - reset: () => set(initialState), - - setLoading: (loading) => set({ loading }), - setTypeLoading: (typeLoading) => set({ typeLoading }), - - setSearchValues: (obj, values) => set((state) => ({ searchValues: values, searchValuesToSub: obj })), - setSearchValuesToSub: (values) => set((state) => ({ searchValuesToSub: values })), - setActiveTab: (tab) => set({ activeTab: tab }), - - setOrderCountDataLines: (data) => set({ orderCountDataLines: data }), - setOrderCountDataByType: (type, data) => - set((state) => { - state.orderCountDataByType[type] = data; - }), - - // data ---- - onChangeDateGroup: (value, data, avg1) => { - // const { lines, dateRadioValue, avgLineValue } = calculateLineData(value, data, avg1); - // set({ orderCountDataLines: lines, avgLineValue, activeDateGroupRadio: dateRadioValue }); - }, - onChangeDateGroupSub: (value, data, avg1) => { - // const { lines, dateRadioValue, avgLineValue } = calculateLineData(value, data, avg1); - // set({ orderCountDataLinesSub: lines, avgLineValueSub: avgLineValue, activeDateGroupRadioSub: dateRadioValue }); - }, - - // site effects - getBizOrderCount: async (params, type, typeVal) => { - const { setLoading } = get(); - setLoading(true); - set({ trainsOrdersSummary: [] }); - try { - const res = await fetchTrainsWithUpsellSummary(params); - // 第一次得到数据 - // const { lines, dateRadioValue, avgLineValue } = resultDataCb(res, 'day', orderCountDataMapper, orderCountDataFieldMapper, calculateLineData); - // if (isEmpty(type)) { - // // index page - set({ trainsOrdersSummary: res }); - // set({ orderCountDataLines: lines, avgLineValue, activeDateGroupRadio: dateRadioValue }); - // } else { - // // sub page - // set({ orderCountDataRawSub: res }); - // set({ orderCountDataLinesSub: lines, avgLineValueSub: avgLineValue, activeDateGroupRadioSub: dateRadioValue }); - // } - } catch (error) { - } finally { - setLoading(false); - } - }, - - getBizOrderCount_type: async (type) => { - const { setTypeLoading, searchValuesToSub, setOrderCountDataByType } = get(); - setTypeLoading(true); - try { - // const res = await fetchBizOrderCountByType(type, searchValuesToSub); - // setOrderCountDataByType(type, res); - } catch (error) { - } finally { - setTypeLoading(false); - } - }, - - onTabChange: async (tab) => { - const { setActiveTab, getBizOrderCount_type } = get(); - setActiveTab(tab); - await getBizOrderCount_type(tab); - }, - - // sub - getBizOrderDetailByType: async (params, type, typeVal) => { - const { setTypeLoading } = get(); - try { - setTypeLoading(true); - // const res = await fetchBizOrderDetailByType(params, type, typeVal, 'detail'); - // console.log('💥111', res); - // set({ orderDetails: res }); - } catch (error) { - } finally { - setTypeLoading(false); - } - }, - }), - { name: 'bizOrder' } - ) + immer((set, get) => ({ + ...initialState, + reset: () => set(initialState), + + setLoading: (loading) => set({ loading }), + setTypeLoading: (typeLoading) => set({ typeLoading }), + + setSearchValues: (obj, values) => set((state) => ({ searchValues: values, searchValuesToSub: obj })), + setSearchValuesToSub: (values) => set((state) => ({ searchValuesToSub: values })), + setActiveTab: (tab) => set({ activeTab: tab }), + + setOrderCountDataLines: (data) => set({ orderCountDataLines: data }), + setOrderCountDataByType: (type, data) => + set((state) => { + state.orderCountDataByType[type] = data; + }), + + // data ---- + + // site effects + getTrainsWithUpsell: async (params, type, typeVal) => { + const { setLoading } = get(); + setLoading(true); + set({ trainsOrdersSummary: [] }); + try { + const res = await fetchTrainsWithUpsellSummary(params); + set({ trainsOrdersSummary: res }); + } catch (error) { + } finally { + setLoading(false); + } + }, + + // sub + })), + { name: 'trains' } ) ); export default useTrainsStore; From 55e9b5d84392efaaaf5bafacfd8de49566f1a268 Mon Sep 17 00:00:00 2001 From: Lei OT Date: Fri, 21 Nov 2025 14:51:40 +0800 Subject: [PATCH 8/8] style: Orders Pie --- src/views/Orders.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/views/Orders.jsx b/src/views/Orders.jsx index c554671..8ec0ea8 100644 --- a/src/views/Orders.jsx +++ b/src/views/Orders.jsx @@ -327,8 +327,8 @@ class Orders extends Component { colorField: "OrderType", radius: 0.8, label: { - type: "outer", - content: "{name} {value} \n {percentage}", + type: "spider", + content: "{name} {value} \t {percentage}", }, legend: false, // 不显示图例 interactions: [