yanj пре 3 месеци
комит
72f5dcd49c
100 измењених фајлова са 12337 додато и 0 уклоњено
  1. 14 0
      .editorconfig
  2. 5 0
      .env
  3. 5 0
      .env.development
  4. 5 0
      .env.test
  5. 8 0
      .eslintignore
  6. 198 0
      .eslintrc.js
  7. 23 0
      .gitignore
  8. 5 0
      .travis.yml
  9. 21 0
      LICENSE
  10. 36 0
      README.en.md
  11. 222 0
      README.es.md
  12. 218 0
      README.ja.md
  13. 222 0
      README.md
  14. 245 0
      README.zh-CN.md
  15. 14 0
      babel.config.js
  16. 35 0
      build/index.js
  17. 1369 0
      develop.sql
  18. 24 0
      jest.config.js
  19. 9 0
      jsconfig.json
  20. 80 0
      mock/app.js
  21. 116 0
      mock/article.js
  22. 62 0
      mock/index.js
  23. 81 0
      mock/mock-server.js
  24. 51 0
      mock/remote-search.js
  25. 51 0
      mock/role.js
  26. 98 0
      mock/role/index.js
  27. 530 0
      mock/role/routes.js
  28. 865 0
      mock/user.js
  29. 48 0
      mock/utils.js
  30. 114 0
      package.json
  31. 9 0
      plopfile.js
  32. 18 0
      postcss.config.js
  33. BIN
      public/favicon.ico
  34. 16 0
      public/index.html
  35. 4 0
      public/static/js/jquery-1.11.1.min.js
  36. 508 0
      public/static/js/require.js
  37. 63 0
      src/App.vue
  38. 4 0
      src/aiHelper/config.js
  39. 846 0
      src/aiHelper/index.vue
  40. 48 0
      src/aiHelper/plugin/blogPlugin/index.vue
  41. 20 0
      src/aiHelper/plugin/blogPlugin/prompt.md
  42. 9 0
      src/aiHelper/plugin/getHtml/prompt.md
  43. 38 0
      src/aiHelper/plugin/getHtml/tool.vue
  44. 47 0
      src/aiHelper/plugin/meetingPlugin/index.vue
  45. 20 0
      src/aiHelper/plugin/meetingPlugin/prompt.md
  46. 47 0
      src/aiHelper/plugin/productPlugin/index.vue
  47. 20 0
      src/aiHelper/plugin/productPlugin/prompt.md
  48. 103 0
      src/aiHelper/plugin/seoPlugin/comp.vue
  49. 96 0
      src/aiHelper/plugin/seoPlugin/components/editColumn.vue
  50. 758 0
      src/aiHelper/plugin/seoPlugin/page.vue
  51. 17 0
      src/aiHelper/plugin/seoPlugin/prompt.md
  52. 949 0
      src/aiHelper/plugin/seoPlugin/setting.vue
  53. 158 0
      src/aiHelper/plugin/seoPlugin/task.vue
  54. 31 0
      src/aiHelper/plugin/seoPlugin/tool.vue
  55. 42 0
      src/aiHelper/plugin/staticPagePlugin/index.vue
  56. 20 0
      src/aiHelper/plugin/staticPagePlugin/prompt.md
  57. 103 0
      src/aiHelper/plugin/translatePlugin/comp.vue
  58. 658 0
      src/aiHelper/plugin/translatePlugin/page.vue
  59. 5 0
      src/aiHelper/plugin/translatePlugin/prompt.md
  60. 103 0
      src/aiHelper/plugin/translatePlugin/task.vue
  61. 159 0
      src/aiHelper/plugin/writePlugin/comp.vue
  62. 559 0
      src/aiHelper/plugin/writePlugin/page.vue
  63. 38 0
      src/aiHelper/plugin/writePlugin/prompt.md
  64. 565 0
      src/aiHelper/plugin/writePlugin/setting.vue
  65. 160 0
      src/aiHelper/plugin/writePlugin/task.vue
  66. 31 0
      src/aiHelper/plugin/writePlugin/tool.vue
  67. 326 0
      src/aiHelper/readme.md
  68. BIN
      src/aiHelper/readme/Clip_2025-08-05_15-02-07.png
  69. BIN
      src/aiHelper/readme/Clip_2025-08-05_16-14-13.png
  70. BIN
      src/aiHelper/readme/Clip_2025-08-05_16-16-28.png
  71. BIN
      src/aiHelper/readme/Clip_2025-08-05_16-18-46.png
  72. BIN
      src/aiHelper/readme/Clip_2025-08-05_16-37-59.png
  73. BIN
      src/aiHelper/readme/Clip_2025-08-05_16-39-02.png
  74. BIN
      src/aiHelper/readme/Clip_2025-08-05_16-47-46.png
  75. BIN
      src/aiHelper/readme/Clip_2025-08-05_16-59-20.png
  76. BIN
      src/aiHelper/readme/Clip_2025-08-05_17-05-28.png
  77. BIN
      src/aiHelper/readme/Clip_2025-08-05_17-18-49.png
  78. BIN
      src/aiHelper/readme/Clip_2025-08-05_17-29-41.png
  79. BIN
      src/aiHelper/readme/Clip_2025-08-05_17-30-02.png
  80. BIN
      src/aiHelper/readme/plugin.png
  81. BIN
      src/aiHelper/readme/recording.gif
  82. 4 0
      src/aiHelper/system/plugin/getCurrentTime/prompt.md
  83. 30 0
      src/aiHelper/system/plugin/getCurrentTime/tool.vue
  84. 73 0
      src/aiHelper/system/plugin/gotoUrl/index.vue
  85. 4 0
      src/aiHelper/system/plugin/gotoUrl/prompt.md
  86. 50 0
      src/aiHelper/system/plugin/initFile/index.vue
  87. 4 0
      src/aiHelper/system/plugin/initFile/prompt.md
  88. 60 0
      src/aiHelper/system/plugin/initInput/index.vue
  89. 4 0
      src/aiHelper/system/plugin/initInput/prompt.md
  90. 47 0
      src/aiHelper/system/plugin/initSelect/index.vue
  91. 5 0
      src/aiHelper/system/plugin/initSelect/prompt.md
  92. 29 0
      src/aiHelper/system/plugin/mainPage/index.vue
  93. 472 0
      src/aiHelper/system/plugin/modelSetting/model.vue
  94. 7 0
      src/aiHelper/system/plugin/modelSetting/prompt.md
  95. 13 0
      src/aiHelper/system/prompt.md
  96. 163 0
      src/api/user.js
  97. BIN
      src/assets/401_images/401.gif
  98. BIN
      src/assets/404_images/404.png
  99. BIN
      src/assets/404_images/404_cloud.png
  100. 0 0
      src/assets/image404.jpg

+ 14 - 0
.editorconfig

@@ -0,0 +1,14 @@
+# https://editorconfig.org
+root = true
+
+[*]
+charset = utf-8
+indent_style = space
+indent_size = 2
+end_of_line = lf
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+[*.md]
+insert_final_newline = false
+trim_trailing_whitespace = false

+ 5 - 0
.env

@@ -0,0 +1,5 @@
+# just a flag
+ENV = 'development'
+VUE_APP_NAME = 'development'
+# base api
+VUE_APP_BASE_API = ''

+ 5 - 0
.env.development

@@ -0,0 +1,5 @@
+# just a flag
+ENV = 'development'
+VUE_APP_NAME = 'development'
+# base api
+VUE_APP_BASE_API = ''

+ 5 - 0
.env.test

@@ -0,0 +1,5 @@
+# just a flag
+ENV = 'development'
+VUE_APP_NAME = 'development'
+# base api
+VUE_APP_BASE_API = ''

+ 8 - 0
.eslintignore

@@ -0,0 +1,8 @@
+build/*.js
+src/assets
+src/assets
+src/api
+src/*.js
+src/components
+public
+dist

+ 198 - 0
.eslintrc.js

@@ -0,0 +1,198 @@
+module.exports = {
+  root: true,
+  parserOptions: {
+    parser: 'babel-eslint',
+    sourceType: 'module'
+  },
+  env: {
+    browser: true,
+    node: true,
+    es6: true,
+  },
+  extends: ['plugin:vue/recommended', 'eslint:recommended'],
+
+  // add your custom rules here
+  //it is base on https://github.com/vuejs/eslint-config-vue
+  rules: {
+    "vue/max-attributes-per-line": [2, {
+      "singleline": 10,
+      "multiline": {
+        "max": 1,
+        "allowFirstLine": false
+      }
+    }],
+    "vue/singleline-html-element-content-newline": "off",
+    "vue/multiline-html-element-content-newline":"off",
+    "vue/name-property-casing": ["error", "PascalCase"],
+    "vue/no-v-html": "off",
+    'accessor-pairs': 2,
+    'arrow-spacing': [2, {
+      'before': true,
+      'after': true
+    }],
+    'block-spacing': [2, 'always'],
+    'brace-style': [2, '1tbs', {
+      'allowSingleLine': true
+    }],
+    'camelcase': [0, {
+      'properties': 'always'
+    }],
+    'comma-dangle': [2, 'never'],
+    'comma-spacing': [2, {
+      'before': false,
+      'after': true
+    }],
+    'comma-style': [2, 'last'],
+    'constructor-super': 2,
+    'curly': [2, 'multi-line'],
+    'dot-location': [2, 'property'],
+    'eol-last': 2,
+    'eqeqeq': ["error", "always", {"null": "ignore"}],
+    'generator-star-spacing': [2, {
+      'before': true,
+      'after': true
+    }],
+    'handle-callback-err': [2, '^(err|error)$'],
+    'indent': [2, 2, {
+      'SwitchCase': 1
+    }],
+    'jsx-quotes': [2, 'prefer-single'],
+    'key-spacing': [2, {
+      'beforeColon': false,
+      'afterColon': true
+    }],
+    'keyword-spacing': [2, {
+      'before': true,
+      'after': true
+    }],
+    'new-cap': [2, {
+      'newIsCap': true,
+      'capIsNew': false
+    }],
+    'new-parens': 2,
+    'no-array-constructor': 2,
+    'no-caller': 2,
+    'no-console': 'off',
+    'no-class-assign': 2,
+    'no-cond-assign': 2,
+    'no-const-assign': 2,
+    'no-control-regex': 0,
+    'no-delete-var': 2,
+    'no-dupe-args': 2,
+    'no-dupe-class-members': 2,
+    'no-dupe-keys': 2,
+    'no-duplicate-case': 2,
+    'no-empty-character-class': 2,
+    'no-empty-pattern': 2,
+    'no-eval': 2,
+    'no-ex-assign': 2,
+    'no-extend-native': 2,
+    'no-extra-bind': 2,
+    'no-extra-boolean-cast': 2,
+    'no-extra-parens': [2, 'functions'],
+    'no-fallthrough': 2,
+    'no-floating-decimal': 2,
+    'no-func-assign': 2,
+    'no-implied-eval': 2,
+    'no-inner-declarations': [2, 'functions'],
+    'no-invalid-regexp': 2,
+    'no-irregular-whitespace': 2,
+    'no-iterator': 2,
+    'no-label-var': 2,
+    'no-labels': [2, {
+      'allowLoop': false,
+      'allowSwitch': false
+    }],
+    'no-lone-blocks': 2,
+    'no-mixed-spaces-and-tabs': 2,
+    'no-multi-spaces': 2,
+    'no-multi-str': 2,
+    'no-multiple-empty-lines': [2, {
+      'max': 1
+    }],
+    'no-native-reassign': 2,
+    'no-negated-in-lhs': 2,
+    'no-new-object': 2,
+    'no-new-require': 2,
+    'no-new-symbol': 2,
+    'no-new-wrappers': 2,
+    'no-obj-calls': 2,
+    'no-octal': 2,
+    'no-octal-escape': 2,
+    'no-path-concat': 2,
+    'no-proto': 2,
+    'no-redeclare': 2,
+    'no-regex-spaces': 2,
+    'no-return-assign': [2, 'except-parens'],
+    'no-self-assign': 2,
+    'no-self-compare': 2,
+    'no-sequences': 2,
+    'no-shadow-restricted-names': 2,
+    'no-spaced-func': 2,
+    'no-sparse-arrays': 2,
+    'no-this-before-super': 2,
+    'no-throw-literal': 2,
+    'no-trailing-spaces': 2,
+    'no-undef': 2,
+    'no-undef-init': 2,
+    'no-unexpected-multiline': 2,
+    'no-unmodified-loop-condition': 2,
+    'no-unneeded-ternary': [2, {
+      'defaultAssignment': false
+    }],
+    'no-unreachable': 2,
+    'no-unsafe-finally': 2,
+    'no-unused-vars': [2, {
+      'vars': 'all',
+      'args': 'none'
+    }],
+    'no-useless-call': 2,
+    'no-useless-computed-key': 2,
+    'no-useless-constructor': 2,
+    'no-useless-escape': 0,
+    'no-whitespace-before-property': 2,
+    'no-with': 2,
+    'one-var': [2, {
+      'initialized': 'never'
+    }],
+    'operator-linebreak': [2, 'after', {
+      'overrides': {
+        '?': 'before',
+        ':': 'before'
+      }
+    }],
+    'padded-blocks': [2, 'never'],
+    'quotes': [2, 'single', {
+      'avoidEscape': true,
+      'allowTemplateLiterals': true
+    }],
+    'semi': [2, 'never'],
+    'semi-spacing': [2, {
+      'before': false,
+      'after': true
+    }],
+    'space-before-blocks': [2, 'always'],
+    'space-before-function-paren': [2, 'never'],
+    'space-in-parens': [2, 'never'],
+    'space-infix-ops': 2,
+    'space-unary-ops': [2, {
+      'words': true,
+      'nonwords': false
+    }],
+    'spaced-comment': [2, 'always', {
+      'markers': ['global', 'globals', 'eslint', 'eslint-disable', '*package', '!', ',']
+    }],
+    'template-curly-spacing': [2, 'never'],
+    'use-isnan': 2,
+    'valid-typeof': 2,
+    'wrap-iife': [2, 'any'],
+    'yield-star-spacing': [2, 'both'],
+    'yoda': [2, 'never'],
+    'prefer-const': 2,
+    'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
+    'object-curly-spacing': [2, 'always', {
+      objectsInObjects: false
+    }],
+    'array-bracket-spacing': [2, 'never']
+  }
+}

+ 23 - 0
.gitignore

@@ -0,0 +1,23 @@
+.DS_Store
+node_modules/
+dist/
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+**/*.log
+
+tests/**/coverage/
+tests/e2e/reports
+selenium-debug.log
+
+# Editor directories and files
+.idea
+.vscode
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.local
+
+package-lock.json
+yarn.lock

+ 5 - 0
.travis.yml

@@ -0,0 +1,5 @@
+language: node_js
+node_js: 10
+script: npm run test
+notifications:
+  email: false

+ 21 - 0
LICENSE

@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2017-present PanJiaChen
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 36 - 0
README.en.md

