Browse Source

初始化用户 和编辑器

黄琪勇 3 months ago
parent
commit
600f0d1d19

+ 1 - 1
.env.development

@@ -1,2 +1,2 @@
 
-VITE_APP_URL = "https://dev.resource.colexiu.com/cbs-app"
+VITE_APP_URL = "/pptApi"

+ 29 - 68
.eslintrc.cjs

@@ -1,77 +1,38 @@
 /* eslint-env node */
-require('@rushstack/eslint-patch/modern-module-resolution')
+require("@rushstack/eslint-patch/modern-module-resolution")
 
 module.exports = {
   root: true,
-  extends: [
-    'plugin:vue/vue3-essential',
-    'eslint:recommended',
-    '@vue/eslint-config-typescript'
-  ],
+  extends: ["plugin:vue/vue3-essential", "eslint:recommended", "@vue/eslint-config-typescript", "plugin:prettier/recommended"],
   parserOptions: {
-    ecmaVersion: 'latest'
+    ecmaVersion: "latest"
   },
   rules: {
-    'curly': ['error', 'multi-line'],
-    'eqeqeq': ['error', 'always'],
-    'semi': ['error', 'never'],
-    'indent': ['error', 2, { 
-      'SwitchCase': 1,
-    }],
-    'quotes': ['error', 'single', {
-      'avoidEscape': true,
-      'allowTemplateLiterals': true,
-    }],
-    'key-spacing': ['error', {
-      'beforeColon': false,
-      'afterColon': true,
-      'mode': 'strict',
-    }],
-    'no-empty': 'error',
-    'no-else-return': 'error',
-    'no-multi-spaces': 'error',
-    'require-await': 'error',
-    'brace-style': ['error', 'stroustrup'],
-    'spaced-comment': ['error', 'always'],
-    'arrow-spacing': 'error',
-    'no-duplicate-imports': 'error',
-    'comma-spacing': ['error', {
-      'before': false,
-      'after': true,
-    }],
-    'default-case': 'error',
-    'consistent-this': ['error', '_this'],
-    'max-depth': ['error', 8],
-    'max-lines': ['error', 1000],
-    'no-multi-str': 'error',
-    'space-infix-ops': 'error',
-    'space-before-blocks': ['error', 'always'],
-    'space-before-function-paren': ['error', {
-      'named': 'never',
-      'anonymous': 'never',
-      'asyncArrow': 'always',
-    }],
-    'keyword-spacing': ['error'],
-    'prefer-const': 'error',
-    'no-useless-return': 'error',
-    'array-bracket-spacing': 'error',
-    'no-useless-escape': 'off',
-    'no-eval': 'error',
-    'no-var': 'error',
-    'no-with': 'error',
-    'no-alert': 'warn',
-    'no-console': 'warn',
-    'no-debugger': 'error',
-    '@typescript-eslint/explicit-module-boundary-types': 'off',
-    '@typescript-eslint/ban-types': ['error', {
-      'extendDefaults': true,
-      'types': {
-        '{}': false,
-      },
-    }],
-    '@typescript-eslint/no-non-null-assertion': 'off',
-    '@typescript-eslint/consistent-type-imports': 'error',
-    'vue/multi-word-component-names': 'off',
-    'vue/no-reserved-component-names': 'off',
+    "prettier/prettier": "error",
+    "no-debugger": process.env.NODE_ENV == "production" ? 2 : 1,
+    "no-unreachable": process.env.NODE_ENV == "production" ? 2 : 1, //return 警告不报错
+    "no-undef": "off", // 没有声明的变量
+    "vue/no-v-html": "off",
+    "no-irregular-whitespace": "off",
+    "vue/html-self-closing": [
+      "error",
+      {
+        html: {
+          void: "always",
+          normal: "never",
+          component: "always"
+        },
+        svg: "always",
+        math: "always"
+      }
+    ],
+    "vue/multi-word-component-names": "off", // 关闭驼峰命名
+    quotes: ["error", "double"],
+    /* ts相关 */
+    "@typescript-eslint/no-empty-function": "off", // 可以为空函数
+    "@typescript-eslint/explicit-module-boundary-types": "off", //函数不需要返回类型也可以
+    "@typescript-eslint/no-explicit-any": "off", //可以为any
+    "@typescript-eslint/no-non-null-assertion": "off", //! 非空断言
+    "@typescript-eslint/no-this-alias": "off" //忽略this关键字
   }
 }

+ 1 - 1
.prettierrc

@@ -1,5 +1,5 @@
 {
-  "singleQuote": true,
+  "singleQuote": false,
   "trailingComma": "none",
   "printWidth": 150,
   "semi": false,

+ 0 - 1
env.d.ts

@@ -1,3 +1,2 @@
-
 // eslint-disable-next-line spaced-comment
 /// <reference types="vite/client" />

+ 6 - 6
index.html

@@ -31,8 +31,8 @@
           z-index: 10000;
         }
         .firstLoading .loadingBox {
-          width: 32px;
-          height: 32px;
+          width: 36px;
+          height: 36px;
           display: flex;
           justify-content: space-between;
           flex-wrap: wrap;
@@ -41,8 +41,8 @@
           animation: rotate 1.5s linear infinite;
         }
         .firstLoading .loadingBox .loadingItem {
-          width: 14px;
-          height: 14px;
+          width: 16px;
+          height: 16px;
           border-radius: 50%;
           background: #569cfe;
           opacity: 0.5;
@@ -51,7 +51,7 @@
           opacity: 1;
         }
         .firstLoading .loadingTip {
-          font-size: 16px;
+          font-size: 20px;
           color: #569cfe;
         }
         @keyframes rotate {
@@ -70,7 +70,7 @@
           <div class="loadingItem"></div>
           <div class="loadingItem"></div>
         </div>
-        <div class="loadingTip">正在加载中,请稍等…</div>
+        <div class="loadingTip">正在加载中…</div>
       </div>
     </div>
     <script type="module" src="/src/main.ts"></script>

File diff suppressed because it is too large
+ 619 - 10
package-lock.json


+ 9 - 1
package.json

@@ -14,17 +14,23 @@
   },
   "dependencies": {
     "@icon-park/vue-next": "^1.4.2",
+    "@types/nprogress": "^0.2.3",
     "animate.css": "^4.1.1",
+    "axios": "^1.3.6",
     "clipboard": "^2.0.11",
+    "cos-js-sdk-v5": "^1.4.20",
     "crypto-js": "^4.2.0",
     "dexie": "3.0.3",
     "echarts": "^5.5.1",
+    "element-plus": "^2.3.4",
     "file-saver": "^2.0.5",
     "hfmath": "^0.0.2",
     "html-to-image": "^1.11.11",
+    "js-cookie": "^3.0.5",
     "lodash": "^4.17.21",
     "mitt": "^3.0.1",
     "nanoid": "^5.0.7",
+    "nprogress": "^0.2.0",
     "number-precision": "^1.6.0",
     "pinia": "^2.1.7",
     "pptxgenjs": "^3.12.0",
@@ -45,6 +51,7 @@
     "tinycolor2": "^1.6.0",
     "tippy.js": "^6.3.7",
     "vue": "^3.4.34",
+    "vue-router": "^4.0.3",
     "vuedraggable": "^4.1.0"
   },
   "devDependencies": {
@@ -52,6 +59,7 @@
     "@tsconfig/node18": "^18.2.2",
     "@types/crypto-js": "^4.2.1",
     "@types/file-saver": "^2.0.7",
+    "@types/js-cookie": "^3.0.3",
     "@types/lodash": "^4.14.202",
     "@types/node": "^18.19.3",
     "@types/svg-arc-to-cubic-bezier": "^3.2.2",
@@ -63,8 +71,8 @@
     "eslint": "^8.49.0",
     "eslint-plugin-vue": "^9.17.0",
     "npm-run-all2": "^6.1.1",
-    "sass": "^1.69.6",
     "prettier": "^3.3.3",
+    "sass": "^1.69.6",
     "typescript": "~5.3.0",
     "vite": "^5.3.5",
     "vue-tsc": "^2.0.29"

+ 3 - 48
src/App.vue

@@ -1,52 +1,7 @@
 <template>
-  <Screen v-if="screening" />
-  <Editor v-else-if="_isPC" />
-  <Mobile v-else />
+  <router-view />
 </template>
 
-<script lang="ts" setup>
-import { onMounted } from 'vue'
-import { storeToRefs } from 'pinia'
-import { useScreenStore, useMainStore, useSnapshotStore } from '@/store'
-import { LOCALSTORAGE_KEY_DISCARDED_DB } from '@/configs/storage'
-import { deleteDiscardedDB } from '@/utils/database'
-import { isPC } from './utils/common'
+<script lang="ts" setup></script>
 
-import Editor from './views/Editor/index.vue'
-import Screen from './views/Screen/index.vue'
-import Mobile from './views/Mobile/index.vue'
-
-const _isPC = isPC()
-
-const mainStore = useMainStore()
-const snapshotStore = useSnapshotStore()
-const { databaseId } = storeToRefs(mainStore)
-const { screening } = storeToRefs(useScreenStore())
-
-if (import.meta.env.MODE !== 'development') {
-  window.onbeforeunload = () => false
-}
-
-onMounted(async () => {
-  await deleteDiscardedDB()
-  snapshotStore.initSnapshotDatabase()
-  mainStore.setAvailableFonts()
-})
-
-// 应用注销时向 localStorage 中记录下本次 indexedDB 的数据库ID,用于之后清除数据库
-window.addEventListener('unload', () => {
-  const discardedDB = localStorage.getItem(LOCALSTORAGE_KEY_DISCARDED_DB)
-  const discardedDBList: string[] = discardedDB ? JSON.parse(discardedDB) : []
-
-  discardedDBList.push(databaseId.value)
-
-  const newDiscardedDB = JSON.stringify(discardedDBList)
-  localStorage.setItem(LOCALSTORAGE_KEY_DISCARDED_DB, newDiscardedDB)
-})
-</script>
-
-<style lang="scss">
-#app {
-  height: 100%;
-}
-</style>
+<style lang="scss"></style>

