瀏覽代碼

巡检项目更改请求头提交

嘀嘀嘀 1 年之前
父節點
當前提交
71c16fb74a
共有 100 個文件被更改,包括 13778 次插入0 次删除
  1. 1 0
      .env
  2. 0 0
      .env.development
  3. 0 0
      .env.production
  4. 22 0
      .gitignore
  5. 5 0
      babel.config.js
  6. 83 0
      package.json
  7. 7 0
      postcss.config.js
  8. 46 0
      public/html/ie.html
  9. 19 0
      public/index.html
  10. 二進制
      public/logo32-32.png
  11. 2 0
      public/robots.txt
  12. 410 0
      public/tinymce/lang/zh_CN.js
  13. 59 0
      public/tinymce/skins/content/dark/content.css
  14. 7 0
      public/tinymce/skins/content/dark/content.min.css
  15. 49 0
      public/tinymce/skins/content/default/content.css
  16. 7 0
      public/tinymce/skins/content/default/content.min.css
  17. 54 0
      public/tinymce/skins/content/document/content.css
  18. 7 0
      public/tinymce/skins/content/document/content.min.css
  19. 50 0
      public/tinymce/skins/content/writer/content.css
  20. 7 0
      public/tinymce/skins/content/writer/content.min.css
  21. 675 0
      public/tinymce/skins/ui/oxide-dark/content.css
  22. 687 0
      public/tinymce/skins/ui/oxide-dark/content.inline.css
  23. 7 0
      public/tinymce/skins/ui/oxide-dark/content.inline.min.css
  24. 7 0
      public/tinymce/skins/ui/oxide-dark/content.min.css
  25. 29 0
      public/tinymce/skins/ui/oxide-dark/content.mobile.css
  26. 7 0
      public/tinymce/skins/ui/oxide-dark/content.mobile.min.css
  27. 二進制
      public/tinymce/skins/ui/oxide-dark/fonts/tinymce-mobile.woff
  28. 2937 0
      public/tinymce/skins/ui/oxide-dark/skin.css
  29. 7 0
      public/tinymce/skins/ui/oxide-dark/skin.min.css
  30. 673 0
      public/tinymce/skins/ui/oxide-dark/skin.mobile.css
  31. 7 0
      public/tinymce/skins/ui/oxide-dark/skin.mobile.min.css
  32. 693 0
      public/tinymce/skins/ui/oxide/content.css
  33. 687 0
      public/tinymce/skins/ui/oxide/content.inline.css
  34. 7 0
      public/tinymce/skins/ui/oxide/content.inline.min.css
  35. 7 0
      public/tinymce/skins/ui/oxide/content.min.css
  36. 29 0
      public/tinymce/skins/ui/oxide/content.mobile.css
  37. 7 0
      public/tinymce/skins/ui/oxide/content.mobile.min.css
  38. 二進制
      public/tinymce/skins/ui/oxide/fonts/tinymce-mobile.woff
  39. 2937 0
      public/tinymce/skins/ui/oxide/skin.css
  40. 7 0
      public/tinymce/skins/ui/oxide/skin.min.css
  41. 673 0
      public/tinymce/skins/ui/oxide/skin.mobile.css
  42. 7 0
      public/tinymce/skins/ui/oxide/skin.mobile.min.css
  43. 15 0
      src/App.vue
  44. 二進制
      src/assets/default-avatar.png
  45. 二進制
      src/assets/logo36-36.png
  46. 206 0
      src/axios/index.js
  47. 192 0
      src/components/AMap/AMapPolyline.vue
  48. 166 0
      src/components/AMap/AMapPosition.vue
  49. 106 0
      src/components/Breadcrumb/index.vue
  50. 42 0
      src/components/Calendar/Calendar.vue
  51. 37 0
      src/components/Checkbox/index.vue
  52. 75 0
      src/components/FormClear.vue
  53. 42 0
      src/components/Hamburger/index.vue
  54. 117 0
      src/components/Identify/index.vue
  55. 38 0
      src/components/PageFooter/index.vue
  56. 75 0
      src/components/PageHeader/index.vue
  57. 73 0
      src/components/Pagination/index.vue
  58. 63 0
      src/components/SvgIcon/index.vue
  59. 54 0
      src/components/TextLabel/index.vue
  60. 169 0
      src/components/ThemePicker/index.vue
  61. 240 0
      src/components/Tinymce/index.vue
  62. 7 0
      src/components/Tinymce/plugins.js
  63. 18 0
      src/components/Tinymce/toolbar.js
  64. 165 0
      src/components/Upload/cropper.vue
  65. 492 0
      src/components/Upload/index.vue
  66. 二進制
      src/components/Upload/zoom-in.png
  67. 143 0
      src/components/UploadExcel/index.vue
  68. 77 0
      src/config/app.js
  69. 14 0
      src/config/index.js
  70. 77 0
      src/config/patrol.js
  71. 100 0
      src/filters/filters.js
  72. 15 0
      src/icons/config.js
  73. 9 0
      src/icons/index.js
  74. 1 0
      src/icons/svg/clear.svg
  75. 1 0
      src/icons/svg/eye-open.svg
  76. 1 0
      src/icons/svg/eye.svg
  77. 1 0
      src/icons/svg/password.svg
  78. 1 0
      src/icons/svg/patrol/PUE管理.svg
  79. 1 0
      src/icons/svg/patrol/临时任务.svg
  80. 1 0
      src/icons/svg/patrol/任务统计.svg
  81. 1 0
      src/icons/svg/patrol/保养任务.svg
  82. 1 0
      src/icons/svg/patrol/保养计划.svg
  83. 1 0
      src/icons/svg/patrol/全部任务.svg
  84. 1 0
      src/icons/svg/patrol/出入库管理.svg
  85. 1 0
      src/icons/svg/patrol/出入库记录.svg
  86. 1 0
      src/icons/svg/patrol/制度管理.svg
  87. 1 0
      src/icons/svg/patrol/备案管理.svg
  88. 1 0
      src/icons/svg/patrol/巡检任务.svg
  89. 1 0
      src/icons/svg/patrol/巡检任务记录.svg
  90. 1 0
      src/icons/svg/patrol/巡检计划.svg
  91. 1 0
      src/icons/svg/patrol/房间管理.svg
  92. 1 0
      src/icons/svg/patrol/拍照项.svg
  93. 1 0
      src/icons/svg/patrol/接口文档.svg
  94. 1 0
      src/icons/svg/patrol/操作日志管理.svg
  95. 1 0
      src/icons/svg/patrol/数字项.svg
  96. 1 0
      src/icons/svg/patrol/数据备份.svg
  97. 1 0
      src/icons/svg/patrol/文本项.svg
  98. 1 0
      src/icons/svg/patrol/权限管理.svg
  99. 1 0
      src/icons/svg/patrol/检查情况.svg
  100. 0 0
      src/icons/svg/patrol/检查统计.svg

+ 1 - 0
.env

@@ -0,0 +1 @@
+VUE_APP_AAA=123

+ 0 - 0
.env.development


+ 0 - 0
.env.production


+ 22 - 0
.gitignore

@@ -0,0 +1,22 @@
+.DS_Store
+node_modules
+dist
+
+# local env files
+.env.local
+.env.*.local
+
+# Log files
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# Editor directories and files
+.idea
+.vscode
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+/package-lock.json

+ 5 - 0
babel.config.js

@@ -0,0 +1,5 @@
+module.exports = {
+  presets: [
+    '@vue/app'
+  ]
+};

+ 83 - 0
package.json

@@ -0,0 +1,83 @@
+{
+  "name": "element",
+  "version": "0.1.0",
+  "private": true,
+  "scripts": {
+    "serve": "vue-cli-service serve",
+    "build": "vue-cli-service build",
+    "svgo": "svgo -f src/icons/svg -r --config=src/icons/config.js"
+  },
+  "dependencies": {
+    "@fullcalendar/daygrid": "^5.4.0",
+    "@fullcalendar/interaction": "^5.4.0",
+    "@fullcalendar/vue": "^5.4.0",
+    "@tinymce/tinymce-vue": "^3.2.2",
+    "ali-oss": "^6.16.0",
+    "aws-sdk": "^2.556.0",
+    "axios": "^0.26.1",
+    "bankcardinfo": "^2.0.5",
+    "clipboard": "^2.0.10",
+    "core-js": "^3.21.1",
+    "crypto-js": "^4.0.0",
+    "echarts": "^5.3.2",
+    "element-resize-detector": "^1.2.4",
+    "element-ui": "^2.15.7",
+    "file-saver": "^2.0.5",
+    "html2canvas": "^1.4.1",
+    "js-cookie": "^3.0.1",
+    "js-md5": "^0.7.3",
+    "jspdf": "^2.5.1",
+    "lodash": "^4.17.14",
+    "normalize.css": "^8.0.1",
+    "nprogress": "^0.2.0",
+    "pinyin-match": "^1.2.2",
+    "print-js": "^1.6.0",
+    "qrcode2": "^1.2.3",
+    "qs": "^6.10.3",
+    "screenfull": "^5.2.0",
+    "spark-md5": "^3.0.2",
+    "tinymce": "^5.4.1",
+    "velocity-animate": "^1.5.2",
+    "vue": "^2.6.14",
+    "vue-amap": "^0.5.10",
+    "vue-cropper": "^0.5.8",
+    "vue-json-viewer": "^2.2.21",
+    "vue-router": "^3.5.3",
+    "vuedraggable": "^2.24.3",
+    "vuex": "^3.6.2",
+    "xlsx": "^0.17.5"
+  },
+  "devDependencies": {
+    "@vue/cli-plugin-babel": "^4.5.17",
+    "@vue/cli-service": "^4.5.17",
+    "babel-plugin-dynamic-import-node": "^2.3.3",
+    "compression-webpack-plugin": "^6.1.1",
+    "less": "^4.1.2",
+    "less-loader": "^7.3.0",
+    "node-sass": "^6.0.1",
+    "optimize-css-assets-webpack-plugin": "^6.0.1",
+    "postcss-import": "^12.0.1",
+    "postcss-loader": "^4.3.0",
+    "postcss-url": "^8.0.0",
+    "sass": "^1.49.10",
+    "sass-loader": "^10.2.1",
+    "sass-resources-loader": "^2.2.4",
+    "script-ext-html-webpack-plugin": "^2.1.5",
+    "script-loader": "^0.7.2",
+    "svg-sprite-loader": "^6.0.11",
+    "svgo": "^2.8.0",
+    "uglifyjs-webpack-plugin": "^2.2.0",
+    "vue-cli-plugin-element": "^1.0.1",
+    "vue-template-compiler": "^2.6.14",
+    "webpack-bundle-analyzer": "^4.5.0"
+  },
+  "engines": {
+    "node": ">= 14.0.0",
+    "npm": ">= 8.0.0"
+  },
+  "browserslist": [
+    "> 1%",
+    "last 2 versions",
+    "not ie <= 11"
+  ]
+}

+ 7 - 0
postcss.config.js

@@ -0,0 +1,7 @@
+module.exports = {
+    plugins: {
+        "postcss-import": {},
+        "postcss-url": {},
+        "autoprefixer": {}
+    }
+};

File diff suppressed because it is too large
+ 46 - 0
public/html/ie.html


+ 19 - 0
public/index.html

@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<html lang="zh">
+<head>
+    <meta charset="utf-8">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
+    <meta name="viewport" content="width=device-width,initial-scale=1.0">
+    <link rel="icon" href="<%= BASE_URL %>logo32-32.png">
+    <title><%= webpackConfig.name %></title>
+    <!--[if lt IE 11]><script>window.location.href='/html/ie.html';</script><![endif]-->
+</head>
+<body>
+<noscript>
+    <strong>We're sorry but element doesn't work properly without JavaScript enabled. Please enable it to
+        continue.</strong>
+</noscript>
+<div id="app"></div>
+<!-- built files will be auto injected -->
+</body>
+</html>

二進制
public/logo32-32.png


+ 2 - 0
public/robots.txt

@@ -0,0 +1,2 @@
+User-agent: *
+Disallow: /

+ 410 - 0
public/tinymce/lang/zh_CN.js

@@ -0,0 +1,410 @@
+tinymce.addI18n('zh_CN',{
+"Redo": "恢复",
+"Undo": "撤销",
+"Cut": "剪切",
+"Copy": "复制",
+"Paste": "粘贴",
+"Select all": "全选",
+"New document": "新建文档",
+"Ok": "确定",
+"Cancel": "取消",
+"Visual aids": "网格线",
+"Bold": "粗体",
+"Italic": "斜体",
+"Underline": "下划线",
+"Strikethrough": "删除线",
+"Superscript": "上标",
+"Subscript": "下标",
+"Clear formatting": "清除格式",
+"Align left": "左对齐",
+"Align center": "居中",
+"Align right": "右对齐",
+"Justify": "两端对齐",
+"Bullet list": "符号列表",
+"Numbered list": "数字列表",
+"Decrease indent": "减少缩进",
+"Increase indent": "增加缩进",
+"Close": "关闭",
+"Formats": "格式",
+"Your browser doesn't support direct access to the clipboard. Please use the Ctrl+X\/C\/V keyboard shortcuts instead.": "当前浏览器不支持访问剪贴板,请使用快捷键Ctrl+X/C/V复制粘贴",
+"Headers": "标题",
+"Header 1": "标题1",
+"Header 2": "标题2",
+"Header 3": "标题3",
+"Header 4": "标题4",
+"Header 5": "标题5",
+"Header 6": "标题6",
+"Headings": "标题",
+"Heading 1": "标题1",
+"Heading 2": "标题2",
+"Heading 3": "标题3",
+"Heading 4": "标题4",
+"Heading 5": "标题5",
+"Heading 6": "标题6",
+"Preformatted": "预格式化",
+"Div": "Div区块",
+"Pre": "预格式文本",
+"Code": "代码",
+"Paragraph": "段落",
+"Blockquote": "引用",
+"Inline": "文本",
+"Blocks": "区块",
+"Paste is now in plain text mode. Contents will now be pasted as plain text until you toggle this option off.": "当前为纯文本粘贴模式,再次点击可以回到普通粘贴模式。",
+"Fonts": "字体",
+"Font Sizes": "字号",
+"Class": "Class",
+"Browse for an image": "浏览图像",
+"OR": "或",
+"Drop an image here": "拖放一张图片文件至此",
+"Upload": "上传",
+"Block": "块",
+"Align": "对齐",
+"Default": "默认",
+"Circle": "空心圆",
+"Disc": "实心圆",
+"Square": "方块",
+"Lower Alpha": "小写英文字母",
+"Lower Greek": "小写希腊字母",
+"Lower Roman": "小写罗马字母",
+"Upper Alpha": "大写英文字母",
+"Upper Roman": "大写罗马字母",
+"Anchor...": "锚点...",
+"Name": "名称",
+"Id": "id",
+"Id should start with a letter, followed only by letters, numbers, dashes, dots, colons or underscores.": "id应该以字母开头,后跟字母、数字、横线、点、冒号或下划线。",
+"You have unsaved changes are you sure you want to navigate away?": "你对文档的修改尚未保存,确定离开吗?",
+"Restore last draft": "恢复上次的草稿",
+"Special characters...": "特殊字符...",
+"Source code": "HTML源码",
+"Insert\/Edit code sample": "插入/编辑代码示例",
+"Language": "语言",
+"Code sample...": "代码示例...",
+"Color Picker": "选取颜色",
+"R": "R",
+"G": "G",
+"B": "B",
+"Left to right": "从左到右",
+"Right to left": "从右到左",
+"Emoticons...": "表情符号...",
+"Metadata and Document Properties": "元数据和文档属性",
+"Title": "标题",
+"Keywords": "关键词",
+"Description": "描述",
+"Robots": "机器人",
+"Author": "作者",
+"Encoding": "编码",
+"Fullscreen": "全屏",
+"Action": "操作",
+"Shortcut": "快捷键",
+"Help": "帮助",
+"Address": "地址",
+"Focus to menubar": "移动焦点到菜单栏",
+"Focus to toolbar": "移动焦点到工具栏",
+"Focus to element path": "移动焦点到元素路径",
+"Focus to contextual toolbar": "移动焦点到上下文菜单",
+"Insert link (if link plugin activated)": "插入链接 (如果链接插件已激活)",
+"Save (if save plugin activated)": "保存(如果保存插件已激活)",
+"Find (if searchreplace plugin activated)": "查找(如果查找替换插件已激活)",
+"Plugins installed ({0}):": "已安装插件 ({0}):",
+"Premium plugins:": "优秀插件:",
+"Learn more...": "了解更多...",
+"You are using {0}": "你正在使用 {0}",
+"Plugins": "插件",
+"Handy Shortcuts": "快捷键",
+"Horizontal line": "水平分割线",
+"Insert\/edit image": "插入/编辑图片",
+"Image description": "图片描述",
+"Source": "地址",
+"Dimensions": "大小",
+"Constrain proportions": "保持宽高比",
+"General": "常规",
+"Advanced": "高级",
+"Style": "样式",
+"Vertical space": "垂直边距",
+"Horizontal space": "水平边距",
+"Border": "边框",
+"Insert image": "插入图片",
+"Image...": "图片...",
+"Image list": "图片列表",
+"Rotate counterclockwise": "逆时针旋转",
+"Rotate clockwise": "顺时针旋转",
+"Flip vertically": "垂直翻转",
+"Flip horizontally": "水平翻转",
+"Edit image": "编辑图片",
+"Image options": "图片选项",
+"Zoom in": "放大",
+"Zoom out": "缩小",
+"Crop": "裁剪",
+"Resize": "调整大小",
+"Orientation": "方向",
+"Brightness": "亮度",
+"Sharpen": "锐化",
+"Contrast": "对比度",
+"Color levels": "色阶",
+"Gamma": "伽马值",
+"Invert": "反转",
+"Apply": "应用",
+"Back": "后退",
+"Insert date\/time": "插入日期/时间",
+"Date\/time": "日期/时间",
+"Insert\/Edit Link": "插入/编辑链接",
+"Insert\/edit link": "插入/编辑链接",
+"Text to display": "显示文字",
+"Url": "地址",
+"Open link in...": "链接打开方式...",
+"Current window": "当前窗口打开",
+"None": "在当前窗口/框架打开",
+"New window": "在新窗口打开",
+"Remove link": "删除链接",
+"Anchors": "锚点",
+"Link...": "链接...",
+"Paste or type a link": "粘贴或输入链接",
+"The URL you entered seems to be an email address. Do you want to add the required mailto: prefix?": "你所填写的URL地址为邮件地址,需要加上mailto:前缀吗?",
+"The URL you entered seems to be an external link. Do you want to add the required http:\/\/ prefix?": "你所填写的URL地址属于外部链接,需要加上http://:前缀吗?",
+"Link list": "链接列表",
+"Insert video": "插入视频",
+"Insert\/edit video": "插入/编辑视频",
+"Insert\/edit media": "插入/编辑媒体",
+"Alternative source": "替代资源",
+"Alternative image URL": "资源备用地址",
+"Media poster (Image URL)": "封面(图片地址)",
+"Paste your embed code below:": "将内嵌代码粘贴在下面:",
+"Embed": "内嵌",
+"Media...": "多媒体...",
+"Nonbreaking space": "不间断空格",
+"Page break": "分页符",
+"Paste as text": "粘贴为文本",
+"Preview": "预览",
+"Print...": "打印...",
+"Save": "保存",
+"Find": "查找",
+"Replace with": "替换为",
+"Replace": "替换",
+"Replace all": "替换全部",
+"Previous": "上一个",
+"Next": "下一个",
+"Find and replace...": "查找并替换...",
+"Could not find the specified string.": "未找到搜索内容。",
+"Match case": "区分大小写",
+"Find whole words only": "全单词匹配",
+"Spell check": "拼写检查",
+"Ignore": "忽略",
+"Ignore all": "忽略全部",
+"Finish": "完成",
+"Add to Dictionary": "添加到字典",
+"Insert table": "插入表格",
+"Table properties": "表格属性",
+"Delete table": "删除表格",
+"Cell": "单元格",
+"Row": "行",
+"Column": "列",
+"Cell properties": "单元格属性",
+"Merge cells": "合并单元格",
+"Split cell": "拆分单元格",
+"Insert row before": "在上方插入",
+"Insert row after": "在下方插入",
+"Delete row": "删除行",
+"Row properties": "行属性",
+"Cut row": "剪切行",
+"Copy row": "复制行",
+"Paste row before": "粘贴到上方",
+"Paste row after": "粘贴到下方",
+"Insert column before": "在左侧插入",
+"Insert column after": "在右侧插入",
+"Delete column": "删除列",
+"Cols": "列",
+"Rows": "行",
+"Width": "宽",
+"Height": "高",
+"Cell spacing": "单元格外间距",
+"Cell padding": "单元格内边距",
+"Show caption": "显示标题",
+"Left": "左对齐",
+"Center": "居中",
+"Right": "右对齐",
+"Cell type": "单元格类型",
+"Scope": "范围",
+"Alignment": "对齐方式",
+"H Align": "水平对齐",
+"V Align": "垂直对齐",
+"Top": "顶部对齐",
+"Middle": "垂直居中",
+"Bottom": "底部对齐",
+"Header cell": "表头单元格",
+"Row group": "行组",
+"Column group": "列组",
+"Row type": "行类型",
+"Header": "表头",
+"Body": "表体",
+"Footer": "表尾",
+"Border color": "边框颜色",
+"Insert template...": "插入模板...",
+"Templates": "模板",
+"Template": "模板",
+"Text color": "文字颜色",
+"Background color": "背景色",
+"Custom...": "自定义...",
+"Custom color": "自定义颜色",
+"No color": "无",
+"Remove color": "删除颜色",
+"Table of Contents": "目录",
+"Show blocks": "显示区块边框",
+"Show invisible characters": "显示不可见字符",
+"Word count": "字数统计",
+"Words: {0}": "字数:{0}",
+"{0} words": "{0} 个字",
+"File": "文件",
+"Edit": "编辑",
+"Insert": "插入",
+"View": "查看",
+"Format": "格式",
+"Table": "表格",
+"Tools": "工具",
+"Powered by {0}": "Powered by {0}",
+"Rich Text Area. Press ALT-F9 for menu. Press ALT-F10 for toolbar. Press ALT-0 for help": "在编辑区按ALT+F9打开菜单,按ALT+F10打开工具栏,按ALT+0查看帮助",
+"Image title": "图片标题",
+"Border width": "边框宽度",
+"Border style": "边框样式",
+"Error": "错误",
+"Warn": "警告",
+"Valid": "有效",
+"To open the popup, press Shift+Enter": "此快捷为软回车(插入<br>)",
+"Rich Text Area. Press ALT-0 for help.": "编辑区. 按Alt+0键打开帮助",
+"System Font": "默认字体",
+"Failed to upload image: {0}": "图片上传失败: {0}",
+"Failed to load plugin: {0} from url {1}": "插件加载失败: {0} - {1}",
+"Failed to load plugin url: {0}": "插件加载失败: {0}",
+"Failed to initialize plugin: {0}": "插件初始化失败: {0}",
+"example": "示例",
+"Search": "查找",
+"All": "全部",
+"Currency": "货币",
+"Text": "文本",
+"Quotations": "引用",
+"Mathematical": "数学运算符",
+"Extended Latin": "拉丁语扩充",
+"Symbols": "符号",
+"Arrows": "箭头",
+"User Defined": "自定义",
+"dollar sign": "美元",
+"currency sign": "货币",
+"euro-currency sign": "欧元",
+"colon sign": "冒号",
+"cruzeiro sign": "克鲁赛罗币",
+"french franc sign": "法郎",
+"lira sign": "里拉",
+"mill sign": "密尔",
+"naira sign": "奈拉",
+"peseta sign": "比塞塔",
+"rupee sign": "卢比",
+"won sign": "韩元",
+"new sheqel sign": "新谢克尔",
+"dong sign": "越南盾",
+"kip sign": "老挝基普",
+"tugrik sign": "图格里克",
+"drachma sign": "德拉克马",
+"german penny symbol": "德国便士",
+"peso sign": "比索",
+"guarani sign": "瓜拉尼",
+"austral sign": "澳元",
+"hryvnia sign": "格里夫尼亚",
+"cedi sign": "塞地",
+"livre tournois sign": "里弗弗尔",
+"spesmilo sign": "一千spesoj的货币符号,该货币未使用",
+"tenge sign": "坚戈",
+"indian rupee sign": "印度卢比",
+"turkish lira sign": "土耳其里拉",
+"nordic mark sign": "北欧马克",
+"manat sign": "马纳特",
+"ruble sign": "卢布",
+"yen character": "日元",
+"yuan character": "人民币元",
+"yuan character, in hong kong and taiwan": "元的繁体字",
+"yen\/yuan character variant one": "元(大写)",
+"Loading emoticons...": "正在加载表情文字...",
+"Could not load emoticons": "不能加载表情文字",
+"People": "人类",
+"Animals and Nature": "动物和自然",
+"Food and Drink": "食物和饮品",
+"Activity": "活动",
+"Travel and Places": "旅游和地点",
+"Objects": "物件",
+"Flags": "旗帜",
+"Characters": "字数",
+"Characters (no spaces)": "字数(不含空格)",
+"Error: Form submit field collision.": "错误: 表单提交字段冲突.",
+"Error: No form element found.": "错误: 未找到可用的form.",
+"Update": "更新",
+"Color swatch": "颜色样本",
+"Turquoise": "青绿",
+"Green": "绿色",
+"Blue": "蓝色",
+"Purple": "紫色",
+"Navy Blue": "海军蓝",
+"Dark Turquoise": "深蓝绿色",
+"Dark Green": "暗绿",
+"Medium Blue": "中蓝",
+"Medium Purple": "中紫",
+"Midnight Blue": "深蓝",
+"Yellow": "黄色",
+"Orange": "橙色",
+"Red": "红色",
+"Light Gray": "浅灰",
+"Gray": "灰色",
+"Dark Yellow": "暗黄",
+"Dark Orange": "暗橙",
+"Dark Red": "暗红",
+"Medium Gray": "中灰",
+"Dark Gray": "深灰",
+"Black": "黑色",
+"White": "白色",
+"Switch to or from fullscreen mode": "切换全屏模式",
+"Open help dialog": "打开帮助对话框",
+"history": "历史",
+"styles": "样式",
+"formatting": "格式化",
+"alignment": "对齐",
+"indentation": "缩进",
+"permanent pen": "记号笔",
+"comments": "注释",
+"Anchor": "锚点",
+"Special character": "特殊字符",
+"Code sample": "代码示例",
+"Color": "颜色",
+"Emoticons": "表情",
+"Document properties": "文档属性",
+"Image": "图片",
+"Insert link": "插入链接",
+"Target": "目标",
+"Link": "链接",
+"Poster": "封面",
+"Media": "音视频",
+"Print": "打印",
+"Prev": "上一个",
+"Find and replace": "查找并替换",
+"Whole words": "全字匹配",
+"Spellcheck": "拼写检查",
+"Caption": "标题",
+"Insert template": "插入模板",
+//以下为补充汉化内容 by 莫若卿
+"Code view": "代码区域",
+"Select...": "选择...",
+"Format Painter": "格式刷",
+"No templates defined.": "无内置模板",
+"Special character...": "特殊字符...",
+"Open link": "打开链接",
+"None": "无",
+"Count": "统计",
+"Document": "整个文档",
+"Selection": "选取部分",
+"Words": "字词数",
+"{0} characters": "{0} 个字符",
+"Alternative source URL": "替代资源地址",
+"Alternative description": "替代说明文字",
+"Accessibility": "可访问性",
+"Image is decorative": "仅用于装饰",
+//帮助窗口内的文字
+"Version": "版本",
+"Keyboard Navigation": "键盘导航",
+"Open popup menu for split buttons": "该组合键的作用是软回车(插入br)",
+});

