pansl před 2 roky
revize
44e0a95bdc
100 změnil soubory, kde provedl 2171 přidání a 0 odebrání
  1. 67 0
      .env.example
  2. 16 0
      .eslintignore
  3. 267 0
      .eslintrc-auto-import.json
  4. 33 0
      .eslintrc.js
  5. 25 0
      .gitignore
  6. 4 0
      .husky/commit-msg
  7. 4 0
      .husky/pre-commit
  8. 9 0
      .prettierignore
  9. 36 0
      .prettierrc.js
  10. 95 0
      README.md
  11. 22 0
      index.html
  12. 9 0
      jsconfig.json
  13. 66 0
      package.json
  14. 6 0
      postcss.config.js
  15. binární
      public/favicon.ico
  16. 3 0
      src/App.vue
  17. 25 0
      src/api/auth/index.ts
  18. 33 0
      src/api/auth/types.ts
  19. 100 0
      src/api/bookManage/index.ts
  20. 106 0
      src/api/cp/index.ts
  21. 176 0
      src/api/notice/index.ts
  22. binární
      src/assets/404.png
  23. 1 0
      src/assets/icons/advert.svg
  24. 1 0
      src/assets/icons/brand.svg
  25. 1 0
      src/assets/icons/bug.svg
  26. 1 0
      src/assets/icons/cascader.svg
  27. 1 0
      src/assets/icons/chart.svg
  28. 1 0
      src/assets/icons/client.svg
  29. 1 0
      src/assets/icons/close.svg
  30. 1 0
      src/assets/icons/close_all.svg
  31. 1 0
      src/assets/icons/close_left.svg
  32. 1 0
      src/assets/icons/close_other.svg
  33. 1 0
      src/assets/icons/close_right.svg
  34. 1 0
      src/assets/icons/coupon.svg
  35. 1 0
      src/assets/icons/dashboard.svg
  36. 18 0
      src/assets/icons/dict.svg
  37. 12 0
      src/assets/icons/dict_item.svg
  38. 1 0
      src/assets/icons/download.svg
  39. 1 0
      src/assets/icons/drag.svg
  40. 1 0
      src/assets/icons/edit.svg
  41. 1 0
      src/assets/icons/exit-fullscreen.svg
  42. 1 0
      src/assets/icons/eye-open.svg
  43. 1 0
      src/assets/icons/eye.svg
  44. 1 0
      src/assets/icons/fullscreen.svg
  45. 1 0
      src/assets/icons/github.svg
  46. 1 0
      src/assets/icons/goods-list.svg
  47. 1 0
      src/assets/icons/goods.svg
  48. 1 0
      src/assets/icons/guide.svg
  49. 1 0
      src/assets/icons/homepage.svg
  50. 1 0
      src/assets/icons/lab.svg
  51. 1 0
      src/assets/icons/language.svg
  52. 1 0
      src/assets/icons/link.svg
  53. 6 0
      src/assets/icons/logo.svg
  54. 1 0
      src/assets/icons/menu.svg
  55. 1 0
      src/assets/icons/message.svg
  56. 1 0
      src/assets/icons/money.svg
  57. 2 0
      src/assets/icons/monitor.svg
  58. 1 0
      src/assets/icons/nested.svg
  59. 1 0
      src/assets/icons/number.svg
  60. 1 0
      src/assets/icons/order.svg
  61. 1 0
      src/assets/icons/password.svg
  62. 1 0
      src/assets/icons/peoples.svg
  63. 1 0
      src/assets/icons/perm.svg
  64. 1 0
      src/assets/icons/publish.svg
  65. 1 0
      src/assets/icons/qq.svg
  66. 1 0
      src/assets/icons/rabbitmq.svg
  67. 1 0
      src/assets/icons/rate.svg
  68. 1 0
      src/assets/icons/redis.svg
  69. 1 0
      src/assets/icons/refresh.svg
  70. 1 0
      src/assets/icons/role.svg
  71. 1 0
      src/assets/icons/security.svg
  72. 1 0
      src/assets/icons/shopping.svg
  73. 1 0
      src/assets/icons/size.svg
  74. 1 0
      src/assets/icons/skill.svg
  75. 1 0
      src/assets/icons/system.svg
  76. 1 0
      src/assets/icons/theme.svg
  77. 1 0
      src/assets/icons/tree.svg
  78. 1 0
      src/assets/icons/user.svg
  79. 1 0
      src/assets/icons/uv.svg
  80. 9 0
      src/assets/icons/valid_code.svg
  81. 1 0
      src/assets/icons/wechat.svg
  82. binární
      src/assets/login-left.png
  83. binární
      src/assets/logo.png
  84. 28 0
      src/components/404/index.vue
  85. 126 0
      src/components/IconSelect/index.vue
  86. 100 0
      src/components/Pagination/index.vue
  87. 141 0
      src/components/SIdentify/index.vue
  88. 15 0
      src/components/Screenfull/index.vue
  89. 40 0
      src/components/SvgIcon/index.vue
  90. 38 0
      src/components/ThemePicker/index.vue
  91. 71 0
      src/components/ToTop/index.vue
  92. 133 0
      src/components/Upload/MultiUpload.vue
  93. 89 0
      src/components/Upload/SingleUpload.vue
  94. 69 0
      src/components/WangEditor/index.vue
  95. 52 0
      src/components/WarningBar/index.vue
  96. 16 0
      src/components/admin/buttons/add.vue
  97. 16 0
      src/components/admin/buttons/destroy.vue
  98. 16 0
      src/components/admin/buttons/show.vue
  99. 18 0
      src/components/admin/buttons/update.vue
  100. 0 0
      src/components/admin/dialog/index.vue

+ 67 - 0
.env.example

@@ -0,0 +1,67 @@
+APP_NAME=内容中台管理系统
+APP_ENV=local
+APP_KEY=
+APP_DEBUG=true
+APP_URL=http://api.zynrzt.com
+
+LOG_CHANNEL=stack
+LOG_DEPRECATIONS_CHANNEL=null
+LOG_LEVEL=debug
+
+DB_CONNECTION=mysql
+DB_HOST=127.0.0.1
+DB_PORT=3306
+DB_DATABASE=laravel
+DB_USERNAME=root
+DB_PASSWORD=
+DB_PREFIX=
+
+BROADCAST_DRIVER=log
+CACHE_DRIVER=file
+FILESYSTEM_DISK=local
+QUEUE_CONNECTION=sync
+SESSION_DRIVER=file
+SESSION_LIFETIME=120
+
+MEMCACHED_HOST=127.0.0.1
+
+REDIS_HOST=127.0.0.1
+REDIS_PASSWORD=null
+REDIS_PORT=6379
+
+MAIL_MAILER=smtp
+MAIL_HOST=mailhog
+MAIL_PORT=1025
+MAIL_USERNAME=null
+MAIL_PASSWORD=null
+MAIL_ENCRYPTION=null
+MAIL_FROM_ADDRESS="hello@example.com"
+MAIL_FROM_NAME="${APP_NAME}"
+
+AWS_ACCESS_KEY_ID=
+AWS_SECRET_ACCESS_KEY=
+AWS_DEFAULT_REGION=us-east-1
+AWS_BUCKET=
+AWS_USE_PATH_STYLE_ENDPOINT=false
+
+PUSHER_APP_ID=
+PUSHER_APP_KEY=
+PUSHER_APP_SECRET=
+PUSHER_HOST=
+PUSHER_PORT=443
+PUSHER_SCHEME=https
+PUSHER_APP_CLUSTER=mt1
+
+VITE_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
+VITE_PUSHER_HOST="${PUSHER_HOST}"
+VITE_PUSHER_PORT="${PUSHER_PORT}"
+VITE_PUSHER_SCHEME="${PUSHER_SCHEME}"
+VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
+VITE_BASE_URL=${APP_URL}/api/
+VITE_APP_NAME=${APP_NAME}
+
+ALIOSS_BUCKET=
+ALIOSS_ACCESS_ID=
+ALIOSS_ACCESS_SECRET=
+ALIOSS_ENDPOINT=
+ALIOSS_UPLOAD_DIR=

+ 16 - 0
.eslintignore

@@ -0,0 +1,16 @@
+*.sh
+node_modules
+*.md
+*.woff
+*.ttf
+.vscode
+.idea
+dist
+/public
+/docs
+.husky
+.local
+/bin
+.eslintrc.js
+prettier.config.js
+src/assets

+ 267 - 0
.eslintrc-auto-import.json

