shaogen1995 8 mesi fa
parent
commit
13c8d3769f

File diff suppressed because it is too large
+ 1 - 1
展示端/.vscode/file-note.json


+ 5 - 2
展示端/package.json

@@ -28,7 +28,10 @@
     "sass": "^1.55.0",
     "swiper": "^9.1.0",
     "typescript": "^4.8.4",
-    "web-vitals": "^2.1.4"
+    "web-vitals": "^2.1.4",
+    "dayjs": "^1.11.7",
+    "html2canvas": "^1.4.1",
+    "jspdf": "^2.5.1"
   },
   "scripts": {
     "dev": "react-app-rewired start",
@@ -62,4 +65,4 @@
     "react-app-rewired": "^2.2.1"
   },
   "homepage": "."
-}
+}

BIN
展示端/src/assets/img/order/err.png


BIN
展示端/src/assets/img/order/info.png


BIN
展示端/src/assets/img/order/succ.png


BIN
展示端/src/assets/img/order/top.png


+ 5 - 0
展示端/src/assets/styles/base.css

@@ -176,3 +176,8 @@ textarea {
     opacity: 1;
   }
 }
+.myBtnNo {
+  background-color: #d9d9d9 !important;
+  pointer-events: none !important;
+  color: #aeaeae !important;
+}

+ 7 - 0
展示端/src/assets/styles/base.less

@@ -212,3 +212,10 @@ textarea {
     opacity: 1;
   }
 }
+
+// 按钮禁用
+.myBtnNo {
+  background-color: #d9d9d9 !important;
+  pointer-events: none !important;
+  color: #aeaeae !important;
+}

+ 46 - 0
展示端/src/components/MyPopconfirm.tsx

@@ -0,0 +1,46 @@
+import React, { useMemo } from "react";
+import { Button, Popconfirm } from "antd";
+
+type Props = {
+  txtK: "删除" | "取消" | "重置密码" | "退出登录";
+  onConfirm: () => void;
+  Dom?: React.ReactNode;
+  loc?: "bottom";
+};
+
+function MyPopconfirm({ txtK, onConfirm, Dom, loc }: Props) {
+  const txt = useMemo(() => {
+    const obj = {
+      删除: ["删除后无法恢复,是否删除?", "删除"],
+      取消: ["放弃编辑后,信息将不会保存!", "放弃"],
+      重置密码: ["密码重制后为123456,是否重置?", "重置"],
+      退出登录: ["确定退出吗?", "确定"],
+    };
+    return Reflect.get(obj, txtK) || ["", ""];
+  }, [txtK]);
+
+  return (
+    <Popconfirm
+      placement={loc}
+      title={txt[0]}
+      okText={txt[1]}
+      cancelText="取消"
+      onConfirm={onConfirm}
+      okButtonProps={{ loading: false }}
+    >
+      {Dom ? (
+        Dom
+      ) : txtK === "删除" ? (
+        <Button size="small" type="text" danger>
+          {txtK}
+        </Button>
+      ) : (
+        <Button>{txtK}</Button>
+      )}
+    </Popconfirm>
+  );
+}
+
+const MemoMyPopconfirm = React.memo(MyPopconfirm);
+
+export default MemoMyPopconfirm;

+ 14 - 0
展示端/src/components/RouterOrder.tsx

@@ -34,6 +34,20 @@ const routerArr = [
     Com: React.lazy(() => import('@/pages/A4selectCourse'))
   },
   {
+    id: 5,
+    name: '填写信息',
+    path: '/order',
+    exact: true,
+    Com: React.lazy(() => import('@/pages/A5order'))
+  },
+  {
+    id: 6,
+    name: '我的申请',
+    path: '/my',
+    exact: true,
+    Com: React.lazy(() => import('@/pages/A6my'))
+  },
+  {
     id: 9,
     name: '找不到页面',
     path: '*',

+ 47 - 0
展示端/src/components/ZinfoPop/index.module.scss

@@ -0,0 +1,47 @@
+.ZinfoPop {
+  position: absolute;
+  z-index: 100;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  background-color: rgba(0, 0, 0, 0.6);
+  backdrop-filter: blur(8px);
+  padding: 40% 10% 0;
+  :global {
+    .ZIbox {
+      background-color: #fff;
+      border-radius: 10px;
+      display: flex;
+      padding: 40px 20px;
+      flex-direction: column;
+      justify-content: center;
+      align-items: center;
+      & > img {
+        width: 70px;
+      }
+      & > h2 {
+        color: var(--themeColor2);
+        font-size: 24px;
+        margin: 23px 0 10px;
+      }
+      & > p {
+        font-size: 16px;
+        margin-bottom: 37px;
+      }
+      & > div {
+        width: 110px;
+        height: 50px;
+        background: var(--themeColor2);
+        border-radius: 5px 5px 5px 5px;
+        display: flex;
+        justify-content: center;
+        align-items: center;
+        font-size: 16px;
+        font-weight: 700;
+        color: #fff;
+        letter-spacing: 4px;
+      }
+    }
+  }
+}

+ 37 - 0
展示端/src/components/ZinfoPop/index.tsx

@@ -0,0 +1,37 @@
+import React, { useMemo } from 'react'
+import styles from './index.module.scss'
+
+import succImg from '@/assets/img/order/succ.png'
+// import infoImg from '@/assets/img/order/info.png'
+// import errImg from '@/assets/img/order/err.png'
+
+type Props = {
+  type: 'succ' | 'info' | 'err'
+  callFu: () => void
+}
+
+function ZinfoPop({ type, callFu }: Props) {
+  const infoObj = useMemo(() => {
+    let obj = {
+      img: succImg,
+      tit1: '预约成功',
+      txt2: '工作人员将尽快与您取得联系'
+    }
+    return obj
+  }, [])
+
+  return (
+    <div id='openDom' className={styles.ZinfoPop}>
+      <div className={`ZIbox ZI${type}`}>
+        <img src={infoObj.img} alt='' />
+        <h2>{infoObj.tit1}</h2>
+        <p>{infoObj.txt2}</p>
+        <div onClick={callFu}>确认</div>
+      </div>
+    </div>
+  )
+}
+
+const MemoZinfoPop = React.memo(ZinfoPop)
+
+export default MemoZinfoPop

+ 1 - 1
展示端/src/pages/A1home/index.tsx

@@ -27,7 +27,7 @@ const list = [
     img: icon3,
     color: '#f18101',
     bac: '#fef3e6',
-    path: '/xxx'
+    path: '/my'
   }
 ]
 