+ 59 - 0
public/tinymce/skins/content/dark/content.css

@@ -0,0 +1,59 @@
+/**
+ * Copyright (c) Tiny Technologies, Inc. All rights reserved.
+ * Licensed under the LGPL or a commercial license.
+ * For LGPL see License.txt in the project root for license information.
+ * For commercial licenses see https://www.tiny.cloud/
+ */
+body {
+  background-color: #2f3742;
+  color: #dfe0e4;
+  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
+  line-height: 1.4;
+  margin: 1rem;
+}
+a {
+  color: #4099ff;
+}
+table {
+  border-collapse: collapse;
+}
+table th,
+table td {
+  border: 1px solid #6d737b;
+  padding: 0.4rem;
+}
+figure {
+  display: table;
+  margin: 1rem auto;
+}
+figure figcaption {
+  color: #8a8f97;
+  display: block;
+  margin-top: 0.25rem;
+  text-align: center;
+}
+hr {
+  border-color: #6d737b;
+  border-style: solid;
+  border-width: 1px 0 0 0;
+}
+code {
+  background-color: #6d737b;
+  border-radius: 3px;
+  padding: 0.1rem 0.2rem;
+}
+/* Make text in selected cells in tables dark and readable */
+td[data-mce-selected],
+th[data-mce-selected] {
+  color: #333;
+}
+.mce-content-body:not([dir=rtl]) blockquote {
+  border-left: 2px solid #6d737b;
+  margin-left: 1.5rem;
+  padding-left: 1rem;
+}
+.mce-content-body[dir=rtl] blockquote {
+  border-right: 2px solid #6d737b;
+  margin-right: 1.5rem;
+  padding-right: 1rem;
+}

File diff suppressed because it is too large
+ 7 - 0
public/tinymce/skins/content/dark/content.min.css


+ 49 - 0
public/tinymce/skins/content/default/content.css

@@ -0,0 +1,49 @@
+/**
+ * Copyright (c) Tiny Technologies, Inc. All rights reserved.
+ * Licensed under the LGPL or a commercial license.
+ * For LGPL see License.txt in the project root for license information.
+ * For commercial licenses see https://www.tiny.cloud/
+ */
+body {
+  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
+  line-height: 1.4;
+  margin: 1rem;
+}
+table {
+  border-collapse: collapse;
+}
+table th,
+table td {
+  border: 1px solid #ccc;
+  padding: 0.4rem;
+}
+figure {
+  display: table;
+  margin: 1rem auto;
+}
+figure figcaption {
+  color: #999;
+  display: block;
+  margin-top: 0.25rem;
+  text-align: center;
+}
+hr {
+  border-color: #ccc;
+  border-style: solid;
+  border-width: 1px 0 0 0;
+}
+code {
+  background-color: #e8e8e8;
+  border-radius: 3px;
+  padding: 0.1rem 0.2rem;
+}
+.mce-content-body:not([dir=rtl]) blockquote {
+  border-left: 2px solid #ccc;
+  margin-left: 1.5rem;
+  padding-left: 1rem;
+}
+.mce-content-body[dir=rtl] blockquote {
+  border-right: 2px solid #ccc;
+  margin-right: 1.5rem;
+  padding-right: 1rem;
+}

File diff suppressed because it is too large
+ 7 - 0
public/tinymce/skins/content/default/content.min.css


+ 54 - 0
public/tinymce/skins/content/document/content.css

@@ -0,0 +1,54 @@
+/**
+ * Copyright (c) Tiny Technologies, Inc. All rights reserved.
+ * Licensed under the LGPL or a commercial license.
+ * For LGPL see License.txt in the project root for license information.
+ * For commercial licenses see https://www.tiny.cloud/
+ */
+@media screen {
+  html {
+    background: #f4f4f4;
+    min-height: 100%;
+  }
+}
+body {
+  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
+}
+@media screen {
+  body {
+    background-color: #fff;
+    box-shadow: 0 0 4px rgba(0, 0, 0, 0.15);
+    box-sizing: border-box;
+    margin: 1rem auto 0;
+    max-width: 820px;
+    min-height: calc(100vh - 1rem);
+    padding: 4rem 6rem 6rem 6rem;
+  }
+}
+table {
+  border-collapse: collapse;
+}
+table th,
+table td {
+  border: 1px solid #ccc;
+  padding: 0.4rem;
+}
+figure figcaption {
+  color: #999;
+  margin-top: 0.25rem;
+  text-align: center;
+}
+hr {
+  border-color: #ccc;
+  border-style: solid;
+  border-width: 1px 0 0 0;
+}
+.mce-content-body:not([dir=rtl]) blockquote {
+  border-left: 2px solid #ccc;
+  margin-left: 1.5rem;
+  padding-left: 1rem;
+}
+.mce-content-body[dir=rtl] blockquote {
+  border-right: 2px solid #ccc;
+  margin-right: 1.5rem;
+  padding-right: 1rem;
+}

File diff suppressed because it is too large
+ 7 - 0
public/tinymce/skins/content/document/content.min.css


+ 50 - 0
public/tinymce/skins/content/writer/content.css

@@ -0,0 +1,50 @@
+/**
+ * Copyright (c) Tiny Technologies, Inc. All rights reserved.
+ * Licensed under the LGPL or a commercial license.
+ * For LGPL see License.txt in the project root for license information.
+ * For commercial licenses see https://www.tiny.cloud/
+ */
+body {
+  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
+  line-height: 1.4;
+  margin: 1rem auto;
+  max-width: 900px;
+}
+table {
+  border-collapse: collapse;
+}
+table th,
+table td {
+  border: 1px solid #ccc;
+  padding: 0.4rem;
+}
+figure {
+  display: table;
+  margin: 1rem auto;
+}
+figure figcaption {
+  color: #999;
+  display: block;
+  margin-top: 0.25rem;
+  text-align: center;
+}
+hr {
+  border-color: #ccc;
+  border-style: solid;
+  border-width: 1px 0 0 0;
+}
+code {
+  background-color: #e8e8e8;
+  border-radius: 3px;
+  padding: 0.1rem 0.2rem;
+}
+.mce-content-body:not([dir=rtl]) blockquote {
+  border-left: 2px solid #ccc;
+  margin-left: 1.5rem;
+  padding-left: 1rem;
+}
+.mce-content-body[dir=rtl] blockquote {
+  border-right: 2px solid #ccc;
+  margin-right: 1.5rem;
+  padding-right: 1rem;
+}

File diff suppressed because it is too large
+ 7 - 0
public/tinymce/skins/content/writer/content.min.css


File diff suppressed because it is too large
+ 675 - 0
public/tinymce/skins/ui/oxide-dark/content.css


File diff suppressed because it is too large
+ 687 - 0
public/tinymce/skins/ui/oxide-dark/content.inline.css


File diff suppressed because it is too large
+ 7 - 0
public/tinymce/skins/ui/oxide-dark/content.inline.min.css


File diff suppressed because it is too large
+ 7 - 0
public/tinymce/skins/ui/oxide-dark/content.min.css


+ 29 - 0
public/tinymce/skins/ui/oxide-dark/content.mobile.css

@@ -0,0 +1,29 @@
+/**
+ * Copyright (c) Tiny Technologies, Inc. All rights reserved.
+ * Licensed under the LGPL or a commercial license.
+ * For LGPL see License.txt in the project root for license information.
+ * For commercial licenses see https://www.tiny.cloud/
+ */
+.tinymce-mobile-unfocused-selections .tinymce-mobile-unfocused-selection {
+  /* Note: this file is used inside the content, so isn't part of theming */
+  background-color: green;
+  display: inline-block;
+  opacity: 0.5;
+  position: absolute;
+}
+body {
+  -webkit-text-size-adjust: none;
+}
+body img {
+  /* this is related to the content margin */
+  max-width: 96vw;
+}
+body table img {
+  max-width: 95%;
+}
+body {
+  font-family: sans-serif;
+}
+table {
+  border-collapse: collapse;
+}

+ 7 - 0
public/tinymce/skins/ui/oxide-dark/content.mobile.min.css

@@ -0,0 +1,7 @@
+/**
+ * Copyright (c) Tiny Technologies, Inc. All rights reserved.
+ * Licensed under the LGPL or a commercial license.
+ * For LGPL see License.txt in the project root for license information.
+ * For commercial licenses see https://www.tiny.cloud/
+ */
+.tinymce-mobile-unfocused-selections .tinymce-mobile-unfocused-selection{background-color:green;display:inline-block;opacity:.5;position:absolute}body{-webkit-text-size-adjust:none}body img{max-width:96vw}body table img{max-width:95%}body{font-family:sans-serif}table{border-collapse:collapse}

二進制
public/tinymce/skins/ui/oxide-dark/fonts/tinymce-mobile.woff


File diff suppressed because it is too large
+ 2937 - 0
public/tinymce/skins/ui/oxide-dark/skin.css


File diff suppressed because it is too large
+ 7 - 0
public/tinymce/skins/ui/oxide-dark/skin.min.css


+ 673 - 0
public/tinymce/skins/ui/oxide-dark/skin.mobile.css