+ 9 - 0
src/api/ApiInstance.ts

@@ -0,0 +1,9 @@
+import Http from "@/libs/axios"
+import { getToken } from "@/libs/auth"
+import { URL_API } from "@/config"
+
+/** axios实例 */
+export const httpAxios = new Http(URL_API, {
+  tokenName: "Authorization",
+  getTokenFun: getToken
+})

+ 9 - 0
src/api/user.ts

@@ -0,0 +1,9 @@
+import { httpAxios } from "@/api/ApiInstance"
+
+//获取用户信息
+export const getUserInfo = () => {
+  return httpAxios.axioseRquest({
+    method: "get",
+    url: "/edu-app/user/getUserInfo"
+  })
+}

+ 24 - 0
src/assets/styles/global.scss

@@ -47,6 +47,11 @@ body {
   color: $textColor;
 }
 
+#app {
+    width: 100%;
+    height: 100%;
+}
+
 body {
   font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
 }
@@ -136,3 +141,22 @@ textarea {
   background-color: #e1e1e1;
   border-radius: 3px;
 }
+
+//<input type="number"> 去掉右侧上下按钮
+.inputNumNone {
+  input::-webkit-outer-spin-button,
+  input::-webkit-inner-spin-button {
+      -webkit-appearance: none !important;
+      -moz-appearance: none !important;
+      -o-appearance: none !important;
+      -ms-appearance: none !important;
+      appearance: none !important;
+  }
+  input[type="number"] {
+      -webkit-appearance: textfield;
+      -moz-appearance: textfield;
+      -o-appearance: textfield;
+      -ms-appearance: textfield;
+      appearance: textfield;
+  }
+}

+ 1 - 0
src/config/index.ts

@@ -0,0 +1 @@
+export const URL_API = import.meta.env.VITE_APP_URL as string

+ 28 - 0
src/libs/auth.ts

@@ -0,0 +1,28 @@
+import Cookies from "js-cookie"
+
+/* code */
+// eslint-disable-next-line prefer-const
+export let CODE401 = 5000 // 没有权限
+export const CODE_ERR_CANCELED = "ERR_CANCELED" // 取消请求的code
+export function setCODE401(code: number) {
+  CODE401 = code
+}
+
+/** cookie */
+const TokenKey = "ppt-Token"
+const expiresDate = 7
+
+export let TokenInvalidFlag = true // 令牌是否有效
+export function getToken() {
+  return Cookies.get(TokenKey)
+}
+
+export function setToken(token: string) {
+  TokenInvalidFlag = true
+  return Cookies.set(TokenKey, token, { expires: expiresDate })
+}
+
+export function removeToken() {
+  TokenInvalidFlag = false
+  return Cookies.remove(TokenKey)
+}

+ 129 - 0
src/libs/axios.ts

@@ -0,0 +1,129 @@
+/** http请求类 */
+import axios from "axios"
+import type { AxiosInstance, AxiosRequestConfig, Method, AxiosPromise } from "axios"
+import Nprogress from "@/plugins/nprogress"
+import { ElMessage } from "element-plus"
+import { TokenInvalidFlag, CODE401, CODE_ERR_CANCELED } from "@/libs/auth"
+import userStore from "@/store/user"
+
+// 重写 axios 传参
+interface AxiosConfigType extends AxiosRequestConfig {
+  method: Method // method 必须为Method 格式
+}
+
+// axiosObj 传值
+type axiosObjType = {
+  getTokenFun?: () => string | undefined // 获取token的函数
+  tokenName?: string // headers token的名字
+  nprogress?: boolean // 是否显示滚动条
+}
+
+/** api接口 */
+export interface axiosApiType {
+  (...param: any[]): AxiosPromise<any>
+}
+
+class HttpAsynAxios {
+  constructor(url: string, axiosObj?: axiosObjType) {
+    this.URL = url
+    this.axiosObj = axiosObj
+  }
+  private URL = ""
+  private axiosObj: axiosObjType | undefined = undefined
+  private httpInterceptor(instance: AxiosInstance) {
+    ;(this.axiosObj ? !(this.axiosObj.nprogress === false) : true) && Nprogress.start() // 开启
+    // http request 请求拦截器
+    instance.interceptors.request.use(
+      config => {
+        return config
+      },
+      error => {
+        console.log(error)
+        Nprogress.done()
+        const rejectData: apiResDataType = {
+          code: 500,
+          msg: "系统运行异常,请联系管理员处理",
+          data: null
+        }
+        return Promise.reject(rejectData)
+      }
+    )
+    // http response 结果拦截器
+    instance.interceptors.response.use(
+      response => {
+        Nprogress.done()
+        // 如果返回401则跳转到登录页
+        if (response.data.code === CODE401) {
+          // 如果token登录状态 才提示和退出
+          if (TokenInvalidFlag) {
+            // const responseData: apiResDataType = response.data
+            // ElMessage.error(responseData.msg)
+            // 登出
+            userStore().resetUser()
+          }
+        }
+        return response
+      },
+      error => {
+        console.log(error)
+        Nprogress.done()
+        // if (error.response) {
+        //     // 响应错误之后的操作
+        //     switch (error.response.status) {
+        //         case 401:
+        //     }
+        // }
+        const rejectData: apiResDataType =
+          error.code === CODE_ERR_CANCELED
+            ? {
+                code: error.code,
+                msg: "系统取消接口",
+                data: null
+              }
+            : {
+                code: 500,
+                msg: "系统运行异常,请联系管理员处理",
+                data: null
+              }
+        return Promise.reject(rejectData) // 返回接口返回的错误信息
+      }
+    )
+  }
+  // 创建实例
+  private createInstance() {
+    const axiosObj = this.axiosObj
+    return axios.create(
+      axiosObj?.tokenName
+        ? {
+            baseURL: this.URL,
+            headers: {
+              [axiosObj.tokenName]: axiosObj.getTokenFun && axiosObj.getTokenFun()
+            }
+          }
+        : {
+            baseURL: this.URL
+          }
+    )
+  }
+  /**
+      axios请求方法
+      ```
+      {
+          data: data,     //get方法的时候为params:data
+          method: "post",
+          url: '/auth/loginIn',
+          headers:{
+                  'Content-Type':'application/json',
+          },
+          responseType: 'blob',
+      }
+      ```
+   */
+  axioseRquest(opt: AxiosConfigType) {
+    const instance = this.createInstance()
+    this.httpInterceptor(instance)
+    return instance(opt)
+  }
+}
+
+export default HttpAsynAxios

