bill vor 2 Jahren
Ursprung
Commit
372a5e131a

+ 5 - 1
src/api/scene.ts

@@ -11,6 +11,7 @@ import {
 
   UPLOAD_HEADS,
 
+  UPDATE_MODEL_TITLE,
   SCENE_LIST, 
   MODEL_SCENE_LIST, 
   UPLOAD_MODEL,
@@ -94,4 +95,7 @@ export const uploadModelScene = ({ file, progressCallback }: UploadModelScenePro
 
 // 删除模型文件
 export const deleteModelScene = (modelId: ServeModelScene['modelId']) => 
-  axios.post<undefined>(DELETE_MODEL, { modelId })
+  axios.post<undefined>(DELETE_MODEL, { modelId })
+
+export const updateModelSceneTitle = (modelId: ServeModelScene['modelId'],modelTitle: ServeModelScene['modelTitle']) => 
+axios.post<undefined>(UPDATE_MODEL_TITLE, { modelId, modelTitle })

+ 30 - 16
src/components/table/index.tsx

@@ -1,4 +1,4 @@
-import { Table as ATable, TableProps as ATableProps } from 'antd'
+import { Table as ATable, TableProps as ATableProps, Empty } from 'antd'
 
 import type { ColumnType, ColumnGroupType } from 'antd/es/table';
 import type { Paging } from 'hook'
@@ -8,23 +8,37 @@ export type TableProps<T> = Pick<ATableProps<T>, 'rowSelection'> & {
   columns: (ColumnGroupType<T> | ColumnType<T>)[],
   paging?: Paging,
   rowKey?: string,
+  unDataMsg?: string,
   onChangePaging?: (paging: Partial<Paging>) => void
 }
-export const Table = <T extends object>({ data, columns, paging, onChangePaging, rowKey = 'id', ...bind }: TableProps<T>) => {
-  return (
-    <ATable
-      columns={columns} 
-      dataSource={data} 
-      rowKey={rowKey}
-      {...bind}
-      pagination={paging ? {
-        showSizeChanger: false,
-        pageSize: paging.pageSize,
-        total: paging.total,
-        onChange: onChangePaging && ((page, pageSize) => onChangePaging({ pageNum: page, pageSize }))
-      } : false}
-    />
-  )
+export const Table = <T extends object>({ 
+  data, 
+  columns, 
+  unDataMsg = '暂无数据', 
+  paging, 
+  onChangePaging, 
+  rowKey = 'id', 
+  ...bind 
+}: TableProps<T>) => {
+  
+  return data.length 
+    ? (
+        <ATable
+          columns={columns} 
+          dataSource={data} 
+          rowKey={rowKey}
+          {...bind}
+          pagination={paging ? {
+            showSizeChanger: false,
+            pageSize: paging.pageSize,
+            total: paging.total,
+            onChange: onChangePaging && ((page, pageSize) => onChangePaging({ pageNum: page, pageSize }))
+          } : false}
+        />
+      )
+    : <div style={{padding: '1px'}}>
+        <Empty description={unDataMsg} image={Empty.PRESENTED_IMAGE_SIMPLE} className="ant-empty ant-empty-normal" />
+      </div>
 }
 
 export default Table

+ 9 - 8
src/components/tabs/index.tsx

@@ -3,8 +3,6 @@ import style from './style.module.scss'
 
 import type { ReactNode, Key } from 'react'
 
-const { TabPane: ATabPane } = ATabs
-
 export type TabsProps<T> = { 
   onChange: (type: string) => void, 
   active: T,
@@ -13,19 +11,22 @@ export type TabsProps<T> = {
 }
 
 export const Tabs = <T extends Key>({ onChange, active, renderContent, items }: TabsProps<T>) => {
-  const renderOptions =items.map(([key, value]) => (
-      <ATabPane tab={value} key={key} className={style['tab-panel']}>
-        {renderContent(key)}
-      </ATabPane>
-    ))
+  const renderOptions =items.map(([key, value]) => ({
+    label: value,
+    key: key.toString(),
+    children: renderContent(key)
+      // <ATabPane tab={value} key={key}>
+      //   {renderContent(key)}
+      // </ATabPane>
+    }))
 
   return (
     <div className={style['tab']}>
       <ATabs 
         onChange={key => onChange(key)} 
         activeKey={active.toString()}
+        items={renderOptions}
       >
-        {renderOptions}
       </ATabs>
     </div>
   )

+ 1 - 0
src/constant/api.ts

@@ -18,6 +18,7 @@ export const LOGOUT = `/fusion/fdLogout`
 
 // 场景列表
 export const SCENE_LIST = `/fusion/scene/list`
+export const UPDATE_MODEL_TITLE = `/fusion/model/updateTitle`
 
 
 // 三维模型

+ 7 - 3
src/index.tsx

@@ -4,6 +4,8 @@ import { AppRouter } from 'router'
 import { AppStore } from 'store'
 import './public.scss'
 import 'antd/dist/antd.less';
+import zhCN from 'antd/es/locale/zh_CN';
+import { ConfigProvider } from 'antd';
 
 const root = ReactDOM.createRoot(
   document.getElementById('root') as HTMLElement
@@ -11,8 +13,10 @@ const root = ReactDOM.createRoot(
 
 root.render(
   <React.StrictMode>
-    <AppStore>
-      <AppRouter />
-    </AppStore>
+    <ConfigProvider locale={zhCN}>
+      <AppStore>
+        <AppRouter />
+      </AppStore>
+    </ConfigProvider>
   </React.StrictMode>
 );

+ 4 - 3
src/views/example/edit.tsx

@@ -13,7 +13,7 @@ export const EditExampleTitle = (props: EditExampleTitleProps) => {
   const [inEditMode, setInEditMode] = useState(false)
   const [title, setTitle] = useState(props.example.caseTitle)
   const enterHandler = async () => {
-    if (title) {
+    if (title.trim().length) {
       if (title !== props.example.caseTitle) {
         await props.onChangeTitle(title)
       }
@@ -26,6 +26,7 @@ export const EditExampleTitle = (props: EditExampleTitleProps) => {
   const renderTitle = inEditMode
     ? <Input.Group style={{width: '200px'}}>
         <Input 
+          maxLength={50}
           autoFocus
           style={{ width: 'calc(100% - 40px)' }} 
           value={title} 
@@ -49,7 +50,7 @@ export type InsertExampleProps = {
 export const InsertExample = (props: InsertExampleProps) => {
   const [title, setTitle] = useState('')
   const enterHandler = async () => {
-    if (title) {
+    if (title.trim().length) {
       await props.onChangeTitle(title)
     } else {
       message.warning('请输入标题')
@@ -68,7 +69,7 @@ export const InsertExample = (props: InsertExampleProps) => {
     >
       <div className={style['add-case-title']}>
         <label htmlFor="case-title">案件名称</label>
-        <Input value={title} id="case-title" onChange={ev => setTitle(ev.target.value)} />
+        <Input value={title} id="case-title" maxLength={50} onChange={ev => setTitle(ev.target.value)} />
       </div>
     </Modal>
   )

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

@@ -12,6 +12,7 @@ export const ExampleHeader = ({ onSearch, onBeforeCreate }: ListHeaderProps) =>
     <div className='content-header'>
       <Button type="primary" children="创建案件" onClick={onBeforeCreate} />
       <Input.Search
+        allowClear
         className='content-header-search'
         placeholder="输入名称搜索" 
         onSearch={caseTitle => onSearch({ caseTitle }) }

+ 17 - 7
src/views/example/index.tsx

@@ -6,10 +6,10 @@ import { Dropdown, Menu, Button } from 'antd'
 import { DownOutlined } from '@ant-design/icons'
 import { useSelector, examplesSelector, fetchExamples } from 'store'
 import { EditExampleTitle, InsertExample } from './edit'
-import { setExample, repExampleScenes, deleteExample, getToken } from 'api'
+import { setExample, repExampleScenes, deleteExample, getToken, getExampleScenes } from 'api'
 import { ExampleScenes } from './scene/list'
 import { useState } from 'react'
-import { confirm, getHref, onlyOpenWindow } from 'utils'
+import { alert, confirm, getHref, onlyOpenWindow } from 'utils'
 import { ExampleFiles } from './files/list'
 import { SceneType, SceneTypeDomain, SceneTypePaths } from 'constant'
 
@@ -70,7 +70,7 @@ export const ExampleAction = ({ example, query, deleteExample, ...actionCallback
 export const ExamplePage = () => {
   const examples = useSelector(examplesSelector)
   const states = useThunkPaging({ caseTitle: '' }, fetchExamples)
-  const [[paging, setPaging], [, setParams], refresh] = states
+  const [[paging, setPaging], [params, setParams], refresh] = states
   const [scenesCaseId, setScenesCaseId] = useState<Example['caseId'] | null>(null)
   const [fileCaseId, setFileCaseId] = useState<Example['caseId'] | null>(null)
   const [inInsert, setInInsert] = useState(false)
@@ -81,6 +81,15 @@ export const ExamplePage = () => {
     }
     return getHref(SceneTypeDomain[SceneType.SWMX]!, SceneTypePaths[SceneType.SWMX][0], params)
   }
+  const checkScenesOpen = async (caseId: Example['caseId'], url: URL | string) => {
+    const scenes = await getExampleScenes({ caseId })
+    if (!scenes.length) {
+      alert('当前案件下无场景,请先添加场景。')
+    } else {
+      onlyOpenWindow(url)
+    }
+  }
+  
 
   const columns: ExampleColumn[] = [
     {
@@ -110,10 +119,10 @@ export const ExamplePage = () => {
           example={record}
           sceneManage={() => setScenesCaseId(record.caseId)}
           file={() => setFileCaseId(record.caseId)}
-          query={() => onlyOpenWindow(`${getFuseCodeLink(record.caseId)}#show/summary`)}
+          query={() => checkScenesOpen(record.caseId, `${getFuseCodeLink(record.caseId)}#show/summary`)}
           fuse={() => onlyOpenWindow(`${getFuseCodeLink(record.caseId)}#fuseEdit/merge`)}
-          getView={() => onlyOpenWindow(`${getFuseCodeLink(record.caseId)}#sceneEdit/view`)}
-          record={() => onlyOpenWindow(`${getFuseCodeLink(record.caseId)}#sceneEdit/record`)} 
+          getView={() => checkScenesOpen(record.caseId, `${getFuseCodeLink(record.caseId)}#sceneEdit/view`)}
+          record={() => checkScenesOpen(record.caseId, `${getFuseCodeLink(record.caseId)}#sceneEdit/record`)} 
         /> 
       )
     }
@@ -148,7 +157,8 @@ export const ExamplePage = () => {
         onBeforeCreate={() => setInInsert(true)}
         onSearch={setParams}
       />
-      <Table 
+      <Table
+        unDataMsg={params.caseTitle ? '未搜索到结果' : '暂无数据'} 
         rowKey={'caseId'}
         columns={columns} 
         data={examples}

+ 12 - 5
src/views/example/scene/list.tsx

@@ -29,7 +29,10 @@ export const ExampleScenes = (props: ExampleScenesProps) => {
       key: 'title',
       render: (_, scene) => SceneTypeDesc[scene.type]
     },
-    sceneTimeColumn, 
+    {
+      ...sceneTimeColumn,
+      title: '拍摄/创建时间'
+    }, 
     {
       title: '操作',
       key: 'action',
@@ -62,7 +65,10 @@ export const ExampleScenes = (props: ExampleScenesProps) => {
   ]
   const fetchExampleScenes = () => {
     getExampleScenes({ caseId: props.caseId })
-      .then(setScenes)
+      .then(scenes => {
+        setScenes(scenes)
+        !scenes.length && setInSelectMode(true)
+      })
   }
   const selectChange = async (newIdents: SceneIdents) => {
     await props.onChangeScenes(newIdents, idents)
@@ -82,10 +88,11 @@ export const ExampleScenes = (props: ExampleScenesProps) => {
       width="800px"
       title="案件场景管理" 
       visible={true} 
-      onOk={props.onClose} 
+      footer={null}
+      // onOk={props.onClose} 
       onCancel={props.onClose}
-      okText="确定"
-      cancelText="取消"
+      // okText="确定"
+      // cancelText="取消"
     >
       <div className={style['model-header']}>
         <Button type="primary" onClick={() => setInSelectMode(true)}>

+ 3 - 1
src/views/example/scene/select.tsx

@@ -49,7 +49,7 @@ export const SelectScenes = ({ sceneIdents, ...props }: SelectScenesProps) => {
   const Content = ({type}: {type: SceneType}) => {
     const scenes = useSelector((state) => filterScenesSelector(state, { type }))
     const states = useThunkPaging({ type, sceneName: '', status: QuoteSceneStatus.SUCCESS }, fetchScenes)
-    const [[paging, setPaging], [, setParams]] = states
+    const [[paging, setPaging], [params, setParams]] = states
     const selectedIds = getSelectIds(type, scenes)
     const refersh = useRefersh()
 
@@ -65,6 +65,7 @@ export const SelectScenes = ({ sceneIdents, ...props }: SelectScenesProps) => {
       <div>
         <div className={style['model-header']}>
           <Input.Search
+            allowClear
             className='content-header-search'
             placeholder="输入名称搜索" 
             onSearch={sceneName => setParams({ sceneName }) }
@@ -76,6 +77,7 @@ export const SelectScenes = ({ sceneIdents, ...props }: SelectScenesProps) => {
           rowSelection={rowSelection}
           data={scenes}
           paging={paging}
+          unDataMsg={params.sceneName ? '未搜索到结果' : '暂无数据'}
           onChangePaging={setPaging}
         />
       </div>

+ 26 - 5
src/views/login/index.tsx

@@ -1,5 +1,5 @@
 import { title } from 'constant'
-import { Input, Form, Button, Spin } from 'antd'
+import { Input, Form, Button, Spin, Checkbox } from 'antd'
 import { UserOutlined, LockOutlined } from '@ant-design/icons'
 import { Background } from 'components'
 import { useDispatch, postLogin } from 'store'
@@ -11,33 +11,54 @@ import type { LoginParams } from 'api'
 import type { FormItemProps } from 'antd'
 import type { ReactElement } from 'react'
 
-const loginInputs: (FormItemProps & { node: ReactElement })[] = [
+type LoginInfo = LoginParams & { markpsw: boolean }
+
+const KEY = '__MARK__'
+const setCache = (data: LoginInfo) => localStorage.setItem(KEY, JSON.stringify(data))
+const getCache = (): LoginInfo | undefined => localStorage.getItem(KEY) && JSON.parse(localStorage.getItem(KEY)!)
+const delCache = () => localStorage.removeItem(KEY)
+
+const getLoginInputs = (): (FormItemProps & { node: ReactElement })[] => ([
   {
     name: 'phoneNum',
+    initialValue: getCache()?.phoneNum,
     rules: [{ required: true, message: '请输入正确的手机号', pattern: /^1[3|4|5|7|8][0-9]\d{8}$/ }],
     node: <Input placeholder='请输入账号' size="large" prefix={<UserOutlined />} />
   },
   {
     name: 'password',
+    initialValue: getCache()?.password,
     rules: [{ required: true, message: '请输入正确的密码', pattern: /^[^\u4e00-\u9fa5]{1,16}$/, min: 1, max: 16 }],
     node: <Input.Password placeholder='请输入密码' size="large" prefix={<LockOutlined />} />
   },
   {
+    name: 'markpsw',
+    initialValue: getCache()?.markpsw,
+    valuePropName: 'checked',
+    node: <Checkbox>记住密码</Checkbox>
+  },
+  {
     node: <Button type='primary' htmlType='submit' size="large" children="登录" block />
   }
-]
+])
 
 export const Login = () => {
   const dispatch = useDispatch()
   const navigate = useNavigate()
   const [loading, setPromise] = useLoading()
 
-  const renderOptions = loginInputs.map(({node, ...props}, i) => (
+  const renderOptions = getLoginInputs().map(({node, ...props}, i) => (
     <Form.Item {...props} key={i} children={node} />
   ))
-  const loginHandler = async (data: LoginParams) => {
+  const loginHandler = async (data: LoginInfo) => {
     await setPromise(dispatch(postLogin(data)).unwrap())
     navigate(RoutePath.home, { replace: true })
+
+    if (data.markpsw) {
+      setCache(data)
+    } else {
+      delCache()
+    }
   }
 
   return (

+ 0 - 1
src/views/scene/columns.tsx

@@ -108,7 +108,6 @@ export const getSceneColumns = (type: SceneType, actionColumn: SceneColumn = sce
       ] as SceneColumn<Scene>[]
     case SceneType.SWMX:
       return [
-        sceneTitleColumn,
         modelSceneRawTypeColumn,
         modelSceneStatusColumn,
         actionColumn

+ 65 - 3
src/views/scene/content.tsx

@@ -1,9 +1,12 @@
 import { SceneType, ModelSceneStatus } from 'constant'
 import { Table, ActionsButton } from 'components'
-import { getSceneColumns, getSceneActions } from './columns'
+import { getSceneColumns, getSceneActions, sceneTitleColumn } from './columns'
 import { useThunkPaging } from 'hook'
 import { SceneHeader } from './header'
 import { confirm } from 'utils'
+import { message, Input, Button } from 'antd'
+import { CheckOutlined, EditOutlined } from '@ant-design/icons'
+import style from './style.module.scss'
 import { 
   useSelector, 
   filterScenesSelector,
@@ -11,14 +14,56 @@ import {
   deleteModelScene,
   useDispatch
 } from 'store'
+import { useState } from 'react'
+import { updateModelSceneTitle } from 'api'
 
 import type { SceneColumn } from './columns'
+import type { Scene } from 'api'
+
+export type EditModelSceneTitleProps = {
+  scene: Scene
+  onChangeTitle: (title: string) => void
+}
+
+export const EditModelSceneTitle = (props: EditModelSceneTitleProps) => {
+  const [inEditMode, setInEditMode] = useState(false)
+  const [title, setTitle] = useState(props.scene.title)
+  const enterHandler = async () => {
+    if (title.trim().length) {
+      if (title !== props.scene.title) {
+        await props.onChangeTitle(title)
+      }
+      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-scene-title']}>
+        <p>{props.scene.title}</p>
+        <EditOutlined onClick={() => setInEditMode(true)} />
+      </div>
+
+  return renderTitle
+}
+
 
 export const SceneTabContent = ({type}: {type: SceneType}) => {
   const dispatch = useDispatch()
   const scenes = useSelector((state) => filterScenesSelector(state, { type }))
   const states = useThunkPaging({ type, sceneName: '' }, fetchScenes)
-  const [[paging, setPaging], [, setParams], refresh] = states
+  const [[paging, setPaging], [params, setParams], refresh] = states
 
   const actionColumn: SceneColumn = {
     title: '操作',
@@ -43,6 +88,22 @@ export const SceneTabContent = ({type}: {type: SceneType}) => {
       return <ActionsButton actions={actions} /> 
     }
   }
+  const columns = getSceneColumns(type, actionColumn)
+
+  if (type === SceneType.SWMX) {
+    columns.unshift({
+      ...sceneTitleColumn,
+      render: (_, record) => (
+        <EditModelSceneTitle 
+          scene={record} 
+          onChangeTitle={async (title) => {
+            await updateModelSceneTitle(record.id, title)
+            await refresh()
+          }}
+        />
+      )
+    })
+  }
 
   return (
     <div className='content-layout'>
@@ -52,9 +113,10 @@ export const SceneTabContent = ({type}: {type: SceneType}) => {
         onDataChange={refresh} 
       />
       <Table 
-        columns={getSceneColumns(type, actionColumn)}
+        columns={columns}
         data={scenes}
         paging={paging}
+        unDataMsg={params.sceneName ? '未搜索到结果' : '暂无数据'}
         onChangePaging={setPaging}
       />
     </div>

+ 5 - 2
src/views/scene/header.tsx

@@ -1,6 +1,6 @@
 import { SceneType } from "constant"
 import { memo } from 'react'
-import { Button, Upload, message, Input } from 'antd'
+import { Button, Upload, message, Input, Popover } from 'antd'
 import { useDispatch, uploadModelScene } from 'store'
 
 import type { GetSceneByTypeParams, PagingRequest } from 'api'
@@ -32,7 +32,9 @@ export const SceneHeader = memo(({ readonly, type, onSearch, onDataChange }: Lis
 
   const renderUpload = readonly || (type === SceneType.SWMX && (
     <Upload beforeUpload={onUpload} multiple={false} accept="application/zip,application/rar">
-      <Button type="primary" children="上传数据" />
+      <Popover content="请上传zip/rar文件(支持osgb/obj/ply/las格式的数据),大小在1GB以内">
+        <Button type="primary" children="上传数据" />
+      </Popover>
     </Upload>
   ))
 
@@ -40,6 +42,7 @@ export const SceneHeader = memo(({ readonly, type, onSearch, onDataChange }: Lis
     <div className='content-header'>
       { renderUpload }
       <Input.Search
+        allowClear
         className='content-header-search'
         placeholder="输入名称搜索" 
         onSearch={sceneName => onSearch({ sceneName }) }

+ 20 - 0
src/views/scene/style.module.scss

@@ -0,0 +1,20 @@
+
+.show-scene-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;
+  }
+}