wjx 1 jaar geleden
bovenliggende
commit
1a28af5aad
50 gewijzigde bestanden met toevoegingen van 4040 en 1930 verwijderingen
  1. 70 75
      config/routes.ts
  2. 101 98
      package.json
  3. BIN
      public/image/play.png
  4. 141 137
      src/app.tsx
  5. 27 32
      src/components/Footer/index.tsx
  6. 43 0
      src/components/LookVideo/index.less
  7. 51 0
      src/components/LookVideo/index.tsx
  8. 43 0
      src/components/Material/index.tsx
  9. 146 133
      src/components/RightContent/AvatarDropdown.tsx
  10. 118 120
      src/components/UploadImg/index.tsx
  11. 90 100
      src/components/UploadZip/index.tsx
  12. 42 0
      src/components/VideoNews/index.less
  13. 39 0
      src/components/VideoNews/index.tsx
  14. 0 62
      src/const.ts
  15. 81 0
      src/const.tsx
  16. 77 55
      src/global.less
  17. 57 54
      src/locales/zh-CN/menu.ts
  18. 84 0
      src/pages/Account/Center/Center.less
  19. 102 0
      src/pages/Account/Center/ModalCenter.tsx
  20. 11 0
      src/pages/Account/Center/data.d.ts
  21. 99 0
      src/pages/Account/Center/index.tsx
  22. 81 0
      src/pages/Account/Center/tableConfig.tsx
  23. 79 0
      src/pages/Download/index.tsx
  24. 77 0
      src/pages/Download/tableConfig.tsx
  25. 109 0
      src/pages/Income/index.tsx
  26. 106 0
      src/pages/Income/tableConfig.tsx
  27. 205 136
      src/pages/MyTask/index.tsx
  28. 283 167
      src/pages/MyTask/taskModal.tsx
  29. 24 0
      src/pages/Opus/components/Details.tsx
  30. 38 0
      src/pages/Opus/components/VideoOpus.tsx
  31. 40 0
      src/pages/Opus/components/index.less
  32. 95 0
      src/pages/Opus/index.less
  33. 113 0
      src/pages/Opus/index.tsx
  34. 143 0
      src/pages/Opus/opusRoll.tsx
  35. 6 0
      src/pages/Task/index.less
  36. 144 148
      src/pages/Task/index.tsx
  37. 112 0
      src/pages/Task/submitTask.tsx
  38. 114 0
      src/pages/Task/video.tsx
  39. 275 272
      src/pages/User/Login/index.tsx
  40. 141 141
      src/requestErrorConfig.ts
  41. 93 92
      src/services/ant-design-pro/typings.d.ts
  42. 15 0
      src/services/task-api/center.ts
  43. 30 0
      src/services/task-api/download.ts
  44. 16 0
      src/services/task-api/income.ts
  45. 44 30
      src/services/task-api/myTask.ts
  46. 16 0
      src/services/task-api/opus.ts
  47. 30 0
      src/services/task-api/task.ts
  48. 107 59
      src/services/task-api/typings.d.ts
  49. 9 0
      src/services/typings.d.ts
  50. 73 19
      src/utils/index.tsx

+ 70 - 75
config/routes.ts

@@ -1,75 +1,70 @@
-/**
- * @name umi 的路由配置
- * @description 只支持 path,component,routes,redirect,wrappers,name,icon 的配置
- * @param path  path 只支持两种占位符配置,第一种是动态参数 :id 的形式,第二种是 * 通配符,通配符只能出现路由字符串的最后。
- * @param component 配置 location 和 path 匹配后用于渲染的 React 组件路径。可以是绝对路径,也可以是相对路径,如果是相对路径,会从 src/pages 开始找起。
- * @param routes 配置子路由,通常在需要为多个路径增加 layout 组件时使用。
- * @param redirect 配置路由跳转
- * @param wrappers 配置路由组件的包装组件,通过包装组件可以为当前的路由组件组合进更多的功能。 比如,可以用于路由级别的权限校验
- * @param name 配置路由的标题,默认读取国际化文件 menu.ts 中 menu.xxxx 的值,如配置 name 为 login,则读取 menu.ts 中 menu.login 的取值作为标题
- * @param icon 配置路由的图标,取值参考 https://ant.design/components/icon-cn, 注意去除风格后缀和大小写,如想要配置图标为 <StepBackwardOutlined /> 则取值应为 stepBackward 或 StepBackward,如想要配置图标为 <UserOutlined /> 则取值应为 user 或者 User
- * @doc https://umijs.org/docs/guides/routes
- */
-export default [
-  {
-    path: '/user',
-    layout: false,
-    routes: [
-      {
-        name: 'login',
-        path: '/user/login',
-        component: './User/Login',
-      },
-    ],
-  },
-  {
-    path: '/home',
-    name: 'home',
-    icon: 'home',
-    component: './Home',
-  },
-  {
-    path: '/task',
-    name: 'task',
-    icon: 'crown',
-    component: './Task',
-  },
-  {
-    path: '/myTask',
-    name: 'my-task',
-    icon: 'fileText',
-    component: './MyTask',
-  },
-  // {
-  //   path: '/admin',
-  //   name: 'admin',
-  //   icon: 'crown',
-  //   access: 'canAdmin',
-  //   routes: [
-  //     {
-  //       path: '/admin',
-  //       redirect: '/admin/sub-page',
-  //     },
-  //     {
-  //       path: '/admin/sub-page',
-  //       name: 'sub-page',
-  //       component: './Admin',
-  //     },
-  //   ],
-  // },
-  // {
-  //   name: 'list.table-list',
-  //   icon: 'table',
-  //   path: '/list',
-  //   component: './TableList',
-  // },
-  {
-    path: '/',
-    redirect: '/Home',
-  },
-  {
-    path: '*',
-    layout: false,
-    component: './404',
-  },
-];
+/**
+ * @name umi 的路由配置
+ * @description 只支持 path,component,routes,redirect,wrappers,name,icon 的配置
+ * @param path  path 只支持两种占位符配置,第一种是动态参数 :id 的形式,第二种是 * 通配符,通配符只能出现路由字符串的最后。
+ * @param component 配置 location 和 path 匹配后用于渲染的 React 组件路径。可以是绝对路径,也可以是相对路径,如果是相对路径,会从 src/pages 开始找起。
+ * @param routes 配置子路由,通常在需要为多个路径增加 layout 组件时使用。
+ * @param redirect 配置路由跳转
+ * @param wrappers 配置路由组件的包装组件,通过包装组件可以为当前的路由组件组合进更多的功能。 比如,可以用于路由级别的权限校验
+ * @param name 配置路由的标题,默认读取国际化文件 menu.ts 中 menu.xxxx 的值,如配置 name 为 login,则读取 menu.ts 中 menu.login 的取值作为标题
+ * @param icon 配置路由的图标,取值参考 https://ant.design/components/icon-cn, 注意去除风格后缀和大小写,如想要配置图标为 <StepBackwardOutlined /> 则取值应为 stepBackward 或 StepBackward,如想要配置图标为 <UserOutlined /> 则取值应为 user 或者 User
+ * @doc https://umijs.org/docs/guides/routes
+ */
+export default [
+  {
+    path: '/user',
+    layout: false,
+    routes: [
+      {
+        name: 'login',
+        path: '/user/login',
+        component: './User/Login',
+      },
+    ],
+  },
+  {
+    path: '/home',
+    name: 'home',
+    icon: 'home',
+    component: './Home',
+  },
+  {
+    path: '/task',
+    name: 'task',
+    icon: 'crown',
+    component: './Task',
+  },
+  {
+    path: '/myTask',
+    name: 'my-task',
+    icon: 'fileText',
+    component: './MyTask',
+  },
+  {
+    path: '/opus',
+    name: 'opus',
+    icon: 'snippets',
+    component: './Opus',
+  },
+  {
+    path: '/income',
+    name: 'income',
+    icon: 'accountBook',
+    component: './Income',
+  },
+  {
+    path: '/download',
+    name: 'download',
+    icon: 'download',
+    component: './Download',
+  },
+  {
+    path: '/',
+    redirect: '/Home',
+  },
+  {
+    path: '*',
+    layout: false,
+    component: './404',
+  },
+];

+ 101 - 98
package.json

@@ -1,98 +1,101 @@
-{
-  "name": "ant-design-pro",
-  "version": "6.0.0",
-  "private": true,
-  "description": "An out-of-box UI solution for enterprise applications",
-  "scripts": {
-    "analyze": "cross-env ANALYZE=1 max build",
-    "build": "max build",
-    "deploy": "npm run build && npm run gh-pages",
-    "dev": "npm run start:dev",
-    "gh-pages": "gh-pages -d dist",
-    "i18n-remove": "pro i18n-remove --locale=zh-CN --write",
-    "postinstall": "max setup",
-    "jest": "jest",
-    "lint": "npm run lint:js && npm run lint:prettier && npm run tsc",
-    "lint-staged": "lint-staged",
-    "lint-staged:js": "eslint --ext .js,.jsx,.ts,.tsx ",
-    "lint:fix": "eslint --fix --cache --ext .js,.jsx,.ts,.tsx --format=pretty ./src ",
-    "lint:js": "eslint --cache --ext .js,.jsx,.ts,.tsx --format=pretty ./src",
-    "lint:prettier": "prettier -c --write \"**/**.{js,jsx,tsx,ts,less,md,json}\" --end-of-line auto",
-    "openapi": "max openapi",
-    "prepare": "husky install",
-    "prettier": "prettier -c --write \"**/**.{js,jsx,tsx,ts,less,md,json}\"",
-    "preview": "npm run build && max preview --port 8000",
-    "record": "cross-env NODE_ENV=development REACT_APP_ENV=test max record --scene=login",
-    "serve": "umi-serve",
-    "start": "cross-env UMI_ENV=dev max dev",
-    "start:dev": "cross-env REACT_APP_ENV=dev MOCK=none UMI_ENV=dev max dev",
-    "start:no-mock": "cross-env MOCK=none UMI_ENV=dev max dev",
-    "start:pre": "cross-env REACT_APP_ENV=pre UMI_ENV=dev max dev",
-    "start:test": "cross-env REACT_APP_ENV=test MOCK=none UMI_ENV=dev max dev",
-    "test": "jest",
-    "test:coverage": "npm run jest -- --coverage",
-    "test:update": "npm run jest -- -u",
-    "tsc": "tsc --noEmit"
-  },
-  "lint-staged": {
-    "**/*.{js,jsx,ts,tsx}": "npm run lint-staged:js",
-    "**/*.{js,jsx,tsx,ts,less,md,json}": [
-      "prettier --write"
-    ]
-  },
-  "browserslist": [
-    "> 1%",
-    "last 2 versions",
-    "not ie <= 10"
-  ],
-  "dependencies": {
-    "@ant-design/icons": "^4.8.0",
-    "@ant-design/pro-components": "^2.6.31",
-    "@ant-design/use-emotion-css": "1.0.4",
-    "@umijs/route-utils": "^2.2.2",
-    "ahooks": "^3.7.8",
-    "antd": "^5.2.2",
-    "classnames": "^2.3.2",
-    "lodash": "^4.17.21",
-    "moment": "^2.29.4",
-    "omit.js": "^2.0.2",
-    "rc-menu": "^9.8.2",
-    "rc-util": "^5.27.2",
-    "react": "^18.2.0",
-    "react-dev-inspector": "^1.8.4",
-    "react-dom": "^18.2.0",
-    "react-helmet-async": "^1.3.0"
-  },
-  "devDependencies": {
-    "@ant-design/pro-cli": "^2.1.5",
-    "@testing-library/react": "^13.4.0",
-    "@types/classnames": "^2.3.1",
-    "@types/express": "^4.17.17",
-    "@types/history": "^4.7.11",
-    "@types/jest": "^29.4.0",
-    "@types/lodash": "^4.14.191",
-    "@types/react": "^18.0.28",
-    "@types/react-dom": "^18.0.11",
-    "@types/react-helmet": "^6.1.6",
-    "@umijs/fabric": "^2.14.1",
-    "@umijs/lint": "^4.0.52",
-    "@umijs/max": "^4.0.52",
-    "cross-env": "^7.0.3",
-    "eslint": "^8.34.0",
-    "express": "^4.18.2",
-    "gh-pages": "^3.2.3",
-    "husky": "^7.0.4",
-    "jest": "^29.4.3",
-    "jest-environment-jsdom": "^29.4.3",
-    "lint-staged": "^10.5.4",
-    "mockjs": "^1.1.0",
-    "prettier": "^2.8.4",
-    "swagger-ui-dist": "^4.15.5",
-    "ts-node": "^10.9.1",
-    "typescript": "^4.9.5",
-    "umi-presets-pro": "^2.0.2"
-  },
-  "engines": {
-    "node": ">=12.0.0"
-  }
-}
+{
+  "name": "qc-sourcematerial",
+  "version": "6.0.0",
+  "private": true,
+  "description": "An out-of-box UI solution for enterprise applications",
+  "scripts": {
+    "analyze": "cross-env ANALYZE=1 max build",
+    "build": "max build",
+    "deploy": "npm run build && npm run gh-pages",
+    "dev": "npm run start:dev",
+    "gh-pages": "gh-pages -d dist",
+    "i18n-remove": "pro i18n-remove --locale=zh-CN --write",
+    "postinstall": "max setup",
+    "jest": "jest",
+    "lint": "npm run lint:js && npm run lint:prettier && npm run tsc",
+    "lint-staged": "lint-staged",
+    "lint-staged:js": "eslint --ext .js,.jsx,.ts,.tsx ",
+    "lint:fix": "eslint --fix --cache --ext .js,.jsx,.ts,.tsx --format=pretty ./src ",
+    "lint:js": "eslint --cache --ext .js,.jsx,.ts,.tsx --format=pretty ./src",
+    "lint:prettier": "prettier -c --write \"**/**.{js,jsx,tsx,ts,less,md,json}\" --end-of-line auto",
+    "openapi": "max openapi",
+    "prepare": "husky install",
+    "prettier": "prettier -c --write \"**/**.{js,jsx,tsx,ts,less,md,json}\"",
+    "preview": "npm run build && max preview --port 8000",
+    "record": "cross-env NODE_ENV=development REACT_APP_ENV=test max record --scene=login",
+    "serve": "umi-serve",
+    "start": "cross-env UMI_ENV=dev max dev",
+    "start:dev": "cross-env REACT_APP_ENV=dev MOCK=none UMI_ENV=dev max dev",
+    "start:no-mock": "cross-env MOCK=none UMI_ENV=dev max dev",
+    "start:pre": "cross-env REACT_APP_ENV=pre UMI_ENV=dev max dev",
+    "start:test": "cross-env REACT_APP_ENV=test MOCK=none UMI_ENV=dev max dev",
+    "test": "jest",
+    "test:coverage": "npm run jest -- --coverage",
+    "test:update": "npm run jest -- -u",
+    "tsc": "tsc --noEmit"
+  },
+  "lint-staged": {
+    "**/*.{js,jsx,ts,tsx}": "npm run lint-staged:js",
+    "**/*.{js,jsx,tsx,ts,less,md,json}": [
+      "prettier --write"
+    ]
+  },
+  "browserslist": [
+    "> 1%",
+    "last 2 versions",
+    "not ie <= 10"
+  ],
+  "dependencies": {
+    "@ant-design/icons": "^4.8.0",
+    "@ant-design/pro-components": "^2.6.31",
+    "@ant-design/use-emotion-css": "1.0.4",
+    "@types/spark-md5": "^3.0.3",
+    "@umijs/route-utils": "^2.2.2",
+    "ahooks": "^3.7.8",
+    "antd": "^5.2.2",
+    "classnames": "^2.3.2",
+    "lodash": "^4.17.21",
+    "moment": "^2.29.4",
+    "omit.js": "^2.0.2",
+    "rc-menu": "^9.8.2",
+    "rc-util": "^5.27.2",
+    "react": "^18.2.0",
+    "react-dev-inspector": "^1.8.4",
+    "react-dom": "^18.2.0",
+    "react-helmet-async": "^1.3.0",
+    "react-infinite-scroll-component": "^6.1.0",
+    "spark-md5": "^3.0.2"
+  },
+  "devDependencies": {
+    "@ant-design/pro-cli": "^2.1.5",
+    "@testing-library/react": "^13.4.0",
+    "@types/classnames": "^2.3.1",
+    "@types/express": "^4.17.17",
+    "@types/history": "^4.7.11",
+    "@types/jest": "^29.4.0",
+    "@types/lodash": "^4.14.191",
+    "@types/react": "^18.0.28",
+    "@types/react-dom": "^18.0.11",
+    "@types/react-helmet": "^6.1.6",
+    "@umijs/fabric": "^2.14.1",
+    "@umijs/lint": "^4.0.52",
+    "@umijs/max": "^4.0.52",
+    "cross-env": "^7.0.3",
+    "eslint": "^8.34.0",
+    "express": "^4.18.2",
+    "gh-pages": "^3.2.3",
+    "husky": "^7.0.4",
+    "jest": "^29.4.3",
+    "jest-environment-jsdom": "^29.4.3",
+    "lint-staged": "^10.5.4",
+    "mockjs": "^1.1.0",
+    "prettier": "^2.8.4",
+    "swagger-ui-dist": "^4.15.5",
+    "ts-node": "^10.9.1",
+    "typescript": "^4.9.5",
+    "umi-presets-pro": "^2.0.2"
+  },
+  "engines": {
+    "node": ">=12.0.0"
+  }
+}

BIN
public/image/play.png


+ 141 - 137
src/app.tsx

@@ -1,137 +1,141 @@
-import Footer from '@/components/Footer';
-import { Question, SelectLang } from '@/components/RightContent';
-import { LinkOutlined } from '@ant-design/icons';
-import type { Settings as LayoutSettings } from '@ant-design/pro-components';
-import { SettingDrawer } from '@ant-design/pro-components';
-import type { RunTimeLayoutConfig } from '@umijs/max';
-import { history, Link } from '@umijs/max';
-import defaultSettings from '../config/defaultSettings';
-import { errorConfig } from './requestErrorConfig';
-import { currentUser as queryCurrentUser } from './services/ant-design-pro/api';
-import React from 'react';
-import { AvatarDropdown, AvatarName } from './components/RightContent/AvatarDropdown';
-import { Button, Result, notification } from 'antd';
-const isDev = process.env.NODE_ENV === 'development';
-const loginPath = '/user/login';
-
-/**
- * @see  https://umijs.org/zh-CN/plugins/plugin-initial-state
- * */
-export async function getInitialState(): Promise<{
-  settings?: Partial<LayoutSettings>;
-  currentUser?: API.CurrentUser;
-  loading?: boolean;
-  fetchUserInfo?: () => Promise<API.CurrentUser | undefined>;
-}> {
-  const fetchUserInfo = async () => {
-    try {
-      const msg = await queryCurrentUser({
-        skipErrorHandler: true,
-      });
-      return msg.data;
-    } catch (error) {
-      history.push(loginPath);
-    }
-    return undefined;
-  };
-  // 如果不是登录页面,执行
-  const { location } = history;
-  if (location.pathname !== loginPath) {
-    const currentUser = await fetchUserInfo();
-    return {
-      fetchUserInfo,
-      currentUser,
-      settings: defaultSettings as Partial<LayoutSettings>,
-    };
-  }
-  return {
-    fetchUserInfo,
-    settings: defaultSettings as Partial<LayoutSettings>,
-  };
-}
-
-// ProLayout 支持的api https://procomponents.ant.design/components/layout
-export const layout: RunTimeLayoutConfig = ({ initialState, setInitialState }) => {
-  return {
-    actionsRender: () => [<Question key="doc" />], // <SelectLang key="SelectLang" />
-    avatarProps: {
-      src: 'https://xsgames.co/randomusers/avatar.php?g=pixel&key=${i}',
-      title: <AvatarName />,
-      render: (_, avatarChildren) => {
-        return <AvatarDropdown>{avatarChildren}</AvatarDropdown>;
-      },
-    },
-    waterMarkProps: {
-      content: initialState?.currentUser?.mobile,
-    },
-    footerRender: () => <Footer />,
-    onPageChange: () => {
-      const { location } = history;
-      // 如果没有登录,重定向到 login
-      if (!initialState?.currentUser && location.pathname !== loginPath) {
-        history.push(loginPath);
-      }
-    },
-    layoutBgImgList: [
-      {
-        src: 'https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/D2LWSqNny4sAAAAAAAAAAAAAFl94AQBr',
-        left: 85,
-        bottom: 100,
-        height: '303px',
-      },
-      {
-        src: 'https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/C2TWRpJpiC0AAAAAAAAAAAAAFl94AQBr',
-        bottom: -68,
-        right: -45,
-        height: '303px',
-      },
-      {
-        src: 'https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/F6vSTbj8KpYAAAAAAAAAAAAAFl94AQBr',
-        bottom: 0,
-        left: 0,
-        width: '331px',
-      },
-    ],
-    links: isDev
-      ? [
-        <Link key="openapi" to="http://47.97.38.17:9988/swagger-ui.html#/" target="_blank">
-          <LinkOutlined />
-          <span>OpenAPI 文档</span>
-        </Link>,
-      ]
-      : [],
-    menuHeaderRender: undefined,
-    // 自定义 403 页面
-    // unAccessible: <div>unAccessible</div>,
-    // 增加一个 loading 的状态
-    childrenRender: (children) => {
-      // if (initialState?.loading) return <PageLoading />;
-      return (
-        <>
-          {children}
-          <SettingDrawer
-            disableUrlParams
-            enableDarkTheme
-            settings={initialState?.settings}
-            onSettingChange={(settings) => {
-              setInitialState((preInitialState) => ({
-                ...preInitialState,
-                settings,
-              }));
-            }}
-          />
-        </>
-      );
-    },
-    ...initialState?.settings,
-  };
-};
-
-/**
- * @name request 配置,可以配置错误处理
- * 它基于 axios 和 ahooks 的 useRequest 提供了一套统一的网络请求和错误处理方案。
- * @doc https://umijs.org/docs/max/request#配置
- */
-export const request = {
-  ...errorConfig
-};
+import Footer from '@/components/Footer';
+import { Question, SelectLang } from '@/components/RightContent';
+import { LinkOutlined } from '@ant-design/icons';
+import type { Settings as LayoutSettings } from '@ant-design/pro-components';
+import { SettingDrawer } from '@ant-design/pro-components';
+import type { RunTimeLayoutConfig } from '@umijs/max';
+import { history, Link } from '@umijs/max';
+import defaultSettings from '../config/defaultSettings';
+import { errorConfig } from './requestErrorConfig';
+import { currentUser as queryCurrentUser } from './services/ant-design-pro/api';
+import React from 'react';
+import { AvatarDropdown, AvatarName } from './components/RightContent/AvatarDropdown';
+const isDev = process.env.NODE_ENV === 'development';
+const loginPath = '/user/login';
+// 不显示页脚页面配置
+const doNotFooter = ['/opus'];
+
+/**
+ * @see  https://umijs.org/zh-CN/plugins/plugin-initial-state
+ * */
+export async function getInitialState(): Promise<{
+  settings?: Partial<LayoutSettings>;
+  currentUser?: API.CurrentUser;
+  loading?: boolean;
+  fetchUserInfo?: () => Promise<API.CurrentUser | undefined>;
+}> {
+  const fetchUserInfo = async () => {
+    try {
+      const msg = await queryCurrentUser({
+        skipErrorHandler: true,
+      });
+      return msg.data;
+    } catch (error) {
+      history.push(loginPath);
+    }
+    return undefined;
+  };
+  // 如果不是登录页面,执行
+  const { location } = history;
+  if (location.pathname !== loginPath) {
+    const currentUser = await fetchUserInfo();
+    return {
+      fetchUserInfo,
+      currentUser: {
+        avatar: `https://xsgames.co/randomusers/avatar.php?g=pixel&key=${Math.floor(Math.random() * 10)}`,
+        ...currentUser
+      },
+      settings: defaultSettings as Partial<LayoutSettings>,
+    };
+  }
+  return {
+    fetchUserInfo,
+    settings: defaultSettings as Partial<LayoutSettings>,
+  };
+}
+
+// ProLayout 支持的api https://procomponents.ant.design/components/layout
+export const layout: RunTimeLayoutConfig = ({ initialState, setInitialState }) => {
+  return {
+    actionsRender: () => [<Question key="doc" />], // <SelectLang key="SelectLang" />
+    avatarProps: {
+      src: initialState?.currentUser?.avatar || `https://xsgames.co/randomusers/avatar.php?g=pixel&key=${Math.floor(Math.random() * 20)}`,
+      title: <AvatarName />,
+      render: (_, avatarChildren) => {
+        return <AvatarDropdown>{avatarChildren}</AvatarDropdown>;
+      },
+    },
+    waterMarkProps: {
+      content: initialState?.currentUser?.mobile,
+    },
+    footerRender: () => !doNotFooter.includes(location.pathname)  ? <Footer /> : null,
+    onPageChange: () => {
+      const { location } = history;
+      // 如果没有登录,重定向到 login
+      if (!initialState?.currentUser && location.pathname !== loginPath) {
+        history.push(loginPath);
+      }
+    },
+    layoutBgImgList: [
+      {
+        src: 'https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/D2LWSqNny4sAAAAAAAAAAAAAFl94AQBr',
+        left: 85,
+        bottom: 100,
+        height: '303px',
+      },
+      {
+        src: 'https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/C2TWRpJpiC0AAAAAAAAAAAAAFl94AQBr',
+        bottom: -68,
+        right: -45,
+        height: '303px',
+      },
+      {
+        src: 'https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/F6vSTbj8KpYAAAAAAAAAAAAAFl94AQBr',
+        bottom: 0,
+        left: 0,
+        width: '331px',
+      },
+    ],
+    links: isDev
+      ? [
+        <Link key="openapi" to="http://47.97.38.17:9988/swagger-ui.html#/" target="_blank">
+          <LinkOutlined />
+          <span>OpenAPI 文档</span>
+        </Link>,
+      ]
+      : [],
+    menuHeaderRender: undefined,
+    // 自定义 403 页面
+    // unAccessible: <div>unAccessible</div>,
+    // 增加一个 loading 的状态
+    childrenRender: (children) => {
+      // if (initialState?.loading) return <PageLoading />;
+      return (
+        <>
+          {children}
+          <SettingDrawer
+            disableUrlParams
+            enableDarkTheme
+            settings={initialState?.settings}
+            onSettingChange={(settings) => {
+              setInitialState((preInitialState) => ({
+                ...preInitialState,
+                settings,
+              }));
+            }}
+          />
+        </>
+      );
+    },
+    ...initialState?.settings,
+  };
+};
+
+/**
+ * @name request 配置,可以配置错误处理
+ * 它基于 axios 和 ahooks 的 useRequest 提供了一套统一的网络请求和错误处理方案。
+ * @doc https://umijs.org/docs/max/request#配置
+ */
+export const request = {
+  ...errorConfig
+};