+ 377 - 0
src/libs/tools.ts

@@ -0,0 +1,377 @@
+/**
+  下载方法 通过blob和文件名
+*/
+export const downloadFile = (blob: Blob, fileName: string) => {
+  const link = document.createElement("a")
+  link.style.display = "none"
+  link.href = window.URL.createObjectURL(blob)
+  link.download = fileName
+  // 触发点击
+  document.body.appendChild(link)
+  link.click()
+  // 然后移除
+  document.body.removeChild(link)
+  window.URL.revokeObjectURL(link.href)
+}
+
+/**
+  下载方法 通过文件路径
+ */
+export const downloadFileUrl = (url: string) => {
+  const link = document.createElement("a")
+  link.style.display = "none"
+  link.href = url
+  // 触发点击
+  document.body.appendChild(link)
+  link.click()
+  // 然后移除
+  document.body.removeChild(link)
+  window.URL.revokeObjectURL(link.href)
+}
+
+/**
+  批量下载方法 通过文件路径 尽量打成压缩包下载 这个方法很多浏览器禁用
+  */
+export const batchDownloadFileUrl = (url: string) => {
+  const iframe = document.createElement("iframe")
+  iframe.style.display = "none"
+  iframe.src = url
+  document.body.appendChild(iframe)
+  iframe.onload = () => {
+    iframe.remove()
+  }
+}
+
+/**
+ * 去掉字符串中的html标签
+ */
+export const removeHtmlTag = (str: string) => {
+  return new DOMParser().parseFromString(str, "text/html").body.textContent || ""
+}
+
+/**
+  复制文本到剪贴板
+*/
+export const copyText = (text: string) => {
+  return navigator.clipboard.writeText(text)
+}
+
+/**
+ * 时间转换
+ * @param fmt yyyy-mm-dd hh:ii:ss
+ */
+export const format = (dateData?: Date | number | string, fmt?: string) => {
+  // 兼容ie  replace(/-/g, "/")
+  const date = dateData ? new Date(typeof dateData === "string" ? dateData.replace(/-/g, "/") : dateData) : new Date()
+  fmt = fmt || "yyyy-mm-dd"
+  const opt = {
+    "y+": date.getFullYear().toString(), // 年
+    "m+": (date.getMonth() + 1).toString(), // 月
+    "d+": date.getDate().toString(), // 日
+    "h+": date.getHours().toString(), // 时
+    "i+": date.getMinutes().toString(), // 分
+    "s+": date.getSeconds().toString() // 秒
+  }
+  let k: keyof typeof opt
+  for (k in opt) {
+    const ret = new RegExp(`(${k})`).exec(fmt)
+    if (ret) {
+      fmt = fmt.replace(ret[1], ret[1].length == 1 ? opt[k] : opt[k].padStart(ret[1].length, "0"))
+    }
+  }
+  return fmt
+}
+
+/**
+  获取两日期之间日期列表函数 返回数组
+*/
+export const getdiffdate = (stime: string, etime: string) => {
+  // 初始化日期列表,数组
+  const diffdate: string[] = []
+  let i = 0
+  // 开始日期小于等于结束日期,并循环
+  while (stime <= etime) {
+    diffdate[i] = stime
+    // 获取开始日期时间戳
+    const stime_ts = new Date(stime.replace(/-/g, "/")).getTime()
+    // 增加一天时间戳后的日期
+    const next_date = stime_ts + 24 * 60 * 60 * 1000
+    // 拼接年月日,这里的月份会返回(0-11),所以要+1
+    const next_dates_y = new Date(next_date).getFullYear() + "-"
+    const next_dates_m =
+      new Date(next_date).getMonth() + 1 < 10 ? "0" + (new Date(next_date).getMonth() + 1) + "-" : new Date(next_date).getMonth() + 1 + "-"
+    const next_dates_d = new Date(next_date).getDate() < 10 ? "0" + new Date(next_date).getDate() : new Date(next_date).getDate()
+    stime = next_dates_y + next_dates_m + next_dates_d
+    // 增加数组key
+    i++
+  }
+  return diffdate
+}
+
+/** 生成 uuid */
+export const UUID = () => {
+  let d = new Date().getTime()
+  if (window.performance && typeof window.performance.now === "function") {
+    d += performance.now() // use high-precision timer if available
+  }
+  const uuid = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
+    const r = (d + Math.random() * 16) % 16 | 0
+    d = Math.floor(d / 16)
+    return (c == "x" ? r : (r & 0x3) | 0x8).toString(16)
+  })
+  return uuid
+}
+
+/** 深拷贝 */
+export const deepCopy = <T extends Record<string, any>>(source: T): T => {
+  const target = (Array.isArray(source) ? [] : {}) as T
+  for (const k in source) {
+    if (Object.hasOwnProperty.call(source, k)) {
+      // __proto__上面的属性不拷贝
+      const sourceVal = source[k]
+      const typeSource = Object.prototype.toString.call(sourceVal).slice(8, -1)
+      if (typeSource === "Object" || typeSource === "Array") {
+        target[k] = deepCopy(sourceVal)
+      } else {
+        target[k] = sourceVal
+      }
+    }
+  }
+  return target
+}
+
+/**
+ * 判断是不是空
+ *
+ * 能判断 "",[],null,undefined
+ */
+export const isEmpty = (val: string | Array<any> | number | null | undefined) => {
+  if (typeof val === "number") {
+    return val ? false : true
+  }
+  return val === null || val === undefined || val.length === 0 ? true : false
+}
+
+/** 根据身份证号获取出生年月日 */
+export const getBirthByIdCard = (idCard: string) => {
+  let birthday = ""
+  if (idCard.length == 15) {
+    birthday = "19" + idCard.slice(6, 12)
+  } else if (idCard.length == 18) {
+    birthday = idCard.slice(6, 14)
+  }
+  birthday = birthday.replace(/(.{4})(.{2})/, "$1-$2-")
+  // 通过正则表达式来指定输出格式为:1990-01-01
+  return birthday
+}
+
+/** 根据省份证获取性别 */
+export const getSexByIdCard = (idCard: string) => {
+  return parseInt(idCard.slice(-2, -1)) % 2 == 1 ? "1" : "0"
+}
+
+interface ThrottleFunc<T extends (...args: any[]) => any> {
+  (...args: Parameters<T>): ReturnType<T>
+  cancel(): void
+}
+/**
+    创建并返回一个像节流阀一样的函数,当重复调用函数的时候,至少每隔 wait毫秒调用一次该函数。对于想控制一些触发频率较高的事件有帮助。
+    默认情况下,throttle将在你调用的第一时间尽快执行这个function,并且,如果你在wait周期内调用任意次数的函数,都将尽快的被覆盖。
+    如果你想禁用第一次首先执行的话,传递{leading: false},还有如果你想禁用最后一次执行的话,传递{trailing: false}。
+    如果需要取消预定的 throttle ,可以在 throttle 函数上调用 .cancel()。
+*/
+export const throttle = <T extends (...args: any[]) => any>(
+  func: T,
+  wait = 600,
+  options: {
+    trailing?: boolean
+    leading?: boolean
+  } = {}
+): ThrottleFunc<T> => {
+  let timeout: number | null, context: any, args: any[], result: ReturnType<T>
+  let previous = 0
+  const later = function () {
+    previous = options.leading === false ? 0 : new Date().getTime()
+    timeout = null
+    result = func.apply(context, args)
+    if (!timeout) (args = []), (context = null)
+  }
+  const throttled: ThrottleFunc<T> = function (this: any, ..._args) {
+    const _now = new Date().getTime()
+    if (!previous && options.leading === false) previous = _now
+    const remaining = wait - (_now - previous)
+    context = this
+    args = _args
+    if (remaining <= 0 || remaining > wait) {
+      if (timeout) {
+        clearTimeout(timeout)
+        timeout = null
+      }
+      previous = _now
+      result = func.apply(context, args)
+      if (!timeout) (args = []), (context = null)
+    } else if (!timeout && options.trailing !== false) {
+      timeout = window.setTimeout(later, remaining)
+    }
+    return result
+  }
+  throttled.cancel = function () {
+    timeout && clearTimeout(timeout)
+    previous = 0
+    args = []
+    timeout = context = null
+  }
+  return throttled
+}
+
+interface DebouncedFunc<T extends (...args: any[]) => any> {
+  (...args: Parameters<T>): ReturnType<T>
+  cancel(): void
+}
+/**
+    返回 function 函数的防反跳版本, 将延迟函数的执行(真正的执行)在函数最后一次调用时刻的 wait 毫秒之后. 对于必须在一些输入(多是一些用户操作)停止到达之后执行的行为有帮助。
+    在 wait 间隔结束时,将使用最近传递给 debounced(去抖动)函数的参数调用该函数。
+    传参 immediate 为 true, debounce会在 wait 时间间隔的开始调用这个函数 。(愚人码头注:并且在 waite 的时间之内,不会再次调用。)在类似不小心点了提交按钮两下而提交了两次的情况下很有用。
+    如果需要取消预定的 debounce ,可以在 debounce 函数上调用 .cancel()。
+*/
+export const debounce = <T extends (...args: any[]) => any>(func: T, wait = 600, immediate = false): DebouncedFunc<T> => {
+  let timeout: number | null, previous: number, args: any[], result: ReturnType<T>, context: any
+  const later = function () {
+    const passed = new Date().getTime() - previous
+    if (wait > passed) {
+      timeout = window.setTimeout(later, wait - passed)
+    } else {
+      timeout = null
+      if (!immediate) result = func.apply(context, args)
+      // This check is needed because `func` can recursively invoke `debounced`.
+      if (!timeout) (args = []), (context = null)
+    }
+  }
+  const debounced: DebouncedFunc<T> = function (this: any, ..._args) {
+    context = this
+    args = _args
+    previous = new Date().getTime()
+    if (!timeout) {
+      timeout = window.setTimeout(later, wait)
+      if (immediate) result = func.apply(context, args)
+    }
+    return result
+  }
+  debounced.cancel = function () {
+    timeout && clearTimeout(timeout)
+    args = []
+    timeout = context = null
+  }
+  return debounced
+}
+
+/**
+ * html字符串关键词高亮
+ * @param html html格式的字符串
+ * @param keyword 需要查找的关键字
+ */
+export function htmlMarkHitClass(html: string, keyword: string) {
+  const repKey = "♽⚁" // 如果查询的关键词与这个替换的冲突,查询就会出bug 排除以下情况
+  if (["♽⚁", "♽", "⚁"].includes(keyword)) {
+    return {
+      html,
+      indexLen: 0
+    }
+  }
+  if (!keyword) {
+    return {
+      html,
+      indexLen: 0
+    }
+  }
+  let indexLen = 0
+  const regTag = /<.*?>/gi
+  const htmlTags = html.match(regTag) || []
+  html = html.replace(regTag, repKey)
+  html = html.replace(new RegExp(keyword, "g"), () => {
+    indexLen++
+    return `<span id="cus_${indexLen}" class="hitClass" >${keyword}</span>`
+  })
+  let regTagIndex = -1
+  html = html.replace(new RegExp(repKey, "g"), () => {
+    regTagIndex++
+    return htmlTags[regTagIndex]
+  })
+  return {
+    html,
+    indexLen
+  }
+}
+
+const classNameToArray = (cls = "") => cls.split(" ").filter(item => !!item.trim())
+/** Boolean 判断有没有这个class */
+export const hasClass = (el: HTMLElement | null | undefined, cls: string) => {
+  if (!el) return false
+  if (cls.includes(" ")) throw new Error("className should not contain space.")
+  return el.classList.contains(cls)
+}
+/**
+    添加class  cls:"a b c" or "a" or "a b"
+*/
+export const addClass = (el: HTMLElement | null | undefined, cls: string) => {
+  if (!el) return
+  el.classList.add(...classNameToArray(cls))
+}
+
+/** 删除class cls:"a b c" or "a" or "a b" */
+export const removeClass = (el: HTMLElement | null | undefined, cls: string) => {
+  if (!el) return
+  el.classList.remove(...classNameToArray(cls))
+}
+/**
+ * 获取el的style值
+ *
+ *  例:getStyle(document.querySelector("body"),"position") relative
+ */
+export const getStyle = (el: HTMLElement | null | undefined, styleName: keyof CSSStyleDeclaration): string => {
+  if (!el) return ""
+  if (styleName === "float") {
+    styleName = "cssFloat"
+  }
+  try {
+    const style = el.style[styleName]
+    if (style) return style as string
+    const computed = document.defaultView?.getComputedStyle(el, "")
+    return computed ? (computed[styleName] as string) : ""
+  } catch {
+    return el.style[styleName] as string
+  }
+}
+/**
+ * 给el修改 style
+ *
+    setStyle(document.querySelector("body"),{left:0,position:"relative"})
+    setStyle(document.querySelector("body"),"left","0"})
+*/
+export const setStyle = (el: HTMLElement | null | undefined, styleName: CSSStyleDeclaration | keyof CSSStyleDeclaration, value?: string) => {
+  if (!el) return
+  if (typeof styleName === "object") {
+    let prop: keyof CSSStyleDeclaration
+    for (prop in styleName) {
+      setStyle(el, prop, styleName[prop])
+    }
+  } else {
+    typeof value === "string" && (el.style[styleName as any] = value)
+  }
+}
+
+/** 删除style
+ *removeStyle(document.querySelector("body"),{left:"",position:""})
+ * removeStyle(document.querySelector("body"),"left")
+ */
+export const removeStyle = (el: HTMLElement | null | undefined, style: CSSStyleDeclaration | keyof CSSStyleDeclaration) => {
+  if (!el) return
+  if (typeof style === "object") {
+    let prop: keyof CSSStyleDeclaration
+    for (prop in style) {
+      setStyle(el, prop, "")
+    }
+  } else {
+    setStyle(el, style, "")
+  }
+}

