mirror of
https://github.com/chaos-zhu/easynode.git
synced 2026-06-21 02:26:04 +08:00
feat: 优化实例列表
This commit is contained in:
@@ -85,7 +85,7 @@ const useStore = defineStore('global', {
|
||||
let { data: newHostList } = await $api.getHostList()
|
||||
newHostList = newHostList.map(newHostObj => {
|
||||
let { expired = null } = newHostObj
|
||||
newHostObj.expired = (isValidDate(expired)) ? dayjs(expired).format('YYYY-MM-DD') : null
|
||||
newHostObj.expired = (isValidDate(expired)) ? dayjs(expired).format('YYYY-MM-DD') : '--'
|
||||
const oldHostObj = this.hostList.find(({ id }) => id === newHostObj.id)
|
||||
return oldHostObj ? Object.assign({}, { ...oldHostObj }, { ...newHostObj }) : newHostObj
|
||||
})
|
||||
|
||||
@@ -172,6 +172,7 @@ export const isDockerComposeYml = (str) => {
|
||||
}
|
||||
|
||||
export const isValidDate = (dateString) => {
|
||||
if (!dateString) return false
|
||||
const date = new Date(dateString)
|
||||
return !isNaN(date.getTime()) && date instanceof Date
|
||||
}
|
||||
@@ -251,36 +251,6 @@
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<!-- <el-form-item
|
||||
key="jumpHosts"
|
||||
prop="jumpHosts"
|
||||
label="跳板机"
|
||||
>
|
||||
<PlusSupportTip>
|
||||
<el-select
|
||||
v-model="hostForm.jumpHosts"
|
||||
placeholder="支持多选,跳板机连接顺序从前到后"
|
||||
multiple
|
||||
:disabled="!isPlusActive"
|
||||
>
|
||||
<template #empty>
|
||||
<div class="empty_text">
|
||||
<span>无可用跳板机器</span>
|
||||
</div>
|
||||
</template>
|
||||
<el-option
|
||||
v-for="item in confHostList"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id"
|
||||
>
|
||||
<div class="select_wrap">
|
||||
<span>{{ item.name }}</span>
|
||||
</div>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</PlusSupportTip>
|
||||
</el-form-item> -->
|
||||
<el-form-item key="command" prop="command" label="登录指令">
|
||||
<el-input
|
||||
v-model="hostForm.command"
|
||||
@@ -316,7 +286,7 @@
|
||||
<el-input
|
||||
v-model.trim="hostForm.consoleUrl"
|
||||
clearable
|
||||
placeholder="用于直达云服务商控制台"
|
||||
placeholder="用于直达云服务控制台"
|
||||
autocomplete="off"
|
||||
@keyup.enter="handleSave"
|
||||
/>
|
||||
|
||||
@@ -8,34 +8,16 @@
|
||||
@sort-change="handleSortChange"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<el-table-column type="expand">
|
||||
<template #default="{ row }">
|
||||
<el-descriptions
|
||||
title=""
|
||||
:column="5"
|
||||
class="host_info"
|
||||
>
|
||||
<el-descriptions-item label="到期时间:" width="20%">
|
||||
<span>{{ row.expired || '--' }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="服务商控制台:" width="20%">
|
||||
<span v-if="row.consoleUrl" class="link" @click="handleToConsole(row)">服务商控制台</span>
|
||||
<span v-else>--</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="备注:" width="20%">
|
||||
<span>{{ row.remark || '--' }}</span>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column type="selection" reserve-selection />
|
||||
<el-table-column v-if="props.columnSettings.selection" type="selection" reserve-selection />
|
||||
<el-table-column
|
||||
v-if="props.columnSettings.index"
|
||||
property="index"
|
||||
label="序号"
|
||||
sortable
|
||||
width="100px"
|
||||
/>
|
||||
<el-table-column
|
||||
v-if="props.columnSettings.name"
|
||||
label="名称"
|
||||
property="name"
|
||||
sortable
|
||||
@@ -43,16 +25,46 @@
|
||||
>
|
||||
<template #default="scope">{{ scope.row.name }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column property="username" label="用户名" />
|
||||
<el-table-column property="host" label="IP">
|
||||
<el-table-column v-if="props.columnSettings.username" property="username" label="用户名" />
|
||||
<el-table-column v-if="props.columnSettings.host" property="host" label="IP">
|
||||
<template #default="scope">
|
||||
<span @click="handleCopy(scope.row.host)">{{ scope.row.host }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column property="port" label="端口" />
|
||||
<el-table-column property="port" label="认证类型">
|
||||
<el-table-column v-if="props.columnSettings.port" property="port" label="端口" />
|
||||
<el-table-column v-if="props.columnSettings.authType" property="port" label="认证类型">
|
||||
<template #default="scope">{{ scope.row.authType === 'password' ? '密码' : '密钥' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="props.columnSettings.proxyType"
|
||||
property="port"
|
||||
show-overflow-tooltip
|
||||
label="代理类型"
|
||||
>
|
||||
<template #default="scope">{{ formatProxyType(scope.row) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="props.columnSettings.expired" property="expired" label="到期时间" />
|
||||
<el-table-column
|
||||
v-if="props.columnSettings.consoleUrl"
|
||||
property="consoleUrl"
|
||||
show-overflow-tooltip
|
||||
label="控制台URL"
|
||||
>
|
||||
<template #default="scope">
|
||||
<span v-if="scope.row.consoleUrl" class="link" @click="handleToConsole(scope.row)">{{ scope.row.consoleUrl }}</span>
|
||||
<span v-else>--</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="props.columnSettings.remark"
|
||||
show-overflow-tooltip
|
||||
property="remark"
|
||||
label="备注"
|
||||
>
|
||||
<template #default="scope">
|
||||
<span>{{ scope.row.remark || '--' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" fixed="right" :width="isMobileScreen ? 'auto' : '260px'">
|
||||
<template #default="{ row }">
|
||||
<el-dropdown v-if="isMobileScreen" trigger="click">
|
||||
@@ -106,23 +118,39 @@ import { ref, computed, getCurrentInstance, nextTick } from 'vue'
|
||||
import { ArrowDown } from '@element-plus/icons-vue'
|
||||
import useMobileWidth from '@/composables/useMobileWidth'
|
||||
|
||||
const { proxy: { $message, $messageBox, $api, $router } } = getCurrentInstance()
|
||||
const { proxy: { $message, $messageBox, $api, $router, $store } } = getCurrentInstance()
|
||||
|
||||
const props = defineProps({
|
||||
hosts: {
|
||||
required: true,
|
||||
type: Array
|
||||
},
|
||||
columnSettings: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
selection: true,
|
||||
index: true,
|
||||
name: true,
|
||||
username: true,
|
||||
host: true,
|
||||
port: true,
|
||||
authType: true,
|
||||
proxyType: true,
|
||||
expired: true,
|
||||
consoleUrl: true,
|
||||
remark: true
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update-list', 'update-host', 'select-change',])
|
||||
|
||||
const { isMobileScreen } = useMobileWidth()
|
||||
let tableRef = ref(null)
|
||||
const tableRef = ref(null)
|
||||
|
||||
let hosts = computed(() => {
|
||||
return props.hosts
|
||||
})
|
||||
const hosts = computed(() => props.hosts)
|
||||
const hostList = computed(() => $store.hostList)
|
||||
const proxyList = computed(() => $store.proxyList)
|
||||
|
||||
const handleUpdate = (hostInfo) => {
|
||||
emit('update-host', hostInfo)
|
||||
@@ -138,16 +166,15 @@ const handleSSH = async (row) => {
|
||||
$router.push({ path: '/terminal', query: { hostIds: id } })
|
||||
}
|
||||
|
||||
let defaultSortLocal = localStorage.getItem('host_table_sort')
|
||||
defaultSortLocal = defaultSortLocal ? JSON.parse(defaultSortLocal) : { prop: 'index', order: null } // 'ascending' or 'descending'
|
||||
let defaultSort = ref(defaultSortLocal)
|
||||
const defaultSortLocal = localStorage.getItem('host_table_sort')
|
||||
const defaultSort = ref(defaultSortLocal ? JSON.parse(defaultSortLocal) : { prop: 'index', order: null }) // 'ascending' or 'descending'
|
||||
|
||||
const handleSortChange = (sortObj) => {
|
||||
defaultSort.value = sortObj
|
||||
localStorage.setItem('host_table_sort', JSON.stringify(sortObj))
|
||||
}
|
||||
|
||||
let selectHosts = ref([])
|
||||
const selectHosts = ref([])
|
||||
const handleSelectionChange = (val) => {
|
||||
// console.log('select: ', val)
|
||||
selectHosts.value = val
|
||||
@@ -162,9 +189,14 @@ const clearSelection = () => {
|
||||
nextTick(() => tableRef.value.clearSelection())
|
||||
}
|
||||
|
||||
const selectAll = () => {
|
||||
nextTick(() => tableRef.value.toggleAllSelection())
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
getSelectHosts,
|
||||
clearSelection
|
||||
clearSelection,
|
||||
selectAll
|
||||
})
|
||||
|
||||
const handleRemoveHost = async ({ id }) => {
|
||||
@@ -188,6 +220,22 @@ const handleCopy = async (host) => {
|
||||
await navigator.clipboard.writeText(host)
|
||||
$message.success({ message: '复制成功', center: true })
|
||||
}
|
||||
|
||||
const formatProxyType = ({ proxyType, jumpHosts, proxyServer }) => {
|
||||
if (!proxyType) return '--'
|
||||
if (proxyType === 'jumpHosts' && jumpHosts?.length > 0) {
|
||||
const jumpHostsName = jumpHosts.map(item => {
|
||||
const hostInfo = hostList.value.find(host => host.id === item)
|
||||
return hostInfo?.name || 'Error'
|
||||
}).join('>>>')
|
||||
return `[跳板机]${ jumpHostsName }`
|
||||
}
|
||||
if (proxyType === 'proxyServer' && proxyList.value.some(item => item.id === proxyServer)) {
|
||||
const proxyServerInfo = proxyList.value.find(item => item.id === proxyServer)
|
||||
return `[${ proxyServerInfo.type }]${ proxyServerInfo.name }`
|
||||
}
|
||||
return '--'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -12,21 +12,20 @@
|
||||
<el-dropdown-item @click="handleBatchSSH">连接终端</el-dropdown-item>
|
||||
<el-dropdown-item @click="handleBatchModify">批量修改</el-dropdown-item>
|
||||
<el-dropdown-item @click="handleBatchRemove">批量删除</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
<el-dropdown trigger="click">
|
||||
<el-button type="primary" class="group_action_btn">
|
||||
导入导出<el-icon class="el-icon--right"><arrow-down /></el-icon>
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item @click="handleSelectAll">反选所有</el-dropdown-item>
|
||||
<el-dropdown-item @click="importVisible = true">导入实例</el-dropdown-item>
|
||||
<el-dropdown-item @click="handleBatchExport">导出实例</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
<el-button type="primary" @click="groupDialogVisible = true">分组管理</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
class="table_header_setting_btn"
|
||||
@click="columnSettingsVisible = true"
|
||||
>
|
||||
表头设置
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="server_group_collapse">
|
||||
<div v-if="isNoHost">
|
||||
@@ -46,6 +45,7 @@
|
||||
<HostTable
|
||||
ref="hostTableRefs"
|
||||
:hosts="hosts"
|
||||
:column-settings="rawColumnSettings"
|
||||
@update-host="handleUpdateHost"
|
||||
@update-list="handleUpdateList"
|
||||
/>
|
||||
@@ -65,6 +65,31 @@
|
||||
@update-list="handleUpdateList"
|
||||
/>
|
||||
<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>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -89,6 +114,61 @@ const hostTableRefs = ref([])
|
||||
const activeGroup = ref([])
|
||||
const groupDialogVisible = ref(false)
|
||||
|
||||
// 列设置相关
|
||||
const columnSettingsVisible = ref(false)
|
||||
|
||||
// 列配置定义
|
||||
const columnConfig = {
|
||||
selection: { label: '选择', disabled: false },
|
||||
index: { label: '序号', disabled: false },
|
||||
name: { label: '名称', disabled: false },
|
||||
username: { label: '用户名', disabled: false },
|
||||
host: { label: 'IP', disabled: false },
|
||||
port: { label: '端口', disabled: false },
|
||||
authType: { label: '认证类型', disabled: false },
|
||||
proxyType: { label: '代理类型', disabled: false },
|
||||
expired: { label: '到期时间', disabled: false },
|
||||
consoleUrl: { label: '控制台URL', disabled: false },
|
||||
remark: { label: '备注', disabled: false }
|
||||
}
|
||||
|
||||
// 默认列设置
|
||||
const defaultColumnSettings = {
|
||||
selection: true,
|
||||
index: true,
|
||||
name: true,
|
||||
username: true,
|
||||
host: true,
|
||||
port: true,
|
||||
authType: true,
|
||||
proxyType: false,
|
||||
expired: false,
|
||||
consoleUrl: false,
|
||||
remark: false
|
||||
}
|
||||
|
||||
// 从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 handleUpdateList = async () => {
|
||||
try {
|
||||
await $store.getHostList()
|
||||
@@ -99,7 +179,7 @@ const handleUpdateList = async () => {
|
||||
}
|
||||
|
||||
// 收集选中的实例
|
||||
let collectSelectHost = () => {
|
||||
const collectSelectHost = () => {
|
||||
let allSelectHosts = []
|
||||
hostTableRefs.value.map(item => {
|
||||
if (item) allSelectHosts = allSelectHosts.concat(item.getSelectHosts())
|
||||
@@ -107,7 +187,7 @@ let collectSelectHost = () => {
|
||||
selectHosts.value = allSelectHosts
|
||||
}
|
||||
|
||||
let handleBatchSSH = () => {
|
||||
const handleBatchSSH = () => {
|
||||
collectSelectHost()
|
||||
if (!selectHosts.value.length) return $message.warning('请选择要批量操作的实例')
|
||||
let ids = selectHosts.value.filter(item => item.isConfig).map(item => item.id)
|
||||
@@ -116,14 +196,18 @@ let handleBatchSSH = () => {
|
||||
$router.push({ path: '/terminal', query: { hostIds: ids.join(',') } })
|
||||
}
|
||||
|
||||
let handleBatchModify = async () => {
|
||||
const handleBatchModify = async () => {
|
||||
collectSelectHost()
|
||||
if (!selectHosts.value.length) return $message.warning('请选择要批量操作的实例')
|
||||
isBatchModify.value = true
|
||||
hostFormVisible.value = true
|
||||
}
|
||||
|
||||
let handleBatchRemove = async () => {
|
||||
const handleSelectAll = () => {
|
||||
hostTableRefs.value.forEach(item => item.selectAll())
|
||||
}
|
||||
|
||||
const handleBatchRemove = async () => {
|
||||
collectSelectHost()
|
||||
if (!selectHosts.value.length) return $message.warning('请选择要批量操作的实例')
|
||||
let ids = selectHosts.value.map(item => item.id)
|
||||
@@ -142,12 +226,12 @@ let handleBatchRemove = async () => {
|
||||
})
|
||||
}
|
||||
|
||||
let handleUpdateHost = (defaultData) => {
|
||||
const handleUpdateHost = (defaultData) => {
|
||||
hostFormVisible.value = true
|
||||
updateHostData.value = defaultData
|
||||
}
|
||||
|
||||
let handleBatchExport = () => {
|
||||
const handleBatchExport = () => {
|
||||
collectSelectHost()
|
||||
if (!selectHosts.value.length) return $message.warning('请选择要批量操作的实例')
|
||||
let exportData = JSON.parse(JSON.stringify(selectHosts.value))
|
||||
@@ -160,9 +244,9 @@ let handleBatchExport = () => {
|
||||
hostTableRefs.value.forEach(item => item.clearSelection())
|
||||
}
|
||||
|
||||
let hostList = computed(() => $store.hostList)
|
||||
const hostList = computed(() => $store.hostList)
|
||||
|
||||
let groupHostList = computed(() => {
|
||||
const groupHostList = computed(() => {
|
||||
let res = {}
|
||||
let groupList = $store.groupList
|
||||
groupList.forEach(group => {
|
||||
@@ -189,9 +273,9 @@ watch(groupHostList, () => {
|
||||
deep: false
|
||||
})
|
||||
|
||||
let isNoHost = computed(() => Object.keys(groupHostList.value).length === 0)
|
||||
const isNoHost = computed(() => Object.keys(groupHostList.value).length === 0)
|
||||
|
||||
let hostFormClosed = () => {
|
||||
const hostFormClosed = () => {
|
||||
updateHostData.value = null
|
||||
isBatchModify.value = false
|
||||
selectHosts.value = []
|
||||
@@ -227,6 +311,9 @@ let hostFormClosed = () => {
|
||||
flex-shrink: 0;
|
||||
min-width: fit-content;
|
||||
}
|
||||
.table_header_setting_btn {
|
||||
margin-left: 0px;
|
||||
}
|
||||
|
||||
> :last-child {
|
||||
margin-right: 0;
|
||||
@@ -254,4 +341,19 @@ let hostFormClosed = () => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.column-settings {
|
||||
.column-item {
|
||||
margin-bottom: 12px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="terminal_container">
|
||||
<div v-if="showLinkTips" class="terminal_link_tips">
|
||||
<h2 class="quick_link_text">最近连接</h2>
|
||||
<el-table :data="recentHostList" :show-header="false">
|
||||
<el-table :data="displayHostList" :show-header="false">
|
||||
<template #empty>
|
||||
<span class="link" @click="handleToServer">去连接</span>
|
||||
</template>
|
||||
@@ -40,7 +40,7 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<span v-show="recentHostList.length" class="link clear_host" @click="handleClearRecentHostList">清空</span>
|
||||
<span v-show="displayHostList.length" class="link clear_host" @click="handleClearRecentHostList">清空</span>
|
||||
</div>
|
||||
<div v-else>
|
||||
<TerminalWrapper
|
||||
@@ -80,6 +80,9 @@ let updateHostData = ref(null)
|
||||
let showLinkTips = computed(() => !Boolean(terminalTabs.length))
|
||||
let hostList = computed(() => $store.hostList)
|
||||
let recentHostList = ref(JSON.parse(localStorage.getItem('recentHostList')) || [])
|
||||
const displayHostList = computed(() => {
|
||||
return recentHostList.value.filter(item => hostList.value.some(host => host.id === item.id))
|
||||
})
|
||||
|
||||
function updateRecentHostList(targetHost) {
|
||||
if (!targetHost) return
|
||||
|
||||
Reference in New Issue
Block a user