mirror of
https://github.com/chaos-zhu/easynode.git
synced 2026-10-07 16:18:31 +08:00
feat: 添加终端高亮系统与配置管理优化
- 新增终端高亮系统,支持8类规则自动识别关键信息 - 使用正则表达式合并算法,终端所有的文本块输出只需要1-2次正则匹配即可,不影响实时的输出速度 - 高亮设置支持实时预览和规则导入导出 - 重构终端配置管理,提供模块化的终端设置界面,分为三部分:基础设置、高亮设置和其他设置,其中基础设置包含原来的的本地设置/终端设置内容,其他设置包含原来的本地设置/快捷操作内容,高亮设置为新增 - 新增TerminalConfigDB类,所有的终端设置使用数据库持久化管理 - 修复终端主题定制中的半透明灰色图层遮盖问题,保证终端背景颜色和效果颜色一致性,支持各种终端内容的深度定制,包括字体、字体大小、字体颜色、光标颜色、选中颜色等,新增了更多预设背景色 - 脚本库中新增执行模式选项,用户可以选择使用直接运行或者base64编码处理后运行,以支持复杂的多行脚本运行问题和灵活管理 - 增强sftp组件,支持sftp窗口宽度拖拽改变,个性化显示文件信息列(大小、修改时间、权限、拥有者)
This commit is contained in:
Generated
+11088
File diff suppressed because it is too large
Load Diff
@@ -23,6 +23,7 @@ module.exports = {
|
||||
favoriteSftpDBPath: path.join(process.cwd(),'app/db/favorite-sftp.db'),
|
||||
proxyDBPath: path.join(process.cwd(),'app/db/proxy.db'),
|
||||
fileTransferDBPath: path.join(process.cwd(),'app/db/file-transfer.db'),
|
||||
terminalConfigDBPath: path.join(process.cwd(),'app/db/terminal-config.db'),
|
||||
apiPrefix: '/api/v1',
|
||||
logConfig: {
|
||||
outDir: path.join(process.cwd(),'./app/db/logs'),
|
||||
|
||||
@@ -24,19 +24,21 @@ async function getLocalScriptList({ res }) {
|
||||
}
|
||||
|
||||
const addScript = async ({ res, request }) => {
|
||||
let { body: { name, description, command, index, group } } = request
|
||||
// useBase64 默认为false
|
||||
let { body: { name, description, command, index, group, useBase64 = false } } = request
|
||||
if (!name || !command) return res.fail({ data: false, msg: '参数错误' })
|
||||
index = Number(index) || 0
|
||||
let record = { name, description, command, index, group }
|
||||
let record = { name, description, command, index, group, useBase64 }
|
||||
await scriptsDB.insertAsync(record)
|
||||
res.success({ data: '添加成功' })
|
||||
}
|
||||
|
||||
const updateScriptList = async ({ res, request }) => {
|
||||
let { params: { id } } = request
|
||||
let { body: { name, description, command, index, group } } = request
|
||||
// useBase64 默认为false
|
||||
let { body: { name, description, command, index, group, useBase64 = false } } = request
|
||||
if (!name || !command) return res.fail({ data: false, msg: '参数错误' })
|
||||
await scriptsDB.updateAsync({ _id: id }, { name, description, command, index, group })
|
||||
await scriptsDB.updateAsync({ _id: id }, { name, description, command, index, group, useBase64 })
|
||||
res.success({ data: '修改成功' })
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
const { TerminalConfigDB } = require('../utils/db-class')
|
||||
|
||||
// 终端默认配置
|
||||
const DEFAULT_TERMINAL_CONFIG = {
|
||||
themeName: 'Afterglow',
|
||||
fontFamily: 'monospace', // 使用多平台通用的等宽字体
|
||||
fontSize: 16,
|
||||
fontColor: '', // 使用主题字体颜色
|
||||
cursorColor: '', // 使用主题光标颜色
|
||||
selectionColor: '#264f78', // 深蓝色选中颜色,适合深色主题
|
||||
background: '', // 使用主题背景
|
||||
keywordHighlight: true,
|
||||
highlightDebugMode: false,
|
||||
customHighlightRules: null, // null表示使用默认规则
|
||||
autoExecuteScript: false, // 脚本执行模式
|
||||
autoReconnect: false // 自动重连
|
||||
}
|
||||
|
||||
const terminalConfigDB = new TerminalConfigDB().getInstance()
|
||||
|
||||
// 获取用户的终端配置
|
||||
async function getTerminalConfig(ctx) {
|
||||
const { uid } = ctx.request.query
|
||||
|
||||
if (!uid || typeof uid !== 'string' || uid.trim() === '') {
|
||||
ctx.body = { success: false, message: '无效的uid参数' }
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// 查找用户配置
|
||||
let userConfig = await terminalConfigDB.findOneAsync({ uid, isDefault: { $ne: true } })
|
||||
|
||||
if (!userConfig) {
|
||||
// 如果没有用户配置,直接返回默认配置
|
||||
userConfig = {
|
||||
...DEFAULT_TERMINAL_CONFIG,
|
||||
uid
|
||||
}
|
||||
} else {
|
||||
// 合并默认配置和用户配置
|
||||
userConfig = {
|
||||
...DEFAULT_TERMINAL_CONFIG,
|
||||
...userConfig // 用户配置覆盖默认配置
|
||||
}
|
||||
}
|
||||
|
||||
ctx.body = { success: true, data: userConfig }
|
||||
} catch (error) {
|
||||
console.error('Error in getTerminalConfig:', error)
|
||||
ctx.body = { success: false, message: '服务器内部错误,请稍后重试' }
|
||||
}
|
||||
}
|
||||
|
||||
// 保存用户的终端配置
|
||||
async function saveTerminalConfig(ctx) {
|
||||
const { uid, isDefault, name, _id, createTime, ...configData } = ctx.request.body
|
||||
|
||||
if (!uid || typeof uid !== 'string' || uid.trim() === '') {
|
||||
ctx.body = { success: false, message: '无效的uid参数' }
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const existingConfig = await terminalConfigDB.findOneAsync({ uid, isDefault: { $ne: true } })
|
||||
|
||||
const updateData = {
|
||||
...configData,
|
||||
uid,
|
||||
updateTime: new Date().toISOString()
|
||||
}
|
||||
|
||||
if (existingConfig) {
|
||||
// 更新现有配置
|
||||
await terminalConfigDB.updateAsync(
|
||||
{ _id: existingConfig._id },
|
||||
{ $set: updateData }
|
||||
)
|
||||
} else {
|
||||
// 创建新配置
|
||||
updateData.createTime = new Date().toISOString()
|
||||
await terminalConfigDB.insertAsync(updateData)
|
||||
}
|
||||
|
||||
ctx.body = { success: true, message: '配置保存成功' }
|
||||
} catch (error) {
|
||||
console.error('Error in saveTerminalConfig:', error)
|
||||
ctx.body = { success: false, message: '服务器内部错误,请稍后重试' }
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getTerminalConfig,
|
||||
saveTerminalConfig
|
||||
}
|
||||
@@ -9,6 +9,7 @@ const { getOnekeyRecord, removeOnekeyRecord } = require('../controller/onekey')
|
||||
const { getLog, saveIpWhiteList, removeSomeLoginRecords } = require('../controller/log')
|
||||
const { getAIConfig, saveAIConfig, getAIModels, getChatHistory, saveChatHistory, removeChatHistory } = require('../controller/chat')
|
||||
const { getProxyList, addProxy, updateProxy, removeProxy } = require('../controller/proxy')
|
||||
const { getTerminalConfig, saveTerminalConfig } = require('../controller/terminal-config')
|
||||
|
||||
const ssh = [
|
||||
{
|
||||
@@ -330,6 +331,19 @@ const proxy = [
|
||||
}
|
||||
]
|
||||
|
||||
const terminalConfig = [
|
||||
{
|
||||
method: 'get',
|
||||
path: '/terminal-config',
|
||||
controller: getTerminalConfig
|
||||
},
|
||||
{
|
||||
method: 'post',
|
||||
path: '/terminal-config',
|
||||
controller: saveTerminalConfig
|
||||
}
|
||||
]
|
||||
|
||||
module.exports = [].concat(
|
||||
ssh,
|
||||
host,
|
||||
@@ -341,5 +355,6 @@ module.exports = [].concat(
|
||||
onekey,
|
||||
log,
|
||||
aiConfig,
|
||||
proxy
|
||||
proxy,
|
||||
terminalConfig
|
||||
)
|
||||
|
||||
@@ -11,6 +11,52 @@ const hostListDB = new HostListDB().getInstance()
|
||||
const favoriteSftpDB = new FavoriteSftpDB().getInstance()
|
||||
const { Client: SSHClient } = require('ssh2')
|
||||
|
||||
/**
|
||||
* https://github.com/theophilusx/ssh2-sftp-client#readme
|
||||
* The objects in the array returned by list() have the following properties:
|
||||
* {
|
||||
* type: '-', // file type(-, d, l)
|
||||
* name: 'example.txt', // file name
|
||||
* size: 43, // file size
|
||||
* modifyTime: 1675645360000, // file timestamp of modified time
|
||||
* accessTime: 1675645360000, // file timestamp of access time
|
||||
* rights: {
|
||||
* user: 'rw',
|
||||
* group: 'r',
|
||||
* other: 'r',
|
||||
* },
|
||||
* owner: 1000, // user ID
|
||||
* group: 1000, // group ID
|
||||
* longname: '-rw-r--r-- 1 fred fred 43 Feb 6 12:02 exaple.txt', // like ls -l line
|
||||
* }
|
||||
*/
|
||||
|
||||
|
||||
// 格式化文件列表数据,添加权限字符串和用户名/组名
|
||||
function formatFileList(fileList) {
|
||||
return fileList.map(file => {
|
||||
const formatted = { ...file }
|
||||
|
||||
// 从 rights 对象生成权限字符串
|
||||
if (file.rights) {
|
||||
const { user, group, other } = file.rights
|
||||
formatted.permissions = `${user}${group}${other}`
|
||||
}
|
||||
|
||||
// 从 longname 中提取ownerName和groupName
|
||||
if (file.longname) {
|
||||
const parts = file.longname.trim().split(/\s+/)
|
||||
if (parts.length >= 4) {
|
||||
// parts[2] 是所有者用户名, parts[3] 是组名
|
||||
formatted.ownerName = parts[2]
|
||||
formatted.groupName = parts[3]
|
||||
}
|
||||
}
|
||||
|
||||
return formatted
|
||||
})
|
||||
}
|
||||
|
||||
const listenAction = (sftpClient, socket) => {
|
||||
// 下载任务管理
|
||||
const downloadTasks = new Map() // taskId -> { abortController, startTime, totalSize, downloadedSize }
|
||||
@@ -23,7 +69,8 @@ const listenAction = (sftpClient, socket) => {
|
||||
try {
|
||||
const dirLs = await sftpClient.list(path)
|
||||
console.log('dirLs:', dirLs.length)
|
||||
socket.emit('dir_ls', dirLs, path)
|
||||
const formattedDirLs = formatFileList(dirLs)
|
||||
socket.emit('dir_ls', formattedDirLs, path)
|
||||
} catch (err) {
|
||||
socket.emit('not_exists_dir', err.message)
|
||||
}
|
||||
@@ -39,7 +86,8 @@ const listenAction = (sftpClient, socket) => {
|
||||
socket.emit('rename_success', { oldName, newName })
|
||||
// 返回最新目录列表
|
||||
const dirLs = await sftpClient.list(dirPath)
|
||||
socket.emit('dir_ls', dirLs, dirPath)
|
||||
const formattedDirLs = formatFileList(dirLs)
|
||||
socket.emit('dir_ls', formattedDirLs, dirPath)
|
||||
} catch (err) {
|
||||
socket.emit('rename_fail', err.message)
|
||||
}
|
||||
@@ -56,7 +104,8 @@ const listenAction = (sftpClient, socket) => {
|
||||
}
|
||||
socket.emit('delete_success')
|
||||
const dirLs = await sftpClient.list(dirPath)
|
||||
socket.emit('dir_ls', dirLs, dirPath)
|
||||
const formattedDirLs = formatFileList(dirLs)
|
||||
socket.emit('dir_ls', formattedDirLs, dirPath)
|
||||
} catch (err) {
|
||||
socket.emit('delete_fail', err.message)
|
||||
}
|
||||
@@ -74,7 +123,8 @@ const listenAction = (sftpClient, socket) => {
|
||||
}
|
||||
socket.emit('delete_success')
|
||||
const dirLs = await sftpClient.list(dirPath)
|
||||
socket.emit('dir_ls', dirLs, dirPath)
|
||||
const formattedDirLs = formatFileList(dirLs)
|
||||
socket.emit('dir_ls', formattedDirLs, dirPath)
|
||||
} catch (err) {
|
||||
socket.emit('delete_fail', err.message)
|
||||
}
|
||||
@@ -89,7 +139,8 @@ const listenAction = (sftpClient, socket) => {
|
||||
await sftpClient.rename(src, dest)
|
||||
socket.emit('move_success')
|
||||
const dirLs = await sftpClient.list(dirPath)
|
||||
socket.emit('dir_ls', dirLs, dirPath)
|
||||
const formattedDirLs = formatFileList(dirLs)
|
||||
socket.emit('dir_ls', formattedDirLs, dirPath)
|
||||
} catch (err) {
|
||||
socket.emit('move_fail', err.message)
|
||||
}
|
||||
@@ -105,7 +156,8 @@ const listenAction = (sftpClient, socket) => {
|
||||
}
|
||||
socket.emit('move_success')
|
||||
const dirLs = await sftpClient.list(dirPath)
|
||||
socket.emit('dir_ls', dirLs, dirPath)
|
||||
const formattedDirLs = formatFileList(dirLs)
|
||||
socket.emit('dir_ls', formattedDirLs, dirPath)
|
||||
} catch (err) {
|
||||
socket.emit('move_fail', err.message)
|
||||
}
|
||||
@@ -250,7 +302,8 @@ const listenAction = (sftpClient, socket) => {
|
||||
|
||||
socket.emit('copy_success')
|
||||
const dirLs = await sftpClient.list(dirPath)
|
||||
socket.emit('dir_ls', dirLs, dirPath)
|
||||
const formattedDirLs = formatFileList(dirLs)
|
||||
socket.emit('dir_ls', formattedDirLs, dirPath)
|
||||
} catch (err) {
|
||||
consola.error('copy error:', err.message)
|
||||
socket.emit('copy_fail', err.message)
|
||||
@@ -296,7 +349,8 @@ const listenAction = (sftpClient, socket) => {
|
||||
|
||||
// 返回最新目录列表
|
||||
const dirLs = await sftpClient.list(dirPath)
|
||||
socket.emit('dir_ls', dirLs, dirPath)
|
||||
const formattedDirLs = formatFileList(dirLs)
|
||||
socket.emit('dir_ls', formattedDirLs, dirPath)
|
||||
} catch (err) {
|
||||
consola.error('create error:', err.message)
|
||||
socket.emit('create_fail', err.message)
|
||||
@@ -344,7 +398,8 @@ const listenAction = (sftpClient, socket) => {
|
||||
|
||||
// 返回最新目录列表
|
||||
const dirLs = await sftpClient.list(dirPath)
|
||||
socket.emit('dir_ls', dirLs, dirPath)
|
||||
const formattedDirLs = formatFileList(dirLs)
|
||||
socket.emit('dir_ls', formattedDirLs, dirPath)
|
||||
} catch (err) {
|
||||
consola.error('compress error:', err.message)
|
||||
socket.emit('compress_fail', err.message)
|
||||
@@ -426,7 +481,8 @@ const listenAction = (sftpClient, socket) => {
|
||||
|
||||
// 返回最新目录列表
|
||||
const dirLs = await sftpClient.list(dirPath)
|
||||
socket.emit('dir_ls', dirLs, dirPath)
|
||||
const formattedDirLs = formatFileList(dirLs)
|
||||
socket.emit('dir_ls', formattedDirLs, dirPath)
|
||||
} catch (err) {
|
||||
consola.error('decompress error:', err.message)
|
||||
socket.emit('decompress_fail', err.message)
|
||||
@@ -1462,9 +1518,12 @@ module.exports = (httpServer) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化文件列表
|
||||
const formattedRootList = formatFileList(rootList)
|
||||
|
||||
// 普通文件-、目录文件d、链接文件l
|
||||
socket.emit('connect_success', {
|
||||
rootList,
|
||||
rootList: formattedRootList,
|
||||
isRootUser,
|
||||
currentPath: currentWorkingDir
|
||||
})
|
||||
|
||||
@@ -15,7 +15,8 @@ const {
|
||||
chatHistoryDBPath,
|
||||
favoriteSftpDBPath,
|
||||
proxyDBPath,
|
||||
fileTransferDBPath
|
||||
fileTransferDBPath,
|
||||
terminalConfigDBPath
|
||||
} = require('../config')
|
||||
|
||||
module.exports.KeyDB = class KeyDB {
|
||||
@@ -201,4 +202,26 @@ module.exports.FileTransferDB = class FileTransferDB {
|
||||
getInstance() {
|
||||
return FileTransferDB.instance
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.HighlightConfigDB = class HighlightConfigDB {
|
||||
constructor() {
|
||||
if (!HighlightConfigDB.instance) {
|
||||
HighlightConfigDB.instance = new Datastore({ filename: terminalConfigDBPath, autoload: true })
|
||||
}
|
||||
}
|
||||
getInstance() {
|
||||
return HighlightConfigDB.instance
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.TerminalConfigDB = class TerminalConfigDB {
|
||||
constructor() {
|
||||
if (!TerminalConfigDB.instance) {
|
||||
TerminalConfigDB.instance = new Datastore({ filename: terminalConfigDBPath, autoload: true })
|
||||
}
|
||||
}
|
||||
getInstance() {
|
||||
return TerminalConfigDB.instance
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,7 @@
|
||||
"crypto-js": "^4.2.0",
|
||||
"dotenv": "^16.4.5",
|
||||
"fs-extra": "^11.2.0",
|
||||
"get-intrinsic": "^1.3.0",
|
||||
"global": "^4.4.0",
|
||||
"iconv-lite": "^0.6.3",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
|
||||
@@ -180,5 +180,12 @@ export default {
|
||||
},
|
||||
removeProxy(id) {
|
||||
return axios({ url: `/proxy/${ id }`, method: 'delete' })
|
||||
},
|
||||
// 终端配置相关API
|
||||
getTerminalConfig(params = {}) {
|
||||
return axios({ url: '/terminal-config', method: 'get', params })
|
||||
},
|
||||
saveTerminalConfig(data) {
|
||||
return axios({ url: '/terminal-config', method: 'post', data })
|
||||
}
|
||||
}
|
||||
|
||||
+54
-15
@@ -30,17 +30,27 @@ const useStore = defineStore('global', {
|
||||
'linear-gradient(-20deg, #2b5876 0%, #4e4376 100%)',
|
||||
'linear-gradient(to top, #1e3c72 0%, #1e3c72 1%, #2a5298 100%)',
|
||||
'linear-gradient(to right, #243949 0%, #517fa4 100%)',
|
||||
|
||||
// 深色背景
|
||||
'linear-gradient(135deg, #0f0c29 0%, #302b63 50%, #24243e 100%)', // 深蓝紫
|
||||
'linear-gradient(to right, #1a1a2e 0%, #16213e 100%)', // 深夜蓝
|
||||
'linear-gradient(to right, #0f2027 0%, #203a43 50%, #2c5364 100%)', // 深青蓝
|
||||
'linear-gradient(135deg, #141e30 0%, #243b55 100%)', // 深靛蓝
|
||||
'linear-gradient(to bottom, #000000 0%, #2c2c2c 100%)', // 纯黑到灰
|
||||
'linear-gradient(135deg, #2c1810 0%, #3d2817 100%)', // 深褐
|
||||
'linear-gradient(to bottom, #1e3a2e 0%, #0f2922 100%)', // 深绿
|
||||
'linear-gradient(to right, #2b1b3d 0%, #3a2449 100%)', // 深紫
|
||||
|
||||
// 浅色背景
|
||||
'linear-gradient(120deg, #ffffff 0%, #f5f5f5 100%)', // 纯白
|
||||
'linear-gradient(120deg, #faf8f3 0%, #f0ebe1 100%)', // 柔和浅米白
|
||||
'linear-gradient(120deg, #d4e4f7 0%, #b5d3e7 100%)', // 柔和浅蓝
|
||||
'linear-gradient(to top, #e8dff5 0%, #d5c6e0 100%)', // 柔和浅紫
|
||||
'linear-gradient(135deg, #e8d5c4 0%, #d4c4b0 100%)', // 柔和浅褐
|
||||
'linear-gradient(120deg, #d5e5d5 0%, #c8dcc8 100%)', // 柔和浅绿
|
||||
],
|
||||
terminalConfig: {
|
||||
...{
|
||||
fontSize: 16,
|
||||
themeName: 'Afterglow',
|
||||
background: 'linear-gradient(-225deg, #CBBACC 0%, #2580B3 100%)',
|
||||
autoReconnect: true,
|
||||
autoExecuteScript: false
|
||||
},
|
||||
...(localStorage.getItem('terminalConfig') ? JSON.parse(localStorage.getItem('terminalConfig')) : {})
|
||||
},
|
||||
// 终端配置占位
|
||||
terminalConfig: {},
|
||||
menuSetting: {
|
||||
...{
|
||||
scriptLibrary: true,
|
||||
@@ -84,6 +94,7 @@ const useStore = defineStore('global', {
|
||||
await this.getScriptGroupList()
|
||||
await this.getPlusInfo()
|
||||
await this.getProxyList()
|
||||
await this.getTerminalConfig() // 添加终端配置获取
|
||||
this.getAIConfig()
|
||||
this.getChatHistory()
|
||||
},
|
||||
@@ -135,7 +146,7 @@ const useStore = defineStore('global', {
|
||||
const isPlusActive = new Date(plusInfo.expiryDate) > new Date()
|
||||
this.$patch({ isPlusActive })
|
||||
if (!isPlusActive) {
|
||||
this.setTerminalSetting({ autoReconnect: false })
|
||||
localStorage.setItem('autoReconnect', 'false')
|
||||
return
|
||||
}
|
||||
plusInfo.expiryDate = dayjs(plusInfo.expiryDate).format('YYYY-MM-DD')
|
||||
@@ -146,10 +157,38 @@ const useStore = defineStore('global', {
|
||||
}
|
||||
this.$patch({ plusInfo })
|
||||
},
|
||||
setTerminalSetting(setTarget = {}) {
|
||||
let newConfig = { ...this.terminalConfig, ...setTarget }
|
||||
localStorage.setItem('terminalConfig', JSON.stringify(newConfig))
|
||||
this.$patch({ terminalConfig: newConfig })
|
||||
async getTerminalConfig() {
|
||||
try {
|
||||
const { data: terminalConfig } = await $api.getTerminalConfig({ uid: this.uid })
|
||||
this.$patch({ terminalConfig })
|
||||
} catch (error) {
|
||||
console.error('获取终端配置失败:', error)
|
||||
// Fallback
|
||||
// 默认配置信息位于: server/app/controller/terminal-config.js
|
||||
this.$patch({ terminalConfig: {
|
||||
themeName: 'Afterglow',
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 16,
|
||||
fontColor: '',
|
||||
cursorColor: '',
|
||||
selectionColor: '#264f78',
|
||||
background: '',
|
||||
keywordHighlight: true,
|
||||
highlightDebugMode: false,
|
||||
customHighlightRules: null,
|
||||
autoExecuteScript: false,
|
||||
autoReconnect: false
|
||||
} })
|
||||
}
|
||||
},
|
||||
async setTerminalSetting(setTarget = {}) {
|
||||
const newConfig = { ...this.terminalConfig, ...setTarget }
|
||||
try {
|
||||
await $api.saveTerminalConfig({ ...newConfig, uid: this.uid })
|
||||
this.$patch({ terminalConfig: newConfig })
|
||||
} catch (error) {
|
||||
console.error('保存终端配置失败:', error)
|
||||
}
|
||||
},
|
||||
setMenuSetting(setTarget = {}) {
|
||||
let newConfig = { ...this.menuSetting, ...setTarget }
|
||||
|
||||
@@ -0,0 +1,579 @@
|
||||
/**
|
||||
* 终端高亮器和默认的规则定义
|
||||
*/
|
||||
|
||||
// ANSI终端文字样式
|
||||
export const TEXT_STYLES = {
|
||||
BOLD: '\x1b[1m', // 粗体/高亮
|
||||
ITALIC: '\x1b[3m', // 斜体
|
||||
UNDERLINE: '\x1b[4m', // 下划线
|
||||
RESET: '\x1b[0m' // 重置样式
|
||||
}
|
||||
|
||||
// 默认颜色映射
|
||||
export const DEFAULT_COLOR_MAPPING = {
|
||||
rule1: '#ff4d4f', // 错误
|
||||
rule2: '#fadb14', // 警告
|
||||
rule3: '#52c41a', // 成功
|
||||
rule4: '#13c2c2', // 信息
|
||||
rule5: '#eb2f96', // 网络
|
||||
rule6: '#1890ff', // 链接
|
||||
rule7: '#ffffff', // 日期时间
|
||||
rule8: '#8b5cf6' // 单位数据
|
||||
}
|
||||
|
||||
// 按颜色分组的高亮规则
|
||||
export const HIGHLIGHT_RULES = {
|
||||
// 规则1 - 错误类关键词
|
||||
rule1: {
|
||||
title: '错误类关键词',
|
||||
pattern: /\b(?:errors?|err|fail(?:ed|ure)?|fatal|critical|denied|refused|broken|crash(?:ed)?|exception|timeout|abort(?:ed)?|reject(?:ed)?|forbidden|unauthorized|conflict|corrupt(?:ed)?|missing|not found|unreachable|disconnect(?:ed)?|kill(?:ed)?|terminate(?:d)?|dead|died|panic|alarm|alert|emergency|severe|cannot|unable|impossible|blocked|locked|disaster|malformed|malicious|virus|breach|hack(?:ed)?|attack|exploit|vulnerability|damaged|destroyed|overload|overflow|outage|down|offline|inaccessible|unavailable|suspended|revoked|expired|expires|blacklisted|infected|compromised|hijacked|suspicious|illegal|loss|death|bad)\b/,
|
||||
flags: 'gi',
|
||||
fullLine: true,
|
||||
displayColor: DEFAULT_COLOR_MAPPING.rule1,
|
||||
backgroundColor: null,
|
||||
bold: true,
|
||||
italic: false,
|
||||
underline: false,
|
||||
enabled: true
|
||||
},
|
||||
|
||||
// 规则2 - 警告类关键词
|
||||
rule2: {
|
||||
title: '警告类关键词',
|
||||
pattern: /\b(?:warn(?:ing)?s?|deprecated|caution|retry|retrying|retried|skipped|ignored|pause(?:d)?|delay(?:ed)?|slow|slower|outdated|obsolete|insecure|vulnerable|risky|unstable|experimental|beta|alpha|preview|temporary|temp|pending|throttle(?:d)?|restrict(?:ed)?|downgrade(?:d)?|fallback|backup|migration|maintenance|partial|limited|degraded|reduced|minor|notice|advisory|reminder|important|security|urgent|attention|required|mandatory|danger|risk|permission)\b/,
|
||||
flags: 'gi',
|
||||
fullLine: false,
|
||||
displayColor: DEFAULT_COLOR_MAPPING.rule2,
|
||||
backgroundColor: null,
|
||||
bold: false,
|
||||
italic: true,
|
||||
underline: false,
|
||||
enabled: true
|
||||
},
|
||||
|
||||
// 规则3 - 成功类关键词
|
||||
rule3: {
|
||||
title: '成功类关键词',
|
||||
pattern: /\b(?:success(?:ful)?|successfully|complete(?:d)?|completed|finish(?:ed)?|finished|ok(?:ay)?|ready|active|running|begin|launch(?:ed)?|launched|connect(?:ed)?|connected|online|available|enabled|valid|verified|confirmed|approved|passed|accepted|resolved|fixed|repaired|restored|recovered|upgraded|updated|installed|deployed|built|compiled|loaded|mounted|synchronized|synced|healthy|stable|secure|safe|protected|authenticated|authorized|granted|allowed|permitted|working|alive|opened|succeeded|established)\b/,
|
||||
flags: 'gi',
|
||||
fullLine: false,
|
||||
displayColor: DEFAULT_COLOR_MAPPING.rule3,
|
||||
backgroundColor: null,
|
||||
bold: false,
|
||||
italic: false,
|
||||
underline: false,
|
||||
enabled: true
|
||||
},
|
||||
|
||||
// 规则4 - 信息类关键词
|
||||
rule4: {
|
||||
title: '信息类关键词',
|
||||
pattern: /\b(?:info|information|notification|message|msg|debug|trace|verbose|status|report|summary|loading|connecting|processing|monitoring|checking|scanning|analyzing|parsing|building|compiling|initializing|setup|preparing|progress|executing|stopped|stopping|resumed|resuming|restarted|restarting|closed|queued|removed|sleeping|zombie)\b/,
|
||||
flags: 'gi',
|
||||
fullLine: false,
|
||||
displayColor: DEFAULT_COLOR_MAPPING.rule4,
|
||||
backgroundColor: null,
|
||||
bold: false,
|
||||
italic: false,
|
||||
underline: false,
|
||||
enabled: true
|
||||
},
|
||||
|
||||
// 规则5 - 网络地址
|
||||
rule5: {
|
||||
title: 'IP地址和端口',
|
||||
pattern: /\b(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)(?::[1-9]\d{0,4})?\b|(?:(?:[0-9a-fA-F]{1,4}:)*)?::(?:[0-9a-fA-F]{1,4}:)*[0-9a-fA-F]{0,4}|(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}/,
|
||||
flags: 'gi',
|
||||
fullLine: false,
|
||||
displayColor: DEFAULT_COLOR_MAPPING.rule5,
|
||||
backgroundColor: null,
|
||||
bold: false,
|
||||
italic: false,
|
||||
underline: false,
|
||||
enabled: true
|
||||
},
|
||||
|
||||
// 规则6 - URL链接、邮箱地址、文件路径
|
||||
rule6: {
|
||||
title: 'URL链接和路径',
|
||||
pattern: /(?:https?|ftp|ftps|ssh|telnet|ws|wss):\/\/[^\s]+|file:\/\/[^\s]+|mailto:[^\s]+|www\.[^\s]+\.[a-z]{2,}[^\s]*|[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}|(?<=^|\s)(?:[/~]|\.\.?\/)[a-zA-Z0-9_\-./]+|(?<=^|\s)[A-Z]:\\[a-zA-Z0-9_\-.\\\s]+/,
|
||||
flags: 'gi',
|
||||
fullLine: false,
|
||||
displayColor: DEFAULT_COLOR_MAPPING.rule6,
|
||||
backgroundColor: null,
|
||||
bold: false,
|
||||
italic: false,
|
||||
underline: true,
|
||||
enabled: true
|
||||
},
|
||||
|
||||
// 规则7 - 日期时间格式
|
||||
rule7: {
|
||||
title: '日期时间',
|
||||
pattern: /\b\d{4}[-/]\d{1,2}[-/]\d{1,2}(?:[Tt\s]\d{1,2}:\d{1,2}(?::\d{1,2})?(?:\.\d+)?[Zz]?)?\b|\b\d{1,2}[-/]\d{1,2}[-/]\d{2,4}\b|\b(?:[01]?\d|2[0-3]):[0-5]\d(?::[0-5]\d)?(?:\.\d+)?(?:\s?[AaPp][Mm])?\b|\[\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2}(?:\.\d+)?\]|\b\d{10,13}\b/,
|
||||
flags: 'gi',
|
||||
fullLine: false,
|
||||
displayColor: '#ffffff',
|
||||
backgroundColor: '#fa541c',
|
||||
bold: false,
|
||||
italic: false,
|
||||
underline: false,
|
||||
enabled: true
|
||||
},
|
||||
|
||||
// 规则8 - 带单位的数据
|
||||
rule8: {
|
||||
title: '数据和单位',
|
||||
pattern: /\b\d+(?:\.\d+)?\s*(?:TiB|GiB|MiB|KiB|TB|GB|MB|KB|Tbps|Gbps|Mbps|Kbps|bps|ns|μs|ms|min|hrs?|°C|°F|Hz|KHz|MHz|GHz|THz|mV|kV|mA|kA|mW|kW|MW|GW|fps|rpm|RPM|dpi|ppi|px|bits?|bytes?|cores?|threads?)\b|\b(?:\d+(?:\.\d+)?|100(?:\.0+)?)\s*%|\b\d+(?:\.\d+)?\s*(?:ops[/]s|req[/]s|qps|tps|rps|iops|IOPS|pps|PPS)\b|\b\d+(?:\.\d+)?\s+(?:milliseconds?|seconds?|minutes?|hours?|days?|weeks?|months?|years?)\b/,
|
||||
flags: 'gi',
|
||||
fullLine: false,
|
||||
displayColor: DEFAULT_COLOR_MAPPING.rule8,
|
||||
backgroundColor: null,
|
||||
bold: false,
|
||||
italic: false,
|
||||
underline: false,
|
||||
enabled: true
|
||||
}
|
||||
}
|
||||
|
||||
export class TerminalHighlighter {
|
||||
// ANSI序列正则表达式
|
||||
// eslint-disable-next-line no-control-regex
|
||||
static ANSI_DETECT = /\x1b\[/
|
||||
// eslint-disable-next-line no-control-regex
|
||||
static ANSI_FULL = /\x1b\[[0-9;]*[mGKH]|\x1b\[[?0-9;]*[lh]|\x1b\[[0-9]*[ABCD]|\x1b\[2J|\x1b\[H|\x1b\[K|\x1b>|\x1b\[7m|\x1b\[27m/g
|
||||
|
||||
constructor(terminal, options = {}) {
|
||||
this.terminal = terminal
|
||||
this.enabled = options.enabled !== false
|
||||
this.debugMode = options.debugMode || false
|
||||
|
||||
// 自定义规则
|
||||
this.customRules = options.customRules || null
|
||||
|
||||
// 编译规则(按flags分组的合并正则)
|
||||
const compiled = this.compileRules()
|
||||
this.mergedPatterns = compiled.mergedPatterns
|
||||
|
||||
if (this.debugMode) {
|
||||
console.log('已初始化终端高亮器')
|
||||
const totalRules = this.mergedPatterns.reduce((sum, group) =>
|
||||
sum + Object.keys(group.ruleMap).length, 0)
|
||||
console.log('已启用规则数:', totalRules)
|
||||
}
|
||||
}
|
||||
|
||||
// 预编译合并的规则 - 按flags分组
|
||||
compileRules() {
|
||||
const rulesToUse = this.customRules || HIGHLIGHT_RULES
|
||||
|
||||
// 按 flags 分组
|
||||
const rulesByFlags = {}
|
||||
|
||||
for (const [name, rule] of Object.entries(rulesToUse)) {
|
||||
// 跳过禁用的规则
|
||||
if (rule.enabled === false) {
|
||||
continue
|
||||
}
|
||||
|
||||
// 使用通用方法处理pattern
|
||||
const patternSource = this.extractPatternSource(rule)
|
||||
if (!patternSource) {
|
||||
console.warn(`跳过无效的规则 ${ name }:`, rule.pattern)
|
||||
continue
|
||||
}
|
||||
|
||||
const flags = rule.flags || 'gi'
|
||||
|
||||
if (!rulesByFlags[flags]) {
|
||||
rulesByFlags[flags] = {
|
||||
patterns: [],
|
||||
rules: []
|
||||
}
|
||||
}
|
||||
|
||||
rulesByFlags[flags].patterns.push(`(${ patternSource })`)
|
||||
rulesByFlags[flags].rules.push({
|
||||
name,
|
||||
displayColor: rule.displayColor,
|
||||
backgroundColor: rule.backgroundColor,
|
||||
bold: rule.bold,
|
||||
italic: rule.italic,
|
||||
underline: rule.underline
|
||||
})
|
||||
}
|
||||
|
||||
// 为每组flags创建合并的正则
|
||||
const mergedPatterns = []
|
||||
|
||||
for (const [flags, group] of Object.entries(rulesByFlags)) {
|
||||
if (group.patterns.length === 0) continue
|
||||
|
||||
const mergedPatternSource = group.patterns.join('|')
|
||||
const mergedPattern = new RegExp(mergedPatternSource, flags)
|
||||
|
||||
// 创建规则映射(捕获组索引 -> 规则)
|
||||
const ruleMap = {}
|
||||
group.rules.forEach((rule, index) => {
|
||||
ruleMap[index + 1] = rule
|
||||
})
|
||||
|
||||
mergedPatterns.push({
|
||||
pattern: mergedPattern,
|
||||
ruleMap,
|
||||
flags
|
||||
})
|
||||
}
|
||||
|
||||
if (this.debugMode) {
|
||||
console.log('合并正则分组数:', mergedPatterns.length)
|
||||
mergedPatterns.forEach(group => {
|
||||
console.log(` flags=${ group.flags }, 规则数=${ Object.keys(group.ruleMap).length }`)
|
||||
})
|
||||
}
|
||||
|
||||
return { mergedPatterns }
|
||||
}
|
||||
|
||||
// 提取并处理规则的pattern源码
|
||||
extractPatternSource(rule) {
|
||||
let patternSource = ''
|
||||
|
||||
if (rule.pattern instanceof RegExp) {
|
||||
patternSource = rule.pattern.source
|
||||
} else if (rule.pattern && rule.pattern.source) {
|
||||
patternSource = rule.pattern.source
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
|
||||
// 如果启用了fullLine,扩展模式到行尾
|
||||
if (rule.fullLine) {
|
||||
patternSource = `(?:${ patternSource }).*?(?=\\r?\\n|$)`
|
||||
}
|
||||
|
||||
return patternSource
|
||||
}
|
||||
|
||||
// 更新自定义规则
|
||||
updateCustomRules(customRules) {
|
||||
this.customRules = customRules
|
||||
|
||||
// 重新编译规则
|
||||
const compiled = this.compileRules()
|
||||
this.mergedPatterns = compiled.mergedPatterns
|
||||
|
||||
if (this.debugMode) {
|
||||
const totalRules = this.mergedPatterns.reduce((sum, group) =>
|
||||
sum + Object.keys(group.ruleMap).length, 0)
|
||||
console.log('规则已更新,启用规则数:', totalRules)
|
||||
}
|
||||
}
|
||||
|
||||
// 主要的高亮方法
|
||||
highlightText(text) {
|
||||
if (!this.enabled || !text) {
|
||||
return text
|
||||
}
|
||||
|
||||
// 跳过空白、极短文本、纯符号文本
|
||||
if (this.shouldSkip(text)) {
|
||||
if (this.debugMode) {
|
||||
console.log('跳过特殊文本:', JSON.stringify(text))
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
// 检查是否包含ANSI序列
|
||||
if (this.hasAnsiSequences(text)) {
|
||||
// 如果文本主要是控制序列(如光标移动、清屏等),则跳过
|
||||
if (this.isControlSequenceOnly(text)) {
|
||||
if (this.debugMode) {
|
||||
console.log('跳过控制序列文本:', text.replace(/\x1b/g, '\\x1b'))
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
// 对于包含少量ANSI但有实际内容的文本,尝试着去处理
|
||||
try {
|
||||
const result = this.applyRulesWithAnsi(text)
|
||||
if (this.debugMode && result !== text) {
|
||||
console.log('原始文本 (含ANSI):', text.replace(/\x1b/g, '\\x1b'))
|
||||
console.log('高亮后:', result.replace(/\x1b/g, '\\x1b'))
|
||||
}
|
||||
return result
|
||||
} catch (error) {
|
||||
if (this.debugMode) {
|
||||
console.log('ANSI文本处理失败,保持原样:', text.replace(/\x1b/g, '\\x1b'))
|
||||
}
|
||||
return text
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const highlightedText = this.applyRules(text)
|
||||
|
||||
if (this.debugMode && text !== highlightedText) {
|
||||
console.log('原始文本:', JSON.stringify(text))
|
||||
console.log('高亮后:', highlightedText.replace(/\x1b/g, '\\x1b'))
|
||||
}
|
||||
|
||||
return highlightedText
|
||||
} catch (error) {
|
||||
console.error('高亮处理失败:', error)
|
||||
return text
|
||||
}
|
||||
}
|
||||
|
||||
// 检测ANSI序列
|
||||
hasAnsiSequences(text) {
|
||||
return TerminalHighlighter.ANSI_DETECT.test(text)
|
||||
}
|
||||
|
||||
// 检查是否主要是控制序列
|
||||
isControlSequenceOnly(text) {
|
||||
// 移除ANSI序列后,如果剩余内容很少,则认为是控制序列
|
||||
const cleanText = text.replace(TerminalHighlighter.ANSI_FULL, '').trim()
|
||||
return cleanText.length < 3 || /^[\s\r\n]*$/.test(cleanText)
|
||||
}
|
||||
|
||||
// 处理包含ANSI序列的文本,保护现有的ANSI序列,只对纯文本部分应用高亮
|
||||
applyRulesWithAnsi(text) {
|
||||
// 分离ANSI序列和纯文本
|
||||
const parts = []
|
||||
let lastIndex = 0
|
||||
|
||||
// 使用全局正则匹配ANSI序列
|
||||
const matches = text.matchAll(TerminalHighlighter.ANSI_FULL)
|
||||
|
||||
for (const match of matches) {
|
||||
// 添加ANSI序列之前的文本
|
||||
if (match.index > lastIndex) {
|
||||
const textPart = text.slice(lastIndex, match.index)
|
||||
if (textPart) {
|
||||
parts.push({ type: 'text', content: textPart })
|
||||
}
|
||||
}
|
||||
|
||||
// 添加ANSI序列
|
||||
parts.push({ type: 'ansi', content: match[0] })
|
||||
lastIndex = match.index + match[0].length
|
||||
}
|
||||
|
||||
// 添加最后剩余的文本
|
||||
if (lastIndex < text.length) {
|
||||
const textPart = text.slice(lastIndex)
|
||||
if (textPart) {
|
||||
parts.push({ type: 'text', content: textPart })
|
||||
}
|
||||
}
|
||||
|
||||
// 对纯文本部分应用高亮规则
|
||||
let result = ''
|
||||
for (const part of parts) {
|
||||
if (part.type === 'ansi') {
|
||||
result += part.content
|
||||
} else {
|
||||
const highlighted = this.applyRules(part.content)
|
||||
result += highlighted
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// 判断是否跳过处理
|
||||
shouldSkip(text) {
|
||||
// 跳过空文本
|
||||
if (!text.trim()) return true
|
||||
|
||||
// 跳过极短文本
|
||||
if (text.length < 3) return true
|
||||
|
||||
// 跳过纯符号文本
|
||||
if (/^[\W_]+$/.test(text)) return true
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// 应用所有规则 - 使用分组合并的正则
|
||||
applyRules(text) {
|
||||
if (!this.mergedPatterns || this.mergedPatterns.length === 0) {
|
||||
return text // 没有启用的规则
|
||||
}
|
||||
|
||||
let result = text
|
||||
|
||||
// 依次应用每个flags分组的合并正则
|
||||
for (const group of this.mergedPatterns) {
|
||||
result = result.replace(group.pattern, (...args) => {
|
||||
const match = args[0]
|
||||
|
||||
// 避免重复高亮已包含ANSI序列的匹配
|
||||
if (TerminalHighlighter.ANSI_DETECT.test(match)) {
|
||||
return match
|
||||
}
|
||||
|
||||
// 找到匹配的捕获组
|
||||
const groups = args.slice(1, -2)
|
||||
|
||||
// 找到第一个非undefined的捕获组
|
||||
for (let i = 0; i < groups.length; i++) {
|
||||
if (groups[i] !== undefined) {
|
||||
const rule = group.ruleMap[i + 1]
|
||||
if (rule) {
|
||||
return this.applyStyle(match, rule)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return match
|
||||
})
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// 应用样式到匹配文本
|
||||
applyStyle(text, rule) {
|
||||
let styleString = ''
|
||||
|
||||
// 先添加背景色
|
||||
if (rule.backgroundColor) {
|
||||
const bgColor = this.getBackgroundColorCode(rule.backgroundColor)
|
||||
if (bgColor) {
|
||||
styleString += bgColor
|
||||
}
|
||||
}
|
||||
|
||||
// 添加文字样式
|
||||
if (rule.bold) styleString += TEXT_STYLES.BOLD
|
||||
if (rule.italic) styleString += TEXT_STYLES.ITALIC
|
||||
if (rule.underline) styleString += TEXT_STYLES.UNDERLINE
|
||||
|
||||
// 添加文本颜色
|
||||
if (rule.displayColor) {
|
||||
const textColor = this.getTextColorCode(rule.displayColor)
|
||||
if (textColor) {
|
||||
styleString += textColor
|
||||
}
|
||||
}
|
||||
|
||||
return `${styleString}${text}${TEXT_STYLES.RESET}`
|
||||
}
|
||||
|
||||
// 获取文本颜色 ANSI 代码
|
||||
getTextColorCode(hexColor) {
|
||||
if (!hexColor) return ''
|
||||
|
||||
// 将十六进制颜色转换为 RGB
|
||||
const rgb = this.hexToRgb(hexColor)
|
||||
if (!rgb) return ''
|
||||
|
||||
// 使用 RGB 模式 (38;2;r;g;b) 38=前景色(文本), 48=背景色
|
||||
return `\x1b[38;2;${ rgb.r };${ rgb.g };${ rgb.b }m`
|
||||
}
|
||||
|
||||
// 获取背景色 ANSI 代码
|
||||
getBackgroundColorCode(hexColor) {
|
||||
if (!hexColor) return ''
|
||||
|
||||
// 将十六进制颜色转换为 RGB
|
||||
const rgb = this.hexToRgb(hexColor)
|
||||
if (!rgb) return ''
|
||||
|
||||
// 使用 RGB 模式 (48;2;r;g;b) 38=前景色(文本), 48=背景色
|
||||
return `\x1b[48;2;${ rgb.r };${ rgb.g };${ rgb.b }m`
|
||||
}
|
||||
|
||||
// 十六进制颜色转 RGB
|
||||
hexToRgb(hex) {
|
||||
if (!hex) return null
|
||||
|
||||
// 移除 # 符号
|
||||
hex = hex.replace('#', '')
|
||||
|
||||
// 简写形式的hex
|
||||
if (hex.length === 3) {
|
||||
hex = hex.split('').map(char => char + char).join('')
|
||||
}
|
||||
|
||||
if (hex.length !== 6) return null
|
||||
|
||||
return {
|
||||
r: parseInt(hex.substring(0, 2), 16),
|
||||
g: parseInt(hex.substring(2, 4), 16),
|
||||
b: parseInt(hex.substring(4, 6), 16)
|
||||
}
|
||||
}
|
||||
|
||||
// 启用/禁用高亮
|
||||
setEnabled(enabled) {
|
||||
this.enabled = enabled
|
||||
}
|
||||
|
||||
// 启用/禁用调试模式
|
||||
setDebugMode(debugMode) {
|
||||
this.debugMode = debugMode
|
||||
if (debugMode) {
|
||||
console.log('终端高亮调试模式已启用')
|
||||
} else {
|
||||
console.log('终端高亮调试模式已禁用')
|
||||
}
|
||||
}
|
||||
|
||||
// 应用单个规则的HTML预览
|
||||
applySingleRuleForHtml(text, rule) {
|
||||
try {
|
||||
// 使用通用方法获取pattern源码
|
||||
const patternSource = this.extractPatternSource(rule)
|
||||
if (!patternSource) {
|
||||
return '正则表达式格式错误'
|
||||
}
|
||||
|
||||
const flags = rule.flags || 'gi'
|
||||
const regex = new RegExp(patternSource, flags)
|
||||
|
||||
return text.replace(regex, (match) => {
|
||||
return this.applyHtmlStyle(match, rule)
|
||||
})
|
||||
} catch (error) {
|
||||
return `正则表达式错误: ${ error.message }`
|
||||
}
|
||||
}
|
||||
|
||||
// 为HTML应用样式
|
||||
applyHtmlStyle(text, rule) {
|
||||
let styleProps = ''
|
||||
|
||||
// 字体设置
|
||||
styleProps += 'font-family: monospace !important;'
|
||||
styleProps += 'font-size: 14px !important;'
|
||||
|
||||
// 文字样式
|
||||
styleProps += 'font-weight: ' + (rule.bold ? 'bold' : 'normal') + ' !important;'
|
||||
styleProps += 'font-style: ' + (rule.italic ? 'italic' : 'normal') + ' !important;'
|
||||
|
||||
// 文本颜色
|
||||
if (rule.displayColor) {
|
||||
styleProps += ` color: ${ rule.displayColor } !important;`
|
||||
}
|
||||
|
||||
// 背景色
|
||||
if (rule.backgroundColor) {
|
||||
styleProps += ` background-color: ${ rule.backgroundColor } !important;`
|
||||
}
|
||||
|
||||
// 下划线
|
||||
if (rule.underline) {
|
||||
styleProps += ' text-decoration: underline !important;'
|
||||
}
|
||||
|
||||
// 基础样式
|
||||
styleProps += ' padding: 2px 4px !important; border-radius: 3px !important;'
|
||||
|
||||
return `<span style="${ styleProps }">${ text }</span>`
|
||||
}
|
||||
}
|
||||
@@ -68,6 +68,39 @@
|
||||
placeholder="shell script"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="执行模式" prop="useBase64">
|
||||
<el-radio-group v-model="formData.useBase64">
|
||||
<el-radio :value="false">
|
||||
<span>直接执行</span>
|
||||
<el-tooltip placement="right">
|
||||
<template #content>
|
||||
<div style="max-width: 300px;">
|
||||
适用于单行简单脚本。<br>
|
||||
脚本内容会直接发送到终端。<br>
|
||||
注意:多行脚本会逐行自动执行。
|
||||
</div>
|
||||
</template>
|
||||
<el-icon style="margin-left: 4px; cursor: help;"><QuestionFilled /></el-icon>
|
||||
</el-tooltip>
|
||||
</el-radio>
|
||||
<el-radio :value="true">
|
||||
<span>Base64编码执行</span>
|
||||
<el-tooltip placement="right">
|
||||
<template #content>
|
||||
<div style="max-width: 300px;">
|
||||
适用于多行复杂脚本。<br>
|
||||
脚本通过Base64编码后执行,可以避免:<br>
|
||||
• 特殊字符转义问题<br>
|
||||
• heredoc标记冲突<br>
|
||||
• 换行符兼容问题<br>
|
||||
命令格式: echo '<script>' | base64 -d | bash
|
||||
</div>
|
||||
</template>
|
||||
<el-icon style="margin-left: 4px; cursor: help;"><QuestionFilled /></el-icon>
|
||||
</el-tooltip>
|
||||
</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span>
|
||||
@@ -81,6 +114,7 @@
|
||||
<script setup>
|
||||
import { ref, computed, reactive, watch } from 'vue'
|
||||
import { getCurrentInstance } from 'vue'
|
||||
import { QuestionFilled } from '@element-plus/icons-vue'
|
||||
|
||||
const props = defineProps({
|
||||
show: {
|
||||
@@ -122,7 +156,8 @@ const formData = reactive({
|
||||
name: '',
|
||||
description: '',
|
||||
index: nextIndex.value || 0,
|
||||
command: ''
|
||||
command: '',
|
||||
useBase64: false
|
||||
})
|
||||
const isModify = computed(() => Boolean(formData.id))
|
||||
|
||||
@@ -164,7 +199,8 @@ const handleClose = () => {
|
||||
name: '',
|
||||
description: '',
|
||||
index: nextIndex.value,
|
||||
command: props.defaultScript || ''
|
||||
command: props.defaultScript || '',
|
||||
useBase64: false
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -68,6 +68,7 @@
|
||||
<el-table-column prop="name" label="名称" />
|
||||
<el-table-column prop="description" label="描述" />
|
||||
<el-table-column prop="command" label="指令内容" show-overflow-tooltip />
|
||||
<el-table-column prop="useBase64" label="执行模式" width="140px" :formatter="formatExecutionMode" />
|
||||
<el-table-column label="操作" fixed="right" width="160px">
|
||||
<template #default="{ row }">
|
||||
<template v-if="row.index !== '--'">
|
||||
@@ -238,6 +239,14 @@ const handleEditSuccess = () => {
|
||||
currentScript.value = {}
|
||||
}
|
||||
|
||||
// 格式化执行模式显示
|
||||
const formatExecutionMode = (row, column, cellValue) => {
|
||||
if (cellValue === true) {
|
||||
return 'Base64编码执行'
|
||||
}
|
||||
return '直接执行'
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
<template>
|
||||
<el-drawer
|
||||
v-model="visible"
|
||||
title="菜单选项"
|
||||
:direction="isMobileScreen ? 'ttb' : 'ltr'"
|
||||
:close-on-click-modal="true"
|
||||
:close-on-press-escape="true"
|
||||
:modal="true"
|
||||
modal-class="menu_options_drawer"
|
||||
:size="isMobileScreen ? '80%' : '30%'"
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
label-suffix=":"
|
||||
label-width="100px"
|
||||
:show-message="false"
|
||||
>
|
||||
<el-form-item label="实例" prop="instanceGroup">
|
||||
<el-radio-group
|
||||
v-model="hostGroupCascader"
|
||||
size="small"
|
||||
text-color="#fff"
|
||||
fill="#13ce66"
|
||||
>
|
||||
<el-radio-button :value="true">分组级联展示</el-radio-button>
|
||||
<el-radio-button :value="false">不分组单列展示</el-radio-button>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="脚本库" prop="scriptLibrary">
|
||||
<el-radio-group
|
||||
v-model="scriptLibraryCascader"
|
||||
size="small"
|
||||
text-color="#fff"
|
||||
:fill="scriptLibrary ? '#13ce66' : '#999'"
|
||||
:disabled="!scriptLibrary"
|
||||
>
|
||||
<el-radio-button :value="true">分组级联展示</el-radio-button>
|
||||
<el-radio-button :value="false">不分组单列展示</el-radio-button>
|
||||
</el-radio-group>
|
||||
<span class="script_library_switch">
|
||||
<el-switch
|
||||
v-model="scriptLibrary"
|
||||
class="switch"
|
||||
inline-prompt
|
||||
style="--el-switch-on-color: #13ce66; --el-switch-off-color: #999"
|
||||
active-text="显示"
|
||||
inactive-text="隐藏"
|
||||
/>
|
||||
</span>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog_footer">
|
||||
<el-button @click="visible = false">关闭</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, getCurrentInstance } from 'vue'
|
||||
import useMobileWidth from '@/composables/useMobileWidth'
|
||||
|
||||
const { proxy: { $store } } = getCurrentInstance()
|
||||
const { isMobileScreen } = useMobileWidth()
|
||||
|
||||
const props = defineProps({
|
||||
show: {
|
||||
required: true,
|
||||
type: Boolean
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:show'])
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.show,
|
||||
set: (newVal) => emit('update:show', newVal)
|
||||
})
|
||||
|
||||
const scriptLibrary = computed({
|
||||
get: () => $store.menuSetting.scriptLibrary,
|
||||
set: (newVal) => $store.setMenuSetting({ scriptLibrary: newVal })
|
||||
})
|
||||
|
||||
const scriptLibraryCascader = computed({
|
||||
get: () => $store.menuSetting.scriptLibraryCascader,
|
||||
set: (newVal) => $store.setMenuSetting({ scriptLibraryCascader: newVal })
|
||||
})
|
||||
|
||||
const hostGroupCascader = computed({
|
||||
get: () => $store.menuSetting.hostGroupCascader,
|
||||
set: (newVal) => $store.setMenuSetting({ hostGroupCascader: newVal })
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.dialog_footer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
.script_library_switch {
|
||||
margin-left: 20px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
.menu_options_drawer {
|
||||
.el-drawer__header {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,111 @@
|
||||
<template>
|
||||
<el-drawer
|
||||
v-model="visible"
|
||||
title="其他设置"
|
||||
:direction="isMobileScreen ? 'ttb' : 'ltr'"
|
||||
:close-on-click-modal="true"
|
||||
:close-on-press-escape="true"
|
||||
:modal="true"
|
||||
modal-class="other_setting_drawer"
|
||||
:size="isMobileScreen ? '80%' : '30%'"
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
label-suffix=":"
|
||||
label-width="100px"
|
||||
:show-message="false"
|
||||
>
|
||||
<el-form-item label="自动重连" prop="autoReconnect">
|
||||
<PlusSupportTip>
|
||||
<span>
|
||||
<el-switch
|
||||
v-model="autoReconnect"
|
||||
class="switch"
|
||||
inline-prompt
|
||||
:disabled="!isPlusActive"
|
||||
style="--el-switch-on-color: #13ce66; --el-switch-off-color: #ff4949"
|
||||
active-text="开启"
|
||||
inactive-text="关闭"
|
||||
/>
|
||||
<span class="plus_support_tip_text">(Plus专属功能)</span>
|
||||
</span>
|
||||
</PlusSupportTip>
|
||||
</el-form-item>
|
||||
<el-form-item label="脚本执行" prop="autoExecuteScript">
|
||||
<el-tooltip
|
||||
effect="dark"
|
||||
content="启用后从脚本库选中脚本后自动执行(回车操作)"
|
||||
placement="right"
|
||||
>
|
||||
<el-switch
|
||||
v-model="autoExecuteScript"
|
||||
class="switch"
|
||||
inline-prompt
|
||||
style="--el-switch-on-color: #13ce66; --el-switch-off-color: #ff4949"
|
||||
active-text="自动"
|
||||
inactive-text="手动"
|
||||
/>
|
||||
</el-tooltip>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog_footer">
|
||||
<el-button @click="visible = false">关闭</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, getCurrentInstance } from 'vue'
|
||||
import useMobileWidth from '@/composables/useMobileWidth'
|
||||
import PlusSupportTip from '@/components/common/PlusSupportTip.vue'
|
||||
|
||||
const { proxy: { $store } } = getCurrentInstance()
|
||||
const { isMobileScreen } = useMobileWidth()
|
||||
|
||||
const props = defineProps({
|
||||
show: {
|
||||
required: true,
|
||||
type: Boolean
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:show'])
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.show,
|
||||
set: (newVal) => emit('update:show', newVal)
|
||||
})
|
||||
|
||||
const autoReconnect = computed({
|
||||
get: () => $store.terminalConfig.autoReconnect,
|
||||
set: (newVal) => $store.setTerminalSetting({ autoReconnect: newVal })
|
||||
})
|
||||
|
||||
const autoExecuteScript = computed({
|
||||
get: () => $store.terminalConfig.autoExecuteScript,
|
||||
set: (newVal) => $store.setTerminalSetting({ autoExecuteScript: newVal })
|
||||
})
|
||||
|
||||
const isPlusActive = computed(() => $store.isPlusActive)
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.dialog_footer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
.other_setting_drawer {
|
||||
.el-drawer__header {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
}
|
||||
.plus_support_tip_text {
|
||||
margin-left: 5px;
|
||||
color: var(--el-text-color-placeholder);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,878 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
title="编辑高亮规则"
|
||||
width="900px"
|
||||
:close-on-click-modal="false"
|
||||
:before-close="handleBeforeClose"
|
||||
>
|
||||
<div class="rule-edit">
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
label-width="120px"
|
||||
>
|
||||
<el-form-item label="规则标题" prop="title">
|
||||
<el-input
|
||||
v-model="formData.title"
|
||||
placeholder="留空则显示默认名称"
|
||||
maxlength="10"
|
||||
show-word-limit
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="颜色设置">
|
||||
<div class="color-row">
|
||||
<div class="color-item">
|
||||
<label>文本颜色</label>
|
||||
<el-color-picker
|
||||
v-model="formData.displayColor"
|
||||
size="small"
|
||||
:predefine="getTextColorPredefines()"
|
||||
@change="handleColorChange"
|
||||
/>
|
||||
</div>
|
||||
<div class="color-item">
|
||||
<label>背景颜色</label>
|
||||
<el-color-picker
|
||||
v-model="formData.backgroundColor"
|
||||
size="small"
|
||||
show-alpha
|
||||
:predefine="getBackgroundColorPredefines()"
|
||||
@change="handleBackgroundColorChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="文字样式">
|
||||
<el-checkbox-group v-model="textStyles" @change="handleStyleChange">
|
||||
<el-checkbox value="bold">加粗</el-checkbox>
|
||||
<el-checkbox value="italic">斜体</el-checkbox>
|
||||
<el-checkbox value="underline">下划线</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="正则表达式" prop="pattern">
|
||||
<el-input
|
||||
v-model="patternSource"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入正则表达式"
|
||||
@input="updatePattern"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="规则选项">
|
||||
<el-checkbox v-model="ignoreCase" @change="updatePattern">忽略大小写</el-checkbox>
|
||||
<el-checkbox v-model="fullLine">高亮整行</el-checkbox>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="测试文本">
|
||||
<el-input
|
||||
v-model="testInput"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
placeholder="输入测试文本"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="匹配结果">
|
||||
<div class="test-result">
|
||||
<div v-if="testMatches.length" class="matches">
|
||||
<el-tag
|
||||
v-for="(match, index) in testMatches"
|
||||
:key="index"
|
||||
size="small"
|
||||
type="success"
|
||||
class="match-tag"
|
||||
>
|
||||
{{ match }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<el-text v-else size="small" type="info">
|
||||
无匹配结果
|
||||
</el-text>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="预览效果">
|
||||
<div class="preview-result" v-html="previewHtml" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="常用模板">
|
||||
<div class="templates">
|
||||
<el-button
|
||||
v-for="template in getTemplates(ruleName)"
|
||||
:key="template.name"
|
||||
size="small"
|
||||
@click="applyTemplate(template)"
|
||||
>
|
||||
{{ template.name }}
|
||||
</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="handleCancel">取消</el-button>
|
||||
<el-button type="primary" @click="handleSave">保存</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch, getCurrentInstance, nextTick } from 'vue'
|
||||
import { HIGHLIGHT_RULES, TerminalHighlighter } from '@/utils/highlighter'
|
||||
|
||||
const { proxy: { $message, $messageBox } } = getCurrentInstance()
|
||||
|
||||
// 检查颜色是否已被使用
|
||||
const isColorUsed = (color, rules, excludeKey = null) => {
|
||||
const usedColors = Object.entries(rules)
|
||||
.filter(([key,]) => key !== excludeKey)
|
||||
.map(([, rule,]) => rule.displayColor.toLowerCase())
|
||||
return usedColors.includes(color.toLowerCase())
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
ruleName: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
ruleData: {
|
||||
type: Object,
|
||||
default: () => null
|
||||
},
|
||||
allRules: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:show', 'save',])
|
||||
|
||||
const dialogVisible = computed({
|
||||
get: () => props.show,
|
||||
set: (val) => emit('update:show', val)
|
||||
})
|
||||
|
||||
const formRef = ref(null)
|
||||
const patternSource = ref('')
|
||||
const ignoreCase = ref(true)
|
||||
const fullLine = ref(false)
|
||||
const testInput = ref('')
|
||||
const textStyles = ref([])
|
||||
|
||||
// 创建一个用于预览的highlighter实例
|
||||
const previewHighlighter = new TerminalHighlighter(null, {
|
||||
enabled: true,
|
||||
debugMode: false
|
||||
})
|
||||
|
||||
// 原始数据,用于检测变化
|
||||
const originalData = ref({})
|
||||
|
||||
const formData = ref({
|
||||
title: '',
|
||||
pattern: null,
|
||||
flags: 'gi',
|
||||
fullLine: false,
|
||||
displayColor: '',
|
||||
backgroundColor: null,
|
||||
bold: false,
|
||||
italic: false,
|
||||
underline: false,
|
||||
enabled: true
|
||||
})
|
||||
|
||||
const formRules = {
|
||||
title: [
|
||||
{ max: 10, message: '标题长度最多 10 个字符', trigger: 'blur' },
|
||||
],
|
||||
pattern: [
|
||||
{
|
||||
required: true,
|
||||
validator: (_rule, _value, callback) => {
|
||||
if (!patternSource.value || patternSource.value.trim() === '') {
|
||||
callback(new Error('请输入正则表达式'))
|
||||
} else {
|
||||
try {
|
||||
new RegExp(patternSource.value, patternFlags.value)
|
||||
callback()
|
||||
} catch (error) {
|
||||
callback(new Error('正则表达式格式错误'))
|
||||
}
|
||||
}
|
||||
},
|
||||
trigger: 'blur'
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
// 计算属性
|
||||
const patternFlags = computed(() => ignoreCase.value ? 'gi' : 'g')
|
||||
|
||||
const testMatches = computed(() => {
|
||||
if (!patternSource.value || !testInput.value) return []
|
||||
|
||||
try {
|
||||
const regex = new RegExp(patternSource.value, patternFlags.value)
|
||||
const matches = testInput.value.match(regex) || []
|
||||
return [...new Set(matches),] // 去重
|
||||
} catch (error) {
|
||||
return []
|
||||
}
|
||||
})
|
||||
|
||||
const previewHtml = computed(() => {
|
||||
if (!patternSource.value || !testInput.value) {
|
||||
return testInput.value || '请输入测试文本'
|
||||
}
|
||||
|
||||
try {
|
||||
// 构造当前编辑的规则对象
|
||||
const currentRule = {
|
||||
pattern: new RegExp(patternSource.value, patternFlags.value),
|
||||
flags: patternFlags.value,
|
||||
fullLine: fullLine.value,
|
||||
displayColor: formData.value.displayColor,
|
||||
backgroundColor: formData.value.backgroundColor,
|
||||
bold: formData.value.bold,
|
||||
italic: formData.value.italic,
|
||||
underline: formData.value.underline,
|
||||
enabled: true
|
||||
}
|
||||
|
||||
// 使用highlighter的HTML预览方法
|
||||
return previewHighlighter.applySingleRuleForHtml(testInput.value, currentRule)
|
||||
} catch (error) {
|
||||
return `正则表达式错误: ${ error.message }`
|
||||
}
|
||||
})
|
||||
|
||||
// 获取文本颜色预定义值
|
||||
const getTextColorPredefines = () => {
|
||||
return [
|
||||
// 默认规则的文本颜色
|
||||
'#ff4d4f', // rule1 - 错误红色
|
||||
'#fadb14', // rule2 - 警告黄色
|
||||
'#52c41a', // rule3 - 成功绿色
|
||||
'#13c2c2', // rule4 - 信息青色
|
||||
'#eb2f96', // rule5 - 网络洋红
|
||||
'#1890ff', // rule6 - 链接蓝色
|
||||
'#ffffff', // rule7 - 日期时间白色
|
||||
'#8b5cf6', // rule8 - 单位数据紫色
|
||||
|
||||
// 基础色
|
||||
'#000000', // 黑色
|
||||
'#808080', // 灰色
|
||||
'#c0c0c0', // 浅灰色
|
||||
'#404040', // 深灰色
|
||||
|
||||
// 其他颜色
|
||||
'#ef4444', '#f97316', '#f59e0b', '#eab308',
|
||||
'#84cc16', '#22c55e', '#10b981', '#14b8a6',
|
||||
'#06b6d4', '#0ea5e9', '#3b82f6', '#6366f1',
|
||||
'#a855f7', '#d946ef', '#ec4899', '#f43f5e',
|
||||
]
|
||||
}
|
||||
|
||||
// 获取背景颜色预定义值
|
||||
const getBackgroundColorPredefines = () => {
|
||||
return [
|
||||
// 基础色
|
||||
'transparent',
|
||||
'#000000', // 黑色
|
||||
'#ffffff', // 白色
|
||||
'#404040', // 深灰色
|
||||
'#808080', // 中灰色
|
||||
'#c0c0c0', // 浅灰色
|
||||
|
||||
// 深色背景
|
||||
'#1f2937', // 深蓝灰
|
||||
'#374151', // 灰蓝色
|
||||
'#4b5563', // 石板灰
|
||||
'#6b7280', // 钢灰色
|
||||
'#fa541c', // 橙色
|
||||
'#dc2626', // 深红色
|
||||
'#b91c1c', // 暗红色
|
||||
'#7c3aed', // 深紫色
|
||||
|
||||
// 中等饱和度
|
||||
'#ef4444', // 红色
|
||||
'#f97316', // 橙色
|
||||
'#eab308', // 黄色
|
||||
'#22c55e', // 绿色
|
||||
'#06b6d4', // 青色
|
||||
'#3b82f6', // 蓝色
|
||||
'#8b5cf6', // 紫色
|
||||
'#ec4899', // 粉色
|
||||
|
||||
// 其他颜色
|
||||
'#1e40af', '#7c2d12', '#166534', '#0f766e',
|
||||
'#0c4a6e', '#581c87', '#9d174d', '#be123c',
|
||||
]
|
||||
}
|
||||
|
||||
// 颜色变化处理
|
||||
const handleColorChange = (newColor) => {
|
||||
if (!newColor) return
|
||||
|
||||
// 检查颜色是否重复
|
||||
if (isColorUsed(newColor, props.allRules, props.ruleName)) {
|
||||
$message.warning('该颜色已被其他规则使用,请选择其他颜色')
|
||||
// 阻止更新,强制恢复原颜色
|
||||
const originalColor = props.ruleData?.displayColor || HIGHLIGHT_RULES[props.ruleName].displayColor
|
||||
// 使用nextTick确保在下一个事件循环中恢复颜色
|
||||
nextTick(() => {
|
||||
formData.value.displayColor = originalColor
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
formData.value.displayColor = newColor
|
||||
}
|
||||
|
||||
// 背景颜色变化处理
|
||||
const handleBackgroundColorChange = (newColor) => {
|
||||
formData.value.backgroundColor = newColor
|
||||
}
|
||||
|
||||
// 文字样式变化处理
|
||||
const handleStyleChange = (styles) => {
|
||||
formData.value.bold = styles.includes('bold')
|
||||
formData.value.italic = styles.includes('italic')
|
||||
formData.value.underline = styles.includes('underline')
|
||||
}
|
||||
|
||||
// 获取模板列表
|
||||
const getTemplates = (ruleName) => {
|
||||
// 从默认规则中提取模板
|
||||
const defaultRule = HIGHLIGHT_RULES[ruleName]
|
||||
|
||||
const templates = [
|
||||
// 第一个模板:真实的默认规则
|
||||
{
|
||||
name: '默认规则(完整)',
|
||||
pattern: defaultRule.pattern.source,
|
||||
flags: defaultRule.flags.split('')
|
||||
},
|
||||
]
|
||||
|
||||
// 根据不同颜色添加特定的常用模板
|
||||
const additionalTemplates = getAdditionalTemplates(ruleName)
|
||||
templates.push(...additionalTemplates)
|
||||
|
||||
return templates
|
||||
}
|
||||
|
||||
// 获取各颜色的额外模板
|
||||
const getAdditionalTemplates = (ruleName) => {
|
||||
const templateMap = {
|
||||
rule1: [
|
||||
{
|
||||
name: '基础错误关键词',
|
||||
pattern: '\\b(error|err|failed?|failure|fatal|critical)\\b',
|
||||
flags: ['g', 'i',]
|
||||
},
|
||||
{
|
||||
name: '系统崩溃异常',
|
||||
pattern: '\\b(crash(ed)?|exception|panic|abort(ed)?|kill(ed)?|terminate(d)?|dead|died)\\b',
|
||||
flags: ['g', 'i',]
|
||||
},
|
||||
{
|
||||
name: '拒绝访问类',
|
||||
pattern: '\\b(denied|refused|forbidden|unauthorized|blocked|locked|invalid)\\b',
|
||||
flags: ['g', 'i',]
|
||||
},
|
||||
{
|
||||
name: '连接超时',
|
||||
pattern: '\\b(timeout|disconnect(ed)?|unreachable|unavailable|missing|not found)\\b',
|
||||
flags: ['g', 'i',]
|
||||
},
|
||||
{
|
||||
name: '安全威胁',
|
||||
pattern: '\\b(virus|breach|hack(ed)?|attack|exploit|vulnerability|malicious|infected|compromised|hijacked|suspicious|illegal)\\b',
|
||||
flags: ['g', 'i',]
|
||||
},
|
||||
],
|
||||
rule2: [
|
||||
{
|
||||
name: '基础警告关键词',
|
||||
pattern: '\\b(warn(ing)?|deprecated|caution)\\b',
|
||||
flags: ['g', 'i',]
|
||||
},
|
||||
{
|
||||
name: '重试延迟状态',
|
||||
pattern: '\\b(retry|retrying|retried|delay(ed)?|slow|slower|pending|waiting)\\b',
|
||||
flags: ['g', 'i',]
|
||||
},
|
||||
{
|
||||
name: '临时状态',
|
||||
pattern: '\\b(temporary|temp|experimental|beta|alpha|preview|unstable)\\b',
|
||||
flags: ['g', 'i',]
|
||||
},
|
||||
{
|
||||
name: '安全风险',
|
||||
pattern: '\\b(insecure|vulnerable|risky|outdated|obsolete)\\b',
|
||||
flags: ['g', 'i',]
|
||||
},
|
||||
{
|
||||
name: '系统维护',
|
||||
pattern: '\\b(maintenance|migration|backup|fallback|suspend(ed)?|pause(d)?)\\b',
|
||||
flags: ['g', 'i',]
|
||||
},
|
||||
],
|
||||
rule3: [
|
||||
{
|
||||
name: '基础成功关键词',
|
||||
pattern: '\\b(success(ful)?|successfully|complete(d)?|completed|ok(ay)?|done)\\b',
|
||||
flags: ['g', 'i',]
|
||||
},
|
||||
{
|
||||
name: '运行启动状态',
|
||||
pattern: '\\b(running|active|online|ready|started|start|begin|launch(ed)?)\\b',
|
||||
flags: ['g', 'i',]
|
||||
},
|
||||
{
|
||||
name: '连接可用状态',
|
||||
pattern: '\\b(connect(ed)?|available|enabled|online|accessible)\\b',
|
||||
flags: ['g', 'i',]
|
||||
},
|
||||
{
|
||||
name: '验证通过状态',
|
||||
pattern: '\\b(valid|verified|confirmed|approved|passed|accepted|authenticated|authorized)\\b',
|
||||
flags: ['g', 'i',]
|
||||
},
|
||||
{
|
||||
name: '安装部署状态',
|
||||
pattern: '\\b(installed|deployed|built|compiled|loaded|mounted|updated|upgraded)\\b',
|
||||
flags: ['g', 'i',]
|
||||
},
|
||||
],
|
||||
rule4: [
|
||||
{
|
||||
name: '基础信息关键词',
|
||||
pattern: '\\b(info|information|notice|message|log)\\b',
|
||||
flags: ['g', 'i',]
|
||||
},
|
||||
{
|
||||
name: '调试日志级别',
|
||||
pattern: '\\b(debug|trace|verbose|log|report)\\b',
|
||||
flags: ['g', 'i',]
|
||||
},
|
||||
{
|
||||
name: '处理连接状态',
|
||||
pattern: '\\b(processing|loading|connecting|checking|monitoring)\\b',
|
||||
flags: ['g', 'i',]
|
||||
},
|
||||
{
|
||||
name: '配置初始化',
|
||||
pattern: '\\b(config|configuration|setting|setup|initializing|preparing)\\b',
|
||||
flags: ['g', 'i',]
|
||||
},
|
||||
{
|
||||
name: '构建编译状态',
|
||||
pattern: '\\b(building|compiling|parsing|analyzing|scanning)\\b',
|
||||
flags: ['g', 'i',]
|
||||
},
|
||||
],
|
||||
rule5: [
|
||||
{
|
||||
name: 'IPv4地址',
|
||||
pattern: '\\b\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\b',
|
||||
flags: ['g',]
|
||||
},
|
||||
{
|
||||
name: 'IP:端口组合',
|
||||
pattern: '\\b\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}:\\d{1,5}\\b',
|
||||
flags: ['g',]
|
||||
},
|
||||
{
|
||||
name: 'IPv6完整格式',
|
||||
pattern: '\\b([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}\\b',
|
||||
flags: ['g',]
|
||||
},
|
||||
{
|
||||
name: 'IPv6简化格式',
|
||||
pattern: '\\b([0-9a-fA-F]{1,4}:)*::([0-9a-fA-F]{1,4}:)*[0-9a-fA-F]{0,4}\\b',
|
||||
flags: ['g',]
|
||||
},
|
||||
{
|
||||
name: 'MAC地址',
|
||||
pattern: '\\b([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}\\b|\\b([0-9a-fA-F]{2}-){5}[0-9a-fA-F]{2}\\b',
|
||||
flags: ['g',]
|
||||
},
|
||||
{
|
||||
name: '内网IPv4地址',
|
||||
pattern: '\\b(10\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}|172\\.(1[6-9]|2\\d|3[01])\\.\\d{1,3}\\.\\d{1,3}|192\\.168\\.\\d{1,3}\\.\\d{1,3})\\b',
|
||||
flags: ['g',]
|
||||
},
|
||||
],
|
||||
rule6: [
|
||||
{
|
||||
name: 'HTTP/HTTPS链接',
|
||||
pattern: 'https?:\\/\\/[^\\s]+',
|
||||
flags: ['g', 'i',]
|
||||
},
|
||||
{
|
||||
name: 'FTP链接',
|
||||
pattern: 'ftps?:\\/\\/[^\\s]+',
|
||||
flags: ['g', 'i',]
|
||||
},
|
||||
{
|
||||
name: '邮箱地址',
|
||||
pattern: '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}',
|
||||
flags: ['g', 'i',]
|
||||
},
|
||||
{
|
||||
name: '文件路径(Unix)',
|
||||
pattern: '\\/[\\w\\-._\\/]*[\\w\\-._]',
|
||||
flags: ['g',]
|
||||
},
|
||||
{
|
||||
name: '文件路径(Windows)',
|
||||
pattern: '[A-Z]:\\\\[\\w\\-._\\\\\\s]*[\\w\\-._]',
|
||||
flags: ['g', 'i',]
|
||||
},
|
||||
{
|
||||
name: 'www域名',
|
||||
pattern: 'www\\.[^\\s]+\\.[a-z]{2,}[^\\s]*',
|
||||
flags: ['g', 'i',]
|
||||
},
|
||||
],
|
||||
rule7: [
|
||||
{
|
||||
name: 'ISO 8601格式',
|
||||
pattern: '\\b\\d{4}-\\d{2}-\\d{2}[Tt]\\d{2}:\\d{2}:\\d{2}(\\.\\d{1,6})?[Zz]?\\b',
|
||||
flags: ['g',]
|
||||
},
|
||||
{
|
||||
name: '标准日期格式',
|
||||
pattern: '\\b\\d{4}[-/]\\d{1,2}[-/]\\d{1,2}\\b|\\b\\d{1,2}[-/]\\d{1,2}[-/]\\d{2,4}\\b',
|
||||
flags: ['g',]
|
||||
},
|
||||
{
|
||||
name: '时间格式',
|
||||
pattern: '\\b\\d{1,2}:\\d{1,2}(:\\d{1,2})?(\\.\\d{1,6})?\\b',
|
||||
flags: ['g',]
|
||||
},
|
||||
{
|
||||
name: '12小时制时间',
|
||||
pattern: '\\b\\d{1,2}:\\d{1,2}(:\\d{1,2})?(\\.\\d{1,6})?\\s?[AaPp][Mm]\\b',
|
||||
flags: ['g',]
|
||||
},
|
||||
{
|
||||
name: '日志时间戳',
|
||||
pattern: '\\[\\d{4}-\\d{2}-\\d{2}\\s\\d{2}:\\d{2}:\\d{2}(\\.\\d{1,6})?\\]',
|
||||
flags: ['g',]
|
||||
},
|
||||
{
|
||||
name: 'Unix时间戳',
|
||||
pattern: '\\b\\d{10,13}\\b',
|
||||
flags: ['g',]
|
||||
},
|
||||
],
|
||||
rule8: [
|
||||
{
|
||||
name: '存储单位',
|
||||
pattern: '\\b\\d+(?:\\.\\d+)?\\s*(?:TiB|GiB|MiB|KiB|TB|GB|MB|KB|B)\\b',
|
||||
flags: ['g', 'i',]
|
||||
},
|
||||
{
|
||||
name: '网络速度',
|
||||
pattern: '\\b\\d+(?:\\.\\d+)?\\s*(?:Tbps|Gbps|Mbps|Kbps|bps)\\b',
|
||||
flags: ['g', 'i',]
|
||||
},
|
||||
{
|
||||
name: '时间单位',
|
||||
pattern: '\\b\\d+(?:\\.\\d+)?\\s*(?:ns|μs|ms|min|hrs?)\\b',
|
||||
flags: ['g', 'i',]
|
||||
},
|
||||
{
|
||||
name: '温度单位',
|
||||
pattern: '\\b\\d+(?:\\.\\d+)?\\s*(?:°C|°F|K)\\b',
|
||||
flags: ['g', 'i',]
|
||||
},
|
||||
{
|
||||
name: '频率单位',
|
||||
pattern: '\\b\\d+(?:\\.\\d+)?\\s*(?:Hz|KHz|MHz|GHz|THz)\\b',
|
||||
flags: ['g', 'i',]
|
||||
},
|
||||
{
|
||||
name: '百分比',
|
||||
pattern: '\\b(?:\\d+(?:\\.\\d+)?|100(?:\\.0+)?)\\s*%',
|
||||
flags: ['g',]
|
||||
},
|
||||
{
|
||||
name: '性能指标',
|
||||
pattern: '\\b\\d+(?:\\.\\d+)?\\s*(?:ops[/]s|req[/]s|qps|tps|rps|iops|IOPS|pps|PPS)\\b',
|
||||
flags: ['g', 'i',]
|
||||
},
|
||||
{
|
||||
name: '完整时间描述',
|
||||
pattern: '\\b\\d+(?:\\.\\d+)?\\s+(?:milliseconds?|seconds?|minutes?|hours?|days?|weeks?|months?|years?)\\b',
|
||||
flags: ['g', 'i',]
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
return templateMap[ruleName] || []
|
||||
}
|
||||
|
||||
// 方法
|
||||
const updatePattern = () => {
|
||||
try {
|
||||
formData.value.pattern = new RegExp(patternSource.value, patternFlags.value)
|
||||
} catch (error) {
|
||||
console.error('正则表达式无效:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const applyTemplate = (template) => {
|
||||
patternSource.value = template.pattern
|
||||
ignoreCase.value = template.flags.includes('i')
|
||||
updatePattern()
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
updatePattern()
|
||||
|
||||
if (!formData.value.pattern) {
|
||||
throw new Error('正则表达式无效')
|
||||
}
|
||||
|
||||
// 最终检查颜色是否重复
|
||||
if (isColorUsed(formData.value.displayColor, props.allRules, props.ruleName)) {
|
||||
$message.error('该颜色已被其他规则使用,无法保存')
|
||||
return
|
||||
}
|
||||
|
||||
emit('save', props.ruleName, {
|
||||
title: formData.value.title,
|
||||
pattern: formData.value.pattern,
|
||||
flags: patternFlags.value,
|
||||
fullLine: fullLine.value,
|
||||
displayColor: formData.value.displayColor,
|
||||
backgroundColor: formData.value.backgroundColor,
|
||||
bold: formData.value.bold,
|
||||
italic: formData.value.italic,
|
||||
underline: formData.value.underline,
|
||||
enabled: formData.value.enabled
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('保存失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 检测数据是否发生变化
|
||||
const hasDataChanged = () => {
|
||||
if (!originalData.value || !Object.keys(originalData.value).length) return false
|
||||
|
||||
const current = {
|
||||
title: formData.value.title,
|
||||
displayColor: formData.value.displayColor,
|
||||
backgroundColor: formData.value.backgroundColor,
|
||||
bold: formData.value.bold,
|
||||
italic: formData.value.italic,
|
||||
underline: formData.value.underline,
|
||||
enabled: formData.value.enabled,
|
||||
patternSource: patternSource.value,
|
||||
ignoreCase: ignoreCase.value,
|
||||
fullLine: fullLine.value
|
||||
}
|
||||
|
||||
return JSON.stringify(current) !== JSON.stringify(originalData.value)
|
||||
}
|
||||
|
||||
// 关闭前确认
|
||||
const handleBeforeClose = async (done) => {
|
||||
if (hasDataChanged()) {
|
||||
try {
|
||||
await $messageBox.confirm(
|
||||
'您有未保存的修改,确定要关闭吗?',
|
||||
'确认关闭',
|
||||
{
|
||||
type: 'warning',
|
||||
confirmButtonText: '确定关闭',
|
||||
cancelButtonText: '继续编辑'
|
||||
}
|
||||
)
|
||||
done()
|
||||
} catch {
|
||||
// 用户取消,不关闭
|
||||
}
|
||||
} else {
|
||||
done()
|
||||
}
|
||||
}
|
||||
|
||||
const handleCancel = async () => {
|
||||
await handleBeforeClose(() => {
|
||||
dialogVisible.value = false
|
||||
})
|
||||
}
|
||||
|
||||
// 监听
|
||||
watch(() => props.show, (show) => {
|
||||
if (show && props.ruleData) {
|
||||
// 获取默认规则作为基础
|
||||
const defaultRule = HIGHLIGHT_RULES[props.ruleName]
|
||||
|
||||
// 使用 ?? 空值合并运算符,只有 null/undefined 时才使用默认值
|
||||
formData.value = {
|
||||
title: props.ruleData.title ?? defaultRule.title,
|
||||
displayColor: props.ruleData.displayColor ?? defaultRule.displayColor,
|
||||
backgroundColor: props.ruleData.backgroundColor ?? defaultRule.backgroundColor,
|
||||
bold: props.ruleData.bold ?? defaultRule.bold,
|
||||
italic: props.ruleData.italic ?? defaultRule.italic,
|
||||
underline: props.ruleData.underline ?? defaultRule.underline,
|
||||
enabled: props.ruleData.enabled !== false
|
||||
}
|
||||
|
||||
// 标准RegExp对象处理
|
||||
const patternData = props.ruleData.pattern ?? defaultRule.pattern
|
||||
if (patternData && patternData.source) {
|
||||
patternSource.value = patternData.source
|
||||
ignoreCase.value = (props.ruleData.flags ?? defaultRule.flags ?? 'gi').includes('i')
|
||||
} else {
|
||||
patternSource.value = ''
|
||||
ignoreCase.value = true
|
||||
console.warn('规则数据中的pattern格式不正确:', props.ruleData.pattern)
|
||||
}
|
||||
|
||||
// 初始化 fullLine 状态
|
||||
fullLine.value = props.ruleData.fullLine ?? defaultRule.fullLine
|
||||
|
||||
// 初始化样式复选框状态
|
||||
const styles = []
|
||||
if (formData.value.bold) styles.push('bold')
|
||||
if (formData.value.italic) styles.push('italic')
|
||||
if (formData.value.underline) styles.push('underline')
|
||||
textStyles.value = styles
|
||||
|
||||
// 保存原始数据用于变化检测
|
||||
originalData.value = {
|
||||
title: formData.value.title,
|
||||
displayColor: formData.value.displayColor,
|
||||
backgroundColor: formData.value.backgroundColor,
|
||||
bold: formData.value.bold,
|
||||
italic: formData.value.italic,
|
||||
underline: formData.value.underline,
|
||||
enabled: formData.value.enabled,
|
||||
patternSource: patternSource.value,
|
||||
ignoreCase: ignoreCase.value,
|
||||
fullLine: fullLine.value
|
||||
}
|
||||
|
||||
// 默认的规则测试文本
|
||||
if (!testInput.value) {
|
||||
testInput.value = `🔴 ERRORS: fatal error! system crashed, expires certificate, virus attack, unauthorized blocked
|
||||
🟡 WARNINGS: deprecated warning! experimental feature, vulnerable system, maintenance mode
|
||||
🟢 SUCCESS: login successful established! task completed, system healthy, verified passed
|
||||
🔵 INFO: loading configuration, debugging trace, sleeping zombie processes, stopped
|
||||
🌐 NETWORK: 192.168.1.100:8080, 2001:db8::1, aa:bb:cc:dd:ee:ff
|
||||
🔗 URLS: https://api.example.com/v1, mailto:admin@domain.com, /usr/local/bin
|
||||
📅 DATETIME: 2024-01-15T10:30:45Z, 3:45 PM, 01/15/2024
|
||||
💾 UNITS: CPU 3.2GHz 68°C, RAM 16GB 85%, Network 1Gbps, Process 2 hours 30 minutes`
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.rule-edit {
|
||||
.rule-options {
|
||||
display: flex;
|
||||
gap: 30px;
|
||||
align-items: center;
|
||||
|
||||
.option-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
|
||||
label {
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-regular);
|
||||
min-width: 80px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.color-row {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
align-items: center;
|
||||
|
||||
.color-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
label {
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-regular);
|
||||
min-width: 60px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.color-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.color-selector {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.test-result {
|
||||
.matches {
|
||||
.match-tag {
|
||||
margin-right: 8px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.preview-result {
|
||||
padding: 12px;
|
||||
background-color: #1e1e1e;
|
||||
color: #ffffff;
|
||||
border-radius: 4px;
|
||||
font-family: Cascadia Code, Menlo, monospace;
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
min-height: 60px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.templates {
|
||||
.el-button {
|
||||
margin-right: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
@@ -134,6 +134,14 @@
|
||||
<View v-if="showHidden" />
|
||||
<Hide v-else />
|
||||
</el-icon>
|
||||
<el-icon
|
||||
ref="columnsConfigPopoverRef"
|
||||
class="action_icon"
|
||||
title="列配置"
|
||||
@click="showColumnsConfig = !showColumnsConfig"
|
||||
>
|
||||
<Setting />
|
||||
</el-icon>
|
||||
<el-icon
|
||||
v-if="hasDownloadTasks"
|
||||
class="action_icon download_icon"
|
||||
@@ -170,11 +178,27 @@
|
||||
<el-table-column
|
||||
label="名称"
|
||||
width="auto"
|
||||
max-width="200"
|
||||
min-width="120"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<div class="file_name_cell">
|
||||
<el-tooltip
|
||||
placement="right"
|
||||
effect="dark"
|
||||
:show-after="500"
|
||||
>
|
||||
<template #content>
|
||||
<div style="font-size: 12px; line-height: 1.6;">
|
||||
<div><strong>名称:</strong> {{ row.name }}</div>
|
||||
<div><strong>类型:</strong> {{ row.type === 'd' ? '文件夹' : row.type === 'l' ? '链接' : '文件' }}</div>
|
||||
<div v-if="row.size"><strong>大小:</strong> {{ sizeFormatter(row, null, row.size) }}</div>
|
||||
<div v-if="row.modifyTime"><strong>修改时间:</strong> {{ timeFormatter(row, null, row.modifyTime) }}</div>
|
||||
<div v-if="row.permissions"><strong>权限:</strong> {{ row.permissions }}</div>
|
||||
<div v-if="row.ownerName"><strong>所有者:</strong> {{ row.ownerName }}</div>
|
||||
<div v-if="row.groupName"><strong>组:</strong> {{ row.groupName }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="file_name_cell">
|
||||
<img :src="getIcon(row.type)" class="file_icon">
|
||||
<template v-if="isEditing(row)">
|
||||
<el-input
|
||||
@@ -201,20 +225,35 @@
|
||||
</el-icon>
|
||||
</template>
|
||||
</div>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="columnsConfig.size"
|
||||
prop="size"
|
||||
label="大小"
|
||||
:formatter="sizeFormatter"
|
||||
width="70"
|
||||
width="55"
|
||||
/>
|
||||
<el-table-column
|
||||
v-if="columnsConfig.modifyTime"
|
||||
prop="modifyTime"
|
||||
label="修改时间"
|
||||
width="80"
|
||||
:formatter="timeFormatter"
|
||||
/>
|
||||
<el-table-column
|
||||
v-if="columnsConfig.permissions"
|
||||
prop="permissions"
|
||||
label="权限"
|
||||
width="80"
|
||||
/>
|
||||
<el-table-column
|
||||
v-if="columnsConfig.owner"
|
||||
prop="ownerName"
|
||||
label="所有者"
|
||||
width="70"
|
||||
/>
|
||||
<!-- 权限列已隐藏,根据需求可再启用 -->
|
||||
</el-table>
|
||||
|
||||
@@ -286,6 +325,29 @@
|
||||
</template>
|
||||
</el-popover>
|
||||
|
||||
<!-- 列配置 Popover -->
|
||||
<el-popover
|
||||
v-model:visible="showColumnsConfig"
|
||||
:virtual-ref="columnsConfigPopoverRef"
|
||||
width="200"
|
||||
trigger="manual"
|
||||
placement="bottom-end"
|
||||
>
|
||||
<template #default>
|
||||
<div style="padding: 8px 0;">
|
||||
<div style="font-weight: 500; margin-bottom: 8px; padding: 0 12px;">显示列</div>
|
||||
<el-checkbox-group v-model="selectedColumns" style="display: flex; flex-direction: column; gap: 8px; padding: 0 12px;">
|
||||
<el-checkbox
|
||||
v-for="col in availableColumns"
|
||||
:key="col.key"
|
||||
:value="col.key"
|
||||
:label="col.label"
|
||||
/>
|
||||
</el-checkbox-group>
|
||||
</div>
|
||||
</template>
|
||||
</el-popover>
|
||||
|
||||
<!-- 隐藏的文件选择器 -->
|
||||
<input
|
||||
ref="uploadInputRef"
|
||||
@@ -589,7 +651,7 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted, watch, getCurrentInstance, nextTick } from 'vue'
|
||||
import { ArrowDown, ArrowLeft, Refresh, View, Hide, Edit, Search, ArrowRight, HomeFilled, Check, Close as CloseIcon, Download, Upload, DocumentCopy, Loading, WarningFilled, Star, StarFilled, Delete, SwitchButton } from '@element-plus/icons-vue'
|
||||
import { ArrowDown, ArrowLeft, Refresh, View, Hide, Edit, Search, ArrowRight, HomeFilled, Check, Close as CloseIcon, Download, Upload, DocumentCopy, Loading, WarningFilled, Star, StarFilled, Delete, SwitchButton, Setting } from '@element-plus/icons-vue'
|
||||
import { generateSocketInstance } from '@/utils'
|
||||
import dirIcon from '@/assets/image/system/dir.png'
|
||||
import linkIcon from '@/assets/image/system/link.png'
|
||||
@@ -619,6 +681,30 @@ const props = defineProps({
|
||||
// 组件实例上下文
|
||||
const { proxy: { $message, $messageBox } } = getCurrentInstance()
|
||||
|
||||
// 列配置
|
||||
const COLUMNS_CONFIG_KEY = 'easynode_sftp_columns_config'
|
||||
const availableColumns = [
|
||||
{ key: 'size', label: '大小', width: 70, defaultShow: true },
|
||||
{ key: 'modifyTime', label: '修改时间', width: 80, defaultShow: true },
|
||||
{ key: 'permissions', label: '权限', width: 80, defaultShow: false },
|
||||
{ key: 'owner', label: '所有者', width: 70, defaultShow: false },
|
||||
]
|
||||
|
||||
const defaultColumnsConfig = availableColumns.reduce((acc, col) => {
|
||||
acc[col.key] = col.defaultShow
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
const columnsConfig = ref(
|
||||
JSON.parse(localStorage.getItem(COLUMNS_CONFIG_KEY) || JSON.stringify(defaultColumnsConfig))
|
||||
)
|
||||
|
||||
watch(columnsConfig, (val) => {
|
||||
localStorage.setItem(COLUMNS_CONFIG_KEY, JSON.stringify(val))
|
||||
}, { deep: true })
|
||||
|
||||
const showColumnsConfig = ref(false)
|
||||
|
||||
// 路径 & 隐藏文件显示
|
||||
const currentPath = ref('/')
|
||||
const SHOW_HIDDEN_KEY = 'easynode_show_hidden_files'
|
||||
@@ -1474,6 +1560,17 @@ const search = () => {
|
||||
}, 100)
|
||||
}
|
||||
|
||||
// 列配置相关
|
||||
const columnsConfigPopoverRef = ref(null)
|
||||
const selectedColumns = computed({
|
||||
get: () => Object.keys(columnsConfig.value).filter(key => columnsConfig.value[key]),
|
||||
set: (newValue) => {
|
||||
availableColumns.forEach(col => {
|
||||
columnsConfig.value[col.key] = newValue.includes(col.key)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const confirmPathInput = () => {
|
||||
if (!pathInput.value) return
|
||||
isEditingPath.value = false
|
||||
|
||||
@@ -0,0 +1,757 @@
|
||||
<template>
|
||||
<el-drawer
|
||||
v-model="dialogVisible"
|
||||
title="高亮设置"
|
||||
:direction="isMobileScreen ? 'ttb' : 'ltr'"
|
||||
:close-on-click-modal="true"
|
||||
:close-on-press-escape="true"
|
||||
:modal="true"
|
||||
modal-class="local_setting_drawer"
|
||||
:size="isMobileScreen ? '80%' : '35%'"
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
label-suffix=":"
|
||||
label-width="130px"
|
||||
:show-message="false"
|
||||
>
|
||||
<el-form-item label="关键词高亮">
|
||||
<el-switch
|
||||
v-model="highlightConfig.enabled"
|
||||
class="switch"
|
||||
inline-prompt
|
||||
style="--el-switch-on-color: #13ce66; --el-switch-off-color: #ff4949"
|
||||
active-text="开启"
|
||||
inactive-text="关闭"
|
||||
@change="handleConfigChange"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="highlightConfig.enabled" label="调试模式">
|
||||
<el-switch
|
||||
v-model="highlightConfig.debugMode"
|
||||
class="switch"
|
||||
inline-prompt
|
||||
style="--el-switch-on-color: #13ce66; --el-switch-off-color: #ff4949"
|
||||
active-text="开启"
|
||||
inactive-text="关闭"
|
||||
@change="handleConfigChange"
|
||||
/>
|
||||
<el-text class="debug-tip" size="small" type="info">
|
||||
开启后会在控制台显示详细的高亮处理信息
|
||||
</el-text>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 规则管理按钮 -->
|
||||
<el-form-item v-if="highlightConfig.enabled" label="规则管理">
|
||||
<div class="rule-actions">
|
||||
<el-button type="warning" size="small" @click="handleRestoreDefaults">
|
||||
恢复默认
|
||||
</el-button>
|
||||
<el-button type="primary" size="small" @click="handleExportRules">
|
||||
导出规则
|
||||
</el-button>
|
||||
<el-button type="success" size="small" @click="handleImportRules">
|
||||
导入规则
|
||||
</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 高亮规则列表 -->
|
||||
<div v-if="highlightConfig.enabled" class="rules-section">
|
||||
<div
|
||||
v-for="(rule, ruleName) in highlightRules"
|
||||
:key="ruleName"
|
||||
class="rule-item"
|
||||
>
|
||||
<el-form-item :label="getRuleTitle(ruleName)">
|
||||
<div class="rule-content">
|
||||
<div class="rule-header">
|
||||
<!-- 启用切换开关 -->
|
||||
<div class="rule-enable">
|
||||
<el-switch
|
||||
v-model="rule.enabled"
|
||||
size="small"
|
||||
inline-prompt
|
||||
style="--el-switch-on-color: #13ce66; --el-switch-off-color: #ff4949"
|
||||
active-text="启用"
|
||||
inactive-text="禁用"
|
||||
@change="handleRuleEnabledChange(ruleName, rule.enabled)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="color-info">
|
||||
<!-- 文本颜色预览 -->
|
||||
<div class="color-display">
|
||||
<span class="color-label">文本</span>
|
||||
<span
|
||||
class="color-preview"
|
||||
:style="{ backgroundColor: getColorDisplay(rule) }"
|
||||
:title="`文本颜色: ${getColorDisplay(rule)}`"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 背景色预览 -->
|
||||
<div v-if="rule.backgroundColor" class="color-display">
|
||||
<span class="color-label">背景</span>
|
||||
<span
|
||||
class="background-preview"
|
||||
:style="{ backgroundColor: rule.backgroundColor }"
|
||||
:title="`背景色: ${rule.backgroundColor}`"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 样式标签 -->
|
||||
<div class="style-tags">
|
||||
<el-tag v-if="rule.bold" size="small" type="danger" effect="dark">粗体</el-tag>
|
||||
<el-tag v-if="rule.italic" size="small" type="warning" effect="dark">斜体</el-tag>
|
||||
<el-tag v-if="rule.underline" size="small" type="info" effect="dark">下划线</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rule-buttons">
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
link
|
||||
:disabled="!rule.enabled"
|
||||
@click="editRule(ruleName)"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 预览效果 -->
|
||||
<el-form-item v-if="highlightConfig.enabled" label="预览效果">
|
||||
<div class="test-panel">
|
||||
<el-input
|
||||
v-model="testText"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
placeholder="输入测试文本,查看高亮效果"
|
||||
class="test-input"
|
||||
/>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 预览输出 -->
|
||||
<div
|
||||
v-if="highlightConfig.enabled"
|
||||
class="test-output"
|
||||
:style="testOutputStyle"
|
||||
v-html="highlightedTestText"
|
||||
/>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog_footer">
|
||||
<el-button @click="handleCancel">关闭</el-button>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<!-- 规则编辑对话框 -->
|
||||
<RuleEditDialog
|
||||
v-model:show="showRuleEdit"
|
||||
:rule-name="editingRuleName"
|
||||
:rule-data="editingRule"
|
||||
:all-rules="highlightRules"
|
||||
@save="handleRuleSave"
|
||||
/>
|
||||
|
||||
<!-- 导入对话框 -->
|
||||
<el-dialog
|
||||
v-model="showImportDialog"
|
||||
title="导入高亮规则"
|
||||
width="600px"
|
||||
>
|
||||
<el-input
|
||||
v-model="importRulesText"
|
||||
type="textarea"
|
||||
:rows="10"
|
||||
placeholder="请粘贴规则JSON内容"
|
||||
/>
|
||||
<template #footer>
|
||||
<el-button @click="showImportDialog = false">取消</el-button>
|
||||
<el-button type="primary" @click="confirmImportRules">确认导入</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch, getCurrentInstance } from 'vue'
|
||||
import { HIGHLIGHT_RULES } from '@/utils/highlighter'
|
||||
import useMobileWidth from '@/composables/useMobileWidth'
|
||||
import RuleEditDialog from './rule-edit-dialog.vue'
|
||||
import themeList from 'xterm-theme'
|
||||
|
||||
const { proxy: { $store, $message, $messageBox } } = getCurrentInstance()
|
||||
const { isMobileScreen } = useMobileWidth()
|
||||
|
||||
const props = defineProps({
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:show',])
|
||||
|
||||
const dialogVisible = computed({
|
||||
get: () => props.show,
|
||||
set: (val) => emit('update:show', val)
|
||||
})
|
||||
|
||||
// 高亮配置
|
||||
const highlightConfig = ref({
|
||||
enabled: true,
|
||||
debugMode: false
|
||||
})
|
||||
|
||||
// 高亮规则
|
||||
const highlightRules = ref({ ...HIGHLIGHT_RULES })
|
||||
|
||||
// 预览测试
|
||||
const testText = ref('ERRORS: fatal error! system crashed!\nWARNINGS: deprecated warning! Retrying!\nSUCCESS: login successful established\nINFO: loading configuration, sleeping processes\nNETWORK: 192.168.1.100:8080,2001:db8::1\nURLS: https://api.example.com/v1, git@github.com\nDATETIME: 2024-01-15T10:30:45Z, 3:45 PM\nUNITS: CPU 3.2GHz, RAM 16GB, Network 1Gbps')
|
||||
const showRuleEdit = ref(false)
|
||||
const editingRuleName = ref('')
|
||||
const editingRule = ref(null)
|
||||
const showImportDialog = ref(false)
|
||||
const importRulesText = ref('')
|
||||
|
||||
// 将RegExp转换为可JSON化的对象
|
||||
const serializeRules = (rules) => {
|
||||
const serialized = {}
|
||||
for (const [key, rule] of Object.entries(rules)) {
|
||||
serialized[key] = {
|
||||
...rule,
|
||||
pattern: rule.pattern instanceof RegExp ? {
|
||||
source: rule.pattern.source,
|
||||
flags: rule.flags
|
||||
} : rule.pattern
|
||||
}
|
||||
}
|
||||
return serialized
|
||||
}
|
||||
|
||||
// 创建默认规则
|
||||
const createDefaultRules = () => {
|
||||
const defaults = {}
|
||||
for (const [key, rule] of Object.entries(HIGHLIGHT_RULES)) {
|
||||
defaults[key] = {
|
||||
title: rule.title,
|
||||
pattern: rule.pattern,
|
||||
flags: rule.flags,
|
||||
displayColor: rule.displayColor,
|
||||
backgroundColor: rule.backgroundColor,
|
||||
bold: rule.bold,
|
||||
italic: rule.italic,
|
||||
underline: rule.underline,
|
||||
fullLine: rule.fullLine,
|
||||
enabled: rule.enabled !== false
|
||||
}
|
||||
}
|
||||
return defaults
|
||||
}
|
||||
|
||||
const getRuleTitle = (ruleName) => {
|
||||
const rule = highlightRules.value[ruleName]
|
||||
return rule.title || ruleName
|
||||
}
|
||||
|
||||
const getColorDisplay = (rule) => {
|
||||
return rule.displayColor
|
||||
}
|
||||
|
||||
// 获取用户终端配置
|
||||
const userFontFamily = computed(() => {
|
||||
return $store.terminalConfig?.fontFamily || 'monospace'
|
||||
})
|
||||
|
||||
const userBackground = computed(() => {
|
||||
return $store.terminalConfig?.background || ''
|
||||
})
|
||||
|
||||
const userTheme = computed(() => {
|
||||
const themeName = $store.terminalConfig?.themeName || 'Afterglow'
|
||||
return themeList[themeName] || themeList['Afterglow']
|
||||
})
|
||||
|
||||
const userFontColor = computed(() => {
|
||||
return $store.terminalConfig?.fontColor || ''
|
||||
})
|
||||
|
||||
// 预览输出样式
|
||||
const testOutputStyle = computed(() => {
|
||||
const style = {
|
||||
fontFamily: userFontFamily.value,
|
||||
fontSize: '14px'
|
||||
}
|
||||
|
||||
// 如果有自定义背景
|
||||
if (userBackground.value) {
|
||||
// 判断背景类型
|
||||
if (userBackground.value.startsWith('http')) {
|
||||
// 图片URL:透明背景 + 背景图
|
||||
style.backgroundColor = 'transparent'
|
||||
style.backgroundImage = `url(${userBackground.value})`
|
||||
style.backgroundSize = 'cover'
|
||||
style.backgroundPosition = 'center'
|
||||
style.backgroundRepeat = 'no-repeat'
|
||||
} else {
|
||||
// 渐变色或纯色:直接使用该值作为背景
|
||||
style.backgroundColor = 'transparent'
|
||||
style.backgroundImage = userBackground.value
|
||||
style.backgroundSize = '100% 100%'
|
||||
style.backgroundRepeat = 'no-repeat'
|
||||
}
|
||||
} else {
|
||||
// 使用主题背景色
|
||||
style.backgroundColor = userTheme.value.background || '#1e1e1e'
|
||||
}
|
||||
|
||||
// 如果有自定义字体颜色
|
||||
if (userFontColor.value) {
|
||||
style.color = userFontColor.value
|
||||
} else {
|
||||
// 使用主题字体颜色
|
||||
style.color = userTheme.value.foreground || '#ffffff'
|
||||
}
|
||||
|
||||
return style
|
||||
})
|
||||
|
||||
// 高亮预览效果中的测试文本,专门兼容html的标签问题
|
||||
const highlightedTestText = computed(() => {
|
||||
if (!testText.value || !highlightConfig.value.enabled) {
|
||||
return testText.value
|
||||
}
|
||||
|
||||
try {
|
||||
const source = testText.value + '' // 强制创建新字符串
|
||||
let output = source
|
||||
|
||||
// 处理所有规则
|
||||
const activeRules = Object.entries(highlightRules.value)
|
||||
.filter(([, rule,]) => rule.enabled && rule.pattern)
|
||||
|
||||
for (const [ruleName, rule,] of activeRules) {
|
||||
// 创建新的正则表达式实例,避免全局状态问题
|
||||
const regex = new RegExp(rule.pattern.source, rule.flags)
|
||||
|
||||
// 先找到所有匹配位置,检查是否在HTML标签内
|
||||
const matches = []
|
||||
let match
|
||||
regex.lastIndex = 0
|
||||
while ((match = regex.exec(output)) !== null) {
|
||||
matches.push({
|
||||
index: match.index,
|
||||
text: match[0]
|
||||
})
|
||||
}
|
||||
|
||||
// 从后往前替换,避免索引偏移问题
|
||||
for (let i = matches.length - 1; i >= 0; i--) {
|
||||
const { index, text } = matches[i]
|
||||
|
||||
// 检查匹配位置是否在HTML标签内部
|
||||
const before = output.substring(0, index)
|
||||
const lastOpenTag = before.lastIndexOf('<')
|
||||
const lastCloseTag = before.lastIndexOf('>')
|
||||
|
||||
// 如果最近的 < 比 > 更近,说明在标签内部,跳过
|
||||
if (lastOpenTag > lastCloseTag) {
|
||||
continue
|
||||
}
|
||||
|
||||
// 构建样式字符串
|
||||
let styles = ''
|
||||
|
||||
// 字体
|
||||
styles += 'font-family:' + userFontFamily.value + ' !important;'
|
||||
styles += 'font-size:14px !important;'
|
||||
styles += 'font-weight:' + (rule.bold ? 'bold' : 'normal') + ' !important;'
|
||||
styles += 'font-style:' + (rule.italic ? 'italic' : 'normal') + ' !important;'
|
||||
|
||||
// 颜色
|
||||
styles += 'color:' + rule.displayColor + ' !important;'
|
||||
|
||||
// 添加背景色
|
||||
if (rule.backgroundColor) {
|
||||
styles += 'background-color:' + rule.backgroundColor + ' !important;'
|
||||
}
|
||||
|
||||
if (rule.underline) {
|
||||
styles += 'text-decoration:underline !important;'
|
||||
}
|
||||
|
||||
// 添加padding,效果更明显
|
||||
if (rule.backgroundColor) {
|
||||
styles += 'padding:2px 4px !important;border-radius:3px !important;'
|
||||
}
|
||||
|
||||
const replacement = '<span style="' + styles + '">' + text + '</span>'
|
||||
|
||||
// 替换
|
||||
output = output.substring(0, index) + replacement + output.substring(index + text.length)
|
||||
}
|
||||
}
|
||||
|
||||
const finalOutput = output.replace(/\n/g, '<br>')
|
||||
|
||||
return finalOutput
|
||||
} catch (error) {
|
||||
console.error('预览错误:', error)
|
||||
return testText.value.replace(/\n/g, '<br>')
|
||||
}
|
||||
})
|
||||
|
||||
// 加载当前配置
|
||||
const loadConfig = () => {
|
||||
const terminalConfig = $store.terminalConfig
|
||||
highlightConfig.value = {
|
||||
enabled: terminalConfig.keywordHighlight !== false,
|
||||
debugMode: terminalConfig.highlightDebugMode || false
|
||||
}
|
||||
|
||||
// 如果有自定义规则,尝试使用自定义规则;如果无有效规则则使用默认规则
|
||||
let useDefaultRules = true
|
||||
|
||||
if (terminalConfig.customHighlightRules) {
|
||||
// 标准化自定义规则,确保pattern是RegExp对象
|
||||
const customRules = {}
|
||||
for (const [key, rule,] of Object.entries(terminalConfig.customHighlightRules)) {
|
||||
// 跳过无效的规则
|
||||
if (!rule.pattern || (typeof rule.pattern === 'object' && !rule.pattern.source)) {
|
||||
console.warn(`跳过无效的自定义规则 ${ key }:`, rule.pattern)
|
||||
continue
|
||||
}
|
||||
|
||||
let validPattern
|
||||
if (rule.pattern instanceof RegExp) {
|
||||
validPattern = rule.pattern
|
||||
} else if (rule.pattern.source) {
|
||||
validPattern = new RegExp(rule.pattern.source, rule.flags || 'gi')
|
||||
} else {
|
||||
console.warn(`跳过无法解析的规则 ${ key }:`, rule.pattern)
|
||||
continue
|
||||
}
|
||||
|
||||
customRules[key] = {
|
||||
...rule,
|
||||
title: rule.title || '',
|
||||
pattern: validPattern,
|
||||
flags: rule.flags || 'gi',
|
||||
backgroundColor: rule.backgroundColor || null,
|
||||
bold: rule.bold || false,
|
||||
italic: rule.italic || false,
|
||||
underline: rule.underline || false
|
||||
}
|
||||
}
|
||||
|
||||
// 如果有有效的自定义规则,使用它们,否则使用默认规则
|
||||
if (Object.keys(customRules).length > 0) {
|
||||
highlightRules.value = customRules
|
||||
useDefaultRules = false
|
||||
}
|
||||
}
|
||||
|
||||
if (useDefaultRules) {
|
||||
highlightRules.value = createDefaultRules()
|
||||
}
|
||||
}
|
||||
|
||||
// 编辑规则
|
||||
const editRule = (ruleName) => {
|
||||
editingRuleName.value = ruleName
|
||||
editingRule.value = { ...highlightRules.value[ruleName] }
|
||||
showRuleEdit.value = true
|
||||
}
|
||||
|
||||
// 处理规则启用状态变化
|
||||
const handleRuleEnabledChange = async (ruleName, enabled) => {
|
||||
try {
|
||||
highlightRules.value[ruleName].enabled = enabled
|
||||
|
||||
// 保存到服务器
|
||||
const config = {
|
||||
customHighlightRules: serializeRules(highlightRules.value)
|
||||
}
|
||||
|
||||
await $store.setTerminalSetting(config)
|
||||
$message.success(`${ getRuleTitle(ruleName) }${ enabled ? '已启用' : '已禁用' }`)
|
||||
} catch (error) {
|
||||
$message.error('保存设置失败')
|
||||
// 回滚状态
|
||||
highlightRules.value[ruleName].enabled = !enabled
|
||||
}
|
||||
}
|
||||
|
||||
// 保存规则编辑
|
||||
const handleRuleSave = async (ruleName, ruleData) => {
|
||||
try {
|
||||
highlightRules.value[ruleName] = { ...ruleData }
|
||||
showRuleEdit.value = false
|
||||
|
||||
// 立即保存自定义规则到服务器
|
||||
const config = {
|
||||
customHighlightRules: serializeRules(highlightRules.value)
|
||||
}
|
||||
|
||||
await $store.setTerminalSetting(config)
|
||||
$message.success('规则保存成功')
|
||||
} catch (error) {
|
||||
$message.error('保存规则失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 恢复默认规则
|
||||
const handleRestoreDefaults = async () => {
|
||||
try {
|
||||
await $messageBox.confirm('确认恢复为默认高亮规则?此操作将覆盖所有自定义规则和颜色。', '确认', {
|
||||
type: 'warning'
|
||||
})
|
||||
|
||||
// 拷贝默认规则,确保包含所有新属性
|
||||
highlightRules.value = createDefaultRules()
|
||||
|
||||
// 立即保存到服务器,清除自定义规则
|
||||
const config = {
|
||||
customHighlightRules: null
|
||||
}
|
||||
|
||||
await $store.setTerminalSetting(config)
|
||||
$message.success('已恢复默认规则和颜色')
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
$message.error('恢复默认规则失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 导出规则
|
||||
const handleExportRules = () => {
|
||||
const rulesData = {
|
||||
highlightConfig: highlightConfig.value,
|
||||
highlightRules: serializeRules(highlightRules.value),
|
||||
exportTime: new Date().toISOString()
|
||||
}
|
||||
|
||||
const blob = new Blob([JSON.stringify(rulesData, null, 2),], { type: 'application/json' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = `terminal-highlight-rules-${ new Date().toISOString().slice(0, 10) }.json`
|
||||
a.click()
|
||||
URL.revokeObjectURL(url)
|
||||
|
||||
$message.success('规则导出成功')
|
||||
}
|
||||
|
||||
// 导入规则
|
||||
const handleImportRules = () => {
|
||||
showImportDialog.value = true
|
||||
importRulesText.value = ''
|
||||
}
|
||||
|
||||
const confirmImportRules = async () => {
|
||||
try {
|
||||
const importData = JSON.parse(importRulesText.value)
|
||||
|
||||
if (importData.highlightConfig) {
|
||||
highlightConfig.value = { ...importData.highlightConfig }
|
||||
}
|
||||
|
||||
if (importData.highlightRules) {
|
||||
// 转换导入的规则,将序列化的pattern重新转换为RegExp对象
|
||||
const convertedRules = {}
|
||||
for (const [key, rule,] of Object.entries(importData.highlightRules)) {
|
||||
convertedRules[key] = {
|
||||
...rule,
|
||||
// 将序列化的pattern转换回RegExp对象
|
||||
pattern: rule.pattern && rule.pattern.source
|
||||
? new RegExp(rule.pattern.source, rule.flags || 'gi')
|
||||
: rule.pattern
|
||||
}
|
||||
}
|
||||
highlightRules.value = convertedRules
|
||||
}
|
||||
|
||||
// 保存到服务器时序列化RegExp
|
||||
const config = {
|
||||
keywordHighlight: highlightConfig.value.enabled,
|
||||
highlightDebugMode: highlightConfig.value.debugMode,
|
||||
customHighlightRules: serializeRules(highlightRules.value)
|
||||
}
|
||||
|
||||
await $store.setTerminalSetting(config)
|
||||
showImportDialog.value = false
|
||||
$message.success('规则导入并保存成功')
|
||||
} catch (error) {
|
||||
$message.error('导入失败,请检查JSON格式')
|
||||
}
|
||||
}
|
||||
|
||||
// 实时保存配置
|
||||
const handleConfigChange = async () => {
|
||||
try {
|
||||
// 立即更新到store
|
||||
$store.terminalConfig.keywordHighlight = highlightConfig.value.enabled
|
||||
$store.terminalConfig.highlightDebugMode = highlightConfig.value.debugMode
|
||||
|
||||
// 保存到服务器
|
||||
const config = {
|
||||
keywordHighlight: highlightConfig.value.enabled,
|
||||
highlightDebugMode: highlightConfig.value.debugMode
|
||||
}
|
||||
|
||||
await $store.setTerminalSetting(config)
|
||||
} catch (error) {
|
||||
$message.error('保存配置失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭对话框
|
||||
const handleCancel = () => {
|
||||
dialogVisible.value = false
|
||||
}
|
||||
|
||||
// 监听对话框显示
|
||||
watch(() => props.show, (show) => {
|
||||
if (show) {
|
||||
loadConfig()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.debug-tip {
|
||||
margin-left: 12px;
|
||||
display: block;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.rule-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
|
||||
.el-button {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.rules-section {
|
||||
margin-top: 20px;
|
||||
|
||||
.rule-item {
|
||||
margin-bottom: 16px;
|
||||
|
||||
.rule-content {
|
||||
.rule-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
gap: 12px;
|
||||
|
||||
.rule-enable {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.color-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.color-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
.color-label {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-regular);
|
||||
min-width: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.color-preview {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 2px;
|
||||
display: inline-block;
|
||||
border: 1px solid var(--el-border-color);
|
||||
}
|
||||
|
||||
.background-preview {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 2px;
|
||||
display: inline-block;
|
||||
border: 1px solid var(--el-border-color);
|
||||
}
|
||||
|
||||
.style-tags {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.rule-buttons {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.test-panel {
|
||||
width: calc(100% + 130px);
|
||||
margin-left: -130px;
|
||||
padding-left: 130px;
|
||||
|
||||
.test-input {
|
||||
margin-bottom: 12px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.el-textarea__inner) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.test-output {
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
min-height: 120px;
|
||||
line-height: 1.5;
|
||||
width: 100%;
|
||||
overflow-wrap: break-word;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.dialog_footer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
.local_setting_drawer {
|
||||
.el-drawer__header {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,149 +1,113 @@
|
||||
<template>
|
||||
<el-drawer
|
||||
v-model="visible"
|
||||
title="本地设置"
|
||||
title="基础设置"
|
||||
:direction="isMobileScreen ? 'ttb' : 'ltr'"
|
||||
:close-on-click-modal="true"
|
||||
:close-on-press-escape="true"
|
||||
:modal="true"
|
||||
modal-class="local_setting_drawer"
|
||||
modal-class="terminal_setting_drawer"
|
||||
:size="isMobileScreen ? '80%' : '30%'"
|
||||
>
|
||||
<el-tabs tab-position="top">
|
||||
<el-tab-pane label="终端设置" lazy>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
label-suffix=":"
|
||||
label-width="100px"
|
||||
:show-message="false"
|
||||
>
|
||||
<el-form-item label="终端主题" prop="theme">
|
||||
<el-select v-model="theme" placeholder="" style="width: 100%;">
|
||||
<el-option
|
||||
v-for="(value, key) in themeList"
|
||||
:key="key"
|
||||
:label="key"
|
||||
:value="key"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="终端字体" prop="fontSize">
|
||||
<el-input-number v-model="fontSize" :min="6" :max="30" />
|
||||
</el-form-item>
|
||||
<el-form-item label="终端背景" prop="backgroundImage">
|
||||
<ul class="background_list">
|
||||
<li :class="background ? '' : 'active'" @click="changeBackground('')">
|
||||
<el-image class="image">
|
||||
<template #error>
|
||||
<div class="theme_background_text">
|
||||
主题背景
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
</li>
|
||||
<li
|
||||
v-for="item in defaultBackgroundImages"
|
||||
:key="item"
|
||||
:class="background === item ? 'active' : ''"
|
||||
:style="`background: ${item};`"
|
||||
@click="changeBackground(item)"
|
||||
/>
|
||||
</ul>
|
||||
<div class="custom_background">
|
||||
<el-input
|
||||
v-model="background"
|
||||
clearable
|
||||
placeholder="自定义背景图片url"
|
||||
autocomplete="on"
|
||||
/>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="快捷操作">
|
||||
<el-form
|
||||
ref="formRef"
|
||||
label-suffix=":"
|
||||
label-width="100px"
|
||||
:show-message="false"
|
||||
>
|
||||
<el-form-item label="自动重连" prop="autoReconnect">
|
||||
<PlusSupportTip>
|
||||
<span>
|
||||
<el-switch
|
||||
v-model="autoReconnect"
|
||||
class="swtich"
|
||||
inline-prompt
|
||||
:disabled="!isPlusActive"
|
||||
style="--el-switch-on-color: #13ce66; --el-switch-off-color: #ff4949"
|
||||
active-text="开启"
|
||||
inactive-text="关闭"
|
||||
/>
|
||||
<span class="plus_support_tip_text">(Plus专属功能)</span>
|
||||
</span>
|
||||
</PlusSupportTip>
|
||||
</el-form-item>
|
||||
<el-form-item label="脚本执行" prop="autoExecuteScript">
|
||||
<el-tooltip
|
||||
effect="dark"
|
||||
content="启用后从脚本库选中脚本后自动执行(回车操作)"
|
||||
placement="right"
|
||||
>
|
||||
<el-switch
|
||||
v-model="autoExecuteScript"
|
||||
class="swtich"
|
||||
inline-prompt
|
||||
style="--el-switch-on-color: #13ce66; --el-switch-off-color: #ff4949"
|
||||
active-text="自动"
|
||||
inactive-text="手动"
|
||||
/>
|
||||
</el-tooltip>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="菜单选项">
|
||||
<el-form
|
||||
ref="formRef"
|
||||
label-suffix=":"
|
||||
label-width="100px"
|
||||
:show-message="false"
|
||||
>
|
||||
<el-form-item label="实例" prop="instanceGroup">
|
||||
<el-radio-group
|
||||
v-model="hostGroupCascader"
|
||||
size="small"
|
||||
text-color="#fff"
|
||||
fill="#13ce66"
|
||||
>
|
||||
<el-radio-button :value="true">分组级联展示</el-radio-button>
|
||||
<el-radio-button :value="false">不分组单列展示</el-radio-button>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="脚本库" prop="scriptLibrary">
|
||||
<el-radio-group
|
||||
v-model="scriptLibraryCascader"
|
||||
size="small"
|
||||
text-color="#fff"
|
||||
:fill="scriptLibrary ? '#13ce66' : '#999'"
|
||||
:disabled="!scriptLibrary"
|
||||
>
|
||||
<el-radio-button :value="true">分组级联展示</el-radio-button>
|
||||
<el-radio-button :value="false">不分组单列展示</el-radio-button>
|
||||
</el-radio-group>
|
||||
<span class="script_library_switch">
|
||||
<el-switch
|
||||
v-model="scriptLibrary"
|
||||
class="swtich"
|
||||
inline-prompt
|
||||
style="--el-switch-on-color: #13ce66; --el-switch-off-color: #999"
|
||||
active-text="显示"
|
||||
inactive-text="隐藏"
|
||||
/>
|
||||
</span>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
label-suffix=":"
|
||||
label-width="100px"
|
||||
:show-message="false"
|
||||
>
|
||||
<el-form-item label="终端主题" prop="theme">
|
||||
<el-select v-model="theme" placeholder="" style="width: 100%;">
|
||||
<el-option
|
||||
v-for="(value, key) in themeList"
|
||||
:key="key"
|
||||
:label="key"
|
||||
:value="key"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="终端字体" prop="fontFamily">
|
||||
<el-select v-model="fontFamily" placeholder="选择字体" style="width: 100%;">
|
||||
<el-option
|
||||
v-for="font in commonFonts"
|
||||
:key="font.value"
|
||||
:label="font.label"
|
||||
:value="font.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="字体大小" prop="fontSize">
|
||||
<el-input-number v-model="fontSize" :min="6" :max="30" />
|
||||
</el-form-item>
|
||||
<el-form-item label="字体颜色" prop="fontColor">
|
||||
<div class="font_color_list">
|
||||
<div :class="['color_item', 'theme_color', { 'active': !fontColor }]" @click="setThemeColor">
|
||||
<span class="color_text">主题颜色</span>
|
||||
</div>
|
||||
<div :class="['color_item', 'color_picker_wrapper', { 'active': !!fontColor }]">
|
||||
<el-color-picker
|
||||
v-model="customFontColor"
|
||||
show-alpha
|
||||
:predefine="predefineColors"
|
||||
@change="handleColorChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="光标颜色" prop="cursorColor">
|
||||
<div class="font_color_list">
|
||||
<div :class="['color_item', 'theme_color', { 'active': !cursorColor }]" @click="setThemeCursorColor">
|
||||
<span class="color_text">主题颜色</span>
|
||||
</div>
|
||||
<div :class="['color_item', 'color_picker_wrapper', { 'active': !!cursorColor }]">
|
||||
<el-color-picker
|
||||
v-model="customCursorColor"
|
||||
show-alpha
|
||||
:predefine="predefineColors"
|
||||
@change="handleCursorColorChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="选中颜色" prop="selectionColor">
|
||||
<div class="font_color_list">
|
||||
<div class="color_item color_picker_wrapper active">
|
||||
<el-color-picker
|
||||
v-model="selectionColor"
|
||||
show-alpha
|
||||
:predefine="predefineColors"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="终端背景" prop="backgroundImage">
|
||||
<ul class="background_list">
|
||||
<li :class="background ? '' : 'active'" @click="changeBackground('')">
|
||||
<el-image class="image">
|
||||
<template #error>
|
||||
<div class="theme_background_text">
|
||||
主题背景
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
</li>
|
||||
<li
|
||||
v-for="item in defaultBackgroundImages"
|
||||
:key="item"
|
||||
:class="background === item ? 'active' : ''"
|
||||
:style="`background: ${item};`"
|
||||
@click="changeBackground(item)"
|
||||
/>
|
||||
</ul>
|
||||
<div class="custom_background">
|
||||
<el-input
|
||||
v-model="background"
|
||||
clearable
|
||||
placeholder="自定义背景图片url"
|
||||
autocomplete="on"
|
||||
/>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog_footer">
|
||||
<el-button @click="visible = false">关闭</el-button>
|
||||
@@ -153,11 +117,12 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, getCurrentInstance } from 'vue'
|
||||
import { computed, getCurrentInstance, ref } from 'vue'
|
||||
import themeList from 'xterm-theme'
|
||||
import useMobileWidth from '@/composables/useMobileWidth'
|
||||
import PlusSupportTip from '@/components/common/PlusSupportTip.vue'
|
||||
|
||||
const { proxy: { $store } } = getCurrentInstance()
|
||||
const { isMobileScreen } = useMobileWidth()
|
||||
|
||||
const props = defineProps({
|
||||
show: {
|
||||
@@ -165,48 +130,131 @@ const props = defineProps({
|
||||
type: Boolean
|
||||
}
|
||||
})
|
||||
const emit = defineEmits(['update:show',])
|
||||
|
||||
const { isMobileScreen } = useMobileWidth()
|
||||
const emit = defineEmits(['update:show'])
|
||||
|
||||
// 全平台通用字体列表
|
||||
const commonFonts = [
|
||||
{ label: 'monospace', value: 'monospace' },
|
||||
{ label: 'Courier New', value: 'Courier New, Courier, monospace' },
|
||||
{ label: 'Courier', value: 'Courier, monospace' },
|
||||
{ label: 'Consolas', value: 'Consolas, monospace' },
|
||||
{ label: 'Monaco', value: 'Monaco, monospace' },
|
||||
{ label: 'Menlo', value: 'Menlo, monospace' },
|
||||
{ label: 'Cascadia Code', value: 'Cascadia Code, monospace' }
|
||||
]
|
||||
|
||||
// 预定义颜色
|
||||
const predefineColors = [
|
||||
'#ffffff',
|
||||
'#00ff41',
|
||||
'#00ffff',
|
||||
'#ff00ff',
|
||||
'#ffff00',
|
||||
'#ff0000',
|
||||
'#00ff00',
|
||||
'#0000ff',
|
||||
'#000000'
|
||||
]
|
||||
|
||||
// 保存最后选择的自定义颜色
|
||||
const lastCustomColor = ref('#ffffff')
|
||||
const lastCustomCursorColor = ref('#00ff41')
|
||||
|
||||
const defaultBackgroundImages = computed(() => $store.defaultBackgroundImages)
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.show,
|
||||
set: (newVal) => emit('update:show', newVal)
|
||||
})
|
||||
|
||||
const theme = computed({
|
||||
get: () => $store.terminalConfig.themeName,
|
||||
set: (newVal) => $store.setTerminalSetting({ themeName: newVal })
|
||||
})
|
||||
|
||||
const background = computed({
|
||||
get: () => $store.terminalConfig.background,
|
||||
set: (newVal) => $store.setTerminalSetting({ background: newVal })
|
||||
})
|
||||
|
||||
const fontSize = computed({
|
||||
get: () => $store.terminalConfig.fontSize,
|
||||
set: (newVal) => $store.setTerminalSetting({ fontSize: newVal })
|
||||
})
|
||||
const autoReconnect = computed({
|
||||
get: () => $store.terminalConfig.autoReconnect,
|
||||
set: (newVal) => $store.setTerminalSetting({ autoReconnect: newVal })
|
||||
|
||||
const fontFamily = computed({
|
||||
get: () => $store.terminalConfig.fontFamily || 'monospace',
|
||||
set: (newVal) => $store.setTerminalSetting({ fontFamily: newVal })
|
||||
})
|
||||
const autoExecuteScript = computed({
|
||||
get: () => $store.terminalConfig.autoExecuteScript,
|
||||
set: (newVal) => $store.setTerminalSetting({ autoExecuteScript: newVal })
|
||||
|
||||
const fontColor = computed({
|
||||
get: () => $store.terminalConfig.fontColor,
|
||||
set: (newVal) => $store.setTerminalSetting({ fontColor: newVal })
|
||||
})
|
||||
const scriptLibrary = computed({
|
||||
get: () => $store.menuSetting.scriptLibrary,
|
||||
set: (newVal) => $store.setMenuSetting({ scriptLibrary: newVal })
|
||||
|
||||
const customFontColor = computed({
|
||||
get: () => {
|
||||
// 如果有自定义颜色,显示自定义颜色;否则显示上次选择的颜色
|
||||
if (fontColor.value) {
|
||||
lastCustomColor.value = fontColor.value
|
||||
return fontColor.value
|
||||
}
|
||||
return lastCustomColor.value
|
||||
},
|
||||
set: (newVal) => {
|
||||
lastCustomColor.value = newVal
|
||||
fontColor.value = newVal
|
||||
}
|
||||
})
|
||||
const scriptLibraryCascader = computed({
|
||||
get: () => $store.menuSetting.scriptLibraryCascader,
|
||||
set: (newVal) => $store.setMenuSetting({ scriptLibraryCascader: newVal })
|
||||
|
||||
const setThemeColor = () => {
|
||||
fontColor.value = ''
|
||||
}
|
||||
|
||||
const setCustomColor = (color) => {
|
||||
fontColor.value = color
|
||||
}
|
||||
|
||||
const handleColorChange = (color) => {
|
||||
if (color) {
|
||||
fontColor.value = color
|
||||
}
|
||||
}
|
||||
|
||||
const cursorColor = computed({
|
||||
get: () => $store.terminalConfig.cursorColor,
|
||||
set: (newVal) => $store.setTerminalSetting({ cursorColor: newVal })
|
||||
})
|
||||
const hostGroupCascader = computed({
|
||||
get: () => $store.menuSetting.hostGroupCascader,
|
||||
set: (newVal) => $store.setMenuSetting({ hostGroupCascader: newVal })
|
||||
|
||||
const customCursorColor = computed({
|
||||
get: () => {
|
||||
if (cursorColor.value) {
|
||||
lastCustomCursorColor.value = cursorColor.value
|
||||
return cursorColor.value
|
||||
}
|
||||
return lastCustomCursorColor.value
|
||||
},
|
||||
set: (newVal) => {
|
||||
lastCustomCursorColor.value = newVal
|
||||
cursorColor.value = newVal
|
||||
}
|
||||
})
|
||||
|
||||
const setThemeCursorColor = () => {
|
||||
cursorColor.value = ''
|
||||
}
|
||||
|
||||
const handleCursorColorChange = (color) => {
|
||||
if (color) {
|
||||
cursorColor.value = color
|
||||
}
|
||||
}
|
||||
|
||||
const selectionColor = computed({
|
||||
get: () => $store.terminalConfig.selectionColor,
|
||||
set: (newVal) => $store.setTerminalSetting({ selectionColor: newVal })
|
||||
})
|
||||
const isPlusActive = computed(() => $store.isPlusActive)
|
||||
|
||||
const changeBackground = (item) => {
|
||||
background.value = item || ''
|
||||
@@ -214,12 +262,77 @@ const changeBackground = (item) => {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.font_color_list {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.color_item {
|
||||
height: 30px;
|
||||
border-radius: 3px;
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
border: 1px solid var(--el-border-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 0 5px #1890ff;
|
||||
}
|
||||
|
||||
&.active {
|
||||
box-shadow: 0 0 5px #1890ff;
|
||||
border: 1px solid #1890ff;
|
||||
}
|
||||
|
||||
&.theme_color {
|
||||
width: 90px;
|
||||
background: var(--el-fill-color-light);
|
||||
color: var(--el-text-color-placeholder);
|
||||
|
||||
.color_text {
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
&.color_picker_wrapper {
|
||||
width: 30px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
background: transparent;
|
||||
|
||||
&:hover {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
:deep(.el-color-picker__trigger) {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 3px;
|
||||
border: 1px solid var(--el-border-color);
|
||||
padding: 0;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 0 5px #1890ff;
|
||||
}
|
||||
}
|
||||
|
||||
&.active :deep(.el-color-picker__trigger) {
|
||||
box-shadow: 0 0 5px #1890ff;
|
||||
border: 1px solid #1890ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.background_list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
li {
|
||||
width: 126px;
|
||||
height: 75px;
|
||||
width: 90px;
|
||||
height: 55px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 3px;
|
||||
margin: 0 5px 5px 0;
|
||||
@@ -244,6 +357,7 @@ const changeBackground = (item) => {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -254,19 +368,12 @@ const changeBackground = (item) => {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
.script_library_switch {
|
||||
margin-left: 20px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
.local_setting_drawer {
|
||||
.terminal_setting_drawer {
|
||||
.el-drawer__header {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
}
|
||||
.plus_support_tip_text {
|
||||
margin-left: 5px;
|
||||
color: var(--el-text-color-placeholder);
|
||||
}
|
||||
</style>
|
||||
@@ -69,6 +69,25 @@
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
<el-dropdown
|
||||
trigger="click"
|
||||
:teleported="isMobileScreen"
|
||||
>
|
||||
<span class="link_text">终端设置<el-icon class="link_icon"><arrow-down /></el-icon></span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item @click="showSetting = true">
|
||||
<span>基础设置</span>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click="showHighlightSettings = true">
|
||||
<span>高亮设置</span>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click="showOtherSettings = true">
|
||||
<span>其他设置</span>
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
<el-dropdown
|
||||
trigger="click"
|
||||
:teleported="isMobileScreen"
|
||||
@@ -79,8 +98,8 @@
|
||||
<el-dropdown-item @click="handleFullScreen">
|
||||
<span>全屏</span>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click="showSetting = true">
|
||||
<span>本地设置</span>
|
||||
<el-dropdown-item @click="showMenuOptions = true">
|
||||
<span>菜单选项</span>
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
@@ -95,7 +114,7 @@
|
||||
>
|
||||
<el-switch
|
||||
v-model="isSyncAllSession"
|
||||
class="swtich"
|
||||
class="switch"
|
||||
inline-prompt
|
||||
style="
|
||||
--el-switch-on-color: #13ce66;
|
||||
@@ -114,7 +133,7 @@
|
||||
>
|
||||
<el-switch
|
||||
v-model="showFooterBar"
|
||||
class="swtich"
|
||||
class="switch"
|
||||
inline-prompt
|
||||
style="
|
||||
--el-switch-on-color: #13ce66;
|
||||
@@ -134,7 +153,7 @@
|
||||
>
|
||||
<el-switch
|
||||
v-model="isSingleWindowMode"
|
||||
class="swtich"
|
||||
class="switch"
|
||||
inline-prompt
|
||||
style="
|
||||
--el-switch-on-color: #409eff;
|
||||
@@ -340,7 +359,19 @@
|
||||
@exec-script="handleExecScript"
|
||||
/>
|
||||
</el-drawer>
|
||||
<div v-else :class="['tab_content_main_sftp', { 'show_sftp': showSftpSide }]">
|
||||
<div
|
||||
v-else
|
||||
:class="['tab_content_main_sftp', { 'show_sftp': showSftpSide }]"
|
||||
:style="showSftpSide ? { width: sftpWidth + 'px', minWidth: sftpWidth + 'px', maxWidth: sftpWidth + 'px' } : {}"
|
||||
>
|
||||
<!-- 拖拽调整条 -->
|
||||
<div
|
||||
v-if="showSftpSide"
|
||||
class="sftp_resize_handle"
|
||||
@mousedown="startResizeSftp"
|
||||
>
|
||||
<div class="sftp_resize_handle_line" />
|
||||
</div>
|
||||
<SftpV2
|
||||
:init-connect="showSftpSide"
|
||||
:host-id="item.id"
|
||||
@@ -382,6 +413,9 @@
|
||||
/>
|
||||
|
||||
<TerminalSetting v-model:show="showSetting" />
|
||||
<TerminalHighlightSettings v-model:show="showHighlightSettings" />
|
||||
<OtherSettings v-model:show="showOtherSettings" />
|
||||
<MenuOptions v-model:show="showMenuOptions" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -403,6 +437,9 @@ import Terminal from './terminal.vue'
|
||||
import ServerStatus from './server-status.vue'
|
||||
import HostForm from '../../server/components/host-form.vue'
|
||||
import TerminalSetting from './terminal-setting.vue'
|
||||
import TerminalHighlightSettings from './terminal-highlight-settings.vue'
|
||||
import OtherSettings from './other-settings.vue'
|
||||
import MenuOptions from './menu-options.vue'
|
||||
import FooterBar from './footer-bar.vue'
|
||||
import SftpV2 from './sftp-v2.vue'
|
||||
import TerminalSingleWindow from './terminal-single-window.vue'
|
||||
@@ -438,10 +475,14 @@ watch(layoutMode, (newMode) => {
|
||||
const hostFormVisible = ref(false)
|
||||
const updateHostData = ref(null)
|
||||
const showSetting = ref(false)
|
||||
const showHighlightSettings = ref(false)
|
||||
const showOtherSettings = ref(false)
|
||||
const showMenuOptions = ref(false)
|
||||
const showInfoSide = ref(isMobileScreen.value ? false : localStorage.getItem('showInfoSide') !== 'false')
|
||||
const showSftpSide = ref(isMobileScreen.value ? false : localStorage.getItem('showSftpSide') !== 'false')
|
||||
const showFooterBar = ref(localStorage.getItem('showFooterBar') === 'true')
|
||||
const footerBarHeight = ref(parseInt(localStorage.getItem('footerBarHeight')) || 250)
|
||||
const sftpWidth = ref(parseInt(localStorage.getItem(SFTP_WIDTH_KEY)) || 450)
|
||||
const longPressCtrl = ref(false)
|
||||
const longPressAlt = ref(false)
|
||||
const scriptDropdownRef = ref(null)
|
||||
@@ -553,6 +594,44 @@ const handleFooterBarHeightChange = (height) => {
|
||||
debouncedResizeTerminal()
|
||||
}
|
||||
|
||||
// SFTP宽度调整相关
|
||||
const SFTP_WIDTH_KEY = 'easynode_sftp_width'
|
||||
const isResizingSftp = ref(false)
|
||||
const startX = ref(0)
|
||||
const startWidth = ref(0)
|
||||
|
||||
const startResizeSftp = (e) => {
|
||||
isResizingSftp.value = true
|
||||
startX.value = e.clientX
|
||||
startWidth.value = sftpWidth.value
|
||||
|
||||
document.addEventListener('mousemove', handleResizeSftp)
|
||||
document.addEventListener('mouseup', stopResizeSftp)
|
||||
document.body.style.cursor = 'ew-resize'
|
||||
document.body.style.userSelect = 'none'
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
const handleResizeSftp = (e) => {
|
||||
if (!isResizingSftp.value) return
|
||||
|
||||
const deltaX = startX.value - e.clientX // 向左拖拽为正值(增大宽度)
|
||||
const newWidth = Math.max(200, Math.min(800, startWidth.value + deltaX)) // 最小200px,最大800px
|
||||
|
||||
sftpWidth.value = newWidth
|
||||
}
|
||||
|
||||
const stopResizeSftp = () => {
|
||||
isResizingSftp.value = false
|
||||
document.removeEventListener('mousemove', handleResizeSftp)
|
||||
document.removeEventListener('mouseup', stopResizeSftp)
|
||||
document.body.style.cursor = ''
|
||||
document.body.style.userSelect = ''
|
||||
// 保存宽度到 localStorage
|
||||
localStorage.setItem(SFTP_WIDTH_KEY, sftpWidth.value.toString())
|
||||
debouncedResizeTerminal()
|
||||
}
|
||||
|
||||
const getStartIndexByTabIndex = (idx) => {
|
||||
let start = 0
|
||||
for (let i = 0; i < idx; i++) {
|
||||
@@ -714,28 +793,32 @@ const handleLinkHost = (hostDescObj) => {
|
||||
const handleExecScript = async (scriptDescObj) => {
|
||||
if (!scriptDescObj) return // clearCheckedNodes二次触发change事件
|
||||
let command = ''
|
||||
let useBase64 = false
|
||||
|
||||
const id = Array.isArray(scriptDescObj) ? scriptDescObj.slice(-1)[0] : scriptDescObj?.id
|
||||
if (id) {
|
||||
const script = scriptList.value.find((item) => item.id === id)
|
||||
command = script?.command
|
||||
useBase64 = script?.useBase64 || false // 读取脚本的base64配置
|
||||
} else {
|
||||
command = scriptDescObj
|
||||
useBase64 = false // 外部传入的命令默认不用base64
|
||||
}
|
||||
if (!command) return $message.warning('未找到对应的脚本')
|
||||
|
||||
if (!isSyncAllSession.value) {
|
||||
// 不同步时,使用 handleInputCommand(会处理分屏同步)
|
||||
await handleInputCommand(command, 'script')
|
||||
await handleInputCommand(command, 'script', useBase64)
|
||||
} else {
|
||||
// 同步输入到所有终端
|
||||
if (isSingleWindowMode.value) {
|
||||
// 单窗口模式:inputCommand 方法内部会处理同步逻辑
|
||||
await $nextTick()
|
||||
singleWindowRef.value?.inputCommandToTerminal(command, 'script')
|
||||
singleWindowRef.value?.inputCommandToTerminal(command, 'script', useBase64)
|
||||
} else {
|
||||
// 多窗口模式:遍历所有终端引用
|
||||
terminalRefs.value.forEach((terminalRef) => {
|
||||
terminalRef.inputCommand(command, 'script')
|
||||
terminalRef.inputCommand(command, 'script', useBase64)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -874,11 +957,11 @@ const resizeTerminal = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const handleInputCommand = async (command, type = 'input') => {
|
||||
const handleInputCommand = async (command, type = 'input', useBase64 = false) => {
|
||||
if (isSingleWindowMode.value) {
|
||||
// 单窗口模式下,使用 singleWindowRef 来执行命令
|
||||
await $nextTick()
|
||||
singleWindowRef.value?.inputCommandToTerminal(command, type)
|
||||
singleWindowRef.value?.inputCommandToTerminal(command, type, useBase64)
|
||||
} else {
|
||||
// 多窗口模式下,优先使用当前聚焦的终端
|
||||
let targetTerminalRef = null
|
||||
@@ -895,7 +978,7 @@ const handleInputCommand = async (command, type = 'input') => {
|
||||
|
||||
await $nextTick()
|
||||
targetTerminalRef?.focusTab()
|
||||
targetTerminalRef?.inputCommand(command, type)
|
||||
targetTerminalRef?.inputCommand(command, type, useBase64)
|
||||
|
||||
// 处理分屏同步逻辑
|
||||
const curTabKey = getTabKeyByIndex(activeTabIndex.value)
|
||||
@@ -908,7 +991,7 @@ const handleInputCommand = async (command, type = 'input') => {
|
||||
|
||||
tabTerminalRefs.forEach((terminalRef) => {
|
||||
if (terminalRef?.$?.uid !== targetUid) {
|
||||
terminalRef.inputCommand(command, type)
|
||||
terminalRef.inputCommand(command, type, useBase64)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -967,6 +1050,8 @@ onMounted(() => {
|
||||
})
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('fullscreenchange', fullScreenCb)
|
||||
document.removeEventListener('mousemove', handleResizeSftp)
|
||||
document.removeEventListener('mouseup', stopResizeSftp)
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1208,13 +1293,37 @@ onUnmounted(() => {
|
||||
overflow: hidden;
|
||||
transition: all 0.2s;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
&.show_sftp {
|
||||
width: 450px;
|
||||
min-width: 450px;
|
||||
max-width: 450px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.sftp_resize_handle {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 8px;
|
||||
cursor: ew-resize;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&:hover .sftp_resize_handle_line {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.sftp_resize_handle_line {
|
||||
width: 2px;
|
||||
height: 40px;
|
||||
background: var(--el-color-primary);
|
||||
border-radius: 1px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ import { terminalStatus } from '@/utils/enum'
|
||||
import { useContextMenu } from '@/composables/useContextMenu'
|
||||
import { EventBus, isDockerId, isDockerComposeYml, generateSocketInstance } from '@/utils'
|
||||
import useMobileWidth from '@/composables/useMobileWidth'
|
||||
import { TerminalHighlighter } from '@/utils/highlighter'
|
||||
|
||||
const { CONNECTING, CONNECT_SUCCESS, CONNECT_FAIL } = terminalStatus
|
||||
|
||||
@@ -62,6 +63,8 @@ const emit = defineEmits(['inputCommand', 'cdCommand', 'ping-data', 'reset-long-
|
||||
const socket = ref(null)
|
||||
// const commandHistoryList = ref([])
|
||||
const term = ref(null)
|
||||
const terminalText = ref('')
|
||||
const highlighter = ref(null)
|
||||
const initCommand = ref('')
|
||||
const timer = ref(null)
|
||||
const pingTimer = ref(null)
|
||||
@@ -71,13 +74,13 @@ const socketConnected = ref(false)
|
||||
const curStatus = ref(CONNECTING)
|
||||
const terminalRef = ref(null)
|
||||
const { showMenu, closeMenu, isVisible } = useContextMenu()
|
||||
const cursorTheme = {
|
||||
cursor: '#00ff41',
|
||||
cursorAccent: '#000000'
|
||||
}
|
||||
|
||||
const theme = computed(() => themeList[$store.terminalConfig.themeName])
|
||||
const fontSize = computed(() => $store.terminalConfig.fontSize)
|
||||
const fontFamily = computed(() => $store.terminalConfig.fontFamily)
|
||||
const fontColor = computed(() => $store.terminalConfig.fontColor)
|
||||
const cursorColor = computed(() => $store.terminalConfig.cursorColor)
|
||||
const selectionColor = computed(() => $store.terminalConfig.selectionColor)
|
||||
const background = computed(() => $store.terminalConfig.background)
|
||||
const hostObj = computed(() => props.hostObj)
|
||||
const hostId = computed(() => hostObj.value.id)
|
||||
@@ -86,31 +89,78 @@ const menuCollapse = computed(() => $store.menuCollapse)
|
||||
const menuPosition = computed(() => $store.menuPosition)
|
||||
const autoExecuteScript = computed(() => $store.terminalConfig.autoExecuteScript)
|
||||
const autoReconnect = computed(() => $store.terminalConfig.autoReconnect)
|
||||
const keywordHighlight = computed(() => $store.terminalConfig.keywordHighlight)
|
||||
const customHighlightRules = computed(() => $store.terminalConfig.customHighlightRules)
|
||||
const highlightDebugMode = computed(() => $store.terminalConfig.highlightDebugMode)
|
||||
const isPlusActive = computed(() => $store.isPlusActive)
|
||||
const isLongPressCtrl = computed(() => props.longPressCtrl)
|
||||
const isLongPressAlt = computed(() => props.longPressAlt)
|
||||
|
||||
// 应用终端主题的公共函数
|
||||
const applyTerminalTheme = () => {
|
||||
if (!term.value || !terminalRef.value) return
|
||||
|
||||
// 构建光标主题
|
||||
const cursorTheme = {
|
||||
cursor: cursorColor.value || theme.value.cursor,
|
||||
cursorAccent: '#000000'
|
||||
}
|
||||
|
||||
// 构建完整主题
|
||||
const customTheme = {
|
||||
...theme.value,
|
||||
...cursorTheme
|
||||
}
|
||||
|
||||
// 应用自定义字体颜色
|
||||
if (fontColor.value) {
|
||||
customTheme.foreground = fontColor.value
|
||||
}
|
||||
|
||||
// 应用自定义选中颜色
|
||||
if (selectionColor.value) {
|
||||
customTheme.selectionBackground = selectionColor.value
|
||||
}
|
||||
|
||||
// 根据背景设置应用主题
|
||||
if (background.value) {
|
||||
// 有自定义背景:xterm背景透明,使用terminalRef显示背景
|
||||
term.value.options.theme = { ...customTheme, background: 'transparent' }
|
||||
terminalRef.value.style.backgroundImage = background.value?.startsWith('http')
|
||||
? `url(${ background.value })`
|
||||
: `${ background.value }`
|
||||
terminalRef.value.style.backgroundColor = ''
|
||||
|
||||
// 设置 viewport 为透明以显示自定义背景
|
||||
const viewport = terminalRef.value.querySelector('.xterm-viewport')
|
||||
if (viewport) {
|
||||
viewport.style.setProperty('background-color', 'transparent', 'important')
|
||||
}
|
||||
} else {
|
||||
// 使用主题背景:清除terminalRef背景,使用xterm主题背景色
|
||||
term.value.options.theme = customTheme
|
||||
terminalRef.value.style.backgroundImage = ''
|
||||
terminalRef.value.style.backgroundColor = ''
|
||||
|
||||
// 设置 viewport 背景色为主题背景色
|
||||
const viewport = terminalRef.value.querySelector('.xterm-viewport')
|
||||
if (viewport) {
|
||||
viewport.style.setProperty('background-color', customTheme.background || '#1e1e1e', 'important')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
watch([menuCollapse, menuPosition,], () => {
|
||||
nextTick(() => {
|
||||
handleResize()
|
||||
})
|
||||
})
|
||||
|
||||
watch(theme, () => {
|
||||
nextTick(() => {
|
||||
// 自定义光标颜色
|
||||
const customTheme = {
|
||||
...theme.value,
|
||||
...cursorTheme
|
||||
}
|
||||
|
||||
if (!background.value) {
|
||||
term.value.options.theme = customTheme
|
||||
} else {
|
||||
term.value.options.theme = { ...customTheme, background: '#00000080' }
|
||||
}
|
||||
})
|
||||
})
|
||||
watch(theme, () => nextTick(applyTerminalTheme))
|
||||
watch(fontColor, () => nextTick(applyTerminalTheme))
|
||||
watch(cursorColor, () => nextTick(applyTerminalTheme))
|
||||
watch(selectionColor, () => nextTick(applyTerminalTheme))
|
||||
watch(background, () => nextTick(applyTerminalTheme), { immediate: true })
|
||||
|
||||
watch(fontSize, () => {
|
||||
nextTick(() => {
|
||||
@@ -119,23 +169,35 @@ watch(fontSize, () => {
|
||||
})
|
||||
})
|
||||
|
||||
watch(background, (newVal) => {
|
||||
watch(fontFamily, () => {
|
||||
nextTick(() => {
|
||||
const customTheme = {
|
||||
...theme.value,
|
||||
...cursorTheme
|
||||
}
|
||||
|
||||
if (newVal) {
|
||||
term.value.options.theme = { ...customTheme, background: '#00000080' }
|
||||
terminalRef.value.style.backgroundImage = background.value?.startsWith('http') ? `url(${ background.value })` : `${ background.value }`
|
||||
// terminalRef.value.style.backgroundImage = `linear-gradient(rgba(0, 0, 0, 0.15), rgba(0, 0, 0, 0.15)), url(${ background.value })`
|
||||
} else {
|
||||
term.value.options.theme = customTheme
|
||||
terminalRef.value.style.backgroundImage = null
|
||||
}
|
||||
term.value.options.fontFamily = fontFamily.value
|
||||
handleResize()
|
||||
})
|
||||
}, { immediate: true })
|
||||
})
|
||||
|
||||
watch(keywordHighlight, (newVal) => {
|
||||
if (highlighter.value) {
|
||||
highlighter.value.setEnabled(newVal)
|
||||
}
|
||||
})
|
||||
|
||||
// 监听自定义高亮规则变化
|
||||
watch(customHighlightRules, (newRules) => {
|
||||
if (highlighter.value) {
|
||||
if (highlightDebugMode.value) {
|
||||
console.log('自定义高亮规则变化,更新高亮器:', newRules)
|
||||
}
|
||||
highlighter.value.updateCustomRules(newRules)
|
||||
}
|
||||
}, { deep: true, immediate: true })
|
||||
|
||||
// 监听调试模式变化
|
||||
watch(highlightDebugMode, (newVal) => {
|
||||
if (highlighter.value) {
|
||||
highlighter.value.setDebugMode(newVal)
|
||||
}
|
||||
})
|
||||
|
||||
watch(curStatus, () => {
|
||||
console.warn(`status: ${ curStatus.value }`)
|
||||
@@ -162,7 +224,15 @@ const connectIO = () => {
|
||||
socket.value.on('terminal_connect_success', () => {
|
||||
socket.value.on('output', (str) => {
|
||||
if (props.isSingleWindow && !isPlusActive.value) return
|
||||
term.value.write(str)
|
||||
|
||||
// 使用高亮器处理
|
||||
if (keywordHighlight.value && highlighter.value) {
|
||||
const highlightedStr = highlighter.value.highlightText(str)
|
||||
term.value.write(highlightedStr)
|
||||
} else {
|
||||
term.value.write(str)
|
||||
}
|
||||
|
||||
terminalText.value += str
|
||||
})
|
||||
socket.value.on('terminal_connect_shell_success', () => {
|
||||
@@ -257,7 +327,7 @@ const reconnectTerminal = (isCommonTips = false, tips) => {
|
||||
term.value.write(`\r\n\x1b[91m${ tips },自动重连中...\x1b[0m \r\n`)
|
||||
connectIO()
|
||||
} else {
|
||||
term.value.write(`\r\n\x1b[91mError: ${ tips },请重新连接。([功能项->本地设置->快捷操作]中开启自动重连)\x1b[0m \r\n`)
|
||||
term.value.write(`\r\n\x1b[91mError: ${ tips },请重新连接。([终端设置->其他设置]中开启自动重连)\x1b[0m \r\n`)
|
||||
}
|
||||
} else {
|
||||
term.value.write(`\n${ tips } \n`)
|
||||
@@ -271,9 +341,9 @@ const createLocalTerminal = () => {
|
||||
convertEol: true,
|
||||
cursorBlink: true,
|
||||
disableStdin: false,
|
||||
minimumContrastRatio: 7,
|
||||
minimumContrastRatio: 1, // 无对比度要求
|
||||
allowTransparency: true,
|
||||
fontFamily: 'Cascadia Code, Menlo, monospace',
|
||||
fontFamily: fontFamily.value,
|
||||
fontSize: fontSize.value,
|
||||
theme: theme.value,
|
||||
scrollback: 5000, // 滚动缓冲区大小
|
||||
@@ -296,6 +366,14 @@ const createLocalTerminal = () => {
|
||||
term.value = terminalInstance
|
||||
terminalInstance.open(terminalRef.value)
|
||||
!isMobileScreen.value && terminalInstance.loadAddon(canvasAddon)
|
||||
|
||||
// 初始化TerminalHighlighter
|
||||
highlighter.value = new TerminalHighlighter(terminalInstance, {
|
||||
enabled: keywordHighlight.value,
|
||||
debugMode: highlightDebugMode.value || false,
|
||||
customRules: customHighlightRules.value
|
||||
})
|
||||
|
||||
terminalInstance.writeln('\x1b[1;32mWelcome to EasyNode terminal\x1b[0m.')
|
||||
terminalInstance.writeln('\x1b[1;32mAn experimental Web-SSH Terminal\x1b[0m.')
|
||||
if (props.autoFocus) {
|
||||
@@ -305,6 +383,11 @@ const createLocalTerminal = () => {
|
||||
onFindText()
|
||||
onWebLinks()
|
||||
onResize()
|
||||
|
||||
// 初始化主题设置
|
||||
nextTick(() => {
|
||||
applyTerminalTheme()
|
||||
})
|
||||
}
|
||||
|
||||
const shellResize = () => {
|
||||
@@ -351,7 +434,6 @@ const onFindText = () => {
|
||||
// term.value.loadAddon(searchBar.value)
|
||||
}
|
||||
|
||||
const terminalText = ref(null)
|
||||
const enterTimer = ref(null)
|
||||
|
||||
function filterAnsiSequences(str) {
|
||||
@@ -673,19 +755,40 @@ const focusTab = () => {
|
||||
}, 200)
|
||||
}
|
||||
|
||||
const inputCommand = (command, type = 'input') => {
|
||||
const inputCommand = (command, type = 'input', useBase64 = false) => {
|
||||
if (type === 'script') {
|
||||
command = command + (autoExecuteScript.value ? '\n' : '')
|
||||
if (useBase64) {
|
||||
// 使用Base64管道模式执行
|
||||
// 处理 UTF-8 字符:使用现代浏览器的 TextEncoder API
|
||||
const utf8Bytes = new TextEncoder().encode(command)
|
||||
const encodedScript = btoa(String.fromCharCode(...utf8Bytes))
|
||||
command = `echo '${ encodedScript }' | base64 -d | bash${ autoExecuteScript.value ? '\n' : '' }`
|
||||
} else {
|
||||
// 直接执行模式:根据配置添加换行符
|
||||
command = command + (autoExecuteScript.value ? '\n' : '')
|
||||
}
|
||||
}
|
||||
socket.value.emit('input', command)
|
||||
}
|
||||
|
||||
const execExternalCommand = (command) => {
|
||||
const execExternalCommand = (command, useBase64 = false) => {
|
||||
if (!socket.value || !socket.value.connected || curStatus.value !== CONNECT_SUCCESS) {
|
||||
$message.error('终端连接已断开,无法执行指令')
|
||||
return
|
||||
}
|
||||
socket.value.emit('input', command + '\n')
|
||||
|
||||
if (useBase64) {
|
||||
// 使用Base64管道模式执行
|
||||
// 处理 UTF-8 字符:使用现代浏览器的 TextEncoder API
|
||||
const utf8Bytes = new TextEncoder().encode(command)
|
||||
const encodedScript = btoa(String.fromCharCode(...utf8Bytes))
|
||||
command = `echo '${ encodedScript }' | base64 -d | bash${ autoExecuteScript.value ? '\n' : '' }`
|
||||
} else {
|
||||
// 直接执行模式:根据配置添加换行符
|
||||
command = command + (autoExecuteScript.value ? '\n' : '')
|
||||
}
|
||||
|
||||
socket.value.emit('input', command)
|
||||
term.value?.focus()
|
||||
}
|
||||
|
||||
@@ -721,13 +824,14 @@ defineExpose({
|
||||
.terminal_container {
|
||||
background-size: 100% 100%;
|
||||
background-repeat: no-repeat;
|
||||
// padding: 10px;
|
||||
:deep(.xterm) {
|
||||
height: 100%;
|
||||
padding: 8px 8px;
|
||||
}
|
||||
|
||||
:deep(.xterm-viewport) {
|
||||
overflow-y: auto;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
:deep(.xterm-screen) {
|
||||
|
||||
Reference in New Issue
Block a user