@@ -0,0 +1,673 @@
+/**
+ * Copyright (c) Tiny Technologies, Inc. All rights reserved.
+ * Licensed under the LGPL or a commercial license.
+ * For LGPL see License.txt in the project root for license information.
+ * For commercial licenses see https://www.tiny.cloud/
+ */
+/* RESET all the things! */
+.tinymce-mobile-outer-container {
+  all: initial;
+  display: block;
+}
+.tinymce-mobile-outer-container * {
+  border: 0;
+  box-sizing: initial;
+  cursor: inherit;
+  float: none;
+  line-height: 1;
+  margin: 0;
+  outline: 0;
+  padding: 0;
+  -webkit-tap-highlight-color: transparent;
+  /* TBIO-3691, stop the gray flicker on touch. */
+  text-shadow: none;
+  white-space: nowrap;
+}
+.tinymce-mobile-icon-arrow-back::before {
+  content: "\e5cd";
+}
+.tinymce-mobile-icon-image::before {
+  content: "\e412";
+}
+.tinymce-mobile-icon-cancel-circle::before {
+  content: "\e5c9";
+}
+.tinymce-mobile-icon-full-dot::before {
+  content: "\e061";
+}
+.tinymce-mobile-icon-align-center::before {
+  content: "\e234";
+}
+.tinymce-mobile-icon-align-left::before {
+  content: "\e236";
+}
+.tinymce-mobile-icon-align-right::before {
+  content: "\e237";
+}
+.tinymce-mobile-icon-bold::before {
+  content: "\e238";
+}
+.tinymce-mobile-icon-italic::before {
+  content: "\e23f";
+}
+.tinymce-mobile-icon-unordered-list::before {
+  content: "\e241";
+}
+.tinymce-mobile-icon-ordered-list::before {
+  content: "\e242";
+}
+.tinymce-mobile-icon-font-size::before {
+  content: "\e245";
+}
+.tinymce-mobile-icon-underline::before {
+  content: "\e249";
+}
+.tinymce-mobile-icon-link::before {
+  content: "\e157";
+}
+.tinymce-mobile-icon-unlink::before {
+  content: "\eca2";
+}
+.tinymce-mobile-icon-color::before {
+  content: "\e891";
+}
+.tinymce-mobile-icon-previous::before {
+  content: "\e314";
+}
+.tinymce-mobile-icon-next::before {
+  content: "\e315";
+}
+.tinymce-mobile-icon-large-font::before,
+.tinymce-mobile-icon-style-formats::before {
+  content: "\e264";
+}
+.tinymce-mobile-icon-undo::before {
+  content: "\e166";
+}
+.tinymce-mobile-icon-redo::before {
+  content: "\e15a";
+}
+.tinymce-mobile-icon-removeformat::before {
+  content: "\e239";
+}
+.tinymce-mobile-icon-small-font::before {
+  content: "\e906";
+}
+.tinymce-mobile-icon-readonly-back::before,
+.tinymce-mobile-format-matches::after {
+  content: "\e5ca";
+}
+.tinymce-mobile-icon-small-heading::before {
+  content: "small";
+}
+.tinymce-mobile-icon-large-heading::before {
+  content: "large";
+}
+.tinymce-mobile-icon-small-heading::before,
+.tinymce-mobile-icon-large-heading::before {
+  font-family: sans-serif;
+  font-size: 80%;
+}
+.tinymce-mobile-mask-edit-icon::before {
+  content: "\e254";
+}
+.tinymce-mobile-icon-back::before {
+  content: "\e5c4";
+}
+.tinymce-mobile-icon-heading::before {
+  /* TODO: Translate */
+  content: "Headings";
+  font-family: sans-serif;
+  font-size: 80%;
+  font-weight: bold;
+}
+.tinymce-mobile-icon-h1::before {
+  content: "H1";
+  font-weight: bold;
+}
+.tinymce-mobile-icon-h2::before {
+  content: "H2";
+  font-weight: bold;
+}
+.tinymce-mobile-icon-h3::before {
+  content: "H3";
+  font-weight: bold;
+}
+.tinymce-mobile-outer-container .tinymce-mobile-disabled-mask {
+  align-items: center;
+  display: flex;
+  justify-content: center;
+  background: rgba(51, 51, 51, 0.5);
+  height: 100%;
+  position: absolute;
+  top: 0;
+  width: 100%;
+}
+.tinymce-mobile-outer-container .tinymce-mobile-disabled-mask .tinymce-mobile-content-container {
+  align-items: center;
+  border-radius: 50%;
+  display: flex;
+  flex-direction: column;
+  font-family: sans-serif;
+  font-size: 1em;
+  justify-content: space-between;
+}
+.tinymce-mobile-outer-container .tinymce-mobile-disabled-mask .tinymce-mobile-content-container .mixin-menu-item {
+  align-items: center;
+  display: flex;
+  justify-content: center;
+  border-radius: 50%;
+  height: 2.1em;
+  width: 2.1em;
+}
+.tinymce-mobile-outer-container .tinymce-mobile-disabled-mask .tinymce-mobile-content-container .tinymce-mobile-content-tap-section {
+  align-items: center;
+  display: flex;
+  justify-content: center;
+  flex-direction: column;
+  font-size: 1em;
+}
+@media only screen and (min-device-width:700px) {
+  .tinymce-mobile-outer-container .tinymce-mobile-disabled-mask .tinymce-mobile-content-container .tinymce-mobile-content-tap-section {
+    font-size: 1.2em;
+  }
+}
+.tinymce-mobile-outer-container .tinymce-mobile-disabled-mask .tinymce-mobile-content-container .tinymce-mobile-content-tap-section .tinymce-mobile-mask-tap-icon {
+  align-items: center;
+  display: flex;
+  justify-content: center;
+  border-radius: 50%;
+  height: 2.1em;
+  width: 2.1em;
+  background-color: white;
+  color: #207ab7;
+}
+.tinymce-mobile-outer-container .tinymce-mobile-disabled-mask .tinymce-mobile-content-container .tinymce-mobile-content-tap-section .tinymce-mobile-mask-tap-icon::before {
+  content: "\e900";
+  font-family: 'tinymce-mobile', sans-serif;
+}
+.tinymce-mobile-outer-container .tinymce-mobile-disabled-mask .tinymce-mobile-content-container .tinymce-mobile-content-tap-section:not(.tinymce-mobile-mask-tap-icon-selected) .tinymce-mobile-mask-tap-icon {
+  z-index: 2;
+}
+.tinymce-mobile-android-container.tinymce-mobile-android-maximized {
+  background: #ffffff;
+  border: none;
+  bottom: 0;
+  display: flex;
+  flex-direction: column;
+  left: 0;
+  position: fixed;
+  right: 0;
+  top: 0;
+}
+.tinymce-mobile-android-container:not(.tinymce-mobile-android-maximized) {
+  position: relative;
+}
+.tinymce-mobile-android-container .tinymce-mobile-editor-socket {
+  display: flex;
+  flex-grow: 1;
+}
+.tinymce-mobile-android-container .tinymce-mobile-editor-socket iframe {
+  display: flex !important;
+  flex-grow: 1;
+  height: auto !important;
+}
+.tinymce-mobile-android-scroll-reload {
+  overflow: hidden;
+}
+:not(.tinymce-mobile-readonly-mode) > .tinymce-mobile-android-selection-context-toolbar {
+  margin-top: 23px;
+}
+.tinymce-mobile-toolstrip {
+  background: #fff;
+  display: flex;
+  flex: 0 0 auto;
+  z-index: 1;
+}
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar {
+  align-items: center;
+  background-color: #fff;
+  border-bottom: 1px solid #cccccc;
+  display: flex;
+  flex: 1;
+  height: 2.5em;
+  width: 100%;
+  /* Make it no larger than the toolstrip, so that it needs to scroll */
+}
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar:not(.tinymce-mobile-context-toolbar) .tinymce-mobile-toolbar-group {
+  align-items: center;
+  display: flex;
+  height: 100%;
+  flex-shrink: 1;
+}
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar:not(.tinymce-mobile-context-toolbar) .tinymce-mobile-toolbar-group > div {
+  align-items: center;
+  display: flex;
+  height: 100%;
+  flex: 1;
+}
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar:not(.tinymce-mobile-context-toolbar) .tinymce-mobile-toolbar-group.tinymce-mobile-exit-container {
+  background: #f44336;
+}
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar:not(.tinymce-mobile-context-toolbar) .tinymce-mobile-toolbar-group.tinymce-mobile-toolbar-scrollable-group {
+  flex-grow: 1;
+}
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar:not(.tinymce-mobile-context-toolbar) .tinymce-mobile-toolbar-group .tinymce-mobile-toolbar-group-item {
+  padding-left: 0.5em;
+  padding-right: 0.5em;
+}
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar:not(.tinymce-mobile-context-toolbar) .tinymce-mobile-toolbar-group .tinymce-mobile-toolbar-group-item.tinymce-mobile-toolbar-button {
+  align-items: center;
+  display: flex;
+  height: 80%;
+  margin-left: 2px;
+  margin-right: 2px;
+}
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar:not(.tinymce-mobile-context-toolbar) .tinymce-mobile-toolbar-group .tinymce-mobile-toolbar-group-item.tinymce-mobile-toolbar-button.tinymce-mobile-toolbar-button-selected {
+  background: #c8cbcf;
+  color: #cccccc;
+}
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar:not(.tinymce-mobile-context-toolbar) .tinymce-mobile-toolbar-group:first-of-type,
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar:not(.tinymce-mobile-context-toolbar) .tinymce-mobile-toolbar-group:last-of-type {
+  background: #207ab7;
+  color: #eceff1;
+}
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar {
+  /* Note, this file is imported inside .tinymce-mobile-context-toolbar, so that prefix is on everything here. */
+}
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group {
+  align-items: center;
+  display: flex;
+  height: 100%;
+  flex: 1;
+  padding-bottom: 0.4em;
+  padding-top: 0.4em;
+  /* Make any buttons appearing on the left and right display in the centre (e.g. color edges) */
+  /* For widgets like the colour picker, use the whole height */
+}
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-serialised-dialog {
+  display: flex;
+  min-height: 1.5em;
+  overflow: hidden;
+  padding-left: 0;
+  padding-right: 0;
+  position: relative;
+  width: 100%;
+}
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-serialised-dialog .tinymce-mobile-serialised-dialog-chain {
+  display: flex;
+  height: 100%;
+  transition: left cubic-bezier(0.4, 0, 1, 1) 0.15s;
+  width: 100%;
+}
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-serialised-dialog .tinymce-mobile-serialised-dialog-chain .tinymce-mobile-serialised-dialog-screen {
+  display: flex;
+  flex: 0 0 auto;
+  justify-content: space-between;
+  width: 100%;
+}
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-serialised-dialog .tinymce-mobile-serialised-dialog-chain .tinymce-mobile-serialised-dialog-screen input {
+  font-family: Sans-serif;
+}
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-serialised-dialog .tinymce-mobile-serialised-dialog-chain .tinymce-mobile-serialised-dialog-screen .tinymce-mobile-input-container {
+  display: flex;
+  flex-grow: 1;
+  position: relative;
+}
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-serialised-dialog .tinymce-mobile-serialised-dialog-chain .tinymce-mobile-serialised-dialog-screen .tinymce-mobile-input-container .tinymce-mobile-input-container-x {
+  -ms-grid-row-align: center;
+      align-self: center;
+  background: inherit;
+  border: none;
+  border-radius: 50%;
+  color: #888;
+  font-size: 0.6em;
+  font-weight: bold;
+  height: 100%;
+  padding-right: 2px;
+  position: absolute;
+  right: 0;
+}
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-serialised-dialog .tinymce-mobile-serialised-dialog-chain .tinymce-mobile-serialised-dialog-screen .tinymce-mobile-input-container.tinymce-mobile-input-container-empty .tinymce-mobile-input-container-x {
+  display: none;
+}
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-serialised-dialog .tinymce-mobile-serialised-dialog-chain .tinymce-mobile-serialised-dialog-screen .tinymce-mobile-icon-previous,
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-serialised-dialog .tinymce-mobile-serialised-dialog-chain .tinymce-mobile-serialised-dialog-screen .tinymce-mobile-icon-next {
+  align-items: center;
+  display: flex;
+}
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-serialised-dialog .tinymce-mobile-serialised-dialog-chain .tinymce-mobile-serialised-dialog-screen .tinymce-mobile-icon-previous::before,
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-serialised-dialog .tinymce-mobile-serialised-dialog-chain .tinymce-mobile-serialised-dialog-screen .tinymce-mobile-icon-next::before {
+  align-items: center;
+  display: flex;
+  font-weight: bold;
+  height: 100%;
+  padding-left: 0.5em;
+  padding-right: 0.5em;
+}
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-serialised-dialog .tinymce-mobile-serialised-dialog-chain .tinymce-mobile-serialised-dialog-screen .tinymce-mobile-icon-previous.tinymce-mobile-toolbar-navigation-disabled::before,
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-serialised-dialog .tinymce-mobile-serialised-dialog-chain .tinymce-mobile-serialised-dialog-screen .tinymce-mobile-icon-next.tinymce-mobile-toolbar-navigation-disabled::before {
+  visibility: hidden;
+}
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-dot-item {
+  color: #cccccc;
+  font-size: 10px;
+  line-height: 10px;
+  margin: 0 2px;
+  padding-top: 3px;
+}
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-dot-item.tinymce-mobile-dot-active {
+  color: #c8cbcf;
+}
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-icon-large-font::before,
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-icon-large-heading::before {
+  margin-left: 0.5em;
+  margin-right: 0.9em;
+}
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-icon-small-font::before,
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-icon-small-heading::before {
+  margin-left: 0.9em;
+  margin-right: 0.5em;
+}
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-slider {
+  display: flex;
+  flex: 1;
+  margin-left: 0;
+  margin-right: 0;
+  padding: 0.28em 0;
+  position: relative;
+}
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-slider .tinymce-mobile-slider-size-container {
+  align-items: center;
+  display: flex;
+  flex-grow: 1;
+  height: 100%;
+}
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-slider .tinymce-mobile-slider-size-container .tinymce-mobile-slider-size-line {
+  background: #cccccc;
+  display: flex;
+  flex: 1;
+  height: 0.2em;
+  margin-bottom: 0.3em;
+  margin-top: 0.3em;
+}
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-slider.tinymce-mobile-hue-slider-container {
+  padding-left: 2em;
+  padding-right: 2em;
+}
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-slider.tinymce-mobile-hue-slider-container .tinymce-mobile-slider-gradient-container {
+  align-items: center;
+  display: flex;
+  flex-grow: 1;
+  height: 100%;
+}
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-slider.tinymce-mobile-hue-slider-container .tinymce-mobile-slider-gradient-container .tinymce-mobile-slider-gradient {
+  background: linear-gradient(to right, hsl(0, 100%, 50%) 0%, hsl(60, 100%, 50%) 17%, hsl(120, 100%, 50%) 33%, hsl(180, 100%, 50%) 50%, hsl(240, 100%, 50%) 67%, hsl(300, 100%, 50%) 83%, hsl(0, 100%, 50%) 100%);
+  display: flex;
+  flex: 1;
+  height: 0.2em;
+  margin-bottom: 0.3em;
+  margin-top: 0.3em;
+}
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-slider.tinymce-mobile-hue-slider-container .tinymce-mobile-hue-slider-black {
+  /* Not part of theming */
+  background: black;
+  height: 0.2em;
+  margin-bottom: 0.3em;
+  margin-top: 0.3em;
+  width: 1.2em;
+}
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-slider.tinymce-mobile-hue-slider-container .tinymce-mobile-hue-slider-white {
+  /* Not part of theming */
+  background: white;
+  height: 0.2em;
+  margin-bottom: 0.3em;
+  margin-top: 0.3em;
+  width: 1.2em;
+}
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-slider .tinymce-mobile-slider-thumb {
+  /* vertically centering trick (margin: auto, top: 0, bottom: 0). On iOS and Safari, if you leave
+     * out these values, then it shows the thumb at the top of the spectrum. This is probably because it is
+     * absolutely positioned with only a left value, and not a top. Note, on Chrome it seems to be fine without
+     * this approach.
+    */
+  align-items: center;
+  background-clip: padding-box;
+  background-color: #455a64;
+  border: 0.5em solid rgba(136, 136, 136, 0);
+  border-radius: 3em;
+  bottom: 0;
+  color: #fff;
+  display: flex;
+  height: 0.5em;
+  justify-content: center;
+  left: -10px;
+  margin: auto;
+  position: absolute;
+  top: 0;
+  transition: border 120ms cubic-bezier(0.39, 0.58, 0.57, 1);
+  width: 0.5em;
+}
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-slider .tinymce-mobile-slider-thumb.tinymce-mobile-thumb-active {
+  border: 0.5em solid rgba(136, 136, 136, 0.39);
+}
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-serializer-wrapper,
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group > div {
+  align-items: center;
+  display: flex;
+  height: 100%;
+  flex: 1;
+}
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-serializer-wrapper {
+  flex-direction: column;
+  justify-content: center;
+}
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-toolbar-group-item {
+  align-items: center;
+  display: flex;
+}
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-toolbar-group-item:not(.tinymce-mobile-serialised-dialog) {
+  height: 100%;
+}
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-dot-container {
+  display: flex;
+}
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group input {
+  background: #ffffff;
+  border: none;
+  border-radius: 0;
+  color: #455a64;
+  flex-grow: 1;
+  font-size: 0.85em;
+  padding-bottom: 0.1em;
+  padding-left: 5px;
+  padding-top: 0.1em;
+}
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group input::-webkit-input-placeholder {
+  /* WebKit, Blink, Edge */
+  color: #888;
+}
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group input::placeholder {
+  /* WebKit, Blink, Edge */
+  color: #888;
+}
+/* dropup */
+.tinymce-mobile-dropup {
+  background: white;
+  display: flex;
+  overflow: hidden;
+  width: 100%;
+}
+.tinymce-mobile-dropup.tinymce-mobile-dropup-shrinking {
+  transition: height 0.3s ease-out;
+}
+.tinymce-mobile-dropup.tinymce-mobile-dropup-growing {
+  transition: height 0.3s ease-in;
+}
+.tinymce-mobile-dropup.tinymce-mobile-dropup-closed {
+  flex-grow: 0;
+}
+.tinymce-mobile-dropup.tinymce-mobile-dropup-open:not(.tinymce-mobile-dropup-growing) {
+  flex-grow: 1;
+}
+/* TODO min-height for device size and orientation */
+.tinymce-mobile-ios-container .tinymce-mobile-dropup:not(.tinymce-mobile-dropup-closed) {
+  min-height: 200px;
+}
+@media only screen and (orientation: landscape) {
+  .tinymce-mobile-dropup:not(.tinymce-mobile-dropup-closed) {
+    min-height: 200px;
+  }
+}
+@media only screen and (min-device-width : 320px) and (max-device-width : 568px) and (orientation : landscape) {
+  .tinymce-mobile-ios-container .tinymce-mobile-dropup:not(.tinymce-mobile-dropup-closed) {
+    min-height: 150px;
+  }
+}
+/* styles menu */
+.tinymce-mobile-styles-menu {
+  font-family: sans-serif;
+  outline: 4px solid black;
+  overflow: hidden;
+  position: relative;
+  width: 100%;
+}
+.tinymce-mobile-styles-menu [role="menu"] {
+  display: flex;
+  flex-direction: column;
+  height: 100%;
+  position: absolute;
+  width: 100%;
+}
+.tinymce-mobile-styles-menu [role="menu"].transitioning {
+  transition: transform 0.5s ease-in-out;
+}
+.tinymce-mobile-styles-menu .tinymce-mobile-styles-item {
+  border-bottom: 1px solid #ddd;
+  color: #455a64;
+  cursor: pointer;
+  display: flex;
+  padding: 1em 1em;
+  position: relative;
+}
+.tinymce-mobile-styles-menu .tinymce-mobile-styles-collapser .tinymce-mobile-styles-collapse-icon::before {
+  color: #455a64;
+  content: "\e314";
+  font-family: 'tinymce-mobile', sans-serif;
+}
+.tinymce-mobile-styles-menu .tinymce-mobile-styles-item.tinymce-mobile-styles-item-is-menu::after {
+  color: #455a64;
+  content: "\e315";
+  font-family: 'tinymce-mobile', sans-serif;
+  padding-left: 1em;
+  padding-right: 1em;
+  position: absolute;
+  right: 0;
+}
+.tinymce-mobile-styles-menu .tinymce-mobile-styles-item.tinymce-mobile-format-matches::after {
+  font-family: 'tinymce-mobile', sans-serif;
+  padding-left: 1em;
+  padding-right: 1em;
+  position: absolute;
+  right: 0;
+}
+.tinymce-mobile-styles-menu .tinymce-mobile-styles-separator,
+.tinymce-mobile-styles-menu .tinymce-mobile-styles-collapser {
+  align-items: center;
+  background: #fff;
+  border-top: #455a64;
+  color: #455a64;
+  display: flex;
+  min-height: 2.5em;
+  padding-left: 1em;
+  padding-right: 1em;
+}
+.tinymce-mobile-styles-menu [data-transitioning-destination="before"][data-transitioning-state],
+.tinymce-mobile-styles-menu [data-transitioning-state="before"] {
+  transform: translate(-100%);
+}
+.tinymce-mobile-styles-menu [data-transitioning-destination="current"][data-transitioning-state],
+.tinymce-mobile-styles-menu [data-transitioning-state="current"] {
+  transform: translate(0%);
+}
+.tinymce-mobile-styles-menu [data-transitioning-destination="after"][data-transitioning-state],
+.tinymce-mobile-styles-menu [data-transitioning-state="after"] {
+  transform: translate(100%);
+}
+@font-face {
+  font-family: 'tinymce-mobile';
+  font-style: normal;
+  font-weight: normal;
+  src: url('fonts/tinymce-mobile.woff?8x92w3') format('woff');
+}
+@media (min-device-width: 700px) {
+  .tinymce-mobile-outer-container,
+  .tinymce-mobile-outer-container input {
+    font-size: 25px;
+  }
+}
+@media (max-device-width: 700px) {
+  .tinymce-mobile-outer-container,
+  .tinymce-mobile-outer-container input {
+    font-size: 18px;
+  }
+}
+.tinymce-mobile-icon {
+  font-family: 'tinymce-mobile', sans-serif;
+}
+.mixin-flex-and-centre {
+  align-items: center;
+  display: flex;
+  justify-content: center;
+}
+.mixin-flex-bar {
+  align-items: center;
+  display: flex;
+  height: 100%;
+}
+.tinymce-mobile-outer-container .tinymce-mobile-editor-socket iframe {
+  background-color: #fff;
+  width: 100%;
+}
+.tinymce-mobile-editor-socket .tinymce-mobile-mask-edit-icon {
+  /* Note, on the iPod touch in landscape, this isn't visible when the navbar appears */
+  background-color: #207ab7;
+  border-radius: 50%;
+  bottom: 1em;
+  color: white;
+  font-size: 1em;
+  height: 2.1em;
+  position: fixed;
+  right: 2em;
+  width: 2.1em;
+  align-items: center;
+  display: flex;
+  justify-content: center;
+}
+@media only screen and (min-device-width:700px) {
+  .tinymce-mobile-editor-socket .tinymce-mobile-mask-edit-icon {
+    font-size: 1.2em;
+  }
+}
+.tinymce-mobile-outer-container:not(.tinymce-mobile-fullscreen-maximized) .tinymce-mobile-editor-socket {
+  height: 300px;
+  overflow: hidden;
+}
+.tinymce-mobile-outer-container:not(.tinymce-mobile-fullscreen-maximized) .tinymce-mobile-editor-socket iframe {
+  height: 100%;
+}
+.tinymce-mobile-outer-container:not(.tinymce-mobile-fullscreen-maximized) .tinymce-mobile-toolstrip {
+  display: none;
+}
+/*
+  Note, that if you don't include this (::-webkit-file-upload-button), the toolbar width gets
+  increased and the whole body becomes scrollable. It's important!
+ */
+input[type="file"]::-webkit-file-upload-button {
+  display: none;
+}
+@media only screen and (min-device-width : 320px) and (max-device-width : 568px) and (orientation : landscape) {
+  .tinymce-mobile-ios-container .tinymce-mobile-editor-socket .tinymce-mobile-mask-edit-icon {
+    bottom: 50%;
+  }
+}

File diff suppressed because it is too large
+ 7 - 0
public/tinymce/skins/ui/oxide-dark/skin.mobile.min.css


File diff suppressed because it is too large
+ 693 - 0
public/tinymce/skins/ui/oxide/content.css


File diff suppressed because it is too large
+ 687 - 0
public/tinymce/skins/ui/oxide/content.inline.css


File diff suppressed because it is too large
+ 7 - 0
public/tinymce/skins/ui/oxide/content.inline.min.css


File diff suppressed because it is too large
+ 7 - 0
public/tinymce/skins/ui/oxide/content.min.css


+ 29 - 0
public/tinymce/skins/ui/oxide/content.mobile.css

@@ -0,0 +1,29 @@
+/**
+ * Copyright (c) Tiny Technologies, Inc. All rights reserved.
+ * Licensed under the LGPL or a commercial license.
+ * For LGPL see License.txt in the project root for license information.
+ * For commercial licenses see https://www.tiny.cloud/
+ */
+.tinymce-mobile-unfocused-selections .tinymce-mobile-unfocused-selection {
+  /* Note: this file is used inside the content, so isn't part of theming */
+  background-color: green;
+  display: inline-block;
+  opacity: 0.5;
+  position: absolute;
+}
+body {
+  -webkit-text-size-adjust: none;
+}
+body img {
+  /* this is related to the content margin */
+  max-width: 96vw;
+}
+body table img {
+  max-width: 95%;
+}
+body {
+  font-family: sans-serif;
+}
+table {
+  border-collapse: collapse;
+}

+ 7 - 0
public/tinymce/skins/ui/oxide/content.mobile.min.css

@@ -0,0 +1,7 @@
+/**
+ * Copyright (c) Tiny Technologies, Inc. All rights reserved.
+ * Licensed under the LGPL or a commercial license.
+ * For LGPL see License.txt in the project root for license information.
+ * For commercial licenses see https://www.tiny.cloud/
+ */
+.tinymce-mobile-unfocused-selections .tinymce-mobile-unfocused-selection{background-color:green;display:inline-block;opacity:.5;position:absolute}body{-webkit-text-size-adjust:none}body img{max-width:96vw}body table img{max-width:95%}body{font-family:sans-serif}table{border-collapse:collapse}

二進制
public/tinymce/skins/ui/oxide/fonts/tinymce-mobile.woff


File diff suppressed because it is too large
+ 2937 - 0
public/tinymce/skins/ui/oxide/skin.css


File diff suppressed because it is too large
+ 7 - 0
public/tinymce/skins/ui/oxide/skin.min.css


+ 673 - 0
public/tinymce/skins/ui/oxide/skin.mobile.css