@@ -0,0 +1,36 @@
+# chaos_center_admin
+
+#### Description
+{**When you're done, you can delete the content in this README and update the file with details for others getting started with your repository**}
+
+#### Software Architecture
+Software architecture description
+
+#### Installation
+
+1.  xxxx
+2.  xxxx
+3.  xxxx
+
+#### Instructions
+
+1.  xxxx
+2.  xxxx
+3.  xxxx
+
+#### Contribution
+
+1.  Fork the repository
+2.  Create Feat_xxx branch
+3.  Commit your code
+4.  Create Pull Request
+
+
+#### Gitee Feature
+
+1.  You can use Readme\_XXX.md to support different languages, such as Readme\_en.md, Readme\_zh.md
+2.  Gitee blog [blog.gitee.com](https://blog.gitee.com)
+3.  Explore open source project [https://gitee.com/explore](https://gitee.com/explore)
+4.  The most valuable open source project [GVP](https://gitee.com/gvp)
+5.  The manual of Gitee [https://gitee.com/help](https://gitee.com/help)
+6.  The most popular members  [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/)

Разлика између датотеке није приказан због своје велике величине
+ 222 - 0
README.es.md


Разлика између датотеке није приказан због своје велике величине
+ 218 - 0
README.ja.md


Разлика између датотеке није приказан због своје велике величине
+ 222 - 0
README.md


Разлика између датотеке није приказан због своје велике величине
+ 245 - 0
README.zh-CN.md


+ 14 - 0
babel.config.js

@@ -0,0 +1,14 @@
+module.exports = {
+  presets: [
+    // https://github.com/vuejs/vue-cli/tree/master/packages/@vue/babel-preset-app
+    '@vue/cli-plugin-babel/preset'
+  ],
+  'env': {
+    'development': {
+      // babel-plugin-dynamic-import-node plugin only does one thing by converting all import() to require().
+      // This plugin can significantly increase the speed of hot updates, when you have a large number of pages.
+      // https://panjiachen.github.io/vue-element-admin-site/guide/advanced/lazy-loading.html
+      'plugins': ['dynamic-import-node']
+    }
+  }
+}

+ 35 - 0
build/index.js

@@ -0,0 +1,35 @@
+const { run } = require('runjs')
+const chalk = require('chalk')
+const config = require('../vue.config.js')
+const rawArgv = process.argv.slice(2)
+const args = rawArgv.join(' ')
+
+if (process.env.npm_config_preview || rawArgv.includes('--preview')) {
+  const report = rawArgv.includes('--report')
+
+  run(`vue-cli-service build ${args}`)
+
+  const port = 9526
+  const publicPath = config.publicPath
+
+  var connect = require('connect')
+  var serveStatic = require('serve-static')
+  const app = connect()
+
+  app.use(
+    publicPath,
+    serveStatic('./dist', {
+      index: ['index.html', '/']
+    })
+  )
+
+  app.listen(port, function () {
+    console.log(chalk.green(`> Preview at  http://localhost:${port}${publicPath}`))
+    if (report) {
+      console.log(chalk.green(`> Report at  http://localhost:${port}${publicPath}report.html`))
+    }
+
+  })
+} else {
+  run(`vue-cli-service build ${args}`)
+}

Разлика између датотеке није приказан због своје велике величине
+ 1369 - 0
develop.sql


+ 24 - 0
jest.config.js

@@ -0,0 +1,24 @@
+module.exports = {
+  moduleFileExtensions: ['js', 'jsx', 'json', 'vue'],
+  transform: {
+    '^.+\\.vue$': 'vue-jest',
+    '.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$':
+      'jest-transform-stub',
+    '^.+\\.jsx?$': 'babel-jest'
+  },
+  moduleNameMapper: {
+    '^@/(.*)$': '<rootDir>/src/$1'
+  },
+  snapshotSerializers: ['jest-serializer-vue'],
+  testMatch: [
+    '**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)'
+  ],
+  collectCoverageFrom: ['src/utils/**/*.{js,vue}', '!src/utils/auth.js', '!src/utils/request.js', 'src/components/**/*.{js,vue}'],
+  coverageDirectory: '<rootDir>/tests/unit/coverage',
+  // 'collectCoverage': true,
+  'coverageReporters': [
+    'lcov',
+    'text-summary'
+  ],
+  testURL: 'http://localhost/'
+}

+ 9 - 0
jsconfig.json

@@ -0,0 +1,9 @@
+{ 
+  "compilerOptions": {
+    "baseUrl": "./",
+    "paths": {
+        "@/*": ["src/*"]
+    }
+  },
+  "exclude": ["node_modules", "dist"]
+}

+ 80 - 0
mock/app.js

@@ -0,0 +1,80 @@
+
+module.exports = [
+  // app-list
+  {
+    url: '/vue-element-admin/app/get-app-list',
+    type: 'get',
+    response: _ => {
+      return {
+        code: 0,
+        message: 'ok',
+        data: {
+          current_page: 1,
+          'data|5': [{
+            'id|+1': 1,
+            'name|1': ['展会系统', '后台系统', '后台系统2', '展会系统2', '后台系统3'],
+            'short_name|1': ['zhxt', 'htxt', 'zhxt1', 'htxt1'],
+            'domain|1': ['http://www.bbshenqian.cn/', 'http://www.baidu.com/']
+          }],
+          'from': 0,
+          'last_page': 1,
+          'per_page': 10,
+          'to': 10,
+          'total': 5
+        }
+      }
+    }
+  },
+  // edit-app-menus
+  {
+    url: '/vue-element-admin/app/edit-app-menus',
+    type: 'get',
+    response: _ => {
+      return {
+        code: 0,
+        message: 'ok',
+        data: {
+          current_page: 1,
+          'data|10': [{
+            'id|+1': 1,
+            'ver_name|1': ['基础版', '至尊纪念版', '标准2.0版'],
+            'name|1': ['展会系统', '后台系统'],
+            'price|1': ['200', '2000'],
+            'tag|1': ['zhxt', 'htxt'],
+            'desc|1': ['基础版拥有一些基础菜单', '至尊纪念版拥有', '标准2.0版拥有'],
+            'update_time|1': ['2020-12-19 07:09:48', '2020-12-19 07:09:00', '2015-06-17 05:20:00']
+          }],
+          'from': 0,
+          'last_page': 1,
+          'per_page': 10,
+          'to': 10,
+          'total': 20
+        }
+      }
+    }
+  },
+  // get-app
+  {
+    url: '/vue-element-admin/app/get-app',
+    type: 'get',
+    response: _ => {
+      return {
+        'code': 0,
+        'message': 'ok',
+        'data': {
+          'id': 5,
+          'short_name': 'kfxt',
+          'name': '开发系统',
+          'domain': 'http://www.bbshenqian.cn',
+          'code': 'kfxt',
+          'key': 'kfxt',
+          'allow_api': 0,
+          'logout_url': 'http://www.bbshenqian.cn/',
+          'status': 0,
+          'update_time': '2020-12-19 03:40:00',
+          'create_time': '2020-12-19 03:14:53'
+        }
+      }
+    }
+  }
+]

+ 116 - 0
mock/article.js

@@ -0,0 +1,116 @@
+const Mock = require('mockjs')
+
+const List = []
+const count = 100
+
+const baseContent = '<p>I am testing data, I am testing data.</p><p><img src="https://wpimg.wallstcn.com/4c69009c-0fd4-4153-b112-6cb53d1cf943"></p>'
+const image_uri = 'https://wpimg.wallstcn.com/e4558086-631c-425c-9430-56ffb46e70b3'
+
+for (let i = 0; i < count; i++) {
+  List.push(Mock.mock({
+    id: '@increment',
+    timestamp: +Mock.Random.date('T'),
+    author: '@first',
+    reviewer: '@first',
+    title: '@title(5, 10)',
+    content_short: 'mock data',
+    content: baseContent,
+    forecast: '@float(0, 100, 2, 2)',
+    importance: '@integer(1, 3)',
+    'type|1': ['CN', 'US', 'JP', 'EU'],
+    'status|1': ['published', 'draft'],
+    display_time: '@datetime',
+    comment_disabled: true,
+    pageviews: '@integer(300, 5000)',
+    image_uri,
+    platforms: ['a-platform']
+  }))
+}
+
+module.exports = [
+  {
+    url: '/vue-element-admin/article/list',
+    type: 'get',
+    response: config => {
+      const { importance, type, title, page = 1, limit = 20, sort } = config.query
+
+      let mockList = List.filter(item => {
+        if (importance && item.importance !== +importance) return false
+        if (type && item.type !== type) return false
+        if (title && item.title.indexOf(title) < 0) return false
+        return true
+      })
+
+      if (sort === '-id') {
+        mockList = mockList.reverse()
+      }
+
+      const pageList = mockList.filter((item, index) => index < limit * page && index >= limit * (page - 1))
+
+      return {
+        code: 20000,
+        data: {
+          total: mockList.length,
+          items: pageList
+        }
+      }
+    }
+  },
+
+  {
+    url: '/vue-element-admin/article/detail',
+    type: 'get',
+    response: config => {
+      const { id } = config.query
+      for (const article of List) {
+        if (article.id === +id) {
+          return {
+            code: 20000,
+            data: article
+          }
+        }
+      }
+    }
+  },
+
+  {
+    url: '/vue-element-admin/article/pv',
+    type: 'get',
+    response: _ => {
+      return {
+        code: 20000,
+        data: {
+          pvData: [
+            { key: 'PC', pv: 1024 },
+            { key: 'mobile', pv: 1024 },
+            { key: 'ios', pv: 1024 },
+            { key: 'android', pv: 1024 }
+          ]
+        }
+      }
+    }
+  },
+
+  {
+    url: '/vue-element-admin/article/create',
+    type: 'post',
+    response: _ => {
+      return {
+        code: 20000,
+        data: 'success'
+      }
+    }
+  },
+
+  {
+    url: '/vue-element-admin/article/update',
+    type: 'post',
+    response: _ => {
+      return {
+        code: 20000,
+        data: 'success'
+      }
+    }
+  }
+]
+

+ 62 - 0
mock/index.js

@@ -0,0 +1,62 @@
+const Mock = require('mockjs')
+const { param2Obj } = require('./utils')
+
+const user = require('./user')
+const role = require('./role')
+const article = require('./article')
+const search = require('./remote-search')
+const app = require('./app')
+
+const mocks = [
+  ...user,
+  ...role,
+  ...app,
+  ...article,
+  ...search
+]
+
+// for front mock
+// please use it cautiously, it will redefine XMLHttpRequest,
+// which will cause many of your third-party libraries to be invalidated(like progress event).
+function mockXHR() {
+  // mock patch
+  // https://github.com/nuysoft/Mock/issues/300
+  Mock.XHR.prototype.proxy_send = Mock.XHR.prototype.send
+  Mock.XHR.prototype.send = function() {
+    if (this.custom.xhr) {
+      this.custom.xhr.withCredentials = this.withCredentials || false
+
+      if (this.responseType) {
+        this.custom.xhr.responseType = this.responseType
+      }
+    }
+    this.proxy_send(...arguments)
+  }
+
+  function XHR2ExpressReqWrap(respond) {
+    return function(options) {
+      let result = null
+      if (respond instanceof Function) {
+        const { body, type, url } = options
+        // https://expressjs.com/en/4x/api.html#req
+        result = respond({
+          method: type,
+          body: JSON.parse(body),
+          query: param2Obj(url)
+        })
+      } else {
+        result = respond
+      }
+      return Mock.mock(result)
+    }
+  }
+
+  for (const i of mocks) {
+    Mock.mock(new RegExp(i.url), i.type || 'get', XHR2ExpressReqWrap(i.response))
+  }
+}
+
+module.exports = {
+  mocks,
+  mockXHR
+}

+ 81 - 0
mock/mock-server.js

@@ -0,0 +1,81 @@
+const chokidar = require('chokidar')
+const bodyParser = require('body-parser')
+const chalk = require('chalk')
+const path = require('path')
+const Mock = require('mockjs')
+
+const mockDir = path.join(process.cwd(), 'mock')
+
+function registerRoutes(app) {
+  let mockLastIndex
+  const { mocks } = require('./index.js')
+  const mocksForServer = mocks.map(route => {
+    return responseFake(route.url, route.type, route.response)
+  })
+  for (const mock of mocksForServer) {
+    app[mock.type](mock.url, mock.response)
+    mockLastIndex = app._router.stack.length
+  }
+  const mockRoutesLength = Object.keys(mocksForServer).length
+  return {
+    mockRoutesLength: mockRoutesLength,
+    mockStartIndex: mockLastIndex - mockRoutesLength
+  }
+}
+
+function unregisterRoutes() {
+  Object.keys(require.cache).forEach(i => {
+    if (i.includes(mockDir)) {
+      delete require.cache[require.resolve(i)]
+    }
+  })
+}
+
+// for mock server
+const responseFake = (url, type, respond) => {
+  return {
+    url: new RegExp(`${process.env.VUE_APP_BASE_API}${url}`),
+    type: type || 'get',
+    response(req, res) {
+      console.log('request invoke:' + req.path)
+      res.json(Mock.mock(respond instanceof Function ? respond(req, res) : respond))
+    }
+  }
+}
+
+module.exports = app => {
+  // parse app.body
+  // https://expressjs.com/en/4x/api.html#req.body
+  app.use(bodyParser.json())
+  app.use(bodyParser.urlencoded({
+    extended: true
+  }))
+
+  const mockRoutes = registerRoutes(app)
+  var mockRoutesLength = mockRoutes.mockRoutesLength
+  var mockStartIndex = mockRoutes.mockStartIndex
+
+  // watch files, hot reload mock server
+  chokidar.watch(mockDir, {
+    ignored: /mock-server/,
+    ignoreInitial: true
+  }).on('all', (event, path) => {
+    if (event === 'change' || event === 'add') {
+      try {
+        // remove mock routes stack
+        app._router.stack.splice(mockStartIndex, mockRoutesLength)
+
+        // clear routes cache
+        unregisterRoutes()
+
+        const mockRoutes = registerRoutes(app)
+        mockRoutesLength = mockRoutes.mockRoutesLength
+        mockStartIndex = mockRoutes.mockStartIndex
+
+        console.log(chalk.magentaBright(`\n > Mock Server hot reload success! changed  ${path}`))
+      } catch (error) {
+        console.log(chalk.redBright(error))
+      }
+    }
+  })
+}

+ 51 - 0
mock/remote-search.js

@@ -0,0 +1,51 @@
+const Mock = require('mockjs')
+
+const NameList = []
+const count = 100
+
+for (let i = 0; i < count; i++) {
+  NameList.push(Mock.mock({
+    name: '@first'
+  }))
+}
+NameList.push({ name: 'mock-Pan' })
+
+module.exports = [
+  // username search
+  {
+    url: '/vue-element-admin/search/user',
+    type: 'get',
+    response: config => {
+      const { name } = config.query
+      const mockNameList = NameList.filter(item => {
+        const lowerCaseName = item.name.toLowerCase()
+        return !(name && lowerCaseName.indexOf(name.toLowerCase()) < 0)
+      })
+      return {
+        code: 20000,
+        data: { items: mockNameList }
+      }
+    }
+  },
+
+  // transaction list
+  {
+    url: '/vue-element-admin/transaction/list',
+    type: 'get',
+    response: _ => {
+      return {
+        code: 20000,
+        data: {
+          total: 20,
+          'items|20': [{
+            order_no: '@guid()',
+            timestamp: +Mock.Random.date('T'),
+            username: '@name()',
+            price: '@float(1000, 15000, 0, 2)',
+            'status|1': ['success', 'pending']
+          }]
+        }
+      }
+    }
+  }
+]

+ 51 - 0
mock/role.js

@@ -0,0 +1,51 @@
+
+const tokens = {
+  admin: {
+    token: 'admin-token'
+  },
+  editor: {
+    token: 'editor-token'
+  }
+}
+
+const users = {
+  'admin-token': {
+    roles: ['admin'],
+    introduction: 'I am a super administrator',
+    avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif',
+    name: 'Super Admin'
+  },
+  'editor-token': {
+    roles: ['editor'],
+    introduction: 'I am an editor',
+    avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif',
+    name: 'Normal Editor'
+  }
+}
+
+module.exports = [
+  // role-lest
+  {
+    url: '/vue-element-admin/role/getRoles',
+    type: 'get',
+    response: _ => {
+      return {
+        code: 0,
+        message: 'ok',
+        data: {
+          current_page: 1,
+          data: [
+            { id: 1, role_name: '管理猿', create_time: '2016-05-11 03:06:16', status: 0 },
+            { id: 2, role_name: '运营猿', create_time: '2016-05-11 03:06:16', status: 0 },
+            { id: 3, role_name: '低级管理猿', create_time: '2016-05-11 03:06:16', status: 0 }
+          ],
+          from: 0,
+          last_page: 1,
+          per_page: 10,
+          to: 3,
+          total: 3
+        }
+      }
+    }
+  }
+]

+ 98 - 0
mock/role/index.js

@@ -0,0 +1,98 @@
+const Mock = require('mockjs')
+const { deepClone } = require('../utils')
+const { asyncRoutes, constantRoutes } = require('./routes.js')
+
+const routes = deepClone([...constantRoutes, ...asyncRoutes])
+
+const roles = [
+  {
+    key: 'admin',
+    name: 'admin',
+    description: 'Super Administrator. Have access to view all pages.',
+    routes: routes
+  },
+  {
+    key: 'editor',
+    name: 'editor',
+    description: 'Normal Editor. Can see all pages except permission page',
+    routes: routes.filter(i => i.path !== '/permission')// just a mock
+  },
+  {
+    key: 'visitor',
+    name: 'visitor',
+    description: 'Just a visitor. Can only see the home page and the document page',
+    routes: [{
+      path: '',
+      redirect: 'dashboard',
+      children: [
+        {
+          path: 'dashboard',
+          name: 'Dashboard',
+          meta: { title: 'dashboard', icon: 'dashboard' }
+        }
+      ]
+    }]
+  }
+]
+
+module.exports = [
+  // mock get all routes form server
+  {
+    url: '/vue-element-admin/routes',
+    type: 'get',
+    response: _ => {
+      return {
+        code: 20000,
+        data: routes
+      }
+    }
+  },
+
+  // mock get all roles form server
+  {
+    url: '/vue-element-admin/roles',
+    type: 'get',
+    response: _ => {
+      return {
+        code: 20000,
+        data: roles
+      }
+    }
+  },
+
+  // add role
+  {
+    url: '/vue-element-admin/role',
+    type: 'post',
+    response: {
+      code: 20000,
+      data: {
+        key: Mock.mock('@integer(300, 5000)')
+      }
+    }
+  },
+
+  // update role
+  {
+    url: '/vue-element-admin/role/[A-Za-z0-9]',
+    type: 'put',
+    response: {
+      code: 20000,
+      data: {
+        status: 'success'
+      }
+    }
+  },
+
+  // delete role
+  {
+    url: '/vue-element-admin/role/[A-Za-z0-9]',
+    type: 'delete',
+    response: {
+      code: 20000,
+      data: {
+        status: 'success'
+      }
+    }
+  }
+]

+ 530 - 0
mock/role/routes.js

@@ -0,0 +1,530 @@
+// Just a mock data
+
+const constantRoutes = [
+  {
+    path: '/redirect',
+    component: 'layout/Layout',
+    hidden: true,
+    children: [
+      {
+        path: '/redirect/:path*',
+        component: 'views/redirect/index'
+      }
+    ]
+  },
+  {
+    path: '/login',
+    component: 'views/login/index',
+    hidden: true
+  },
+  {
+    path: '/auth-redirect',
+    component: 'views/login/auth-redirect',
+    hidden: true
+  },
+  {
+    path: '/404',
+    component: 'views/error-page/404',
+    hidden: true
+  },
+  {
+    path: '/401',
+    component: 'views/error-page/401',
+    hidden: true
+  },
+  {
+    path: '',
+    component: 'layout/Layout',
+    redirect: 'dashboard',
+    children: [
+      {
+        path: 'dashboard',
+        component: 'views/dashboard/index',
+        name: 'Dashboard',
+        meta: { title: 'Dashboard', icon: 'dashboard', affix: true }
+      }
+    ]
+  },
+  {
+    path: '/documentation',
+    component: 'layout/Layout',
+    children: [
+      {
+        path: 'index',
+        component: 'views/documentation/index',
+        name: 'Documentation',
+        meta: { title: 'Documentation', icon: 'documentation', affix: true }
+      }
+    ]
+  },
+  {
+    path: '/guide',
+    component: 'layout/Layout',
+    redirect: '/guide/index',
+    children: [
+      {
+        path: 'index',
+        component: 'views/guide/index',
+        name: 'Guide',
+        meta: { title: 'Guide', icon: 'guide', noCache: true }
+      }
+    ]
+  }
+]
+
+const asyncRoutes = [
+  {
+    path: '/permission',
+    component: 'layout/Layout',
+    redirect: '/permission/index',
+    alwaysShow: true,
+    meta: {
+      title: 'Permission',
+      icon: 'lock',
+      roles: ['admin', 'editor']
+    },
+    children: [
+      {
+        path: 'page',
+        component: 'views/permission/page',
+        name: 'PagePermission',
+        meta: {
+          title: 'Page Permission',
+          roles: ['admin']
+        }
+      },
+      {
+        path: 'directive',
+        component: 'views/permission/directive',
+        name: 'DirectivePermission',
+        meta: {
+          title: 'Directive Permission'
+        }
+      },
+      {
+        path: 'role',
+        component: 'views/permission/role',
+        name: 'RolePermission',
+        meta: {
+          title: 'Role Permission',
+          roles: ['admin']
+        }
+      }
+    ]
+  },
+
+  {
+    path: '/icon',
+    component: 'layout/Layout',
+    children: [
+      {
+        path: 'index',
+        component: 'views/icons/index',
+        name: 'Icons',
+        meta: { title: 'Icons', icon: 'icon', noCache: true }
+      }
+    ]
+  },
+
+  {
+    path: '/components',
+    component: 'layout/Layout',
+    redirect: 'noRedirect',
+    name: 'ComponentDemo',
+    meta: {
+      title: 'Components',
+      icon: 'component'
+    },
+    children: [
+      {
+        path: 'tinymce',
+        component: 'views/components-demo/tinymce',
+        name: 'TinymceDemo',
+        meta: { title: 'Tinymce' }
+      },
+      {
+        path: 'markdown',
+        component: 'views/components-demo/markdown',
+        name: 'MarkdownDemo',
+        meta: { title: 'Markdown' }
+      },
+      {
+        path: 'json-editor',
+        component: 'views/components-demo/json-editor',
+        name: 'JsonEditorDemo',
+        meta: { title: 'Json Editor' }
+      },
+      {
+        path: 'split-pane',
+        component: 'views/components-demo/split-pane',
+        name: 'SplitpaneDemo',
+        meta: { title: 'SplitPane' }
+      },
+      {
+        path: 'avatar-upload',
+        component: 'views/components-demo/avatar-upload',
+        name: 'AvatarUploadDemo',
+        meta: { title: 'Avatar Upload' }
+      },
+      {
+        path: 'dropzone',
+        component: 'views/components-demo/dropzone',
+        name: 'DropzoneDemo',
+        meta: { title: 'Dropzone' }
+      },
+      {
+        path: 'sticky',
+        component: 'views/components-demo/sticky',
+        name: 'StickyDemo',
+        meta: { title: 'Sticky' }
+      },
+      {
+        path: 'count-to',
+        component: 'views/components-demo/count-to',
+        name: 'CountToDemo',
+        meta: { title: 'Count To' }
+      },
+      {
+        path: 'mixin',
+        component: 'views/components-demo/mixin',
+        name: 'ComponentMixinDemo',
+        meta: { title: 'componentMixin' }
+      },
+      {
+        path: 'back-to-top',
+        component: 'views/components-demo/back-to-top',
+        name: 'BackToTopDemo',
+        meta: { title: 'Back To Top' }
+      },
+      {
+        path: 'drag-dialog',
+        component: 'views/components-demo/drag-dialog',
+        name: 'DragDialogDemo',
+        meta: { title: 'Drag Dialog' }
+      },
+      {
+        path: 'drag-select',
+        component: 'views/components-demo/drag-select',
+        name: 'DragSelectDemo',
+        meta: { title: 'Drag Select' }
+      },
+      {
+        path: 'dnd-list',
+        component: 'views/components-demo/dnd-list',
+        name: 'DndListDemo',
+        meta: { title: 'Dnd List' }
+      },
+      {
+        path: 'drag-kanban',
+        component: 'views/components-demo/drag-kanban',
+        name: 'DragKanbanDemo',
+        meta: { title: 'Drag Kanban' }
+      }
+    ]
+  },
+  {
+    path: '/charts',
+    component: 'layout/Layout',
+    redirect: 'noRedirect',
+    name: 'Charts',
+    meta: {
+      title: 'Charts',
+      icon: 'chart'
+    },
+    children: [
+      {
+        path: 'keyboard',
+        component: 'views/charts/keyboard',
+        name: 'KeyboardChart',
+        meta: { title: 'Keyboard Chart', noCache: true }
+      },
+      {
+        path: 'line',
+        component: 'views/charts/line',
+        name: 'LineChart',
+        meta: { title: 'Line Chart', noCache: true }
+      },
+      {
+        path: 'mixchart',
+        component: 'views/charts/mixChart',
+        name: 'MixChart',
+        meta: { title: 'Mix Chart', noCache: true }
+      }
+    ]
+  },
+  {
+    path: '/nested',
+    component: 'layout/Layout',
+    redirect: '/nested/menu1/menu1-1',
+    name: 'Nested',
+    meta: {
+      title: 'Nested',
+      icon: 'nested'
+    },
+    children: [
+      {
+        path: 'menu1',
+        component: 'views/nested/menu1/index',
+        name: 'Menu1',
+        meta: { title: 'Menu1' },
+        redirect: '/nested/menu1/menu1-1',
+        children: [
+          {
+            path: 'menu1-1',
+            component: 'views/nested/menu1/menu1-1',
+            name: 'Menu1-1',
+            meta: { title: 'Menu1-1' }
+          },
+          {
+            path: 'menu1-2',
+            component: 'views/nested/menu1/menu1-2',
+            name: 'Menu1-2',
+            redirect: '/nested/menu1/menu1-2/menu1-2-1',
+            meta: { title: 'Menu1-2' },
+            children: [
+              {
+                path: 'menu1-2-1',
+                component: 'views/nested/menu1/menu1-2/menu1-2-1',
+                name: 'Menu1-2-1',
+                meta: { title: 'Menu1-2-1' }
+              },
+              {
+                path: 'menu1-2-2',
+                component: 'views/nested/menu1/menu1-2/menu1-2-2',
+                name: 'Menu1-2-2',
+                meta: { title: 'Menu1-2-2' }
+              }
+            ]
+          },
+          {
+            path: 'menu1-3',
+            component: 'views/nested/menu1/menu1-3',
+            name: 'Menu1-3',
+            meta: { title: 'Menu1-3' }
+          }
+        ]
+      },
+      {
+        path: 'menu2',
+        name: 'Menu2',
+        component: 'views/nested/menu2/index',
+        meta: { title: 'Menu2' }
+      }
+    ]
+  },
+
+  {
+    path: '/example',
+    component: 'layout/Layout',
+    redirect: '/example/list',
+    name: 'Example',
+    meta: {
+      title: 'Example',
+      icon: 'example'
+    },
+    children: [
+      {
+        path: 'create',
+        component: 'views/example/create',
+        name: 'CreateArticle',
+        meta: { title: 'Create Article', icon: 'edit' }
+      },
+      {
+        path: 'edit/:id(\\d+)',
+        component: 'views/example/edit',
+        name: 'EditArticle',
+        meta: { title: 'Edit Article', noCache: true },
+        hidden: true
+      },
+      {
+        path: 'list',
+        component: 'views/example/list',
+        name: 'ArticleList',
+        meta: { title: 'Article List', icon: 'list' }
+      }
+    ]
+  },
+
+  {
+    path: '/tab',
+    component: 'layout/Layout',
+    children: [
+      {
+        path: 'index',
+        component: 'views/tab/index',
+        name: 'Tab',
+        meta: { title: 'Tab', icon: 'tab' }
+      }
+    ]
+  },
+
+  {
+    path: '/error',
+    component: 'layout/Layout',
+    redirect: 'noRedirect',
+    name: 'ErrorPages',
+    meta: {
+      title: 'Error Pages',
+      icon: '404'
+    },
+    children: [
+      {
+        path: '401',
+        component: 'views/error-page/401',
+        name: 'Page401',
+        meta: { title: 'Page 401', noCache: true }
+      },
+      {
+        path: '404',
+        component: 'views/error-page/404',
+        name: 'Page404',
+        meta: { title: 'Page 404', noCache: true }
+      }
+    ]
+  },
+
+  {
+    path: '/error-log',
+    component: 'layout/Layout',
+    redirect: 'noRedirect',
+    children: [
+      {
+        path: 'log',
+        component: 'views/error-log/index',
+        name: 'ErrorLog',
+        meta: { title: 'Error Log', icon: 'bug' }
+      }
+    ]
+  },
+
+  {
+    path: '/excel',
+    component: 'layout/Layout',
+    redirect: '/excel/export-excel',
+    name: 'Excel',
+    meta: {
+      title: 'Excel',
+      icon: 'excel'
+    },
+    children: [
+      {
+        path: 'export-excel',
+        component: 'views/excel/export-excel',
+        name: 'ExportExcel',
+        meta: { title: 'Export Excel' }
+      },
+      {
+        path: 'export-selected-excel',
+        component: 'views/excel/select-excel',
+        name: 'SelectExcel',
+        meta: { title: 'Select Excel' }
+      },
+      {
+        path: 'export-merge-header',
+        component: 'views/excel/merge-header',
+        name: 'MergeHeader',
+        meta: { title: 'Merge Header' }
+      },
+      {
+        path: 'upload-excel',
+        component: 'views/excel/upload-excel',
+        name: 'UploadExcel',
+        meta: { title: 'Upload Excel' }
+      }
+    ]
+  },
+
+  {
+    path: '/zip',
+    component: 'layout/Layout',
+    redirect: '/zip/download',
+    alwaysShow: true,
+    meta: { title: 'Zip', icon: 'zip' },
+    children: [
+      {
+        path: 'download',
+        component: 'views/zip/index',
+        name: 'ExportZip',
+        meta: { title: 'Export Zip' }
+      }
+    ]
+  },
+
+  {
+    path: '/pdf',
+    component: 'layout/Layout',
+    redirect: '/pdf/index',
+    children: [
+      {
+        path: 'index',
+        component: 'views/pdf/index',
+        name: 'PDF',
+        meta: { title: 'PDF', icon: 'pdf' }
+      }
+    ]
+  },
+  {
+    path: '/pdf/download',
+    component: 'views/pdf/download',
+    hidden: true
+  },
+
+  {
+    path: '/theme',
+    component: 'layout/Layout',
+    redirect: 'noRedirect',
+    children: [
+      {
+        path: 'index',
+        component: 'views/theme/index',
+        name: 'Theme',
+        meta: { title: 'Theme', icon: 'theme' }
+      }
+    ]
+  },
+
+  {
+    path: '/clipboard',
+    component: 'layout/Layout',
+    redirect: 'noRedirect',
+    children: [
+      {
+        path: 'index',
+        component: 'views/clipboard/index',
+        name: 'ClipboardDemo',
+        meta: { title: 'Clipboard Demo', icon: 'clipboard' }
+      }
+    ]
+  },
+
+  {
+    path: '/i18n',
+    component: 'layout/Layout',
+    children: [
+      {
+        path: 'index',
+        component: 'views/i18n-demo/index',
+        name: 'I18n',
+        meta: { title: 'I18n', icon: 'international' }
+      }
+    ]
+  },
+
+  {
+    path: 'external-link',
+    component: 'layout/Layout',
+    children: [
+      {
+        path: 'https://github.com/PanJiaChen/vue-element-admin',
+        meta: { title: 'External Link', icon: 'link' }
+      }
+    ]
+  },
+
+  { path: '*', redirect: '/404', hidden: true }
+]
+
+module.exports = {
+  constantRoutes,
+  asyncRoutes
+}

+ 865 - 0
mock/user.js

@@ -0,0 +1,865 @@
+
+const tokens = {
+  admin: {
+    token: 'admin-token'
+  },
+  editor: {
+    token: 'editor-token'
+  }
+}
+
+const users = {
+  'admin-token': {
+    roles: ['admin'],
+    introduction: 'I am a super administrator',
+    avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif',
+    name: 'Super Admin'
+  },
+  'editor-token': {
+    roles: ['editor'],
+    introduction: 'I am an editor',
+    avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif',
+    name: 'Normal Editor'
+  }
+}
+
+module.exports = [
+  // user login
+  {
+    url: '/vue-element-admin/user/login',
+    type: 'post',
+    response: config => {
+      /* const { username } = config.body
+      const token = tokens[username]
+
+      // mock error
+      if (!token) {
+        return {
+          code: 60204,
+          message: 'Account and password are incorrect.'
+        }
+      }
+
+      return {
+        code: 20000,
+        data: token
+      }*/
+      return { code: 0, message: '', data: { token: '0ac9709d32945302ef5ce62d8a5a0324' }}
+    }
+  },
+
+  // get user info
+  {
+    url: '/vue-element-admin/user/info\.*',
+    type: 'get',
+    response: config => {
+      /* const { token } = config.query
+      const info = users[token]
+
+      // mock error
+      if (!info) {
+        return {
+          code: 50008,
+          message: 'Login failed, unable to get user details.'
+        }
+      }
+
+      return {
+        code: 20000,
+        data: info
+      }*/
+      return { code: 0, message: 'ok', data: { user_name: 'youwl', nick_name: '游1', avatar: 'http:\/\/oss.bbshenqian.cn\/bbshenqian\/common\/2020\/0215\/logo.png', phone: '13555555555', is_super: 0, update_time: '2020-12-15T02:31:48.000000Z', create_time: '2016-05-11T11:06:16.000000Z' }}
+    }
+  },
+
+  // user logout
+  {
+    url: '/vue-element-admin/user/logout',
+    type: 'post',
+    response: _ => {
+      return {
+        code: 0,
+        data: 'success'
+      }
+    }
+  },
+  // getAdminList
+  {
+    url: '/vue-element-admin/user/get-admin-list',
+    type: 'get',
+    response: _ => {
+      return {
+        code: 0,
+        message: 'ok',
+        data: {
+          current_page: 1,
+          data: [
+            {
+              id: 1,
+              company_id: 3,
+              user_id: 3,
+              user_name: '游文亮',
+              nick_name: '游文亮',
+              real_name: '游文亮',
+              role_name: '角色',
+              role_id: 2,
+              phone: '13777777777',
+              create_time: '2020-12-08 11:18:50',
+              update_time: '2020-12-08 11:18:50'
+            },
+            {
+              id: 2,
+              company_id: 3,
+              user_id: 3,
+              user_name: '游文亮1',
+              nick_name: '游文亮1',
+              real_name: '游文亮1',
+              role_name: '角色1',
+              role_id: 1,
+              phone: '13777777777',
+              create_time: '2020-12-08 11:18:50',
+              update_time: '2020-12-08 11:18:50'
+            },
+            {
+              id: 3,
+              company_id: 3,
+              user_id: 3,
+              user_name: '游文亮2',
+              nick_name: '游文亮3',
+              real_name: '游文亮3',
+              role_name: '角色3',
+              role_id: 3,
+              phone: '13777777777',
+              create_time: '2020-12-08 11:18:50',
+              update_time: '2020-12-08 11:18:50'
+            }
+          ],
+          from: 1,
+          last_page: 1,
+          per_page: '10',
+          to: 8,
+          total: 12
+        }
+      }
+    }
+  },
+  // role-lest
+  {
+    url: '/vue-element-admin/user/role-lest',
+    type: 'get',
+    response: _ => {
+      return {
+        code: 0,
+        message: 'ok',
+        data: {
+          current_page: 1,
+          data: [
+            { id: 1, role_name: '管理猿', create_time: '2016-05-11 03:06:16', status: 0 },
+            { id: 2, role_name: '运营猿', create_time: '2016-05-11 03:06:16', status: 0 },
+            { id: 3, role_name: '低级管理猿', create_time: '2016-05-11 03:06:16', status: 0 }
+          ],
+          from: 0,
+          last_page: 1,
+          per_page: 10,
+          to: 3,
+          total: 3
+        }
+      }
+    }
+  },
+  // 公司账号列表
+  {
+    url: '/vue-element-admin/user/get-company-list',
+    type: 'get',
+    response: _ => {
+      return {
+        'code': 0,
+        'message': 'ok',
+        'data': {
+          'current_page': 1,
+          'data': [
+            {
+              'id': 10313,
+              'name': '游文亮',
+              'phone': '',
+              'address': '',
+              'update_time': '2016-03-15 10:45:52',
+              'create_time': '2016-03-15 10:40:37',
+              'status': '0',
+              'active_time': '2016-03-15 00:00:00',
+              'invalid_time': '2020-12-23 00:00:00',
+              'type_id': 1,
+              'bind_app': '[{"id":"4","vers":["8"]},{"id":"6","vers":["11","13","14","19"]},{"id":"9","vers":["15"]},{"id":"27","vers":["18"]}]',
+              'agent_id': 1,
+              'is_login_code': 0,
+              'login_code_mobile': '',
+              'province': '',
+              'city': '',
+              'domain': '',
+              'user_type_id': 0,
+              'sub_user_count': 10,
+              'website_count': '50'
+            },
+            {
+              'id': 10314,
+              'name': '厦门聚百客科技有限公司',
+              'phone': '',
+              'address': '',
+              'update_time': '2020-04-05 12:16:49',
+              'create_time': '2020-03-10 03:39:15',
+              'status': '0',
+              'active_time': '2020-02-01 00:00:00',
+              'invalid_time': '2029-12-31 00:00:00',
+              'type_id': 0,
+              'bind_app': '[{"id":"4","vers":["8"]},{"id":"6","vers":["11","13","14","19"]},{"id":"9","vers":["15"]},{"id":"27","vers":["18"]}]',
+              'agent_id': 1,
+              'is_login_code': 0,
+              'login_code_mobile': '',
+              'province': '',
+              'city': '',
+              'domain': '',
+              'user_type_id': 0,
+              'sub_user_count': 5,
+              'website_count': '50'
+            },
+            {
+              'id': 10315,
+              'name': '科勒尔',
+              'phone': '',
+              'address': '',
+              'update_time': '2020-04-05 12:16:28',
+              'create_time': '2020-03-30 05:45:08',
+              'status': '0',
+              'active_time': '2020-03-30 00:00:00',
+              'invalid_time': '2021-12-31 00:00:00',
+              'type_id': 0,
+              'bind_app': '',
+              'agent_id': 1,
+              'is_login_code': 0,
+              'login_code_mobile': '',
+              'province': '',
+              'city': '',
+              'domain': '',
+              'user_type_id': 0,
+              'sub_user_count': 5,
+              'website_count': '7'
+            },
+            {
+              'id': 10316,
+              'name': 'CBME童装展-新英富曼集团',
+              'phone': '',
+              'address': '',
+              'update_time': '2020-04-05 12:02:21',
+              'create_time': '2020-04-05 12:02:21',
+              'status': '0',
+              'active_time': '2020-04-05 00:00:00',
+              'invalid_time': '2021-12-31 00:00:00',
+              'type_id': 0,
+              'bind_app': '',
+              'agent_id': 1,
+              'is_login_code': 0,
+              'login_code_mobile': '',
+              'province': '',
+              'city': '',
+              'domain': '',
+              'user_type_id': 0,
+              'sub_user_count': 5,
+              'website_count': '1'
+            },
+            {
+              'id': 10317,
+              'name': '东网电力',
+              'phone': '',
+              'address': '',
+              'update_time': '2020-04-05 12:06:37',
+              'create_time': '2020-04-05 12:06:37',
+              'status': '0',
+              'active_time': '2020-04-05 00:00:00',
+              'invalid_time': '2021-12-31 00:00:00',
+              'type_id': 0,
+              'bind_app': '',
+              'agent_id': 1,
+              'is_login_code': 0,
+              'login_code_mobile': '',
+              'province': '',
+              'city': '',
+              'domain': '',
+              'user_type_id': 0,
+              'sub_user_count': 5,
+              'website_count': '4'
+            },
+            {
+              'id': 10318,
+              'name': '悦容整形',
+              'phone': '',
+              'address': '',
+              'update_time': '2020-04-05 12:09:44',
+              'create_time': '2020-04-05 12:09:44',
+              'status': '0',
+              'active_time': '2020-04-05 00:00:00',
+              'invalid_time': '2021-12-01 00:00:00',
+              'type_id': 0,
+              'bind_app': '',
+              'agent_id': 1,
+              'is_login_code': 0,
+              'login_code_mobile': '',
+              'province': '',
+              'city': '',
+              'domain': '',
+              'user_type_id': 0,
+              'sub_user_count': 5,
+              'website_count': '3'
+            },
+            {
+              'id': 10319,
+              'name': 'china-homecare',
+              'phone': '',
+              'address': '',
+              'update_time': '2020-04-15 02:10:21',
+              'create_time': '2020-04-15 02:09:01',
+              'status': '0',
+              'active_time': '0000-11-30 00:00:00',
+              'invalid_time': '2021-04-15 00:00:00',
+              'type_id': 0,
+              'bind_app': '',
+              'agent_id': 1,
+              'is_login_code': 0,
+              'login_code_mobile': '',
+              'province': '13',
+              'city': '',
+              'domain': '',
+              'user_type_id': 0,
+              'sub_user_count': 5,
+              'website_count': '2'
+            },
+            {
+              'id': 10320,
+              'name': '翡翠侠',
+              'phone': '',
+              'address': '',
+              'update_time': '2020-04-28 14:28:06',
+              'create_time': '2020-04-28 14:28:06',
+              'status': '0',
+              'active_time': '2020-04-28 00:00:00',
+              'invalid_time': '2021-12-31 00:00:00',
+              'type_id': 0,
+              'bind_app': '',
+              'agent_id': 1,
+              'is_login_code': 0,
+              'login_code_mobile': '',
+              'province': '',
+              'city': '',
+              'domain': '',
+              'user_type_id': 0,
+              'sub_user_count': 1,
+              'website_count': '1'
+            },
+            {
+              'id': 10323,
+              'name': '苏凯欣',
+              'phone': '',
+              'address': '',
+              'update_time': '2020-06-24 01:36:54',
+              'create_time': '2020-06-24 01:36:54',
+              'status': '0',
+              'active_time': '2020-06-23 00:00:00',
+              'invalid_time': '2022-04-01 00:00:00',
+              'type_id': 1,
+              'bind_app': '',
+              'agent_id': 1,
+              'is_login_code': 0,
+              'login_code_mobile': '',
+              'province': '',
+              'city': '',
+              'domain': '',
+              'user_type_id': 0,
+              'sub_user_count': 5,
+              'website_count': '50'
+            },
+            {
+              'id': 10325,
+              'name': 'standard ME',
+              'phone': '',
+              'address': '',
+              'update_time': '2020-09-01 07:41:16',
+              'create_time': '2020-09-01 07:41:16',
+              'status': '0',
+              'active_time': '2020-09-01 00:00:00',
+              'invalid_time': '2029-12-31 00:00:00',
+              'type_id': 1,
+              'bind_app': '',
+              'agent_id': 1,
+              'is_login_code': 0,
+              'login_code_mobile': '',
+              'province': '',
+              'city': '',
+              'domain': '',
+              'user_type_id': 0,
+              'sub_user_count': 5,
+              'website_count': '50'
+            }
+          ],
+          'from': 0,
+          'last_page': 2,
+          'per_page': 10,
+          'to': 10,
+          'total': 15
+        }
+      }
+    }
+  },
+  // 省份列表
+  {
+    url: '/vue-element-admin/user/get-province-list',
+    type: 'get',
+    response: _ => {
+      return {
+        'code': 0,
+        'message': 'ok',
+        'data': [
+          {
+            'id': 1,
+            'province': '直辖市'
+          },
+          {
+            'id': 2,
+            'province': '河北省'
+          },
+          {
+            'id': 3,
+            'province': '江西省'
+          },
+          {
+            'id': 4,
+            'province': '山东省'
+          },
+          {
+            'id': 5,
+            'province': '山西省'
+          },
+          {
+            'id': 6,
+            'province': '内蒙古自治区'
+          },
+          {
+            'id': 7,
+            'province': '河南省'
+          },
+          {
+            'id': 8,
+            'province': '辽宁省'
+          },
+          {
+            'id': 9,
+            'province': '湖北省'
+          },
+          {
+            'id': 10,
+            'province': '吉林省'
+          },
+          {
+            'id': 11,
+            'province': '湖南省'
+          },
+          {
+            'id': 12,
+            'province': '黑龙江'
+          },
+          {
+            'id': 13,
+            'province': '广东省'
+          },
+          {
+            'id': 14,
+            'province': '江苏省'
+          },
+          {
+            'id': 15,
+            'province': '广西壮族自治区'
+          },
+          {
+            'id': 16,
+            'province': '海南省'
+          },
+          {
+            'id': 17,
+            'province': '四川省'
+          },
+          {
+            'id': 18,
+            'province': '浙江省'
+          },
+          {
+            'id': 19,
+            'province': '贵州省'
+          },
+          {
+            'id': 20,
+            'province': '安徽省'
+          },
+          {
+            'id': 21,
+            'province': '云南省'
+          },
+          {
+            'id': 22,
+            'province': '福建省'
+          },
+          {
+            'id': 23,
+            'province': '澳门特别行政区'
+          },
+          {
+            'id': 24,
+            'province': '甘肃省'
+          },
+          {
+            'id': 25,
+            'province': '黑龙江省'
+          },
+          {
+            'id': 26,
+            'province': '宁夏回族自治区'
+          },
+          {
+            'id': 27,
+            'province': '青海省'
+          },
+          {
+            'id': 28,
+            'province': '陕西省'
+          },
+          {
+            'id': 29,
+            'province': '台湾省'
+          },
+          {
+            'id': 30,
+            'province': '西藏自治区'
+          },
+          {
+            'id': 31,
+            'province': '香港特别行政区'
+          },
+          {
+            'id': 32,
+            'province': '新疆维吾尔自治区'
+          },
+          {
+            'id': 33,
+            'province': 'asdfasd'
+          },
+          {
+            'id': 34,
+            'province': '加利福尼亚州'
+          },
+          {
+            'id': 35,
+            'province': 'abcdefg'
+          },
+          {
+            'id': 36,
+            'province': '新泽西州'
+          }
+        ]
+      }
+    }
+  },
+  // 城市列表
+  {
+    url: '/vue-element-admin/user/get-city-list',
+    type: 'get',
+    response: _ => {
+      return {
+        'code': 0,
+        'message': 'ok',
+        'data|5': [
+          {
+            'id|+1': 1,
+            'province_id|+1': 2,
+            'city|1': ['张家口市', '沧州市', '承德市', '邯郸市', '衡水市', '廊坊市'],
+            'area_code|+1': '0313',
+            'province': '河北省'
+          }
+        ]
+      }
+    }
+  },
+  // 公司详情
+  {
+    url: '/vue-element-admin/user/get-company',
+    type: 'get',
+    response: _ => {
+      return {
+        'code': 0,
+        'message': 'ok',
+        'data': {
+          'id': 10333,
+          'name': 'jy',
+          'phone': '13311111111',
+          'address': '厦门市思明区xxxxxxx',
+          'update_time': '2020-12-22 09:17:27',
+          'create_time': '2020-12-22 07:57:23',
+          'status': 0,
+          'active_time': '2016-03-15 00:00:00',
+          'invalid_time': '2020-12-23 00:00:00',
+          'type_id': 1,
+          'bind_app': '',
+          'agent_id': 1,
+          'is_login_code': 0,
+          'login_code_mobile': '',
+          'province': 22,
+          'city': 1,
+          'domain': '',
+          'user_type_id': 0,
+          'sub_user_count': 5,
+          'website_count': null
+        }
+      }
+    }
+  },
+  // 公司授权应用详情
+  {
+    url: '/vue-element-admin/user/get-user-soft-bind',
+    type: 'get',
+    response: _ => {
+      return {
+        'code': 0,
+        'message': 'ok',
+        'data': {
+          'id': 10333,
+          'name': '李肖明2',
+          'phone': '13311111111',
+          'address': '集美区',
+          'update_time': '2020-12-22 09:42:44',
+          'create_time': '2020-12-22 07:57:23',
+          'status': 1,
+          'active_time': '2016-03-15 00:00:00',
+          'invalid_time': '2020-12-23 00:00:00',
+          'type_id': 1,
+          'bind_app': '',
+          'agent_id': 1,
+          'is_login_code': 0,
+          'login_code_mobile': '',
+          'province': '22',
+          'city': '261',
+          'domain': '',
+          'user_type_id': 0,
+          'sub_user_count': 5,
+          'website_count': null,
+          'bind_apps': [
+            {
+              'id': 5942,
+              'bind_user': 10333,
+              'app_id': 1,
+              'ver_id': 32,
+              'ver_func': '',
+              'start_time': '2016-03-15 09:42:44',
+              'end_time': '2020-12-23 09:42:44',
+              'status': 0,
+              'create_time': '2020-12-23 06:07:40',
+              'update_time': '2020-12-23 07:12:20'
+            },
+            {
+              'id': 5945,
+              'bind_user': 10333,
+              'app_id': 2,
+              'ver_id': 33,
+              'ver_func': [93, 96],
+              'start_time': '2016-03-15 09:42:44',
+              'end_time': '2020-12-28 09:42:44',
+              'status': 0,
+              'create_time': '2020-12-23 06:35:33',
+              'update_time': '2020-12-23 07:12:20'
+            }
+          ]
+        }
+      }
+    }
+  },
+  // 获取公司应用管理菜单\功能权限列表详情
+  {
+    url: '/vue-element-admin/user/get-app-menus-list',
+    type: 'get',
+    response: _ => {
+      return {
+        'code': 0,
+        'message': 'ok',
+        'data': {
+          'current_page': 1,
+          'data': [
+            {
+              'id': 1,
+              'menus_name': '展会管理',
+              'sort': 1,
+              'role_name': '',
+              'role_key': '',
+              'pid': 0,
+              'icon': '',
+              'app_id': 1,
+              'is_default': 0,
+              'status': 0,
+              'update_time': '2019-08-24 11:24:39',
+              'create_time': null,
+              'name': '展会系统',
+              'p_name': ''
+            },
+            {
+              'id': 2,
+              'menus_name': '创建展会',
+              'sort': 1,
+              'role_name': 'MNU-SYS-EXHIBITION',
+              'role_key': 'Account.Index.index',
+              'pid': 1,
+              'icon': 'fa fa-slideshare',
+              'app_id': 1,
+              'is_default': 0,
+              'status': 0,
+              'update_time': '2019-08-24 11:18:00',
+              'create_time': null,
+              'name': '展会系统',
+              'p_name': '展会管理'
+            },
+            {
+              'id': 3,
+              'menus_name': '测试菜单',
+              'sort': 0,
+              'role_name': 'cs',
+              'role_key': 'Account.Index.cs',
+              'pid': 2,
+              'icon': '',
+              'app_id': 1,
+              'is_default': 0,
+              'status': 0,
+              'update_time': '2020-12-15 10:34:01',
+              'create_time': null,
+              'name': '展会系统',
+              'p_name': '创建展会'
+            },
+            {
+              'id': 4,
+              'menus_name': '页面',
+              'sort': 0,
+              'role_name': 'Page',
+              'role_key': 'page',
+              'pid': 0,
+              'icon': 'fa fa-slideshare',
+              'app_id': 1,
+              'is_default': 0,
+              'status': 0,
+              'update_time': '2020-12-18 09:53:45',
+              'create_time': null,
+              'name': '展会系统',
+              'p_name': ''
+            },
+            {
+              'id': 5,
+              'menus_name': '页面',
+              'sort': 1,
+              'role_name': 'PageIdex',
+              'role_key': 'page.pageindex',
+              'pid': 4,
+              'icon': '',
+              'app_id': 1,
+              'is_default': 0,
+              'status': 0,
+              'update_time': '2020-12-18 09:53:54',
+              'create_time': null,
+              'name': '展会系统',
+              'p_name': '页面'
+            }
+          ],
+          'from': 0,
+          'last_page': 1,
+          'per_page': 10,
+          'to': 5,
+          'total': 5
+        }
+      }
+    }
+  },
+  // 新增公司
+  {
+    url: '/vue-element-admin/user/add-company',
+    type: 'post',
+    response: config => {
+      return {
+        'code': 0,
+        'message': 'ok',
+        'data': 10333
+      }
+    }
+  },
+  // 修改公司状态
+  {
+    url: '/vue-element-admin/user/ban-company',
+    type: 'post',
+    response: config => {
+      return {
+        'code': 0,
+        'message': 'ok',
+        'data': 10333
+      }
+    }
+  },
+  // 公司授权应用提交
+  {
+    url: '/vue-element-admin/user/save-user-soft-bind',
+    type: 'post',
+    response: config => {
+      return {
+        'code': 0,
+        'message': 'ok',
+        'data': 10333
+      }
+    }
+  },
+  // 新增后台账号
+  {
+    url: '/vue-element-admin/user/add-admin-user',
+    type: 'post',
+    response: config => {
+      return {
+        'code': 0,
+        'message': 'ok',
+        'data': 10333
+      }
+    }
+  },
+  // 编辑后台账号
+  {
+    url: '/vue-element-admin/user/edit-admin-user',
+    type: 'post',
+    response: config => {
+      return {
+        'code': 0,
+        'message': 'ok',
+        'data': 10333
+      }
+    }
+  },
+  // 修改后台账号状态
+  {
+    url: '/vue-element-admin/user/ban-admin-userr',
+    type: 'post',
+    response: config => {
+      return {
+        'code': 0,
+        'message': 'ok',
+        'data': 10333
+      }
+    }
+  },
+  // 修改角色状态
+  {
+    url: '/vue-element-admin/user/add-role',
+    type: 'post',
+    response: config => {
+      return {
+        'code': 0,
+        'message': 'ok',
+        'data': 10333
+      }
+    }
+  }
+]

+ 48 - 0
mock/utils.js

@@ -0,0 +1,48 @@
+/**
+ * @param {string} url
+ * @returns {Object}
+ */
+function param2Obj(url) {
+  const search = decodeURIComponent(url.split('?')[1]).replace(/\+/g, ' ')
+  if (!search) {
+    return {}
+  }
+  const obj = {}
+  const searchArr = search.split('&')
+  searchArr.forEach(v => {
+    const index = v.indexOf('=')
+    if (index !== -1) {
+      const name = v.substring(0, index)
+      const val = v.substring(index + 1, v.length)
+      obj[name] = val
+    }
+  })
+  return obj
+}
+
+/**
+ * This is just a simple version of deep copy
+ * Has a lot of edge cases bug
+ * If you want to use a perfect deep copy, use lodash's _.cloneDeep
+ * @param {Object} source
+ * @returns {Object}
+ */
+function deepClone(source) {
+  if (!source && typeof source !== 'object') {
+    throw new Error('error arguments', 'deepClone')
+  }
+  const targetObj = source.constructor === Array ? [] : {}
+  Object.keys(source).forEach(keys => {
+    if (source[keys] && typeof source[keys] === 'object') {
+      targetObj[keys] = deepClone(source[keys])
+    } else {
+      targetObj[keys] = source[keys]
+    }
+  })
+  return targetObj
+}
+
+module.exports = {
+  param2Obj,
+  deepClone
+}

+ 114 - 0
package.json

@@ -0,0 +1,114 @@
+{
+  "name": "vue-element-admin",
+  "version": "4.4.0",
+  "description": "A magical vue admin. An out-of-box UI solution for enterprise applications. Newest development stack of vue. Lots of awesome features",
+  "author": "Pan <panfree23@gmail.com>",
+  "scripts": {
+    "dev": "vue-cli-service serve",
+    "lint": "eslint --ext .js,.vue src",
+    "build:prod": "vue-cli-service build",
+    "build:test": "vue-cli-service build --mode test",
+    "preview": "node build/index.js --preview",
+    "new": "plop",
+    "svgo": "svgo -f src/icons/svg --config=src/icons/svgo.yml",
+    "test:unit": "jest --clearCache && vue-cli-service test:unit",
+    "test:ci": "npm run lint && npm run test:unit"
+  },
+  "dependencies": {
+    "@google/genai": "^0.13.0",
+    "axios": "^0.21.1",
+    "clipboard": "2.0.4",
+    "codemirror": "5.45.0",
+    "core-js": "3.6.5",
+    "driver.js": "0.9.5",
+    "dropzone": "5.5.1",
+    "echarts": "4.2.1",
+    "element-ui": "2.13.2",
+    "file-saver": "2.0.1",
+    "font-awesome": "^4.7.0",
+    "fuse.js": "3.4.4",
+    "js-cookie": "2.2.0",
+    "js-md5": "^0.8.3",
+    "jsonlint": "1.6.3",
+    "jszip": "3.2.1",
+    "normalize.css": "7.0.0",
+    "nprogress": "0.2.0",
+    "path-to-regexp": "2.4.0",
+    "screenfull": "4.2.0",
+    "script-loader": "0.7.2",
+    "sortablejs": "1.8.4",
+    "tui-editor": "1.3.3",
+    "vue": "2.6.10",
+    "vue-count-to": "1.0.13",
+    "vue-cropper": "^0.4.9",
+    "vue-highlightjs": "^1.3.3",
+    "vue-i18n": "^8.28.2",
+    "vue-qr": "^4.0.9",
+    "vue-router": "^3.5.2",
+    "vue-splitpane": "1.0.4",
+    "vuedraggable": "2.20.0",
+    "vuex": "3.1.0",
+    "xlsx": "0.14.1"
+  },
+  "devDependencies": {
+    "@vue/cli-plugin-babel": "4.4.4",
+    "@vue/cli-plugin-eslint": "4.4.4",
+    "@vue/cli-plugin-unit-jest": "4.4.4",
+    "@vue/cli-service": "4.4.4",
+    "@vue/test-utils": "1.0.0-beta.29",
+    "autoprefixer": "9.5.1",
+    "babel-eslint": "10.1.0",
+    "babel-jest": "23.6.0",
+    "babel-plugin-dynamic-import-node": "2.3.3",
+    "chalk": "2.4.2",
+    "chokidar": "2.1.5",
+    "connect": "3.6.6",
+    "eslint": "6.7.2",
+    "eslint-plugin-vue": "6.2.2",
+    "html-webpack-plugin": "3.2.0",
+    "lint-staged": "8.1.5",
+    "mockjs": "1.0.1-beta3",
+    "plop": "2.3.0",
+    "postcss-px-to-viewport": "^1.1.1",
+    "raw-loader": "^4.0.2",
+    "runjs": "4.3.2",
+    "sass": "1.26.2",
+    "sass-loader": "8.0.2",
+    "script-ext-html-webpack-plugin": "2.1.3",
+    "serve-static": "1.13.2",
+    "svg-sprite-loader": "4.1.3",
+    "svgo": "1.2.0",
+    "vue-template-compiler": "2.6.10"
+  },
+  "browserslist": [
+    "> 1%",
+    "last 2 versions"
+  ],
+  "bugs": {
+    "url": "https://github.com/PanJiaChen/vue-element-admin/issues"
+  },
+  "engines": {
+    "node": ">=8.9",
+    "npm": ">= 3.0.0"
+  },
+  "keywords": [
+    "vue",
+    "admin",
+    "dashboard",
+    "element-ui",
+    "boilerplate",
+    "admin-template",
+    "management-system"
+  ],
+  "license": "MIT",
+  "lint-staged": {
+    "src/**/*.{js,vue}": [
+      "eslint --fix",
+      "git add"
+    ]
+  },
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/PanJiaChen/vue-element-admin.git"
+  }
+}

+ 9 - 0
plopfile.js

@@ -0,0 +1,9 @@
+const viewGenerator = require('./plop-templates/view/prompt')
+const componentGenerator = require('./plop-templates/component/prompt')
+const storeGenerator = require('./plop-templates/store/prompt.js')
+
+module.exports = function(plop) {
+  plop.setGenerator('view', viewGenerator)
+  plop.setGenerator('component', componentGenerator)
+  plop.setGenerator('store', storeGenerator)
+}

+ 18 - 0
postcss.config.js

@@ -0,0 +1,18 @@
+module.exports = {
+  plugins: {
+    'postcss-px-to-viewport': {
+      unitToConvert: 'px', // 要转换的单位
+      viewportWidth: 1920, // 设计稿的视口宽度
+      unitPrecision: 5, // 单位转换后保留的精度
+      propList: ['*'], // 能转换的属性列表
+      viewportUnit: 'vw', // 希望使用的视口单位
+      fontViewportUnit: 'vw', // 字体使用的视口单位
+      selectorBlackList: [], // 需要忽略的CSS选择器
+      minPixelValue: 1, // 设置要替换的最小像素值
+      mediaQuery: false, // 允许在媒体查询中转换px
+      replace: true, // 是否直接替换而不添加备用属性
+      exclude: /node_modules/i, // 排除的文件
+      landscape: false // 是否添加横向媒体查询
+    }
+  }
+}

BIN
public/favicon.ico


+ 16 - 0
public/index.html

@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <meta charset="utf-8">
+  <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
+  <meta name="renderer" content="webkit">
+  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
+  <link rel="icon" href="/static/image/<%= VUE_APP_NAME %>/favicon.png">
+  <title><%= VUE_APP_WEBSITE_NAME %></title>
+</head>
+<body>
+<div id="app"></div>
+<!-- built files will be auto injected -->
+
+</body>
+</html>

Разлика између датотеке није приказан због своје велике величине
+ 4 - 0
public/static/js/jquery-1.11.1.min.js


Разлика између датотеке није приказан због своје велике величине
+ 508 - 0
public/static/js/require.js


+ 63 - 0
src/App.vue

@@ -0,0 +1,63 @@
+<template>
+  <div id="app" class="custom-theme">
+    <router-view />
+  </div>
+</template>
+
+<script>
+import i18n from '@/locales/i18n'
+export default {
+  name: 'App'
+}
+</script>
+<style lang="scss">
+ body,html,#app{
+   margin-top: 0;
+ }
+ .high-light{
+   animation: 4s high-light linear;
+   border-radius: 6px;
+ }
+ @keyframes high-light {
+   0%{
+     box-shadow:
+       0 0 0 8px #ffffff,
+       0 0 0 10px #ffffff;
+   }
+   25%{
+     box-shadow:
+       0 0 0 8px #ffffff,
+       0 0 0 10px #4F46E5;
+   }
+   35%{
+     box-shadow:
+       0 0 0 4px #ffffff,
+       0 0 0 6px #4F46E5;
+   }
+   45%{
+     box-shadow:
+       0 0 0 8px #ffffff,
+       0 0 0 10px #4F46E5;
+   }
+   55%{
+     box-shadow:
+       0 0 0 4px #ffffff,
+       0 0 0 6px #4F46E5;
+   }
+   65%{
+     box-shadow:
+       0 0 0 8px #ffffff,
+       0 0 0 10px #4F46E5;
+   }
+   75%{
+     box-shadow:
+       0 0 0 4px #ffffff,
+       0 0 0 6px #4F46E5;
+   }
+   100%{
+     box-shadow:
+       0 0 0 4px #ffffff,
+       0 0 0 6px #ffffff;
+   }
+ }
+</style>

+ 4 - 0
src/aiHelper/config.js

@@ -0,0 +1,4 @@
+
+export default {
+
+}

+ 846 - 0
src/aiHelper/index.vue

@@ -0,0 +1,846 @@
+<script>
+import Prompt from '!!raw-loader!./system/prompt.md'
+const systemPlugin = require.context('./system/plugin', true, /\.vue$/)
+const otherPlugin = require.context('./plugin', true, /\.vue$/)
+const components = {}
+otherPlugin.keys().forEach(key => {
+  initComponent(otherPlugin(key))
+})
+systemPlugin.keys().forEach(key => {
+  initComponent(systemPlugin(key))
+})
+function initComponent(plugin) {
+  const data = plugin.default.options.data()
+  if (data.type === 'comp') {
+    components[plugin.default.options.name] = plugin.default
+  }
+}
+import Vue from 'vue'
+import { GoogleGenAI, Type } from '@google/genai'
+export default Vue.extend({
+  name: 'Index',
+  components: components,
+  data() {
+    return {
+      AiKey: '',
+      AiModel: '',
+      aiConfig: {},
+      showText: false,
+      showMessage: false,
+      showMenu: false,
+      text: '',
+      message: '',
+      timer: null,
+      target: null,
+      inProcess: false,
+      aiState: '0',
+      workLength: 0,
+      retryCount: 0,
+      aiContext: [],
+      tool_name: '',
+      tool_input: '',
+      tool_data: {},
+      toolList: [],
+      taskTollList: [],
+      pluginList: [],
+      componentList: [],
+      pageList: [],
+      taskList: [],
+      prompt: Prompt,
+      aboutList: [
+        '优化一下首页的SEO数据',
+        '翻译一下关于我们页面',
+        '站内有多少种产品类型'
+      ],
+      aboutText: '',
+      guideData: {
+        step: 0,
+        apiKey: '',
+        model: ''
+      }
+    }
+  },
+  mounted() {
+    this.importPlugin()
+    this.getConfig()
+    this.workStart()
+    this.scrollText()
+    this.$bus.$on('sentAi', (tool_data, tool_role) => {
+      this.sentAi(tool_data, tool_role)
+    })
+    this.$bus.$on('runWork', (workData) => {
+      this.runWork(workData)
+    })
+  },
+  methods: {
+    scrollText() {
+      let index = 0
+      this.aboutText = this.aboutList[index]
+      setInterval(() => {
+        if (index === this.aboutList.length) {
+          index = 0
+        }
+        this.aboutText = this.aboutList[index]
+        index += 1
+      }, 4000)
+    },
+    importPlugin() {
+      systemPlugin.keys().forEach(key => {
+        const plugin = systemPlugin(key)
+        const data = plugin.default.options.data()
+        if (data.type === 'page') {
+          this.importAsPage(plugin)
+        } else if (data.type === 'tool') {
+          this.importAsTool(plugin)
+        } else if (data.type === 'task') {
+          this.importAsTask(plugin)
+        } else if (data.type === 'comp') {
+          this.importAsComp(plugin)
+        } else {
+          console.log(plugin.default.options.name + '类型未知,无法加载')
+        }
+      })
+      otherPlugin.keys().forEach(key => {
+        const plugin = otherPlugin(key)
+        const data = plugin.default.options.data()
+        if (data.type === 'page') {
+          this.importAsPage(plugin)
+        } else if (data.type === 'tool') {
+          this.importAsTool(plugin)
+        } else if (data.type === 'task') {
+          this.importAsTask(plugin)
+        } else if (data.type === 'comp') {
+          this.importAsComp(plugin)
+        } else {
+          console.log(plugin.default.options.name + '类型未知,无法加载')
+        }
+      })
+    },
+    importAsComp(plugin) {
+      const data = plugin.default.options.data()
+      this.componentList.push(plugin.default.options.name)
+      this.prompt += data.prompt
+      console.log('已从' + plugin.default.options.name + '中加载组件')
+    },
+    importAsPage(plugin) {
+      const data = plugin.default.options.data()
+      const routeConfig = data.router
+      this.pageList.push({
+        name: routeConfig.name,
+        title: routeConfig.meta.title
+      })
+      if (!routeConfig.component) {
+        routeConfig.component = plugin.default
+      }
+      if (data.parent) {
+        this.$router.addRoute(data.parent, routeConfig)
+      } else {
+        this.$router.addRoute(routeConfig)
+      }
+      this.prompt += data.prompt
+      console.log('已加载' + data.router.meta.title + '页面')
+    },
+    importAsTool(plugin) {
+      const tools = plugin.default.options.methods
+      for (const key in tools) {
+        this.toolList.push({
+          name: key,
+          tool: tools[key]
+        })
+      }
+      const data = plugin.default.options.data()
+      this.prompt += data.prompt
+      console.log('已从' + plugin.default.options.name + '中加载' + Object.keys(tools).length + '项能力')
+    },
+    importAsTask(plugin) {
+      const tools = plugin.default.options.methods
+      for (const key in tools) {
+        this.taskTollList.push({
+          name: key,
+          tool: tools[key]
+        })
+      }
+      const data = plugin.default.options.data()
+      this.prompt += data.prompt
+      console.log('已从' + plugin.default.options.name + '中加载' + Object.keys(tools).length + '项技能')
+    },
+    workStart() {
+      setInterval(() => {
+        const that = this
+        this.workLength = this.taskList.length
+        if (this.taskList.length && this.taskList[0].states === 'wait') {
+          if (this.retryCount > this.aiConfig.maxRetry) {
+            this.postMessage('已达失败重试上限,请检查AI配置以及额度')
+            this.taskList.splice(0, 0)
+            return
+          }
+          this.taskList[0].states = 'running'
+          this.taskTollList.forEach(tool => {
+            if (tool.name === this.taskList[0].type) {
+              tool.tool(this.taskList[0].data, that)
+                .then(result => {
+                  this.retryCount = 0
+                  this.postMessage(this.taskList[0].data.title + '任务结束')
+                  this.taskList.shift()
+                }).catch(err => {
+                  this.retryCount += 1
+                  this.postMessage(this.taskList[0].data.title + '任务失败')
+                  const task = this.taskList.shift()
+                  if (task.count) {
+                    if (task.count > this.aiConfig.retryOne) {
+                      this.postMessage(this.taskList[0].data.title + '已达失败重试上限,任务移除')
+                    } else {
+                      task.count += 1
+                      task.states = 'wait'
+                      this.taskList.push(task)
+                    }
+                  }
+                })
+            }
+          })
+        }
+      }, 100)
+    },
+    textInput(e) {
+      this.text = this.target.innerText
+    },
+    sent(e) {
+      if (this.inProcess && this.aiState === '2') {
+        this.resetContext()
+      } else if (!this.inProcess) {
+        this.inProcess = true
+        this.sentAi()
+      } else {
+
+      }
+    },
+    guide(tool_data) {
+      console.log(tool_data)
+      if (tool_data) {
+        if (
+          tool_data.text === '用户反馈先前的操作或者数据存在错误,请询问是何处错误' ||
+          tool_data.text === '用户反馈需要结束对话,若有未完成的工作确认是否放弃工作,然后结束对话') {
+          this.inProcess = true
+          this.parseAiData({
+            context: '在开始使用前,需要先进行相关配置',
+            tool_name: 'initSelect',
+            finish: false,
+            data: JSON.stringify([
+              { label: '继续', value: 'go' }
+            ])
+          })
+          this.guideData.step = 0
+          return
+        }
+        if (this.guideData.step === 0) {
+          this.guideData.step = 1
+          this.parseAiData({
+            context: '请输入Gemini apiKey。可以前往https://aistudio.google.com/apikey获取',
+            tool_name: 'initInput',
+            finish: false,
+            data: ''
+          })
+          return
+        }
+        if (this.guideData.step === 1) {
+          if (tool_data.text.length < 10) {
+            this.parseAiData({
+              context: 'apiKey似乎不对。可以前往https://aistudio.google.com/apikey获取',
+              tool_name: 'initInput',
+              finish: false,
+              data: ''
+            })
+            return
+          }
+          this.guideData.apiKey = tool_data.text
+          this.guideData.step = 2
+          this.parseAiData({
+            context: '你想使用哪个Ai模型?',
+            tool_name: 'initSelect',
+            data: JSON.stringify([
+              {
+                label: 'Gemini 2.5 Pro',
+                value: 'gemini-2.5-pro'
+              },
+              {
+                label: 'Gemini 2.5 Flash',
+                value: 'gemini-2.5-flash'
+              },
+              {
+                label: 'Gemini 2.5 Flash Lite',
+                value: 'gemini-2.5-flash-lite'
+              }
+            ])
+          })
+          return
+        }
+        if (this.guideData.step === 2) {
+          this.guideData.step = 3
+          this.guideData.model = tool_data.value
+          const AiConfig = {}
+          AiConfig.setting = {
+            maxRetry: 20,
+            retryOne: 5,
+            language: '',
+            debug: false
+          }
+          AiConfig.Google_Ai = {
+            apiKey: this.guideData.apiKey,
+            model: this.guideData.model
+          }
+          AiConfig.active = 'Google_Ai'
+          localStorage.setItem('aiConfig', JSON.stringify(AiConfig))
+          this.parseAiData({
+            context: '一切就绪,手动刷新页面后就可以使用自由聊天功能了。',
+            tool_name: 'initSelect',
+            finish: false,
+            data: JSON.stringify([
+              { label: '刷新页面', value: 'go' }
+            ])
+          })
+          return
+        }
+        if (this.guideData.step === 3) {
+          location.reload()
+        }
+      } else {
+        this.inProcess = true
+        this.parseAiData({
+          context: '在开始使用前,需要先进行相关配置',
+          tool_name: 'initSelect',
+          finish: false,
+          data: JSON.stringify([
+            { label: '继续', value: 'go' }
+          ])
+        })
+      }
+    },
+    sentAi(tool_data, tool_role) {
+      if (this.AiKey === '') {
+        this.guide(tool_data)
+        return
+      }
+      const ai = new GoogleGenAI({
+        apiKey: this.AiKey
+      })
+      let role = 'user'
+      if (tool_role) { role = tool_role }
+      const model = this.AiModel
+      const config = {
+        thinkingConfig: {
+          thinkingBudget: -1
+        },
+        responseMimeType: 'application/json',
+        responseSchema: {
+          type: Type.OBJECT,
+          required: ['context', 'data', 'finish'],
+          properties: {
+            context: {
+              type: Type.STRING
+            },
+            tool_name: {
+              type: Type.STRING
+            },
+            data: {
+              type: Type.STRING
+            },
+            finish: {
+              type: Type.BOOLEAN
+            }
+          }
+        }
+      }
+      if (this.aiContext.length === 0) {
+        this.aiContext.push({
+          role: 'model',
+          parts: [{
+            text: this.prompt + Prompt
+          }]
+        })
+        this.aiContext.push({
+          role: role,
+          parts: [{
+            text: this.text
+          }]
+        })
+      } else {
+        this.aiContext.push({
+          role: role,
+          parts: [{
+            text: '工具调用成功,数据为' + JSON.stringify(tool_data)
+          }]
+        })
+      }
+      this.aiState = '0'
+      this.tool_name = ''
+      this.tool_input = ''
+      this.tool_data = ''
+      ai.models.generateContent({
+        model: model,
+        config: config,
+        contents: this.aiContext
+      }).then(response => {
+        this.parseAiData(JSON.parse(response.text))
+      }).catch(error => {
+        const errMessage = JSON.parse(error.message.split(' . ')[1])
+        this.aiState = '2'
+        this.target.innerText = '呼哟!出错了,把这些内容给到开发说不定有用:' + errMessage.error.message
+        console.log(errMessage)
+      })
+    },
+    parseAiData(data) {
+      this.target.innerText = data.context
+      this.aiContext.push({
+        role: 'model',
+        parts: [{
+          text: JSON.stringify(data)
+        }]
+      })
+      if (!data.tool_name && !data.finish) {
+        this.sentAi('若会话未完成,必须调用一个工具', 'user')
+        return
+      }
+      this.aiState = '1'
+      this.useTool(data.tool_name ? data.tool_name : '', data.data ? JSON.parse(data.data) : {})
+      if (data.finish) {
+        this.aiState = '2'
+      }
+    },
+
+    // 任务
+    runWork(workData) {
+      for (let i = 0; i < this.taskList.length; i++) {
+        if (this.taskList[i].type === workData.type && this.taskList[i].data.id === workData.data.id) {
+          this.postMessage('已有相同的任务,请勿重复添加')
+          return
+        }
+      }
+      this.taskList.push({
+        ...workData,
+        states: 'wait'
+      })
+      console.log(workData)
+      this.postMessage(workData.data.title + '加入队列')
+    },
+    postMessage(message) {
+      if (this.timer) { clearTimeout(this.timer) }
+      this.message = message
+      this.showMessage = true
+      this.timer = setTimeout(() => {
+        this.showMessage = false
+      }, 2000)
+    },
+    useTool(toolName, data) {
+      this.tool_name = toolName
+      this.aiState = '1'
+      this.toolList.forEach((item) => {
+        if (toolName === item.name) {
+          item.tool(data)
+            .then(res => {
+              this.sentAi(res.data)
+              console.log(res)
+            }).catch(err => {
+              console.log(err)
+            })
+        }
+      })
+      this.componentList.forEach((item) => {
+        if (toolName === item) {
+          this.tool_name = item
+          this.tool_data = data
+        }
+      })
+    },
+    gotoUrl(pageName) {
+      this.$router.push({
+        name: pageName
+      })
+    },
+    resetContext() {
+      this.aiContext = this.aiContext.slice(0, 0)
+      this.aiState = '1'
+      this.tool_name = ''
+      this.tool_input = ''
+      this.tool_data = {}
+      this.inProcess = false
+      this.target.innerText = ''
+      this.text = ''
+    },
+    getConfig() {
+      this.target = document.getElementById('ai-input')
+      const config = JSON.parse(localStorage.getItem('aiConfig')) || {}
+      this.aiConfig = config.setting
+      this.AiKey = config.active ? config[config.active].apiKey : ''
+      this.AiModel = config.active ? config[config.active].model : ''
+      if (!config.active) {
+        this.guide()
+      }
+    }
+  }
+})
+</script>
+<template>
+  <div :class="['ai-ball',showText?'':'hide',showMessage?'':'message-hide',workLength?'working':'',inProcess?'process':'']">
+    <div class="ai-inner" @click="showText=!showText">
+      <div class="eye" />
+      <div class="eye" />
+    </div>
+    <div :class="['page-menu',showMenu?'show':'']">
+      <div class="page-list">
+        <div v-for="page in pageList" class="list-item" @click="gotoUrl(page.name);showMenu=false">{{ page.title }}</div>
+      </div>
+      <div /><div />
+      <div class="button" @click="showMenu=!showMenu">
+        <div class="line" />
+        <div class="line" />
+        <div class="line" />
+      </div>
+    </div>
+    <div class="text-box">
+      <div id="ai-input" class="input" :contenteditable="!inProcess" @input="textInput" @keydown.ctrl.enter="sent" />
+      <div :class="['send',inProcess?aiState==='2'?'el-icon-check':'el-icon-loading':'el-icon-top']" @click="sent" />
+      <div v-for="comp in componentList" :class="['tool',tool_name===comp?'':'hide']">
+        <component
+          :is="comp"
+          :tool_name.sync="tool_name"
+          :tool_input.sync="tool_input"
+          :tool_data.sync="tool_data"
+          @runWork="runWork"
+          @sentAi="sentAi"
+        />
+      </div>
+      <div class="state">
+        <span v-if="aiState === '0'">AI助理思考中...</span>
+        <span v-if="aiState === '1'">工具调用中...</span>
+        <span v-if="aiState === '2'">对话已结束,请重新开始</span>
+        <span v-if="inProcess&&aiState==='1'" style="margin-left: auto;cursor: pointer" @click="sentAi({text:'用户反馈先前的操作或者数据存在错误,请询问是何处错误'},'model')">有误</span>
+        <span v-if="inProcess&&aiState==='1'" style="cursor: pointer" @click="sentAi({text:'用户反馈需要结束对话,若有未完成的工作确认是否放弃工作,然后结束对话'},'model')">结束</span>
+      </div>
+      <div v-show="!inProcess && text===''" class="about-text">
+        <span>{{ aboutText }}</span>
+      </div>
+    </div>
+    <div class="message">
+      <div class="inner">{{ message }}</div>
+    </div>
+    <div class="counter">
+      {{ workLength }}
+    </div>
+  </div>
+</template>
+
+<style scoped lang="scss">
+  .ai-ball{
+    filter: drop-shadow( 0 0 8px #4F46E522);
+    width: 70px;
+    height: 70px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    border-radius: 50%;
+    background: #DBEAFE;
+    position: fixed;
+    z-index: 100;
+    right: 40px;
+    bottom: 120px;
+    .page-menu{
+      transition-duration: 300ms;
+      background: #DBEAFE;
+      width: 44px;
+      border-radius: 22px;
+      height: 44px;
+      position: absolute;
+      bottom: 60px;
+      right: 8px;
+      pointer-events: none;
+      opacity: 0;
+      display: grid;
+      grid-template-rows: 0 44px;
+      grid-template-columns: 0 44px;
+      .page-list{
+        padding: 0;
+        overflow: hidden;
+        box-sizing: border-box;
+        .list-item{
+          box-sizing: border-box;
+          border-radius: 36px;
+          font-size: 16px;
+          padding: 6px 16px;
+          width: 100%;
+          cursor: pointer;
+          transition-duration: 300ms;
+          color: #2b2677;
+          &:hover{
+            background: #b4cbec;
+          }
+        }
+      }
+      .button{
+        border-radius: 50%;
+        cursor: pointer;
+        padding: 12px;
+        width: 44px;
+        height: 44px;
+        display: flex;
+        flex-direction: column;
+        grid-gap: 4px;
+        align-items: flex-end;
+        justify-content: center;
+        transition-duration: 300ms;
+        &:hover{
+          background: #b4cbec;
+        }
+        .line{
+          transition-duration: 300ms;
+          transform-origin: 100% 50%;
+          width: 20px;
+          height: 2px;
+          background: #4F46E5;
+        }
+      }
+      &.show{
+        width: 244px;
+        height: 344px;
+        grid-template-rows: 300px 44px;
+        grid-template-columns: 200px 44px;
+        .page-list{
+          padding: 16px;
+        }
+        .button{
+          grid-gap: 0;
+          .line{
+            &:first-child{
+              width: 14px;
+              transform:  translateY(2px) translateX(2px) rotate(40deg);
+            }
+            &:last-child{
+              width: 14px;
+              transform:  translateY(-2px) translateX(2px) rotate(-40deg);
+            }
+          }
+        }
+      }
+    }
+    .counter{
+      pointer-events: none;
+      position: absolute;
+      padding: 0 7px;
+      font-size: 16px;
+      height: 24px;
+      line-height: 24px;
+      text-align: center;
+      color: white;
+      border-radius: 12px;
+      right: -12px;
+      bottom: -2px;
+      transition-duration: 300ms;
+      background: #4F46E5;
+      scale: 0;
+    }
+    .message{
+      pointer-events: none;
+      width: 1000px;
+      height: 36px;
+      position: absolute;
+      bottom: -42px;
+      right: 60px;
+      transition-duration: 300ms;
+      .inner{
+        float: right;
+        font-size: 16px;
+        border-radius: 16px;
+        text-align: end;
+        line-height: 36px;
+        padding: 0 20px;
+        color: #2b2677;
+        background: #DBEAFE;
+        width: fit-content;
+        height: 100%;
+        overflow: hidden;
+      }
+    }
+    .text-box{
+      padding: 8px;
+      position: absolute;
+      border-radius: 26px;
+      width: 500px;
+      min-height: 50px;
+      background: #DBEAFE;
+      right: 86px;
+      bottom: 8px;
+      transition-duration: 300ms;
+      display: flex;
+      flex-wrap: wrap;
+      align-items: flex-end;
+      grid-gap: 0 10px;
+      .about-text{
+        pointer-events: none;
+        overflow: hidden;
+        width: 100%;
+        height: 100%;
+        position: absolute;
+        display: flex;
+        padding: 0 24px;
+        font-size: 16px;
+        color: gray;
+        align-items: center;
+        left: 0;
+        top: 0;
+      }
+      .tool{
+        width: 100%;
+        display: grid;
+        transition-duration: 300ms;
+        grid-template-rows: 1fr;
+        overflow: hidden;
+        padding: 10px 10px;
+        &.hide{
+          padding: 0 10px;
+          grid-template-rows: 0fr;
+        }
+      }
+      .state{
+        display: flex;
+        grid-gap: 6px;
+        transition-duration: 300ms;
+        overflow: hidden;
+        height: 0;
+        font-size: 13px;
+        margin: 0 12px;
+        width: 100%;
+        color: #2b2677
+      }
+      .send{
+        cursor: pointer;
+        background: #4F46E5;
+        width: 36px;
+        height: 36px;
+        font-size: 24px;
+        color: white;
+        line-height: 36px;
+        text-align: center;
+        transition-duration: 300ms;
+        border-radius: 50%;
+        &:hover{
+          scale: 1.08;
+        }
+      }
+      .input{
+        color: #2b2677;
+        margin: 6px 12px;
+        flex: 1;
+        height: fit-content;
+      }
+    }
+    .ai-inner{
+      cursor: pointer;
+      width: 44px;
+      height: 32px;
+      transition-duration: 300ms;
+      border-radius: 12px;
+      border-bottom-right-radius: 2px;
+      background: #4F46E5;
+      display: flex;
+      align-items: center;
+      justify-content: space-evenly;
+      @keyframes eyes {
+        0%{
+          height: 12px;
+        }
+        4%{
+          height: 0;
+        }
+        8%{
+          height: 12px;
+        }
+        12%{
+          height: 12px;
+        }
+        16%{
+          height: 0;
+        }
+        20%{
+          height: 12px;
+        }
+      }
+      &:hover{
+        transform: rotate(6deg) scale(1.04);
+      }
+    }
+    .eye{
+      position: relative;
+      height: 12px;
+      width: 6px;
+      background: white;
+      border-radius: 2px;
+      animation: 5s eyes infinite;
+      transition-duration: 300ms;
+      transform: translateX(-60%);
+    }
+    &.hide{
+      .page-menu{
+        bottom: 80px;
+        right: 4px;
+        opacity: 1;
+        pointer-events: auto;
+      }
+      .message{
+        bottom: 12px;
+        right: 86px;
+      }
+      .text-box{
+        right: 70px;
+        opacity: 0;
+        pointer-events: none;
+      }
+      .ai-inner{
+        .eye{
+          transform: translateX(0);
+        }
+      }
+    }
+    &.message-hide{
+      .message{
+        opacity: 0;
+      }
+    }
+    &.working{
+      .counter{
+        scale: 1;
+        animation: working 1s infinite;
+      }
+      @keyframes working {
+        from{
+          box-shadow: 0 0 0 0 #4F46E5ff;
+        }
+        to{
+          box-shadow: 0 0 0 6px #4F46E500;
+        }
+      }
+    }
+    &.process{
+      .text-box{
+        .input{
+          max-height: 600px;
+          overflow: hidden;
+          overflow-y: scroll;
+        }
+        .tool{
+          .init-select{
+            overflow-y: scroll;
+          }
+        }
+        .send{
+          background: #DBEAFE;
+          color: #4F46E5;
+          &:hover{
+            scale: 1;
+          }
+        }
+        .state{
+          height: 16px;
+        }
+      }
+    }
+  }
+</style>

+ 48 - 0
src/aiHelper/plugin/blogPlugin/index.vue

@@ -0,0 +1,48 @@
+<script>
+import Prompt from '!!raw-loader!./prompt.md'
+import Vue from 'vue'
+import {
+  getBlogList,
+  getBlogListByTag,
+  getBlogListByType,
+  getBlogTagList,
+  getBlogTypeList
+} from '@/api/blog'
+
+export default Vue.extend({
+  name: 'BlogPlugin',
+  data() {
+    return {
+      type: 'tool',
+      // tool:methods内所有方法将作为AI能力加载
+      // page:template将被注册为页面。
+      // comp:template将被作为小组件加载。
+      // task: methods内方法作为任务执行。
+      prompt: Prompt
+    }
+  },
+  methods: {
+    getBlogList(data) {
+      return getBlogList(data)
+    },
+    getBlogTagList(data) {
+      return getBlogTagList(data)
+    },
+    getBlogTypeList(data) {
+      return getBlogTypeList(data)
+    },
+    getBlogListByTag(data) {
+      return getBlogListByTag(data)
+    },
+    getBlogListByType(data) {
+      return getBlogListByType(data)
+    }
+  }
+})
+</script>
+
+<template />
+
+<style scoped lang="scss">
+
+</style>

+ 20 - 0
src/aiHelper/plugin/blogPlugin/prompt.md

@@ -0,0 +1,20 @@
+## 获取文章列表
+获取文章列表。新闻,博客等数据在此获取。
+tool_name:getBlogList
+data:{page:number,page_size:number}
+## 获取文章分类列表
+获取文章分类列表。
+tool_name:getBlogTypeList
+data:无
+## 获取文章标签列表
+获取文章标签列表。
+tool_name:getBlogTagList
+data:无
+## 根据分类获取文章列表
+根据分类获取文章列表。
+tool_name:getBlogListByType
+data:{type_id:number,page:number,page_size:number}
+## 根据标签获取文章列表
+根据标签获取文章列表。
+tool_name:getBlogListByTag
+data:{tag_id:number,page:number,page_size:number}

+ 9 - 0
src/aiHelper/plugin/getHtml/prompt.md

@@ -0,0 +1,9 @@
+## 获取用户界面
+获取用户当前所看到界面的html代码。
+tool_name:getPageHtml
+data:无
+## 点击用户界面元素
+对用户界面的某个元素执行点击动作。在使用此工具前,先使用getPageHtml获取用户界面
+tool_name:clickHtmlElement
+data:{selector:string}
+- selector:需要被点击元素的html选择器

+ 38 - 0
src/aiHelper/plugin/getHtml/tool.vue

@@ -0,0 +1,38 @@
+<script>
+import Prompt from '!!raw-loader!./prompt.md'
+import Vue from 'vue'
+export default Vue.extend({
+  name: 'HtmlPlugin',
+  data() {
+    return {
+      type: 'tool',
+      // tool:methods内所有方法将作为AI能力加载
+      // page:template将被注册为页面。
+      // comp:template将被作为小组件加载。
+      // task: methods内方法作为任务执行。
+      prompt: Prompt
+    }
+  },
+  methods: {
+    getPageHtml(data) {
+      return new Promise((resolve, reject) => {
+        const page = document.getElementById('app').innerHTML
+        resolve({ data: page })
+      })
+    },
+    clickHtmlElement(data) {
+      return new Promise((resolve, reject) => {
+        const element = document.querySelector(data.selector)
+        element.click()
+        resolve({ data: '元素已点击' })
+      })
+    }
+  }
+})
+</script>
+
+<template />
+
+<style scoped lang="scss">
+
+</style>

+ 47 - 0
src/aiHelper/plugin/meetingPlugin/index.vue

@@ -0,0 +1,47 @@
+<script>
+import Prompt from '!!raw-loader!./prompt.md'
+import Vue from 'vue'
+import {
+  getMeetingList,
+  getMeetingListByTag,
+  getMeetingListByType,
+  getMeetingTagList,
+  getMeetingTypeList
+} from '@/api/meeting'
+
+export default Vue.extend({
+  name: 'MeetingPlugin',
+  data() {
+    return {
+      type: 'tool',
+      // tool:methods内所有方法将作为AI能力加载
+      // page:template将被注册为页面,与Vue页面无异
+      // plugin:template将被加载至AI助手的小工具中
+      prompt: Prompt
+    }
+  },
+  methods: {
+    getMeetingList(data) {
+      return getMeetingList(data)
+    },
+    getMeetingListByTag(data) {
+      return getMeetingListByTag(data)
+    },
+    getMeetingListByType(data) {
+      return getMeetingListByType(data)
+    },
+    getMeetingTypeList(data) {
+      return getMeetingTypeList(data)
+    },
+    getMeetingTagList(data) {
+      return getMeetingTagList(data)
+    }
+  }
+})
+</script>
+
+<template />
+
+<style scoped lang="scss">
+
+</style>

+ 20 - 0
src/aiHelper/plugin/meetingPlugin/prompt.md

@@ -0,0 +1,20 @@
+## 获取会议列表
+获取会议列表。会议相关数据在此获取。
+tool_name:getMeetingList
+data:{page:number,page_size:number}
+## 获取会议分类列表
+获取会议分类列表。
+tool_name:getMeetingTypeList
+data:无
+## 获取会议标签列表
+获取会议标签列表。
+tool_name:getMeetingTagList
+data:无
+## 根据分类获取会议列表
+根据分类获取会议列表。
+tool_name:getMeetingListByType
+data:{type_id:number,page:number,page_size:number}
+## 根据标签获取会议列表
+根据标签获取会议列表。
+tool_name:getMeetingListByTag
+data:{tag_id:number,page:number,page_size:number}

+ 47 - 0
src/aiHelper/plugin/productPlugin/index.vue

@@ -0,0 +1,47 @@
+<script>
+import Prompt from '!!raw-loader!./prompt.md'
+import Vue from 'vue'
+import {
+  getProductList,
+  getProductListByTag,
+  getProductListByType,
+  getProductTagList,
+  getProductTypeList
+} from '@/api/product'
+
+export default Vue.extend({
+  name: 'ProductPlugin',
+  data() {
+    return {
+      type: 'tool',
+      // tool:methods内所有方法将作为AI能力加载
+      // page:template将被注册为页面,与Vue页面无异
+      // plugin:template将被加载至AI助手的小工具中
+      prompt: Prompt
+    }
+  },
+  methods: {
+    getProductList(data) {
+      return getProductList(data)
+    },
+    getProductListByTag(data) {
+      return getProductListByTag(data)
+    },
+    getProductListByType(data) {
+      return getProductListByType(data)
+    },
+    getProductTagList(data) {
+      return getProductTagList(data)
+    },
+    getProductTypeList(data) {
+      return getProductTypeList(data)
+    }
+  }
+})
+</script>
+
+<template />
+
+<style scoped lang="scss">
+
+</style>

+ 20 - 0
src/aiHelper/plugin/productPlugin/prompt.md

@@ -0,0 +1,20 @@
+## 获取产品列表
+获取产品列表。产品相关数据在此获取。
+tool_name:getProductList
+data:{page:number,page_size:number}
+## 获取产品分类列表
+获取产品分类列表。
+tool_name:getProductTypeList
+data:无
+## 获取产品标签列表
+获取产品标签列表。
+tool_name:getProductTagList
+data:无
+## 根据分类获取产品列表
+根据分类获取产品列表。
+tool_name:getProductListByType
+data:{type_id:number,page:number,page_size:number}
+## 根据标签获取产品列表
+根据标签获取产品列表。
+tool_name:getProductListByTag
+data:{tag_id:number,page:number,page_size:number}

+ 103 - 0
src/aiHelper/plugin/seoPlugin/comp.vue

@@ -0,0 +1,103 @@
+<script>
+import Prompt from '!!raw-loader!./prompt.md'
+import Vue from 'vue'
+export default Vue.extend({
+  name: 'StartSeo',
+  props: [
+    'tool_name',
+    'tool_input',
+    'tool_data'
+  ],
+  data() {
+    return {
+      type: 'comp',
+      prompt: Prompt,
+      tagList: [],
+      typeList: [],
+      writeConfig: {}
+    }
+  },
+  methods: {
+    delWork(index) {
+      this.tool_data.splice(index, 1)
+      if (this.tool_data.length === 0) {
+        this.$emit('sentAi', { text: '用户已处理完成所有任务' })
+      }
+    },
+    startWork(type, index) {
+      const workData = this.tool_data[index]
+      this.$emit('runWork', { data: workData, type })
+      this.tool_data.splice(index, 1)
+      if (this.tool_data.length === 0) {
+        this.$emit('sentAi', { text: '用户已处理完成所有任务' })
+      }
+    },
+    delAllWork() {
+      this.tool_data.slice(0, 0)
+      this.$emit('sentAi', { text: '用户已处理完成所有任务' })
+    },
+    startAllWork(type) {
+      this.tool_data.forEach((item, index) => {
+        const workData = item
+        this.$emit('runWork', { data: workData, type })
+      })
+      this.$emit('sentAi', { text: '用户已处理完成所有任务' })
+    }
+  }
+})
+</script>
+
+<template>
+  <div class="init-translate">
+    <div v-for="(item,index) in tool_data" class="item">
+      <span>{{ item.title }}</span>
+      <div class="button-list">
+        <div class="button" @click="delWork(index)">放弃</div>
+        <div class="button" @click="startWork('seo',index)">优化</div>
+      </div>
+    </div>
+    <div class="button-list">
+      <div class="button" @click="delAllWork">放弃余下任务</div>
+      <div class="button" @click="startAllWork('seo')">优化余下任务</div>
+    </div>
+  </div>
+</template>
+
+<style scoped lang="scss">
+.init-translate{
+  max-height: 600px;
+  overflow: hidden;
+  overflow-y: auto;
+  .item{
+    display: flex;
+    flex-direction: column;
+    width: 100%;
+    background: #c0d7f6;
+    padding: 8px 16px;
+    border-radius: 16px;
+    color: #2b2677;
+    font-size: 16px;
+    margin-bottom: 6px;
+  }
+  .button-list{
+    margin-top: 12px;
+    display: flex;
+    grid-gap: 8px;
+    justify-content: flex-end;
+  }
+  .button{
+    align-self: flex-end;
+    width: fit-content;
+    background: #c0d7f6;
+    padding: 8px 16px;
+    border-radius: 64px;
+    color: #2b2677;
+    font-size: 16px;
+    transition-duration: 300ms;
+    cursor: pointer;
+    &:hover{
+      background: #b4cbec;
+    }
+  }
+}
+</style>

+ 96 - 0
src/aiHelper/plugin/seoPlugin/components/editColumn.vue

@@ -0,0 +1,96 @@
+<template>
+  <div v-loading="loading" class="edit_table_column">
+    <el-input v-show="edit" v-model="scope.row[prop]" type="textarea" :rows="6" />
+    <div v-show="!edit" class="content" :title="scope.row[prop]" v-html="scope.row[prop]" />
+    <el-button v-show="!edit" size="mini" icon="el-icon-edit" @click="editSeoTableColumnEvent(scope,'edit')" />
+    <el-button v-show="edit" type="primary" size="mini" icon="el-icon-check" @click="editSeoTableColumnEvent(scope,'save')" />
+    <div v-if="prop === 'seo_title' && scope.row[prop]" class="limit_tips">
+      (<span v-if="word>66" style="color: red">{{ word }}</span><span v-else>{{ word }}</span>/66)
+    </div>
+    <div v-else-if="prop === 'seo_describe' && scope.row[prop]" class="limit_tips">
+      (<span v-if="word>250" style="color: red">{{ word }}</span><span v-else>{{ word }}</span>/250)
+    </div>
+    <div v-else class="limit_tips" />
+  </div>
+</template>
+
+<script>
+import { updateSEO } from '@/api/system'
+import Vue from 'vue'
+export default Vue.extend({
+  name: 'EditColumn',
+  props: {
+    prop: {
+      default: '',
+      type: String
+    },
+    scope: {
+      default: null,
+      type: Object
+    }
+  },
+  data() {
+    return {
+      loading: false,
+      edit: false
+    }
+  },
+  computed: {
+    word() {
+      return this.scope.row[this.prop].replace(/[^\x00-\xff]/g, '01').length
+    }
+  },
+  methods: {
+    editSeoTableColumnEvent(scope, type) {
+      if (type === 'edit') {
+        this.edit = true
+      } else {
+        this.loading = true
+        this.$emit('loadingEvent', true)
+        updateSEO({
+          id: scope.row.seo_id ? scope.row.seo_id : scope.row.id,
+          seo_title: scope.row.seo_title,
+          seo_keyword: scope.row.seo_keyword,
+          seo_describe: scope.row.seo_describe
+        }).then(() => {
+          this.edit = false
+          this.loading = false
+          this.$emit('save', scope)
+          this.$emit('loadingEvent', false)
+        })
+      }
+    }
+  }
+})
+</script>
+
+<style lang="scss" scoped>
+.edit_table_column {
+  position: relative;
+  .limit_tips {
+    height: 32px;
+    width: 100%;
+    display: block;
+  }
+  .content {
+    display: -webkit-box;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    -webkit-box-orient: vertical;
+    -webkit-line-clamp: 4;
+    word-break: break-word;
+  }
+  .el-button {
+    opacity: 0;
+    z-index: 2;
+    position: absolute;
+    right: 0;
+    bottom: 0;
+  }
+  &:hover {
+    .el-button {
+      opacity: 1;
+    }
+  }
+}
+</style>

+ 758 - 0
src/aiHelper/plugin/seoPlugin/page.vue

@@ -0,0 +1,758 @@
+<script>
+import Vue from 'vue'
+import {
+  getBlogList
+} from '@/api/blog'
+import {
+  getProductList
+} from '@/api/product'
+import {
+  getMeetingList
+} from '@/api/meeting'
+import {
+  getStaticPageList
+} from '@/api/static_page'
+import { updateSEO } from '@/api/system'
+import EditColumn from '@/aiHelper/plugin/seoPlugin/components/editColumn'
+
+export default Vue.extend({
+  name: 'AiSeoPage',
+  components: { EditColumn },
+  data() {
+    return {
+      type: 'page',
+      parent: 'Ai',
+      router: {
+        path: '/AiSeo',
+        name: 'AiSeo',
+        meta: {
+          title: 'SEO大师',
+          affix: false,
+          icon: 'el-icon-search',
+          roles: ['ai.seo']
+        }
+      },
+      prompt: '',
+      tabIndex: 0,
+      pageSizes: [10, 20, 50, 100],
+      loading: false,
+      worker: null,
+      window: null,
+      blog: {
+        list: [],
+        total: 0,
+        page: 1,
+        page_size: 10
+      },
+      product: {
+        list: [],
+        total: 0,
+        page: 1,
+        page_size: 10
+      },
+      meeting: {
+        list: [],
+        total: 0,
+        page: 1,
+        page_size: 10
+      },
+      static_page: {
+        list: [],
+        total: 0,
+        page: 1,
+        page_size: 10
+      },
+      editSeoTableColumn: {},
+      selectionList: []
+    }
+  },
+  mounted() {
+    this.getBlogData()
+  },
+  methods: {
+    updateTable() {
+      this.loading = true
+      this.$nextTick(() => {
+        this.loading = false
+        this.$forceUpdate()
+      })
+    },
+    editSeoTableColumnEvent(scope, type) {
+      this.loading = true
+      console.log(scope)
+      if (type === 'edit') {
+        this.blog.list[scope.$index].edit = true
+        scope.row = true
+      } else {
+        scope.row.loading = true
+        updateSEO({
+          id: scope.row.seo_id,
+          seo_title: scope.row.seo_title,
+          seo_keyword: scope.row.seo_keyword,
+          seo_describe: scope.row.seo_describe
+        }).then(res => {
+          this.blog.list[scope.$index].seo_title = scope.row.seo_title
+          this.blog.list[scope.$index].seo_keyword = scope.row.seo_keyword
+          this.blog.list[scope.$index].seo_describe = scope.row.seo_describe
+          delete this.blog.list[scope.$index].edit
+          delete scope.row.edit
+          delete scope.row.loading
+        })
+      }
+
+      this.$nextTick(() => {
+        this.$forceUpdate()
+        this.loading = false
+      })
+    },
+    handleSelectionChange(e) {
+      this.selectionList = e
+    },
+    setSeo() {
+      let type = ''
+      if (this.tabIndex === 0) {
+        type = 'blog'
+      }
+      if (this.tabIndex === 1) {
+        type = 'product'
+      }
+      if (this.tabIndex === 2) {
+        type = 'meeting'
+      }
+      if (this.tabIndex === 3) {
+        type = 'static_page'
+      }
+      this.selectionList.forEach(item => {
+        this.translate(item, type)
+      })
+    },
+    checkConfig() {
+      const aiConfig = JSON.parse(localStorage.getItem('aiConfig'))
+      if (aiConfig === null) {
+        this.$confirm('请先至少配置一个AI模型', '配置未完成', {
+          confirmButtonText: '去配置',
+          cancelButtonText: '取消',
+          type: 'warning'
+        }).then(() => {
+          this.$router.push({
+            name: 'AiModelSetting',
+            query: {
+              tabindex: 0,
+              highlight: 0
+            }
+          })
+        })
+        return false
+      }
+      if (aiConfig.setting.language === '') {
+        this.$confirm('请先配置大模型语言', '配置未完成', {
+          confirmButtonText: '去配置',
+          cancelButtonText: '取消',
+          type: 'warning'
+        }).then(() => {
+          this.$router.push({
+            name: 'AiModelSetting',
+            query: {
+              tabindex: 1,
+              highlight: 2
+            }
+          })
+        })
+        return false
+      }
+      const seoConfig = JSON.parse(localStorage.getItem('seoConfig'))
+      if (seoConfig === null) {
+        this.$confirm('请先配置文章SEO提示词', '配置未完成', {
+          confirmButtonText: '去配置',
+          cancelButtonText: '取消',
+          type: 'warning'
+        }).then(() => {
+          this.$router.push({
+            name: 'AiSeoSetting',
+            query: {
+              tabindex: 0,
+              highlight: 0
+            }
+          })
+        })
+        return false
+      }
+      if (seoConfig.blogSeo.titleData[0].value === '' && this.tabIndex === 0) {
+        this.$confirm('请先配置文章SEO提示词', '配置未完成', {
+          confirmButtonText: '去配置',
+          cancelButtonText: '取消',
+          type: 'warning'
+        }).then(() => {
+          this.$router.push({
+            name: 'AiSeoSetting',
+            query: {
+              tabindex: 0,
+              highlight: 0
+            }
+          })
+        })
+        return false
+      }
+      if (seoConfig.productSeo.titleData[0].value === '' && this.tabIndex === 1) {
+        this.$confirm('请先配置产品SEO提示词', '配置未完成', {
+          confirmButtonText: '去配置',
+          cancelButtonText: '取消',
+          type: 'warning'
+        }).then(() => {
+          this.$router.push({
+            name: 'AiSeoSetting',
+            query: {
+              tabindex: 1,
+              highlight: 1
+            }
+          })
+        })
+        return false
+      }
+      if (seoConfig.meetingSeo.titleData[0].value === '' && this.tabIndex === 2) {
+        this.$confirm('请先会议SEO提示词', '配置未完成', {
+          confirmButtonText: '去配置',
+          cancelButtonText: '取消',
+          type: 'warning'
+        }).then(() => {
+          this.$router.push({
+            name: 'AiSeoSetting',
+            query: {
+              tabindex: 2,
+              highlight: 2
+            }
+          })
+        })
+        return false
+      }
+      if (seoConfig.staticPageSeo.titleData[0].value === '' && this.tabIndex === 3) {
+        this.$confirm('请先网站SEO提示词', '配置未完成', {
+          confirmButtonText: '去配置',
+          cancelButtonText: '取消',
+          type: 'warning'
+        }).then(() => {
+          this.$router.push({
+            name: 'AiSeoSetting',
+            query: {
+              tabindex: 3,
+              highlight: 3
+            }
+          })
+        })
+        return false
+      }
+      return true
+    },
+    // 发起翻译
+    translate(row, type) {
+      if (!this.checkConfig()) {
+        return
+      }
+      this.$bus.$emit('runWork', {
+        type: 'seo',
+        message: '"' + (row.page_name ? row.page_name : row.title) + '"的SEO优化任务加入队列',
+        data: {
+          id: row.id,
+          title: row.title,
+          type: type
+        }
+      })
+    },
+    // 获取文章列表
+    getBlogData() {
+      this.loading = true
+      getBlogList({
+        page: this.blog.page,
+        page_size: this.blog.page_size,
+        is_admin: 1
+      }).then(
+        response => {
+          if (response && response.code === 0) {
+            this.blog.list = response.data.data
+            this.blog.total = response.data.total
+            console.log(response)
+          }
+          this.loading = false
+        }
+      ).catch(error => {
+        console.log(error)
+        this.loading = false
+      })
+    },
+    // 获取产品列表
+    getProductData() {
+      this.loading = true
+      getProductList({
+        page: this.product.page,
+        page_size: this.product.page_size
+      }).then(
+        response => {
+          if (response && response.code === 0) {
+            this.product.list = response.data.data
+            this.product.total = response.data.total
+            console.log(response)
+          }
+          this.loading = false
+        }
+      ).catch(error => {
+        console.log(error)
+        this.loading = false
+      })
+    },
+    // 获取会议列表
+    getMeetingData() {
+      this.loading = true
+      getMeetingList({
+        page: this.meeting.page,
+        page_size: this.meeting.page_size
+      }).then(
+        response => {
+          if (response && response.code === 0) {
+            this.meeting.list = response.data.data
+            this.meeting.total = response.data.total
+            console.log(response)
+          }
+          this.loading = false
+        }
+      ).catch(error => {
+        console.log(error)
+        this.loading = false
+      })
+    },
+    // 获取静态页面列表
+    getStaticPageData() {
+      this.loading = true
+      getStaticPageList({
+        page: this.static_page.page,
+        page_size: this.static_page.page_size
+      }).then(
+        response => {
+          if (response && response.code === 0) {
+            this.static_page.list = response.data.data
+            this.static_page.total = response.data.total
+            console.log(response)
+          }
+          this.loading = false
+        }
+      )
+    },
+
+    changeTab(index) {
+      this.tabIndex = index
+      this.selectionList = []
+      if (index === 0) { this.getBlogData() }
+      if (index === 1) { this.getProductData() }
+      if (index === 2) { this.getMeetingData() }
+      if (index === 3) { this.getStaticPageData() }
+    },
+    currentChange(name, val) {
+      this[name].page = val
+      if (this.tabIndex === 0) { this.getBlogData() }
+      if (this.tabIndex === 1) { this.getProductData() }
+      if (this.tabIndex === 2) { this.getMeetingData() }
+      if (this.tabIndex === 3) { this.getStaticPageData() }
+    },
+    sizeChange(name, val) {
+      this[name].page_size = val
+      if (this.tabIndex === 0) { this.getBlogData() }
+      if (this.tabIndex === 1) { this.getProductData() }
+      if (this.tabIndex === 2) { this.getMeetingData() }
+      if (this.tabIndex === 3) { this.getStaticPageData() }
+    }
+  }
+})
+</script>
+
+<template>
+  <div class="page-cont">
+    <div class="head-cont">
+      <div :class="['head-item',tabIndex===0?'active':'']" @click="changeTab(0)">
+        文章SEO
+      </div>
+      <div :class="['head-item',tabIndex===1?'active':'']" @click="changeTab(1)">
+        产品SEO
+      </div>
+      <div :class="['head-item',tabIndex===2?'active':'']" @click="changeTab(2)">
+        会议SEO
+      </div>
+      <div :class="['head-item',tabIndex===3?'active':'']" @click="changeTab(3)">
+        网站SEO
+      </div>
+      <div class="button" @click="setSeo">
+        批量优化
+      </div>
+    </div>
+    <div class="page-body">
+      <div v-show="tabIndex===0" class="body-item">
+        <el-table
+          v-loading="loading"
+          height="100%"
+          :data="blog.list"
+          style="width: 100%"
+          @selection-change="handleSelectionChange"
+        >
+          <el-table-column
+            type="selection"
+            width="55"
+          />
+          <el-table-column
+            prop="title"
+            :show-overflow-tooltip="true"
+            label="标题"
+          />
+          <el-table-column
+            prop="seo_title"
+            label="SEO标题"
+          >
+            <template slot-scope="scope">
+              <edit-column prop="seo_title" :scope="scope" @edit="updateTable" />
+            </template>
+          </el-table-column>
+          <el-table-column
+            prop="seo_keyword"
+            label="SEO关键字"
+          >
+            <template slot-scope="scope">
+              <edit-column prop="seo_keyword" :scope="scope" @edit="updateTable" />
+            </template>
+          </el-table-column>
+          <el-table-column
+            prop="seo_describe"
+            label="SEO描述"
+          >
+            <template slot-scope="scope">
+              <edit-column prop="seo_describe" :scope="scope" @edit="updateTable" />
+            </template>
+          </el-table-column>
+          <el-table-column
+            prop="urla"
+            :show-overflow-tooltip="true"
+            label="URL"
+          />
+          <el-table-column
+            label="操作"
+            width="200"
+            align="center"
+          >
+            <template slot-scope="scope">
+              <div class="button" @click="translate(scope.row,'blog')">优化SEO</div>
+            </template>
+          </el-table-column>
+        </el-table>
+        <el-pagination
+          :current-page="blog.page"
+          :page-sizes="pageSizes"
+          :page-size="blog.page_size"
+          layout="total, sizes, prev, pager, next, jumper"
+          :total="blog.total"
+          @size-change="sizeChange('blog',$event)"
+          @current-change="currentChange('blog',$event)"
+        />
+      </div>
+      <div v-show="tabIndex===1" class="body-item">
+        <el-table
+          v-loading="loading"
+          height="100%"
+          :data="product.list"
+          style="width: 100%"
+          @selection-change="handleSelectionChange"
+        >
+          <el-table-column
+            type="selection"
+            width="55"
+          />
+          <el-table-column
+            prop="title"
+            :show-overflow-tooltip="true"
+            label="标题"
+          />
+          <el-table-column
+            prop="seo_data.seo_title"
+            label="SEO标题"
+          >
+            <template slot-scope="scope">
+              <edit-column prop="seo_title" :scope="{row:scope.row.seo_data}" @edit="updateTable" />
+            </template>
+          </el-table-column>
+          <el-table-column
+            prop="seo_data.seo_keyword"
+            label="SEO关键字"
+          >
+            <template slot-scope="scope">
+              <edit-column prop="seo_keyword" :scope="{row:scope.row.seo_data}" @edit="updateTable" />
+            </template>
+          </el-table-column>
+          <el-table-column
+            prop="seo_data.seo_describe"
+            label="SEO描述"
+          >
+            <template slot-scope="scope">
+              <edit-column prop="seo_describe" :scope="{row:scope.row.seo_data}" @edit="updateTable" />
+            </template>
+          </el-table-column>
+          <el-table-column
+            prop="seo_data.urla"
+            :show-overflow-tooltip="true"
+            label="URL"
+          />
+          <el-table-column
+            label="操作"
+            width="200"
+            align="center"
+          >
+            <template slot-scope="scope">
+              <div class="button" @click="translate(scope.row,'product')">优化SEO</div>
+            </template>
+          </el-table-column>
+        </el-table>
+        <el-pagination
+          :current-page="product.page"
+          :page-sizes="pageSizes"
+          :page-size="product.page_size"
+          layout="total, sizes, prev, pager, next, jumper"
+          :total="product.total"
+          @size-change="sizeChange('product',$event)"
+          @current-change="currentChange('product',$event)"
+        />
+      </div>
+      <div v-show="tabIndex===2" class="body-item">
+        <el-table
+          v-loading="loading"
+          height="100%"
+          :data="meeting.list"
+          style="width: 100%"
+          @selection-change="handleSelectionChange"
+        >
+          <el-table-column
+            type="selection"
+            width="55"
+          />
+          <el-table-column
+            prop="title"
+            :show-overflow-tooltip="true"
+            label="标题"
+          />
+          <el-table-column
+            prop="seo_data.seo_title"
+            label="SEO标题"
+          >
+            <template slot-scope="scope">
+              <edit-column prop="seo_title" :scope="{row:scope.row.seo_data}" @edit="updateTable" />
+            </template>
+          </el-table-column>
+          <el-table-column
+            prop="seo_data.seo_keyword"
+            label="SEO关键字"
+          >
+            <template slot-scope="scope">
+              <edit-column prop="seo_keyword" :scope="{row:scope.row.seo_data}" @edit="updateTable" />
+            </template>
+          </el-table-column>
+          <el-table-column
+            prop="seo_data.seo_describe"
+            label="SEO描述"
+          >
+            <template slot-scope="scope">
+              <edit-column prop="seo_describe" :scope="{row:scope.row.seo_data}" @edit="updateTable" />
+            </template>
+          </el-table-column>
+          <el-table-column
+            prop="seo_data.urla"
+            :show-overflow-tooltip="true"
+            label="URL"
+          />
+          <el-table-column
+            label="操作"
+            width="200"
+            align="center"
+          >
+            <template slot-scope="scope">
+              <div class="button" @click="translate(scope.row,'meeting')">优化SEO</div>
+            </template>
+          </el-table-column>
+        </el-table>
+        <el-pagination
+          :current-page="meeting.page"
+          :page-sizes="pageSizes"
+          :page-size="meeting.page_size"
+          layout="total, sizes, prev, pager, next, jumper"
+          :total="meeting.total"
+          @size-change="sizeChange('meeting',$event)"
+          @current-change="currentChange('meeting',$event)"
+        />
+      </div>
+      <div v-show="tabIndex===3" class="body-item">
+        <el-table
+          v-loading="loading"
+          :data="static_page.list"
+          height="100%"
+          style="width: 100%"
+          @selection-change="handleSelectionChange"
+        >
+          <el-table-column
+            type="selection"
+            width="55"
+          />
+          <el-table-column
+            prop="page_name"
+            :show-overflow-tooltip="true"
+            label="标题"
+          />
+
+          <el-table-column
+            prop="seo_title"
+            label="SEO标题"
+          >
+            <template slot-scope="scope">
+              <edit-column prop="seo_title" :scope="scope" @edit="updateTable" />
+            </template>
+          </el-table-column>
+          <el-table-column
+            prop="seo_keyword"
+            label="SEO关键字"
+          >
+            <template slot-scope="scope">
+              <edit-column prop="seo_keyword" :scope="scope" @edit="updateTable" />
+            </template>
+          </el-table-column>
+          <el-table-column
+            prop="seo_describe"
+            label="SEO描述"
+          >
+            <template slot-scope="scope">
+              <edit-column prop="seo_describe" :scope="scope" @edit="updateTable" />
+            </template>
+          </el-table-column>
+          <el-table-column
+            prop="urla"
+            :show-overflow-tooltip="true"
+            label="URL"
+          />
+          <el-table-column
+            label="操作"
+            width="200"
+            align="center"
+          >
+            <template slot-scope="scope">
+              <div class="button" @click="translate(scope.row,'static_page')">优化SEO</div>
+            </template>
+          </el-table-column>
+        </el-table>
+        <el-pagination
+          :current-page="static_page.page"
+          :page-sizes="pageSizes"
+          :page-size="static_page.page_size"
+          layout="total, sizes, prev, pager, next, jumper"
+          :total="static_page.total"
+          @size-change="sizeChange('static_page',$event)"
+          @current-change="currentChange('static_page',$event)"
+        />
+      </div>
+    </div>
+  </div>
+</template>
+
+<style scoped lang="scss">
+
+.page-cont{
+  position: absolute;
+  width: 100%;
+  height: calc(100% - 20px);
+  display: grid;
+  grid-template-rows: 50px 1fr;
+  .head-cont{
+    box-sizing: border-box;
+    position: relative;
+    padding: 0 5px;
+    width: calc(100% - 10px);
+    height: 100%;
+    display: flex;
+    align-items: flex-start;
+    justify-content: center;
+    .button{
+      margin-left: auto;
+      background: #4F46E5;
+      color: white;
+      height: 40px;
+      width: 120px;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      border-radius: 6px;
+      font-size: 16px;
+      cursor: pointer;
+    }
+    .head-item{
+      font-size: 16px;
+      width: 140px;
+      cursor: pointer;
+      background: #fdfdfd;
+      height: 100%;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      position: relative;
+      box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.1);
+      &:first-child{
+        border-top-left-radius: 10px;
+      }
+      &:nth-last-child(2){
+        border-top-right-radius: 10px;
+      }
+      &.active{
+        background: white;
+        border-radius: 10px 10px 0 0;
+        z-index: 1;
+        &::after{
+          content: "";
+          position: absolute;
+          width: 100%;
+          height: 50%;
+          left: 0;
+          bottom: -25%;
+          background: white;
+        }
+      }
+    }
+  }
+  .page-body{
+    overflow: hidden;
+    overflow-y: auto;
+    margin: 0 5px 5px;
+    position: relative;
+    background: white;
+    border-radius: 0 10px 10px 10px ;
+    padding: 16px;
+    border: 1px solid #eeeeee;
+    box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.1);
+    .body-item{
+      width: 100%;
+      height: 100%;
+      display: grid;
+      grid-gap: 10px;
+      grid-template-rows: 1fr auto;
+      .button{
+        color: #4F46E5;
+        cursor: pointer;
+        width: fit-content;
+        display: inline-block;
+      }
+      .state{
+        display: inline-block;
+        padding: 2px 12px;
+        border-radius: 20px;
+        width: fit-content;
+        background: lightgray;
+        font-weight: bold;
+        &.finish{
+          background: #DCFCE7;
+          color: #166534;
+        }
+      }
+    }
+  }
+  .body{
+    height: 100%;
+  }
+}
+</style>

+ 17 - 0
src/aiHelper/plugin/seoPlugin/prompt.md

@@ -0,0 +1,17 @@
+## 发起SEO搜索优化任务
+发起SEO搜索优化任务,其中id为单个文章(blog)、产品(product)、会议(meeting)、页面(static_page)的id;type为前边括号里的内容;title为其标题。发起前需先调用getSEOTemplate工具检查对应生成模板内容是否为空。
+tool_name:startSeo
+data::[{id:number,title:string,type:string}]
+说明:data中需要至少一项
+## 获取SEO生成模板
+获取SEO生成模板。
+tool_name:getSEOTemplate
+data:无
+## 前往SEO模板配置页面
+tool_name:gotoUrl
+data:{name:'AiSeoSetting',query:{tabindex:number,highlight:number}}
+说明:
+- tabindex为0,1,2,3时分别为文章SEO模板,产品SEO模板,会议SEO模板,页面SEO模板
+- 每个tab页签都有SEO标题提示词输入框(0,4,8,12),SEO关键词提示词输入框(1,5,9,13),URL模板提示词输入框(2,6,10,14),SEO描述提示词输入库(3,7,11,15)
+- 括号内第一个数是第一个tab内的对应控件,另外三个tab以此类推
+- highlight:高亮对应的控件

+ 949 - 0
src/aiHelper/plugin/seoPlugin/setting.vue

@@ -0,0 +1,949 @@
+<script lang="ts">
+import Vue from 'vue'
+import AiEditor from '@/components/AiEditor/index.vue'
+
+export default Vue.extend({
+  name: 'AiSeoSetting',
+  components: {
+    AiEditor
+  },
+  data() {
+    return {
+      type: 'page',
+      parent: 'Ai',
+      router: {
+        path: '/AiSeoSetting',
+        name: 'AiSeoSetting',
+        meta: {
+          title: 'SEO模板配置',
+          affix: false,
+          roles: ['ai.setting.seo']
+        }
+      },
+      prompt: '',
+      tabIndex: 0,
+      highlight: -1,
+      pageSeo: {
+        titleData: [
+          {
+            type: 'text',
+            value: ''
+          }
+        ],
+        titleVariableList: [
+          {
+            name: '页面标题',
+            value: 'page_name'
+          },
+          {
+            name: '发布时间',
+            value: 'create_time'
+          },
+          {
+            name: '更新时间',
+            value: 'update_time'
+          }
+        ],
+        keywordData: [
+          {
+            type: 'text',
+            value: ''
+          }
+        ],
+        keywordVariableList: [
+          {
+            name: '页面标题',
+            value: 'page_name'
+          },
+          {
+            name: '发布时间',
+            value: 'create_time'
+          },
+          {
+            name: '更新时间',
+            value: 'update_time'
+          }
+        ],
+        descData: [
+          {
+            type: 'text',
+            value: ''
+          }
+        ],
+        descVariableList: [
+          {
+            name: '页面标题',
+            value: 'page_name'
+          },
+          {
+            name: '发布时间',
+            value: 'create_time'
+          },
+          {
+            name: '更新时间',
+            value: 'update_time'
+          }
+        ],
+        urlData: [
+          {
+            type: 'text',
+            value: ''
+          }
+        ],
+        urlVariableList: [
+          {
+            name: '页面标题',
+            value: 'page_name'
+          },
+          {
+            name: '发布时间',
+            value: 'create_time'
+          },
+          {
+            name: '更新时间',
+            value: 'update_time'
+          }
+        ]
+      },
+      meetingSeo: {
+        titleData: [
+          {
+            type: 'text',
+            value: ''
+          }
+        ],
+        titleVariableList: [
+          {
+            name: '会议标题',
+            value: 'title'
+          },
+          {
+            name: '会议主办方',
+            value: 'meeting_host'
+          },
+          {
+            name: '会议地址',
+            value: 'address'
+          },
+          {
+            name: '会议城市',
+            value: 'country'
+          },
+          {
+            name: '会议分类',
+            value: 'type_ids'
+          },
+          {
+            name: '会议标签',
+            value: 'tag_ids'
+          },
+          {
+            name: '会议开始时间',
+            value: 'start_date'
+          },
+          {
+            name: '会议结束时间',
+            value: 'end_date'
+          },
+          {
+            name: '发布时间',
+            value: 'pub_date'
+          },
+          {
+            name: '更新时间',
+            value: 'update_time'
+          }
+        ],
+        keywordData: [
+          {
+            type: 'text',
+            value: ''
+          }
+        ],
+        keywordVariableList: [
+          {
+            name: '会议标题',
+            value: 'title'
+          },
+          {
+            name: '会议主办方',
+            value: 'meeting_host'
+          },
+          {
+            name: '会议地址',
+            value: 'address'
+          },
+          {
+            name: '会议城市',
+            value: 'country'
+          },
+          {
+            name: '会议分类',
+            value: 'type_ids'
+          },
+          {
+            name: '会议标签',
+            value: 'tag_ids'
+          },
+          {
+            name: '会议开始时间',
+            value: 'start_date'
+          },
+          {
+            name: '会议结束时间',
+            value: 'end_date'
+          },
+          {
+            name: '发布时间',
+            value: 'pub_date'
+          },
+          {
+            name: '更新时间',
+            value: 'update_time'
+          }
+        ],
+        descData: [
+          {
+            type: 'text',
+            value: ''
+          }
+        ],
+        descVariableList: [
+          {
+            name: '会议标题',
+            value: 'title'
+          },
+          {
+            name: '会议主办方',
+            value: 'meeting_host'
+          },
+          {
+            name: '会议地址',
+            value: 'address'
+          },
+          {
+            name: '会议城市',
+            value: 'country'
+          },
+          {
+            name: '会议分类',
+            value: 'type_ids'
+          },
+          {
+            name: '会议标签',
+            value: 'tag_ids'
+          },
+          {
+            name: '会议开始时间',
+            value: 'start_date'
+          },
+          {
+            name: '会议结束时间',
+            value: 'end_date'
+          },
+          {
+            name: '发布时间',
+            value: 'pub_date'
+          },
+          {
+            name: '更新时间',
+            value: 'update_time'
+          }
+        ],
+        urlData: [
+          {
+            type: 'text',
+            value: ''
+          }
+        ],
+        urlVariableList: [
+          {
+            name: '会议标题',
+            value: 'title'
+          },
+          {
+            name: '会议主办方',
+            value: 'meeting_host'
+          },
+          {
+            name: '会议地址',
+            value: 'address'
+          },
+          {
+            name: '会议城市',
+            value: 'country'
+          },
+          {
+            name: '会议分类',
+            value: 'type_ids'
+          },
+          {
+            name: '会议标签',
+            value: 'tag_ids'
+          },
+          {
+            name: '会议开始时间',
+            value: 'start_date'
+          },
+          {
+            name: '会议结束时间',
+            value: 'end_date'
+          },
+          {
+            name: '发布时间',
+            value: 'pub_date'
+          },
+          {
+            name: '更新时间',
+            value: 'update_time'
+          }
+        ]
+      },
+      productSeo: {
+        titleData: [
+          {
+            type: 'text',
+            value: ''
+          }
+        ],
+        titleVariableList: [
+          {
+            name: '产品名称',
+            value: 'title'
+          },
+          {
+            name: '产品分类',
+            value: 'type_ids'
+          },
+          {
+            name: '产品标签',
+            value: 'tag_ids'
+          },
+          {
+            name: '产品规格',
+            value: 'spec'
+          },
+          {
+            name: '创建时间',
+            value: 'create_time'
+          },
+          {
+            name: '发布时间',
+            value: 'pub_date'
+          },
+          {
+            name: '更新时间',
+            value: 'update_time'
+          }
+        ],
+        keywordData: [
+          {
+            type: 'text',
+            value: ''
+          }
+        ],
+        keywordVariableList: [
+          {
+            name: '产品名称',
+            value: 'title'
+          },
+          {
+            name: '产品分类',
+            value: 'type_ids'
+          },
+          {
+            name: '产品标签',
+            value: 'tag_ids'
+          },
+          {
+            name: '产品规格',
+            value: 'spec'
+          },
+          {
+            name: '创建时间',
+            value: 'create_time'
+          },
+          {
+            name: '发布时间',
+            value: 'pub_date'
+          },
+          {
+            name: '更新时间',
+            value: 'update_time'
+          }
+        ],
+        descData: [
+          {
+            type: 'text',
+            value: ''
+          }
+        ],
+        descVariableList: [
+          {
+            name: '产品名称',
+            value: 'title'
+          },
+          {
+            name: '产品分类',
+            value: 'type_ids'
+          },
+          {
+            name: '产品标签',
+            value: 'tag_ids'
+          },
+          {
+            name: '产品规格',
+            value: 'spec'
+          },
+          {
+            name: '创建时间',
+            value: 'create_time'
+          },
+          {
+            name: '发布时间',
+            value: 'pub_date'
+          },
+          {
+            name: '更新时间',
+            value: 'update_time'
+          }
+        ],
+        urlData: [
+          {
+            type: 'text',
+            value: ''
+          }
+        ],
+        urlVariableList: [
+          {
+            name: '产品名称',
+            value: 'title'
+          },
+          {
+            name: '产品分类',
+            value: 'type_ids'
+          },
+          {
+            name: '产品标签',
+            value: 'tag_ids'
+          },
+          {
+            name: '产品规格',
+            value: 'spec'
+          },
+          {
+            name: '创建时间',
+            value: 'create_time'
+          },
+          {
+            name: '发布时间',
+            value: 'pub_date'
+          },
+          {
+            name: '更新时间',
+            value: 'update_time'
+          }
+        ]
+      },
+      blogSeo: {
+        titleData: [
+          {
+            type: 'text',
+            value: ''
+          }
+        ],
+        titleVariableList: [
+          {
+            name: '文章标题',
+            value: 'title'
+          },
+          {
+            name: '文章作者',
+            value: 'author'
+          },
+          {
+            name: '文章板块',
+            value: 'plate_id'
+          },
+          {
+            name: '文章分类',
+            value: 'type_ids'
+          },
+          {
+            name: '文章标签',
+            value: 'tag_ids'
+          },
+          {
+            name: '发布时间',
+            value: 'pub_date'
+          },
+          {
+            name: '更新时间',
+            value: 'update_time'
+          }
+        ],
+        keywordData: [
+          {
+            type: 'text',
+            value: ''
+          }
+        ],
+        keywordVariableList: [
+          {
+            name: '文章标题',
+            value: 'title'
+          },
+          {
+            name: '文章作者',
+            value: 'author'
+          },
+          {
+            name: '文章板块',
+            value: 'plate_id'
+          },
+          {
+            name: '文章分类',
+            value: 'type_ids'
+          },
+          {
+            name: '文章标签',
+            value: 'tag_ids'
+          },
+          {
+            name: '发布时间',
+            value: 'pub_date'
+          },
+          {
+            name: '更新时间',
+            value: 'update_time'
+          }
+        ],
+        descData: [
+          {
+            type: 'text',
+            value: ''
+          }
+        ],
+        descVariableList: [
+          {
+            name: '文章标题',
+            value: 'title'
+          },
+          {
+            name: '文章作者',
+            value: 'author'
+          },
+          {
+            name: '文章板块',
+            value: 'plate_id'
+          },
+          {
+            name: '文章分类',
+            value: 'type_ids'
+          },
+          {
+            name: '文章标签',
+            value: 'tag_ids'
+          },
+          {
+            name: '发布时间',
+            value: 'pub_date'
+          },
+          {
+            name: '更新时间',
+            value: 'update_time'
+          }
+        ],
+        urlData: [
+          {
+            type: 'text',
+            value: ''
+          }
+        ],
+        urlVariableList: [
+          {
+            name: '文章标题',
+            value: 'title'
+          },
+          {
+            name: '文章作者',
+            value: 'author'
+          },
+          {
+            name: '文章板块',
+            value: 'plate_id'
+          },
+          {
+            name: '文章分类',
+            value: 'type_ids'
+          },
+          {
+            name: '文章标签',
+            value: 'tag_ids'
+          },
+          {
+            name: '发布时间',
+            value: 'pub_date'
+          },
+          {
+            name: '更新时间',
+            value: 'update_time'
+          }
+        ]
+      }
+    }
+  },
+  watch: {
+    $route() {
+      this.routerEvent()
+    }
+  },
+  mounted() {
+    this.routerEvent()
+    this.getData()
+  },
+  methods: {
+    routerEvent() {
+      const tabindex = this.$route.query.tabindex
+      if (tabindex !== undefined) {
+        this.tabIndex = tabindex
+      }
+      const highlight = this.$route.query.highlight
+      if (highlight !== undefined) {
+        this.highlight = highlight
+      }
+      console.log(highlight)
+    },
+    changeTab(index) {
+      this.tabIndex = index
+    },
+    save() {
+      const data = {
+        blogSeo: {},
+        productSeo: {},
+        meetingSeo: {},
+        staticPageSeo: {}
+      }
+
+      data.blogSeo.titleData = this.blogSeo.titleData
+      data.blogSeo.keywordData = this.blogSeo.keywordData
+      data.blogSeo.descData = this.blogSeo.descData
+      data.blogSeo.urlData = this.blogSeo.urlData
+      data.productSeo.titleData = this.productSeo.titleData
+      data.productSeo.keywordData = this.productSeo.keywordData
+      data.productSeo.descData = this.productSeo.descData
+      data.productSeo.urlData = this.productSeo.urlData
+      data.meetingSeo.titleData = this.meetingSeo.titleData
+      data.meetingSeo.keywordData = this.meetingSeo.keywordData
+      data.meetingSeo.descData = this.meetingSeo.descData
+      data.meetingSeo.urlData = this.meetingSeo.urlData
+      data.staticPageSeo.titleData = this.pageSeo.titleData
+      data.staticPageSeo.keywordData = this.pageSeo.keywordData
+      data.staticPageSeo.descData = this.pageSeo.descData
+      data.staticPageSeo.urlData = this.pageSeo.urlData
+      localStorage.setItem('seoConfig', JSON.stringify(data))
+      this.$message.success('保存成功')
+    },
+    getData() {
+      const data = JSON.parse(localStorage.getItem('seoConfig')) || {}
+      this.blogSeo.titleData = data.blogSeo.titleData || [
+        {
+          type: 'text',
+          value: ''
+        }
+      ]
+      this.blogSeo.keywordData = data.blogSeo.keywordData || [
+        {
+          type: 'text',
+          value: ''
+        }
+      ]
+      this.blogSeo.descData = data.blogSeo.descData || [
+        {
+          type: 'text',
+          value: ''
+        }
+      ]
+      this.blogSeo.urlData = data.blogSeo.urlData || [
+        {
+          type: 'text',
+          value: ''
+        }
+      ]
+      this.productSeo.titleData = data.productSeo.titleData || [
+        {
+          type: 'text',
+          value: ''
+        }
+      ]
+      this.productSeo.keywordData = data.productSeo.keywordData || [
+        {
+          type: 'text',
+          value: ''
+        }
+      ]
+      this.productSeo.descData = data.productSeo.descData || [
+        {
+          type: 'text',
+          value: ''
+        }
+      ]
+      this.productSeo.urlData = data.productSeo.urlData || [
+        {
+          type: 'text',
+          value: ''
+        }
+      ]
+      this.meetingSeo.titleData = data.meetingSeo.titleData || [
+        {
+          type: 'text',
+          value: ''
+        }
+      ]
+      this.meetingSeo.keywordData = data.meetingSeo.keywordData || [
+        {
+          type: 'text',
+          value: ''
+        }
+      ]
+      this.meetingSeo.descData = data.meetingSeo.descData || [
+        {
+          type: 'text',
+          value: ''
+        }
+      ]
+      this.meetingSeo.urlData = data.meetingSeo.urlData || [
+        {
+          type: 'text',
+          value: ''
+        }
+      ]
+      this.pageSeo.titleData = data.staticPageSeo.titleData || [
+        {
+          type: 'text',
+          value: ''
+        }
+      ]
+      this.pageSeo.keywordData = data.staticPageSeo.keywordData || [
+        {
+          type: 'text',
+          value: ''
+        }
+      ]
+      this.pageSeo.descData = data.staticPageSeo.descData || [
+        {
+          type: 'text',
+          value: ''
+        }
+      ]
+      this.pageSeo.urlData = data.staticPageSeo.urlData || [
+        {
+          type: 'text',
+          value: ''
+        }
+      ]
+    }
+  }
+})
+</script>
+
+<template>
+  <div class="page-cont">
+    <div class="head-cont">
+      <div :class="['head-item',tabIndex===0?'active':'']" @click="changeTab(0)">
+        文章SEO
+      </div>
+      <div :class="['head-item',tabIndex===1?'active':'']" @click="changeTab(1)">
+        产品SEO
+      </div>
+      <div :class="['head-item',tabIndex===2?'active':'']" @click="changeTab(2)">
+        会议SEO
+      </div>
+      <div :class="['head-item',tabIndex===3?'active':'']" @click="changeTab(3)">
+        网站SEO
+      </div>
+      <div class="button" @click="save">
+        保存
+      </div>
+    </div>
+    <div class="page-body">
+      <div v-show="tabIndex===0" class="body-item">
+        <div class="label">
+          SEO标题
+        </div>
+        <ai-editor :class="highlight===0?'high-light':''" class="editor title" :placeholder="'请编辑SEO标题模板'" :variable-list="blogSeo.titleVariableList" :editor-data="blogSeo.titleData" />
+        <div class="label">
+          SEO关键词
+        </div>
+        <ai-editor :class="highlight===1?'high-light':''" class="editor keyword" :placeholder="'请编辑SEO关键词模板'" :variable-list="blogSeo.keywordVariableList" :editor-data="blogSeo.keywordData" />
+        <div class="label">
+          URL模板
+        </div>
+        <ai-editor :class="highlight===2?'high-light':''" class="editor url" :placeholder="'请编辑URL模板'" :variable-list="blogSeo.urlVariableList" :editor-data="blogSeo.urlData" />
+        <div class="label">
+          SEO描述
+        </div>
+        <ai-editor :class="highlight===3?'high-light':''" class="editor des" :placeholder="'请编辑SEO描述模板'" :textarea="true" :variable-list="blogSeo.descVariableList" :editor-data="blogSeo.descData" />
+      </div>
+      <div v-show="tabIndex===1" class="body-item">
+        <div class="label">
+          SEO标题
+        </div>
+        <ai-editor :class="highlight===4?'high-light':''" class="editor title" :placeholder="'请编辑SEO标题模板'" :variable-list="productSeo.titleVariableList" :editor-data="productSeo.titleData" />
+        <div class="label">
+          SEO关键词
+        </div>
+        <ai-editor :class="highlight===5?'high-light':''" class="editor keyword" :placeholder="'请编辑SEO关键词模板'" :variable-list="productSeo.keywordVariableList" :editor-data="productSeo.keywordData" />
+        <div class="label">
+          URL模板
+        </div>
+        <ai-editor :class="highlight===6?'high-light':''" class="editor url" :placeholder="'请编辑URL模板'" :variable-list="productSeo.urlVariableList" :editor-data="productSeo.urlData" />
+        <div class="label">
+          SEO描述
+        </div>
+        <ai-editor :class="highlight===7?'high-light':''" class="editor des" :placeholder="'请编辑SEO描述模板'" :textarea="true" :variable-list="productSeo.descVariableList" :editor-data="productSeo.descData" />
+      </div>
+      <div v-show="tabIndex===2" class="body-item">
+        <div class="label">
+          SEO标题
+        </div>
+        <ai-editor :class="highlight===8?'high-light':''" class="editor title" :placeholder="'请编辑SEO标题模板'" :variable-list="meetingSeo.titleVariableList" :editor-data="meetingSeo.titleData" />
+        <div class="label">
+          SEO关键词
+        </div>
+        <ai-editor :class="highlight===9?'high-light':''" class="editor keyword" :placeholder="'请编辑SEO关键词模板'" :variable-list="meetingSeo.keywordVariableList" :editor-data="meetingSeo.keywordData" />
+        <div class="label">
+          URL模板
+        </div>
+        <ai-editor :class="highlight===10?'high-light':''" class="editor url" :placeholder="'请编辑URL模板'" :variable-list="meetingSeo.urlVariableList" :editor-data="meetingSeo.urlData" />
+        <div class="label">
+          SEO描述
+        </div>
+        <ai-editor :class="highlight===11?'high-light':''" class="editor des" :placeholder="'请编辑SEO描述模板'" :textarea="true" :variable-list="meetingSeo.descVariableList" :editor-data="meetingSeo.descData" />
+      </div>
+      <div v-show="tabIndex===3" class="body-item">
+        <div class="label">
+          SEO标题
+        </div>
+        <ai-editor :class="highlight===12?'high-light':''" class="editor title" :placeholder="'请编辑SEO标题模板'" :variable-list="pageSeo.titleVariableList" :editor-data="pageSeo.titleData" />
+        <div class="label">
+          SEO关键词
+        </div>
+        <ai-editor :class="highlight===13?'high-light':''" class="editor keyword" :placeholder="'请编辑SEO关键词模板'" :variable-list="pageSeo.keywordVariableList" :editor-data="pageSeo.keywordData" />
+        <div class="label">
+          URL模板
+        </div>
+        <ai-editor :class="highlight===14?'high-light':''" class="editor url" :placeholder="'请编辑URL模板'" :variable-list="pageSeo.urlVariableList" :editor-data="pageSeo.urlData" />
+        <div class="label">
+          SEO描述
+        </div>
+        <ai-editor :class="highlight===15?'high-light':''" class="editor des" :placeholder="'请编辑SEO描述模板'" :textarea="true" :variable-list="pageSeo.descVariableList" :editor-data="pageSeo.descData" />
+      </div>
+    </div>
+  </div>
+</template>
+
+<style scoped lang="scss">
+  .page-cont{
+    position: absolute;
+    width: 100%;
+    height: calc(100% - 20px);
+    display: grid;
+    grid-template-rows: 50px 1fr;
+    .head-cont{
+      box-sizing: border-box;
+      position: relative;
+      padding: 0 5px;
+      width: calc(100% - 10px);
+      height: 100%;
+      display: flex;
+      align-items: flex-start;
+      justify-content: flex-start;
+      .button{
+        margin-left: auto;
+        background: #4F46E5;
+        color: white;
+        height: 40px;
+        width: 120px;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        border-radius: 6px;
+        font-size: 16px;
+        cursor: pointer;
+      }
+      .head-item{
+        font-size: 16px;
+        width: 140px;
+        cursor: pointer;
+        background: #fdfdfd;
+        height: 100%;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        position: relative;
+        box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.1);
+        &:first-child{
+          border-top-left-radius: 10px;
+        }
+        &:nth-last-child(1){
+          border-top-right-radius: 10px;
+        }
+        &.active{
+          background: white;
+          border-radius: 10px 10px 0 0;
+          z-index: 1;
+          &::after{
+            content: "";
+            position: absolute;
+            width: 100%;
+            height: 50%;
+            left: 0;
+            bottom: -25%;
+            background: white;
+          }
+        }
+      }
+    }
+    .page-body{
+      overflow: hidden;
+      overflow-y: auto;
+      margin: 0 5px 5px;
+      position: relative;
+      background: white;
+      border-radius: 0 10px 10px 10px ;
+      padding: 16px;
+      box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.1);
+      .body-item{
+        width: 100%;
+        height: 100%;
+        display: flex;
+        flex-direction: column;
+        grid-gap: 6px;
+        .label{
+          margin-top: 10px;
+          font-size: 14px;
+        }
+        .editor{
+          position: relative;
+          &.title{
+            z-index: 5;
+          }
+          &.keyword{
+            z-index: 4;
+          }
+          &.url{
+            z-index: 3;
+          }
+          &.des{
+            z-index: 2;
+            height: 240px;
+          }
+        }
+      }
+    }
+    .body{
+      height: 100%;
+    }
+  }
+</style>

