Prechádzať zdrojové kódy

fix: 添加标注排序

bill 2 rokov pred
rodič
commit
a202a65c07

+ 1 - 0
package.json

@@ -36,6 +36,7 @@
     "react-redux": "^8.0.2",
     "react-router-dom": "^6.3.0",
     "react-scripts": "5.0.1",
+    "react-sortable-hoc": "^2.0.0",
     "redux": "^4.2.0",
     "sass": "^1.54.0",
     "typescript": "^4.7.4",

+ 21 - 0
pnpm-lock.yaml

@@ -35,6 +35,7 @@ specifiers:
   react-redux: ^8.0.2
   react-router-dom: ^6.3.0
   react-scripts: 5.0.1
+  react-sortable-hoc: ^2.0.0
   redux: ^4.2.0
   sass: ^1.54.0
   typescript: ^4.7.4
@@ -74,6 +75,7 @@ dependencies:
   react-redux: 8.0.5_moha6x5fbqoiok2ot63p7hwafm
   react-router-dom: 6.4.3_biqbaboplfbrettd7655fr4n2y
   react-scripts: 5.0.1_vrhrsyowp6vs4524xuw7v67ume
+  react-sortable-hoc: 2.0.0_biqbaboplfbrettd7655fr4n2y
   redux: 4.2.0
   sass: 1.56.1
   typescript: 4.9.3
@@ -6086,6 +6088,12 @@ packages:
       side-channel: 1.0.4
     dev: false
 
+  /invariant/2.2.4:
+    resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==}
+    dependencies:
+      loose-envify: 1.4.0
+    dev: false
+
   /ipaddr.js/1.9.1:
     resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
     engines: {node: '>= 0.10'}
@@ -9605,6 +9613,19 @@ packages:
       - webpack-plugin-serve
     dev: false
 
+  /react-sortable-hoc/2.0.0_biqbaboplfbrettd7655fr4n2y:
+    resolution: {integrity: sha512-JZUw7hBsAHXK7PTyErJyI7SopSBFRcFHDjWW5SWjcugY0i6iH7f+eJkY8cJmGMlZ1C9xz1J3Vjz0plFpavVeRg==}
+    peerDependencies:
+      react: ^16.3.0 || ^17.0.0
+      react-dom: ^16.3.0 || ^17.0.0
+    dependencies:
+      '@babel/runtime': 7.20.1
+      invariant: 2.2.4
+      prop-types: 15.8.1
+      react: 18.2.0
+      react-dom: 18.2.0_react@18.2.0
+    dev: false
+
   /react/18.2.0:
     resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==}
     engines: {node: '>=0.10.0'}

+ 1 - 1
src/api/board.ts

@@ -139,8 +139,8 @@ export enum BoardType {
 
 
 export const BoardTypeDesc = {
-  [BoardType.map]: '方位图',
   [BoardType.scene]: '现场图',
+  [BoardType.map]: '方位图',
 }
 
 export type SaveBoardProps = Pick<ExampleFile, 'caseId' | 'filesTitle'> & {

+ 10 - 0
src/api/files.ts

@@ -4,6 +4,7 @@ import {
   EXAMPLE_FILE_LIST,
   INSERT_EXAMPLE_FILE,
   DELETE_EXAMPLE_FILE,
+  UPDATE_EXAMPLE_FILE,
   UPLOAD_HEADS
  } from 'constant'
 
@@ -51,6 +52,15 @@ export const addExampleFile = (props: AddExampleFilesProps) =>
     data: jsonToForm(props),
   })
 
+export const updateExampleFile = async (props: Pick<ExampleFile, 'filesId' | 'filesTitle'>) => {
+  await axios({
+    url: UPDATE_EXAMPLE_FILE,
+    method: 'POST',
+    headers: UPLOAD_HEADS,
+    data: jsonToForm(props),
+  })
+}
+
 
 export type DeleteExampleFileProps = Pick<ExampleFile, 'caseId' | 'filesId'>
 export const deleteExampleFile = (props: DeleteExampleFileProps) => 

+ 41 - 0
src/components/edit-title/index.tsx

