wjx há 1 ano atrás
pai
commit
af27a1b579

+ 1 - 1
config/defaultSettings.ts

@@ -17,7 +17,7 @@ const Settings: ProLayoutProps & {
   colorWeak: false,
   title: '趣程素材库',
   pwa: true,
-  logo: 'https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg',
+  logo: 'https://zx-material-center-test.oss-cn-hangzhou.aliyuncs.com/image/287CA9F756B94313AAD20672B5AED3F0.jpg',
   iconfontUrl: '',
   token: {
     // 参见ts声明,demo 见文档,通过token 修改样式

+ 48 - 0
src/components/Interval/index.tsx

@@ -0,0 +1,48 @@
+import { QuestionCircleOutlined } from "@ant-design/icons"
+import { InputNumber, Space, Tooltip } from "antd"
+import React, { useEffect, useState } from "react"
+
+
+interface Props {
+    value?: [number | undefined, number | undefined]
+    onChange?: (value?: [number | undefined, number | undefined]) => void
+    tips?: string
+    unit?: string
+    placeholder?: [string, string]
+}
+/**
+ * 时间区间
+ * @returns 
+ */
+const Interval: React.FC<Props> = ({ value = [undefined, undefined], onChange, tips, unit="~", placeholder=[undefined, undefined] }) => {
+
+    /********************************************/
+    const [data, setData] = useState<[number | undefined, number | undefined]>([undefined, undefined])
+    /********************************************/
+
+    useEffect(() => {
+        let [val1, val2] = value
+        let [data1, data2] = data
+        if ((val1 && val1 !== data1) || (val2 && val2 !== data2)) {
+            setData([val1, val2])
+        }
+    }, [value, data])
+
+    const handleOk = (value: any, index: number) => {
+        let newData = JSON.parse(JSON.stringify(data))
+        newData[index] = value
+        setData(newData)
+        onChange?.(newData)
+    }
+
+    return <Space>
+        <InputNumber value={data[0]} style={{ width: 90 }} min={0} placeholder={placeholder[0]} onChange={(num) => handleOk(num, 0)} />
+        {unit}
+        <InputNumber value={data[1]} style={{ width: 90 }} min={0} placeholder={placeholder[1]} onChange={(num) => handleOk(num, 1)} />
+        {tips && <Tooltip title={tips}>
+            <QuestionCircleOutlined />
+        </Tooltip>}
+    </Space>
+}
+
+export default React.memo(Interval)

+ 25 - 0
src/components/UploadImg/index.less

@@ -0,0 +1,25 @@
+.myUpload {
+    position: relative;
+
+    .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;
+
+        a {
+            font-size: 12px;
+        }
+    }
+
+    &:hover .look {
+        opacity: 1;
+    }
+}

+ 121 - 0
src/components/UploadImg/index.tsx

@@ -0,0 +1,121 @@
+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>
+}
+
+
+export default React.memo(UploadImg)

+ 34 - 0
src/components/UploadImgs/index.less

@@ -0,0 +1,34 @@
+.myUpload {
+    position: relative;
+
+    .ant-upload-list-picture-card-container,
+    .ant-upload.ant-upload-select-picture-card {
+        width: 85px;
+        height: 85px;
+    }
+
+    .preViewContent {
+        overflow: hidden;
+        border: 1px solid rgb(217, 217, 217);
+        border-radius: 6px;
+        display: flex;
+        justify-content: center;
+        align-items: center;
+        position: relative;
+        .clear {
+            position: absolute;
+            top: 0;
+            right: 0;
+            opacity: 0;
+            transition: all .5s;
+        }
+
+        &:hover .clear {
+            opacity: 1;
+        }
+    }
+}
+.avatar-uploader .ant-upload-select {
+    margin-inline-end: 0px !important;
+    margin-bottom: 0px !important;
+}

+ 120 - 0
src/components/UploadImgs/index.tsx