+ 158 - 0
src/aiHelper/plugin/seoPlugin/task.vue

@@ -0,0 +1,158 @@
+<script>
+import Vue from 'vue'
+import { getBlogInfo, blogSave, getBlogTagList, getBlogTypeList } from '@/api/blog'
+import { getMeetingInfo, meetingSave, getMeetingTagList, getMeetingTypeList } from '@/api/meeting'
+import { getProductInfo, productSave, getProductTagList, getProductTypeList } from '@/api/product'
+import { getStaticPageDetail, saveStaticPageData } from '@/api/static_page'
+import { GoogleGenAI, Type } from '@google/genai'
+export default Vue.extend({
+  name: 'AiSeoTask',
+  data() {
+    return {
+      type: 'task',
+      prompt: ''
+    }
+  },
+  methods: {
+    seo(data) {
+      return new Promise(async(resolve, reject) => {
+        const config = JSON.parse(localStorage.getItem('aiConfig')) || null
+        const seoConfig = JSON.parse(localStorage.getItem('seoConfig')) || null
+        const AiKey = config[config.active].apiKey
+        const AiModel = config[config.active].model
+        const Language = config.setting.language
+        const pageData = null
+        let get = null
+        let save = null
+        let seoPrompt = null
+        let getType = null
+        let getTags = null
+        const list = {}
+        if (data.type === 'blog') {
+          get = getBlogInfo
+          save = blogSave
+          seoPrompt = seoConfig.blogSeo
+          getType = getBlogTypeList
+          getTags = getBlogTagList
+        }
+        if (data.type === 'meeting') {
+          get = getMeetingInfo
+          save = meetingSave
+          seoPrompt = seoConfig.meetingSeo
+          getType = getMeetingTypeList
+          getTags = getMeetingTagList
+        }
+        if (data.type === 'product') {
+          get = getProductInfo
+          save = productSave
+          seoPrompt = seoConfig.productSeo
+          getType = getProductTypeList
+          getTags = getProductTagList
+        }
+        if (data.type === 'static_page') {
+          get = getStaticPageDetail
+          save = saveStaticPageData
+          seoPrompt = seoConfig.staticPageSeo
+        }
+        if (getTags) {
+          const data = await getTags({})
+          list.tags = data.data.data
+        }
+        if (getType) {
+          const data = await getType({})
+          list.types = data.data.data
+        }
+        get({
+          id: data.id
+        }).then(response => {
+          const pageData = response.data
+          const ai = new GoogleGenAI({
+            apiKey: AiKey
+          })
+          const config = {
+            thinkingConfig: {
+              thinkingBudget: -1
+            },
+            responseMimeType: 'application/json',
+            responseSchema: {
+              type: Type.OBJECT,
+              required: ['descData', 'urlData', 'keywordData', 'titleData'],
+              properties: {
+                descData: {
+                  type: Type.STRING
+                },
+                urlData: {
+                  type: Type.STRING
+                },
+                keywordData: {
+                  type: Type.STRING
+                },
+                titleData: {
+                  type: Type.STRING
+                }
+              }
+            }
+          }
+          ai.models.generateContent({
+            model: AiModel,
+            config: config,
+            contents: [
+              {
+                role: 'model',
+                parts: [
+                  {
+                    'text': `
+                    无论用户使用何种语言提问,返回给用户的数据一律使用${Language}回答。
+                    你需要处理用户发送的数据,将数据中的'descData','keywordData','titleData','urlData' 四项根据用户的要求生成字符串后返回给用户。这些数据将被使用在用户的网站元信息中用于SEO优化。
+                    descData的内容参考提示${seoPrompt.descData[0].value}进行生成,
+                    keywordData的内容参考提示${seoPrompt.keywordData[0].value}进行生成,
+                    titleData的内容参考提示${seoPrompt.titleData[0].value}进行生成,
+                    urlData的内容参考提示${seoPrompt.urlData[0].value}进行生成,注意此项需要作为地址参数使用,不能使用特殊字符,只可使用小写字母与'-'。
+                    需要SEO优化的文章具体内容数据为${JSON.stringify(response.data)};
+                    类型和标签数据为id数组,其具体内容可在此处取得${JSON.stringify(list)}。
+                  `
+                  }
+                ]
+              },
+              {
+                role: 'user',
+                parts: [
+                  {
+                    'text': `
+                    生成数据
+                  `
+                  }
+                ]
+              }
+            ]
+          }).then(response => {
+            const data = JSON.parse(response.candidates[0].content.parts[0].text)
+            pageData.seo_data.seo_describe = data.descData
+            pageData.seo_data.seo_keyword = data.keywordData
+            pageData.seo_data.seo_title = data.titleData
+            pageData.seo_data.seo_urla = data.urlData
+            save({
+              ...pageData
+            }).then(response => {
+              resolve({ title: pageData.title })
+            }).catch(err => {
+              reject(err)
+            })
+          }).catch(err => {
+            reject(err)
+          })
+        })
+          .catch(err => {
+            reject(err)
+          })
+      })
+    }
+  }
+})
+</script>
+
+<template />
+
+<style scoped lang="scss">
+
+</style>