@@ -0,0 +1,267 @@
+{
+  "globals": {
+    "EffectScope": true,
+    "ElForm": true,
+    "ElMessage": true,
+    "ElMessageBox": true,
+    "asyncComputed": true,
+    "autoResetRef": true,
+    "computed": true,
+    "computedAsync": true,
+    "computedEager": true,
+    "computedInject": true,
+    "computedWithControl": true,
+    "controlledComputed": true,
+    "controlledRef": true,
+    "createApp": true,
+    "createEventHook": true,
+    "createGlobalState": true,
+    "createInjectionState": true,
+    "createReactiveFn": true,
+    "createSharedComposable": true,
+    "createUnrefFn": true,
+    "customRef": true,
+    "debouncedRef": true,
+    "debouncedWatch": true,
+    "defineAsyncComponent": true,
+    "defineComponent": true,
+    "eagerComputed": true,
+    "effectScope": true,
+    "extendRef": true,
+    "getCurrentInstance": true,
+    "getCurrentScope": true,
+    "h": true,
+    "ignorableWatch": true,
+    "inject": true,
+    "isDefined": true,
+    "isProxy": true,
+    "isReactive": true,
+    "isReadonly": true,
+    "isRef": true,
+    "makeDestructurable": true,
+    "markRaw": true,
+    "nextTick": true,
+    "onActivated": true,
+    "onBeforeMount": true,
+    "onBeforeUnmount": true,
+    "onBeforeUpdate": true,
+    "onClickOutside": true,
+    "onDeactivated": true,
+    "onErrorCaptured": true,
+    "onKeyStroke": true,
+    "onLongPress": true,
+    "onMounted": true,
+    "onRenderTracked": true,
+    "onRenderTriggered": true,
+    "onScopeDispose": true,
+    "onServerPrefetch": true,
+    "onStartTyping": true,
+    "onUnmounted": true,
+    "onUpdated": true,
+    "pausableWatch": true,
+    "provide": true,
+    "reactify": true,
+    "reactifyObject": true,
+    "reactive": true,
+    "reactiveComputed": true,
+    "reactiveOmit": true,
+    "reactivePick": true,
+    "readonly": true,
+    "ref": true,
+    "refAutoReset": true,
+    "refDebounced": true,
+    "refDefault": true,
+    "refThrottled": true,
+    "refWithControl": true,
+    "resolveComponent": true,
+    "resolveDirective": true,
+    "resolveRef": true,
+    "resolveUnref": true,
+    "shallowReactive": true,
+    "shallowReadonly": true,
+    "shallowRef": true,
+    "syncRef": true,
+    "syncRefs": true,
+    "templateRef": true,
+    "throttledRef": true,
+    "throttledWatch": true,
+    "toRaw": true,
+    "toReactive": true,
+    "toRef": true,
+    "toRefs": true,
+    "triggerRef": true,
+    "tryOnBeforeMount": true,
+    "tryOnBeforeUnmount": true,
+    "tryOnMounted": true,
+    "tryOnScopeDispose": true,
+    "tryOnUnmounted": true,
+    "unref": true,
+    "unrefElement": true,
+    "until": true,
+    "useActiveElement": true,
+    "useArrayEvery": true,
+    "useArrayFilter": true,
+    "useArrayFind": true,
+    "useArrayFindIndex": true,
+    "useArrayJoin": true,
+    "useArrayMap": true,
+    "useArrayReduce": true,
+    "useArraySome": true,
+    "useArrayUnique": true,
+    "useAsyncQueue": true,
+    "useAsyncState": true,
+    "useAttrs": true,
+    "useBase64": true,
+    "useBattery": true,
+    "useBluetooth": true,
+    "useBreakpoints": true,
+    "useBroadcastChannel": true,
+    "useBrowserLocation": true,
+    "useCached": true,
+    "useClipboard": true,
+    "useCloned": true,
+    "useColorMode": true,
+    "useConfirmDialog": true,
+    "useCounter": true,
+    "useCssModule": true,
+    "useCssVar": true,
+    "useCssVars": true,
+    "useCurrentElement": true,
+    "useCycleList": true,
+    "useDark": true,
+    "useDateFormat": true,
+    "useDebounce": true,
+    "useDebounceFn": true,
+    "useDebouncedRefHistory": true,
+    "useDeviceMotion": true,
+    "useDeviceOrientation": true,
+    "useDevicePixelRatio": true,
+    "useDevicesList": true,
+    "useDisplayMedia": true,
+    "useDocumentVisibility": true,
+    "useDraggable": true,
+    "useDropZone": true,
+    "useElementBounding": true,
+    "useElementByPoint": true,
+    "useElementHover": true,
+    "useElementSize": true,
+    "useElementVisibility": true,
+    "useEventBus": true,
+    "useEventListener": true,
+    "useEventSource": true,
+    "useEyeDropper": true,
+    "useFavicon": true,
+    "useFetch": true,
+    "useFileDialog": true,
+    "useFileSystemAccess": true,
+    "useFocus": true,
+    "useFocusWithin": true,
+    "useFps": true,
+    "useFullscreen": true,
+    "useGamepad": true,
+    "useGeolocation": true,
+    "useIdle": true,
+    "useImage": true,
+    "useInfiniteScroll": true,
+    "useIntersectionObserver": true,
+    "useInterval": true,
+    "useIntervalFn": true,
+    "useKeyModifier": true,
+    "useLastChanged": true,
+    "useLocalStorage": true,
+    "useMagicKeys": true,
+    "useManualRefHistory": true,
+    "useMediaControls": true,
+    "useMediaQuery": true,
+    "useMemoize": true,
+    "useMemory": true,
+    "useMounted": true,
+    "useMouse": true,
+    "useMouseInElement": true,
+    "useMousePressed": true,
+    "useMutationObserver": true,
+    "useNavigatorLanguage": true,
+    "useNetwork": true,
+    "useNow": true,
+    "useObjectUrl": true,
+    "useOffsetPagination": true,
+    "useOnline": true,
+    "usePageLeave": true,
+    "useParallax": true,
+    "usePermission": true,
+    "usePointer": true,
+    "usePointerLock": true,
+    "usePointerSwipe": true,
+    "usePreferredColorScheme": true,
+    "usePreferredContrast": true,
+    "usePreferredDark": true,
+    "usePreferredLanguages": true,
+    "usePreferredReducedMotion": true,
+    "usePrevious": true,
+    "useRafFn": true,
+    "useRefHistory": true,
+    "useResizeObserver": true,
+    "useScreenOrientation": true,
+    "useScreenSafeArea": true,
+    "useScriptTag": true,
+    "useScroll": true,
+    "useScrollLock": true,
+    "useSessionStorage": true,
+    "useShare": true,
+    "useSlots": true,
+    "useSorted": true,
+    "useSpeechRecognition": true,
+    "useSpeechSynthesis": true,
+    "useStepper": true,
+    "useStorage": true,
+    "useStorageAsync": true,
+    "useStyleTag": true,
+    "useSupported": true,
+    "useSwipe": true,
+    "useTemplateRefsList": true,
+    "useTextDirection": true,
+    "useTextSelection": true,
+    "useTextareaAutosize": true,
+    "useThrottle": true,
+    "useThrottleFn": true,
+    "useThrottledRefHistory": true,
+    "useTimeAgo": true,
+    "useTimeout": true,
+    "useTimeoutFn": true,
+    "useTimeoutPoll": true,
+    "useTimestamp": true,
+    "useTitle": true,
+    "useToNumber": true,
+    "useToString": true,
+    "useToggle": true,
+    "useTransition": true,
+    "useUrlSearchParams": true,
+    "useUserMedia": true,
+    "useVModel": true,
+    "useVModels": true,
+    "useVibrate": true,
+    "useVirtualList": true,
+    "useWakeLock": true,
+    "useWebNotification": true,
+    "useWebSocket": true,
+    "useWebWorker": true,
+    "useWebWorkerFn": true,
+    "useWindowFocus": true,
+    "useWindowScroll": true,
+    "useWindowSize": true,
+    "watch": true,
+    "watchArray": true,
+    "watchAtMost": true,
+    "watchDebounced": true,
+    "watchEffect": true,
+    "watchIgnorable": true,
+    "watchOnce": true,
+    "watchPausable": true,
+    "watchPostEffect": true,
+    "watchSyncEffect": true,
+    "watchThrottled": true,
+    "watchTriggerable": true,
+    "watchWithFilter": true,
+    "whenever": true
+  }
+}

+ 33 - 0
.eslintrc.js

@@ -0,0 +1,33 @@
+module.exports = {
+  env: {
+    browser: true,
+    es2021: true,
+    node: true
+  },
+  globals: {
+    defineProps: 'readonly',
+    defineEmits: 'readonly',
+    defineExpose: 'readonly',
+    DialogType: "readonly",
+    OptionType: "readonly",
+  },
+  parser: 'vue-eslint-parser',
+  extends: [
+    'eslint:recommended',
+    'plugin:vue/vue3-essential',
+    'plugin:@typescript-eslint/recommended',
+    './.eslintrc-auto-import.json'
+  ],
+  parserOptions: {
+    ecmaVersion: 'latest',
+    parser: '@typescript-eslint/parser',
+    sourceType: 'module'
+  },
+  plugins: ['vue', '@typescript-eslint'],
+  rules: {
+    'vue/multi-word-component-names': 'off',
+    '@typescript-eslint/no-empty-function': 'off', // 关闭空方法检查
+    '@typescript-eslint/no-explicit-any': 'off', // 关闭any类型的警告
+    'vue/no-v-model-argument': 'off'
+  }
+};

+ 25 - 0
.gitignore

@@ -0,0 +1,25 @@
+node_modules
+.DS_Store
+dist
+dist-ssr
+*.local
+dist.*
+
+# Editor directories and files
+.idea
+.vscode
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.local
+
+types/components.d.ts
+types/auto-imports.d.ts
+components.d.ts
+auto-imports.d.ts
+pnpm-lock.yaml
+package-lock.json
+yarn.lock
+.env*
+!.env.example

