Quellcode durchsuchen

chore(core): 修改权限

wjx vor 1 Jahr
Ursprung
Commit
680fa729ec

+ 6 - 1
config/routes.ts

@@ -32,35 +32,40 @@ export default [
     path: '/task',
     name: 'task',
     icon: 'crown',
+    access: 'canAdmin',
     component: './Task',
   },
   {
     path: '/myTask',
     name: 'my-task',
     icon: 'fileText',
+    access: 'canAdmin',
     component: './MyTask',
   },
   {
     path: '/opus',
     name: 'opus',
     icon: 'snippets',
+    access: 'canAdmin',
     component: './Opus',
   },
   {
     path: '/income',
     name: 'income',
     icon: 'accountBook',
+    access: 'canAdmin',
     component: './Income',
   },
   {
     path: '/download',
     name: 'download',
     icon: 'download',
+    access: 'canAdmin',
     component: './Download',
   },
   {
     path: '/',
-    redirect: '/Task',
+    redirect: '/User/Login',
   },
   {
     path: '*',

+ 15 - 1
src/access.ts

@@ -3,7 +3,21 @@
  * */
 export default function access(initialState: { currentUser?: API.CurrentUser } | undefined) {
   const { currentUser } = initialState ?? {};
+  let hasRoutes: string[] = [];
+  if (currentUser) {
+    switch (currentUser.role) {
+      case 'USER_ROLE_ADMIN': // 超管
+        hasRoutes = ['task', 'my-task', 'opus', 'income', 'download'];
+        break;
+      case 'USER_ROLE_MEMBER': // 成员
+        hasRoutes = ['my-task', 'opus', 'download'];
+        break;
+      case 'USER_ROLE_USER': // 用户
+        hasRoutes = ['task', 'income'];
+        break;
+    }
+  }
   return {
-    canAdmin: currentUser && currentUser.access === 'admin',
+    canAdmin: (route: { name: string }) => hasRoutes.includes(route.name),
   };
 }

+ 149 - 141
src/app.tsx

@@ -1,141 +1,149 @@
-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
-};
+import Footer from '@/components/Footer';
+import { Question } from '@/components/RightContent';
+import { LinkOutlined } from '@ant-design/icons';
+import type { Settings as LayoutSettings } from '@ant-design/pro-components';
+import { PageLoading, SettingDrawer } from '@ant-design/pro-components';
+import type { RunTimeLayoutConfig } from '@umijs/max';
+import { history } from '@umijs/max';
+import { Button } from 'antd';
+import defaultSettings from '../config/defaultSettings';
+import { AvatarDropdown, AvatarName } from './components/RightContent/AvatarDropdown';
+import { errorConfig } from './requestErrorConfig';
+import { currentUser as queryCurrentUser } from './services/ant-design-pro/api';
+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;
+  };
+  // 判断是否有token,执行
+  if (localStorage.getItem('Admin-Token')) {
+    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
+      ? [
+          <Button
+            type="link"
+            key="openApi"
+            href="http://47.97.38.17:9988/swagger-ui.html#/"
+            target="_blank"
+          >
+            <LinkOutlined />
+            <span>OpenAPI 文档</span>
+          </Button>,
+        ]
+      : [],
+    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,
+};

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

@@ -1,51 +1,108 @@
-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)
+import { getVideoImgUrl } from '@/utils';
+import { CloseOutlined } from '@ant-design/icons';
+import { Divider, Image, Modal, Space, theme } from 'antd';
+import React, { useState } from 'react';
+import play from '../../../public/image/play.png';
+import style from './index.less';
+
+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
+              controlsList="nodownload"
+            >
+              您的浏览器不支持 video 标签。
+            </video>
+          </Modal>
+        </div>
+      )}
+    </div>
+  ) : (
+    '无素材'
+  );
+};
+
+export default React.memo(LookVideo);

+ 10 - 0
src/components/Material/index.less

@@ -0,0 +1,10 @@
+.video {
+  display: block;
+  width: 100%;
+  max-width: 320px;
+  max-height: 600px;
+  margin: auto;
+}
+.playVideo {
+  padding: 0;
+}