+ 31 - 0
src/aiHelper/plugin/seoPlugin/tool.vue

@@ -0,0 +1,31 @@
+<script>
+import Vue from 'vue'
+export default Vue.extend({
+  name: 'SeoPlugin',
+  data() {
+    return {
+      type: 'tool',
+      // tool:methods内所有方法将作为AI能力加载
+      // page:template将被注册为页面。
+      // comp:template将被作为小组件加载。
+      // task: methods内方法作为任务执行。
+      prompt: ''
+    }
+  },
+  methods: {
+    getSEOTemplate(data) {
+      return new Promise((resolve, reject) => {
+        const config = {}
+        config.data = JSON.parse(localStorage.getItem('seoConfig')) || {}
+        resolve(config)
+      })
+    }
+  }
+})
+</script>
+
+<template />
+
+<style scoped lang="scss">
+
+</style>

+ 42 - 0
src/aiHelper/plugin/staticPagePlugin/index.vue

@@ -0,0 +1,42 @@
+<script>
+import Prompt from '!!raw-loader!./prompt.md'
+import Vue from 'vue'
+import {
+  getStaticPageList,
+  getStaticPageDetail,
+  getMenuInfo
+} from '@/api/static_page'
+
+export default Vue.extend({
+  name: 'StaticPagePlugin',
+  data() {
+    return {
+      type: 'tool',
+      // tool:methods内所有方法将作为AI能力加载
+      // page:template将被注册为页面,与Vue页面无异
+      // plugin:template将被加载至AI助手的小工具中
+      prompt: Prompt
+    }
+  },
+  methods: {
+    getPageList(data) {
+      return getStaticPageList(data)
+    },
+    getPageData(data) {
+      return getStaticPageDetail(data)
+    },
+    getPageMenuInfo(data) {
+      return getMenuInfo({
+        data,
+        route_path: 'layout_menu.index'
+      })
+    }
+  }
+})
+</script>
+
+<template />
+
+<style scoped lang="scss">
+
+</style>