+ 4 - 0
.husky/commit-msg

@@ -0,0 +1,4 @@
+#!/bin/sh
+. "$(dirname "$0")/_/husky.sh"
+
+npx --no-install commitlint --edit $1

+ 4 - 0
.husky/pre-commit

@@ -0,0 +1,4 @@
+#!/bin/sh
+. "$(dirname "$0")/_/husky.sh"
+
+npm run lint

+ 9 - 0
.prettierignore

@@ -0,0 +1,9 @@
+/dist/*
+.local
+.output.js
+/node_modules/**
+
+**/*.svg
+**/*.sh
+
+/public/*

+ 36 - 0
.prettierrc.js

@@ -0,0 +1,36 @@
+/**
+ * 代码格式化配置
+ */
+module.exports = {
+  // 指定每个缩进级别的空格数
+  tabWidth: 2,
+  // 使用制表符而不是空格缩进行
+  useTabs: false,
+  // 在语句末尾打印分号
+  semi: true,
+  // 使用单引号而不是双引号
+  singleQuote: true,
+  // 更改引用对象属性的时间 可选值"<as-needed|consistent|preserve>"
+  quoteProps: 'as-needed',
+  // 多行时尽可能打印尾随逗号。(例如,单行数组永远不会出现逗号结尾。) 可选值"<none|es5|all>",默认none
+  trailingComma: 'none',
+  // 在对象文字中的括号之间打印空格
+  bracketSpacing: true,
+  // 在单独的箭头函数参数周围包括括号 always:(x) => x \ avoid:x => x
+  arrowParens: 'avoid',
+  // 这两个选项可用于格式化以给定字符偏移量(分别包括和不包括)开始和结束的代码
+  rangeStart: 0,
+  rangeEnd: Infinity,
+  // 指定要使用的解析器,不需要写文件开头的 @prettier
+  requirePragma: false,
+  // 不需要自动在文件开头插入 @prettier
+  insertPragma: false,
+  // 换行设置 always\never\preserve
+  proseWrap: 'never',
+  // 指定HTML文件的全局空格敏感度 css\strict\ignore
+  htmlWhitespaceSensitivity: 'css',
+  // Vue文件脚本和样式标签缩进
+  vueIndentScriptAndStyle: false,
+  // 换行符使用 lf 结尾是 可选值"<auto|lf|crlf|cr>"
+  endOfLine: 'lf'
+};

+ 95 - 0
README.md