+ 16 - 13
src/main.ts

@@ -1,19 +1,22 @@
-import { createApp } from 'vue'
-import { createPinia } from 'pinia'
-import App from './App.vue'
+import { createApp } from "vue"
+import { store } from "@/store"
+import router from "@/router"
+import App from "./App.vue"
 
-import '@icon-park/vue-next/styles/index.css'
-import 'prosemirror-view/style/prosemirror.css'
-import 'animate.css'
-import '@/assets/styles/prosemirror.scss'
-import '@/assets/styles/global.scss'
-import '@/assets/styles/font.scss'
+import "@icon-park/vue-next/styles/index.css"
+import "prosemirror-view/style/prosemirror.css"
+import "animate.css"
+import "element-plus/dist/index.css"
+import "@/assets/styles/prosemirror.scss"
+import "@/assets/styles/global.scss"
+import "@/assets/styles/font.scss"
 
-import Icon from '@/plugins/icon'
-import Directive from '@/plugins/directive'
+import Icon from "@/plugins/icon"
+import Directive from "@/plugins/directive"
 
 const app = createApp(App)
 app.use(Icon)
 app.use(Directive)
-app.use(createPinia())
-app.mount('#app')
+app.use(store)
+app.use(router)
+app.mount("#app")

+ 187 - 0
src/plugins/httpAjax.ts

