index.tsx 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320
  1. import { Button, message, Tooltip } from 'antd'
  2. import { SortableContainer, SortableElement, SortableHandle } from 'react-sortable-hoc'
  3. import { arrayMoveImmutable } from 'array-move'
  4. import { useEffect, useRef, useState } from 'react'
  5. import { useRequest } from '@umijs/max'
  6. import { addDataSourceItem, queryDataSource, updateDataSourceItem } from '@/services/api/schema'
  7. import { CopyOutlined, MenuOutlined, PlusOutlined } from '@ant-design/icons'
  8. import { CopyToClipboard } from 'react-copy-to-clipboard'
  9. import { isEmpty, isNullOrUnDef } from '@/utils/is'
  10. import { ModalForm, ProFormRadio, ProFormText } from '@ant-design/pro-form'
  11. import { ModalType } from '@/utils/enum'
  12. import { PageContainer } from '@ant-design/pro-layout'
  13. import ProTable from '@ant-design/pro-table'
  14. import LeftMenu from './components/LeftMenu'
  15. import classNames from 'classnames'
  16. import type { ProFormInstance } from '@ant-design/pro-form'
  17. import type { ColumnsType } from 'antd/lib/table'
  18. import type { SortableContainerProps, SortEnd } from 'react-sortable-hoc'
  19. import './index.less'
  20. enum OptionModalType {
  21. ADD,
  22. UPDATE
  23. }
  24. const DragHandle = SortableHandle(() => <MenuOutlined style={{ cursor: 'grab', color: '#999' }} />)
  25. const SortableItem = SortableElement((props: React.HTMLAttributes<HTMLTableRowElement>) => <tr {...props} />)
  26. const SortableBody = SortableContainer((props: React.HTMLAttributes<HTMLTableSectionElement>) => (
  27. <tbody {...props} />
  28. ))
  29. type iState = {
  30. menuData: API.DataSourceMenuItem[]
  31. activeID: Nullable<string>
  32. menuDataTitle: Nullable<string>
  33. current: NonNullable<API.DataSourceItem>
  34. modalType: ModalType
  35. menuDataItems?: API.DataSourceItem[]
  36. }
  37. const Option = () => {
  38. const formRef = useRef<ProFormInstance>(null)
  39. const [state, setState] = useState<iState>({
  40. menuData: [],
  41. activeID: null,
  42. modalVisible: false,
  43. modalType: OptionModalType.ADD,
  44. menuDataItems: [],
  45. current: null,
  46. menuDataTitle: null,
  47. menuTitleUrl: null
  48. })
  49. const { run: tryFetchList } = useRequest(queryDataSource, {
  50. onSuccess: (result: API.DataSourceMenuItem) => {
  51. setState({
  52. ...state,
  53. menuData: result
  54. })
  55. }
  56. })
  57. const { run: tryAddDataSourceItem } = useRequest(
  58. (params: API.DataSourceItem) => addDataSourceItem(params),
  59. {
  60. manual: true,
  61. onSuccess: async () => {
  62. await tryFetchList()
  63. }
  64. }
  65. )
  66. const { run: tryUpdateDataSourceItem } = useRequest(
  67. (params: API.DataSourceItem) => updateDataSourceItem(params),
  68. {
  69. manual: true,
  70. onSuccess: async () => {
  71. await tryFetchList()
  72. }
  73. }
  74. )
  75. const onSelect = (key: string, node) => {
  76. setState({ ...state, activeID: key, menuDataItems: node.node.items })
  77. }
  78. const columns: ColumnsType<API.DataSourceItem> = [
  79. {
  80. title: '排序',
  81. dataIndex: 'sort',
  82. width: 50,
  83. render: () => <DragHandle />
  84. },
  85. {
  86. title: '序号',
  87. dataIndex: 'num',
  88. width: 50,
  89. render: (num, record, index) => `${index + 1}`
  90. },
  91. {
  92. title: '选项名称',
  93. dataIndex: 'name'
  94. },
  95. {
  96. title: '状态',
  97. dataIndex: 'enable',
  98. render: text => (
  99. <div className="flex items-center">
  100. <span
  101. className={classNames('w-3 h-3 rounded-1/2 inline-flex', text ? 'bg-green-500' : 'bg-red-500')}
  102. />
  103. <span className="ml-1">{text ? '已启用' : '已停用'}</span>
  104. </div>
  105. )
  106. },
  107. {
  108. title: '操作',
  109. dataIndex: 'opreate',
  110. render: (_, record) => (
  111. <div
  112. className="text-primary cursor-pointer hover:text-hex-967bbd"
  113. onClick={() => {
  114. setTimeout(() => {
  115. formRef.current?.setFieldsValue({ ...record })
  116. }, 80)
  117. setState({
  118. ...state,
  119. modalType: ModalType.UPDATE,
  120. modalVisible: true,
  121. current: {
  122. ID: record.ID,
  123. name: record.name,
  124. enable: record.enable
  125. }
  126. })
  127. }}>
  128. 编辑
  129. </div>
  130. )
  131. }
  132. ]
  133. const onSortEnd = ({ oldIndex, newIndex }: SortEnd) => {
  134. const { menuDataItems } = state
  135. if (oldIndex !== newIndex) {
  136. const newData = arrayMoveImmutable([].concat(menuDataItems), oldIndex, newIndex).filter(
  137. (el: DataType) => !!el
  138. )
  139. setState({ ...state, menuDataItems: newData })
  140. const oldIndexItem = menuDataItems?.find(item => item.index === oldIndex)
  141. const newIndexItem = menuDataItems?.find(item => item.index === newIndex)
  142. if (oldIndexItem && newIndexItem) {
  143. tryUpdateDataSourceItem({ ID: state.activeID, items: newData })
  144. message.success('编辑成功')
  145. }
  146. }
  147. }
  148. const DraggableContainer = (props: SortableContainerProps) => (
  149. <SortableBody
  150. useDragHandle
  151. disableAutoscroll
  152. helperClass="row-dragging"
  153. onSortEnd={onSortEnd}
  154. {...props}
  155. />
  156. )
  157. useEffect(() => {
  158. if (state.menuData.length) {
  159. setState({
  160. ...state,
  161. activeID: state.menuData[0].ID
  162. })
  163. }
  164. const menuTitleCopyUrl = `/api/form/v1/ds/items?name=${
  165. state.menuData.find(i => i.ID === state.activeID)?.name
  166. }`
  167. if (isNullOrUnDef(menuTitleCopyUrl) || isEmpty(menuTitleCopyUrl)) return null
  168. if (state.activeID) {
  169. setState({
  170. ...state,
  171. menuDataTitle: state.menuData.find(i => i.ID === state.activeID)?.name + `:` + menuTitleCopyUrl,
  172. menuTitleUrl: menuTitleCopyUrl,
  173. menuDataItems: state.menuData.find(item => item.ID === state.activeID)?.items
  174. })
  175. }
  176. }, [state.activeID, state.menuData])
  177. const DraggableBodyRow = ({ ...restProps }) => {
  178. if (state.menuDataItems?.length) {
  179. const index = state.menuDataItems?.findIndex(x => x.index === restProps['data-row-key'])
  180. return <SortableItem index={index} {...restProps} />
  181. }
  182. return <SortableItem index={0} {...restProps} />
  183. }
  184. const wrapHeight = document.querySelector('.ant-pro-page-container-warp')?.clientHeight || 0
  185. return (
  186. <PageContainer title={false}>
  187. <div className="h-full w-full flex flex-row">
  188. <LeftMenu
  189. onSelect={onSelect}
  190. defaultActiveID={state.activeID}
  191. showDelIcon={!state.menuDataItems?.length}
  192. options={state.menuData}
  193. initFn={() => tryFetchList()}
  194. />
  195. <div className="w-6/7 ml-8 bg-white p-4 rounded-20px shadow-hex-3e2c5a relative">
  196. {state.menuData.length ? (
  197. <ProTable<API.DataSourceItem>
  198. columns={columns}
  199. search={false}
  200. scroll={{
  201. y: document.body.clientHeight - (287 + wrapHeight)
  202. }}
  203. dataSource={state.menuDataItems || []}
  204. rowKey="index"
  205. toolbar={{
  206. title: state.activeID ? (
  207. <div className="max-w-400px truncate">{state.menuDataTitle}</div>
  208. ) : null,
  209. subTitle: state.activeID ? (
  210. <CopyToClipboard onCopy={() => message.success('复制成功')} text={state.menuTitleUrl}>
  211. <Tooltip placement="right" title="复制" className="ml-5px">
  212. <CopyOutlined />
  213. </Tooltip>
  214. </CopyToClipboard>
  215. ) : null,
  216. actions: [
  217. state.activeID ? (
  218. <Button
  219. key="btn-key"
  220. size="small"
  221. type="primary"
  222. onClick={() => {
  223. setState({
  224. ...state,
  225. modalType: OptionModalType.ADD,
  226. modalVisible: true
  227. })
  228. }}
  229. ghost>
  230. <PlusOutlined />
  231. 添加选项
  232. </Button>
  233. ) : null
  234. ]
  235. }}
  236. components={{
  237. body: {
  238. wrapper: DraggableContainer,
  239. row: DraggableBodyRow
  240. }
  241. }}
  242. />
  243. ) : null}
  244. <ModalForm
  245. labelCol={{ span: 6 }}
  246. key="form"
  247. width="30%"
  248. title={`${state.modalType === OptionModalType.ADD ? '添加' : '编辑'}选项`}
  249. formRef={formRef}
  250. layout="horizontal"
  251. visible={state.modalVisible}
  252. onVisibleChange={visible => {
  253. setState({ ...state, modalVisible: visible })
  254. setTimeout(() => {
  255. if (!visible) formRef.current?.resetFields()
  256. }, 80)
  257. }}
  258. initialValues={{ enable: true }}
  259. onFinish={async values => {
  260. try {
  261. if (state.modalType === OptionModalType.ADD) {
  262. await tryAddDataSourceItem({ ...values, dataSourceID: state.activeID })
  263. } else {
  264. const newItemData = state.menuDataItems?.map(item => {
  265. if (item.ID === values.ID) {
  266. const newItem = { ...values }
  267. return newItem
  268. }
  269. return item
  270. })
  271. await tryUpdateDataSourceItem({
  272. ID: state.activeID,
  273. items: newItemData
  274. })
  275. }
  276. message.success(`${state.modalType === OptionModalType.ADD ? '新增' : '编辑'}成功`)
  277. return true
  278. } catch (error) {
  279. message.error(error)
  280. return false
  281. }
  282. }}>
  283. <ProFormText name="ID" hidden />
  284. <ProFormText
  285. name="name"
  286. label="选项名称"
  287. rules={[{ required: true, message: '请输入选项名称' }]}
  288. />
  289. <ProFormRadio.Group
  290. name="enable"
  291. label="状态"
  292. options={[
  293. { label: '启用', value: true },
  294. { label: '停用', value: false }
  295. ]}
  296. rules={[{ required: true, message: '请选择启用/停用' }]}
  297. />
  298. </ModalForm>
  299. </div>
  300. </div>
  301. </PageContainer>
  302. )
  303. }
  304. export default Option