+ 27 - 32
src/components/Footer/index.tsx

@@ -1,32 +1,27 @@
-import { DefaultFooter } from '@ant-design/pro-components';
-import { useIntl } from '@umijs/max';
-import React from 'react';
-
-const Footer: React.FC = () => {
-  const intl = useIntl();
-  const defaultMessage = intl.formatMessage({
-    id: 'app.copyright.produced',
-    defaultMessage: '杭州趣程出品',
-  });
-
-  const currentYear = new Date().getFullYear();
-
-  return (
-    <DefaultFooter
-      style={{
-        background: 'none',
-      }}
-      copyright={`${currentYear} ${defaultMessage}`}
-      links={[
-        {
-          key: 'qc',
-          title: '趣程',
-          href: 'http://www.questnet.cn/',
-          blankTarget: true,
-        },
-      ]}
-    />
-  );
-};
-
-export default Footer;
+import { DefaultFooter } from '@ant-design/pro-components';
+import React from 'react';
+
+const Footer: React.FC = () => {
+  const defaultMessage = "杭州趣程出品";
+
+  const currentYear = new Date().getFullYear();
+
+  return (
+    <DefaultFooter
+      style={{
+        background: 'none',
+      }}
+      copyright={`${currentYear} ${defaultMessage}`}
+      links={[
+        {
+          key: 'qc',
+          title: '趣程',
+          href: 'http://www.questnet.cn/',
+          blankTarget: true,
+        },
+      ]}
+    />
+  );
+};
+
+export default Footer;

+ 43 - 0
src/components/LookVideo/index.less

@@ -0,0 +1,43 @@
+.imgNews {
+    display: inline-block;
+    overflow: hidden;
+    position: relative;
+
+    &>div {
+        .img {
+            height: 80px;
+            cursor: pointer;
+        }
+        .imgList {
+            height: 45px;
+            cursor: pointer;
+        }
+    }
+
+    .mask {
+        position: absolute;
+        width: 100%;
+        height: 100%;
+        top: 0;
+        left: 0;
+        background-color: rgba(0, 0, 0, .3);
+        display: flex;
+        justify-content: center;
+        align-items: center;
+
+        &>img {
+            width: 30px;
+            height: 30px;
+            display: inline-block;
+            cursor: pointer;
+        }
+    }
+}
+
+.video {
+    width: 100%;
+    max-width: 320px;
+    max-height: 600px;
+    display: block;
+    margin: auto;
+}

+ 51 - 0
src/components/LookVideo/index.tsx

@@ -0,0 +1,51 @@
+import React, { useState } from "react"
+import { Divider, Image, Modal, Space, theme } from 'antd'
+import play from "../../../public/image/play.png"
+import style from './index.less'
+import { getVideoImgUrl } from "@/utils"
+import { CloseOutlined } from "@ant-design/icons"
+
+interface Props {
+    urlList: string[]
+}
+const LookVideo: React.FC<Props> = ({ urlList = [] }) => {
+
+    /***************************/
+    const { useToken } = theme;
+    const { token } = useToken()
+    const [count, setCount] = useState<number>(0)
+    const [toPlay, setToPlay] = useState<boolean>(false)
+    /***************************/
+
+    return urlList.length > 0 ? <div style={{ display: 'inline-block' }}>
+        <div className={style.imgNews} style={{ borderRadius: token.borderRadius }}>
+            <Image src={urlList[0] ? getVideoImgUrl(urlList[0]) : 'error'} preview={false} className={style.img} />
+            <div className={style.mask}>
+                <img src={play} onClick={(e) => { e.stopPropagation(); setToPlay(true) }} />
+            </div>
+        </div>
+        {toPlay && <div onClick={(e) => { e.stopPropagation(); }}>
+            <Modal
+                open={toPlay}
+                styles={{ body: { backgroundColor: 'rgba(0,0,0,0.8)', overflow: 'hidden', borderRadius: token.borderRadius, padding: '20px 24px' } }}
+                footer={null}
+                closeIcon={<CloseOutlined style={{ color: token.colorPrimary }} />}
+                onCancel={(e) => { e.stopPropagation(); setToPlay(false) }}
+                className="playVideo"
+            >
+                <Space wrap>
+                    {urlList.map((url, index) => <div className={style.imgNews} key={index} style={{ borderRadius: token.borderRadius, boxShadow: count === index ? `0px 0px 0px 1px #FFF` : 'none' }}>
+                        <Image src={urlList[0] ? getVideoImgUrl(urlList[index]) : 'error'} preview={false} className={style.imgList} />
+                        <div className={style.mask}>
+                            <img src={play} onClick={(e) => { e.stopPropagation(); setCount(index) }} />
+                        </div>
+                    </div>)}
+                </Space>
+                <Divider dashed/>
+                <video className={style.video} style={{ borderRadius: token.borderRadius }} src={urlList[count]} autoPlay controls>您的浏览器不支持 video 标签。</video>
+            </Modal>
+        </div>}
+    </div> : '无素材'
+}
+
+export default React.memo(LookVideo)

+ 43 - 0
src/components/Material/index.tsx

@@ -0,0 +1,43 @@
+import React, { useState } from "react"
+import { Button, Flex, Image, Space, Typography } from 'antd'
+import { RatioEnum } from "@/const"
+
+interface Props {
+    claimJson: string,
+    items?: string[],
+    resourceUrl?: string
+}
+const Material: React.FC<Props> = ({ items = [], resourceUrl, claimJson }) => {
+
+    /******************************/
+    const [visible, setVisible] = useState<boolean>(false)
+    const { size, extent } = JSON.parse(claimJson)
+    /******************************/
+
+    return <>
+        <Flex vertical={true}>
+            <Typography.Title level={5}>素材制作要求</Typography.Title>
+            <Typography.Text>素材尺寸:{size?.[0]} * {size?.[1]}</Typography.Text>
+            <Typography.Text>素材大小:{extent?.[0]}MB ~ {extent?.[1]}MB</Typography.Text>
+        </Flex>
+
+        <Space>
+            {items.length > 0 && <>
+                <Image.PreviewGroup
+                    items={items}
+                    preview={{
+                        visible: visible,
+                        onVisibleChange: (value) => {
+                            setVisible(value);
+                        },
+                    }}
+                />
+                <Button type="link" style={{ padding: 0 }} onClick={() => setVisible(true)}>素材示例</Button>
+            </>}
+            {resourceUrl && <Button type="link" style={{ paddingLeft: 0, paddingRight: 0 }} href={resourceUrl} download="资源包下载">资源包下载</Button>}
+        </Space>
+
+    </>
+}
+
+export default React.memo(Material)

+ 146 - 133
src/components/RightContent/AvatarDropdown.tsx

@@ -1,133 +1,146 @@
-import { outLogin } from '@/services/ant-design-pro/api';
-import { LogoutOutlined, SettingOutlined, UserOutlined } from '@ant-design/icons';
-import { useEmotionCss } from '@ant-design/use-emotion-css';
-import { history, useModel } from '@umijs/max';
-import { Spin } from 'antd';
-import { stringify } from 'querystring';
-import type { MenuInfo } from 'rc-menu/lib/interface';
-import React, { useCallback } from 'react';
-import { flushSync } from 'react-dom';
-import HeaderDropdown from '../HeaderDropdown';
-
-export type GlobalHeaderRightProps = {
-  menu?: boolean;
-  children?: React.ReactNode;
-};
-
-export const AvatarName = () => {
-  const { initialState } = useModel('@@initialState');
-  const { currentUser } = initialState || {};
-  return <span className="anticon">{currentUser?.mobile}</span>;
-};
-
-export const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({ menu, children }) => {
-  /**
-   * 退出登录,并且将当前的 url 保存
-   */
-  const loginOut = async () => {
-    localStorage.removeItem('Admin-Token')
-    const { search, pathname } = window.location;
-    const urlParams = new URL(window.location.href).searchParams;
-    /** 此方法会跳转到 redirect 参数所在的位置 */
-    const redirect = urlParams.get('redirect');
-    // Note: There may be security issues, please note
-    if (window.location.pathname !== '/user/login' && !redirect) {
-      history.replace({
-        pathname: '/user/login',
-        search: stringify({
-          redirect: pathname + search,
-        }),
-      });
-    }
-  };
-  const actionClassName = useEmotionCss(({ token }) => {
-    return {
-      display: 'flex',
-      height: '48px',
-      marginLeft: 'auto',
-      overflow: 'hidden',
-      alignItems: 'center',
-      padding: '0 8px',
-      cursor: 'pointer',
-      borderRadius: token.borderRadius,
-      '&:hover': {
-        backgroundColor: token.colorBgTextHover,
-      },
-    };
-  });
-  const { initialState, setInitialState } = useModel('@@initialState');
-
-  const onMenuClick = useCallback(
-    (event: MenuInfo) => {
-      const { key } = event;
-      if (key === 'logout') {
-        flushSync(() => {
-          setInitialState((s) => ({ ...s, currentUser: undefined }));
-        });
-        loginOut();
-        return;
-      }
-      history.push(`/account/${key}`);
-    },
-    [setInitialState],
-  );
-
-  const loading = (
-    <span className={actionClassName}>
-      <Spin
-        size="small"
-        style={{
-          marginLeft: 8,
-          marginRight: 8,
-        }}
-      />
-    </span>
-  );
-
-  if (!initialState) {
-    return loading;
-  }
-
-  const { currentUser } = initialState;
-
-  if (!currentUser || !currentUser.mobile) {
-    return loading;
-  }
-
-  const menuItems = [
-    ...(menu
-      ? [
-          {
-            key: 'center',
-            icon: <UserOutlined />,
-            label: '个人中心',
-          },
-          {
-            key: 'settings',
-            icon: <SettingOutlined />,
-            label: '个人设置',
-          },
-          {
-            type: 'divider' as const,
-          },
-        ]
-      : []),
-    {
-      key: 'logout',
-      icon: <LogoutOutlined />,
-      label: '退出登录',
-    },
-  ];
-
-  return (
-    <HeaderDropdown
-      menu={{
-        selectedKeys: [],
-        onClick: onMenuClick,
-        items: menuItems,
-      }}
-    >
-      {children}
-    </HeaderDropdown>
-  );
-};
+import { LogoutOutlined, SettingOutlined, UserOutlined } from '@ant-design/icons';
+import { useEmotionCss } from '@ant-design/use-emotion-css';
+import { history, useModel } from '@umijs/max';
+import { Spin } from 'antd';
+import { stringify } from 'querystring';
+import type { MenuInfo } from 'rc-menu/lib/interface';
+import React, { useCallback, useState } from 'react';
+import { flushSync } from 'react-dom';
+import HeaderDropdown from '../HeaderDropdown';
+import ModalCenter from '@/pages/Account/Center/ModalCenter';
+
+export type GlobalHeaderRightProps = {
+  menu?: boolean;
+  children?: React.ReactNode;
+};
+
+export const AvatarName = () => {
+  const { initialState } = useModel('@@initialState');
+  const { currentUser } = initialState || {};
+  return <span className="anticon">{currentUser?.mobile}</span>;
+};
+
+export const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({ menu, children }) => {
+  /**
+   * 退出登录,并且将当前的 url 保存
+   */
+  const loginOut = async () => {
+    localStorage.removeItem('Admin-Token')
+    const { search, pathname } = window.location;
+    const urlParams = new URL(window.location.href).searchParams;
+    /** 此方法会跳转到 redirect 参数所在的位置 */
+    const redirect = urlParams.get('redirect');
+    // Note: There may be security issues, please note
+    if (window.location.pathname !== '/user/login' && !redirect) {
+      history.replace({
+        pathname: '/user/login',
+        search: stringify({
+          redirect: pathname + search,
+        }),
+      });
+    }
+  };
+  const actionClassName = useEmotionCss(({ token }) => {
+    return {
+      display: 'flex',
+      height: '48px',
+      marginLeft: 'auto',
+      overflow: 'hidden',
+      alignItems: 'center',
+      padding: '0 8px',
+      cursor: 'pointer',
+      borderRadius: token.borderRadius,
+      '&:hover': {
+        backgroundColor: token.colorBgTextHover,
+      },
+    };
+  });
+  const { initialState, setInitialState } = useModel('@@initialState');
+
+  const [visible, setVisible] = useState<boolean>(false)
+  const onMenuClick = useCallback(
+    (event: MenuInfo) => {
+      const { key } = event;
+      if (key === 'logout') {
+        flushSync(() => {
+          setInitialState((s) => ({ ...s, currentUser: undefined }));
+        });
+        loginOut();
+        return;
+      }
+      if (key === 'center') {
+        setVisible(true)
+        return
+      }
+      history.push(`/account/${key}`);
+    },
+    [setInitialState],
+  );
+
+  const loading = (
+    <span className={actionClassName}>
+      <Spin
+        size="small"
+        style={{
+          marginLeft: 8,
+          marginRight: 8,
+        }}
+      />
+    </span>
+  );
+
+  if (!initialState) {
+    return loading;
+  }
+
+  const { currentUser } = initialState;
+
+  if (!currentUser || !currentUser.mobile) {
+    return loading;
+  }
+
+  const menuItems = [
+    ...(menu
+      ? [
+        {
+          key: 'center',
+          icon: <UserOutlined />,
+          label: '个人中心',
+        },
+        {
+          key: 'settings',
+          icon: <SettingOutlined />,
+          label: '个人设置',
+        },
+        {
+          type: 'divider' as const,
+        },
+      ]
+      : []),
+    {
+      key: 'center',
+      icon: <UserOutlined />,
+      label: '个人中心',
+    },
+    {
+      key: 'logout',
+      icon: <LogoutOutlined />,
+      label: '退出登录',
+    },
+  ];
+
+  return (
+    <>
+      {visible && <ModalCenter visible={visible} onClose={() => setVisible(false)} />}
+      <HeaderDropdown
+        menu={{
+          selectedKeys: [],
+          onClick: onMenuClick,
+          items: menuItems,
+        }}
+      >
+        {children}
+      </HeaderDropdown>
+    </>
+  );
+};

+ 118 - 120
src/components/UploadImg/index.tsx

@@ -1,121 +1,119 @@
-import { message, Space, Upload, Image } from "antd"
-import { PlusOutlined, LoadingOutlined } from "@ant-design/icons";
-import { RcFile } from "antd/lib/upload";
-import React, { useState } from "react";
-import './index.less'
-import { getBase64, getImgSizeProper } from "@/utils";
-import { getOssInfo } from "@/services/ant-design-pro/api";
-import { request } from "@umijs/max";
-
-interface Props {
-    value?: string,  // 图片地址
-    maxCount?: number
-    onChange?: (data: RcFile | string) => void,
-    tooltip?: JSX.Element
-    isUpload?: boolean, // 是否上传
-    sizeData?: {
-        width: number,
-        height: number
-    }
-}
-const UploadImg: React.FC<Props> = (props) => {
-
-    /** 变量START */
-    const { value, maxCount = 1, tooltip, sizeData, isUpload = false, onChange } = props
-    const [imageFile, setImageFile] = useState<string>(value || '');
-    const [loading, setLoading] = useState<boolean>(false)
-    const [visible, setVisible] = useState<boolean>(false)
-    /** 变量END */
-
-    const beforeUpload = async (file: RcFile) => {
-        const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png';
-        if (!isJpgOrPng) {
-            message.error('您只能上传JPG/PNG文件!');
-        }
-        const isLt2M = file.size / 1024 / 1024 < 2;
-        if (!isLt2M) {
-            message.error('图像必须小于2MB!');
-        }
-
-        if (sizeData) {
-            console.log('sizeData---->', sizeData)
-            let imgData: any = await getImgSizeProper(file)
-            if (sizeData?.width !== imgData.width || sizeData?.height !== imgData.height) {
-                message.error(`传入的图片大小不符, 图片大小${imgData.width}*${imgData.height}, 需要图片大小${sizeData.width}*${sizeData.height}`)
-                return false
-            }
-        }
-        return isJpgOrPng && isLt2M;
-    };
-
-    const uploadButton = (
-        <div>
-            {loading ? <LoadingOutlined /> : <PlusOutlined />}
-            {/* <div style={{ marginTop: 8 }}>Upload</div> */}
-        </div>
-    );
-
-    return <div className="myUpload">
-        <Space align="start">
-            <Upload
-                name="avatar"
-                listType="picture-card"
-                accept='image/gif,image/jpeg,image/png,image/jpg'
-                className="avatar-uploader"
-                beforeUpload={beforeUpload}
-                maxCount={maxCount}
-                showUploadList={false}
-                customRequest={(options: any) => {
-                    if (isUpload) { // 上传oss
-                        setLoading(true)
-                        getOssInfo({ type: 'image/jpeg', fileType: 'image' }).then(async res => {
-                            try {
-                                let formData = new FormData();
-                                Object.keys(res.data).forEach((key: string) => {
-                                    if (key !== 'url') {
-                                        formData.append(key, res.data[key])
-                                    }
-                                })
-                                formData.append('file', options.file)
-                                let urlData = await request(res?.data?.ossUrl, { method: 'POST', data: formData })
-                                setLoading(false)
-                                setImageFile(urlData?.data?.url);
-                                if (urlData?.data?.url) {
-                                    onChange && onChange(urlData?.data?.url)
-                                }
-                            } catch (error) {
-                                setLoading(false)
-                            }
-                        })
-                    } else {
-                        getBase64(options.file as RcFile, url => {
-                            setImageFile(url);
-                        })
-                        onChange && onChange(options.file)
-                    }
-                }}
-            >
-                {imageFile ? <Image src={imageFile} preview={false} style={{ height: '85px' }} /> : uploadButton}
-            </Upload>
-            <div>{tooltip && tooltip}</div>
-            {imageFile && <div className="look">
-                <a onClick={() => setVisible(true)}>预览</a>
-                <Image
-                    style={{ display: 'none' }}
-                    src={imageFile}
-                    preview={{
-                        visible,
-                        src: imageFile,
-                        onVisibleChange: value => {
-                            setVisible(value);
-                        },
-                    }}
-                />
-            </div>}
-
-        </Space>
-    </div>
-}
-
-
+import { message, Space, Upload, Image } from "antd"
+import { PlusOutlined, LoadingOutlined } from "@ant-design/icons";
+import { RcFile } from "antd/lib/upload";
+import React, { useState } from "react";
+import './index.less'
+import { getBase64, getImgSizeProper } from "@/utils";
+import { getOssInfo } from "@/services/ant-design-pro/api";
+import { request } from "@umijs/max";
+
+interface Props {
+    value?: string,  // 图片地址
+    maxCount?: number
+    onChange?: (data: RcFile | string) => void,
+    tooltip?: JSX.Element
+    isUpload?: boolean, // 是否上传
+    sizeData?: {
+        width: number,
+        height: number
+    }
+}
+const UploadImg: React.FC<Props> = (props) => {
+
+    /** 变量START */
+    const { value, maxCount = 1, tooltip, sizeData, isUpload = false, onChange } = props
+    const [imageFile, setImageFile] = useState<string>(value || '');
+    const [loading, setLoading] = useState<boolean>(false)
+    const [visible, setVisible] = useState<boolean>(false)
+    /** 变量END */
+
+    const beforeUpload = async (file: RcFile) => {
+        const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png';
+        if (!isJpgOrPng) {
+            message.error('您只能上传JPG/PNG文件!');
+        }
+        const isLt2M = file.size / 1024 / 1024 < 2;
+        if (!isLt2M) {
+            message.error('图像必须小于2MB!');
+        }
+
+        if (sizeData) {
+            let imgData: any = await getImgSizeProper(file)
+            if (sizeData?.width !== imgData.width || sizeData?.height !== imgData.height) {
+                message.error(`传入的图片大小不符, 图片大小${imgData.width}*${imgData.height}, 需要图片大小${sizeData.width}*${sizeData.height}`)
+                return false
+            }
+        }
+        return isJpgOrPng && isLt2M;
+    };
+
+    const uploadButton = (
+        <div>
+            {loading ? <LoadingOutlined /> : <PlusOutlined />}
+        </div>
+    );
+
+    return <div className="myUpload">
+        <Space align="start">
+            <Upload
+                name="avatar"
+                listType="picture-card"
+                accept='image/gif,image/jpeg,image/png,image/jpg'
+                className="avatar-uploader"
+                beforeUpload={beforeUpload}
+                maxCount={maxCount}
+                showUploadList={false}
+                customRequest={(options: any) => {
+                    if (isUpload) { // 上传oss
+                        setLoading(true)
+                        getOssInfo({ type: 'image/jpeg', fileType: 'image' }).then(async res => {
+                            try {
+                                let formData = new FormData();
+                                Object.keys(res.data).forEach((key: string) => {
+                                    if (key !== 'url') {
+                                        formData.append(key, res.data[key])
+                                    }
+                                })
+                                formData.append('file', options.file)
+                                let urlData = await request(res?.data?.ossUrl, { method: 'POST', data: formData })
+                                setLoading(false)
+                                setImageFile(urlData?.data?.url);
+                                if (urlData?.data?.url) {
+                                    onChange && onChange(urlData?.data?.url)
+                                }
+                            } catch (error) {
+                                setLoading(false)
+                            }
+                        })
+                    } else {
+                        getBase64(options.file as RcFile, url => {
+                            setImageFile(url);
+                        })
+                        onChange && onChange(options.file)
+                    }
+                }}
+            >
+                {imageFile ? <Image src={imageFile} preview={false} style={{ height: '85px' }} /> : uploadButton}
+            </Upload>
+            <div>{tooltip && tooltip}</div>
+            {imageFile && <div className="look">
+                <a onClick={() => setVisible(true)}>预览</a>
+                <Image
+                    style={{ display: 'none' }}
+                    src={imageFile}
+                    preview={{
+                        visible,
+                        src: imageFile,
+                        onVisibleChange: value => {
+                            setVisible(value);
+                        },
+                    }}
+                />
+            </div>}
+
+        </Space>
+    </div>
+}
+
+
 export default React.memo(UploadImg)
 export default React.memo(UploadImg)

+ 90 - 100
src/components/UploadZip/index.tsx

@@ -1,100 +1,90 @@
-import { message, Space, Upload, Image } from "antd"
-import { PlusOutlined, LoadingOutlined } from "@ant-design/icons";
-import { RcFile } from "antd/lib/upload";
-import React, { useState } from "react";
-import './index.less'
-import { getBase64 } from "@/utils";
-import { getOssInfo } from "@/services/ant-design-pro/api";
-import { request } from "@umijs/max";
-
-interface Props {
-    value?: string,  // 压缩包地址
-    onChange?: (data: RcFile | string) => void,
-    tooltip?: JSX.Element
-}
-const UploadImg: React.FC<Props> = (props) => {
-
-    /** 变量START */
-    const { value, tooltip,  onChange } = props
-    const [zipFile, setZipFile] = useState<string>(value || '');
-    const [loading, setLoading] = useState<boolean>(false)
-    const [visible, setVisible] = useState<boolean>(false)
-    /** 变量END */
-
-    const beforeUpload = async (file: RcFile) => {
-        const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png';
-        if (!isJpgOrPng) {
-            message.error('您只能上传JPG/PNG文件!');
-        }
-        const isLt2M = file.size / 1024 / 1024 < 2;
-        if (!isLt2M) {
-            message.error('图像必须小于2MB!');
-        }
-
-        return isJpgOrPng && isLt2M;
-    };
-
-    const uploadButton = (
-        <div>
-            {loading ? <LoadingOutlined /> : <PlusOutlined />}
-            {/* <div style={{ marginTop: 8 }}>Upload</div> */}
-        </div>
-    );
-
-    return <div className="myUpload">
-        <Space align="start">
-            <Upload
-                name="avatar"
-                listType="picture-card"
-                accept='.zip, .rar, application/zip, application/x-rar-compressed'
-                className="avatar-uploader"
-                beforeUpload={beforeUpload}
-                maxCount={1}
-                showUploadList={false}
-                customRequest={(options: any) => {
-                    // setLoading(true)
-                    // getOssInfo({ type: 'image/jpeg', fileType: 'image' }).then(async res => {
-                    //     try {
-                    //         let formData = new FormData();
-                    //         Object.keys(res.data).forEach((key: string) => {
-                    //             if (key !== 'url') {
-                    //                 formData.append(key, res.data[key])
-                    //             }
-                    //         })
-                    //         formData.append('file', options.file)
-                    //         let urlData = await request(res?.data?.ossUrl, { method: 'POST', data: formData })
-                    //         setLoading(false)
-                    //         setImageFile(urlData?.data?.url);
-                    //         if (urlData?.data?.url) {
-                    //             onChange && onChange(urlData?.data?.url)
-                    //         }
-                    //     } catch (error) {
-                    //         setLoading(false)
-                    //     }
-                    // })
-                }}
-            >
-                {zipFile ? <Image src={zipFile} preview={false} style={{ height: '85px' }} /> : uploadButton}
-            </Upload>
-            <div>{tooltip && tooltip}</div>
-            {/* {zipFile && <div className="look">
-                <a onClick={() => setVisible(true)}>预览</a>
-                <Image
-                    style={{ display: 'none' }}
-                    src={imageFile}
-                    preview={{
-                        visible,
-                        src: imageFile,
-                        onVisibleChange: value => {
-                            setVisible(value);
-                        },
-                    }}
-                />
-            </div>} */}
-
-        </Space>
-    </div>
-}
-
-
-export default React.memo(UploadImg)
+import { message, Space, Upload, Button, Flex, Typography } from "antd"
+import { CloudUploadOutlined, DeleteOutlined, PaperClipOutlined } from "@ant-design/icons";
+import { RcFile } from "antd/lib/upload";
+import React, { useState } from "react";
+import './index.less'
+import { getOssInfo } from "@/services/ant-design-pro/api";
+import { request } from "@umijs/max";
+
+interface Props {
+    value?: string,  // 压缩包地址
+    onChange?: (data: RcFile | string) => void,
+    tooltip?: JSX.Element
+}
+const UploadZip: React.FC<Props> = (props) => {
+
+    /** 变量START */
+    const { value, tooltip, onChange } = props
+    const [zipFile, setZipFile] = useState<string>(value || '');
+    const [loading, setLoading] = useState<boolean>(false)
+    /** 变量END */
+
+    const beforeUpload = async (file: RcFile) => {
+        console.log('file--->', file)
+        const isZipOrRar =
+            file.type === 'application/zip' ||
+            file.type === 'application/x-zip-compressed' ||
+            file.type === 'application/x-rar-compressed' ||
+            file.type === 'application/x-gzip' ||
+            file.type === 'application/x-7z-compressed' ||
+            file.name.includes('.7z');
+        if (!isZipOrRar) {
+            message.error('您只能上传压缩包文件!');
+        }
+        const isLt20M = file.size / 1024 / 1024 < 20;
+        if (!isLt20M) {
+            message.error('压缩包必须小于20MB!');
+        }
+
+        return isZipOrRar && isLt20M;
+    };
+
+    return <div className="myUpload">
+        <Space align="start">
+            {zipFile && <Flex style={{}} gap={10}>
+                <PaperClipOutlined />
+                <Typography.Text ellipsis style={{ maxWidth: 300 }}>{zipFile}</Typography.Text>
+                <DeleteOutlined style={{ color: 'red' }} onClick={() => {
+                    setZipFile('')
+                    onChange?.('')
+                }}/>
+            </Flex >}
+            <Upload
+                name="avatar"
+                accept='.zip, .rar, .7z, .gz, application/zip, application/x-zip-compressed, application/x-rar-compressed, application/x-7z-compressed, application/x-gzip'
+                className="avatar-uploader"
+                beforeUpload={beforeUpload}
+                maxCount={1}
+                showUploadList={false}
+                customRequest={(options: any) => {
+                    setLoading(true)
+                    getOssInfo({ type: 'application/zip', fileType: 'zip' }).then(async res => {
+                        try {
+                            let formData = new FormData();
+                            Object.keys(res.data).forEach((key: string) => {
+                                if (key !== 'url') {
+                                    formData.append(key, res.data[key])
+                                }
+                            })
+                            formData.append('file', options.file)
+                            let urlData = await request(res?.data?.ossUrl, { method: 'POST', data: formData })
+                            setLoading(false)
+                            setZipFile(urlData?.data?.url);
+                            if (urlData?.data?.url) {
+                                onChange && onChange(urlData?.data?.url)
+                            }
+                        } catch (error) {
+                            setLoading(false)
+                        }
+                    })
+                }}
+            >
+                {zipFile.length > 0 ? null : <Button loading={loading} icon={<CloudUploadOutlined />} type="primary">上传文件</Button>}
+            </Upload>
+            <div>{tooltip && tooltip}</div>
+        </Space>
+    </div>
+}
+
+
+export default React.memo(UploadZip)