@@ -0,0 +1,187 @@
+/**  http 方法封装 */
+import type { axiosApiType } from "@/libs/axios"
+import LoadingBar from "@/plugins/loadingBar"
+import { ElNotification, ElMessageBox, ElMessage } from "element-plus"
+import { downloadFile } from "@/libs/tools"
+
+/** ajax 请求 */
+export const httpAjax = function <T extends axiosApiType, P extends Parameters<T>>(axiosApi: T, ...nargs: P): Promise<apiResDataType> {
+  return new Promise(resolve => {
+    axiosApi(...nargs)
+      .then(res => {
+        resolve(res.data)
+      })
+      .catch(err => {
+        resolve(err)
+      })
+  })
+}
+/**错误会提示 ajax 请求 */
+export const httpAjaxErrMsg = function <T extends axiosApiType, P extends Parameters<T>>(axiosApi: T, ...nargs: P): Promise<apiResDataType> {
+  return new Promise(resolve => {
+    axiosApi(...nargs)
+      .then(res => {
+        const data = res.data as apiResDataType
+        if (data.code !== 200) {
+          ElMessage({
+            showClose: true,
+            message: data.msg,
+            type: "error"
+          })
+        }
+        resolve(res.data)
+      })
+      .catch(err => {
+        ElMessage({
+          showClose: true,
+          message: err.msg,
+          type: "error"
+        })
+        resolve(err)
+      })
+  })
+}
+/** 带loadingBar ajax 请求 */
+export const httpAjaxLoading = function <T extends axiosApiType, P extends Parameters<T>>(axiosApi: T, ...nargs: P): Promise<apiResDataType> {
+  return new Promise(resolve => {
+    LoadingBar.loading(true)
+    axiosApi(...nargs)
+      .then(res => {
+        LoadingBar.loading(false)
+        resolve(res.data)
+      })
+      .catch(err => {
+        LoadingBar.loading(false)
+        resolve(err)
+      })
+  })
+}
+/**
+ * 带loadingBar并且错误会提示 ajax 请求
+ */
+export const httpAjaxLoadingErrMsg = function <T extends axiosApiType, P extends Parameters<T>>(axiosApi: T, ...nargs: P): Promise<apiResDataType> {
+  return new Promise(resolve => {
+    LoadingBar.loading(true)
+    axiosApi(...nargs)
+      .then(res => {
+        LoadingBar.loading(false)
+        const data = res.data as apiResDataType
+        if (data.code !== 200) {
+          ElMessage({
+            showClose: true,
+            message: data.msg,
+            type: "error"
+          })
+        }
+        resolve(res.data)
+      })
+      .catch(err => {
+        LoadingBar.loading(false)
+        ElMessage({
+          showClose: true,
+          message: err.msg,
+          type: "error"
+        })
+        resolve(err)
+      })
+  })
+}
+/** 右上角弹窗提醒 ajax请求 */
+export const httpAjaxCrud = function <T extends axiosApiType, P extends Parameters<T>>(axiosApi: T, ...nargs: P): Promise<apiResDataType> {
+  return new Promise(resolve => {
+    LoadingBar.loading(true)
+    axiosApi(...nargs)
+      .then(res => {
+        LoadingBar.loading(false)
+        const data = res.data as apiResDataType
+        if (data.code === 200) {
+          ElNotification({
+            type: "success",
+            title: "成功",
+            message: data.msg,
+            position: "top-right",
+            duration: 3000
+          })
+        } else {
+          ElNotification({
+            type: "error",
+            title: "失败",
+            message: data.msg,
+            position: "top-right",
+            duration: 3000
+          })
+        }
+        resolve(data)
+      })
+      .catch(err => {
+        LoadingBar.loading(false)
+        ElNotification({
+          type: "error",
+          title: "失败",
+          message: err.msg,
+          position: "top-right",
+          duration: 3000
+        })
+        resolve(err)
+      })
+  })
+}
+
+/** 中间弹窗提醒 ajax请求 */
+export const httpAjaxAlert = function <T extends axiosApiType, P extends Parameters<T>>(axiosApi: T, ...nargs: P): Promise<apiResDataType> {
+  return new Promise(resolve => {
+    LoadingBar.loading(true)
+    axiosApi(...nargs)
+      .then(res => {
+        LoadingBar.loading(false)
+        const data = res.data as apiResDataType
+        if (data.code === 200) {
+          ElMessageBox.alert(data.msg, "提示", {
+            confirmButtonText: "关闭",
+            type: "success"
+          })
+        } else {
+          ElMessageBox.alert(data.msg, "提示", {
+            confirmButtonText: "关闭",
+            type: "error"
+          })
+        }
+        resolve(data)
+      })
+      .catch(err => {
+        LoadingBar.loading(false)
+        ElMessageBox.alert(err.msg, "提示", {
+          confirmButtonText: "关闭",
+          type: "error"
+        })
+        resolve(err)
+      })
+  })
+}
+
+/** 下载 接口 文件名字从content-disposition拿*/
+export const httpAjaxDownload = function <T extends axiosApiType, P extends Parameters<T>>(axiosApi: T, ...nargs: P) {
+  LoadingBar.loading(true)
+  axiosApi(...nargs)
+    .then(res => {
+      LoadingBar.loading(false)
+      const filename: undefined | string = res.headers["content-disposition"]?.split("=")[1]
+      if (filename) {
+        downloadFile(res.data, decodeURIComponent(filename))
+      } else {
+        ElMessage({
+          showClose: true,
+          message: "未获取到文件名",
+          type: "error"
+        })
+      }
+    })
+    .catch(err => {
+      LoadingBar.loading(false)
+      ElMessage({
+        showClose: true,
+        message: err.msg,
+        type: "error"
+      })
+    })
+}

+ 12 - 0
src/plugins/loadingBar/index.ts

@@ -0,0 +1,12 @@
+import loadingBar from './loadingBar.vue'
+import { h, render } from 'vue'
+const vnode = h(loadingBar)
+render(vnode, document.body)
+const vm = vnode.component!.proxy as InstanceType<typeof loadingBar>
+/** 全局加载条
+  import LoadingBar from "@/plugin/loadingBar"
+
+  LoadingBar.loading(isshow,text)
+*/
+const LoadingBar = vm
+export default LoadingBar

+ 124 - 0
src/plugins/loadingBar/loadingBar.vue

