diff --git a/src/components/Common/ExecutorShowComp/index.tsx b/src/components/Common/ExecutorShowComp/index.tsx index 71d1bc379aee5a49d3b47d910d3b90bc07680a9f..fa9a92ad3471f48db4c8455d90136d34f7b56f10 100644 --- a/src/components/Common/ExecutorShowComp/index.tsx +++ b/src/components/Common/ExecutorShowComp/index.tsx @@ -9,7 +9,18 @@ import { IWork } from '@/ts/core'; import { ShareIdSet } from '@/ts/core/public/entity'; import { ProFormInstance } from '@ant-design/pro-form'; import ProTable from '@ant-design/pro-table'; -import { Button, Card, Checkbox, Empty, Input, message, Modal, Space, Table, Switch } from 'antd'; +import { + Button, + Card, + Checkbox, + Empty, + Input, + message, + Modal, + Space, + Table, + Switch, +} from 'antd'; import { SelectBox } from 'devextreme-react'; import React, { ReactNode, useEffect, useRef, useState } from 'react'; import { AiOutlineCloseCircle } from 'react-icons/ai'; @@ -704,6 +715,27 @@ export const FieldChangeTable: React.FC = (props) => { }}> 添加归属变更标记 , + , ]} columns={[ ...changeRecords, @@ -1057,7 +1089,7 @@ export const Webhook: React.FC> = (props) => { checked={dataType} onChange={(e) => { props.executor.dataType = e; - setDataType(e) + setDataType(e); }} /> @@ -1069,7 +1101,7 @@ export const Webhook: React.FC> = (props) => { checked={autoType} onChange={(e) => { props.executor.autoType = e; - setAutoType(e) + setAutoType(e); }} /> diff --git a/src/components/Common/StringDatePickers/RangePicker.tsx b/src/components/Common/StringDatePickers/RangePicker.tsx index eca23dfc41c0fca273754de34c92f1b2eeca73ed..0cad40eb5a035df250fdbd43f7ee7f4cbe4e312d 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 0000000000000000000000000000000000000000..993aa0cc5ccab4e0d666c88d13dd2035621b5295 --- /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 0000000000000000000000000000000000000000..3eaf5bcd6238fea00de1cc6229ecb50a90787319 --- /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/order/index.tsx b/src/components/DataPreview/order/index.tsx index 417b12e91b1f90f07dc85b21054f6b627f36732f..b3e4c00a130211e3bb0e5942c351a325bdbd0094 100644 --- a/src/components/DataPreview/order/index.tsx +++ b/src/components/DataPreview/order/index.tsx @@ -91,10 +91,19 @@ const OrderViewer: React.FC = ({ current }) => { }; const setExtraParams = (instance: Work) => { - instance.setExtraParams({ - id: current.id, - type: 'orderHandler', - }); + const isTime = JSON.parse(current.itemList[0].resource).timeArray?.length > 0; + if (isTime) { + instance.setExtraParams({ + id: current.id, + type: 'orderHandler', + orderInfoId: current.orderNumber, + }); + } else { + instance.setExtraParams({ + id: current.id, + type: 'orderHandler', + }); + } }; return ( diff --git a/src/components/DataPreview/session/WorkBench/components/Banner/Common/BannerImg.tsx b/src/components/DataPreview/session/WorkBench/components/Banner/Common/BannerImg.tsx index 3b4631f2ef3268fc50104365db30ce9594adfe18..3fd17e59b2668997b287f3abadde29792535d5e1 100644 --- a/src/components/DataPreview/session/WorkBench/components/Banner/Common/BannerImg.tsx +++ b/src/components/DataPreview/session/WorkBench/components/Banner/Common/BannerImg.tsx @@ -8,6 +8,7 @@ interface IProps { bannerImg: any[]; target: ITarget; bannerkey: string; + allowEdit?: boolean; } interface IUploadFile extends model.FileItemShare { uid: string; @@ -91,8 +92,11 @@ const Banner: React.FC = (props) => { operateBanner(allFileData); } } - }} - /> + }}> + {props.target.hasRelationAuth() && props.allowEdit && ( +
编辑封面
+ )} + ))} diff --git a/src/components/DataPreview/session/portal/index.tsx b/src/components/DataPreview/session/portal/index.tsx index efa2336f43f9f5f0a662dfe7ed9f63cd460c7204..9e1830ea7a8a99cf9e25fa865622c4a9dd4d3404 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'; @@ -60,6 +61,8 @@ const PortalPage: React.FC = ({ target }) => { } case '商城模板': return ; + case '商城模板-大仪': + return ; default: return <>暂不支持; } diff --git a/src/components/DataStandard/WorkForm/Design/config/form.tsx b/src/components/DataStandard/WorkForm/Design/config/form.tsx index 65278320acdd1f326af1be0e704df6f314f246b5..0978b4f6a62ba7f50e1e67cc31b2aacfe7a20ed9 100644 --- a/src/components/DataStandard/WorkForm/Design/config/form.tsx +++ b/src/components/DataStandard/WorkForm/Design/config/form.tsx @@ -52,6 +52,23 @@ const FormConfig: React.FC = ({ }; const setFilterCondition = (value: string, text: string) => { + if (tryParseJson(value)) { + // 处理时间段 + if (Array.isArray(JSON.parse(value)) && JSON.parse(value)[0] === 'timeArray') { + const filterValue = { + $elemMatch: { + isSelected: false, + }, + }; + current.metadata.options!.dataRange!.filterExp = JSON.stringify([ + 'timeArray', + filterValue, + ]); + } else { + current.metadata.options!.dataRange!.filterExp = value; + } + current.metadata.options!.dataRange!.filterDisplay = text; + } if (!current.metadata.options?.dataRange) { current.metadata.options!.dataRange = {}; } @@ -207,7 +224,7 @@ const FormConfig: React.FC = ({ label={{ text: '导入匹配设置' }} editorOptions={{ items: fields.filter((item) => item.name !== 'id').map((i) => i.caption), - searchEnabled: true + searchEnabled: true, }} /> ; 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 76aa2af74b39b97c353744a0bd1fadc11bee207c..b70314f60156886a15d39ec271f1f4e5c98500cd 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 b885a32e0ad2a3061881bd06df8b839c43d37772..26c14b228a361e927d2b181c2cbb67614034077f 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 '附件型': @@ -47,6 +49,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 79d263c7ab6e81564de2697a11044ae2cf710b03..3754aa4d9adbe82a31cb6f5ad7d815eb0f2c06ae 100644 --- a/src/components/DataStandard/WorkForm/Viewer/formItem.tsx +++ b/src/components/DataStandard/WorkForm/Viewer/formItem.tsx @@ -23,6 +23,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; @@ -295,6 +296,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 a55cbc69efb34091f7efe196e5408d534957a2f2..1671b6eefd9c880ffa631744eb2ab26046be0964 100644 --- a/src/config/column.tsx +++ b/src/config/column.tsx @@ -288,6 +288,9 @@ export const FullProperties = (typeName: string, form: schema.XForm) => { case '实体商品': propertys = PhysicalProperties(ProductProperties()); break; + case '大仪商品': + propertys = InstrumentProperties(ProductProperties()); + break; case '报表数据': propertys = ReportProperties(); break; @@ -752,7 +755,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[]; +}; /** 商品属性 */ const ProductProperties = () => { return [ @@ -799,8 +827,8 @@ const ProductProperties = () => { remark: '商品价格', }, { - id: 'belongId', - code: 'belongId', + id: 'supplier', + code: 'supplier', name: '供给方', valueType: '用户型', remark: '供给方', diff --git a/src/executor/design/index.tsx b/src/executor/design/index.tsx index 078462fdb3dc40106559c133846eff8af2f9454c..37e5fd3b362c256df7695f4b1f65123318beeb6b 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 783269eda6b0f3014c287c8015dfd9a2fd892dfc..e77c8b56302d9c72245fb58aa309ded4f4120f9b 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 4a21817f1c8739fec06b3f56e01b37d2f071b669..62c0f191713d87206bbfa2576c0733d02d3fac22 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 0000000000000000000000000000000000000000..110e4457101fdac74b2b52680b3cdbe206efa8bf --- /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 0000000000000000000000000000000000000000..f4c50bdf6bb605135ab98cf029a6b8c7ba9e7a16 --- /dev/null +++ b/src/executor/open/instrumentTemplate/components/dataDetails/index.tsx @@ -0,0 +1,243 @@ +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['T' + type] ?? 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.TtimeArray ?? 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.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, + }))} + /> +
+ 加入购物车 +
+ + + + + + ); +}; + +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 0000000000000000000000000000000000000000..1a09dff92023a4ec50fe04fb9d217cbd3f4d37e4 --- /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 0000000000000000000000000000000000000000..b81f8372ff9cd1881e9e4bf885a73715b57a8f12 --- /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 0000000000000000000000000000000000000000..6a1c67bd9e82348c9594e529179198a765159b4b --- /dev/null +++ b/src/executor/open/instrumentTemplate/pages/index.tsx @@ -0,0 +1,900 @@ +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, + 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 > ((newProduct.Tcount ?? newProduct.count) || 0)) { + message.error('库存不足'); + return; + } + const res = await current.shoppingCar.create({ + ...newProduct, + carCount: num, + }); + if (res) { + message.success('已加入购物车'); + } else { + message.error('加入购物车失败'); + } + } else { + if (staging) { + const res = await current.shoppingCar.remove(newProduct); + if (res) { + message.success('已删除购物车商品'); + } else { + message.error('删除购物车商品失败'); + } + } else { + const res = await current.shoppingCar.create(newProduct); + 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, + newProduct: 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 ( + p.id === item.id) || item} + newProduct={item} + onAddCar={onAddCar} + onPurchase={onPurchase} + /> + ); + })} + + + ); + }} + /> +
+ ); +}; + +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, + newProduct: 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[], + newProducts: schema.XProduct[], + ) => { + let el = <>; + switch (openMode) { + case 'horiz': + el = ( + + {products.map((item) => { + return ( + + + {current.template === TemplateType.realTemplate || + current.template === TemplateType.instrumentTemplate ? ( + p.id === item.id) || item} + newProduct={item} + form={form} + works={works} + onAddCar={onAddCar} + onPurchase={onPurchase} + /> + ) : ( + p.id === item.id) || item} + newProduct={item} + works={works} + onAddCar={onAddCar} + onPurchase={onPurchase} + /> + )} + + ); + })} + + ); + break; + case 'map': + el = ( + + ); + } + return el; + }; + return ( + + {openMode !== 'vertical' && } + +
+ + { + return ( + + {openMode !== 'vertical' ? ( + renderProducts(products, newProducts) + ) : ( + + + + )} + {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 === '描述型' && + [ + 'TbelongId', + 'belongId', + 'Tsupplier', + 'Ttitle', + 'Ttitle', + '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[], + newProducts: 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 [newProducts, setNewProducts] = 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 }; + }, {}), + }, + }, + }); + 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); + }; + + useEffect(() => { + loadData(page, size); + }, [current, form]); + + return ( + + {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); + 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; + } + } + } + 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); + }; + 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, newProducts, { + page, + setPage, + size, + setSize, + total, + loadData, + })} + + ); +}; + +interface MapProps { + current: IMallTemplate; + products: schema.XProduct[]; + 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, + newProducts, + 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?.TlatitudeAndLongitude + ? [ + product.TlatitudeAndLongitude.split(',')[0], + product.TlatitudeAndLongitude.split(',')[1], + ] + : [120.139327, 30.28718], + zoom: 15, + }); + products.forEach((item) => { + if (!item.TlatitudeAndLongitude) return; + const marker = new AMap.Marker({ + position: [ + item.TlatitudeAndLongitude.split(',')[0], + item.TlatitudeAndLongitude.split(',')[1], + ], + }); + marker.setMap(map); + marker.on('click', () => + setCenter( + setCenter(<>)} + onAddCar={onAddCar.bind( + this, + newProducts.find((p) => p.id === item.id) || item, + 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 0000000000000000000000000000000000000000..fbf25d5393d033fa54776b0afc24a82b99bfba25 --- /dev/null +++ b/src/executor/open/instrumentTemplate/pages/shoppingCar.tsx @@ -0,0 +1,232 @@ +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['T' + key] ?? 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 0000000000000000000000000000000000000000..14eb0b7f57d61a8ce7994096438892b3834f7e72 --- /dev/null +++ b/src/executor/open/instrumentTemplate/widget/item.tsx @@ -0,0 +1,69 @@ +import { model, schema } from '@/ts/base'; +import { Carousel, Image, Skeleton, Tag, InputNumber, 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.TtimeArray ?? product.timeArray) + .filter((item) => item.thisSelected) + ?.map((item) => item.startTime) || [], + ); + }, []); + return ( +
+
+ + {images(product).map((item, index) => { + return ( + } + /> + ); + })} + +
+
+
{product.Ttitle ?? product.title ?? '[未设置名称]'}
+ + {product.TtypeName ?? product.typeName ?? '商品'} + +
+
{product.Tremark ?? product.remark}
+
{product.Tprice ?? product.price ?? 0}¥
+
+