@@ -0,0 +1,95 @@
+## 官方文档
+
+<a target="_blank" href="https://catchadmin.com/docs/3.0/intro">官方文档</a>
+
+## 项目介绍
+复制 .env.example 重名名为:一下文件
+.env.development
+.env.production
+.env.staging
+
+本项目是基于 [catchAdmin](https://gitee.com/catchadmin/catchAdmin) 抽离的 Vue3 版本后台管理前端解决方案;使用前端主流技术栈 Vue3 + Vite4 + TypeScript + Vue Router + Pinia + Element Plus 等;实现功能包括不限于动态权限路由、按钮权限控制、国际化、主题大小切换等;
+
+## 项目优势
+
+- 基于 vue-element-admin 升级的 Vue3 版本 ,极易上手,减少学习成本;
+- 一套完整适配的微服务权限系统线上接口;
+- 功能全面:国际化、动态路由、按钮权限、主题大小切换;
+- TypeScript 全面支持,包括组件和 API 调用层面;
+- 主流 Vue3 生态和前端技术栈,常用组件极简封装;
+
+## 技术栈
+
+| 技术栈 | 描述 | 官网 |
+| --- | --- | --- |
+| Vue3 | 渐进式 JavaScript 框架 | https://v3.cn.vuejs.org/ |
+| TypeScript | JavaScript 的一个超集 | https://www.tslang.cn/ |
+| Vite | 前端开发与构建工具 | https://cn.vitejs.dev/ |
+| Element Plus | 基于 Vue 3,面向设计师和开发者的组件库 | https://element-plus.gitee.io/zh-CN/ |
+| Pinia | 新一代状态管理工具 | https://pinia.vuejs.org/ |
+| Vue Router | Vue.js 的官方路由 | https://router.vuejs.org/zh/ |
+
+## 环境要求
+
+- Node 环境
+
+  版本:16+
+
+- 开发工具
+
+  VSCode
+
+- 必装插件
+
+  - Vue Language Features (Volar)
+  - TypeScript Vue Plugin (Volar)
+
+## 项目启动
+
+1. 安装依赖
+
+   ```bash
+   npm install | pnpm install
+   ```
+
+2. 启动运行
+
+   ```bash
+   npm run dev | pnpm dev
+   ```
+
+
+
+## 项目部署
+
+- 本地打包
+
+  ```bash
+  npm run build | pnpm build
+  ```
+
+  生成的静态文件位于项目根目录 `dist` 文件夹下
+
+- 上传文件
+
+  创建 `/mnt/nginx/html` 目录,将打包生成 `dist` 下的所有文件拷贝至此工作目录下
+
+- nginx.cofig 配置
+
+  ```bash
+  server {
+      listen     80;
+      server_name  localhost;
+
+      location / {
+          root /mnt/nginx/html;
+          index index.html index.htm;
+      }
+
+      # 代理转发请求至网关,prod-api标识解决跨域,vapi.youlai.tech 线上接口地址,注意后面/
+      location /prod-api/ {
+          proxy_pass http://vapi.youlai.tech/;
+      }
+  }
+
+  ```

+ 22 - 0
index.html

@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html lang="zh">
+<head>
+    <meta charset="UTF-8" />
+    <link rel="icon" href="/public/favicon.ico" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title><%- title %></title>
+    <style>
+        body {
+            margin: 0;
+            padding: 0;
+            overflow: hidden;
+            width: 100%;
+            height: 100%;
+        }
+    </style>
+</head>
+<body>
+<div id="app"></div>
+<script type="module" src="/src/main.ts"></script>
+</body>
+</html>

+ 9 - 0
jsconfig.json

@@ -0,0 +1,9 @@
+{
+    "_comment": "This file is used to trick IntelliJ/Webstorm/PHPStorm to use the correct alias as defined in vite.config.js",
+    "compilerOptions": {
+        "baseUrl": ".",
+        "paths": {
+            "@/*": ["src/*"]
+        }
+    }
+}

+ 66 - 0
package.json

@@ -0,0 +1,66 @@
+{
+  "name": "catchadmin",
+  "private": false,
+  "version": "0.0.1",
+  "license": "ISC",
+  "scripts": {
+    "serve": "pnpm dev",
+    "dev": "vite",
+    "build": "vite build",
+    "build:tsc": "vue-tsc --noEmit && vite build",
+    "build:staging": "rimraf dist && vite build --mode staging",
+    "preview": "vite preview"
+  },
+  "dependencies": {
+    "@element-plus/icons-vue": "^2.1.0",
+    "@heroicons/vue": "^2.0.14",
+    "@tinymce/tinymce-vue": "^5.0.1",
+    "@vueuse/core": "^9.12.0",
+    "@wangeditor/editor": "^5.0.0",
+    "@wangeditor/editor-for-vue": "^5.1.10",
+    "autoprefixer": "^10.4.13",
+    "axios": "^1.3.4",
+    "element-plus": "^2.2.33",
+    "moment": "^2.29.4",
+    "nprogress": "^0.2.0",
+    "pinia": "^2.0.32",
+    "postcss": "^8.4.21",
+    "sortablejs": "^1.15.0",
+    "tailwindcss": "^3.2.2",
+    "terser": "^5.16.5",
+    "vue": "^3.2.47",
+    "vue-i18n": "9",
+    "vue-router": "4.1.6",
+    "vuedraggable": "^4.1.0",
+    "xlsx": "^0.18.5"
+  },
+  "devDependencies": {
+    "@iconify-json/ep": "^1.1.10",
+    "@iconify-json/logos": "^1.1.22",
+    "@rollup/plugin-alias": "^4.0.3",
+    "@types/mockjs": "^1.0.7",
+    "@types/node": "^18.14.6",
+    "@types/nprogress": "^0.2.0",
+    "@typescript-eslint/eslint-plugin": "^5.54.0",
+    "@typescript-eslint/parser": "^5.54.0",
+    "@vitejs/plugin-vue": "^4.0.0",
+    "@vitejs/plugin-vue-jsx": "^3.0.0",
+    "eslint": "^8.35.0",
+    "eslint-config-standard": "^17.0.0",
+    "eslint-plugin-import": "^2.27.5",
+    "eslint-plugin-n": "^15.6.0",
+    "eslint-plugin-promise": "^6.1.1",
+    "eslint-plugin-vue": "^9.9.0",
+    "prettier": "2.8.4",
+    "rxjs": "^7.8.0",
+    "sass": "^1.58.0",
+    "typescript": "^4.9.5",
+    "unplugin-auto-import": "^0.14.4",
+    "unplugin-icons": "^0.15.3",
+    "unplugin-vue-components": "^0.24.0",
+    "vite": "^4.1.4",
+    "vite-plugin-html": "^3.2.0",
+    "vite-plugin-svg-icons": "^2.0.1",
+    "vue-tsc": "^1.2.0"
+  }
+}

+ 6 - 0
postcss.config.js

@@ -0,0 +1,6 @@
+module.exports = {
+  plugins: {
+    tailwindcss: {},
+    autoprefixer: {}
+  }
+}

binární
public/favicon.ico


+ 3 - 0
src/App.vue

@@ -0,0 +1,3 @@
+<template>
+  <router-view />
+</template>

+ 25 - 0
src/api/auth/index.ts

@@ -0,0 +1,25 @@
+import http from '@/support/http';
+/**
+ * 忘记密码发送验证码
+ */
+export function sendCode(params: object) {
+  return http.post('/send_code', params).then(res => {
+    const r = res.data;
+    const code = r.code;
+    if (code === 1e4) {
+      return r;
+    }
+  });
+}
+/**
+ * 忘记密码重置
+ */
+export function resetPassword(params: object) {
+  return http.post('/reset_password', params).then(res => {
+    const r = res.data;
+    const code = r.code;
+    if (code === 1e4) {
+      return r;
+    }
+  });
+}

+ 33 - 0
src/api/auth/types.ts

@@ -0,0 +1,33 @@
+/**
+ * 登录数据类型
+ */
+export interface LoginData {
+  username: string;
+  password: string;
+  verifyCode: string
+  /**
+   * 验证码Code
+   */
+  //verifyCode: string;
+  /**
+   * 验证码Code服务端缓存key(UUID)
+   */
+  // verifyCodeKey: string;
+}
+
+/**
+ * Token响应类型
+ */
+export interface TokenResult {
+  accessToken: string;
+  refreshToken: string;
+  expires: number;
+}
+
+/**
+ * 验证码类型
+ */
+export interface VerifyCode {
+  verifyCodeImg: string;
+  verifyCodeKey: string;
+}

+ 100 - 0
src/api/bookManage/index.ts

@@ -0,0 +1,100 @@
+import http from '@/support/http';
+/**
+ * 籍列表接口
+ */
+export function bookList(params: object) {
+  return http.post('/contentManage/book/list', params).then(res => {
+    const r = res.data;
+    const code = r.code;
+    if (code === 1e4) {
+      return r;
+    }
+  });
+}
+/**
+ * 合作结算方式获取
+ */
+export function bookSettlementypes() {
+  return http.get('/contentManage/book/settlementypes').then(res => {
+    const r = res.data;
+    const code = r.code;
+    if (code === 1e4) {
+      return r;
+    }
+  });
+}
+/**
+ * 获取书籍版权分库信息
+ */
+export function bookDistribute(bid: number, params?: object) {
+  return http.get(`/contentManage/book/distribute/${bid}`, params).then(res => {
+    const r = res.data;
+    const code = r.code;
+    if (code === 1e4) {
+      return r;
+    }
+  });
+}
+/**
+ * 提交书籍版权分库信息
+ */
+export function bookDistributeSave(bid: number, params: object) {
+  return http
+    .post(`/contentManage/book/distribute/${bid}`, params)
+    .then(res => {
+      const r = res.data;
+      const code = r.code;
+      if (code === 1e4) {
+        return r;
+      }
+    });
+}
+/**
+ * 提交书籍版权分库信息
+ */
+export function bookEditAuthor(params: object) {
+  return http.post('contentManage/book/edit_author', params).then(res => {
+    const r = res.data;
+    const code = r.code;
+    if (code === 1e4) {
+      return r;
+    }
+  });
+}
+/**
+ * 上传书籍
+ */
+export function bookImport(params: object) {
+  return http.post('/contentManage/book/import', params).then(res => {
+    const r = res.data;
+    const code = r.code;
+    if (code === 1e4) {
+      return r;
+    }
+  });
+}
+/**
+ * 创建书籍
+ */
+export function bookCreateBook(params: object) {
+  return http.post('/contentManage/book/createBook', params).then(res => {
+    const r = res.data;
+    const code = r.code;
+    if (code === 1e4) {
+      return r;
+    }
+  });
+}
+
+/**
+ * 书籍分类
+ */
+export function bookCategorylist(params?: object) {
+  return http.get(`/contentManage/book/categorylist`, params).then(res => {
+    const r = res.data;
+    const code = r.code;
+    if (code === 1e4) {
+      return r;
+    }
+  });
+}

+ 106 - 0
src/api/cp/index.ts

@@ -0,0 +1,106 @@
+import http from '@/support/http';
+import { Message } from '@element-plus/icons-vue/dist/types';
+/**
+ * cp选择项
+ */
+export function cpOptions(params: object) {
+  return http.get('/contentManage/cp/options', params).then(res => {
+    const r = res.data;
+    const code = r.code;
+    if (code === 1e4) {
+      return r;
+    }
+  });
+}
+/**
+ * 数据中心列表
+ */
+export function subscribeStatisticDataList(params: object) {
+  return http
+    .get('/contentManage/cp/subscribeStatisticData/list', params)
+    .then(res => {
+      const r = res.data;
+      const code = r.code;
+      if (code === 1e4) {
+        return r;
+      }
+    });
+}
+/**
+ * 数据中心列表
+ */
+export function subscribeStatisticDataListStatistic(params: object) {
+  return http
+    .get('/contentManage/cp/subscribeStatisticData/listStatistic', params)
+    .then(res => {
+      const r = res.data;
+      const code = r.code;
+      if (code === 1e4) {
+        return r;
+      }
+    });
+}
+/**
+ * cp结算列表
+ */
+export function subscribeStatisticDataMonthList(params: object) {
+  return http
+    .get('/contentManage/cp/subscribeStatisticData/monthList', params)
+    .then(res => {
+      const r = res.data;
+      const code = r.code;
+      if (code === 1e4) {
+        return r;
+      }
+    });
+}
+/**
+ * cp列表
+ */
+export function cpManageCpList(params?: object) {
+  return http.get('/contentManage/cp/manage/cp_list', params).then(res => {
+    const r = res.data;
+    const code = r.code;
+    if (code === 1e4) {
+      return r;
+    }
+  });
+}
+/**
+ * cp结算列表-某个cp某月的全部书籍结算金额导出
+ */
+export function exportListCpMonthFinalAmount(params: object) {
+  return http
+    .get(
+      '/contentManage/cp/subscribeStatisticData/listCpMonthFinalAmount',
+      params
+    )
+    .then(res => {
+      const r = res.data;
+      const code = r.code;
+      if (code === 1e4) {
+        return r;
+      }
+    });
+}
+/**
+ * 同步CP书籍
+ */
+export function cpCpCollection(params: object) {
+  return http.get('/contentManage/cp/cpCollection', params).then(res => {
+    const r = res.data;
+    const code = r.code;
+    if (code === 1e4) {
+      return r;
+    }
+  });
+}
+/**
+ * 修改结算状态
+ */
+export function saveFinalState(params: object) {
+  return http.post(
+    '/contentManage/cp/subscribeStatisticData/saveFinalState',
+    params
+  );
+}

+ 176 - 0
src/api/notice/index.ts

@@ -0,0 +1,176 @@
+import http from '@/support/http';
+import { Message } from '@element-plus/icons-vue/dist/types';
+/**
+ * 通知分类列表
+ */
+export function noticesTypesList(params: object) {
+  return http.get('/contentManage/notices/types/list', params).then(res => {
+    const r = res.data;
+    const code = r.code;
+    if (code === 1e4) {
+      return r;
+    }
+  });
+}
+/**
+ * 通知管理的公告列表
+ */
+export function noticesList(params: object) {
+  return http.get('/contentManage/notices/notice/list', params).then(res => {
+    const r = res.data;
+    const code = r.code;
+    if (code === 1e4) {
+      return r;
+    }
+  });
+}
+/**
+ * 添加公告分类
+ */
+export function noticesTypesAdd(params: object) {
+  return http.post('/contentManage/notices/types/add', params).then(res => {
+    const r = res.data;
+    const code = r.code;
+    if (code === 1e4) {
+      return r;
+    }
+  });
+}
+/**
+ * 删除公告分类
+ */
+export function noticesTypesDel(id: number | string, params: object) {
+  return http
+    .post(`/contentManage/notices/types/del/${id}`, params)
+    .then(res => {
+      const r = res.data;
+      const code = r.code;
+      if (code === 1e4) {
+        return r;
+      }
+    });
+}
+/**
+ * 添加公告
+ */
+export function noticesAdd(params: object) {
+  return http.post('/contentManage/notices/notice/add', params).then(res => {
+    const r = res.data;
+    const code = r.code;
+    if (code === 1e4) {
+      return r;
+    }
+  });
+}
+
+/**
+ * 保存编辑公告
+ */
+export function noticesEdit(id: number | string, params: object) {
+  return http
+    .post(`/contentManage/notices/notice/edit/${id}`, params)
+    .then(res => {
+      const r = res.data;
+      const code = r.code;
+      if (code === 1e4) {
+        return r;
+      }
+    });
+}
+
+/**
+ * 删除公告
+ */
+export function noticesdel(id: number | string, params: object) {
+  return http
+    .post(`/contentManage/notices/notice/del/${id}`, params)
+    .then(res => {
+      const r = res.data;
+      const code = r.code;
+      if (code === 1e4) {
+        return r;
+      }
+    });
+}
+/**
+ * 获取公告通知对象选择项
+ */
+export function noticesObjOption(params: object) {
+  return http
+    .post(`/contentManage/notices/notice/obj_option`, params)
+    .then(res => {
+      const r = res.data;
+      const code = r.code;
+      if (code === 1e4) {
+        return r;
+      }
+    });
+}
+/**
+ * 我的公告列表
+ */
+export function noticeListMine(params: object) {
+  return http.get(`/contentManage/notices/notice/mine`, params).then(res => {
+    const r = res.data;
+    const code = r.code;
+    if (code === 1e4) {
+      return r;
+    }
+  });
+}
+/**
+ * 用户查看详情
+ */
+export function noticeDetail(id: string | number, params?: object) {
+  return http
+    .get(`/contentManage/notices/notice/detail/${id}`, params)
+    .then(res => {
+      const r = res.data;
+      const code = r.code;
+      if (code === 1e4) {
+        return r;
+      }
+    });
+}
+/**
+ * 用户设置已读
+ */
+export function noticeRead(id: string | number, params?: object) {
+  return http
+    .get(`/contentManage/notices/notice/read/${id}`, params)
+    .then(res => {
+      const r = res.data;
+      const code = r.code;
+      if (code === 1e4) {
+        return r;
+      }
+    });
+}
+/**
+ * 用户删除
+ */
+export function noticeUserDel(id: string | number, params?: object) {
+  return http
+    .get(`/contentManage/notices/notice/user_del/${id}`, params)
+    .then(res => {
+      const r = res.data;
+      const code = r.code;
+      if (code === 1e4) {
+        return r;
+      }
+    });
+}
+/**
+ * 获取一条弹窗
+ */
+export function noticePopup( params?: object) {
+  return http
+    .get(`/contentManage/notices/notice/popup`, params)
+    .then(res => {
+      const r = res.data;
+      const code = r.code;
+      if (code === 1e4) {
+        return r;
+      }
+    });
+}

binární
src/assets/404.png


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 1 - 0
src/assets/icons/advert.svg


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 1 - 0
src/assets/icons/brand.svg


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 1 - 0
src/assets/icons/bug.svg


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 1 - 0
src/assets/icons/cascader.svg


+ 1 - 0
src/assets/icons/chart.svg

@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M0 54.857h36.571V128H0V54.857zM91.429 27.43H128V128H91.429V27.429zM45.714 0h36.572v128H45.714V0z"/></svg>

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 1 - 0
src/assets/icons/client.svg


+ 1 - 0
src/assets/icons/close.svg

@@ -0,0 +1 @@
+ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 36 36"><path d="M19.41 18l8.29-8.29a1 1 0 0 0-1.41-1.41L18 16.59l-8.29-8.3a1 1 0 0 0-1.42 1.42l8.3 8.29l-8.3 8.29A1 1 0 1 0 9.7 27.7l8.3-8.29l8.29 8.29a1 1 0 0 0 1.41-1.41z" fill="currentColor"></path></svg>

+ 1 - 0
src/assets/icons/close_all.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 36 36"><path d="M26 17H10a1 1 0 0 0 0 2h16a1 1 0 0 0 0-2z" fill="currentColor"></path></svg>

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 1 - 0
src/assets/icons/close_left.svg


+ 1 - 0
src/assets/icons/close_other.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 20 20"><path d="M3 5h14V3H3v2zm12 8V7H5v6h10zM3 17h14v-2H3v2z" fill="currentColor"></path></svg>

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 1 - 0
src/assets/icons/close_right.svg


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 1 - 0
src/assets/icons/coupon.svg


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 1 - 0
src/assets/icons/dashboard.svg


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 18 - 0
src/assets/icons/dict.svg


+ 12 - 0
src/assets/icons/dict_item.svg

@@ -0,0 +1,12 @@
+<svg t="1655022848495" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2190"
+  width="200" height="200">
+  <path d="M239.9 492.6m-42 0a42 42 0 1 0 84 0 42 42 0 1 0-84 0Z" p-id="2191"></path>
+  <path d="M826 513.5H378.5c-11.6 0-21-9.4-21-21s9.4-21 21-21H826c11.6 0 21 9.4 21 21s-9.4 21-21 21z" p-id="2192">
+  </path>
+  <path d="M239.9 632.4m-42 0a42 42 0 1 0 84 0 42 42 0 1 0-84 0Z" p-id="2193"></path>
+  <path d="M826 653.4H378.5c-11.6 0-21-9.4-21-21s9.4-21 21-21H826c11.6 0 21 9.4 21 21s-9.4 21-21 21z" p-id="2194">
+  </path>
+  <path
+    d="M882.6 234.9H531.2l-29-46.3c-6.9-11.7-35-54.7-77.8-54.7h-304c-30.9 0-55.9 25-55.9 55.9v623.3c0 42.4 34.5 76.9 76.9 76.9h741.2c42.4 0 76.9-34.5 76.9-76.9V311.8c0-42.4-34.5-76.9-76.9-76.9z m34.9 578.2c0 19.3-15.7 35-35 35H141.4c-19.3 0-35-15.7-35-35V311.8c0-19.3 15.7-35 35-35h741.2c19.3 0 35 15.7 35 35v501.3z"
+    p-id="2195"></path>
+</svg>

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 1 - 0
src/assets/icons/download.svg


+ 1 - 0
src/assets/icons/drag.svg

@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M73.137 29.08h-9.209 29.7L63.886.093 34.373 29.08h20.49v27.035H27.238v17.948h27.625v27.133h18.274V74.063h27.41V56.115h-27.41V29.08zm-9.245 98.827l27.518-26.711H36.59l27.302 26.71zM.042 64.982l27.196 27.029V38.167L.042 64.982zm100.505-26.815V92.01l27.41-27.029-27.41-26.815z"/></svg>

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 1 - 0
src/assets/icons/edit.svg


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 1 - 0
src/assets/icons/exit-fullscreen.svg


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 1 - 0
src/assets/icons/eye-open.svg


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 1 - 0
src/assets/icons/eye.svg


+ 1 - 0
src/assets/icons/fullscreen.svg

@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M38.47 52L52 38.462l-23.648-23.67L43.209 0H.035L0 43.137l14.757-14.865L38.47 52zm74.773 47.726L89.526 76 76 89.536l23.648 23.672L84.795 128h43.174L128 84.863l-14.757 14.863zM89.538 52l23.668-23.648L128 43.207V.038L84.866 0 99.73 14.76 76 38.472 89.538 52zM38.46 76L14.792 99.651 0 84.794v43.173l43.137.033-14.865-14.757L52 89.53 38.46 76z"/></svg>

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 1 - 0
src/assets/icons/github.svg


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 1 - 0
src/assets/icons/goods-list.svg


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 1 - 0
src/assets/icons/goods.svg


+ 1 - 0
src/assets/icons/guide.svg

@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M1.482 70.131l36.204 16.18 69.932-65.485-61.38 70.594 46.435 18.735c1.119.425 2.397-.17 2.797-1.363v-.085L127.998.047 1.322 65.874c-1.12.597-1.519 1.959-1.04 3.151.32.511.72.937 1.2 1.107zm44.676 57.821L64.22 107.26l-18.062-7.834v28.527z"/></svg>

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 1 - 0
src/assets/icons/homepage.svg


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 1 - 0
src/assets/icons/lab.svg


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 1 - 0
src/assets/icons/language.svg


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 1 - 0
src/assets/icons/link.svg


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 6 - 0
src/assets/icons/logo.svg


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 1 - 0
src/assets/icons/menu.svg


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 1 - 0
src/assets/icons/message.svg


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 1 - 0
src/assets/icons/money.svg


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 2 - 0
src/assets/icons/monitor.svg


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 1 - 0
src/assets/icons/nested.svg


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 1 - 0
src/assets/icons/number.svg


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 1 - 0
src/assets/icons/order.svg


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 1 - 0
src/assets/icons/password.svg


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 1 - 0
src/assets/icons/peoples.svg


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 1 - 0
src/assets/icons/perm.svg


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 1 - 0
src/assets/icons/publish.svg


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 1 - 0
src/assets/icons/qq.svg


+ 1 - 0
src/assets/icons/rabbitmq.svg

@@ -0,0 +1 @@
+<svg t="1650625601015" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="38305" width="200" height="200"><path d="M192 128h-128v768h896V384h-384V128h-128v256h-256V128zM384 320v-256h256v256h384v640H0V64h256v256h128z" p-id="38306"></path><path d="M640 576v128h128v-128H640zM576 512h256v256h-256V512z" p-id="38307"></path></svg>

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 1 - 0
src/assets/icons/rate.svg


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 1 - 0
src/assets/icons/redis.svg


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 1 - 0
src/assets/icons/refresh.svg


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 1 - 0
src/assets/icons/role.svg


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 1 - 0
src/assets/icons/security.svg


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 1 - 0
src/assets/icons/shopping.svg


+ 1 - 0
src/assets/icons/size.svg

@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M0 54.857h54.796v18.286H36.531V128H18.265V73.143H0V54.857zm127.857-36.571H91.935V128H72.456V18.286H36.534V0h91.326l-.003 18.286z"/></svg>

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 1 - 0
src/assets/icons/skill.svg


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 1 - 0
src/assets/icons/system.svg


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 1 - 0
src/assets/icons/theme.svg


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 1 - 0
src/assets/icons/tree.svg


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 1 - 0
src/assets/icons/user.svg


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 1 - 0
src/assets/icons/uv.svg


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 9 - 0
src/assets/icons/valid_code.svg


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 1 - 0
src/assets/icons/wechat.svg


binární
src/assets/login-left.png


binární
src/assets/logo.png


+ 28 - 0
src/components/404/index.vue

@@ -0,0 +1,28 @@
+<template>
+  <div :style="bgColor" class="flex flex-col w-full">
+    <img :src="notFound" class="w-full sm:w-3/5 m-auto" />
+    <div class="mr-auto w-full bottom-0 m-auto">
+      <div class="w-full text-center text-base text-gray-400">抱歉,您访问的页面不存在</div>
+      <div @click="push('/')" class="text-center w-full mt-2">
+        <el-button type="primary">回到首页</el-button>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { useRouter } from 'vue-router'
+import { useAppStore } from '@/stores/modules/app'
+import { computed } from 'vue'
+import notFound from '@/assets/404.png'
+
+const { push } = useRouter()
+
+const dark: string = '#161d31;'
+const light: string = 'rgb(241,245,249);'
+
+const appStore = useAppStore()
+const bgColor = computed(() => {
+  return 'background-color:' + (appStore.getIsDarkMode ? dark : light)
+})
+</script>

+ 126 - 0
src/components/IconSelect/index.vue

@@ -0,0 +1,126 @@
+<script setup lang="ts">
+import { ref, toRef, onMounted } from 'vue';
+
+const props = defineProps({
+  modelValue: {
+    type: String,
+    require: false
+  },
+  /**
+   * 图标选择器宽度
+   */
+  width: {
+    type: String,
+    require: false,
+    default: '400px'
+  }
+});
+
+const emit = defineEmits(['update:modelValue']);
+const visible = ref(false);
+const inputValue = toRef(props, 'modelValue');
+const width = toRef(props, 'width');
+const iconNames: string[] = [];
+const filterIconNames = ref<string[]>([]);
+
+const filterValue = ref('');
+
+function loadIcons() {
+  const icons = import.meta.glob('../../assets/icons/*.svg');
+  for (const icon in icons) {
+    const iconName = icon.split('assets/icons/')[1].split('.svg')[0];
+    iconNames.push(iconName);
+  }
+  filterIconNames.value = iconNames;
+}
+
+/**
+ * 筛选图标
+ */
+function handleIconFilter() {
+  if (filterValue.value) {
+    filterIconNames.value = iconNames.filter(iconName =>
+      iconName.includes(filterValue.value)
+    );
+  } else {
+    filterIconNames.value = iconNames;
+  }
+}
+
+/**
+ * 选择图标
+ *
+ * @param iconName 选择的图标名称
+ */
+function onIconSelect(iconName: string) {
+  emit('update:modelValue', iconName);
+  visible.value = false;
+}
+
+onMounted(() => {
+  loadIcons();
+});
+</script>
+
+<template>
+  <div class="relative" :style="{ width: width }">
+    <el-input v-model="inputValue" readonly @click="visible = !visible" placeholder="点击选择图标">
+      <template #prepend>
+        <svg-icon :icon-class="inputValue"></svg-icon>
+      </template>
+    </el-input>
+
+    <el-popover shadow="none" :visible="visible" placement="bottom-end" trigger="click" :width="400">
+      <template #reference>
+        <div @click="visible = !visible" class="cursor-pointer text-[#999] absolute right-[10px] top-0 height-[32px] leading-[32px]">
+          <i-ep-caret-top v-show="visible"></i-ep-caret-top>
+          <i-ep-caret-bottom v-show="!visible"></i-ep-caret-bottom>
+        </div>
+      </template>
+
+      <!-- 下拉选择弹窗 -->
+      <el-input class="p-2" v-model="filterValue" placeholder="搜索图标" clearable @input="handleIconFilter" />
+      <el-divider border-style="dashed" />
+
+      <el-scrollbar height="300px">
+        <ul class="icon-list">
+          <li class="icon-item" v-for="(iconName, index) in filterIconNames" :key="index" @click="onIconSelect(iconName)">
+            <el-tooltip :content="iconName" placement="bottom" effect="light">
+              <svg-icon color="var(--el-text-color-regular)" :icon-class="iconName" />
+            </el-tooltip>
+          </li>
+        </ul>
+      </el-scrollbar>
+    </el-popover>
+  </div>
+</template>
+
+<style scoped lang="scss">
+.el-divider--horizontal {
+  margin: 10px auto !important;
+}
+.icon-list {
+  display: flex;
+  flex-wrap: wrap;
+  padding-left: 10px;
+  margin-top: 10px;
+
+  .icon-item {
+    cursor: pointer;
+    width: 10%;
+    margin: 0 10px 10px 0;
+    padding: 5px;
+    display: flex;
+    flex-direction: column;
+    justify-items: center;
+    align-items: center;
+    border: 1px solid #ccc;
+    &:hover {
+      border-color: var(--el-color-primary);
+      color: var(--el-color-primary);
+      transition: all 0.2s;
+      transform: scaleX(1.1);
+    }
+  }
+}
+</style>

+ 100 - 0
src/components/Pagination/index.vue

@@ -0,0 +1,100 @@
+<template>
+  <div :class="{ hidden: hidden }" class="pagination-container">
+    <el-pagination
+      :background="background"
+      v-model:current-page="currentPage"
+      v-model:page-size="pageSize"
+      :layout="layout"
+      :page-sizes="pageSizes"
+      :total="total"
+      @size-change="handleSizeChange"
+      @current-change="handleCurrentChange"
+    />
+  </div>
+</template>
+
+<script setup lang="ts">
+import { computed, PropType } from 'vue';
+import { scrollTo } from '@/utils/scroll-to';
+
+const props = defineProps({
+  total: {
+    required: true,
+    type: Number as PropType<number>,
+    default: 0
+  },
+  page: {
+    type: Number,
+    default: 1
+  },
+  limit: {
+    type: Number,
+    default: 20
+  },
+  pageSizes: {
+    type: Array as PropType<number[]>,
+    default() {
+      return [10, 20, 30, 50];
+    }
+  },
+  layout: {
+    type: String,
+    default: 'total, sizes, prev, pager, next, jumper'
+  },
+  background: {
+    type: Boolean,
+    default: true
+  },
+  autoScroll: {
+    type: Boolean,
+    default: true
+  },
+  hidden: {
+    type: Boolean,
+    default: false
+  }
+});
+
+const emit = defineEmits(['update:page', 'update:limit', 'pagination']);
+
+const currentPage = computed<number | undefined>({
+  get: () => props.page,
+  set: value => {
+    emit('update:page', value);
+  }
+});
+
+const pageSize = computed<number | undefined>({
+  get() {
+    return props.limit;
+  },
+  set(val) {
+    emit('update:limit', val);
+  }
+});
+
+function handleSizeChange(val: number) {
+  emit('pagination', { page: currentPage, limit: val });
+  if (props.autoScroll) {
+    scrollTo(0, 800);
+  }
+}
+
+function handleCurrentChange(val: number) {
+  currentPage.value = val;
+  emit('pagination', { page: val, limit: props.limit });
+  if (props.autoScroll) {
+    scrollTo(0, 800);
+  }
+}
+</script>
+
+<style scoped>
+.pagination-container {
+  padding: 32px 16px;
+}
+
+.pagination-container.hidden {
+  display: none;
+}
+</style>

+ 141 - 0
src/components/SIdentify/index.vue

@@ -0,0 +1,141 @@
+<template>
+    <div class="s-canvas">
+        <canvas id="s-canvas" :width="props.contentWidth" :height="props.contentHeight"></canvas>
+    </div>
+</template>
+ 
+<script setup>
+import { onMounted, watch } from 'vue';
+
+const props = defineProps({
+    identifyCode: {
+        type: String,
+        default: '1234'
+    },
+    fontSizeMin: {
+        type: Number,
+        default: 35
+    },
+    fontSizeMax: {
+        type: Number,
+        default: 35
+    },
+    backgroundColorMin: {
+        type: Number,
+        default: 180
+    },
+    backgroundColorMax: {
+        type: Number,
+        default: 240
+    },
+    colorMin: {
+        type: Number,
+        default: 50
+    },
+    colorMax: {
+        type: Number,
+        default: 160
+    },
+    lineColorMin: {
+        type: Number,
+        default: 100
+    },
+    lineColorMax: {
+        type: Number,
+        default: 200
+    },
+    dotColorMin: {
+        type: Number,
+        default: 0
+    },
+    dotColorMax: {
+        type: Number,
+        default: 255
+    },
+    contentWidth: {
+        type: Number,
+        default: 120
+    },
+    contentHeight: {
+        type: Number,
+        default: 40
+    }
+})
+
+// 生成一个随机数
+const randomNum = (min, max) => {
+    return Math.floor(Math.random() * (max - min) + min)
+}
+
+// 生成一个随机的颜色
+const randomColor = (min, max) => {
+    let r = randomNum(min, max)
+    let g = randomNum(min, max)
+    let b = randomNum(min, max)
+    return 'rgb(' + r + ',' + g + ',' + b + ')'
+}
+
+// 绘制干扰线
+const drawLine = (ctx) => {
+    for (let i = 0; i < 3; i++) {
+        ctx.strokeStyle = randomColor(props.lineColorMin, props.lineColorMax)
+        ctx.beginPath()
+        ctx.moveTo(randomNum(0, props.contentWidth), randomNum(0, props.contentHeight))
+        ctx.lineTo(randomNum(0, props.contentWidth), randomNum(0, props.contentHeight))
+        ctx.stroke()
+    }
+}
+
+const drawText = (ctx, txt, i) => {
+    ctx.fillStyle = randomColor(props.colorMin, props.colorMax)
+    ctx.font = randomNum(props.fontSizeMin, props.fontSizeMax) + 'px SimHei'
+    let x = (i + 1) * (props.contentWidth / (props.identifyCode.length + 1))
+    let y = randomNum(props.fontSizeMax, props.contentHeight - 5)
+    var deg = randomNum(-10, 10)
+    // 修改坐标原点和旋转角度
+    ctx.translate(x, y)
+    ctx.rotate(deg * Math.PI / 100)
+    ctx.fillText(txt, 0, 0)
+    // 恢复坐标原点和旋转角度
+    ctx.rotate(-deg * Math.PI / 100)
+    ctx.translate(-x, -y)
+}
+
+const drawDot = (ctx) => {
+    // 绘制干扰点
+    for (let i = 0; i < 30; i++) {
+        ctx.fillStyle = randomColor(0, 255)
+        ctx.beginPath()
+        ctx.arc(randomNum(0, props.contentWidth), randomNum(0, props.contentHeight), 1, 0, 2 * Math.PI)
+        ctx.fill()
+    }
+}
+
+const drawPic = () => {
+    let canvas = document.getElementById('s-canvas')
+    let ctx = canvas.getContext('2d')
+    ctx.textBaseline = 'bottom'
+    // 绘制背景
+    ctx.fillStyle = randomColor(props.backgroundColorMin, props.backgroundColorMax)
+    ctx.fillRect(0, 0, props.contentWidth, props.contentHeight)
+    // 绘制文字
+    for (let i = 0; i < props.identifyCode.length; i++) {
+        drawText(ctx, props.identifyCode[i], i)
+    }
+    drawLine(ctx)
+    drawDot(ctx)
+}
+
+watch(() => props.identifyCode, (newValue, oldValue) => {
+    drawPic()
+})
+
+onMounted(() => {
+    drawPic()
+})
+</script>
+<style scoped lang='css'>
+.s-canvas {
+    cursor: pointer;
+}
+</style>

+ 15 - 0
src/components/Screenfull/index.vue

@@ -0,0 +1,15 @@
+<template>
+  <div class="cursor-pointer w-[40px] h-[50px] leading-[50px] text-center">
+    <svg-icon
+      :icon-class="isFullscreen ? 'exit-fullscreen' : 'fullscreen'"
+      @click="toggle"
+    />
+  </div>
+</template>
+
+<script setup lang="ts">
+import { useFullscreen } from '@vueuse/core';
+import SvgIcon from '@/components/SvgIcon/index.vue';
+
+const { isFullscreen, toggle } = useFullscreen();
+</script>

+ 40 - 0
src/components/SvgIcon/index.vue

@@ -0,0 +1,40 @@
+<template>
+  <svg
+    aria-hidden="true"
+    class="svg-icon"
+    :style="'width:' + size + ';height:' + size"
+  >
+    <use :xlink:href="symbolId" :fill="color" />
+  </svg>
+</template>
+
+<script setup lang="ts">
+import { computed } from 'vue';
+
+const props = defineProps({
+  prefix: {
+    type: String,
+    default: 'icon'
+  },
+  iconClass: {
+    type: String,
+    required: false
+  },
+  color: {
+    type: String
+  },
+  size: {
+    type: String,
+    default: '1em'
+  }
+});
+
+const symbolId = computed(() => `#${props.prefix}-${props.iconClass}`);
+</script>
+
+<style scoped>
+.svg-icon {
+  overflow: hidden;
+  fill: currentColor;
+}
+</style>

+ 38 - 0
src/components/ThemePicker/index.vue

@@ -0,0 +1,38 @@
+<template>
+  <el-color-picker
+    :predefine="[
+      '#409EFF',
+      '#1890ff',
+      '#304156',
+      '#212121',
+      '#11a983',
+      '#13c2c2',
+      '#6959CD',
+      '#f5222d'
+    ]"
+    class="theme-picker"
+    popper-class="theme-picker-dropdown"
+  />
+</template>
+
+<script setup lang="ts">
+import { useSettingsStore } from '@/store/modules/settings';
+const settingsStore = useSettingsStore();
+</script>
+
+<style>
+.theme-message,
+.theme-picker-dropdown {
+  z-index: 9999 !important;
+}
+
+.theme-picker .el-color-picker__trigger {
+  height: 26px !important;
+  width: 26px !important;
+  padding: 2px;
+}
+
+.theme-picker-dropdown .el-color-dropdown__link-btn {
+  display: none;
+}
+</style>