@@ -0,0 +1,673 @@
+/**
+ * Copyright (c) Tiny Technologies, Inc. All rights reserved.
+ * Licensed under the LGPL or a commercial license.
+ * For LGPL see License.txt in the project root for license information.
+ * For commercial licenses see https://www.tiny.cloud/
+ */
+/* RESET all the things! */
+.tinymce-mobile-outer-container {
+  all: initial;
+  display: block;
+}
+.tinymce-mobile-outer-container * {
+  border: 0;
+  box-sizing: initial;
+  cursor: inherit;
+  float: none;
+  line-height: 1;
+  margin: 0;
+  outline: 0;
+  padding: 0;
+  -webkit-tap-highlight-color: transparent;
+  /* TBIO-3691, stop the gray flicker on touch. */
+  text-shadow: none;
+  white-space: nowrap;
+}
+.tinymce-mobile-icon-arrow-back::before {
+  content: "\e5cd";
+}
+.tinymce-mobile-icon-image::before {
+  content: "\e412";
+}
+.tinymce-mobile-icon-cancel-circle::before {
+  content: "\e5c9";
+}
+.tinymce-mobile-icon-full-dot::before {
+  content: "\e061";
+}
+.tinymce-mobile-icon-align-center::before {
+  content: "\e234";
+}
+.tinymce-mobile-icon-align-left::before {
+  content: "\e236";
+}
+.tinymce-mobile-icon-align-right::before {
+  content: "\e237";
+}
+.tinymce-mobile-icon-bold::before {
+  content: "\e238";
+}
+.tinymce-mobile-icon-italic::before {
+  content: "\e23f";
+}
+.tinymce-mobile-icon-unordered-list::before {
+  content: "\e241";
+}
+.tinymce-mobile-icon-ordered-list::before {
+  content: "\e242";
+}
+.tinymce-mobile-icon-font-size::before {
+  content: "\e245";
+}
+.tinymce-mobile-icon-underline::before {
+  content: "\e249";
+}
+.tinymce-mobile-icon-link::before {
+  content: "\e157";
+}
+.tinymce-mobile-icon-unlink::before {
+  content: "\eca2";
+}
+.tinymce-mobile-icon-color::before {
+  content: "\e891";
+}
+.tinymce-mobile-icon-previous::before {
+  content: "\e314";
+}
+.tinymce-mobile-icon-next::before {
+  content: "\e315";
+}
+.tinymce-mobile-icon-large-font::before,
+.tinymce-mobile-icon-style-formats::before {
+  content: "\e264";
+}
+.tinymce-mobile-icon-undo::before {
+  content: "\e166";
+}
+.tinymce-mobile-icon-redo::before {
+  content: "\e15a";
+}
+.tinymce-mobile-icon-removeformat::before {
+  content: "\e239";
+}
+.tinymce-mobile-icon-small-font::before {
+  content: "\e906";
+}
+.tinymce-mobile-icon-readonly-back::before,
+.tinymce-mobile-format-matches::after {
+  content: "\e5ca";
+}
+.tinymce-mobile-icon-small-heading::before {
+  content: "small";
+}
+.tinymce-mobile-icon-large-heading::before {
+  content: "large";
+}
+.tinymce-mobile-icon-small-heading::before,
+.tinymce-mobile-icon-large-heading::before {
+  font-family: sans-serif;
+  font-size: 80%;
+}
+.tinymce-mobile-mask-edit-icon::before {
+  content: "\e254";
+}
+.tinymce-mobile-icon-back::before {
+  content: "\e5c4";
+}
+.tinymce-mobile-icon-heading::before {
+  /* TODO: Translate */
+  content: "Headings";
+  font-family: sans-serif;
+  font-size: 80%;
+  font-weight: bold;
+}
+.tinymce-mobile-icon-h1::before {
+  content: "H1";
+  font-weight: bold;
+}
+.tinymce-mobile-icon-h2::before {
+  content: "H2";
+  font-weight: bold;
+}
+.tinymce-mobile-icon-h3::before {
+  content: "H3";
+  font-weight: bold;
+}
+.tinymce-mobile-outer-container .tinymce-mobile-disabled-mask {
+  align-items: center;
+  display: flex;
+  justify-content: center;
+  background: rgba(51, 51, 51, 0.5);
+  height: 100%;
+  position: absolute;
+  top: 0;
+  width: 100%;
+}
+.tinymce-mobile-outer-container .tinymce-mobile-disabled-mask .tinymce-mobile-content-container {
+  align-items: center;
+  border-radius: 50%;
+  display: flex;
+  flex-direction: column;
+  font-family: sans-serif;
+  font-size: 1em;
+  justify-content: space-between;
+}
+.tinymce-mobile-outer-container .tinymce-mobile-disabled-mask .tinymce-mobile-content-container .mixin-menu-item {
+  align-items: center;
+  display: flex;
+  justify-content: center;
+  border-radius: 50%;
+  height: 2.1em;
+  width: 2.1em;
+}
+.tinymce-mobile-outer-container .tinymce-mobile-disabled-mask .tinymce-mobile-content-container .tinymce-mobile-content-tap-section {
+  align-items: center;
+  display: flex;
+  justify-content: center;
+  flex-direction: column;
+  font-size: 1em;
+}
+@media only screen and (min-device-width:700px) {
+  .tinymce-mobile-outer-container .tinymce-mobile-disabled-mask .tinymce-mobile-content-container .tinymce-mobile-content-tap-section {
+    font-size: 1.2em;
+  }
+}
+.tinymce-mobile-outer-container .tinymce-mobile-disabled-mask .tinymce-mobile-content-container .tinymce-mobile-content-tap-section .tinymce-mobile-mask-tap-icon {
+  align-items: center;
+  display: flex;
+  justify-content: center;
+  border-radius: 50%;
+  height: 2.1em;
+  width: 2.1em;
+  background-color: white;
+  color: #207ab7;
+}
+.tinymce-mobile-outer-container .tinymce-mobile-disabled-mask .tinymce-mobile-content-container .tinymce-mobile-content-tap-section .tinymce-mobile-mask-tap-icon::before {
+  content: "\e900";
+  font-family: 'tinymce-mobile', sans-serif;
+}
+.tinymce-mobile-outer-container .tinymce-mobile-disabled-mask .tinymce-mobile-content-container .tinymce-mobile-content-tap-section:not(.tinymce-mobile-mask-tap-icon-selected) .tinymce-mobile-mask-tap-icon {
+  z-index: 2;
+}
+.tinymce-mobile-android-container.tinymce-mobile-android-maximized {
+  background: #ffffff;
+  border: none;
+  bottom: 0;
+  display: flex;
+  flex-direction: column;
+  left: 0;
+  position: fixed;
+  right: 0;
+  top: 0;
+}
+.tinymce-mobile-android-container:not(.tinymce-mobile-android-maximized) {
+  position: relative;
+}
+.tinymce-mobile-android-container .tinymce-mobile-editor-socket {
+  display: flex;
+  flex-grow: 1;
+}
+.tinymce-mobile-android-container .tinymce-mobile-editor-socket iframe {
+  display: flex !important;
+  flex-grow: 1;
+  height: auto !important;
+}
+.tinymce-mobile-android-scroll-reload {
+  overflow: hidden;
+}
+:not(.tinymce-mobile-readonly-mode) > .tinymce-mobile-android-selection-context-toolbar {
+  margin-top: 23px;
+}
+.tinymce-mobile-toolstrip {
+  background: #fff;
+  display: flex;
+  flex: 0 0 auto;
+  z-index: 1;
+}
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar {
+  align-items: center;
+  background-color: #fff;
+  border-bottom: 1px solid #cccccc;
+  display: flex;
+  flex: 1;
+  height: 2.5em;
+  width: 100%;
+  /* Make it no larger than the toolstrip, so that it needs to scroll */
+}
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar:not(.tinymce-mobile-context-toolbar) .tinymce-mobile-toolbar-group {
+  align-items: center;
+  display: flex;
+  height: 100%;
+  flex-shrink: 1;
+}
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar:not(.tinymce-mobile-context-toolbar) .tinymce-mobile-toolbar-group > div {
+  align-items: center;
+  display: flex;
+  height: 100%;
+  flex: 1;
+}
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar:not(.tinymce-mobile-context-toolbar) .tinymce-mobile-toolbar-group.tinymce-mobile-exit-container {
+  background: #f44336;
+}
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar:not(.tinymce-mobile-context-toolbar) .tinymce-mobile-toolbar-group.tinymce-mobile-toolbar-scrollable-group {
+  flex-grow: 1;
+}
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar:not(.tinymce-mobile-context-toolbar) .tinymce-mobile-toolbar-group .tinymce-mobile-toolbar-group-item {
+  padding-left: 0.5em;
+  padding-right: 0.5em;
+}
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar:not(.tinymce-mobile-context-toolbar) .tinymce-mobile-toolbar-group .tinymce-mobile-toolbar-group-item.tinymce-mobile-toolbar-button {
+  align-items: center;
+  display: flex;
+  height: 80%;
+  margin-left: 2px;
+  margin-right: 2px;
+}
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar:not(.tinymce-mobile-context-toolbar) .tinymce-mobile-toolbar-group .tinymce-mobile-toolbar-group-item.tinymce-mobile-toolbar-button.tinymce-mobile-toolbar-button-selected {
+  background: #c8cbcf;
+  color: #cccccc;
+}
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar:not(.tinymce-mobile-context-toolbar) .tinymce-mobile-toolbar-group:first-of-type,
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar:not(.tinymce-mobile-context-toolbar) .tinymce-mobile-toolbar-group:last-of-type {
+  background: #207ab7;
+  color: #eceff1;
+}
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar {
+  /* Note, this file is imported inside .tinymce-mobile-context-toolbar, so that prefix is on everything here. */
+}
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group {
+  align-items: center;
+  display: flex;
+  height: 100%;
+  flex: 1;
+  padding-bottom: 0.4em;
+  padding-top: 0.4em;
+  /* Make any buttons appearing on the left and right display in the centre (e.g. color edges) */
+  /* For widgets like the colour picker, use the whole height */
+}
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-serialised-dialog {
+  display: flex;
+  min-height: 1.5em;
+  overflow: hidden;
+  padding-left: 0;
+  padding-right: 0;
+  position: relative;
+  width: 100%;
+}
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-serialised-dialog .tinymce-mobile-serialised-dialog-chain {
+  display: flex;
+  height: 100%;
+  transition: left cubic-bezier(0.4, 0, 1, 1) 0.15s;
+  width: 100%;
+}
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-serialised-dialog .tinymce-mobile-serialised-dialog-chain .tinymce-mobile-serialised-dialog-screen {
+  display: flex;
+  flex: 0 0 auto;
+  justify-content: space-between;
+  width: 100%;
+}
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-serialised-dialog .tinymce-mobile-serialised-dialog-chain .tinymce-mobile-serialised-dialog-screen input {
+  font-family: Sans-serif;
+}
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-serialised-dialog .tinymce-mobile-serialised-dialog-chain .tinymce-mobile-serialised-dialog-screen .tinymce-mobile-input-container {
+  display: flex;
+  flex-grow: 1;
+  position: relative;
+}
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-serialised-dialog .tinymce-mobile-serialised-dialog-chain .tinymce-mobile-serialised-dialog-screen .tinymce-mobile-input-container .tinymce-mobile-input-container-x {
+  -ms-grid-row-align: center;
+      align-self: center;
+  background: inherit;
+  border: none;
+  border-radius: 50%;
+  color: #888;
+  font-size: 0.6em;
+  font-weight: bold;
+  height: 100%;
+  padding-right: 2px;
+  position: absolute;
+  right: 0;
+}
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-serialised-dialog .tinymce-mobile-serialised-dialog-chain .tinymce-mobile-serialised-dialog-screen .tinymce-mobile-input-container.tinymce-mobile-input-container-empty .tinymce-mobile-input-container-x {
+  display: none;
+}
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-serialised-dialog .tinymce-mobile-serialised-dialog-chain .tinymce-mobile-serialised-dialog-screen .tinymce-mobile-icon-previous,
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-serialised-dialog .tinymce-mobile-serialised-dialog-chain .tinymce-mobile-serialised-dialog-screen .tinymce-mobile-icon-next {
+  align-items: center;
+  display: flex;
+}
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-serialised-dialog .tinymce-mobile-serialised-dialog-chain .tinymce-mobile-serialised-dialog-screen .tinymce-mobile-icon-previous::before,
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-serialised-dialog .tinymce-mobile-serialised-dialog-chain .tinymce-mobile-serialised-dialog-screen .tinymce-mobile-icon-next::before {
+  align-items: center;
+  display: flex;
+  font-weight: bold;
+  height: 100%;
+  padding-left: 0.5em;
+  padding-right: 0.5em;
+}
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-serialised-dialog .tinymce-mobile-serialised-dialog-chain .tinymce-mobile-serialised-dialog-screen .tinymce-mobile-icon-previous.tinymce-mobile-toolbar-navigation-disabled::before,
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-serialised-dialog .tinymce-mobile-serialised-dialog-chain .tinymce-mobile-serialised-dialog-screen .tinymce-mobile-icon-next.tinymce-mobile-toolbar-navigation-disabled::before {
+  visibility: hidden;
+}
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-dot-item {
+  color: #cccccc;
+  font-size: 10px;
+  line-height: 10px;
+  margin: 0 2px;
+  padding-top: 3px;
+}
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-dot-item.tinymce-mobile-dot-active {
+  color: #c8cbcf;
+}
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-icon-large-font::before,
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-icon-large-heading::before {
+  margin-left: 0.5em;
+  margin-right: 0.9em;
+}
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-icon-small-font::before,
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-icon-small-heading::before {
+  margin-left: 0.9em;
+  margin-right: 0.5em;
+}
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-slider {
+  display: flex;
+  flex: 1;
+  margin-left: 0;
+  margin-right: 0;
+  padding: 0.28em 0;
+  position: relative;
+}
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-slider .tinymce-mobile-slider-size-container {
+  align-items: center;
+  display: flex;
+  flex-grow: 1;
+  height: 100%;
+}
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-slider .tinymce-mobile-slider-size-container .tinymce-mobile-slider-size-line {
+  background: #cccccc;
+  display: flex;
+  flex: 1;
+  height: 0.2em;
+  margin-bottom: 0.3em;
+  margin-top: 0.3em;
+}
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-slider.tinymce-mobile-hue-slider-container {
+  padding-left: 2em;
+  padding-right: 2em;
+}
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-slider.tinymce-mobile-hue-slider-container .tinymce-mobile-slider-gradient-container {
+  align-items: center;
+  display: flex;
+  flex-grow: 1;
+  height: 100%;
+}
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-slider.tinymce-mobile-hue-slider-container .tinymce-mobile-slider-gradient-container .tinymce-mobile-slider-gradient {
+  background: linear-gradient(to right, hsl(0, 100%, 50%) 0%, hsl(60, 100%, 50%) 17%, hsl(120, 100%, 50%) 33%, hsl(180, 100%, 50%) 50%, hsl(240, 100%, 50%) 67%, hsl(300, 100%, 50%) 83%, hsl(0, 100%, 50%) 100%);
+  display: flex;
+  flex: 1;
+  height: 0.2em;
+  margin-bottom: 0.3em;
+  margin-top: 0.3em;
+}
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-slider.tinymce-mobile-hue-slider-container .tinymce-mobile-hue-slider-black {
+  /* Not part of theming */
+  background: black;
+  height: 0.2em;
+  margin-bottom: 0.3em;
+  margin-top: 0.3em;
+  width: 1.2em;
+}
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-slider.tinymce-mobile-hue-slider-container .tinymce-mobile-hue-slider-white {
+  /* Not part of theming */
+  background: white;
+  height: 0.2em;
+  margin-bottom: 0.3em;
+  margin-top: 0.3em;
+  width: 1.2em;
+}
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-slider .tinymce-mobile-slider-thumb {
+  /* vertically centering trick (margin: auto, top: 0, bottom: 0). On iOS and Safari, if you leave
+     * out these values, then it shows the thumb at the top of the spectrum. This is probably because it is
+     * absolutely positioned with only a left value, and not a top. Note, on Chrome it seems to be fine without
+     * this approach.
+    */
+  align-items: center;
+  background-clip: padding-box;
+  background-color: #455a64;
+  border: 0.5em solid rgba(136, 136, 136, 0);
+  border-radius: 3em;
+  bottom: 0;
+  color: #fff;
+  display: flex;
+  height: 0.5em;
+  justify-content: center;
+  left: -10px;
+  margin: auto;
+  position: absolute;
+  top: 0;
+  transition: border 120ms cubic-bezier(0.39, 0.58, 0.57, 1);
+  width: 0.5em;
+}
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-slider .tinymce-mobile-slider-thumb.tinymce-mobile-thumb-active {
+  border: 0.5em solid rgba(136, 136, 136, 0.39);
+}
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-serializer-wrapper,
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group > div {
+  align-items: center;
+  display: flex;
+  height: 100%;
+  flex: 1;
+}
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-serializer-wrapper {
+  flex-direction: column;
+  justify-content: center;
+}
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-toolbar-group-item {
+  align-items: center;
+  display: flex;
+}
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-toolbar-group-item:not(.tinymce-mobile-serialised-dialog) {
+  height: 100%;
+}
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group .tinymce-mobile-dot-container {
+  display: flex;
+}
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group input {
+  background: #ffffff;
+  border: none;
+  border-radius: 0;
+  color: #455a64;
+  flex-grow: 1;
+  font-size: 0.85em;
+  padding-bottom: 0.1em;
+  padding-left: 5px;
+  padding-top: 0.1em;
+}
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group input::-webkit-input-placeholder {
+  /* WebKit, Blink, Edge */
+  color: #888;
+}
+.tinymce-mobile-toolstrip .tinymce-mobile-toolbar.tinymce-mobile-context-toolbar .tinymce-mobile-toolbar-group input::placeholder {
+  /* WebKit, Blink, Edge */
+  color: #888;
+}
+/* dropup */
+.tinymce-mobile-dropup {
+  background: white;
+  display: flex;
+  overflow: hidden;
+  width: 100%;
+}
+.tinymce-mobile-dropup.tinymce-mobile-dropup-shrinking {
+  transition: height 0.3s ease-out;
+}
+.tinymce-mobile-dropup.tinymce-mobile-dropup-growing {
+  transition: height 0.3s ease-in;
+}
+.tinymce-mobile-dropup.tinymce-mobile-dropup-closed {
+  flex-grow: 0;
+}
+.tinymce-mobile-dropup.tinymce-mobile-dropup-open:not(.tinymce-mobile-dropup-growing) {
+  flex-grow: 1;
+}
+/* TODO min-height for device size and orientation */
+.tinymce-mobile-ios-container .tinymce-mobile-dropup:not(.tinymce-mobile-dropup-closed) {
+  min-height: 200px;
+}
+@media only screen and (orientation: landscape) {
+  .tinymce-mobile-dropup:not(.tinymce-mobile-dropup-closed) {
+    min-height: 200px;
+  }
+}
+@media only screen and (min-device-width : 320px) and (max-device-width : 568px) and (orientation : landscape) {
+  .tinymce-mobile-ios-container .tinymce-mobile-dropup:not(.tinymce-mobile-dropup-closed) {
+    min-height: 150px;
+  }
+}
+/* styles menu */
+.tinymce-mobile-styles-menu {
+  font-family: sans-serif;
+  outline: 4px solid black;
+  overflow: hidden;
+  position: relative;
+  width: 100%;
+}
+.tinymce-mobile-styles-menu [role="menu"] {
+  display: flex;
+  flex-direction: column;
+  height: 100%;
+  position: absolute;
+  width: 100%;
+}
+.tinymce-mobile-styles-menu [role="menu"].transitioning {
+  transition: transform 0.5s ease-in-out;
+}
+.tinymce-mobile-styles-menu .tinymce-mobile-styles-item {
+  border-bottom: 1px solid #ddd;
+  color: #455a64;
+  cursor: pointer;
+  display: flex;
+  padding: 1em 1em;
+  position: relative;
+}
+.tinymce-mobile-styles-menu .tinymce-mobile-styles-collapser .tinymce-mobile-styles-collapse-icon::before {
+  color: #455a64;
+  content: "\e314";
+  font-family: 'tinymce-mobile', sans-serif;
+}
+.tinymce-mobile-styles-menu .tinymce-mobile-styles-item.tinymce-mobile-styles-item-is-menu::after {
+  color: #455a64;
+  content: "\e315";
+  font-family: 'tinymce-mobile', sans-serif;
+  padding-left: 1em;
+  padding-right: 1em;
+  position: absolute;
+  right: 0;
+}
+.tinymce-mobile-styles-menu .tinymce-mobile-styles-item.tinymce-mobile-format-matches::after {
+  font-family: 'tinymce-mobile', sans-serif;
+  padding-left: 1em;
+  padding-right: 1em;
+  position: absolute;
+  right: 0;
+}
+.tinymce-mobile-styles-menu .tinymce-mobile-styles-separator,
+.tinymce-mobile-styles-menu .tinymce-mobile-styles-collapser {
+  align-items: center;
+  background: #fff;
+  border-top: #455a64;
+  color: #455a64;
+  display: flex;
+  min-height: 2.5em;
+  padding-left: 1em;
+  padding-right: 1em;
+}
+.tinymce-mobile-styles-menu [data-transitioning-destination="before"][data-transitioning-state],
+.tinymce-mobile-styles-menu [data-transitioning-state="before"] {
+  transform: translate(-100%);
+}
+.tinymce-mobile-styles-menu [data-transitioning-destination="current"][data-transitioning-state],
+.tinymce-mobile-styles-menu [data-transitioning-state="current"] {
+  transform: translate(0%);
+}
+.tinymce-mobile-styles-menu [data-transitioning-destination="after"][data-transitioning-state],
+.tinymce-mobile-styles-menu [data-transitioning-state="after"] {
+  transform: translate(100%);
+}
+@font-face {
+  font-family: 'tinymce-mobile';
+  font-style: normal;
+  font-weight: normal;
+  src: url('fonts/tinymce-mobile.woff?8x92w3') format('woff');
+}
+@media (min-device-width: 700px) {
+  .tinymce-mobile-outer-container,
+  .tinymce-mobile-outer-container input {
+    font-size: 25px;
+  }
+}
+@media (max-device-width: 700px) {
+  .tinymce-mobile-outer-container,
+  .tinymce-mobile-outer-container input {
+    font-size: 18px;
+  }
+}
+.tinymce-mobile-icon {
+  font-family: 'tinymce-mobile', sans-serif;
+}
+.mixin-flex-and-centre {
+  align-items: center;
+  display: flex;
+  justify-content: center;
+}
+.mixin-flex-bar {
+  align-items: center;
+  display: flex;
+  height: 100%;
+}
+.tinymce-mobile-outer-container .tinymce-mobile-editor-socket iframe {
+  background-color: #fff;
+  width: 100%;
+}
+.tinymce-mobile-editor-socket .tinymce-mobile-mask-edit-icon {
+  /* Note, on the iPod touch in landscape, this isn't visible when the navbar appears */
+  background-color: #207ab7;
+  border-radius: 50%;
+  bottom: 1em;
+  color: white;
+  font-size: 1em;
+  height: 2.1em;
+  position: fixed;
+  right: 2em;
+  width: 2.1em;
+  align-items: center;
+  display: flex;
+  justify-content: center;
+}
+@media only screen and (min-device-width:700px) {
+  .tinymce-mobile-editor-socket .tinymce-mobile-mask-edit-icon {
+    font-size: 1.2em;
+  }
+}
+.tinymce-mobile-outer-container:not(.tinymce-mobile-fullscreen-maximized) .tinymce-mobile-editor-socket {
+  height: 300px;
+  overflow: hidden;
+}
+.tinymce-mobile-outer-container:not(.tinymce-mobile-fullscreen-maximized) .tinymce-mobile-editor-socket iframe {
+  height: 100%;
+}
+.tinymce-mobile-outer-container:not(.tinymce-mobile-fullscreen-maximized) .tinymce-mobile-toolstrip {
+  display: none;
+}
+/*
+  Note, that if you don't include this (::-webkit-file-upload-button), the toolbar width gets
+  increased and the whole body becomes scrollable. It's important!
+ */
+input[type="file"]::-webkit-file-upload-button {
+  display: none;
+}
+@media only screen and (min-device-width : 320px) and (max-device-width : 568px) and (orientation : landscape) {
+  .tinymce-mobile-ios-container .tinymce-mobile-editor-socket .tinymce-mobile-mask-edit-icon {
+    bottom: 50%;
+  }
+}

File diff suppressed because it is too large
+ 7 - 0
public/tinymce/skins/ui/oxide/skin.mobile.min.css


+ 15 - 0
src/App.vue

@@ -0,0 +1,15 @@
+<template>
+  <div id="app">
+    <router-view/>
+  </div>
+</template>
+
+<script>
+
+export default {
+  name: 'App',
+}
+</script>
+
+<style>
+</style>

二進制
src/assets/default-avatar.png


二進制
src/assets/logo36-36.png


+ 206 - 0
src/axios/index.js

