Browse Source

fix:题卡搜索

lvkun996 3 years ago
parent
commit
abb17664d0

+ 3 - 0
.eslintrc.js

@@ -5,4 +5,7 @@ module.exports = {
     page: true,
     REACT_APP_ENV: true,
   },
+  rules: {
+    "@typescript-eslint/no-use-before-define": " eslint-disable-next-line"
+  }
 };

+ 2 - 1
.vscode/settings.json

@@ -1,5 +1,6 @@
 {
   "editor.formatOnSave": true,
   "prettier.requireConfig": true,
-  "editor.defaultFormatter": "esbenp.prettier-vscode"
+  "editor.defaultFormatter": "esbenp.prettier-vscode",
+  "compile-hero.disable-compile-files-on-did-save-code": true
 }

+ 6 - 1
config/config.ts

@@ -9,14 +9,19 @@ import routes from './routes';
 const { REACT_APP_ENV } = process.env;
 
 export default defineConfig({
+  base: './',
+  publicPath: './',
   hash: true,
   antd: {},
   dva: {
     hmr: true,
   },
+  history: {
+    type: 'hash'
+  },
   layout: {
     // https://umijs.org/zh-CN/plugins/plugin-layout
-    locale: true,
+    locale: false,
     siderWidth: 208,
     ...defaultSettings,
   },

+ 1 - 1
config/defaultSettings.ts

@@ -12,7 +12,7 @@ const Settings: LayoutSettings & {
   fixedHeader: false,
   fixSiderbar: true,
   colorWeak: false,
-  title: 'Thinking Magic',
+  title: 'Think Magic',
   pwa: false,
   logo: 'https://s4.aconvert.com/convert/p3r68-cdx67/aydsj-vzrjo.svg',
   iconfontUrl: '',

+ 8 - 3
config/proxy.ts

@@ -9,12 +9,15 @@
 export default {
   dev: {
     // localhost:8000/api/** -> https://preview.pro.ant.design/api/**
-    '/api/': {
+    '/api': {
       // 要代理的地址
-      target: 'https://preview.pro.ant.design',
+      // target: 'https://open.luojigou.vip/',
+      target: 'http://192.168.1.149:38092',
+      // target: 'https://preview.pro.ant.design',
       // 配置了这个可以从 http 代理到 https
       // 依赖 origin 的功能可能需要这个,比如 cookie
       changeOrigin: true,
+      pathRewrite: { '^/api' : '' }
     },
   },
   test: {
@@ -26,9 +29,11 @@ export default {
   },
   pre: {
     '/api/': {
-      target: 'your pre url',
+      target: '',
       changeOrigin: true,
       pathRewrite: { '^': '' },
     },
   },
 };
+
+

+ 61 - 18
config/routes.ts

@@ -18,36 +18,79 @@
       },
     ],
   },
+  // {
+  //   path: '/welcome',
+  //   name: 'welcome',
+  //   hideInMenu: true,
+  //   icon: 'smile',
+  //   component: './Welcome',
+  // },
+  // {
+  //   path: '/admin',
+  //   name: 'admin',
+  //   icon: 'crown',
+  //   access: 'canAdmin',
+  //   component: './Admin',
+  //   routes: [
+  //     {
+  //       path: '/admin/sub-page',
+  //       name: 'sub-page',
+  //       icon: 'smile',
+  //       component: './Welcome',
+  //     },
+  //     {
+  //       component: './404',
+  //     },
+  //   ],
+  // },
+  // {
+  //   name: 'list.table-list',
+  //   icon: 'table',
+  //   path: '/list',
+  //   component: './TableList',
+  // },
   {
-    path: '/welcome',
-    name: 'welcome',
+    path: '/QuestionBankManage',
+    name: '题库',
     icon: 'smile',
-    component: './Welcome',
-  },
-  {
-    path: '/admin',
-    name: 'admin',
-    icon: 'crown',
     access: 'canAdmin',
-    component: './Admin',
     routes: [
       {
-        path: '/admin/sub-page',
-        name: 'sub-page',
+        path: '/QuestionBankManage/question-bank',
+        name: '学段',
         icon: 'smile',
-        component: './Welcome',
+        component: './QuestionBankManage/QuestionBank/index',
       },
+      {
+        path: '/QuestionBankManage/question-card',
+        name: '题卡',
+        icon: 'smile',
+        component: './QuestionBankManage/QuestionCard/index',
+      },
+      {
+        path: '/QuestionBankManage/question-book',
+        name: '教材',
+        icon: 'smile',
+        component: './QuestionBankManage/QuestionBook/index',
+      },
+      {
+        path: '/QuestionBankManage/opration-card',
+        name: '创建题卡',
+        icon: 'smile',
+        hideInMenu: true,
+        component: './QuestionBankManage',
+      },
+      // {
+      //   path: '/QuestionBankManage/Ability',
+      //   name: '能力点',
+      //   icon: 'smile',
+      //   component: './Ability',
+      // },
       {
         component: './404',
       },
     ],
   },
-  {
-    name: 'list.table-list',
-    icon: 'table',
-    path: '/list',
-    component: './TableList',
-  },
   {
     path: '/',
     redirect: '/welcome',

+ 13 - 13
mock/user.ts

@@ -30,21 +30,21 @@ const getAccess = () => {
 export default {
   // 支持值为 Object 和 Array
   'GET /api/currentUser': (req: Request, res: Response) => {
-    if (!getAccess()) {
-      res.status(401).send({
-        data: {
-          isLogin: false,
-        },
-        errorCode: '401',
-        errorMessage: '请先登录!',
-        success: true,
-      });
-      return;
-    }
+    // if (!getAccess()) {
+    //   res.status(401).send({
+    //     data: {
+    //       isLogin: false,
+    //     },
+    //     errorCode: '401',
+    //     errorMessage: '请先登录!',
+    //     success: true,
+    //   });
+    //   return;
+    // }
     res.send({
       success: true,
       data: {
-        name: 'Serati Ma',
+        name: 'Thinging Magic',
         avatar: 'https://gw.alipayobjects.com/zos/antfincdn/XAosXuNZyF/BiazfanxmamNRoxxVxka.png',
         userid: '00000001',
         email: 'antdesign@alipay.com',
@@ -120,7 +120,7 @@ export default {
   'POST /api/login/account': async (req: Request, res: Response) => {
     const { password, username, type } = req.body;
     await waitTime(2000);
-    if (password === 'ant.design' && username === 'admin') {
+    if (password === '123456' && username === 'admin') {
       res.send({
         status: 'ok',
         type,

+ 15 - 4
package.json

@@ -37,9 +37,15 @@
   "lint-staged": {
     "**/*.less": "stylelint --syntax less",
     "**/*.{js,jsx,ts,tsx}": "npm run lint-staged:js",
-    "**/*.{js,jsx,tsx,ts,less,md,json}": ["prettier --write"]
+    "**/*.{js,jsx,tsx,ts,less,md,json}": [
+      "prettier --write"
+    ]
   },
-  "browserslist": ["> 1%", "last 2 versions", "not ie <= 10"],
+  "browserslist": [
+    "> 1%",
+    "last 2 versions",
+    "not ie <= 10"
+  ],
   "dependencies": {
     "@ant-design/icons": "^4.7.0",
     "@ant-design/pro-card": "^1.19.0",
@@ -59,6 +65,7 @@
     "react-dev-inspector": "^1.7.0",
     "react-dom": "^17.0.0",
     "react-helmet-async": "^1.2.0",
+    "react-rnd": "^10.3.5",
     "umi": "^3.5.0"
   },
   "devDependencies": {
@@ -94,6 +101,10 @@
     "typescript": "^4.5.0",
     "umi-serve": "^1.9.10"
   },
-  "engines": { "node": ">=12.0.0" },
-  "gitHooks": { "commit-msg": "fabric verify-commit" }
+  "engines": {
+    "node": ">=12.0.0"
+  },
+  "gitHooks": {
+    "commit-msg": "fabric verify-commit"
+  }
 }

+ 1 - 1
src/access.ts

@@ -4,6 +4,6 @@
 export default function access(initialState: { currentUser?: API.CurrentUser } | undefined) {
   const { currentUser } = initialState ?? {};
   return {
-    canAdmin: currentUser && currentUser.access === 'admin',
+    // canAdmin: currentUser && currentUser.access === 'admin',
   };
 }

+ 4 - 4
src/app.tsx

@@ -9,7 +9,7 @@ import { currentUser as queryCurrentUser } from './services/ant-design-pro/api';
 import { BookOutlined, LinkOutlined } from '@ant-design/icons';
 import defaultSettings from '../config/defaultSettings';
 
-const isDev = process.env.NODE_ENV === 'development';
+// const isDev = process.env.NODE_ENV === 'development';
 const loginPath = '/user/login';
 
 /** 获取用户信息比较慢的时候会展示一个 loading */
@@ -62,9 +62,9 @@ export const layout: RunTimeLayoutConfig = ({ initialState, setInitialState }) =
     onPageChange: () => {
       const { location } = history;
       // 如果没有登录,重定向到 login
-      if (!initialState?.currentUser && location.pathname !== loginPath) {
-        history.push(loginPath);
-      }
+      // if (!initialState?.currentUser && location.pathname !== loginPath) {
+      //   history.push(loginPath);
+      // }
     },
     // links: isDev
     //   ? [

+ 24 - 0
src/common/AudioManage.ts

@@ -0,0 +1,24 @@
+class AudioManage {
+
+  // private audioCtx: AudioContext;
+  private audioNode: HTMLAudioElement 
+
+  constructor () {
+    this.audioNode = document.createElement("audio")
+    // this.audioCtx = new AudioContext();
+  }
+
+  async play (src: string) {
+    this.audioNode.src = src
+    const r = await this.audioNode.play()
+    console.log('播放完毕哦', r);
+  }
+
+  async pause () {
+    this.audioNode.pause()
+    // return await this.audioCtx.close()
+  }
+
+}
+
+export default new AudioManage()

+ 22 - 0
src/common/MessageManage.ts

@@ -0,0 +1,22 @@
+
+// class MessageManage {
+
+
+
+// }
+
+// export default new MessageManage()
+
+
+const addTextSuccess = '🎉新增成功'
+const addTextFail = '😭新增失败'
+
+const delTextSuccess = '🎉删除成功'
+const delTextFail = '😭删除失败'
+
+const editTextSuccess = '🎉编辑成功'
+const editTextFail = '😭编辑失败'
+
+const descText = {addTextSuccess, addTextFail, delTextSuccess, delTextFail, editTextSuccess, editTextFail}
+
+export { descText }

+ 3 - 0
src/common/global.less

@@ -0,0 +1,3 @@
+.text-canter {
+  text-align: center;
+}

+ 22 - 0
src/common/global.ts

@@ -0,0 +1,22 @@
+
+
+
+class Global {
+
+  private isDev: boolean = process.env.NODE_ENV === 'development';
+
+  private baseUrl: string = 'https://open.luojigou.vip/'
+
+  get getIsDev (): boolean {
+    return this.isDev
+  }
+
+  get getBaseUrl () {
+    return this.baseUrl
+  }
+
+}
+
+
+
+export default new Global()

+ 19 - 0
src/components/BaseTmp/index.tsx

@@ -0,0 +1,19 @@
+import React from 'react'
+import { PageContainer } from '@ant-design/pro-layout';
+import { Card } from 'antd'
+
+const BaseTmp: React.FC = ({
+  children
+}) => {
+
+  return  (
+    <PageContainer >
+      <Card >
+        {children}
+      </Card>
+    </PageContainer>
+  )
+
+}
+
+export default BaseTmp

+ 2 - 2
src/components/Footer/index.tsx

@@ -15,8 +15,8 @@ const Footer: React.FC = () => {
       copyright={`${currentYear} ${defaultMessage}`}
       links={[
         {
-          key: 'thinking magic design',
-          title: 'thinking magic design',
+          key: 'think magic design',
+          title: 'thinkg magic design',
           href: 'https://www.zaojiao.net',
           blankTarget: true,
         }

+ 18 - 0
src/components/RightClick/index.less

@@ -0,0 +1,18 @@
+#My-Menu {
+}
+
+.My-Menu {
+  width: 125px;
+  background-color: #fff;
+  border: 1px solid #eee;
+  box-sizing: border-box;
+  .col {
+    display: flex;
+    align-items: center;
+    padding-left: 8px;
+  }
+
+  .col:hover {
+    background-color: #ececec;
+  }
+}

+ 83 - 0
src/components/RightClick/index.tsx

@@ -0,0 +1,83 @@
+import React, { useState } from 'react'
+import { Row, Col } from 'antd'
+import styles from './index.less'
+import classNames from 'classnames';
+console.log(styles['My-Menu']);
+
+interface IProps {
+  render?: boolean,
+  deleteImg: (imgUrl: string) => void
+}
+
+const RightClick: React.FC<IProps> = ({
+  render = false,
+  deleteImg
+}) => {
+
+  const [ imgUrl, setImgUrl ] = useState<string>('')
+
+  const closeMyMenu = () => {
+
+    let menu = document.getElementById("My-Menu") as HTMLElement
+    
+    if (!menu) return
+    menu.style.display = 'none';
+
+    window.oncontextmenu = () => true
+  }
+
+  if (render) {
+
+    window.oncontextmenu = function (e) {
+
+      console.log('oncontextmenu', e);
+      setImgUrl(e?.target?.src)
+      e.preventDefault();
+
+      let menu = document.getElementById("My-Menu") as HTMLElement;
+
+      console.log(e.clientX, e.clientY);
+      
+      menu.style.position = 'fixed'
+
+      menu.style.left = e.clientX + 'px';
+
+      menu.style.top = e.clientY + 'px';
+
+      menu.style.display = 'block'
+
+      menu.style.zIndex = `1000`
+
+    }
+
+    window.onclick = function (e) {
+
+      closeMyMenu()
+
+    }
+
+  } else {
+    closeMyMenu()
+  }
+
+
+  const MyMenu = classNames( styles['My-Menu'] )
+
+  const RenderMenu = () => {
+    return  <Row id='My-Menu' style={{width: '125px', height: '30px', position: 'absolute', left: 0, top: 0}} className={MyMenu} >
+              <Col span={24} style={{height: '30px'}} className={styles.col}  onClick={(e) => deleteImg(imgUrl)}>
+                <span style={{color: '#000', width: `100%`, cursor: "pointer"}} >删除</span>
+              </Col>
+            </Row>
+  }
+
+  return <>
+    {
+      render ? <RenderMenu /> : null
+    }
+  </>
+}
+
+export default RightClick
+
+

+ 0 - 14
src/components/RightContent/AvatarDropdown.tsx

@@ -70,20 +70,6 @@ const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({ menu }) => {
 
   const menuHeaderDropdown = (
     <Menu className={styles.menu} selectedKeys={[]} onClick={onMenuClick}>
-      {/* {menu && (
-        <Menu.Item key="center">
-          <UserOutlined />
-          个人中心
-        </Menu.Item>
-      )}
-      {menu && (
-        <Menu.Item key="settings">
-          <SettingOutlined />
-          个人设置
-        </Menu.Item>
-      )}
-      {menu && <Menu.Divider />} */}
-
       <Menu.Item key="logout">
         <LogoutOutlined />
         退出登录

+ 0 - 21
src/components/RightContent/index.tsx

@@ -23,28 +23,7 @@ const GlobalHeaderRight: React.FC = () => {
   }
   return (
     <Space className={className}>
-      {/* <HeaderSearch
-        className={`${styles.action} ${styles.search}`}
-        placeholder="站内搜索"
-        defaultValue="umi ui"
-        options={[
-          { label: <a href="https://umijs.org/zh/guide/umi-ui.html">umi ui</a>, value: 'umi ui' },
-          {
-            label: <a href="next.ant.design">Ant Design</a>,
-            value: 'Ant Design',
-          },
-          {
-            label: <a href="https://protable.ant.design/">Pro Table</a>,
-            value: 'Pro Table',
-          },
-          {
-            label: <a href="https://prolayout.ant.design/">Pro Layout</a>,
-            value: 'Pro Layout',
-          },
-        ]}
-      /> */}
       <Avatar />
-      {/* <SelectLang className={styles.action} /> */}
     </Space>
   );
 };

+ 4 - 0
src/components/UploadImage/index.less

@@ -0,0 +1,4 @@
+.ant-upload.ant-upload-select-picture-card {
+  width: 80px;
+  height: 80px;
+}

+ 91 - 0
src/components/UploadImage/index.tsx

@@ -0,0 +1,91 @@
+import React, { useEffect, useRef, useState } from 'react'
+
+import { Row, Col, Upload, message, UploadProps} from 'antd'
+
+import { LoadingOutlined, PlusOutlined } from '@ant-design/icons';
+
+import { uploadImage } from '@/services/api/upload'
+
+import Global from '@/common/global'
+import './index.less'
+
+interface IProps {
+  imageUrl: string,
+  saveImageUrl: (imgUrl: string) => void,
+  uploadCount?: number // -1 代表可反复上传 0 代表不能上传 > 1 代表上传次数
+}
+
+const UploadImage: React.FC<IProps> = ({
+  saveImageUrl,
+  imageUrl,
+  uploadCount = -1
+}) => {
+  
+  const [ loading, setLoading ]  = useState<boolean>(false)
+  const uploadCountRef = useRef<number>(uploadCount)
+  
+  useEffect( () => {
+    imageUrl && setLoading(false)
+  }, [imageUrl])
+
+  const action = Global.getIsDev ? '/api' : Global.getBaseUrl
+
+  const allowClick = uploadCountRef.current !== 0
+
+  const beforeUpload = (e: any) => {
+  }
+
+  const handleChange = (info: { file: { status: string; originFileObj: any; response: any }; }) => {
+    if (info.file.status === 'uploading') {
+      setLoading(true)
+      return;
+    }
+    if (info.file.status === 'done') {
+      if (info.file.response.status === 200) {
+        saveImageUrl(info.file.response.result)
+        changeUploadCount()
+      }
+    }
+  }
+
+  /** 上传次数修改 */
+  const changeUploadCount = () => {
+    if (uploadCountRef.current >= 1) {
+      uploadCountRef.current--
+    }
+  }
+
+  const uploadLoadingButton = (
+    <div style={{display: "flex", flexDirection: 'column', justifyContent: 'center', alignItems: 'center'}} >
+      {loading ? <LoadingOutlined /> : <PlusOutlined />}
+      <div style={{ marginTop: 8 }}>上传答案</div>
+    </div>
+  );
+  return (
+    <Row style={{width: '100%'}} > 
+    {/* style={{ width: '80px', height: '80px',border: '1px solid #000', display: "flex", justifyContent: 'center', alignItems: 'center' }} */}
+      <Col span={24} >
+          <Upload
+              openFileDialogOnClick={allowClick}
+              style={{width: '80px', height: '80px'}}
+              name="file"
+              listType="picture-card"
+              className="avatar-uploader"
+              showUploadList={false}
+              action={ action  + '/gamecontest/admin/file/uploadImage' }
+              data={{
+                token: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiIxMzY2NzAwNTY3OTUzMzYyOTQ2IiwiZXhwIjoxNjIxODI1MDEzfQ.8LLmL6Mfb_pQxLjDmM13ekzq7nnWGWwWuANLQHvF3CM',
+              }}
+              beforeUpload={beforeUpload}
+              onChange={handleChange}
+            >
+            {imageUrl ? <img src={imageUrl} alt="avatar" style={{ width: '80px' }} /> : uploadLoadingButton} 
+          </Upload>
+      </Col>
+    </Row>
+  )
+}
+
+export default UploadImage
+
+

+ 90 - 0
src/hooks/index.ts

@@ -0,0 +1,90 @@
+import { useEffect, useRef, useState } from 'react'
+
+/**
+ * @description 获取useState 上一次的值
+ * @returns 
+ */
+
+export function useStatePrevious<T>(): [ T | undefined, (value: T) => void ] {
+
+  const ref = useRef<T>();
+
+  const setValue = (value: T) => {
+    ref.current = value
+  }
+
+  return [ ref.current , setValue ]
+
+}
+
+/**
+ * 
+ * @description 同步useState 
+ * 
+ * @param { T } params 参数 
+ * 
+ * @return { Array }
+ * 
+ * @example 
+ *  
+ *  const [name , setName] = useStateSync<string>('怡宝')
+ *  
+ *  useEffect( () => {
+ *      setName('农夫山泉', (res) => {
+ *         console.log(res)  // 农夫山泉
+ *      })
+ *  }, [])
+ * 
+ */
+
+
+ export const useStateSync = <T>(params: T): Array<any> => {
+
+  const cbRef = useRef((_params: T) => {})
+
+  const [ state, setState ] = useState<T>(params)
+
+  useEffect( () => {
+      cbRef.current &&  cbRef.current(state)
+  }, [state] )
+
+
+  return [state,  (val: T, callback: (_params: T) => void): void => {
+      cbRef.current = callback;
+      setState(val);
+  }]
+}
+
+/**
+* @description setInterval hook
+* @param callback 执行的回调函数
+* @param delay 定时器的执行间隔,传入null 则停止计时器 
+* @example 
+*  useInterval( () => {
+*   setXxxx(x + 1)
+* }, x ? 1000 : null) 
+*/
+
+type callback = () => void
+
+export const useInterval = (
+callback: () => void, 
+delay: number | null
+) => {
+  const savedCallback = useRef<callback>();
+
+  useEffect(() => {
+    savedCallback.current = callback;
+  });
+
+  useEffect(() => {
+    function tick() {
+      savedCallback.current && savedCallback.current()
+    }
+    if (delay !== null) {
+      let id = setInterval(tick, delay);
+      return () => clearInterval(id);
+    }
+   
+  }, [delay]);
+}

+ 63 - 0
src/pages/Ability/index.tsx

@@ -0,0 +1,63 @@
+import React, { useState } from 'react'
+import BaseTmp from '@/components/BaseTmp'
+import { Button, Col, Divider, Modal, Row, Table } from 'antd'
+
+const Ability: React.FC = () => {
+
+  const [ dataSource, setDataSource  ] = useState()
+
+  const [ type, setType ] = useState<"edit" | "add">('add')
+
+  const title = type == 'edit' ? '编辑能力' : '新增能力'
+
+  const columns = [
+    {
+      title: '能力点',
+      dataIndex: 'able',
+      key: 'able'
+    },
+    {
+      title: '创建时间',
+      dataIndex: 'createTime',
+      key: 'createTime'
+    },
+    {
+      title: '操作',
+      dataIndex: 'action',
+      key: 'action',
+      render: () => (
+        <>
+          <a  >编辑</a>
+          <Divider type="vertical" />
+          <a  >删除</a>
+        </>
+      )
+    }
+  ]
+
+  return (
+    <BaseTmp>
+      <Row>
+        <Col
+          span={24}
+          style={{display: 'flex', justifyContent: 'flex-end'}}
+        >
+          <Button>新增能力</Button>
+        </Col>
+        <Col span={24} style={{marginTop: '20px'}} >
+          <Table  dataSource={dataSource}  columns={columns} />
+        </Col>
+      </Row>
+
+      <Modal
+        title={title}
+      >
+
+      </Modal>
+    </BaseTmp>
+  )
+}
+
+export default Ability
+
+

+ 197 - 0
src/pages/QuestionBankManage/QuestionBank/index.tsx

@@ -0,0 +1,197 @@
+import React, { useEffect, useState } from 'react'
+import { Row, Col, Table, Button, Divider, Switch, Modal, Input, Popconfirm, message } from 'antd'
+import BaseTmp from '@/components/BaseTmp'
+import api from '@/services/api/index'
+import { PlusOutlined } from '@ant-design/icons'
+import { descText } from '@/common/MessageManage' 
+
+const { 
+  getQuestionBank, 
+  addQuestionBank,
+  delQuestionBank,
+  putQuestionBank 
+} = api.question
+
+const QuestionBank: React.FC = () => {
+
+  const [ dataSource, setDataSource ] = useState([])
+
+  const [ isModalVisible, setIsModalVisible ] = useState<boolean>(false)
+
+  /**modal loading */
+  const [ confirmLoading, setConfirmLoading ] = useState<boolean>(false)
+
+  /**table loading */
+  const [ loading, setLoading ] = useState<boolean>(false)
+
+  /**编辑还是新增 */
+  const [ type, setType ] = useState<'edit' | 'add'>('add')
+
+  const [ currentDataSource,  setCurrentDataSource ] = useState<{ id: string, label: string }>({id: '', label: ''})
+
+  useEffect( () => {
+    _getQuestionBank()
+  }, [])
+
+  const columns = [
+    {
+      title: '学段名',
+      dataIndex: 'label',
+      key: 'label',
+    },
+    {
+      title: '教材数',
+      dataIndex: 'textBookNum',
+      key: 'textBookNum',
+    },
+    {
+      title: '卡片数',
+      dataIndex: 'cardNum',
+      key: 'cardNum',
+    },
+    {
+      title: '状态',
+      dataIndex: 'state',
+      key: 'state',
+      render: () => <Switch checkedChildren="开启" unCheckedChildren="关闭" defaultChecked />
+    },
+    {
+      title: '操作',
+      dataIndex: 'action',
+      key: 'action',
+      render: (action: any, record: any) => (
+        <>
+          <a  onClick={() => openModel('edit', record)} >编辑</a>
+          <Divider type="vertical" />
+          <Popconfirm 
+            title="确定要删除这个学段吗?"
+            onConfirm={() => onConfirm(action, record)}
+            okText="是"
+            cancelText="我点错了"
+          >
+            <a >删除</a>
+          </Popconfirm>
+        </>
+      )
+    }
+  ]
+
+  const modelTitle = type === 'add' ? '新增学段' : '编辑学段'
+
+  const handleOk = () => {
+    setConfirmLoading(true)
+    if (type === 'add') {
+      _addQuestionBank()
+    } else {
+      _putQuestionBank()
+    }
+  }
+  
+  const onInput = (e: InputEvent) => {
+    setCurrentDataSource({
+      ...currentDataSource,
+      label: e.target.value
+    })
+  }
+
+  /**删除学段 */
+  const onConfirm = (action: any, records: any) => {
+    _delQuestionBank(records.id)
+  }
+
+  /**修改题库 */
+  const _putQuestionBank = async () => {
+    const { status, result } = await putQuestionBank(currentDataSource)
+    setIsModalVisible(false)
+    setConfirmLoading(false)
+    if (status === 200) {
+      console.log(result);
+      setCurrentDataSource({id: '', label: ''})
+      _getQuestionBank()
+    }
+  }
+
+  /**删除题库 */
+  const _delQuestionBank = async (id: string | number) => {
+    const { status, result } = await delQuestionBank(id)
+    if ( status === 200 ) {
+      console.log(result);
+      message.success(descText.delTextSuccess)
+      _getQuestionBank()
+    }
+  }
+
+  /**增加题库 */
+  const _addQuestionBank = async () => {
+    setCurrentDataSource({id: '', label: ''})
+    const { status, result} = await addQuestionBank({
+      bankName: currentDataSource.label
+    })
+    setConfirmLoading(false)
+    if (status === 200) {
+      console.log(result, 'result');
+      message.success(descText.addTextSuccess)
+      setIsModalVisible(false)
+      _getQuestionBank()
+    }
+    
+  }
+
+  /**打开弹窗 */
+  const openModel = (type: 'edit' | 'add', record?: any) => {
+    setType(type)
+    setIsModalVisible(true)
+    if (type === 'edit' ) {
+      setCurrentDataSource(record)
+    }
+  }
+
+  /**关闭弹窗 */
+  const closeModel = () => {
+    setIsModalVisible(false)
+  }
+
+  /** 查询学段 */
+  const _getQuestionBank = async () => {
+    setLoading(true)
+    const { status, result } = await getQuestionBank()
+    setLoading(false)
+    if ( status === 200 ) {
+      console.log("result:", result);
+      setDataSource(result)
+    }
+  }
+
+  return (
+    <BaseTmp>
+      <Row>
+        <Col span={24} style={{ display: 'flex', justifyContent: 'flex-end' }} >
+          <Button type="primary" icon={<PlusOutlined />} onClick={() => openModel('add')} >创建学段</Button>
+        </Col>
+        <Col span={24} style={{marginTop: '20px'}} >
+          <Table 
+            dataSource={dataSource} 
+            columns={columns}
+            loading={loading}
+          />
+        </Col>
+      </Row>
+
+      <Modal
+        title={modelTitle}
+        confirmLoading={confirmLoading}
+        visible={isModalVisible}
+        onOk={handleOk}
+        onCancel={closeModel}
+      >
+        <Input placeholder='请输入学段名' value={currentDataSource.label} onInput={onInput}  />
+      </Modal>
+
+    </BaseTmp>
+  )
+
+}
+
+export default QuestionBank
+
+

+ 323 - 0
src/pages/QuestionBankManage/QuestionBook/index.tsx

@@ -0,0 +1,323 @@
+import React, { useEffect, useLayoutEffect, memo, useState } from 'react'
+import BaseTmp from '@/components/BaseTmp'
+import { Row, Select, Button, Table, Col, Tabs, Divider, Popconfirm, Modal, Input, message } from 'antd'
+import api from '@/services/api/index'
+import { PlusOutlined } from '@ant-design/icons'
+import { descText } from '@/common/MessageManage' 
+const { getQuestionBank, getQuestionBook, postQuestionBook, putQuestionBook } = api.question
+
+const { TabPane } = Tabs
+
+interface IBank {
+  id: string | number,
+  label: string
+}
+
+interface IQueryTmp {
+  bankList: IBank[],
+  onChangeBank: ( id: string ) => void,
+  openModel: (type: 'add') => void,
+  currentBank: IBank
+}
+
+const QueryTmp: React.FC<IQueryTmp> = ({
+  bankList,
+  onChangeBank,
+  openModel,
+  currentBank
+}) => {
+
+  const [ bank, setBank ] = useState<IBank>()
+
+  useEffect( () => {
+    setBank(bankList[0])
+  }, [bankList])
+  
+  return <Row justify="space-between" >
+    <Col span={20} >
+
+      <Tabs onChange={onChangeBank} >
+        {
+          bankList.map( bank =>  <TabPane tab={bank.label} key={bank.id} />)
+        }
+      </Tabs>
+    </Col>
+    <Col span={2} >
+      <Button
+        type="primary" 
+        icon={<PlusOutlined />}
+        onClick={() => openModel('add')}
+      >
+        新增教材
+      </Button>
+    </Col>
+
+  </Row>
+}
+
+
+interface ITableFn {
+  loading: boolean,
+  dataSource: any[],
+  onDelBook: ( record: IDataSource ) => void,
+  openModel: (type: 'edit' , record: IDataSource) => void
+}
+
+const TabletTmp: React.FC<ITableFn> = ({
+  loading,
+  dataSource,
+  onDelBook,
+  openModel
+}) => {
+
+  const columns = [
+    {
+      title: '题卡',
+      dataIndex: 'label',
+      key: 'label',
+    },
+    {
+      title: '卡片数',
+      dataIndex: 'cardNum',
+      key: 'cardNum',
+    },
+    {
+      title: '创建时间',
+      dataIndex: 'createTime',
+      key: 'createTime',
+    },
+    // {
+    //   title: '启用',
+    //   dataIndex: 'state',
+    //   key: 'state',
+    //   render: () => <Switch />
+    // },
+    {
+      title: '操作',
+      dataIndex: 'action',
+      key: 'action',
+      render: (text: any, record: IDataSource) => (
+        <>
+          <a  onClick={() => openModel('edit', record )} >编辑</a>
+          {/* <Divider type="vertical" /> */}
+          {/* <Popconfirm
+            title="确定要删除这本教材吗?"
+            onConfirm={() => onDelBook(record)}
+            okText="是"
+            cancelText="我点错了"
+          >
+             <a  >删除</a>
+          </Popconfirm> */}
+        </>
+      )
+    },
+  ]
+
+  return <Table style={{marginTop: "20px"}} loading={loading} dataSource={dataSource} columns={columns} />
+}
+
+interface IDataSource {
+  id: string,
+  label: string,
+  bankId: string,
+  cardNum: number,
+  createTime: string
+}
+
+const QuestionBook: React.FC = () => {
+
+  const [ bankList, setBankList ] = useState<IBank[]>([])
+
+  const [ currentBank, setCurrentBank ] = useState<IBank>({id: '', label: ''})
+
+  const [ loading, setLoading ] = useState<boolean>(false)
+
+  const [ dataSource, setDataSource ] = useState<IDataSource[]>([])
+
+  const [ currentDataSource, setCurrentDataSource ] = useState<IDataSource>({
+    id: '',
+    label: '',
+    cardNum: 0,
+    createTime: '',
+    bankId: '' 
+  })
+
+  const [ confirmLoading, setConfirmLoading ] = useState<boolean>(false)
+  
+  const [ isModalVisible, setIsModalVisible ] = useState<boolean>(false)
+
+  const [ type, setType ] = useState<'edit' | 'add'>('add')
+  
+  useEffect( () => {
+    _getQuestionBank()
+  }, [])
+
+  useEffect( () => {
+    console.log(currentBank, 'currentBank');
+    
+    currentBank.id && _getQuestionBook()
+  }, [currentBank.id])
+
+  const modelTitle = type === 'edit' ? '编辑教材' : '新增教材'
+
+  const onChangeBank = (id: string) => {
+    const targetBank = bankList.find( bank => bank.id === id )
+    setCurrentBank(targetBank!)
+  }
+
+  const onInput = (e: InputEvent) => {
+    setCurrentDataSource({
+      ...currentDataSource!,
+      label:  e?.target?.value
+    })
+  }
+
+  /**删除教材 */
+  const onDelBook = (record: IDataSource) => {
+
+  }
+
+  const handleOk = () => {
+
+    if (type === 'add' ) {
+      _postQuestionBook()
+    } else {
+      _putQuestionBook()
+    }
+  }
+
+  /**打开弹窗 */
+  const openModel = (type: 'edit' | 'add', record?: IDataSource) => {
+
+    setType(type)
+    setIsModalVisible(true)
+    setConfirmLoading(false)
+
+    if (type === 'edit') {
+      setCurrentDataSource(record!)
+    }
+
+  }
+
+  /**关闭弹窗 */
+  const closeModel = () => {
+    setCurrentDataSource({
+      id: '',
+      bankId: '',
+      label: '',
+      cardNum: 0,
+      createTime: ''
+    })
+    setIsModalVisible(false)
+  } 
+
+  /**编辑教材 */
+  const _putQuestionBook = async () => {
+    setConfirmLoading(true)
+    const {status, result} = await putQuestionBook(currentDataSource!.id, {
+      label: currentDataSource!.label
+    })
+    closeModel()
+    if (status === 200) {
+      message.success(descText.editTextSuccess)
+      _getQuestionBook()
+    }
+  }
+
+  /**新增教材 */
+  const _postQuestionBook = async () => {
+    if ( currentDataSource.label.length === 0 ) {
+      message.error("请输入教材名")
+      return
+    }
+    setConfirmLoading(true)
+    const { status, result } = await postQuestionBook({
+      bankId: currentBank.id,
+      label: currentDataSource.label
+    })
+    closeModel()
+    if ( status === 200 ) {
+      console.log(result);
+      _getQuestionBook()
+      message.success(descText.addTextSuccess)
+    }
+    
+  }
+
+  /** 查教材 */
+  const _getQuestionBook = async () => {
+    setLoading(true)
+    const {status, result} = await getQuestionBook({
+      bankId: currentBank.id
+    })
+    setLoading(false)
+    if ( status === 200) {
+      console.log(result);
+      setDataSource(result)
+    }
+  }
+
+  /**查题库 */
+  const _getQuestionBank = async () => {
+    const { status, result } = await getQuestionBank()
+    if (status === 200) {
+      setBankList(result)
+      setCurrentBank(result[0])
+    }
+  } 
+
+  return (
+    <BaseTmp>
+
+      <QueryTmp
+        bankList={bankList}
+        currentBank={currentBank}
+        onChangeBank={onChangeBank}
+        openModel={openModel}
+      />
+
+      <TabletTmp
+        loading={loading}
+        dataSource={dataSource}
+        onDelBook={onDelBook}
+        openModel={openModel}
+      />
+
+      <Modal
+        title={modelTitle}
+        confirmLoading={confirmLoading}
+        visible={isModalVisible}
+        onOk={handleOk}
+        onCancel={closeModel}
+      > 
+        <Row>
+          <Col span={4} style={{textAlign: "right"}} >学段选择:</Col>
+          <Col span={20} > 
+            <Select key={currentBank.id} style={{width: "150px"}} defaultValue={currentBank.id} >
+              {
+                bankList.map( bank =>  <Select.Option  key={bank.id} value={bank.id}  > { bank.label} </Select.Option>  )
+              }
+            </Select>
+          </Col>
+        </Row>  
+        <Row  style={{marginTop: '20px'}}>
+          <Col span={4}  style={{textAlign: "right"}} >教材名:</Col>
+          <Col span={20} > 
+            <Input
+              min={1}
+              placeholder='请输入教材名' 
+              value={currentDataSource!.label} 
+              onInput={onInput}
+            />
+          </Col>
+        </Row>
+
+
+       
+       
+      </Modal>
+    </BaseTmp>
+  ) 
+}
+
+export default QuestionBook

+ 429 - 0
src/pages/QuestionBankManage/QuestionCard/index.tsx

@@ -0,0 +1,429 @@
+import React, { useEffect, useState } from 'react'
+import BaseTmp from '@/components/BaseTmp'
+import api from '@/services/api/index'
+import { PlayCircleTwoTone, PauseCircleTwoTone } from '@ant-design/icons'
+import { Row, Tabs, Button, Select, Input, Col, Table, Tooltip, Divider, Switch, message } from 'antd';
+import AudioManage from '@/common/AudioManage';
+import { useStateSync } from '@/hooks/index' 
+import { history } from 'umi';
+import { descText } from '@/common/MessageManage';
+
+const { TabPane } = Tabs
+
+const {
+  getQuestionBank,
+  getQuestionCard,
+  getQuestionBook,
+  useQuestionCard,
+  delQuestionCard
+} = api.question
+
+interface ISelectTmp {
+  dataList?: Record<string, any>[],
+  onChange?: (value: string) => void
+}
+
+/** 搜索模板 */
+const SelectTmp: React.FC<ISelectTmp> = ({
+  dataList,
+  onChange
+}) => {
+
+  return  <Select allowClear style={{ width: 120 }} onChange={onChange} >
+            {
+              dataList?.map( data => <Select.Option value={data.id} key={data.id} >{data.label}</Select.Option>)
+            }
+          </Select>
+}
+
+/** 表格 */
+const QuestionTable = ({
+  loading,
+  dataSource,
+  onChangePagination,
+  pagination,
+  onChangeSwitch,
+  onDel,
+  onEdit
+}: {
+  loading: boolean,
+  dataSource: any[],
+  pagination: IPagination
+  onChangePagination: (current: number) => void,
+  onChangeSwitch: (check: boolean, state: any) => void,
+  onDel: (record: any) => void,
+  onEdit: (record: any) => void
+}) => {
+
+  const [ playingVoice, setPlayingVoice ] = useState<string>()
+
+  const playAudio = (voiceUrl: string) => {
+    AudioManage.play(voiceUrl)
+    setPlayingVoice(voiceUrl)
+  }
+
+  const closeAudio = () => {
+    AudioManage.pause()
+    setPlayingVoice('')
+  }
+
+  const columns = [
+    {
+      title: '题卡',
+      dataIndex: 'title',
+      key: 'title',
+    },
+    {
+      title: '问题',
+      dataIndex: 'label',
+      key: 'label',
+      ellipsis: {
+        showTitle: false,
+      },
+      render: (label: string) => <Tooltip   placement="topLeft" title={label} > { label }  </Tooltip>
+    },
+    {
+      title: '音频',
+      dataIndex: 'voiceUrl',
+      key: 'voiceUrl',
+      render: ( voiceUrl: string ) => (
+        <>
+          <span style={{cursor: "pointer"}} >
+            {
+              playingVoice === voiceUrl ?  
+                <PauseCircleTwoTone onClick={closeAudio} style={{fontSize: '30px'}} /> 
+                :  
+                <PlayCircleTwoTone onClick={() => playAudio(voiceUrl)} style={{fontSize: '30px'}} /> 
+            }
+          </span>
+        </>
+      )
+    },
+    {
+      title: '是否启用',
+      dataIndex: 'state',
+      key: 'state',
+      render: (state, record) => 
+      <Switch 
+        checked={record.state === 1} 
+        checkedChildren="已启用" 
+        unCheckedChildren="已禁用" 
+        onChange={(check) => onChangeSwitch(check, record)} 
+      />
+    },
+    {
+      title: '创建时间',
+      dataIndex: 'createTime',
+      key: 'createTime',
+    },
+    {
+      title: '操作',
+      dataIndex: 'action',
+      key: 'action',
+      render: (action, records: any) => (
+        <>
+          {/* <a >查看</a>
+          <Divider type="vertical" /> */}
+          <a onClick={() => onEdit(records) } >编辑</a>
+          <Divider type="vertical" />
+          <a  onClick={onDel} >删除</a>
+        </>
+      )
+    }
+  ];
+
+  return <Table 
+    loading={loading}
+    style={{marginTop: '20px'}} 
+    dataSource={dataSource}   
+    columns={columns} 
+    pagination={{
+      showSizeChanger: false,
+      position: ["bottomRight"],
+      total: pagination?.total,
+      current: pagination?.current, 
+      onChange: onChangePagination
+    }}
+   />;
+}
+
+interface ITabsState {
+  id: string,
+  label: string
+}
+
+interface IQuestionSearch {
+  bookList: any[],
+  onChangeBook: (id: string) => void,
+  onChangeUseState: (state: number) => void,
+  onSearch: () => void,
+  addQuestionCard: () => void,
+  onInputLabel: (e: InputEvent) => void
+}
+
+const useStateList = [
+  {label: '启用', id: 1, state: 1},
+  {label: '禁用', id: -1, state: -1},
+]
+
+/** 搜索 */
+const QuestionSearch: React.FC<IQuestionSearch> = ({
+  bookList,
+  onChangeBook,
+  onChangeUseState,
+  onSearch,
+  addQuestionCard,
+  onInputLabel
+}) => {
+
+  return <Row>
+    {/* <Col span={4}> <span>学段筛选: </span> <SelectTmp  dataList={bookList} onChange={onChange}  /> </Col> */}
+    <Col span={4}> <span>教材筛选: </span> <SelectTmp  dataList={bookList} onChange={onChangeBook} /></Col>
+    <Col span={4}> <span>启用状态: </span> <SelectTmp  dataList={useStateList} onChange={onChangeUseState}  /></Col>
+    <Col span={3}> <Input placeholder='可通过题干搜索' style={{width: '150px'}} onInput={onInputLabel} /> </Col>
+    <Col span={3}> <Button type="primary" onClick={onSearch} >搜索</Button> </Col>
+    {/* <Col span={2}> <Button type="primary" danger  >批量删除</Button></Col> */}
+    {/* <Col span={2}> <Button type="primary" color='' >加入比赛</Button></Col> */}
+    <Col span={2}> <Button type="primary" onClick={addQuestionCard} > 添加题卡 </Button> </Col>
+  </Row>
+}
+
+interface IQuestionTabs {
+  children: React.ReactChild,
+  onChangeTabs: (e: string) => void
+  tabsList: ITabsState[] 
+}
+
+interface IPagination {
+  total: number,
+  current: number,
+  state: number | string,
+  textbookId: string
+}
+
+/** tabs  */
+const QuestionTabs: React.FC<IQuestionTabs> = ({
+  children,
+  onChangeTabs,
+  tabsList
+}) => {
+
+  return  <Tabs defaultActiveKey="1" onChange={(e) => onChangeTabs(e)}>
+              {
+                tabsList.map( item => (
+                  <TabPane tab={item.label} key={item.id}>
+                    { children }
+                  </TabPane>
+                ))
+              }
+          </Tabs>
+}
+
+const initPagination = {
+  total: 0,
+  current: 1,
+  state: '',
+  textbookId: ''
+}
+
+const QuestionCard: React.FC = () => {
+
+  /**TabsData */
+  const [ tabsList, setTabsList ] = useState<ITabsState[]>([])
+  
+  /**current tabs key */
+  const [ currentTabsKey,  setCurrentTabsKey ] = useState<string>('')
+
+  /**分页 */
+  const [ pagination, setPgination ] =  useStateSync<IPagination>(initPagination)
+
+  const [ loading, setLoading ] = useState<boolean>(false)
+
+  /**数据 */
+  const [ dataSource, setDataSource ] = useState<Record<string, any>[]>([])
+ 
+  /**教材数据 */
+  const [ bookList, setBookList ] = useState([])
+
+  /**current table item */
+  const [ currentDataSource, setCurrentDataSource ] = useState()
+
+  const [ label, setLabel ] = useState<String>('')
+
+  useEffect( () => {
+    _getQuestionBank()
+  }, [])
+
+  useEffect( () => {
+
+    _getQuestionBook()
+    
+    setPgination(initPagination)
+
+  }, [currentTabsKey])
+
+  useEffect(() => {
+
+    _getQuestionCard()
+
+  }, [ pagination.current, currentTabsKey])
+
+  /**删除题卡  */
+   const _delQuestionCard = async (records: any) => {
+     const { status, result } = await delQuestionCard(records.id)
+     if ( status === 200 ) {
+       message.success( descText.delTextSuccess )
+       _getQuestionCard()
+     }
+   }
+
+   /**编辑题卡 --> 携带题卡的cardId 跳转到下一添加题卡页面 */
+  const onEdit = (records: any) => {
+    history.push(`/QuestionBankManage/opration-card?cardId=${records.id}`)
+  }
+
+  /**添加题卡 --> 跳转到添加题卡页面 */
+  const addQuestionCard = () => {
+    history.push('/QuestionBankManage/opration-card')
+  }
+
+  /**上下架状态 */
+  const onChangeSwitch = (check: boolean, records: any) => {
+    _useQuestionCard(check, records)
+  }
+
+  /**search */
+  const onSearch = () => _getQuestionCard()
+
+  /**search label  */ 
+  const onInputLabel = (e) => setLabel(e.target.value)
+
+  /**query condition book */
+  const onChangeBook = (id: string) => {
+    console.log("uery condition book:", id);
+    
+    setPgination({
+      ...pagination,
+      textbookId: id
+    })
+  }
+
+  /**query condition useState */
+  const onChangeUseState = (state: number) => {
+    console.log('onChangeUseState:', state);
+    setPgination({
+      ...pagination,
+      state
+    })
+  }
+
+  /**分页 */
+  const onChangePagination = (current: number) => {
+    setPgination({
+      ...pagination,
+      current: current
+    })
+  }
+
+  /** rquest 启用或者停用题卡 */
+  const _useQuestionCard = async (check: boolean, records: any) => {
+    console.log(check, 'check');
+    const state = check ? 1 : -1
+    
+    const { status, result } = await useQuestionCard(records.id , {state: state})
+    if ( status === 200 && result ) {
+        message.success(state ===  1 ? '启用成功' : '禁用成功')
+        const $r = dataSource.map( ds =>  {
+          return {
+            ...ds,
+            state: records.id === ds.id ? state :  ds.state
+          }
+        })
+        setDataSource( $r)
+        // _getQuestionCard()
+    }
+  }
+
+  /**获取教材 */
+  const _getQuestionBook = async () => {
+    const { status, result } = await getQuestionBook({bankId: currentTabsKey})
+    if (status === 200) { 
+      setBookList(result)
+    }
+  }
+
+  /**获取题卡 */
+  const _getQuestionCard = async (_pagination?: IPagination) => {
+
+    setLoading(true)
+    console.log(currentTabsKey, 'pagination');
+    
+    const { status, result } = await getQuestionCard(_pagination? { 
+      bankId: currentTabsKey,
+        ..._pagination
+      } : {
+      bankId: currentTabsKey,
+      page: pagination?.current, 
+      pageSize: 10,
+      state: pagination.state,
+      label,
+      textbookId: pagination.textbookId
+    })
+    setLoading(false)
+    if ( status === 200 ) {
+      console.log(result.records, 'result');
+      setDataSource(result.records)
+      setPgination({
+        ...pagination,
+        total: result.total,
+        current: result.current
+      })
+    }
+  }
+
+  const _getQuestionBank = async () => {
+    const { status, result } =  await getQuestionBank()
+    
+    if (status === 200) {
+      setTabsList(result)
+      setCurrentTabsKey(result[0].id)
+    }
+
+  }
+
+  const onChangeTabs = (e: string) => {
+    console.log(e, 'onChangeTabs');
+    setCurrentTabsKey(e)
+  }
+
+  return  <BaseTmp>
+            <QuestionTabs  
+              onChangeTabs={onChangeTabs}
+              tabsList={tabsList}
+            >
+              <>
+                <QuestionSearch 
+                  bookList={bookList}
+                  addQuestionCard={addQuestionCard}
+                  onSearch={onSearch}
+                  onInputLabel={onInputLabel}
+                  onChangeBook={onChangeBook}
+                  onChangeUseState={onChangeUseState}
+                />
+                <QuestionTable 
+                  loading={loading}
+                  pagination={pagination}
+                  dataSource={dataSource}
+                  onDel={_delQuestionCard}
+                  onEdit={onEdit}
+                  onChangeSwitch={onChangeSwitch}
+                  onChangePagination={onChangePagination}
+                />
+              </>
+            </QuestionTabs>
+          </BaseTmp>
+}
+
+export default QuestionCard
+
+

+ 735 - 0
src/pages/QuestionBankManage/index.tsx

@@ -0,0 +1,735 @@
+import React, { forwardRef, useEffect, useLayoutEffect, useRef, useState } from 'react'
+import BaseTmp from '@/components/BaseTmp';
+import { Row, Col, Radio, RadioChangeEvent , Select, Input, Button, message } from 'antd'
+import classNames from 'classnames';
+import type { Position } from 'react-rnd';
+import { Rnd } from 'react-rnd'
+import styles from './style/index.less'
+import logo from  '@/static/common/logo.png'
+import { useStatePrevious } from '@/hooks'
+import RightClick from '@/components/RightClick'
+import UploadImg from '@/components/UploadImage/index'
+import api from '@/services/api'
+import { history, useLocation, useParams, useRouteMatch } from 'umi';
+
+const { getQuestionBank, getQuestionBook, addQuestionCard, getQuestionCardByid, putQuestionCard } = api.question
+
+type TMerge = IQPData & IQData & IIData & {
+  color: string,
+  imgUrl: string,
+}
+
+interface IQPData {
+  pw: number,
+  ph: number,
+  px: number,
+  py: number
+}
+
+interface IQData {
+  w: number,
+  h: number,
+  x: number,
+  y: number
+}
+
+/** 命名和小程序对应 */
+interface IIData {
+  iw: number,
+  ih: number,
+  ix: number,
+  iy: number
+}
+
+type TType = 'parent' | 'children' | 'button'
+
+/** 红 绿 黄 蓝 */
+const colorList = [
+  {color: '#FA0F0C', label: '红色'},
+  {color: '#396C28', label: '绿色'},
+  {color: '#F7DF1E', label: '黄色'},
+  {color: '#23398C', label: '蓝色'},
+  {color: '', label: ''}
+]
+
+const QPData = { pw: 100, ph: 100, px: 100, py: 100}
+const QData = { w: 80, h: 80, x: 0, y: 0}
+const QIData = { iw: 40, ih: 40,  ix: 0, iy: 0 }
+
+const permutationKey = ( type: TType ) => {
+  
+  if ( type === 'parent' ) return ['pw', 'ph', 'px', 'py']
+  else if ( type === "children" ) return ['w', 'h', 'x', 'y']
+  else return ['iw', 'ih', 'ix', 'iy']
+
+}
+
+interface IQuestionCard {
+  content: TMerge[],
+  onResizeStop: (
+    type: TType,
+    index: number,
+    {w, h, pos}: {w: string, h: string, pos: Position}
+  ) => void,
+  resizeChildrenRnd: (index: number) => void
+  label: string,
+  onInput: (e: InputEvent) => void,
+  deleteImg: ( _imgUrl: string ) => void,
+  onDragStop: (type: TType,  index: number, {x, y}: {x: number, y: number}) => void
+  saveImageUrl: (imgUrl: string, index: number) => void
+}
+
+const QuestionCard: React.FC<IQuestionCard> = ({
+  content,
+  onResizeStop,
+  label,
+  deleteImg,
+  onInput,
+  saveImageUrl,
+  onDragStop
+}) => {
+
+  const [ menuState, setMenuState ] = useState<boolean>(false)
+
+  const bordeLiene = classNames(styles['border-line'], styles['question-card-timing'])
+
+  const questionId = 'questionId'
+
+  console.log("content:", content);
+  
+  /**
+   * 
+   * @description 鼠标右键事件
+   * 
+   */
+
+  const pictureRightClick = ( e: React.MouseEvent ) => {
+    e.preventDefault()
+    if (e.button === 2) {
+      setMenuState(true)
+    }
+  }
+
+  const RenderButton = ({color}: {color: string}) => {
+    return <div className={styles['render-button']} style={{backgroundColor: color}} ></div>
+  }
+
+  return (
+    <div className={styles.question} >
+      <RightClick render={menuState} deleteImg={(_imgUrl) => deleteImg(_imgUrl)} />
+      <div className={styles['question-card-iphonex']} >
+        <img src={require('@/static/common/iPhoneX-top.png')} alt="" />
+      </div>
+      <Row  className={bordeLiene} justify='center' align='middle' >
+        <Col span={24} >计时区</Col>
+      </Row>
+      <Row className={styles.stem} justify='center' align='middle' >
+        <Col span={24} >
+          <Input.TextArea 
+            style={{height: '90px'}} 
+            placeholder="请输入题卡描述" 
+            maxLength={6}
+            value={label}
+            onInput={onInput}
+          />
+        </Col>
+      </Row>
+      <Row className={styles['opration-area']}  justify='start' >
+        <Col className='opration-left' span={16} style={{height: `438px`}}>
+          {
+            content?.map( (item, index) => (
+              <Rnd
+                key={index}
+                minHeight={80}
+                minWidth={80}
+                bounds='parent'
+                className='parent-rnd'
+                style={{ border: `1px solid ${item.color}`}}
+                size={{ width: item.pw,  height: item.ph }}
+                position={{ x: item.px, y: item.py }}
+                onDragStop={(e, d) => { onDragStop('parent', index, { x: d.x, y: d.y }) }}
+                onResizeStop={(e, direction, ref, delta, position) => {
+                  onResizeStop('parent', index, {
+                    w: ref.style.width,
+                    h: ref.style.height,
+                    pos: position,
+                  });
+                }}
+              >
+                <Rnd
+                  bounds='parent'
+                  onMouseDown={e => e.stopPropagation()}
+                  style={{ border: `1px dashed ${item.color}`}}
+                  size={{ width: item.w,  height: item.h }}
+                  position={{ x: item.x, y: item.y }}
+                  onDragStop={(e, d) => { onDragStop( 'children', index, { x: d.x, y: d.y }) }}
+                  onResizeStop={(e, direction, ref, delta, position) => {
+                    onResizeStop( 'children', index, {
+                      w: ref.style.width,
+                      h: ref.style.height,
+                      pos: position,
+                    });
+                  }}
+                > 
+                {
+                  item.imgUrl ? 
+                    <img
+                    src={item.imgUrl}
+                    id={questionId}
+                    style={{width: item.w + 'px',  height: item.h  + 'px', boxSizing: 'border-box'}}
+                    onMouseDown={ (e: React.MouseEvent) => pictureRightClick(e) }
+                  />
+                  :
+                  <UploadImg 
+                    imageUrl={item.imgUrl}
+                    saveImageUrl={ (imgUrl) => saveImageUrl(imgUrl, index)}
+                    uploadCount={1}
+                  />
+                }
+                </Rnd>
+                <Rnd
+                  bounds='parent'
+                  onMouseDown={e => e.stopPropagation()}
+                  size={{ width: item.iw,  height: item.ih }}
+                  position={{ x: item.ix, y: item.iy }}
+                  onDragStop={(e, d) => { onDragStop( 'button', index, { x: d.x, y: d.y }) }}
+                >
+                  <RenderButton color={colorList[index].color}  />
+                </Rnd>
+              </Rnd>
+            ))
+          }
+        </Col>
+        <Col span={8} className={styles['answer-area']} > 答案区域 </Col>
+      </Row>
+    </div>
+  )
+}
+
+interface IAnswer {
+  color: string,
+  imgUrl: string
+}
+
+interface IAnswerCard {
+  onChangeAnswer: (e: RadioChangeEvent, index: number) => void,
+  saveImageAnswerUrl: (imgUrl: string, index: number) => void,
+  answerList: IAnswer[]
+}
+
+const AnswerCard: React.FC<IAnswerCard> = ({
+  onChangeAnswer,
+  saveImageAnswerUrl,
+  answerList
+}) => {
+
+  /**渲染颜色选择radio */
+  const RenderColorRadio = ({_color, index}: {_color: string, index: number}) => {
+    return (
+      <Radio.Group  value={_color} >
+        {
+          colorList.map( (_color, i) => (
+            _color.color ? 
+              <Radio value={_color.color} key={i} onChange={(e) => onChangeAnswer(e, index)}>
+                {_color.label}
+              </Radio>
+              :
+              <></>
+            )
+           
+          )
+        }
+      </Radio.Group>
+    )
+  }
+
+  /** @description 答案选择render */
+  const RenderAnswer = () => {
+
+    return (
+      <Row style={{width: '100%'}}>
+        {
+          answerList.map( (answer, index) => (
+            <Col span={24} key={index} > 
+              <Row gutter={3} style={{width: '100%'}} align="middle" justify="space-around" >
+                <Col>选项1:</Col>
+                <Col>
+                  <UploadImg 
+                    saveImageUrl={(imageUrl) => saveImageAnswerUrl(imageUrl, index) } 
+                    imageUrl={answer.imgUrl}
+                  />
+                </Col>
+                <Col>对应答案</Col>
+                <Col> 
+
+                  <RenderColorRadio  _color={answer.color} index={index} />
+                </Col>
+              </Row>
+            </Col>
+          ))
+        }
+      </Row>
+    )
+  }
+
+  return (
+    <Row className={styles['answer-card'] }  >
+      <Col 
+        span={24} 
+        className={styles['ability-point']}
+      >
+        &nbsp;
+        {/* 能力选择: */}
+      </Col>
+      <Col 
+        span={24} 
+        className={styles['answer-setting']} 
+        style={{height: '232px', borderTop: '1px solid #000', paddingTop: '40px'}} 
+      >
+        <RenderAnswer />
+      </Col>
+    </Row>
+  )
+}
+
+interface ISelectTmp {
+  defaultValue: string
+  dataList: Record<string, any>[],
+  onChange: (value: string) => void
+}
+
+const SelectTmp: React.FC<ISelectTmp> = ({
+  dataList,
+  onChange,
+  defaultValue
+}) => {
+  console.log('defaultValue', defaultValue, dataList);
+  
+  return <Select defaultValue={defaultValue}  style={{ width: 120 }} onChange={onChange} >
+            {
+              dataList.map( data => 
+                <Select.Option value={data.id} key={data.id} >{data.label}</Select.Option>)
+            }
+          </Select>
+}
+
+const QuestionBankManage: React.FC = () => {
+
+  const [ bankList, setBankList  ] = useState<{id: string, label: string}[]>([])
+
+  const [ bookList, setBookList  ] = useState([])
+
+  const [ currentBank, setCurrentBank ] = useState({})
+
+  const [ currentBook, setCurrentBook ] = useState({})
+
+  const [ title, setTitle ] = useState<string>()
+
+  const [ content, setContent ] = useState<TMerge[]>([])
+
+  const [ previous, setPrevious ] = useStatePrevious<TMerge[]>()
+
+  const [ label, setLabel ] = useState<string>('')
+
+  /**当前操作的父盒子下标 */
+  const [ OIndex, setOIndex ] = useState<number>()
+
+  const [ answerList, setAnswerList ] = useState<IAnswer[]>([
+    {color: '', imgUrl: ''},
+    {color: '', imgUrl: ''},
+    {color: '', imgUrl: ''},
+    {color: '', imgUrl: ''},
+  ])
+
+  /**能力点 */
+  const [ ability, setAbility ] = useState<string>('')
+
+  const location = useLocation()
+
+  const cardId = location.query.cardId
+
+  const submitButtonLabel = cardId ? '编辑题卡' : '新增题卡'
+
+  useEffect( () => {
+    _getQuestionBank()
+  }, [])
+
+  useEffect( () => {
+    _getQuestionBook()
+  }, [currentBank])
+
+  useEffect( () => {
+   
+    
+    if (cardId) {
+      _getQuestionCardByid(cardId)
+    } else {
+      initContent()
+    }
+  }, [])
+
+  
+
+  useEffect( () => {
+    
+    typeof OIndex === 'number' && resizeChildrenRnd(OIndex!)
+    
+  }, [previous])
+
+  /**保存结果答案 */
+  const saveImageAnswerUrl = (imgUrl: string, index: number) => {
+    const r = answerList.map( (answer, i) => {
+      return {
+        ...answer,
+        imgUrl: index === i ? imgUrl: answer.imgUrl
+      }
+    })
+    setAnswerList(r)
+  }
+
+  /** */
+  const onChangeAnswer =  (e: RadioChangeEvent, index: number) => {
+    const r = answerList.map( (answer, i) => {
+      return {
+        ...answer,
+        color: index === i ? e.target.value: answer.color
+      }
+    })
+    setAnswerList(r)
+  }
+
+  /**修改title */
+  const onInputTitle = (e: InputEvent) => setTitle(e.target.value)
+  
+  /**修改title */
+  const onInputAbility = (e: InputEvent) => setAbility(e.target.value)
+
+  const onInput = (e: InputEvent) => setLabel(e.target.value)
+
+  /** 保存图片 */
+  const saveImageUrl = (imgUrl: string, index: number) => {
+    const r = content?.map( (item, i) => {
+      return {
+        ...item,
+        imgUrl: index === i ? imgUrl : item.imgUrl
+      }
+    })
+    
+    setContent(r)
+  }
+
+  const onDragStop = (
+    type: TType,
+    index: number,
+    {x, y}: {x: number, y: number}
+  ) => {
+    
+    const [ _w, _h , _x, _y ] = permutationKey(type)
+
+    const r = content?.map( (item, i) => {
+      const target = i === index
+      return {
+        ...item,
+        [_x]: target? x : item[_x],
+        [_y]: target? y : item[_y]
+      }
+    })
+
+    setContent(r)
+
+  }
+
+  const deleteImg = (_imgUrl: string) => {
+    const r = content?.map( (item, index) => {
+      return {
+        ...item,
+      imgUrl: _imgUrl === item.imgUrl ? '' : item.imgUrl
+      }
+    })
+    setContent(r)
+  }
+
+  /**
+   * @description 初始化init数据
+   */
+
+  const initContent = () => {
+      let mergeList: TMerge[] = []
+      Array.from(new Array(4)).forEach( (count, index) => {
+        console.log('count:', index);
+        mergeList.push({
+          ...QPData, 
+          ...QData,
+          ...QIData, 
+          color: colorList[index].color, 
+          imgUrl: 
+          ''}
+        )
+      })
+      setContent(mergeList)
+  }
+
+  const onChangeBank = (id: string) => {
+    const bank = bankList.find( bank => bank.id === id ) 
+    setCurrentBank({
+      ...bank,
+      id
+    })
+  }
+
+  const onChangeBook = (id: string) => {
+    const book = bookList.find( book => book.id === id ) 
+    setCurrentBook({
+      ...book,
+      id
+    })
+  }
+
+  /**
+   * 
+   * @description 当父盒子缩小 宽高小于子盒子时 按照缩放比例 缩小子盒子
+   * 
+  */
+
+  const resizeChildrenRnd = ( index: number) => {
+
+      const _previous: TMerge = previous![index]
+  
+      const _contentItem = content![index]
+  
+      let rate = 1
+  
+      let _ix = 0
+      let _iy = 0
+  
+      if ( _contentItem.pw < _contentItem.w  ) {
+  
+        rate = _contentItem.pw / _previous.pw
+  
+      } else if ( _contentItem.ph < _contentItem.h ) {
+  
+        rate = _contentItem.ph / _previous.ph
+  
+      }
+  
+      if ( _contentItem.pw < (_contentItem.ix + _contentItem.iw)) {
+  
+        _ix = _contentItem.pw - _contentItem.iw
+      } else if (_contentItem.ph < (_contentItem.iy + _contentItem.ih)) {
+  
+        _iy = _contentItem.ph - _contentItem.ih
+      }
+  
+      const r = content?.map( (item, i) => {
+        const target = i === index
+        return {
+          ...item,
+          w: target? item.w * rate: item.w,
+          h: target? item.h * rate: item.h,
+          x: target? item.x * rate: item.x,
+          y: target? item.y * rate: item.y,
+          ix:  target && _ix !== 0 ? _ix : item.ix,
+          iy:  target && _iy !== 0 ? _iy : item.iy,
+        }
+      })
+  
+      setContent(r)
+  }
+  
+  /**修改题卡大小 */
+  const onResizeStop = (
+    type: TType,
+    index: number,
+    {w, h, pos}: {w: string, h: string, pos: Position}
+  ) => {
+
+    const rw = Number(w.replace('px', ''))
+    const rh = Number(h.replace('px', ''))
+
+    const [ _w, _h , _x, _y ] = permutationKey(type)
+
+    const r = content?.map( (item, i) => {
+      const target = i === index
+      return {
+        ...item,
+        [_w]: target? rw : item[_w],
+        [_h]: target? rh : item[_h],
+        [_x]: target? pos.x : item[_x],
+        [_y]: target? pos.y : item[_y]
+      }
+    })
+    if (type === 'parent') {
+      setPrevious(content!)
+    }
+    setContent(r)
+    setOIndex(index)
+  }
+
+  /***根据id获取卡片数据, 用于编辑 */
+  const _getQuestionCardByid = async (cardId: string) => {
+
+    const { status, result } = await getQuestionCardByid(cardId)
+
+    if ( status === 200 ) {
+      
+      setContent(result.content)
+      setAnswerList(result.options)
+      setLabel(result.label)
+      setTitle(result.title)
+      setAbility(result.ableVar)
+
+      console.log('bankList:', bankList);
+      
+      setCurrentBank({
+        label: result.bankLabel,
+        id: result.bankId
+      })
+
+      setCurrentBook({
+        label: result.bookLabel,
+        id: result.bookId
+      })
+
+    }
+  }
+  
+  /** submit question card */
+  const submit = () => {
+
+    const _colorlist = answerList.map( answer => answer.color )
+
+    if (!label.length) {
+      message.error("请填写卡片描述")
+      return 
+    }
+
+    if (_colorlist.includes('')) {
+      message.error("存在未选择的答案")
+      return
+    }
+
+    let hasSameColor = false
+    _colorlist.forEach( (_color, index) => {
+      const r = _colorlist.lastIndexOf(_color)
+      if ( r !== -1 && r !== index ) {
+        hasSameColor = true
+      }
+    })
+
+    if (hasSameColor) {
+      message.error("存在一样的答案")
+      return
+    }
+
+    const $par = {
+      content,
+      options: answerList,
+      label,
+      title,
+      bookId: currentBook.id,
+      ableVar: ability,
+      id: cardId ? cardId : ''
+    }
+
+    _addQuestionCard($par)
+  }
+
+  /**增加题卡 */
+  const _addQuestionCard = async ($par: any) => {
+    let $r = null
+    if (cardId) {
+      $r = await putQuestionCard(1, $par)
+    } else {
+      $r = await addQuestionCard(1, $par)
+    }
+
+   
+  
+    if ( $r.status === 200 ) {
+      message.success( cardId ? "编辑成功" : "增加成功" )
+      history.goBack()
+    }
+
+  }
+
+  /**获取教材 */
+  const _getQuestionBook = async () => {
+    const { status, result } = await getQuestionBook({bankId: currentBank.id})
+    if ( status === 200 ) {
+      setBookList(result)
+    }  
+  }
+
+  /**获取学段 */
+  const _getQuestionBank = async () => {
+    const { status, result } = await getQuestionBank()
+    if ( status === 200 ) {
+      setBankList(result)
+    }
+  }
+
+  return (
+    <BaseTmp>
+      <Row  style={{marginTop: '20px'}} >
+        <Col> 卡片名称:  </Col>
+        <Col> <Input onInput={onInputTitle} value={title}  /> </Col>
+      </Row>
+      <Row  style={{marginTop: '20px'}} >
+        <Col> 学段:  </Col>
+        <Col>  
+         <SelectTmp key={currentBank.id}  defaultValue={currentBank.id} dataList={bankList} onChange={onChangeBank} /> 
+        </Col>
+      </Row>
+      <Row style={{marginTop: '20px'}}  >
+        <Col> 教材:  </Col>
+        <Col> 
+          <SelectTmp
+            key={currentBook.id}
+            defaultValue={currentBook.id}
+            dataList={bookList} 
+            onChange={onChangeBook} 
+          /> 
+        </Col>
+      </Row>
+      <Row style={{marginTop: '20px'}} >
+        <Col> 能力点:  </Col>
+        <Col> <Input onInput={onInputAbility} value={ability}  /> </Col>
+      </Row>
+      <Row 
+        className={styles['question-bank-manage']} 
+        style={{marginTop: '20px'}} 
+      >
+        <Col>题卡:</Col>
+        <Col span={7.5} > 
+          <QuestionCard
+            label={label}
+            content={content}
+            onDragStop={onDragStop}
+            resizeChildrenRnd={resizeChildrenRnd}
+            onResizeStop={onResizeStop}
+            onInput={onInput}
+            deleteImg={deleteImg}
+            saveImageUrl={saveImageUrl}
+          />
+        </Col>
+        <Col span={13.5}>
+          <AnswerCard
+            answerList={answerList}
+            saveImageAnswerUrl={saveImageAnswerUrl}
+            onChangeAnswer={onChangeAnswer}
+          />
+        </Col>
+      </Row>
+      <Row justify="center" style={{marginTop: "20px"}} >
+        <Col><Button type="primary" onClick={submit} > 
+          {submitButtonLabel}
+      </Button></Col>
+      </Row>
+    </BaseTmp>
+  )
+
+}
+
+export default QuestionBankManage
+
+

+ 73 - 0
src/pages/QuestionBankManage/style/index.less

@@ -0,0 +1,73 @@
+.question {
+  width: 378px;
+  height: 667px;
+  border: 1px solid #000;
+  box-sizing: border-box;
+  .question-card-iphonex {
+    img {
+      width: 375px;
+      height: 90px;
+    }
+  }
+
+  .question-card-timing {
+    width: 100%;
+    height: 48px;
+    text-align: center;
+  }
+
+  .border-line {
+    border-top: 1px solid #000;
+    border-bottom: 1px solid #000;
+    box-sizing: border-box;
+  }
+
+  .stem {
+    width: 100%;
+    height: 91px;
+    text-align: center;
+    border-bottom: 1px solid #000;
+    box-sizing: border-box;
+  }
+
+  .opration-area {
+    height: 438px;
+  }
+
+  .answer-area {
+    height: 100%;
+    border-left: 1px solid #000;
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    align-items: center;
+    box-sizing: border-box;
+  }
+
+}
+
+.answer-card {
+  width: 700px;
+  height: 667px;
+  border: 1px solid #000;
+  // border-top: none;
+  border-left: none;
+}
+
+.question-bank-manage {
+  min-width: 1300px;
+}
+
+.ability-point {
+
+}
+
+.answer-setting {
+  
+}
+
+.render-button {
+  width: 40px;
+  height: 40px;
+  border-radius: 50%;
+}

+ 13 - 13
src/pages/user/Login/index.tsx

@@ -4,7 +4,7 @@ import {
   UserOutlined,
 } from '@ant-design/icons';
 import { Alert, message, Tabs } from 'antd';
-import React, { useState } from 'react';
+import React, { useEffect, useState } from 'react';
 import { ProFormText, LoginForm } from '@ant-design/pro-form';
 import { useIntl, history, FormattedMessage, SelectLang, useModel } from 'umi';
 import Footer from '@/components/Footer';
@@ -42,27 +42,27 @@ const Login: React.FC = () => {
     }
   };
 
+
   const handleSubmit = async (values: API.LoginParams) => {
     try {
       // 登录
-      const msg = await login({ ...values, type });
-      if (msg.status === 'ok') {
+      // const msg = await login({ ...values, type });
+      // if (msg.status === 'ok') {
         const defaultLoginSuccessMessage = intl.formatMessage({
           id: 'pages.login.success',
           defaultMessage: '登录成功!',
         });
         message.success(defaultLoginSuccessMessage);
-        await fetchUserInfo();
-        /** 此方法会跳转到 redirect 参数所在的位置 */
+        // await fetchUserInfo();
+        
         if (!history) return;
-        const { query } = history.location;
-        const { redirect } = query as { redirect: string };
-        history.push(redirect || '/');
+        
+        history.push( '/QuestionBankManage/question-bank');
         return;
-      }
-      console.log(msg);
+      // }
+      // console.log(msg);
       // 如果失败去设置用户错误信息
-      setUserLoginState(msg);
+      // setUserLoginState(msg);
     } catch (error) {
       const defaultLoginFailureMessage = intl.formatMessage({
         id: 'pages.login.failure',
@@ -80,8 +80,8 @@ const Login: React.FC = () => {
       </div>
       <div className={styles.content}>
         <LoginForm
-          logo={<img alt="logo" src="https://s4.aconvert.com/convert/p3r68-cdx67/aydsj-vzrjo.svg" />}
-          title="Thinking Magic "
+          logo={<img alt="logo" src={require(`@/static/common/logo.png`)} />}
+          title="Think Magic "
           subTitle={intl.formatMessage({ id: 'pages.layouts.userLayout.title' })}
           initialValues={{
             autoLogin: true,

+ 9 - 0
src/services/api/index.ts

@@ -0,0 +1,9 @@
+import * as upload from './upload'
+import * as question from './question'
+
+export default {
+  upload,
+  question
+}
+
+

+ 214 - 0
src/services/api/question.ts

@@ -0,0 +1,214 @@
+import { CRequest } from "./request"
+import Global from "@/common/global"
+const proxyApi = Global.getIsDev ? '/api' : 'https://open.luojigou.vip/'
+
+/** /gamecontest/admin/bank */
+export async function getQuestionBank (options?: any) {
+  return CRequest<API.ResponseFormat>(`${proxyApi}/gamecontest/admin/bank`, {
+    method: "GET",
+    ...options
+  })
+}
+
+/** gamecontest/admin/card */
+export async function getQuestionCard (params: any, options?: any) {
+  return CRequest<API.ResponseFormat>(`${proxyApi}/gamecontest/admin/card`, {
+    method: "GET",
+    params: params,
+    ...options
+  })
+}
+
+/** /gamecontest/admin/bank/add */
+export async function addQuestionBank (
+  params: {
+    bankName: string
+  },
+  options?: any
+) {
+  return CRequest<API.ResponseFormat>(`${proxyApi}/gamecontest/admin/bank/add`, {
+    method: "POST",
+    params: params,
+    ...options
+  })
+}
+
+/** /gamecontest/admin/bank/add */
+export async function delQuestionBank (id: number | string) {
+  return CRequest<API.ResponseFormat>(`${proxyApi}/gamecontest/admin/bank/${id}`, {
+    method: "DELETE"
+  })
+}
+
+/** gamecontest/admin/bank */
+export async function putQuestionBank (data: any) {
+  return CRequest<API.ResponseFormat>(`${proxyApi}/gamecontest/admin/bank`, {
+    data,
+    method: "PUT"
+  })
+}
+
+/** /gamecontest/admin/textbook */
+export async function getQuestionBook (
+  params: { bankId: string | number}
+) {
+  return CRequest<API.ResponseFormat>(`${proxyApi}/gamecontest/admin/textbook`, {
+    params,
+    method: "GET"
+  })
+}
+
+/** /gamecontest/admin/textbook/addTextBook */
+export async function postQuestionBook (
+  data: any
+) {
+  return CRequest<API.ResponseFormat>(`${proxyApi}/gamecontest/admin/textbook/addTextBook`, {
+    data,
+    method: "POST"
+  })
+}
+
+export async function putQuestionBook (
+  id: string,
+  params: {label: string}
+) {
+  return CRequest<API.ResponseFormat>(`${proxyApi}/gamecontest/admin/textbook/${id}`, {
+    params,
+    method: "PUT"
+  })
+}
+
+/**
+ * 
+ * @description 启用或者停用题卡
+ * @api /gamecontest/admin/card/{id}
+ * 
+ */
+
+export async function useQuestionCard (
+  id: string,
+  params: {
+    state: number
+  }
+) {
+  return CRequest<API.ResponseFormat>(`${proxyApi}/gamecontest/admin/card/${id}`, {
+    params,
+    method: "PUT"
+  })
+}
+
+/**
+ * 
+ * @description 删除题卡
+ * @api /gamecontest/admin/card/{id}
+ * 
+ */
+
+export async function delQuestionCard (
+  id: string,
+) {
+  return CRequest<API.ResponseFormat>(`${proxyApi}/gamecontest/admin/card/${id}`, {
+    method: "PUT"
+  })
+}
+
+
+/**
+ * 
+ * @description 新增题卡
+ * @api /gamecontest/admin/card/{state}
+ * 
+ */
+
+//  {
+// 	"ableVar": "",
+// 	"bookId": "",
+// 	"content": [
+// 		{
+// 			"bg": "",
+// 			"cardId": "",
+// 			"color": "",
+// 			"createTime": "",
+// 			"h": 0,
+// 			"id": "",
+// 			"ih": 0,
+// 			"imgUrl": "",
+// 			"iw": 0,
+// 			"ix": 0,
+// 			"iy": 0,
+// 			"label": "",
+// 			"ph": 0,
+// 			"pw": 0,
+// 			"px": 0,
+// 			"py": 0,
+// 			"rotate": 0,
+// 			"roundOrArrow": "",
+// 			"updateTime": "",
+// 			"voiceUrl": "",
+// 			"w": 0,
+// 			"x": 0,
+// 			"y": 0
+// 		}
+// 	],
+// 	"createTime": "",
+// 	"id": "",
+// 	"label": "",
+// 	"options": [
+// 		{
+// 			"cardId": "",
+// 			"color": "",
+// 			"createTime": "",
+// 			"id": "",
+// 			"ifTrue": true,
+// 			"imgUrl": "",
+// 			"label": "",
+// 			"myAnswer": "",
+// 			"updateTime": ""
+// 		}
+// 	],
+// 	"rightOptionCount": 0,
+// 	"state": 0,
+// 	"takeTime": 0,
+// 	"title": "",
+// 	"totalOptionCount": 0,
+// 	"updateTime": "",
+// 	"voiceUrl": ""
+// }
+
+export async function addQuestionCard (
+  state: 0 | 1,
+  data: any
+) {
+  return CRequest<API.ResponseFormat>(`${proxyApi}/gamecontest/admin/card/${state}`, {
+    data,
+    method: "POST"
+  })
+}
+
+/**
+ * 编辑题卡
+ */
+export async function putQuestionCard (
+  state: 0 | 1,
+  data: any
+) {
+  return CRequest<API.ResponseFormat>(`${proxyApi}/gamecontest/admin/card/questionCard/${state}`, {
+    data,
+    method: "PUT"
+  })
+}
+
+
+
+
+/**
+ * @description  根据id 获取 题卡数据
+ * 
+ */
+export async function getQuestionCardByid (cardId: string | number) {
+  return CRequest<API.ResponseFormat>(`${proxyApi}/gamecontest/admin/card/${cardId}`, {
+    method: "GET"
+  })
+}
+
+

+ 11 - 0
src/services/api/request.ts

@@ -0,0 +1,11 @@
+import { request } from 'umi'
+
+
+export const CRequest = <T>(
+  url: string,
+  data: any
+): Promise<T> => {
+  return new Promise( async resolve => {
+    resolve( (await request(url, data)) )
+  })
+}

+ 6 - 0
src/services/api/typing.d.ts

@@ -0,0 +1,6 @@
+declare namespace API {
+  type ResponseFormat = {
+    status: number,
+    result: any
+  }
+}

+ 26 - 0
src/services/api/upload.ts

@@ -0,0 +1,26 @@
+import { request } from 'umi'
+
+
+/** admin/file/uploadImage */
+export async function uploadImage (options?: any) {
+  return request('/api/admin/file/uploadImage', {
+    method: 'POST',
+    headers: {
+      'Content-Type': 'multipart/form-data',
+    },
+    ...options
+  })
+}
+
+
+export async function getTest (options?: any) {
+  return request('/api/gamecontest/location', {
+    method: 'GET',
+    ...options
+  })
+}
+
+
+
+
+

+ 6 - 0
src/services/base.ts

@@ -0,0 +1,6 @@
+import { RequestConfig } from 'umi'
+
+export const request: RequestConfig = {
+  middlewares: [],
+  requestInterceptors: [  ]
+}

BIN
src/static/common/iPhoneX-top.png


BIN
src/static/common/logo.png


+ 1 - 1
tsconfig.json

@@ -39,4 +39,4 @@
     "mock/*"
   ],
   "exclude": ["node_modules", "build", "dist", "scripts", "src/.umi/*", "webpack", "jest"]
-}
+}