瀏覽代碼

提交初始分支

yanj 3 月之前
父節點
當前提交
54fbc80adc

+ 1 - 1
public/index.html

@@ -6,7 +6,7 @@
   <meta name="renderer" content="webkit">
   <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
   <link rel="icon" href="/static/image/<%= VUE_APP_NAME %>/favicon.png">
-  <title><%= VUE_APP_WEBSITE_NAME %></title>
+  <title>Website</title>
 </head>
 <body>
 <div id="app"></div>

+ 0 - 20
src/main.js

@@ -5,22 +5,9 @@ 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 '@/styles/index.scss' // global css
-// import 'element-ui/packages/theme-chalk/src/index.scss'
-import './assets/iconfont/iconfont.css' // 字体编码
-import './assets/iconfont/iconfont.js' // 字体编码
-import App from './App'
-import store from './store'
-import router from './router'
 
-import UDialog from '@/components/Common/ui/dialog'
-import USelect from '@/components/Common/ui/select'
-
-import './icons' // icon
 import './permission' // permission control
-import './utils/error-log' // error log
 // import 'font-awesome/css/font-awesome.min.css'
-import * as common from './utils/index.js'
 
 import * as filters from './filters' // global filters
 
@@ -42,18 +29,12 @@ Vue.use(Element, {
   // locale: enLang // 如果使用中文,无需设置,请删除
 })
 Vue.use(VueHighlightJS)
-Vue.component(UDialog.name, UDialog)
-Vue.component(USelect.name, USelect)
 
 // register global utility filters
 Object.keys(filters).forEach(key => {
   Vue.filter(key, filters[key])
 })
 