+ 42 - 0
src/components/VideoNews/index.less

@@ -0,0 +1,42 @@
+.imgNews {
+    display: inline-block;
+    overflow: hidden;
+    position: relative;
+
+    &>div {
+        .img {
+            min-width: 40px;
+            min-height: 40px;
+            max-width: 450px;
+            max-height: 130px;
+            cursor: pointer;
+        }
+    }
+
+    .mask {
+        position: absolute;
+        width: 100%;
+        height: 100%;
+        top: 0;
+        left: 0;
+        background-color: rgba(0, 0, 0, .3);
+        display: flex;
+        justify-content: center;
+        align-items: center;
+
+        &>img {
+            width: 20px;
+            height: 20px;
+            display: inline-block;
+            cursor: pointer;
+        }
+    }
+}
+
+.video {
+    width: 100%;
+    max-width: 320px;
+    max-height: 600px;
+    display: block;
+    margin: auto;
+}

+ 39 - 0
src/components/VideoNews/index.tsx

@@ -0,0 +1,39 @@
+import React, { useState } from "react"
+import style from './index.less'
+import { Image, ImageProps, Modal, theme } from 'antd'
+import play from "../../../public/image/play.png"
+import { CloseOutlined } from '@ant-design/icons';
+import { getVideoImgUrl } from "@/utils"
+
+
+const VideoNews: React.FC<ImageProps> = ({ preview = false, src, ...data }) => {
+
+    /*****************************/
+    const { useToken } = theme;
+    const { token } = useToken()
+    const [toPlay, setToPlay] = useState<boolean>(false)
+    /*****************************/
+
+    return <div style={{ display: 'inline-block' }}>
+        <div className={style.imgNews} style={{ borderRadius: token.borderRadius }}>
+            <Image src={src ? getVideoImgUrl(src) : 'error'} preview={false} {...data} className={style.img} />
+            <div className={style.mask}>
+                <img src={play} onClick={(e) => { e.stopPropagation(); setToPlay(true) }} />
+            </div>
+        </div>
+        {toPlay && <div onClick={(e) => { e.stopPropagation(); }}>
+            <Modal
+                open={toPlay}
+                styles={{ body: { backgroundColor: 'rgba(0,0,0,0.8)', overflow: 'hidden', borderRadius: token.borderRadius, padding: '20px 24px' } }}
+                footer={null}
+                closeIcon={<CloseOutlined style={{ color: token.colorPrimary }} />}
+                onCancel={(e) => { e.stopPropagation(); setToPlay(false) }}
+                className="playVideo"
+            >
+                <video className={style.video} style={{ borderRadius: token.borderRadius }} src={src} autoPlay controls>您的浏览器不支持 video 标签。</video>
+            </Modal>
+        </div>}
+    </div>
+}
+
+export default React.memo(VideoNews)

+ 0 - 62
src/const.ts

@@ -1,62 +0,0 @@
-
-/**
- * 任务分类枚举
- */
-export enum TaskTypeEnum {
-    TASK_TYPE_GAME = '游戏',
-    TASK_TYPE_NOVEL = '小说',
-    TASK_TYPE_SHORT_PLAY = '短剧'
-}
-
-export enum TaskStatusEnum {
-    ALL = '全部任务',
-    RELEASE = '发布中',
-    END = '已截止',
-    LAPSE = '失效'
-}
-
-/**
- * 紧急度
- */
-export enum UrgencyEnum {
-    URGENCY_ORDINARY = '普通',
-    URGENCY_TOP = '置顶'
-}
-
-
-/**
- * 任务状态
- */
-export enum StatusEnum {
-    STATUS_NORMAL = '正常',
-    STATUS_EXPIRE = '失效'
-}
-
-
-/**
- * 素材类型枚举
- */
-export enum MaterialTypeEnum {
-    // MATERIAL_TYPE_SINGLE_PICTURE = '单图',
-    // MATERIAL_TYPE_GROUP_PICTURE = '组图',
-    // MATERIAL_TYPE_VOICE = '音频',
-    MATERIAL_TYPE_VIDEO = '视频',
-}
-
-
-/**
- * 素材来源要求枚举
- */
-export enum MaterialSourceEnum {
-    MATERIAL_SOURCE_ORIGINAL = '原创'
-}
-
-/**
- * 素材比例枚举
- */
-export enum RatioEnum {
-    ONE_ONE = '1:1',
-    FOUR_THREE = '4:3',
-    SIXTEEN_NINE = '16:9',
-    NINE_SIXTEEN = '9:16',
-}

+ 81 - 0
src/const.tsx

@@ -0,0 +1,81 @@
+import { Badge, Tag } from "antd"
+import React from "react"
+
+/**
+ * 任务分类枚举
+ */
+export enum TaskTypeEnum {
+    TASK_TYPE_GAME = '游戏',
+    TASK_TYPE_NOVEL = '小说',
+    TASK_TYPE_SHORT_PLAY = '短剧'
+}
+
+export const TaskTypeEle: any = {
+    TASK_TYPE_GAME: <Tag color="success">游戏</Tag>,
+    TASK_TYPE_NOVEL: <Tag color="processing">小说</Tag>,
+    TASK_TYPE_SHORT_PLAY: <Tag color="error">短剧</Tag>
+}
+
+/**
+ * 紧急度
+ */
+export const UrgencyEnum = {
+    '0': '普通',
+    '1': '置顶'
+}
+
+
+/**
+ * 任务状态
+ */
+export enum StatusEnum {
+    STATUS_NORMAL = '正常',
+    STATUS_EXPIRE = '失效'
+}
+export const TaskStatusEle: any = {
+    STATUS_NORMAL: <Badge status="success" text="正常" />,
+    STATUS_EXPIRE: <Badge status="error" text="失效" />
+}
+
+/**
+ * 素材类型枚举
+ */
+export enum MaterialTypeEnum {
+    // MATERIAL_TYPE_SINGLE_PICTURE = '单图',
+    // MATERIAL_TYPE_GROUP_PICTURE = '组图',
+    // MATERIAL_TYPE_VOICE = '音频',
+    MATERIAL_TYPE_VIDEO = '视频',
+}
+
+export const MaterialTypeEle: any = {
+    MATERIAL_TYPE_SINGLE_PICTURE: <Tag color="#55acee">单图</Tag>,
+    MATERIAL_TYPE_GROUP_PICTURE: <Tag color="#3b5999">组图</Tag>,
+    MATERIAL_TYPE_VOICE: <Tag color="#cd201f">音频</Tag>,
+    MATERIAL_TYPE_VIDEO: <Tag color="#55acee">视频</Tag>,
+}
+
+
+/**
+ * 素材来源要求枚举
+ */
+export enum MaterialSourceEnum {
+    MATERIAL_SOURCE_ORIGINAL = '原创'
+}
+
+/**
+ * 素材比例枚举
+ */
+export enum RatioEnum {
+    ONE_ONE = '1:1',
+    FOUR_THREE = '4:3',
+    SIXTEEN_NINE = '16:9',
+    NINE_SIXTEEN = '9:16',
+}
+
+/**
+ * 结算类型
+ */
+export enum CheckoutTypeEnum {
+    CHECKOUT_TYPE_SCALE = '消耗比例',
+    CHECKOUT_TYPE_AMOUNT = '任务'
+}

+ 77 - 55
src/global.less

@@ -1,56 +1,78 @@
-html,
-body,
-#root {
-  height: 100%;
-  margin: 0;
-  padding: 0;
-  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial,
-    'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
-    'Noto Color Emoji';
-}
-
-.colorWeak {
-  filter: invert(80%);
-}
-
-.ant-layout {
-  min-height: 100vh;
-}
-.ant-pro-sider.ant-layout-sider.ant-pro-sider-fixed {
-  left: unset;
-}
-
-canvas {
-  display: block;
-}
-
-body {
-  text-rendering: optimizeLegibility;
-  -webkit-font-smoothing: antialiased;
-  -moz-osx-font-smoothing: grayscale;
-}
-
-ul,
-ol {
-  list-style: none;
-}
-
-@media (max-width: 768px) {
-  .ant-table {
-    width: 100%;
-    overflow-x: auto;
-    &-thead > tr,
-    &-tbody > tr {
-      > th,
-      > td {
-        white-space: pre;
-        > span {
-          display: block;
-        }
-      }
-    }
-  }
-}
-.drawSelect .ant-select-selector{
-  padding: 0 !important;
+html,
+body,
+#root {
+  height: 100%;
+  margin: 0;
+  padding: 0;
+  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial,
+    'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
+    'Noto Color Emoji';
+}
+
+.colorWeak {
+  filter: invert(80%);
+}
+
+.ant-layout {
+  min-height: 100vh;
+}
+.ant-pro-sider.ant-layout-sider.ant-pro-sider-fixed {
+  left: unset;
+}
+
+canvas {
+  display: block;
+}
+
+body {
+  text-rendering: optimizeLegibility;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}
+
+ul,
+ol {
+  list-style: none;
+}
+
+@media (max-width: 768px) {
+  .ant-table {
+    width: 100%;
+    overflow-x: auto;
+    &-thead > tr,
+    &-tbody > tr {
+      > th,
+      > td {
+        white-space: pre;
+        > span {
+          display: block;
+        }
+      }
+    }
+  }
+}
+.drawSelect .ant-select-selector{
+  padding: 0 !important;
+}
+
+.playVideo .ant-modal-content {
+  padding: 0;
+}
+
+
+::-webkit-scrollbar {
+  width: 2px;
+  height: 8px;
+}
+
+::-webkit-scrollbar-thumb {
+  border-radius: 4px;
+  -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0);
+  background-color: #ddd;
+  background: rgba(0, 0, 0, 0);
+}
+
+:hover::-webkit-scrollbar-thumb {
+  background: rgba(82, 82, 82, 0.3);
+  -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.2);
 }
 }

+ 57 - 54
src/locales/zh-CN/menu.ts

@@ -1,54 +1,57 @@
-export default {
-  'menu.welcome': '欢迎',
-  'menu.more-blocks': '更多区块',
-  'menu.home': '首页',
-  'menu.task': '任务中心',
-  'menu.my-task': '我的任务',
-  'menu.admin': '管理页',
-  'menu.admin.sub-page': '二级管理页',
-  'menu.login': '登录',
-  'menu.register': '注册',
-  'menu.register-result': '注册结果',
-  'menu.dashboard': 'Dashboard',
-  'menu.dashboard.analysis': '分析页',
-  'menu.dashboard.monitor': '监控页',
-  'menu.dashboard.workplace': '工作台',
-  'menu.exception.403': '403',
-  'menu.exception.404': '404',
-  'menu.exception.500': '500',
-  'menu.form': '表单页',
-  'menu.form.basic-form': '基础表单',
-  'menu.form.step-form': '分步表单',
-  'menu.form.step-form.info': '分步表单(填写转账信息)',
-  'menu.form.step-form.confirm': '分步表单(确认转账信息)',
-  'menu.form.step-form.result': '分步表单(完成)',
-  'menu.form.advanced-form': '高级表单',
-  'menu.list': '列表页',
-  'menu.list.table-list': '查询表格',
-  'menu.list.basic-list': '标准列表',
-  'menu.list.card-list': '卡片列表',
-  'menu.list.search-list': '搜索列表',
-  'menu.list.search-list.articles': '搜索列表(文章)',
-  'menu.list.search-list.projects': '搜索列表(项目)',
-  'menu.list.search-list.applications': '搜索列表(应用)',
-  'menu.profile': '详情页',
-  'menu.profile.basic': '基础详情页',
-  'menu.profile.advanced': '高级详情页',
-  'menu.result': '结果页',
-  'menu.result.success': '成功页',
-  'menu.result.fail': '失败页',
-  'menu.exception': '异常页',
-  'menu.exception.not-permission': '403',
-  'menu.exception.not-find': '404',
-  'menu.exception.server-error': '500',
-  'menu.exception.trigger': '触发错误',
-  'menu.account': '个人页',
-  'menu.account.center': '个人中心',
-  'menu.account.settings': '个人设置',
-  'menu.account.trigger': '触发报错',
-  'menu.account.logout': '退出登录',
-  'menu.editor': '图形编辑器',
-  'menu.editor.flow': '流程编辑器',
-  'menu.editor.mind': '脑图编辑器',
-  'menu.editor.koni': '拓扑编辑器',
-};
+export default {
+  'menu.welcome': '欢迎',
+  'menu.more-blocks': '更多区块',
+  'menu.home': '首页',
+  'menu.task': '任务中心',
+  'menu.my-task': '我的任务',
+  'menu.opus': '作品库',
+  'menu.download': '下载任务',
+  'menu.income': '收益',
+  'menu.admin': '管理页',
+  'menu.admin.sub-page': '二级管理页',
+  'menu.login': '登录',
+  'menu.register': '注册',
+  'menu.register-result': '注册结果',
+  'menu.dashboard': 'Dashboard',
+  'menu.dashboard.analysis': '分析页',
+  'menu.dashboard.monitor': '监控页',
+  'menu.dashboard.workplace': '工作台',
+  'menu.exception.403': '403',
+  'menu.exception.404': '404',
+  'menu.exception.500': '500',
+  'menu.form': '表单页',
+  'menu.form.basic-form': '基础表单',
+  'menu.form.step-form': '分步表单',
+  'menu.form.step-form.info': '分步表单(填写转账信息)',
+  'menu.form.step-form.confirm': '分步表单(确认转账信息)',
+  'menu.form.step-form.result': '分步表单(完成)',
+  'menu.form.advanced-form': '高级表单',
+  'menu.list': '列表页',
+  'menu.list.table-list': '查询表格',
+  'menu.list.basic-list': '标准列表',
+  'menu.list.card-list': '卡片列表',
+  'menu.list.search-list': '搜索列表',
+  'menu.list.search-list.articles': '搜索列表(文章)',
+  'menu.list.search-list.projects': '搜索列表(项目)',
+  'menu.list.search-list.applications': '搜索列表(应用)',
+  'menu.profile': '详情页',
+  'menu.profile.basic': '基础详情页',
+  'menu.profile.advanced': '高级详情页',
+  'menu.result': '结果页',
+  'menu.result.success': '成功页',
+  'menu.result.fail': '失败页',
+  'menu.exception': '异常页',
+  'menu.exception.not-permission': '403',
+  'menu.exception.not-find': '404',
+  'menu.exception.server-error': '500',
+  'menu.exception.trigger': '触发错误',
+  'menu.account': '个人页',
+  'menu.account.center': '个人中心',
+  'menu.account.settings': '个人设置',
+  'menu.account.trigger': '触发报错',
+  'menu.account.logout': '退出登录',
+  'menu.editor': '图形编辑器',
+  'menu.editor.flow': '流程编辑器',
+  'menu.editor.mind': '脑图编辑器',
+  'menu.editor.koni': '拓扑编辑器',
+};

+ 84 - 0
src/pages/Account/Center/Center.less

@@ -0,0 +1,84 @@
+.avatarHolder {
+    margin-bottom: 24px;
+    text-align: center;
+
+    &>img {
+        width: 104px;
+        height: 104px;
+        margin-bottom: 20px;
+    }
+
+    .name {
+        margin-bottom: 4px;
+        color: @heading-color;
+        font-weight: 500;
+        font-size: 20px;
+        line-height: 28px;
+    }
+}
+
+.detail {
+    p {
+        position: relative;
+        margin-bottom: 8px;
+        padding-left: 26px;
+
+        &:last-child {
+            margin-bottom: 0;
+        }
+    }
+
+    i {
+        position: absolute;
+        top: 4px;
+        left: 0;
+        width: 14px;
+        height: 14px;
+    }
+}
+
+.tagsTitle,
+.teamTitle {
+    margin-bottom: 12px;
+    color: @heading-color;
+    font-weight: 500;
+}
+
+.tags {
+    :global {
+        .ant-tag {
+            margin-bottom: 8px;
+        }
+    }
+}
+
+.team {
+    :global {
+        .ant-avatar {
+            margin-right: 12px;
+        }
+    }
+
+    a {
+        display: block;
+        margin-bottom: 24px;
+        overflow: hidden;
+        color: @text-color;
+        white-space: nowrap;
+        text-overflow: ellipsis;
+        word-break: break-all;
+        transition: color 0.3s;
+
+        &:hover {
+            color: @primary-color;
+        }
+    }
+}
+
+.tabsCard {
+    :global {
+        .ant-card-head {
+            padding: 0 16px;
+        }
+    }
+}

+ 102 - 0
src/pages/Account/Center/ModalCenter.tsx

@@ -0,0 +1,102 @@
+import { getCenterUserInfoApi } from "@/services/task-api/center"
+import { useModel } from "@umijs/max"
+import { useRequest } from "ahooks"
+import { Avatar, Col, Divider, Modal, Row, Statistic } from "antd"
+import React from "react"
+import styles from './Center.less';
+import { CurrentUser } from "./data"
+import { FieldTimeOutlined, PhoneOutlined } from "@ant-design/icons"
+
+interface Props {
+    visible?: boolean
+    onClose?: () => void
+}
+/**
+ * 弹窗个人中心
+ * @returns 
+ */
+const ModalCenter: React.FC<Props> = ({ visible, onClose }) => {
+
+    /*******************************/
+    const { initialState } = useModel('@@initialState');
+    const { data } = useRequest(getCenterUserInfoApi)
+    /*******************************/
+
+    //  渲染用户信息
+    const renderUserInfo = ({ mobile, createTime }: Partial<CurrentUser>) => {
+        return (
+            <div className={styles.detail}>
+                <p>
+                    <PhoneOutlined
+                        style={{
+                            marginRight: 8,
+                        }}
+                    />
+                    {mobile}
+                </p>
+                <p>
+                    <FieldTimeOutlined
+                        style={{
+                            marginRight: 8,
+                        }}
+                    />
+                    {createTime}
+                </p>
+            </div>
+        );
+    };
+
+    return <Modal
+        title="个人中心"
+        open={visible}
+        onCancel={onClose}
+        footer={null}
+        style={{ maxWidth: 400 }}
+    >
+        <div>
+            <div className={styles.avatarHolder}>
+                <Avatar
+                    style={{ backgroundColor: '#7265e6', verticalAlign: 'middle', marginBottom: 20 }}
+                    size={104}
+                    src={initialState?.currentUser?.avatar}
+                >{data?.data?.mobile}</Avatar>
+                <div className={styles.name}>{data?.data?.userName}</div>
+                <div>海纳百川,有容乃大</div>
+            </div>
+            {renderUserInfo(data?.data || {})}
+            <Divider dashed />
+            <Row gutter={[16, 16]}>
+                <Col span={12}>
+                    <Statistic
+                        title="累计上传作品"
+                        value={data?.data?.materilaCount ? data?.data?.materilaCount : data?.data?.materilaCount === 0 ? 0 : '--'}
+                        valueStyle={{ color: '#3f8600' }}
+                    />
+                </Col>
+                <Col span={12}>
+                    <Statistic
+                        title="全部收益"
+                        value={data?.data?.sumIncome ? data?.data?.sumIncome : data?.data?.sumIncome === 0 ? 0 : '--'}
+                        valueStyle={{ color: '#3f8600' }}
+                    />
+                </Col>
+                <Col span={12}>
+                    <Statistic
+                        title="已结算"
+                        value={data?.data?.settled ? data?.data?.settled : data?.data?.settled === 0 ? 0 : '--'}
+                        valueStyle={{ color: '#3f8600' }}
+                    />
+                </Col>
+                <Col span={12}>
+                    <Statistic
+                        title="当前收益"
+                        value={data?.data?.nowIncome ? data?.data?.nowIncome : data?.data?.nowIncome === 0 ? 0 : '--'}
+                        valueStyle={{ color: '#3f8600' }}
+                    />
+                </Col>
+            </Row>
+        </div>
+    </Modal>
+}
+
+export default React.memo(ModalCenter)

+ 11 - 0
src/pages/Account/Center/data.d.ts

@@ -0,0 +1,11 @@
+
+
+export type CurrentUser = {
+    avatar: string;
+    mobile: string;
+    role: string;
+    userid: string;
+    token: string;
+    userName: string;
+    createTime: string;
+};

+ 99 - 0
src/pages/Account/Center/index.tsx

@@ -0,0 +1,99 @@
+import { GridContent } from "@ant-design/pro-components";
+import { useModel } from "@umijs/max";
+import { Avatar, Card, Col, Divider, Row, Statistic, theme } from "antd"
+import styles from './Center.less';
+import { CurrentUser } from "./data";
+import { FieldTimeOutlined, PhoneOutlined } from "@ant-design/icons";
+import { useRequest } from "ahooks";
+import { getCenterUserInfoApi } from "@/services/task-api/center";
+
+const Center: React.FC = () => {
+
+    /************************************/
+    const { useToken } = theme;
+    const { token } = useToken();
+    const { initialState } = useModel('@@initialState');
+    const { data } = useRequest(getCenterUserInfoApi)
+    /************************************/
+
+    //  渲染用户信息
+    const renderUserInfo = ({ mobile, createTime }: Partial<CurrentUser>) => {
+        return (
+            <div className={styles.detail}>
+                <p>
+                    <PhoneOutlined
+                        style={{
+                            marginRight: 8,
+                        }}
+                    />
+                    {mobile}
+                </p>
+                <p>
+                    <FieldTimeOutlined
+                        style={{
+                            marginRight: 8,
+                        }}
+                    />
+                    {createTime}
+                </p>
+            </div>
+        );
+    };
+
+    return <GridContent>
+        <Row gutter={24}>
+            <Col lg={7} md={24}>
+                <Card bordered={false} style={{ marginBottom: 24 }} >
+                    <div>
+                        <div className={styles.avatarHolder}>
+                            <Avatar
+                                style={{ backgroundColor: '#7265e6', verticalAlign: 'middle', marginBottom: 20 }}
+                                size={104}
+                                src={initialState?.currentUser?.avatar}
+                            >{data?.data?.mobile}</Avatar>
+                            <div className={styles.name}>{data?.data?.userName}</div>
+                            <div>海纳百川,有容乃大</div>
+                        </div>
+                        {renderUserInfo(data?.data || {})}
+                        <Divider dashed />
+                        <Row gutter={[16, 16]}>
+                            <Col span={12}>
+                                <Statistic
+                                    title="累计上传作品"
+                                    value={data?.data?.materilaCount ? data?.data?.materilaCount : data?.data?.materilaCount === 0 ? 0 : '--'}
+                                    valueStyle={{ color: '#3f8600' }}
+                                />
+                            </Col>
+                            <Col span={12}>
+                                <Statistic
+                                    title="全部收益"
+                                    value={data?.data?.sumIncome ? data?.data?.sumIncome : data?.data?.sumIncome === 0 ? 0 : '--'}
+                                    valueStyle={{ color: '#3f8600' }}
+                                />
+                            </Col>
+                            <Col span={12}>
+                                <Statistic
+                                    title="已结算"
+                                    value={data?.data?.settled ? data?.data?.settled : data?.data?.settled === 0 ? 0 : '--'}
+                                    valueStyle={{ color: '#3f8600' }}
+                                />
+                            </Col>
+                            <Col span={12}>
+                                <Statistic
+                                    title="当前收益"
+                                    value={data?.data?.nowIncome ? data?.data?.nowIncome : data?.data?.nowIncome === 0 ? 0 : '--'}
+                                    valueStyle={{ color: '#3f8600' }}
+                                />
+                            </Col>
+                        </Row>
+                    </div>
+                </Card>
+            </Col>
+            <Col lg={17} md={24}>
+                
+            </Col>
+        </Row>
+    </GridContent>
+}
+
+export default Center