@@ -0,0 +1,124 @@
+<!--
+* @FileDescription: 全局加载条
+* @Author: 黄琪勇
+* @Date:2022-05-23 15:33:04
+-->
+<template>
+  <div v-show="loadingBarData.show" class="h-loading-bar">
+    <div class="loading-bar-content">
+      <div class="loadingBox">
+        <div class="loadingItem"></div>
+        <div class="loadingItem"></div>
+        <div class="loadingItem"></div>
+        <div class="loadingItem"></div>
+      </div>
+      <div class="loadingTip">{{ loadingBarData.text }}</div>
+    </div>
+  </div>
+</template>
+
+<!-- <script setup lang="ts">
+import { shallowReactive } from "vue"
+import { addClass, removeClass } from "@/libs/tools"
+
+const loadingBarData = shallowReactive({
+  text: "",
+  show: false
+})
+const body = document.querySelector("body"),
+  name = "h-loadingBarBody",
+  defaultText = "正在拼命为您加载中..."
+/** 显示或关闭加载条 */
+const loading = (isShow: boolean, text?: string) => {
+  loadingBarData.text = text ?? defaultText
+  loadingBarData.show = isShow
+  loadingBarData.show ? addClass(body, name) : removeClass(body, name)
+}
+defineExpose({ loading })
+</script> -->
+
+<!-- 由于setup模式 编译方式好像改了 defineExpose出去的属性proxy上面访问不到  setup()访问的到 所以改为这种模式-->
+<script lang="ts">
+import { shallowReactive, defineComponent } from 'vue'
+import { addClass, removeClass } from '@/libs/tools'
+export default defineComponent({
+  setup() {
+    const loadingBarData = shallowReactive({
+      text: '',
+      show: false
+    })
+    const body = document.querySelector('body'),
+      name = 'h-loadingBarBody',
+      defaultText = '正在加载中...'
+    /** 显示或关闭加载条 */
+    const loading = (isShow: boolean, text?: string) => {
+      loadingBarData.text = text ?? defaultText
+      loadingBarData.show = isShow
+      loadingBarData.show ? addClass(body, name) : removeClass(body, name)
+    }
+    return {
+      loadingBarData,
+      loading
+    }
+  }
+})
+</script>
+<!-- 取消scoped 减少属性权重  外部添加class可以修改里面的样式 -->
+<style lang="scss">
+body.h-loadingBarBody {
+  overflow: hidden;
+}
+.h-loading-bar {
+  position: fixed;
+  top: 0;
+  left: 0;
+  z-index: 12345678;
+  cursor: wait;
+  width: 100%;
+  height: 100%;
+  background: rgba(255, 255, 255, 0.4);
+  .loading-bar-content {
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    transform: translate(-50%, -100%);
+    z-index: 1;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    color: $themeColor;
+    .loadingBox {
+      width: 36px;
+      height: 36px;
+      display: flex;
+      justify-content: space-between;
+      flex-wrap: wrap;
+      align-content: space-between;
+      margin-bottom: 28px;
+      animation: rotate 1.5s linear infinite;
+      .loadingItem {
+        width: 16px;
+        height: 16px;
+        border-radius: 50%;
+        background: #569cfe;
+        opacity: 0.5;
+        &:nth-child(2) {
+          opacity: 1;
+        }
+      }
+    }
+    .loadingTip {
+      font-size: 20px;
+      color: #569cfe;
+    }
+    @keyframes rotate {
+      from {
+        transform: rotate(0deg);
+      }
+      to {
+        transform: rotate(360deg);
+      }
+    }
+  }
+}
+</style>

+ 19 - 0
src/plugins/nprogress/index.ts

@@ -0,0 +1,19 @@
+import NProgress from 'nprogress'
+import 'nprogress/nprogress.css'
+import './nprogress.scss'
+
+NProgress.configure({
+  // 动画方式
+  easing: 'ease',
+  // 递增进度条的速度
+  speed: 500,
+  // 是否显示加载ico
+  showSpinner: false,
+  // 自动递增间隔
+  trickleSpeed: 200,
+  // 初始化时的最小百分比
+  minimum: 0.3
+})
+
+/** 进度条 */
+export default NProgress

+ 18 - 0
src/plugins/nprogress/nprogress.scss

@@ -0,0 +1,18 @@
+/*
+    重置颜色
+ */
+
+#nprogress .bar {
+  background: $themeColor;
+}
+
+#nprogress .peg {
+  box-shadow:
+    0 0 10px $themeColor,
+    0 0 5px $themeColor;
+}
+
+#nprogress .spinner-icon {
+  border-top-color: $themeColor;
+  border-left-color: $themeColor;
+}

+ 74 - 0
src/router/index.ts

@@ -0,0 +1,74 @@
+import { createRouter, createWebHashHistory } from "vue-router"
+import { constRoutes } from "./routers"
+import NProgress from "@/plugins/nprogress"
+import { ElMessage } from "element-plus"
+import { getToken, CODE401 } from "@/libs/auth"
+import userStore from "@/store/user"
+
+const userStoreHook = userStore()
+
+userStoreHook.login()
+
+const router = createRouter({
+  history: createWebHashHistory(import.meta.env.BASE_URL),
+  routes: constRoutes,
+  scrollBehavior() {
+    return { left: 0, top: 0 }
+  }
+})
+function isRegWhite(regs: RegExp[], path: string) {
+  return (
+    regs.findIndex(reg => {
+      return reg.test(path)
+    }) > -1
+  )
+}
+const authWhiteList = ["/login"] // 不访问接口权限白名单
+const regWhiteList: RegExp[] = [] //正则白名单(用于系统不需要权限的分享页面)
+
+router.beforeEach((to, from, next) => {
+  NProgress.start()
+  const hasToken = getToken()
+  //如果有token且不是白名单页面则加载info
+  if (hasToken && !authWhiteList.includes(to.path)) {
+    //有roles时候
+    const hasRoles = userStoreHook.roles
+    if (hasRoles) {
+      next()
+    } else {
+      userStoreHook
+        .getUserInfo()
+        .then(() => {
+          next()
+        })
+        .catch((err: apiResDataType) => {
+          //退出 清空,当是登录权限问题时候 axios 里面会清空
+          if (err.code !== CODE401) {
+            //ElMessage.error(err.msg)
+            userStoreHook.resetUser()
+          }
+        })
+    }
+  } else {
+    // 如果有token,并且是登录页 直接返回首页
+    if (hasToken && to.path === "/login") {
+      next({
+        path: "/"
+      })
+      return
+    }
+    if (authWhiteList.includes(to.path) || isRegWhite(regWhiteList, to.path)) {
+      next()
+    } else {
+      next({
+        path: "/login"
+      })
+    }
+  }
+})
+
+router.afterEach(() => {
+  NProgress.done()
+})
+
+export default router

+ 24 - 0
src/router/routers.ts

@@ -0,0 +1,24 @@
+import type { RouteRecordRaw } from "vue-router"
+
+/** 静态路由 */
+export const constRoutes: Array<RouteRecordRaw> = [
+  {
+    path: "/",
+    redirect: "/pptEditor"
+  },
+  {
+    path: "/pptEditor",
+    name: "pptEditor",
+    component: () => import("@/views/pptEditor")
+  },
+  {
+    path: "/login",
+    name: "login",
+    component: () => import("@/viewsframe/login")
+  },
+  {
+    path: "/:pathMatch(.*)*",
+    name: "errorPage",
+    component: () => import("@/viewsframe/errorPage")
+  }
+]

+ 9 - 12
src/store/index.ts

@@ -1,13 +1,10 @@
-import { useMainStore } from './main'
-import { useSlidesStore } from './slides'
-import { useSnapshotStore } from './snapshot'
-import { useKeyboardStore } from './keyboard'
-import { useScreenStore } from './screen'
+import { useMainStore } from "./main"
+import { useSlidesStore } from "./slides"
+import { useSnapshotStore } from "./snapshot"
+import { useKeyboardStore } from "./keyboard"
+import { useScreenStore } from "./screen"
 
-export {
-  useMainStore,
-  useSlidesStore,
-  useSnapshotStore,
-  useKeyboardStore,
-  useScreenStore,
-}
+import { createPinia } from "pinia"
+
+export const store = createPinia()
+export { useMainStore, useSlidesStore, useSnapshotStore, useKeyboardStore, useScreenStore }

+ 4 - 4
src/store/screen.ts

@@ -6,12 +6,12 @@ export interface ScreenState {
 
 export const useScreenStore = defineStore('screen', {
   state: (): ScreenState => ({
-    screening: false, // 是否进入放映状态
+    screening: false // 是否进入放映状态
   }),
 
   actions: {
     setScreening(screening: boolean) {
       this.screening = screening
-    },
-  },
-})
+    }
+  }
+})

+ 71 - 0
src/store/user.ts