@@ -0,0 +1,41 @@
+
+import { useState } from "react"
+import { Button, Input, message } from "antd"
+import { CheckOutlined, EditOutlined } from "@ant-design/icons"
+import style from './style.module.scss'
+
+export const EditTitle = (props: {title: string, onChangeTitle: (title: string) => void}) => {
+  const [inEditMode, setInEditMode] = useState(false)
+  const [title, setTitle] = useState(props.title)
+  const enterHandler = async () => {
+    if (title.trim().length) {
+      if (title !== props.title) {
+        await props.onChangeTitle(title)
+      }
+      console.log('完成!')
+      setInEditMode(false)
+    } else {
+      message.warning('请输入标题')
+    }
+  }
+
+  const renderTitle = inEditMode
+    ? <Input.Group style={{width: '200px'}}>
+        <Input 
+          maxLength={50}
+          autoFocus
+          style={{ width: 'calc(100% - 40px)' }} 
+          value={title} 
+          onChange={ev => setTitle(ev.target.value)} 
+        />
+        <Button icon={<CheckOutlined />} onClick={enterHandler} />
+      </Input.Group>
+    : <div className={style['show-case-title']}>
+        <p>{props.title}</p>
+        <EditOutlined onClick={() => setInEditMode(true)} />
+      </div>
+
+  return renderTitle
+}
+
+export default EditTitle

+ 20 - 0
src/components/edit-title/style.module.scss

@@ -0,0 +1,20 @@
+
+.show-case-title {
+  display: flex;
+  align-items: center;
+
+  p {
+    margin-bottom: 0;
+    margin-right: 10px;
+  }
+
+  span {
+    opacity: 0;
+    transition: opacity .3s ease;
+    cursor: pointer;
+  }
+
+  &:hover span {
+    opacity: 1;
+  }
+}

+ 2 - 1
src/components/index.ts

@@ -3,4 +3,5 @@ export * from './route-menu'
 export * from './tabs'
 export * from './background'
 export * from './actions-button'
-export * from './table'
+export * from './table'
+export * from './edit-title'

+ 1 - 0
src/constant/api.ts

@@ -40,6 +40,7 @@ export const EXAMPLE_FILE_TYPE_LIST = `/fusion/caseFilesType/allList`
 export const EXAMPLE_FILE_LIST = `/fusion/caseFiles/allList`
 export const INSERT_EXAMPLE_FILE = `/fusion/caseFiles/add`
 export const DELETE_EXAMPLE_FILE = `/fusion/caseFiles/delete`
+export const UPDATE_EXAMPLE_FILE = `/fusion/caseFiles/updateTitle`
 export const INSERT_EXAMPLE_FILE_IMAGE = `/fusion/caseFiles/addOrUpdateImg`
 export const FETCH_FILE_INFO = '/fusion/caseFiles/info'
 

+ 26 - 0
src/utils/drawImage.ts

@@ -0,0 +1,26 @@
+export const drawImage = (
+  ctx: CanvasRenderingContext2D, 
+  bg_w: number, 
+  bg_h: number, 
+  imgPath: CanvasImageSource, 
+  imgWidth: number, 
+  imgHeight: number, 
+  x: number, 
+  y: number
+) => {
+  let dWidth = bg_w/imgWidth;  // canvas与图片的宽度比例
+  let dHeight = bg_h/imgHeight;  // canvas与图片的高度比例
+  if (imgWidth > bg_w && imgHeight > bg_h || imgWidth < bg_w && imgHeight < bg_h) {
+    if (dWidth > dHeight) {
+      ctx.drawImage(imgPath, 0, (imgHeight - bg_h/dWidth)/2, imgWidth, bg_h/dWidth, x, y, bg_w, bg_h)
+    } else {
+      ctx.drawImage(imgPath, (imgWidth - bg_w/dHeight)/2, 0, bg_w/dHeight, imgHeight, x, y, bg_w, bg_h)
+    }
+  } else {
+    if (imgWidth < bg_w) {
+      ctx.drawImage(imgPath, 0, (imgHeight - bg_h/dWidth)/2, imgWidth, bg_h/dWidth, x, y, bg_w, bg_h)
+    } else {
+      ctx.drawImage(imgPath, (imgWidth - bg_w/dHeight)/2, 0, bg_w/dHeight, imgHeight, x, y, bg_w, bg_h)
+    }
+  }
+}

+ 1 - 0
src/utils/index.ts

@@ -9,6 +9,7 @@ export * from './url'
 export * from './file-serve'
 export * from './only-open'
 export * from './transform'