@@ -0,0 +1,206 @@
+import axios from 'axios'
+import settings from '@/config'
+import router from '@/router/index'
+import {Message, MessageBox} from 'element-ui';
+import {storage} from "@/utils/storage";
+import qs from 'qs';
+import Vue from "vue";
+
+const baseURL = process.env.NODE_ENV === 'development' ? settings.baseUrl.dev : settings.baseUrl.pro;
+
+axios.defaults.baseURL = baseURL;
+axios.defaults.paramsSerializer = (params) => qs.stringify(params, {arrayFormat: 'repeat'});
+axios.defaults.headers.common['Platform'] = "web";
+// axios.defaults.headers.get['Content-Type'] = 'application/x-www-form-urlencoded';
+axios.defaults.headers.get['Content-Type'] = 'application/json;charset=UTF-8';
+axios.defaults.headers.post['Content-Type'] = 'application/json;charset=UTF-8';
+axios.defaults.headers.delete['Content-Type'] = 'application/json;charset=UTF-8';
+axios.defaults.headers.put['Content-Type'] = 'application/json;charset=UTF-8';
+axios.defaults.withCredentials = false;
+axios.defaults.timeout = 5000;
+
+const requestPermissions = () => {
+    // 更新本地权限
+    const userInfo = storage.getUserInfo();
+    axios.get(settings.responseKey.permissionsUrl).then(data => {
+        userInfo.permissions = data;
+        if (process.env.NODE_ENV === 'development') {
+            if (userInfo.permissions) {
+                userInfo.permissions.push("developer");
+            } else {
+                userInfo.permissions = ["developer"];
+            }
+        }
+        storage.setUserInfo(userInfo);
+    })
+}
+
+axios.interceptors.request.use(config => {
+    config.headers[settings.tokenKey] = storage.getToken();
+    console.log(config.url, config);
+    // TODO 重复提交校验
+    return config;
+}, error => {
+    console.log("request", error);
+    Message.warning(error);
+    return Promise.reject(error);
+});
+
+axios.interceptors.response.use(response => {
+    console.log(response.config.url, response);
+    const message = response.data[settings.responseKey.message];
+    let code = response.data[settings.responseKey.code];
+    code = code && parseInt(code)
+    if (code === settings.responseKey.successCode) {
+        return response.data[settings.responseKey.data];
+    } else if (code === settings.responseKey.authFailCode) { // 登录过期
+        // 可以操作store
+        // store.dispatch('user/LOGOUT').then(() => {});
+        if (router.currentRoute.path !== '/login') {
+            message && Message.warning(message);
+            storage.removeUserInfo();
+            router.push({
+                path: '/login',
+                query: {redirect: router.currentRoute.path, ...router.currentRoute.query}
+            }).then();
+        }
+        return Promise.reject(new Error(message))
+    } else if (code === settings.responseKey.permissionsExpiredCode) { // 权限过期
+        // 更新本地权限
+        requestPermissions();
+        // 提示用户刷新界面
+        MessageBox.confirm('权限已被更改, 页面将被刷新?', '提示', {
+            confirmButtonText: '立即刷新',
+            cancelButtonText: '稍后手动刷新',
+            type: 'warning'
+        }).then(() => {
+            location.reload();
+        });
+        return Promise.reject(new Error(message))
+    } else if (!code) {
+        return response
+    } else { // 其他错误
+        Message.warning(message || "error");
+        return Promise.reject(new Error(message))
+    }
+}, error => {
+    console.log("response", error.response && error.response.data || error);
+    error.response && error.response.data && Message.warning(error.response.data.message || error.response.data);
+    return Promise.reject(error);
+});
+
+if (storage.getToken()) {
+    requestPermissions();
+}
+
+
+const axiosLoading = {
+    ...axios,
+    get: (url, config, showLoading = true) => {
+        if (config?.params) {
+            Object.keys(config.params).forEach(key => {
+                if (config.params[key] === null) {
+                    delete config.params[key];
+                }
+            });
+            config.params = JSON.parse(JSON.stringify(config.params))
+        }
+        if (showLoading) {
+            const loading = Vue.prototype.loading();
+            return new Promise((resolve, reject) => {
+                axios.get(url, config).then(data => {
+                    resolve(data);
+                }).catch(error => {
+                    reject(error)
+                }).finally(() => {
+                    loading.close();
+                })
+            })
+        } else {
+            return axios.get(url, config);
+        }
+    },
+    post: (url, data, showLoading = true) => {
+        // let config = {};
+        // if (data instanceof FormData) {
+        //     config.headers = {"Content-Type": "multipart/form-data"};
+        // } else
+        if (data) {
+            data = JSON.parse(JSON.stringify(data))
+        }
+        if (showLoading) {
+            const loading = Vue.prototype.loading();
+            return new Promise((resolve, reject) => {
+                axios.post(url, data).then(data => {
+                    resolve(data);
+                }).catch(error => {
+                    reject(error)
+                }).finally(() => {
+                    loading.close();
+                })
+            })
+        } else {
+            return axios.post(url, data);
+        }
+    },
+    put: (url, data, showLoading = true) => {
+        if (data) {
+            data = JSON.parse(JSON.stringify(data))
+        }
+        if (showLoading) {
+            const loading = Vue.prototype.loading();
+            return new Promise((resolve, reject) => {
+                axios.put(url, data).then(data => {
+                    resolve(data);
+                }).catch(error => {
+                    reject(error)
+                }).finally(() => {
+                    loading.close();
+                })
+            })
+        } else {
+            return axios.put(url, data);
+        }
+    },
+    delete: (url, config, showLoading = true) => {
+        if (config?.params) {
+            Object.keys(config.params).forEach(key => {
+                if (config.params[key] === null) {
+                    delete config.params[key];
+                }
+            });
+            config.params = JSON.parse(JSON.stringify(config.params))
+        }
+        if (showLoading) {
+            const loading = Vue.prototype.loading();
+            return new Promise((resolve, reject) => {
+                axios.delete(url, config).then(data => {
+                    resolve(data);
+                }).catch(error => {
+                    reject(error)
+                }).finally(() => {
+                    loading.close();
+                })
+            })
+        } else {
+            return axios.delete(url, config);
+        }
+    },
+    // POST multipart/form-data 格式上传文件
+    upload: (url, file, filename) => {
+        let formData = new FormData();
+        formData.append("file", file, filename || file.name);
+        let config = {
+            headers: {"Content-Type": "multipart/form-data"}
+        }
+        return axios.post(url, formData, config);
+    }
+};
+
+export const $axios = axiosLoading;
+
+export default {
+    install: function (Vue) {
+        Vue.prototype.$axios = axiosLoading;
+    }
+};

+ 192 - 0
src/components/AMap/AMapPolyline.vue