+ 71 - 0
src/components/ToTop/index.vue

@@ -0,0 +1,71 @@
+<template>
+  <div class="affix">
+    <div class="back-to-top" v-show="isshow" @click="onBackToTop">
+      <ArrowUp class="w-6" />
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref, onMounted } from 'vue'
+import { ArrowUp } from '@element-plus/icons-vue';
+
+const isshow = ref(false)
+onMounted(() => {
+  window.addEventListener("scroll", () => {
+    const scrolltop = document.getElementById('content')?.scrollTop;
+    if (scrolltop !== undefined) {
+      if (scrolltop >= 50) {
+        isshow.value = true;
+      }
+      if (scrolltop == 0) {
+        isshow.value = false;
+      }
+    }
+  }, true);
+})
+function onBackToTop() {
+  let top = document.getElementById('content')?.scrollTop //获取点击时页面的滚动条纵坐标位置
+  const timeTop = setInterval(() => {
+    const contentHtml = document.getElementById('content')
+    if (contentHtml !== null && top !== undefined) {
+      contentHtml.scrollTop = top -= 180 //一次减180往上滑动
+      if (top <= 0) {
+        clearInterval(timeTop)
+      }
+    }
+  }, 10) //定时调用函数使其更顺滑
+}
+</script>
+
+<style scoped lang="scss">
+.affix {
+  position: fixed;
+  z-index: 9999;
+  bottom: 40px;
+  right: 40px;
+
+  .back-to-top {
+    cursor: pointer;
+    width: 40px;
+    height: 40px;
+    border: 2px solid #000;
+    border-radius: 50%;
+    transition: none;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+
+    &:hover {
+      transform: scale(1.2);
+    }
+  }
+}
+</style>
+
+<style lang="scss" scoped>
+.is-dark.back-to-top {
+  color: #fff;
+  border: 2px solid #fff;
+}
+</style>

