modal.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362
  1. import { Empty, Input, Modal } from 'antd'
  2. import { Button, Upload, message } from 'antd'
  3. import ImgCrop from 'antd-img-crop'
  4. import { useEffect, useMemo, useRef, useState } from 'react'
  5. import AMapLoader from '@amap/amap-jsapi-loader';
  6. import style from './style.module.scss'
  7. import { SceneType, SceneTypeDomain, SceneTypePaths } from 'constant';
  8. import { base64ToBlob, getHref, drawImage } from 'utils';
  9. import { asyncLoading } from 'components/loading';
  10. import { RedoOutlined } from '@ant-design/icons';
  11. import { fetchTaggings } from 'api'
  12. import { SortTransfer } from './sort-transfer'
  13. import { BoardType, BoardTypeDesc } from 'api'
  14. import type { Tagging } from 'api'
  15. import type { UploadProps } from 'antd'
  16. const domScreenshot = async (dom: HTMLElement) => {
  17. const canvas = (dom.tagName.toUpperCase() === 'CANVAS' ? dom : dom.querySelector('canvas')) as HTMLCanvasElement
  18. return new Promise<Blob | null>(resolve => {
  19. if (!canvas) {
  20. return resolve(null)
  21. }
  22. canvas.toBlob(resolve)
  23. })
  24. }
  25. type SelectImageProps = {
  26. onClose: () => void
  27. onSave: (url: Blob | null, tagging: Tagging[]) => void
  28. }
  29. let AMap: any
  30. AMapLoader.load({
  31. plugins: ['AMap.PlaceSearch'],
  32. key: 'e661b00bdf2c44cccf71ef6070ef41b8',
  33. version: '2.0',
  34. }).then(result => AMap = result)
  35. type MapInfo = { lat: number, lng: number, zoom: number }
  36. export const SelectMap = (props: SelectImageProps) => {
  37. const [open, setOpen] = useState(true)
  38. const [info, setInfo] = useState<MapInfo>()
  39. const [keyword, setKeyword] = useState('')
  40. const mapEle = useRef<HTMLDivElement>(null)
  41. const searchResultEle = useRef<HTMLDivElement>(null)
  42. const searchAMap = useRef<any>()
  43. const onSubmit = async () => {
  44. if (mapEle.current) {
  45. const blob = await domScreenshot(mapEle.current)
  46. await props.onSave(blob, [])
  47. setOpen(false)
  48. }
  49. }
  50. const renderInfo = info && <div className={style['def-map-info']}>
  51. <p><span>经度</span>{ info.lat }</p>
  52. <p><span>维度</span>{ info.lng }</p>
  53. <p><span>缩放级别</span>{ info.zoom }</p>
  54. </div>
  55. useEffect(() => {
  56. if (!mapEle.current) {
  57. return;
  58. }
  59. const map = new AMap.Map(mapEle.current, {
  60. WebGLParams: {
  61. preserveDrawingBuffer: true
  62. },
  63. resizeEnable: true
  64. })
  65. const placeSearch = new AMap.PlaceSearch({
  66. pageSize: 5,
  67. pageIndex: 1,
  68. map: map,
  69. panel: searchResultEle.current,
  70. autoFitView: true
  71. });
  72. const getMapInfo = (): MapInfo => {
  73. var zoom = map.getZoom(); //获取当前地图级别
  74. var center = map.getCenter();
  75. return {
  76. zoom,
  77. lat: center.lat,
  78. lng: center.lng
  79. }
  80. }
  81. //绑定地图移动与缩放事件
  82. map.on('moveend', () => setInfo(getMapInfo()));
  83. map.on('zoomend', () => setInfo(getMapInfo()));
  84. searchAMap.current = placeSearch
  85. return () => {
  86. searchAMap.current = null
  87. map.destroy()
  88. }
  89. }, [mapEle])
  90. useEffect(() => {
  91. keyword && searchAMap.current?.search(keyword)
  92. }, [keyword, searchAMap])
  93. return (
  94. <Modal
  95. width="588px"
  96. title="选择地址"
  97. open={open}
  98. onCancel={() => setOpen(false)}
  99. onOk={() => asyncLoading(onSubmit())}
  100. afterClose={props.onClose}
  101. okText="确定"
  102. cancelText="取消"
  103. >
  104. <div className={style['search-layout']}>
  105. <Input.Search
  106. allowClear
  107. placeholder="输入名称搜索"
  108. onSearch={setKeyword}
  109. style={{ width: 350 }}
  110. />
  111. <div
  112. className={`${style['search-result']} ${keyword ? style['show']: ''}`}
  113. ref={searchResultEle}
  114. />
  115. </div>
  116. <div ref={mapEle} className={style['def-select-map']}></div>
  117. { renderInfo }
  118. </Modal>
  119. )
  120. }
  121. const getFuseUrl = (caseId: number) =>
  122. `${getHref(SceneTypeDomain[SceneType.SWMX]!, SceneTypePaths[SceneType.SWMX][0], { caseId: caseId.toString() })}&share=1#show/summary`
  123. enum ImageType {
  124. FUSE,
  125. KANKAN,
  126. LASER
  127. }
  128. type FuseImageRet = { type: ImageType, blob: Blob | null }
  129. const getFuseImage = async (iframe: HTMLIFrameElement) => {
  130. const iframeElement = iframe.contentWindow?.document.documentElement
  131. if (!iframeElement) {
  132. return null
  133. }
  134. const extIframe = iframeElement.querySelector('.external') as HTMLIFrameElement
  135. const targetIframe = extIframe || iframe
  136. const targetWindow: any = targetIframe.contentWindow
  137. const fuseCnavas = targetWindow.document.querySelector('.scene-canvas > canvas') as HTMLElement
  138. if (fuseCnavas) {
  139. const dataURL = await targetWindow.sdk.screenshot(targetIframe.offsetWidth, targetIframe.offsetHeight)
  140. const res = await fetch(dataURL)
  141. const blob = await res.blob()
  142. return { type: ImageType.FUSE, blob }
  143. // return domScreenshot(fuseCnavas).then(blob => ({ type: ImageType.FUSE, blob }))
  144. }
  145. const isLaser = targetWindow.document.querySelector('.laser-layer')
  146. if (isLaser) {
  147. const sdk = await targetWindow.__sdk
  148. return new Promise<FuseImageRet>(resolve => {
  149. sdk.scene.screenshot(540, 390).done((data: string) => {
  150. resolve({ type: ImageType.FUSE, blob: base64ToBlob(data) })
  151. })
  152. })
  153. } else {
  154. const sdk = targetWindow.__sdk
  155. return new Promise<FuseImageRet>(resolve => {
  156. sdk.Camera.screenshot([ {width: 540, height: 390, name: '2k' }],false).then((result: any)=>{
  157. resolve({ type: ImageType.KANKAN, blob: base64ToBlob(result[0].data) })
  158. })
  159. })
  160. }
  161. }
  162. export const SelectFuse = (props: SelectImageProps & { caseId: number }) => {
  163. const [open, setOpen] = useState(true)
  164. const [blob, setBlob] = useState<Blob | null>(null)
  165. const [tags, setTags] = useState<Tagging[]>([])
  166. const [selectTags, setSelectTags] = useState<string[]>([])
  167. const [addTagIds, setAddTagIds] = useState<string[]>([])
  168. const iframeRef = useRef<HTMLIFrameElement>(null)
  169. const coverUrl = useMemo(() => blob && URL.createObjectURL(blob), [blob])
  170. const url = useMemo(() => getFuseUrl(props.caseId), [props.caseId])
  171. const addTags = useMemo(() => addTagIds.map(id => tags.find(tag => tag.tagId.toString() === id)!), [addTagIds, tags])
  172. const mockData = useMemo(() => tags.map(tag => ({
  173. data: tag,
  174. key: tag.tagId.toString()
  175. })), [tags])
  176. useEffect(() => {
  177. fetchTaggings(props.caseId.toString()).then(setTags)
  178. }, [props.caseId])
  179. const onSubmit = async () => {
  180. const filterTags = addTagIds.map(id => tags.find(tag => tag.tagId.toString() === id)!)
  181. props.onSave(blob, filterTags)
  182. setOpen(false)
  183. }
  184. const getCover = async () => {
  185. if (iframeRef.current) {
  186. const fuseImage = await getFuseImage(iframeRef.current)
  187. if (!fuseImage?.blob) {
  188. return;
  189. } else if (fuseImage.type !== ImageType.FUSE) {
  190. setBlob(fuseImage.blob)
  191. return;
  192. }
  193. const img = new Image()
  194. img.src = URL.createObjectURL(fuseImage.blob)
  195. await new Promise(resolve => img.onload = resolve)
  196. const $canvas = document.createElement('canvas')
  197. $canvas.width = img.width
  198. $canvas.height = img.height
  199. const ctx = $canvas.getContext('2d')!
  200. ctx.drawImage(img, 0, 0, img.width, img.height)
  201. const contentDoc = iframeRef.current.contentWindow!.document
  202. const hotItems = Array.from(contentDoc.querySelectorAll('.hot-item')) as HTMLDivElement[]
  203. hotItems.forEach(hot => {
  204. const hotTitle = (hot.querySelector('.tip') as HTMLDivElement).innerText
  205. const index = addTags.findIndex(tag => tag.tagTitle.trim() === hotTitle.trim())
  206. if (index !== -1) {
  207. const bound = hot.getBoundingClientRect()
  208. const size = (img.width / 540) * 32
  209. const left = bound.left + size / 2
  210. const top = bound.top + size / 2
  211. ctx.save()
  212. ctx.translate(left, top)
  213. ctx.beginPath()
  214. ctx.arc(0, 0, size / 2, 0, 2 * Math.PI)
  215. ctx.strokeStyle = '#000'
  216. ctx.fillStyle = '#fff'
  217. ctx.stroke()
  218. ctx.fill()
  219. ctx.beginPath()
  220. ctx.fillStyle = '#000'
  221. ctx.textAlign = 'center'
  222. ctx.textBaseline = 'middle'
  223. ctx.font = `normal ${size / 2}px serif`
  224. ctx.fillText((index + 1).toString(), 0, 0)
  225. ctx.restore()
  226. }
  227. })
  228. const $ccanvas = document.createElement('canvas')
  229. $ccanvas.width = 540
  230. $ccanvas.height = 390
  231. const cctx = $ccanvas.getContext('2d')!
  232. drawImage(
  233. cctx,
  234. $ccanvas.width,
  235. $ccanvas.height,
  236. $canvas,
  237. img.width,
  238. img.height,
  239. 0, 0
  240. )
  241. console.error('???')
  242. const blob = await new Promise<Blob | null>(resolve => $ccanvas.toBlob(resolve, 'png'))
  243. setBlob(blob)
  244. }
  245. }
  246. return (
  247. <Modal
  248. width="1500px"
  249. title="选择户型图"
  250. open={open}
  251. onCancel={() => setOpen(false)}
  252. onOk={() => asyncLoading(onSubmit())}
  253. afterClose={props.onClose}
  254. okText="确定"
  255. cancelText="取消"
  256. >
  257. <div className={style['house-layout']}>
  258. <div className={style['iframe-layout']}>
  259. <iframe src={url} ref={iframeRef} title="fuce-code" />
  260. </div>
  261. <div className={style['content-layout']}>
  262. <div className={style['house-tags']}>
  263. <h4>请选择要同步到现场图的标注:</h4>
  264. <div className={style['tagging-transfer']}>
  265. <SortTransfer
  266. dataSource={mockData}
  267. titles={['所有', '需要']}
  268. targetKeys={addTagIds}
  269. selectedKeys={selectTags}
  270. onChange={setAddTagIds}
  271. onSelectChange={(sKeys, tKeys) => setSelectTags([...sKeys, ...tKeys])}
  272. onChangeSort={tags => setAddTagIds(tags.map(tag => tag.data.tagId.toString()))}
  273. />
  274. </div>
  275. </div>
  276. <div className={style['house-image-layout']}>
  277. <h4>户型图:<RedoOutlined className='icon' onClick={getCover} /></h4>
  278. <div className={style['house-image']}>
  279. <div>
  280. { coverUrl ? <img src={coverUrl} alt="预览图" /> : <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} /> }
  281. </div>
  282. </div>
  283. </div>
  284. </div>
  285. </div>
  286. </Modal>
  287. )
  288. }
  289. type DfUploadCropProp = Partial<SelectImageProps> & {
  290. type: BoardType
  291. caseId: number
  292. }
  293. export const DfUploadCrop = ({ type, caseId, onClose, onSave }: DfUploadCropProp) => {
  294. const onUpload: UploadProps['beforeUpload'] = async file => {
  295. const filename = file.name
  296. const ext = filename.substring(filename.lastIndexOf('.'))
  297. const isImg = ['.png', '.jpg'].includes(ext.toLocaleLowerCase())
  298. if (!isImg) {
  299. message.error('只能上传png或jpg文件')
  300. return Upload.LIST_IGNORE
  301. } else if (file.size > 100 * 1024 * 1024) {
  302. message.error('大小在100MB以内')
  303. return Upload.LIST_IGNORE
  304. }
  305. const img = new Image();
  306. img.src = URL.createObjectURL(file);
  307. await new Promise(resolve => {
  308. img.onload = resolve
  309. })
  310. const $canvas = document.createElement("canvas");
  311. $canvas.width = 540;
  312. $canvas.height = 390;
  313. const ctx = $canvas.getContext("2d")
  314. ctx?.drawImage(img, 0, 0, 540, 390);
  315. const blob = await domScreenshot($canvas);
  316. onSave && onSave(blob, [])
  317. return Upload.LIST_IGNORE
  318. }
  319. return (
  320. <ImgCrop rotationSlider modalTitle={"裁剪" + BoardTypeDesc[type]} aspect={540 / 390} minZoom={1} maxZoom={5}>
  321. <Upload beforeUpload={onUpload} multiple={false} accept="png">
  322. <Button type="primary" ghost block>上传{ BoardTypeDesc[type] }</Button>
  323. </Upload>
  324. </ImgCrop>
  325. )
  326. }