+ 52 - 11
src/components/Material/index.tsx

@@ -1,15 +1,20 @@
-import { Button, Flex, Image, Space, Typography } from 'antd';
+import { CloseOutlined } from '@ant-design/icons';
+import { Button, Flex, Image, Modal, Space, theme, Typography } from 'antd';
 import React, { useState } from 'react';
+import style from './index.less';
 
 interface Props {
+  materialExampleType: string;
   claimJson: string;
   items?: string[];
   resourceUrl?: string;
 }
-const Material: React.FC<Props> = ({ items = [], resourceUrl, claimJson }) => {
+const Material: React.FC<Props> = ({ materialExampleType, items = [], resourceUrl, claimJson }) => {
   /******************************/
   const [visible, setVisible] = useState<boolean>(false);
   const { size, extent } = JSON.parse(claimJson);
+  const { useToken } = theme;
+  const { token } = useToken();
   /******************************/
 
   return (
@@ -36,15 +41,51 @@ const Material: React.FC<Props> = ({ items = [], resourceUrl, claimJson }) => {
       <Space>
         {items.length > 0 && (
           <>
-            <Image.PreviewGroup
-              items={items}
-              preview={{
-                visible: visible,
-                onVisibleChange: (value) => {
-                  setVisible(value);
-                },
-              }}
-            />
+            {materialExampleType === 'MATERIAL_TYPE_VIDEO' && visible ? (
+              <Modal
+                open={visible}
+                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();
+                  setVisible(false);
+                }}
+                className="playVideo"
+                destroyOnClose={true}
+              >
+                {visible && (
+                  <video
+                    className={style.video}
+                    style={{ borderRadius: token.borderRadius }}
+                    src={items[0]}
+                    autoPlay
+                    controls
+                    controlsList="nodownload"
+                  >
+                    您的浏览器不支持 video 标签。
+                  </video>
+                )}
+              </Modal>
+            ) : (
+              <Image.PreviewGroup
+                items={items}
+                preview={{
+                  visible: visible,
+                  onVisibleChange: (value) => {
+                    setVisible(value);
+                  },
+                }}
+              />
+            )}
+
             <Button type="link" style={{ padding: 0 }} onClick={() => setVisible(true)}>
               素材示例
             </Button>

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

@@ -1,146 +1,146 @@
-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>
-    </>
-  );
-};
+import ModalCenter from '@/pages/Account/Center/ModalCenter';
+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';
+
+export type GlobalHeaderRightProps = {
+  menu?: boolean;
+  children?: React.ReactNode;
+};
+
+export const AvatarName = () => {
+  const { initialState } = useModel('@@initialState');
+  const { currentUser } = initialState || {};
+  return <span className="anticon">{currentUser?.userName}</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>
+    </>
+  );
+};

+ 89 - 0
src/components/UploadImg/UploadAvatar.tsx

@@ -0,0 +1,89 @@
+import { getOssInfo } from '@/services/ant-design-pro/api';
+import { getImgSizeProper } from '@/utils';
+import { request } from '@umijs/max';
+import { Avatar, message, Spin, Upload } from 'antd';
+import { RcFile } from 'antd/lib/upload';
+import React, { useState } from 'react';
+import './index.less';
+
+interface Props {
+  mobile: string;
+  defaultUrl: string;
+  value?: string; // 图片地址
+  onChange?: (data: string) => void;
+}
+const UploadAvatar: React.FC<Props> = (props) => {
+  /** 变量START */
+  const { mobile, defaultUrl, value, onChange } = props;
+  const [imageFile, setImageFile] = useState<string>(value || '');
+  const [loading, setLoading] = 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!');
+    }
+
+    let imgData: any = await getImgSizeProper(file);
+    if (imgData.width < 50 || imgData.width > 600 || imgData.height < 50 || imgData.height > 600) {
+      message.error(`请传入图片长宽在50-600`);
+      return false;
+    } else if (imgData.width !== imgData.height) {
+      message.error(`请传入正方形的图片`);
+      return false;
+    }
+    return isJpgOrPng && isLt2M;
+  };
+
+  return (
+    <Upload
+      name="avatar"
+      listType="picture-card"
+      accept="image/gif,image/jpeg,image/png,image/jpg"
+      className="uploaderAvatar"
+      beforeUpload={beforeUpload}
+      maxCount={1}
+      disabled={loading}
+      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);
+          }
+        });
+      }}
+    >
+      <Spin spinning={loading}>
+        <Avatar
+          style={{ backgroundColor: '#7265e6', verticalAlign: 'middle' }}
+          size={104}
+          src={imageFile || defaultUrl}
+        >
+          {mobile}
+        </Avatar>
+      </Spin>
+    </Upload>
+  );
+};
+
+export default React.memo(UploadAvatar);