+ 81 - 0
src/pages/Account/Center/tableConfig.tsx

@@ -0,0 +1,81 @@
+import { ColumnsType } from "antd/es/table";
+import React from "react";
+
+
+const Columns = (): ColumnsType<any> => {
+    let arr: ColumnsType<any> = [
+        {
+            title: '已结算账单',
+            dataIndex: 'a',
+            key: 'a',
+            align: 'center',
+            width: 120,
+            render: (a, b) => {
+                return <div>
+                    222222222
+                </div>
+            }
+        },
+        {
+            title: '制作者',
+            dataIndex: 'b',
+            key: 'b',
+            align: 'center',
+            width: 90
+        },
+        {
+            title: '上传时间',
+            dataIndex: 'c',
+            key: 'c',
+            align: 'center',
+            width: 120
+        },
+        {
+            title: '上传任务名称',
+            dataIndex: 'd',
+            key: 'd',
+            align: 'center',
+            width: 120
+        },
+        {
+            title: '素材规格',
+            dataIndex: 'e',
+            key: 'e',
+            align: 'center',
+            width: 120
+        },
+        {
+            title: '素材审核',
+            dataIndex: 'f',
+            key: 'f',
+            align: 'center',
+            width: 80
+        },
+        {
+            title: '素材奖励结算',
+            dataIndex: 'g',
+            key: 'g',
+            align: 'center',
+            width: 100
+        },
+        {
+            title: '素材总消耗',
+            dataIndex: 'm',
+            key: 'm',
+            align: 'center',
+            width: 100
+        },
+        {
+            title: '收益',
+            dataIndex: 'l',
+            key: 'l',
+            align: 'center',
+            fixed: 'right',
+            width: 100
+        },
+    ]
+
+    return arr
+}
+
+export default Columns

+ 79 - 0
src/pages/Download/index.tsx

@@ -0,0 +1,79 @@
+import { PageContainer } from "@ant-design/pro-components"
+import { Card, Table, Tag } from "antd"
+import Columns from "./tableConfig"
+import { useRequest } from "ahooks"
+import { downloadEscalationApi, getDownloadListApi } from "@/services/task-api/download"
+import { useEffect, useState } from "react"
+import { request } from "@umijs/max"
+
+
+/**
+ * 下载任务
+ * @returns 
+ */
+const Download: React.FC = () => {
+
+    /********************************/
+    const [queryForm, setQueryForm] = useState<TASKAPI.DownloadList>({ pageNum: 1, pageSize: 20 })
+    const getDownloadList = useRequest(getDownloadListApi, { manual: true })
+    const downloadEscalation = useRequest(downloadEscalationApi, { manual: true })
+    /********************************/
+
+    useEffect(() => {
+        getDownloadList.runAsync(queryForm)
+    }, [queryForm])
+
+    const download = (url: string, userMaterialId: number) => {
+        downloadEscalation.runAsync({ userMaterialId }).then(res => {
+            const fileName = 'downloaded.mp4'; // 可以自定义下载的文件名
+            let link = url
+            let x = new XMLHttpRequest()
+            x.open('GET', link, true)
+            x.responseType = 'blob'
+            x.onload = (e) => {
+                let url = window.URL.createObjectURL(x.response)
+                let a = document.createElement('a')
+                a.href = url
+                a.download = fileName
+                a.click()
+            }
+            x.send()
+            getDownloadList.refresh()
+        })
+    }
+
+    return <PageContainer
+        extra={<a onClick={() => getDownloadList.refresh()}>刷新</a>}
+    >
+        <Card
+            style={{
+                borderRadius: 8,
+            }}
+        >
+            <Table
+                columns={Columns(download)}
+                dataSource={getDownloadList?.data?.data?.records}
+                scroll={{ x: 1000 }}
+                rowKey={(s) => {
+                    return s.id
+                }}
+                size='small'
+                pagination={{
+                    total: 0,
+                    showTotal: (total) => <Tag color="cyan">总共{total}数据</Tag>,
+                    showSizeChanger: true,
+                    showLessItems: true,
+                    defaultCurrent: 1,
+                    defaultPageSize: 20,//默认初始的每页条数
+                    current: getDownloadList?.data?.data?.current || 1,
+                    pageSize: getDownloadList?.data?.data?.size || 20,
+                    onChange: (page, pageSize) => {
+                        setQueryForm({ ...queryForm, pageNum: page, pageSize })
+                    }
+                }}
+            />
+        </Card>
+    </PageContainer>
+}
+
+export default Download

+ 77 - 0
src/pages/Download/tableConfig.tsx

@@ -0,0 +1,77 @@
+import LookVideo from "@/components/LookVideo";
+import { Button, Flex, Typography } from "antd";
+import { ColumnsType } from "antd/es/table";
+import React from "react";
+
+
+const Columns = (download: (url: string, userMaterialId: number) => void): ColumnsType<any> => {
+    let arr: ColumnsType<any> = [
+        {
+            title: '素材预览',
+            dataIndex: 'link',
+            key: 'link',
+            align: 'center',
+            width: 120,
+            fixed: 'left',
+            render: (a, b) => {
+                return <LookVideo urlList={[a]} />
+            }
+        },
+        {
+            title: '上传时间',
+            dataIndex: 'materialCreateTime',
+            key: 'materialCreateTime',
+            align: 'center',
+            width: 120
+        },
+        {
+            title: '上传任务名称',
+            dataIndex: 'taskName',
+            key: 'taskName',
+            align: 'center',
+            width: 150,
+            render(value) {
+                return <Typography.Paragraph style={{ marginBottom: 0 }} ellipsis={{ rows: 3 }}>{value}</Typography.Paragraph>
+            },
+        },
+        {
+            title: '素材规格',
+            dataIndex: 'materialClaimJson',
+            key: 'materialClaimJson',
+            width: 150,
+            render(value) {
+                if (value) {
+                    const { size, extent } = JSON.parse(value)
+                    return <Flex vertical={true}>
+                        <Typography.Text>素材尺寸:{size?.[0]} * {size?.[1]}</Typography.Text>
+                        <Typography.Text>素材大小:{extent?.[0]}MB ~ {extent?.[1]}MB</Typography.Text>
+                    </Flex>
+                } else {
+                    return '--'
+                }
+            },
+        },
+        {
+            title: '下载时间',
+            dataIndex: 'createTime',
+            key: 'createTime',
+            align: 'center',
+            width: 120
+        },
+        {
+            title: '操作',
+            dataIndex: 'cz',
+            key: 'cz',
+            align: 'center',
+            width: 75,
+            fixed: 'right',
+            render(value, record) {
+                return <Button type="link" onClick={() => download(record.link, record.materialId)}>再次下载</Button>
+            },
+        },
+    ]
+
+    return arr
+}
+
+export default Columns

+ 109 - 0
src/pages/Income/index.tsx

@@ -0,0 +1,109 @@
+import { AlertOutlined, SearchOutlined } from "@ant-design/icons";
+import { PageContainer } from "@ant-design/pro-components"
+import { Affix, Alert, Button, Card, Col, DatePicker, Form, Input, Row, Space, Table, Tag, theme } from "antd"
+import moment from "moment";
+import { useEffect, useState } from "react";
+import Columns from "./tableConfig";
+import { useRequest } from "ahooks";
+import { getIncomeListApi } from "@/services/task-api/income";
+
+/**
+ * 收益
+ * @returns 
+ */
+const Income: React.FC = () => {
+
+    /*****************************/
+    const { useToken } = theme;
+    const { token } = useToken();
+    const [form] = Form.useForm()
+    const [queryForm, setQueryForm] = useState<TASKAPI.IncomeList>({ pageNum: 1, pageSize: 20 })
+    const getIncomeList = useRequest(getIncomeListApi, { manual: true })
+    /*****************************/
+
+    useEffect(() => {
+        getIncomeList.runAsync(queryForm)
+    }, [queryForm])
+
+    const onFinish = (data: any) => {
+        const { time, ...pre } = data
+        let newQueryForm = JSON.parse(JSON.stringify(queryForm))
+        newQueryForm.pageNum = 1
+        if (time) {
+            newQueryForm.startTime = moment(time[0]).format('YYYY-MM-DD')
+            newQueryForm.endTime = moment(time[1]).format('YYYY-MM-DD')
+        } else {
+            delete newQueryForm?.startTime
+            delete newQueryForm?.endTime
+        }
+        setQueryForm({ ...newQueryForm, ...pre })
+    }
+
+    return <PageContainer>
+        <Card
+            style={{ borderRadius: 8 }}
+            bodyStyle={{ padding: 0 }}
+        >
+            <Space direction="vertical" style={{ width: '100%' }} size={0}>
+                <Affix offsetTop={56}>
+                    <div style={{ backgroundColor: token.colorBgBase, padding: token.paddingContentHorizontalLG, borderRadius: 8, borderBottom: `1px solid ${token.colorBorderSecondary}` }}>
+                        <Form layout="inline" className='queryForm' name="Incomebasic" form={form} onFinish={onFinish}>
+                            <Row gutter={[0, 6]}>
+                                <Col>
+                                    <Form.Item name='taskName'>
+                                        <Input prefix={<SearchOutlined />} placeholder="搜索你发布的素材的任务名称" />
+                                    </Form.Item>
+                                </Col>
+
+                                <Col><Form.Item name='time'>
+                                    <DatePicker.RangePicker style={{ width: 250 }} placeholder={['上传开始日期', '上传开始日期']} />
+                                </Form.Item></Col>
+
+                                <Col>
+                                    <Space>
+                                        <Button type="primary" htmlType="submit">搜索</Button>
+                                        <Button onClick={() => form.resetFields()}>重置</Button>
+                                        <Alert
+                                            style={{ padding: `4px 12px` }}
+                                            type="warning"
+                                            showIcon
+                                            icon={<AlertOutlined />}
+                                            message="官方公告:收益结算请点击页面右上角头像“用户中心”查看!"
+                                        />
+                                    </Space>
+                                </Col>
+                            </Row>
+                        </Form>
+                    </div>
+                </Affix>
+
+                <div style={{ padding: token.paddingContentHorizontalLG }}>
+                    <Table
+                        columns={Columns()}
+                        dataSource={getIncomeList?.data?.data?.records}
+                        scroll={{ x: 1000 }}
+                        rowKey={(s) => {
+                            return s.id
+                        }}
+                        size='small'
+                        pagination={{
+                            total: 0,
+                            showTotal: (total) => <Tag color="cyan">总共{total}数据</Tag>,
+                            showSizeChanger: true,
+                            showLessItems: true,
+                            defaultCurrent: 1,
+                            defaultPageSize: 20,//默认初始的每页条数
+                            current: getIncomeList?.data?.data?.current || 1,
+                            pageSize: getIncomeList?.data?.data?.size || 20,
+                            onChange: (page, pageSize) => {
+                                setQueryForm({ ...queryForm, pageNum: page, pageSize })
+                            }
+                        }}
+                    />
+                </div>
+            </Space>
+        </Card>
+    </PageContainer>
+}
+
+export default Income

+ 106 - 0
src/pages/Income/tableConfig.tsx

@@ -0,0 +1,106 @@
+import LookVideo from "@/components/LookVideo";
+import { Flex, Typography } from "antd";
+import { ColumnsType } from "antd/es/table";
+import React from "react";
+
+
+const Columns = (): ColumnsType<any> => {
+    let arr: ColumnsType<any> = [
+        {
+            title: '素材预览',
+            dataIndex: 'materialLinkList',
+            key: 'materialLinkList',
+            align: 'center',
+            width: 120,
+            render: (a, b) => {
+                return <LookVideo urlList={a} />
+            }
+        },
+        {
+            title: '制作者',
+            dataIndex: 'userName',
+            key: 'userName',
+            align: 'center',
+            width: 90
+        },
+        {
+            title: '上传时间',
+            dataIndex: 'createTime',
+            key: 'createTime',
+            align: 'center',
+            width: 120
+        },
+        {
+            title: '上传任务名称',
+            dataIndex: 'taskName',
+            key: 'taskName',
+            align: 'center',
+            width: 120,
+            render(value) {
+                return <Typography.Paragraph style={{ marginBottom: 0 }} ellipsis={{ rows: 3 }}>{value}</Typography.Paragraph>
+            },
+        },
+        {
+            title: '素材规格',
+            dataIndex: 'materialClaimJson',
+            key: 'materialClaimJson',
+            width: 150,
+            render(value) {
+                if (value) {
+                    const { size, extent } = JSON.parse(value)
+                    return <Flex vertical={true}>
+                        <Typography.Text>素材尺寸:{size?.[0]} * {size?.[1]}</Typography.Text>
+                        <Typography.Text>素材大小:{extent?.[0]}MB ~ {extent?.[1]}MB</Typography.Text>
+                    </Flex>
+                } else {
+                    return '--'
+                }
+            },
+        },
+        {
+            title: '素材审核',
+            dataIndex: 'checkStatus',
+            key: 'checkStatus',
+            align: 'center',
+            width: 100,
+            render(value) {
+                return (value || value === 0) ? value : '--'
+            },
+        },
+        {
+            title: '素材奖励结算',
+            dataIndex: 'checkoutType',
+            key: 'checkoutType',
+            align: 'center',
+            width: 100,
+            render(value, record) {
+                return <span>{value === 'CHECKOUT_TYPE_SCALE' ? `总消耗 ${record.checkout * 100}%` : `${record.checkout}元(审核合格)`}</span>
+            },
+        },
+        {
+            title: '素材总消耗',
+            dataIndex: 'cost',
+            key: 'cost',
+            align: 'center',
+            width: 100,
+            render(value) {
+                return (value || value === 0) ? value : '--'
+            },
+        },
+        {
+            title: '收益',
+            dataIndex: 'income',
+            key: 'income',
+            align: 'center',
+            fixed: 'right',
+            width: 100,
+            render(value) {
+                return (value || value === 0) ? value : '--'
+            },
+        },
+    ]
+
+    return arr
+}
+
+export default Columns

+ 205 - 136
src/pages/MyTask/index.tsx

@@ -1,137 +1,206 @@
-import { SearchOutlined } from "@ant-design/icons"
-import { PageContainer } from "@ant-design/pro-components"
-import { useSize } from "ahooks";
-import { Affix, Avatar, Badge, Button, Card, Col, Flex, Form, Input, List, Row, Select, Space, Tag, Typography, theme } from "antd"
-import { useRef, useState } from "react";
-import TaskModal from "./taskModal";
-
-
-const data = Array.from({ length: 23 }).map((_, i) => ({
-    // href: 'https://ant.design',
-    title: `素材任务名称 ${i}`,
-    avatar: `https://xsgames.co/randomusers/avatar.php?g=pixel&key=${i}`,
-    description: '这是一条任务描述,XXXXXXXXXXXXXXXXXXXXXXXXXXXXX好长好长Ant Design, a design language',
-    content:
-        'We supply a series of design principles, practical patterns and high quality design resources (Sketch and Axure), to help people create their product prototypes beautifully and efficiently.',
-}));
-/**
- * 我的任务
- * @returns 
- */
-const MyTask: React.FC = () => {
-
-
-    /***********************************/
-    const { useToken } = theme;
-    const ref = useRef(null);
-    const size = useSize(ref)
-    const { token } = useToken();
-    const [form] = Form.useForm()
-    const [initialValues, setInitialState] = useState<any>({})
-    const [visible, setVisible] = useState<boolean>(false)
-    /***********************************/
-
-    const onFinish = (data: any) => {
-
-    }
-
-    return <PageContainer>
-        <Card
-            style={{
-                borderRadius: 8,
-            }}
-            bodyStyle={{
-                paddingTop: 0,
-                paddingLeft: 0,
-                paddingRight: 0
-            }}
-            ref={ref}
-        >
-            <Space direction="vertical" style={{ width: '100%' }} size={0}>
-
-                <Affix offsetTop={56}>
-                    <div style={{ backgroundColor: token.colorBgBase, padding: token.paddingContentHorizontalLG, borderRadius: 8, borderBottom: `1px solid ${token.colorBorderSecondary}` }}>
-                        <Form layout="inline" className='queryForm' name="basic" form={form} onFinish={onFinish}>
-                            <Row gutter={[0, 6]}>
-                                <Col>
-                                    <Form.Item name='taskName'>
-                                        <Input prefix={<SearchOutlined />} placeholder="任务名称" />
-                                    </Form.Item>
-                                </Col>
-
-                                <Col>
-                                    <Space>
-                                        <Button type="primary" htmlType="submit">搜索</Button>
-                                        <Button onClick={() => form.resetFields()}>重置</Button>
-                                        <Button type="primary" onClick={() => { setInitialState({}); setVisible(true) }}>发布素材任务</Button>
-                                    </Space>
-                                </Col>
-                            </Row>
-                        </Form>
-
-                    </div>
-                </Affix>
-
-                <List
-                    itemLayout="vertical"
-                    size="large"
-                    dataSource={data}
-                    pagination={{
-                        onChange: (page) => {
-                            console.log(page);
-                        },
-                        pageSize: 10,
-                    }}
-                    renderItem={(item: any) => (
-                        <List.Item
-                            key={item.title}
-                            extra={<Flex gap={20} style={{ height: '100%' }}>
-                                <Flex style={{ width: size?.width ? `${size?.width / 2 - 200}px` : 300 }} vertical={true} align="center" justify="center">
-                                    <span>素材制作要求:素材比例、素材尺寸、素材分辨率、素材大小</span>
-                                    <Space>
-                                        <Button type="link">素材示例</Button>
-                                        <Button type="link">资源包下载</Button>
-                                    </Space>
-                                </Flex>
-                                <Flex style={{ minWidth: 100, height: '100%' }} gap={10} vertical={true} align="center" justify="center">
-                                    <Badge status="success" text="任务发布中" />
-                                    <Tag color="error">结算:总消耗2%</Tag>
-                                    <Button type="primary">修改任务</Button>
-                                </Flex>
-                            </Flex>}
-                            actions={[
-                                <span>发布时间:2023/10/01</span>,
-                                <Space>
-                                    <Tag color="success">游戏</Tag>
-                                    <Tag color="processing">小说</Tag>
-                                    <Tag color="error">error</Tag>
-                                    <Tag color="warning">warning</Tag>
-                                    <Tag color="default">default</Tag>
-                                </Space>,
-                            ]}
-                        >
-                            <List.Item.Meta
-                                avatar={<Avatar src={item.avatar} style={{ backgroundColor: '#87d068' }} />}
-                                title={<Space size={20}>
-                                    <span>{item.title}</span>
-                                    <Space size={4}><span style={{ color: 'red' }}>置顶</span>🆙</Space>
-                                </Space>}
-                                description={<Space direction="vertical" style={{ width: '100%' }} size={0}>
-                                    {/* 有效时间 */}
-                                    <Typography.Text type="secondary" ellipsis>任务有效时间:2023年10月01日~2023年10月31日</Typography.Text>
-                                    {/* 任务描述 */}
-                                    <Typography.Text type="secondary" ellipsis={{ tooltip: true }}>{item.description}</Typography.Text>
-                                </Space>}
-                            />
-                            {item.content}
-                        </List.Item>
-                    )}
-                />
-            </Space>
-
-            {visible && <TaskModal visible={visible} onClose={() => setVisible(false)} onChange={() => {  }} initialValues={initialValues}/>}
-        </Card>
-    </PageContainer>
-}
-
+import { SearchOutlined } from "@ant-design/icons"
+import { PageContainer } from "@ant-design/pro-components"
+import { useRequest, useSize } from "ahooks";
+import { Affix, Avatar, Image, Button, Card, Col, Flex, Form, Input, List, Row, Space, Tag, Typography, theme, DatePicker, Select, Tooltip } from "antd"
+import { useEffect, useRef, useState } from "react";
+import TaskModal from "./taskModal";
+import { getTaskListApi } from "@/services/task-api/myTask";
+import { MaterialSourceEnum, MaterialTypeEle, MaterialTypeEnum, StatusEnum, TaskStatusEle, TaskTypeEle, TaskTypeEnum, UrgencyEnum } from "@/const";
+import Material from "@/components/Material";
+import moment from "moment";
+
+/**
+ * 我的任务
+ * @returns 
+ */
+const MyTask: React.FC = () => {
+
+
+    /***********************************/
+    const { useToken } = theme;
+    const ref = useRef(null);
+    const size = useSize(ref)
+    const { token } = useToken();
+    const [form] = Form.useForm()
+    const [initialValues, setInitialState] = useState<any>({})
+    const [visible, setVisible] = useState<boolean>(false)
+    const [queryForm, setQueryForm] = useState<TASKAPI.TaskList>({ pageNum: 1, pageSize: 10 })
+    const getTaskList = useRequest(getTaskListApi, { manual: true })//添加请求
+    /***********************************/
+
+    useEffect(() => {
+        getTaskList.run(queryForm)
+    }, [queryForm])
+
+    const onFinish = (data: any) => {
+        const { time, ...pre } = data
+        let newQueryForm = JSON.parse(JSON.stringify(queryForm))
+        newQueryForm.pageNum = 1
+        if (time) {
+            newQueryForm.startTime = moment(time[0]).format('YYYY-MM-DD')
+            newQueryForm.endTime = moment(time[1]).format('YYYY-MM-DD')
+        } else {
+            delete newQueryForm?.startTime
+            delete newQueryForm?.endTime
+        }
+        setQueryForm({ ...newQueryForm, ...pre })
+    }
+
+    const editHandle = (data: any) => {
+        console.log(data)
+        let newData: any ={}
+        const { startTime, endTime, materialClaimJson, materialExample, checkoutType, checkout, urgency, ...pre } = data
+        if (checkoutType === 'CHECKOUT_TYPE_SCALE') {
+            newData['checkout'] = checkout * 100
+        } else {
+            newData['checkout'] = checkout
+        }
+        newData['endTime'] = endTime ? moment(endTime) : null
+        newData['materialExamples'] = materialExample ? materialExample.split(',') : []
+        setInitialState({ ...newData, ...pre, ...JSON.parse(materialClaimJson), checkoutType, startTime: moment(startTime), urgency: urgency + '' })
+        setVisible(true)
+    }
+
+    return <PageContainer>
+        <Card
+            style={{
+                borderRadius: 8,
+            }}
+            bodyStyle={{
+                paddingTop: 0,
+                paddingLeft: 0,
+                paddingRight: 0
+            }}
+            ref={ref}
+        >
+            <Space direction="vertical" style={{ width: '100%' }} size={0}>
+                <Affix offsetTop={56}>
+                    <div style={{ backgroundColor: token.colorBgBase, padding: token.paddingContentHorizontalLG, borderRadius: 8, borderBottom: `1px solid ${token.colorBorderSecondary}` }}>
+                        <Form layout="inline" className='queryForm' name="basic" form={form} onFinish={onFinish}>
+                            <Row gutter={[0, 6]}>
+                                <Col>
+                                    <Form.Item name='name'>
+                                        <Input prefix={<SearchOutlined />} placeholder="任务名称" />
+                                    </Form.Item>
+                                </Col>
+
+                                <Col><Form.Item name='time'>
+                                    <DatePicker.RangePicker style={{ width: 250 }} placeholder={['发布开始日期', '发布开始日期']} />
+                                </Form.Item></Col>
+
+                                <Col><Form.Item name='taskType'>
+                                    <Select
+                                        placeholder="任务分类"
+                                        style={{ width: 100 }}
+                                        allowClear
+                                        options={Object.keys(TaskTypeEnum).map(key => ({ label: (TaskTypeEnum as any)[key], value: key }))}
+                                    />
+                                </Form.Item></Col>
+
+                                <Col><Form.Item name='status'>
+                                    <Select
+                                        placeholder="任务状态"
+                                        style={{ width: 100 }}
+                                        allowClear
+                                        options={Object.keys(StatusEnum).map(key => ({ label: (StatusEnum as any)[key], value: key }))}
+                                    />
+                                </Form.Item></Col>
+
+                                <Col><Form.Item name='urgency'>
+                                    <Select
+                                        placeholder="任务紧急度"
+                                        style={{ width: 120 }}
+                                        allowClear
+                                        options={Object.keys(UrgencyEnum).map(key => ({ label: (UrgencyEnum as any)[key], value: key }))}
+                                    />
+                                </Form.Item></Col>
+
+                                <Col><Form.Item name='materialType'>
+                                    <Select
+                                        placeholder="素材类型"
+                                        style={{ width: 100 }}
+                                        allowClear
+                                        options={Object.keys(MaterialTypeEnum).map(key => ({ label: (MaterialTypeEnum as any)[key], value: key }))}
+                                    />
+                                </Form.Item></Col>
+
+                                <Col><Form.Item name='materialSource'>
+                                    <Select
+                                        placeholder="素材来源"
+                                        style={{ width: 100 }}
+                                        allowClear
+                                        options={Object.keys(MaterialSourceEnum).map(key => ({ label: (MaterialSourceEnum as any)[key], value: key }))}
+                                    />
+                                </Form.Item></Col>
+
+                                <Col>
+                                    <Space>
+                                        <Button type="primary" htmlType="submit">搜索</Button>
+                                        <Button onClick={() => form.resetFields()}>重置</Button>
+                                        <Button type="primary" onClick={() => { setInitialState({}); setVisible(true) }}>发布素材任务</Button>
+                                    </Space>
+                                </Col>
+                            </Row>
+                        </Form>
+
+                    </div>
+                </Affix>
+
+                <List
+                    itemLayout="vertical"
+                    size="large"
+                    dataSource={getTaskList.data?.data?.records}
+                    loading={getTaskList.loading}
+                    pagination={{
+                        onChange: (page) => {
+                            console.log(page);
+                            setQueryForm({ ...queryForm, pageNum: page })
+                        },
+                        current: queryForm.pageNum,
+                        pageSize: queryForm.pageSize,
+                    }}
+                    renderItem={(item: any, index) => (
+                        <List.Item
+                            key={item.id}
+                            extra={<Flex gap={20} style={{ height: '100%' }}>
+                                <Flex style={{ width: size?.width ? `${size?.width / 2 - 265}px` : 300 }} vertical={true} align="flex-start" justify="space-between">
+                                    <Material items={item?.materialExample?.split(',')} resourceUrl={item.materialResource} claimJson={item.materialClaimJson} />
+                                </Flex>
+                                <Flex style={{ width: 165, height: '100%' }} gap={10} vertical={true} align="center" justify="center">
+                                    {TaskStatusEle[item.status]}
+                                    <Tag color="error">结算:{item.checkoutType === 'CHECKOUT_TYPE_SCALE' ? `总消耗 ${item.checkout * 100}%` : `${item.checkout}元(审核合格)`}</Tag>
+                                    <Button type="primary" onClick={() => editHandle(item)}>修改任务</Button>
+                                </Flex>
+                            </Flex>}
+                            actions={[
+                                <Tooltip title='发布时间'><span>{item.createTime}</span></Tooltip>,
+                                <Tooltip title='任务分类'>{TaskTypeEle[item.taskType]}</Tooltip>,
+                                <Tooltip title='素材类型'>{MaterialTypeEle[item.materialType]}</Tooltip>,
+                                <Tooltip title='素材来源要求'><Tag>{(MaterialSourceEnum as any)[item.materialSource]}</Tag></Tooltip>
+                            ]}
+                        >
+                            <List.Item.Meta
+                                avatar={<Avatar src={item.avatar || `https://xsgames.co/randomusers/avatar.php?g=pixel&key=${index}`} style={{ backgroundColor: '#87d068' }} />}
+                                title={<Space size={20}>
+                                    <Typography.Title level={5} style={{ marginBottom: 0 }}>{item.name}</Typography.Title>
+                                    {!!item.urgency && <Space size={4}><span style={{ color: 'red' }}>置顶</span>🆙</Space>}
+                                </Space>}
+                                description={<Space direction="vertical" style={{ width: '100%' }} size={0}>
+                                    {/* 有效时间 */}
+                                    <Typography.Text type="secondary" ellipsis>任务有效时间:{item.startTime}~{item.endTime ? item.endTime : '长期有效'}</Typography.Text>
+                                    {/* 任务描述 */}
+                                    {item.remark && <Typography.Text type="secondary" ellipsis={{ tooltip: true }}>{item.remark}</Typography.Text>}
+                                </Space>}
+                            />
+                            <Typography.Paragraph ellipsis={{ rows: 3, tooltip: true }} style={{ marginBottom: 0 }}>{item.remarkMore}</Typography.Paragraph>
+                        </List.Item>
+                    )}
+                />
+            </Space>
+
+            {visible && <TaskModal visible={visible} onClose={() => setVisible(false)} onChange={() => { setVisible(false); getTaskList.refresh() }} initialValues={initialValues} />}
+        </Card>
+    </PageContainer>
+}
+
 export default MyTask
 export default MyTask