+ 0 - 5
展示端/src/pages/A3selectDay/index.module.scss

@@ -44,11 +44,6 @@
                 background-color: var(--themeColor);
                 color: #fff;
               }
-              .A3no {
-                background-color: #d9d9d9;
-                pointer-events: none;
-                color: #aeaeae;
-              }
             }
           }
         }

+ 2 - 2
展示端/src/pages/A3selectDay/index.tsx

@@ -71,7 +71,7 @@ function A3selectDay() {
                     onClick={() => setAcObj({ id: item.id, txt: '上午' })}
                     className={classNames(
                       item.id === acObj.id && acObj.txt === '上午' ? 'A3ac' : '',
-                      item.shang ? '' : 'A3no'
+                      item.shang ? '' : 'myBtnNo'
                     )}
                   >
                     上午{item.shang ? '' : '(已满)'}
@@ -80,7 +80,7 @@ function A3selectDay() {
                     onClick={() => setAcObj({ id: item.id, txt: '下午' })}
                     className={classNames(
                       item.id === acObj.id && acObj.txt === '下午' ? 'A3ac' : '',
-                      item.xia ? '' : 'A3no'
+                      item.xia ? '' : 'myBtnNo'
                     )}
                   >
                     下午{item.xia ? '' : '(已满)'}

+ 2 - 0
展示端/src/pages/A4selectCourse/index.tsx

@@ -4,6 +4,7 @@ import TopCom from '@/components/TopCom'
 
 import xingImg from '@/assets/img/selectCourse/xing.png'
 import A4look from './A4look'
+import history from '@/utils/history'
 
 export type SonType = {
   id: number
@@ -61,6 +62,7 @@ function A4selectCourse() {
   const arrangeFu = useCallback(() => {
     if (1 + 1 === 2) {
       // 跳预约页面
+      history.push('/order')
     } else {
       // 去团队认证
     }

+ 244 - 0
展示端/src/pages/A5order/index.module.scss

@@ -0,0 +1,244 @@
+.A5order {
+  position: relative;
+  :global {
+    .A5main {
+      padding: 5px 0 14px;
+      background-image: url('../../assets/img/selectCourse/bg.jpg');
+      background-size: cover;
+      height: calc(100% - 60px);
+
+      .A5list {
+        width: 100%;
+        height: calc(100% - 80px);
+        margin-bottom: 20px;
+        overflow-y: auto;
+
+        .A5listMain {
+          padding: 0 16px;
+        }
+
+        .A5lTop {
+          height: 130px;
+          width: 100%;
+          background-image: url(../../assets/img/order/top.png);
+          background-size: 100% 100%;
+          display: flex;
+          flex-direction: column;
+          justify-content: center;
+          padding: 0 0 4px 13%;
+          font-size: 16px;
+          color: #828282;
+          & > p {
+            margin-bottom: 6px;
+            & > span {
+              font-weight: 700;
+              color: var(--themeColor2);
+            }
+          }
+        }
+        .A5lKaBox {
+          padding: 0 6px;
+
+          .A5btn {
+            position: absolute;
+            z-index: 10;
+            width: 90%;
+            bottom: 3%;
+            margin: auto;
+            height: 60px;
+            background-color: var(--themeColor2);
+            text-align: center;
+            color: #fff;
+            font-size: 20px;
+            font-weight: 700;
+            border-radius: 4px;
+          }
+
+          .A5lKa {
+            margin-bottom: 15px;
+            background-color: #fff;
+            border-radius: 5px;
+            box-shadow: 0px 4px 10px 0px rgba(0, 0, 0, 0.25);
+            padding: 18px 0 10px;
+            .A5tit {
+              padding-left: 25px;
+              font-weight: 700;
+              font-size: 18px;
+              color: var(--themeColor);
+              position: relative;
+              margin-bottom: 20px;
+              &::before {
+                content: '';
+                position: absolute;
+                width: 6px;
+                height: 100%;
+                top: 0;
+                left: 0;
+                background-color: var(--themeColor);
+              }
+            }
+
+            .ant-form-item {
+              padding: 0 13px;
+              margin-bottom: 12px;
+              height: 40px;
+              .ant-row {
+                display: flex;
+                .ant-form-item-label {
+                  width: 110px;
+                  flex: none;
+                  display: block;
+                }
+                .ant-form-item-control {
+                  flex: none;
+                  display: block;
+                  width: calc(100% - 110px);
+                }
+
+                .ant-input {
+                  border: none !important;
+                  box-shadow: none !important;
+                  border-bottom: 1px solid #bebebe !important;
+                  border-radius: 0;
+                }
+              }
+
+              .ant-input-number {
+                width: 100%;
+                border: none !important;
+                box-shadow: none !important;
+                border-bottom: 1px solid #bebebe !important;
+                border-radius: 0;
+              }
+            }
+            .A5llmove {
+              label {
+                padding-left: 12px;
+              }
+            }
+            .A5Text {
+              label {
+                padding-left: 12px;
+              }
+
+              min-height: 80px;
+              height: auto;
+
+              textarea {
+                min-height: 80px !important;
+              }
+            }
+            .A5Text20 {
+              height: 41px;
+              textarea {
+                min-height: 30px !important;
+              }
+            }
+
+            // 文本域不换行问题
+            .A5TextPdf {
+              display: flex;
+              padding: 0 13px;
+              margin-bottom: 12px;
+              min-height: 40px;
+              span {
+                color: #ff4d4f;
+                margin-right: 4px;
+              }
+              & > div {
+                &:nth-of-type(1) {
+                  width: 110px;
+                }
+                &:nth-of-type(2) {
+                  width: calc(100% - 110px);
+                  border-bottom: 1px solid #bebebe;
+                  padding-bottom: 10px;
+                }
+              }
+            }
+
+            // 下面下载
+            .A5LA1 {
+              display: flex;
+              justify-content: space-between;
+              padding: 0 23px;
+              font-size: 16px;
+              height: 40px;
+              align-items: center;
+              margin-bottom: 10px;
+
+              .A5LA1ll {
+                height: 100%;
+                width: calc(100% - 70px);
+                span {
+                  display: block;
+                  font-size: 10px;
+                  color: #999;
+                }
+              }
+
+              .A5LA1btn {
+                height: 100%;
+                width: 60px;
+                background-color: var(--themeColor2);
+                display: flex;
+                justify-content: center;
+                align-items: center;
+                border-radius: 4px;
+                color: #fff;
+              }
+              div.A5LA1btn {
+                background-color: var(--themeColor);
+              }
+            }
+            .A5file {
+              font-size: 16px;
+              padding: 0 23px;
+              position: relative;
+
+              & > img {
+                border-radius: 4px;
+                width: 100%;
+                object-fit: contain !important;
+                display: block;
+              }
+
+              .A5file2 {
+                display: flex;
+                flex-direction: column;
+                z-index: 10;
+                color: #fff;
+                position: absolute;
+                top: 10px;
+                right: 33px;
+                font-size: 24px;
+                & > span {
+                  background-color: rgba(0, 0, 0, 0.6);
+                  padding: 5px;
+                  border-radius: 6px;
+                  margin-bottom: 10px;
+                }
+              }
+            }
+
+            .A5LAkuang {
+              margin-top: 10px;
+              padding: 0 23px;
+              margin-bottom: 8px;
+              & > div {
+                background-color: #f3f8fd;
+                border-radius: 4px;
+                font-size: 14px;
+                color: var(--themeColor);
+                padding: 12px 0 12px 20px;
+                & > p {
+                  margin-bottom: 5px;
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}

+ 299 - 0
展示端/src/pages/A5order/index.tsx

@@ -0,0 +1,299 @@
+import React, { useCallback, useEffect, useRef, useState } from 'react'
+import styles from './index.module.scss'
+import TopCom from '@/components/TopCom'
+import classNames from 'classnames'
+import { Button, Form, FormInstance, Input, InputNumber } from 'antd'
+import TextArea from 'antd/es/input/TextArea'
+import { EyeOutlined, CloseOutlined } from '@ant-design/icons'
+import MyPopconfirm from '@/components/MyPopconfirm'
+import { ImageViewer } from 'antd-mobile'
+import { domShowFu } from '@/utils/domShow'
+import dayjs from 'dayjs'
+import htmlToPdf2 from '@/utils/htmlToPdf2'
+import { MessageFu } from '@/utils/message'
+import ZinfoPop from '@/components/ZinfoPop'
+import history from '@/utils/history'
+
+type FormType = {
+  name: string
+  phone: string
+  ID: string
+  num1: null | number
+  num2: null | null
+  jigou: string
+  miaoshu: string
+}
+
+function A5order() {
+  useEffect(() => {
+    FormBoxRef.current?.setFieldsValue({
+      name: '王大锤',
+      phone: '18702025091',
+      ID: '421083199504071212',
+      num1: 3,
+      num2: null,
+      jigou: '机构机构',
+      miaoshu: ''
+    })
+  }, [])
+
+  // 表单的ref
+  const FormBoxRef = useRef<FormInstance>(null)
+
+  // 没有通过校验
+  const onFinishFailed = useCallback(() => {}, [])
+
+  // 点击提交的页面元素显示和隐藏
+  const [time, setTime] = useState('')
+  // 导出pdf文本域换行问题
+  const [tetx, setText] = useState({
+    1: '',
+    2: ''
+  })
+
+  // 打开提示弹窗
+  const [titPop, setTitPop] = useState('')
+
+  //  通过校验点击确定
+  const onFinish = useCallback(async (values: FormType) => {
+    setTitPop('succ')
+
+    console.log(123, values)
+    // domShowFu('#AsyncSpinLoding', true)
+
+    // setText({
+    //   1: values.jigou.replaceAll(/(\n|\r|\r\n)/g, '<br />'),
+    //   2: values.miaoshu.replaceAll(/(\n|\r|\r\n)/g, '<br />')
+    // })
+
+    // const time = dayjs(new Date()).format('YYYY-MM-DD HH:mm')
+    // setTime(time)
+    // window.setTimeout(() => {
+    //   const dom = document.querySelector('.A5listMain') as HTMLDivElement
+    //   if (dom) {
+    //     const name = '预约申请单'
+
+    //     htmlToPdf2(dom, name, () => {
+    //       // 打开预约成功的弹窗
+    //       // setTime('')
+    //       // domShowFu('#AsyncSpinLoding', false)
+    //     })
+    //   } else {
+    //     MessageFu.warning('找不到元素!')
+    //     setTime('')
+    //     domShowFu('#AsyncSpinLoding', false)
+    //   }
+    // }, 500)
+  }, [])
+
+  const [fileUrl, setFileUrl] = useState(
+    'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png?undefined'
+  )
+
+  return (
+    <div className={styles.A5order}>
+      <TopCom txt='填写信息' />
+      <div className={classNames('A5main')}>
+        <div className='A5list'>
+          <div className='A5listMain'>
+            {/* 顶部 */}
+            <div className='A5lTop'>
+              <p>
+                <span>预约日期:</span>11月21日上午
+              </p>
+              <p>
+                <span>预约课程:</span>英语课程英语课程英语
+              </p>
+              {time ? (
+                <p>
+                  <span>申请时间:</span>
+                  {time}
+                </p>
+              ) : null}
+            </div>
+            <div className='A5lKaBox'>
+              <Form
+                ref={FormBoxRef}
+                name='basic'
+                // labelCol={{ span: 3 }}
+                onFinish={onFinish}
+                onFinishFailed={onFinishFailed}
+                autoComplete='off'
+                scrollToFirstError
+              >
+                {/* 第一个卡片 */}
+                <div className='A5lKa'>
+                  <div className='A5tit'>负责人信息</div>
+
+                  <Form.Item
+                    label='负责人姓名'
+                    name='name'
+                    rules={[{ required: true, message: '请输入负责人姓名!' }]}
+                    getValueFromEvent={e => e.target.value.replace(/\s+/g, '')}
+                  >
+                    <Input placeholder='请输入内容,不超过6个字' maxLength={6} />
+                  </Form.Item>
+
+                  <Form.Item
+                    label='联系方式'
+                    name='phone'
+                    rules={[
+                      { required: true, message: '请输入联系方式!' },
+                      {
+                        pattern: /^1[3-9][0-9]{9}$/,
+                        message: '请输入正确格式的手机号!'
+                      }
+                    ]}
+                  >
+                    <Input placeholder='请输入11位数字' maxLength={11} />
+                  </Form.Item>
+
+                  <Form.Item
+                    label='身份证号'
+                    name='ID'
+                    rules={[
+                      { required: true, message: '请输入身份证号!' },
+                      {
+                        pattern:
+                          /^[1-9]\d{5}(19|20)\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$/,
+                        message: '请输入正确格式的身份证号!'
+                      }
+                    ]}
+                  >
+                    <Input placeholder='请输入18位证件编码' maxLength={18} />
+                  </Form.Item>
+                </div>
+
+                {/* 第二个卡片 */}
+                <div className='A5lKa'>
+                  <div className='A5tit'>团队信息</div>
+
+                  <Form.Item
+                    label='参观学生人数'
+                    name='num1'
+                    rules={[{ required: true, message: '请输入参观学生人数!' }]}
+                  >
+                    <InputNumber min={0} max={999} precision={0} placeholder='请输入数字' />
+                  </Form.Item>
+
+                  <Form.Item className='A5llmove' label='随堂老师人数' name='num2'>
+                    <InputNumber
+                      min={0}
+                      max={999}
+                      precision={0}
+                      placeholder={time ? ' - ' : '请输入数字'}
+                    />
+                  </Form.Item>
+
+                  {/* 文本域不换行问题 */}
+                  {time ? (
+                    <>
+                      <div className='A5TextPdf'>
+                        <div>
+                          <span>*</span>所属机构
+                        </div>
+                        <div dangerouslySetInnerHTML={{ __html: tetx[1] || ' - ' }}></div>
+                      </div>
+                      <div className='A5TextPdf'>
+                        <div>
+                          <span style={{ opacity: 0 }}>*</span>团队描述
+                        </div>
+                        <div dangerouslySetInnerHTML={{ __html: tetx[2] || ' - ' }}></div>
+                      </div>
+                    </>
+                  ) : (
+                    <>
+                      <Form.Item
+                        className='A5Text20'
+                        label='所属机构'
+                        name='jigou'
+                        rules={[{ required: true, message: '请输入所属机构!' }]}
+                        getValueFromEvent={e => e.target.value.replace(/\s+/g, '')}
+                      >
+                        <TextArea autoSize placeholder='请输入内容,不超过20个字' maxLength={20} />
+                      </Form.Item>
+
+                      <Form.Item label='团队描述' name='miaoshu' className='A5Text'>
+                        <TextArea
+                          autoSize
+                          maxLength={200}
+                          placeholder='请输入内容,不超过200个字'
+                        />
+                      </Form.Item>
+                    </>
+                  )}
+                </div>
+
+                {time ? null : (
+                  <Button className='A5btn' type='primary' htmlType='submit'>
+                    提交
+                  </Button>
+                )}
+              </Form>
+
+              {/* 第三个卡片 */}
+              <div className='A5lKa' hidden={!!time && !fileUrl}>
+                <div className='A5tit'>健康生活教育课程预约单</div>
+
+                {time ? null : (
+                  <div className='A5LA1'>
+                    预约单模板
+                    <a className='A5LA1btn' href='xxx' download='预约单模板'>
+                      下载
+                    </a>
+                  </div>
+                )}
+
+                {time ? null : (
+                  <div className='A5LA1'>
+                    <div className='A5LA1ll'>
+                      <p>上传填写结果</p>
+                      <span>仅限jpg,png格式;大小不超过5M;最多1个文件</span>
+                    </div>
+                    <div className='A5LA1btn' hidden={!!fileUrl}>
+                      上传
+                    </div>
+                  </div>
+                )}
+
+                {fileUrl ? (
+                  <div className='A5file'>
+                    <img src={fileUrl} alt='' />
+                    {time ? null : (
+                      <div className='A5file2'>
+                        <EyeOutlined onClick={() => ImageViewer.show({ image: fileUrl })} />
+
+                        <MyPopconfirm
+                          txtK='删除'
+                          onConfirm={() => setFileUrl('')}
+                          Dom={<CloseOutlined className='clearCover' />}
+                        />
+                      </div>
+                    )}
+                  </div>
+                ) : null}
+                {time ? null : (
+                  <div className='A5LAkuang'>
+                    <div>
+                      <p>1.下载模板</p>
+                      <p>2.填写模板 </p>
+                      <p>3.将填写结果导出PNG或JPG图片格式</p>
+                      <p>4.上传填写结果</p>
+                    </div>
+                  </div>
+                )}
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+
+      {/* 预约成功的弹窗 */}
+      {titPop ? <ZinfoPop type={titPop as 'succ'} callFu={() => history.push('/my')} /> : null}
+    </div>
+  )
+}
+
+const MemoA5order = React.memo(A5order)
+
+export default MemoA5order

+ 4 - 0
展示端/src/pages/A6my/index.module.scss

@@ -0,0 +1,4 @@
+.A6my {
+  :global {
+  }
+}

+ 13 - 0
展示端/src/pages/A6my/index.tsx

@@ -0,0 +1,13 @@
+import React from 'react'
+import styles from './index.module.scss'
+function A6my() {
+  return (
+    <div className={styles.A6my}>
+      <h1>A6my</h1>
+    </div>
+  )
+}
+
+const MemoA6my = React.memo(A6my)
+
+export default MemoA6my

+ 118 - 0
展示端/src/utils/htmlToPdf2.ts

@@ -0,0 +1,118 @@
+// 这个导出多张pdf,文件小。但是分页中间有断开的问题
+import html2canvas from 'html2canvas'
+import JsPDF from 'jspdf'
+
+/**
+ * @param  ele          要生成 pdf 的DOM元素(容器)
+ * @param  padfName     PDF文件生成后的文件名字
+ * @param  callBackFu   完成之后的回调函数
+ * */
+
+function downloadPDF2(ele: HTMLDivElement, pdfName: string, callBackFu: () => void) {
+  const eleW = ele.offsetWidth // 获得该容器的宽
+  const eleH = ele.offsetHeight // 获得该容器的高
+
+  const eleOffsetTop = ele.offsetTop // 获得该容器到文档顶部的距离
+  const eleOffsetLeft = ele.offsetLeft // 获得该容器到文档最左的距离
+
+  const canvas = document.createElement('canvas')
+  let abs = 0
+
+  const win_in = document.documentElement.clientWidth || document.body.clientWidth // 获得当前可视窗口的宽度(不包含滚动条)
+  const win_out = window.innerWidth // 获得当前窗口的宽度(包含滚动条)
+
+  if (win_out > win_in) {
+    // abs = (win_o - win_i)/2;    // 获得滚动条长度的一半
+    abs = (win_out - win_in) / 2 // 获得滚动条宽度的一半
+    // console.log(a, '新abs');
+  }
+
+  canvas.width = eleW * 2 // 将画布宽&&高放大两倍
+  canvas.height = eleH * 2
+
+  const context = canvas.getContext('2d')!
+
+  context.scale(2, 2)
+
+  context.translate(-eleOffsetLeft - abs, -eleOffsetTop)
+  // 这里默认横向没有滚动条的情况,因为offset.left(),有无滚动条的时候存在差值,因此
+  // translate的时候,要把这个差值去掉
+
+  // html2canvas(element).then( (canvas)=>{ //报错
+  // html2canvas(element[0]).then( (canvas)=>{
+  html2canvas(ele, {
+    scale: 1.5,
+    // dpi: 300,
+    // allowTaint: true,  //允许 canvas 污染, allowTaint参数要去掉,否则是无法通过toDataURL导出canvas数据的
+    useCORS: true, // 允许canvas画布内 可以跨域请求外部链接图片, 允许跨域请求。
+    backgroundColor: '#FFFFFF'
+  }).then(canvas => {
+    const contentWidth = canvas.width
+    const contentHeight = canvas.height
+    // 一页pdf显示html页面生成的canvas高度;
+    const pageHeight = (contentWidth / 592.28) * 841.89
+    // 未生成pdf的html页面高度
+    let leftHeight = contentHeight
+    // 页面偏移
+    let position = 0
+    // a4纸的尺寸[595.28,841.89],html页面生成的canvas在pdf中图片的宽高
+    const imgWidth = 595.28
+    const imgHeight = (595.28 / contentWidth) * contentHeight
+
+    const pageData = canvas.toDataURL('image/jpeg', 1.0)
+
+    const pdf = new JsPDF(undefined, 'pt', 'a4')
+
+    // 有两个高度需要区分,一个是html页面的实际高度,和生成pdf的页面高度(841.89)
+    // 当内容未超过pdf一页显示的范围,无需分页
+    if (leftHeight < pageHeight) {
+      // 在pdf.addImage(pageData, 'JPEG', 左,上,宽度,高度)设置在pdf中显示;
+      pdf.addImage(pageData, 'JPEG', 0, 0, imgWidth, imgHeight)
+      // pdf.addImage(pageData, 'JPEG', 20, 40, imgWidth, imgHeight);
+    } else {
+      // 分页
+      while (leftHeight > 0) {
+        pdf.addImage(pageData, 'JPEG', 0, position, imgWidth, imgHeight)
+        leftHeight -= pageHeight
+        position -= 841.89
+        // 避免添加空白页
+        if (leftHeight > 0) {
+          pdf.addPage()
+        }
+      }
+    }
+
+    // -------转 formData 上传后端
+    // 获取pdf的base64
+    const pdfBase64Str = pdf.output('datauristring')
+    const myfile = dataURLtoFile(pdfBase64Str, pdfName) //调用一下下面的转文件流函数
+
+    const formdata = new FormData()
+    formdata.append('file', myfile) // 文件对象
+
+    console.log('formData上传后端', formdata)
+
+    // 可动态生成(下载)
+    // pdf.save(pdfName)
+    // 成功之后的回调
+    callBackFu()
+  })
+}
+
+/*
+将base64转换为文件,接收2个参数,第一是base64,第二个是文件名字
+最后返回文件对象
+*/
+const dataURLtoFile = (dataurl: any, filename: string) => {
+  var arr = dataurl.split(','),
+    mime = arr[0].match(/:(.*?);/)[1],
+    bstr = atob(arr[1]),
+    n = bstr.length,
+    u8arr = new Uint8Array(n)
+  while (n--) {
+    u8arr[n] = bstr.charCodeAt(n)
+  }
+  return new File([u8arr], filename, { type: mime })
+}
+
+export default downloadPDF2

+ 109 - 1
展示端/yarn.lock

@@ -2349,6 +2349,11 @@
   resolved "https://registry.npmjs.org/@types/qs/-/qs-6.9.9.tgz#66f7b26288f6799d279edf13da7ccd40d2fa9197"
   integrity sha512-wYLxw35euwqGvTDx6zfY1vokBFnsK0HNrzc6xNHchxfO2hpuRg74GbkEW7e3sSmPvj0TjCDT1VCa6OtHXnubsg==
 
+"@types/raf@^3.4.0":
+  version "3.4.3"
+  resolved "https://registry.npmmirror.com/@types/raf/-/raf-3.4.3.tgz#85f1d1d17569b28b8db45e16e996407a56b0ab04"
+  integrity sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==
+
 "@types/range-parser@*":
   version "1.2.6"
   resolved "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.6.tgz#7cb33992049fd7340d5b10c0098e104184dfcd2a"
@@ -3162,6 +3167,11 @@ at-least-node@^1.0.0:
   resolved "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2"
   integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==
 
+atob@^2.1.2:
+  version "2.1.2"
+  resolved "https://registry.npmmirror.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9"
+  integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==
+
 autoprefixer@^10.4.13:
   version "10.4.16"
   resolved "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.16.tgz#fad1411024d8670880bdece3970aa72e3572feb8"
@@ -3341,6 +3351,11 @@ balanced-match@^1.0.0:
   resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
   integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
 
+base64-arraybuffer@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.npmmirror.com/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz#1c37589a7c4b0746e34bd1feb951da2df01c1bdc"
+  integrity sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==
+
 batch@0.6.1:
   version "0.6.1"
   resolved "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16"
@@ -3449,6 +3464,11 @@ bser@2.1.1:
   dependencies:
     node-int64 "^0.4.0"
 
+btoa@^1.2.1:
+  version "1.2.1"
+  resolved "https://registry.npmmirror.com/btoa/-/btoa-1.2.1.tgz#01a9909f8b2c93f6bf680ba26131eb30f7fa3d73"
+  integrity sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==
+
 buffer-from@^1.0.0:
   version "1.1.2"
   resolved "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5"
@@ -3521,6 +3541,20 @@ caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001538, caniuse-lite@^1.0.30001541:
   resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001551.tgz#1f2cfa8820bd97c971a57349d7fd8f6e08664a3e"
   integrity sha512-vtBAez47BoGMMzlbYhfXrMV1kvRF2WP/lqiMuDu1Sb4EE4LKEgjopFDSRtZfdVnslNRpOqV/woE+Xgrwj6VQlg==
 
+canvg@^3.0.6:
+  version "3.0.10"
+  resolved "https://registry.npmmirror.com/canvg/-/canvg-3.0.10.tgz#8e52a2d088b6ffa23ac78970b2a9eebfae0ef4b3"
+  integrity sha512-qwR2FRNO9NlzTeKIPIKpnTY6fqwuYSequ8Ru8c0YkYU7U0oW+hLUvWadLvAu1Rl72OMNiFhoLu4f8eUjQ7l/+Q==
+  dependencies:
+    "@babel/runtime" "^7.12.5"
+    "@types/raf" "^3.4.0"
+    core-js "^3.8.3"
+    raf "^3.4.1"
+    regenerator-runtime "^0.13.7"
+    rgbcolor "^1.0.1"
+    stackblur-canvas "^2.0.0"
+    svg-pathdata "^6.0.3"
+
 case-sensitive-paths-webpack-plugin@^2.4.0:
   version "2.4.0"
   resolved "https://registry.npmjs.org/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.4.0.tgz#db64066c6422eed2e08cc14b986ca43796dbc6d4"
@@ -3813,6 +3847,11 @@ core-js@^3.19.2:
   resolved "https://registry.npmjs.org/core-js/-/core-js-3.33.0.tgz#70366dbf737134761edb017990cf5ce6c6369c40"
   integrity sha512-HoZr92+ZjFEKar5HS6MC776gYslNOKHt75mEBKWKnPeFDpZ6nH5OeF3S6HFT1mUAUZKrzkez05VboaX8myjSuw==
 
+core-js@^3.6.0, core-js@^3.8.3:
+  version "3.39.0"
+  resolved "https://registry.npmmirror.com/core-js/-/core-js-3.39.0.tgz#57f7647f4d2d030c32a72ea23a0555b2eaa30f83"
+  integrity sha512-raM0ew0/jJUqkJ0E6e8UDtl+y/7ktFivgWvqw8dNSQeNWoSDLvQ1H/RN3aPXB9tBd4/FhyR4RDPGhsNIMsAn7g==
+
 core-util-is@~1.0.0:
   version "1.0.3"
   resolved "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85"
@@ -3873,6 +3912,13 @@ css-has-pseudo@^3.0.4:
   dependencies:
     postcss-selector-parser "^6.0.9"
 
+css-line-break@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.npmmirror.com/css-line-break/-/css-line-break-2.1.0.tgz#bfef660dfa6f5397ea54116bb3cb4873edbc4fa0"
+  integrity sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==
+  dependencies:
+    utrie "^1.0.2"
+
 css-loader@^6.5.1:
   version "6.8.1"
   resolved "https://registry.npmjs.org/css-loader/-/css-loader-6.8.1.tgz#0f8f52699f60f5e679eab4ec0fcd68b8e8a50a88"
@@ -4319,6 +4365,11 @@ domhandler@^4.0.0, domhandler@^4.2.0, domhandler@^4.3.1:
   dependencies:
     domelementtype "^2.2.0"
 
+dompurify@^2.5.4:
+  version "2.5.8"
+  resolved "https://registry.npmmirror.com/dompurify/-/dompurify-2.5.8.tgz#2809d89d7e528dc7a071dea440d7376df676f824"
+  integrity sha512-o1vSNgrmYMQObbSSvF/1brBYEQPHhV1+gsmrusO7/GXtp1T9rCS8cXFqVxK/9crT1jA6Ccv+5MTSjBNqr7Sovw==
+
 domutils@^1.7.0:
   version "1.7.0"
   resolved "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a"
@@ -4999,6 +5050,11 @@ fb-watchman@^2.0.0:
   dependencies:
     bser "2.1.1"
 
+fflate@^0.8.1:
+  version "0.8.2"
+  resolved "https://registry.npmmirror.com/fflate/-/fflate-0.8.2.tgz#fc8631f5347812ad6028bbe4a2308b2792aa1dea"
+  integrity sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==
+
 file-entry-cache@^6.0.1:
   version "6.0.1"
   resolved "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027"
@@ -5508,6 +5564,14 @@ html-webpack-plugin@^5.5.0:
     pretty-error "^4.0.0"
     tapable "^2.0.0"
 
+html2canvas@^1.0.0-rc.5, html2canvas@^1.4.1:
+  version "1.4.1"
+  resolved "https://registry.npmmirror.com/html2canvas/-/html2canvas-1.4.1.tgz#7cef1888311b5011d507794a066041b14669a543"
+  integrity sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==
+  dependencies:
+    css-line-break "^2.1.0"
+    text-segmentation "^1.0.3"
+
 htmlparser2@^6.1.0:
   version "6.1.0"
   resolved "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz#c4d762b6c3371a05dbe65e94ae43a9f845fb8fb7"
@@ -6718,6 +6782,21 @@ jsonpointer@^5.0.0:
   resolved "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz#2110e0af0900fd37467b5907ecd13a7884a1b559"
   integrity sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==
 
+jspdf@^2.5.1:
+  version "2.5.2"
+  resolved "https://registry.npmmirror.com/jspdf/-/jspdf-2.5.2.tgz#3c35bb1063ee3ad9428e6353852b0d685d1f923a"
+  integrity sha512-myeX9c+p7znDWPk0eTrujCzNjT+CXdXyk7YmJq5nD5V7uLLKmSXnlQ/Jn/kuo3X09Op70Apm0rQSnFWyGK8uEQ==
+  dependencies:
+    "@babel/runtime" "^7.23.2"
+    atob "^2.1.2"
+    btoa "^1.2.1"
+    fflate "^0.8.1"
+  optionalDependencies:
+    canvg "^3.0.6"
+    core-js "^3.6.0"
+    dompurify "^2.5.4"
+    html2canvas "^1.0.0-rc.5"
+
 "jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.3.3:
   version "3.3.5"
   resolved "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz#4766bd05a8e2a11af222becd19e15575e52a853a"
@@ -8862,7 +8941,7 @@ regenerate@^1.4.2:
   resolved "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a"
   integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==
 
-regenerator-runtime@^0.13.9:
+regenerator-runtime@^0.13.7, regenerator-runtime@^0.13.9:
   version "0.13.11"
   resolved "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9"
   integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==
@@ -9014,6 +9093,11 @@ reusify@^1.0.4:
   resolved "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
   integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
 
+rgbcolor@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.npmmirror.com/rgbcolor/-/rgbcolor-1.0.1.tgz#d6505ecdb304a6595da26fa4b43307306775945d"
+  integrity sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==
+
 rimraf@^3.0.0, rimraf@^3.0.2:
   version "3.0.2"
   resolved "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
@@ -9437,6 +9521,11 @@ stack-utils@^2.0.3:
   dependencies:
     escape-string-regexp "^2.0.0"
 
+stackblur-canvas@^2.0.0:
+  version "2.7.0"
+  resolved "https://registry.npmmirror.com/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz#af931277d0b5096df55e1f91c530043e066989b6"
+  integrity sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==
+
 stackframe@^1.3.4:
   version "1.3.4"
   resolved "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz#b881a004c8c149a5e8efef37d51b16e412943310"
@@ -9687,6 +9776,11 @@ svg-parser@^2.0.2:
   resolved "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz#fdc2e29e13951736140b76cb122c8ee6630eb6b5"
   integrity sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==
 
+svg-pathdata@^6.0.3:
+  version "6.0.3"
+  resolved "https://registry.npmmirror.com/svg-pathdata/-/svg-pathdata-6.0.3.tgz#80b0e0283b652ccbafb69ad4f8f73e8d3fbf2cac"
+  integrity sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==
+
 svgo@^1.2.2:
   version "1.3.2"
   resolved "https://registry.npmjs.org/svgo/-/svgo-1.3.2.tgz#b6dc511c063346c9e415b81e43401145b96d4167"
@@ -9822,6 +9916,13 @@ test-exclude@^6.0.0:
     glob "^7.1.4"
     minimatch "^3.0.4"
 
+text-segmentation@^1.0.3:
+  version "1.0.3"
+  resolved "https://registry.npmmirror.com/text-segmentation/-/text-segmentation-1.0.3.tgz#52a388159efffe746b24a63ba311b6ac9f2d7943"
+  integrity sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==
+  dependencies:
+    utrie "^1.0.2"
+
 text-table@^0.2.0:
   version "0.2.0"
   resolved "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
@@ -10180,6 +10281,13 @@ utils-merge@1.0.1:
   resolved "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
   integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==
 
+utrie@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.npmmirror.com/utrie/-/utrie-1.0.2.tgz#d42fe44de9bc0119c25de7f564a6ed1b2c87a645"
+  integrity sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==
+  dependencies:
+    base64-arraybuffer "^1.0.2"
+
 uuid@^8.3.2:
   version "8.3.2"
   resolved "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"