+ 133 - 0
src/components/Upload/MultiUpload.vue

@@ -0,0 +1,133 @@
+<!--
+  多图上传组件
+  @author: youlaitech
+  @date 2022/11/20
+-->
+
+<template>
+  <el-upload v-model:file-list="fileList" list-type="picture-card" :before-upload="handleBeforeUpload" :http-request="handleUpload" :on-remove="handleRemove" :on-preview="previewImg" :limit="props.limit">
+    <i-ep-plus />
+  </el-upload>
+
+  <el-dialog v-model="dialogVisible">
+    <img w-full :src="previewImgUrl" alt="Preview Image" />
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+import {
+  UploadRawFile,
+  UploadRequestOptions,
+  UploadUserFile,
+  UploadFile,
+  UploadProps
+} from 'element-plus';
+// import { uploadFileApi, deleteFileApi } from '@/api/file';
+
+const emit = defineEmits(['update:modelValue']);
+
+const props = defineProps({
+  /**
+   * 文件路径集合
+   */
+  modelValue: {
+    type: Array<string>,
+    default: [] as Array<string>
+  },
+  /**
+   * 文件上传数量限制
+   */
+  limit: {
+    type: Number,
+    default: 5
+  }
+});
+
+const previewImgUrl = ref('');
+const dialogVisible = ref(false);
+
+const fileList = ref([] as UploadUserFile[]);
+watch(
+  () => props.modelValue,
+  (newVal: string[]) => {
+    const filePaths = fileList.value.map(file => file.url);
+    // 监听modelValue文件集合值未变化时,跳过赋值
+    if (
+      filePaths.length > 0 &&
+      filePaths.length === newVal.length &&
+      filePaths.every(x => newVal.some(y => y === x)) &&
+      newVal.every(y => filePaths.some(x => x === y))
+    ) {
+      return;
+    }
+
+    fileList.value = newVal.map(filePath => {
+      return { url: filePath } as UploadUserFile;
+    });
+  },
+  { immediate: true }
+);
+
+/**
+ * 自定义图片上传
+ *
+ * @param params
+ */
+async function handleUpload(options: UploadRequestOptions): Promise<any> {
+  return;
+  // 上传API调用
+  const { data: fileInfo } = await uploadFileApi(options.file);
+
+  // 上传成功需手动替换文件路径为远程URL,否则图片地址为预览地址 blob:http://
+  const fileIndex = fileList.value.findIndex(
+    file => file.uid == (options.file as any).uid
+  );
+
+  fileList.value.splice(fileIndex, 1, {
+    name: fileInfo.name,
+    url: fileInfo.url
+  } as UploadUserFile);
+
+  emit(
+    'update:modelValue',
+    fileList.value.map(file => file.url)
+  );
+}
+
+/**
+ * 删除图片
+ */
+function handleRemove(removeFile: UploadFile) {
+  return;
+  const filePath = removeFile.url;
+
+  if (filePath) {
+    deleteFileApi(filePath).then(() => {
+      // 删除成功回调
+      emit(
+        'update:modelValue',
+        fileList.value.map(file => file.url)
+      );
+    });
+  }
+}
+
+/**
+ * 限制用户上传文件的格式和大小
+ */
+function handleBeforeUpload(file: UploadRawFile) {
+  if (file.size > 2 * 1048 * 1048) {
+    ElMessage.warning('上传图片不能大于2M');
+    return false;
+  }
+  return true;
+}
+
+/**
+ * 预览图片
+ */
+const previewImg: UploadProps['onPreview'] = uploadFile => {
+  previewImgUrl.value = uploadFile.url!;
+  dialogVisible.value = true;
+};
+</script>

