diff --git a/server/app/config/index.js b/server/app/config/index.js
index 4c41ea7..075bde5 100644
--- a/server/app/config/index.js
+++ b/server/app/config/index.js
@@ -24,6 +24,7 @@ module.exports = {
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'),
+ serverListDBPath: path.join(process.cwd(),'app/db/server-list-config.db'),
apiPrefix: '/api/v1',
logConfig: {
outDir: path.join(process.cwd(),'./app/db/logs'),
diff --git a/server/app/controller/server-list-config.js b/server/app/controller/server-list-config.js
new file mode 100644
index 0000000..247eb47
--- /dev/null
+++ b/server/app/controller/server-list-config.js
@@ -0,0 +1,70 @@
+const { ServerListDB } = require('../utils/db-class')
+const serverListDB = new ServerListDB().getInstance()
+
+// 获取服务器列表配置
+async function getServerListConfig({ res }) {
+ try {
+ const config = await serverListDB.findOneAsync({})
+ if (!config) {
+ // 返回默认配置
+ const defaultConfig = {
+ columnSettings: {
+ selection: true,
+ index: true,
+ name: true,
+ username: true,
+ host: true,
+ port: true,
+ authType: true,
+ proxyType: false,
+ expired: false,
+ consoleUrl: false,
+ tag: true
+ },
+ displayMode: 'group'
+ }
+ return res.success({ data: defaultConfig })
+ }
+ // eslint-disable-next-line no-unused-vars
+ const { _id, ...configData } = config
+ res.success({ data: configData })
+ } catch (error) {
+ console.error('getServerListConfig error: ', error)
+ res.fail({ msg: '获取配置失败' })
+ }
+}
+
+// 保存服务器列表配置
+async function saveServerListConfig({ res, request }) {
+ try {
+ const { body } = request
+ if (!body || typeof body !== 'object') {
+ return res.fail({ msg: '参数错误' })
+ }
+
+ const existingConfig = await serverListDB.findOneAsync({})
+
+ if (existingConfig) {
+ // 更新现有配置
+ await serverListDB.updateAsync(
+ { _id: existingConfig._id },
+ { $set: body },
+ {}
+ )
+ } else {
+ // 插入新配置
+ await serverListDB.insertAsync(body)
+ }
+
+ res.success({ msg: '保存成功' })
+ } catch (error) {
+ console.error('saveServerListConfig error: ', error)
+ res.fail({ msg: '保存配置失败' })
+ }
+}
+
+module.exports = {
+ getServerListConfig,
+ saveServerListConfig
+}
+
diff --git a/server/app/router/routes.js b/server/app/router/routes.js
index 7d70f71..7830ea1 100644
--- a/server/app/router/routes.js
+++ b/server/app/router/routes.js
@@ -10,6 +10,7 @@ const { getLog, saveIpWhiteList, removeSomeLoginRecords } = require('../controll
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 { getServerListConfig, saveServerListConfig } = require('../controller/server-list-config')
const ssh = [
{
@@ -349,6 +350,19 @@ const terminalConfig = [
}
]
+const serverListConfig = [
+ {
+ method: 'get',
+ path: '/server-list-config',
+ controller: getServerListConfig
+ },
+ {
+ method: 'post',
+ path: '/server-list-config',
+ controller: saveServerListConfig
+ }
+]
+
module.exports = [].concat(
ssh,
host,
@@ -361,5 +375,6 @@ module.exports = [].concat(
log,
aiConfig,
proxy,
- terminalConfig
+ terminalConfig,
+ serverListConfig
)
diff --git a/server/app/utils/db-class.js b/server/app/utils/db-class.js
index 8ca0cd9..1a4a930 100644
--- a/server/app/utils/db-class.js
+++ b/server/app/utils/db-class.js
@@ -16,7 +16,8 @@ const {
favoriteSftpDBPath,
proxyDBPath,
fileTransferDBPath,
- terminalConfigDBPath
+ terminalConfigDBPath,
+ serverListDBPath
} = require('../config')
module.exports.KeyDB = class KeyDB {
@@ -213,4 +214,15 @@ module.exports.TerminalConfigDB = class TerminalConfigDB {
getInstance() {
return TerminalConfigDB.instance
}
+}
+
+module.exports.ServerListDB = class ServerListDB {
+ constructor() {
+ if (!ServerListDB.instance) {
+ ServerListDB.instance = new Datastore({ filename: serverListDBPath, autoload: true })
+ }
+ }
+ getInstance() {
+ return ServerListDB.instance
+ }
}
\ No newline at end of file
diff --git a/web/src/api/index.js b/web/src/api/index.js
index 8ac0b8a..cf92c82 100644
--- a/web/src/api/index.js
+++ b/web/src/api/index.js
@@ -190,5 +190,12 @@ export default {
},
saveTerminalConfig(data) {
return axios({ url: '/terminal-config', method: 'post', data })
+ },
+ // 服务器列表配置相关API
+ getServerListConfig(params = {}) {
+ return axios({ url: '/server-list-config', method: 'get', params })
+ },
+ saveServerListConfig(data) {
+ return axios({ url: '/server-list-config', method: 'post', data })
}
}
diff --git a/web/src/store/index.js b/web/src/store/index.js
index d3b885f..5725814 100644
--- a/web/src/store/index.js
+++ b/web/src/store/index.js
@@ -51,6 +51,23 @@ const useStore = defineStore('global', {
],
// 终端配置占位
terminalConfig: {},
+ // 服务器列表配置
+ serverListConfig: {
+ columnSettings: {
+ selection: true,
+ index: true,
+ name: true,
+ username: true,
+ host: true,
+ port: true,
+ authType: true,
+ proxyType: false,
+ expired: false,
+ consoleUrl: false,
+ tag: false
+ },
+ displayMode: localStorage.getItem('host_list_display_mode') || 'group'
+ },
menuSetting: {
...{
scriptLibrary: true,
@@ -95,6 +112,7 @@ const useStore = defineStore('global', {
await this.getPlusInfo()
await this.getProxyList()
await this.getTerminalConfig() // 添加终端配置获取
+ await this.getServerListConfig() // 添加服务器列表配置获取
this.getAIConfig()
this.getChatHistory()
},
@@ -162,6 +180,19 @@ const useStore = defineStore('global', {
await $api.saveTerminalConfig(newConfig)
this.$patch({ terminalConfig: newConfig })
},
+ async getServerListConfig() {
+ const { data: serverListConfig } = await $api.getServerListConfig()
+ if (serverListConfig.displayMode) {
+ // 将displayMode写入localStorage,避免刷新时闪烁
+ localStorage.setItem('host_list_display_mode', serverListConfig.displayMode)
+ }
+ this.$patch({ serverListConfig })
+ },
+ async setServerListConfig(setTarget = {}) {
+ const newConfig = { ...this.serverListConfig, ...setTarget }
+ await $api.saveServerListConfig(newConfig)
+ await this.getServerListConfig()
+ },
setMenuSetting(setTarget = {}) {
let newConfig = { ...this.menuSetting, ...setTarget }
localStorage.setItem('menuSetting', JSON.stringify(newConfig))
diff --git a/web/src/views/scripts/index.vue b/web/src/views/scripts/index.vue
index e573973..a9a39b1 100644
--- a/web/src/views/scripts/index.vue
+++ b/web/src/views/scripts/index.vue
@@ -68,7 +68,12 @@
-
+
diff --git a/web/src/views/server/components/host-form.vue b/web/src/views/server/components/host-form.vue
index f486362..9bf7419 100644
--- a/web/src/views/server/components/host-form.vue
+++ b/web/src/views/server/components/host-form.vue
@@ -15,7 +15,7 @@
v-for="item in batchHosts"
:key="item.id"
class="host_name_tag"
- type="warning"
+ type="success"
>
{{ item.name }}
diff --git a/web/src/views/server/components/host-table.vue b/web/src/views/server/components/host-table.vue
index 0bfb54f..ebaa1b9 100644
--- a/web/src/views/server/components/host-table.vue
+++ b/web/src/views/server/components/host-table.vue
@@ -213,10 +213,20 @@ const selectAll = () => {
nextTick(() => tableRef.value.toggleAllSelection())
}
+// 反选:已选的变成不选,未选的变成已选
+const toggleSelection = () => {
+ nextTick(() => {
+ hosts.value.forEach(row => {
+ tableRef.value.toggleRowSelection(row)
+ })
+ })
+}
+
defineExpose({
getSelectHosts,
clearSelection,
- selectAll
+ selectAll,
+ toggleSelection
})
const handleRemoveHost = async ({ id }) => {
diff --git a/web/src/views/server/components/list-settings.vue b/web/src/views/server/components/list-settings.vue
new file mode 100644
index 0000000..2765c24
--- /dev/null
+++ b/web/src/views/server/components/list-settings.vue
@@ -0,0 +1,240 @@
+
+
+
+
+
+
+
+
+ {{ item.label }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/views/server/index.vue b/web/src/views/server/index.vue
index 272f5d2..d3cc6be 100644
--- a/web/src/views/server/index.vue
+++ b/web/src/views/server/index.vue
@@ -1,8 +1,25 @@
@@ -35,7 +52,8 @@
批量导入实例
-
+
+
@@ -45,13 +63,24 @@
+
+
+
+
+
-
-
-
-
-
- {{ item.label }}
-
-
-
-
-
-
-
+
+
@@ -111,7 +124,8 @@ import HostTable from './components/host-table.vue'
import HostForm from './components/host-form.vue'
import ImportHost from './components/import-host.vue'
import GroupDialog from './components/group.vue'
-import { ArrowDown, ArrowUp } from '@element-plus/icons-vue'
+import ListSettings from './components/list-settings.vue'
+import { ArrowDown, ArrowUp, Search } from '@element-plus/icons-vue'
import { exportFile } from '@/utils'
const { proxy: { $api, $store, $router, $message, $messageBox, $tools } } = getCurrentInstance()
@@ -126,7 +140,10 @@ const activeGroup = ref([])
const groupDialogVisible = ref(false)
// 列设置相关
-const columnSettingsVisible = ref(false)
+const listSettingsVisible = ref(false)
+
+// 展示模式(从store获取)
+const displayMode = computed(() => $store.serverListConfig.displayMode)
// 滚动到顶部相关
const showScrollToTop = ref(false)
@@ -147,41 +164,19 @@ const columnConfig = {
tag: { label: 'Tag', disabled: false }
}
-// 默认列设置
-const defaultColumnSettings = {
- selection: true,
- index: true,
- name: true,
- username: true,
- host: true,
- port: true,
- authType: true,
- proxyType: false,
- expired: false,
- consoleUrl: false,
- tag: false
-}
+// 列设置状态(从store获取)
+const columnSettings = computed(() => $store.serverListConfig.columnSettings)
-// 从localStorage获取列设置
-const getColumnSettings = () => {
- const saved = localStorage.getItem('host_table_column_settings')
- return saved ? { ...defaultColumnSettings, ...JSON.parse(saved) } : { ...defaultColumnSettings }
-}
-
-// 列设置状态
-const columnSettings = ref(getColumnSettings())
-const rawColumnSettings = ref({ ...columnSettings.value })
-
-// 保存列设置到localStorage
-const saveColumnSettings = () => {
- localStorage.setItem('host_table_column_settings', JSON.stringify(columnSettings.value))
- rawColumnSettings.value = { ...columnSettings.value }
- columnSettingsVisible.value = false
-}
-
-// 重置列设置
-const resetColumnSettings = () => {
- columnSettings.value = { ...defaultColumnSettings }
+// 处理设置确认
+const handleSettingsConfirm = async (settings) => {
+ try {
+ // 保存到数据库
+ await $store.setServerListConfig(settings)
+ $message.success('设置已保存')
+ } catch (error) {
+ $message.error('保存设置失败')
+ console.error('保存设置失败:', error)
+ }
}
const handleUpdateList = async () => {
@@ -193,10 +188,17 @@ const handleUpdateList = async () => {
}
}
+// 获取所有 HostTable 组件的引用(兼容分组和列表模式)
+const getHostTableRefs = () => {
+ if (!hostTableRefs.value) return []
+ // 列表模式下是单个组件,分组模式下是数组
+ return Array.isArray(hostTableRefs.value) ? hostTableRefs.value : [hostTableRefs.value,]
+}
+
// 收集选中的实例
const collectSelectHost = () => {
let allSelectHosts = []
- hostTableRefs.value.map(item => {
+ getHostTableRefs().forEach(item => {
if (item) allSelectHosts = allSelectHosts.concat(item.getSelectHosts())
})
selectHosts.value = allSelectHosts
@@ -207,7 +209,6 @@ const handleBatchConnect = () => {
if (!selectHosts.value.length) return $message.warning('请选择要批量操作的实例')
let ids = selectHosts.value.filter(item => item.isConfig).map(item => item.id)
if (!ids.length) return $message.warning('所选实例未配置ssh连接信息')
- if (ids.length < selectHosts.value.length) $message.warning('部分实例未配置ssh连接信息,已忽略')
// $router.push({ path: '/terminal', query: { hostIds: ids.join(',') } })
if (selectHosts.value.every(item => item.connectType === 'rdp')) {
$router.push({ path: '/rdp', query: { hostIds: ids.join(',') } })
@@ -215,7 +216,9 @@ const handleBatchConnect = () => {
$router.push({ path: '/terminal', query: { hostIds: ids.join(',') } })
} else {
$message.warning('所选实例包含rdp和ssh连接信息,请选择同一终端类型进行批量连接')
+ return
}
+ if (ids.length < selectHosts.value.length) $message.warning('部分实例未配置ssh连接信息,已忽略')
}
const handleBatchModify = async () => {
@@ -226,7 +229,11 @@ const handleBatchModify = async () => {
}
const handleSelectAll = () => {
- hostTableRefs.value.forEach(item => item.selectAll())
+ getHostTableRefs().forEach(item => {
+ if (item && item.toggleSelection) {
+ item.toggleSelection()
+ }
+ })
}
const handleBatchRemove = async () => {
@@ -244,7 +251,7 @@ const handleBatchRemove = async () => {
$message({ message: data, type: 'success', center: true })
selectHosts.value = []
await handleUpdateList()
- hostTableRefs.value.forEach(item => item.clearSelection())
+ getHostTableRefs().forEach(item => item.clearSelection())
})
}
@@ -263,18 +270,39 @@ const handleBatchExport = () => {
})
const fileName = `easynode-${ $tools.formatTimestamp(Date.now(), 'time', '.') }.json`
exportFile(exportData, fileName, 'application/json')
- hostTableRefs.value.forEach(item => item.clearSelection())
+ getHostTableRefs().forEach(item => item.clearSelection())
}
const hostList = computed(() => $store.hostList)
+const searchKeyword = ref('')
+
+const filteredHostList = computed(() => {
+ if (!searchKeyword.value) return hostList.value
+
+ const keyword = searchKeyword.value.toLowerCase()
+ return hostList.value.filter(item => {
+ // 搜索实例名称
+ if (item.name && item.name.toLowerCase().includes(keyword)) return true
+ // 搜索用户名
+ if (item.username && item.username.toLowerCase().includes(keyword)) return true
+ // 搜索IP
+ if (item.host && item.host.toLowerCase().includes(keyword)) return true
+ // 搜索标签
+ if (item.tag && Array.isArray(item.tag)) {
+ return item.tag.some(tag => tag.toLowerCase().includes(keyword))
+ }
+ return false
+ })
+})
+
const groupHostList = computed(() => {
let res = {}
let groupList = $store.groupList
groupList.forEach(group => {
res[group.name] = []
})
- hostList.value.forEach(item => {
+ filteredHostList.value.forEach(item => {
const group = groupList.find(group => group.id === item.group)
if (group?.name) {
res[group.name].push(item)
@@ -302,7 +330,7 @@ const hostFormClosed = () => {
updateHostData.value = null
isBatchModify.value = false
selectHosts.value = []
- hostTableRefs.value.forEach(item => item.clearSelection())
+ getHostTableRefs().forEach(item => item.clearSelection())
}
// 滚动监听处理
@@ -353,7 +381,6 @@ onUnmounted(() => {
overflow-x: auto;
white-space: nowrap;
-webkit-overflow-scrolling: touch;
- justify-content: flex-end;
@media screen and (max-width: 768px) {
justify-content: flex-start;
@@ -363,12 +390,26 @@ onUnmounted(() => {
display: none;
}
+ .search_input {
+ width: 300px;
+ margin-right: auto;
+ flex-shrink: 0;
+
+ @media screen and (max-width: 768px) {
+ width: 200px;
+ }
+ }
+
.add_host_btn,
.group_action_btn,
.el-button {
flex-shrink: 0;
min-width: fit-content;
}
+
+ .first_btn {
+ margin-left: auto;
+ }
.table_header_setting_btn {
margin-left: 0px;
}
@@ -401,6 +442,10 @@ onUnmounted(() => {
margin: 0 25px;
}
+ .list-mode-container {
+ padding: 10px;
+ }
+
.scroll-to-top-btn {
position: fixed;
right: 15px;
@@ -431,21 +476,6 @@ onUnmounted(() => {
}
}
-.column-settings {
- .column-item {
- margin-bottom: 12px;
-
- &:last-child {
- margin-bottom: 0;
- }
- }
-}
-
-.dialog-footer {
- display: flex;
- justify-content: space-between;
-}
-
// 滚动到顶部按钮的过渡动画
.scroll-to-top-enter-active,
.scroll-to-top-leave-active {