CzRger 3 місяців тому
батько
коміт
38e08cd023

+ 15 - 7
web/app/components/datasets/rename-modal/index.tsx

@@ -1,6 +1,7 @@
 'use client'
 
 import type { MouseEventHandler } from 'react'
+import { useEffect } from 'react'
 import { useState } from 'react'
 import { RiCloseLine } from '@remixicon/react'
 import { useContext } from 'use-context-selector'
@@ -14,6 +15,8 @@ import Modal from '@/app/components/base/modal'
 import { ToastContext } from '@/app/components/base/toast'
 import type { DataSet } from '@/models/datasets'
 import { updateDatasetSetting } from '@/service/datasets'
+import { useModalContext } from '@/context/modal-context'
+import { fetchTypes } from '@/service/common'
 
 type RenameDatasetModalProps = {
   show: boolean
@@ -32,12 +35,15 @@ const RenameDatasetModal = ({ show, dataset, onSuccess, onClose }: RenameDataset
   const [externalKnowledgeApiId, setExternalKnowledgeApiId] = useState<string>(dataset.external_knowledge_info.external_knowledge_api_id)
   const [type, setType] = useState<any>(dataset.type)
   const [options, setOptions] = useState<any>([])
-  setTimeout(() => {
-    const arr = []
-    for (let i = 1; i < 10; i++)
-      arr.push({ name: `选项${i}`, value: i })
-    setOptions(arr)
-  }, 1000)
+  useEffect(() => {
+    fetchTypes({
+      url: '/workspaces/123123',
+      params: {},
+    }).then((res: any) => {
+      setOptions(res.data.map((v: any) => ({ name: v.name, value: v.id })) || [])
+    })
+  }, [])
+
   const onConfirm: MouseEventHandler = async () => {
     if (!name.trim()) {
       notify({ type: 'error', message: t('datasetSettings.form.nameError') })
@@ -70,6 +76,7 @@ const RenameDatasetModal = ({ show, dataset, onSuccess, onClose }: RenameDataset
       setLoading(false)
     }
   }
+  const { setShowAccountSettingModal } = useModalContext()
 
   return (
     <Modal
@@ -87,7 +94,8 @@ const RenameDatasetModal = ({ show, dataset, onSuccess, onClose }: RenameDataset
             分类管理
             <div style={{
               color: '#1E98D7',
-            }} className='ml-3 cursor-pointer hover:opacity-75'>设置</div>
+            }} className='ml-3 cursor-pointer hover:opacity-75'
+            onClick={() => setShowAccountSettingModal({ payload: 'type' })}>设置</div>
           </div>
           <div className='w-full'>
             <SimpleSelect

+ 20 - 0
web/app/components/header/account-setting/index.tsx

@@ -2,6 +2,8 @@
 import { useTranslation } from 'react-i18next'
 import { useEffect, useRef, useState } from 'react'
 import {
+  RiBook3Fill,
+  RiBook3Line,
   RiBrain2Fill,
   RiBrain2Line,
   RiCloseLine,
@@ -13,6 +15,8 @@ import {
   RiGroup2Line,
   RiMoneyDollarCircleFill,
   RiMoneyDollarCircleLine,
+  RiPagesFill,
+  RiPagesLine,
   RiPuzzle2Fill,
   RiPuzzle2Line,
   RiTranslate2,
@@ -23,6 +27,8 @@ import LanguagePage from './language-page'
 import ApiBasedExtensionPage from './api-based-extension-page'
 import DataSourcePage from './data-source-page'
 import ModelProviderPage from './model-provider-page'
+import TypesPage from './types-page'
+import KnowledgesPage from './knowledges-page'
 import cn from '@/utils/classnames'
 import BillingPage from '@/app/components/billing/billing-page'
 import CustomPage from '@/app/components/custom/custom-page'
@@ -100,6 +106,18 @@ export default function AccountSetting({
         icon: <RiColorFilterLine className={iconClassName} />,
         activeIcon: <RiColorFilterFill className={iconClassName} />,
       },
+      {
+        key: 'type',
+        name: '类型管理',
+        icon: <RiPagesLine className={iconClassName} />,
+        activeIcon: <RiPagesFill className={iconClassName} />,
+      },
+      {
+        key: 'knowledge',
+        name: '知识服务',
+        icon: <RiBook3Line className={iconClassName} />,
+        activeIcon: <RiBook3Fill className={iconClassName} />,
+      },
     ].filter(item => !!item.key) as GroupItem[]
   })()
 
@@ -220,6 +238,8 @@ export default function AccountSetting({
               {activeMenu === 'api-based-extension' && <ApiBasedExtensionPage />}
               {activeMenu === 'custom' && <CustomPage />}
               {activeMenu === 'language' && <LanguagePage />}
+              {activeMenu === 'type' && <TypesPage />}
+              {activeMenu === 'knowledge' && <KnowledgesPage />}
             </div>
           </div>
         </div>

+ 4 - 0
web/app/components/header/account-setting/knowledges-page/detail-modal/index.module.css

@@ -0,0 +1,4 @@
+.modal {
+  padding: 24px 32px !important;
+  width: 400px !important;
+}

+ 121 - 0
web/app/components/header/account-setting/knowledges-page/detail-modal/index.tsx

@@ -0,0 +1,121 @@
+'use client'
+import { useCallback, useState } from 'react'
+import { RiCloseLine } from '@remixicon/react'
+import s from './index.module.css'
+import cn from '@/utils/classnames'
+import Modal from '@/app/components/base/modal'
+import Button from '@/app/components/base/button'
+import { addKnowledge, editKnowledge } from '@/service/common'
+import 'react-multi-email/dist/style.css'
+import Input from '@/app/components/base/input'
+import { SimpleSelect } from '@/app/components/base/select'
+
+const InviteModal = ({
+  transfer,
+  onCancel,
+  onSend,
+}: any) => {
+  const [serviceType, setServiceType] = useState<any>(transfer.row?.serviceType || '')
+  const [serviceName, setServiceName] = useState<string>(transfer.row?.serviceName || '')
+  const [url, setUrl] = useState<string>(transfer.row?.url || '')
+  const [method, setMethod] = useState<string>(transfer.row?.method || '')
+  const options = [
+    { name: '智能问答', value: 1 },
+    { name: '智能搜索', value: 2 },
+    { name: '智能推荐', value: 3 },
+  ]
+  const handleSave = useCallback(async () => {
+    try {
+      let res: any = () => {}
+      if (transfer.mode === 'add') {
+        res = await addKnowledge({
+          url: '/workspaces/123123',
+          body: { serviceType, serviceName, url, method },
+        })
+      }
+      else {
+        res = await editKnowledge({
+          url: '/workspaces/123123',
+          body: { id: transfer.id, serviceType, serviceName, url, method },
+        })
+      }
+      const { result }: any = res
+      console.log(result)
+      if (result === 'success') {
+        onCancel()
+        onSend()
+      }
+    }
+    catch (e) { }
+  }, [name, onCancel, onSend, transfer])
+
+  return (
+    <div className={cn(s.wrap)}>
+      <Modal overflowVisible isShow onClose={() => { }} className={cn(s.modal)}>
+        <div className='mb-2 flex justify-between'>
+          <div className='text-xl font-semibold text-text-primary'>{transfer.mode === 'add' ? '添加' : '编辑'}类型</div>
+          <RiCloseLine className='h-4 w-4 cursor-pointer text-text-tertiary' onClick={onCancel} />
+        </div>
+        <div>
+          <div className={cn('flex flex-wrap items-center justify-between')}>
+            <div className='shrink-0 py-2 text-sm font-medium leading-[20px] text-text-primary'>
+              服务类型
+            </div>
+            <div className='w-full'>
+              <SimpleSelect
+                defaultValue={serviceType}
+                onSelect={(i) => { setServiceType(i.value) }}
+                items={options}
+                allowSearch={false}
+              />
+            </div>
+          </div>
+          <div className={cn('flex flex-wrap items-center justify-between')}>
+            <div className='shrink-0 py-2 text-sm font-medium leading-[20px] text-text-primary'>
+              系统名称
+            </div>
+            <Input
+              value={serviceName}
+              onChange={e => setServiceName(e.target.value)}
+              className='h-9'
+              placeholder='请输入系统名称'
+            />
+          </div>
+          <div className={cn('flex flex-wrap items-center justify-between')}>
+            <div className='shrink-0 py-2 text-sm font-medium leading-[20px] text-text-primary'>
+              URL
+            </div>
+            <Input
+              value={url}
+              onChange={e => setUrl(e.target.value)}
+              className='h-9'
+              placeholder='请输入URL'
+            />
+          </div>
+          <div className={cn('flex flex-wrap items-center justify-between')}>
+            <div className='shrink-0 py-2 text-sm font-medium leading-[20px] text-text-primary'>
+              请求方式
+            </div>
+            <Input
+              value={method}
+              onChange={e => setMethod(e.target.value)}
+              className='h-9'
+              placeholder='请输入请求方式'
+            />
+          </div>
+          <Button
+            tabIndex={0}
+            className='mt-4 w-full'
+            onClick={handleSave}
+            disabled={!serviceType.length || !serviceName.length || !url.length || !method.length}
+            variant='primary'
+          >
+            保存
+          </Button>
+        </div>
+      </Modal>
+    </div>
+  )
+}
+
+export default InviteModal

+ 149 - 0
web/app/components/header/account-setting/knowledges-page/index.tsx

@@ -0,0 +1,149 @@
+'use client'
+import { useState } from 'react'
+import useSWR from 'swr'
+import dayjs from 'dayjs'
+import 'dayjs/locale/zh-cn'
+import relativeTime from 'dayjs/plugin/relativeTime'
+import DetailModal from './detail-modal'
+import { useContext } from 'use-context-selector'
+import { RiAddLine } from '@remixicon/react'
+import { useTranslation } from 'react-i18next'
+import { fetchKnowledges } from '@/service/common'
+import I18n from '@/context/i18n'
+import { useAppContext } from '@/context/app-context'
+import LogoEmbeddedChatHeader from '@/app/components/base/logo/logo-embedded-chat-header'
+import { useProviderContext } from '@/context/provider-context'
+import { Plan } from '@/app/components/billing/type'
+import Button from '@/app/components/base/button'
+import UpgradeBtn from '@/app/components/billing/upgrade-btn'
+import { NUM_INFINITE } from '@/app/components/billing/config'
+import { LanguagesSupported } from '@/i18n/language'
+import cn from '@/utils/classnames'
+dayjs.extend(relativeTime)
+
+const KnowledgesPage = () => {
+  const { t } = useTranslation()
+  const { locale } = useContext(I18n)
+  const ServiceTypeMap: any = {
+    1: '智能问答',
+    2: '智能搜索',
+    3: '智能推荐',
+  }
+  const StatusMap: any = {
+    0: '未启用',
+    1: '已启用',
+  }
+  const { userProfile, currentWorkspace, isCurrentWorkspaceOwner, isCurrentWorkspaceManager, systemFeatures } = useAppContext()
+  const { data, mutate }: any = useSWR(
+    {
+      url: '/workspaces/123123',
+      params: {},
+    },
+    fetchKnowledges,
+  )
+  const [detailModalVisible, setDetailModalVisible] = useState(false)
+  const [transfer, setTransfer] = useState<any>({
+    mode: 'add',
+    id: null,
+  })
+  const knowledgeList = data?.data || []
+  const { plan, enableBilling } = useProviderContext()
+  const isNotUnlimitedMemberPlan = enableBilling && plan.type !== Plan.team && plan.type !== Plan.enterprise
+  const isMemberFull = enableBilling && isNotUnlimitedMemberPlan && knowledgeList.length >= plan.total.teamMembers
+
+  return (
+    <>
+      <div className='flex flex-col'>
+        <div className='mb-4 flex items-center gap-3 rounded-xl border-l-[0.5px] border-t-[0.5px] border-divider-subtle bg-gradient-to-r from-background-gradient-bg-fill-chat-bg-2 to-background-gradient-bg-fill-chat-bg-1 p-3 pr-5'>
+          <LogoEmbeddedChatHeader className='!h-12 !w-12' />
+          <div className='grow'>
+            <div className='system-md-semibold text-text-secondary'>{currentWorkspace?.name}</div>
+            {enableBilling && (
+              <div className='system-xs-medium mt-1 text-text-tertiary'>
+                {isNotUnlimitedMemberPlan
+                  ? (
+                    <div className='flex space-x-1'>
+                      <div>{t('billing.plansCommon.member')}{locale !== LanguagesSupported[1] && knowledgeList.length > 1 && 's'}</div>
+                      <div className=''>{knowledgeList.length}</div>
+                      <div>/</div>
+                      <div>{plan.total.teamMembers === NUM_INFINITE ? t('billing.plansCommon.unlimited') : plan.total.teamMembers}</div>
+                    </div>
+                  )
+                  : (
+                    <div className='flex space-x-1'>
+                      <div>{knowledgeList.length}</div>
+                      <div>{t('billing.plansCommon.memberAfter')}{locale !== LanguagesSupported[1] && knowledgeList.length > 1 && 's'}</div>
+                    </div>
+                  )}
+              </div>
+            )}
+          </div>
+          {isMemberFull && (
+            <UpgradeBtn className='mr-2' loc='member-invite' />
+          )}
+          <Button variant='primary' className={cn('shrink-0')} disabled={!isCurrentWorkspaceManager || isMemberFull}
+            onClick={() => {
+              setTransfer({ mode: 'add', row: null })
+              setDetailModalVisible(true)
+            }}>
+            <RiAddLine className='mr-1 h-4 w-4' />
+            新增
+          </Button>
+        </div>
+        <div className='overflow-visible lg:overflow-visible'>
+          <div className='flex min-w-[480px] items-center border-b border-divider-regular py-[7px]'>
+            <div className='system-xs-medium-uppercase w-[100px] shrink-0 text-center text-text-tertiary'>服务类型</div>
+            <div className='system-xs-medium-uppercase shrink-0 grow text-center text-text-tertiary'>系统名称</div>
+            <div className='system-xs-medium-uppercase shrink-0 grow text-center text-text-tertiary'>URL</div>
+            <div className='system-xs-medium-uppercase w-[80px] shrink-0 text-center text-text-tertiary'>连接应用状态</div>
+            <div className='system-xs-medium-uppercase w-[160px] shrink-0 text-center text-text-tertiary'>更新时间</div>
+            <div className='system-xs-medium-uppercase w-[120px] shrink-0 px-3 text-center text-text-tertiary'>操作</div>
+          </div>
+          <div className='relative min-w-[480px]'>
+            {
+              knowledgeList.map((know: any) => (
+                <div key={know.id} className='flex justify-between border-b border-divider-subtle'>
+                  <div className='system-sm-regular w-[100px] shrink-0 py-2 text-center text-text-secondary'>{ServiceTypeMap[Number(know.serviceType)]}</div>
+                  <div className='system-sm-regular shrink-0 grow py-2 text-center text-text-secondary'>{know.serviceName}</div>
+                  <div className='system-sm-regular shrink-0 grow py-2 text-center text-text-secondary'>{know.url}</div>
+                  <div className='system-sm-regular w-[80px] shrink-0 py-2 text-center text-text-secondary'>{StatusMap[Number(know.status)]}</div>
+                  <div className='system-sm-regular w-[160px] shrink-0 py-2 text-center text-text-secondary'>{know.time}</div>
+                  <div className='flex w-[120px] shrink-0 items-center justify-center'>
+                    <Button variant='ghost-accent' size='small' className={cn('shrink-0')} disabled={!isCurrentWorkspaceManager || isMemberFull}
+                      onClick={() => {
+                        setTransfer({
+                          mode: 'edit',
+                          row: JSON.parse(JSON.stringify(know)),
+                        })
+                        setDetailModalVisible(true)
+                      }}>
+                      <RiAddLine className='mr-1 h-4 w-4' />
+                      编辑
+                    </Button>
+                    <Button variant='ghost' size='small' className={cn('shrink-0 text-red-600')} disabled={!isCurrentWorkspaceManager || isMemberFull} onClick={() => setDetailModalVisible(true)}>
+                      <RiAddLine className='mr-1 h-4 w-4' />
+                      刪除
+                    </Button>
+                  </div>
+                </div>
+              ))
+            }
+          </div>
+        </div>
+      </div>
+      {
+        detailModalVisible && (
+          <DetailModal
+            transfer={transfer}
+            onCancel={() => setDetailModalVisible(false)}
+            onSend={() => {
+              mutate()
+            }}
+          />
+        )
+      }
+    </>
+  )
+}
+
+export default KnowledgesPage

+ 4 - 0
web/app/components/header/account-setting/types-page/detail-modal/index.module.css

@@ -0,0 +1,4 @@
+.modal {
+  padding: 24px 32px !important;
+  width: 400px !important;
+}

+ 78 - 0
web/app/components/header/account-setting/types-page/detail-modal/index.tsx

@@ -0,0 +1,78 @@
+'use client'
+import { useCallback, useState } from 'react'
+import { RiCloseLine } from '@remixicon/react'
+import s from './index.module.css'
+import cn from '@/utils/classnames'
+import Modal from '@/app/components/base/modal'
+import Button from '@/app/components/base/button'
+import { addType, editType } from '@/service/common'
+import 'react-multi-email/dist/style.css'
+import Input from '@/app/components/base/input'
+
+const InviteModal = ({
+  transfer,
+  onCancel,
+  onSend,
+}: any) => {
+  const [name, setName] = useState<string>(transfer.row?.name || '')
+
+  const handleSave = useCallback(async () => {
+    try {
+      let res: any = () => {}
+      if (transfer.mode === 'add') {
+        res = await addType({
+          url: '/workspaces/123123',
+          body: { name },
+        })
+      }
+      else {
+        res = await editType({
+          url: '/workspaces/123123',
+          body: { id: transfer.id, name },
+        })
+      }
+      const { result }: any = res
+      console.log(result)
+      if (result === 'success') {
+        onCancel()
+        onSend()
+      }
+    }
+    catch (e) { }
+  }, [name, onCancel, onSend, transfer])
+
+  return (
+    <div className={cn(s.wrap)}>
+      <Modal overflowVisible isShow onClose={() => { }} className={cn(s.modal)}>
+        <div className='mb-2 flex justify-between'>
+          <div className='text-xl font-semibold text-text-primary'>{transfer.mode === 'add' ? '添加' : '编辑'}类型</div>
+          <RiCloseLine className='h-4 w-4 cursor-pointer text-text-tertiary' onClick={onCancel} />
+        </div>
+        <div>
+          <div className={cn('flex flex-wrap items-center justify-between py-4')}>
+            <div className='shrink-0 py-2 text-sm font-medium leading-[20px] text-text-primary'>
+              类型名称
+            </div>
+            <Input
+              value={name}
+              onChange={e => setName(e.target.value)}
+              className='h-9'
+              placeholder='请输入类型名称'
+            />
+          </div>
+          <Button
+            tabIndex={0}
+            className='w-full'
+            onClick={handleSave}
+            disabled={!name.length}
+            variant='primary'
+          >
+            保存
+          </Button>
+        </div>
+      </Modal>
+    </div>
+  )
+}
+
+export default InviteModal

+ 135 - 0
web/app/components/header/account-setting/types-page/index.tsx

@@ -0,0 +1,135 @@
+'use client'
+import { useState } from 'react'
+import useSWR from 'swr'
+import dayjs from 'dayjs'
+import 'dayjs/locale/zh-cn'
+import relativeTime from 'dayjs/plugin/relativeTime'
+import DetailModal from './detail-modal'
+import { useContext } from 'use-context-selector'
+import { RiAddLine } from '@remixicon/react'
+import { useTranslation } from 'react-i18next'
+import { fetchTypes } from '@/service/common'
+import I18n from '@/context/i18n'
+import { useAppContext } from '@/context/app-context'
+import LogoEmbeddedChatHeader from '@/app/components/base/logo/logo-embedded-chat-header'
+import { useProviderContext } from '@/context/provider-context'
+import { Plan } from '@/app/components/billing/type'
+import Button from '@/app/components/base/button'
+import UpgradeBtn from '@/app/components/billing/upgrade-btn'
+import { NUM_INFINITE } from '@/app/components/billing/config'
+import { LanguagesSupported } from '@/i18n/language'
+import cn from '@/utils/classnames'
+dayjs.extend(relativeTime)
+
+const TypesPage = () => {
+  const { t } = useTranslation()
+  const { locale } = useContext(I18n)
+
+  const { userProfile, currentWorkspace, isCurrentWorkspaceOwner, isCurrentWorkspaceManager, systemFeatures } = useAppContext()
+  const { data, mutate }: any = useSWR(
+    {
+      url: '/workspaces/123123',
+      params: {},
+    },
+    fetchTypes,
+  )
+  const [detailModalVisible, setDetailModalVisible] = useState(false)
+  const [transfer, setTransfer] = useState<any>({
+    mode: 'add',
+    id: null,
+  })
+  const typeList = data?.data || []
+  const { plan, enableBilling } = useProviderContext()
+  const isNotUnlimitedMemberPlan = enableBilling && plan.type !== Plan.team && plan.type !== Plan.enterprise
+  const isMemberFull = enableBilling && isNotUnlimitedMemberPlan && typeList.length >= plan.total.teamMembers
+
+  return (
+    <>
+      <div className='flex flex-col'>
+        <div className='mb-4 flex items-center gap-3 rounded-xl border-l-[0.5px] border-t-[0.5px] border-divider-subtle bg-gradient-to-r from-background-gradient-bg-fill-chat-bg-2 to-background-gradient-bg-fill-chat-bg-1 p-3 pr-5'>
+          <LogoEmbeddedChatHeader className='!h-12 !w-12' />
+          <div className='grow'>
+            <div className='system-md-semibold text-text-secondary'>{currentWorkspace?.name}</div>
+            {enableBilling && (
+              <div className='system-xs-medium mt-1 text-text-tertiary'>
+                {isNotUnlimitedMemberPlan
+                  ? (
+                    <div className='flex space-x-1'>
+                      <div>{t('billing.plansCommon.member')}{locale !== LanguagesSupported[1] && typeList.length > 1 && 's'}</div>
+                      <div className=''>{typeList.length}</div>
+                      <div>/</div>
+                      <div>{plan.total.teamMembers === NUM_INFINITE ? t('billing.plansCommon.unlimited') : plan.total.teamMembers}</div>
+                    </div>
+                  )
+                  : (
+                    <div className='flex space-x-1'>
+                      <div>{typeList.length}</div>
+                      <div>{t('billing.plansCommon.memberAfter')}{locale !== LanguagesSupported[1] && typeList.length > 1 && 's'}</div>
+                    </div>
+                  )}
+              </div>
+            )}
+          </div>
+          {isMemberFull && (
+            <UpgradeBtn className='mr-2' loc='member-invite' />
+          )}
+          <Button variant='primary' className={cn('shrink-0')} disabled={!isCurrentWorkspaceManager || isMemberFull}
+            onClick={() => {
+              setTransfer({ mode: 'add', row: null })
+              setDetailModalVisible(true)
+            }}>
+            <RiAddLine className='mr-1 h-4 w-4' />
+            新增
+          </Button>
+        </div>
+        <div className='overflow-visible lg:overflow-visible'>
+          <div className='flex min-w-[480px] items-center border-b border-divider-regular py-[7px]'>
+            <div className='system-xs-medium-uppercase grow px-3 text-text-tertiary'>类型名称</div>
+            <div className='system-xs-medium-uppercase w-[200px] shrink-0 text-center text-text-tertiary'>关联数据库数量</div>
+            <div className='system-xs-medium-uppercase w-[120px] shrink-0 px-3 text-center text-text-tertiary'>操作</div>
+          </div>
+          <div className='relative min-w-[480px]'>
+            {
+              typeList.map((type: any) => (
+                <div key={type.id} className='flex justify-between border-b border-divider-subtle'>
+                  <div className='system-sm-regular flex shrink-0 grow items-center px-3 py-2 text-text-secondary'>{type.name}</div>
+                  <div className='system-sm-regular w-[200px] shrink-0 items-center py-2 text-center text-text-secondary'>{type.relation}</div>
+                  <div className='flex w-[120px] shrink-0 items-center justify-center'>
+                    <Button variant='ghost-accent' size='small' className={cn('shrink-0')} disabled={!isCurrentWorkspaceManager || isMemberFull || Number(type.relation) > 0}
+                      onClick={() => {
+                        setTransfer({
+                          mode: 'edit',
+                          row: JSON.parse(JSON.stringify(type)),
+                        })
+                        setDetailModalVisible(true)
+                      }}>
+                      <RiAddLine className='mr-1 h-4 w-4' />
+                      编辑
+                    </Button>
+                    <Button variant='ghost' size='small' className={cn('shrink-0 text-red-600')} disabled={!isCurrentWorkspaceManager || isMemberFull} onClick={() => setDetailModalVisible(true)}>
+                      <RiAddLine className='mr-1 h-4 w-4' />
+                      刪除
+                    </Button>
+                  </div>
+                </div>
+              ))
+            }
+          </div>
+        </div>
+      </div>
+      {
+        detailModalVisible && (
+          <DetailModal
+            transfer={transfer}
+            onCancel={() => setDetailModalVisible(false)}
+            onSend={() => {
+              mutate()
+            }}
+          />
+        )
+      }
+    </>
+  )
+}
+
+export default TypesPage

+ 91 - 0
web/service/common.ts

@@ -101,6 +101,49 @@ export const fetchMembers: Fetcher<{ accounts: Member[] | null }, { url: string;
   return get<{ accounts: Member[] | null }>(url, { params })
 }
 
+export const fetchTypes = ({ url, params }) => {
+  // return get<{ accounts: Member[] | null }>(url, { params })
+  console.log('查询类型列表')
+  return new Promise((resolve) => {
+    setTimeout(() => {
+      const arr: any = []
+      for (let i = 1; i < 10; i++) {
+        arr.push({
+          id: i,
+          name: `类型${i}`,
+          relation: i % 2,
+        })
+      }
+      resolve({
+        data: arr,
+      })
+    }, 1000)
+  })
+}
+
+export const fetchKnowledges = ({ url, params }) => {
+  // return get<{ accounts: Member[] | null }>(url, { params })
+  console.log('查询知识服务')
+  return new Promise((resolve) => {
+    setTimeout(() => {
+      const arr: any = []
+      for (let i = 1; i < 10; i++) {
+        arr.push({
+          id: i,
+          serviceType: i % 3 + 1,
+          serviceName: `深圳口岸服务网${i}`,
+          url: '74.10.28.118',
+          status: i % 2,
+          time: '2021-09-22 14:22:22',
+        })
+      }
+      resolve({
+        data: arr,
+      })
+    }, 1000)
+  })
+}
+
 export const fetchProviders: Fetcher<Provider[] | null, { url: string; params: Record<string, any> }> = ({ url, params }) => {
   return get<Provider[] | null>(url, { params })
 }
@@ -128,6 +171,54 @@ export const deleteMemberOrCancelInvitation: Fetcher<CommonResponse, { url: stri
   return del<CommonResponse>(url)
 }
 
+export const addType = ({ url, body }) => {
+  // return post<InvitationResponse>(url, { body })
+  console.log('新增类型', body)
+  return new Promise((resolve) => {
+    setTimeout(() => {
+      resolve({
+        result: 'success',
+      })
+    }, 1000)
+  })
+}
+
+export const editType = ({ url, body }) => {
+  // return post<InvitationResponse>(url, { body })
+  console.log('编辑类型', body)
+  return new Promise((resolve) => {
+    setTimeout(() => {
+      resolve({
+        result: 'success',
+      })
+    }, 1000)
+  })
+}
+
+export const addKnowledge = ({ url, body }) => {
+  // return post<InvitationResponse>(url, { body })
+  console.log('新增知识', body)
+  return new Promise((resolve) => {
+    setTimeout(() => {
+      resolve({
+        result: 'success',
+      })
+    }, 1000)
+  })
+}
+
+export const editKnowledge = ({ url, body }) => {
+  // return post<InvitationResponse>(url, { body })
+  console.log('编辑知识', body)
+  return new Promise((resolve) => {
+    setTimeout(() => {
+      resolve({
+        result: 'success',
+      })
+    }, 1000)
+  })
+}
+
 export const fetchFilePreview: Fetcher<{ content: string }, { fileID: string }> = ({ fileID }) => {
   return get<{ content: string }>(`/files/${fileID}/preview`)
 }