+ 89 - 0
src/components/Upload/SingleUpload.vue

@@ -0,0 +1,89 @@
+<template>
+  <!-- 上传组件 -->
+  <el-upload class="single-uploader" v-model="imgUrl" :show-file-list="false" list-type="picture-card" :before-upload="handleBeforeUpload" :http-request="uploadFile">
+    <img v-if="imgUrl" :src="imgUrl" class="single" />
+    <el-icon v-else class="single-uploader-icon">
+      <Plus />
+    </el-icon>
+  </el-upload>
+</template>
+
+<script setup lang="ts">
+import { computed } from 'vue';
+import { Plus } from '@element-plus/icons-vue';
+import { UploadRawFile, UploadRequestOptions } from 'element-plus';
+// import { uploadFileApi } from '@/api/file';
+
+const emit = defineEmits(['update:modelValue']);
+
+const props = defineProps({
+  modelValue: {
+    type: String,
+    default: ''
+  }
+});
+
+const imgUrl = computed<string | undefined>({
+  get() {
+    return props.modelValue;
+  },
+  set(val) {
+    // imgUrl改变时触发修改父组件绑定的v-model的值
+    emit('update:modelValue', val);
+  }
+});
+
+/**
+ * 自定义图片上传
+ *
+ * @param options
+ */
+async function uploadFile(options: UploadRequestOptions): Promise<any> {
+  return;
+  const { data: fileInfo } = await uploadFileApi(options.file);
+  imgUrl.value = fileInfo.url;
+}
+
+/**
+ * 限制用户上传文件的格式和大小
+ */
+function handleBeforeUpload(file: UploadRawFile) {
+  return;
+  if (file.size > 2 * 1048 * 1048) {
+    ElMessage.warning('上传图片不能大于2M');
+    return false;
+  }
+  return true;
+}
+</script>
+
+<style scoped>
+.single-uploader .single {
+  width: 178px;
+  height: 178px;
+  display: block;
+}
+</style>
+
+<style>
+.single-uploader .el-upload {
+  border: 1px dashed var(--el-border-color);
+  border-radius: 6px;
+  cursor: pointer;
+  position: relative;
+  overflow: hidden;
+  transition: var(--el-transition-duration-fast);
+}
+
+.single-uploader .el-upload:hover {
+  border-color: var(--el-color-primary);
+}
+
+.el-icon.single-uploader-icon {
+  font-size: 28px;
+  color: #8c939d;
+  width: 178px;
+  height: 178px;
+  text-align: center;
+}
+</style>