+ 20 - 0
src/aiHelper/plugin/staticPagePlugin/prompt.md

@@ -0,0 +1,20 @@
+## 获取静态页面列表
+获取静态页面列表,网站存在一些独立的页面,调用此工具获取页面列表数据,相应的页面名称可以通过getPageMenuInfo工具获取。
+tool_name:getPageList
+data:{page:number,page_size:number}
+## 获取静态页面数据
+获取静态页面数据,可以获取到这个独立页面的具体数据。传入入页面id进行调用。页面的组件信息在page_content下,组件内可能会有多个控件,每个控件都有一个唯一的module_key和一个key。
+tool_name:getPageData
+data:{id:number}
+## 获取网站地图数据
+获取网站地图数据以及页面的父子关系,获取到的数据中name为页面名称、router为页面别称,router与getPageList工具获取到的数据中的page_name对应。
+tool_name:getPageMenuInfo
+data:无
+## 前往网站独立页面编辑页
+tool_name:gotoUrl
+data:{name:'StaticPageEdit',query:{id:number,m_key:string,t_key:string}}
+说明:
+- id为该页面的id
+- m_key为所需要聚焦的控件的module_key
+- t_key为所需要聚焦的控件的key
+- 可通过getPageData工具从page_content中获取

+ 103 - 0
src/aiHelper/plugin/translatePlugin/comp.vue

@@ -0,0 +1,103 @@
+<script>
+import Prompt from '!!raw-loader!./prompt.md'
+import Vue from 'vue'
+export default Vue.extend({
+  name: 'StartTranslate',
+  props: [
+    'tool_name',
+    'tool_input',
+    'tool_data'
+  ],
+  data() {
+    return {
+      type: 'comp',
+      prompt: Prompt,
+      tagList: [],
+      typeList: [],
+      writeConfig: {}
+    }
+  },
+  methods: {
+    delWork(index) {
+      this.tool_data.splice(index, 1)
+      if (this.tool_data.length === 0) {
+        this.$emit('sentAi', { text: '用户已处理完成所有任务' })
+      }
+    },
+    startWork(type, index) {
+      const workData = this.tool_data[index]
+      this.$emit('runWork', { data: workData, type })
+      this.tool_data.splice(index, 1)
+      if (this.tool_data.length === 0) {
+        this.$emit('sentAi', { text: '用户已处理完成所有任务' })
+      }
+    },
+    delAllWork() {
+      this.tool_data.slice(0, 0)
+      this.$emit('sentAi', { text: '用户已处理完成所有任务' })
+    },
+    startAllWork(type) {
+      this.tool_data.forEach((item, index) => {
+        const workData = item
+        this.$emit('runWork', { data: workData, type })
+      })
+      this.$emit('sentAi', { text: '用户已处理完成所有任务' })
+    }
+  }
+})
+</script>
+
+<template>
+  <div class="init-translate">
+    <div v-for="(item,index) in tool_data" class="item">
+      <span>{{ item.title }}</span>
+      <div class="button-list">
+        <div class="button" @click="delWork(index)">放弃</div>
+        <div class="button" @click="startWork('translate',index)">翻译</div>
+      </div>
+    </div>
+    <div class="button-list">
+      <div class="button" @click="delAllWork">翻译余下任务</div>
+      <div class="button" @click="startAllWork('translate')">优化余下任务</div>
+    </div>
+  </div>
+</template>
+
+<style scoped lang="scss">
+.init-translate{
+  max-height: 600px;
+  overflow: hidden;
+  overflow-y: auto;
+  .item{
+    display: flex;
+    flex-direction: column;
+    width: 100%;
+    background: #c0d7f6;
+    padding: 8px 16px;
+    border-radius: 16px;
+    color: #2b2677;
+    font-size: 16px;
+    margin-bottom: 6px;
+  }
+  .button-list{
+    margin-top: 12px;
+    display: flex;
+    grid-gap: 8px;
+    justify-content: flex-end;
+  }
+  .button{
+    align-self: flex-end;
+    width: fit-content;
+    background: #c0d7f6;
+    padding: 8px 16px;
+    border-radius: 64px;
+    color: #2b2677;
+    font-size: 16px;
+    transition-duration: 300ms;
+    cursor: pointer;
+    &:hover{
+      background: #b4cbec;
+    }
+  }
+}
+</style>