+export * from './drawImage'
 
 // 字符串转params对象
 export const strToParams = (str: string) => {

+ 27 - 11
src/views/draw-file/modal.tsx

@@ -1,12 +1,13 @@
-import { Input, Modal, Transfer } from 'antd'
+import { Input, Modal } from 'antd'
 import { useEffect, useMemo, useRef, useState } from 'react'
 import AMapLoader from '@amap/amap-jsapi-loader';
 import style from './style.module.scss'
 import { SceneType, SceneTypeDomain, SceneTypePaths } from 'constant';
-import { base64ToBlob, getHref } from 'utils';
+import { base64ToBlob, getHref, drawImage } from 'utils';
 import { asyncLoading } from 'components/loading';
 import { RedoOutlined } from '@ant-design/icons';
 import { fetchTaggings } from 'api'
+import { SortTransfer } from './sort-transfer'
 
 import type { Tagging } from 'api'
 
@@ -148,7 +149,7 @@ const getFuseImage = async (iframe: HTMLIFrameElement) => {
   const fuseCnavas = targetWindow.document.querySelector('.scene-canvas > canvas') as HTMLElement
   
   if (fuseCnavas) {
-    const dataURL = await targetWindow.sdk.screenshot(540, 390)
+    const dataURL = await targetWindow.sdk.screenshot(targetIframe.offsetWidth, targetIframe.offsetHeight)
     const res = await fetch(dataURL)
     const blob = await res.blob()
     return { type: ImageType.FUSE, blob }
@@ -173,7 +174,6 @@ const getFuseImage = async (iframe: HTMLIFrameElement) => {
   }
 }
 
-
 export const SelectFuse = (props: SelectImageProps & { caseId: number }) => {
   const [open, setOpen] = useState(true)
   const [blob, setBlob] = useState<Blob | null>(null)
@@ -224,10 +224,11 @@ export const SelectFuse = (props: SelectImageProps & { caseId: number }) => {
       const hotItems = Array.from(contentDoc.querySelectorAll('.hot-item')) as HTMLDivElement[]
       hotItems.forEach(hot => {
         const hotTitle = (hot.querySelector('.tip') as HTMLDivElement).innerText
-        const index = addTags.findIndex(tag => tag.tagTitle === hotTitle)
+        const index = addTags.findIndex(tag => tag.tagTitle.trim() === hotTitle.trim())
         if (index !== -1) {
           const bound = hot.getBoundingClientRect()
-          const size = 32
+          const size = (img.width / 540) * 32
+
           const left = bound.left + size / 2
           const top = bound.top + size / 2
           ctx.save()
@@ -247,15 +248,30 @@ export const SelectFuse = (props: SelectImageProps & { caseId: number }) => {
           ctx.restore()
         }
       })
-
-      const blob = await new Promise<Blob | null>(resolve => $canvas.toBlob(resolve, 'png'))
+      
+      const $ccanvas = document.createElement('canvas')
+      $ccanvas.width = 540 
+      $ccanvas.height = 390
+      const cctx = $ccanvas.getContext('2d')!
+      drawImage(
+        cctx, 
+        $ccanvas.width,
+        $ccanvas.height,
+        $canvas,
+        img.width,
+        img.height,
+        0, 0
+      )
+      
+      const blob = await new Promise<Blob | null>(resolve => $ccanvas.toBlob(resolve, 'png'))
       setBlob(blob)
     }
   }
 
+
   return (
     <Modal 
-      width="1000px"
+      width="1500px"
       title="选择户型图" 
       open={open} 
       onCancel={() => setOpen(false)}
@@ -272,14 +288,14 @@ export const SelectFuse = (props: SelectImageProps & { caseId: number }) => {
           <div className={style['house-tags']}>
             <h4>请选择要同步到现场图的标注:</h4>  
             <div className={style['tagging-transfer']}>
-              <Transfer
+              <SortTransfer
                 dataSource={mockData}
                 titles={['所有', '需要']}
                 targetKeys={addTagIds}
                 selectedKeys={selectTags}
                 onChange={setAddTagIds}
                 onSelectChange={(sKeys, tKeys) => setSelectTags([...sKeys, ...tKeys])}
-                render={(item) => item.data.tagTitle}
+                onChangeSort={tags => setAddTagIds(tags.map(tag => tag.data.tagId.toString()))}
               />
             </div>
           </div>  

+ 74 - 0
src/views/draw-file/sort-transfer.tsx

@@ -0,0 +1,74 @@
+import { Checkbox, Transfer } from 'antd';
+import {
+  SortableContainer,
+  SortableElement,
+  SortableHandle,
+  arrayMove,
+} from 'react-sortable-hoc';
+import style from './style.module.scss'
+
+import type { Tagging } from 'api';
+import type { TransferItem, TransferProps } from 'antd/es/transfer';
+import type { ReactElement } from 'react';
+
+const Layout = ({children}: {children: any}) => <ul className={style['tagging-transfer']}>{children}</ul>;
+
+type ItemProps = { value: Tagging, checked: boolean, onChange: (checked: boolean) => void, append?: ReactElement }
+const Item = ({ value, append, checked, onChange }: ItemProps) => {
+  return (
+    <li className={style['sort-item'] + ' ' + (append ? style["sort-bar-parent"] : '')}>
+      <Checkbox checked={checked} onChange={(e) => onChange(e.target.checked)}>
+        {value.tagTitle}
+      </Checkbox>
+      { append && <span className={style['sort-bar']} children={append} /> }
+    </li>
+  )
+}
+
+const SortLayout = SortableContainer(Layout)
+const SortDrag = SortableHandle(() => <i className="iconfont icon-order" />);
+const SortItem = SortableElement((props: Omit<ItemProps, 'append'>) => <Item {...props} append={<SortDrag />} />)
+
+
+interface DataType {
+  key: string;
+  data: Tagging
+}
+
+interface TableTransferProps extends TransferProps<TransferItem & DataType> {
+  dataSource: DataType[];
+  onChangeSort?: (data: DataType[]) => void
+}
+
+
+// Customize Table Transfer
+export const SortTransfer = ({ onChangeSort, ...restProps }: TableTransferProps) => (
+  <Transfer {...restProps}>
+    {({
+      direction,
+      filteredItems,
+      onItemSelect,
+      selectedKeys: listSelectedKeys,
+    }) => {
+      const SLayout = direction === 'left' ? Layout : SortLayout
+      const SItem = direction === 'left' ? Item : SortItem
+      return (
+        <SLayout
+          onSortEnd={(data) => onChangeSort && onChangeSort(arrayMove(filteredItems, data.oldIndex, data.newIndex))}
+          useDragHandle
+          children={
+            filteredItems.map((item, index) => (
+              <SItem 
+                value={item.data} 
+                key={item.data.tagId} 
+                index={index} 
+                checked={listSelectedKeys.some(key => key === item.key)}
+                onChange={(check) => onItemSelect(item.key, check)}
+              />
+            ))
+          }
+        />
+      )
+    }}
+  </Transfer>
+);

+ 24 - 2
src/views/draw-file/style.module.scss

@@ -239,11 +239,12 @@ body {
 
 .house-layout {
   display: flex;
+  height: 556px;
 }
 
 .iframe-layout {
-  height: 390px;
-  flex: 0 0 540px;
+  height: 100%;
+  flex: 0 0 1040px;
   iframe {
     border: none;
     width: 100%;
@@ -302,4 +303,25 @@ body {
 .add-table-row {
   text-align: center;
   margin: 10px;
+}
+
+.tagging-transfer {
+  padding: 8px;
+}
+
+.sort-item {
+  list-style: none;
+  position: relative;
+  z-index: 2000;
+}
+
+.sort-bar-parent {
+  position: relative;
+  padding-right: 18px;
+  .sort-bar {
+    position: absolute;
+    top: 0;
+    right: 0;
+    cursor: pointer;
+  }
 }

+ 1 - 0
src/views/example/edit.tsx

@@ -9,6 +9,7 @@ export type EditExampleTitleProps = {
   example: Example
   onChangeTitle: (title: string) => void
 }
+
 export const EditExampleTitle = (props: EditExampleTitleProps) => {
   const [inEditMode, setInEditMode] = useState(false)
   const [title, setTitle] = useState(props.example.caseTitle)

+ 20 - 3
src/views/files/columns.tsx

@@ -1,6 +1,6 @@
 import { confirm, onlyOpenWindow } from "utils"
-import { ActionsButton } from 'components'
-import { deleteExampleFile } from 'api'
+import { ActionsButton, EditTitle } from 'components'
+import { deleteExampleFile, updateExampleFile } from 'api'
 import { fetchExampleFiles, useDispatch } from "store"
 
 import type { ExampleFile } from "api"
@@ -37,12 +37,29 @@ const OperActions = (data: ExampleFile) => {
   return <ActionsButton actions={actions} />
 }
 
+
+
 export const fileColumns: ColumnsType<ExampleFile> = [
   {
     width: '300px',
     title: '名称',
-    dataIndex: 'filesTitle',
+    // dataIndex: 'filesTitle',
     key: 'filesTitle',
+    render: (data: ExampleFile) => {
+      // eslint-disable-next-line react-hooks/rules-of-hooks
+      const dispatch = useDispatch()
+      if (data.imgType !== undefined && data.imgType !== null) {
+        return data.filesTitle
+      } else {
+        return <EditTitle 
+          title={data.filesTitle} 
+          onChangeTitle={async title => {
+            await updateExampleFile({ filesId: data.filesId, filesTitle: title })
+            dispatch(fetchExampleFiles({ caseId: Number(data.caseId) }))
+          }} 
+        />
+      }
+    }
   },
   {
     width: '200px',

+ 1 - 1
src/views/files/index.tsx

@@ -34,7 +34,7 @@ export const FileTable = (props: FileTableProps) => {
     return exampleTypeFiles(store).find(item => item.filesTypeId === props.type)?.children || []
   })
   const renderUpload = insert && <AddExampleFile {...props} onClose={() => setInser(false)} />
-  const renderRraws = props.type === DrawType && Object.keys(BoardTypeDesc).map((type) => (
+  const renderRraws = props.type === DrawType && [BoardType.scene, BoardType.map].map((type) => (
     <Button 
       key={type}
       type="primary"