@@ -0,0 +1,120 @@
+import { message, Space, Upload, Image, Button } from "antd"
+import { PlusOutlined, LoadingOutlined, DeleteOutlined } 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 UploadImgs: 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)
+    /** 变量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>
+    );
+
+    const clearHandle = (index: number) => {
+        if (index > -1) {
+            let newImageFile = JSON.parse(JSON.stringify(imageFile))
+            newImageFile.splice(index, 1)
+            console.log(newImageFile)
+            setImageFile(newImageFile);
+            onChange?.(newImageFile)
+        }
+    }
+
+    return <div className="myUpload">
+        <Image.PreviewGroup preview={{ onChange: (current, prev) => console.log(`current index: ${current}, prev index: ${prev}`), }}>
+            <Space wrap>
+                {imageFile.map((item, index) => <div key={index} className="preViewContent" style={{ width: 100, height: 100 }}>
+                    <Image src={item} style={{ width: '100%', height: '100%' }} />
+                    <div className="clear">
+                        <Button icon={<DeleteOutlined />} onClick={() => clearHandle(index)} type="link" danger style={{ padding: 0, width: 'auto', height: 'auto' }} />
+                    </div>
+                </div>)}
+                <Upload
+                    name="avatar"
+                    listType="picture-card"
+                    accept='image/gif,image/jpeg,image/png,image/jpg'
+                    className="avatar-uploader"
+                    beforeUpload={beforeUpload}
+                    showUploadList={false}
+                    customRequest={async (options: any) => {
+                        if (isUpload) { // 上传oss
+                            try {
+                                setLoading(true)
+                                let res = await getOssInfo({ type: 'image/jpeg', fileType: 'image' })
+                                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)
+                                imageFile.push(urlData?.data?.url)
+                                setImageFile(imageFile);
+                                onChange?.(imageFile)
+                            } catch (error) {
+                                setLoading(false)
+                            }
+                        } else {
+                            getBase64(options.file as RcFile, url => {
+                                imageFile.push(url)
+                                setImageFile(imageFile);
+                            })
+                            onChange && onChange(options.file)
+                        }
+                    }}
+                >
+                    {imageFile.length < maxCount && uploadButton}
+                </Upload>
+            </Space>
+        </Image.PreviewGroup>
+
+        <div>{tooltip && tooltip}</div>
+    </div>
+}
+
+
+export default React.memo(UploadImgs)

+ 25 - 0
src/components/UploadZip/index.less

@@ -0,0 +1,25 @@
+.myUpload {
+    position: relative;
+
+    .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;
+
+        a {
+            font-size: 12px;
+        }
+    }
+
+    &:hover .look {
+        opacity: 1;
+    }
+}

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

@@ -0,0 +1,100 @@
+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)

+ 50 - 5
src/const.ts

@@ -2,11 +2,10 @@
 /**
  * 任务分类枚举
  */
