feat: 优化实例列表

This commit is contained in:
chaoszhu
2025-08-03 19:22:14 +08:00
parent 5aff39e755
commit 3bfce044ff
6 changed files with 212 additions and 88 deletions

View File

@@ -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
})

View File

@@ -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
}

View File

@@ -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"
/>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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