Преглед на файлове

feat: 对接案件管理

bill преди 3 години
родител
ревизия
3cda340126

+ 1 - 0
package.json

@@ -16,6 +16,7 @@
     "antd": "^4.21.7",
     "axios": "^0.27.2",
     "canvas-nest.js": "^2.0.4",
+    "classnames": "^2.3.1",
     "craco-less": "^2.0.0",
     "js-base64": "^3.7.2",
     "lodash": "^4.17.21",

+ 37 - 4
src/api/example.ts

@@ -1,11 +1,25 @@
 import axios from './instance'
-import { EXAMPLE_SCENE_LIST } from 'constant'
+import { 
+  EXAMPLE_LIST,
+  SET_EXAMPLE,
+  DELETE_EXAMPLE,
+  EXAMPLE_SCENE_LIST,
+  REP_EXAMPLE_SCENES,
+ } from 'constant'
+
+import type { 
+  PagingRequest, 
+  PagingResult,
+  QuoteScene,
+  ModelScene
+} from 'api'
+
+import type { QuoteSceneType, ModelSceneType } from 'constant'
 
-import type { PagingRequest, PagingResult } from 'api'
 
 export interface Example {
   caseId: number
-  caseTitle: number
+  caseTitle: string
   createTime: string
   name: string
   tbStatus: string
@@ -16,4 +30,23 @@ export type Examples = Example[]
 
 export type GetExamplesParams = PagingRequest<{caseTitle?: string}>
 export const getExamples = (props: GetExamplesParams) => 
-  axios.post<PagingResult<Example[]>>(EXAMPLE_SCENE_LIST, props)
+  axios.post<PagingResult<Example[]>>(EXAMPLE_LIST, props)
+
+export type SetExampleParams = Pick<Example, 'caseTitle'> & Partial<Pick<Example, 'caseId'>>
+export const setExample = (props: SetExampleParams) =>
+  axios.post<undefined>(SET_EXAMPLE, props)
+
+export const deleteExample = (props: Pick<Example, 'caseId'>) =>
+  axios.post<undefined>(DELETE_EXAMPLE, props)
+
+export const getExampleScenes = (props: Pick<Example, 'caseId'>) => 
+  axios.post<QuoteScene[]>(EXAMPLE_SCENE_LIST, props)
+
+export type SceneIdents = Array<
+  { type: QuoteSceneType, numList: QuoteScene['num'][] } | 
+  { type: ModelSceneType, numList: ModelScene['id'][] }
+>
+
+export type RepExampleSceneParams = Pick<Example, 'caseId'> & { sceneNumParam: SceneIdents }
+export const repExampleScenes = (props: RepExampleSceneParams) =>
+  axios.post<undefined>(REP_EXAMPLE_SCENES, props)

+ 4 - 3
src/api/scene.ts

@@ -19,6 +19,7 @@ import {
 
 
 import type { PagingResult, PagingRequest } from '.'
+import type { QuoteSceneType, ModelSceneType } from 'constant'
 
 export interface BaseScene {
   id: string,
@@ -27,7 +28,7 @@ export interface BaseScene {
 }
 
 export interface QuoteScene extends BaseScene {
-  type: SceneType.SWKJ | SceneType.SWKK | SceneType.SWSS,
+  type: QuoteSceneType,
   status: QuoteSceneStatus,
   snCode: string,
   num: string
@@ -35,7 +36,7 @@ export interface QuoteScene extends BaseScene {
 
 export interface ModelScene extends BaseScene {
   status: ModelSceneStatus,
-  type: SceneType.SWMX,
+  type: ModelSceneType,
   rawType: string,
   progress?: number
 } 
@@ -56,7 +57,7 @@ export interface ServeModelScene {
 export type Scenes = Scene[]
 
 // 获取场景列表
-export type GetSceneByTypeParams = PagingRequest<{type: SceneType, sceneName?: string}>
+export type GetSceneByTypeParams = PagingRequest<{type: SceneType, sceneName?: string, status?: QuoteScene['status']}>
 export const getSceneByType = async (data: GetSceneByTypeParams): Promise<PagingResult<Scene[]>> => {
   if (data.type === SceneType.SWMX) {
     const modelScenes = await axios.post<PagingResult<ServeModelScene[]>>(MODEL_SCENE_LIST, data)

+ 7 - 6
src/components/table/index.tsx

@@ -1,27 +1,28 @@
-import { Table as ATable } from 'antd'
+import { Table as ATable, TableProps as ATableProps } from 'antd'
 
 import type { ColumnType, ColumnGroupType } from 'antd/es/table';
 import type { Paging } from 'hook'
 
-export type TableProps<T> = { 
+export type TableProps<T> = Pick<ATableProps<T>, 'rowSelection'> & { 
   data: T[],
   columns: (ColumnGroupType<T> | ColumnType<T>)[],
-  paging: Paging,
+  paging?: Paging,
   rowKey?: string,
   onChangePaging?: (paging: Partial<Paging>) => void
 }
-export const Table = <T extends object>({ data, columns, paging, onChangePaging, rowKey = 'id' }: TableProps<T>) => {
+export const Table = <T extends object>({ data, columns, paging, onChangePaging, rowKey = 'id', ...bind }: TableProps<T>) => {
   return (
     <ATable
       columns={columns} 
       dataSource={data} 
       rowKey={rowKey}
-      pagination={{
+      {...bind}
+      pagination={paging ? {
         showSizeChanger: false,
         pageSize: paging.pageSize,
         total: paging.total,
         onChange: onChangePaging && ((page, pageSize) => onChangePaging({ pageNum: page, pageSize }))
-      }}
+      } : false}
     />
   )
 }

+ 5 - 1
src/constant/api.ts

@@ -25,4 +25,8 @@ export const UPLOAD_MODEL = `/fusion/model/uploadObj`
 export const DELETE_MODEL = `/fusion/model/delete`
 
 // 案件
-export const EXAMPLE_SCENE_LIST = `/fusion/case/list`
+export const EXAMPLE_LIST = `/fusion/case/list`
+export const SET_EXAMPLE = `/fusion/case/addOrUpdate`
+export const DELETE_EXAMPLE = `/fusion/case/delete`
+export const EXAMPLE_SCENE_LIST = `/fusion/case/sceneList`
+export const REP_EXAMPLE_SCENES = `/fusion/case/addScene`

+ 3 - 0
src/constant/scene.ts

@@ -6,6 +6,9 @@ export enum SceneType {
   // QJKK
 }
 
+export type QuoteSceneType = SceneType.SWKJ | SceneType.SWKK | SceneType.SWSS
+export type ModelSceneType = SceneType.SWMX
+
 export const SceneTypeDesc: { [key in SceneType]: string }  = {
   [SceneType.SWKK]: '四维看看',
   [SceneType.SWKJ]: '四维看见',

+ 2 - 1
src/hook/index.ts

@@ -35,4 +35,5 @@ export const useLoading = () => {
   return [loading, setPromise] as const
 }
 
-export * from './paging'
+export * from './paging'
+export * from './refersh'

+ 5 - 3
src/hook/paging.ts

@@ -30,15 +30,17 @@ export const usePaging = <T extends any[], R>(
   const surplusParams = states.map(state => state[0])
   const refresh = () => {
     const allParams = [paging, ...surplusParams] as GetPagingDataParams<T>
-    getData(...allParams).then(data => {
+    return getData(...allParams).then(data => {
       if (paging.total !== data.total) {
         setPaging(paging => ({ ...paging, total: data.total }))
       }
     })
   }
 
-  // eslint-disable-next-line react-hooks/exhaustive-deps
-  useEffect(refresh, [...surplusParams, paging.pageNum, paging.pageSize])
+  useEffect(() => { 
+    refresh() 
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [...surplusParams, paging.pageNum, paging.pageSize])
 
   return [
     [paging, quickSetState(setPaging)],

+ 9 - 0
src/hook/refersh.ts

@@ -0,0 +1,9 @@
+import { useState } from 'react'
+
+export const useRefersh = () => {
+  const [refershCount, setRefershCount] = useState(0)
+
+  return () => {
+    setRefershCount(refershCount + 1)
+  }
+}

+ 4 - 0
src/public.scss

@@ -24,4 +24,8 @@ html, body, #root {
 
 .content-header-search {
   margin-left: 20px;
+}
+
+.ant-table-tbody > tr > td {
+  word-break: break-all;
 }

+ 26 - 2
src/store/scene.ts

@@ -15,7 +15,7 @@ import {
 } from 'constant'
 
 import type { ThunkState } from './help'
-import type { Scenes, Scene } from 'api'
+import type { Scenes, Scene, SceneIdents } from 'api'
 import type { StoreState } from './'
 
 export type SceneState = ThunkState<{value: Scenes}>
@@ -88,4 +88,28 @@ export const sceneIsSuccess = (scene: Scene) =>
 
 export const sceneIsError = (scene: Scene) => 
   (scene.type !== SceneType.SWMX && scene.status === QuoteSceneStatus.ERR)
-    || (scene.type === SceneType.SWMX && scene.status === ModelSceneStatus.ERR)
+    || (scene.type === SceneType.SWMX && scene.status === ModelSceneStatus.ERR)
+
+
+export const getSceneIdent = (scene: Scene) => {
+  if (scene.type === SceneType.SWMX) {
+    return scene.id
+  } else {
+    return scene.num
+  }
+}
+
+export const getScenesIdents = (scenes: Scene[]): SceneIdents => {
+  const typeIdents: SceneIdents = [
+    { type: SceneType.SWKJ, numList: [] },
+    { type: SceneType.SWKK, numList: [] },
+    { type: SceneType.SWMX, numList: [] },
+    { type: SceneType.SWSS, numList: [] }
+  ]
+
+  for (const scene of scenes) {
+    const typeIdent = typeIdents.find(ident => ident.type === scene.type)
+    typeIdent?.numList.push(getSceneIdent(scene))
+  }
+  return typeIdents
+}

+ 2 - 1
src/utils/index.ts

@@ -3,4 +3,5 @@ export * from './base'
 export * from './route'
 export * from './encode'
 export * from './setState'
-export * from './serve'
+export * from './serve'
+export * from './sys'

+ 34 - 0
src/utils/sys.ts

@@ -0,0 +1,34 @@
+import { Modal } from 'antd';
+
+const transform = (
+  api: 'success' | 'error' | 'warning' | 'confirm', 
+  defaultTitle = '系统提示',
+  defaultOkText = '确定',
+  defaultCancelText = '取消'
+) => {
+
+  return (
+    content: string,
+    title = defaultTitle,
+    okText = defaultOkText,
+    cancelText = defaultCancelText
+  ) => {
+    return new Promise<boolean>((resolve) => {
+      Modal[api]({
+        title,
+        content,
+        okText,
+        cancelText,
+        onOk() {
+          resolve(true)
+        },
+        onCancel() {
+          resolve(false)
+        }
+      })
+    })
+  }
+}
+
+export const alert = transform('warning')
+export const confirm = transform('confirm')

+ 5 - 57
src/views/example/columns.tsx

@@ -1,75 +1,23 @@
-
-import { ActionsButton } from 'components'
-import { Dropdown, Menu, Button } from 'antd'
-import { DownOutlined } from '@ant-design/icons'
-
 import type { Example } from 'api'
 import type { ColumnsType } from 'antd/es/table';
-import type { ColumnAction } from 'components'
-import type { MenuProps } from 'antd'
 
 
 export type ExampleColumn = ColumnsType<Example>[number]
 
-const titleColumn: ExampleColumn = {
+export const exampleTitleColumn: ExampleColumn = {
   title: '标题',
   dataIndex: 'caseTitle',
   key: 'caseTitle',
 }
 
-const timeColumn: ExampleColumn = {
+export const exampleTimeColumn: ExampleColumn = {
   title: '创建时间',
   dataIndex: 'createTime',
   key: 'createTime',
 }
 
-export const SceneAction = ({ example }: { example: Example }) => {
-  const actions: ColumnAction[] = [
-    { text: '查看', action() {} },
-    { text: '删除', action() {}, bind: { danger: true } }
-  ]
-  const menus = [
-    { key: 'sceneManage', label: '场景管理' },
-    { key: 'fuse', label: '多元融合' },
-    { key: 'getView', label: '试图提取' },
-    { key: 'record', label: '屏幕录制' },
-    { key: 'file', label: '卷宗管理' },
-  ]
-  const handleMenuClick: MenuProps['onClick']  = (menu) => {
-    switch(menu.key) {
-      case 'sceneManage': 
-        break;
-      case 'fuse': 
-        break;
-      case 'getView': 
-        break;
-      case 'record': 
-        break;
-      case 'file': 
-        break;
-    }
-  }
-
-  return (
-    <>
-      <Dropdown overlay={<Menu items={menus} onClick={handleMenuClick} />}>
-        <Button type="link" >
-          编辑<DownOutlined />
-        </Button>
-      </Dropdown>
-      <ActionsButton actions={actions} />
-    </>
-  )
-}
-
-export const actionColumn: ExampleColumn = {
-  title: '操作',
-  key: 'action',
-  render: (_, record) => <SceneAction example={record} /> 
-}
 
-export const exampleColumns = [
-  titleColumn, 
-  timeColumn,
-  actionColumn
+export const exampleColumns: ExampleColumn[] = [
+  exampleTitleColumn, 
+  exampleTimeColumn
 ]

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

@@ -0,0 +1,235 @@
+import { getExampleScenes } from 'api'
+import { useEffect, useState } from 'react'
+import { SceneType, QuoteSceneStatus, SceneTypeDesc } from 'constant';
+import { Table } from 'components'
+import { Modal, Button, Input, message } from 'antd'
+import { EditOutlined, CheckOutlined } from '@ant-design/icons'
+import { ScenePage, sceneTitleColumn, sceneTimeColumn, sceneActionColumn } from 'views/scene'
+import { fetchScenes, filterScenesSelector, useSelector, getScenesIdents, getSceneIdent } from 'store'
+import { useThunkPaging, useRefersh } from 'hook'
+import style from './style.module.scss'
+
+import type { Example, Scene, SceneIdents } from "api";
+import type { SceneColumn } from 'views/scene'
+
+export type ExampleScenesProps = Pick<Example, 'caseId'> & { 
+  onClose: () => void,
+  onChangeScenes: (newSceneIds: SelectScenesProps['sceneIdents'], oldSceneIds: SelectScenesProps['sceneIdents']) => void
+  // onAddScene: () => void
+}
+
+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)
+  const enterHandler = async () => {
+    if (title) {
+      if (title !== props.example.caseTitle) {
+        await props.onChangeTitle(title)
+      }
+      setInEditMode(false)
+    } else {
+      message.warning('请输入标题')
+    }
+  }
+
+  const renderTitle = inEditMode
+    ? <Input.Group style={{width: '200px'}}>
+        <Input 
+          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.example.caseTitle}</p>
+        <EditOutlined onClick={() => setInEditMode(true)} />
+      </div>
+
+  return renderTitle
+}
+
+
+export type InsertExampleProps = {
+  onChangeTitle: (title: string) => void
+  onClose: () => void
+}
+export const InsertExample = (props: InsertExampleProps) => {
+  const [title, setTitle] = useState('')
+  const enterHandler = async () => {
+    if (title) {
+      await props.onChangeTitle(title)
+    } else {
+      message.warning('请输入标题')
+    }
+  }
+
+  return (
+    <Modal 
+      width="400px"
+      title="添加案件" 
+      visible={true} 
+      onOk={enterHandler} 
+      onCancel={props.onClose}
+      okText="确定"
+      cancelText="取消"
+    >
+      <div className={style['add-case-title']}>
+        <label htmlFor="case-title">案件名称</label>
+        <Input value={title} id="case-title" onChange={ev => setTitle(ev.target.value)} />
+      </div>
+    </Modal>
+  )
+}
+
+export const ExampleScenes = (props: ExampleScenesProps) => {
+  const [scenes, setScenes] = useState<Scene[]>([])
+  const [inSelectMode, setInSelectMode] = useState(false)
+  const idents = getScenesIdents(scenes)
+  const columns: SceneColumn[] = [
+    sceneTitleColumn, 
+    {
+      title: '类型',
+      key: 'title',
+      render: (_, scene) => SceneTypeDesc[scene.type]
+    },
+    sceneTimeColumn, 
+    sceneActionColumn
+  ]
+  const fetchExampleScenes = () => {
+    getExampleScenes({ caseId: props.caseId })
+      .then(setScenes)
+  }
+  useEffect(fetchExampleScenes, [props.caseId])
+
+  const renderSelectMode = inSelectMode 
+    && <SelectScenes 
+        onClose={() => setInSelectMode(false)}
+        sceneIdents={idents}
+        onSelect={async newIdents => {
+          await props.onChangeScenes(newIdents, idents)
+          await fetchExampleScenes()
+          setInSelectMode(false)
+        }}
+      />
+  const renderSelf = (
+    <Modal 
+      width="800px"
+      title="案件场景管理" 
+      visible={true} 
+      onOk={props.onClose} 
+      onCancel={props.onClose}
+      okText="确定"
+      cancelText="取消"
+    >
+      <div className={style['model-header']}>
+        <Button type="primary" onClick={() => setInSelectMode(true)}>
+          添加场景
+        </Button>
+      </div>
+      <Table 
+        rowKey={'id'}
+        columns={columns} 
+        data={scenes}
+      />
+    </Modal>
+  )
+  
+  return <>
+    {renderSelectMode}
+    {renderSelf}
+  </>
+}
+
+export type SelectScenesProps = { 
+  sceneIdents: SceneIdents
+  onClose: () => void,
+  onSelect: (ids: SelectScenesProps['sceneIdents']) => void
+}
+export const SelectScenes = ({ sceneIdents, ...props }: SelectScenesProps) => {
+  let idents:SceneIdents = sceneIdents.map(ident => ({
+    ...ident,
+    numList: [...ident.numList]
+  }))
+  const getTypeIdents = (type: SceneType) => idents.find(ident => ident.type === type) as SceneIdents[number]
+
+  const getSelectIds = (type: SceneType, scenes: Scene[]) => {
+    const typeIdents = getTypeIdents(type)
+    const selectedIds = []
+    for (const scene of scenes) {
+      const sceneIdent = getSceneIdent(scene)
+      if (typeIdents.numList.includes(sceneIdent)) {
+        selectedIds.push(scene.id)
+      }
+    }
+    return selectedIds
+  }
+  const replaceIdents = (type: SceneType, scenes: Scene[], selectScenes: Scene[]) => {
+    const typeIdents = getTypeIdents(type)
+    for (const scene of scenes) {
+      const sceneIdent = getSceneIdent(scene)
+      const inSelect = selectScenes.includes(scene)
+      const identIndex = typeIdents?.numList.indexOf(sceneIdent)
+      if (~identIndex && !inSelect) {
+        typeIdents.numList.splice(identIndex, 1)
+      } else if (!~identIndex && inSelect) {
+        typeIdents.numList.push(sceneIdent)
+      }
+    }
+  }
+
+  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 selectedIds = getSelectIds(type, scenes)
+    const refersh = useRefersh()
+
+    const rowSelection: any = { 
+      selectedRowKeys: selectedIds, 
+      onChange(ids: string, selectScenes: Scene[]) {
+        replaceIdents(type, scenes, selectScenes)
+        refersh()
+      }
+    };
+    
+    return (
+      <div>
+        <div className={style['model-header']}>
+          <Input.Search
+            className='content-header-search'
+            placeholder="输入名称搜索" 
+            onSearch={sceneName => setParams({ sceneName }) }
+            style={{ width: 264 }} 
+          />
+        </div>
+        <Table 
+          columns={[sceneTitleColumn, sceneTimeColumn]}
+          rowSelection={rowSelection}
+          data={scenes}
+          paging={paging}
+          onChangePaging={setPaging}
+        />
+      </div>
+    )
+  }
+
+  return (
+    <Modal 
+      width="800px"
+      title="添加场景" 
+      visible={true} 
+      onOk={() => props.onSelect(idents)} 
+      onCancel={props.onClose}
+      okText="确定"
+      cancelText="取消"
+    >
+      <ScenePage TabContent={Content} />
+    </Modal>
+  )
+}

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

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

+ 114 - 9
src/views/example/index.tsx

@@ -1,27 +1,132 @@
 import { ExampleHeader } from './header'
-import { exampleColumns } from './columns'
-import { Table } from 'components'
+import { exampleTitleColumn, exampleTimeColumn } from './columns'
+import { Table, ActionsButton } from 'components'
 import { useThunkPaging } from 'hook'
-import { 
-  useSelector, 
-  examplesSelector,
-  fetchExamples,
-} from 'store'
+import { Dropdown, Menu, Button } from 'antd'
+import { DownOutlined } from '@ant-design/icons'
+import { useSelector, examplesSelector, fetchExamples } from 'store'
+import { ExampleScenes, EditExampleTitle, InsertExample } from './edit'
+import { setExample, repExampleScenes, deleteExample } from 'api'
+import { useState } from 'react'
+import { confirm } from 'utils'
 
+import type { ColumnAction } from 'components'
+import type { MenuProps } from 'antd'
+import type { Example } from 'api'
+import type { ExampleColumn } from './columns'
+
+export type ExampleActionProps = {
+  example: Example,
+  deleteExample: () => void
+  sceneManage?: (example: Example) => void
+  fuse?: (example: Example) => void
+  getView?: (example: Example) => void
+  record?: (example: Example) => void
+  file?: (example: Example) => void
+}
+
+export const ExampleAction = ({ example, deleteExample, ...actionCallback }: ExampleActionProps) => {
+  const actions: ColumnAction[] = [
+    { text: '查看', action() {} },
+    { text: '删除', action: deleteExample , bind: { danger: true } }
+  ]
+  const menus = [
+    { key: 'sceneManage', label: '场景管理' },
+    { key: 'fuse', label: '多元融合' },
+    { key: 'getView', label: '试图提取' },
+    { key: 'record', label: '屏幕录制' },
+    { key: 'file', label: '卷宗管理' },
+  ]
+  const handleMenuClick: MenuProps['onClick']  = (menu) => {
+    (actionCallback as any)[menu.key]();
+  }
+
+  for (let i = 0; i < menus.length; i++) {
+    if (!(menus[i].key in actionCallback)) {
+      menus.splice(i--, 1)
+    }
+  }
+
+  const renderMenus = menus.length 
+    ? (<Dropdown overlay={<Menu items={menus} onClick={handleMenuClick} />}>
+          <Button type="link" >
+            编辑<DownOutlined />
+          </Button>
+      </Dropdown>)
+    : null
+
+  return (
+    <>
+      {renderMenus}
+      <ActionsButton actions={actions} />
+    </>
+  )
+}
 
 export const ExamplePage = () => {
   const examples = useSelector(examplesSelector)
   const states = useThunkPaging({ caseTitle: '' }, fetchExamples)
-  const [[paging, setPaging], [, setParams]] = states
+  const [[paging, setPaging], [, setParams], refresh] = states
+  const [editId, setEditId] = useState<Example['caseId'] | null>(null)
+  const [inInsert, setInInsert] = useState(false)
+  const columns: ExampleColumn[] = [
+    {
+      ...exampleTitleColumn,
+      render: (_, record) => (
+        <EditExampleTitle 
+          example={record} 
+          onChangeTitle={async (caseTitle) => {
+            await setExample({ caseTitle, caseId: record.caseId })
+            await refresh()
+          }}
+        />
+      )
+    }, 
+    exampleTimeColumn,
+    {
+      title: '操作',
+      key: 'action',
+      render: (_, record) => (
+        <ExampleAction
+          deleteExample={async () => {
+            if (await confirm('确定要删除此数据?')) {
+              await deleteExample({ caseId: record.caseId })
+              await refresh()
+            }
+          }}
+          example={record}
+          sceneManage={() => setEditId(record.caseId)}
+        /> 
+      )
+    }
+  ]
+  const renderAddExample = inInsert 
+    && <InsertExample 
+        onClose={() => setInInsert(false)} 
+        onChangeTitle={async (caseTitle) => {
+          await setExample({ caseTitle })
+          await refresh()
+          setInInsert(false)
+        }}
+      />
+  const renderEditScene = editId 
+    && <ExampleScenes
+        caseId={editId} 
+        onClose={() => setEditId(null)} 
+        onChangeScenes={(idents) => repExampleScenes({ sceneNumParam: idents, caseId: editId })}
+      />
 
   return (
     <div className='content-layout'>
+      { renderAddExample }
+      { renderEditScene }
       <ExampleHeader 
+        onBeforeCreate={() => setInInsert(true)}
         onSearch={setParams}
       />
       <Table 
         rowKey={'caseId'}
-        columns={exampleColumns} 
+        columns={columns} 
         data={examples}
         paging={paging}
         onChangePaging={setPaging}

+ 34 - 0
src/views/example/style.module.scss

@@ -0,0 +1,34 @@
+.model-header {
+  display: flex;
+  justify-content: flex-end;
+  padding-bottom: 24px;
+}
+
+.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;
+  }
+}
+
+.add-case-title {
+  display: flex;
+  align-items: center;
+  label {
+    margin-right: 10px;
+    flex: none;
+  }
+}

+ 31 - 48
src/views/scene/columns.tsx

@@ -1,55 +1,49 @@
 import { Button } from 'antd'
-import { ColumnAction, ActionsButton } from 'components'
+import { openSceneEditPage, openSceneQueryPage, sceneIsSuccess } from 'store'
+import { ActionsButton } from 'components'
 import { 
   SceneType, 
   QuoteSceneStatusDesc, 
   ModelSceneStatusDesc, 
-  ModelSceneStatus 
+  ModelSceneStatus
 } from 'constant';
-import { 
-  openSceneEditPage, 
-  openSceneQueryPage, 
-  sceneIsSuccess,
-  deleteModelScene,
-  useDispatch
-} from 'store'
 
 import type { ModelScene, QuoteScene, Scene } from 'api'
 import type { ColumnsType } from 'antd/es/table';
+import type { ColumnAction } from 'components'
 
 export type SceneColumn<T = Scene> = ColumnsType<T>[number]
 
-export const titleColumn: SceneColumn = {
+export const sceneTitleColumn: SceneColumn = {
   title: '名称',
   dataIndex: 'title',
   key: 'title',
 }
 
-export const sncodeColumn: SceneColumn = {
+export const sceneSNcodeColumn: SceneColumn = {
   title: 'SN码',
   dataIndex: 'snCode',
   key: 'snCode',
 }
-export const timeColumn: SceneColumn = {
+export const sceneTimeColumn: SceneColumn = {
   title: '拍摄时间',
   dataIndex: 'createTime',
   key: 'createTime',
 }
-export const quoteStatusColumn: SceneColumn<QuoteScene> = {
+export const quoteSceneStatusColumn: SceneColumn<QuoteScene> = {
   title: '状态',
   dataIndex: 'status',
   key: 'status',
   render: (_, record) => QuoteSceneStatusDesc[record.status]
 }
 
-
-export const rawTypeColumn: SceneColumn<ModelScene> = {
+export const modelSceneRawTypeColumn: SceneColumn<ModelScene> = {
   title: '原始数据格式',
   dataIndex: 'rawType',
   key: 'rawType',
 }
 
-export const modelStatusColumn: SceneColumn<ModelScene> = {
+export const modelSceneStatusColumn: SceneColumn<ModelScene> = {
   title: '状态',
   dataIndex: 'status',
   key: 'status',
@@ -71,53 +65,42 @@ export const modelStatusColumn: SceneColumn<ModelScene> = {
   }
 }
 
-export const SceneAction = ({ scene }: { scene: Scene }) => {
-  const dispatch = useDispatch()
-  const actions: ColumnAction[] = []
-  if (sceneIsSuccess(scene)) {
-    actions.push(
-      { text: '查看', action: openSceneQueryPage.bind(null, scene) },
-      { text: '编辑', action: openSceneEditPage.bind(null, scene) }
-    )
-  }
-    
-  if (scene.type === SceneType.SWKK) {
-    actions.push({ text: '仿真', action: () => {} })
-  } else if (scene.type === SceneType.SWMX && scene.status !== ModelSceneStatus.RUN) {
-    actions.push({ 
-      text: '删除', 
-      action: () => dispatch(deleteModelScene(scene.id)), 
-      bind: { danger: true } 
-    })
-  }
-
-  return <ActionsButton actions={actions} />
+export const getSceneActions = (scene: Scene): ColumnAction[] => {
+  return sceneIsSuccess(scene) 
+    ? [
+        { text: '查看', action: openSceneQueryPage.bind(null, scene) },
+        { text: '编辑', action: openSceneEditPage.bind(null, scene) }
+      ]
+    : []
 }
 
-
-export const actionColumn: SceneColumn = {
+export const sceneActionColumn: SceneColumn = {
   title: '操作',
   key: 'action',
-  render: (_, record) => <SceneAction scene={record} /> 
+  render: (_, record) => {
+    const actions = getSceneActions(record)
+    return <ActionsButton actions={actions} /> 
+  }
 }
 
-export const getSceneColumns = (type: SceneType): SceneColumn<Scene>[] => {
+
+export const getSceneColumns = (type: SceneType, actionColumn: SceneColumn = sceneActionColumn): SceneColumn<Scene>[] => {
   switch(type) {
     case SceneType.SWKJ:
     case SceneType.SWSS:
     case SceneType.SWKK:
       return [
-        titleColumn,
-        sncodeColumn,
-        timeColumn,
-        quoteStatusColumn,
+        sceneTitleColumn,
+        sceneSNcodeColumn,
+        sceneTimeColumn,
+        quoteSceneStatusColumn,
         actionColumn
       ] as SceneColumn<Scene>[]
     case SceneType.SWMX:
       return [
-        titleColumn,
-        rawTypeColumn,
-        modelStatusColumn,
+        sceneTitleColumn,
+        modelSceneRawTypeColumn,
+        modelSceneStatusColumn,
         actionColumn
       ] as SceneColumn<Scene>[]
   }

+ 56 - 0
src/views/scene/content.tsx

@@ -0,0 +1,56 @@
+import { SceneType, ModelSceneStatus } from 'constant'
+import { Table, ActionsButton } from 'components'
+import { getSceneColumns, getSceneActions } from './columns'
+import { useThunkPaging } from 'hook'
+import { SceneHeader } from './header'
+import { 
+  useSelector, 
+  filterScenesSelector,
+  fetchScenes,
+  deleteModelScene,
+  useDispatch
+} from 'store'
+
+import type { SceneColumn } from './columns'
+
+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 actionColumn: SceneColumn = {
+    title: '操作',
+    key: 'action',
+    render: (_, scene) => {
+      const actions = getSceneActions(scene)
+      
+      if (scene.type === SceneType.SWKK) {
+        actions.push({ text: '仿真', action: () => {} })
+      } else if (scene.type === SceneType.SWMX && scene.status !== ModelSceneStatus.RUN) {
+        actions.push({ 
+          text: '删除', 
+          action: () => dispatch(deleteModelScene(scene.id)), 
+          bind: { danger: true } 
+        })
+      }
+      return <ActionsButton actions={actions} /> 
+    }
+  }
+
+  return (
+    <div className='content-layout'>
+      <SceneHeader 
+        type={type} 
+        onSearch={setParams} 
+        onDataChange={refresh} 
+      />
+      <Table 
+        columns={getSceneColumns(type, actionColumn)}
+        data={scenes}
+        paging={paging}
+        onChangePaging={setPaging}
+      />
+    </div>
+  )
+}

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

@@ -9,11 +9,12 @@ import type { UploadProps } from 'antd'
 
 type ListHeaderProps = {
   type: SceneType
+  readonly?: boolean
   onDataChange?: () => void,
   onSearch: (params: (GetSceneByTypeParams extends PagingRequest<infer T> ? Omit<T, 'type'> : never)) => void
 }
 
-export const SceneHeader = memo(({ type, onSearch, onDataChange }: ListHeaderProps) => {
+export const SceneHeader = memo(({ readonly, type, onSearch, onDataChange }: ListHeaderProps) => {
   const dispatch = useDispatch()
   const onUpload: UploadProps['beforeUpload'] = file => {
     const isZip = ['application/zip', 'application/rar'].includes(file.type)
@@ -26,17 +27,17 @@ export const SceneHeader = memo(({ type, onSearch, onDataChange }: ListHeaderPro
     return Upload.LIST_IGNORE
   }
 
-  const renderUpload = type === SceneType.SWMX && (
+  const renderUpload = readonly || (type === SceneType.SWMX && (
     <Upload beforeUpload={onUpload} multiple={false} >
       <Button type="primary" children="上传数据" />
     </Upload>
-  )
+  ))
 
   return (
     <div className='content-header'>
       { renderUpload }
       <Input.Search
-        className='content-table-search'
+        className='content-header-search'
         placeholder="输入名称搜索" 
         onSearch={sceneName => onSearch({ sceneName }) }
         style={{ width: 264 }} 

+ 11 - 34
src/views/scene/index.tsx

@@ -1,40 +1,14 @@
 import { SceneType, SceneTypeDesc } from 'constant'
 import { useStoreState } from 'hook'
-import { Tabs, Table } from 'components'
-import { getSceneColumns } from './columns'
-import { useThunkPaging } from 'hook'
-import { SceneHeader } from './header'
-import { 
-  useSelector, 
-  filterScenesSelector,
-  fetchScenes,
-} from 'store'
+import { SceneTabContent } from './content'
+import { Tabs } from 'components'
 
+import type { ComponentType } from 'react'
 
-const SceneTabContent = ({type}: {type: SceneType}) => {
-  const scenes = useSelector((state) => filterScenesSelector(state, { type }))
-  const states = useThunkPaging({ type, sceneName: '' }, fetchScenes)
-  const [[paging, setPaging], [, setParams], refresh] = states
-  const columns = getSceneColumns(type)
-
-  return (
-    <div className='content-layout'>
-      <SceneHeader 
-        type={type} 
-        onSearch={setParams} 
-        onDataChange={refresh} 
-      />
-      <Table 
-        columns={columns}
-        data={scenes}
-        paging={paging}
-        onChangePaging={setPaging}
-      />
-    </div>
-  )
+export type ScenePageProps = {
+  TabContent: ComponentType<{type: SceneType}>
 }
-
-const ScenePage = () => {
+export const ScenePage = ({ TabContent = SceneTabContent }: ScenePageProps) => {
   const [type, setType] = useStoreState(
     'scene-page-type', 
     SceneType.SWSS,
@@ -48,9 +22,12 @@ const ScenePage = () => {
       items={tabItems} 
       active={type} 
       onChange={type => setType(Number(type))} 
-      renderContent={type => <SceneTabContent type={type} />}
+      renderContent={type => <TabContent type={type} />}
     />
   )
 }
 
-export default ScenePage
+export default ScenePage
+export * from './header'
+export * from './columns'
+export * from './content'