+ 658 - 0
src/aiHelper/plugin/translatePlugin/page.vue

@@ -0,0 +1,658 @@
+<script>
+import Vue from 'vue'
+import {
+  getBlogList
+} from '@/api/blog'
+import {
+  getProductList
+} from '@/api/product'
+import {
+  getMeetingList
+} from '@/api/meeting'
+import {
+  getStaticPageList
+} from '@/api/static_page'
+
+export default Vue.extend({
+  name: 'AiTranslatePage',
+  data() {
+    return {
+      type: 'page',
+      parent: 'Ai',
+      router: {
+        path: '/AiTranslate',
+        name: 'AiTranslate',
+        meta: {
+          title: 'AI翻译大师',
+          icon: 'el-icon-magic-stick',
+          roles: ['ai.translate']
+        }
+      },
+      prompt: '',
+      tabIndex: 0,
+      pageSizes: [10, 20, 50, 100],
+      loading: false,
+      worker: null,
+      blog: {
+        list: [],
+        total: 0,
+        page: 1,
+        page_size: 10
+      },
+      product: {
+        list: [],
+        total: 0,
+        page: 1,
+        page_size: 10
+      },
+      meeting: {
+        list: [],
+        total: 0,
+        page: 1,
+        page_size: 10
+      },
+      static_page: {
+        list: [],
+        total: 0,
+        page: 1,
+        page_size: 10
+      },
+      selectionList: []
+    }
+  },
+  mounted() {
+    this.checkConfig()
+    this.getBlogData()
+  },
+  methods: {
+    handleSelectionChange(e) {
+      this.selectionList = e
+    },
+    checkConfig() {
+      const aiConfig = JSON.parse(localStorage.getItem('aiConfig'))
+      if (aiConfig === null) {
+        this.$confirm('请先至少配置一个AI模型', '配置未完成', {
+          confirmButtonText: '去配置',
+          cancelButtonText: '取消',
+          type: 'warning'
+        }).then(() => {
+          this.$router.push({
+            name: 'AiModelSetting',
+            query: {
+              tabindex: 0,
+              highlight: 0
+            }
+          })
+        }).catch(() => {
+          this.$router.push({
+            name: 'Dashboard'
+          })
+        })
+        return
+      }
+      if (aiConfig.setting.language === '') {
+        this.$confirm('请先配置大模型语言', '配置未完成', {
+          confirmButtonText: '去配置',
+          cancelButtonText: '取消',
+          type: 'warning'
+        }).then(() => {
+          this.$router.push({
+            name: 'AiModelSetting',
+            query: {
+              tabindex: 1,
+              highlight: 2
+            }
+          })
+        }).catch(() => {
+          this.$router.push({
+            name: 'Dashboard'
+          })
+        })
+        return
+      }
+    },
+    // 发起work连接
+    initWorker() {
+      this.worker = new SharedWorker(this.getWorker(), 'aiWorker')
+      this.worker.port.postMessage({
+        type: 'connect',
+        message: 'AI翻译页面连接'
+      })
+      this.worker.port.onmessage = (e) => {
+        console.log(e.data)
+        if (e.data.type === 'success') {
+          if (e.data.data.refresh) {
+            if (this.tabIndex === 0) { this.getBlogData() }
+            if (this.tabIndex === 1) { this.getProductData() }
+            if (this.tabIndex === 2) { this.getMeetingData() }
+            if (this.tabIndex === 3) { this.getStaticPageData() }
+          }
+        }
+      }
+    },
+    translateAll() {
+      let type = ''
+      if (this.tabIndex === 0) {
+        type = 'blog'
+      }
+      if (this.tabIndex === 1) {
+        type = 'product'
+      }
+      if (this.tabIndex === 2) {
+        type = 'meeting'
+      }
+      if (this.tabIndex === 3) {
+        type = 'staticPage'
+      }
+      this.selectionList.forEach(item => {
+        this.translate(item, type)
+      })
+    },
+    // 发起翻译
+    translate(row, type) {
+      this.$bus.$emit('runWork', {
+        type: 'translate',
+        message: '"' + (row.page_name ? row.page_name : row.title) + '"的翻译任务加入队列',
+        data: {
+          id: row.id,
+          title: row.title,
+          type: type
+        }
+      })
+    },
+    // 获取文章列表
+    getBlogData() {
+      this.loading = true
+      getBlogList({
+        page: this.blog.page,
+        page_size: this.blog.page_size,
+        is_admin: 1
+      }).then(
+        response => {
+          if (response && response.code === 0) {
+            this.blog.list = response.data.data
+            this.blog.total = response.data.total
+            console.log(response)
+          }
+          this.loading = false
+        }
+      ).catch(error => {
+        console.log(error)
+        this.loading = false
+      })
+    },
+    // 获取产品列表
+    getProductData() {
+      this.loading = true
+      getProductList({
+        page: this.product.page,
+        page_size: this.product.page_size,
+        is_admin: 1
+      }).then(
+        response => {
+          if (response && response.code === 0) {
+            this.product.list = response.data.data
+            this.product.total = response.data.total
+            console.log(response)
+          }
+          this.loading = false
+        }
+      ).catch(error => {
+        console.log(error)
+        this.loading = false
+      })
+    },
+    // 获取会议列表
+    getMeetingData() {
+      this.loading = true
+      getMeetingList({
+        page: this.meeting.page,
+        page_size: this.meeting.page_size,
+        is_admin: 1
+
+      }).then(
+        response => {
+          if (response && response.code === 0) {
+            this.meeting.list = response.data.data
+            this.meeting.total = response.data.total
+            console.log(response)
+          }
+          this.loading = false
+        }
+      ).catch(error => {
+        console.log(error)
+        this.loading = false
+      })
+    },
+    // 获取静态页面列表
+    getStaticPageData() {
+      this.loading = true
+      getStaticPageList({
+        page: this.static_page.page,
+        page_size: this.static_page.page_size,
+        is_admin: 1
+      }).then(
+        response => {
+          if (response && response.code === 0) {
+            this.static_page.list = response.data.data
+            this.static_page.total = response.data.total
+            console.log(response)
+          }
+          this.loading = false
+        }
+      )
+    },
+
+    changeTab(index) {
+      this.tabIndex = index
+      if (index === 0) { this.getBlogData() }
+      if (index === 1) { this.getProductData() }
+      if (index === 2) { this.getMeetingData() }
+      if (index === 3) { this.getStaticPageData() }
+    },
+    currentChange(name, val) {
+      this[name].page = val
+      if (this.tabIndex === 0) { this.getBlogData() }
+      if (this.tabIndex === 1) { this.getProductData() }
+      if (this.tabIndex === 2) { this.getMeetingData() }
+      if (this.tabIndex === 3) { this.getStaticPageData() }
+    },
+    sizeChange(name, val) {
+      this[name].page_size = val
+      if (this.tabIndex === 0) { this.getBlogData() }
+      if (this.tabIndex === 1) { this.getProductData() }
+      if (this.tabIndex === 2) { this.getMeetingData() }
+      if (this.tabIndex === 3) { this.getStaticPageData() }
+    }
+  }
+})
+</script>
+
+<template>
+  <div class="page-cont">
+    <div class="head-cont">
+      <div :class="['head-item',tabIndex===0?'active':'']" @click="changeTab(0)">
+        文章翻译
+      </div>
+      <div :class="['head-item',tabIndex===1?'active':'']" @click="changeTab(1)">
+        产品翻译
+      </div>
+      <div :class="['head-item',tabIndex===2?'active':'']" @click="changeTab(2)">
+        会议翻译
+      </div>
+      <div :class="['head-item',tabIndex===3?'active':'']" @click="changeTab(3)">
+        网站翻译
+      </div>
+      <div class="button" @click="translateAll">
+        批量翻译
+      </div>
+    </div>
+    <div class="page-body">
+      <div v-show="tabIndex===0" class="body-item">
+        <el-table
+          v-loading="loading"
+          height="100%"
+          :data="blog.list"
+          style="width: 100%"
+          @selection-change="handleSelectionChange"
+        >
+          <el-table-column
+            type="selection"
+            width="55"
+          />
+          <el-table-column
+            prop="title"
+            :show-overflow-tooltip="true"
+            label="标题"
+          />
+          <el-table-column
+            prop="urla"
+            :show-overflow-tooltip="true"
+            label="URL"
+          />
+          <el-table-column
+            label="状态"
+            width="200"
+            align="center"
+          >
+            <template slot-scope="scope">
+              <div v-if="['',0,null].includes(scope.row.trans_status)" class="state">
+                未翻译
+              </div>
+              <div v-else-if="scope.row.trans_status == 1" class="state working">
+                翻译中
+              </div>
+              <div v-else-if="scope.row.trans_status == 2" class="state failed">
+                翻译失败
+              </div>
+              <div v-else class="state finish">
+                {{ scope.row.trans_status }}
+              </div>
+            </template>
+          </el-table-column>
+          <el-table-column
+            label="操作"
+            fixed="right"
+            width="200"
+            align="center"
+          >
+            <template slot-scope="scope">
+              <div class="button" @click="translate(scope.row,'blog')">翻译页面</div>
+            </template>
+          </el-table-column>
+        </el-table>
+        <el-pagination
+          :current-page="blog.page"
+          :page-sizes="pageSizes"
+          :page-size="blog.page_size"
+          layout="total, sizes, prev, pager, next, jumper"
+          :total="blog.total"
+          @size-change="sizeChange('blog',$event)"
+          @current-change="currentChange('blog',$event)"
+        />
+      </div>
+      <div v-show="tabIndex===1" class="body-item">
+        <el-table
+          v-loading="loading"
+          height="100%"
+          :data="product.list"
+          style="width: 100%"
+          @selection-change="handleSelectionChange"
+        >
+          <el-table-column
+            type="selection"
+            width="55"
+          />
+          <el-table-column
+            prop="title"
+            :show-overflow-tooltip="true"
+            label="标题"
+          />
+          <el-table-column
+            prop="seo_data.urla"
+            :show-overflow-tooltip="true"
+            label="URL"
+          />
+          <el-table-column
+            label="状态"
+            width="200"
+            align="center"
+          >
+            <template slot-scope="scope">
+              <div v-if="['',0,null].includes(scope.row.seo_data.trans_status)" class="state">
+                未翻译
+              </div>
+              <div v-else-if="scope.row.seo_data.trans_status == 1" class="state working">
+                翻译中
+              </div>
+              <div v-else-if="scope.row.seo_data.trans_status == 2" class="state failed">
+                翻译失败
+              </div>
+              <div v-else class="state finish">
+                {{ scope.row.seo_data.trans_status }}
+              </div>
+            </template>
+          </el-table-column>
+          <el-table-column
+            label="操作"
+            fixed="right"
+            width="200"
+            align="center"
+          >
+            <template slot-scope="scope">
+              <div class="button" @click="translate(scope.row,'product')">翻译页面</div>
+            </template>
+          </el-table-column>
+        </el-table>
+        <el-pagination
+          :current-page="product.page"
+          :page-sizes="pageSizes"
+          :page-size="product.page_size"
+          layout="total, sizes, prev, pager, next, jumper"
+          :total="product.total"
+          @size-change="sizeChange('product',$event)"
+          @current-change="currentChange('product',$event)"
+        />
+      </div>
+      <div v-show="tabIndex===2" class="body-item">
+        <el-table
+          v-loading="loading"
+          height="100%"
+          :data="meeting.list"
+          style="width: 100%"
+          @selection-change="handleSelectionChange"
+        >
+          <el-table-column
+            type="selection"
+            width="55"
+          />
+          <el-table-column
+            prop="title"
+            :show-overflow-tooltip="true"
+            label="标题"
+          />
+          <el-table-column
+            prop="seo_data.urla"
+            :show-overflow-tooltip="true"
+            label="URL"
+          />
+          <el-table-column
+            label="状态"
+            width="200"
+            align="center"
+          >
+            <template slot-scope="scope">
+              <div v-if="['',0,null].includes(scope.row.seo_data.trans_status)" class="state">
+                未翻译
+              </div>
+              <div v-else-if="scope.row.seo_data.trans_status == 1" class="state working">
+                翻译中
+              </div>
+              <div v-else-if="scope.row.seo_data.trans_status == 2" class="state failed">
+                翻译失败
+              </div>
+              <div v-else class="state finish">
+                {{ scope.row.seo_data.trans_status }}
+              </div>
+            </template>
+          </el-table-column>
+          <el-table-column
+            label="操作"
+            fixed="right"
+            width="200"
+            align="center"
+          >
+            <template slot-scope="scope">
+              <div class="button" @click="translate(scope.row,'meeting')">翻译页面</div>
+            </template>
+          </el-table-column>
+        </el-table>
+        <el-pagination
+          :current-page="meeting.page"
+          :page-sizes="pageSizes"
+          :page-size="meeting.page_size"
+          layout="total, sizes, prev, pager, next, jumper"
+          :total="meeting.total"
+          @size-change="sizeChange('meeting',$event)"
+          @current-change="currentChange('meeting',$event)"
+        />
+      </div>
+      <div v-show="tabIndex===3" class="body-item">
+        <el-table
+          v-loading="loading"
+          :data="static_page.list"
+          height="100%"
+          style="width: 100%"
+          @selection-change="handleSelectionChange"
+        >
+          <el-table-column
+            type="selection"
+            width="55"
+          />
+          <el-table-column
+            prop="page_name"
+            :show-overflow-tooltip="true"
+            label="标题"
+          />
+          <el-table-column
+            prop="urla"
+            :show-overflow-tooltip="true"
+            label="URL"
+          />
+          <el-table-column
+            label="状态"
+            width="200"
+            align="center"
+          >
+            <template slot-scope="scope">
+              <div v-if="['',0,null].includes(scope.row.trans_status)" class="state">
+                未翻译
+              </div>
+              <div v-else-if="scope.row.trans_status == 1" class="state working">
+                翻译中
+              </div>
+              <div v-else-if="scope.row.trans_status == 2" class="state failed">
+                翻译失败
+              </div>
+              <div v-else class="state finish">
+                {{ scope.row.trans_status }}
+              </div>
+            </template>
+          </el-table-column>
+          <el-table-column
+            label="操作"
+            fixed="right"
+            width="200"
+            align="center"
+            @selection-change="handleSelectionChange"
+          >
+            <template slot-scope="scope">
+              <div class="button" @click="translate(scope.row,'staticPage')">翻译页面</div>
+            </template>
+          </el-table-column>
+        </el-table>
+        <el-pagination
+          :current-page="static_page.page"
+          :page-sizes="pageSizes"
+          :page-size="static_page.page_size"
+          layout="total, sizes, prev, pager, next, jumper"
+          :total="static_page.total"
+          @size-change="sizeChange('static_page',$event)"
+          @current-change="currentChange('static_page',$event)"
+        />
+      </div>
+    </div>
+  </div>
+</template>
+
+<style scoped lang="scss">
+  .page-cont{
+    position: absolute;
+    width: 100%;
+    height: calc(100% - 20px);
+    display: grid;
+    grid-template-rows: 50px 1fr;
+    .head-cont{
+      box-sizing: border-box;
+      position: relative;
+      padding: 0 5px;
+      width: calc(100% - 10px);
+      height: 100%;
+      display: flex;
+      align-items: flex-start;
+      justify-content: center;
+      .button{
+        margin-left: auto;
+        background: #4F46E5;
+        color: white;
+        height: 40px;
+        width: 120px;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        border-radius: 6px;
+        font-size: 16px;
+        cursor: pointer;
+      }
+      .head-item{
+        font-size: 16px;
+        width: 140px;
+        cursor: pointer;
+        background: #fdfdfd;
+        height: 100%;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        position: relative;
+        box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.1);
+        &:first-child{
+          border-top-left-radius: 10px;
+        }
+        &:nth-last-child(2){
+          border-top-right-radius: 10px;
+        }
+        &.active{
+          background: white;
+          border-radius: 10px 10px 0 0;
+          z-index: 1;
+          &::after{
+            content: "";
+            position: absolute;
+            width: 100%;
+            height: 50%;
+            left: 0;
+            bottom: -25%;
+            background: white;
+          }
+        }
+      }
+    }
+    .page-body{
+      overflow: hidden;
+      overflow-y: auto;
+      margin: 0 5px 5px;
+      position: relative;
+      background: white;
+      border-radius: 0 10px 10px 10px ;
+      padding: 16px;
+      border: 1px solid #eeeeee;
+      box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.1);
+      .body-item{
+        width: 100%;
+        height: 100%;
+        display: grid;
+        grid-gap: 10px;
+        grid-template-rows: 1fr auto;
+        .button{
+          color: #4F46E5;
+          cursor: pointer;
+          width: fit-content;
+          display: inline-block;
+        }
+        .state{
+          display: inline-block;
+          padding: 2px 12px;
+          border-radius: 20px;
+          width: fit-content;
+          background: lightgray;
+          font-weight: bold;
+          &.finish{
+            background: #DCFCE7;
+            color: #166534;
+          }
+          &.working{
+            background: #DBEAFE;
+            color: #1E40AF;
+          }
+          &.failed{
+            background: #FFE5E5;
+            color: #FF3D00;
+          }
+        }
+      }
+    }
+    .body{
+      height: 100%;
+    }
+  }
+</style>

+ 5 - 0
src/aiHelper/plugin/translatePlugin/prompt.md

@@ -0,0 +1,5 @@
+## 发起翻译任务
+发起翻译任务,其中id为单个文章(blog)、产品(product)、会议(meeting)、页面(static_page)的id;type为前边括号里的内容;title为其标题。
+tool_name:startTranslate
+data:[{id:number,title:string,type:string}]
+说明:data中需要至少一项

+ 103 - 0
src/aiHelper/plugin/translatePlugin/task.vue

@@ -0,0 +1,103 @@
+<script>
+import Vue from 'vue'
+import { getBlogInfo, blogSave } from '@/api/blog'
+import { getMeetingInfo, meetingSave } from '@/api/meeting'
+import { getProductInfo, productSave } from '@/api/product'
+import { getStaticPageDetail, saveStaticPageData } from '@/api/static_page'
+import { updateTransStatus } from '@/api/ai'
+import { GoogleGenAI, Type } from '@google/genai'
+export default Vue.extend({
+  name: 'AiTranslateTask',
+  data() {
+    return {
+      type: 'task',
+      prompt: ''
+    }
+  },
+  methods: {
+    translate(data) {
+      return new Promise((resolve, reject) => {
+        const config = JSON.parse(localStorage.getItem('aiConfig')) || null
+        const AiKey = config[config.active].apiKey
+        const AiModel = config[config.active].model
+        const Language = config.setting.language
+        const pageData = null
+        let get = null
+        let save = null
+        if (data.type === 'blog') { get = getBlogInfo; save = blogSave }
+        if (data.type === 'meeting') { get = getMeetingInfo; save = meetingSave }
+        if (data.type === 'product') { get = getProductInfo; save = productSave }
+        if (data.type === 'static_page') { get = getStaticPageDetail; save = saveStaticPageData }
+        updateTransStatus({
+          id: data.id,
+          trans_status: 1
+        })
+        get({
+          id: data.id
+        }).then(response => {
+          const ai = new GoogleGenAI({
+            apiKey: AiKey
+          })
+          const config = {
+            thinkingConfig: {
+              thinkingBudget: -1
+            },
+            responseMimeType: 'application/json'
+          }
+          ai.models.generateContent({
+            model: AiModel,
+            config: config,
+            contents: [
+              {
+                role: 'model',
+                parts: [
+                  {
+                    'text': `
+                    无论用户使用何种语言提问,返回给用户的数据一律使用${Language}回答。
+                    将用户提供的JSON数据翻译成${Language}并以纯json字符串的形式输出,不要输出任何markdown格式,输出纯字符串。
+                  `
+                  }
+                ]
+              },
+              {
+                role: 'user',
+                parts: [
+                  {
+                    'text': `
+                    翻译数据${JSON.stringify(response.data)}
+                  `
+                  }
+                ]
+              }
+            ]
+          }).then(response => {
+            const data = JSON.parse(response.candidates[0].content.parts[0].text)
+            save({
+              ...data
+            }).then(response => {
+              updateTransStatus({
+                id: data.seo_id,
+                trans_status: Language
+              })
+              resolve(response)
+            }).catch(err => {
+              reject(err)
+            })
+            console.log(data)
+          }).catch(err => {
+            reject(err)
+          })
+        }).catch(err => [
+          reject(err)
+        ])
+      })
+    }
+  }
+})
+</script>
+
+<template />
+
+<style scoped lang="scss">
+
+</style>

+ 159 - 0
src/aiHelper/plugin/writePlugin/comp.vue

@@ -0,0 +1,159 @@
+<script>
+import Prompt from '!!raw-loader!./prompt.md'
+import Vue from 'vue'
+import { getBlogTagList, getBlogTypeList } from '@/api/blog'
+export default Vue.extend({
+  name: 'StartWrite',
+  props: [
+    'tool_name',
+    'tool_input',
+    'tool_data'
+  ],
+  data() {
+    return {
+      type: 'comp',
+      prompt: Prompt,
+      tagList: [],
+      typeList: [],
+      writeConfig: {}
+    }
+  },
+  mounted() {
+    this.getWriteConfig()
+    this.getTagList()
+    this.getTypeList()
+  },
+  methods: {
+    getTemplate(id) {
+      let template = {}
+      this.writeConfig.templateList.forEach((item) => {
+        if (id === item.id) {
+          template = item
+        }
+      })
+      return template
+    },
+    getWriteConfig() {
+      this.writeConfig = JSON.parse(localStorage.getItem('writeConfig')) || {}
+    },
+    getTagList() {
+      getBlogTagList({
+        page: 1,
+        page_size: 100
+      }).then(
+        response => {
+          console.log(response)
+          if (response && response.code === 0) {
+            this.tagList = response.data.data
+          }
+        }
+      ).catch(error => {
+        console.log(error)
+      })
+    },
+    getTypeList() {
+      getBlogTypeList({
+        page: 1,
+        page_size: 100
+      }).then(
+        response => {
+          console.log(response)
+          if (response && response.code === 0) {
+            this.typeList = response.data.data
+          }
+        }
+      ).catch(error => {
+        console.log(error)
+      })
+    },
+    delWork(index) {
+      this.tool_data.splice(index, 1)
+      if (this.tool_data.length === 0) {
+        this.$emit('sentAi', { text: '用户已处理完成所有任务' })
+      }
+    },
+    startWork(type, index) {
+      const workData = this.tool_data[index]
+      workData.title = workData.writeInfo.title.text
+      workData.tagList = this.tagList
+      workData.typeList = this.typeList
+      workData.template = this.getTemplate(workData.id)
+      this.$emit('runWork', { data: workData, type })
+      this.tool_data.splice(index, 1)
+      if (this.tool_data.length === 0) {
+        this.$emit('sentAi', { text: '用户已处理完成所有任务' })
+      }
+    },
+    delAllWork() {
+      this.tool_data.slice(0, 0)
+      this.$emit('sentAi', { text: '用户已处理完成所有任务' })
+    },
+    startAllWork(type) {
+      this.tool_data.forEach((item, index) => {
+        const workData = item
+        workData.title = workData.writeInfo.title.text
+        workData.tagList = this.tagList
+        workData.typeList = this.typeList
+        workData.template = this.getTemplate(workData.id)
+        this.$emit('runWork', { data: workData, type })
+      })
+      this.$emit('sentAi', { text: '用户已处理完成所有任务' })
+    }
+  }
+})
+</script>
+
+<template>
+  <div class="init-translate">
+    <div v-for="(item,index) in tool_data" class="item">
+      <span>{{ item.writeInfo?item.writeInfo.title.text:'' }}</span>
+      <div class="button-list">
+        <div class="button" @click="delWork(index)">放弃</div>
+        <div class="button" @click="startWork('write',index)">写作</div>
+      </div>
+    </div>
+    <div class="button-list">
+      <div class="button" @click="delAllWork">放弃余下任务</div>
+      <div class="button" @click="startAllWork('write')">写作余下任务</div>
+    </div>
+  </div>
+</template>
+
+<style scoped lang="scss">
+.init-translate{
+  max-height: 600px;
+  overflow: hidden;
+  overflow-y: auto;
+  .item{
+    display: flex;
+    flex-direction: column;
+    width: 100%;
+    background: #c0d7f6;
+    padding: 8px 16px;
+    border-radius: 16px;
+    color: #2b2677;
+    font-size: 16px;
+    margin-bottom: 6px;
+  }
+  .button-list{
+    margin-top: 12px;
+    display: flex;
+    grid-gap: 8px;
+    justify-content: flex-end;
+  }
+  .button{
+    align-self: flex-end;
+    width: fit-content;
+    background: #c0d7f6;
+    padding: 8px 16px;
+    border-radius: 64px;
+    color: #2b2677;
+    font-size: 16px;
+    transition-duration: 300ms;
+    cursor: pointer;
+    &:hover{
+      background: #b4cbec;
+    }
+  }
+}
+</style>

+ 559 - 0
src/aiHelper/plugin/writePlugin/page.vue

@@ -0,0 +1,559 @@
+<script>
+import Vue from 'vue'
+import UploadFile from '@/components/UploadFile'
+import {
+  getBlogList, getBlogTypeList, getBlogTagList
+} from '@/api/blog'
+import md5 from 'js-md5'
+import axios from 'axios'
+export default Vue.extend({
+  name: 'AiWritePage',
+  components: {
+    UploadFile
+  },
+  data() {
+    return {
+      type: 'page',
+      parent: 'Ai',
+      router: {
+        path: '/AiWrite',
+        name: 'AiWrite',
+        meta: {
+          title: '文案大师',
+          affix: false,
+          icon: 'el-icon-edit',
+          roles: ['ai.write']
+        }
+      },
+      prompt: '',
+      tabIndex: 0,
+      pageSizes: [10, 20, 50, 100],
+      tagList: [],
+      typeList: [],
+      writeConfig: {},
+      loading: false,
+      worker: null,
+      templateValue: 'ep-uchZw5e0Lxe9',
+      templateList: [
+        {
+          label: '默认模板',
+          value: 'ep-uchZw5e0Lxe9'
+        }
+      ],
+      templateImage: '',
+      blog: {
+        list: [],
+        total: 0,
+        page: 1,
+        page_size: 10
+      },
+      writeInfo: {
+        title: {
+          is_ai: false,
+          text: ''
+        },
+        cover: {
+          enable_template: false,
+          url: ''
+        },
+        types: {
+          ids: [],
+          is_ai: false
+        },
+        tags: {
+          ids: [],
+          is_ai: false
+        },
+        template: {
+          id: ''
+        },
+        number: 1,
+        author: ''
+      }
+    }
+  },
+  mounted() {
+    this.checkConfig()
+    this.getBlogData()
+    this.getTagList()
+    this.getTypeList()
+    this.getWriteConfig()
+  },
+  methods: {
+    checkConfig() {
+      const aiConfig = JSON.parse(localStorage.getItem('aiConfig'))
+      if (aiConfig === null) {
+        this.$confirm('请先至少配置一个AI模型', '配置未完成', {
+          confirmButtonText: '去配置',
+          cancelButtonText: '取消',
+          type: 'warning'
+        }).then(() => {
+          this.$router.push({
+            name: 'AiModelSetting',
+            query: {
+              tabindex: 0,
+              highlight: 0
+            }
+          })
+        }).catch(() => {
+          this.$router.push({
+            name: 'Dashboard'
+          })
+        })
+        return
+      }
+      if (aiConfig.setting.language === '') {
+        this.$confirm('请先配置大模型语言', '配置未完成', {
+          confirmButtonText: '去配置',
+          cancelButtonText: '取消',
+          type: 'warning'
+        }).then(() => {
+          this.$router.push({
+            name: 'AiModelSetting',
+            query: {
+              tabindex: 1,
+              highlight: 2
+            }
+          })
+        }).catch(() => {
+          this.$router.push({
+            name: 'Dashboard'
+          })
+        })
+        return
+      }
+      const writeConfig = JSON.parse(localStorage.getItem('writeConfig'))
+      if (writeConfig === null) {
+        this.$confirm('请先至少配置一个写作模板', '配置未完成', {
+          confirmButtonText: '去配置',
+          cancelButtonText: '取消',
+          type: 'warning'
+        }).then(() => {
+          this.$router.push({
+            name: 'AiWriteSetting',
+            query: {
+              tabindex: 1,
+              highlight: 1
+            }
+          })
+        }).catch(() => {
+          this.$router.push({
+            name: 'Dashboard'
+          })
+        })
+      }
+    },
+    // 发起任务
+    runWork() {
+      if (!this.writeInfo.title.is_ai) { this.writeInfo.number = 1 }
+      let currentTemplate = {}
+      this.writeConfig.templateList.forEach((item) => {
+        if (item.id === this.writeInfo.template.id) {
+          currentTemplate = item
+        }
+      })
+      const tagList = this.tagList
+      const typeList = this.typeList
+      this.$bus.$emit('runWork', {
+        type: 'write',
+        message: '"' + currentTemplate.templateName + '"的写作任务加入队列',
+        data: {
+          title: '"' + currentTemplate.templateName + '"的写作任务',
+          tagList: tagList,
+          typeList: typeList,
+          template: currentTemplate,
+          writeInfo: this.writeInfo
+        }
+      })
+    },
+    // 获取文章列表
+    getBlogData() {
+      this.loading = true
+      getBlogList({
+        page: this.blog.page,
+        page_size: this.blog.page_size
+      }).then(
+        response => {
+          if (response && response.code === 0) {
+            this.blog.list = response.data.data
+            this.blog.total = response.data.total
+            console.log(response)
+          }
+          this.loading = false
+        }
+      ).catch(error => {
+        console.log(error)
+        this.loading = false
+      })
+    },
+    getTagList() {
+      getBlogTagList({
+        page: 1,
+        page_size: 100
+      }).then(
+        response => {
+          console.log(response)
+          if (response && response.code === 0) {
+            this.tagList = response.data.data
+          }
+        }
+      ).catch(error => {
+        console.log(error)
+      })
+    },
+    getTypeList() {
+      getBlogTypeList({
+        page: 1,
+        page_size: 100
+      }).then(
+        response => {
+          console.log(response)
+          if (response && response.code === 0) {
+            this.typeList = response.data.data
+          }
+        }
+      ).catch(error => {
+        console.log(error)
+      })
+    },
+    getWriteConfig() {
+      const config = JSON.parse(localStorage.getItem('writeConfig')) || {}
+      this.writeConfig = config
+    },
+    getTemplateImage() {
+      // URL 前缀
+      const urlPrefix = 'https://image.edgeone.app'
+      // 模板中设置的图片格式,可在 Settings 中查看
+      const format = 'png'
+      // 用户 Id,可在 Settings 中查看
+      const userId = '3e5b976c108b4febb15687047013beff'
+      // 模板 Id,可在 Settings 中查看
+      const templateId = 'ep-uchZw5e0Lxe9'
+      // 生成签名的API Key,可在 Settings 中查看
+      const apiKey = 'rC5asgUt51is'
+      // 在这里填入要修改的模板参数
+      const params = {
+        image: this.writeInfo.cover.url,
+        title: this.writeInfo.title.is_ai ? 'AI生成标题预览' : this.writeInfo.title.text
+      }
+      // 对参数key进行排序
+      const sortedKeys = Object.keys(params).sort()
+      // 对参数进行拼接
+      const searchParams = sortedKeys.map(key => `${key}=${params[key]}`).join('&')// 待签名的数据
+      const signData = JSON.stringify({
+        apiKey: apiKey,
+        searchParams: searchParams
+      })
+      // 调用 md5 生成签名
+      const sign = md5(signData)
+      // 对 URL 参数的值执行 encodeURIComponent,以编码 URL 中的特殊字符
+      const encodedSearchParams = sortedKeys.map(key => `${key}=${encodeURIComponent(params[key])}`).join('&')
+      const finalUrl = urlPrefix + '/' + sign + '/' + userId + '/' + templateId + '.' + format + '?' + encodedSearchParams
+      this.templateImage = finalUrl
+    },
+    changeTab(index) {
+      this.tabIndex = index
+      if (index === 0) { this.getBlogData() }
+    },
+    currentChange(name, val) {
+      this[name].page = val
+      if (this.tabIndex === 0) { this.getBlogData() }
+    },
+    sizeChange(name, val) {
+      this[name].page_size = val
+      if (this.tabIndex === 0) { this.getBlogData() }
+    }
+  }
+})
+</script>
+
+<template>
+  <div class="page-cont">
+    <div class="head-cont">
+      <div :class="['head-item',tabIndex===0?'active':'']" @click="changeTab(0)">
+        文章列表
+      </div>
+      <div :class="['head-item',tabIndex===1?'active':'']" @click="changeTab(1)">
+        生成任务
+      </div>
+      <div v-show="tabIndex===1" class="button" @click="runWork()">
+        执行任务
+      </div>
+    </div>
+    <div class="page-body">
+      <div v-show="tabIndex===0" class="body-item">
+        <el-table
+          v-loading="loading"
+          height="100%"
+          :data="blog.list"
+          style="width: 100%"
+        >
+          <el-table-column
+            type="selection"
+            width="55"
+          />
+          <el-table-column
+            prop="title"
+            :show-overflow-tooltip="true"
+            label="标题"
+          />
+          <el-table-column
+            prop="seo_data.urla"
+            :show-overflow-tooltip="true"
+            label="URL"
+          />
+          <el-table-column
+            label="操作"
+            fixed="right"
+            width="200"
+            align="center"
+          >
+            <template slot-scope="scope">
+              <router-link :to="'/website/manage_blog/blog_edit?id='+scope.row.id" class="button">预览页面</router-link>
+            </template>
+          </el-table-column>
+        </el-table>
+        <el-pagination
+          :current-page="blog.page"
+          :page-sizes="pageSizes"
+          :page-size="blog.page_size"
+          layout="total, sizes, prev, pager, next, jumper"
+          :total="blog.total"
+          @size-change="sizeChange('blog',$event)"
+          @current-change="currentChange('blog',$event)"
+        />
+      </div>
+      <div v-show="tabIndex===1" class="body-item">
+        <div class="main-info">
+          <div class="cover">
+            <div class="config-item">
+              <div class="label">封面</div>
+              <div class="cover-view">
+                <img v-if="!writeInfo.cover.enable_template" :src="writeInfo.cover.url">
+                <img v-else :src="templateImage">
+              </div>
+            </div>
+            <div v-show="writeInfo.cover.enable_template" class="config-item">
+              <div class="label">选择模板</div>
+              <el-select v-model="templateValue">
+                <el-option v-for="item in templateList" :key="item.label" :value="item.value" :label="item.label" />
+              </el-select>
+              <el-button style="margin-left: 8px" @click="getTemplateImage">生成预览图片</el-button>
+            </div>
+            <div class="config-group">
+              <div class="config-item">
+                <div class="label">选择图片</div>
+                <upload-file :data.sync="writeInfo.cover.url" :show-alt="false" />
+              </div>
+              <div v-if="false" class="config-item">
+                <div class="label">启用封面模板</div>
+                <el-switch v-model="writeInfo.cover.enable_template" />
+              </div>
+            </div>
+          </div>
+          <div class="info">
+            <div class="config-group">
+              <div v-show="!writeInfo.title.is_ai" class="config-item">
+                <div class="label">标题</div>
+                <el-input v-model="writeInfo.title.text" />
+              </div>
+              <div class="config-item">
+                <div class="label">AI生成标题</div>
+                <el-switch v-model="writeInfo.title.is_ai" />
+              </div>
+            </div>
+            <div class="config-item">
+              <div class="label">作者</div>
+              <el-input v-model="writeInfo.author" />
+            </div>
+            <div class="config-group">
+              <div v-show="!writeInfo.types.is_ai" class="config-item">
+                <div class="label">文章分类</div>
+                <el-select v-model="writeInfo.types.ids" class="select">
+                  <el-option v-for="item in typeList" :key="item.id" :value="item.id" :label="item.type_name" />
+                </el-select>
+              </div>
+              <div class="config-item">
+                <div class="label">AI自行选择分类</div>
+                <el-switch v-model="writeInfo.types.is_ai" />
+              </div>
+            </div>
+            <div class="config-group">
+              <div v-show="!writeInfo.tags.is_ai" class="config-item">
+                <div class="label">文章标签</div>
+                <el-select v-model="writeInfo.tags.ids" class="select" multiple>
+                  <el-option v-for="item in tagList" :key="item.id" :value="item.id" :label="item.tag_name" />
+                </el-select>
+              </div>
+              <div class="config-item">
+                <div class="label">AI自行选择标签</div>
+                <el-switch v-model="writeInfo.tags.is_ai" />
+              </div>
+            </div>
+            <div class="config-item">
+              <div class="label">选择AI写作模板</div>
+              <el-select v-model="writeInfo.template.id" class="select">
+                <el-option v-for="item in writeConfig.templateList" :key="item.id" :value="item.id" :label="item.templateName" />
+              </el-select>
+            </div>
+            <div v-show="writeInfo.title.is_ai" class="config-item">
+              <div class="label">生成数量</div>
+              <el-slider v-model="writeInfo.number" class="slider" show-input :max="10" :min="1" />
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<style scoped lang="scss">
+.page-cont{
+  position: absolute;
+  width: 100%;
+  height: calc(100% - 20px);
+  display: grid;
+  grid-template-rows: 50px 1fr;
+  .head-cont{
+    box-sizing: border-box;
+    position: relative;
+    padding: 0 5px;
+    width: calc(100% - 10px);
+    height: 100%;
+    display: flex;
+    align-items: flex-start;
+    justify-content: flex-start;
+    .button{
+      margin-left: auto;
+      background: #4F46E5;
+      color: white;
+      height: 40px;
+      width: 120px;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      border-radius: 6px;
+      font-size: 16px;
+      cursor: pointer;
+    }
+    .head-item{
+      font-size: 16px;
+      width: 140px;
+      cursor: pointer;
+      background: #fdfdfd;
+      height: 100%;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      position: relative;
+      box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.1);
+      &:first-child{
+        border-top-left-radius: 10px;
+      }
+      &:nth-last-child(1){
+        border-top-right-radius: 10px;
+      }
+      &.active{
+        background: white;
+        border-radius: 10px 10px 0 0;
+        z-index: 1;
+        &::after{
+          content: "";
+          position: absolute;
+          width: 100%;
+          height: 50%;
+          left: 0;
+          bottom: -25%;
+          background: white;
+        }
+      }
+    }
+  }
+  .page-body{
+    overflow: hidden;
+    overflow-y: auto;
+    margin: 0 5px 5px;
+    position: relative;
+    background: white;
+    border-radius: 0 10px 10px 10px ;
+    padding: 16px;
+    border: 1px solid #eeeeee;
+    box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.1);
+    .body-item{
+      width: 100%;
+      height: 100%;
+      display: grid;
+      grid-gap: 10px;
+      grid-template-rows: 1fr auto;
+      .select{
+        width: 100%;
+      }
+      .slider{
+        margin: 0 10px;
+      }
+      .config-group{
+        display: flex;
+        grid-gap: 16px;
+        .config-item{
+          &:first-child{
+            flex: 1;
+          }
+          &:last-child{
+            width: 160px;
+          }
+        }
+      }
+
+      .label{
+        font-size: 16px;
+        margin-bottom: 8px;
+        margin-top: 8px;
+        color: gray;
+      }
+      .main-info{
+        width: 100%;
+        display: grid;
+        grid-gap: 16px;
+        grid-template-columns: 400px 1fr;
+        .cover{
+          .cover-view{
+            outline: 1px solid lightgray;
+            width: 100%;
+            aspect-ratio: 16/9;
+            img{
+              display: block;
+              object-fit: cover;
+              width: 100%;
+              height: 100%;
+            }
+          }
+        }
+        .info{
+
+        }
+      }
+      .button{
+        color: #4F46E5;
+        cursor: pointer;
+        width: fit-content;
+        display: inline-block;
+      }
+      .state{
+        display: inline-block;
+        padding: 2px 12px;
+        border-radius: 20px;
+        width: fit-content;
+        background: lightgray;
+        font-weight: bold;
+        &.finish{
+          background: #DCFCE7;
+          color: #166534;
+        }
+      }
+    }
+  }
+  .body{
+    height: 100%;
+  }
+}
+</style>