-export enum TaskEnum {
-    ALL = '全部',
-    GAME = '游戏',
-    BOOK = '小说',
-    SHORTPLAY = '短剧'
+export enum TaskTypeEnum {
+    TASK_TYPE_GAME = '游戏',
+    TASK_TYPE_NOVEL = '小说',
+    TASK_TYPE_SHORT_PLAY = '短剧'
 }
 
 export enum TaskStatusEnum {
@@ -14,4 +13,50 @@ export enum TaskStatusEnum {
     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',
 }

+ 6 - 2
src/pages/MyTask/index.tsx

@@ -3,6 +3,7 @@ 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) => ({
@@ -27,6 +28,7 @@ const MyTask: React.FC = () => {
     const { token } = useToken();
     const [form] = Form.useForm()
     const [initialValues, setInitialState] = useState<any>({})
+    const [visible, setVisible] = useState<boolean>(false)
     /***********************************/
 
     const onFinish = (data: any) => {
@@ -49,7 +51,7 @@ const MyTask: React.FC = () => {
 
                 <Affix offsetTop={56}>
                     <div style={{ backgroundColor: token.colorBgBase, padding: token.paddingContentHorizontalLG, borderRadius: 8, borderBottom: `1px solid ${token.colorBorderSecondary}` }}>
-                        <Form layout="inline" className='queryForm' initialValues={initialValues} name="basic" form={form} onFinish={onFinish}>
+                        <Form layout="inline" className='queryForm' name="basic" form={form} onFinish={onFinish}>
                             <Row gutter={[0, 6]}>
                                 <Col>
                                     <Form.Item name='taskName'>
@@ -61,7 +63,7 @@ const MyTask: React.FC = () => {
                                     <Space>
                                         <Button type="primary" htmlType="submit">搜索</Button>
                                         <Button onClick={() => form.resetFields()}>重置</Button>
-                                        
+                                        <Button type="primary" onClick={() => { setInitialState({}); setVisible(true) }}>发布素材任务</Button>
                                     </Space>
                                 </Col>
                             </Row>
@@ -126,6 +128,8 @@ const MyTask: React.FC = () => {
                     )}
                 />
             </Space>
+
+            {visible && <TaskModal visible={visible} onClose={() => setVisible(false)} onChange={() => {  }} initialValues={initialValues}/>}
         </Card>
     </PageContainer>
 }

+ 132 - 17
src/pages/MyTask/taskModal.tsx

@@ -1,5 +1,12 @@
-import { Form, Modal, Select } from "antd"
+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
@@ -14,38 +21,146 @@ interface Props {
 const TaskModal: React.FC<Props> = ({ initialValues, visible, onChange, onClose }) => {
 
     /*************************************/
-    const [form] = Form.useForm()
+    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={initialValues}
+            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}
-            layout="vertical"
-            labelCol={{ span: 4 }}
-            wrapperCol={{ span: 20 }}
+            labelCol={{ span: 6 }}
+            wrapperCol={{ span: 18 }}
+            colon={false}
+            labelAlign="left"
         >
-            <Form.Item label="任务紧急度" name='taskName'>
-                <Select
-                    placeholder="请选择任务紧急度"
-                    options={[
-                        { value: 'jack', label: 'Jack' },
-                        { value: 'lucy', label: 'Lucy' },
-                        { value: 'Yiminghe', label: 'yiminghe' },
-                        { value: 'disabled', label: 'Disabled', disabled: true },
-                    ]}
-                />
-            </Form.Item>
+            <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>
 }

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

@@ -1,4 +1,4 @@
-import { TaskEnum, TaskStatusEnum } from "@/const"
+import { TaskTypeEnum, TaskStatusEnum } from "@/const"
 import { AlertOutlined, SearchOutlined } from "@ant-design/icons"
 import { PageContainer } from "@ant-design/pro-components"
 import { useSize } from "ahooks";
@@ -59,7 +59,7 @@ const Task: React.FC = () => {
                                     bordered={false}
                                     options={[
                                         { value: 'taskType', label: '任务分类' },
-                                        ...Object.keys(TaskEnum).map(key => ({ value: key, label: (TaskEnum as any)[key] }))
+                                        ...Object.keys(TaskTypeEnum).map(key => ({ value: key, label: (TaskTypeEnum as any)[key] }))
                                     ]}
                                 />
                                 <Select

+ 1 - 1
src/pages/User/Login/index.tsx

@@ -36,7 +36,7 @@ const LoginMessage: React.FC<{
 };
 
 const Login: React.FC = () => {
-    const [userLoginState, setUserLoginState] = useState<API.LoginResult>({});
+    const [userLoginState, setUserLoginState] = useState<API.Result>({});
     const [type, setType] = useState<string>('mobile');
     const { initialState, setInitialState } = useModel('@@initialState');
 

+ 4 - 7
src/requestErrorConfig.ts

@@ -41,10 +41,9 @@ export const errorConfig: RequestConfig = {
   errorConfig: {
     // 错误抛出
     errorThrower: (res) => {
-      const { success, data, code, msg, fail } =
-        res as unknown as ResponseStructure;
+      const { success, data, code, msg, fail } = res as unknown as ResponseStructure;
       if (!success) {
-        const error: any = new Error(msg);
+        let error: any = new Error(msg);
         error.name = 'BizError';
         error.info = { code, msg, fail, data };
         throw error; // 抛出自制的错误
@@ -55,9 +54,9 @@ export const errorConfig: RequestConfig = {
       const { response } = error;
       if (response && response.status) {
         const errorText = codeMessage[(response as any).status] || response.statusText;
-        const { status, url } = response;
+        const { status, data } = response;
         notification.error({
-          message: `请求错误 ${status}: ${url}`,
+          message: `请求错误 ${status}: ${data}`,
           description: errorText,
         });
       }
@@ -107,8 +106,6 @@ export const errorConfig: RequestConfig = {
                   }
                 });
               }
-            } else {
-              localStorage.removeItem('Admin-Token')
             }
             if (!msg) {
               sessionStorage.setItem('msg', 'true')

+ 11 - 1
src/services/ant-design-pro/api.ts

@@ -20,7 +20,7 @@ export async function outLogin(options?: { [key: string]: any }) {
 
 /** 登录接口 POST /api/login/account */
 export async function login(body: API.LoginParams, options?: { [key: string]: any }) {
-  return request<API.LoginResult>('/api/login/mobile', {
+  return request<API.Result>('/api/login/mobile', {
     method: 'POST',
     headers: {
       'Content-Type': 'application/json',
@@ -81,3 +81,13 @@ export async function removeRule(options?: { [key: string]: any }) {
     ...(options || {}),
   });
 }
+
+
+/** 获取oss GET /api/notices */
+export async function getOssInfo(params: { fileType: string, type: string }, options?: { [key: string]: any }) {
+  return request<API.Result>('/api/oss/form/upload', {
+    method: 'GET',
+    params,
+    ...(options || {}),
+  });
+}

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

@@ -10,7 +10,7 @@ declare namespace API {
     userName?: string;
   };
 
-  type LoginResult = {
+  type Result = {
     code?: number
     data?: any
     fail?: boolean;

+ 31 - 0
src/services/task-api/myTask.ts

@@ -0,0 +1,31 @@
+// @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 || {}),
+    });
+}

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

@@ -0,0 +1,60 @@
+// @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
+    }
+}

+ 20 - 0
src/utils/index.tsx

@@ -0,0 +1,20 @@
+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);
+    })
+}