Kaynağa Gözat

提交初始分支

yanj 3 ay önce
ebeveyn
işleme
6f1012c662
82 değiştirilmiş dosya ile 2289 ekleme ve 6785 silme
  1. 0 4
      src/aiHelper/config.js
  2. 0 846
      src/aiHelper/index.vue
  3. 0 48
      src/aiHelper/plugin/blogPlugin/index.vue
  4. 0 20
      src/aiHelper/plugin/blogPlugin/prompt.md
  5. 0 9
      src/aiHelper/plugin/getHtml/prompt.md
  6. 0 38
      src/aiHelper/plugin/getHtml/tool.vue
  7. 0 47
      src/aiHelper/plugin/meetingPlugin/index.vue
  8. 0 20
      src/aiHelper/plugin/meetingPlugin/prompt.md
  9. 0 47
      src/aiHelper/plugin/productPlugin/index.vue
  10. 0 20
      src/aiHelper/plugin/productPlugin/prompt.md
  11. 0 103
      src/aiHelper/plugin/seoPlugin/comp.vue
  12. 0 96
      src/aiHelper/plugin/seoPlugin/components/editColumn.vue
  13. 0 758
      src/aiHelper/plugin/seoPlugin/page.vue
  14. 0 17
      src/aiHelper/plugin/seoPlugin/prompt.md
  15. 0 949
      src/aiHelper/plugin/seoPlugin/setting.vue
  16. 0 158
      src/aiHelper/plugin/seoPlugin/task.vue
  17. 0 31
      src/aiHelper/plugin/seoPlugin/tool.vue
  18. 0 42
      src/aiHelper/plugin/staticPagePlugin/index.vue
  19. 0 20
      src/aiHelper/plugin/staticPagePlugin/prompt.md
  20. 0 103
      src/aiHelper/plugin/translatePlugin/comp.vue
  21. 0 658
      src/aiHelper/plugin/translatePlugin/page.vue
  22. 0 5
      src/aiHelper/plugin/translatePlugin/prompt.md
  23. 0 103
      src/aiHelper/plugin/translatePlugin/task.vue
  24. 0 159
      src/aiHelper/plugin/writePlugin/comp.vue
  25. 0 559
      src/aiHelper/plugin/writePlugin/page.vue
  26. 0 38
      src/aiHelper/plugin/writePlugin/prompt.md
  27. 0 565
      src/aiHelper/plugin/writePlugin/setting.vue
  28. 0 160
      src/aiHelper/plugin/writePlugin/task.vue
  29. 0 31
      src/aiHelper/plugin/writePlugin/tool.vue
  30. 0 326
      src/aiHelper/readme.md
  31. BIN
      src/aiHelper/readme/Clip_2025-08-05_15-02-07.png
  32. BIN
      src/aiHelper/readme/Clip_2025-08-05_16-14-13.png
  33. BIN
      src/aiHelper/readme/Clip_2025-08-05_16-16-28.png
  34. BIN
      src/aiHelper/readme/Clip_2025-08-05_16-18-46.png
  35. BIN
      src/aiHelper/readme/Clip_2025-08-05_16-37-59.png
  36. BIN
      src/aiHelper/readme/Clip_2025-08-05_16-39-02.png
  37. BIN
      src/aiHelper/readme/Clip_2025-08-05_16-47-46.png
  38. BIN
      src/aiHelper/readme/Clip_2025-08-05_16-59-20.png
  39. BIN
      src/aiHelper/readme/Clip_2025-08-05_17-05-28.png
  40. BIN
      src/aiHelper/readme/Clip_2025-08-05_17-18-49.png
  41. BIN
      src/aiHelper/readme/Clip_2025-08-05_17-29-41.png
  42. BIN
      src/aiHelper/readme/Clip_2025-08-05_17-30-02.png
  43. BIN
      src/aiHelper/readme/plugin.png
  44. BIN
      src/aiHelper/readme/recording.gif
  45. 0 4
      src/aiHelper/system/plugin/getCurrentTime/prompt.md
  46. 0 30
      src/aiHelper/system/plugin/getCurrentTime/tool.vue
  47. 0 73
      src/aiHelper/system/plugin/gotoUrl/index.vue
  48. 0 4
      src/aiHelper/system/plugin/gotoUrl/prompt.md
  49. 0 50
      src/aiHelper/system/plugin/initFile/index.vue
  50. 0 4
      src/aiHelper/system/plugin/initFile/prompt.md
  51. 0 60
      src/aiHelper/system/plugin/initInput/index.vue
  52. 0 4
      src/aiHelper/system/plugin/initInput/prompt.md
  53. 0 47
      src/aiHelper/system/plugin/initSelect/index.vue
  54. 0 5
      src/aiHelper/system/plugin/initSelect/prompt.md
  55. 0 29
      src/aiHelper/system/plugin/mainPage/index.vue
  56. 0 472
      src/aiHelper/system/plugin/modelSetting/model.vue
  57. 0 7
      src/aiHelper/system/plugin/modelSetting/prompt.md
  58. 0 13
      src/aiHelper/system/prompt.md
  59. 82 0
      src/components/Breadcrumb/index.vue
  60. 78 0
      src/components/ErrorLog/index.vue
  61. 44 0
      src/components/Hamburger/index.vue
  62. 180 0
      src/components/HeaderSearch/index.vue
  63. 145 0
      src/components/RightPanel/index.vue
  64. 60 0
      src/components/Screenfull/index.vue
  65. 175 0
      src/components/ThemePicker/index.vue
  66. 268 0
      src/components/layout/ListLayout.vue
  67. 58 0
      src/layout/components/AppMain.vue
  68. 171 0
      src/layout/components/Navbar.vue
  69. 108 0
      src/layout/components/Settings/index.vue
  70. 26 0
      src/layout/components/Sidebar/FixiOSBug.js
  71. 42 0
      src/layout/components/Sidebar/Item.vue
  72. 43 0
      src/layout/components/Sidebar/Link.vue
  73. 93 0
      src/layout/components/Sidebar/Logo.vue
  74. 95 0
      src/layout/components/Sidebar/SidebarItem.vue
  75. 54 0
      src/layout/components/Sidebar/index.vue
  76. 94 0
      src/layout/components/TagsView/ScrollPane.vue
  77. 292 0
      src/layout/components/TagsView/index.vue
  78. 5 0
      src/layout/components/index.js
  79. 126 0
      src/layout/index.vue
  80. 45 0
      src/layout/mixin/ResizeHandler.js
  81. 4 2
      src/main.js
  82. 1 1
      src/router/index.js

+ 0 - 4
src/aiHelper/config.js

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

+ 0 - 846
src/aiHelper/index.vue

@@ -1,846 +0,0 @@
-<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>

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

@@ -1,48 +0,0 @@
-<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>

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

@@ -1,20 +0,0 @@
-## 获取文章列表
-获取文章列表。新闻,博客等数据在此获取。
-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}

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

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

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

@@ -1,38 +0,0 @@
-<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>

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

@@ -1,47 +0,0 @@
-<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>

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

@@ -1,20 +0,0 @@
-## 获取会议列表
-获取会议列表。会议相关数据在此获取。
-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}

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

@@ -1,47 +0,0 @@
-<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>

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

@@ -1,20 +0,0 @@
-## 获取产品列表
-获取产品列表。产品相关数据在此获取。
-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}

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

@@ -1,103 +0,0 @@
-<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>

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

@@ -1,96 +0,0 @@
-<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>

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

@@ -1,758 +0,0 @@
-<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>

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

@@ -1,17 +0,0 @@
-## 发起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:高亮对应的控件

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

@@ -1,949 +0,0 @@
-<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>

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

@@ -1,158 +0,0 @@
-<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>

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

@@ -1,31 +0,0 @@
-<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>

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

@@ -1,42 +0,0 @@
-<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>

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

@@ -1,20 +0,0 @@
-## 获取静态页面列表
-获取静态页面列表,网站存在一些独立的页面,调用此工具获取页面列表数据,相应的页面名称可以通过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中获取

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

@@ -1,103 +0,0 @@
-<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>

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

@@ -1,658 +0,0 @@
-<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>

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

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

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

@@ -1,103 +0,0 @@
-<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>

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

@@ -1,159 +0,0 @@
-<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>

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

@@ -1,559 +0,0 @@
-<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>

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

@@ -1,38 +0,0 @@
-## 发起文章写作任务
-发起文章写作任务,其中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:高亮对应的控件

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

@@ -1,565 +0,0 @@
-<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>

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

@@ -1,160 +0,0 @@
-<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>

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

@@ -1,31 +0,0 @@
-<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>

+ 0 - 326
src/aiHelper/readme.md

@@ -1,326 +0,0 @@
-# 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


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

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

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

@@ -1,30 +0,0 @@
-<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>

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

@@ -1,73 +0,0 @@
-<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>

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

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

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

@@ -1,50 +0,0 @@
-<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>

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

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

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

@@ -1,60 +0,0 @@
-<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>

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

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

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