@@ -0,0 +1,192 @@
+<template>
+  <div style="position: relative;">
+    <el-amap-search-box class="search-box" :search-option="mapSearchOption" :on-search-result="onSearchPoiResult"></el-amap-search-box>
+    <el-amap vid="amap" ref="map" :zoom="11" :center="mapOption.position" :plugin="mapPlugin" :events="mapOption.events" class="map">
+      <el-amap-marker
+          vid="poi-marker"
+          :position="mapOption.position"/>
+      <el-amap-polygon
+          vid="polygon"
+          ref="polygon"
+          :path="polygonOption.path"
+          :visible="polygonOption.visible"
+          :editable="polygonOption.editable"
+          :zIndex="polygonOption.zIndex"
+          :strokeColor="polygonOption.strokeColor"
+          :strokeOpacity="polygonOption.strokeOpacity"
+          :strokeWeight="polygonOption.strokeWeight"
+          :fillColor="polygonOption.fillColor"
+          :fillOpacity="polygonOption.fillOpacity"
+          :strokeStyle="polygonOption.strokeStyle"
+          :draggable="polygonOption.draggable"
+          :events="polygonOption.events"
+      >
+      </el-amap-polygon>
+    </el-amap>
+  </div>
+</template>
+
+<script>
+export default {
+  name: "AMapPolyline",
+  data() {
+    const position = [116.3975, 39.9092];
+    const path = this.calculatePathWithCenter(position);
+    return {
+      polygonEditor: undefined, // 矢量图编辑器
+      map: undefined, // 地图
+      polygon: undefined, // 多边形
+      // 地图
+      mapOption: {
+        position,
+        events: {
+          init: (instance) => {
+            this.map = instance;
+          },
+          click: (e) => {
+            this.mapOption.position = [e.lnglat.lng, e.lnglat.lat];
+          }
+        },
+        draggable: true
+      },
+      // 多边形选项
+      polygonOption: {
+        path,
+        visible: true, // 是否可见
+        editable: false, // 多边形当前是否可编辑
+        zIndex: 10, // 多边形覆盖物的叠加顺序。地图上存在多个多边形覆盖物叠加时,通过该属性使级别较高的多边形覆盖物在上层显示默认zIndex:10
+        strokeColor: '#e50000', //	线条颜色,使用16进制颜色代码赋值。默认值为#006600
+        strokeOpacity: 0.6, // 轮廓线透明度,取值范围[0,1],0表示完全透明,1表示不透明。默认为0.9
+        strokeWeight: 5, // 轮廓线宽度
+        fillColor: '#4071ff', // 多边形填充颜色,使用16进制颜色代码赋值,如:#FFAA00
+        fillOpacity: 0.6, // 多边形填充透明度,取值范围[0,1],0表示完全透明,1表示不透明。默认为0.9
+        strokeStyle: 'solid', // 轮廓线样式,实线:solid,虚线:dashed
+        draggable: true, // 设置多边形是否可拖拽移动,默认为false
+        events: {
+          init: (instance) => {
+            this.polygon = instance;
+          }
+        },
+      },
+      // 搜索选项
+      mapSearchOption: {
+        city: "全国",
+        citylimit: false
+      },
+      // 地图插件
+      mapPlugin: [
+        {
+          pName: 'Geolocation', // 定位
+          enableHighAccuracy: true,// 是否使用高精度定位,默认:true
+          timeout: 30000,          // 超过10秒后停止定位,默认:无穷大
+          noIpLocate: 0,           // 禁止使用Ip定位
+          maximumAge: 0,           // 定位结果缓存0毫秒,默认:0
+          convert: true,           // 自动偏移坐标,偏移后的坐标为高德坐标,默认:true
+          showButton: false,       // 显示定位按钮,默认:true
+          buttonPosition: 'LB',    // 定位按钮停靠位置,默认:'LB',左下角
+          showMarker: true,        // 定位成功后在定位到的位置显示点标记,默认:true
+          showCircle: true,        // 定位成功后用圆圈表示定位精度范围,默认:true
+          panToLocation: true,     // 定位成功后将定位到的位置作为地图中心点,默认:true
+          zoomToAccuracy: true,    // 定位成功后调整地图视野范围使定位位置及精度范围视野内可见,默认:false
+          extensions: 'all',
+          events: {
+            init: (instance) => {
+              // instance.getCurrentPosition((status, result) => {
+              //   console.log(result);
+              //   if (result && result.position && result.status) {
+              //     this.mapOption.position = [result.position.lng, result.position.lat];
+              //     console.log(result.formattedAddress);
+              //   } else {
+              //     this.$message("定位失败,请检查位置权限");
+              //   }
+              // })
+            },
+            click(e) {
+              console.log(e);
+            }
+          }
+        },
+      ],
+    }
+  },
+
+  watch: {
+    map() {
+      this.editPolygon();
+    },
+    polygon() {
+      this.editPolygon();
+    }
+  },
+
+  mounted() {
+
+  },
+
+  methods: {
+
+    calculatePathWithCenter(center) {
+      return [[center[0] - 0.06, center[1] + 0.04], [center[0] + 0.06, center[1] + 0.04], [center[0] + 0.06, center[1] - 0.04], [center[0] - 0.06, center[1] - 0.04]]
+    },
+
+    // 搜索结果处理
+    onSearchPoiResult(pois) {
+      let latSum = 0;
+      let lngSum = 0;
+      if (pois.length > 0) {
+        pois.forEach(poi => {
+          let {lng, lat} = poi;
+          lngSum += lng;
+          latSum += lat;
+        });
+        let center = {
+          lng: lngSum / pois.length,
+          lat: latSum / pois.length
+        };
+        this.mapOption.position = [center.lng, center.lat];
+        this.polygonOption.path = this.calculatePathWithCenter(this.mapOption.position)
+      }
+    },
+
+    // 开始编辑多边形
+    editPolygon() {
+      if (!this.map || !this.polygon) return;
+      this.map.add(this.polygon)
+      setTimeout(() => {
+        this.map.setFitView([this.polygon]) // 缩放地图到合适的视野级别
+        this.polyEditor = new AMap.PolyEditor(this.map, this.polygon);
+        this.polyEditor.open()
+      }, 500)
+    },
+
+    // 获取多边形顶点
+    getPath() {
+      this.polyEditor.close();
+      return this.$refs.polygon.$$getPath();
+    },
+
+    // 设置多边形顶点
+    setPath(path) {
+      this.polygonOption.path = path;
+      setTimeout(() => {
+        this.map.setFitView([this.polygon]) // 缩放地图到合适的视野级别
+      }, 200)
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.search-box {
+  z-index: 10;
+  margin-bottom: 20px;
+  left: 0;
+  top: 0;
+  position: absolute;
+}
+
+.map {
+  height: 400px;
+  width: 800px;
+}
+</style>

+ 166 - 0
src/components/AMap/AMapPosition.vue

@@ -0,0 +1,166 @@
+<template>
+  <div>
+    <el-amap-search-box class="search-box" :search-option="mapSearchOption" :on-search-result="onSearchPoiResult"></el-amap-search-box>
+    <el-amap vid="marker" :zoom="poiMapMarker.zoom" :center="poiMapMarker.position" :plugin="mapPlugin" :events="events" class="map">
+      <el-amap-marker
+          vid="poi-marker"
+          :position="poiMapMarker.position"
+          :events="poiMapMarker.events"
+          :draggable="poiMapMarker.draggable"/>
+    </el-amap>
+  </div>
+</template>
+
+<script>
+export default {
+  name: "AMapPosition",
+  props: {
+    position: undefined,
+  },
+  data() {
+    return {
+      geocoder: undefined, // 编码器
+      // 地图标点
+      poiMapMarker: {
+        position: [116.3975, 39.9092],
+        events: {
+          dragend: e => {
+            console.log(this)
+            this.poiMapMarker.position = [e.lnglat.lng, e.lnglat.lat];
+            this.getAddress(e.lnglat.lng, e.lnglat.lat);
+          }
+        },
+        draggable: true,
+        zoom:14,
+      },
+      // 地图插件
+      mapPlugin: [
+        // {
+        //     pName: 'ToolBar',
+        //     events: {
+        //         init(instance) {
+        //             console.log(instance);
+        //         }
+        //     }
+        // },
+        {
+          pName: 'Geolocation', // 定位
+          enableHighAccuracy: true,// 是否使用高精度定位,默认:true
+          timeout: 30000,          // 超过10秒后停止定位,默认:无穷大
+          noIpLocate: 0,           // 禁止使用Ip定位
+          maximumAge: 0,           // 定位结果缓存0毫秒,默认:0
+          convert: true,           // 自动偏移坐标,偏移后的坐标为高德坐标,默认:true
+          showButton: false,       // 显示定位按钮,默认:true
+          buttonPosition: 'LB',    // 定位按钮停靠位置,默认:'LB',左下角
+          showMarker: true,        // 定位成功后在定位到的位置显示点标记,默认:true
+          showCircle: true,        // 定位成功后用圆圈表示定位精度范围,默认:true
+          panToLocation: true,     // 定位成功后将定位到的位置作为地图中心点,默认:true
+          zoomToAccuracy: true,    // 定位成功后调整地图视野范围使定位位置及精度范围视野内可见,默认:false
+          extensions: 'all',
+          events: {
+            init: (instance) => {
+              instance.getCurrentPosition((status, result) => {
+                console.log(result);
+                if (result && result.position && result.status) {
+                  this.poiMapMarker.position = [result.position.lng, result.position.lat];
+                  console.log(result.formattedAddress);
+                } else {
+                  this.$message("定位失败,请检查位置权限");
+                }
+              })
+            },
+            click(e) {
+              console.log(e);
+            }
+          }
+        },
+        {
+          pName: 'Geocoder', // 编码
+          events: {
+            init: (instance) => {
+              this.geocoder = instance;
+            }
+          }
+        }],
+      // 搜索选项
+      mapSearchOption: {
+        city: "全国",
+        citylimit: false
+      },
+      // 地图事件
+      events: {
+        init: (instance) => {
+          console.log(instance);
+        },
+        // 点击获取地址的数据
+        click: (e) => {
+          this.poiMapMarker.position = [e.lnglat.lng, e.lnglat.lat];
+          this.getAddress(e.lnglat.lng, e.lnglat.lat);
+        }
+      }
+    }
+  },
+
+
+  watch: {
+    position(value) {
+      if (value && value[0] && value[1]) {
+        this.poiMapMarker.position = value;
+      }
+    }
+  },
+
+  methods: {
+    // 搜索结果处理
+    onSearchPoiResult(pois) {
+      let latSum = 0;
+      let lngSum = 0;
+      if (pois.length > 0) {
+        pois.forEach(poi => {
+          let {lng, lat} = poi;
+          lngSum += lng;
+          latSum += lat;
+        });
+        let center = {
+          lng: lngSum / pois.length,
+          lat: latSum / pois.length
+        };
+        this.poiMapMarker.position = [center.lng, center.lat];
+        this.getAddress(center.lng, center.lat);
+      }
+    },
+
+    // 根据经纬度获取地址
+    getAddress(lng, lat) {
+      if (this.geocoder) {
+        this.geocoder.getAddress([lng, lat], (status, result) => {
+          console.log(result);
+          if (status === 'complete' && result.info === 'OK') {
+            if (result && result.regeocode) {
+              console.log(result.regeocode.formattedAddress);
+              this.$emit("position", {lng, lat, address: result.regeocode.formattedAddress})
+            }
+          }
+        })
+      } else {
+        this.$message("地图初始化中,请稍后");
+      }
+    },
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.search-box {
+  z-index: 10;
+  margin-bottom: 20px;
+  left: 0;
+  top: 0;
+  position: absolute;
+}
+
+.map {
+  height: 400px;
+  width: 800px;
+}
+</style>

+ 106 - 0
src/components/Breadcrumb/index.vue

@@ -0,0 +1,106 @@
+<template>
+  <el-breadcrumb class="app-breadcrumb" separator="/">
+    <transition-group name="breadcrumb">
+      <el-breadcrumb-item v-for="(item,index) in levelList" :key="item.path" v-if="isVisible(item, index)">
+        <span v-if="item.redirect==='noRedirect' || index===levelList.length-1 || index === 0" class="no-redirect">{{ item.meta.title }}</span>
+        <a v-else @click.prevent="handleLink(item)">{{ item.meta.title }}</a>
+      </el-breadcrumb-item>
+    </transition-group>
+  </el-breadcrumb>
+</template>
+
+<script>
+
+export default {
+  data() {
+    return {
+      levelList: [],
+    }
+  },
+  watch: {
+    $route(route) {
+      // if you go to the redirect page, do not update the breadcrumbs
+      if (route.path.startsWith('/redirect/')) {
+        return
+      }
+      this.getBreadcrumb()
+    }
+  },
+  created() {
+    this.getBreadcrumb()
+  },
+  methods: {
+    // 获取面包屑列表
+    getBreadcrumb() {
+      let node = this.findNode(this.$router.options.routes, this.$route.name);
+      if (node) {
+        let list = [];
+        this.generateLevelList(node, list)
+        this.levelList = list;
+      }
+    },
+
+    // 从路由列表中找到当前路由对应的node
+    findNode(routes, name) {
+      for (let i = 0; i < routes.length; i++) {
+        if (routes[i].name === name) {
+          return routes[i];
+        } else if (routes[i].children) {
+          let node = this.findNode(routes[i].children, name);
+          if (node) {
+            return node;
+          }
+        }
+      }
+      return undefined;
+    },
+
+    // 根据节点生成面包屑列表
+    generateLevelList(node, list) {
+      if (node && node.meta && node.meta.title && (node.meta.breadcrumb !== false)) {
+        list.unshift(node)
+      }
+      if (node.parent) {
+        this.generateLevelList(node.parent, list);
+      }
+    },
+
+    isHome(route) {
+      const name = route && route.name;
+      if (!name) {
+        return false
+      }
+      return name.trim().toLocaleLowerCase() === 'Home'.toLocaleLowerCase()
+    },
+
+    // 此面包屑是否可见
+    isVisible(item, index) {
+      return !(index === 0 && item.children && item.children.filter(item => !item.hidden) && item.children.filter(item => !item.hidden).length === 1);
+    },
+
+    // 面包屑点击处理
+    handleLink(item) {
+      const {redirect, path} = item
+      if (redirect) {
+        this.$router.push(redirect)
+        return
+      }
+      this.$router.push(path);
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.app-breadcrumb.el-breadcrumb {
+  display: inline-block;
+  font-size: 14px;
+  line-height: 50px;
+  margin-left: 8px;
+
+  .no-redirect {
+    color: #97a8be;
+    cursor: text;
+  }
+}
+</style>

+ 42 - 0
src/components/Calendar/Calendar.vue

@@ -0,0 +1,42 @@
+<template>
+  <FullCalendar ref="fullCalendar" :options="calendarOptions"/>
+</template>
+<script>
+import FullCalendar from '@fullcalendar/vue'
+import dayGridPlugin from '@fullcalendar/daygrid'
+import interactionPlugin from '@fullcalendar/interaction'
+
+export default {
+  name: "Calendar",
+  components: {
+    FullCalendar // make the <FullCalendar> tag available
+  },
+  data() {
+    return {
+      calendar: undefined,
+      calendarOptions: {
+        plugins: [dayGridPlugin, interactionPlugin],
+        initialView: 'dayGridMonth', // 初始视图:月视图
+        weekends: true, // 是否显示周末
+
+        dateClick: this.onDate, // 日期点击事件
+        events: [ // 待办事件列表
+          {title: 'event 1', date: '2019-04-01'},
+          {title: 'event 2', date: '2019-04-02'}
+        ]
+      }
+    }
+  },
+
+  mounted() {
+    this.calendar = this.$refs.fullCalendar.getApi();
+  },
+
+  methods: {
+    onDate: function (arg) {
+      alert('date click! ' + arg.dateStr)
+      this.calendar.next();
+    }
+  }
+}
+</script>

+ 37 - 0
src/components/Checkbox/index.vue

@@ -0,0 +1,37 @@
+<template>
+  <el-checkbox :value="checked" @change="onChange"><span v-if="label">{{ label }}</span></el-checkbox>
+</template>
+
+<script>
+export default {
+  name: "Checkbox",
+  model: {
+    prop: 'value',
+    event: 'change',
+  },
+  props: {
+    value: {
+      type: [Number, String],
+      require: true
+    },
+    label: {
+      type: [Number, String],
+      default: undefined
+    },
+  },
+  computed: {
+    checked() {
+      return Number(this.value) === 1;
+    }
+  },
+  methods: {
+    onChange(value) {
+      this.$emit("change", value ? 1 : 2);
+    }
+  }
+}
+</script>
+
+<style scoped>
+
+</style>

+ 75 - 0
src/components/FormClear.vue

@@ -0,0 +1,75 @@
+<template>
+  <div class="form-clear-container" :class="{shrinkable:shrinkable}">
+    <el-tooltip effect="dark" content="清空筛选" placement="top">
+      <svg-icon icon-class="clear" @click="onClear" class="clear-button"/>
+    </el-tooltip>
+    <div @click="onChange" style="margin-left: 16px;" v-if="shrinkable">
+      <span style="margin-right: 8px;">{{ show ? '收起' : '展开' }}</span>
+      <i class="el-icon-arrow-down" :class="{up:show, down:!show}"></i>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: "FormClear",
+  props: {
+    shrinkable: {
+      type: Boolean,
+      default: false
+    }
+  },
+  data() {
+    return {
+      show: true,
+    }
+  },
+  methods: {
+    onClear() {
+      this.$emit('clear');
+    },
+    onChange() {
+      this.show = !this.show;
+      this.$emit('change', this.show);
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.form-clear-container {
+  display: flex;
+  align-items: center;
+  margin-bottom: 18px;
+  color: #666666;
+  font-size: 14px;
+  height: 32px;
+  line-height: 32px;
+  min-width: 40px;
+
+  .clear-button {
+    width: 32px;
+    height: 32px;
+    line-height: 32px;
+    text-align: center;
+    padding: 8px;
+  }
+
+  &:hover {
+    cursor: pointer;
+  }
+
+  .up {
+    transform: rotate(180deg);
+    transition: all .5s;
+  }
+
+  .down {
+    transition: all .5s;
+  }
+}
+
+.shrinkable {
+  min-width: 100px;
+}
+</style>

+ 42 - 0
src/components/Hamburger/index.vue

@@ -0,0 +1,42 @@
+<template>
+  <div style="padding: 0 15px;" @click="toggleClick">
+    <svg :class="{'is-active':isActive}"
+         class="hamburger"
+         viewBox="0 0 1024 1024"
+         xmlns="http://www.w3.org/2000/svg"
+         width="64"
+         height="64">
+      <path d="M408 442h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm-8 204c0 4.4 3.6 8 8 8h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56zm504-486H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 632H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM142.4 642.1L298.7 519a8.84 8.84 0 0 0 0-13.9L142.4 381.9c-5.8-4.6-14.4-.5-14.4 6.9v246.3a8.9 8.9 0 0 0 14.4 7z"/>
+    </svg>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'Hamburger',
+  props: {
+    isActive: {
+      type: Boolean,
+      default: false
+    }
+  },
+  methods: {
+    toggleClick() {
+      this.$emit('toggleClick')
+    }
+  }
+}
+</script>
+
+<style scoped>
+.hamburger {
+  display: inline-block;
+  vertical-align: middle;
+  width: 20px;
+  height: 20px;
+}
+
+.hamburger.is-active {
+  transform: rotate(180deg);
+}
+</style>

+ 117 - 0
src/components/Identify/index.vue

@@ -0,0 +1,117 @@
+<template>
+  <canvas ref="s-canvas" :width="contentWidth" :height="contentHeight" @click="onClick"></canvas>
+</template>
+<script>
+export default {
+  name: 'SIdentify',
+  props: {
+    identifyCode: { //默认注册码
+      type: String,
+      default: '1234'
+    },
+    fontSizeMin: { // 字体最小值
+      type: Number,
+      default: 25
+    },
+    fontSizeMax: { // 字体最大值
+      type: Number,
+      default: 35
+    },
+    backgroundColorMin: { // 验证码图片背景色最小值
+      type: Number,
+      default: 200
+    },
+    backgroundColorMax: {  // 验证码图片背景色最大值
+      type: Number,
+      default: 220
+    },
+    dotColorMin: { // 背景干扰点最小值
+      type: Number,
+      default: 60
+    },
+    dotColorMax: { // 背景干扰点最大值
+      type: Number,
+      default: 120
+    },
+    contentWidth: { //容器宽度
+      type: Number,
+      default: 116
+    },
+    contentHeight: { //容器高度
+      type: Number,
+      default: 38
+    }
+  },
+  methods: {
+    onClick() {
+      this.$emit('onClick');
+    },
+    // 生成一个随机数
+    randomNum(min, max) {
+      return Math.floor(Math.random() * (max - min) + min)
+    },
+    // 生成一个随机的颜色
+    randomColor(min, max) {
+      let r = this.randomNum(min, max);
+      let g = this.randomNum(min, max);
+      let b = this.randomNum(min, max);
+      return 'rgb(' + r + ',' + g + ',' + b + ')';
+    },
+    drawPic() {
+      let canvas = this.$refs['s-canvas'];
+      let ctx = canvas.getContext('2d');
+      ctx.textBaseline = 'bottom';
+      // 绘制背景
+      ctx.fillStyle = this.randomColor(this.backgroundColorMin, this.backgroundColorMax);
+      ctx.fillRect(0, 0, this.contentWidth, this.contentHeight);
+      // 绘制文字
+      for (let i = 0; i < this.identifyCode.length; i++) {
+        this.drawText(ctx, this.identifyCode[i], i);
+      }
+      this.drawLine(ctx);
+      this.drawDot(ctx);
+    },
+    drawText(ctx, txt, i) {
+      ctx.fillStyle = this.randomColor(50, 160); //随机生成字体颜色
+      ctx.font = this.randomNum(this.fontSizeMin, this.fontSizeMax) + 'px SimHei'; //随机生成字体大小
+      let x = (i + 1) * (this.contentWidth / (this.identifyCode.length + 1));
+      let y = this.randomNum(this.fontSizeMax, this.contentHeight - 5);
+      let deg = this.randomNum(-30, 30);
+      // 修改坐标原点和旋转角度
+      ctx.translate(x, y);
+      ctx.rotate(deg * Math.PI / 180);
+      ctx.fillText(txt, 0, 0);
+      // 恢复坐标原点和旋转角度
+      ctx.rotate(-deg * Math.PI / 180);
+      ctx.translate(-x, -y)
+    },
+    drawLine(ctx) {
+      // 绘制干扰线
+      for (let i = 0; i < 4; i++) {
+        ctx.strokeStyle = this.randomColor(100, 200);
+        ctx.beginPath();
+        ctx.moveTo(this.randomNum(0, this.contentWidth), this.randomNum(0, this.contentHeight));
+        ctx.lineTo(this.randomNum(0, this.contentWidth), this.randomNum(0, this.contentHeight));
+        ctx.stroke();
+      }
+    },
+    drawDot(ctx) {
+      // 绘制干扰点
+      for (let i = 0; i < 30; i++) {
+        ctx.fillStyle = this.randomColor(0, 255);
+        ctx.beginPath();
+        ctx.arc(this.randomNum(0, this.contentWidth), this.randomNum(0, this.contentHeight), 1, 0, 2 * Math.PI);
+        ctx.fill();
+      }
+    }
+  },
+  watch: {
+    identifyCode() {
+      this.drawPic()
+    }
+  },
+  mounted() {
+    this.drawPic()
+  }
+}
+</script>

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

@@ -0,0 +1,38 @@
+<template>
+  <div class="page-footer" :style="size">
+    <el-button v-if="showConfirm" type="primary" @click="$emit('confirm')">确 认</el-button>
+    <el-button @click="$emit('cancel')">取 消</el-button>
+    <slot></slot>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'PageFooter',
+
+  props: {
+    showConfirm: Boolean,
+    width: {
+      type: [Number, String],
+      default: "100%"
+    }
+  },
+
+  computed: {
+    size() {
+      return {width: this.width};
+    }
+  },
+
+  methods: {}
+};
+</script>
+
+<style scoped lang="scss">
+.page-footer {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin: 30px 0;
+}
+</style>

+ 75 - 0
src/components/PageHeader/index.vue

@@ -0,0 +1,75 @@
+<template>
+  <div class="page-header">
+    <div class="page-header-back-container" @click="onBack">
+      <i class="el-icon-back"></i>
+      <div class="page-header-title">
+        <slot name="title">{{ title }}</slot>
+      </div>
+    </div>
+    <div class="page-header-content">
+      <slot name="content">{{ content }}</slot>
+    </div>
+    <div class="page-header-right">
+      <slot name="right"></slot>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'PageHeader',
+
+  props: {
+    title: {
+      type: String,
+      default: "返回"
+    },
+    content: String
+  },
+
+  methods:{
+    onBack() {
+      this.$emit('back');
+      this.$router.back()
+    }
+  }
+};
+</script>
+
+<style scoped lang="scss">
+.page-header {
+  display: flex;
+  align-items: center;
+  position: relative;
+  justify-content: center;
+  margin-bottom: 30px;
+
+  .page-header-back-container {
+    display: flex;
+    align-items: center;
+    position: absolute;
+    left: 0;
+    top: 0;
+    bottom: 0;
+    cursor: pointer;
+
+    .page-header-title {
+      font-size: 16px;
+      color: #303133;
+      margin-left: 10px;
+    }
+  }
+
+  .page-header-content {
+    font-size: 20px;
+    color: #303133;
+  }
+
+  .page-header-right {
+    position: absolute;
+    right: 0;
+    top: 0;
+    bottom: 0;
+  }
+}
+</style>

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

@@ -0,0 +1,73 @@
+<template>
+  <el-pagination
+      v-if="total>0"
+      @size-change="handleSizeChange"
+      @current-change="handleCurrentChange"
+      :background="!small"
+      :pager-count="small?5:7"
+      :small="small"
+      :current-page="currentPage"
+      :page-sizes="[5, 10, 20, 30, 40, 50]"
+      :page-size="pageSize"
+      :layout="small?'total, prev, pager, next':'total, sizes, prev, pager, next, jumper'"
+      :total="total">
+  </el-pagination>
+</template>
+
+<script>
+export default {
+  name: "Pagination",
+  props: {
+    total: {
+      type: Number,
+      require: true
+    },
+    page: {
+      type: Number,
+      require: false,
+      default: 1
+    },
+    size: {
+      type: Number,
+      require: false,
+      default: 10
+    },
+    small: Boolean
+  },
+  computed: {
+    currentPage: {
+      get() {
+        return this.page
+      },
+      set(val) {
+        this.$emit('update:page', val)
+      }
+    },
+    pageSize: {
+      get() {
+        return this.size
+      },
+      set(val) {
+        this.$emit('update:size', val)
+      }
+    }
+  },
+  methods: {
+    handleSizeChange(val) {
+      this.pageSize = val;
+      this.$emit("change", this.pageSize, this.currentPage)
+    },
+    handleCurrentChange(val) {
+      this.currentPage = val;
+      this.$emit("change", this.pageSize, this.currentPage)
+    }
+  },
+}
+</script>
+
+<style scoped lang="scss">
+.el-pagination {
+  text-align: end;
+  padding: 20px 0;
+}
+</style>

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

@@ -0,0 +1,63 @@
+<template>
+  <div v-if="isExternal" :style="styleExternalIcon" class="svg-external-icon svg-icon" v-on="$listeners"/>
+  <svg v-else :class="svgClass" aria-hidden="true" v-on="$listeners">
+    <use :xlink:href="iconName"/>
+  </svg>
+</template>
+
+<script>
+// doc: https://panjiachen.github.io/vue-element-admin-site/feature/component/svg-icon.html#usage
+import {isExternal} from '@/utils/validate'
+
+export default {
+  name: 'SvgIcon',
+  props: {
+    iconClass: {
+      type: String,
+      required: true
+    },
+    className: {
+      type: String,
+      default: ''
+    }
+  },
+  computed: {
+    isExternal() {
+      return isExternal(this.iconClass)
+    },
+    iconName() {
+      return `#icon-${this.iconClass}`
+    },
+    svgClass() {
+      if (this.className) {
+        return 'svg-icon ' + this.className
+      } else {
+        return 'svg-icon'
+      }
+    },
+    styleExternalIcon() {
+      return {
+        mask: `url(${this.iconClass}) no-repeat 50% 50%`,
+        '-webkit-mask': `url(${this.iconClass}) no-repeat 50% 50%`
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.svg-icon {
+  width: 1em;
+  height: 1em;
+  vertical-align: -0.15em;
+  fill: currentColor;
+  overflow: hidden;
+  font-size: 21px;
+}
+
+.svg-external-icon {
+  background-color: currentColor;
+  mask-size: cover !important;
+  display: inline-block;
+}
+</style>

+ 54 - 0
src/components/TextLabel/index.vue

@@ -0,0 +1,54 @@
+<template>
+  <div :class="[type ? `text--${type}` : '']" v-bind="$attrs">
+    <i :class="icon" v-if="icon"></i>
+    <span v-if="$slots.default" class="el-link--inner">
+      <slot></slot>
+    </span>
+
+    <template v-if="$slots.icon">
+      <slot v-if="$slots.icon" name="icon"></slot>
+    </template>
+  </div>
+</template>
+
+<script>
+
+export default {
+  name: 'TextLabel',
+
+  props: {
+    type: {
+      type: String,
+      default: 'default'
+    },
+    icon: String
+  },
+};
+</script>
+<style scoped lang="scss">
+@import "../../styles/variables";
+
+.text--primary {
+  color: $--color-primary;
+}
+
+.text--success {
+  color: $--color-success;
+}
+
+.text--warning {
+  color: $--color-warning;
+}
+
+.text--danger {
+  color: $--color-danger;
+}
+
+.text--info {
+  color: $--color-info;
+}
+
+.text--default {
+  color: black;
+}
+</style>

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

@@ -0,0 +1,169 @@
+<template>
+  <el-color-picker
+      :value="theme"
+      @change="onThemeChange"
+      :predefine="['#409EFF', '#1890ff', '#304156','#212121','#11a983', '#13c2c2', '#6959CD', '#f5222d' ]"
+      class="theme-picker"
+      popper-class="theme-picker-dropdown"
+  />
+
+</template>
+
+<script>
+
+const version = require('element-ui/package.json').version // element-ui version from node_modules
+const ORIGINAL_THEME = '#409EFF' // original color,element原始颜色不要修改
+
+export default {
+  name: 'ThemePicker',
+  model: {
+    prop: 'defaultThemeColor',
+    event: 'change',
+  },
+  props: {
+    defaultThemeColor: String
+  },
+  data() {
+    return {
+      chalk: '', // content of theme-chalk css
+      theme: '', // 主题色
+    }
+  },
+  watch: {
+    defaultThemeColor(color, oldColor) {
+      if (color !== ORIGINAL_THEME || (color === ORIGINAL_THEME && oldColor !== undefined)) {
+        this.themeChangeHandler(color);
+      }
+    }
+  },
+  methods: {
+    onThemeChange(val) {
+      this.$emit("change", val); // 发送Url改变事件
+    },
+
+    async themeChangeHandler(val) {
+      const beginTime = Date.now();
+      const oldVal = this.chalk ? this.theme : ORIGINAL_THEME
+      this.theme = val
+      if (typeof val !== 'string') return
+      const themeCluster = this.getThemeCluster(val.replace('#', ''))
+      const originalCluster = this.getThemeCluster(oldVal.replace('#', ''))
+      console.log(themeCluster, originalCluster)
+      const $message = this.$message({
+        message: this.chalk ? '正在切换自定义主题' : '正在恢复自定义主题',
+        customClass: 'theme-message',
+        type: 'success',
+        duration: 0,
+        iconClass: 'el-icon-loading'
+      })
+      const getHandler = (variable, id) => {
+        return () => {
+          const originalCluster = this.getThemeCluster(ORIGINAL_THEME.replace('#', ''))
+          const newStyle = this.updateStyle(this[variable], originalCluster, themeCluster)
+          let styleTag = document.getElementById(id)
+          if (!styleTag) {
+            styleTag = document.createElement('style')
+            styleTag.setAttribute('id', id)
+            document.head.append(styleTag);
+          }
+          styleTag.innerText = newStyle
+        }
+      }
+      if (!this.chalk) {
+        const url = `https://unpkg.com/element-ui@${version}/lib/theme-chalk/index.css`
+        await this.getCSSString(url, 'chalk')
+      }
+
+      // 保证至少0.8秒后完成,
+      setTimeout(() => {
+        const chalkHandler = getHandler('chalk', 'chalk-style')
+        chalkHandler()
+
+        this.$emit('change', val)
+        $message.close()
+      }, Math.max(Date.now() - beginTime + 800, 0))
+    },
+
+    updateStyle(style, oldCluster, newCluster) {
+      let newStyle = style
+      oldCluster.forEach((color, index) => {
+        newStyle = newStyle.replace(new RegExp(color, 'ig'), newCluster[index])
+      })
+      return newStyle
+    },
+    getCSSString(url, variable) {
+      return new Promise(resolve => {
+        const xhr = new XMLHttpRequest()
+        xhr.onreadystatechange = () => {
+          if (xhr.readyState === 4 && xhr.status === 200) {
+            this[variable] = xhr.responseText.replace(/@font-face{[^}]+}/, '')
+            resolve()
+          }
+        }
+        xhr.open('GET', url)
+        xhr.send()
+      })
+    },
+    getThemeCluster(theme) {
+      const tintColor = (color, tint) => {
+        let red = parseInt(color.slice(0, 2), 16)
+        let green = parseInt(color.slice(2, 4), 16)
+        let blue = parseInt(color.slice(4, 6), 16)
+        if (tint === 0) { // when primary color is in its rgb space
+          return [red, green, blue].join(',')
+        } else {
+          red += Math.round(tint * (255 - red))
+          green += Math.round(tint * (255 - green))
+          blue += Math.round(tint * (255 - blue))
+          red = red.toString(16)
+          green = green.toString(16)
+          blue = blue.toString(16)
+          return `#${red}${green}${blue}`
+        }
+      }
+      const shadeColor = (color, shade) => {
+        let red = parseInt(color.slice(0, 2), 16)
+        let green = parseInt(color.slice(2, 4), 16)
+        let blue = parseInt(color.slice(4, 6), 16)
+        red = Math.round((1 - shade) * red)
+        green = Math.round((1 - shade) * green)
+        blue = Math.round((1 - shade) * blue)
+        red = red.toString(16)
+        green = green.toString(16)
+        blue = blue.toString(16)
+        return `#${red}${green}${blue}`
+      }
+      const clusters = [theme]
+      for (let i = 0; i <= 9; i++) {
+        clusters.push(tintColor(theme, Number((i / 10).toFixed(2))))
+      }
+      clusters.push(shadeColor(theme, 0.1))
+      return clusters
+    }
+  }
+}
+</script>
+
+<style scoped>
+.theme-message,
+.theme-picker-dropdown {
+  z-index: 99999 !important;
+}
+
+.theme-picker {
+  height: 26px !important;
+  width: 26px !important;
+}
+
+
+::v-deep .el-color-picker__trigger {
+  height: 26px !important;
+  width: 26px !important;
+  padding: 2px;
+}
+
+.theme-picker-dropdown .el-color-dropdown__link-btn {
+  display: none;
+}
+
+</style>

+ 240 - 0
src/components/Tinymce/index.vue

@@ -0,0 +1,240 @@
+<template>
+    <editor :id="id" v-model="content" :init="initTinymce"></editor>
+</template>
+<script>
+
+import tinymce from 'tinymce/tinymce'
+import Editor from '@tinymce/tinymce-vue'
+import 'tinymce/themes/silver'
+import 'tinymce/icons/default'
+import plugins from './plugins'
+import toolbar from './toolbar'
+
+import 'tinymce/plugins/anchor' // 插入瞄点
+import 'tinymce/plugins/advlist' // 高级列表插件
+import 'tinymce/plugins/autolink' // 自动链接
+// import 'tinymce/plugins/autoresize' // 自动适应大小
+import 'tinymce/plugins/autosave' // 自动保存
+import 'tinymce/plugins/charmap' // 插入特殊字符
+import 'tinymce/plugins/code' // 插入html源码
+import 'tinymce/plugins/emoticons' // 插入unicode字符表情
+import emojis from 'tinymce/plugins/emoticons/js/emojis' // emoji字符文件
+import 'tinymce/plugins/fullscreen' // 全屏
+import 'tinymce/plugins/help' // 帮助
+import 'tinymce/plugins/hr' // 水平分割线
+import 'tinymce/plugins/image' // 插入上传图片插件
+import 'tinymce/plugins/insertdatetime' // 插入日期时间
+import 'tinymce/plugins/link' // 插入超链接
+import 'tinymce/plugins/lists' // 列表插件
+import 'tinymce/plugins/media' // 插入视频插件
+import 'tinymce/plugins/pagebreak' // 插入分页符
+import 'tinymce/plugins/preview' // 预览
+import 'tinymce/plugins/print' // 打印
+import 'tinymce/plugins/quickbars' // 快捷操作
+// import 'tinymce/plugins/save'// 保存
+import 'tinymce/plugins/searchreplace' // 查找和替换
+import 'tinymce/plugins/table' // 插入表格插件
+import 'tinymce/plugins/wordcount' // 字数统计插件
+// import bdmap from  './tinymce/bdmap/plugin.js'// 百度地图
+import {upload} from "@/utils/httpUtil";
+
+export default {
+  name: 'Tinymce',
+  components: {Editor},
+  props: {
+    id: {
+      type: String,
+      default: 'tinymce'
+    },
+    value: {
+      type: String,
+      default: ''
+    },
+    height: {
+      type: [Number, String],
+      required: false,
+      default: 360
+    },
+    width: {
+      type: [Number, String],
+      required: false,
+      default: 'auto'
+    },
+    url: { // 文件上传地址
+      type: String,
+      default: '/v1/file/upload'
+    }
+  },
+  data() {
+    return {
+      fullscreen: false,
+      content: this.value,
+      initTinymce: {
+        // 基本设置
+        language_url: process.env.BASE_URL + 'tinymce/lang/zh_CN.js',// 语言包的路径
+        language: 'zh_CN',
+        skin_url: process.env.BASE_URL + 'tinymce/skins/ui/oxide',// skin路径 oxide-dark:深色  oxide:浅色
+        menubar: 'file edit insert view format table tools help', // 菜单栏
+        toolbar: toolbar, // 工具栏,false:隐藏工具栏
+        plugins: plugins, // 指定需加载的插件
+
+        // UI界面配置
+        statusbar: false, // 状态栏指的是编辑器最底下、左侧显示dom信息、右侧显示Tiny版权链接和调整大小的那一条。
+        branding: false, // 显示右下角技术支持
+        elementpath: false, // 显示底栏的元素路径
+        width: typeof this.width === "number" ? `${this.width}px` : this.width, // 宽度,默认100%
+        height: typeof this.height === "number" ? `${this.height}px` : this.height,  // 高度
+        min_width: '400px', // 最小宽度400px
+        min_height: '400px', // 最小高度400px
+        max_height: '800px', // 最大高度800px
+        resize: true, // true 调整高度  false 完全不动, 'both' 宽高都能改变
+        draggable_modal: true, // 模态窗口允许拖动
+        // fixed_toolbar_container: '#mytoolbar', // 指定工具栏在某一容器顶部固定。
+        toolbar_mode: 'sliding',
+        toolbar_sticky: true, // 粘性工具栏(或停靠工具栏),在向下滚动网页直到不再可见编辑器时,将工具栏和菜单停靠在屏幕顶部。
+
+        // 集成配置
+        auto_focus: true, // 自动获得焦点
+        cache_suffix: '?v=5.4.1', // 缓存请求后缀
+        setup: this.setup, // 初始化前执行
+        init_instance_callback: this.init_instance_callback, // 初始化结束后执行
+
+        // 内容外观配置
+        body_class: 'panel-body', // 为编辑区body指定类或id
+        // content_css : ['mycontent.css', 'mycontent2.css'], // 允许自定义TinyMCE编辑区域内的样式
+        placeholder: '在这里输入文字',
+        color_cols: 8, // 颜色选择列表的列数
+        // color_map:[], // 自定义颜色选择列表的颜色
+        custom_colors: true, // 调色盘开关
+
+        // 格式化配置
+        fontsize_formats: '8pt 10pt 12pt 14pt 16pt 18pt 24pt 36pt 48pt 56pt 72pt',
+        font_formats: '微软雅黑=Microsoft YaHei,Helvetica Neue,PingFang SC,sans-serif;苹果苹方=PingFang SC,Microsoft YaHei,sans-serif;宋体=simsun,serif;仿宋体=FangSong,serif;黑体=SimHei,sans-serif;Arial=arial,helvetica,sans-serif;Arial Black=arial black,avant garde;Book Antiqua=book antiqua,palatino;',
+
+        // 其他配置
+        custom_undo_redo_levels: 10, // 撤销次数
+        end_container_on_empty_block: true, // 空元素回车将其拆分
+        nowrap: false, // 文本水平不换行
+        object_resizing: false, // 调整大小控件开关
+        typeahead_urls: true, // 键入网址判断
+
+        // image - 图片上传
+        // image_caption: true, // 在弹出框中增加一个“标题”选项,
+        // image_list: [ // 预定义图片列表。也可以使用一个返回json数据的URL。如 image_list: "/mylist.php"。它还支持自定义异步函数
+        //   {title: '狗', value: 'mydog.jpg'},
+        //   {title: '猫', value: 'mycat.gif'}
+        // ],
+        image_advtab: true, // 可以自定义图片的css样式(内置style)、边距(margin)和边框(border)。
+        images_upload_url: '/demo/upimg.php', // 指定一个接受上传文件的后端处理程序地址
+        images_upload_base_path: '/demo', // 如果返回的地址是相对路径,可以给相对路径指定它所相对的基本路径
+        images_upload_credentials: false, // 对images_upload_url中指定的地址调用时是否传递cookie等跨域的凭据。值为布尔值,默认false。
+        images_upload_handler: this.imageHandler, // 此选项允许你使用自定义函数代替TinyMCE来处理上传操作。该自定义函数需提供三个参数:blobInfo、成功回调和失败回调。
+
+        // link - 超链接
+        link_default_protocol: 'http', // 默认协议
+        default_link_target: '_blank', // 默认链接打开方式
+        file_picker_callback: this.fileHandler, // 文件上传处理函数
+        file_picker_types: 'file image media', // 指定文件上传类型
+
+        // advlist - 高级列表插件
+        advlist_bullet_styles: 'default,circle,disc,square',
+        advlist_number_styles: 'default,lower-alpha,lower-greek,lower-roman,upper-alpha,upper-roman',
+
+        // autoresize - 自适应插件
+        autoresize_bottom_margin: 50, // 指定编辑器body初始化时底边距,也就是加一个margin-bottom。
+        autoresize_max_height: 650, // 编辑区域的最大高
+        autoresize_min_height: 600, // 编辑区域的最小高度
+
+        // autosave - 自动保存
+        autosave_ask_before_unload: true, // 当关闭或跳转URL时,弹出提示框提醒用户仍未保存变更内容。默认开启提示。
+        autosave_interval: '30s', // 自动存稿的世界间隔。注意该值为字符串,以秒为单位
+        // autosave_prefix: "tinymce-autosave-{path}{query}-{id}-", // 自动存稿在本地存储(local storage)中的字段(key)前缀。
+        autosave_restore_when_empty: true, // 当编辑器初始化时内容区为空时,Tinymce是否应自动还原存储在本地存储中的草稿。
+        autosave_retention: '20m', // 设置自动草稿的有效期。当草稿超过有效期则忽略。值是字符串,单位是分。
+
+        // emoticons - emoji加载
+        emoticons_database_url: emojis
+
+        // imagetools_cors_hosts: ['www.tinymce.com', 'codepen.io'],
+      }
+    }
+  },
+  computed: {
+  },
+  watch: {
+    value(val) {
+      this.content = val
+    },
+    content(val) {
+      this.$emit('input', val)
+    }
+  },
+  mounted() {
+    tinymce.init({theme: 'silver'})
+  },
+  methods: {
+    // tinymce.editors['编辑器id'].setMode('design'); // 开启编辑模式
+    // tinymce.editors['编辑器id'].setMode('readonly'); // 开启只读模式
+    // tinymce.editors['编辑器id'].notificationManager.open({
+    //   text: '这是一条提示信息。',
+    //   type: 'info' // success 成功  info 普通信息  warning 警告信息  error 错误信息
+    //   timeout: 5000,
+    //   closeButton: false,
+    // });
+
+    // 图片上传处理
+    imageHandler(blobInfo, successCallback, failCallback) {
+      let file = blobInfo.blob()//转化为易于理解的file对象
+      upload(this.url, file).then(url => {
+        successCallback(url)
+      })
+    },
+
+    // 文件处理
+    fileHandler(callback, value, meta) {
+      // Provide file and text for the link dialog
+      if (meta.filetype === 'file') {
+        callback(value, {text: '文字描述'})
+      }
+      // Provide image and alt text for the image dialog
+      else if (meta.filetype === 'image') {
+        callback(value, {alt: '文字描述'})
+      }
+      // Provide alternative source and posted for the media dialog
+      else if (meta.filetype === 'media') {
+        callback(value, {source2: 'alt.ogg', poster: 'image.jpg'})
+      }
+    },
+
+    // 初始化前执行
+    setup(editor) {
+      console.log('ID为: ' + editor.id + ' 的编辑器即将初始化.')
+      editor.on('FullscreenStateChanged', (e) => {
+        this.fullscreen = e.state
+      })
+    },
+
+    // 初始化结束后执行
+    init_instance_callback(editor) {
+      console.log('ID为: ' + editor.id + ' 的编辑器已初始化完成.')
+      this.$emit('input', editor.getContent());
+    },
+
+    // 销毁tinymce
+    destroyTinymce() {
+      const tinymce = tinymce.get(this.id)
+      if (this.fullscreen) {
+        tinymce.execCommand('mceFullScreen')
+      }
+
+      if (tinymce) {
+        tinymce.destroy()
+      }
+    },
+  }
+}
+</script>
+
+<style scoped>
+
+</style>

+ 7 - 0
src/components/Tinymce/plugins.js

@@ -0,0 +1,7 @@
+// Any plugins you want to use has to be imported
+// Detail plugins list see https://www.tinymce.com/docs/plugins/
+// Custom builds see https://www.tinymce.com/download/custom-builds/
+const plugins = 'anchor autolink advlist autosave charmap code emoticons fullscreen' // autoresize
+    + ' help hr image insertdatetime link lists  media pagebreak preview print ' // quickbars
+    + ' searchreplace table wordcount'
+export default plugins

+ 18 - 0
src/components/Tinymce/toolbar.js

@@ -0,0 +1,18 @@
+// Here is a list of the toolbar
+// Detail list see https://www.tinymce.com/docs/advanced/editor-control-identifiers/#toolbarcontrols
+
+const toolbar = [
+    'code undo redo restoredraft'
+    + ' | searchreplace cut copy paste'
+    + ' | forecolor backcolor'
+    + ' | bold italic underline strikethrough'
+    + ' | alignleft aligncenter alignright alignjustify outdent indent',
+    'styleselect formatselect fontselect fontsizeselect'
+    + ' | bullist numlist'
+    + ' | blockquote subscript superscript removeformat',
+    'link anchor table image media charmap emoticons insertdatetime'
+    + ' | hr pagebreak'
+    + ' | preview print fullscreen'
+]
+
+export default toolbar

+ 165 - 0
src/components/Upload/cropper.vue

@@ -0,0 +1,165 @@
+<template>
+  <el-dialog :visible.sync="dialogVisible" width="600px" class="cropper-dialog" center title="图片剪裁">
+    <div class="cropper-container">
+      <div class="cropper">
+        <vue-cropper ref="cropper"
+                     :img="cropperImg"
+                     :output-size="option.size"
+                     :output-type="option.outputType"
+                     :info="true"
+                     :full="option.full"
+                     :can-move="option.canMove"
+                     :can-move-box="option.canMoveBox"
+                     :fixed-box="option.fixedBox"
+                     :original="option.original"
+                     :auto-crop="option.autoCrop"
+                     :auto-crop-width="option.autoCropWidth"
+                     :auto-crop-height="option.autoCropHeight"
+                     :center-box="option.centerBox"
+                     :high="option.high"
+                     :info-true="option.infoTrue"
+                     @realTime="realTime"
+                     :enlarge="option.enlarge"
+                     :fixed="option.fixed"
+                     :fixed-number="option.fixedNumber"/>
+      </div>
+      <!-- 预览 -->
+      <div class="preview-container">
+        <div class="preview" :style="previews.div">
+          <el-image v-if="previews.url" :src="previews.url" :style="previews.img" fit="cover"></el-image>
+        </div>
+      </div>
+    </div>
+    <div slot="footer">
+      <el-button @click="onClose">取 消</el-button>
+      <el-button type="primary" @click="saveImg" style="margin-left: 50px;">确 定</el-button>
+    </div>
+  </el-dialog>
+</template>
+
+<script>
+import {VueCropper} from 'vue-cropper';
+
+export default {
+  name: 'Cropper',
+  components: {
+    VueCropper
+  },
+  props: {
+    visible: {
+      type: Boolean,
+      default: false
+    },
+    imgType: {
+      type: String,
+      default: 'blob'
+    },
+    cropperImg: {
+      type: String,
+      default: ''
+    },
+    options: {
+      type: Object,
+      default: ()=>{
+        return {}
+      }
+    }
+  },
+  computed: {
+    dialogVisible: {
+      get() {
+        return this.visible
+      },
+      set(val) {
+        this.$emit('update:visible', val)
+      }
+    },
+    option() {
+      return {...this.defaultOption, ...this.options};
+    }
+  },
+  data() {
+    return {
+      previews: {},
+      defaultOption: {
+        img: '', // 裁剪图片的地址
+        size: 1, // 裁剪生成图片的质量
+        full: false, // 是否输出原图比例的截图 默认false
+        outputType: 'png', // 裁剪生成图片的格式 默认jpg
+        canMove: false, // 上传图片是否可以移动
+        fixedBox: false, // 固定截图框大小 不允许改变
+        original: false, // 上传图片按照原始比例渲染
+        canMoveBox: true, // 截图框能否拖动
+        autoCrop: true, // 是否默认生成截图框
+        // 只有自动截图开启 宽度高度才生效
+        autoCropWidth: 200, // 默认生成截图框宽度
+        autoCropHeight: 200, // 默认生成截图框高度
+        centerBox: true, // 截图框是否被限制在图片里面
+        high: false, // 是否按照设备的dpr 输出等比例图片
+        enlarge: 1, // 图片根据截图框输出比例倍数
+        mode: 'contain', // 图片默认渲染方式
+        maxImgSize: 2000, // 限制图片最大宽度和高度
+        limitMinSize: [100, 100], // 更新裁剪框最小属性
+        infoTrue: false, // true 为展示真实输出图片宽高 false 展示看到的截图框宽高
+        fixed: true, // 是否开启截图框宽高固定比例  (默认:true)
+        fixedNumber: [1, 1] // 截图框的宽高比例
+      }
+    };
+  },
+  methods: {
+    // 裁剪时触发的方法,用于实时预览
+    realTime(data) {
+      this.previews = data;
+    },
+
+    // 取消关闭弹框
+    onClose() {
+      this.$set(this, "dialogVisible", false);
+    },
+
+    // 获取裁剪之后的图片,默认blob,也可以获取base64的图片
+    saveImg() {
+      if (this.imgType === 'blob') {
+        this.$refs.cropper.getCropBlob(blob => {
+          this.$emit('crop', blob);
+        });
+      } else {
+        this.$refs.cropper.getCropData(data => {
+          this.$emit('crop', data);
+        });
+      }
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+.cropper-dialog {
+  .cropper-container {
+    display: flex;
+    justify-content: space-between;
+
+    .cropper {
+      height: 240px;
+      width: 240px;
+    }
+
+    .preview-container {
+      width: 240px;
+      height: 240px;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      background: #ededed;
+
+      .preview {
+        overflow: hidden;
+      }
+
+      .el-button {
+        margin-top: 20px;
+      }
+    }
+  }
+}
+</style>

+ 492 - 0
src/components/Upload/index.vue

@@ -0,0 +1,492 @@
+<template>
+<div>
+  <el-upload
+      ref="fileUpload"
+      action=""
+      :show-file-list="multiple"
+      :auto-upload="false"
+      :accept="accept"
+      :http-request="fileUpload"
+      :before-upload="beforeFileUpload"
+      :multiple="multiple"
+      :limit="limit"
+      :on-exceed="multipleFileExceed"
+      :on-error="fileError"
+      :on-change="fileChange"
+      :on-success="fileSuccess"
+      :on-remove="multipleFileRemove"
+      :file-list="fileList"
+      :list-type="multiple?'picture-card':'text'">
+    <div v-if="!multiple && fileType === 'image' && file.url" class="image" :style="size">
+      <el-image :src="file.url" :style="size" :fit="fit" v-loading="loading"></el-image>
+      <div class="preview-hover" @click.stop="e=>e.stopPropagation()">
+        <el-link v-if="!disabled" class="link-button" icon="el-icon-download" @click="fileDownload(file)"></el-link>
+        <el-link v-if="!disabled" class="link-button" icon="el-icon-delete" @click="e=> onFileRemove(e)"></el-link>
+      </div>
+    </div>
+
+    <video v-else-if="!multiple && fileType === 'video' && file.url" :src="file.url" class="video" :style="size" controls="controls" v-loading="loading">
+      您的浏览器不支持视频播放
+    </video>
+    <audio v-else-if="!multiple && fileType === 'audio' && file.url" :src="file.url" class="audio" :style="size" controls="controls" ref="audio" v-loading="loading">
+      您的浏览器不支持音频播放
+    </audio>
+    <div v-else-if="!multiple && fileType === '*' && file.url" class="file border" :style="size">
+      <div class="filename" @click="fileDownload(file)">{{ file.name }}</div>
+      <div class="preview-hover" @click.stop="e=>e.stopPropagation()">
+        <el-link v-if="!disabled" class="link-button" icon="el-icon-download" @click="fileDownload(file)"></el-link>
+        <el-link v-if="!disabled" class="link-button" icon="el-icon-delete" @click="e=> onFileRemove(e)"></el-link>
+      </div>
+    </div>
+    <div v-else slot="default" class="image-upload-icon border" :style="size" ref="default">
+      <slot>
+        <i class="el-icon-plus"></i>
+        <div class="tips-text" v-html="tips"></div>
+      </slot>
+    </div>
+    <div slot="file" slot-scope="{file}" :ref="file.uid">
+      <template v-if="fileType === 'image'">
+        <el-image class="el-upload-list__item-thumbnail w100p h100p" :src="file.url"></el-image>
+        <span class="el-upload-list__item-actions">
+         <span class="el-upload-list__item-preview" style="line-height: 21px;">
+           <el-image class="el-upload-list__item-preview" style="line-height: 21px; display: inline;" :src="require('./zoom-in.png')" :preview-src-list="fileList.map(item => item.url)"></el-image>
+         </span>
+
+        <span v-if="!disabled" class="el-upload-list__item-delete" @click="fileDownload(file)">
+          <i class="el-icon-download"></i>
+        </span>
+        <span v-if="!disabled" class="el-upload-list__item-delete" @click="multipleFileRemove(file, fileList)">
+          <i class="el-icon-delete"></i>
+        </span>
+      </span>
+      </template>
+      <template v-else-if="fileType === '*'">
+        <div class="file" :style="size">
+          <div class="filename">{{ file.name }}</div>
+        </div>
+        <div class="el-upload-list__item-actions">
+          <span v-if="!disabled" class="el-upload-list__item-delete" @click="fileDownload(file)">
+            <i class="el-icon-download"></i>
+          </span>
+          <span v-if="!disabled" class="el-upload-list__item-delete" @click="multipleFileRemove(file, fileList)">
+            <i class="el-icon-delete"></i>
+          </span>
+        </div>
+      </template>
+    </div>
+  </el-upload>
+  <cropper
+      v-if="cropperDialogVisible"
+      :visible.sync="cropperDialogVisible"
+      :cropper-img="cropperImage"
+      :options="options"
+      @crop="onCrop"
+  />
+</div>
+</template>
+
+<script>
+import Cropper from "./cropper.vue";
+import {checkImage} from "@/utils/validate";
+import {compressFile} from "@/utils/fileUtil";
+import {downloadImage, OSSFileUpload, upload} from "@/utils/httpUtil";
+
+export default {
+  name: "Upload",
+  components: {
+    Cropper,
+  },
+  model: {
+    prop: 'src',
+    event: 'change',
+  },
+  props: {
+    accept: { // 文件类型
+      type: String,
+      require: true
+    },
+    url: {
+      type: String,
+      default: '/v1/file/upload'
+    },
+    stsUrl: {
+      type: String,
+      default: '/v1/file/sts'
+    },
+    sts: Boolean,
+    src: { // 图片地址
+      type: [String, Object, Array],
+      require: true
+    },
+    tips: { // 提示文字
+      type: String,
+      default: ''
+    },
+    width: { // 图片宽度
+      type: Number,
+      default: 160
+    },
+    height: { // 图片高度
+      type: Number,
+      default: 106
+    },
+    fit: { // 图片填充方式
+      type: String,
+      default: 'cover'
+    },
+    multiple: Boolean, // 多文件上传
+
+    limit: { // 多文件上传,最大文件数量
+      type: Number,
+      default: 5
+    },
+    cropper: { // 是否裁剪
+      type: Boolean,
+      default: false
+    },
+    options: { // 裁剪选项
+      type: Object,
+      default: ()=>{
+        return {}
+      }
+    }
+  },
+
+  computed: {
+    size() { // 尺寸大小
+      return {
+        width: this.width + "px",
+        height: this.height + "px",
+      }
+    }
+  },
+
+  watch: {
+    accept: {
+      handler: function (value) {
+        this.fileType = value?.split('/')[0] || "*";
+      },
+      immediate: true
+    },
+    src: {
+      handler() {
+        if (this.multiple) {
+          if (this.src && Array.isArray(this.src)) {
+            this.fileList = this.src;
+            this.$nextTick(() => {
+              this.src.forEach(file => {
+                this.$refs[file.uid].parentElement.style.transition = "none";
+                this.$refs[file.uid].parentElement.style.width = this.width + 'px';
+                this.$refs[file.uid].parentElement.style.height = this.height + 'px';
+                this.$refs[file.uid].parentElement.style.display = "inline-block";
+              })
+              this.$refs["default"].parentElement.style.border = 'none';
+              this.$refs["default"].parentElement.style.width = this.width + 'px';
+              this.$refs["default"].parentElement.style.height = this.height + 'px';
+            })
+          } else {
+            this.fileList = [];
+          }
+        } else {
+          if (typeof this.src === "string") {
+            this.file.url = this.src;
+            this.file.name = this.src.substring(this.src.lastIndexOf('/') + 1)
+          } else if (this.src instanceof Object) {
+            this.file.url = this.src.url;
+            this.file.name = this.src.name;
+          }
+        }
+      },
+      immediate: true
+    }
+  },
+
+  data() {
+    return {
+      file: { // 文件
+        url: undefined,
+        name: undefined
+      },
+      fileType: undefined, // 文件类型
+      fileList: [], // 文件列表,多文件时使用
+      loading: false, // 加载中
+
+      disabled: false,
+      dialogImageUrl: '',
+      dialogVisible: false,
+
+      cropperDialogVisible: false, // 是否显示裁剪框
+      cropperFile: "", // 需要裁剪的原始文件
+      cropperImage: "", // 需要裁剪的图片
+      uploadFile: "", // 裁剪后的文件
+    }
+  },
+
+  mounted() {
+  },
+  methods: {
+    // 多文件图片预览
+    handlePictureCardPreview(file) {
+      this.dialogImageUrl = file.url;
+      this.dialogVisible = true;
+    },
+
+    // 多文件文件下载(每次一个)
+    fileDownload(file) {
+      downloadImage(file.url, file.name);
+    },
+
+    // 图片上传前检查
+    beforeFileUpload(file) {
+      this.fileType = this.fileType === "*" ? "*" : file.type.split("/")[0]; // fileType="*"时显示为文件
+      return this.fileType === "image" ? checkImage(file) : true;
+    },
+
+    // 多文件上传时超出限制
+    multipleFileExceed(files, fileList) {
+      this.$message.warning(`当前限制选择 ${this.limit} 个文件,本次选择了 ${files.length} 个文件,共选择了 ${files.length + fileList.length} 个文件`);
+    },
+
+    // 多文件上传时出错
+    fileError(err, file, fileList) {
+      console.log("fileError", err, file, fileList);
+    },
+
+    // 多文件上传成功(多次调用)
+    fileSuccess(response, file, fileList) {
+      if (this.multiple) {
+        const files = fileList.map(f => {
+          return {
+            url: f.uid === file.uid ? file.response : f.url,
+            name: f.name,
+            status: f.status,
+            uid: f.uid,
+          };
+        })
+        this.$emit("change", files); // 发送Url改变事件
+      } else if (this.src instanceof Object) {
+        this.$emit("change", {url: file.response, name: file.name}); // 发送Url改变事件
+      } else {
+        this.$emit("change", file.response); // 发送Url改变事件
+      }
+      this.$parent.$emit("el.form.change"); // 自定义组件的校验触发器
+    },
+
+    // 多文件上传删除
+    multipleFileRemove(file, fileList) {
+      const files = fileList.filter(item => item.uid !== file.uid);
+      this.$emit("change", files); // 发送Url改变事件
+      this.$parent.$emit("el.form.change"); // 自定义组件的校验触发器
+    },
+
+    // 文件上传
+    fileUpload(request) {
+      this.$nextTick(() => {
+        if (this.$refs["preview"]) {
+          this.$refs["preview"].parentElement.style.transition = "none";
+          this.$refs["preview"].parentElement.style.width = this.width + 'px';
+          this.$refs["preview"].parentElement.style.height = this.height + 'px';
+          this.$refs["preview"].parentElement.style.display = "inline-block";
+        }
+      })
+      return new Promise(async (resolve, reject) => {
+        try {
+          this.loading = true;
+
+          if (!this.multiple) {
+            if (this.src instanceof Object) {
+              this.$emit("change", {url: URL.createObjectURL(request.file), name: request.file.name});
+            } else {
+              this.$emit("change", URL.createObjectURL(request.file)); // 单文件,更新本地Url
+            }
+          }
+
+          // 图片压缩
+          const file = this.fileType === "image" ? await compressFile(request.file) : request.file;
+
+          // 文件上传
+          const url = this.sts ? await OSSFileUpload(this.stsUrl, file) : await upload(this.url, file);
+
+          if (!this.multiple) {
+            // 获取音频时长
+            if (this.fileType === "audio") {
+              const audio = this.$refs.audio;
+              audio.load(); // 因为source标签不能直接更改路径,所以整个audio标签必须重新加载一次
+              audio.oncanplay = () => {
+                this.$emit("duration", audio.duration);
+              }
+            }
+          }
+          resolve(url);
+        } finally {
+          this.loading = false;
+        }
+      });
+    },
+
+    // 当文件移除
+    onFileRemove(e) {
+      e.stopPropagation();
+
+      // 单文件,发送Url改变事件
+      this.$emit("change", this.src instanceof Object ? {} : "");
+      // 自定义组件的校验触发器
+      this.$parent.$emit("el.form.change");
+    },
+
+
+    // 文件改变
+    fileChange(file) {
+      if (file.status === "ready") {
+        if (this.cropper) {
+          this.cropperFile = file.raw;
+          this.cropperImage = URL.createObjectURL(file.raw);
+          this.cropperDialogVisible = true;
+        } else {
+          this.$refs.fileUpload.submit();
+        }
+      }
+    },
+
+    // 图片裁剪回调
+    onCrop(blob) {
+      this.cropperDialogVisible = false;
+      const rawFile = new File([blob], this.cropperFile.name, {type: this.cropperFile.type});
+      rawFile.uid = this.cropperFile.uid;
+      let file = {
+        status: 'ready',
+        name: rawFile.name,
+        size: rawFile.size,
+        percentage: 0,
+        uid: rawFile.uid,
+        raw: rawFile
+      };
+      this.fileList = [file];
+      this.$nextTick(() => {
+        this.$refs.fileUpload.submit();
+      })
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+@import "./src/styles/variables";
+
+// 关闭动画
+::v-deep .el-list-enter-active, .el-list-leave-active {
+  transition: none !important;
+}
+
+::v-deep .el-list-enter, .el-list-leave {
+  transition: none !important;
+}
+
+::v-deep .el-list-enter-to, .el-list-leave-to {
+  transition: none !important;
+}
+
+::v-deep .el-list-move {
+  transition: none !important;
+}
+
+// 默认隐藏,js修改尺寸后显示
+::v-deep .el-upload-list--picture-card .el-upload-list__item {
+  display: none;
+}
+
+::v-deep .el-upload--picture-card {
+  border: none;
+  width: auto;
+  height: auto;
+}
+
+.image, .video, .audio {
+  display: block;
+  cursor: pointer;
+  background-color: white;
+  position: relative;
+
+  ::v-deep .el-upload, .el-image {
+    height: 100%;
+  }
+
+  &:hover {
+    .preview-hover {
+      display: flex;
+    }
+  }
+}
+
+.border {
+  border: 1px dashed $--color-primary;
+  border-radius: 6px;
+  color: $--color-primary;
+}
+
+.file {
+  background-color: white;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 0 10px;
+  height: 100%;
+  color: $--color-primary;
+  position: relative;
+
+  .filename {
+    //width: 100%;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+  }
+
+  &:hover {
+    .preview-hover {
+      display: flex;
+    }
+  }
+}
+
+.preview-hover {
+  display: none;
+  align-items: center;
+  justify-content: center;
+  background: #0000007f;
+  position: absolute;
+  left: 0;
+  top: 0;
+  width: 100%;
+  height: 100%;
+  font-size: 16px;
+}
+
+.image-upload-icon {
+  font-size: 28px;
+  line-height: 50px;
+  text-align: center;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+
+  .tips-text {
+    width: 100%;
+    font-size: 14px;
+    font-family: Microsoft YaHei;
+    font-weight: 400;
+    line-height: 24px;
+  }
+
+  &:hover {
+    border-color: $--color-primary-light-7;
+    background-color: $--color-primary-light-9;
+  }
+}
+
+.link-button {
+  font-size: 20px;
+  margin: 0 12px;
+  color: white;
+}
+</style>

二進制
src/components/Upload/zoom-in.png


+ 143 - 0
src/components/UploadExcel/index.vue

@@ -0,0 +1,143 @@
+<template>
+  <div>
+    <input ref="excel-upload-input" class="excel-upload-input" type="file" accept=".xlsx, .xls" @change="handleClick">
+    <div class="drop" @drop="handleDrop" @dragover="handleDragover" @dragenter="handleDragover">
+      Drop excel file here or
+      <el-button :loading="loading" style="margin-left:16px;" size="mini" type="primary" @click="handleUpload">
+        Browse
+      </el-button>
+    </div>
+  </div>
+</template>
+
+<script>
+import XLSX from 'xlsx'
+
+export default {
+  props: {
+    startRow: {
+      type: Number,
+      default: 1
+    },
+    beforeUpload: Function, // eslint-disable-line
+    onSuccess: Function// eslint-disable-line
+  },
+  data() {
+    return {
+      loading: false,
+      excelData: {
+        header: null,
+        results: null
+      }
+    }
+  },
+  methods: {
+    generateData({header, results}) {
+      this.excelData.header = header
+      this.excelData.results = results
+      this.onSuccess && this.onSuccess(this.excelData)
+    },
+    handleDrop(e) {
+      e.stopPropagation()
+      e.preventDefault()
+      if (this.loading) return
+      const files = e.dataTransfer.files
+      if (files.length !== 1) {
+        this.$message.error('Only support uploading one file!')
+        return
+      }
+      const rawFile = files[0] // only use files[0]
+
+      if (!this.isExcel(rawFile)) {
+        this.$message.error('Only supports upload .xlsx, .xls, .csv suffix files')
+        return false
+      }
+      this.upload(rawFile)
+      e.stopPropagation()
+      e.preventDefault()
+    },
+    handleDragover(e) {
+      e.stopPropagation()
+      e.preventDefault()
+      e.dataTransfer.dropEffect = 'copy'
+    },
+    handleUpload() {
+      this.$refs['excel-upload-input'].click()
+    },
+    handleClick(e) {
+      const files = e.target.files
+      const rawFile = files[0] // only use files[0]
+      if (!rawFile) return
+      this.upload(rawFile)
+    },
+    upload(rawFile) {
+      this.$refs['excel-upload-input'].value = null // fix can't select the same excel
+
+      if (!this.beforeUpload) {
+        this.readerData(rawFile)
+        return
+      }
+      const before = this.beforeUpload(rawFile)
+      if (before) {
+        this.readerData(rawFile)
+      }
+    },
+    readerData(rawFile) {
+      this.loading = true
+      return new Promise((resolve, reject) => {
+        const reader = new FileReader()
+        reader.onload = e => {
+          const data = e.target.result
+          const workbook = XLSX.read(data, {type: 'array'})
+          const firstSheetName = workbook.SheetNames[0]
+          const worksheet = workbook.Sheets[firstSheetName]
+          const header = this.getHeaderRow(worksheet)
+          const results = XLSX.utils.sheet_to_json(worksheet, {range: this.startRow})
+          this.generateData({header, results})
+          this.loading = false
+          resolve()
+        }
+        reader.readAsArrayBuffer(rawFile)
+      })
+    },
+    getHeaderRow(sheet) {
+      const headers = []
+      const range = XLSX.utils.decode_range(sheet['!ref'])
+      let C
+      const R = range.s.r + this.startRow;
+      /* start in the first row */
+      for (C = range.s.c; C <= range.e.c; ++C) { /* walk every column in the range */
+        const cell = sheet[XLSX.utils.encode_cell({c: C, r: R})]
+        /* find the cell in the first row */
+        let hdr = 'UNKNOWN ' + C // <-- replace with your desired default
+        if (cell && cell.t) hdr = XLSX.utils.format_cell(cell)
+        headers.push(hdr)
+      }
+      return headers
+    },
+    isExcel(file) {
+      return /\.(xlsx|xls|csv)$/.test(file.name)
+    }
+  }
+}
+</script>
+
+<style scoped>
+.excel-upload-input {
+  display: none;
+  z-index: -9999;
+}
+
+.drop {
+  border: 2px dashed #bbb;
+  width: 600px;
+  height: 160px;
+  line-height: 160px;
+  margin: 0 auto;
+  font-size: 24px;
+  border-radius: 5px;
+  text-align: center;
+  color: #bbb;
+  position: relative;
+}
+</style>

+ 77 - 0
src/config/app.js

@@ -0,0 +1,77 @@
+export default {
+    /**
+     * 应用名称
+     */
+    appName: "app",
+
+    /**
+     * 应用默认主题色
+     */
+    defaultThemeColor: "#409EFF",
+
+    /**
+     * @description api请求基础路径
+     */
+    baseUrl: {
+        dev: '/app',
+        pro: 'http://127.0.0.1:8080'
+    },
+
+    /**
+     * 网络响应信息配置
+     */
+    responseKey: {
+        data: "data",
+        message: "message",
+        code: "code",
+        successCode: 200,
+        authFailCode: 401,
+        permissionsExpiredCode: 498,
+        permissionsUrl: '/v1/home/permissions',
+    },
+
+    /**
+     * 授权信息关键字
+     */
+    tokenKey: 'Authorization',
+
+    /**
+     * 首页侧边栏显示的title
+     */
+    title: '赣西云数据中心',
+
+    /**
+     * 登录页面显示的title
+     */
+    loginTitle: '赣西云数据中心运维平台',
+
+    /**
+     * @type {boolean} true | false
+     * @description header是否固定
+     */
+    fixedHeader: true,
+
+    /**
+     * @type {boolean} true | false
+     * @description 侧边栏是否显示logo及标题
+     */
+    sidebarLogo: true,
+
+    /**
+     * @type {boolean} true | false
+     * @description 过滤器是折叠显示
+     */
+    filterShrink: false,
+
+    /**
+     * @type {boolean} true | false
+     * @description 是否启用参数缓存
+     */
+    paramCache: false,
+
+    /**
+     * @type {boolean} true | false
+     * @description 是否启用编辑对话框关闭前确认
+     */
+    closeConfirmEnable: false,
+}

+ 14 - 0
src/config/index.js

@@ -0,0 +1,14 @@
+import {storage} from "@/utils/storage";
+
+
+export const appList = [
+    {name: "APP", config: require('./app').default},
+    {name: "巡检", config: require('./patrol').default},
+];
+
+export const allowChange = process.env.NODE_ENV !== 'production';
+export const defaultAppName = "巡检";
+
+let appName = storage.getString("appName", defaultAppName);
+storage.setUserInfoKey(appName);
+export default appList.find(item => item.name === appName).config;

+ 77 - 0
src/config/patrol.js

@@ -0,0 +1,77 @@
+export default {
+    /**
+     * 应用名称
+     */
+    appName: "patrol",
+
+    /**
+     * 应用默认主题色
+     */
+    defaultThemeColor: "#409EFF",
+
+    /**
+     * @description api请求基础路径
+     */
+    baseUrl: {
+        dev: '/request_patrol',
+        pro: 'https://www.jxydyw.cn/patrol-web'
+    },
+
+    /**
+     * 网络响应信息配置
+     */
+    responseKey: {
+        data: "data",
+        message: "message",
+        code: "code",
+        successCode: 200,
+        authFailCode: 401,
+        permissionsExpiredCode: 498,
+        permissionsUrl: '/v1/home/permissions',
+    },
+
+    /**
+     * 授权信息关键字
+     */
+    tokenKey: 'Authorization',
+
+    /**
+     * 首页侧边栏显示的title
+     */
+    title: '赣西云数据中心',
+
+    /**
+     * 登录页面显示的title
+     */
+    loginTitle: '赣西云数据中心运维平台',
+
+    /**
+     * @type {boolean} true | false
+     * @description header是否固定
+     */
+    fixedHeader: true,
+
+    /**
+     * @type {boolean} true | false
+     * @description 侧边栏是否显示logo及标题
+     */
+    sidebarLogo: true,
+
+    /**
+     * @type {boolean} true | false
+     * @description 过滤器是折叠显示
+     */
+    filterShrink: false,
+
+    /**
+     * @type {boolean} true | false
+     * @description 是否启用参数缓存
+     */
+    paramCache: false,
+
+    /**
+     * @type {boolean} true | false
+     * @description 是否启用编辑对话框关闭前确认
+     */
+    closeConfirmEnable: false,
+}

+ 100 - 0
src/filters/filters.js

@@ -0,0 +1,100 @@
+import * as dateUtil from '@/utils/dateUtil'
+
+/**
+ * 时间格式化
+ * @param {Number|String} date 传入的时间
+ * @param {String} fmt 格式化表达式
+ * @param {String} defaultValue 为空时显示内容
+ **/
+export const dateFormat = (date, fmt = "yyyy-MM-dd HH:mm:ss", defaultValue = '/') => {
+    return dateUtil.dateFormat(fmt, date, defaultValue);
+}
+
+// getDay() 获取星期
+// new Date(timeStamp); 通过时间戳创建日期
+// new Date(year, month, date, hours, minutes, second); 通过年月日时分秒创建日期   2020,0,32 => 为2020年2月1日
+
+// 获取00:00格式的时间
+export const audioTime = (timestamp) => {
+    let second = Math.floor(timestamp % 60);
+    let minute = Math.floor(timestamp / 60);
+    if (second < 10) {
+        second = "0" + second;
+    }
+    if (minute < 10) {
+        minute = "0" + minute;
+    }
+    return minute + ":" + second;
+}
+
+export const parseGender = (gender) => {
+    return gender === 1 ? "男" : gender === 2 ? "女" : "未知";
+}
+
+export const parseBoolean = (value) => {
+    return value ? "是" : "否";
+}
+
+/**
+ * Number formatting
+ * like 10000 => 10k
+ * @param {number} num
+ * @param {number} digits
+ */
+export function numberFormatter(num, digits) {
+    const si = [
+        {value: 1E18, symbol: 'E'},
+        {value: 1E15, symbol: 'P'},
+        {value: 1E12, symbol: 'T'},
+        {value: 1E9, symbol: 'G'},
+        {value: 1E6, symbol: 'M'},
+        {value: 1E3, symbol: 'k'}
+    ]
+    for (let i = 0; i < si.length; i++) {
+        if (num >= si[i].value) {
+            return (num / si[i].value).toFixed(digits).replace(/\.0+$|(\.[0-9]*[1-9])0+$/, '$1') + si[i].symbol
+        }
+    }
+    return num.toString()
+}
+
+/**
+ * 10000 => "10,000"
+ * @param {number} num
+ */
+export function toThousandFilter(num) {
+    return (+num || 0).toString().replace(/^-?\d+/g, m => m.replace(/(?=(?!\b)(\d{3})+$)/g, ','))
+}
+
+/**
+ * Upper case first char
+ * @param {String} string
+ */
+export function uppercaseFirst(string) {
+    return string.charAt(0).toUpperCase() + string.slice(1)
+}
+
+
+/**
+ * filters 使用
+ *   <!-- 在双花括号中 -->
+ *   {{ message | filter }}
+ *
+ *   <!-- 在 `v-bind` 中 -->
+ *   <div v-bind:id="rawId | filter"></div>
+ *
+ *   <!-- 过滤器可以串联  -->
+ *   {{ message | filterA | filterB }}
+ *
+ *   <!-- 过滤器传参  -->
+ *   {{ message | filterA('arg1', arg2) }}
+ */
+
+// 组件内部定义局部过滤器
+// filters: {
+//     capitalize: function (value) {
+//         if (!value) return ''
+//         value = value.toString()
+//         return value.charAt(0).toUpperCase() + value.slice(1)
+//     }
+// }

+ 15 - 0
src/icons/config.js

@@ -0,0 +1,15 @@
+module.exports = {
+    multipass: true, // boolean. false by default
+    plugins: [
+        // set of built-in plugins enabled by default
+        'preset-default',
+
+        // or by expanded notation which allows to configure plugin
+        {
+            name: 'removeAttrs',
+            params: {
+                attrs: "(fill|stroke)"
+            }
+        },
+    ],
+};

+ 9 - 0
src/icons/index.js

@@ -0,0 +1,9 @@
+import Vue from 'vue'
+import SvgIcon from '@/components/SvgIcon'// svg component
+
+// register globally
+Vue.component('svg-icon', SvgIcon);
+
+const req = require.context('./svg', true, /\.svg$/);
+const requireAll = requireContext => requireContext.keys().map(requireContext);
+requireAll(req);

File diff suppressed because it is too large
+ 1 - 0
src/icons/svg/clear.svg


File diff suppressed because it is too large
+ 1 - 0
src/icons/svg/eye-open.svg


File diff suppressed because it is too large
+ 1 - 0
src/icons/svg/eye.svg


File diff suppressed because it is too large
+ 1 - 0
src/icons/svg/password.svg


File diff suppressed because it is too large
+ 1 - 0
src/icons/svg/patrol/PUE管理.svg


File diff suppressed because it is too large
+ 1 - 0
src/icons/svg/patrol/临时任务.svg


File diff suppressed because it is too large
+ 1 - 0
src/icons/svg/patrol/任务统计.svg


File diff suppressed because it is too large
+ 1 - 0
src/icons/svg/patrol/保养任务.svg


File diff suppressed because it is too large
+ 1 - 0
src/icons/svg/patrol/保养计划.svg


File diff suppressed because it is too large
+ 1 - 0
src/icons/svg/patrol/全部任务.svg


File diff suppressed because it is too large
+ 1 - 0
src/icons/svg/patrol/出入库管理.svg


File diff suppressed because it is too large
+ 1 - 0
src/icons/svg/patrol/出入库记录.svg


File diff suppressed because it is too large
+ 1 - 0
src/icons/svg/patrol/制度管理.svg


File diff suppressed because it is too large
+ 1 - 0
src/icons/svg/patrol/备案管理.svg


File diff suppressed because it is too large
+ 1 - 0
src/icons/svg/patrol/巡检任务.svg


File diff suppressed because it is too large
+ 1 - 0
src/icons/svg/patrol/巡检任务记录.svg


File diff suppressed because it is too large
+ 1 - 0
src/icons/svg/patrol/巡检计划.svg


File diff suppressed because it is too large
+ 1 - 0
src/icons/svg/patrol/房间管理.svg


File diff suppressed because it is too large
+ 1 - 0
src/icons/svg/patrol/拍照项.svg


File diff suppressed because it is too large
+ 1 - 0
src/icons/svg/patrol/接口文档.svg


File diff suppressed because it is too large
+ 1 - 0
src/icons/svg/patrol/操作日志管理.svg


File diff suppressed because it is too large
+ 1 - 0
src/icons/svg/patrol/数字项.svg


+ 1 - 0
src/icons/svg/patrol/数据备份.svg

@@ -0,0 +1 @@
+<svg class="icon" style="width:1em;height:1em;vertical-align:middle;fill:currentColor;overflow:hidden" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M512 384c-229.8 0-416-57.3-416-128v256c0 70.7 186.2 128 416 128s416-57.3 416-128V256c0 70.7-186.2 128-416 128z"/><path d="M512 704c-229.8 0-416-57.3-416-128v256c0 70.7 186.2 128 416 128s416-57.3 416-128V576c0 70.7-186.2 128-416 128zm0-384c229.8 0 416-57.3 416-128S741.8 64 512 64 96 121.3 96 192s186.2 128 416 128z"/></svg>

File diff suppressed because it is too large
+ 1 - 0
src/icons/svg/patrol/文本项.svg


File diff suppressed because it is too large
+ 1 - 0
src/icons/svg/patrol/权限管理.svg


File diff suppressed because it is too large
+ 1 - 0
src/icons/svg/patrol/检查情况.svg


+ 0 - 0
src/icons/svg/patrol/检查统计.svg


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