+ 283 - 167
src/pages/MyTask/taskModal.tsx

@@ -1,168 +1,284 @@
-import UploadImg from "@/components/UploadImg"
-import { QuestionCircleOutlined } from "@ant-design/icons"
-import { Col, DatePicker, Form, Input, InputNumber, Modal, Radio, Row, Select, Space, Tooltip } from "antd"
-import { RangePickerProps } from "antd/es/date-picker"
-import React from "react"
-import moment from "moment"
-import { MaterialSourceEnum, MaterialTypeEnum, RatioEnum, StatusEnum, TaskTypeEnum } from "@/const"
-import Interval from "@/components/Interval"
-import UploadImgs from "@/components/UploadImgs"
-
-interface Props {
-    initialValues?: any
-    visible?: boolean
-    onChange?: () => void
-    onClose?: () => void
-}
-/**
- * 发布修改任务
- * @returns 
- */
-const TaskModal: React.FC<Props> = ({ initialValues, visible, onChange, onClose }) => {
-
-    /*************************************/
-    const [form] = Form.useForm<TASKAPI.AddTask>()
-    const endTime = Form.useWatch('endTime', form)
-    const startTime = Form.useWatch('startTime', form)
-    /*************************************/
-
-    const handleOk = async () => {
-
-    }
-
-    const disabledStartDate: RangePickerProps['disabledDate'] = (current) => {
-        // Can not select days before today and today
-        if (endTime) {
-            return current && (current < moment().startOf('day') || current > moment(endTime).endOf('day'));
-        }
-        return current && (current < moment().startOf('day') || current > moment().add(30, 'day').endOf('day'));
-    };
-
-    const disabledEndDate: RangePickerProps['disabledDate'] = (current) => {
-        // Can not select days before today and today
-        if (startTime) {
-            return current && (current < moment(startTime).startOf('day') || current > moment().add(30, 'day').endOf('day'));
-        }
-        return current && (current < moment().startOf('day') || current > moment().add(30, 'day').endOf('day'));
-    };
-
-    return <Modal
-        title={`发布任务`}
-        open={visible}
-        onCancel={onClose}
-        onOk={handleOk}
-        width={1000}
-    >
-        <Form
-            initialValues={
-                Object.keys(initialValues).length > 0 ?
-                    initialValues :
-                    {
-                        taskType: 'TASK_TYPE_GAME',
-                        materialType: 'MATERIAL_TYPE_VIDEO',
-                        status: 'STATUS_NORMAL',
-                        materialSource: 'MATERIAL_SOURCE_ORIGINAL',
-                        checkout: 2,
-                        materialExamples: ['https://zx-material-center-test.oss-cn-hangzhou.aliyuncs.com/image/AAFE07F5819A488EB0A7B14E03FBBE7E.jpg', 'https://zx-material-center-test.oss-cn-hangzhou.aliyuncs.com/image/2E6677DBEC314344B0416557531899D2.jpg', 'https://zx-material-center-test.oss-cn-hangzhou.aliyuncs.com/image/AAFE07F5819A488EB0A7B14E03FBBE7E.jpg', 'https://zx-material-center-test.oss-cn-hangzhou.aliyuncs.com/image/2E6677DBEC314344B0416557531899D2.jpg']
-                    }
-            }
-            name="taskModal"
-            form={form}
-            labelCol={{ span: 6 }}
-            wrapperCol={{ span: 18 }}
-            colon={false}
-            labelAlign="left"
-        >
-            <Row gutter={20}>
-                <Col span={12}>
-                    <Form.Item label={<strong>任务头像</strong>} name='avatar'>
-                        <UploadImg isUpload={true}/>
-                    </Form.Item>
-                    <Form.Item label={<strong>任务名称</strong>} name='name' rules={[{ required: true, message: '请输入任务名称!' }]}>
-                        <Input placeholder="请输入任务名称" />
-                    </Form.Item>
-                    <Form.Item label={<strong>描述</strong>} name='remark'>
-                        <Input placeholder="请输入任务描述" />
-                    </Form.Item>
-                    <Form.Item label={<strong>补充说明</strong>} name='remarkMore'>
-                        <Input placeholder="请输入补充说明" />
-                    </Form.Item>
-                    <Form.Item label={<strong>
-                        <span style={{ color: '#ff4d4f' }}>*</span>有效时间
-                        <Tooltip title="开始日期必填">
-                            <QuestionCircleOutlined style={{ marginLeft: 2 }} />
-                        </Tooltip>
-                    </strong>}>
-                        <Space>
-                            <Form.Item name="startTime" noStyle rules={[{ required: true, message: '请设置开始日期!' }]}>
-                                <DatePicker placeholder="开始日期" disabledDate={disabledStartDate} />
-                            </Form.Item>
-                            <span>-</span>
-                            <Form.Item name="endTime" noStyle>
-                                <DatePicker placeholder="结束日期" disabledDate={disabledEndDate} />
-                            </Form.Item>
-                        </Space>
-                    </Form.Item>
-                    <Form.Item label={<strong>紧急度</strong>} tooltip="任务紧急度决定任务的展示情况,默认将“置顶”任务展示页面最前,其次按照发布时间倒序排列展示(即最新发布的任务展示在页面最前端)" name='urgency' rules={[{ required: true, message: '请选择任务紧急度!' }]}>
-                        <Select
-                            placeholder="请选择任务紧急度"
-                            options={[
-                                { value: false, label: '普通' },
-                                { value: true, label: '置顶' }
-                            ]}
-                        />
-                    </Form.Item>
-                    <Form.Item label={<strong>任务状态</strong>} name='status' rules={[{ required: true, message: '请选择任务状态!' }]}>
-                        <Radio.Group>
-                            {Object.keys(StatusEnum).filter(key => initialValues?.id ? true : key === 'STATUS_EXPIRE' ? false : true).map(key => <Radio value={key} key={key}>{(StatusEnum as any)[key]}</Radio>)}
-                        </Radio.Group>
-                    </Form.Item>
-                    <Form.Item label={<strong>任务分类</strong>} name='taskType' rules={[{ required: true, message: '请选择任务分类!' }]}>
-                        <Radio.Group>
-                            {Object.keys(TaskTypeEnum).map(key => <Radio value={key} key={key}>{(TaskTypeEnum as any)[key]}</Radio>)}
-                        </Radio.Group>
-                    </Form.Item>
-                    <Form.Item label={<strong>素材类型</strong>} name='materialType' rules={[{ required: true, message: '请选择素材类型!' }]}>
-                        <Radio.Group>
-                            {Object.keys(MaterialTypeEnum).map(key => <Radio value={key} key={key}>{(MaterialTypeEnum as any)[key]}</Radio>)}
-                        </Radio.Group>
-                    </Form.Item>
-                    <Form.Item label={<strong>素材来源</strong>} name='materialSource' rules={[{ required: true, message: '请选择素材类型!' }]}>
-                        <Radio.Group>
-                            {Object.keys(MaterialSourceEnum).map(key => <Radio value={key} key={key}>{(MaterialSourceEnum as any)[key]}</Radio>)}
-                        </Radio.Group>
-                    </Form.Item>
-                    <Form.Item label={<strong>奖励结算</strong>} name='checkout' rules={[{ required: true, message: '请输入奖励结算!' }]}>
-                        <InputNumber
-                            addonBefore="消耗比例"
-                            min={0}
-                            max={100}
-                            formatter={(value) => `${value}%`}
-                            parser={(value) => value!.replace('%', '') as any}
-                        />
-                    </Form.Item>
-                </Col>
-                <Col span={12}>
-                    <Form.Item label={<strong>素材比例</strong>} name='ratio' rules={[{ required: true, message: '请选择素材比例!' }]}>
-                        <Select
-                            placeholder="请选择素材比例"
-                            options={Object.keys(RatioEnum).map(key => ({ label: (RatioEnum as any)[key], value: key }))}
-                        />
-                    </Form.Item>
-                    <Form.Item label={<strong>素材尺寸</strong>} name='size' rules={[{ required: true, message: '请选择素材比例!' }]}>
-                        <Interval unit="*" placeholder={['720', '1280']}/>
-                    </Form.Item>
-                    <Form.Item label={<strong>素材大小</strong>} name='extent' tooltip="单位(MB)" rules={[{ required: true, message: '请选择素材比例!' }]}>
-                        <Interval placeholder={['20M', '40M']}/>
-                    </Form.Item>
-                    <Form.Item label={<strong>素材示例上传</strong>} name='materialExamples'>
-                        <UploadImgs isUpload={true} maxCount={10} />
-                    </Form.Item>
-                </Col>
-            </Row>
-
-        </Form>
-    </Modal>
-}
-
+import UploadImg from "@/components/UploadImg"
+import { QuestionCircleOutlined } from "@ant-design/icons"
+import { Col, DatePicker, Form, Input, InputNumber, Modal, Radio, Row, Select, Space, Tooltip, message } from "antd"
+import { RangePickerProps } from "antd/es/date-picker"
+import React from "react"
+import moment from "moment"
+import { CheckoutTypeEnum, MaterialSourceEnum, MaterialTypeEnum, RatioEnum, StatusEnum, TaskTypeEnum, UrgencyEnum } from "@/const"
+import Interval from "@/components/Interval"
+import UploadImgs from "@/components/UploadImgs"
+import UploadZip from "@/components/UploadZip"
+import { addTaskApi, modifyTaskApi } from "@/services/task-api/myTask"
+import { useRequest } from "ahooks"
+
+const disabledRangeTime = (current: any) => {
+    let upDate = moment(current).format('YYYY-MM-DD')//选中的年月日
+    let upHours = moment(current).format('HH')//选中的时
+    let upMinutes = moment(current).format('mm')//选中的分
+    let atDate = moment(new Date()).format('YYYY-MM-DD')//今天的年月日
+    let atHours = moment(new Date()).format('HH')//现在的时
+    let atMinutes = moment(new Date()).format('mm')//现在的分
+    let atSeconds = moment(new Date()).format('ss')//现在的秒
+    let range = (num: number, at: string) => {
+        if (upDate == atDate) {//假如选中时间的今天
+            let arr: number[] = []
+            Array(num).fill('').forEach((a, b) => { if (b < Number(at)) { arr.push(b) } })
+            return arr
+        } else {
+            return []
+        }
+    }
+    let minutes = (num: number, at: string) => {
+        if (upDate == atDate) {//假如选中时间的今天
+            let arr: number[] = []
+            if (upHours == atHours) {
+                Array(num).fill('').forEach((a, b) => { if (b < Number(at) + 5) { arr.push(b) } })
+            }
+            return arr
+        } else {
+            return []
+        }
+    }
+    let seconds = (num: number, at: string) => {
+        if (upDate == atDate) {//假如选中时间的今天
+            let arr: number[] = []
+            if (upHours == atHours) {
+                Array(num).fill('').forEach((a, b) => {
+                    if (Number(upMinutes) - Number(atMinutes) > 5) {
+                    } else {
+                        if (b < Number(at) + 5) { arr.push(b) }
+                    }
+                })
+            }
+            return arr
+        } else {
+            return []
+        }
+    }
+    return {
+        disabledHours: () => range(24, atHours),
+        disabledMinutes: () => minutes(60, atMinutes),
+        disabledSeconds: () => seconds(60, atSeconds),
+    }
+};
+
+interface FormProps extends TASKAPI.AddTask {
+    ratio: any
+    size: any
+    extent: any
+    materialExamples: string[]
+}
+
+interface ModifyFormProps extends TASKAPI.ModifyTask {
+    materialExamples: string[]
+}
+
+interface Props {
+    initialValues?: any
+    visible?: boolean
+    onChange?: () => void
+    onClose?: () => void
+}
+/**
+ * 发布修改任务
+ * @returns 
+ */
+const TaskModal: React.FC<Props> = ({ initialValues, visible, onChange, onClose }) => {
+
+    /*************************************/
+    const [form] = Form.useForm<FormProps>()
+    const endTime = Form.useWatch('endTime', form)
+    const startTime = Form.useWatch('startTime', form)
+    const checkoutType = Form.useWatch('checkoutType', form)
+    const addTask = useRequest(addTaskApi, { manual: true })
+    const modifyTask = useRequest(modifyTaskApi, { manual: true })
+    /*************************************/
+
+    const handleOk = async () => {
+        form.submit()
+        let data = await form.validateFields()
+        if (initialValues?.id) {
+            data.id = initialValues?.id
+            const { startTime, materialExamples, ...par } = data as ModifyFormProps
+            if (par?.endTime) {
+                par['endTime'] = moment(par.endTime).format('YYYY-MM-DD HH:mm:ss')
+            }
+            if (materialExamples?.length > 0) {
+                par['materialExample'] = materialExamples.toString()
+            }
+            modifyTask.runAsync({ ...par, startTime: moment(startTime).format('YYYY-MM-DD HH:mm:ss') }).then(res => {
+                if (res?.data) {
+                    message.success('修改成功')
+                    onChange?.()
+                }
+            })
+        } else {
+            const { size, extent, startTime, checkoutType, checkout, materialExamples, ...par } = data
+            let materialClaimJson = JSON.stringify({ size, extent })
+            if (par?.endTime) {
+                par['endTime'] = moment(par.endTime).format('YYYY-MM-DD HH:mm:ss')
+            }
+            if (materialExamples?.length > 0) {
+                par['materialExample'] = materialExamples.toString()
+            }
+            addTask.runAsync({ ...par, materialClaimJson, startTime: moment(startTime).format('YYYY-MM-DD HH:mm:ss'), checkoutType, checkout: checkoutType === 'CHECKOUT_TYPE_SCALE' ? checkout / 100 : checkout }).then(res => {
+                console.log(res)
+                if (res?.data) {
+                    message.success('发布成功')
+                    onChange?.()
+                }
+            })
+        }
+    }
+
+    const disabledStartDate: RangePickerProps['disabledDate'] = (current) => {
+        // Can not select days before today and today
+        if (endTime) {
+            return current && (current < moment().startOf('day') || current > moment(endTime).endOf('day'));
+        }
+        return current && (current < moment().startOf('day') || current > moment().add(30, 'day').endOf('day'));
+    };
+
+    const disabledEndDate: RangePickerProps['disabledDate'] = (current) => {
+        // Can not select days before today and today
+        if (startTime) {
+            return current && (current < moment(startTime).startOf('day') || current > moment().add(30, 'day').endOf('day'));
+        }
+        return current && (current < moment().startOf('day') || current > moment().add(30, 'day').endOf('day'));
+    };
+
+    return <Modal
+        title={initialValues?.id ? `修改任务` : `发布任务`}
+        open={visible}
+        onCancel={onClose}
+        onOk={handleOk}
+        width={1000}
+        confirmLoading={addTask.loading || modifyTask.loading}
+    >
+        <Form
+            initialValues={
+                Object.keys(initialValues).length > 0 ?
+                    initialValues :
+                    {
+                        taskType: 'TASK_TYPE_GAME',
+                        materialType: 'MATERIAL_TYPE_VIDEO',
+                        status: 'STATUS_NORMAL',
+                        materialSource: 'MATERIAL_SOURCE_ORIGINAL',
+                        checkoutType: 'CHECKOUT_TYPE_SCALE',
+                        checkout: 2,
+                        // avatar: 'https://zx-material-center-test.oss-cn-hangzhou.aliyuncs.com/image/AAFE07F5819A488EB0A7B14E03FBBE7E.jpg',
+                        // materialExamples: ['https://zx-material-center-test.oss-cn-hangzhou.aliyuncs.com/image/AAFE07F5819A488EB0A7B14E03FBBE7E.jpg', 'https://zx-material-center-test.oss-cn-hangzhou.aliyuncs.com/image/2E6677DBEC314344B0416557531899D2.jpg', 'https://zx-material-center-test.oss-cn-hangzhou.aliyuncs.com/image/AAFE07F5819A488EB0A7B14E03FBBE7E.jpg', 'https://zx-material-center-test.oss-cn-hangzhou.aliyuncs.com/image/2E6677DBEC314344B0416557531899D2.jpg'],
+                        // materialResource: 'https://zx-material-center-test.oss-cn-hangzhou.aliyuncs.com/zip/BBB2531663F448A7A4D599469F354DC9.zip'
+                    }
+            }
+            name="taskModal"
+            form={form}
+            labelCol={{ span: 6 }}
+            wrapperCol={{ span: 18 }}
+            colon={false}
+            labelAlign="left"
+        >
+            <Row gutter={20}>
+                <Col span={12}>
+                    <Form.Item label={<strong>任务头像</strong>} name='avatar'>
+                        <UploadImg isUpload={true} />
+                    </Form.Item>
+                    <Form.Item label={<strong>任务名称</strong>} name='name' rules={[{ required: true, message: '请输入任务名称!' }]}>
+                        <Input placeholder="请输入任务名称" />
+                    </Form.Item>
+                    <Form.Item label={<strong>描述</strong>} name='remark'>
+                        <Input placeholder="请输入任务描述" />
+                    </Form.Item>
+                    <Form.Item label={<strong>说明</strong>} name='remarkMore' rules={[{ required: true, message: '请输入说明!' }]}>
+                        <Input placeholder="请输入补充说明" />
+                    </Form.Item>
+                    <Form.Item label={<strong>
+                        <span style={{ color: '#ff4d4f' }}>*</span>有效时间
+                        <Tooltip title="开始日期必填">
+                            <QuestionCircleOutlined style={{ marginLeft: 2 }} />
+                        </Tooltip>
+                    </strong>}>
+                        <Space>
+                            <Form.Item name="startTime" noStyle rules={[{ required: true, message: '请设置开始日期!' }]}>
+                                <DatePicker placeholder="开始日期" disabledDate={disabledStartDate} showTime disabledTime={disabledRangeTime} />
+                            </Form.Item>
+                            <span>-</span>
+                            <Form.Item name="endTime" noStyle>
+                                <DatePicker placeholder="结束日期" disabledDate={disabledEndDate} showTime disabledTime={disabledRangeTime} />
+                            </Form.Item>
+                        </Space>
+                    </Form.Item>
+                    <Form.Item label={<strong>紧急度</strong>} name='urgency' rules={[{ required: true, message: '请选择任务紧急度!' }]} tooltip="任务紧急度决定任务的展示情况,默认将“置顶”任务展示页面最前,其次按照发布时间倒序排列展示(即最新发布的任务展示在页面最前端)">
+                        <Select
+                            placeholder="请选择任务紧急度"
+                            options={Object.keys(UrgencyEnum).map(key => ({ label: (UrgencyEnum as any)[key], value: key }))}
+                        />
+                    </Form.Item>
+                    <Form.Item label={<strong>任务状态</strong>} name='status' rules={[{ required: true, message: '请选择任务状态!' }]}>
+                        <Radio.Group>
+                            {Object.keys(StatusEnum).filter(key => initialValues?.id ? true : key === 'STATUS_EXPIRE' ? false : true).map(key => <Radio value={key} key={key}>{(StatusEnum as any)[key]}</Radio>)}
+                        </Radio.Group>
+                    </Form.Item>
+                    {!initialValues?.id && <>
+                        <Form.Item label={<strong>任务分类</strong>} name='taskType' rules={[{ required: true, message: '请选择任务分类!' }]}>
+                            <Radio.Group>
+                                {Object.keys(TaskTypeEnum).map(key => <Radio value={key} key={key}>{(TaskTypeEnum as any)[key]}</Radio>)}
+                            </Radio.Group>
+                        </Form.Item>
+                        <Form.Item label={<strong>素材类型</strong>} name='materialType' rules={[{ required: true, message: '请选择素材类型!' }]}>
+                            <Radio.Group>
+                                {Object.keys(MaterialTypeEnum).map(key => <Radio value={key} key={key}>{(MaterialTypeEnum as any)[key]}</Radio>)}
+                            </Radio.Group>
+                        </Form.Item>
+                        <Form.Item label={<strong>素材来源</strong>} name='materialSource' rules={[{ required: true, message: '请选择素材来源!' }]}>
+                            <Radio.Group>
+                                {Object.keys(MaterialSourceEnum).map(key => <Radio value={key} key={key}>{(MaterialSourceEnum as any)[key]}</Radio>)}
+                            </Radio.Group>
+                        </Form.Item>
+                        <Form.Item label={<strong>结算类型</strong>} name='checkoutType' rules={[{ required: true, message: '请选择结算类型!' }]} tooltip="消耗比例:按照最终素材的总消耗比例计算奖励,月度结算奖励;任务:按照任务最终审核状态为“合格”的素材计算奖励,月度结算奖励">
+                            <Radio.Group onChange={(e) => {
+                                if (e.target.value === 'CHECKOUT_TYPE_SCALE') form.setFieldsValue({ checkout: 2 })
+                                else form.setFieldsValue({ checkout: 100 })
+                            }}>
+                                {Object.keys(CheckoutTypeEnum).map(key => <Radio value={key} key={key}>{(CheckoutTypeEnum as any)[key]}</Radio>)}
+                            </Radio.Group>
+                        </Form.Item>
+                        <Form.Item label={<strong>奖励结算</strong>} name='checkout' rules={[{ required: true, message: '请输入奖励结算!' }]}>
+                            {checkoutType === 'CHECKOUT_TYPE_SCALE' ? <InputNumber
+                                min={0}
+                                max={100}
+                                formatter={(value) => `${value}%`}
+                                parser={(value) => value!.replace('%', '') as any}
+                            /> : <InputNumber min={0} suffix="元" />}
+                        </Form.Item>
+                    </>}
+                </Col>
+                <Col span={12}>
+                    {!initialValues?.id && <>
+                        {/* <Form.Item label={<strong>素材比例</strong>} name='ratio' rules={[{ required: true, message: '请选择素材比例!' }]}>
+                            <Select
+                                placeholder="请选择素材比例"
+                                options={Object.keys(RatioEnum).map(key => ({ label: (RatioEnum as any)[key], value: key }))}
+                            />
+                        </Form.Item> */}
+                        <Form.Item label={<strong>素材尺寸</strong>} name='size' rules={[{ required: true, message: '请选择素材比例!' }]}>
+                            <Interval unit="*" placeholder={['720', '1280']} />
+                        </Form.Item>
+                        <Form.Item label={<strong>素材大小</strong>} name='extent' tooltip="单位(MB)" rules={[{ required: true, message: '请选择素材比例!' }]}>
+                            <Interval placeholder={['20M', '40M']} />
+                        </Form.Item>
+                    </>}
+                    <Form.Item label={<strong>素材示例</strong>} name='materialExamples'>
+                        <UploadImgs isUpload={true} maxCount={10} />
+                    </Form.Item>
+                    <Form.Item label={<strong>素材资源包</strong>} name='materialResource'>
+                        <UploadZip />
+                    </Form.Item>
+                </Col>
+            </Row>
+
+        </Form>
+    </Modal>
+}
+
 export default React.memo(TaskModal)
 export default React.memo(TaskModal)

+ 24 - 0
src/pages/Opus/components/Details.tsx