@@ -0,0 +1,71 @@
+import { defineStore } from "pinia"
+import { removeToken, setToken, setCODE401 } from "@/libs/auth"
+import router from "@/router"
+import { httpAjax } from "@/plugins/httpAjax"
+import { store } from "./index"
+import { getUserInfo } from "@/api/user"
+interface userType {
+  userInfo: {
+    username?: string
+  }
+  roles: ""
+}
+
+const useStore = defineStore("user", {
+  state: (): userType => {
+    return {
+      userInfo: {}, //用户信息
+      roles: "" //用户角色
+    }
+  },
+  actions: {
+    /** 登录 */
+    async login() {
+      function getAuthorizationFromUrl() {
+        const fullUrl = window.location.href
+        const queryIndex = fullUrl.indexOf("?")
+        if (queryIndex === -1) return undefined
+        const queryString = fullUrl.slice(queryIndex + 1)
+        const params = new URLSearchParams(queryString)
+        return params.get("Authorization") || undefined
+      }
+      const token = getAuthorizationFromUrl()
+      token && setToken(token)
+    },
+    /** 获取用户信息 */
+    async getUserInfo() {
+      const userInfoRes = await httpAjax(getUserInfo)
+      if (userInfoRes.code !== 200) {
+        return Promise.reject(userInfoRes)
+      }
+      const userInfo = userInfoRes.data || {}
+      if (!userInfo.id) {
+        return Promise.reject({
+          code: "500",
+          data: null,
+          msg: "获取用户信息出错,请联系管理员!"
+        })
+      }
+      this.userInfo = userInfo
+      this.roles = userInfo.id
+      return Promise.resolve()
+    },
+    /** 退出登录 */
+    async loginOut() {
+      this.resetUser()
+      return Promise.resolve()
+    },
+    /** 清空所有登录信息,退出 */
+    resetUser() {
+      this.userInfo = {}
+      this.roles = ""
+      removeToken()
+      router.push({
+        path: "/login"
+      })
+    }
+  }
+})
+export default () => {
+  return useStore(store)
+}

+ 57 - 0
src/type.d.ts

@@ -0,0 +1,57 @@
+/**
+ * 全局类型定义
+ */
+
+/** 接口返回值 */
+declare interface apiResDataType {
+  code: number | "ERR_CANCELED"
+  data: any
+  msg: string
+}
+
+/*
+  store
+*/
+
+/** 菜单 */
+declare interface menuType {
+  path: string
+  title: string
+  icon: string
+  component: string
+  children: menuType[]
+  meta: {
+    routeType: "layout" | "singlepage" // 菜单或者单页 模式
+  }
+}
+
+/**
+ *type tool
+ */
+
+/**
+ * 提取obj中的某个属性的值类型
+ *
+ *例: type a={b:{c:1}}
+ *
+ * type c=ExtractVByK<a,"b"> /{c:number}
+ */
+declare type ExtractVByK<T extends Record<string, any>, P extends keyof T> = T[P]
+
+/**
+ * 获取 数组的类型
+ *
+ * 例: type a=string[]
+ *
+ * type b=ArrElement< a >  //string       另外: type b=a[number] 可以获取数组的类型,同时也能获取元祖的类型
+ */
+declare type ArrElement<ArrType extends any[]> = ArrType extends (infer ElementType)[] ? ElementType : never
+
+/**
+ * 将obj的某些类型变为可选
+ *
+ * 例: type a={a:string,b:string,c:string}
+ *
+ * type b=ObjPartial< a , 'a'|'b' >  //{a?:string,b?:string,c:string}
+ */
+declare type ObjPartial<T extends Record<string, any>, P extends keyof T> = Partial<Pick<T, P>> & Omit<T, P>

+ 67 - 0
src/utils/oss-file-upload.ts

@@ -0,0 +1,67 @@
+import COS from 'cos-js-sdk-v5'
+
+const tencentBucket = 'daya-online-1303457149'
+const ossType = 'tencent'
+
+export async function fileUpload(fileName: string, file: Blob) {
+  const { data } = await getUploadSign(fileName)
+  return await onOnlyFileUpload(data.signature, {
+    fileName,
+    file
+  })
+}
+
+const getUploadSign = async (fileName: string) => {
+  const fileUrl = 'yjl/' + fileName
+  return request.post('/getUploadSign', {
+    data: {
+      postData: {
+        key: fileUrl
+      },
+      pluginName: ossType,
+      bucketName: tencentBucket,
+      filename: fileUrl
+    },
+    requestType: 'json',
+    params: { pluginName: ossType }
+  })
+}
+
+const onOnlyFileUpload = async (signature: string, params: { fileName: string; file: Blob }) => {
+  let file = ''
+  let errorObj: any = null
+  const cos = new COS({
+    Domain: 'https://oss.dayaedu.com',
+    Protocol: 'https',
+    getAuthorization: async (options, callback: any) => {
+      callback({ Authorization: signature })
+    }
+  })
+  await cos
+    .uploadFile({
+      Bucket: tencentBucket /* 填写自己的 bucket,必须字段 */,
+      Region: 'ap-nanjing' /* 存储桶所在地域,必须字段 */,
+      Key: `yjl/${params.fileName}`,
+      /* 存储在桶里的对象键(例如:1.jpg,a/b/test.txt,图片.jpg)支持中文,必须字段 */
+      Body: params.file, // 上传文件对象
+      SliceSize: 1024 * 1024 * 500 /* 触发分块上传的阈值,超过5MB使用分块上传,小于5MB使用简单上传。可自行设置,非必须 */,
+      onProgress: function (progressData) {
+        // onProgress({ percent: Math.ceil((progressData.percent || 0) * 100) })
+      }
+    })
+    .then((res: any) => {
+      if (res.Location?.indexOf('http') >= 0) {
+        file = res.Location
+      } else {
+        file = 'https://' + res.Location
+      }
+    })
+    .catch((error: any) => {
+      errorObj = error
+    })
+  if (file) {
+    return file
+  } else {
+    throw new Error(errorObj)
+  }
+}

+ 41 - 31
src/views/Editor/EditorHeader/index.vue

@@ -3,41 +3,54 @@
     <div class="left">
       <Popover trigger="click" placement="bottom-start" v-model:value="mainMenuVisible">
         <template #content>
-          <FileInput accept=".pptist"  @change="files => {
-            importSpecificFile(files)
-            mainMenuVisible = false
-          }">
+          <FileInput
+            accept=".pptist"
+            @change="
+              files => {
+                importSpecificFile(files)
+                mainMenuVisible = false
+              }
+            "
+          >
             <PopoverMenuItem>导入 pptist 文件</PopoverMenuItem>
           </FileInput>
-          <FileInput accept="application/vnd.openxmlformats-officedocument.presentationml.presentation"  @change="files => {
-            importPPTXFile(files)
-            mainMenuVisible = false
-          }">
+          <FileInput
+            accept="application/vnd.openxmlformats-officedocument.presentationml.presentation"
+            @change="
+              files => {
+                importPPTXFile(files)
+                mainMenuVisible = false
+              }
+            "
+          >
             <PopoverMenuItem>导入 pptx 文件(测试版)</PopoverMenuItem>
           </FileInput>
           <PopoverMenuItem @click="setDialogForExport('pptx')">导出文件</PopoverMenuItem>
