mirror of
https://github.com/chaos-zhu/easynode.git
synced 2026-10-07 13:18:34 +08:00
feat: 实例列表支持搜索&列表设置持久化
This commit is contained in:
@@ -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'),
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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 })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -68,7 +68,12 @@
|
||||
<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
|
||||
prop="useBase64"
|
||||
label="编码方式"
|
||||
width="140px"
|
||||
:formatter="formatExecutionMode"
|
||||
/>
|
||||
<el-table-column label="操作" fixed="right" width="160px">
|
||||
<template #default="{ row }">
|
||||
<template v-if="row.index !== '--'">
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
v-for="item in batchHosts"
|
||||
:key="item.id"
|
||||
class="host_name_tag"
|
||||
type="warning"
|
||||
type="success"
|
||||
>
|
||||
{{ item.name }}
|
||||
</el-tag>
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -0,0 +1,240 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
title="列表设置"
|
||||
width="500px"
|
||||
append-to-body
|
||||
@close="handleClose"
|
||||
>
|
||||
<el-tabs v-model="activeTab">
|
||||
<!-- 表头设置 Tab -->
|
||||
<el-tab-pane label="表头设置" name="columns">
|
||||
<div class="column_settings">
|
||||
<div v-for="(item, key) in columnConfig" :key="key" class="column_item">
|
||||
<el-checkbox
|
||||
v-model="localColumnSettings[key]"
|
||||
:disabled="item.disabled"
|
||||
>
|
||||
{{ item.label }}
|
||||
</el-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab_footer" />
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- 展现形式 Tab -->
|
||||
<el-tab-pane label="展现形式" name="display">
|
||||
<div class="display_settings">
|
||||
<div class="display_mode_cards">
|
||||
<div
|
||||
class="display_mode_card"
|
||||
:class="{ active: localDisplayMode === 'group' }"
|
||||
@click="localDisplayMode = 'group'"
|
||||
>
|
||||
<div class="card_content">
|
||||
<div class="card_title">分组展示</div>
|
||||
<div class="card_description">按照分组折叠展示主机列表</div>
|
||||
</div>
|
||||
<div class="card_check">
|
||||
<el-icon v-if="localDisplayMode === 'group'" class="check_icon">
|
||||
<CircleCheckFilled />
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="display_mode_card"
|
||||
:class="{ active: localDisplayMode === 'list' }"
|
||||
@click="localDisplayMode = 'list'"
|
||||
>
|
||||
<div class="card_content">
|
||||
<div class="card_title">列表展示</div>
|
||||
<div class="card_description">在一个列表中展示所有主机</div>
|
||||
</div>
|
||||
<div class="card_check">
|
||||
<el-icon v-if="localDisplayMode === 'list'" class="check_icon">
|
||||
<CircleCheckFilled />
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog_footer">
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" @click="handleConfirm">确定</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { CircleCheckFilled } from '@element-plus/icons-vue'
|
||||
|
||||
const props = defineProps({
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
columnConfig: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
columnSettings: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
displayMode: {
|
||||
type: String,
|
||||
default: 'group'
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:show', 'confirm',])
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.show,
|
||||
set: (val) => emit('update:show', val)
|
||||
})
|
||||
|
||||
const activeTab = ref('columns')
|
||||
const localColumnSettings = ref({ ...props.columnSettings })
|
||||
const localDisplayMode = ref(props.displayMode)
|
||||
|
||||
// 监听props变化,更新本地状态
|
||||
watch(() => props.columnSettings, (newVal) => {
|
||||
localColumnSettings.value = { ...newVal }
|
||||
}, { deep: true })
|
||||
|
||||
watch(() => props.displayMode, (newVal) => {
|
||||
localDisplayMode.value = newVal
|
||||
})
|
||||
|
||||
// 重置弹窗状态
|
||||
watch(() => props.show, (newVal) => {
|
||||
if (newVal) {
|
||||
localColumnSettings.value = { ...props.columnSettings }
|
||||
localDisplayMode.value = props.displayMode
|
||||
activeTab.value = 'columns'
|
||||
}
|
||||
})
|
||||
|
||||
// 关闭弹窗
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
}
|
||||
|
||||
// 确认设置
|
||||
const handleConfirm = () => {
|
||||
emit('confirm', {
|
||||
columnSettings: localColumnSettings.value,
|
||||
displayMode: localDisplayMode.value
|
||||
})
|
||||
visible.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.column_settings {
|
||||
padding: 10px 0;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px 20px;
|
||||
|
||||
.column_item {
|
||||
flex: 0 0 auto;
|
||||
min-width: fit-content;
|
||||
}
|
||||
}
|
||||
|
||||
.tab_footer {
|
||||
margin-top: 20px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid var(--el-border-color-lighter);
|
||||
}
|
||||
|
||||
.display_settings {
|
||||
padding: 20px 0;
|
||||
|
||||
.display_mode_cards {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
|
||||
.display_mode_card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16px 20px;
|
||||
border: 2px solid var(--el-border-color);
|
||||
border-radius: 6px;
|
||||
background-color: var(--el-fill-color-blank);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--el-color-primary-light-5);
|
||||
}
|
||||
|
||||
&.active {
|
||||
border-color: var(--el-color-primary);
|
||||
background-color: var(--el-color-primary-light-9);
|
||||
|
||||
.card_icon {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.card_title {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.card_icon {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 16px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.card_content {
|
||||
flex: 1;
|
||||
|
||||
.card_title {
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
color: var(--el-text-color-primary);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.card_description {
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
.card_check {
|
||||
flex-shrink: 0;
|
||||
margin-left: 12px;
|
||||
|
||||
.check_icon {
|
||||
font-size: 22px;
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dialog_footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
}
|
||||
</style>
|
||||
|
||||
+119
-89
@@ -1,8 +1,25 @@
|
||||
<template>
|
||||
<div class="server_group_container">
|
||||
<div class="server_group_header">
|
||||
<el-button type="primary" class="add_host_btn" @click="hostFormVisible = true">添加实例</el-button>
|
||||
|
||||
<el-input
|
||||
v-if="hostList.length > 2"
|
||||
v-model="searchKeyword"
|
||||
placeholder="搜索实例"
|
||||
class="search_input"
|
||||
clearable
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button
|
||||
type="primary"
|
||||
class="add_host_btn"
|
||||
:class="{ 'first_btn': hostList.length <= 2 }"
|
||||
@click="hostFormVisible = true"
|
||||
>
|
||||
添加实例
|
||||
</el-button>
|
||||
<el-dropdown trigger="click">
|
||||
<el-button type="primary" class="group_action_btn">
|
||||
批量操作<el-icon class="el-icon--right"><arrow-down /></el-icon>
|
||||
@@ -22,9 +39,9 @@
|
||||
<el-button
|
||||
type="primary"
|
||||
class="table_header_setting_btn"
|
||||
@click="columnSettingsVisible = true"
|
||||
@click="listSettingsVisible = true"
|
||||
>
|
||||
表头设置
|
||||
列表设置
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="server_group_collapse">
|
||||
@@ -35,7 +52,8 @@
|
||||
<el-button type="primary" @click="importVisible = true">批量导入实例</el-button>
|
||||
</el-empty>
|
||||
</div>
|
||||
<el-collapse v-else v-model="activeGroup">
|
||||
<!-- 分组展示模式 -->
|
||||
<el-collapse v-else-if="displayMode === 'group'" v-model="activeGroup">
|
||||
<el-collapse-item v-for="(hosts, groupName) in groupHostList" :key="groupName" :name="groupName">
|
||||
<template #title>
|
||||
<div class="group_title">
|
||||
@@ -45,13 +63,24 @@
|
||||
<HostTable
|
||||
ref="hostTableRefs"
|
||||
:hosts="hosts"
|
||||
:column-settings="rawColumnSettings"
|
||||
:column-settings="columnSettings"
|
||||
@update-host="handleUpdateHost"
|
||||
@update-list="handleUpdateList"
|
||||
/>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
|
||||
<!-- 列表展示模式 -->
|
||||
<div v-else class="list_mode_container">
|
||||
<HostTable
|
||||
ref="hostTableRefs"
|
||||
:hosts="filteredHostList"
|
||||
:column-settings="columnSettings"
|
||||
@update-host="handleUpdateHost"
|
||||
@update-list="handleUpdateList"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 滚动到顶部按钮 -->
|
||||
<Transition name="scroll-to-top">
|
||||
<div
|
||||
@@ -77,30 +106,14 @@
|
||||
/>
|
||||
<GroupDialog v-model:show="groupDialogVisible" />
|
||||
|
||||
<!-- 表头设置弹窗 -->
|
||||
<el-dialog
|
||||
v-model="columnSettingsVisible"
|
||||
title="表头设置"
|
||||
width="400px"
|
||||
append-to-body
|
||||
>
|
||||
<div class="column-settings">
|
||||
<div v-for="(item, key) in columnConfig" :key="key" class="column-item">
|
||||
<el-checkbox
|
||||
v-model="columnSettings[key]"
|
||||
:disabled="item.disabled"
|
||||
>
|
||||
{{ item.label }}
|
||||
</el-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="resetColumnSettings">重置默认</el-button>
|
||||
<el-button type="primary" @click="saveColumnSettings">确定</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<!-- 列表设置弹窗 -->
|
||||
<ListSettings
|
||||
v-model:show="listSettingsVisible"
|
||||
:column-config="columnConfig"
|
||||
:column-settings="columnSettings"
|
||||
:display-mode="displayMode"
|
||||
@confirm="handleSettingsConfirm"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user