@@ -0,0 +1,24 @@
+import { Drawer } from "antd"
+import React from "react"
+
+interface Props {
+    visible?: boolean
+    onClose?: () => void
+}
+/**
+ * 素材详情
+ * @returns 
+ */
+const Details: React.FC<Props> = ({ visible, onClose }) => {
+
+
+    return <Drawer
+        title={`详情`}
+        open={visible}
+        onClose={onClose}
+    >
+
+    </Drawer>
+}
+
+export default React.memo(Details)

+ 38 - 0
src/pages/Opus/components/VideoOpus.tsx

@@ -0,0 +1,38 @@
+import { ImageProps, theme, Image, Modal } from "antd"
+import React, { useState } from "react"
+import style from './index.less'
+import { getVideoImgUrl } from "@/utils";
+import play from "../../../../public/image/play.png"
+import { CloseOutlined } from "@ant-design/icons";
+
+const VideoOpus: React.FC<ImageProps> = ({ preview = false, src, ...data }) => {
+
+    /*****************************/
+    const { useToken } = theme;
+    const { token } = useToken()
+    const [toPlay, setToPlay] = useState<boolean>(false)
+    /*****************************/
+
+    return <div style={{ display: 'inline-block',  }}>
+        <div className={style.imgNews} style={{ borderRadius: token.borderRadius }}>
+            <Image src={src ? getVideoImgUrl(src) : 'error'} preview={false} {...data} className={style.img} />
+            <div className={style.mask}>
+                <img src={play} onClick={(e) => { e.stopPropagation(); setToPlay(true) }} />
+            </div>
+        </div>
+        {toPlay && <div onClick={(e) => { e.stopPropagation(); }}>
+            <Modal
+                open={toPlay}
+                styles={{ body: { backgroundColor: 'rgba(0,0,0,0.8)', overflow: 'hidden', borderRadius: token.borderRadius, padding: '20px 24px' } }}
+                footer={null}
+                closeIcon={<CloseOutlined style={{ color: token.colorPrimary }} />}
+                onCancel={(e) => { e.stopPropagation(); setToPlay(false) }}
+                className="playVideo"
+            >
+                <video className={style.video} style={{ borderRadius: token.borderRadius }} src={src} autoPlay controls>您的浏览器不支持 video 标签。</video>
+            </Modal>
+        </div>}
+    </div>
+}
+
+export default React.memo(VideoOpus)

+ 40 - 0
src/pages/Opus/components/index.less

@@ -0,0 +1,40 @@
+.imgNews {
+    display: inline-block;
+    overflow: hidden;
+    position: relative;
+
+    &>div {
+        .img {
+            height: 150px;
+            display: inline-block;
+            cursor: pointer;
+        }
+    }
+
+    .mask {
+        position: absolute;
+        width: 100%;
+        height: 100%;
+        top: 0;
+        left: 0;
+        background-color: rgba(0, 0, 0, .3);
+        display: flex;
+        justify-content: center;
+        align-items: center;
+
+        &>img {
+            width: 40px;
+            height: 40px;
+            display: inline-block;
+            cursor: pointer;
+        }
+    }
+}
+
+.video {
+    width: 100%;
+    max-width: 320px;
+    max-height: 600px;
+    display: block;
+    margin: auto;
+}

+ 95 - 0
src/pages/Opus/index.less

@@ -0,0 +1,95 @@
+.flexColumn {
+    display: flex;
+    flex-direction: column;
+}
+
+.weMaterial_left:extend(.flexColumn) {
+    width: 100%;
+    flex-shrink: 0;
+
+    .left_root {
+        flex-shrink: 0;
+        // height: calc(100% - 68px);
+        position: relative;
+        width: 100%;
+
+        .content_spin:extend(.flexBase) {
+            position: fixed;
+            width: 100%;
+            height: 100%;
+            top: 0;
+            left: 0;
+            bottom: 0;
+            right: 0;
+            background-color: rgba(0, 0, 0, .1);
+            z-index: 1000;
+            text-align: center;
+            padding: 50%;
+            // transform: translate(-50%, -50%);
+            // pointer-events: none;
+        }
+    }
+
+    .left_content {
+        height: 100%;
+        box-sizing: border-box;
+        overflow: hidden;
+        overflow-y: auto;
+
+        .left_content_caover {
+            height: 150px;
+            overflow: hidden;
+            background-color: rgb(235, 235, 235);
+            display: flex;
+            justify-content: center;
+        }
+
+        .left_content_card {
+            cursor: pointer;
+
+            &:hover .action {
+                opacity: 1;
+            }
+        }
+
+        div.memo_content {
+            display: flex;
+            // padding-bottom: 10px;
+
+            &>div {
+                padding: 0 5px;
+                box-sizing: border-box;
+            }
+        }
+
+        .tips:extend(.flexBetween) {
+            width: 100%;
+            font-size: 13px;
+        }
+
+        .action {
+            position: absolute;
+            bottom: 5px;
+            width: 100%;
+            left: 0;
+            padding: 0 10px;
+            box-sizing: border-box;
+            opacity: 0;
+            transition: all .5s;
+
+            &>div:extend(.flexBetween) {
+                &>div:extend(.flexBase) {
+                    // width: 25%;
+                    flex: 1;
+                }
+            }
+
+        }
+
+        .content_row:extend(.flexColumn) {
+            flex: 1;
+            flex-shrink: 0;
+            gap: 10px;
+        }
+    }
+}

+ 113 - 0
src/pages/Opus/index.tsx

@@ -0,0 +1,113 @@
+import { SearchOutlined } from "@ant-design/icons"
+import { PageContainer } from "@ant-design/pro-components"
+import { Affix, Button, Card, Col, DatePicker, Form, Input, Radio, Row, Select, Space, theme } from "antd"
+import moment from "moment";
+import { useEffect, useState } from "react";
+import OpusRoll from "./opusRoll";
+import style from './index.less'
+import { useRequest } from "ahooks";
+import { getOpusListApi } from "@/services/task-api/opus";
+import { MaterialTypeEnum } from "@/const";
+
+const Opus: React.FC = () => {
+
+    /**************************************/
+    const { useToken } = theme;
+    const { token } = useToken();
+    const [form] = Form.useForm<TASKAPI.OpusList>()
+    const [queryForm, setQueryForm] = useState<TASKAPI.OpusList>({ pageNum: 1, pageSize: 10 })
+    const getOpusList = useRequest(getOpusListApi, { manual: true })
+    const [data, setData] = useState<any[]>([])
+    /**************************************/
+
+    useEffect(() => {
+        getOpusList.runAsync(queryForm).then(res => {
+            if (res) {
+                if (queryForm.pageNum === 1) {
+                    setData(res?.data?.records)
+                } else {
+                    setData(data.concat(res?.data?.records))
+                }
+            }
+        })
+    }, [queryForm])
+
+    const onFinish = (data: any) => {
+        const { taskTime, uploadTime, ...pre } = data
+        let newQueryForm = JSON.parse(JSON.stringify(queryForm))
+        newQueryForm.pageNum = 1
+        if (taskTime) {
+            newQueryForm.taskCreateStartTime = moment(taskTime[0]).format('YYYY-MM-DD')
+            newQueryForm.taskCreateEndTime = moment(taskTime[1]).format('YYYY-MM-DD')
+        } else {
+            delete newQueryForm?.taskCreateStartTime
+            delete newQueryForm?.taskCreateEndTime
+        }
+        if (uploadTime) {
+            newQueryForm.uploadStartTime = moment(uploadTime[0]).format('YYYY-MM-DD')
+            newQueryForm.uploadEndTime = moment(uploadTime[1]).format('YYYY-MM-DD')
+        } else {
+            delete newQueryForm?.uploadStartTime
+            delete newQueryForm?.uploadEndTime
+        }
+        setQueryForm({ ...newQueryForm, ...pre })
+    }
+
+    // 下一页
+    const loadMoreData = () => {
+        setQueryForm({ ...queryForm, pageNum: queryForm.pageNum + 1 })
+    }
+
+    return <PageContainer>
+        <Card
+            style={{ borderRadius: 8 }}
+            bodyStyle={{ padding: 0 }}
+        >
+            <Space direction="vertical" style={{ width: '100%' }} size={0}>
+                <Affix offsetTop={56}>
+                    <div style={{ backgroundColor: token.colorBgBase, padding: token.paddingContentHorizontalLG, borderRadius: 8, borderBottom: `1px solid ${token.colorBorderSecondary}` }}>
+                        <Form layout="inline" className='queryForm' name="basic" form={form} onFinish={onFinish}>
+                            <Row gutter={[0, 6]}>
+                                <Col>
+                                    <Form.Item name='materialType'>
+                                        <Select
+                                            placeholder="素材类型"
+                                            options={Object.keys(MaterialTypeEnum).map(key => ({ label: (MaterialTypeEnum as any)[key], value: key }))}
+                                        />
+                                    </Form.Item>
+                                </Col>
+
+                                <Col>
+                                    <Form.Item name='taskName'>
+                                        <Input prefix={<SearchOutlined />} placeholder="搜索任务名称" />
+                                    </Form.Item>
+                                </Col>
+
+                                <Col><Form.Item name='taskTime'>
+                                    <DatePicker.RangePicker style={{ width: 250 }} placeholder={['任务开始日期', '任务开始日期']} />
+                                </Form.Item></Col>
+
+                                <Col><Form.Item name='uploadTime'>
+                                    <DatePicker.RangePicker style={{ width: 250 }} placeholder={['上传开始日期', '上传开始日期']} />
+                                </Form.Item></Col>
+
+                                <Col>
+                                    <Space>
+                                        <Button type="primary" htmlType="submit">搜索</Button>
+                                        <Button onClick={() => form.resetFields()}>重置</Button>
+                                    </Space>
+                                </Col>
+                            </Row>
+                        </Form>
+                    </div>
+                </Affix>
+
+                <div style={{ padding: 19 }} className={style.weMaterial_left}>
+                    <OpusRoll data={data} total={getOpusList.data?.data?.total || 0} loading={getOpusList.loading} loadMore={loadMoreData}/>
+                </div>
+            </Space>
+        </Card>
+    </PageContainer>
+}
+
+export default Opus

+ 143 - 0
src/pages/Opus/opusRoll.tsx

@@ -0,0 +1,143 @@
+import React, { useEffect, useMemo, useRef, useState } from "react"
+import style from './index.less'
+import { Button, Card, Divider, Flex, Skeleton, Space, Spin, Tag, Typography } from "antd";
+import InfiniteScroll from "react-infinite-scroll-component";
+import { useRequest, useSize } from "ahooks";
+import VideoOpus from "./components/VideoOpus";
+import { MaterialSourceEnum } from "@/const";
+import { downloadEscalationApi } from "@/services/task-api/download";
+
+interface Props {
+    data: any[]
+    total: number,
+    loading?: boolean
+    loadMore?: () => void
+    flexNum?: number
+    id?: string
+}
+
+const OpusRoll: React.FC<Props> = ({ loading, data, total, loadMore, flexNum = 0, id = 'scrollableDiv' }) => {
+
+    /************************/
+    const ref = useRef<HTMLDivElement>(null);
+    const size = useSize(ref);
+    const [rowNum, setRowNum] = useState<number>(flexNum)
+    const [dataList, setDataList] = useState<any[]>([])
+    const downloadEscalation = useRequest(downloadEscalationApi, { manual: true })
+    /************************/
+
+    useEffect(() => {
+        setDataList(data || [])
+    }, [data])
+
+    // 根据内容宽度计算列数
+    useEffect(() => {
+        if (size?.width && !flexNum) {
+            let rowNum = Math.floor((size?.width - 26) / 260)
+            setRowNum(rowNum || 1)
+        }
+    }, [size?.width])
+
+    const download = (url: string, userMaterialId: number) => {
+        downloadEscalation.runAsync({ userMaterialId }).then(res => {
+            const fileName = 'downloaded.mp4'; // 可以自定义下载的文件名
+            let link = url
+            let x = new XMLHttpRequest()
+            x.open('GET', link, true)
+            x.responseType = 'blob'
+            x.onload = (e) => {
+                let url = window.URL.createObjectURL(x.response)
+                let a = document.createElement('a')
+                a.href = url
+                a.download = fileName
+                a.click()
+            }
+            x.send()
+        })
+    }
+
+    const content = useMemo(() => {
+        let sourceData: any[] = []
+        dataList.forEach((item, index) => {
+            sourceData[index % rowNum] ? sourceData[index % rowNum]?.push(item) : sourceData[index % rowNum] = [item]
+        })
+
+        // 不足
+        if (sourceData.length < rowNum) {
+            let fewNum = rowNum - sourceData.length
+            sourceData = [...sourceData, ...Array(fewNum).fill([]).map(item => item)]
+        }
+        return <>
+            {sourceData.map((item: any[], index) => <div key={index} className={style.content_row} style={{ width: (1 / rowNum * 100) + '%' }}>
+                {item?.map((list: any) => {
+                    const { size } = JSON.parse(list.materialClaimJson)
+                    return <Card
+                        className={style.left_content_card}
+                        key={list.id}
+                        bodyStyle={{ padding: 10, position: 'relative' }}
+                        headStyle={{ padding: '5px 10px' }}
+                        cover={<div className={style.left_content_caover}>
+                            <VideoOpus src={list.link} />
+                        </div>}
+                        title={<Space direction="vertical" size={0} style={{ width: '100%' }}>
+                            <Typography.Title style={{ margin: 0, fontSize: 15 }} level={5} ellipsis>{list.taskName}</Typography.Title>
+                            <Typography.Text type="secondary" style={{ fontSize: 13 }}>{list.createTime}</Typography.Text>
+                        </Space>}
+                    >
+                        <Flex justify="space-between">
+                            <Typography.Text>素材尺寸:{size[0]}*{size[1]}</Typography.Text>
+                            <Typography.Text>素材大小:{(list.size / 1024 / 1024).toFixed(2)}M</Typography.Text>
+                        </Flex>
+                        <Flex justify="space-between">
+                            <Typography.Text>素材ID:{list.taskId}</Typography.Text>
+                            {(!list.cost && list.cost !== 0) ? null : <Typography.Text>消耗:{list.cost}</Typography.Text>}
+                        </Flex>
+                        <Flex justify="space-between">
+                            <Space size={0}>
+                                <Tag color="#55acee">{(MaterialSourceEnum as any)[list.materialSource]}</Tag>
+                                {list.checkStatus ?  <Tag color="success">合格</Tag> :  <Tag color="warning">审核中</Tag>}
+                            </Space>
+                            <Space>
+                                <Button type="link" style={{ padding: 0 }}>下载</Button>
+                                {/* <Button type="link" style={{ padding: 0 }}>详情</Button> */}
+                            </Space>
+                        </Flex>
+                    </Card>
+                })}
+            </div>)}
+        </>
+    }, [rowNum, size, dataList])
+
+    const loadMoreData = () => {
+        console.log('触发了--->')
+        if (dataList.length === total) {
+            return
+        }
+        loadMore?.()
+    }
+
+    return <div className={style.left_root}>
+        <div className={style.left_content} ref={ref}>
+            <InfiniteScroll
+                dataLength={dataList.length}
+                next={loadMoreData}
+                hasMore={dataList.length < total}
+                loader={<div style={{ display: 'flex', marginTop: 10 }}>
+                    {Array(rowNum).fill('').map((item, index) => <div key={index} className={style.content_row} style={{ width: (1 / rowNum * 100) + '%', padding: '0 5px', boxSizing: 'border-box' }}>
+                        <Skeleton.Input active style={{ width: '100%' }} />
+                    </div>)}
+                </div>}
+                endMessage={<Divider plain>It is all, nothing more 🤐</Divider>}
+                scrollableTarget={id}
+                // height={height}
+            >
+                <div className={style.memo_content}>
+                    {content}
+                </div>
+            </InfiniteScroll>
+        </div>
+        {loading && <div className={style.content_spin}><Spin /></div>}
+    </div>
+}
+
+export default React.memo(OpusRoll)

+ 6 - 0
src/pages/Task/index.less

@@ -0,0 +1,6 @@
+.myUploadVideo {
+    .video-content {
+        width: 450px;
+        height: 130px;
+    }
+}

+ 144 - 148
src/pages/Task/index.tsx

@@ -1,149 +1,145 @@
-import { TaskTypeEnum, TaskStatusEnum } from "@/const"
-import { AlertOutlined, SearchOutlined } from "@ant-design/icons"
-import { PageContainer } from "@ant-design/pro-components"
-import { useSize } from "ahooks";
-import { Affix, Alert, Avatar, Badge, Button, Card, Flex, Input, List, Select, Space, Tag, Typography, theme } from "antd"
-import { useEffect, useRef } from "react";
-
-const data = Array.from({ length: 23 }).map((_, i) => ({
-    // href: 'https://ant.design',
-    title: `素材任务名称 ${i}`,
-    avatar: `https://xsgames.co/randomusers/avatar.php?g=pixel&key=${i}`,
-    description: '这是一条任务描述,XXXXXXXXXXXXXXXXXXXXXXXXXXXXX好长好长Ant Design, a design language',
-    content:
-        'We supply a series of design principles, practical patterns and high quality design resources (Sketch and Axure), to help people create their product prototypes beautifully and efficiently.',
-}));
-/**
- * 任务中心
- * @returns 
- */
-const Task: React.FC = () => {
-
-    const { useToken } = theme;
-    const ref = useRef(null);
-    const size = useSize(ref)
-    const { token } = useToken();
-
-    return <PageContainer>
-        <Card
-            style={{
-                borderRadius: 8,
-            }}
-            bodyStyle={{
-                paddingTop: 0,
-                paddingLeft: 0,
-                paddingRight: 0
-            }}
-            ref={ref}
-        >
-            <Space direction="vertical" style={{ width: '100%' }} size={0}>
-
-                <Affix offsetTop={56}>
-                    <div style={{ backgroundColor: token.colorBgBase, padding: token.paddingContentHorizontalLG, paddingBottom: 5, borderRadius: 8, borderBottom: `1px solid ${token.colorBorderSecondary}` }}>
-                        <Space direction="vertical" style={{ width: '100%' }}>
-                            <Space wrap>
-                                <Input prefix={<SearchOutlined />} placeholder="搜索你感兴趣的悬赏任务" style={{ width: 300 }} />
-                                <Alert
-                                    style={{ padding: `4px 12px` }}
-                                    type="warning"
-                                    showIcon
-                                    icon={<AlertOutlined />}
-                                    message="官方公告:点击任务内的素材上传按钮完成任务,即有机会获取丰厚的奖励!"
-                                />
-                            </Space>
-                            <Space>
-                                <Select
-                                    className="drawSelect"
-                                    defaultValue="taskType"
-                                    style={{ width: 100, padding: 0 }}
-                                    bordered={false}
-                                    options={[
-                                        { value: 'taskType', label: '任务分类' },
-                                        ...Object.keys(TaskTypeEnum).map(key => ({ value: key, label: (TaskTypeEnum as any)[key] }))
-                                    ]}
-                                />
-                                <Select
-                                    className="drawSelect"
-                                    defaultValue="taskStatus"
-                                    style={{ width: 100, padding: 0 }}
-                                    bordered={false}
-                                    options={[
-                                        { value: 'taskStatus', label: '任务状态' },
-                                        ...Object.keys(TaskStatusEnum).map(key => ({ value: key, label: (TaskStatusEnum as any)[key] }))
-                                    ]}
-                                />
-                                <Select
-                                    className="drawSelect"
-                                    defaultValue="time"
-                                    style={{ width: 180, padding: 0 }}
-                                    bordered={false}
-                                    options={[
-                                        { value: 'time', label: '按任务发布时间排序' },
-                                    ]}
-                                />
-                            </Space>
-                        </Space>
-                    </div>
-                </Affix>
-
-                <List
-                    itemLayout="vertical"
-                    size="large"
-                    dataSource={data}
-                    pagination={{
-                        onChange: (page) => {
-                            console.log(page);
-                        },
-                        pageSize: 10,
-                    }}
-                    renderItem={(item: any) => (
-                        <List.Item
-                            key={item.title}
-                            extra={<Flex gap={20} style={{ height: '100%' }}>
-                                <Flex style={{ width: size?.width ? `${size?.width / 2  - 200}px` : 300 }} vertical={true} align="center" justify="center">
-                                    <span>素材制作要求:素材比例、素材尺寸、素材分辨率、素材大小</span>
-                                    <Space>
-                                        <Button type="link">素材示例</Button>
-                                        <Button type="link">资源包下载</Button>
-                                    </Space>
-                                </Flex>
-                                <Flex style={{ minWidth: 100, height: '100%' }} gap={10} vertical={true} align="center" justify="center">
-                                    <Badge status="success" text="任务发布中" />
-                                    <Tag color="error">结算:总消耗2%</Tag>
-                                    <Button type="primary">提交素材</Button>
-                                </Flex>
-                            </Flex>}
-                            actions={[
-                                <span>发布时间:2023/10/01</span>,
-                                <Space>
-                                    <Tag color="success">游戏</Tag>
-                                    <Tag color="processing">小说</Tag>
-                                    <Tag color="error">error</Tag>
-                                    <Tag color="warning">warning</Tag>
-                                    <Tag color="default">default</Tag>
-                                </Space>,
-                            ]}
-                        >
-                            <List.Item.Meta
-                                avatar={<Avatar src={item.avatar} style={{ backgroundColor: '#87d068' }} />}
-                                title={<Space size={20}>
-                                    <span>{item.title}</span>
-                                    <Space size={4}><span style={{ color: 'red' }}>置顶</span>🆙</Space>
-                                </Space>}
-                                description={<Space direction="vertical" style={{ width: '100%' }} size={0}>
-                                    {/* 有效时间 */}
-                                    <Typography.Text type="secondary" ellipsis>任务有效时间:2023年10月01日~2023年10月31日</Typography.Text>
-                                    {/* 任务描述 */}
-                                    <Typography.Text type="secondary" ellipsis={{ tooltip: true }}>{item.description}</Typography.Text>
-                                </Space>}
-                            />
-                            {item.content}
-                        </List.Item>
-                    )}
-                />
-            </Space>
-        </Card>
-    </PageContainer>
-}
-
+import Material from "@/components/Material";
+import { MaterialSourceEnum, MaterialTypeEle, StatusEnum, TaskStatusEle, TaskTypeEle, TaskTypeEnum } from "@/const"
+import { getTaskMemberListApi } from "@/services/task-api/task";
+import { AlertOutlined, SearchOutlined } from "@ant-design/icons"
+import { PageContainer } from "@ant-design/pro-components"
+import { useRequest, useSize } from "ahooks";
+import { Affix, Alert, Avatar, Badge, Button, Card, Flex, Input, List, Select, Space, Tag, Tooltip, Typography, theme } from "antd"
+import { useEffect, useRef, useState } from "react";
+import SubmitTask from "./submitTask";
+
+/**
+ * 任务中心
+ * @returns 
+ */
+const Task: React.FC = () => {
+
+    /***********************************/
+    const { useToken } = theme;
+    const ref = useRef(null);
+    const size = useSize(ref)
+    const { token } = useToken();
+    const [queryForm, setQueryForm] = useState<TASKAPI.TaskMemberList>({ pageNum: 1, pageSize: 10 })
+    const getTaskMemberList = useRequest(getTaskMemberListApi, { manual: true })//添加请求
+    /***********************************/
+
+    useEffect(() => {
+        let data = JSON.parse(JSON.stringify(queryForm))
+        if (data?.taskType === 'taskType') {
+            delete data.taskType
+        }
+        if (data?.status === 'status') {
+            delete data.status
+        }
+        getTaskMemberList.run(data)
+    }, [queryForm])
+
+    return <PageContainer>
+        <Card
+            style={{
+                borderRadius: 8,
+            }}
+            bodyStyle={{
+                paddingTop: 0,
+                paddingLeft: 0,
+                paddingRight: 0
+            }}
+            ref={ref}
+        >
+            <Space direction="vertical" style={{ width: '100%' }} size={0}>
+
+                <Affix offsetTop={56}>
+                    <div style={{ backgroundColor: token.colorBgBase, padding: token.paddingContentHorizontalLG, paddingBottom: 5, borderRadius: 8, borderBottom: `1px solid ${token.colorBorderSecondary}` }}>
+                        <Space direction="vertical" style={{ width: '100%' }}>
+                            <Space wrap>
+                                <Input prefix={<SearchOutlined />} value={queryForm?.name} onChange={(e) => setQueryForm({ ...queryForm, name: e.target.value, pageNum: 1 })} placeholder="搜索你感兴趣的悬赏任务" style={{ width: 300 }} />
+                                <Alert
+                                    style={{ padding: `4px 12px` }}
+                                    type="warning"
+                                    showIcon
+                                    icon={<AlertOutlined />}
+                                    message="官方公告:点击任务内的素材上传按钮完成任务,即有机会获取丰厚的奖励!"
+                                />
+                            </Space>
+                            <Space>
+                                <Select
+                                    className="drawSelect"
+                                    style={{ width: 100, padding: 0 }}
+                                    bordered={false}
+                                    allowClear
+                                    placeholder="任务分类"
+                                    value={queryForm?.taskType}
+                                    onChange={(e) => setQueryForm({ ...queryForm, taskType: e, pageNum: 1 })}
+                                    options={Object.keys(TaskTypeEnum).map(key => ({ value: key, label: (TaskTypeEnum as any)[key] }))}
+                                />
+                                <Select
+                                    className="drawSelect"
+                                    style={{ width: 100, padding: 0 }}
+                                    bordered={false}
+                                    allowClear
+                                    value={queryForm?.status}
+                                    placeholder="任务状态"
+                                    onChange={(e) => setQueryForm({ ...queryForm, status: e, pageNum: 1 })}
+                                    options={Object.keys(StatusEnum).map(key => ({ value: key, label: (StatusEnum as any)[key] }))}
+                                />
+                            </Space>
+                        </Space>
+                    </div>
+                </Affix>
+
+                <List
+                    itemLayout="vertical"
+                    size="large"
+                    dataSource={getTaskMemberList.data?.data?.records}
+                    loading={getTaskMemberList.loading}
+                    pagination={{
+                        onChange: (page) => {
+                            console.log(page);
+                            setQueryForm({ ...queryForm, pageNum: page })
+                        },
+                        current: queryForm.pageNum,
+                        pageSize: queryForm.pageSize,
+                    }}
+                    renderItem={(item: any, index) => (
+                        <List.Item
+                            key={item.title}
+                            extra={<Flex gap={20} style={{ height: '100%' }}>
+                                <Flex style={{ width: size?.width ? `${size?.width / 2 - 265}px` : 300 }} vertical={true} align="flex-start" justify="space-between">
+                                    <Material items={item?.materialExample?.split(',')} resourceUrl={item.materialResource} claimJson={item.materialClaimJson} />
+                                </Flex>
+                                <Flex style={{ width: 165, height: '100%' }} gap={10} vertical={true} align="center" justify="center">
+                                    {TaskStatusEle[item.status]}
+                                    <Tag color="error">结算:{item.checkoutType === 'CHECKOUT_TYPE_SCALE' ? `总消耗 ${item.checkout * 100}%` : `${item.checkout}元(审核合格)`}</Tag>
+                                    <SubmitTask taskId={item.id} name={item.name} materialType={item.materialType} materialClaimJson={item.materialClaimJson} onChange={() => getTaskMemberList.refresh()}/>
+                                </Flex>
+                            </Flex>}
+                            actions={[
+                                <Tooltip title='发布时间'><span>{item.createTime}</span></Tooltip>,
+                                <Tooltip title='任务分类'>{TaskTypeEle[item.taskType]}</Tooltip>,
+                                <Tooltip title='素材类型'>{MaterialTypeEle[item.materialType]}</Tooltip>,
+                                <Tooltip title='素材来源要求'><Tag>{(MaterialSourceEnum as any)[item.materialSource]}</Tag></Tooltip>
+                            ]}
+                        >
+                            <List.Item.Meta
+                                avatar={<Avatar src={item.avatar || `https://xsgames.co/randomusers/avatar.php?g=pixel&key=${index}`} style={{ backgroundColor: '#87d068' }} />}
+                                title={<Space size={20}>
+                                    <Typography.Title level={5} style={{ marginBottom: 0 }}>{item.name}</Typography.Title>
+                                    {!!item.urgency && <Space size={4}><span style={{ color: 'red' }}>置顶</span>🆙</Space>}
+                                </Space>}
+                                description={<Space direction="vertical" style={{ width: '100%' }} size={0}>
+                                    {/* 有效时间 */}
+                                    <Typography.Text type="secondary" ellipsis>任务有效时间:{item.startTime}~{item.endTime ? item.endTime : '长期有效'}</Typography.Text>
+                                    {/* 任务描述 */}
+                                    {item.remark && <Typography.Text type="secondary" ellipsis={{ tooltip: true }}>{item.remark}</Typography.Text>}
+                                </Space>}
+                            />
+                            <Typography.Paragraph ellipsis={{ rows: 3, tooltip: true }} style={{ marginBottom: 0 }}>{item.remarkMore}</Typography.Paragraph>
+                        </List.Item>
+                    )}
+                />
+            </Space>
+        </Card>
+    </PageContainer>
+}
+
 export default Task
 export default Task