+ 24 - 19
src/components/UploadImg/index.less

@@ -1,25 +1,30 @@
 .myUpload {
-    position: relative;
+  position: relative;
 
-    .ant-upload-list-picture-card-container,
-    .ant-upload.ant-upload-select-picture-card {
-        width: 85px;
-        height: 85px;
-    }
+  .ant-upload-list-picture-card-container,
+  .ant-upload.ant-upload-select-picture-card {
+    width: 85px;
+    height: 85px;
+  }
 
-    .look {
-        position: absolute;
-        left: 100px;
-        bottom: 5px;
-        opacity: 0;
-        transition: all .5s;
+  .look {
+    position: absolute;
+    bottom: 5px;
+    left: 100px;
+    opacity: 0;
+    transition: all 0.5s;
 
-        a {
-            font-size: 12px;
-        }
+    a {
+      font-size: 12px;
     }
+  }
 
-    &:hover .look {
-        opacity: 1;
-    }
-}
+  &:hover .look {
+    opacity: 1;
+  }
+}
+
+.uploaderAvatar .ant-upload {
+  background-color: transparent !important;
+  border: none !important;
+}

+ 6 - 0
src/components/UploadVideo/index.less

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

+ 93 - 0
src/components/UploadVideo/index.tsx

@@ -0,0 +1,93 @@
+import VideoNews from '@/components/VideoNews';
+import { getOssInfo } from '@/services/ant-design-pro/api';
+import { getMD5 } from '@/utils';
+import { InboxOutlined } from '@ant-design/icons';
+import { request } from '@umijs/max';
+import { message, Spin, Upload } from 'antd';
+import { RcFile } from 'antd/lib/upload';
+import React, { useState } from 'react';
+import './index.less';
+
+interface Props {
+  value?: string;
+  onChange?: (value?: string) => void;
+}
+/**
+ * 视频上传
+ * @returns
+ */
+const UploadVideo: React.FC<Props> = ({ value, onChange }) => {
+  /** 变量START */
+  const [videoFile, setVideoFile] = useState<string | undefined>(value);
+  const [loading, setLoading] = useState<boolean>(false);
+  /** 变量END */
+
+  const beforeUpload = async (file: RcFile) => {
+    console.log('file--->', file);
+    const isZipOrRar = file.type === 'video/mp4';
+    if (!isZipOrRar) {
+      message.error('您只能上传MP4文件!');
+    }
+    const fileSizeM = file.size / 1024 / 1024;
+    const isLt20M = fileSizeM < 20;
+    if (!isLt20M) {
+      message.error(`视频大小必须小于20MB!`);
+    }
+    return isZipOrRar && isLt20M;
+  };
+
+  return (
+    <div className="myUploadVideo">
+      <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) {
+                setVideoFile(urlData?.data?.url);
+                onChange?.(urlData?.data?.url);
+              }
+            } catch (error) {
+              setLoading(false);
+            }
+          });
+        }}
+      >
+        <Spin spinning={loading}>
+          {videoFile ? (
+            <div className="video-content">
+              <VideoNews src={videoFile} />
+            </div>
+          ) : (
+            <>
+              <p className="ant-upload-drag-icon">
+                <InboxOutlined />
+              </p>
+              <p className="ant-upload-text">单击或拖动视频到此区域进行上传</p>
+              <p className="ant-upload-hint">素材大小:小于20M</p>
+            </>
+          )}
+        </Spin>
+      </Upload.Dragger>
+    </div>
+  );
+};
+
+export default React.memo(UploadVideo);

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