-          <PopoverMenuItem @click="resetSlides(); mainMenuVisible = false">重置幻灯片</PopoverMenuItem>
-          <PopoverMenuItem @click="goLink('https://github.com/pipipi-pikachu/PPTist/issues')">意见反馈</PopoverMenuItem>
-          <PopoverMenuItem @click="goLink('https://github.com/pipipi-pikachu/PPTist/blob/master/doc/Q&A.md')">常见问题</PopoverMenuItem>
-          <PopoverMenuItem @click="mainMenuVisible = false; hotkeyDrawerVisible = true">快捷操作</PopoverMenuItem>
+          <PopoverMenuItem
+            @click="
+              () => {
+                resetSlides()
+                mainMenuVisible = false
+              }
+            "
+            >重置幻灯片</PopoverMenuItem
+          >
+          <PopoverMenuItem
+            @click="
+              () => {
+                mainMenuVisible = false
+                hotkeyDrawerVisible = true
+              }
+            "
+            >快捷操作</PopoverMenuItem
+          >
         </template>
         <div class="menu-item"><IconHamburgerButton class="icon" /></div>
       </Popover>
 
       <div class="title">
-        <Input
-          class="title-input"
-          ref="titleInputRef"
-          v-model:value="titleValue"
-          @blur="handleUpdateTitle()"
-          v-if="editingTitle"
-        ></Input>
-        <div
-          class="title-text"
-          @click="startEditTitle()"
-          :title="title"
-          v-else
-        >{{ title }}</div>
+        <Input class="title-input" ref="titleInputRef" v-model:value="titleValue" @blur="handleUpdateTitle()" v-if="editingTitle"></Input>
+        <div class="title-text" @click="startEditTitle()" :title="title" v-else>{{ title }}</div>
       </div>
     </div>
 
@@ -59,11 +72,7 @@
       </div>
     </div>
 
-    <Drawer
-      :width="320"
-      v-model:visible="hotkeyDrawerVisible"
-      placement="right"
-    >
+    <Drawer :width="320" v-model:visible="hotkeyDrawerVisible" placement="right">
       <HotkeyDoc />
       <template v-slot:title>快捷操作</template>
     </Drawer>
@@ -133,7 +142,8 @@ const setDialogForExport = (type: DialogForExportTypes) => {
   justify-content: space-between;
   padding: 0 5px;
 }
-.left, .right {
+.left,
+.right {
   display: flex;
   justify-content: center;
   align-items: center;

+ 4 - 4
src/views/Editor/index.vue

@@ -7,8 +7,8 @@
         <CanvasTool class="center-top" />
         <Canvas class="center-body" :style="{ height: `calc(100% - ${remarkHeight + 40}px)` }" />
         <Remark
-          class="center-bottom" 
-          v-model:height="remarkHeight" 
+          class="center-bottom"
+          v-model:height="remarkHeight"
           :style="{ height: `${remarkHeight}px` }"
         />
       </div>
@@ -21,7 +21,7 @@
   <NotesPanel v-if="showNotesPanel" />
 
   <Modal
-    :visible="!!dialogForExport" 
+    :visible="!!dialogForExport"
     :width="680"
     @closed="closeExportDialog()"
   >
@@ -85,4 +85,4 @@ usePasteEvent()
   width: 260px;
   height: 100%;
 }
-</style>
+</style>

+ 2 - 0
src/views/pptEditor/index.ts

@@ -0,0 +1,2 @@
+import pptEditor from "./pptEditor.vue"
+export default pptEditor

+ 44 - 0
src/views/pptEditor/pptEditor.vue

@@ -0,0 +1,44 @@
+<template>
+  <div class="pptEditor"><Editor /></div>
+</template>
+
+<script setup lang="ts">
+import Editor from "../Editor/index.vue"
+import { onMounted } from "vue"
+import { storeToRefs } from "pinia"
+import { useMainStore, useSnapshotStore } from "@/store"
+import { LOCALSTORAGE_KEY_DISCARDED_DB } from "@/configs/storage"
+import { deleteDiscardedDB } from "@/utils/database"
+
+const mainStore = useMainStore()
+const snapshotStore = useSnapshotStore()
+const { databaseId } = storeToRefs(mainStore)
+
+if (import.meta.env.MODE !== "development") {
+  window.onbeforeunload = () => false
+}
+
+onMounted(async () => {
+  await deleteDiscardedDB()
+  snapshotStore.initSnapshotDatabase()
+  mainStore.setAvailableFonts()
+})
+
+// 应用注销时向 localStorage 中记录下本次 indexedDB 的数据库ID,用于之后清除数据库
+window.addEventListener("unload", () => {
+  const discardedDB = localStorage.getItem(LOCALSTORAGE_KEY_DISCARDED_DB)
+  const discardedDBList: string[] = discardedDB ? JSON.parse(discardedDB) : []
+
+  discardedDBList.push(databaseId.value)
+
+  const newDiscardedDB = JSON.stringify(discardedDBList)
+  localStorage.setItem(LOCALSTORAGE_KEY_DISCARDED_DB, newDiscardedDB)
+})
+</script>
+
+<style lang="scss" scoped>
+.pptEditor {
+  width: 100%;
+  height: 100%;
+}
+</style>

+ 44 - 0
src/viewsframe/errorPage/errorPage.vue

@@ -0,0 +1,44 @@
+<template>
+  <div class="errorPage">
+    <div class="error">
+      <div>您访问的页面不存在!</div>
+      <ElButton type="primary" @click="handleGoHome">返回首页</ElButton>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { useRouter } from "vue-router"
+import { ElButton } from "element-plus"
+const router = useRouter()
+
+function handleGoHome() {
+  router.push({
+    path: "/"
+  })
+}
+</script>
+
+<style lang="scss" scoped>
+.errorPage {
+  width: 100%;
+  height: 100%;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  .error {
+    width: 600px;
+    height: 160px;
+    background-color: #fff;
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    align-items: center;
+    & > div:first-child {
+      font-size: 16px;
+      color: #ccc;
+      margin-bottom: 10px;
+    }
+  }
+}
+</style>

+ 2 - 0
src/viewsframe/errorPage/index.ts

@@ -0,0 +1,2 @@
+import errorPage from "./errorPage.vue"
+export default errorPage

+ 2 - 0
src/viewsframe/login/index.ts

@@ -0,0 +1,2 @@
+import login from "./login.vue"
+export default login

+ 7 - 0
src/viewsframe/login/login.vue

@@ -0,0 +1,7 @@
+<template>
+  <div class="login">没有登录的页面</div>
+</template>
+
+<script setup lang="ts"></script>
+
+<style lang="scss" scoped></style>

+ 1 - 7
tsconfig.node.json

@@ -1,12 +1,6 @@
 {
   "extends": "@tsconfig/node18/tsconfig.json",
-  "include": [
-    "vite.config.*",
-    "vitest.config.*",
-    "cypress.config.*",
-    "nightwatch.conf.*",
-    "playwright.config.*"
-  ],
+  "include": ["vite.config.*", "vitest.config.*", "cypress.config.*", "nightwatch.conf.*", "playwright.config.*"],
   "compilerOptions": {
     "composite": true,
     "noEmit": true,

+ 14 - 6
vite.config.ts

@@ -1,11 +1,11 @@
-import { fileURLToPath, URL } from 'node:url'
+import { fileURLToPath, URL } from "node:url"
 
-import { defineConfig } from 'vite'
-import vue from '@vitejs/plugin-vue'
+import { defineConfig } from "vite"
+import vue from "@vitejs/plugin-vue"
 
 // https://vitejs.dev/config/
 export default defineConfig({
-  base: '',
+  base: "",
   plugins: [vue()],
   css: {
     preprocessorOptions: {
@@ -19,11 +19,19 @@ export default defineConfig({
   },
   resolve: {
     alias: {
-      '@': fileURLToPath(new URL('./src', import.meta.url))
+      "@": fileURLToPath(new URL("./src", import.meta.url))
     }
   },
   server: {
     port: 9527,
-    host: '0.0.0.0'
+    host: "0.0.0.0",
+    proxy: {
+      // 正则表达式写法
+      "^/pptApi/.*": {
+        target: "https://test.kt.colexiu.com",
+        changeOrigin: true,
+        rewrite: path => path.replace(/^\/pptApi/, "")
+      }
+    }
   }
 })

Some files were not shown because too many files changed in this diff