+ 112 - 0
src/pages/Task/submitTask.tsx

@@ -0,0 +1,112 @@
+import { MaterialTypeEnum } from "@/const"
+import { App, Button, Form, Modal, Popconfirm, Space, message } from "antd"
+import React, { useMemo, useState } from "react"
+import Video from "./video"
+import { MinusCircleOutlined, PlusOutlined } from "@ant-design/icons"
+import { useRequest } from "ahooks"
+import { addMaterialApi } from "@/services/task-api/task"
+
+interface Props {
+    taskId: number,
+    name: string
+    materialType: MaterialTypeEnum,
+    materialClaimJson: string
+    onChange?: () => void
+}
+/**
+ * 提交素材
+ * @returns 
+ */
+const SubmitTask: React.FC<Props> = ({ taskId, name, materialType, materialClaimJson, onChange }) => {
+
+    /*********************************/
+    const [form] = Form.useForm<{ materialBeanList: TASKAPI.MaterialBean[] }>()
+    const [visible, setVisible] = useState<boolean>(false)
+    const addMaterial = useRequest(addMaterialApi, { manual: true })
+    /*********************************/
+
+    const hanldeOk = async () => {
+        form.submit()
+        let data = await form.validateFields()
+        console.log(data)
+        addMaterial.runAsync({ ...data, taskId, materialType }).then(res => {
+            if (res?.data) {
+                message.success('提交成功')
+                setVisible(false)
+                form.resetFields()
+                onChange?.()
+            }
+        })
+    }
+
+    const typeEle = useMemo(() => {
+        switch (materialType) {
+            // 视频提交
+            case 'MATERIAL_TYPE_VIDEO' as MaterialTypeEnum:
+                return <Video materialClaimJson={materialClaimJson} />
+            default:
+                return null
+        }
+    }, [materialType, materialClaimJson])
+
+    return <>
+        <Button type="primary" onClick={() => { setVisible(true) }}>提交素材</Button>
+        {visible && <Modal
+            title={`提交${(MaterialTypeEnum as any)[materialType]}素材到 ${name}`}
+            open={visible}
+            onCancel={() => setVisible(false)}
+            onOk={hanldeOk}
+        >
+            <Form
+                name="submitTaskModal"
+                form={form}
+                initialValues={{
+                    materialBeanList: [
+                        {}
+                        // {
+                        //     md5: '9c7a0c1350f9d096b2fdfae1b38051f0',
+                        //     size: 10005155,
+                        //     url: "https://zx-material-center-test.oss-cn-hangzhou.aliyuncs.com/video/14A37EB1B19641CEB337DBBBC58A7B60.mp4"
+
+                        // }, {
+                        //     md5: "b18584adcedff777f2c35685390209af",
+                        //     size: 2300583,
+                        //     url: "https://zx-material-center-test.oss-cn-hangzhou.aliyuncs.com/video/1616F0AB21DE4F088D70A12FBB3F6C23.mp4"
+                        // }
+                    ]
+                }}
+            >
+                <Form.List name="materialBeanList">
+                    {(fields, { add, remove }) => {
+                        return <>
+                            {fields.map(({ key, name, ...restField }) => (
+                                <Space key={key} style={{ display: 'flex', marginBottom: 8 }} align="center">
+                                    <Form.Item
+                                        {...restField}
+                                        name={[name]}
+                                        rules={[{ required: true, message: '请上传素材' }]}
+                                    >
+                                        {typeEle}
+                                    </Form.Item>
+                                    {fields?.length > 1 && <Popconfirm
+                                        title="确定删除?"
+                                        onConfirm={() => remove(name)}
+                                    >
+                                        <MinusCircleOutlined style={{ color: 'red' }} />
+                                    </Popconfirm>}
+                                </Space>
+                            ))}
+                            <Form.Item>
+                                <Button type="dashed" onClick={() => add()} block icon={<PlusOutlined />}>
+                                    新增素材
+                                </Button>
+                            </Form.Item>
+                        </>
+                    }}
+                </Form.List>
+            </Form>
+        </Modal>}
+    </>
+}
+
+export default React.memo(SubmitTask)

+ 114 - 0
src/pages/Task/video.tsx

@@ -0,0 +1,114 @@
+import { message, Space, Upload, Spin } from "antd"
+import { InboxOutlined } from "@ant-design/icons";
+import { RcFile } from "antd/lib/upload";
+import React, { useState } from "react";
+import './index.less'
+import { getOssInfo } from "@/services/ant-design-pro/api";
+import { request } from "@umijs/max";
+import { getMD5, videoMessage } from "@/utils";
+import VideoNews from "@/components/VideoNews";
+
+interface Props {
+    materialClaimJson: string,
+    value?: TASKAPI.MaterialBean,
+    onChange?: (value?: TASKAPI.MaterialBean) => void
+}
+/**
+ * 视频上传
+ * @returns 
+ */
+const Video: React.FC<Props> = ({ value, onChange, materialClaimJson }) => {
+    /** 变量START */
+    const [videoFile, setVideoFile] = useState<TASKAPI.MaterialBean | undefined>(value);
+    const [loading, setLoading] = useState<boolean>(false)
+    const { size, extent } = JSON.parse(materialClaimJson)
+    /** 变量END */
+
+    const beforeUpload = async (file: RcFile) => {
+        console.log('file--->', file)
+        const isZipOrRar = file.type === 'video/mp4';
+        if (!isZipOrRar) {
+            message.error('您只能上传MP4文件!');
+        }
+        const [minSize, maxSize] = extent
+        const fileSizeM = file.size / 1024 / 1024
+        const isLt20M = fileSizeM >= minSize && fileSizeM <= maxSize;
+        if (!isLt20M) {
+            message.error(`视频大小必须在${minSize}MB ~ ${maxSize}MB区间!`);
+        }
+        let videoInfo = await videoMessage([file])
+        const { width = 0, height = 0 } = videoInfo[0]
+        const [w, h] = size
+        const isWHCorrect = Number(width) === Number(w) && Number(h) === Number(height)
+        if (!isWHCorrect) {
+            message.error(`视频宽高必须${w} * ${h}!`);
+        }
+        return isZipOrRar && isLt20M && isWHCorrect;
+    };
+
+    return <div className="myUploadVideo">
+        <Space align="start">
+            <Upload.Dragger
+                name="file"
+                accept='video/mp4'
+                className="avatar-uploader"
+                beforeUpload={beforeUpload}
+                maxCount={1}
+                showUploadList={false}
+                disabled={loading}
+                customRequest={(options: any) => {
+                    setLoading(true)
+                    getOssInfo({ type: 'video/mp4', fileType: 'video' }).then(async res => {
+                        try {
+                            let formData = new FormData();
+                            Object.keys(res.data).forEach((key: string) => {
+                                if (key !== 'url') {
+                                    formData.append(key, res.data[key])
+                                }
+                            })
+                            formData.append('file', options.file)
+                            const md5 = await getMD5(options.file)
+                            let urlData = await request(res?.data?.ossUrl, { method: 'POST', data: formData })
+                            setLoading(false)
+                            if (urlData?.data?.url) {
+                                console.log('=====>', {
+                                    url: urlData?.data?.url,
+                                    size: options.file.size,
+                                    md5
+                                })
+                                setVideoFile({
+                                    url: urlData?.data?.url,
+                                    size: options.file.size,
+                                    md5
+                                });
+                                onChange?.({
+                                    url: urlData?.data?.url,
+                                    size: options.file.size,
+                                    md5
+                                })
+                            }
+                        } catch (error) {
+                            setLoading(false)
+                        }
+                    })
+                }}
+            >
+                <Spin spinning={loading}>
+                    {videoFile?.url ? <div className="video-content">
+                        <VideoNews src={videoFile?.url}/>
+                    </div> : <>
+                        <p className="ant-upload-drag-icon">
+                            <InboxOutlined />
+                        </p>
+                        <p className="ant-upload-text">单击或拖动视频到此区域进行上传</p>
+                        <p className="ant-upload-hint" style={{ width: 450 }}>
+                            素材要求:素材尺寸:{size?.[0]} * {size?.[1]},素材大小:{extent?.[0]}MB ~ {extent?.[1]}MB
+                        </p>
+                    </>}
+                </Spin>
+            </Upload.Dragger>
+        </Space>
+    </div>
+}
+
+export default React.memo(Video)

+ 275 - 272
src/pages/User/Login/index.tsx

@@ -1,272 +1,275 @@
-import Footer from '@/components/Footer';
-import { login } from '@/services/ant-design-pro/api';
-import { getFakeCaptcha } from '@/services/ant-design-pro/login';
-import {
-    LockOutlined,
-    MobileOutlined,
-    UserOutlined,
-} from '@ant-design/icons';
-import {
-    LoginForm,
-    ProFormCaptcha,
-    ProFormCheckbox,
-    ProFormText,
-} from '@ant-design/pro-components';
-import { useEmotionCss } from '@ant-design/use-emotion-css';
-import { FormattedMessage, history, useIntl, useModel, Helmet } from '@umijs/max';
-import { Alert, message, Tabs } from 'antd';
-import Settings from '../../../../config/defaultSettings';
-import React, { useState } from 'react';
-import { flushSync } from 'react-dom';
-
-
-const LoginMessage: React.FC<{
-    content: string;
-}> = ({ content }) => {
-    return (
-        <Alert
-            style={{
-                marginBottom: 24,
-            }}
-            message={content}
-            type="error"
-            showIcon
-        />
-    );
-};
-
-const Login: React.FC = () => {
-    const [userLoginState, setUserLoginState] = useState<API.Result>({});
-    const [type, setType] = useState<string>('mobile');
-    const { initialState, setInitialState } = useModel('@@initialState');
-
-    const containerClassName = useEmotionCss(() => {
-        return {
-            display: 'flex',
-            flexDirection: 'column',
-            height: '100vh',
-            overflow: 'auto',
-            backgroundImage:
-                "url('https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/V-_oS6r-i7wAAAAAAAAAAAAAFl94AQBr')",
-            backgroundSize: '100% 100%',
-        };
-    });
-
-    const intl = useIntl();
-
-    const fetchUserInfo = async () => {
-        const userInfo = await initialState?.fetchUserInfo?.();
-        if (userInfo) {
-            console.log('userInfo--->', userInfo)
-            flushSync(() => {
-                setInitialState((s) => ({
-                    ...s,
-                    currentUser: userInfo,
-                }));
-            });
-        }
-    };
-
-    const handleSubmit = async (values: API.LoginParams) => {
-        try {
-            const { savePhone, ...value } = values
-            // 登录
-            const msg = await login({ ...value });
-            if (msg.success) {
-                localStorage.setItem('Admin-Token', msg?.data?.token)
-                const defaultLoginSuccessMessage = intl.formatMessage({
-                    id: 'pages.login.success',
-                    defaultMessage: '登录成功!',
-                });
-                if (savePhone) {
-                    localStorage.setItem('MOBILE', values.mobile as string)
-                }
-                message.success(defaultLoginSuccessMessage);
-                await fetchUserInfo();
-                const urlParams = new URL(window.location.href).searchParams;
-                history.push(urlParams.get('redirect') || '/');
-                return;
-            }
-            // 如果失败去设置用户错误信息
-            setUserLoginState(msg);
-        } catch (error) {
-            const defaultLoginFailureMessage = intl.formatMessage({
-                id: 'pages.login.failure',
-                defaultMessage: '登录失败,请重试!',
-            });
-            console.log(error);
-            message.error(defaultLoginFailureMessage);
-        }
-    };
-    const { success = true } = userLoginState;
-
-    return (
-        <div className={containerClassName}>
-            <Helmet>
-                <title>
-                    {intl.formatMessage({
-                        id: 'menu.login',
-                        defaultMessage: '登录页',
-                    })}
-                    - {Settings.title}
-                </title>
-            </Helmet>
-            <div
-                style={{
-                    flex: '1',
-                    padding: '32px 0',
-                }}
-            >
-                <LoginForm
-                    contentStyle={{
-                        minWidth: 280,
-                        maxWidth: '75vw',
-                    }}
-                    // logo={<img alt="logo" src="/logo.svg" />}
-                    title="趣程素材库"
-                    subTitle={<span style={{ display: 'inline-block', height: 16 }}>{/** intl.formatMessage({ id: 'pages.layouts.userLayout.title' }) */}</span>}
-                    initialValues={{
-                        savePhone: true,
-                        mobile: localStorage.getItem('MOBILE') || null
-                    }}
-                    onFinish={async (values) => {
-                        await handleSubmit(values as API.LoginParams);
-                    }}
-
-                >
-                    {/* <Tabs
-                        activeKey={type}
-                        onChange={setType}
-                        centered
-                        items={[
-                            {
-                                key: 'account',
-                                label: '账户密码登录',
-                            },
-                            {
-                                key: 'mobile',
-                                label: '手机号登录',
-                            },
-                        ]}
-                    /> */}
-
-                    {!success && type === 'account' && (
-                        <LoginMessage content={'账户或密码错误'} />
-                    )}
-                    {type === 'account' && (
-                        <>
-                            <ProFormText
-                                name="username"
-                                fieldProps={{
-                                    size: 'large',
-                                    prefix: <UserOutlined />,
-                                }}
-                                placeholder={intl.formatMessage({
-                                    id: 'pages.login.username.placeholder',
-                                    defaultMessage: '用户名: admin or user',
-                                })}
-                                rules={[
-                                    {
-                                        required: true,
-                                        message: (
-                                            <FormattedMessage
-                                                id="pages.login.username.required"
-                                                defaultMessage="请输入用户名!"
-                                            />
-                                        ),
-                                    },
-                                ]}
-                            />
-                            <ProFormText.Password
-                                name="password"
-                                fieldProps={{
-                                    size: 'large',
-                                    prefix: <LockOutlined />,
-                                }}
-                                placeholder={intl.formatMessage({
-                                    id: 'pages.login.password.placeholder',
-                                    defaultMessage: '密码: ant.design',
-                                })}
-                                rules={[
-                                    {
-                                        required: true,
-                                        message: (
-                                            <FormattedMessage
-                                                id="pages.login.password.required"
-                                                defaultMessage="请输入密码!"
-                                            />
-                                        ),
-                                    },
-                                ]}
-                            />
-                        </>
-                    )}
-
-                    {!success && type === 'mobile' && <LoginMessage content="验证码错误" />}
-                    {type === 'mobile' && (
-                        <>
-                            <ProFormText
-                                fieldProps={{
-                                    size: 'large',
-                                    prefix: <MobileOutlined />,
-                                }}
-                                name="mobile"
-                                placeholder='手机号'
-                                rules={[
-                                    {
-                                        required: true,
-                                        message: "请输入手机号!",
-                                    },
-                                    {
-                                        pattern: /^1\d{10}$/,
-                                        message: "手机号格式错误!",
-                                    },
-                                ]}
-                            />
-                            <ProFormCaptcha
-                                fieldProps={{
-                                    size: 'large',
-                                    prefix: <LockOutlined />,
-                                }}
-                                phoneName="mobile"
-                                captchaProps={{
-                                    size: 'large',
-                                }}
-                                placeholder='请输入验证码'
-                                captchaTextRender={(timing, count) => {
-                                    if (timing) {
-                                        return `${count} 获取验证码`;
-                                    }
-                                    return '获取验证码';
-                                }}
-                                name="code"
-                                rules={[
-                                    {
-                                        required: true,
-                                        message: "请输入验证码!",
-                                    },
-                                ]}
-                                onGetCaptcha={async (mobile) => {
-                                    const result: any = await getFakeCaptcha({
-                                        mobile,
-                                        smsType: 'SMS_REG_LOGIN'
-                                    });
-                                    if (!result?.success) {
-                                        throw new Error();
-                                    }
-                                    message.success('获取验证码成功!');
-                                }}
-                            />
-                        </>
-                    )}
-                    <div style={{ marginBottom: 24 }} >
-                        <ProFormCheckbox noStyle name="savePhone">保存手机号</ProFormCheckbox>
-                    </div>
-                </LoginForm>
-            </div>
-            <Footer />
-        </div>
-    );
-};
-
-export default Login;
+import Footer from '@/components/Footer';
+import { login } from '@/services/ant-design-pro/api';
+import { getFakeCaptcha } from '@/services/ant-design-pro/login';
+import {
+    LockOutlined,
+    MobileOutlined,
+    UserOutlined,
+} from '@ant-design/icons';
+import {
+    LoginForm,
+    ProFormCaptcha,
+    ProFormCheckbox,
+    ProFormText,
+} from '@ant-design/pro-components';
+import { useEmotionCss } from '@ant-design/use-emotion-css';
+import { FormattedMessage, history, useIntl, useModel, Helmet } from '@umijs/max';
+import { Alert, message, Tabs } from 'antd';
+import Settings from '../../../../config/defaultSettings';
+import React, { useState } from 'react';
+import { flushSync } from 'react-dom';
+
+
+const LoginMessage: React.FC<{
+    content: string;
+}> = ({ content }) => {
+    return (
+        <Alert
+            style={{
+                marginBottom: 24,
+            }}
+            message={content}
+            type="error"
+            showIcon
+        />
+    );
+};
+
+const Login: React.FC = () => {
+    const [userLoginState, setUserLoginState] = useState<API.Result>({});
+    const [type, setType] = useState<string>('mobile');
+    const { initialState, setInitialState } = useModel('@@initialState');
+
+    const containerClassName = useEmotionCss(() => {
+        return {
+            display: 'flex',
+            flexDirection: 'column',
+            height: '100vh',
+            overflow: 'auto',
+            backgroundImage:
+                "url('https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/V-_oS6r-i7wAAAAAAAAAAAAAFl94AQBr')",
+            backgroundSize: '100% 100%',
+        };
+    });
+
+    const intl = useIntl();
+
+    const fetchUserInfo = async () => {
+        const userInfo = await initialState?.fetchUserInfo?.();
+        if (userInfo) {
+            console.log('userInfo--->', userInfo)
+            flushSync(() => {
+                setInitialState((s) => ({
+                    ...s,
+                    currentUser: {
+                        avatar: `https://xsgames.co/randomusers/avatar.php?g=pixel&key=${Math.floor(Math.random() * 10)}`,
+                        ...userInfo
+                    },
+                }));
+            });
+        }
+    };
+
+    const handleSubmit = async (values: API.LoginParams) => {
+        try {
+            const { savePhone, ...value } = values
+            // 登录
+            const msg = await login({ ...value });
+            if (msg.success) {
+                localStorage.setItem('Admin-Token', msg?.data?.token)
+                const defaultLoginSuccessMessage = intl.formatMessage({
+                    id: 'pages.login.success',
+                    defaultMessage: '登录成功!',
+                });
+                if (savePhone) {
+                    localStorage.setItem('MOBILE', values.mobile as string)
+                }
+                message.success(defaultLoginSuccessMessage);
+                await fetchUserInfo();
+                const urlParams = new URL(window.location.href).searchParams;
+                history.push(urlParams.get('redirect') || '/');
+                return;
+            }
+            // 如果失败去设置用户错误信息
+            setUserLoginState(msg);
+        } catch (error) {
+            const defaultLoginFailureMessage = intl.formatMessage({
+                id: 'pages.login.failure',
+                defaultMessage: '登录失败,请重试!',
+            });
+            console.log(error);
+            message.error(defaultLoginFailureMessage);
+        }
+    };
+    const { success = true } = userLoginState;
+
+    return (
+        <div className={containerClassName}>
+            <Helmet>
+                <title>
+                    {intl.formatMessage({
+                        id: 'menu.login',
+                        defaultMessage: '登录页',
+                    })}
+                    - {Settings.title}
+                </title>
+            </Helmet>
+            <div
+                style={{
+                    flex: '1',
+                    padding: '32px 0',
+                }}
+            >
+                <LoginForm
+                    contentStyle={{
+                        minWidth: 280,
+                        maxWidth: '75vw',
+                    }}
+                    // logo={<img alt="logo" src="/logo.svg" />}
+                    title="趣程素材库"
+                    subTitle={<span style={{ display: 'inline-block', height: 16 }}>{/** intl.formatMessage({ id: 'pages.layouts.userLayout.title' }) */}</span>}
+                    initialValues={{
+                        savePhone: true,
+                        mobile: localStorage.getItem('MOBILE') || null
+                    }}
+                    onFinish={async (values) => {
+                        await handleSubmit(values as API.LoginParams);
+                    }}
+
+                >
+                    {/* <Tabs
+                        activeKey={type}
+                        onChange={setType}
+                        centered
+                        items={[
+                            {
+                                key: 'account',
+                                label: '账户密码登录',
+                            },
+                            {
+                                key: 'mobile',
+                                label: '手机号登录',
+                            },
+                        ]}
+                    /> */}
+
+                    {!success && type === 'account' && (
+                        <LoginMessage content={'账户或密码错误'} />
+                    )}
+                    {type === 'account' && (
+                        <>
+                            <ProFormText
+                                name="username"
+                                fieldProps={{
+                                    size: 'large',
+                                    prefix: <UserOutlined />,
+                                }}
+                                placeholder={intl.formatMessage({
+                                    id: 'pages.login.username.placeholder',
+                                    defaultMessage: '用户名: admin or user',
+                                })}
+                                rules={[
+                                    {
+                                        required: true,
+                                        message: (
+                                            <FormattedMessage
+                                                id="pages.login.username.required"
+                                                defaultMessage="请输入用户名!"
+                                            />
+                                        ),
+                                    },
+                                ]}
+                            />
+                            <ProFormText.Password
+                                name="password"
+                                fieldProps={{
+                                    size: 'large',
+                                    prefix: <LockOutlined />,
+                                }}
+                                placeholder={intl.formatMessage({
+                                    id: 'pages.login.password.placeholder',
+                                    defaultMessage: '密码: ant.design',
+                                })}
+                                rules={[
+                                    {
+                                        required: true,
+                                        message: (
+                                            <FormattedMessage
+                                                id="pages.login.password.required"
+                                                defaultMessage="请输入密码!"
+                                            />
+                                        ),
+                                    },
+                                ]}
+                            />
+                        </>
+                    )}
+
+                    {!success && type === 'mobile' && <LoginMessage content="验证码错误" />}
+                    {type === 'mobile' && (
+                        <>
+                            <ProFormText
+                                fieldProps={{
+                                    size: 'large',
+                                    prefix: <MobileOutlined />,
+                                }}
+                                name="mobile"
+                                placeholder='手机号'
+                                rules={[
+                                    {
+                                        required: true,
+                                        message: "请输入手机号!",
+                                    },
+                                    {
+                                        pattern: /^1\d{10}$/,
+                                        message: "手机号格式错误!",
+                                    },
+                                ]}
+                            />
+                            <ProFormCaptcha
+                                fieldProps={{
+                                    size: 'large',
+                                    prefix: <LockOutlined />,
+                                }}
+                                phoneName="mobile"
+                                captchaProps={{
+                                    size: 'large',
+                                }}
+                                placeholder='请输入验证码'
+                                captchaTextRender={(timing, count) => {
+                                    if (timing) {
+                                        return `${count} 获取验证码`;
+                                    }
+                                    return '获取验证码';
+                                }}
+                                name="code"
+                                rules={[
+                                    {
+                                        required: true,
+                                        message: "请输入验证码!",
+                                    },
+                                ]}
+                                onGetCaptcha={async (mobile) => {
+                                    const result: any = await getFakeCaptcha({
+                                        mobile,
+                                        smsType: 'SMS_REG_LOGIN'
+                                    });
+                                    if (!result?.success) {
+                                        throw new Error();
+                                    }
+                                    message.success('获取验证码成功!');
+                                }}
+                            />
+                        </>
+                    )}
+                    <div style={{ marginBottom: 24 }} >
+                        <ProFormCheckbox noStyle name="savePhone">保存手机号</ProFormCheckbox>
+                    </div>
+                </LoginForm>
+            </div>
+            <Footer />
+        </div>
+    );
+};
+
+export default Login;

