From a55a28c07ef3fa487e785f5be444fc0aaf69d9ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9C=E6=9E=97=E8=BD=A9=E2=80=9D?= <317035359@qq.com> Date: Wed, 18 Jun 2025 16:22:38 +0800 Subject: [PATCH 1/8] =?UTF-8?q?feat:=E5=A2=9E=E5=8A=A0=E5=A4=A7=E4=BB=AA?= =?UTF-8?q?=E5=95=86=E5=9F=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Common/StringDatePickers/RangePicker.tsx | 4 +- .../StringDatePickers/RangePickerMultiple.tsx | 155 ++++ .../Common/StringDatePickers/styles.less | 3 + .../DataPreview/session/portal/index.tsx | 3 + .../WorkForm/Design/form/formItem.tsx | 11 + .../WorkForm/Design/form/index.tsx | 1 + .../DataStandard/WorkForm/Utils/index.tsx | 4 + .../DataStandard/WorkForm/Viewer/formItem.tsx | 11 + src/config/column.tsx | 29 +- src/executor/design/index.tsx | 2 + .../open/form/detail/OrderView/index.tsx | 1 + src/executor/open/index.tsx | 9 +- .../components/dataDetails/index.module.less | 136 +++ .../components/dataDetails/index.tsx | 224 +++++ .../open/instrumentTemplate/index.module.less | 255 ++++++ .../open/instrumentTemplate/index.tsx | 27 + .../open/instrumentTemplate/pages/index.tsx | 799 ++++++++++++++++++ .../instrumentTemplate/pages/shoppingCar.tsx | 230 +++++ .../open/instrumentTemplate/widget/item.tsx | 62 ++ .../open/instrumentTemplate/widget/list.tsx | 130 +++ .../instrumentTemplate/widget/product.tsx | 268 ++++++ .../instrumentTemplate/widget/virtually.tsx | 130 +++ src/executor/open/mallTemplate/index.tsx | 1 - .../open/mallTemplate/pages/index.tsx | 2 +- .../open/mallTemplate/pages/shoppingCar.tsx | 16 +- .../operate/entityForm/templateForm.tsx | 10 + src/executor/operate/index.tsx | 1 + src/executor/tools/generate/columns.tsx | 42 + src/executor/tools/generate/thingTable.tsx | 5 +- src/executor/tools/workForm/detail.tsx | 81 +- src/pages/Home/index.tsx | 4 + src/pages/View/index.tsx | 4 + src/ts/core/mall/order/order.ts | 38 +- src/ts/core/public/consts.ts | 1 + src/ts/core/public/enums.ts | 3 + src/ts/core/thing/resource.ts | 1 + src/ts/core/thing/standard/form.ts | 11 + src/ts/core/thing/standard/index.ts | 5 + 38 files changed, 2695 insertions(+), 24 deletions(-) create mode 100644 src/components/Common/StringDatePickers/RangePickerMultiple.tsx create mode 100644 src/components/Common/StringDatePickers/styles.less create mode 100644 src/executor/open/instrumentTemplate/components/dataDetails/index.module.less create mode 100644 src/executor/open/instrumentTemplate/components/dataDetails/index.tsx create mode 100644 src/executor/open/instrumentTemplate/index.module.less create mode 100644 src/executor/open/instrumentTemplate/index.tsx create mode 100644 src/executor/open/instrumentTemplate/pages/index.tsx create mode 100644 src/executor/open/instrumentTemplate/pages/shoppingCar.tsx create mode 100644 src/executor/open/instrumentTemplate/widget/item.tsx create mode 100644 src/executor/open/instrumentTemplate/widget/list.tsx create mode 100644 src/executor/open/instrumentTemplate/widget/product.tsx create mode 100644 src/executor/open/instrumentTemplate/widget/virtually.tsx diff --git a/src/components/Common/StringDatePickers/RangePicker.tsx b/src/components/Common/StringDatePickers/RangePicker.tsx index eca23dfc4..0cad40eb5 100644 --- a/src/components/Common/StringDatePickers/RangePicker.tsx +++ b/src/components/Common/StringDatePickers/RangePicker.tsx @@ -3,7 +3,7 @@ import { DatePicker } from 'antd'; import moment, { Moment } from 'moment'; import { RangePickerProps as AntdRangePickerProps } from 'antd/lib/date-picker/generatePicker'; import { omit } from 'lodash'; - +import './styles.less'; type ValueType = [Moment | null, Moment | null]; type StringValueType = [string, string]; @@ -14,6 +14,7 @@ export type RangePickerProps = Omit< format?: string; value?: StringValueType; onChange?: (value: StringValueType) => void; + showTime?: boolean; }; /** @@ -49,6 +50,7 @@ export function RangePicker(props: RangePickerProps) { {...omit(props, ['value', 'onChange'])} value={dateRange} onChange={valueChange} + dropdownClassName="custom-date-picker-dropdown" /> ); } diff --git a/src/components/Common/StringDatePickers/RangePickerMultiple.tsx b/src/components/Common/StringDatePickers/RangePickerMultiple.tsx new file mode 100644 index 000000000..993aa0cc5 --- /dev/null +++ b/src/components/Common/StringDatePickers/RangePickerMultiple.tsx @@ -0,0 +1,155 @@ +import React, { useEffect, useState } from 'react'; +import { DropDownBox, List } from 'devextreme-react'; +import moment from 'moment'; +import { RangePicker } from './RangePicker'; +import message from '@/utils/message'; + +interface TimeRange { + startTime: string; + endTime: string; + [key: string]: any; // 允许添加其他属性 +} + +export type RangePickerProps = { + value?: TimeRange[]; + onChange?: (value: TimeRange[]) => void; + format?: string; + displayFormat?: string; + className?: string; + label?: string; + width?: string; +}; + +/** + * 支持多选的时间段选择器组件 + */ +export function RangePickerMultiple(props: RangePickerProps) { + const [selectedRanges, setSelectedRanges] = useState([]); + const [isDropDownVisible, setIsDropDownVisible] = useState(false); + + useEffect(() => { + if (props.value) { + setSelectedRanges(props.value); + } + }, [props.value]); + + // 检查时间段是否有重叠 + const hasOverlap = (start: string, end: string, excludeIndex?: number): boolean => { + return selectedRanges.some((range, index) => { + if (excludeIndex !== undefined && index === excludeIndex) { + return false; + } + const rangeStart = moment(range.startTime); + const rangeEnd = moment(range.endTime); + const newStart = moment(start); + const newEnd = moment(end); + + // 检查是否有重叠 + return ( + (newStart.isSameOrAfter(rangeStart) && newStart.isBefore(rangeEnd)) || + (newEnd.isAfter(rangeStart) && newEnd.isSameOrBefore(rangeEnd)) || + (newStart.isSameOrBefore(rangeStart) && newEnd.isSameOrAfter(rangeEnd)) + ); + }); + }; + + const handleDateChange = (value: [string, string]) => { + if (value && value[0] && value[1]) { + const [start, end] = value; + + // 检查新时间段是否与现有时间段重叠 + if (hasOverlap(start, end)) { + // 可以在这里添加提示信息 + message.error('时间段重叠,请选择其他时间段'); + return; + } + + const newRange: TimeRange = { + startTime: start, + endTime: end, + isSelected: false, + }; + const newRanges = [...selectedRanges, newRange]; + setSelectedRanges(newRanges); + props.onChange?.(newRanges); + } + }; + + const handleRemoveRange = (index: number) => { + const newRanges = selectedRanges.filter((_, i) => i !== index); + setSelectedRanges(newRanges); + props.onChange?.(newRanges); + }; + + const dropDownContent = () => { + return ( +
e.stopPropagation()} + > +
e.stopPropagation()} + > +
e.stopPropagation()} + onMouseDown={(e) => e.stopPropagation()} + onMouseUp={(e) => e.stopPropagation()} + > + { + if (!open) { + setTimeout(() => { + setIsDropDownVisible(true); + }, 0); + } + }} + /> +
+
+ ( +
+ + {moment(range.startTime).format(props.displayFormat || 'YYYY-MM-DD HH:mm:ss')} + {' ~ '} + {moment(range.endTime).format(props.displayFormat || 'YYYY-MM-DD HH:mm:ss')} + + +
+ )} + /> +
+ ); + }; + + return ( + { + if (e.name === 'opened') { + setIsDropDownVisible(e.value); + } + }} + contentRender={dropDownContent} + value={selectedRanges} + valueExpr="value" + displayValueFormatter={() => `已选择 ${selectedRanges.length} 个时间段`} + /> + ); +} \ No newline at end of file diff --git a/src/components/Common/StringDatePickers/styles.less b/src/components/Common/StringDatePickers/styles.less new file mode 100644 index 000000000..3eaf5bcd6 --- /dev/null +++ b/src/components/Common/StringDatePickers/styles.less @@ -0,0 +1,3 @@ +.custom-date-picker-dropdown { + z-index: 9999 !important; + } \ No newline at end of file diff --git a/src/components/DataPreview/session/portal/index.tsx b/src/components/DataPreview/session/portal/index.tsx index 89c4e5577..10b49c782 100644 --- a/src/components/DataPreview/session/portal/index.tsx +++ b/src/components/DataPreview/session/portal/index.tsx @@ -10,6 +10,7 @@ import { DashboardTemplateHomeView } from '@/executor/open/dashboardTemplate'; import LedgerForm from '@/executor/open/view/ledger/LedgerForm'; import { TotalView } from '@/ts/core/thing/standard/view/totalView'; import { MallTemplate } from '@/executor/open/mallTemplate/pages'; +import { InstrumentTemplate } from '@/executor/open/instrumentTemplate/pages'; import { IMallTemplate } from '@/ts/core/thing/standard/page/mallTemplate'; import { IElementHost } from '@/ts/element/standard'; @@ -61,6 +62,8 @@ const PortalPage: React.FC = ({ target, space }) => { } case '商城模板': return ; + case '商城模板-大仪': + return ; default: return <>暂不支持; } diff --git a/src/components/DataStandard/WorkForm/Design/form/formItem.tsx b/src/components/DataStandard/WorkForm/Design/form/formItem.tsx index ceb004659..06d79c243 100644 --- a/src/components/DataStandard/WorkForm/Design/form/formItem.tsx +++ b/src/components/DataStandard/WorkForm/Design/form/formItem.tsx @@ -23,6 +23,7 @@ import React, { useEffect, useState, createRef, useCallback } from 'react'; import { DropDownBoxTypes } from 'devextreme-react/drop-down-box'; import styles from './index.module.less'; import MapEditItem from "@/components/DataStandard/WorkForm/Design/form/customItem/mapItem"; +import { RangePickerMultiple } from '@/components/Common/StringDatePickers/RangePickerMultiple'; export const FormItem: React.FC<{ current: IForm; @@ -176,6 +177,16 @@ export const FormItem: React.FC<{ return ; case '时间选择框': return ; + case '时间段选择框': + return ( + { + mixOptions.onValueChanged?.({ value }); + }} + /> + ); case '文件选择框': return ; case '地图选择框': diff --git a/src/components/DataStandard/WorkForm/Design/form/index.tsx b/src/components/DataStandard/WorkForm/Design/form/index.tsx index e364029ff..5fff2dbc8 100644 --- a/src/components/DataStandard/WorkForm/Design/form/index.tsx +++ b/src/components/DataStandard/WorkForm/Design/form/index.tsx @@ -27,6 +27,7 @@ const FormRender: React.FC<{ ); const [options, setOptions] = useState([ '实体商品', + '大仪商品', '虚拟商品', '报表数据', '办事数据', diff --git a/src/components/DataStandard/WorkForm/Utils/index.tsx b/src/components/DataStandard/WorkForm/Utils/index.tsx index 4b6789b55..5c11fbd93 100644 --- a/src/components/DataStandard/WorkForm/Utils/index.tsx +++ b/src/components/DataStandard/WorkForm/Utils/index.tsx @@ -18,6 +18,8 @@ export const getWidget = (valueType?: string, widget?: string) => { return '日期选择框'; case '时间型': return '时间选择框'; + case '时间段型': + return '时间段选择框'; case '用户型': return '人员搜索框'; case '附件型': @@ -45,6 +47,8 @@ export const loadwidgetOptions = (attribute: schema.XAttribute) => { return ['日期选择框']; case '时间型': return ['时间选择框']; + case '时间段型': + return ['时间段选择框']; case '用户型': return [ '操作人', diff --git a/src/components/DataStandard/WorkForm/Viewer/formItem.tsx b/src/components/DataStandard/WorkForm/Viewer/formItem.tsx index 2301861fa..c69201ff4 100644 --- a/src/components/DataStandard/WorkForm/Viewer/formItem.tsx +++ b/src/components/DataStandard/WorkForm/Viewer/formItem.tsx @@ -22,6 +22,7 @@ import { DisplayType } from '@/utils/work'; import TreeSelect from './customItem/treeSelect'; import { isNumber } from 'lodash'; import MapEditItem from '@/components/DataStandard/WorkForm/Design/form/customItem/mapItem'; +import { RangePickerMultiple } from '@/components/Common/StringDatePickers/RangePickerMultiple'; interface IFormItemProps { data: any; @@ -291,6 +292,16 @@ const FormItem: React.FC = (props) => { }} /> ); + case '时间段选择框': + return ( + { + mixOptions.onValueChanged?.({ value }); + }} + /> + ); case '文件选择框': return ; case '地图选择框': diff --git a/src/config/column.tsx b/src/config/column.tsx index b59fe89f5..4dbd37d89 100644 --- a/src/config/column.tsx +++ b/src/config/column.tsx @@ -285,6 +285,8 @@ export const FullProperties = (typeName: string) => { return ProductProperties(); case '实体商品': return PhysicalProperties(ProductProperties()); + case '大仪商品': + return InstrumentProperties(ProductProperties()); case '报表数据': return ReportProperties(); case '办事数据': @@ -732,7 +734,32 @@ export const PhysicalProperties = (props: schema.XProperty[]) => { ...props, ] as schema.XProperty[]; }; - +export const InstrumentProperties = (props: schema.XProperty[]) => { + return [ + { + id: 'images', + name: '缩略图', + code: 'images', + valueType: '附件型', + remark: '缩略图', + }, + { + id: 'count', + name: '实体商品数量', + code: 'count', + valueType: '数值型', + remark: '实体商品数量', + }, + { + id: 'timeArray', + name: '时间段', + code: 'timeArray', + valueType: '时间段型', + remark: '时间段', + }, + ...props, + ] as schema.XProperty[]; +}; /** 商品属性 */ export const ProductProperties = () => { return [ diff --git a/src/executor/design/index.tsx b/src/executor/design/index.tsx index 078462fdb..37e5fd3b3 100644 --- a/src/executor/design/index.tsx +++ b/src/executor/design/index.tsx @@ -46,6 +46,8 @@ const OperateModal: React.FC = ({ cmd, entity, finished }) => { return ; case '商城模板': return ; + case '商城模板-大仪': + return ; case '空间模板': return ; case '文档模板': diff --git a/src/executor/open/form/detail/OrderView/index.tsx b/src/executor/open/form/detail/OrderView/index.tsx index 783269eda..e77c8b563 100644 --- a/src/executor/open/form/detail/OrderView/index.tsx +++ b/src/executor/open/form/detail/OrderView/index.tsx @@ -110,6 +110,7 @@ const OrderView: React.FC<{ fields={resultFields} dataSource={itemList} remoteOperations={true} + orderNumber={props.thingData.orderNumber} onExporting={(e) => exportToExcel( e, diff --git a/src/executor/open/index.tsx b/src/executor/open/index.tsx index 4a21817f1..62c0f1917 100644 --- a/src/executor/open/index.tsx +++ b/src/executor/open/index.tsx @@ -16,6 +16,7 @@ import JoinApply from './task/joinApply'; import { model, schema } from '@/ts/base'; import TemplateView from './page'; import MallTemplateView from './mallTemplate'; +import InstrumentView from './instrumentTemplate'; import ThingPreview from './thing'; import { PreviewDialog } from '@/components/DataPreview'; import PropertyModal from './property'; @@ -157,10 +158,10 @@ const ExecutorOpen: React.FC = (props: IOpenProps) => { return ; case '页面模板': return ; - case '商城模板': - return ( - - ); + case '商城模板': + return + case '商城模板-大仪': + return case '空间模板': return ( diff --git a/src/executor/open/instrumentTemplate/components/dataDetails/index.module.less b/src/executor/open/instrumentTemplate/components/dataDetails/index.module.less new file mode 100644 index 000000000..110e44571 --- /dev/null +++ b/src/executor/open/instrumentTemplate/components/dataDetails/index.module.less @@ -0,0 +1,136 @@ +.header { + min-height: 72px; + display: flex; + margin-bottom: 24px; + .productImg { + border-radius: 12px; + width: 72px; + height: 72px; + margin-right: 12px; + } + .carousel { + border-radius: 12px; + width: 200px; + height: 200px; + } + .left { + width: 100%; + display: flex; + justify-content: space-between; + margin-left: 12px; + .info { + width: 305px; + .title { + font-size: 18px; + color: #15181d; + margin-bottom: 10px; + } + .introduce { + font-size: 12px; + font-weight: 400; + color: #424a57; + } + .buyerFooter { + color: #6f7686; + font-size: 10px; + display: flex; + align-items: baseline; + .price { + font-size: 18px; + color: #366ef4; + margin-right: 4px; + } + } + } + .controls { + display: flex; + align-items: center; + font-size: 14px; + text-align: center; + & > div { + padding: 8px 16px; + border-radius: 5px; + cursor: pointer; + } + .purchaseNow { + background-color: #366ef4; + color: #fff; + margin-right: 10px; + } + .purchaseCar { + background-color: #e7e8eb; + color: #15181d; + } + } + } +} +.productImage { + width: 100%; + display: flex; + justify-content: space-between; + :global(.ogo-image) { + width: 100%; + } + .image { + width: 100%; + border-radius: 12px; + border: none; + } + margin-bottom: 24px; +} +.productDescription { + font-size: 12px; + font-weight: 400; + color: #424a57; +} +.footer { + display: flex; + justify-content: center; + margin-top: 24px; + :global(.ogo-image) { + width: 100%; + } +} +.evaluate { + font-size: 14px; + font-weight: 400; + color: #424a57; + margin-bottom: 12px; + .evaluateInfo { + width: 400px; + display: flex; + justify-content: space-between; + align-items: baseline; + .evaluationScore { + font-size: 50px; + color: #15181d; + } + } + .evaluateItem { + display: flex; + flex-direction: column; + align-items: flex-start; + padding: 12px; + font-size: 12px; + border-radius: 12px; + background-color: #f7f8fa; + color: #424a57; + margin-top: 12px; + font-weight: 400; + .evaluateTitle { + font-weight: 500; + } + .evaluateFooter { + width: 100%; + display: flex; + justify-content: space-between; + color: #6f7686; + } + } +} +.productFormData { + width: 100%; + display: flex; + gap: 10px; + flex-wrap: wrap; +} \ No newline at end of file diff --git a/src/executor/open/instrumentTemplate/components/dataDetails/index.tsx b/src/executor/open/instrumentTemplate/components/dataDetails/index.tsx new file mode 100644 index 000000000..347aae852 --- /dev/null +++ b/src/executor/open/instrumentTemplate/components/dataDetails/index.tsx @@ -0,0 +1,224 @@ +import React, { memo, useCallback, useEffect, useState } from 'react'; +import { Modal, Image, Tabs, Carousel, Skeleton,Select } from 'antd'; +import type { schema, model } from '@/ts/base'; +import { IMallTemplate } from '@/ts/core/thing/standard/page/mallTemplate'; +import cls from './index.module.less'; +import { IForm, IWork } from '@/ts/core'; +import { getDefaultImg } from '@/utils/tools'; +import FormItem from '@/components/DataStandard/WorkForm/Viewer/formItem'; + +interface IDataDetails { + data: schema.XProduct; + current: IMallTemplate; + form?: IForm; + works?: IWork[]; + onCancel: () => void; + onAddCar: () => void; + onPurchase: (selectedRows: schema.XProduct[], work: IWork) => void; +} + +const InstrumentDataDetails = ({ + current, + data, + form, + works, + onCancel, + onAddCar, + onPurchase, +}: IDataDetails) => { + const [selectedTimes, setSelectedTimes] = useState([]); + const getImage = useCallback((type: string, num?: number) => { + let images: model.FileItemShare[] = []; + images = JSON.parse(data[type] || '[]'); + if (!images.length && form) { + const species = form.fields.flatMap((item) => + item.options?.species ? item ?? [] : [], + ); + images.push({ + shareLink: getDefaultImg(data, species), + } as any); + } + return num ? images.splice(0, num) : images; + }, []); + useEffect(()=>{ + const thisSelectedTimes =data.timeArray?.filter(item=>item.thisSelected)?.map(item=>item.startTime) + setSelectedTimes(thisSelectedTimes) + },[]) + const tabs = [ + { + key: '1', + label: '商品概括', + children: ( + <> +
+ {form?.fields.map((field) => { + if(field.valueType=='时间段型')return false + return ( + + ); + })} +
+
+ {getImage('introduceImage', 2).map((item, index) => { + return ( + } + /> + ); + })} +
+
{data.introduceInfo}
+ + ), + }, + { + key: '2', + label: '功能介绍', + children: ( + <> +
{data.useInfo}
+
+ {!!getImage('useInfoImage', 1).length && ( + } + className={cls.featureImage} + /> + )} +
+ + ), + }, + { + key: '3', + label: '预约时间', + children: ( + <> + { + data.timeArray?.forEach(item=>{ + item.thisSelected=values.includes(item.startTime) + }) + setSelectedTimes(values) + }} + options={data.timeArray?.map((time) => ({ + label: `${time.startTime.substring(0, 19)} - ${time.endTime.substring(0, 19)}`, + value: time.startTime, + disabled: time.isSelected + }))} + /> +
+ 加入购物车 +
+ + + + + + ); +}; + +export default memo(InstrumentDataDetails); diff --git a/src/executor/open/instrumentTemplate/index.module.less b/src/executor/open/instrumentTemplate/index.module.less new file mode 100644 index 000000000..1a09dff92 --- /dev/null +++ b/src/executor/open/instrumentTemplate/index.module.less @@ -0,0 +1,255 @@ +.cardInfo { + width: 100%; + padding: 16px 24px; + background-color: #fff; + .header { + width: 100%; + display: flex; + justify-content: space-between; + align-items: center; + height: 40px; + .title { + height: 20px; + line-height: 20px; + color: #15181d; + border-left: 4px #366ef4 solid; + font-size: 16px; + font-weight: 500; + padding-left: 8px; + } + } +} +.space { + width: 100%; + :global(.ogo-space-item) { + width: 100%; + } +} + +.pagination { + margin-top: 16px; + display: flex; + justify-content: center; + align-items: center; +} + +.listItem { + border-radius: 8px; + font-weight: 500; + margin-top: 16px; + width: 245px; + .productImg { + display: flex; + justify-content: center; + width: 100%; + height: 240px; + line-height: 24px; + color: #15181d; + font-size: 14px; + .ant-skeleton-image { + width: 100%; + } + } + .productInfo { + display: flex; + flex-direction: column; + justify-content: space-between; + font-size: 12px; + color: #6f7686; + font-weight: 400; + background-color: #fff; + margin-top: 4px; + .title { + color: #15181d; + font-size: 13px; + font-weight: 500; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + .belong { + display: flex; + align-items: center; + .belongName { + flex: 1; + color: #154ad8; + margin-left: 6px; + font-size: 14px; + font-weight: 500; + } + } + .introduce { + font-size: 12px; + color: #424a57; + font-weight: 500; + } + .provider { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + } + .detail { + cursor: pointer; + width: 100%; + } + + .footer { + width: 100%; + height: 40px; + display: flex; + justify-content: space-between; + align-items: center; + .purchaseInfo { + .price { + color: #15181d; + font-size: 16px; + } + .buyersNum { + font-size: 10px; + color: #6f7686; + margin-left: 8px; + } + } + .purchaseControls { + min-width: 100px; + color: #366ef4; + display: flex; + align-items: center; + font-size: 12px; + .purchaseNow { + height: 22px; + display: flex; + align-items: center; + &:hover { + cursor: pointer; + } + } + .purchaseCar { + cursor: pointer; + margin-left: 20px; + } + .purchase { + margin-left: 4px; + } + } + } +} + +.physical { + padding: 0px 20px; + height: calc(100vh - 80px) !important; + overflow-y: auto; + .content { + width: 100%; + display: flex; + margin-top: 12px; + .tab { + width: 284px; + padding-right: 24px; + } + .productList { + flex: 1; + flex-wrap: wrap; + } + } + .extra { + height: 20px; + display: flex; + align-items: center; + line-height: 20px; + font-size: 14px; + font-weight: 400; + &:hover { + color: #366ef4; + cursor: pointer; + } + } +} + +.rightCar { + height: 100%; + display: flex; + flex-direction: column; + .products { + flex: 1; + overflow: scroll; + :global(.ogo-pro-list-row-header) { + flex: 0 1 auto; + } + } + .bottomSum { + display: flex; + justify-content: space-between; + position: relative; + bottom: 0; + > * { + flex: 1; + text-align: center; + } + .buttons { + flex: 2; + display: flex; + justify-content: space-between; + gap: 10px; + > * { + flex: 1; + } + } + } +} +.carItem { + display: flex; + justify-content: space-between; + align-items: center; + margin-left: 16px; + .carItemInfo { + display: flex; + width: 100%; + margin-top: 12px; + .image { + display: flex; + justify-content: center; + width: 80px; + height: 80px; + line-height: 24px; + color: #15181d; + font-size: 14px; + .ant-skeleton-image { + width: 100%; + } + } + .content { + margin-left: 10px; + display: flex; + flex-direction: column; + .title { + display: flex; + justify-content: space-between; + font-weight: bold; + } + .tag { + margin-left: 10px; + } + .remark { + margin-top: 4px; + font-size: 12px; + color: #6f7686; + } + .amount { + font-weight: bold; + } + } + } +} +.mallList { + position: relative; + width: 0; + flex: 1; + .extraButton { + position: absolute; + top: 5px; + left: 0px; + z-index: 1; + } +} diff --git a/src/executor/open/instrumentTemplate/index.tsx b/src/executor/open/instrumentTemplate/index.tsx new file mode 100644 index 000000000..b81f8372f --- /dev/null +++ b/src/executor/open/instrumentTemplate/index.tsx @@ -0,0 +1,27 @@ +import FullScreenModal from '@/components/Common/fullScreen'; +import { IMallTemplate } from '@/ts/core/thing/standard/page/mallTemplate'; +import React from 'react'; +import { InstrumentTemplate } from './pages'; + +interface IProps { + current: IMallTemplate; + finished: () => void; +} + +const InstrumentView: React.FC = ({ current, finished }) => { + return ( + finished()}> + + + ); +}; + +export default InstrumentView; diff --git a/src/executor/open/instrumentTemplate/pages/index.tsx b/src/executor/open/instrumentTemplate/pages/index.tsx new file mode 100644 index 000000000..03fe9bfeb --- /dev/null +++ b/src/executor/open/instrumentTemplate/pages/index.tsx @@ -0,0 +1,799 @@ +import OrgIcons from '@/components/Common/GlobalComps/orgIcons'; +import Banner from '@/components/DataPreview/session/WorkBench/components/Banner/Common/BannerImg'; +import { LoadBanner } from '@/components/DataPreview/session/WorkBench/components/Banner/Common/bannerDefaultConfig'; +import { kernel, schema } from '@/ts/base'; +import { clicksCount } from '@/ts/base/schema'; +import { IForm, IWork } from '@/ts/core'; +import { TemplateType } from '@/ts/core/public/enums'; +import { Form } from '@/ts/core/thing/standard/form'; +import { IMallTemplate } from '@/ts/core/thing/standard/page/mallTemplate'; +import { SearchOutlined, ShoppingCartOutlined } from '@ant-design/icons'; +import { + Badge, + Col, + Drawer, + Empty, + Input, + Layout, + message, + Pagination, + Row, + Segmented, + Space, + Spin, +} from 'antd'; +import { Content, Header } from 'antd/lib/layout/layout'; +import { ScrollView, TreeView } from 'devextreme-react'; +import { cloneDeep } from 'lodash'; +import React, { ReactNode, useEffect, useRef, useState } from 'react'; +import WorkStartDo from '../../work'; +import InstrumentDataDetails from '../components/dataDetails'; +import List from '../widget/list'; +import { Product } from '../widget/product'; +import { Virtually } from '../widget/virtually'; +import cls from './../index.module.less'; +import { RightCar } from './shoppingCar'; +interface IProps { + current: IMallTemplate; +} + +export const InstrumentTemplate: React.FC = ({ current }) => { + const [center, setCenter] = useState(<>); + const [works, setWorks] = useState(); + const [form, setForm] = useState(); + const [loading, setLoading] = useState(true); + // 加入购物车 + const onAddCar = async (product: schema.XProduct, staging: boolean) => { + if (product.isMultiple) { + let num = + current.shoppingCar.products.find((i) => i.id === product.id)?.carCount || 0; + num++; + if (!num) return; + if (num > (product.count || 0)) { + message.error('库存不足'); + return; + } + const res = await current.shoppingCar.create({ + ...product, + carCount: num, + }); + if (res) { + message.success('已加入购物车'); + } else { + message.error('加入购物车失败'); + } + } else { + if (staging) { + const res = await current.shoppingCar.remove(product); + if (res) { + message.success('已删除购物车商品'); + } else { + message.error('删除购物车商品失败'); + } + } else { + const res = await current.shoppingCar.create(product); + if (res) { + message.success('已加入购物车'); + } else { + message.error('加入购物车失败'); + } + } + } + }; + + // 批量操作购物车 + const onBatchAddCar = async (products: schema.XProduct[], staging: boolean = false) => { + let _products = cloneDeep(products); + // 处理可选择多数量商品 + _products = _products.filter((product) => { + if (product.isMultiple) { + let num = + current.shoppingCar.products.find((i) => i.id === product.id)?.carCount || 0; + num++; + if (num < (product.count || 0)) { + current.shoppingCar.create({ + ...product, + carCount: num, + }); + return; + } + message.error('库存不足'); + } + return product; + }); + if (staging) { + const res = await current.shoppingCar.batchRemove(_products); + if (res) { + message.success('已删除购物车商品'); + } else { + message.error('删除购物车商品失败'); + } + } else { + const res = await current.shoppingCar.batchCreate(_products); + if (res) { + message.success('已加入购物车'); + } else { + message.error('加入购物车失败'); + } + } + }; + // 结算 + const onPurchase = async (selectedRows: schema.XProduct[], work: IWork) => { + if (!selectedRows.length) { + message.error('请先选择商品'); + return; + } + const node = await work?.loadNode(); + if (work && node) { + const rows = selectedRows.map((item) => { + return { + ...item, + orderCount: item.carCount || (item.isMultiple && 1), + }; + }); + const instance = await work.applyData(node, rows); + setCenter( + { + if (success) { + await current.shoppingCar.batchRemove(selectedRows); + } + setCenter(<>); + }} + />, + ); + } + return; + }; + + // 加载办事 + const loadContent = async () => { + setLoading(true); + setWorks(await current.findWork()); + setForm(await current.loadForm()); + setLoading(false); + }; + + useEffect(() => { + loadContent(); + }, [current]); + + if (!current.params?.form) { + return ( + + 未绑定商品表单 + + ); + } + if (!form) { + if (!loading) { + return ( + + 未绑定商品表单 + + ); + } else { + return } description="">; + } + } + return ( + +
+ +
+ + + {center} +
+ ); +}; + +interface IHotGroup { + current: IMallTemplate; + form: IForm; + works?: IWork[]; + onAddCar: (product: schema.XProduct, staging: boolean) => void; + onPurchase: (selectedRows: schema.XProduct[], work: IWork) => void; +} + +const HotBody: React.FC = ({ current, form, works, onPurchase, onAddCar }) => { + const [hot, setHot] = useState(); + useEffect(() => { + const id = current.subscribe(async () => setHot(await current.loadHot())); + return () => current.unsubscribe(id); + }, [current, form]); + if (!hot) { + return <>; + } + if (!current.params?.form) { + return ( + + 未绑定商品表单 + + ); + } + return ( +
+
+
+ {current.metadata?.params?.boxName2 ?? '热门商品'} +
+
+ { + return ( + + + {products.map((item) => { + return ( + + ); + })} + + + ); + }} + /> +
+ ); +}; + +interface DataProps extends IProps { + form: IForm; +} +interface IContentBody extends IProps { + works?: IWork[]; + form: IForm; + onPurchase: (selectedRows: schema.XProduct[], work: IWork) => void; + onAddCar: (product: schema.XProduct, staging: boolean) => void; + onBatchAddCar: (products: schema.XProduct[]) => void; +} + +const ContentBody: React.FC = ({ + current, + works, + form, + onPurchase, + onAddCar, + onBatchAddCar, +}) => { + const [openMode, setOpenMode] = useState( + current.metadata.openMode || 'horiz', + ); + const toggleView = (mapV: schema.IOpenMode) => { + setOpenMode(mapV); + }; + + useEffect(() => { + setOpenMode(current.metadata.openMode || 'horiz'); + }, [current]); + + if (!current.params?.form) { + return ( + + 未绑定商品表单 + + ); + } + const renderProducts = (products: schema.XProduct[]) => { + let el = <>; + switch (openMode) { + case 'horiz': + el = ( + + {products.map((item) => { + return ( + + + {current.template === TemplateType.realTemplate||current.template === TemplateType.instrumentTemplate ? ( + + ) : ( + + )} + + ); + })} + + ); + break; + case 'map': + el = ( + + ); + } + return el; + }; + return ( + + {openMode !== 'vertical' && } + +
+ + { + return ( + + {openMode !== 'vertical' ? ( + renderProducts(products) + ) : ( + + + + )} + {openMode === 'horiz' ? ( + `共 ${total} 条`} + showSizeChanger + pageSizeOptions={['12', '24', '48', '96']} + onChange={(pageNum, pageSize) => { + loader.setPage(pageNum); + loader.setSize(pageSize); + loader.loadData(pageNum, pageSize, current, form); + }} + /> + ) : ( + <> + )} + + ); + }} + /> +
+
+
+ ); +}; + +interface IGroup extends DataProps { + onPurchase: (selectedRows: schema.XProduct[], work: IWork) => void; + toggleView: (mapV: 'horiz' | 'vertical' | 'map') => void; + openMode: 'horiz' | 'vertical' | 'map'; + works?: IWork[]; +} + +const Group: React.FC = ({ + current, + form, + onPurchase, + toggleView, + openMode, + works, +}) => { + const [visible, setVisible] = useState(false); + const [length, setLength] = useState(current.shoppingCar.products.length); + useEffect(() => { + const id = current.shoppingCar.subscribe(async () => { + await current.shoppingCar.loadProducts(); + setLength(current.shoppingCar.products.length); + }); + return () => current.shoppingCar.unsubscribe(id); + }, [current]); + + return ( +
+
{current.metadata?.params?.boxName1 ?? '实体商品'}
+ + {openMode !== 'vertical' && ( + { + try { + const inputValue = e.currentTarget?.value ?? e.target?.value; + if (!inputValue) { + current.command.emitter('filter', 'all', []); + return; + } + let fields = form.fields; + if (!fields.length) { + const metadata = await form.loadReferenceForm(form.id); + const myForm = new Form(metadata, form.directory); + fields = await myForm.loadFields(); + } + const searchFields = fields.filter( + (item) => + item.valueType === '描述型' && + ['belongId', 'title'].includes(item.code), + ); + if (!searchFields.length) return; + const filters: any = searchFields.flatMap((item, index) => + index === searchFields.length - 1 + ? [[item.code, 'contains', inputValue]] + : [[item.code, 'contains', inputValue], 'or'], + ); + if ( + filters.length && + !['', null, undefined].includes(filters[filters.length - 1]) + ) { + current.command.emitter('filter', 'all', filters); + } + } catch (error) { + console.error('搜索异常:', error); + } + }} + prefix={} + /> + )} + + setVisible(true)} + /> + setVisible(false)} + open={visible}> + + + + { + toggleView(value as schema.IOpenMode); + }} + options={[ + { + value: 'horiz', + icon: , + }, + { + value: 'vertical', + icon: , + }, + { + value: 'map', + icon: , + }, + ]} + /> + +
+ ); +}; + +const Filter: React.FC = ({ current, form }) => { + const lookups = form.fields.flatMap((item) => + item.options?.species ? item.lookups ?? [] : [], + ); + if (lookups.length === 0) { + return <>; + } + return ( +
+ { + const match: any = {}; + for (const { itemData } of e.component.getSelectedNodes()) { + if (itemData?.value?.startsWith('T')) { + match[itemData.value] = { _exists_: true }; + } else if (itemData?.value?.startsWith('S')) { + if (match['T' + itemData.propertyId]?._in_) { + match['T' + itemData.propertyId]._in_.push(itemData.value); + } else { + match['T' + itemData.propertyId] = { + _in_: [itemData.value], + }; + } + } + } + current.command.emitter('filter', 'species', { match }); + }} + /> +
+ ); +}; + +interface FilterProps { + species: { match: any }; +} + +interface ProviderProps extends DataProps { + renderBody: (products: schema.XProduct[], loader: PageLoader) => ReactNode; +} + +interface PageLoader { + page: number; + setPage: (page: number) => void; + size: number; + setSize: (size: number) => void; + total: number; + loadData: ( + page: number, + size: number, + current: IMallTemplate, + form: IForm, + ) => Promise; +} +const HotProvider: React.FC = ({ current, form, renderBody }) => { + const [loading, setLoading] = useState(false); + const [products, setProducts] = useState([]); + const [page, setPage] = useState(1); + const [size, setSize] = useState(24); + const [total, setTotal] = useState(0); + const filter = useRef([]); + const match = useRef({ species: { match: {} } }); + const loadData = async (page: number, size: number) => { + setLoading(true); + const result = await form.loadThing({ + requireTotalCount: true, + skip: (page - 1) * size, + take: size, + filter: form.parseFilter(filter.current), + options: { + match: { + ...Object.keys(match.current).reduce((p, n) => { + return { ...p, ...(match.current as any)[n].match }; + }, {}), + }, + }, + }); + + setProducts(result.data as schema.XProduct[]); + setTotal(result.totalCount); + setLoading(false); + }; + + useEffect(() => { + loadData(page, size); + }, [current, form]); + + return ( + + {renderBody(products, { page, setPage, size, setSize, total, loadData })} + + ); +}; + +const Provider: React.FC = ({ current, form, renderBody }) => { + const [loading, setLoading] = useState(false); + const [products, setProducts] = useState([]); + const [page, setPage] = useState(1); + const [size, setSize] = useState(24); + const [total, setTotal] = useState(0); + const filter = useRef([]); + const match = useRef({ species: { match: {} } }); + products.map((item) => { + if (item.mode === '空间共享') { + let fieldId = item.fieldId; + const mapProduct = new Map(Object.entries(item)); + const mapArray = [...JSON.parse(mapProduct.get('T' + fieldId))]; + const field = mapArray[0]; + item.field = field; + item.title = field.title; + item.images = field.images; + item.useInfo = field.useInfo; + item.useInfoImage = field.useInfoImage; + item.introduceInfo = field.introduceInfo; + item.introduceImage = field.introduceImage; + item.remark = item.field.remarks; + item.latitudeAndLongitude = item.field.latitudeAndLongitude; + } + }); + const loadData = async ( + page: number, + size: number, + current: IMallTemplate, + form: IForm, + ) => { + setLoading(true); + const result = await form.loadThing({ + requireTotalCount: true, + skip: (page - 1) * size, + take: size, + filter: form.parseFilter(filter.current), + options: { + match: { + ...Object.keys(match.current).reduce((p, n) => { + return { ...p, ...(match.current as any)[n].match }; + }, {}), + }, + }, + }); + const coll = current.directory.resource.genColl('-clicks-count'); + const clicksResult: clicksCount[] = await coll.loadSpace({}); + if (clicksResult) { + for (let i = 0; i < result.data.length; i++) { + const item = result.data[i]; + const existingItemIndex = clicksResult.findIndex((click) => click.id == item.id); + if (existingItemIndex == -1) { + const insertData = { + id: item.id, + count: 0, + } as clicksCount; + const coll = current.directory.resource.genColl('-clicks-count'); + await coll.replace(insertData); + result.data[i].clicksCount = 0; + } else { + result.data[i].clicksCount = clicksResult[existingItemIndex].count; + } + } + } + setProducts(result.data as schema.XProduct[]); + setTotal(result.totalCount); + setLoading(false); + }; + useEffect(() => { + loadData(page, size, current, form); + }, [current, form]); + + useEffect(() => { + kernel.subscribe(`mall-change`, ['mall-change'], (data: any) => { + if (data?.operate === 'mall-change') { + loadData(page, size, current, form); + } + }); + const id = current.command.subscribe((type, cmd, args) => { + switch (type) { + case 'filter': + switch (cmd) { + case 'species': + match.current.species = args; + break; + case 'all': + filter.current = args; + break; + } + loadData(page, size, current, form); + break; + } + }); + return () => current.command.unsubscribe(id); + }, [current]); + return ( + + {renderBody(products, { page, setPage, size, setSize, total, loadData })} + + ); +}; + +interface MapProps { + current: IMallTemplate; + products: schema.XProduct[]; + onAddCar: (product: schema.XProduct, staging: boolean) => void; + onPurchase: (selectedRows: schema.XProduct[], work: IWork) => void; +} + +const MapMall: React.FC = ({ current, products, onAddCar, onPurchase }) => { + let map: any = null; + const [center, setCenter] = useState(<>); + const product = products[0]; + const [staging, setStaging] = useState( + current.shoppingCar.products.some((a) => !a.isMultiple && a.id == product.id), + ); + + useEffect(() => { + const id = current.shoppingCar.subscribe(() => { + setStaging( + current.shoppingCar.products.some((a) => !a.isMultiple && a.id == product.id), + ); + }); + initMap(); + return () => current.shoppingCar.unsubscribe(id); + }, [current]); + + const initMap = () => { + map = new AMap.Map('map', { + center: product?.latitudeAndLongitude + ? [ + product.latitudeAndLongitude.split(',')[0], + product.latitudeAndLongitude.split(',')[1], + ] + : [120.139327, 30.28718], + zoom: 15, + }); + products.forEach((item) => { + if (!item.latitudeAndLongitude) return; + const marker = new AMap.Marker({ + position: [ + item.latitudeAndLongitude.split(',')[0], + item.latitudeAndLongitude.split(',')[1], + ], + }); + marker.setMap(map); + marker.on('click', () => + setCenter( + setCenter(<>)} + onAddCar={onAddCar.bind(this, product, staging)} + onPurchase={onPurchase} + />, + ), + ); + return marker; + }); + }; + return ( + <> +
+ {center} + + ); +}; diff --git a/src/executor/open/instrumentTemplate/pages/shoppingCar.tsx b/src/executor/open/instrumentTemplate/pages/shoppingCar.tsx new file mode 100644 index 000000000..091bdac22 --- /dev/null +++ b/src/executor/open/instrumentTemplate/pages/shoppingCar.tsx @@ -0,0 +1,230 @@ +import EntityIcon from '@/components/Common/GlobalComps/entityIcon'; +import { model, schema } from '@/ts/base'; +import { List as Link } from '@/ts/base/common/linq'; +import { IForm, IWork } from '@/ts/core'; +import { IMallTemplate } from '@/ts/core/thing/standard/page/mallTemplate'; +import { getDefaultImg } from '@/utils/tools'; +import { Button, Divider, Skeleton, Space, Statistic } from 'antd'; +import { CheckBox, List } from 'devextreme-react'; +import _ from 'lodash'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import InfiniteScroll from 'react-infinite-scroll-component'; +import WorkStartDo from '../../work'; +import { ItemProduct } from '../widget/item'; +import cls from './../index.module.less'; +interface IProps { + page: IMallTemplate; + form: IForm; + works?: IWork[]; + onPurchase: (selectedRows: schema.XProduct[], work: IWork) => void; +} + +interface IGroup { + key: string; + items: schema.XProduct[]; +} + +type IChecked = { + [key: string]: boolean | null; +}; + +export const RightCar: React.FC = ({ page, form, works, onPurchase }) => { + const [products, setProducts] = useState([]); + const [selectedRows, setSelectedRows] = useState([]); + const [checked, setChecked] = useState(); + const [center, setCenter] = useState(<>); + const [borrow, setBorrow] = useState(); + const [loading, setLoading] = useState(false); + const [isOrdering, setIsOrdering] = useState(false); + const groups: IGroup[] = useMemo(() => { + const result = new Link(products).GroupBy((item) => item.belongId); + return Object.keys(result).map((item) => { + return { key: item, items: result[item] }; + }); + }, [products]); + const loadContent = async () => { + setLoading(true); + setBorrow(await page.findBorrow()); + setProducts([...(await page.shoppingCar.loadProducts())]); + setLoading(false); + }; + useEffect(() => { + const id = page.shoppingCar.subscribe(() => loadContent()); + return () => page.shoppingCar.unsubscribe(id); + }, []); + const onValueChange = (id: string, val: boolean | null) => { + const _checked = checked; + const extraChecked = selectedRows.filter((i) => i.belongId !== id); + if (val) { + const belongChecked = groups.find((i) => i.key === id)?.items as schema.XProduct[]; + setSelectedRows([...extraChecked, ...belongChecked]); + } else { + setSelectedRows(extraChecked); + } + _checked[id] = val; + setChecked({ ..._checked }); + }; + + useEffect(() => { + const result = new Link(selectedRows).GroupBy((item) => item.belongId); + let checked: IChecked = {}; + groups.forEach((group) => { + checked[group.key] = + group.items.length == result[group.key]?.length + ? true + : result[group.key]?.length + ? null + : false; + }); + setChecked(checked); + }, [selectedRows.length, groups]); + + const selectedRowsGroups = useMemo(() => { + const result = new Link(selectedRows).GroupBy((item) => item.belongId); + return Object.keys(result).map((item) => { + return { key: item, items: result[item] }; + }); + }, [selectedRows]); + + // 是否展示办事结算 + const isShowWork = useMemo(() => { + if (selectedRows.length) { + const belongId = selectedRows[0].belongId; + return !selectedRows.find((item) => item.belongId !== belongId); + } + return false; + }, [selectedRows]); + + // 创建订单 + const debouncedCreateOrder = useCallback( + _.debounce(async () => { + setIsOrdering(true); + const results = await page.order.createOrder(selectedRows); + if (results.length) { + const failItems = results.filter((item) => !item.success); + const tempSelectedRows = selectedRows.filter((item) => + failItems.some((f) => f.id === item.id), + ); + setSelectedRows(tempSelectedRows); + } + setIsOrdering(false); + }, 500), + [selectedRows], + ); + + return ( + <> +
+
+ {}} + hasMore={false} + loader={} + endMessage={到底了,没有更多了哦 🤐} + scrollableTarget="scrollableDiv"> + + dataSource={groups} + height="100%" + grouped + selectedItems={selectedRowsGroups} + collapsibleGroups + showSelectionControls + selectionMode="multiple" + selectByClick={true} + onSelectedItemsChange={(e: IGroup[]) => { + setSelectedRows(e.flatMap((i) => i.items)); + }} + groupRender={(item: IGroup) => { + return ( + + { + e.event?.stopPropagation(); + }} + onValueChange={onValueChange.bind(this, item.key)}> + 供给方 + + + ); + }} + itemRender={(item: schema.XProduct) => { + return ( + { + const getImages = (key: string) => { + const images = JSON.parse(product[key] || '[]'); + if (images.length == 0) { + const species = form.fields.flatMap((item) => + item.options?.species ? item ?? [] : [], + ); + images.push({ + shareLink: getDefaultImg(product, species), + } as model.FileItemShare); + } + return images; + }; + switch (product.typeName) { + case '应用': + return getImages('icons'); + default: + return getImages('images'); + } + }} + /> + ); + }} + /> + +
+
+ + p + ((n.price || 0) * (n.carCount || 1) ?? 0), + 0, + )} + prefix={'合计:'} + suffix="¥" + /> + +
+ + + {works && + isShowWork && + works.map((work) => { + return ( + + ); + })} +
+
+
+ {center} + + ); +}; diff --git a/src/executor/open/instrumentTemplate/widget/item.tsx b/src/executor/open/instrumentTemplate/widget/item.tsx new file mode 100644 index 000000000..f67d3930a --- /dev/null +++ b/src/executor/open/instrumentTemplate/widget/item.tsx @@ -0,0 +1,62 @@ +import { model, schema } from '@/ts/base'; +import { Carousel, Image, Skeleton, Tag, Select } from 'antd'; +import React, { useEffect, useState } from 'react'; +import cls from './../index.module.less'; + +interface IProps { + product: schema.XProduct; + images: (product: schema.XProduct) => model.FileItemShare[]; +} + +export const ItemProduct: React.FC = ({ product, images }) => { + const [selectedTimes, setSelectedTimes] = useState([]); + useEffect(() => { + setSelectedTimes(product.timeArray.filter(item=>item.thisSelected)?.map(item=>item.startTime)|| []); + }, []); + return ( +
+
+ + {images(product).map((item, index) => { + return ( + } + /> + ); + })} + +
+
+
{product.title ?? '[未设置名称]'}
+ + {product.typeName ?? '商品'} + +
+
{product.remark}
+
{product.price ?? 0}¥
+
+ { - data.timeArray?.forEach(item=>{ - item.thisSelected=values.includes(item.startTime) - }) - setSelectedTimes(values) - }} - options={data.timeArray?.map((time) => ({ - label: `${time.startTime.substring(0, 19)} - ${time.endTime.substring(0, 19)}`, - value: time.startTime, - disabled: time.isSelected - }))} - /> + mode="multiple" + style={{ width: '50%' }} + placeholder="请选择预约时间" + value={selectedTimes} + onChange={(values) => { + (data.TtimeArray ?? data.timeArray)?.forEach((item) => { + item.thisSelected = values.includes(item.startTime); + }); + setSelectedTimes(values); + }} + options={(data.TtimeArray ?? data.timeArray)?.map((time) => ({ + label: `${time.startTime.substring(0, 19)} - ${time.endTime.substring( + 0, + 19, + )}`, + value: time.startTime, + disabled: time.isSelected, + }))} + />
加入购物车
diff --git a/src/executor/open/instrumentTemplate/pages/index.tsx b/src/executor/open/instrumentTemplate/pages/index.tsx index 03fe9bfeb..6a1c67bd9 100644 --- a/src/executor/open/instrumentTemplate/pages/index.tsx +++ b/src/executor/open/instrumentTemplate/pages/index.tsx @@ -43,18 +43,22 @@ export const InstrumentTemplate: React.FC = ({ current }) => { const [form, setForm] = useState(); const [loading, setLoading] = useState(true); // 加入购物车 - const onAddCar = async (product: schema.XProduct, staging: boolean) => { - if (product.isMultiple) { + const onAddCar = async ( + product: schema.XProduct, + newProduct: schema.XProduct, + staging: boolean, + ) => { + if (product.TisMultiple ?? product.isMultiple) { let num = current.shoppingCar.products.find((i) => i.id === product.id)?.carCount || 0; num++; if (!num) return; - if (num > (product.count || 0)) { + if (num > ((newProduct.Tcount ?? newProduct.count) || 0)) { message.error('库存不足'); return; } const res = await current.shoppingCar.create({ - ...product, + ...newProduct, carCount: num, }); if (res) { @@ -64,14 +68,14 @@ export const InstrumentTemplate: React.FC = ({ current }) => { } } else { if (staging) { - const res = await current.shoppingCar.remove(product); + const res = await current.shoppingCar.remove(newProduct); if (res) { message.success('已删除购物车商品'); } else { message.error('删除购物车商品失败'); } } else { - const res = await current.shoppingCar.create(product); + const res = await current.shoppingCar.create(newProduct); if (res) { message.success('已加入购物车'); } else { @@ -214,7 +218,11 @@ interface IHotGroup { current: IMallTemplate; form: IForm; works?: IWork[]; - onAddCar: (product: schema.XProduct, staging: boolean) => void; + onAddCar: ( + product: schema.XProduct, + newProduct: schema.XProduct, + staging: boolean, + ) => void; onPurchase: (selectedRows: schema.XProduct[], work: IWork) => void; } @@ -244,7 +252,7 @@ const HotBody: React.FC = ({ current, form, works, onPurchase, onAddC { + renderBody={(products, newProducts) => { return ( = ({ current, form, works, onPurchase, onAddC current={current} form={form} works={works} - product={item} + product={newProducts.find((p) => p.id === item.id) || item} + newProduct={item} onAddCar={onAddCar} onPurchase={onPurchase} /> @@ -280,7 +289,11 @@ interface IContentBody extends IProps { works?: IWork[]; form: IForm; onPurchase: (selectedRows: schema.XProduct[], work: IWork) => void; - onAddCar: (product: schema.XProduct, staging: boolean) => void; + onAddCar: ( + product: schema.XProduct, + newProduct: schema.XProduct, + staging: boolean, + ) => void; onBatchAddCar: (products: schema.XProduct[]) => void; } @@ -310,7 +323,10 @@ const ContentBody: React.FC = ({ ); } - const renderProducts = (products: schema.XProduct[]) => { + const renderProducts = ( + products: schema.XProduct[], + newProducts: schema.XProduct[], + ) => { let el = <>; switch (openMode) { case 'horiz': @@ -320,10 +336,12 @@ const ContentBody: React.FC = ({ return ( - {current.template === TemplateType.realTemplate||current.template === TemplateType.instrumentTemplate ? ( + {current.template === TemplateType.realTemplate || + current.template === TemplateType.instrumentTemplate ? ( p.id === item.id) || item} + newProduct={item} form={form} works={works} onAddCar={onAddCar} @@ -332,7 +350,8 @@ const ContentBody: React.FC = ({ ) : ( p.id === item.id) || item} + newProduct={item} works={works} onAddCar={onAddCar} onPurchase={onPurchase} @@ -349,6 +368,7 @@ const ContentBody: React.FC = ({ @@ -372,11 +392,11 @@ const ContentBody: React.FC = ({ { + renderBody={(products, newProducts, loader) => { return ( {openMode !== 'vertical' ? ( - renderProducts(products) + renderProducts(products, newProducts) ) : ( = ({ const searchFields = fields.filter( (item) => item.valueType === '描述型' && - ['belongId', 'title'].includes(item.code), + [ + 'TbelongId', + 'belongId', + 'Tsupplier', + 'Ttitle', + 'Ttitle', + 'title', + ].includes(item.code), ); if (!searchFields.length) return; const filters: any = searchFields.flatMap((item, index) => @@ -574,7 +601,11 @@ interface FilterProps { } interface ProviderProps extends DataProps { - renderBody: (products: schema.XProduct[], loader: PageLoader) => ReactNode; + renderBody: ( + products: schema.XProduct[], + newProducts: schema.XProduct[], + loader: PageLoader, + ) => ReactNode; } interface PageLoader { @@ -593,6 +624,7 @@ interface PageLoader { const HotProvider: React.FC = ({ current, form, renderBody }) => { const [loading, setLoading] = useState(false); const [products, setProducts] = useState([]); + const [newProducts, setNewProducts] = useState([]); const [page, setPage] = useState(1); const [size, setSize] = useState(24); const [total, setTotal] = useState(0); @@ -613,7 +645,26 @@ const HotProvider: React.FC = ({ current, form, renderBody }) => }, }, }); - + const nData = result.data.map((item: any) => { + const newItem: any = {}; + for (const key in item) { + if (Object.prototype.hasOwnProperty.call(item, key)) { + // 检查字段名是否以 'T' 开头且后面是数字 + if (key.startsWith('T')) { + const newKey = key.substring(1); // 去除 'T' + newItem[newKey] = item[key]; // 优先使用带 'T' 的值 + } else { + // 如果新对象中没有这个键,或者这个键是带T的,则赋值 + if (!newItem.hasOwnProperty(key)) { + newItem[key] = item[key]; + } + } + } + } + return newItem; + }); + setProducts(result.data as schema.XProduct[]); + setNewProducts(nData); setProducts(result.data as schema.XProduct[]); setTotal(result.totalCount); setLoading(false); @@ -625,13 +676,21 @@ const HotProvider: React.FC = ({ current, form, renderBody }) => return ( - {renderBody(products, { page, setPage, size, setSize, total, loadData })} + {renderBody(products, newProducts, { + page, + setPage, + size, + setSize, + total, + loadData, + })} ); }; const Provider: React.FC = ({ current, form, renderBody }) => { const [loading, setLoading] = useState(false); + const [newProducts, setNewProducts] = useState([]); const [products, setProducts] = useState([]); const [page, setPage] = useState(1); const [size, setSize] = useState(24); @@ -694,7 +753,26 @@ const Provider: React.FC = ({ current, form, renderBody }) => { } } } + const nData = result.data.map((item: any) => { + const newItem: any = {}; + for (const key in item) { + if (Object.prototype.hasOwnProperty.call(item, key)) { + // 检查字段名是否以 'T' 开头且后面是数字 + if (key.startsWith('T')) { + const newKey = key.substring(1); // 去除 'T' + newItem[newKey] = item[key]; // 优先使用带 'T' 的值 + } else { + // 如果新对象中没有这个键,或者这个键是带T的,则赋值 + if (!newItem.hasOwnProperty(key)) { + newItem[key] = item[key]; + } + } + } + } + return newItem; + }); setProducts(result.data as schema.XProduct[]); + setNewProducts(nData); setTotal(result.totalCount); setLoading(false); }; @@ -727,7 +805,14 @@ const Provider: React.FC = ({ current, form, renderBody }) => { }, [current]); return ( - {renderBody(products, { page, setPage, size, setSize, total, loadData })} + {renderBody(products, newProducts, { + page, + setPage, + size, + setSize, + total, + loadData, + })} ); }; @@ -735,11 +820,22 @@ const Provider: React.FC = ({ current, form, renderBody }) => { interface MapProps { current: IMallTemplate; products: schema.XProduct[]; - onAddCar: (product: schema.XProduct, staging: boolean) => void; + newProducts: schema.XProduct[]; + onAddCar: ( + product: schema.XProduct, + newProduct: schema.XProduct, + staging: boolean, + ) => void; onPurchase: (selectedRows: schema.XProduct[], work: IWork) => void; } -const MapMall: React.FC = ({ current, products, onAddCar, onPurchase }) => { +const MapMall: React.FC = ({ + current, + newProducts, + products, + onAddCar, + onPurchase, +}) => { let map: any = null; const [center, setCenter] = useState(<>); const product = products[0]; @@ -759,20 +855,20 @@ const MapMall: React.FC = ({ current, products, onAddCar, onPurchase } const initMap = () => { map = new AMap.Map('map', { - center: product?.latitudeAndLongitude + center: product?.TlatitudeAndLongitude ? [ - product.latitudeAndLongitude.split(',')[0], - product.latitudeAndLongitude.split(',')[1], + product.TlatitudeAndLongitude.split(',')[0], + product.TlatitudeAndLongitude.split(',')[1], ] : [120.139327, 30.28718], zoom: 15, }); products.forEach((item) => { - if (!item.latitudeAndLongitude) return; + if (!item.TlatitudeAndLongitude) return; const marker = new AMap.Marker({ position: [ - item.latitudeAndLongitude.split(',')[0], - item.latitudeAndLongitude.split(',')[1], + item.TlatitudeAndLongitude.split(',')[0], + item.TlatitudeAndLongitude.split(',')[1], ], }); marker.setMap(map); @@ -782,7 +878,12 @@ const MapMall: React.FC = ({ current, products, onAddCar, onPurchase } current={current} data={item} onCancel={() => setCenter(<>)} - onAddCar={onAddCar.bind(this, product, staging)} + onAddCar={onAddCar.bind( + this, + newProducts.find((p) => p.id === item.id) || item, + product, + staging, + )} onPurchase={onPurchase} />, ), diff --git a/src/executor/open/instrumentTemplate/pages/shoppingCar.tsx b/src/executor/open/instrumentTemplate/pages/shoppingCar.tsx index 091bdac22..fbf25d539 100644 --- a/src/executor/open/instrumentTemplate/pages/shoppingCar.tsx +++ b/src/executor/open/instrumentTemplate/pages/shoppingCar.tsx @@ -156,7 +156,9 @@ export const RightCar: React.FC = ({ page, form, works, onPurchase }) => product={item} images={(product) => { const getImages = (key: string) => { - const images = JSON.parse(product[key] || '[]'); + const images = JSON.parse( + (product['T' + key] ?? product[key]) || '[]', + ); if (images.length == 0) { const species = form.fields.flatMap((item) => item.options?.species ? item ?? [] : [], @@ -203,13 +205,13 @@ export const RightCar: React.FC = ({ page, form, works, onPurchase }) => }}> 批量删除 - + {works && isShowWork && works.map((work) => { diff --git a/src/executor/open/instrumentTemplate/widget/item.tsx b/src/executor/open/instrumentTemplate/widget/item.tsx index f67d3930a..14eb0b7f5 100644 --- a/src/executor/open/instrumentTemplate/widget/item.tsx +++ b/src/executor/open/instrumentTemplate/widget/item.tsx @@ -1,5 +1,5 @@ import { model, schema } from '@/ts/base'; -import { Carousel, Image, Skeleton, Tag, Select } from 'antd'; +import { Carousel, Image, Skeleton, Tag, InputNumber, Select } from 'antd'; import React, { useEffect, useState } from 'react'; import cls from './../index.module.less'; @@ -11,7 +11,11 @@ interface IProps { export const ItemProduct: React.FC = ({ product, images }) => { const [selectedTimes, setSelectedTimes] = useState([]); useEffect(() => { - setSelectedTimes(product.timeArray.filter(item=>item.thisSelected)?.map(item=>item.startTime)|| []); + setSelectedTimes( + (product.TtimeArray ?? product.timeArray) + .filter((item) => item.thisSelected) + ?.map((item) => item.startTime) || [], + ); }, []); return (
@@ -31,31 +35,34 @@ export const ItemProduct: React.FC = ({ product, images }) => {
-
{product.title ?? '[未设置名称]'}
+
{product.Ttitle ?? product.title ?? '[未设置名称]'}
- {product.typeName ?? '商品'} + {product.TtypeName ?? product.typeName ?? '商品'}
-
{product.remark}
-
{product.price ?? 0}¥
+
{product.Tremark ?? product.remark}
+
{product.Tprice ?? product.price ?? 0}¥