@@ -1,90 +1,100 @@
-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)
+import { getOssInfo } from '@/services/ant-design-pro/api';
+import { CloudUploadOutlined, DeleteOutlined, PaperClipOutlined } from '@ant-design/icons';
+import { request } from '@umijs/max';
+import { Button, Flex, message, Space, Typography, Upload } from 'antd';
+import { RcFile } from 'antd/lib/upload';
+import React, { useState } from 'react';
+import './index.less';
+
+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) => {
+    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;
+  };
+
+  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);

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

@@ -1,39 +1,75 @@
-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)
+import { getVideoImgUrl } from '@/utils';
+import { CloseOutlined } from '@ant-design/icons';
+import { Image, ImageProps, Modal, theme } from 'antd';
+import React, { useState } from 'react';
+import play from '../../../public/image/play.png';
+import style from './index.less';
+
+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
+              controlsList="nodownload"
+            >
+              您的浏览器不支持 video 标签。
+            </video>
+          </Modal>
+        </div>
+      )}
+    </div>
+  );
+};
+
+export default React.memo(VideoNews);

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

@@ -1,84 +1,86 @@
-.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;
-        }
-    }
-}
+.avatarHolder {
+  margin-bottom: 24px;
+  text-align: center;
+
+  & > img {
+    width: 104px;
+    height: 104px;
+    margin-bottom: 20px;
+  }
+
+  .name {
+    margin-top: auto;
+    margin-bottom: 4px;
+    color: @heading-color;
+    font-weight: 500;
+    font-size: 20px;
+    line-height: 28px;
+    inset-inline-start: 0;
+  }
+}
+
+.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;
+    }
+  }
+}

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

@@ -1,102 +1,188 @@
-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)
+import UploadAvatar from '@/components/UploadImg/UploadAvatar';
+import { getCenterUserInfoApi, setUserInfoApi } from '@/services/task-api/center';
+import { FieldTimeOutlined, PhoneOutlined } from '@ant-design/icons';
+import { useModel } from '@umijs/max';
+import { useRequest } from 'ahooks';
+import { Col, Divider, message, Modal, Row, Statistic, Typography } from 'antd';
+import React, { useState } from 'react';
+import { flushSync } from 'react-dom';
+import styles from './Center.less';
+import { CurrentUser } from './data';
+
+interface Props {
+  visible?: boolean;
+  onClose?: () => void;
+}
+/**
+ * 弹窗个人中心
+ * @returns
+ */
+const ModalCenter: React.FC<Props> = ({ visible, onClose }) => {
+  /*******************************/
+  const { initialState, setInitialState } = useModel('@@initialState');
+  const { data, refresh } = useRequest(getCenterUserInfoApi);
+  const [editing, setEditing] = useState<boolean>(false);
+  const setUserInfo = useRequest(setUserInfoApi, { manual: true });
+  /*******************************/
+
+  const fetchUserInfo = async () => {
+    const userInfo = await initialState?.fetchUserInfo?.();
+    if (userInfo) {
+      flushSync(() => {
+        setInitialState((s) => ({
+          ...s,
+          currentUser: {
+            avatar: `https://xsgames.co/randomusers/avatar.php?g=pixel&key=${Math.floor(
+              Math.random() * 10,
+            )}`,
+            ...userInfo,
+          },
+        }));
+      });
+    }
+  };
+
+  //  渲染用户信息
+  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}>
+          <UploadAvatar
+            defaultUrl={data?.data?.avatar || ''}
+            mobile={data?.data?.mobile}
+            value={data?.data?.avatar}
+            onChange={(e) => {
+              if (!e) {
+                message.error('请选择头像!');
+                return;
+              }
+              setUserInfo.runAsync({ avatar: e, userName: data?.data?.userName }).then((res) => {
+                if (res.data) {
+                  refresh();
+                  fetchUserInfo();
+                }
+              });
+            }}
+          />
+          <div className={styles.name} style={{ marginTop: 20 }}>
+            <Typography.Text
+              editable={{
+                maxLength: 6,
+                editing,
+                onStart() {
+                  setEditing(true);
+                },
+                onChange: (value) => {
+                  if (!value) {
+                    message.error('昵称不能为空!');
+                    return;
+                  }
+                  if (value === data?.data?.userName) {
+                    setEditing(false);
+                    return;
+                  }
+                  setUserInfo
+                    .runAsync({ avatar: data?.data?.avatar, userName: value })
+                    .then((res) => {
+                      if (res.data) {
+                        setEditing(false);
+                        refresh();
+                        fetchUserInfo();
+                      }
+                    });
+                },
+              }}
+              className={styles.name}
+            >
+              {data?.data?.userName}
+            </Typography.Text>
+          </div>
+          <div>海纳百川,有容乃大</div>
+        </div>
+        {renderUserInfo(data?.data || {})}
+        <Divider dashed />
+        <Row gutter={[16, 16]}>
+          <Col span={12}>
+            <Statistic
+              title="累计上传作品"
+              value={
+                data?.data?.materialCount
+                  ? data?.data?.materialCount
+                  : data?.data?.materialCount === 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);

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