@@ -1,47 +0,0 @@
-<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>

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

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

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

@@ -1,29 +0,0 @@
-<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>

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

@@ -1,472 +0,0 @@
-<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>

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

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

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

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

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

@@ -0,0 +1,82 @@
+<template>
+  <el-breadcrumb class="app-breadcrumb" separator="/">
+    <transition-group name="breadcrumb">
+      <el-breadcrumb-item v-for="(item,index) in levelList" :key="item.path">
+        <span v-if="item.redirect==='noRedirect'||index==levelList.length-1" class="no-redirect">{{ item.meta.title }}</span>
+        <a v-else @click.prevent="handleLink(item)">{{ item.meta.title }}</a>
+      </el-breadcrumb-item>
+    </transition-group>
+  </el-breadcrumb>
+</template>
+
+<script>
+import pathToRegexp from 'path-to-regexp'
+
+export default {
+  data() {
+    return {
+      levelList: null
+    }
+  },
+  watch: {
+    $route(route) {
+      // if you go to the redirect page, do not update the breadcrumbs
+      if (route.path.startsWith('/redirect/')) {
+        return
+      }
+      this.getBreadcrumb()
+    }
+  },
+  created() {
+    this.getBreadcrumb()
+  },
+  methods: {
+    getBreadcrumb() {
+      // only show routes with meta.title
+      let matched = this.$route.matched.filter(item => item.meta && item.meta.title)
+      const first = matched[0]
+
+      if (!this.isDashboard(first)) {
+        matched = [{ path: '/dashboard', meta: { title: 'Dashboard' }}].concat(matched)
+      }
+
+      this.levelList = matched.filter(item => item.meta && item.meta.title && item.meta.breadcrumb !== false)
+    },
+    isDashboard(route) {
+      const name = route && route.name
+      if (!name) {
+        return false
+      }
+      return name.trim().toLocaleLowerCase() === 'Dashboard'.toLocaleLowerCase()
+    },
+    pathCompile(path) {
+      // To solve this problem https://github.com/PanJiaChen/vue-element-admin/issues/561
+      const { params } = this.$route
+      var toPath = pathToRegexp.compile(path)
+      return toPath(params)
+    },
+    handleLink(item) {
+      const { redirect, path } = item
+      if (redirect) {
+        this.$router.push(redirect)
+        return
+      }
+      this.$router.push(this.pathCompile(path))
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.app-breadcrumb.el-breadcrumb {
+  display: inline-block;
+  font-size: 14px;
+  line-height: 50px;
+  margin-left: 8px;
+
+  .no-redirect {
+    color: #97a8be;
+    cursor: text;
+  }
+}
+</style>

+ 78 - 0
src/components/ErrorLog/index.vue

@@ -0,0 +1,78 @@
+<template>
+  <div v-if="errorLogs.length>0">
+    <el-badge :is-dot="true" style="line-height: 25px;margin-top: -5px;" @click.native="dialogTableVisible=true">
+      <el-button style="padding: 8px 10px;" size="small" type="danger">
+        <svg-icon icon-class="bug" />
+      </el-button>
+    </el-badge>
+
+    <el-dialog :visible.sync="dialogTableVisible" width="80%" append-to-body>
+      <div slot="title">
+        <span style="padding-right: 10px;">Error Log</span>
+        <el-button size="mini" type="primary" icon="el-icon-delete" @click="clearAll">Clear All</el-button>
+      </div>
+      <el-table :data="errorLogs" border>
+        <el-table-column label="Message">
+          <template slot-scope="{row}">
+            <div>
+              <span class="message-title">Msg:</span>
+              <el-tag type="danger">
+                {{ row.err.message }}
+              </el-tag>
+            </div>
+            <br>
+            <div>
+              <span class="message-title" style="padding-right: 10px;">Info: </span>
+              <el-tag type="warning">
+                {{ row.vm.$vnode.tag }} error in {{ row.info }}
+              </el-tag>
+            </div>
+            <br>
+            <div>
+              <span class="message-title" style="padding-right: 16px;">Url: </span>
+              <el-tag type="success">
+                {{ row.url }}
+              </el-tag>
+            </div>
+          </template>
+        </el-table-column>
+        <el-table-column label="Stack">
+          <template slot-scope="scope">
+            {{ scope.row.err.stack }}
+          </template>
+        </el-table-column>
+      </el-table>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'ErrorLog',
+  data() {
+    return {
+      dialogTableVisible: false
+    }
+  },
+  computed: {
+    errorLogs() {
+      return this.$store.getters.errorLogs
+    }
+  },
+  methods: {
+    clearAll() {
+      this.dialogTableVisible = false
+      this.$store.dispatch('errorLog/clearErrorLog')
+    }
+  }
+}
+</script>
+
+<style scoped>
+.message-title {
+  font-size: 16px;
+  color: #333;
+  font-weight: bold;
+  padding-right: 8px;
+}
+</style>

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

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

+ 180 - 0
src/components/HeaderSearch/index.vue

@@ -0,0 +1,180 @@
+<template>
+  <div :class="{'show':show}" class="header-search">
+    <svg-icon class-name="search-icon" icon-class="search" @click.stop="click" />
+    <el-select
+      ref="headerSearchSelect"
+      v-model="search"
+      :remote-method="querySearch"
+      filterable
+      default-first-option
+      remote
+      placeholder="Search"
+      class="header-search-select"
+      @change="change"
+    >
+      <el-option v-for="item in options" :key="item.path" :value="item" :label="item.title.join(' > ')" />
+    </el-select>
+  </div>
+</template>
+
+<script>
+// fuse is a lightweight fuzzy-search module
+// make search results more in line with expectations
+import Fuse from 'fuse.js'
+import path from 'path'
+
+export default {
+  name: 'HeaderSearch',
+  data() {
+    return {
+      search: '',
+      options: [],
+      searchPool: [],
+      show: false,
+      fuse: undefined
+    }
+  },
+  computed: {
+    routes() {
+      return this.$store.getters.permission_routes
+    }
+  },
+  watch: {
+    routes() {
+      this.searchPool = this.generateRoutes(this.routes)
+    },
+    searchPool(list) {
+      this.initFuse(list)
+    },
+    show(value) {
+      if (value) {
+        document.body.addEventListener('click', this.close)
+      } else {
+        document.body.removeEventListener('click', this.close)
+      }
+    }
+  },
+  mounted() {
+    this.searchPool = this.generateRoutes(this.routes)
+  },
+  methods: {
+    click() {
+      this.show = !this.show
+      if (this.show) {
+        this.$refs.headerSearchSelect && this.$refs.headerSearchSelect.focus()
+      }
+    },
+    close() {
+      this.$refs.headerSearchSelect && this.$refs.headerSearchSelect.blur()
+      this.options = []
+      this.show = false
+    },
+    change(val) {
+      this.$router.push(val.path)
+      this.search = ''
+      this.options = []
+      this.$nextTick(() => {
+        this.show = false
+      })
+    },
+    initFuse(list) {
+      this.fuse = new Fuse(list, {
+        shouldSort: true,
+        threshold: 0.4,
+        location: 0,
+        distance: 100,
+        maxPatternLength: 32,
+        minMatchCharLength: 1,
+        keys: [{
+          name: 'title',
+          weight: 0.7
+        }, {
+          name: 'path',
+          weight: 0.3
+        }]
+      })
+    },
+    // Filter out the routes that can be displayed in the sidebar
+    // And generate the internationalized title
+    generateRoutes(routes, basePath = '/', prefixTitle = []) {
+      let res = []
+
+      for (const router of routes) {
+        // skip hidden router
+        if (router.hidden) { continue }
+
+        const data = {
+          path: path.resolve(basePath, router.path),
+          title: [...prefixTitle]
+        }
+
+        if (router.meta && router.meta.title) {
+          data.title = [...data.title, router.meta.title]
+
+          if (router.redirect !== 'noRedirect') {
+            // only push the routes with title
+            // special case: need to exclude parent router without redirect
+            res.push(data)
+          }
+        }
+
+        // recursive child routes
+        if (router.children) {
+          const tempRoutes = this.generateRoutes(router.children, data.path, data.title)
+          if (tempRoutes.length >= 1) {
+            res = [...res, ...tempRoutes]
+          }
+        }
+      }
+      return res
+    },
+    querySearch(query) {
+      if (query !== '') {
+        this.options = this.fuse.search(query)
+      } else {
+        this.options = []
+      }
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.header-search {
+  font-size: 0 !important;
+
+  .search-icon {
+    cursor: pointer;
+    font-size: 18px;
+    vertical-align: middle;
+  }
+
+  .header-search-select {
+    font-size: 18px;
+    transition: width 0.2s;
+    width: 0;
+    overflow: hidden;
+    background: transparent;
+    border-radius: 0;
+    display: inline-block;
+    vertical-align: middle;
+
+    ::v-deep .el-input__inner {
+      border-radius: 0;
+      border: 0;
+      padding-left: 0;
+      padding-right: 0;
+      box-shadow: none !important;
+      border-bottom: 1px solid #d9d9d9;
+      vertical-align: middle;
+    }
+  }
+
+  &.show {
+    .header-search-select {
+      width: 210px;
+      margin-left: 10px;
+    }
+  }
+}
+</style>

+ 145 - 0
src/components/RightPanel/index.vue

@@ -0,0 +1,145 @@
+<template>
+  <div ref="rightPanel" :class="{show:show}" class="rightPanel-container">
+    <div class="rightPanel-background" />
+    <div class="rightPanel">
+      <div class="handle-button" :style="{'top':buttonTop+'px','background-color':theme}" @click="show=!show">
+        <i :class="show?'el-icon-close':'el-icon-setting'" />
+      </div>
+      <div class="rightPanel-items">
+        <slot />
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { addClass, removeClass } from '@/utils'
+
+export default {
+  name: 'RightPanel',
+  props: {
+    clickNotClose: {
+      default: false,
+      type: Boolean
+    },
+    buttonTop: {
+      default: 250,
+      type: Number
+    }
+  },
+  data() {
+    return {
+      show: false
+    }
+  },
+  computed: {
+    theme() {
+      return this.$store.state.settings.theme
+    }
+  },
+  watch: {
+    show(value) {
+      if (value && !this.clickNotClose) {
+        this.addEventClick()
+      }
+      if (value) {
+        addClass(document.body, 'showRightPanel')
+      } else {
+        removeClass(document.body, 'showRightPanel')
+      }
+    }
+  },
+  mounted() {
+    this.insertToBody()
+  },
+  beforeDestroy() {
+    const elx = this.$refs.rightPanel
+    elx.remove()
+  },
+  methods: {
+    addEventClick() {
+      window.addEventListener('click', this.closeSidebar)
+    },
+    closeSidebar(evt) {
+      const parent = evt.target.closest('.rightPanel')
+      if (!parent) {
+        this.show = false
+        window.removeEventListener('click', this.closeSidebar)
+      }
+    },
+    insertToBody() {
+      const elx = this.$refs.rightPanel
+      const body = document.querySelector('body')
+      body.insertBefore(elx, body.firstChild)
+    }
+  }
+}
+</script>
+
+<style>
+.showRightPanel {
+  overflow: hidden;
+  position: relative;
+  width: calc(100% - 15px);
+}
+</style>
+
+<style lang="scss" scoped>
+.rightPanel-background {
+  position: fixed;
+  top: 0;
+  left: 0;
+  opacity: 0;
+  transition: opacity .3s cubic-bezier(.7, .3, .1, 1);
+  background: rgba(0, 0, 0, .2);
+  z-index: -1;
+}
+
+.rightPanel {
+  width: 100%;
+  max-width: 260px;
+  height: 100vh;
+  position: fixed;
+  top: 0;
+  right: 0;
+  box-shadow: 0px 0px 15px 0px rgba(0, 0, 0, .05);
+  transition: all .25s cubic-bezier(.7, .3, .1, 1);
+  transform: translate(100%);
+  background: #fff;
+  z-index: 40000;
+}
+
+.show {
+  transition: all .3s cubic-bezier(.7, .3, .1, 1);
+
+  .rightPanel-background {
+    z-index: 20000;
+    opacity: 1;
+    width: 100%;
+    height: 100%;
+  }
+
+  .rightPanel {
+    transform: translate(0);
+  }
+}
+
+.handle-button {
+  width: 48px;
+  height: 48px;
+  position: absolute;
+  left: -48px;
+  text-align: center;
+  font-size: 24px;
+  border-radius: 6px 0 0 6px !important;
+  z-index: 0;
+  pointer-events: auto;
+  cursor: pointer;
+  color: #fff;
+  line-height: 48px;
+  i {
+    font-size: 24px;
+    line-height: 48px;
+  }
+}
+</style>

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

@@ -0,0 +1,60 @@
+<template>
+  <div>
+    <svg-icon :icon-class="isFullscreen?'exit-fullscreen':'fullscreen'" @click="click" />
+  </div>
+</template>
+
+<script>
+import screenfull from 'screenfull'
+
+export default {
+  name: 'Screenfull',
+  data() {
+    return {
+      isFullscreen: false
+    }
+  },
+  mounted() {
+    this.init()
+  },
+  beforeDestroy() {
+    this.destroy()
+  },
+  methods: {
+    click() {
+      if (!screenfull.enabled) {
+        this.$message({
+          message: 'you browser can not work',
+          type: 'warning'
+        })
+        return false
+      }
+      screenfull.toggle()
+    },
+    change() {
+      this.isFullscreen = screenfull.isFullscreen
+    },
+    init() {
+      if (screenfull.enabled) {
+        screenfull.on('change', this.change)
+      }
+    },
+    destroy() {
+      if (screenfull.enabled) {
+        screenfull.off('change', this.change)
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.screenfull-svg {
+  display: inline-block;
+  cursor: pointer;
+  fill: #5a5e66;;
+  width: 20px;
+  height: 20px;
+  vertical-align: 10px;
+}
+</style>

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

@@ -0,0 +1,175 @@
+<template>
+  <el-color-picker
+    v-model="theme"
+    :predefine="['#409EFF', '#1890ff', '#304156','#212121','#11a983', '#13c2c2', '#6959CD', '#f5222d', ]"
+    class="theme-picker"
+    popper-class="theme-picker-dropdown"
+  />
+</template>
+
+<script>
+const version = require('element-ui/package.json').version // element-ui version from node_modules
+const ORIGINAL_THEME = '#409EFF' // default color
+
+export default {
+  data() {
+    return {
+      chalk: '', // content of theme-chalk css
+      theme: ''
+    }
+  },
+  computed: {
+    defaultTheme() {
+      return this.$store.state.settings.theme
+    }
+  },
+  watch: {
+    defaultTheme: {
+      handler: function(val, oldVal) {
+        this.theme = val
+      },
+      immediate: true
+    },
+    async theme(val) {
+      const oldVal = this.chalk ? this.theme : ORIGINAL_THEME
+      if (typeof val !== 'string') return
+      const themeCluster = this.getThemeCluster(val.replace('#', ''))
+      const originalCluster = this.getThemeCluster(oldVal.replace('#', ''))
+      console.log(themeCluster, originalCluster)
+
+      const $message = this.$message({
+        message: '  Compiling the theme',
+        customClass: 'theme-message',
+        type: 'success',
+        duration: 0,
+        iconClass: 'el-icon-loading'
+      })
+
+      const getHandler = (variable, id) => {
+        return () => {
+          const originalCluster = this.getThemeCluster(ORIGINAL_THEME.replace('#', ''))
+          const newStyle = this.updateStyle(this[variable], originalCluster, themeCluster)
+
+          let styleTag = document.getElementById(id)
+          if (!styleTag) {
+            styleTag = document.createElement('style')
+            styleTag.setAttribute('id', id)
+            document.head.appendChild(styleTag)
+          }
+          styleTag.innerText = newStyle
+        }
+      }
+
+      if (!this.chalk) {
+        const url = `https://unpkg.com/element-ui@${version}/lib/theme-chalk/index.css`
+        await this.getCSSString(url, 'chalk')
+      }
+
+      const chalkHandler = getHandler('chalk', 'chalk-style')
+
+      chalkHandler()
+
+      const styles = [].slice.call(document.querySelectorAll('style'))
+        .filter(style => {
+          const text = style.innerText
+          return new RegExp(oldVal, 'i').test(text) && !/Chalk Variables/.test(text)
+        })
+      styles.forEach(style => {
+        const { innerText } = style
+        if (typeof innerText !== 'string') return
+        style.innerText = this.updateStyle(innerText, originalCluster, themeCluster)
+      })
+
+      this.$emit('change', val)
+
+      $message.close()
+    }
+  },
+
+  methods: {
+    updateStyle(style, oldCluster, newCluster) {
+      let newStyle = style
+      oldCluster.forEach((color, index) => {
+        newStyle = newStyle.replace(new RegExp(color, 'ig'), newCluster[index])
+      })
+      return newStyle
+    },
+
+    getCSSString(url, variable) {
+      return new Promise(resolve => {
+        const xhr = new XMLHttpRequest()
+        xhr.onreadystatechange = () => {
+          if (xhr.readyState === 4 && xhr.status === 200) {
+            this[variable] = xhr.responseText.replace(/@font-face{[^}]+}/, '')
+            resolve()
+          }
+        }
+        xhr.open('GET', url)
+        xhr.send()
+      })
+    },
+
+    getThemeCluster(theme) {
+      const tintColor = (color, tint) => {
+        let red = parseInt(color.slice(0, 2), 16)
+        let green = parseInt(color.slice(2, 4), 16)
+        let blue = parseInt(color.slice(4, 6), 16)
+
+        if (tint === 0) { // when primary color is in its rgb space
+          return [red, green, blue].join(',')
+        } else {
+          red += Math.round(tint * (255 - red))
+          green += Math.round(tint * (255 - green))
+          blue += Math.round(tint * (255 - blue))
+
+          red = red.toString(16)
+          green = green.toString(16)
+          blue = blue.toString(16)
+
+          return `#${red}${green}${blue}`
+        }
+      }
+
+      const shadeColor = (color, shade) => {
+        let red = parseInt(color.slice(0, 2), 16)
+        let green = parseInt(color.slice(2, 4), 16)
+        let blue = parseInt(color.slice(4, 6), 16)
+
+        red = Math.round((1 - shade) * red)
+        green = Math.round((1 - shade) * green)
+        blue = Math.round((1 - shade) * blue)
+
+        red = red.toString(16)
+        green = green.toString(16)
+        blue = blue.toString(16)
+
+        return `#${red}${green}${blue}`
+      }
+
+      const clusters = [theme]
+      for (let i = 0; i <= 9; i++) {
+        clusters.push(tintColor(theme, Number((i / 10).toFixed(2))))
+      }
+      clusters.push(shadeColor(theme, 0.1))
+      return clusters
+    }
+  }
+}
+</script>
+
+<style>
+.theme-message,
+.theme-picker-dropdown {
+  z-index: 99999 !important;
+}
+
+.theme-picker .el-color-picker__trigger {
+  height: 26px !important;
+  width: 26px !important;
+  padding: 2px;
+}
+
+.theme-picker-dropdown .el-color-dropdown__link-btn {
+  display: none;
+}
+</style>

+ 268 - 0
src/components/layout/ListLayout.vue

@@ -0,0 +1,268 @@
+<template>
+  <div class="website-page">
+    <div class="page-head">
+      <slot name="head"></slot>
+    </div>
+    <div class="page-body">
+      <div class="table-option" v-if="showTable && tableData && tableData.length > 0">
+        <i class="el-icon-setting" @click="tableSettingVisible = true"></i>
+      </div>
+      <el-table
+        v-if="showTable && tableData && tableData.length > 0"
+        :data="tableData"
+        style="width: 100%"
+        @sort-change="tableSortChange"
+        @selection-change="selectionChange"
+      >
+        <el-table-column
+          v-if="selection"
+          type="selection"
+          width="55"
+        />
+        <template v-for="(column_item,column_index) in tableColumn">
+          <el-table-column
+            v-if="column_item.type === 'Index'"
+            :key="'column' + column_index"
+            type="index"
+            :label="column_item.label"
+          />
+          <el-table-column
+            v-else-if="column_item.type === 'Type'"
+            :key="'column' + column_index"
+            :prop="column_item.prop"
+            :label="column_item.label"
+          >
+            <template slot-scope="scope">
+              <el-tag v-for="(column_item_prop,column_item_prop_key) in scope.row[column_item.prop]" :key="'column_item_prop' + column_item_prop_key" :title="column_item_prop.type_name" style="margin-right: 4px;margin-bottom: 4px;">
+                {{ column_item_prop.type_name }}
+              </el-tag>
+            </template>
+          </el-table-column>
+          <el-table-column
+            v-else-if="column_item.type === 'Image'"
+            :key="'column' + column_index"
+            :prop="column_item.prop"
+            :label="column_item.label"
+          >
+            <template slot-scope="scope">
+              <div class="show-list-img">
+                <img v-if="scope.row.image_url !==''" class="blog-img" alt="image" :src="scope.row.image_url">
+                <img v-else :src="$store.getters && $store.getters.emptyImg" class="blog-img" alt="image">
+              </div>
+            </template>
+          </el-table-column>
+          <el-table-column
+            v-else-if="column_item.prop === 'status'"
+            :key="column_index"
+            :prop="column_item.prop"
+            :label="column_item.label"
+          >
+            <template slot-scope="scope">
+              {{ scope.row.status < 1 ? '已发布' : '草稿' }}
+            </template>
+          </el-table-column>
+          <el-table-column
+            v-else
+            :key="column_index"
+            :prop="column_item.prop"
+            :label="column_item.label"
+          >
+            <template slot-scope="scope">
+              <div class="text_clamp_3" :title="scope.row[column_item.prop]" v-html="scope.row[column_item.prop]" />
+            </template>
+          </el-table-column>
+        </template>
+        <slot name="tableColumn"></slot>
+      </el-table>
+      <pagination v-if="showTable && tableData && tableData.length > 0" v-bind="paginationParams" @pagination="pagination" />
+      <transition name="el-fade-in-linear">
+        <empty v-show="!(tableData && tableData.length > 0) && showTable"></empty>
+      </transition>
+      <slot name="body"></slot>
+    </div>
+    <el-dialog title="表头设置" :visible.sync="tableSettingVisible">
+      <div class="dialog-body">
+        <el-checkbox-group v-model="columnValue">
+          <el-checkbox v-for="item in columnListData" :label="item.label"></el-checkbox>
+        </el-checkbox-group>
+      </div>
+      <slot name="footer">
+        <el-button type="primary" @click="setColumnList">确定</el-button>
+      </slot>
+    </el-dialog>
+    <show-qr ref="showQr" />
+  </div>
+</template>
+
+<script>
+import Cookies from 'js-cookie'
+import Pagination from '@/components/Pagination'
+import showQr from '@/components/showQr/showQr.vue'
+import empty from '@/components/Empty/empty'
+export default {
+  name: 'ListLayout',
+  components:{
+    Pagination,
+    showQr,
+    empty
+  },
+  props: {
+    showTable: {
+      type: Boolean,
+      default: true
+    },
+    paginationParams: {
+      type: Object,
+      default: {
+        total: 0,
+        limit: 10,
+        page: 1
+      },
+      immediate: true
+    },
+    selection: {
+      type: Boolean,
+      default: false
+    },
+    tableData: {
+      default: [],
+      type: Array
+    },
+    name: {
+      default: '',
+      type: String
+    },
+    columnList: {
+      default: [],
+      type: Array
+    }
+  },
+  watch: {
+    columnList: {
+      handler(val) {
+        if (this.name) {
+          this.columnListData = JSON.parse(JSON.stringify(val))
+          this.getColumn()
+        }
+      },
+      immediate: true
+    }
+  },
+  data() {
+    return {
+      columnListData: [],
+      tableSettingVisible: false,
+      columnValue: [],
+      tableColumn: []
+    }
+  },
+  mounted() {
+
+  },
+  methods: {
+    // 二维码
+    showQrCode(row) {
+      this.$refs.showQr.showQrCode(row.urla)
+    },
+    tableSortChange(e) {
+      this.$emit('tableSortChange',e)
+    },
+    selectionChange(e) {
+      this.$emit('selectionChange',e)
+    },
+    pagination(e) {
+      this.$emit('pagination',e)
+    },
+    getColumn() {
+     let cookie_value = Cookies.get(this.name)
+      if (cookie_value && cookie_value !== '[]') {
+        this.columnValue = JSON.parse(cookie_value)
+      } else {
+        this.columnValue = this.columnList.map((item,index)=>{if (index < 4) return item.label})
+      }
+      this.setColumnList()
+    },
+    setColumnList() {
+      let list = []
+      if (this.columnValue && this.columnValue.length > 0) {
+        this.columnList.forEach(c_item=>{
+          this.columnValue.forEach(v_item => {
+            if ( v_item === c_item.label) {
+              list.push(c_item)
+            }
+          })
+        })
+        Cookies.set(this.name,this.columnValue, { expires: new Date(Date.now() + 5 * 365 * 24 * 60 * 60 * 1000) })
+        this.tableColumn = list
+      }
+      this.tableSettingVisible = false
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.dialog-body {
+  padding-bottom: 24px;
+}
+.website-page {
+  padding: 12px 24px 24px 24px;
+  ::v-deep {
+    .el-table__empty-block {
+      width: 100% !important;
+    }
+  }
+  .table-option {
+    i {
+      font-size: 20px;
+      &:hover {
+        color: #00a0e9;
+      }
+    }
+  }
+  .table-option {
+    position: absolute;
+    z-index: 10;
+    right: 20px;
+    top: 20px;
+    i {
+      cursor: pointer;
+    }
+  }
+  .page-head {
+    padding: 24px;
+    border-radius: 6px;
+    box-shadow: 0 0 .4vw 0 rgba(0, 0, 0, 0.1);
+    border: 1px solid #eeeeee;
+    margin-bottom: 24px;
+    display:flex;
+    flex-direction: column;
+    grid-gap: 24px;
+    position: sticky;
+    top: 0;
+    &>.el-row {
+      display: flex;
+      align-items: center;
+    }
+    ::v-deep {
+      .el-cascader {
+        width: 100%;
+      }
+      .el-dropdown {
+        margin-left: 12px;
+      }
+      .el-button {
+      }
+    }
+  }
+  .page-body {
+    position: relative;
+    border-radius: 6px;
+    padding: 12px 24px;
+    border: 1px solid #eeeeee;
+    box-shadow: 0 0 .4vw 0 rgba(0, 0, 0, 0.1);
+    transition: .3s ease-in-out;
+  }
+
+}
+</style>

+ 58 - 0
src/layout/components/AppMain.vue

@@ -0,0 +1,58 @@
+<template>
+  <section class="app-main">
+    <transition name="fade-transform" mode="out-in">
+      <keep-alive :include="cachedViews">
+        <router-view :key="key" />
+      </keep-alive>
+    </transition>
+  </section>
+</template>
+
+<script>
+export default {
+  name: 'AppMain',
+  computed: {
+    cachedViews() {
+      return this.$store.state.tagsView.cachedViews
+    },
+    key() {
+      return this.$route.path
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.app-main {
+  /* 50= navbar  50  */
+  min-height: calc(100vh - 50px);
+  width: 100%;
+  position: relative;
+  overflow: hidden;
+  padding-top: 16px;
+}
+
+.fixed-header+.app-main {
+  padding-top: 50px;
+}
+
+.hasTagsView {
+  .app-main {
+    /* 84 = navbar + tags-view = 50 + 34 */
+    min-height: calc(100vh - 84px);
+  }
+
+  .fixed-header+.app-main {
+    padding-top: 84px;
+  }
+}
+</style>
+
+<style lang="scss">
+// fix css style bug in open el-dialog
+.el-popup-parent--hidden {
+  .fixed-header {
+    padding-right: 15px;
+  }
+}
+</style>

+ 171 - 0
src/layout/components/Navbar.vue

@@ -0,0 +1,171 @@
+<template>
+  <div class="navbar">
+    <hamburger
+      id="hamburger-container"
+      :is-active="sidebar.opened"
+      class="hamburger-container"
+      @toggleClick="toggleSideBar"
+    />
+
+    <breadcrumb id="breadcrumb-container" class="breadcrumb-container" />
+
+    <div class="right-menu">
+      <template v-if="device!=='mobile'">
+        <search id="header-search" class="right-menu-item" />
+
+        <error-log class="errLog-container right-menu-item hover-effect" />
+
+        <screenfull id="screenfull" class="right-menu-item hover-effect" />
+
+      </template>
+
+      <el-dropdown class="avatar-container right-menu-item hover-effect" trigger="click">
+        <div class="avatar-wrapper">
+          <span class="nick_name">{{ $store.getters.nick_name }}</span>
+          <i class="el-icon-caret-bottom" />
+        </div>
+        <el-dropdown-menu slot="dropdown">
+          <!--  <el-dropdown-item>
+            <router-link to="/profile/index">Profile</router-link>
+          </el-dropdown-item>-->
+          <!--          <el-dropdown-item>
+            <router-link to="/">Dashboard</router-link>
+          </el-dropdown-item>-->
+          <el-dropdown-item v-for="(item,index) in langWebsiteList" :key="index" @click.native="locationTo(item.url)">{{ item.name }}</el-dropdown-item>
+          <el-dropdown-item @click.native="logout">
+            登出
+          </el-dropdown-item>
+        </el-dropdown-menu>
+      </el-dropdown>
+    </div>
+  </div>
+</template>
+
+<script>
+import { mapGetters } from 'vuex'
+import Breadcrumb from '@/components/Breadcrumb'
+import Hamburger from '@/components/Hamburger'
+import ErrorLog from '@/components/ErrorLog'
+import Screenfull from '@/components/Screenfull'
+import Search from '@/components/HeaderSearch'
+
+export default {
+  components: {
+    Breadcrumb,
+    Hamburger,
+    ErrorLog,
+    Screenfull,
+    Search
+  },
+  computed: {
+    ...mapGetters([
+      'sidebar',
+      'avatar',
+      'device'
+    ])
+  },
+
+  created() {
+  },
+  methods: {
+    locationTo(url) {
+      window.open(url,'blank')
+    },
+    toggleSideBar() {
+      this.$store.dispatch('app/toggleSideBar')
+    },
+    async logout() {
+      await this.$store.dispatch('user/logout')
+      this.$router.push(`/login?redirect=${this.$route.fullPath}`)
+    }
+  },
+  data: function() {
+    return {
+      langWebsiteList: []
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+  .navbar {
+    height: 50px;
+    overflow: hidden;
+    position: relative;
+    background: #fff;
+    box-shadow: 0 1px 4px rgba(0, 21, 41, .08);
+
+    .hamburger-container {
+      line-height: 46px;
+      height: 100%;
+      float: left;
+      cursor: pointer;
+      transition: background .3s;
+      -webkit-tap-highlight-color: transparent;
+
+      &:hover {
+        background: rgba(0, 0, 0, .025)
+      }
+    }
+
+    .breadcrumb-container {
+      float: left;
+    }
+
+    .errLog-container {
+      display: inline-block;
+      vertical-align: top;
+    }
+
+    .right-menu {
+      float: right;
+      height: 100%;
+      line-height: 50px;
+
+      &:focus {
+        outline: none;
+      }
+
+      .right-menu-item {
+        display: inline-block;
+        padding: 0 8px;
+        height: 100%;
+        font-size: 18px;
+        color: #5a5e66;
+        vertical-align: text-bottom;
+
+        &.hover-effect {
+          cursor: pointer;
+          transition: background .3s;
+
+          &:hover {
+            background: rgba(0, 0, 0, .025)
+          }
+        }
+      }
+
+      .avatar-container {
+        margin-right: 30px;
+
+        .avatar-wrapper {
+          /*margin-top: 5px;*/
+          position: relative;
+          .user-avatar {
+            cursor: pointer;
+            width: 40px;
+            height: 40px;
+            border-radius: 10px;
+          }
+
+          .el-icon-caret-bottom {
+            cursor: pointer;
+            position: absolute;
+            right: -20px;
+            top: 25px;
+            font-size: 12px;
+          }
+        }
+      }
+    }
+  }
+</style>

+ 108 - 0
src/layout/components/Settings/index.vue

@@ -0,0 +1,108 @@
+<template>
+  <div class="drawer-container">
+    <div>
+      <h3 class="drawer-title">Page style setting</h3>
+
+      <div class="drawer-item">
+        <span>Theme Color</span>
+        <theme-picker style="float: right;height: 26px;margin: -3px 8px 0 0;" @change="themeChange" />
+      </div>
+
+      <div class="drawer-item">
+        <span>Open Tags-View</span>
+        <el-switch v-model="tagsView" class="drawer-switch" />
+      </div>
+
+      <div class="drawer-item">
+        <span>Fixed Header</span>
+        <el-switch v-model="fixedHeader" class="drawer-switch" />
+      </div>
+
+      <div class="drawer-item">
+        <span>Sidebar Logo</span>
+        <el-switch v-model="sidebarLogo" class="drawer-switch" />
+      </div>
+
+    </div>
+  </div>
+</template>
+
+<script>
+import ThemePicker from '@/components/ThemePicker'
+
+export default {
+  components: { ThemePicker },
+  data() {
+    return {}
+  },
+  computed: {
+    fixedHeader: {
+      get() {
+        return this.$store.state.settings.fixedHeader
+      },
+      set(val) {
+        this.$store.dispatch('settings/changeSetting', {
+          key: 'fixedHeader',
+          value: val
+        })
+      }
+    },
+    tagsView: {
+      get() {
+        return this.$store.state.settings.tagsView
+      },
+      set(val) {
+        this.$store.dispatch('settings/changeSetting', {
+          key: 'tagsView',
+          value: val
+        })
+      }
+    },
+    sidebarLogo: {
+      get() {
+        return this.$store.state.settings.sidebarLogo
+      },
+      set(val) {
+        this.$store.dispatch('settings/changeSetting', {
+          key: 'sidebarLogo',
+          value: val
+        })
+      }
+    }
+  },
+  methods: {
+    themeChange(val) {
+      this.$store.dispatch('settings/changeSetting', {
+        key: 'theme',
+        value: val
+      })
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.drawer-container {
+  padding: 24px;
+  font-size: 14px;
+  line-height: 1.5;
+  word-wrap: break-word;
+
+  .drawer-title {
+    margin-bottom: 12px;
+    color: rgba(0, 0, 0, .85);
+    font-size: 14px;
+    line-height: 22px;
+  }
+
+  .drawer-item {
+    color: rgba(0, 0, 0, .65);
+    font-size: 14px;
+    padding: 12px 0;
+  }
+
+  .drawer-switch {
+    float: right
+  }
+}
+</style>

+ 26 - 0
src/layout/components/Sidebar/FixiOSBug.js

@@ -0,0 +1,26 @@
+export default {
+  computed: {
+    device() {
+      return this.$store.state.app.device
+    }
+  },
+  mounted() {
+    // In order to fix the click on menu on the ios device will trigger the mouseleave bug
+    // https://github.com/PanJiaChen/vue-element-admin/issues/1135
+    this.fixBugIniOS()
+  },
+  methods: {
+    fixBugIniOS() {
+      const $subMenu = this.$refs.subMenu
+      if ($subMenu) {
+        const handleMouseleave = $subMenu.handleMouseleave
+        $subMenu.handleMouseleave = (e) => {
+          if (this.device === 'mobile') {
+            return
+          }
+          handleMouseleave(e)
+        }
+      }
+    }
+  }
+}

+ 42 - 0
src/layout/components/Sidebar/Item.vue

@@ -0,0 +1,42 @@
+<script>
+export default {
+  name: 'MenuItem',
+  functional: true,
+  props: {
+    icon: {
+      type: String,
+      default: ''
+    },
+    title: {
+      type: String,
+      default: ''
+    }
+  },
+  render(h, context) {
+    const { icon, title } = context.props
+    const vnodes = []
+
+    if (icon) {
+      if (icon.includes('el-icon')) {
+        vnodes.push(<i class={[icon, 'sub-el-icon']} />)
+      } else {
+        vnodes.push(<svg-icon icon-class={icon}/>)
+      }
+    }
+
+    if (title) {
+      vnodes.push(<span slot='title'>{(title)}</span>)
+    }
+    return vnodes
+  }
+}
+</script>
+
+<style scoped>
+.sub-el-icon {
+  color: currentColor;
+  width: 1em;
+  height: 1em;
+  font-size: 20px;
+}
+</style>

+ 43 - 0
src/layout/components/Sidebar/Link.vue

@@ -0,0 +1,43 @@
+<template>
+  <component :is="type" v-bind="linkProps(to)">
+    <slot />
+  </component>
+</template>
+
+<script>
+import { isExternal } from '@/utils/validate'
+
+export default {
+  props: {
+    to: {
+      type: String,
+      required: true
+    }
+  },
+  computed: {
+    isExternal() {
+      return isExternal(this.to)
+    },
+    type() {
+      if (this.isExternal) {
+        return 'a'
+      }
+      return 'router-link'
+    }
+  },
+  methods: {
+    linkProps(to) {
+      if (this.isExternal) {
+        return {
+          href: to,
+          target: '_blank',
+          rel: 'noopener'
+        }
+      }
+      return {
+        to: to
+      }
+    }
+  }
+}
+</script>

+ 93 - 0
src/layout/components/Sidebar/Logo.vue

@@ -0,0 +1,93 @@
+<template>
+  <div class="sidebar-logo-container" :class="{'collapse':collapse}">
+    <transition name="sidebarLogoFade">
+      <router-link v-if="collapse" key="collapse" class="sidebar-logo-link" to="/">
+        <img :src="ico" onerror="this.src='/static/image/logo.png';this.onerror=null;" style="background:white;" class="sidebar-logo" alt="logo">
+      </router-link>
+      <router-link v-else key="expand" class="sidebar-logo-link" to="/">
+        <img :src="logo" onerror="this.src='/static/image/logo.png';this.onerror=null;" style="background: white;" class="sidebar-logo" alt="logo">
+<!--        <h1 class="sidebar-title">{{ title }} </h1>-->
+      </router-link>
+    </transition>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'SidebarLogo',
+  props: {
+    collapse: {
+      type: Boolean,
+      required: true
+    }
+  },
+  data() {
+    return {
+      title: 'Admin',
+      logo: (process.env.VUE_APP_NAME ? '/static/image/' + process.env.VUE_APP_NAME + '/logo.png' : '/static/image/logo.png'),
+      ico: (process.env.VUE_APP_NAME ? '/static/image/' + process.env.VUE_APP_NAME + '/favicon.png' : '/static/image/logo.png')
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.sidebarLogoFade-enter-active {
+  transition: opacity 1.5s;
+}
+
+.sidebarLogoFade-enter,
+.sidebarLogoFade-leave-to {
+  opacity: 0;
+}
+
+.sidebar-logo-container {
+  position: relative;
+  width: 100%;
+  height: 80px;
+  //line-height: 50px;
+  //background: #2b2f3a;
+  text-align: center;
+  overflow: hidden;
+  padding: 10px;
+  border-bottom: 1px solid #eeeeee;
+  & .sidebar-logo-link {
+    height: 100%;
+    width: 100%;
+
+    & .sidebar-logo {
+      height: 100%;
+      width: 100%;
+      object-fit: contain;
+      vertical-align: middle;
+      //margin-right: 12px;
+    }
+
+    & .sidebar-title {
+      display: inline-block;
+      margin: 0;
+      color: #fff;
+      font-weight: 600;
+      line-height: 50px;
+      font-size: 14px;
+      font-family: Avenir, Helvetica Neue, Arial, Helvetica, sans-serif;
+      vertical-align: middle;
+    }
+  }
+
+  &.collapse {
+    .sidebar-logo {
+      margin-right: 0px;
+    }
+  }
+}
+.hideSidebar  .sidebar-logo-container{
+  & .sidebar-logo-link {
+    & .sidebar-logo {
+      height: 100% !important;
+      width: 100% !important;
+      object-fit: contain;
+    }
+  }
+}
+</style>

+ 95 - 0
src/layout/components/Sidebar/SidebarItem.vue

@@ -0,0 +1,95 @@
+<template>
+  <div v-if="!item.hidden">
+    <template v-if="hasOneShowingChild(item.children,item) && (!onlyOneChild.children||onlyOneChild.noShowingChildren)&&!item.alwaysShow">
+      <app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path)">
+        <el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{'submenu-title-noDropdown':!isNest}">
+          <item :icon="onlyOneChild.meta.icon||(item.meta&&item.meta.icon)" :title="onlyOneChild.meta.title" />
+        </el-menu-item>
+      </app-link>
+    </template>
+
+    <el-submenu v-else ref="subMenu" :index="resolvePath(item.path)" popper-append-to-body>
+      <template slot="title">
+        <item v-if="item.meta" :icon="item.meta && item.meta.icon" :title="item.meta.title" />
+      </template>
+      <sidebar-item
+        v-for="child in item.children"
+        :key="child.path"
+        :is-nest="true"
+        :item="child"
+        :base-path="resolvePath(child.path)"
+        class="nest-menu"
+      />
+    </el-submenu>
+  </div>
+</template>
+
+<script>
+import path from 'path'
+import { isExternal } from '@/utils/validate'
+import Item from './Item'
+import AppLink from './Link'
+import FixiOSBug from './FixiOSBug'
+
+export default {
+  name: 'SidebarItem',
+  components: { Item, AppLink },
+  mixins: [FixiOSBug],
+  props: {
+    // route object
+    item: {
+      type: Object,
+      required: true
+    },
+    isNest: {
+      type: Boolean,
+      default: false
+    },
+    basePath: {
+      type: String,
+      default: ''
+    }
+  },
+  data() {
+    // To fix https://github.com/PanJiaChen/vue-admin-template/issues/237
+    // TODO: refactor with render function
+    this.onlyOneChild = null
+    return {}
+  },
+  methods: {
+    hasOneShowingChild(children = [], parent) {
+      const showingChildren = children.filter(item => {
+        if (item.hidden) {
+          return false
+        } else {
+          // Temp set(will be used if only has one showing child)
+          this.onlyOneChild = item
+          return true
+        }
+      })
+
+      // When there is only one child router, the child router is displayed by default
+      if (showingChildren.length === 1) {
+        return true
+      }
+
+      // Show parent if there are no child router to display
+      if (showingChildren.length === 0) {
+        this.onlyOneChild = { ... parent, path: '', noShowingChildren: true }
+        return true
+      }
+
+      return false
+    },
+    resolvePath(routePath) {
+      if (isExternal(routePath)) {
+        return routePath
+      }
+      if (isExternal(this.basePath)) {
+        return this.basePath
+      }
+      return path.resolve(this.basePath, routePath)
+    }
+  }
+}
+</script>

+ 54 - 0
src/layout/components/Sidebar/index.vue

@@ -0,0 +1,54 @@
+<template>
+  <div :class="{'has-logo':showLogo}">
+    <logo v-if="showLogo" :collapse="isCollapse" />
+    <el-scrollbar wrap-class="scrollbar-wrapper" style="flex: 1;">
+      <el-menu
+        :default-active="activeMenu"
+        :collapse="isCollapse"
+        :background-color="variables.menuBg"
+        :text-color="variables.menuText"
+        :unique-opened="false"
+        :active-text-color="variables.menuActiveText"
+        :collapse-transition="false"
+        mode="vertical"
+      >
+        <sidebar-item v-for="route in permission_routes" :key="route.path" :item="route" :base-path="route.path" />
+      </el-menu>
+    </el-scrollbar>
+  </div>
+</template>
+
+<script>
+import { mapGetters } from 'vuex'
+import Logo from './Logo'
+import SidebarItem from './SidebarItem'
+import variables from '@/styles/variables.scss'
+
+export default {
+  components: { SidebarItem, Logo },
+  computed: {
+    ...mapGetters([
+      'permission_routes',
+      'sidebar'
+    ]),
+    activeMenu() {
+      const route = this.$route
+      const { meta, path } = route
+      // if set path, the sidebar will highlight the path you set
+      if (meta.activeMenu) {
+        return meta.activeMenu
+      }
+      return path
+    },
+    showLogo() {
+      return this.$store.state.settings.sidebarLogo
+    },
+    variables() {
+      return variables
+    },
+    isCollapse() {
+      return !this.sidebar.opened
+    }
+  }
+}
+</script>

+ 94 - 0
src/layout/components/TagsView/ScrollPane.vue

@@ -0,0 +1,94 @@
+<template>
+  <el-scrollbar ref="scrollContainer" :vertical="false" class="scroll-container" @wheel.native.prevent="handleScroll">
+    <slot />
+  </el-scrollbar>
+</template>
+
+<script>
+const tagAndTagSpacing = 4 // tagAndTagSpacing
+
+export default {
+  name: 'ScrollPane',
+  data() {
+    return {
+      left: 0
+    }
+  },
+  computed: {
+    scrollWrapper() {
+      return this.$refs.scrollContainer.$refs.wrap
+    }
+  },
+  mounted() {
+    this.scrollWrapper.addEventListener('scroll', this.emitScroll, true)
+  },
+  beforeDestroy() {
+    this.scrollWrapper.removeEventListener('scroll', this.emitScroll)
+  },
+  methods: {
+    handleScroll(e) {
+      const eventDelta = e.wheelDelta || -e.deltaY * 40
+      const $scrollWrapper = this.scrollWrapper
+      $scrollWrapper.scrollLeft = $scrollWrapper.scrollLeft + eventDelta / 4
+    },
+    emitScroll() {
+      this.$emit('scroll')
+    },
+    moveToTarget(currentTag) {
+      const $container = this.$refs.scrollContainer.$el
+      const $containerWidth = $container.offsetWidth
+      const $scrollWrapper = this.scrollWrapper
+      const tagList = this.$parent.$refs.tag
+
+      let firstTag = null
+      let lastTag = null
+
+      // find first tag and last tag
+      if (tagList.length > 0) {
+        firstTag = tagList[0]
+        lastTag = tagList[tagList.length - 1]
+      }
+
+      if (firstTag === currentTag) {
+        $scrollWrapper.scrollLeft = 0
+      } else if (lastTag === currentTag) {
+        $scrollWrapper.scrollLeft = $scrollWrapper.scrollWidth - $containerWidth
+      } else {
+        // find preTag and nextTag
+        const currentIndex = tagList.findIndex(item => item === currentTag)
+        const prevTag = tagList[currentIndex - 1]
+        const nextTag = tagList[currentIndex + 1]
+
+        // the tag's offsetLeft after of nextTag
+        const afterNextTagOffsetLeft = nextTag.$el.offsetLeft + nextTag.$el.offsetWidth + tagAndTagSpacing
+
+        // the tag's offsetLeft before of prevTag
+        const beforePrevTagOffsetLeft = prevTag.$el.offsetLeft - tagAndTagSpacing
+
+        if (afterNextTagOffsetLeft > $scrollWrapper.scrollLeft + $containerWidth) {
+          $scrollWrapper.scrollLeft = afterNextTagOffsetLeft - $containerWidth
+        } else if (beforePrevTagOffsetLeft < $scrollWrapper.scrollLeft) {
+          $scrollWrapper.scrollLeft = beforePrevTagOffsetLeft
+        }
+      }
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.scroll-container {
+  white-space: nowrap;
+  position: relative;
+  overflow: hidden;
+  width: 100%;
+  ::v-deep {
+    .el-scrollbar__bar {
+      bottom: 0px;
+    }
+    .el-scrollbar__wrap {
+      height: 49px;
+    }
+  }
+}
+</style>

+ 292 - 0
src/layout/components/TagsView/index.vue

@@ -0,0 +1,292 @@
+<template>
+  <div id="tags-view-container" class="tags-view-container">
+    <scroll-pane ref="scrollPane" class="tags-view-wrapper" @scroll="handleScroll">
+      <router-link
+        v-for="tag in visitedViews"
+        ref="tag"
+        :key="tag.path"
+        :class="isActive(tag)?'active':''"
+        :to="{ path: tag.path, query: tag.query, fullPath: tag.fullPath }"
+        tag="span"
+        class="tags-view-item"
+        @click.middle.native="!isAffix(tag)?closeSelectedTag(tag):''"
+        @contextmenu.prevent.native="openMenu(tag,$event)"
+      >
+        {{ tag.title }}
+        <span v-if="!isAffix(tag)" class="el-icon-close" @click.prevent.stop="closeSelectedTag(tag)" />
+      </router-link>
+    </scroll-pane>
+    <ul v-show="visible" :style="{left:left+'px',top:top+'px'}" class="contextmenu">
+      <li @click="refreshSelectedTag(selectedTag)">Refresh</li>
+      <li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)">Close</li>
+      <li @click="closeOthersTags">Close Others</li>
+      <li @click="closeAllTags(selectedTag)">Close All</li>
+    </ul>
+  </div>
+</template>
+
+<script>
+import ScrollPane from './ScrollPane'
+import path from 'path'
+
+export default {
+  components: { ScrollPane },
+  data() {
+    return {
+      visible: false,
+      top: 0,
+      left: 0,
+      selectedTag: {},
+      affixTags: []
+    }
+  },
+  computed: {
+    visitedViews() {
+      return this.$store.state.tagsView.visitedViews
+    },
+    routes() {
+      return this.$store.state.permission.routes
+    }
+  },
+  watch: {
+    $route() {
+      this.addTags()
+      this.moveToCurrentTag()
+    },
+    visible(value) {
+      if (value) {
+        document.body.addEventListener('click', this.closeMenu)
+      } else {
+        document.body.removeEventListener('click', this.closeMenu)
+      }
+    }
+  },
+  mounted() {
+    this.initTags()
+    this.addTags()
+  },
+  methods: {
+    isActive(route) {
+      return route.path === this.$route.path
+    },
+    isAffix(tag) {
+      return tag.meta && tag.meta.affix
+    },
+    filterAffixTags(routes, basePath = '/') {
+      let tags = []
+      routes.forEach(route => {
+        if (route.meta && route.meta.affix) {
+          const tagPath = path.resolve(basePath, route.path)
+          tags.push({
+            fullPath: tagPath,
+            path: tagPath,
+            name: route.name,
+            meta: { ...route.meta }
+          })
+        }
+        if (route.children) {
+          const tempTags = this.filterAffixTags(route.children, route.path)
+          if (tempTags.length >= 1) {
+            tags = [...tags, ...tempTags]
+          }
+        }
+      })
+      return tags
+    },
+    initTags() {
+      const affixTags = this.affixTags = this.filterAffixTags(this.routes)
+      for (const tag of affixTags) {
+        // Must have tag name
+        if (tag.name) {
+          this.$store.dispatch('tagsView/addVisitedView', tag)
+        }
+      }
+    },
+    addTags() {
+      const { name } = this.$route
+      if (name) {
+        this.$store.dispatch('tagsView/addView', this.$route)
+      }
+      return false
+    },
+    moveToCurrentTag() {
+      const tags = this.$refs.tag
+      this.$nextTick(() => {
+        for (const tag of tags) {
+          if (tag.to.path === this.$route.path) {
+            this.$refs.scrollPane.moveToTarget(tag)
+            // when query is different then update
+            if (tag.to.fullPath !== this.$route.fullPath) {
+              this.$store.dispatch('tagsView/updateVisitedView', this.$route)
+            }
+            break
+          }
+        }
+      })
+    },
+    refreshSelectedTag(view) {
+      this.$store.dispatch('tagsView/delCachedView', view).then(() => {
+        const { fullPath } = view
+        this.$nextTick(() => {
+          this.$router.replace({
+            path: '/redirect' + fullPath
+          })
+        })
+      })
+    },
+    closeSelectedTag(view) {
+      this.$store.dispatch('tagsView/delView', view).then(({ visitedViews }) => {
+        if (this.isActive(view)) {
+          this.toLastView(visitedViews, view)
+        }
+      })
+    },
+    closeOthersTags() {
+      this.$router.push(this.selectedTag)
+      this.$store.dispatch('tagsView/delOthersViews', this.selectedTag).then(() => {
+        this.moveToCurrentTag()
+      })
+    },
+    closeAllTags(view) {
+      this.$store.dispatch('tagsView/delAllViews').then(({ visitedViews }) => {
+        if (this.affixTags.some(tag => tag.path === view.path)) {
+          return
+        }
+        this.toLastView(visitedViews, view)
+      })
+    },
+    toLastView(visitedViews, view) {
+      const latestView = visitedViews.slice(-1)[0]
+      if (latestView) {
+        this.$router.push(latestView.fullPath)
+      } else {
+        // now the default is to redirect to the home page if there is no tags-view,
+        // you can adjust it according to your needs.
+        if (view.name === 'Dashboard') {
+          // to reload home page
+          this.$router.replace({ path: '/redirect' + view.fullPath })
+        } else {
+          this.$router.push('/')
+        }
+      }
+    },
+    openMenu(tag, e) {
+      const menuMinWidth = 105
+      const offsetLeft = this.$el.getBoundingClientRect().left // container margin left
+      const offsetWidth = this.$el.offsetWidth // container width
+      const maxLeft = offsetWidth - menuMinWidth // left boundary
+      const left = e.clientX - offsetLeft + 15 // 15: margin right
+
+      if (left > maxLeft) {
+        this.left = maxLeft
+      } else {
+        this.left = left
+      }
+
+      this.top = e.clientY
+      this.visible = true
+      this.selectedTag = tag
+    },
+    closeMenu() {
+      this.visible = false
+    },
+    handleScroll() {
+      this.closeMenu()
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.tags-view-container {
+  height: 34px;
+  width: 100%;
+  background: #fff;
+  border-bottom: 1px solid #d8dce5;
+  box-shadow: 0 1px 3px 0 rgba(0, 0, 0, .12), 0 0 3px 0 rgba(0, 0, 0, .04);
+  .tags-view-wrapper {
+    .tags-view-item {
+      display: inline-block;
+      position: relative;
+      cursor: pointer;
+      height: 26px;
+      line-height: 26px;
+      border: 1px solid #d8dce5;
+      color: #495060;
+      background: #fff;
+      padding: 0 8px;
+      font-size: 12px;
+      margin-left: 5px;
+      margin-top: 4px;
+      &:first-of-type {
+        margin-left: 15px;
+      }
+      &:last-of-type {
+        margin-right: 15px;
+      }
+      &.active {
+        background-color: #42b983;
+        color: #fff;
+        border-color: #42b983;
+        &::before {
+          content: '';
+          background: #fff;
+          display: inline-block;
+          width: 8px;
+          height: 8px;
+          border-radius: 50%;
+          position: relative;
+          margin-right: 2px;
+        }
+      }
+    }
+  }
+  .contextmenu {
+    margin: 0;
+    background: #fff;
+    z-index: 3000;
+    position: absolute;
+    list-style-type: none;
+    padding: 5px 0;
+    border-radius: 4px;
+    font-size: 12px;
+    font-weight: 400;
+    color: #333;
+    box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, .3);
+    li {
+      margin: 0;
+      padding: 7px 16px;
+      cursor: pointer;
+      &:hover {
+        background: #eee;
+      }
+    }
+  }
+}
+</style>
+
+<style lang="scss">
+//reset element css of el-icon-close
+.tags-view-wrapper {
+  .tags-view-item {
+    .el-icon-close {
+      width: 16px;
+      height: 16px;
+      vertical-align: 2px;
+      border-radius: 50%;
+      text-align: center;
+      transition: all .3s cubic-bezier(.645, .045, .355, 1);
+      transform-origin: 100% 50%;
+      &:before {
+        transform: scale(.6);
+        display: inline-block;
+        vertical-align: -3px;
+      }
+      &:hover {
+        background-color: #b4bccc;
+        color: #fff;
+      }
+    }
+  }
+}
+</style>

+ 5 - 0
src/layout/components/index.js

@@ -0,0 +1,5 @@
+export { default as AppMain } from './AppMain'
+export { default as Navbar } from './Navbar'
+export { default as Settings } from './Settings'
+export { default as Sidebar } from './Sidebar/index.vue'
+export { default as TagsView } from './TagsView/index.vue'

+ 126 - 0
src/layout/index.vue

@@ -0,0 +1,126 @@
+<template>
+  <div :class="classObj" class="app-wrapper">
+    <div v-if="device==='mobile'&&sidebar.opened" class="drawer-bg" @click="handleClickOutside" />
+    <sidebar class="sidebar-container" />
+    <div :class="{hasTagsView:needTagsView}" class="main-container">
+      <div :class="{'fixed-header':fixedHeader}">
+        <navbar />
+        <tags-view v-if="needTagsView" />
+      </div>
+      <app-main />
+      <right-panel v-if="showSettings">
+        <settings />
+      </right-panel>
+    </div>
+  </div>
+</template>
+
+<script>
+import RightPanel from '@/components/RightPanel'
+import { AppMain, Navbar, Settings, Sidebar, TagsView } from './components'
+import ResizeMixin from './mixin/ResizeHandler'
+import { mapState } from 'vuex'
+import { heartbeat } from '@/api/user'
+import { getToken } from '@/utils/auth'
+export default {
+  name: 'Layout',
+  components: {
+    AppMain,
+    Navbar,
+    RightPanel,
+    Settings,
+    Sidebar,
+    TagsView
+  },
+  mixins: [ResizeMixin],
+  data() {
+    return {
+      heartbeatTimer: null
+    }
+  },
+  computed: {
+    ...mapState({
+      sidebar: state => state.app.sidebar,
+      device: state => state.app.device,
+      showSettings: state => state.settings.showSettings,
+      needTagsView: state => state.settings.tagsView,
+      fixedHeader: state => state.settings.fixedHeader
+    }),
+    classObj() {
+      return {
+        hideSidebar: !this.sidebar.opened,
+        openSidebar: this.sidebar.opened,
+        withoutAnimation: this.sidebar.withoutAnimation,
+        mobile: this.device === 'mobile'
+      }
+    }
+  },
+  created() {
+    if (getToken() && !this.heartbeatTimer) {
+      // 如果有token,并且未启动心跳,进行心跳维持
+      this.heartbeatTimer = setInterval(() => {
+        heartbeat({}).then(async res => {})
+      }, 60000)
+    }
+  },
+  methods: {
+    handleClickOutside() {
+      this.$store.dispatch('app/closeSideBar', { withoutAnimation: false })
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+  @import "~@/styles/mixin.scss";
+  @import "~@/styles/variables.scss";
+
+  .app-wrapper {
+    @include clearfix;
+    position: relative;
+    height: 100%;
+    width: 100%;
+    &.hideSidebar {
+      ::v-deep {
+        .sidebar-logo-link {
+          .sidebar-logo {
+            width: 40px;
+            height: 40px;
+            object-fit: contain;
+          }
+        }
+      }
+    }
+    &.mobile.openSidebar {
+      position: fixed;
+      top: 0;
+    }
+  }
+
+  .drawer-bg {
+    background: #000;
+    opacity: 0.3;
+    width: 100%;
+    top: 0;
+    height: 100%;
+    position: absolute;
+    z-index: 999;
+  }
+
+  .fixed-header {
+    position: fixed;
+    top: 0;
+    right: 0;
+    z-index: 9;
+    width: calc(100% - #{$sideBarWidth});
+    transition: width 0.28s;
+  }
+
+  .hideSidebar .fixed-header {
+    width: calc(100% - 54px)
+  }
+
+  .mobile .fixed-header {
+    width: 100%;
+  }
+</style>

+ 45 - 0
src/layout/mixin/ResizeHandler.js

@@ -0,0 +1,45 @@
+import store from '@/store'
+
+const { body } = document
+const WIDTH = 992 // refer to Bootstrap's responsive design
+
+export default {
+  watch: {
+    $route(route) {
+      if (this.device === 'mobile' && this.sidebar.opened) {
+        store.dispatch('app/closeSideBar', { withoutAnimation: false })
+      }
+    }
+  },
+  beforeMount() {
+    window.addEventListener('resize', this.$_resizeHandler)
+  },
+  beforeDestroy() {
+    window.removeEventListener('resize', this.$_resizeHandler)
+  },
+  mounted() {
+    const isMobile = this.$_isMobile()
+    if (isMobile) {
+      store.dispatch('app/toggleDevice', 'mobile')
+      store.dispatch('app/closeSideBar', { withoutAnimation: true })
+    }
+  },
+  methods: {
+    // use $_ for mixins properties
+    // https://vuejs.org/v2/style-guide/index.html#Private-property-names-essential
+    $_isMobile() {
+      const rect = body.getBoundingClientRect()
+      return rect.width - 1 < WIDTH
+    },
+    $_resizeHandler() {
+      if (!document.hidden) {
+        const isMobile = this.$_isMobile()
+        store.dispatch('app/toggleDevice', isMobile ? 'mobile' : 'desktop')
+
+        if (isMobile) {
+          store.dispatch('app/closeSideBar', { withoutAnimation: true })
+        }
+      }
+    }
+  }
+}

+ 4 - 2
src/main.js

@@ -5,10 +5,11 @@ import Cookies from 'js-cookie'
 import Element from 'element-ui'
 import VueHighlightJS from 'vue-highlightjs'
 // import enLang from 'element-ui/lib/locale/lang/en'// 如果使用中文语言包请默认支持,无需额外引入,请删除该依赖
-
 import './permission' // permission control
 // import 'font-awesome/css/font-awesome.min.css'
-
+import App from './App'
+import store from './store'
+import router from './router'
 import * as filters from './filters' // global filters
 
 /**
@@ -111,6 +112,7 @@ Vue.prototype.$bus = new Vue()
 new Vue({
   el: '#app',
   store,
+  router,
   render: h => h(App)
 })
 

+ 1 - 1
src/router/index.js

@@ -4,7 +4,7 @@ import Router from 'vue-router'
 Vue.use(Router)
 
 /* Layout */
-// import Layout from '@/layout'
+import Layout from '@/layout'
 /* Router Modules */
 /**
  * Note: sub-menu only appear when route children.length >= 1