+ 69 - 0
src/components/WangEditor/index.vue

@@ -0,0 +1,69 @@
+<template>
+  <div style="border: 1px solid #ccc">
+    <!-- 工具栏 -->
+    <Toolbar :editor="editorRef" :defaultConfig="toolbarConfig" style="border-bottom: 1px solid #ccc" :mode="mode" />
+    <!-- 编辑器 -->
+    <Editor :defaultConfig="editorConfig" v-model="defaultHtml" @onChange="handleChange"
+      style="height: 500px; overflow-y: hidden" :mode="mode" @onCreated="handleCreated" />
+  </div>
+</template>
+
+<script setup lang="ts">
+import { onBeforeUnmount, shallowRef, reactive, toRefs } from 'vue';
+import { Editor, Toolbar } from '@wangeditor/editor-for-vue';
+
+// API 引用
+// import { uploadFileApi } from '@/api/file';
+
+const props = defineProps({
+  modelValue: {
+    type: [String],
+    default: ''
+  }
+});
+
+const emit = defineEmits(['update:modelValue']);
+
+// 编辑器实例,必须用 shallowRef
+const editorRef = shallowRef();
+
+const state = reactive({
+  toolbarConfig: {},
+  editorConfig: {
+    placeholder: '请输入内容...',
+    MENU_CONF: {
+      uploadImage: {
+        // 自定义图片上传
+        async customUpload(file: any, insertFn: any) {
+          return
+          uploadFileApi(file).then(response => {
+            const url = response.data.url;
+            insertFn(url);
+          });
+        }
+      }
+    }
+  },
+  defaultHtml: props.modelValue,
+  mode: 'default'
+});
+
+const { toolbarConfig, editorConfig, defaultHtml, mode } = toRefs(state);
+
+const handleCreated = (editor: any) => {
+  editorRef.value = editor; // 记录 editor 实例,重要!
+};
+
+function handleChange(editor: any) {
+  emit('update:modelValue', editor.getHtml());
+}
+
+// 组件销毁时,也及时销毁编辑器
+onBeforeUnmount(() => {
+  const editor = editorRef.value;
+  if (editor == null) return;
+  editor.destroy();
+});
+</script>
+
+<style src="@wangeditor/editor/dist/css/style.css"></style>

+ 52 - 0
src/components/WarningBar/index.vue

@@ -0,0 +1,52 @@
+<template>
+  <div class="warning-bar" :class="href && 'can-click'" @click="open">
+    <el-icon>
+      <warning-filled />
+    </el-icon>
+    <span>
+      {{ title }}
+    </span>
+  </div>
+</template>
+<script setup>
+import { WarningFilled } from '@element-plus/icons-vue';
+const prop = defineProps({
+  title: {
+    type: String,
+    default: ''
+  },
+  href: {
+    type: String,
+    default: ''
+  }
+});
+
+const open = () => {
+  if (prop.href) {
+    window.open(prop.href);
+  }
+};
+</script>
+<style lang="scss" scoped>
+.warning-bar {
+  background-color: #fff5ed;
+  font-size: 14px;
+  padding: 6px 14px;
+  display: flex;
+  align-items: center;
+  border-radius: 2px;
+  .el-icon {
+    font-size: 18px;
+    color: #ed6a0c;
+  }
+  margin-bottom: 12px;
+  span {
+    line-height: 22px;
+    color: #f67207;
+    margin-left: 8px;
+  }
+}
+.can-click {
+  cursor: pointer;
+}
+</style>

+ 16 - 0
src/components/admin/buttons/add.vue

@@ -0,0 +1,16 @@
+<template>
+  <el-button type="primary" :size="size"><Icon name="plus" className="w-4 h-4 mr-1" /> {{ text }}</el-button>
+</template>
+
+<script lang="ts" setup>
+defineProps({
+  size: {
+    type: String,
+    default: 'default',
+  },
+  text: {
+    type: String,
+    default: '新增',
+  },
+})
+</script>

+ 16 - 0
src/components/admin/buttons/destroy.vue

@@ -0,0 +1,16 @@
+<template>
+  <el-button type="danger" :size="size"><Icon name="trash" className="w-4 h-4 mr-1" /> {{ text }}</el-button>
+</template>
+
+<script lang="ts" setup>
+defineProps({
+  size: {
+    type: String,
+    default: 'small',
+  },
+  text: {
+    type: String,
+    default: '删除',
+  },
+})
+</script>

+ 16 - 0
src/components/admin/buttons/show.vue

@@ -0,0 +1,16 @@
+<template>
+  <el-button type="primary" :size="size"><Icon name="eye" className="w-4 h-4 mr-1" /> {{ text }}</el-button>
+</template>
+
+<script lang="ts" setup>
+defineProps({
+  size: {
+    type: String,
+    default: 'small',
+  },
+  text: {
+    type: String,
+    default: '详情',
+  },
+})
+</script>

+ 18 - 0
src/components/admin/buttons/update.vue

@@ -0,0 +1,18 @@
+<template>
+  <el-button type="success" :size="size"><Icon name="pencil-square" className="w-4 h-4 mr-1" /> {{ text }}</el-button>
+</template>
+
+<script lang="ts" setup>
+defineProps({
+  size: {
+    type: String,
+    default: 'small',
+  },
+  text: {
+    type: String,
+    default: '更新',
+  },
+})
+</script>
+
+<style scoped></style>

+ 0 - 0
src/components/admin/dialog/index.vue


Některé soubory nejsou zobrazeny, neboť je v těchto rozdílových datech změněno mnoho souborů