@@ -1,77 +1,96 @@
-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
+import LookVideo from '@/components/LookVideo';
+import { Button, Flex, Typography } from 'antd';
+import { ColumnsType } from 'antd/es/table';
+
+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: '上传任务ID',
+      dataIndex: 'taskId',
+      key: 'taskId',
+      align: 'center',
+      width: 90,
+    },
+    {
+      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;

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

@@ -1,106 +1,127 @@
-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
+import LookVideo from '@/components/LookVideo';
+import { Flex, Typography } from 'antd';
+import { ColumnsType } from 'antd/es/table';
+
+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: '任务ID',
+      dataIndex: 'taskId',
+      key: 'taskId',
+      align: 'center',
+      width: 90,
+    },
+    {
+      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;

+ 7 - 4
src/pages/MyTask/index.tsx

@@ -82,6 +82,7 @@ const MyTask: React.FC = () => {
       checkoutType,
       checkout,
       urgency,
+      materialExampleType,
       ...pre
     } = data;
     if (checkoutType === 'CHECKOUT_TYPE_SCALE') {
@@ -96,6 +97,7 @@ const MyTask: React.FC = () => {
       ...pre,
       ...JSON.parse(materialClaimJson),
       checkoutType,
+      materialExampleType: materialExampleType || 'MATERIAL_TYPE_SINGLE_PICTURE',
       startTime: moment(startTime),
       urgency: urgency + '',
     });
@@ -265,6 +267,7 @@ const MyTask: React.FC = () => {
                       justify="space-between"
                     >
                       <Material
+                        materialExampleType={item?.materialExampleType}
                         items={item?.materialExample?.split(',')}
                         resourceUrl={item.materialResource}
                         claimJson={item.materialClaimJson}
@@ -275,18 +278,18 @@ const MyTask: React.FC = () => {
                       gap={10}
                       vertical={true}
                       align="center"
-                      justify="center"
+                      justify="space-between"
                     >
                       {TaskStatusEle[item.status]}
+                      <Button type="primary" onClick={() => editHandle(item)}>
+                        修改任务
+                      </Button>
                       <Tag color="error">
                         结算:
                         {item.checkoutType === 'CHECKOUT_TYPE_SCALE'
                           ? `总消耗 ${item.checkout * 100}%`
                           : `${item.checkout}元(审核合格)`}
                       </Tag>
-                      <Button type="primary" onClick={() => editHandle(item)}>
-                        修改任务
-                      </Button>
                     </Flex>
                   </Flex>
                 }

+ 28 - 9
src/pages/MyTask/taskModal.tsx

@@ -1,10 +1,12 @@
 import Interval from '@/components/Interval';
 import UploadImg from '@/components/UploadImg';
 import UploadImgs from '@/components/UploadImgs';
+import UploadVideo from '@/components/UploadVideo';
 import UploadZip from '@/components/UploadZip';
 import { addTaskApi, modifyTaskApi } from '@/services/task-api/myTask';
 import {
   CheckoutTypeEnum,
+  MaterialExampleEnum,
   MaterialSourceEnum,
   MaterialTypeEnum,
   StatusEnum,
@@ -128,6 +130,7 @@ const TaskModal: React.FC<Props> = ({ initialValues, visible, onChange, onClose
   const endTime = Form.useWatch('endTime', form);
   const startTime = Form.useWatch('startTime', form);
   const checkoutType = Form.useWatch('checkoutType', form);
+  const materialExampleType = Form.useWatch('materialExampleType', form);
   const addTask = useRequest(addTaskApi, { manual: true });
   const modifyTask = useRequest(modifyTaskApi, { manual: true });
   /*************************************/
@@ -187,8 +190,7 @@ const TaskModal: React.FC<Props> = ({ initialValues, visible, onChange, onClose
       );
     }
     return (
-      current &&
-      (current < moment().startOf('day') || current > moment().add(30, 'day').endOf('day'))
+      current && current < moment().startOf('day') // || current > moment().add(30, 'day').endOf('day')
     );
   };
 
@@ -196,14 +198,11 @@ const TaskModal: React.FC<Props> = ({ initialValues, visible, onChange, onClose
     // Can not select days before today and today
     if (startTime) {
       return (
-        current &&
-        (current < moment(startTime).startOf('day') ||
-          current > moment().add(30, 'day').endOf('day'))
+        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'))
+      current && current < moment().startOf('day') // || current > moment().add(30, 'day').endOf('day')
     );
   };
 
@@ -316,6 +315,7 @@ const TaskModal: React.FC<Props> = ({ initialValues, visible, onChange, onClose
             >
               <Radio.Group>
                 {Object.keys(StatusEnum)
+                  .filter((key) => key !== 'STATUS_TIME_END')
                   .filter((key) =>
                     initialValues?.id ? true : key === 'STATUS_EXPIRE' ? false : true,
                   )
@@ -450,9 +450,28 @@ const TaskModal: React.FC<Props> = ({ initialValues, visible, onChange, onClose
                 </Form.Item>
               </>
             )}
-            <Form.Item label={<strong>素材示例</strong>} name="materialExamples">
-              <UploadImgs isUpload={true} maxCount={10} />
+            <Form.Item label={<strong>素材示例类型</strong>} name="materialExampleType">
+              <Select
+                placeholder="请选择任务紧急度"
+                options={Object.keys(MaterialExampleEnum).map((key) => ({
+                  label: (MaterialExampleEnum as any)[key],
+                  value: key,
+                }))}
+              />
             </Form.Item>
+            {materialExampleType && (
+              <Form.Item
+                label={<strong>素材示例</strong>}
+                name="materialExamples"
+                rules={[{ required: true, message: '请选择素材示例!' }]}
+              >
+                {materialExampleType === 'MATERIAL_TYPE_SINGLE_PICTURE' ? (
+                  <UploadImgs isUpload={true} maxCount={10} />
+                ) : (
+                  <UploadVideo />
+                )}
+              </Form.Item>
+            )}
             <Form.Item label={<strong>素材资源包</strong>} name="materialResource">
               <UploadZip />
             </Form.Item>

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

@@ -1,38 +1,75 @@
-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)
+import { getVideoImgUrl } from '@/utils';
+import { CloseOutlined } from '@ant-design/icons';
+import { Image, ImageProps, Modal, theme } from 'antd';
+import React, { useState } from 'react';
+import play from '../../../../public/image/play.png';
+import style from './index.less';
+
+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
+              controlsList="nodownload"
+            >
+              您的浏览器不支持 video 标签。
+            </video>
+          </Modal>
+        </div>
+      )}
+    </div>
+  );
+};
+
+export default React.memo(VideoOpus);

+ 2 - 2
src/pages/Opus/index.tsx

@@ -96,14 +96,14 @@ const Opus: React.FC = () => {
                     </Form.Item>
                   </Col>
 
-                  <Col>
+                  {/* <Col>
                     <Form.Item name="taskTime">
                       <DatePicker.RangePicker
                         style={{ width: 250 }}
                         placeholder={['任务开始日期', '任务开始日期']}
                       />
                     </Form.Item>
-                  </Col>
+                  </Col> */}
 
                   <Col>
                     <Form.Item name="uploadTime">

+ 12 - 9
src/pages/Task/index.tsx

@@ -154,6 +154,7 @@ const Task: React.FC = () => {
                       justify="space-between"
                     >
                       <Material
+                        materialExampleType={item?.materialExampleType}
                         items={item?.materialExample?.split(',')}
                         resourceUrl={item.materialResource}
                         claimJson={item.materialClaimJson}
@@ -163,16 +164,10 @@ const Task: React.FC = () => {
                       style={{ width: 165, height: '100%' }}
                       gap={10}
                       vertical={true}
-                      align="center"
-                      justify="center"
+                      align="flex-end"
+                      justify="space-between"
                     >
                       {TaskStatusEle[item.status]}
-                      <Tag color="error">
-                        结算:
-                        {item.checkoutType === 'CHECKOUT_TYPE_SCALE'
-                          ? `总消耗 ${item.checkout * 100}%`
-                          : `${item.checkout}元(审核合格)`}
-                      </Tag>
                       {item.status === 'STATUS_NORMAL' && (
                         <SubmitTask
                           taskId={item.id}
@@ -182,6 +177,12 @@ const Task: React.FC = () => {
                           onChange={() => getTaskMemberList.refresh()}
                         />
                       )}
+                      <Tag color="error" style={{ marginInlineEnd: 0 }}>
+                        结算:
+                        {item.checkoutType === 'CHECKOUT_TYPE_SCALE'
+                          ? `总消耗 ${item.checkout * 100}%`
+                          : `${item.checkout}元(审核合格)`}
+                      </Tag>
                     </Flex>
                   </Flex>
                 }
@@ -196,7 +197,9 @@ const Task: React.FC = () => {
                     {MaterialTypeEle[item.materialType]}
                   </Tooltip>,
                   <Tooltip title="素材来源要求" key={4}>
-                    <Tag>{(MaterialSourceEnum as any)[item.materialSource]}</Tag>
+                    <Tag style={{ marginInlineEnd: 0 }}>
+                      {(MaterialSourceEnum as any)[item.materialSource]}
+                    </Tag>
                   </Tooltip>,
                 ]}
               >

+ 13 - 4
src/pages/User/Login/index.tsx

@@ -11,7 +11,7 @@ import {
 import { useEmotionCss } from '@ant-design/use-emotion-css';
 import { FormattedMessage, Helmet, history, useIntl, useModel } from '@umijs/max';
 import { Alert, message } from 'antd';
-import React, { useState } from 'react';
+import React, { useEffect, useState } from 'react';
 import { flushSync } from 'react-dom';
 import Settings from '../../../../config/defaultSettings';
 
@@ -35,6 +35,18 @@ const Login: React.FC = () => {
   const [type, setType] = useState<string>('mobile');
   const { initialState, setInitialState } = useModel('@@initialState');
 
+  useEffect(() => {
+    // 判断不同角色登录成功跳到哪里
+    if (initialState?.currentUser?.role && localStorage.getItem('Admin-Token')) {
+      const urlParams = new URL(window.location.href).searchParams;
+      let defaultPath = '/task';
+      if (initialState.currentUser.role === 'USER_ROLE_MEMBER') {
+        defaultPath = '/myTask';
+      }
+      history.push(urlParams.get('redirect') || defaultPath);
+    }
+  }, [initialState?.currentUser?.role, localStorage.getItem('Admin-Token')]);
+
   const containerClassName = useEmotionCss(() => {
     return {
       display: 'flex',
@@ -52,7 +64,6 @@ const Login: React.FC = () => {
   const fetchUserInfo = async () => {
     const userInfo = await initialState?.fetchUserInfo?.();
     if (userInfo) {
-      console.log('userInfo--->', userInfo);
       flushSync(() => {
         setInitialState((s) => ({
           ...s,
@@ -83,8 +94,6 @@ const Login: React.FC = () => {
         }
         message.success(defaultLoginSuccessMessage);
         await fetchUserInfo();
-        const urlParams = new URL(window.location.href).searchParams;
-        history.push(urlParams.get('redirect') || '/');
         return;
       }
       // 如果失败去设置用户错误信息

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

@@ -1,93 +1,94 @@
-// @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;
-  };
-}
+// @ts-ignore
+/* eslint-disable */
+
+declare namespace API {
+  type CurrentUser = {
+    avatar?: string;
+    mobile?: string;
+    role?: string;
+    userid?: string;
+    token?: string;
+    userName?: string;
+    role?: 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;
+  };
+}

+ 14 - 1
src/services/task-api/center.ts

@@ -3,7 +3,6 @@ import config from '../config';
 
 /**
  * 个人中心获取用户信息
- * @param data
  * @param options
  * @returns
  */
@@ -13,3 +12,17 @@ export async function getCenterUserInfoApi(options?: { [key: string]: any }) {
     ...(options || {}),
   });
 }
+
+/**
+ * 设置头像昵称
+ * @param data
+ * @param options
+ * @returns
+ */
+export async function setUserInfoApi(data: TASKAPI.SetUserInfo, options?: { [key: string]: any }) {
+  return request<API.Result>(config.API_BASE_URL + '/api/login/user/update', {
+    method: 'POST',
+    data,
+    ...(options || {}),
+  });
+}

+ 4 - 0
src/services/task-api/typings.d.ts

@@ -103,4 +103,8 @@ declare namespace TASKAPI {
     uploadEndTime?: string;
     uploadStartTime?: string;
   }
+  type SetUserInfo = {
+    avatar: string;
+    userName: string;
+  };
 }

+ 43 - 7
src/utils/constEnum.tsx

@@ -10,9 +10,21 @@ export enum TaskTypeEnum {
 }
 
 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>,
+  TASK_TYPE_GAME: (
+    <Tag color="success" style={{ marginInlineEnd: 0 }}>
+      游戏
+    </Tag>
+  ),
+  TASK_TYPE_NOVEL: (
+    <Tag color="processing" style={{ marginInlineEnd: 0 }}>
+      小说
+    </Tag>
+  ),
+  TASK_TYPE_SHORT_PLAY: (
+    <Tag color="error" style={{ marginInlineEnd: 0 }}>
+      短剧
+    </Tag>
+  ),
 };
 
 /**
@@ -48,10 +60,26 @@ export enum MaterialTypeEnum {
 }
 
 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>,
+  MATERIAL_TYPE_SINGLE_PICTURE: (
+    <Tag color="#55acee" style={{ marginInlineEnd: 0 }}>
+      单图
+    </Tag>
+  ),
+  MATERIAL_TYPE_GROUP_PICTURE: (
+    <Tag color="#3b5999" style={{ marginInlineEnd: 0 }}>
+      组图
+    </Tag>
+  ),
+  MATERIAL_TYPE_VOICE: (
+    <Tag color="#cd201f" style={{ marginInlineEnd: 0 }}>
+      音频
+    </Tag>
+  ),
+  MATERIAL_TYPE_VIDEO: (
+    <Tag color="#55acee" style={{ marginInlineEnd: 0 }}>
+      视频
+    </Tag>
+  ),
 };
 
 /**
@@ -78,3 +106,11 @@ export enum CheckoutTypeEnum {
   CHECKOUT_TYPE_SCALE = '消耗比例',
   CHECKOUT_TYPE_AMOUNT = '任务',
 }
+
+/**
+ * 素材示例类型
+ */
+export enum MaterialExampleEnum {
+  MATERIAL_TYPE_VIDEO = '视频',
+  MATERIAL_TYPE_SINGLE_PICTURE = '图片',
+}