+ 38 - 0
src/aiHelper/plugin/writePlugin/prompt.md

@@ -0,0 +1,38 @@
+## 发起文章写作任务
+发起文章写作任务,其中id为写作模板id,type为seo,有is_ai属性的项请使用initSelect工具一一向用户确认每一项是否由ai生成,其它项也一一询问。发起前需先调用getWriteTemplate工具查看是否存在写作模板。
+tool_name:startWrite
+data:[{id:number,title:string,type:string,writeInfo:{
+title: {
+is_ai: boolean,  // 是否ai生成标题
+text: string,    // 若is_ai为true,则此处填写提示词
+},
+cover: {
+enable_template: boolean,  // 此处只能为false,封面模板功能目前无法使用
+url: string  // 新闻封面图片地址,使用initFile工具向用户获取
+},
+types: {
+ids: [number],  // 文章类型列表,虽是列表,但此处只能填一个
+is_ai: boolean  // 是否ai自行选择类型
+},
+tags: {
+ids: [number],  // 文章标签列表
+is_ai: boolean  // 是否ai自行选择标签
+},
+template: {
+id: string  // 写作模板id
+},
+number: number,  // 生成数量,若标题不由ai生成,则number必须为1
+author: string  // 作者名称
+}}]
+说明:list中需要至少一项
+## 获取所有AI写作模板
+获取所有AI写作模板。
+tool_name:getWriteTemplate
+data:无
+## 前往写作模板配置页
+tool_name:gotoUrl
+data:{name:'AiWriteSetting',query:{tabindex:number,highlight:number}}
+说明:
+- tabindex为0时分别为模板列表页:模板列表(0)在此处
+- tabindex为1时为模板新建页:模板名称(1),文章生成提示词(2),模板编辑器在此处(3)
+- highlight:高亮对应的控件

+ 565 - 0
src/aiHelper/plugin/writePlugin/setting.vue

@@ -0,0 +1,565 @@
+<script lang="ts">
+import Vue from 'vue'
+import draggable from 'vuedraggable'
+
+export default Vue.extend({
+  name: 'AiWriteSetting',
+  components: {
+    draggable
+  },
+  data() {
+    return {
+      type: 'page',
+      parent: 'Ai',
+      router: {
+        path: '/AiWriteSetting',
+        name: 'AiWriteSetting',
+        meta: {
+          title: '文案模板配置',
+          affix: false,
+          roles: ['ai.setting.write']
+        }
+      },
+      prompt: '',
+      tabIndex: 0,
+      dragging: false,
+      highlight: -1,
+      currentTemplate: {
+        id: '',
+        templateName: '',
+        searchKeyWord: '',
+        templatePrompt: '',
+        templateList: [
+
+        ]
+      },
+      writeConfig: {},
+      templateList: [
+        {
+          value: '标题',
+          isAi: false,
+          titleWeight: 1,
+          type: 'title'
+        },
+        {
+          value: '段落',
+          isAi: false,
+          canTitle: false,
+          type: 'paragraph'
+        },
+        {
+          value: '根据我给的提示写把',
+          isAi: true,
+          canTitle: true,
+          type: 'allInOne'
+        }
+      ]
+    }
+  },
+  watch: {
+    $route() {
+      this.routerEvent()
+    }
+  },
+  mounted() {
+    this.routerEvent()
+    this.readConfig()
+  },
+  methods: {
+    routerEvent() {
+      const tabindex = this.$route.query.tabindex
+      if (tabindex !== undefined) {
+        this.tabIndex = tabindex
+      }
+      const highlight = this.$route.query.highlight
+      if (highlight !== undefined) {
+        this.highlight = highlight
+      }
+      console.log(highlight)
+    },
+    changeTab(index) {
+      this.tabIndex = index
+    },
+    cloneData(origin) {
+      return JSON.parse(JSON.stringify(origin))
+    },
+    delItem(index) {
+      this.currentTemplate.templateList.splice(index, 1)
+    },
+    readConfig() {
+      this.writeConfig = JSON.parse(localStorage.getItem('writeConfig')) || {
+        templateList: []
+      }
+      console.log(this.writeConfig)
+    },
+    saveTemplate() {
+      if (this.currentTemplate.id) {
+        const index = this.findItem(this.currentTemplate.id).index
+        this.writeConfig.templateList[index] = this.currentTemplate
+        localStorage.setItem('writeConfig', JSON.stringify(this.writeConfig))
+        this.$message.success('保存成功')
+        this.changeTab(0)
+      } else {
+        this.currentTemplate.id = new Date().getTime() + '' + Math.random()
+        this.writeConfig.templateList.push(this.currentTemplate)
+        this.currentTemplate = {
+          id: '',
+          templateName: '',
+          searchKeyWord: '',
+          templatePrompt: '',
+          templateList: []
+        }
+        localStorage.setItem('writeConfig', JSON.stringify(this.writeConfig))
+        this.tabIndex = 0
+        this.$message.success('保存成功')
+      }
+    },
+    findItem(id) {
+      for (let i = 0; i < this.writeConfig.templateList.length; i++) {
+        if (this.writeConfig.templateList[i].id === id) {
+          return {
+            index: i,
+            data: this.writeConfig.templateList[i]
+          }
+        }
+      }
+      return null
+    },
+    edit(row) {
+      this.currentTemplate = this.findItem(row.id).data
+      this.changeTab(1)
+    },
+    copy(row) {
+      this.currentTemplate = this.cloneData(this.findItem(row.id)).data
+      this.currentTemplate.id = ''
+      this.changeTab(1)
+    },
+    del(row) {
+      this.$confirm('删除模板?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        this.writeConfig.templateList.splice(this.findItem(row.id).index, 1)
+        localStorage.setItem('writeConfig', JSON.stringify(this.writeConfig))
+        this.$message.success('已删除!')
+      }).catch(() => {
+        // 取消操作
+      })
+    }
+  }
+})
+</script>
+
+<template>
+  <div class="page-cont">
+    <div class="head-cont">
+      <div :class="['head-item',tabIndex===0?'active':'']" @click="changeTab(0)">
+        模板管理
+      </div>
+      <div :class="['head-item',tabIndex===1?'active':'']" @click="changeTab(1)">
+        模板编辑
+      </div>
+      <div v-if="tabIndex === 1" class="button" @click="saveTemplate">
+        {{ currentTemplate.id?'保存':'新建' }}
+      </div>
+    </div>
+    <div class="page-body">
+      <div v-show="tabIndex===0" class="body-item" :class="highlight===0?'high-light':''">
+        <el-table
+          class="table"
+          height="100%"
+          :data="writeConfig.templateList"
+          style="width: 100%"
+        >
+          <el-table-column
+            type="selection"
+            width="55"
+          />
+          <el-table-column
+            prop="templateName"
+            :show-overflow-tooltip="true"
+            label="模板名称"
+          />
+          <el-table-column
+            prop="templatePrompt"
+            :show-overflow-tooltip="true"
+            label="提示词"
+          />
+          <el-table-column
+            label="操作"
+            fixed="right"
+            width="200"
+            align="center"
+          >
+            <template slot-scope="scope">
+              <div class="button" @click="edit(scope.row)">编辑</div>
+              <div class="button" @click="copy(scope.row)">复制</div>
+              <div class="button" @click="del(scope.row)">删除</div>
+            </template>
+          </el-table-column>
+        </el-table>
+      </div>
+      <div v-show="tabIndex===1" class="body-item">
+        <div class="label">模板名称</div>
+        <div :class="highlight===1?'high-light':''" class="editor">
+          <input v-model="currentTemplate.templateName">
+        </div>
+        <div v-if="false" class="label">网络搜索关键字:(AI将使用该内容进行网络搜索,并参考搜索到的内容进行文章生成,需在<router-link class="link" to="/ai/setting/model">模型配置</router-link>中启用搜索能力)</div>
+        <div v-if="false" :class="highlight===2?'high-light':''" class="editor">
+          <input v-model="currentTemplate.searchKeyWord">
+        </div>
+        <div class="label">文章生成提示词:(AI生成文章时将会参考该内容)</div>
+        <div :class="highlight===3?'high-light':''" class="editor">
+          <textarea v-model="currentTemplate.templatePrompt" />
+        </div>
+        <div class="label">模板编辑器</div>
+        <div :class="highlight===4?'high-light':''" class="editor drag">
+          <draggable v-model="currentTemplate.templateList" handle=".draggable" :options="{group:{name: 'editor',put:true}}" animation="300" class="drag-editor" @start="dragging=true" @end="dragging=false">
+            <transition-group style="min-height: 300px;display: block">
+              <div v-for="(item,index) in currentTemplate.templateList" :key="index" class="editor-item">
+                <div :class="[item.type,'content',dragging?'dragging':'']">
+                  <div v-if="item.type==='title'" class="cont">
+                    <div :class="['text','h'+item.titleWeight]">{{ item.isAi?'AI生成标题':item.value }}</div>
+                    <div class="config">
+                      <div class="config-item">
+                        <div class="config-handel">
+                          <span class="label">AI生成</span>
+                          <el-switch v-model="item.isAi" class="handel" />
+                        </div>
+                        <div class="tips">此部分内容将被AI生成</div>
+                      </div>
+                      <div class="config-item">
+                        <div class="config-handel">
+                          <span class="label">{{ item.isAi?'提示词':'内容' }}</span>
+                          <el-input v-model="item.value" class="handel" :type="item.isAi?'textarea':'input'" />
+                        </div>
+                        <div class="tips">{{ item.isAi?'提示词':'标题内容' }}</div>
+                      </div>
+                      <div class="config-item">
+                        <div class="config-handel">
+                          <span class="label">标题等级</span>
+                          <el-slider v-model="item.titleWeight" class="handel" :min="1" :max="6" />
+                        </div>
+                        <div class="tips">标题等级</div>
+                      </div>
+                    </div>
+                  </div>
+                  <div v-if="item.type==='paragraph'" class="cont">
+                    <div :class="['text','h'+item.titleWeight]">{{ item.isAi?'AI生成的段落':item.value }}</div>
+                    <div class="config">
+                      <div class="config-item">
+                        <div class="config-handel">
+                          <span class="label">AI生成</span>
+                          <el-switch v-model="item.isAi" class="handel" />
+                        </div>
+                        <div class="tips">此部分内容将被AI生成</div>
+                      </div>
+                      <div v-if="item.isAi" class="config-item">
+                        <div class="config-handel">
+                          <span class="label">允许生成下级标题</span>
+                          <el-switch v-model="item.canTitle" class="handel" />
+                        </div>
+                        <div class="tips">启用选项AI可能会在段落内生成下一级标题,否则只会生成一个段落</div>
+                      </div>
+                      <div class="config-item">
+                        <div class="config-handel">
+                          <span class="label">{{ item.isAi?'提示词':'内容' }}</span>
+                          <el-input v-model="item.value" class="handel" type="textarea" />
+                        </div>
+                        <div class="tips">{{ item.isAi?'提示词':'段落内容' }}</div>
+                      </div>
+                    </div>
+                  </div>
+                  <div v-if="item.type==='allInOne'">
+                    Ai会搞定一切,不要往里添加标题或者段落了
+                  </div>
+                  <div class="drag-handel">
+                    <div class="remove el-icon-delete" @click="delItem(index)" />
+                    <div class="draggable el-icon-rank" />
+                  </div>
+                </div>
+              </div>
+            </transition-group>
+          </draggable>
+          <div class="hr" />
+          <div>
+            <draggable v-model="templateList" handle=".draggable" :clone="cloneData" :sort="false" :options="{group:{name: 'editor',pull:'clone',put:false}}" animation="300" class="drag-editor float">
+              <transition-group>
+                <div v-for="(item,index) in templateList" :key="index" class="editor-item">
+                  <div :class="[item.type,'content']">
+                    <div v-if="item.type==='title'" class="cont">
+                      <div :class="['text','h'+item.titleWeight]">{{ item.value }}</div>
+                    </div>
+                    <div v-if="item.type==='paragraph'" class="cont">
+                      <div class="text">{{ item.value }}</div>
+                    </div>
+                    <div v-if="item.type==='allInOne'" class="cont">
+                      <div class="all-in-one">全部交给AI生成</div>
+                    </div>
+                    <div class="drag-handel">
+                      <div class="draggable el-icon-rank" />
+                    </div>
+                  </div>
+                </div>
+              </transition-group>
+            </draggable>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<style scoped lang="scss">
+.page-cont {
+  position: absolute;
+  width: 100%;
+  height: calc(100% - 20px);
+  display: grid;
+  grid-template-rows: 50px 1fr;
+  .head-cont {
+    box-sizing: border-box;
+    position: relative;
+    padding: 0 5px;
+    width: calc(100% - 10px);
+    height: 100%;
+    display: flex;
+    align-items: flex-start;
+    justify-content: flex-start;
+    .button{
+      margin-left: auto;
+      background: #4F46E5;
+      color: white;
+      height: 40px;
+      width: 120px;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      border-radius: 6px;
+      font-size: 16px;
+      cursor: pointer;
+    }
+    .head-item{
+      font-size: 16px;
+      width: 140px;
+      cursor: pointer;
+      background: #fdfdfd;
+      height: 100%;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      position: relative;
+      box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.1);
+      &:first-child{
+        border-top-left-radius: 10px;
+      }
+      &:nth-last-child(2){
+        border-top-right-radius: 10px;
+      }
+      &.active{
+        background: white;
+        border-radius: 10px 10px 0 0;
+        z-index: 1;
+        &::after{
+          content: "";
+          position: absolute;
+          width: 100%;
+          height: 50%;
+          left: 0;
+          bottom: -25%;
+          background: white;
+        }
+      }
+    }
+  }
+  .page-body {
+    overflow: hidden;
+    overflow-y: auto;
+    margin: 0 5px 5px;
+    position: relative;
+    background: white;
+    border-radius: 0 10px 10px 10px;
+    padding: 16px;
+    border: 1px solid #eeeeee;
+    box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.1);
+
+    .body-item {
+      width: 100%;
+      height: 100%;
+      display: flex;
+      flex-direction: column;
+      grid-gap: 6px;
+      .table{
+        .button{
+          display: inline-block;
+          cursor: pointer;
+          color: #4F46E5;
+          margin: 0 4px;
+        }
+      }
+      .label{
+        margin-top: 10px;
+        font-size: 14px;
+        .link{
+          color: #4F46E5;
+        }
+      }
+
+      .editor {
+        display: flex;
+        flex-direction: column;
+        padding: 12px 12px;
+        border-radius: 12px;
+        width: 100%;
+        transition-duration: 300ms;
+        border: 2px solid lightgray;
+        &.drag{
+          display: grid;
+          grid-gap: 12px;
+          grid-template-columns: 1fr 1px 400px;
+        }
+        .hr{
+          height: 100%;
+          width: 100%;
+          background: lightgray;
+        }
+        .drag-editor{
+          .editor-item{
+            .content{
+              background: white;
+              padding: 4px 16px;
+              display: grid;
+              grid-template-columns: 1fr auto;
+              transition-duration: 300ms;
+              border: 1px solid transparent;
+              border-radius: 8px;
+              grid-gap: 8px;
+              .cont{
+                transition-duration: 300ms;
+                display: grid;
+                grid-template-rows: auto 0fr;
+                grid-gap: 0;
+                .config{
+                  display: flex;
+                  flex-direction: column;
+                  grid-gap: 12px;
+                  padding: 0 16px;
+                  overflow: hidden;
+                  .config-item{
+                    .config-handel{
+                      display: flex;
+                      align-items: flex-start;
+                      justify-content: space-between;
+                      .label{
+                        width: 200px;
+                        font-size: 18px;
+                      }
+                      .handel{
+                        flex: 1;
+                      }
+                    }
+                    .tips{
+                      margin-top: 6px;
+                      font-size: 14px;
+                      color: gray;
+                    }
+                  }
+                }
+              }
+              &:hover,&:has(input:focus),&:has(textarea:focus){
+                padding: 8px 16px;
+                border: 1px solid lightgray;
+                .cont{
+                  grid-template-rows: auto 1fr;
+                  grid-gap: 10px;
+                  .config{
+                  }
+                }
+              }
+              &.dragging{
+                padding: 4px 16px;
+                border: 1px solid transparent;
+                .cont{
+                  grid-template-rows: auto 0fr;
+                  grid-gap: 0;
+                  .config{
+                  }
+                }
+              }
+              &.title{
+                .cont{
+                  .text{
+                    font-weight: bold;
+                    &.h1{
+                      font-size: 36px
+                    }
+                    &.h2{
+                      font-size: 32px
+                    }
+                    &.h3{
+                      font-size: 30px
+                    }
+                    &.h4{
+                      font-size: 28px
+                    }
+                    &.h5{
+                      font-size: 26px
+                    }
+                    &.h6{
+                      font-size: 24px
+                    }
+                  }
+                }
+              }
+              &.paragraph{
+                .cont{
+                  .text{
+                    font-size: 17px;
+                  }
+                }
+              }
+              &.allInOne{
+                font-size: 17px;
+              }
+              .drag-handel{
+                font-size: 20px;
+                display: flex;
+                grid-gap: 8px;
+                .remove{
+                  cursor: pointer;
+                }
+                .draggable{
+                  align-items: center;
+                  cursor: grab;
+                  &:active{
+                    cursor: grabbing;
+                  }
+                }
+              }
+            }
+          }
+          &.float{
+            position: sticky;
+            top:0;
+          }
+        }
+        input{
+          border: none;
+          width: 100%;
+          outline: none;
+        }
+        textarea{
+          border: none;
+          outline: none;
+          width: 100%;
+          resize: vertical;
+        }
+        &:has(textarea:focus), &:has(input:focus) {
+          border: 2px solid #4F46E5;
+        }
+      }
+    }
+  }
+}
+</style>

+ 160 - 0
src/aiHelper/plugin/writePlugin/task.vue

@@ -0,0 +1,160 @@
+<script>
+import Vue from 'vue'
+import { getBlogList, getBlogTypeList, getBlogTagList, blogSave } from '@/api/blog'
+import { GoogleGenAI, Type } from '@google/genai'
+export default Vue.extend({
+  name: 'AiWriteTask',
+  data() {
+    return {
+      type: 'task',
+      prompt: ''
+    }
+  },
+  methods: {
+    write(data, that) {
+      return new Promise(async(resolve, reject) => {
+        const config = JSON.parse(localStorage.getItem('aiConfig')) || null
+        const titleList = []
+        const AiKey = config[config.active].apiKey
+        const AiModel = config[config.active].model
+        const Language = config.setting.language
+        const titleData = await getBlogList({
+          page: 1,
+          page_size: 999
+        })
+        titleData.data.data.forEach(item => {
+          titleList.push(item.title)
+        })
+        let typeList = await getBlogTypeList({})
+        typeList = typeList.data.data
+        let tagList = await getBlogTagList({})
+        tagList = tagList.data.data
+        let templateCreatWay = ''
+        data.template.templateList.forEach((item, index) => {
+          let prompt = ''
+          if (item.type === 'title') {
+            prompt = `
+              第${index}部分是一个标题,其内容${item.isAi ? '需要你根据以下提示生成:' + item.value : "值为'" + item.value + "'"}。
+            `
+          } else {
+            prompt = `
+              第${index}部分是一个段落,其内容${item.isAi ? '需要你根据以下提示生成:' + item.value : "值为'" + item.value + "'"}。${item.canTitle ? '你可以在这个部分创建多个段落,以及次级标题,也就是创建的标题等级不能高于这个段落所属的那个标题' : '你只能生成一个段落,也就是只能创建一个p标签'}。
+            `
+          }
+          templateCreatWay += prompt
+        })
+        const ai = new GoogleGenAI({
+          apiKey: AiKey
+        })
+        const aiConfig = {
+          thinkingConfig: {
+            thinkingBudget: -1
+          },
+          responseMimeType: 'application/json',
+          responseSchema: {
+            type: Type.OBJECT,
+            properties: {
+              blog_list: {
+                type: Type.ARRAY,
+                items: {
+                  type: Type.OBJECT,
+                  required: ['title', 'author', 'content', 'description', 'type_ids', 'tag_ids'],
+                  properties: {
+                    title: {
+                      type: Type.STRING
+                    },
+                    author: {
+                      type: Type.STRING
+                    },
+                    content: {
+                      type: Type.STRING
+                    },
+                    description: {
+                      type: Type.STRING
+                    },
+                    type_ids: {
+                      type: Type.ARRAY,
+                      items: {
+                        type: Type.NUMBER
+                      }
+                    },
+                    tag_ids: {
+                      type: Type.ARRAY,
+                      items: {
+                        type: Type.NUMBER
+                      }
+                    }
+                  }
+                }
+              }
+            }
+          }
+        }
+        ai.models.generateContent({
+          model: AiModel,
+          config: aiConfig,
+          contents: [
+            {
+              role: 'model',
+              parts: [
+                {
+                  text: `
+                    无论用户使用何种语言提问,返回给用户的数据一律使用${Language}回答,除非我直接告诉你这条数据值为多少。
+                      你需要按照以下规则生成指定条数的文章数据输出至blog_list数组,接下来我将告诉你单条文章数据内的每个属性该如何生成。
+                      title:这是文章标题数据,内容${data.writeInfo.title.is_ai ? '根据文章具体内容生成' : "值为'" + data.writeInfo.title.text + "'"}。生成的标题不能与${JSON.stringify(titleList)}中的文章标题有重复。
+                      author:这是文章作者,值为${data.writeInfo.author}。
+                      content:这是文章正文,${data.template.templatePrompt},使用html格式输出,你只可以使用'p','h1-h6','ul','ol','b','em','sub','sup','u','table','blockquote' 进行文章生成,${templateCreatWay}。
+                      description:这是文章梗概,根据内容生成,50个字以内。
+                      type_ids:这是文章类型,${data.writeInfo.types.is_ai ? '根据文章内容从' + JSON.stringify(typeList) + '挑选一个,将其id填入' : "值为'" + JSON.stringify(data.writeInfo.types.ids) + "'。这是所有可用类型数据,你可用根据id找到对应的数据,作为生成文章的参考:" + JSON.stringify(typeList) + '。'}
+                      tag_ids:这是文章标签,${data.writeInfo.tags.is_ai ? '根据文章内容从' + JSON.stringify(tagList) + '挑选任意数量,将其id填入' : "值为'" + JSON.stringify(data.writeInfo.tags.ids) + "'。这是所有可用标签数据,你可用根据id找到对应的数据,作为生成文章的参考:" + JSON.stringify(tagList) + '。'}
+                  `
+                }
+              ]
+            },
+            {
+              role: 'user',
+              parts: [
+                {
+                  'text': `
+                    生成${data.writeInfo.number}条文章数据
+                  `
+                }
+              ]
+            }
+          ]
+        }).then(async response => {
+          const aiData = JSON.parse(response.candidates[0].content.parts[0].text).blog_list
+          for (let i = 0; i < aiData.length; i++) {
+            const saveData = await blogSave({
+              ...aiData[i],
+              image_url: data.writeInfo.cover.url,
+              image_alt: data.writeInfo.cover.url,
+              status: 1,
+              plate_id: 1
+            })
+            that.$bus.$emit('runWork', {
+              type: 'seo',
+              message: '',
+              data: {
+                id: saveData.data,
+                title: aiData[i].title,
+                type: 'blog'
+              }
+            })
+          }
+          resolve({ title: '文章生成完成' })
+          console.log(data)
+        }).catch(err => {
+          reject(err)
+        })
+      })
+    }
+  }
+})
+</script>
+
+<template />
+
+<style scoped lang="scss">
+
+</style>

+ 31 - 0
src/aiHelper/plugin/writePlugin/tool.vue

@@ -0,0 +1,31 @@
+<script>
+import Vue from 'vue'
+export default Vue.extend({
+  name: 'WritePlugin',
+  data() {
+    return {
+      type: 'tool',
+      // tool:methods内所有方法将作为AI能力加载
+      // page:template将被注册为页面。
+      // comp:template将被作为小组件加载。
+      // task: methods内方法作为任务执行。
+      prompt: ''
+    }
+  },
+  methods: {
+    getWriteTemplate(data) {
+      return new Promise((resolve, reject) => {
+        const config = {}
+        config.data = JSON.parse(localStorage.getItem('writeConfig')) || {}
+        resolve(config)
+      })
+    }
+  }
+})
+</script>
+
+<template />
+
+<style scoped lang="scss">
+
+</style>

+ 326 - 0
src/aiHelper/readme.md