+ 141 - 141
src/requestErrorConfig.ts

@@ -1,141 +1,141 @@
-import type { RequestOptions } from '@@/plugin-request/request';
-import type { RequestConfig } from '@umijs/max';
-import { message, notification } from 'antd';
-import { history } from '@umijs/max';
-
-// 与后端约定的响应数据格式
-interface ResponseStructure {
-  success: boolean;
-  data: any;
-  code: number;
-  msg: string;
-  fail: boolean
-}
-
-const codeMessage: any = {
-  200: '服务器成功返回请求的数据。',
-  201: '新建或修改数据成功。',
-  202: '一个请求已经进入后台排队(异步任务)。',
-  204: '删除数据成功。',
-  400: '发出的请求有错误,服务器没有进行新建或修改数据的操作。',
-  401: '用户没有权限(令牌、用户名、密码错误)。',
-  403: '用户得到授权,但是访问是被禁止的。',
-  404: '发出的请求针对的是不存在的记录,服务器没有进行操作。',
-  405: '请求方法不被允许。',
-  406: '请求的格式不可得。',
-  410: '请求的资源被永久删除,且不会再得到的。',
-  422: '当创建一个对象时,发生一个验证错误。',
-  500: '服务器发生错误,请检查服务器。',
-  502: '网关错误。',
-  503: '服务不可用,服务器暂时过载或维护。',
-  504: '网关超时。',
-};
-
-/**
- * @name 错误处理
- * pro 自带的错误处理, 可以在这里做自己的改动
- * @doc https://umijs.org/docs/max/request#配置
- */
-export const errorConfig: RequestConfig = {
-  // 错误处理: umi@3 的错误处理方案。
-  errorConfig: {
-    // 错误抛出
-    errorThrower: (res) => {
-      const { success, data, code, msg, fail } = res as unknown as ResponseStructure;
-      if (!success) {
-        let error: any = new Error(msg);
-        error.name = 'BizError';
-        error.info = { code, msg, fail, data };
-        throw error; // 抛出自制的错误
-      }
-    },
-    // 错误接收及处理
-    errorHandler: (error: any, opts: any) => {
-      const { response } = error;
-      if (response && response.status) {
-        const errorText = codeMessage[(response as any).status] || response.statusText;
-        const { status, data } = response;
-        notification.error({
-          message: `请求错误 ${status}: ${data}`,
-          description: errorText,
-        });
-      }
-
-      if (!response) {
-        notification.error({
-          description: '您的网络发生异常,无法连接服务器',
-          message: '网络异常',
-        });
-      }
-      throw error;
-    },
-  },
-
-  // 请求拦截器
-  requestInterceptors: [
-    (config: RequestOptions) => {
-      // 拦截请求配置,进行个性化处理。
-      const url = config?.url;
-      if (config?.headers) {
-        config.headers['token'] = localStorage.getItem('Admin-Token') || ''
-      }
-      return { ...config, url };
-    },
-  ],
-
-  // 响应拦截器
-  responseInterceptors: [
-    (response) => {
-      // 拦截响应数据,进行个性化处理
-      const { data } = response as unknown as ResponseStructure;
-      if (data?.success === false) {
-        let msg: any
-        switch (data?.code) {
-          case 500:
-            msg = sessionStorage.getItem('msg')
-            if (data.msg === '令牌验证失败') {//
-              localStorage.removeItem('Admin-Token')
-              if (!msg) {
-                sessionStorage.setItem('msg', 'true')
-                message.error({
-                  content: data?.msg,
-                  onClose: () => {
-                    localStorage.removeItem('Admin-Token')
-                    history.push('/user/login')
-                    sessionStorage.removeItem('msg')
-                  }
-                });
-              }
-            }
-            if (!msg) {
-              sessionStorage.setItem('msg', 'true')
-              message.error({
-                content: data.msg,
-                onClose: () => {
-                  sessionStorage.removeItem('msg')
-                }
-              });
-            }
-            break
-          case 310:
-            msg = sessionStorage.getItem('msg')
-            localStorage.removeItem('Admin-Token')
-            history.push('/user/login')
-            if (!msg) {
-              sessionStorage.setItem('msg', 'true')
-              notification.error({
-                message: data?.msg,
-                onClose: () => {
-                  sessionStorage.removeItem('msg')
-                }
-              });
-            }
-            break
-          default:
-            message.error(data?.msg || '请求失败!');
-        }
-      }
-      return response;
-    },
-  ],
-};
+import type { RequestOptions } from '@@/plugin-request/request';
+import type { RequestConfig } from '@umijs/max';
+import { message, notification } from 'antd';
+import { history } from '@umijs/max';
+
+// 与后端约定的响应数据格式
+interface ResponseStructure {
+  success: boolean;
+  data: any;
+  code: number;
+  msg: string;
+  fail: boolean
+}
+
+const codeMessage: any = {
+  200: '服务器成功返回请求的数据。',
+  201: '新建或修改数据成功。',
+  202: '一个请求已经进入后台排队(异步任务)。',
+  204: '删除数据成功。',
+  400: '发出的请求有错误,服务器没有进行新建或修改数据的操作。',
+  401: '用户没有权限(令牌、用户名、密码错误)。',
+  403: '用户得到授权,但是访问是被禁止的。',
+  404: '发出的请求针对的是不存在的记录,服务器没有进行操作。',
+  405: '请求方法不被允许。',
+  406: '请求的格式不可得。',
+  410: '请求的资源被永久删除,且不会再得到的。',
+  422: '当创建一个对象时,发生一个验证错误。',
+  500: '服务器发生错误,请检查服务器。',
+  502: '网关错误。',
+  503: '服务不可用,服务器暂时过载或维护。',
+  504: '网关超时。',
+};
+
+/**
+ * @name 错误处理
+ * pro 自带的错误处理, 可以在这里做自己的改动
+ * @doc https://umijs.org/docs/max/request#配置
+ */
+export const errorConfig: RequestConfig = {
+  // 错误处理: umi@3 的错误处理方案。
+  errorConfig: {
+    // 错误抛出
+    // errorThrower: (res) => {
+    //   const { success, data, code, msg, fail } = res as unknown as ResponseStructure;
+    //   if (!success) {
+    //     let error: any = new Error(msg);
+    //     error.name = 'BizError';
+    //     error.info = { code, msg, fail, data };
+    //     throw error; // 抛出自制的错误
+    //   }
+    // },
+    // // 错误接收及处理
+    errorHandler: (error: any, opts: any) => {
+      const { response } = error;
+      if (response && response.status) {
+        const errorText = codeMessage[(response as any).status] || response.statusText;
+        const { status, path } = response;
+        notification.error({
+          message: `请求错误 ${status}: ${path}`,
+          description: errorText,
+        });
+      }
+
+      if (!response) {
+        notification.error({
+          description: '您的网络发生异常,无法连接服务器',
+          message: '网络异常',
+        });
+      }
+      throw error;
+    },
+  },
+
+  // 请求拦截器
+  requestInterceptors: [
+    (config: RequestOptions) => {
+      // 拦截请求配置,进行个性化处理。
+      const url = config?.url;
+      if (config?.headers) {
+        config.headers['token'] = localStorage.getItem('Admin-Token') || ''
+      }
+      return { ...config, url };
+    },
+  ],
+
+  // 响应拦截器
+  responseInterceptors: [
+    (response) => {
+      // 拦截响应数据,进行个性化处理
+      const { data } = response as unknown as ResponseStructure;
+      if (data?.success === false) {
+        let msg: any
+        switch (data?.code) {
+          case 500:
+            msg = sessionStorage.getItem('msg')
+            if (data.msg === '令牌验证失败') {//
+              localStorage.removeItem('Admin-Token')
+              if (!msg) {
+                sessionStorage.setItem('msg', 'true')
+                message.error({
+                  content: data?.msg,
+                  onClose: () => {
+                    localStorage.removeItem('Admin-Token')
+                    history.push('/user/login')
+                    sessionStorage.removeItem('msg')
+                  }
+                });
+              }
+            }
+            if (!msg) {
+              sessionStorage.setItem('msg', 'true')
+              message.error({
+                content: data.msg,
+                onClose: () => {
+                  sessionStorage.removeItem('msg')
+                }
+              });
+            }
+            break
+          case 310:
+            msg = sessionStorage.getItem('msg')
+            localStorage.removeItem('Admin-Token')
+            history.push('/user/login')
+            if (!msg) {
+              sessionStorage.setItem('msg', 'true')
+              notification.error({
+                message: data?.msg,
+                onClose: () => {
+                  sessionStorage.removeItem('msg')
+                }
+              });
+            }
+            break
+          default:
+            message.error(data?.msg || '请求失败!');
+        }
+      }
+      return response;
+    },
+  ],
+};

+ 93 - 92
src/services/ant-design-pro/typings.d.ts

@@ -1,92 +1,93 @@
-// @ts-ignore
-/* eslint-disable */
-
-declare namespace API {
-  type CurrentUser = {
-    mobile?: string;
-    role?: string;
-    userid?: string;
-    token?: string;
-    userName?: string;
-  };
-
-  type Result = {
-    code?: number
-    data?: any
-    fail?: boolean;
-    msg?: string;
-    success?: boolean;
-  };
-
-  type PageParams = {
-    current?: number;
-    pageSize?: number;
-  };
-
-  type RuleListItem = {
-    key?: number;
-    disabled?: boolean;
-    href?: string;
-    avatar?: string;
-    name?: string;
-    owner?: string;
-    desc?: string;
-    callNo?: number;
-    status?: number;
-    updatedAt?: string;
-    createdAt?: string;
-    progress?: number;
-  };
-
-  type RuleList = {
-    data?: RuleListItem[];
-    /** 列表的内容总数 */
-    total?: number;
-    success?: boolean;
-  };
-
-  type FakeCaptcha = {
-    code?: number;
-    status?: string;
-  };
-
-  type LoginParams = {
-    username?: string;
-    password?: string;
-    mobile?: string;
-    code?: string;
-    savePhone?: boolean;
-    type?: string;
-  };
-
-  type ErrorResponse = {
-    /** 业务约定的错误码 */
-    errorCode: string;
-    /** 业务上的错误信息 */
-    errorMessage?: string;
-    /** 业务上的请求是否成功 */
-    success?: boolean;
-  };
-
-  type NoticeIconList = {
-    data?: NoticeIconItem[];
-    /** 列表的内容总数 */
-    total?: number;
-    success?: boolean;
-  };
-
-  type NoticeIconItemType = 'notification' | 'message' | 'event';
-
-  type NoticeIconItem = {
-    id?: string;
-    extra?: string;
-    key?: string;
-    read?: boolean;
-    avatar?: string;
-    title?: string;
-    status?: string;
-    datetime?: string;
-    description?: string;
-    type?: NoticeIconItemType;
-  };
-}
+// @ts-ignore
+/* eslint-disable */
+
+declare namespace API {
+  type CurrentUser = {
+    avatar?: string;
+    mobile?: string;
+    role?: string;
+    userid?: string;
+    token?: string;
+    userName?: string;
+  };
+
+  type Result = {
+    code?: number
+    data?: any
+    fail?: boolean;
+    msg?: string;
+    success?: boolean;
+  };
+
+  type PageParams = {
+    current?: number;
+    pageSize?: number;
+  };
+
+  type RuleListItem = {
+    key?: number;
+    disabled?: boolean;
+    href?: string;
+    avatar?: string;
+    name?: string;
+    owner?: string;
+    desc?: string;
+    callNo?: number;
+    status?: number;
+    updatedAt?: string;
+    createdAt?: string;
+    progress?: number;
+  };
+
+  type RuleList = {
+    data?: RuleListItem[];
+    /** 列表的内容总数 */
+    total?: number;
+    success?: boolean;
+  };
+
+  type FakeCaptcha = {
+    code?: number;
+    status?: string;
+  };
+
+  type LoginParams = {
+    username?: string;
+    password?: string;
+    mobile?: string;
+    code?: string;
+    savePhone?: boolean;
+    type?: string;
+  };
+
+  type ErrorResponse = {
+    /** 业务约定的错误码 */
+    errorCode: string;
+    /** 业务上的错误信息 */
+    errorMessage?: string;
+    /** 业务上的请求是否成功 */
+    success?: boolean;
+  };
+
+  type NoticeIconList = {
+    data?: NoticeIconItem[];
+    /** 列表的内容总数 */
+    total?: number;
+    success?: boolean;
+  };
+
+  type NoticeIconItemType = 'notification' | 'message' | 'event';
+
+  type NoticeIconItem = {
+    id?: string;
+    extra?: string;
+    key?: string;
+    read?: boolean;
+    avatar?: string;
+    title?: string;
+    status?: string;
+    datetime?: string;
+    description?: string;
+    type?: NoticeIconItemType;
+  };
+}

+ 15 - 0
src/services/task-api/center.ts

@@ -0,0 +1,15 @@
+import { request } from "@umijs/max";
+
+
+/**
+ * 个人中心获取用户信息
+ * @param data 
+ * @param options 
+ * @returns 
+ */
+export async function getCenterUserInfoApi(options?: { [key: string]: any }) {
+    return request<API.Result>('/api/login/user/info', {
+        method: 'GET',
+        ...(options || {}),
+    });
+}

+ 30 - 0
src/services/task-api/download.ts

@@ -0,0 +1,30 @@
+import { request } from "@umijs/max";
+
+
+/**
+ * 下载任务列表
+ * @param data 
+ * @param options 
+ * @returns 
+ */
+export async function getDownloadListApi(data: TASKAPI.DownloadList, options?: { [key: string]: any }) {
+    return request<API.Result>('/api/download/log/pageList', {
+        method: 'POST',
+        data,
+        ...(options || {}),
+    });
+}
+
+/**
+ * 下载上报
+ * @param data 
+ * @param options 
+ * @returns 
+ */
+export async function downloadEscalationApi(data: TASKAPI.DownloadEscalation, options?: { [key: string]: any }) {
+    return request<API.Result>('/api/download/log/add', {
+        method: 'POST',
+        data,
+        ...(options || {}),
+    });
+}

+ 16 - 0
src/services/task-api/income.ts

@@ -0,0 +1,16 @@
+import { request } from "@umijs/max";
+
+
+/**
+ * 收益列表
+ * @param data 
+ * @param options 
+ * @returns 
+ */
+export async function getIncomeListApi(data: TASKAPI.IncomeList, options?: { [key: string]: any }) {
+    return request<API.Result>('/api/material/income/list', {
+        method: 'POST',
+        data,
+        ...(options || {}),
+    });
+}

+ 44 - 30
src/services/task-api/myTask.ts

@@ -1,31 +1,45 @@
-// @ts-ignore
-/* eslint-disable */
-import { request } from '@umijs/max';
-
-/**
- * 任务发布
- * @param data 
- * @param options 
- * @returns 
- */
-export async function addTaskApi(data: TASKAPI.AddTask, options?: { [key: string]: any }) {
-    return request<{ data: API.Result; }>('/api/task/add', {
-        method: 'POST',
-        data,
-        ...(options || {}),
-    });
-}
-
-/**
- * 任务修改
- * @param data 
- * @param options 
- * @returns 
- */
-export async function modifyTaskApi(data: TASKAPI.ModifyTask, options?: { [key: string]: any }) {
-    return request<{ data: API.Result; }>('/api/task/modify', {
-        method: 'POST',
-        data,
-        ...(options || {}),
-    });
+// @ts-ignore
+/* eslint-disable */
+import { request } from '@umijs/max';
+
+/**
+ * 任务发布
+ * @param data 
+ * @param options 
+ * @returns 
+ */
+export async function addTaskApi(data: TASKAPI.AddTask, options?: { [key: string]: any }) {
+    return request<API.Result>('/api/task/add', {
+        method: 'POST',
+        data,
+        ...(options || {}),
+    });
+}
+
+/**
+ * 任务修改
+ * @param data 
+ * @param options 
+ * @returns 
+ */
+export async function modifyTaskApi(data: TASKAPI.ModifyTask, options?: { [key: string]: any }) {
+    return request<API.Result>('/api/task/modify', {
+        method: 'POST',
+        data,
+        ...(options || {}),
+    });
+}
+
+/**
+ * 我的任务列表
+ * @param data 
+ * @param options 
+ * @returns 
+ */
+export async function getTaskListApi(data: TASKAPI.TaskList, options?: { [key: string]: any }) {
+    return request<API.Result>('/api/task/manage/list', {
+        method: 'POST',
+        data,
+        ...(options || {}),
+    });
 }
 }

+ 16 - 0
src/services/task-api/opus.ts

@@ -0,0 +1,16 @@
+import { request } from "@umijs/max";
+
+
+/**
+ * 任务列表
+ * @param data 
+ * @param options 
+ * @returns 
+ */
+export async function getOpusListApi(data: TASKAPI.OpusList, options?: { [key: string]: any }) {
+    return request<API.Result>('/api/material/list', {
+        method: 'POST',
+        data,
+        ...(options || {}),
+    });
+}

+ 30 - 0
src/services/task-api/task.ts

@@ -0,0 +1,30 @@
+import { request } from "@umijs/max";
+
+
+/**
+ * 任务列表
+ * @param data 
+ * @param options 
+ * @returns 
+ */
+export async function getTaskMemberListApi(data: TASKAPI.TaskMemberList, options?: { [key: string]: any }) {
+    return request<API.Result>('/api/task/member/list', {
+        method: 'POST',
+        data,
+        ...(options || {}),
+    });
+}
+
+/**
+ * 提交素材
+ * @param data 
+ * @param options 
+ * @returns 
+ */
+export async function addMaterialApi(data: TASKAPI.AddMaterial, options?: { [key: string]: any }) {
+    return request<API.Result>('/api/material/add', {
+        method: 'POST',
+        data,
+        ...(options || {}),
+    });
+}

+ 107 - 59
src/services/task-api/typings.d.ts

@@ -1,60 +1,108 @@
-// @ts-ignore
-/* eslint-disable */
-
-declare namespace TASKAPI {
-    type AddTask = {
-        // 任务头像
-        avatar?: string;
-        // 结算比率
-        checkout?: string;
-        // 结束时间
-        endTime?: string;
-        // 开始时间
-        startTime?: string;
-        // 素材要求json, 包含比例要求, 尺寸要求, 大小要求
-        materialClaimJson?: string;
-        // 素材示例
-        materialExample?: string;
-        // 素材资源
-        materialResource?: string;
-        // 素材来源
-        materialSource?: string
-        // 素材类型
-        materialType?: string
-        // 任务名称
-        name: string
-        // 任务描述
-        remark?: string
-        // 补充说明
-        remarkMore?: string
-        // 状态
-        status?: string
-        // 任务类型
-        taskType?: string
-        // 紧急度
-        urgency?: string
-    };
-    type ModifyTask = {
-        id: number
-        // 任务头像
-        avatar?: string;
-        // 结束时间
-        endTime?: string;
-        // 开始时间
-        startTime?: string;
-        // 素材示例
-        materialExample?: string;
-        // 素材资源
-        materialResource?: string;
-        // 任务名称
-        name: string
-        // 任务描述
-        remark?: string
-        // 补充说明
-        remarkMore?: string
-        // 状态
-        status?: string
-        // 紧急度
-        urgency?: string
-    }
+// @ts-ignore
+/* eslint-disable */
+
+declare namespace TASKAPI {
+    type AddTask = {
+        // 任务头像
+        avatar?: string;
+        // 结算比率
+        checkout: number;
+        // 结束时间
+        endTime?: string;
+        // 开始时间
+        startTime?: string;
+        // 素材要求json, 包含比例要求, 尺寸要求, 大小要求
+        materialClaimJson?: string;
+        // 素材示例
+        materialExample?: string;
+        // 素材资源
+        materialResource?: string;
+        // 素材来源
+        materialSource?: string
+        // 素材类型
+        materialType?: string
+        // 任务名称
+        name: string
+        // 任务描述
+        remark?: string
+        // 补充说明
+        remarkMore?: string
+        // 状态
+        status?: string
+        // 任务类型
+        taskType?: string
+        // 紧急度
+        urgency?: string
+        // 结算类型
+        checkoutType: string
+        id?: number
+    };
+    type ModifyTask = {
+        id: number
+        // 任务头像
+        avatar?: string;
+        // 结束时间
+        endTime?: string;
+        // 开始时间
+        startTime?: string;
+        // 素材示例
+        materialExample?: string;
+        // 素材资源
+        materialResource?: string;
+        // 任务名称
+        name: string
+        // 任务描述
+        remark?: string
+        // 补充说明
+        remarkMore?: string
+        // 状态
+        status?: string
+        // 紧急度
+        urgency?: string
+    };
+    interface TaskList extends DEFAULTAPI.Paging {
+        startTime?: string,
+        endTime?: string,
+        materialSource?: string,
+        materialType?: string,
+        name?: string,
+        status?: string
+        taskType?: string
+        urgency?: number
+    };
+    interface TaskMemberList extends DEFAULTAPI.Paging {
+        name?: string,
+        status?: string,
+        taskType?: string
+    };
+    type MaterialBean = {
+        md5: string,
+        size: number,
+        url: string
+    };
+    interface AddMaterial {
+        materialBeanList: MaterialBean[],
+        materialType: string
+        taskId: number
+    };
+    interface IncomeList extends DEFAULTAPI.Paging {
+        endTime?: string
+        startTime?: string
+        taskName?: string
+    };
+    interface DownloadList extends DEFAULTAPI.Paging {
+
+    };
+    type DownloadEscalation = {
+        userMaterialId: number
+    };
+    interface OpusList extends DEFAULTAPI.Paging {
+        isCost?: boolean
+        materialType?: string
+        taskCreateEndTime?: string
+        taskCreateStartTime?: string
+        taskName?: string
+        uploadEndTime?: string
+        uploadStartTime?: string
+    };
 }
 }

+ 9 - 0
src/services/typings.d.ts

@@ -0,0 +1,9 @@
+// @ts-ignore
+/* eslint-disable */
+
+declare namespace DEFAULTAPI {
+    type Paging = {
+        pageNum: number,
+        pageSize: number
+    }
+}

+ 73 - 19
src/utils/index.tsx

@@ -1,20 +1,74 @@
-import { RcFile } from "antd/es/upload";
-
-/** 获取base64 */
-export const getBase64 = (file: RcFile, callback: (url: string) => void) => {
-    const reader = new FileReader();
-    reader.addEventListener('load', () => callback(reader.result as string));
-    reader.readAsDataURL(file);
-};
-
-/** 获取图片上传宽高 */
-export const getImgSizeProper = (file: RcFile): Promise<void> => {
-    return new Promise((resolve: (value: any | PromiseLike<void>) => void) => {
-        let img: any = new Image();
-        let _URL = window.URL || window.webkitURL;
-        img.onload = function (e: any) {
-            resolve({ width: this.width, height: this.height })
-        }
-        img.src = _URL.createObjectURL(file);
-    })
+import { RcFile } from "antd/es/upload";
+import SparkMD5 from "spark-md5";
+
+/** 获取base64 */
+export const getBase64 = (file: RcFile, callback: (url: string) => void) => {
+    const reader = new FileReader();
+    reader.addEventListener('load', () => callback(reader.result as string));
+    reader.readAsDataURL(file);
+};
+
+/** 获取图片上传宽高 */
+export const getImgSizeProper = (file: RcFile): Promise<void> => {
+    return new Promise((resolve: (value: any | PromiseLike<void>) => void) => {
+        let img: any = new Image();
+        let _URL = window.URL || window.webkitURL;
+        img.onload = function (e: any) {
+            resolve({ width: this.width, height: this.height })
+        }
+        img.src = _URL.createObjectURL(file);
+    })
+}
+
+
+/* 获取视频宽高*/
+export const videoMessage = (videos: RcFile[]): Promise<{ width: number, height: number, videoLength: number }[]> => {
+    return new Promise((resolve, reject) => {
+        if (videos.length > 0) {
+            let videoAll = videos?.map((item: RcFile) => {
+                return new Promise((resolve) => {
+                    var videoUrl = URL.createObjectURL(item);
+                    var videoObj = document.createElement("video");
+                    videoObj.onloadedmetadata = function (evt) {
+                        URL.revokeObjectURL(videoUrl);
+                        // 执行上传的方法,获取外网路径,上传进度等
+                        resolve({ width: videoObj.videoWidth, height: videoObj.videoHeight, videoLength: videoObj.duration })
+                    };
+                    videoObj.src = videoUrl;
+                    videoObj.load();
+                })
+            })
+            Promise.all(videoAll).then((res: any) => {
+                resolve(res)
+            })
+        } else {
+            reject([])
+        }
+    })
+};
+
+/**
+ * 获取素材MD5
+ * @param file 
+ * @returns 
+ */
+export const getMD5 = (file: RcFile): Promise<string> => {
+    return new Promise(resolve => {
+        let spark = new SparkMD5();
+        let fileReader = new FileReader();
+        fileReader.readAsBinaryString(file);
+        fileReader.onload = function (e: any) {
+            spark.appendBinary(e.target.result);
+            let md5 = spark.end()
+            resolve(md5)
+        }
+    })
+}
+
+/**
+ * 拼接oss参数获取视频首针图
+ * @param videoUrl 
+ */
+export const getVideoImgUrl = (videoUrl: string) => {
+    return videoUrl + "?x-oss-process=video/snapshot,t_0,f_jpg,w_0,h_0,m_fast,ar_auto"
 }
 }