-Object.keys(common).forEach(key => {
-  Vue.filter(key, filters[key])
-})
-Vue.prototype.$coomon = common
 Vue.config.productionTip = false
 Vue.prototype.$alert = function(message, ...values) {
   Element.MessageBox.alert(this['$tc'](message, ...values), '系统提示')
@@ -129,7 +110,6 @@ Vue.prototype.generateSlug = function(title) {
 Vue.prototype.$bus = new Vue()
 new Vue({
   el: '#app',
-  router,
   store,
   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

+ 15 - 0
src/utils/auth.js

@@ -0,0 +1,15 @@
+import Cookies from 'js-cookie'
+
+const TokenKey = 'A09791F308B1D415C98BBFBAF6B7D66A3Admin-Token'
+
+export function getToken() {
+  return Cookies.get(TokenKey)
+}
+
+export function setToken(token) {
+  return Cookies.set(TokenKey, token, { expires: 999 })
+}
+
+export function removeToken() {
+  return Cookies.remove(TokenKey)
+}

+ 32 - 0
src/utils/clipboard.js

@@ -0,0 +1,32 @@
+import Vue from 'vue'
+import Clipboard from 'clipboard'
+
+function clipboardSuccess() {
+  Vue.prototype.$message({
+    message: 'Copy successfully',
+    type: 'success',
+    duration: 1500
+  })
+}
+
+function clipboardError() {
+  Vue.prototype.$message({
+    message: 'Copy failed',
+    type: 'error'
+  })
+}
+
+export default function handleClipboard(text, event) {
+  const clipboard = new Clipboard(event.target, {
+    text: () => text
+  })
+  clipboard.on('success', () => {
+    clipboardSuccess()
+    clipboard.destroy()
+  })
+  clipboard.on('error', () => {
+    clipboardError()
+    clipboard.destroy()
+  })
+  clipboard.onClick(event)
+}

+ 35 - 0
src/utils/error-log.js

@@ -0,0 +1,35 @@
+import Vue from 'vue'
+import store from '@/store'
+import { isString, isArray } from '@/utils/validate'
+import settings from '@/settings'
+
+// you can set in settings.js
+// errorLog:'production' | ['production', 'development']
+const { errorLog: needErrorLog } = settings
+
+function checkNeed() {
+  const env = process.env.NODE_ENV
+  if (isString(needErrorLog)) {
+    return env === needErrorLog
+  }
+  if (isArray(needErrorLog)) {
+    return needErrorLog.includes(env)
+  }
+  return false
+}
+
+if (checkNeed()) {
+  Vue.config.errorHandler = function(err, vm, info, a) {
+  // Don't ask me why I use Vue.nextTick, it just a hack.
+  // detail see https://forum.vuejs.org/t/dispatch-in-vue-config-errorhandler-has-some-problem/23500
+    Vue.nextTick(() => {
+      store.dispatch('errorLog/addErrorLog', {
+        err,
+        vm,
+        info,
+        url: window.location.href
+      })
+      console.error(err, info)
+    })
+  }
+}

+ 10 - 0
src/utils/get-page-title.js

@@ -0,0 +1,10 @@
+import defaultSettings from '@/settings'
+
+const title = defaultSettings.title || 'Vue Element Admin'
+
+export default function getPageTitle(pageTitle) {
+  if (pageTitle) {
+    return `${pageTitle} - ${title}`
+  }
+  return `${title}`
+}

+ 579 - 0
src/utils/index.js

@@ -0,0 +1,579 @@
+import axios from 'axios'
+
+/**
+ * 数据补全方法:将数据b的value值合并到数据a的对应key中
+ * @param {Array} dataA 标准数据(需补全的原始数据)
+ * @param {Array} dataB 待合并数据(提供补全值的数据)
+ * @returns {Array} 补全后的数据
+ */
+export function mergeData(dataA, dataB) {
+  // 深拷贝原始数据避免污染‌:ml-citation{ref="1" data="citationList"}
+  const mergedData = JSON.parse(JSON.stringify(dataA))
+
+  // 遍历数据a,根据key匹配数据b并补全value‌:ml-citation{ref="4" data="citationList"}
+  mergedData.forEach(itemA => {
+    const matchedItemB = dataB.find(itemB => itemB.key === itemA.key)
+    if (matchedItemB) {
+      itemA.value = matchedItemB.value // 合并value
+      itemA.alt = matchedItemB.alt || itemA.alt // 可选:合并其他字段如alt
+    }
+  })
+
+  return mergedData
+}
+
+/**
+ * Created by PanJiaChen on 16/11/18.
+ */
+
+/**
+ * Parse the time to string
+ * @param {(Object|string|number)} time
+ * @param {string} cFormat
+ * @returns {string | null}
+ */
+export function parseTime(time, cFormat) {
+  if (arguments.length === 0 || !time) {
+    return null
+  }
+  const format = cFormat || '{y}-{m}-{d} {h}:{i}:{s}'
+  let date
+  if (typeof time === 'object') {
+    date = time
+  } else {
+    if ((typeof time === 'string')) {
+      if ((/^[0-9]+$/.test(time))) {
+        // support "1548221490638"
+        time = parseInt(time)
+      } else {
+        // support safari
+        // https://stackoverflow.com/questions/4310953/invalid-date-in-safari
+        time = time.replace(new RegExp(/-/gm), '/')
+      }
+    }
+
+    if ((typeof time === 'number') && (time.toString().length === 10)) {
+      time = time * 1000
+    }
+    date = new Date(time)
+  }
+  const formatObj = {
+    y: date.getFullYear(),
+    m: date.getMonth() + 1,
+    d: date.getDate(),
+    h: date.getHours(),
+    i: date.getMinutes(),
+    s: date.getSeconds(),
+    a: date.getDay()
+  }
+  const time_str = format.replace(/{([ymdhisa])+}/g, (result, key) => {
+    const value = formatObj[key]
+    // Note: getDay() returns 0 on Sunday
+    if (key === 'a') {
+      return ['日', '一', '二', '三', '四', '五', '六'][value]
+    }
+    return value.toString().padStart(2, '0')
+  })
+  return time_str
+}
+
+/**
+ * @param {number} time
+ * @param {string} option
+ * @returns {string}
+ */
+export function formatTime(time, option) {
+  if (('' + time).length === 10) {
+    time = parseInt(time) * 1000
+  } else {
+    time = +time
+  }
+  const d = new Date(time)
+  const now = Date.now()
+
+  const diff = (now - d) / 1000
+
+  if (diff < 30) {
+    return '刚刚'
+  } else if (diff < 3600) {
+    // less 1 hour
+    return Math.ceil(diff / 60) + '分钟前'
+  } else if (diff < 3600 * 24) {
+    return Math.ceil(diff / 3600) + '小时前'
+  } else if (diff < 3600 * 24 * 2) {
+    return '1天前'
+  }
+  if (option) {
+    return parseTime(time, option)
+  } else {
+    return (
+      d.getMonth() +
+      1 +
+      '月' +
+      d.getDate() +
+      '日' +
+      d.getHours() +
+      '时' +
+      d.getMinutes() +
+      '分'
+    )
+  }
+}
+
+/**
+ * @param {string} url
+ * @returns {Object}
+ */
+export function getQueryObject(url) {
+  url = url == null ? window.location.href : url
+  const search = url.substring(url.lastIndexOf('?') + 1)
+  const obj = {}
+  const reg = /([^?&=]+)=([^?&=]*)/g
+  search.replace(reg, (rs, $1, $2) => {
+    const name = decodeURIComponent($1)
+    let val = decodeURIComponent($2)
+    val = String(val)
+    obj[name] = val
+    return rs
+  })
+  return obj
+}
+
+/**
+ * @param {string} input value
+ * @returns {number} output value
+ */
+export function byteLength(str) {
+  // returns the byte length of an utf8 string
+  let s = str.length
+  for (var i = str.length - 1; i >= 0; i--) {
+    const code = str.charCodeAt(i)
+    if (code > 0x7f && code <= 0x7ff) {
+      s++
+    } else if (code > 0x7ff && code <= 0xffff) s += 2
+    if (code >= 0xDC00 && code <= 0xDFFF) i--
+  }
+  return s
+}
+
+/**
+ * @param {Array} actual
+ * @returns {Array}
+ */
+export function cleanArray(actual) {
+  const newArray = []
+  for (let i = 0; i < actual.length; i++) {
+    if (actual[i]) {
+      newArray.push(actual[i])
+    }
+  }
+  return newArray
+}
+
+/**
+ * @param {Object} json
+ * @returns {Array}
+ */
+export function param(json) {
+  if (!json) return ''
+  return cleanArray(
+    Object.keys(json).map(key => {
+      if (json[key] === undefined) return ''
+      return encodeURIComponent(key) + '=' + encodeURIComponent(json[key])
+    })
+  ).join('&')
+}
+
+/**
+ * @param {string} url
+ * @returns {Object}
+ */
+export function param2Obj(url) {
+  const search = decodeURIComponent(url.split('?')[1]).replace(/\+/g, ' ')
+  if (!search) {
+    return {}
+  }
+  const obj = {}
+  const searchArr = search.split('&')
+  searchArr.forEach(v => {
+    const index = v.indexOf('=')
+    if (index !== -1) {
+      const name = v.substring(0, index)
+      const val = v.substring(index + 1, v.length)
+      obj[name] = val
+    }
+  })
+  return obj
+}
+
+/**
+ * @param {string} val
+ * @returns {string}
+ */
+export function html2Text(val) {
+  const div = document.createElement('div')
+  div.innerHTML = val
+  return div.textContent || div.innerText
+}
+
+/**
+ * Merges two objects, giving the last one precedence
+ * @param {Object} target
+ * @param {(Object|Array)} source
+ * @returns {Object}
+ */
+export function objectMerge(target, source) {
+  if (typeof target !== 'object') {
+    target = {}
+  }
+  if (Array.isArray(source)) {
+    return source.slice()
+  }
+  Object.keys(source).forEach(property => {
+    const sourceProperty = source[property]
+    if (typeof sourceProperty === 'object') {
+      target[property] = objectMerge(target[property], sourceProperty)
+    } else {
+      target[property] = sourceProperty
+    }
+  })
+  return target
+}
+
+/**
+ * @param {HTMLElement} element
+ * @param {string} className
+ */
+export function toggleClass(element, className) {
+  if (!element || !className) {
+    return
+  }
+  let classString = element.className
+  const nameIndex = classString.indexOf(className)
+  if (nameIndex === -1) {
+    classString += '' + className
+  } else {
+    classString =
+      classString.substr(0, nameIndex) +
+      classString.substr(nameIndex + className.length)
+  }
+  element.className = classString
+}
+
+/**
+ * @param {string} type
+ * @returns {Date}
+ */
+export function getTime(type) {
+  if (type === 'start') {
+    return new Date().getTime() - 3600 * 1000 * 24 * 90
+  } else {
+    return new Date(new Date().toDateString())
+  }
+}
+
+/**
+ * @param {Function} func
+ * @param {number} wait
+ * @param {boolean} immediate
+ * @return {*}
+ */
+export function debounce(func, wait, immediate) {
+  let timeout, args, context, timestamp, result
+
+  const later = function() {
+    // 据上一次触发时间间隔
+    const last = +new Date() - timestamp
+
+    // 上次被包装函数被调用时间间隔 last 小于设定时间间隔 wait
+    if (last < wait && last > 0) {
+      timeout = setTimeout(later, wait - last)
+    } else {
+      timeout = null
+      // 如果设定为immediate===true,因为开始边界已经调用过了此处无需调用
+      if (!immediate) {
+        result = func.apply(context, args)
+        if (!timeout) context = args = null
+      }
+    }
+  }
+
+  return function(...args) {
+    context = this
+    timestamp = +new Date()
+    const callNow = immediate && !timeout
+    // 如果延时不存在,重新设定延时
+    if (!timeout) timeout = setTimeout(later, wait)
+    if (callNow) {
+      result = func.apply(context, args)
+      context = args = null
+    }
+
+    return result
+  }
+}
+
+/**
+ * This is just a simple version of deep copy
+ * Has a lot of edge cases bug
+ * If you want to use a perfect deep copy, use lodash's _.cloneDeep
+ * @param {Object} source
+ * @returns {Object}
+ */
+export function deepClone(source) {
+  if (!source && typeof source !== 'object') {
+    throw new Error('error arguments', 'deepClone')
+  }
+  const targetObj = source.constructor === Array ? [] : {}
+  Object.keys(source).forEach(keys => {
+    if (source[keys] && typeof source[keys] === 'object') {
+      targetObj[keys] = deepClone(source[keys])
+    } else {
+      targetObj[keys] = source[keys]
+    }
+  })
+  return targetObj
+}
+
+/**
+ * @param {Array} arr
+ * @returns {Array}
+ */
+export function uniqueArr(arr) {
+  return Array.from(new Set(arr))
+}
+
+/**
+ * @returns {string}
+ */
+export function createUniqueString() {
+  const timestamp = +new Date() + ''
+  const randomNum = parseInt((1 + Math.random()) * 65536) + ''
+  return (+(randomNum + timestamp)).toString(32)
+}
+
+/**
+ * Check if an element has a class
+ * @param {HTMLElement} elm
+ * @param {string} cls
+ * @returns {boolean}
+ */
+export function hasClass(ele, cls) {
+  return !!ele.className.match(new RegExp('(\\s|^)' + cls + '(\\s|$)'))
+}
+
+/**
+ * Add class to element
+ * @param {HTMLElement} elm
+ * @param {string} cls
+ */
+export function addClass(ele, cls) {
+  if (!hasClass(ele, cls)) ele.className += ' ' + cls
+}
+
+/**
+ * Remove class from element
+ * @param {HTMLElement} elm
+ * @param {string} cls
+ */
+export function removeClass(ele, cls) {
+  if (hasClass(ele, cls)) {
+    const reg = new RegExp('(\\s|^)' + cls + '(\\s|$)')
+    ele.className = ele.className.replace(reg, ' ')
+  }
+}
+
+/**
+ * 引入dateFormat的处理
+ */
+export function dateFormat(time, format) {
+  format = format || 'Y-m-d H:i'
+  var data = new Date(time)
+  var o = {
+    'm+': prefixInteger(data.getMonth() + 1, 2), // 月份
+    'd+': prefixInteger(data.getDate(), 2), // 日
+    'H+': prefixInteger(data.getHours(), 2), // 小时
+    'i+': prefixInteger(data.getMinutes(), 2), // 分
+    's+': prefixInteger(data.getSeconds(), 2), // 秒
+    'q+': Math.floor((data.getMonth() + 3) / 3), // 季度
+    'S': prefixInteger(data.getMilliseconds(), 3) // 毫秒
+  }
+  if (/(Y+)/.test(format)) {
+    format = format.replace(RegExp.$1, (data.getFullYear() + ''))
+  }
+  for (var k in o) {
+    if (new RegExp('(' + k + ')').test(format)) {
+      format = format.replace(RegExp.$1, (RegExp.$1.length === 1) ? (o[k]) : (('00' + o[k]).substr(('' + o[k]).length)))
+    }
+  }
+  return format
+}
+
+/**
+ * 自动补零
+ * @param num
+ * @param length
+ * @returns {string}
+ * @constructor
+ */
+function prefixInteger(num, length) {
+  return (Array(length).join('0') + num).slice(-length)
+}
+
+/**
+ * 获取语言
+ * @returns string
+ */
+export function getLanguage() {
+  const defLanguage = 'zh-cn'
+  let language = localStorage.getItem('trtc-tuiPusher-language') || getUrlParam('lang') || defLanguage
+  language = language.replace(/_/, '-').toLowerCase()
+  if (language === 'zh-cn' || language === 'zh') {
+    language = 'zh-cn'
+  } else if (language === 'en' || language === 'en-us' || language === 'en-gb') {
+    language = 'en-us'
+  }
+  return language
+}
+
+/**
+ * 从 window.location.href 中获取指定key的value
+ * @param {*} key 要获取的 key
+ * @returns window.location.href 中指定key对应的value
+ * @example
+ * const value = getUrlParam(key);
+ */
+export function getUrlParam(key) {
+  const url = window.location.href.replace(/^[^?]*\?/, '')
+  const regexp = new RegExp(`(^|&)${key}=([^&#]*)(&|$|)`, 'i')
+  const paramMatch = url.match(regexp)
+
+  return paramMatch ? paramMatch[2] : null
+}
+
+export function copyValue(val, callback) {
+  const input = document.createElement('input')
+  input.setAttribute('readonly', 'readonly') // 设置为只读, 防止在 ios 下拉起键盘
+  // input.setAttribute('value', value); // textarea 不能用此方式赋值, 否则无法复制内容
+  input.value = val
+  document.body.appendChild(input)
+  input.setSelectionRange(0, 9999) // 防止 ios 下没有全选内容而无法复制
+  input.select()
+  document.execCommand('copy')
+  document.body.removeChild(input)
+  if (callback) {
+    callback()
+  }
+}
+
+/**
+ * 返回不合法的head内的标签
+ * @param text
+ * @returns {string}
+ */
+export function getInvalidHeadTags(text) {
+  const validTags = new RegExp('title|meta|base|style|script|noscript|link', 'i')
+  const pattern = new RegExp('<[^>]+>', 'ig')
+  const complain = new RegExp('<!--[^>]*>', 'ig')
+  let tagArr = []
+  let boolValidTag = -1
+  let boolComplain = -1
+  if (text) {
+    tagArr = text.match(pattern)
+    if (!tagArr) {
+      return text
+    }
+    if (tagArr.length > 0) {
+      const newArr = []
+      for (const i in tagArr) {
+        boolValidTag = tagArr[i].search(validTags)
+        boolComplain = tagArr[i].search(complain)
+        if (boolValidTag === -1 && boolComplain === -1) {
+          newArr.push(tagArr[i])
+        }
+      }
+      tagArr = newArr
+    }
+  }
+  return tagArr.join('')
+}
+
+export function uploadFileData(input, callback) {
+  if (input) {
+    if (!input.hasAttribute('data-change-listener')) {
+      input.setAttribute('data-change-listener', 'true')
+      input.addEventListener('change', (e) => {
+        let file = ''
+        if (e.target.files && e.target.files[0]) {
+          file = e.target.files[0]
+        }
+        const max_size = 1048576 * 20
+        if (file.size > max_size) {
+          return false
+        }
+        if (callback) {
+          callback(file)
+        }
+      })
+    }
+  }
+}
+
+export async function getAiToken(callback) {
+  await axios.post('https://apiv2-cn.matchexpo.cn/admin/admin-login', {
+    password: 'Yj1132192460!',
+    user_name: 'yanjian1'
+  }).then(res => {
+    if (res.data.token) {
+      if (callback) {
+        callback(res.data.token)
+      }
+      return res.data.token
+    }
+  })
+}
+
+export async function translateText(content, translate_lang, token, callback) {
+  if (typeof content === 'string' && !(content.replace(/\s/g, ''))) {
+    return ''
+  }
+  await axios.post('https://apiv2-cn.matchexpo.cn/admin/translate/content', {
+    'content': content,
+    'language': [translate_lang],
+    'prompte': '请将内容翻译为' + translate_lang
+  }, {
+    headers: {
+      token: token
+    }
+  }).then(async res => {
+    if (typeof content === 'string') {
+      let text = res.data[translate_lang].replace('[', '')
+      text = text.replace(']', '')
+      await callback(text)
+    } else {
+      await callback(res.data[translate_lang])
+    }
+  })
+}
+
+export async function translateContent(content, item, translate_lang, token) {
+  const parser = new DOMParser()
+  const doc = parser.parseFromString(content, 'text/html')
+  // 解析为html节点
+  await traverse(doc.body)
+  item.value = doc.body.innerHTML
+
+  async function traverse(node) {
+    if (node.nodeType === Node.TEXT_NODE) {
+      const originalText = node.textContent
+      // originalText 为文本节点内容,下面执行你的方法
+      await translateText(originalText, translate_lang, token, (text) => {
+        node.textContent = text
+      })
+    } else {
+      for (const child of node.childNodes) {
+        await traverse(child)
+      }
+    }
+  }
+}

+ 25 - 0
src/utils/open-window.js

@@ -0,0 +1,25 @@
+/**
+ *Created by PanJiaChen on 16/11/29.
+ * @param {Sting} url
+ * @param {Sting} title
+ * @param {Number} w
+ * @param {Number} h
+ */
+export default function openWindow(url, title, w, h) {
+  // Fixes dual-screen position                            Most browsers       Firefox
+  const dualScreenLeft = window.screenLeft !== undefined ? window.screenLeft : screen.left
+  const dualScreenTop = window.screenTop !== undefined ? window.screenTop : screen.top
+
+  const width = window.innerWidth ? window.innerWidth : document.documentElement.clientWidth ? document.documentElement.clientWidth : screen.width
+  const height = window.innerHeight ? window.innerHeight : document.documentElement.clientHeight ? document.documentElement.clientHeight : screen.height
+
+  const left = ((width / 2) - (w / 2)) + dualScreenLeft
+  const top = ((height / 2) - (h / 2)) + dualScreenTop
+  const newWindow = window.open(url, title, 'toolbar=no, location=no, directories=no, status=no, menubar=no, scrollbars=no, resizable=yes, copyhistory=no, width=' + w + ', height=' + h + ', top=' + top + ', left=' + left)
+
+  // Puts focus on the newWindow
+  if (window.focus) {
+    newWindow.focus()
+  }
+}
+

+ 21 - 0
src/utils/permission.js

@@ -0,0 +1,21 @@
+import store from '@/store'
+
+/**
+ * @param {Array} value
+ * @returns {Boolean}
+ * @example see @/views/permission/directive.vue
+ */
+export default function checkPermission(value) {
+  if (value && value instanceof Array && value.length > 0) {
+    const roles = store.getters && store.getters.roles
+    const permissionRoles = value
+
+    const hasPermission = roles.some(role => {
+      return permissionRoles.includes(role)
+    })
+    return hasPermission
+  } else {
+    console.error(`need roles! Like v-permission="['admin','editor']"`)
+    return false
+  }
+}

+ 139 - 0
src/utils/request.js

@@ -0,0 +1,139 @@
+import axios from 'axios'
+import { MessageBox, Message } from 'element-ui'
+import store from '@/store'
+import { getToken } from '@/utils/auth'
+
+/**
+ * 创建请求
+ * @param baseURL
+ */
+export const createRequest = function(baseURL) {
+  // create an axios instance
+  const service = axios.create({
+    baseURL: baseURL || process.env.VUE_APP_BASE_API, // url = base url + request url
+    // withCredentials: true, // send cookies when cross-domain requests
+    timeout: 30000, // request timeout,
+    headers: {
+      'X-Requested-With': 'XMLHttpRequest'
+    }
+  })
+  // request interceptor
+  service.interceptors.request.use(
+    config => {
+      // do something before request is sent
+
+      if (store.getters.token) {
+        // let each request carry token
+        // ['X-Token'] is a custom headers key
+        // please modify it according to the actual situation
+        // config.headers['X-Token'] = getToken()
+        config.headers['token'] = getToken()
+      }
+      return config
+    },
+    error => {
+      // do something with request error
+      console.log(error) // for debug
+      return Promise.reject(error)
+    }
+  )
+
+  // response interceptor
+  service.interceptors.response.use(
+    /**
+     * If you want to get http information such as headers or status
+     * Please return  response => response
+     */
+
+    /**
+     * Determine the request status by custom code
+     * Here is just an example
+     * You can also judge the status by HTTP Status Code
+     */
+    response => {
+      const res = response.data
+      let code = 0
+      let message = ''
+      if (res.ret !== undefined) {
+        code = res.ret
+        message = res.msg
+      } else {
+        code = res.code
+        message = res.message
+      }
+      // if the custom code is not 20000, it is judged as an error.
+      if (code !== 0) {
+        var errorStr = ''
+        if (message) {
+          errorStr = message
+        } else {
+          errorStr = typeof res === 'string' ? res : 'Error'
+        }
+        if (code === 401) {
+          Message({
+            message: errorStr,
+            type: 'error',
+            duration: 5 * 1000
+          })
+          store.dispatch('user/reLogout').then(() => {
+            location.reload()
+          })
+        } else {
+          Message({
+            message: errorStr,
+            type: 'error',
+            duration: 5 * 1000
+          })
+        }
+        // 50008: Illegal token; 50012: Other clients logged in; 50014: Token expired;
+        if (code === 50008 || code === 50012 || code === 50014 || code === 500) {
+          // to re-login
+          MessageBox.confirm('You have been logged out, you can cancel to stay on this page, or log in again', 'Confirm logout', {
+            confirmButtonText: 'Re-Login',
+            cancelButtonText: 'Cancel',
+            type: 'warning'
+          }).then(() => {
+            store.dispatch('user/reLogout').then(() => {
+              location.reload()
+            })
+          })
+        }
+        return Promise.reject(new Error(res.message || 'Error'))
+      } else {
+        return res
+      }
+    },
+    (error) => {
+      Message({
+        message: error.message,
+        type: 'error',
+        duration: 5 * 1000
+      })
+      console.log(error.config.url)
+      if (error.config.url === '/api/admin/user/get-admin-user') {
+        store.dispatch('user/reLogout').then(() => {
+          location.reload()
+        })
+      }
+      return Promise.reject(error)
+    }
+  )
+  service.api = {
+    get: (url) => (params, cancelToken) => {
+      return service.get(url, {
+        'params': params,
+        'cancelToken': cancelToken
+      })
+    },
+    post: (url) => (params, config) => {
+      return service.post(url, params, config)
+    },
+    delete: (url) => (params) => {
+      return service.delete(url, params)
+    },
+    link: url => url
+  }
+  return service
+}
+const service = createRequest()
+export default service

+ 58 - 0
src/utils/scroll-to.js

@@ -0,0 +1,58 @@
+Math.easeInOutQuad = function(t, b, c, d) {
+  t /= d / 2
+  if (t < 1) {
+    return c / 2 * t * t + b
+  }
+  t--
+  return -c / 2 * (t * (t - 2) - 1) + b
+}
+
+// requestAnimationFrame for Smart Animating http://goo.gl/sx5sts
+var requestAnimFrame = (function() {
+  return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || function(callback) { window.setTimeout(callback, 1000 / 60) }
+})()
+
+/**
+ * Because it's so fucking difficult to detect the scrolling element, just move them all
+ * @param {number} amount
+ */
+function move(amount) {
+  document.documentElement.scrollTop = amount
+  document.body.parentNode.scrollTop = amount
+  document.body.scrollTop = amount
+}
+
+function position() {
+  return document.documentElement.scrollTop || document.body.parentNode.scrollTop || document.body.scrollTop
+}
+
+/**
+ * @param {number} to
+ * @param {number} duration
+ * @param {Function} callback
+ */
+export function scrollTo(to, duration, callback) {
+  const start = position()
+  const change = to - start
+  const increment = 20
+  let currentTime = 0
+  duration = (typeof (duration) === 'undefined') ? 500 : duration
+  var animateScroll = function() {
+    // increment the time
+    currentTime += increment
+    // find the value with the quadratic in-out easing function
+    var val = Math.easeInOutQuad(currentTime, start, change, duration)
+    // move the document.body
+    move(val)
+    // do the animation unless its over
+    if (currentTime < duration) {
+      requestAnimFrame(animateScroll)
+    } else {
+      if (callback && typeof (callback) === 'function') {
+        // the animation is done so lets callback
+        callback()
+      }
+    }
+  }
+  animateScroll()
+}

+ 258 - 0
src/utils/validate.js

@@ -0,0 +1,258 @@
+/**
+ * Created by PanJiaChen on 16/11/18.
+ */
+
+/**
+ * @param {string} path
+ * @returns {Boolean}
+ */
+export function isExternal(path) {
+  return /^(https?:|mailto:|tel:)/.test(path)
+}
+
+/**
+ * @param {string} url
+ * @returns {Boolean}
+ */
+export function validURL(url) {
+  const reg = /^(https?|ftp):\/\/([a-zA-Z0-9.-]+(:[a-zA-Z0-9.&%$-]+)*@)*((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.(com|edu|gov|int|mil|net|org|biz|arpa|info|name|pro|aero|coop|museum|[a-zA-Z]{2}))(:[0-9]+)*(\/($|[a-zA-Z0-9.,?'\\+&%$#=~_-]+))*$/
+  return reg.test(url)
+}
+
+/**
+ * @param {string} str
+ * @returns {Boolean}
+ */
+export function validLowerCase(str) {
+  const reg = /^[a-z]+$/
+  return reg.test(str)
+}
+
+/**
+ * @param {string} str
+ * @returns {Boolean}
+ */
+export function validUpperCase(str) {
+  const reg = /^[A-Z]+$/
+  return reg.test(str)
+}
+
+/**
+ * @param {string} str
+ * @returns {Boolean}
+ */
+export function validAlphabets(str) {
+  const reg = /^[A-Za-z]+$/
+  return reg.test(str)
+}
+
+/**
+ * @param {string} email
+ * @returns {Boolean}
+ */
+export function validEmail(email) {
+  const reg = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
+  return reg.test(email)
+}
+
+/**
+ * @param {string} str
+ * @returns {Boolean}
+ */
+export function isString(str) {
+  if (typeof str === 'string' || str instanceof String) {
+    return true
+  }
+  return false
+}
+
+/**
+ * @param {Array} arg
+ * @returns {Boolean}
+ */
+export function isArray(arg) {
+  if (typeof Array.isArray === 'undefined') {
+    return Object.prototype.toString.call(arg) === '[object Array]'
+  }
+  return Array.isArray(arg)
+}
+/**
+ * @author Matchexpo
+ * @description 判断是否是端口号
+ * @param value
+ * @returns {boolean}
+ */
+export function isPort(value) {
+  const reg =
+    /^([0-9]|[1-9]\d|[1-9]\d{2}|[1-9]\d{3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])$/
+  return reg.test(value)
+}
+
+/**
+ * @author Matchexpo
+ * @description 判断是否是手机号
+ * @param value
+ * @returns {boolean}
+ */
+export function isPhone(value) {
+  const reg = /^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\d{8}$/
+  return reg.test(value)
+}
+
+/**
+ * @author Matchexpo
+ * @description 判断是否是国际手机号
+ * @param value
+ * @returns {boolean}
+ */
+export function isInternationalPhone(value) {
+  const reg = /^(0?|\+?)(\d){8,20}$/
+  return reg.test(value)
+}
+
+/**
+ * @author Matchexpo
+ * @description 判断是否是身份证号(第二代)
+ * @param value
+ * @returns {boolean}
+ */
+export function isIdCard(value) {
+  const reg = /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/
+  return reg.test(value)
+}
+
+/**
+ * @author Matchexpo
+ * @description 判断是否是邮箱
+ * @param value
+ * @returns {boolean}
+ */
+export function isEmail(value) {
+  const reg = /^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/
+  return reg.test(value)
+}
+
+/**
+ * @author Matchexpo
+ * @description 判断是否中文
+ * @param value
+ * @returns {boolean}
+ */
+export function isChina(value) {
+  const reg = /^[\u4E00-\u9FA5]{2,4}$/
+  return reg.test(value)
+}
+
+/**
+ * @author Matchexpo
+ * @description 判断是否为空
+ * @param value
+ * @returns {boolean}
+ */
+export function isBlank(value) {
+  return (
+    value == null ||
+    false ||
+    value === '' ||
+    value.trim() === '' ||
+    value.toLocaleLowerCase().trim() === 'null'
+  )
+}
+
+/**
+ * @author Matchexpo
+ * @description 判断是否为固话
+ * @param value
+ * @returns {boolean}
+ */
+export function isTel(value) {
+  const reg =
+    /^(400|800)([0-9\\-]{7,10})|(([0-9]{4}|[0-9]{3})([- ])?)?([0-9]{7,8})(([- 转])*([0-9]{1,4}))?$/
+  return reg.test(value)
+}
+
+/**
+ * @author Matchexpo
+ * @description 判断是否为数字且最多两位小数
+ * @param value
+ * @returns {boolean}
+ */
+export function isNum(value) {
+  const reg = /^\d+(\.\d{1,2})?$/
+  return reg.test(value)
+}
+
+/**
+ * @author Matchexpo
+ * @description 判断经度 -180.0~+180.0(整数部分为0~180,必须输入1到5位小数)
+ * @param value
+ * @returns {boolean}
+ */
+export function isLongitude(value) {
+  const reg = /^[-|+]?(0?\d{1,2}\.\d{1,5}|1[0-7]?\d{1}\.\d{1,5}|180\.0{1,5})$/
+  return reg.test(value)
+}
+
+/**
+ * @author Matchexpo
+ * @description 判断纬度 -90.0~+90.0(整数部分为0~90,必须输入1到5位小数)
+ * @param value
+ * @returns {boolean}
+ */
+export function isLatitude(value) {
+  const reg = /^[-|+]?([0-8]?\d{1}\.\d{1,5}|90\.0{1,5})$/
+  return reg.test(value)
+}
+
+/**
+ * @author Matchexpo
+ * @description rtsp校验,只要有rtsp://
+ * @param value
+ * @returns {boolean}
+ */
+export function isRTSP(value) {
+  const reg =
+    /^rtsp:\/\/([a-z]{0,10}:.{0,10}@)?(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])$/
+  const reg1 =
+    /^rtsp:\/\/([a-z]{0,10}:.{0,10}@)?(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5]):[0-9]{1,5}/
+  const reg2 =
+    /^rtsp:\/\/([a-z]{0,10}:.{0,10}@)?(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\//
+  return reg.test(value) || reg1.test(value) || reg2.test(value)
+}
+
+/**
+ * @author Matchexpo
+ * @description 判断是否为json
+ * @param value
+ * @returns {boolean}
+ */
+export function isJson(value) {
+  if (typeof value === 'string') {
+    try {
+      var obj = JSON.parse(value)
+      if (typeof obj === 'object' && obj) {
+        return true
+      } else {
+        return false
+      }
+    } catch (e) {
+      return false
+    }
+  }
+}
+
+/**
+ * @author Matchexpo
+ * @description 校验密码
+ * @param value
+ * @returns {boolean}
+ */
+export function passWordRules(value) {
+  /* const reg = new RegExp(
+    '^(?=\\S.*\\d.*)(?=.*[a-z].*)(?=.*[A-Z].*)(?=.*[~!@#$%^&*()_\\+].*).?\\S{8,}$'
+  )*/
+  const reg = new RegExp(
+    '^(?=\\S.*\\d.*)(?=.*[a-zA-Z].*).?\\S{8,}$'
+  )
+  return reg.test(value)
+}

+ 687 - 0
src/utils/worker.js

@@ -0,0 +1,687 @@
+/**
+ * @fileoverview 一个 SharedWorker,用于处理 Web 应用程序的后台任务,例如 AI 内容生成、翻译和 SEO 优化。
+ */
+
+'use strict'
+
+// ===================================================================================
+// 1. 状态与配置
+// ===================================================================================
+
+/**
+ * Worker 的全局状态。
+ */
+const state = {
+  token: null,
+  mainPort: null, // 主“守护”页面的端口
+  helperPort: null, // AI 助手 UI 的端口
+  aiUrl: '',
+  baseURL: '',
+  settings: {
+    maxRetry: 5, // 默认全局重试次数限制
+    retryOne: 3, // 默认单个任务重试次数限制
+    debug: false
+  },
+  seoConfig: {},
+  workList: [],
+  isWorkLoopActive: false,
+  globalRetryCount: 0
+}
+
+/**
+ * API 端点定义。
+ * 使用 getter 确保 state 中的 baseURL 始终是最新的。
+ */
+const api = {
+  _constructUrl(path) { return state.baseURL + path },
+  get getBlog() { return this._constructUrl('api/blog/info') },
+  get getBlogTags() { return this._constructUrl('api/blog/get-tag-list') },
+  get getBlogTypes() { return this._constructUrl('api/blog/get-type-list') },
+  get getBlogPlates() { return this._constructUrl('api/blog/get-plate-list') },
+  get getBlogList() { return this._constructUrl('/api/blog/get-list') },
+  get saveBlog() { return this._constructUrl('api/blog/save') },
+  get getProduct() { return this._constructUrl('api/product/info') },
+  get getProductTags() { return this._constructUrl('api/product/get-tag-list') },
+  get getProductTypes() { return this._constructUrl('api/product/get-type-list') },
+  get saveProduct() { return this._constructUrl('api/product/save') },
+  get getMeeting() { return this._constructUrl('api/meeting/info') },
+  get getMeetingTypes() { return this._constructUrl('api/meeting/get-type-list') },
+  get getMeetingTags() { return this._constructUrl('api/meeting/get-tag-list') },
+  get saveMeeting() { return this._constructUrl('api/meeting/save') },
+  get getStaticPage() { return this._constructUrl('api/static-page/get-info') },
+  get saveStaticPage() { return this._constructUrl('api/static-page/save') },
+  get transStatus() { return this._constructUrl('api/web/page/update-trans-status') }
+}
+
+// ===================================================================================
+// 2. WORKER 核心逻辑
+// ===================================================================================
+
+console.log('Worker: 脚本已加载,准备接收连接。')
+
+/**
+ * 处理到 SharedWorker 的新连接。
+ */
+onconnect = (event) => {
+  const port = event.ports[0]
+  console.log('Worker: 新连接已建立。')
+
+  port.onmessage = (e) => {
+    const { type, data, message } = e.data
+    console.log(`Worker: 接收到类型为 "${type}" 的消息`, e.data)
+
+    // 仅启动一次工作循环。
+    if (!state.isWorkLoopActive) {
+      state.isWorkLoopActive = true
+      startWorkLoop()
+    }
+
+    switch (type) {
+      case 'init':
+        handleInit(port, e.data)
+        break
+      case 'helper':
+        state.helperPort = port
+        console.log('Worker: AI 助手页面已连接。')
+        break
+      case 'connect':
+        console.log('Worker: 普通页面已连接。')
+        if (state.mainPort) {
+          state.mainPort.postMessage(createMessage('connect', message, {}))
+        }
+        break
+      case 'translate':
+      case 'seo':
+      case 'write':
+        handleNewTask(type, data, port, message)
+        break
+      default:
+        console.warn(`Worker: 接收到未知消息类型 "${type}"`)
+    }
+  }
+
+  port.postMessage(createMessage('message', '新页面已连接'))
+}
+
+/**
+ * 使用主页面提供的必要数据初始化 Worker。
+ * @param {MessagePort} port - 连接页面的端口。
+ * @param {object} eventData - 消息事件中的数据对象。
+ */
+function handleInit(port, eventData) {
+  state.mainPort = port
+  const { token, aiUrl, setting, seoConfig, baseURL } = eventData.data
+  state.token = token
+  state.aiUrl = aiUrl
+  state.settings = { ...state.settings, ...setting }
+  state.seoConfig = seoConfig
+  state.baseURL = baseURL
+  console.log('Worker: 主页面已连接,状态已初始化。', { settings: state.settings, baseURL: state.baseURL })
+  port.postMessage(createMessage('init', eventData.message, {}))
+}
+
+/**
+ * 处理新任务的提交。
+ * @param {string} type - 任务类型 ('translate', 'seo', 'write')。
+ * @param {object} data - 任务所需的数据。
+ * @param {MessagePort} port - 提交任务的端口。
+ * @param {string} message - 附带的消息。
+ */
+function handleNewTask(type, data, port, message) {
+  if (addTask(type, data, port)) {
+    const successMsg = createMessage('success', message)
+    // 通知所有相关端口任务已成功入队
+    broadcastMessage(createMessage(type, `新的 ${type} 任务已添加到队列。`))
+    port.postMessage(successMsg)
+  } else {
+    port.postMessage(createMessage('error', '创建任务失败,任务可能已存在。'))
+  }
+}
+
+/**
+ * 处理 workList 中任务的主循环。
+ */
+async function startWorkLoop() {
+  console.log('Worker: 工作循环已启动。')
+  while (true) {
+    const workItem = state.workList.find(w => w.state === 'waiting')
+
+    if (!workItem || !state.token) {
+      await new Promise(resolve => setTimeout(resolve, 1000))
+      continue
+    }
+
+    workItem.state = 'running'
+    console.log(`Worker: 开始执行任务 ${workItem.id},类型为 ${workItem.type}`)
+    broadcastMessage(createMessage('update', `开始任务: ${workItem.type} (ID: ${workItem.data.id || ''})`))
+
+    try {
+      let taskResult
+      switch (workItem.type) {
+        case 'translate':
+          taskResult = await doTranslateTask(workItem)
+          break
+        case 'seo':
+          taskResult = await doSeoTask(workItem)
+          break
+        case 'write':
+          taskResult = await doWriteTask(workItem)
+          break
+        default:
+          throw new Error(`未知的任务类型: ${workItem.type}`)
+      }
+
+      // 关键改动:先更新列表,再发送消息
+      workSuccess(workItem)
+
+      // 使用任务返回的结果来广播消息
+      broadcastMessage(createMessage('success', taskResult.message, {}, taskResult.response))
+    } catch (error) {
+      console.error(`Worker: 任务 ${workItem.id} 执行失败。`, error)
+      broadcastMessage(createMessage('error', `类型为 ${workItem.type} 的任务失败。详情请查看控制台。`, {}, error.message))
+      workFail(workItem)
+    }
+  }
+}
+
+// ===================================================================================
+// 3. 任务管理
+// ===================================================================================
+
+/**
+ * 如果不存在相似任务,则向工作队列中添加一个新任务。
+ * @param {string} type - 任务类型 ('translate', 'seo', 'write')。
+ * @param {object} data - 任务特定数据,必须包含 'id' 和 'type' 以进行唯一性检查。
+ * @param {MessagePort} port - 发起任务的端口。
+ * @returns {boolean} - 如果任务被添加则返回 true,否则返回 false。
+ */
+function addTask(type, data, port) {
+  const exists = state.workList.some(item =>
+    item.type === type &&
+    item.data.id === data.id &&
+    item.data.type === data.type // 内部类型,例如 'blog'
+  )
+
+  if (exists) {
+    console.warn(`类型为 "${type}" 且针对项目 ${data.id} 的任务已在队列中。`)
+    return false
+  }
+
+  const workItem = {
+    id: `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`,
+    type: type,
+    data: data,
+    port: port,
+    state: 'waiting',
+    retry: 0
+  }
+
+  state.workList.push(workItem)
+  console.log(`任务 ${workItem.id} 已添加。队列长度: ${state.workList.length}`)
+  return true
+}
+
+/**
+ * 从列表中移除已完成的任务,并重置全局重试计数器。
+ * @param {object} workItem - 成功完成的工作项。
+ */
+function workSuccess(workItem) {
+  state.workList = state.workList.filter(w => w.id !== workItem.id)
+  state.globalRetryCount = 0 // 任何任务成功后重置
+  console.log(`Worker: 任务 ${workItem.id} 已成功完成。`)
+}
+
+/**
+ * 处理失败的任务,并实现重试逻辑。
+ * @param {object} workItem - 失败的工作项。
+ */
+function workFail(workItem) {
+  // 从列表中移除,以便后续可能重新添加
+  state.workList = state.workList.filter(w => w.id !== workItem.id)
+  state.globalRetryCount++
+
+  if (state.globalRetryCount >= state.settings.maxRetry) {
+    broadcastMessage(createMessage('error', '已达到全局最大重试次数。所有任务已中止。'))
+    state.workList = [] // 清空所有任务
+    return
+  }
+
+  workItem.retry++
+  if (workItem.retry < state.settings.retryOne) {
+    workItem.state = 'waiting'
+    state.workList.push(workItem) // 添加回队列末尾
+    const msg = `任务失败,正在重新排队。尝试次数 ${workItem.retry}/${state.settings.retryOne}。`
+    console.warn(msg)
+    broadcastMessage(createMessage('warning', msg))
+  } else {
+    const msg = `任务已达到其重试上限,已被丢弃。`
+    console.error(msg, workItem.data)
+    broadcastMessage(createMessage('error', msg, {}, workItem.data))
+  }
+}
+
+// ===================================================================================
+// 4. 异步任务处理器
+// ===================================================================================
+
+/**
+ * 执行一个 'write' 写作任务。
+ * @param {object} work - 写作任务的工作项。
+ */
+async function doWriteTask(work) {
+  const { writeInfo, typeList, tagList, template } = work.data
+  const titleList = []
+
+  // 获取现有博客标题以避免重复
+  const blogListResponse = await apiClient(api.getBlogList, 'GET', { page: 1, page_size: 999 })
+  blogListResponse.data.data.forEach(item => titleList.push(item.title))
+
+  // 构建 AI 提示
+  let templateCreatWay = ''
+  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 requestJson = {
+    contents: [{
+      role: 'model',
+      parts: [{
+        text: `
+          无论用户使用何种语言提问,返回给用户的数据一律使用${state.settings.language}回答,除非我直接告诉你这条数据值为多少。
+          你需要按照以下规则生成指定条数的文章数据输出至blog_list数组,接下来我将告诉你单条文章数据内的每个属性该如何生成。
+          title:这是文章标题数据,内容${writeInfo.title.is_ai ? '根据文章具体内容生成' : "值为'" + writeInfo.title.text + "'"}。生成的标题不能与${JSON.stringify(titleList)}中的文章标题重复。
+          author:这是文章作者,值为${writeInfo.author}。
+          content:这是文章正文,${template.templatePrompt},使用html格式输出,你只可以使用'p','h1-h6','ul','ol','b','em','sub','sup','u','table','blockquote' 进行文章生成,${templateCreatWay}。
+          description:这是文章梗概,根据内容生成,50个字以内。
+          type_ids:这是文章类型,${writeInfo.types.is_ai ? '根据文章内容从' + JSON.stringify(typeList) + '挑选一个,将其id填入' : "值为'" + JSON.stringify(writeInfo.types.ids) + "'。这是所有可用类型数据,你可用根据id找到对应的数据,作为生成文章的参考:" + JSON.stringify(typeList) + '。'}
+          tag_ids:这是文章标签,${writeInfo.tags.is_ai ? '根据文章内容从' + JSON.stringify(tagList) + '挑选任意数量,将其id填入' : "值为'" + JSON.stringify(writeInfo.tags.ids) + "'。这是所有可用标签数据,你可用根据id找到对应的数据,作为生成文章的参考:" + JSON.stringify(tagList) + '。'}
+        `
+      }]
+    }, {
+      role: 'user',
+      parts: [{
+        text: `生成${writeInfo.number}条文章数据`
+      }]
+    }],
+    generationConfig: {
+      responseMimeType: 'application/json',
+      responseSchema: {
+        type: 'object',
+        properties: {
+          blog_list: {
+            type: 'array',
+            items: {
+              type: 'object',
+              properties: {
+                title: { type: 'string' },
+                author: { type: 'string' },
+                content: { type: 'string' },
+                description: { type: 'string' },
+                type_ids: { type: 'array', items: { type: 'number' }},
+                tag_ids: { type: 'array', items: { type: 'number' }}
+              },
+              required: ['title', 'author', 'content']
+            }
+          }
+        }
+      }
+    }
+  }
+
+  // 调用 AI 并处理结果
+  const aiResult = await callAI(requestJson)
+  const blogList = aiResult.blog_list
+
+  for (const blog of blogList) {
+    const saveData = {
+      ...blog,
+      image_url: writeInfo.cover.url,
+      image_alt: writeInfo.cover.url,
+      status: 1,
+      plate_id: 1
+    }
+    const saveResponse = await apiClient(api.saveBlog, 'POST', saveData)
+    const newBlogId = JSON.parse(saveResponse).data
+
+    // 为新创建的博客自动排队一个 SEO 任务
+    addTask('seo', { id: newBlogId, title: blog.title, type: 'blog' }, work.port)
+    broadcastMessage(createMessage('seo', `"${blog.title}" 已创建并加入 SEO 队列。`))
+  }
+
+  // 原来的代码:
+  // const successMsg = `${writeInfo.number} 篇文章已成功生成。`
+  // broadcastMessage(createMessage('write', successMsg, {}, blogList))
+  // work.port.postMessage(createMessage('success', '文章生成完成。'))
+
+  // 修改为:
+  const successMsg = `${writeInfo.number} 篇文章已成功生成。`
+  return {
+    type: 'write',
+    message: successMsg,
+    response: blogList,
+    port: work.port,
+    portMessage: createMessage('success', '文章生成完成。')
+  }
+}
+
+/**
+ * 执行一个 'seo' 优化任务。
+ * @param {object} work - SEO 任务的工作项。
+ */
+async function doSeoTask(work) {
+  const { type, id } = work.data
+  console.log('调用接口',`get${capitalize(type)}`)
+  console.log('调用接口',api[`get${capitalize(type)}`])
+  const pageData = (await apiClient(api[`get${capitalize(type)}`], 'GET', { id })).data
+  const title = pageData.page_name || pageData.title
+
+  // 并行获取相关数据(标签、类型等)
+  const lists = {}
+  const promises = []
+  if (type !== 'staticPage') {
+    promises.push(apiClient(api[`get${capitalize(type)}Tags`], 'GET', { page: 1, page_size: 1000 }).then(r => {
+      lists.tags = r.data.data
+    }))
+    promises.push(apiClient(api[`get${capitalize(type)}Types`], 'GET', { page: 1, page_size: 1000 }).then(r => { lists.types = r.data.data }))
+  }
+  if (type === 'blog') {
+    promises.push(apiClient(api.getBlogPlates, 'GET', { page: 1, page_size: 1000 }).then(r => { lists.plates = r.data.data }))
+  }
+  await Promise.all(promises)
+
+  const seoData = state.seoConfig[`${type}Seo`]
+  if (!seoData) throw new Error(`未找到类型为 ${type} 的 SEO 配置`)
+
+  // 构建 AI 提示
+  const requestJson = {
+    contents: [{
+      role: 'model',
+      parts: [{
+        text: `
+          无论用户使用何种语言提问,返回给用户的数据一律使用${state.settings.language}回答。
+          你需要处理用户发送的数据,将数据中的'descData','keywordData','titleData','urlData' 四项根据用户的要求生成字符串后返回给用户。这些数据将被使用在用户的网站元信息中用于SEO优化。
+          descData的内容参考提示${seoData.descData[0].value}进行生成,
+          keywordData的内容参考提示${seoData.keywordData[0].value}进行生成,
+          titleData的内容参考提示${seoData.titleData[0].value}进行生成,
+          urlData的内容参考提示${seoData.urlData[0].value}进行生成,注意此项需要作为地址参数使用,不能使用特殊字符,只可使用小写字母与'-'。
+          需要SEO优化的文章具体内容数据为${JSON.stringify(pageData)};
+          类型和标签数据为id数组,其具体内容可在此处取得${JSON.stringify(lists)}。
+        `
+      }]
+    }, {
+      role: 'user',
+      parts: [{ text: '生成数据' }]
+    }],
+    generationConfig: {
+      responseMimeType: 'application/json',
+      responseSchema: {
+        type: 'object',
+        properties: {
+          descData: { type: 'string' },
+          urlData: { type: 'string' },
+          keywordData: { type: 'string' },
+          titleData: { type: 'string' }
+        },
+        required: ['descData', 'keywordData', 'titleData', 'urlData']
+      }
+    }
+  }
+
+  // 调用 AI 并保存结果
+  const aiResult = await callAI(requestJson)
+  pageData.seo_data.seo_describe = aiResult.descData
+  pageData.seo_data.seo_keyword = aiResult.keywordData
+  pageData.seo_data.seo_title = aiResult.titleData
+  pageData.seo_data.urla = aiResult.urlData
+
+  const saveResponse = await apiClient(api[`save${capitalize(type)}`], 'POST', pageData)
+
+  // 原来的代码:
+  // const successMsg = `"${title}" 的 SEO 优化已完成。`
+  // broadcastMessage(createMessage('seo', successMsg, {}, saveResponse))
+  // work.port.postMessage(createMessage('success', successMsg, {}, saveResponse))
+
+  // 修改为:
+  const successMsg = `"${title}" 的 SEO 优化已完成。`
+  return {
+    type: 'seo',
+    message: successMsg,
+    response: saveResponse,
+    port: work.port,
+    portMessage: createMessage('success', successMsg, {}, saveResponse)
+  }
+}
+
+/**
+ * 执行一个 'translate' 翻译任务。
+ * @param {object} work - 翻译任务的工作项。
+ */
+async function doTranslateTask(work) {
+  const { type, id } = work.data
+  const pageData = (await apiClient(api[`get${capitalize(type)}`], 'GET', { id })).data
+  const title = pageData.page_name || pageData.title
+
+  // 1. 更新状态为“翻译中”
+  await apiClient(api.transStatus, 'POST', { id: pageData.seo_id, trans_status: 1 })
+
+  // 2. 构建 AI 提示并调用 AI
+  const requestJson = {
+    contents: [{
+      role: 'model',
+      parts: [{
+        text: `
+          无论用户使用何种语言提问,返回给用户的数据一律使用${state.settings.language}回答。
+          将用户提供的JSON数据翻译成${state.settings.language}并以纯json字符串的形式输出,不要输出任何markdown格式,输出纯字符串。
+        `
+      }]
+    }, {
+      role: 'user',
+      parts: [{
+        text: `翻译数据${JSON.stringify(pageData)}`
+      }]
+    }],
+    generationConfig: {
+      responseMimeType: 'text/plain' // AI 将返回一个字符串化的 JSON
+    }
+  }
+
+  const translatedText = await callAI(requestJson, true) // rawText = true
+  const translatedData = JSON.parse(translatedText)
+  if (translatedData.content === '') {
+    translatedData.content = translatedData.title
+  }
+
+  // 3. 保存翻译后的数据
+  const saveResponse = await apiClient(api[`save${capitalize(type)}`], 'POST', translatedData)
+
+  // 4. 更新状态为“已翻译”
+  await apiClient(api.transStatus, 'POST', { id: pageData.seo_id, trans_status: state.settings.language })
+
+  // 原来的代码:
+  // const successMsg = `"${title}" 翻译成功。`
+  // broadcastMessage(createMessage('translate', successMsg, {}, saveResponse))
+  // work.port.postMessage(createMessage('success', successMsg, { refresh: true, type: work.data.type }, work.data))
+
+  // 修改为:
+  const successMsg = `"${title}" 翻译成功。`
+  return {
+    type: 'translate',
+    message: successMsg,
+    response: saveResponse,
+    port: work.port,
+    portMessage: createMessage('success', successMsg, { refresh: true, type: work.data.type }, work.data)
+  }
+}
+
+// ===================================================================================
+// 5. API 及 AI 客户端
+// ===================================================================================
+
+/**
+ * 一个健壮的、用于应用后端的异步 API 客户端。
+ * @param {string} url - 请求的完整 URL。
+ * @param {'GET'|'POST'} method - HTTP 方法。
+ * @param {object} [data] - 要发送的数据。对于 GET 请求会转换为查询参数,对于 POST 请求会作为 JSON 主体。
+ * @returns {Promise<any>} - 一个解析为响应数据的 Promise。
+ * @throws {Error} 如果请求失败或返回非 200 状态码。
+ */
+async function apiClient(url, method, data) {
+  if (!state.token) throw new Error('API 客户端调用失败:认证令牌未设置。')
+
+  const options = {
+    method: method.toUpperCase(),
+    headers: {
+      token: state.token,
+      'Content-Type': 'application/json',
+      'X-Requested-With': 'XMLHttpRequest'
+    }
+  }
+
+  let fullUrl = url
+  if (method.toUpperCase() === 'GET' && data) {
+    fullUrl += '?' + new URLSearchParams(data).toString()
+  } else if (method.toUpperCase() === 'POST' && data) {
+    options.body = JSON.stringify(data)
+  }
+
+  const response = await fetch(fullUrl, options)
+
+  if (!response.ok) {
+    const errorBody = await response.text()
+    throw new Error(`API 错误: ${response.status} ${response.statusText} on ${url}. Body: ${errorBody}`)
+  }
+  // 原始 API 在某些情况下似乎返回 JSON 字符串,因此我们返回原始响应的文本
+  // 以便进行灵活的解析 (.json() 或 .text())
+  return response.json()
+}
+
+/**
+ * 使用给定的负载调用 AI 服务。
+ * @param {object} payload - 发送给 AI 请求的 JSON 负载。
+ * @param {boolean} [expectRawText=false] - 如果为 true,则返回原始文本响应。如果为 false,则解析内部的 JSON。
+ * @returns {Promise<any>} - 一个解析为 AI 响应数据的 Promise。
+ * @throws {Error} 如果 AI 调用失败或响应格式错误。
+ */
+async function callAI(payload, expectRawText = false) {
+  const response = await fetch(state.aiUrl, {
+    method: 'POST',
+    headers: { 'Content-Type': 'application/json' },
+    body: JSON.stringify(payload)
+  })
+
+  if (!response.ok) {
+    const errorBody = await response.text()
+    throw new Error(`AI API 错误: ${response.status} ${response.statusText}. Body: ${errorBody}`)
+  }
+
+  const responseData = await response.json()
+  const textPart = responseData?.candidates?.[0]?.content?.parts?.[0]?.text
+  if (typeof textPart !== 'string') {
+    throw new Error('AI 响应格式无效:缺少文本部分或非字符串类型。')
+  }
+
+  if (expectRawText) {
+    return textPart
+  }
+
+  try {
+    // AI 有时会将其 JSON 输出包裹在 markdown 的 ```json ... ``` 中,因此我们需要清理它。
+    const cleanedText = textPart.replace(/^```json\s*|```\s*$/g, '')
+    return JSON.parse(cleanedText)
+  } catch (e) {
+    console.error('从 AI 响应中解析内部 JSON 失败:', textPart)
+    throw new Error('AI 返回了格式错误的 JSON 字符串。')
+  }
+}
+
+// ===================================================================================
+// 6. 工具函数
+// ===================================================================================
+
+/**
+ * 创建一个标准的消息对象,用于通过端口发送。
+ * @param {string} type - 消息类型。
+ * @param {string} message - 主要的消息文本。
+ * @param {object} data - 任何要发送的附加数据。
+ * @param {any} [debugCode] - 调试信息。
+ * @returns {object} 结构化的消息对象。
+ */
+function createMessage(type, message, data = {}, debugCode = {}) {
+  return {
+    type: state.settings.debug ? 'code' : type,
+    message: message,
+    time: Date.now(),
+    code: state.settings.debug ? debugCode : '',
+    data: {
+      ...data,
+      workLength: state.workList.length,
+      isInit: !!state.mainPort
+    }
+  }
+}
+
+/**
+ * 如果主端口和助手端口存在,则向它们广播消息。
+ * @param {object} messageObject - 由 createMessage 创建的消息对象。
+ */
+function broadcastMessage(messageObject) {
+  if (state.mainPort) {
+    state.mainPort.postMessage(messageObject)
+  }
+  if (state.helperPort) {
+    // 向助手页面发送一个简化的成功/失败消息
+    const helperMsgType = (messageObject.type === 'error' || messageObject.type === 'warning') ? messageObject.type : 'success'
+    state.helperPort.postMessage(createMessage(helperMsgType, messageObject.message, {}, messageObject.code))
+  }
+}
+
+/**
+ * 将字符串的第一个字母大写。
+ * 例如:'blog' -> 'Blog'
+ * @param {string} s - 要大写的字符串。
+ * @returns {string}
+ */
+function capitalize(s) {
+  if (typeof s !== 'string' || !s) return s
+  return s.charAt(0).toUpperCase() + s.slice(1)
+}
+
+/**
+ * 从模板生成封面图片 URL。
+ * 注意:此函数存在于原始文件中但未使用。
+ * 它依赖于一个此处未定义的 'md5' 函数。
+ * @param {string} title - 要嵌入图片中的标题。
+ * @param {string} image - 背景图片 URL。
+ * @returns {string} - 最终的模板化图片 URL。
+ */
+function getTemplateImage(title, image) {
+  // 此函数需要一个全局可用的 'md5' 函数。
+  if (typeof md5 !== 'function') {
+    console.error('getTemplateImage 需要一个 md5() 函数,但该函数未定义。')
+    return ''
+  }
+  const urlPrefix = 'https://image.edgeone.app'
+  const format = 'png'
+  const userId = '3e5b976c108b4febb15687047013beff'
+  const templateId = 'ep-uchZw5e0Lxe9'
+  const apiKey = 'rC5asgUt51is'
+  const params = { image, title }
+
+  const sortedKeys = Object.keys(params).sort()
+  const searchParams = sortedKeys.map(key => `${key}=${params[key]}`).join('&')
+  const signData = JSON.stringify({ apiKey, searchParams })
+  const sign = md5(signData)
+  const encodedSearchParams = sortedKeys.map(key => `${key}=${encodeURIComponent(params[key])}`).join('&')
+
+  return `${urlPrefix}/${sign}/${userId}/${templateId}.${format}?${encodedSearchParams}`
+}