@@ -0,0 +1,326 @@
+# AI助手
+
+## 快速安装
+
+### 依赖
+
+安装前需要补全依赖库。
+
+> [package.json](../../package.json)
+
+```json
+"dependencies": {
+	"@google/genai": "^0.13.0",
+	"vue-router": "^3.5.2",
+}
+"devDependencies": {
+	"raw-loader": "^4.0.2",
+}
+```
+
+### 事件总线
+
+需要在Vue对象新建前安装事件总线用于跨组件通信。
+
+> [main.js](../main.js)
+
+```javascript
+Vue.prototype.$bus = new Vue()
+new Vue({
+  
+})
+```
+
+### 引用入口组件
+
+然后在合适的位置引入这个组件
+
+> [index.vue](./index.vue)
+
+```vue
+<template>
+    <ai-helper></ai-helper>
+</template>
+<script>
+    import AiHelper from "@/aiHelper/index.vue";
+    export default {
+      name: 'AppMain',
+      components: {
+        AiHelper
+      }
+    }
+</script>
+```
+
+### 引入完成
+
+不出意外助手已经成功引入了
+
+![recording](readme/recording.gif)
+
+## 目录结构
+
+> 系统插件和普通插件本身并无区别。一般与业务无关的可通用的插件放置在系统插件的目录,业务相关的插件放在最外层的插件目录。此外,系统插件会先于普通插件加载。
+
+![Clip_2025-08-05_15-02-07](readme/Clip_2025-08-05_15-02-07.png)
+
+## 整体结构
+
+整个助手是由一个个插件组合而成,每个插件相互独立,分别为助手提供不同的能力。
+
+![](readme/plugin.png)
+
+### 扩展AI能力
+
+AI能力本质是一个JS方法,AI会自行判断并且自由调用这些方法。为了安全起见AI能力只能编写一些数据获取的方法,扩展保存数据的能力将在扩展AI技能部分讲到。
+
+> 我们以一个获取当前时间的能力为例子介绍如何扩展AI能力。
+
+AI模型本身是不能联网的,所以它无法获取到目前的时间。我们可以为AI扩展这个能力。
+
+![Clip_2025-08-05_16-14-13](readme/Clip_2025-08-05_16-14-13.png)
+
+因为获取时间是一个通用的能力,与具体业务无关,所以在system/plugin下新建插件。首先建立一个插件文件夹。
+
+![Clip_2025-08-05_16-16-28](readme/Clip_2025-08-05_16-16-28.png)
+
+再依次建立两个文件,prompt.md用于存放提示词,tool.vue用于编写代码。
+
+![Clip_2025-08-05_16-18-46](readme/Clip_2025-08-05_16-18-46.png)
+
+根据以下格式编写提示词
+
+> [prompt.md](system/plugin/getCurrentTime/prompt.md)
+
+```markdown
+## 获取当前时间
+获取当前的系统时间。utc属性为true时返回utc时间,为false时返回本地时间。
+tool_name:getCurrentTime
+data:{utc: boolean}
+```
+
+根据以下格式编写代码
+
+> [tool.vue](system/plugin/getCurrentTime/tool.vue)
+
+```vue
+<script>
+import Vue from 'vue'
+import Prompt from '!!raw-loader!./prompt.md' //导入刚刚写的提示词
+export default Vue.extend({
+  name: "getCurrentTime", //这个名字建议修改,虽然重名不会报错
+  data() {
+    return {
+      type: 'tool', //类型为tool,必填
+      prompt: Prompt //定义提示词,必填
+    }
+  },
+  methods:{
+    //像写普通的js方法一样书写代码即可,注意因为这些代码并不会被vue初始化,所以需要注意this指向。
+    getCurrentTime(data) {//函数名不能与任何已存在的AI能力重名
+    // 返回一个Promise
+      return new Promise((resolve, reject) => {
+        if(data.utc){
+        	//返回数据需要使用一个对象包裹 
+          resolve({data:new Date().toUTCString()})
+        }else{
+          resolve({data:new Date().toLocaleString()})
+        }
+      })
+    }
+  	//一个文件内是可以定义多个方法的
+  }
+})
+</script>
+```
+
+现在回到助手再试一下,已经可以正常获取当前时间了。
+
+![Clip_2025-08-05_16-37-59](readme/Clip_2025-08-05_16-37-59.png)
+
+![Clip_2025-08-05_16-39-02](readme/Clip_2025-08-05_16-39-02.png)
+
+### 扩展用户交互
+
+可以编写vue组件来丰富AI与用户的交互。
+
+> 这里以AI对话框为例子,介绍如何扩展用户交互。
+
+与扩展能力一样,先建立文件夹和两个文件。
+
+![Clip_2025-08-05_16-47-46](readme/Clip_2025-08-05_16-47-46.png)
+
+编写提示词
+
+> [prompt.md](system/plugin/initSelect/prompt.md)
+
+```markdown
+## 展示选项对话框
+展示一个选项对话框,然后你能获取到用户选择的选项,使用此工具时需要在context中解释这个操作。
+tool_name:initSelect
+data:[{label:string,value:string}]
+说明:data中需要至少一项
+```
+
+编写组件
+
+> [index.vue](system/plugin/initSelect/index.vue)
+
+```vue
+<script>
+import Prompt from '!!raw-loader!./prompt.md' //导入提示词
+import Vue from 'vue'
+export default Vue.extend({
+  name: "initSelect",
+  data() {
+    return {
+      type: 'comp', //类型,必填
+      prompt: Prompt //提示词,必填
+    }
+  },
+  props: [
+    'tool_name', //当前正在被调用的能力的名称
+    'tool_input', //一个公用的prop
+    'tool_data' //AI传入的数据,具体需要什么样的数据需要在提示词里描述。
+  ]
+})
+    //接下来的部分和普通的vue组件一样
+</script>
+
+<template>
+  <div class="init-select">
+      使用$emit向AI发送数据
+    <div @click="$emit('sentAi',{...item})" v-for="item in tool_data" class="button">{{item.label}}</div>
+  </div>
+</template>
+
+<style scoped lang="scss">
+.init-select{
+  max-height: 600px;
+	//根容器需要一个overflow: hidden,否则即使没有被调用也会显示,可以用来调试
+  overflow: hidden;
+  .button{
+    width: 100%;
+    background: #c0d7f6;
+    padding: 8px 16px;
+    border-radius: 64px;
+    color: #2b2677;
+    font-size: 16px;
+    transition-duration: 300ms;
+    cursor: pointer;
+    &:hover{
+      background: #b4cbec;
+    }
+    &:not(:last-child){
+      margin-bottom: 6px;
+    }
+  }
+}
+</style>
+
+```
+
+现在AI可以唤起对话框了,可以看到我们编写的组件会被渲染至消息中间。
+
+![Clip_2025-08-05_16-59-20](readme/Clip_2025-08-05_16-59-20.png)
+
+### 添加附属页面
+
+还是一样建立文件夹和新建文件
+
+![Clip_2025-08-05_17-05-28](readme/Clip_2025-08-05_17-05-28.png)
+
+编写提示词,这里是共用了gotoUrl工具
+
+> [prompt.md](system/plugin/modelSetting/prompt.md)
+
+```
+## 前往AI模型基础配置页面
+tool_name:gotoUrl
+data:{name:'AiModelSetting',query:{tabindex:number,highlight:number}}
+说明:
+- tabindex为0时为模型基础配置:apiKey与模型配置(0)在此处
+- tabindex为1时为调用配置:调试模式开关(1),模型输出语言(2),总失败重试次数(3),单项任务失败重试次数(4)在此处
+- highlight:高亮对应的控件
+```
+
+像普通页面一样编写就行
+
+> [model.vue](system/plugin/modelSetting/model.vue)
+
+```vue
+<script>
+import Vue from 'vue'
+import Prompt from '!!raw-loader!./prompt.md'
+export default Vue.extend({
+  name: "aiModelSetting",
+  data() {
+    return {
+      type: 'page', //类型,必填
+      parent: 'Ai', //父页面,Ai页面是由系统插件建立的
+      router: { //这个页面的路由注册信息,必填
+        path: '/AiModelSetting',
+        name: 'AiModelSetting',
+        meta: {
+          title: '员工基础配置',
+          affix: false,
+          roles: ['ai.model.seo']
+        }
+      },
+      prompt:Prompt, //提示词,必填
+    },
+    methods:{
+        runWork(){
+            //通过事件总线向Ai发送消息
+            this.$bus.$emit('runWork',{})
+        }
+    }
+  })
+</script>
+```
+
+然后页面将会被自动注册。不过因为页面注册晚于vue初始化,所以地址是无法直接通过链接跳转的。
+
+![Clip_2025-08-05_17-18-49](readme/Clip_2025-08-05_17-18-49.png)
+
+### 扩展AI技能
+
+先建立好插件的目录结构
+
+![Clip_2025-08-05_17-30-02](readme/Clip_2025-08-05_17-30-02.png)
+
+扩展AI技能的编写方式与扩展AI能力是一样的,只有type不同。
+
+> [task.vue](plugin/translatePlugin/task.vue)
+
+```vue
+data() {
+    return {
+      type: 'task',
+      prompt:'',
+    }
+  },
+```
+
+因为AI技能不能由AI自行执行,需要先编写一个用户交互扩展,编写相关的操作逻辑。然后在界面上使用runWork将任务发送至任务队列。当处理完成后使用sentAi通知AI用户已完成操作。
+
+> [task.vue](plugin/translatePlugin/comp.vue)
+
+```vue
+<template>
+  <div class="init-translate">
+    <div v-for="(item,index) in tool_data" class="item">
+      <span>{{item.title}}</span>
+      <div class="button-list">
+        <div @click="delWork(index)" class="button">放弃</div>
+        <div @click="startWork('translate',index)" class="button">翻译</div>
+      </div>
+    </div>
+    <div class="button-list">
+      <div @click="delAllWork" class="button">翻译余下任务</div>
+      <div @click="startAllWork('translate')" class="button">优化余下任务</div>
+    </div>
+  </div>
+</template>
+```
+

BIN
src/aiHelper/readme/Clip_2025-08-05_15-02-07.png


BIN
src/aiHelper/readme/Clip_2025-08-05_16-14-13.png


BIN
src/aiHelper/readme/Clip_2025-08-05_16-16-28.png


BIN
src/aiHelper/readme/Clip_2025-08-05_16-18-46.png


BIN
src/aiHelper/readme/Clip_2025-08-05_16-37-59.png


BIN
src/aiHelper/readme/Clip_2025-08-05_16-39-02.png


BIN
src/aiHelper/readme/Clip_2025-08-05_16-47-46.png


BIN
src/aiHelper/readme/Clip_2025-08-05_16-59-20.png


BIN
src/aiHelper/readme/Clip_2025-08-05_17-05-28.png


BIN
src/aiHelper/readme/Clip_2025-08-05_17-18-49.png


BIN
src/aiHelper/readme/Clip_2025-08-05_17-29-41.png


BIN
src/aiHelper/readme/Clip_2025-08-05_17-30-02.png


BIN
src/aiHelper/readme/plugin.png


BIN
src/aiHelper/readme/recording.gif


+ 4 - 0
src/aiHelper/system/plugin/getCurrentTime/prompt.md

@@ -0,0 +1,4 @@
+## 获取当前时间
+获取当前的系统时间。utc属性为true时返回utc时间,为false时返回本地时间。
+tool_name:getCurrentTime
+data:{utc: boolean}

+ 30 - 0
src/aiHelper/system/plugin/getCurrentTime/tool.vue

@@ -0,0 +1,30 @@
+<script>
+import Vue from 'vue'
+import Prompt from '!!raw-loader!./prompt.md'
+export default Vue.extend({
+  name: 'GetCurrentTime',
+  data() {
+    return {
+      type: 'tool',
+      prompt: Prompt
+    }
+  },
+  methods: {
+    getCurrentTime(data) {
+      return new Promise((resolve, reject) => {
+        if (data.utc) {
+          resolve({ data: new Date().toUTCString() })
+        } else {
+          resolve({ data: new Date().toLocaleString() })
+        }
+      })
+    }
+  }
+})
+</script>
+
+<template />
+
+<style scoped lang="scss">
+
+</style>

+ 73 - 0
src/aiHelper/system/plugin/gotoUrl/index.vue

@@ -0,0 +1,73 @@
+<script>
+import Prompt from '!!raw-loader!./prompt.md'
+import Vue from 'vue'
+export default Vue.extend({
+  name: 'GotoUrl',
+  props: [
+    'tool_name',
+    'tool_input',
+    'tool_data'
+  ],
+  data() {
+    return {
+      type: 'comp',
+      prompt: Prompt
+    }
+  },
+  methods: {
+    getPageName(name) {
+      return this.$router.resolve({ name: name }).route.meta.title
+    },
+    gotoUrl(data) {
+      this.$router.push(data)
+      console.log('router', data)
+      this.$emit('sentAi', { text: '用户已处理完成所有任务' })
+    }
+  }
+})
+</script>
+
+<template>
+  <div class="init-goto">
+    <div class="item">
+      <div class="text">跳转页面:{{ getPageName(tool_data.name) }}</div>
+      <div class="button" @click="gotoUrl(tool_data)">前往</div>
+    </div>
+  </div>
+</template>
+
+<style scoped lang="scss">
+.init-goto{
+  overflow: hidden;
+  .text{
+    width: 100%;
+  }
+  .item{
+    display: flex;
+    flex-direction: column;
+    width: 100%;
+    background: #c0d7f6;
+    padding: 8px 16px;
+    border-radius: 16px;
+    align-items: flex-end;
+    color: #2b2677;
+    font-size: 16px;
+    margin-bottom: 6px;
+  }
+  .button{
+    float: right;
+    margin-right: 10px;
+    width: fit-content;
+    background: #c0d7f6;
+    padding: 8px 16px;
+    border-radius: 64px;
+    color: #2b2677;
+    font-size: 16px;
+    transition-duration: 300ms;
+    cursor: pointer;
+    &:hover{
+      background: #b4cbec;
+    }
+  }
+}
+</style>

+ 4 - 0
src/aiHelper/system/plugin/gotoUrl/prompt.md

@@ -0,0 +1,4 @@
+## 帮助用户跳转至相应的页面
+帮助用户跳转至相应的页面。
+tool_name:gotoUrl
+data:{name:string,query:object}

+ 50 - 0
src/aiHelper/system/plugin/initFile/index.vue

@@ -0,0 +1,50 @@
+<script>
+import Prompt from '!!raw-loader!./prompt.md'
+import Vue from 'vue'
+import UploadFile from '@/components/UploadFile/index.vue'
+export default Vue.extend({
+  name: 'InitFile',
+  components: {
+    UploadFile
+  },
+  props: [
+    'tool_name',
+    'tool_input',
+    'tool_data'
+  ],
+  data() {
+    return {
+      type: 'comp',
+      prompt: Prompt
+    }
+  }
+})
+</script>
+
+<template>
+  <div class="init-file">
+    <upload-file :data.sync="tool_input" />
+    <div class="button" @click="$emit('sentAi',{url:tool_input})">提交</div>
+  </div>
+</template>
+
+<style scoped lang="scss">
+.init-file{
+  overflow: hidden;
+  .button{
+    float: right;
+    margin-right: 10px;
+    width: fit-content;
+    background: #c0d7f6;
+    padding: 8px 16px;
+    border-radius: 64px;
+    color: #2b2677;
+    font-size: 16px;
+    transition-duration: 300ms;
+    cursor: pointer;
+    &:hover{
+      background: #b4cbec;
+    }
+  }
+}
+</style>

+ 4 - 0
src/aiHelper/system/plugin/initFile/prompt.md

@@ -0,0 +1,4 @@
+## 展示文件选择器
+展示一个文件选择器,然后你能获取到用户选择的文件的网络地址。
+tool_name:initFile
+data:无

+ 60 - 0
src/aiHelper/system/plugin/initInput/index.vue

@@ -0,0 +1,60 @@
+<script>
+import Prompt from '!!raw-loader!./prompt.md'
+import Vue from 'vue'
+export default Vue.extend({
+  name: 'InitInput',
+  props: [
+    'tool_name',
+    'tool_input',
+    'tool_data'
+  ],
+  data() {
+    return {
+      type: 'comp',
+      prompt: Prompt,
+      text: ''
+    }
+  }
+})
+</script>
+
+<template>
+  <div class="init-input">
+    <textarea v-model="text" class="textarea" />
+    <div class="button" @click="$emit('sentAi',{text:text})">提交</div>
+  </div>
+</template>
+
+<style scoped lang="scss">
+.init-input{
+  overflow: hidden;
+  .textarea{
+    height: 200px;
+    width: 100%;
+    resize: vertical;
+    outline: none;
+    border: none;
+    background: #c0d7f6;
+    margin-bottom: 6px;
+    border-radius: 8px;
+    padding: 8px;
+    color: #2b2677;
+    font-family: inherit;
+  }
+  .button{
+    float: right;
+    margin-right: 10px;
+    width: fit-content;
+    background: #c0d7f6;
+    padding: 8px 16px;
+    border-radius: 64px;
+    color: #2b2677;
+    font-size: 16px;
+    transition-duration: 300ms;
+    cursor: pointer;
+    &:hover{
+      background: #b4cbec;
+    }
+  }
+}
+</style>

+ 4 - 0
src/aiHelper/system/plugin/initInput/prompt.md

@@ -0,0 +1,4 @@
+## 展示输入框
+展示一个输入框,然后你可以获取到用户输入的内容,使用此工具时需要在context中解释你想要用户输入的内容。设计选择的优先使用initSelect工具而不是让用户输入。
+tool_name:initInput
+data:无

+ 47 - 0
src/aiHelper/system/plugin/initSelect/index.vue

@@ -0,0 +1,47 @@
+<script>
+import Prompt from '!!raw-loader!./prompt.md'
+import Vue from 'vue'
+export default Vue.extend({
+  name: 'InitSelect',
+  props: [
+    'tool_name',
+    'tool_input',
+    'tool_data'
+  ],
+  data() {
+    return {
+      type: 'comp',
+      prompt: Prompt
+    }
+  }
+})
+</script>
+
+<template>
+  <div class="init-select">
+    <div v-for="item in tool_data" class="button" @click="$emit('sentAi',{...item})">{{ item.label }}</div>
+  </div>
+</template>
+
+<style scoped lang="scss">
+.init-select{
+  max-height: 600px;
+  overflow: hidden;
+  .button{
+    width: 100%;
+    background: #c0d7f6;
+    padding: 8px 16px;
+    border-radius: 64px;
+    color: #2b2677;
+    font-size: 16px;
+    transition-duration: 300ms;
+    cursor: pointer;
+    &:hover{
+      background: #b4cbec;
+    }
+    &:not(:last-child){
+      margin-bottom: 6px;
+    }
+  }
+}
+</style>

+ 5 - 0
src/aiHelper/system/plugin/initSelect/prompt.md

@@ -0,0 +1,5 @@
+## 展示选项对话框
+展示一个选项对话框,然后你能获取到用户选择的选项,使用此工具时需要在context中解释这个操作。
+tool_name:initSelect
+data:[{label:string,value:string}]
+说明:data中需要至少一项

+ 29 - 0
src/aiHelper/system/plugin/mainPage/index.vue

@@ -0,0 +1,29 @@
+<script>
+import Vue from 'vue'
+import Layout from '@/layout/index.vue'
+export default Vue.extend({
+  name: 'AiMainPage',
+  data() {
+    return {
+      type: 'page',
+      router: {
+        path: '/ai',
+        name: 'Ai',
+        component: Layout,
+        meta: {
+          title: 'AI员工',
+          icon: 'el-icon-magic-stick',
+          roles: ['ai']
+        }
+      },
+      prompt: ''
+    }
+  }
+})
+</script>
+
+<template />
+
+<style scoped lang="scss">
+
+</style>

+ 472 - 0
src/aiHelper/system/plugin/modelSetting/model.vue

@@ -0,0 +1,472 @@
+<script>
+import Vue from 'vue'
+import Prompt from '!!raw-loader!./prompt.md'
+export default Vue.extend({
+  name: 'AiModelSetting',
+  data() {
+    return {
+      type: 'page',
+      parent: 'Ai',
+      router: {
+        path: '/AiModelSetting',
+        name: 'AiModelSetting',
+        meta: {
+          title: '员工基础配置',
+          affix: false,
+          roles: ['ai.model.seo']
+        }
+      },
+      prompt: Prompt,
+      providerList: [
+        {
+          name: 'Google_Ai',
+          image: '/static/image/module/google-ai.png',
+          color: '#0054D4',
+          active: false,
+          configItem: [
+            {
+              label: 'apiKey',
+              name: 'apiKey',
+              type: 'text',
+              tips: '输入Google Ai Studio申请的API key',
+              placeholder: '请输入apiKey'
+            },
+            {
+              label: '语言模型',
+              name: 'model',
+              type: 'select',
+              tips: '请选择调用的大语言模型',
+              options: [
+                {
+                  label: 'Gemini 2.5 Pro',
+                  value: 'gemini-2.5-pro'
+                },
+                {
+                  label: 'Gemini 2.5 Flash',
+                  value: 'gemini-2.5-flash'
+                },
+                {
+                  label: 'Gemini 2.5 Flash Lite',
+                  value: 'gemini-2.5-flash-lite'
+                }
+              ]
+            }
+          ],
+          config: {}
+        },
+        {
+          name: 'volc_engine',
+          image: '/static/image/module/volcengine.png',
+          color: '#1664FF',
+          active: false,
+          configItem: [
+            {
+              label: 'apiKey',
+              name: 'apiKey',
+              type: 'text',
+              placeholder: '请输入apiKey'
+            },
+            {
+              label: '模型',
+              name: 'model',
+              type: 'select',
+              options: [
+                {
+                  label: 'Gemini 2.5 Flash',
+                  value: 'gemini-2.5-flash'
+                },
+                {
+                  label: 'Gemini 2.5 Lite',
+                  value: 'gemini-2.5-lite'
+                },
+                {
+                  label: 'Gemini 2.0 Flash',
+                  value: 'gemini-2.0-flash'
+                }
+              ]
+            }
+          ],
+          config: {}
+        }
+      ],
+      modelSetting: {
+        maxRetry: 20,
+        retryOne: 5,
+        language: '',
+        debug: false
+      },
+      languageList: [],
+      providerIndex: 0,
+      moduleIndex: 0,
+      tabIndex: 0,
+      highlight: -1
+    }
+  },
+  watch: {
+    $route() {
+      this.routerEvent()
+    }
+  },
+  mounted() {
+    this.routerEvent()
+    this.readConfig()
+  },
+  methods: {
+    routerEvent() {
+      const tabindex = this.$route.query.tabindex
+      if (tabindex !== undefined) {
+        this.tabIndex = tabindex
+      }
+      const highlight = this.$route.query.highlight
+      if (highlight !== undefined) {
+        this.highlight = highlight
+      }
+      console.log(highlight)
+    },
+    readConfig() {
+      const aiConfig = JSON.parse(localStorage.getItem('aiConfig')) || {}
+      this.providerList.forEach((item, index) => {
+        if (aiConfig[item.name]) {
+          item.config = aiConfig[item.name]
+        } else {
+          item.configItem.forEach(configItem => {
+            this.$set(item.config, configItem.name, null)
+          })
+        }
+        if (aiConfig.active === item.name) {
+          item.active = true
+        }
+      })
+      this.modelSetting = aiConfig.setting || {
+        maxRetry: 20,
+        retryOne: 5,
+        debug: false,
+        language: ''
+      }
+      this.languageList = this.$store.getters.translate_list
+      console.log(this.languageList)
+    },
+    isConfigInLaw(provider) {
+      console.log(provider)
+      for (var item in provider) {
+        if (provider[item] === null) {
+          return false
+        }
+      }
+      return true
+    },
+    saveConfig(provider) {
+      const AiConfig = JSON.parse(localStorage.getItem('aiConfig')) || {}
+      AiConfig[provider.name] = provider.config
+      localStorage.setItem('aiConfig', JSON.stringify(AiConfig))
+      this.readConfig()
+      this.$message.success('保存成功')
+    },
+    save() {
+      const AiConfig = JSON.parse(localStorage.getItem('aiConfig')) || {}
+      this.providerList.forEach(item => {
+        if (item.active) {
+          AiConfig.active = item.name
+        }
+        if (this.isConfigInLaw(item.config)) {
+          AiConfig[item.name] = item.config
+        }
+        AiConfig.setting = this.modelSetting
+        localStorage.setItem('aiConfig', JSON.stringify(AiConfig))
+      })
+      this.$message.success('保存成功')
+    },
+    activeProvider(provider) {
+      const AiConfig = JSON.parse(localStorage.getItem('aiConfig')) || {}
+      AiConfig.active = provider.name
+      localStorage.setItem('aiConfig', JSON.stringify(AiConfig))
+      this.save()
+    },
+    changeTab(index) {
+      this.tabIndex = index
+    }
+  }
+})
+</script>
+
+<template>
+  <div class="page-cont">
+    <div class="head-cont">
+      <div :class="['head-item',tabIndex===0?'active':'']" @click="changeTab(0)">
+        模型配置
+      </div>
+      <div :class="['head-item',tabIndex===1?'active':'']" @click="changeTab(1)">
+        调用配置
+      </div>
+      <div class="button" @click="save">
+        保存
+      </div>
+    </div>
+    <div class="page-body">
+      <div v-show="tabIndex===0" :class="highlight===0?'high-light':''" class="body-item">
+        <div
+          v-for="(provider,index) in providerList"
+          :key="provider.name"
+          :style="{'--color':provider.color}"
+          :class="['module-item',provider.active?'active':'',providerIndex===index?'open':'']"
+        >
+          <div class="item-head">
+            <img class="logo" :src="provider.image" :alt="provider.name">
+            <div class="button-list">
+              <div v-show="providerIndex !== index" class="button active" @click="providerIndex=index">配置</div>
+              <div :class="['button',isConfigInLaw(provider.config)?'active':'']" @click="activeProvider(provider)">启用</div>
+            </div>
+          </div>
+          <div class="item-body">
+            <div v-for="(configItem,index) in provider.configItem">
+              <div v-if="configItem.type === 'text'" class="config-item">
+                <div class="label">{{ configItem.label }}</div>
+                <el-input
+                  v-model="provider.config[configItem.name]"
+                  class="input"
+                  type="text"
+                  placeholder="请输入"
+                />
+                <div class="tips">{{ configItem.tips }}</div>
+              </div>
+              <div v-if="configItem.type === 'select'" class="config-item">
+                <div class="label">{{ configItem.label }}</div>
+                <el-select v-model="provider.config[configItem.name]" class="select">
+                  <el-option v-for="(option,index) in configItem.options" :label="option.label" :value="option.value" />
+                </el-select>
+                <div class="tips">{{ configItem.tips }}</div>
+              </div>
+              <div v-if="configItem.type === 'switch'" class="config-item">
+                <div class="label">{{ configItem.label }}</div>
+                <el-switch v-model="provider.config[configItem.name]" class="switch" />
+                <div class="tips">{{ configItem.tips }}</div>
+              </div>
+            </div>
+            <!--          <div @click="saveConfig(provider)" class="button">保存</div>-->
+          </div>
+        </div>
+      </div>
+      <div v-show="tabIndex===1" class="body-item">
+        <div :class="highlight===1?'high-light':''" class="form-item">
+          <span class="text">调试模式</span>
+          <el-switch v-model="modelSetting.debug" />
+        </div>
+        <div class="tips">在任务控制台中输出详细信息</div>
+        <div :class="highlight===2?'high-light':''" class="form-item">
+          <span class="text">模型语言</span>
+          <el-select v-model="modelSetting.language">
+            <el-option
+              v-for="item in languageList"
+              :key="item.value"
+              :label="item.name"
+              :value="item.value"
+            />
+          </el-select>
+        </div>
+        <div class="tips">模型返回文本的语言</div>
+        <div :class="highlight===3?'high-light':''" class="form-item">
+          <span class="text">总失败重试数</span>
+          <el-slider v-model="modelSetting.maxRetry" class="slider" :max="80" show-input />
+        </div>
+        <div class="tips">当任务连续执行失败的次数超过{{ modelSetting.maxRetry }}时,放弃所有任务。</div>
+        <div :class="highlight===4?'high-light':''" class="form-item">
+          <span class="text">单项任务失败重试数</span>
+          <el-slider v-model="modelSetting.retryOne" class="slider" :max="20" show-input />
+        </div>
+        <div class="tips">当某项任务执行失败的次数超过{{ modelSetting.retryOne }}时,放弃这个任务。</div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<style scoped lang="scss">
+  .page-cont {
+    position: absolute;
+    width: 100%;
+    height: calc(100% - 20px);
+    display: grid;
+    grid-template-rows: 50px 1fr;
+    .head-cont{
+      box-sizing: border-box;
+      position: relative;
+      padding: 0 5px;
+      width: calc(100% - 10px);
+      height: 100%;
+      display: flex;
+      align-items: flex-start;
+      justify-content: flex-start;
+      .button{
+        margin-left: auto;
+        background: #4F46E5;
+        color: white;
+        height: 40px;
+        width: 120px;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        border-radius: 6px;
+        font-size: 16px;
+        cursor: pointer;
+      }
+      .head-item{
+        font-size: 16px;
+        width: 140px;
+        cursor: pointer;
+        background: #fdfdfd;
+        height: 100%;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        position: relative;
+        box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.1);
+        &:first-child{
+          border-top-left-radius: 10px;
+        }
+        &:nth-last-child(2){
+          border-top-right-radius: 10px;
+        }
+        &.active{
+          background: white;
+          border-radius: 10px 10px 0 0;
+          z-index: 1;
+          &::after{
+            content: "";
+            position: absolute;
+            width: 100%;
+            height: 50%;
+            left: 0;
+            bottom: -25%;
+            background: white;
+          }
+        }
+      }
+    }
+    .page-body {
+      overflow: hidden;
+      overflow-y: auto;
+      margin: 0 5px 5px;
+      position: relative;
+      background: white;
+      border-radius: 0 10px 10px 10px ;
+      padding: 16px;
+      border: 1px solid #eeeeee;
+      box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.1);
+      .tips{
+        font-size: 15px;
+        color: gray;
+        margin-bottom: 20px;
+      }
+      .form-item{
+        height: 40px;
+        justify-content: space-between;
+        display: flex;
+        align-items: center;
+        .text{
+          width: 200px;
+        }
+        .slider{
+          flex: 1;
+        }
+      }
+      .module-item{
+        padding: 12px 36px;
+        border: 3px solid grey;
+        border-radius: 10px;
+        margin-bottom: 10px;
+        display: grid;
+        grid-template-rows: 50px 0fr;
+        grid-gap: 0;
+        transition-duration: 300ms;
+        .item-head{
+          height: 50px;
+          display: flex;
+          justify-content: space-between;
+          .logo{
+            height: 100%;
+            filter: grayscale(1);
+          }
+          .button-list{
+            display: flex;
+            height: 100%;
+            align-items: center;
+            grid-gap: 8px;
+            .button{
+              height: fit-content;
+              font-size: 16px;
+              border-radius: 8px;
+              display: flex;
+              align-items: center;
+              justify-content: center;
+              padding: 8px 24px;
+              background: grey;
+              color: white;
+              cursor: not-allowed;
+              &.active{
+                cursor: pointer;
+                background: var(--color);
+              }
+            }
+          }
+        }
+        .item-body{
+          overflow: hidden;
+          display: flex;
+          flex-direction: column;
+          grid-gap: 8px;
+          .button{
+            height: fit-content;
+            font-size: 16px;
+            border-radius: 8px;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            padding: 8px 24px;
+            cursor: pointer;
+            color: white;
+            margin-left: auto;
+            width: fit-content;
+            background: var(--color);
+          }
+          .config-item{
+            height: 70px;
+            display: flex;
+            flex-wrap: wrap;
+            grid-gap: 4px 8px;
+            justify-content: center;
+            align-items: center;
+            .label{
+              width: 100px;
+              font-size: 16px;
+              font-weight: 500;
+            }
+            .tips{
+              width: 100%;
+              font-size: 13px;
+            }
+            .input{
+              flex: 1;
+            }
+            .select{
+              flex: 1;
+            }
+            .switch{
+              flex: 1;
+            }
+          }
+        }
+        &.active{
+          border: 3px solid var(--color);
+          .item-head{
+            .logo{
+              filter: grayscale(0);
+            }
+          }
+        }
+        &.open{
+          grid-gap: 10px;
+          grid-template-rows: 50px 1fr;
+        }
+      }
+    }
+  }
+</style>

+ 7 - 0
src/aiHelper/system/plugin/modelSetting/prompt.md

@@ -0,0 +1,7 @@
+## 前往AI模型基础配置页面
+tool_name:gotoUrl
+data:{name:'AiModelSetting',query:{tabindex:number,highlight:number}}
+说明:
+- tabindex为0时为模型基础配置:apiKey与模型配置(0)在此处
+- tabindex为1时为调用配置:调试模式开关(1),模型输出语言(2),总失败重试次数(3),单项任务失败重试次数(4)在此处
+- highlight:高亮对应的控件

+ 13 - 0
src/aiHelper/system/prompt.md

@@ -0,0 +1,13 @@
+你是一个网站运维助手,协助运维人员操作运维系统完成各项任务,给予运维人员的提示信息需输出至context。
+# 注意事项
+1. 对于需要传入id的工具,不允许向用户直接询问id值,而是通过相关工具获取到目标数据的id。
+2. 工具返回数据的total为数据总数,若返回信息不足,改变page或增大page_size直接再次调用工具,再次调用的过程无需询问用户,直到获取到目标数据或者已获取完所有数据。
+3. 若你觉得目前输出的内容已经解决了运维人员的问题可以通过向finish中输出true来结束对话。若对话没有结束不能仅输出内容,必须调用一个工具,反之向finish输出true后则不允许调用任何工具。
+你可以通过向tool_name中输出工具名称来调用工具以获取更多信息,调用工具所需要传入的参数通过向data中输出JSON字符串来进行传递。你可以调用的工具如下。
+# 工具列表
+
+
+
+
+
+

+ 163 - 0
src/api/user.js

@@ -0,0 +1,163 @@
+import request from '@/utils/request'
+
+
+
+
+/**
+ * 获取后台用户列表
+ * */
+export function getAdminList(data) {
+  return request({
+    url: '/api/admin/user/get-admin-list',
+    method: 'get',
+    params: data
+  })
+}
+
+/**
+ * 新增后台用户
+ * */
+export function adminUserAdd(data) {
+  return request({
+    url: '/api/admin/user/add/admin-user',
+    method: 'put',
+    data
+  })
+}
+
+/**
+ * 修改后台角色
+ * */
+export function adminUserEdit(data) {
+  return request({
+    url: '/api/admin/user/edit/admin-user',
+    method: 'post',
+    data
+  })
+}
+
+
+/**
+ * 后台用户启用禁用
+ * */
+export function setAdminStatus(data) {
+  return request({
+    url: '/api/admin/user/ban/admin-user',
+    method: 'post',
+    data
+  })
+}
+
+/**
+ * 删除后台用户
+ * */
+export function delAdminUser(data) {
+  return request({
+    url: '/api/admin/user/del/admin-user',
+    method: 'delete',
+    data
+  })
+}
+
+/**
+ * 获取组织或者个人列表
+ * */
+export function getOrganizationList(data) {
+  return request({
+    url: '/api/admin/organization/get-organization-list',
+    method: 'get',
+    params: data
+  })
+}
+/**
+ * 获取组织或者个人微信列表
+ * */
+export function getOrganizationWxList(data) {
+  return request({
+    url: '/api/admin/organization/get-organization-wx-list',
+    method: 'get',
+    params: data
+  })
+}
+/**
+ * 组织或者个人启用禁用
+ * */
+export function setOrganizationStatus(data) {
+  return request({
+    url: '/api/admin/organization/set-organization-status',
+    method: 'post',
+    data
+  })
+}
+
+/**
+ * 组织或者个普查员申请
+ * */
+export function setWorkerStatus(data) {
+  return request({
+    url: '/api/admin/organization/set-worker',
+    method: 'post',
+    data
+  })
+}
+
+/**
+ * 组织或者个普查员申请
+ * */
+export function setLeaderStatus(data) {
+  return request({
+    url: '/api/admin/organization/set-leader',
+    method: 'post',
+    data
+  })
+}
+/**
+ * 获取组织或者个人用户列表
+ * */
+export function getOrganizationUserList(data) {
+  return request({
+    url: '/api/admin/organization/get-organization-user-list',
+    method: 'get',
+    params: data
+  })
+}
+/**
+ * 组织或者个人用户启用禁用
+ * */
+export function setOrganizationUserStatus(data) {
+  return request({
+    url: '/api/admin/organization/set-organization-user-status',
+    method: 'post',
+    data
+  })
+}
+
+/**
+ * 获取组织或者个人App列表
+ * */
+export function getOrganizationAppList(data) {
+  return request({
+    url: '/api/admin/organization/get-organization-app-list',
+    method: 'get',
+    params: data
+  })
+}
+
+/**
+ * 组织或者个人用户App版本授权
+ * */
+export function organizationAppAuth(data) {
+  return request({
+    url: '/api/admin/organization/organization-app-auth',
+    method: 'post',
+    data
+  })
+}
+
+export function heartbeat(data) {
+  return request({
+    url: '/api/admin/user/heartbeat',
+    method: 'post',
+    data
+  })
+}

BIN
src/assets/401_images/401.gif


BIN
src/assets/404_images/404.png


BIN
src/assets/404_images/404_cloud.png


+ 0 - 0
src/assets/image404.jpg


Неке датотеке нису приказане због велике количине промена