731 lines
25 KiB
Vue
731 lines
25 KiB
Vue
<template>
|
||
<div class="host-group-mapping-container">
|
||
<!-- 顶部信息 -->
|
||
<div class="page-header" v-if="!embedded">
|
||
<div class="header-left">
|
||
<div class="header-info">
|
||
<h3>宿主机组映射管理</h3>
|
||
</div>
|
||
</div>
|
||
<div class="header-right">
|
||
<el-select v-model="selectedServiceId" placeholder="选择主控服务" filterable style="width: 240px" @change="handleServiceChange">
|
||
<el-option v-for="s in serviceOptions" :key="s.id" :label="`${s.name} (ID: ${s.id})`" :value="s.id" />
|
||
</el-select>
|
||
<el-button type="primary" @click="handleSync" :loading="syncLoading" :disabled="!serviceId">
|
||
<el-icon><RefreshRight /></el-icon>从远程同步
|
||
</el-button>
|
||
<el-button @click="handleRefresh">
|
||
<el-icon><Refresh /></el-icon>刷新
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
<div class="embedded-toolbar" v-if="embedded">
|
||
<el-button type="primary" @click="handleSync" :loading="syncLoading"><el-icon><RefreshRight /></el-icon>从远程同步</el-button>
|
||
<el-button @click="loadHostGroups"><el-icon><Refresh /></el-icon>刷新</el-button>
|
||
</div>
|
||
|
||
<!-- 本地主机组列表(树形折叠) -->
|
||
<div class="main-panel">
|
||
<div class="panel-header">
|
||
<h4>本地主机组列表</h4>
|
||
</div>
|
||
<div class="panel-body" v-loading="loading">
|
||
<el-table :data="treeGroupList" stripe style="width: 100%" row-key="_rowKey"
|
||
:tree-props="{ children: '_children', hasChildren: '_hasChildren' }">
|
||
<el-table-column prop="name" label="名称" min-width="140" show-overflow-tooltip />
|
||
<el-table-column prop="id" label="ID" width="70" />
|
||
<el-table-column label="远程ID" width="80">
|
||
<template #default="{ row }">{{ row.remoteId || '-' }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="父级远程ID" width="100">
|
||
<template #default="{ row }">{{ row.parentRemoteId || '-' }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="绑定商品组" min-width="120">
|
||
<template #default="{ row }">
|
||
<el-tag v-if="row.goodGroupId" type="success" size="small">商品组#{{ row.goodGroupId }}</el-tag>
|
||
<span v-else class="text-muted">未绑定</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="绑定商品" min-width="100">
|
||
<template #default="{ row }">
|
||
<el-tag v-if="row.goodId" type="warning" size="small">商品#{{ row.goodId }}</el-tag>
|
||
<span v-else class="text-muted">未绑定</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="note" label="备注" min-width="120" show-overflow-tooltip>
|
||
<template #default="{ row }">{{ row.note || '-' }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作" width="260" fixed="right">
|
||
<template #default="{ row }">
|
||
<el-button link type="primary" size="small" @click.stop="handleEditGroup(row)">编辑</el-button>
|
||
<el-button link type="primary" size="small" @click.stop="handleBind(row)">绑定</el-button>
|
||
<el-button link type="success" size="small" @click.stop="handleGenerateGoods(row)">生成商品</el-button>
|
||
<el-button link type="danger" size="small" @click.stop="handleDeleteGroup(row)">删除</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 编辑本地主机组弹窗 -->
|
||
<el-dialog v-model="editDialogVisible" title="编辑本地主机组" width="480px" destroy-on-close>
|
||
<el-form ref="editFormRef" :model="editForm" :rules="editFormRules" label-width="90px">
|
||
<el-form-item label="名称" prop="name">
|
||
<el-input v-model="editForm.name" placeholder="请输入名称" />
|
||
</el-form-item>
|
||
<el-form-item label="备注" prop="note">
|
||
<el-input v-model="editForm.note" type="textarea" :rows="3" placeholder="备注(可选)" />
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button @click="editDialogVisible = false">取消</el-button>
|
||
<el-button type="primary" :loading="editSubmitLoading" @click="submitEdit">保存</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- 绑定弹窗 -->
|
||
<el-dialog v-model="bindDialogVisible" title="绑定商品组/商品" width="520px" destroy-on-close>
|
||
<el-form ref="bindFormRef" :model="bindForm" label-width="100px">
|
||
<el-form-item label="主机组">
|
||
<el-input :model-value="bindForm._name" disabled />
|
||
</el-form-item>
|
||
<el-form-item label="绑定商品组">
|
||
<div class="bind-selector-row">
|
||
<el-input
|
||
:model-value="bindForm.good_group_id ? `商品组 #${bindForm.good_group_id}${bindForm._groupName ? ' - ' + bindForm._groupName : ''}` : '未绑定'"
|
||
disabled
|
||
style="flex: 1"
|
||
/>
|
||
<el-button type="primary" @click="showGroupSelector = true" style="margin-left: 8px" :disabled="!!bindForm.good_id">选择</el-button>
|
||
<el-button v-if="bindForm.good_group_id" @click="clearBindGroup" style="margin-left: 4px">清除</el-button>
|
||
</div>
|
||
<div v-if="bindForm.good_id" style="font-size: 12px; color: #e6a23c; margin-top: 4px">已绑定商品,请先清除商品后再绑定商品组</div>
|
||
</el-form-item>
|
||
<el-form-item label="绑定商品">
|
||
<div class="bind-selector-row">
|
||
<el-input
|
||
:model-value="bindForm.good_id ? `商品 #${bindForm.good_id}${bindForm._goodName ? ' - ' + bindForm._goodName : ''}` : '未绑定'"
|
||
disabled
|
||
style="flex: 1"
|
||
/>
|
||
<el-button type="primary" @click="showProductSelector = true" style="margin-left: 8px" :disabled="!!bindForm.good_group_id">选择</el-button>
|
||
<el-button v-if="bindForm.good_id" @click="clearBindProduct" style="margin-left: 4px">清除</el-button>
|
||
</div>
|
||
<div v-if="bindForm.good_group_id" style="font-size: 12px; color: #e6a23c; margin-top: 4px">已绑定商品组,请先清除商品组后再绑定商品</div>
|
||
</el-form-item>
|
||
<el-alert type="info" :closable="false" style="margin-bottom: 12px;">
|
||
<template #title>
|
||
可选择绑定商品组或商品(选择其中一个即可),点击"清除"按钮可取消对应绑定。
|
||
</template>
|
||
</el-alert>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button @click="bindDialogVisible = false">取消</el-button>
|
||
<el-button type="primary" :loading="bindSubmitLoading" @click="submitBind">绑定</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- 生成商品 - 父级商品组选择器 -->
|
||
<ProductGroupSelector
|
||
v-model="showGenerateGroupSelector"
|
||
:current-group-id="generateForm.parent_group_id"
|
||
@confirm="g => { generateForm.parent_group_id = g.id; generateForm._parentGroupName = g.name }"
|
||
/>
|
||
|
||
<!-- 生成商品 - 标签选择器 -->
|
||
<el-dialog v-model="showGenerateTagSelector" title="选择标签" width="560px" append-to-body destroy-on-close>
|
||
<div style="margin-bottom: 12px; display: flex; gap: 8px">
|
||
<el-input v-model="tagKeyword" placeholder="搜索标签名称" clearable style="width: 220px" @input="filterTags">
|
||
<template #prefix><el-icon><Search /></el-icon></template>
|
||
</el-input>
|
||
<el-button :icon="Refresh" @click="() => { tagOptions.value = []; fetchTagOptions() }" :loading="tagLoading">刷新</el-button>
|
||
</div>
|
||
<el-table :data="filteredTagOptions" v-loading="tagLoading" highlight-current-row
|
||
@current-change="row => selectedTagRow = row" :height="300" stripe size="small">
|
||
<el-table-column prop="id" label="ID" width="70" />
|
||
<el-table-column prop="name" label="名称" min-width="160" show-overflow-tooltip />
|
||
</el-table>
|
||
<el-empty v-if="!filteredTagOptions.length && !tagLoading" description="暂无标签" :image-size="60" />
|
||
<template #footer>
|
||
<el-button @click="showGenerateTagSelector = false">取消</el-button>
|
||
<el-button type="primary" :disabled="!selectedTagRow" @click="confirmTagSelect">确定选择</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- 绑定弹窗用商品组选择器 -->
|
||
<ProductGroupSelector
|
||
v-model="showGroupSelector"
|
||
:current-group-id="bindForm.good_group_id"
|
||
@confirm="handleGroupSelected"
|
||
/>
|
||
|
||
<!-- 商品选择器 -->
|
||
<ProductSelector
|
||
v-model="showProductSelector"
|
||
:current-product-id="bindForm.good_id"
|
||
@confirm="handleProductSelected"
|
||
/>
|
||
|
||
<!-- 生成商品弹窗 -->
|
||
<el-dialog v-model="generateDialogVisible" title="根据主机组自动生成商品" width="520px" destroy-on-close>
|
||
<el-alert type="warning" :closable="false" style="margin-bottom: 16px;">
|
||
<template #title>
|
||
此操作将根据所选主机组树自动生成 GoodGroup(商品分组)、Goods(商品)和 Args(参数),请谨慎操作。
|
||
</template>
|
||
</el-alert>
|
||
<el-form ref="generateFormRef" :model="generateForm" :rules="generateFormRules" label-width="120px">
|
||
<el-form-item label="起始主机组ID" prop="id">
|
||
<el-input :model-value="generateForm.id" disabled style="width: 100%" />
|
||
</el-form-item>
|
||
<el-form-item label="父级GoodGroup">
|
||
<div class="bind-selector-row">
|
||
<el-input
|
||
:model-value="generateForm.parent_group_id ? `商品组 #${generateForm.parent_group_id}${generateForm._parentGroupName ? ' - ' + generateForm._parentGroupName : ''}` : '不挂载父级'"
|
||
disabled
|
||
style="flex: 1"
|
||
/>
|
||
<el-button type="primary" @click="showGenerateGroupSelector = true" style="margin-left: 8px">选择</el-button>
|
||
<el-button v-if="generateForm.parent_group_id" @click="generateForm.parent_group_id = 0; generateForm._parentGroupName = ''" style="margin-left: 4px">清除</el-button>
|
||
</div>
|
||
</el-form-item>
|
||
<el-form-item label="标签">
|
||
<div class="bind-selector-row">
|
||
<el-input
|
||
:model-value="generateForm.tag_id ? `标签 #${generateForm.tag_id}${generateForm._tagName ? ' - ' + generateForm._tagName : ''}` : '不设置标签'"
|
||
disabled
|
||
style="flex: 1"
|
||
/>
|
||
<el-button type="primary" @click="showGenerateTagSelector = true" style="margin-left: 8px">选择</el-button>
|
||
<el-button v-if="generateForm.tag_id" @click="generateForm.tag_id = 0; generateForm._tagName = ''" style="margin-left: 4px">清除</el-button>
|
||
</div>
|
||
</el-form-item>
|
||
<el-form-item label="Table标识" prop="table">
|
||
<el-input v-model="generateForm.table" placeholder="如 kvm_service" />
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button @click="generateDialogVisible = false">取消</el-button>
|
||
<el-button type="primary" :loading="generateSubmitLoading" @click="submitGenerate">确定生成</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, reactive, computed, inject, onMounted, watch } from 'vue'
|
||
import { useRoute, useRouter } from 'vue-router'
|
||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||
import { Plus, Refresh, RefreshRight, Search, ArrowLeft } from '@element-plus/icons-vue'
|
||
import {
|
||
getHostGroupList,
|
||
syncHostGroup,
|
||
bindHostGroup,
|
||
updateHostGroup,
|
||
generateGoodsByHostGroup,
|
||
deleteHostGroup,
|
||
getKvmServiceList
|
||
} from '@/api/admin/kvmService'
|
||
import { extractApiError } from '@/utils/kvmErrorUtil'
|
||
import { getProductGroupTagList } from '@/api/admin/product'
|
||
import ProductGroupSelector from '@/components/admin/ProductGroupSelector.vue'
|
||
import ProductSelector from '@/components/admin/ProductSelector.vue'
|
||
import dayjs from 'dayjs'
|
||
|
||
const route = useRoute()
|
||
const router = useRouter()
|
||
const embedded = inject('embedded', false)
|
||
const injectedServiceId = inject('serviceId', null)
|
||
const injectedServiceName = inject('serviceName', null)
|
||
|
||
const selectedServiceId = ref(parseInt(route.query.service_id) || null)
|
||
const serviceOptions = ref([])
|
||
|
||
const serviceId = computed(() => injectedServiceId?.value || selectedServiceId.value || 0)
|
||
const serviceName = computed(() => {
|
||
if (injectedServiceName?.value) return injectedServiceName.value
|
||
const s = serviceOptions.value.find(x => x.id === selectedServiceId.value)
|
||
return s?.name || route.query.service_name || ''
|
||
})
|
||
|
||
const normalizeService = (s) => ({
|
||
id: s.Id ?? s.id,
|
||
name: s.Name ?? s.name,
|
||
host: s.Host ?? s.host,
|
||
port: s.Port ?? s.port,
|
||
note: s.Note ?? s.note
|
||
})
|
||
|
||
const loadServiceOptions = async () => {
|
||
try {
|
||
const res = await getKvmServiceList({ page: 1, count: 10, key: '' })
|
||
if (res?.data?.code === 200 && res?.data?.data) {
|
||
const inner = res.data.data
|
||
const raw = inner.data || inner.list || (Array.isArray(inner) ? inner : [])
|
||
serviceOptions.value = raw.map(normalizeService)
|
||
}
|
||
} catch { /* */ }
|
||
}
|
||
|
||
const handleServiceChange = () => {
|
||
hostGroupList.value = []
|
||
if (serviceId.value) {
|
||
loadHostGroups()
|
||
}
|
||
}
|
||
|
||
const handleRefresh = () => {
|
||
if (!serviceId.value) {
|
||
ElMessage.warning('请先选择主控服务')
|
||
return
|
||
}
|
||
loadHostGroups()
|
||
}
|
||
|
||
const loading = ref(false)
|
||
const syncLoading = ref(false)
|
||
const hostGroupList = ref([])
|
||
const selectedGroup = ref(null)
|
||
|
||
const treeGroupList = computed(() => {
|
||
const items = hostGroupList.value
|
||
if (!items.length) return []
|
||
const map = new Map()
|
||
items.forEach(item => {
|
||
map.set(item.remoteId, { ...item, _rowKey: `g-${item.id}`, _children: [], _hasChildren: false })
|
||
})
|
||
const roots = []
|
||
map.forEach(item => {
|
||
if (item.parentRemoteId && map.has(item.parentRemoteId)) {
|
||
const parent = map.get(item.parentRemoteId)
|
||
parent._children.push(item)
|
||
parent._hasChildren = true
|
||
} else {
|
||
roots.push(item)
|
||
}
|
||
})
|
||
return roots
|
||
})
|
||
|
||
// 规范化后端 PascalCase 字段为前端 camelCase
|
||
// 同时保留原始字段以便在需要时直接访问
|
||
const normalizeHostGroup = (item) => {
|
||
if (!item) return item
|
||
return {
|
||
...item,
|
||
id: item.Id ?? item.id,
|
||
serviceId: item.ServiceId ?? item.serviceId ?? item.service_id,
|
||
remoteId: item.ServiceHostGroupId ?? item.serviceHostGroupId ?? item.remoteId ?? item.remote_id,
|
||
parentRemoteId: item.ServiceParentHostGroupId ?? item.serviceParentHostGroupId ?? item.parentRemoteId ?? item.parent_remote_id ?? 0,
|
||
goodGroupId: item.GoodGroupId ?? item.goodGroupId ?? item.good_group_id ?? 0,
|
||
goodId: item.GoodId ?? item.goodId ?? item.good_id ?? 0,
|
||
name: item.Name ?? item.name,
|
||
note: item.Note ?? item.note,
|
||
CreatedAt: item.CreatedAt ?? item.createdAt ?? item.created_at,
|
||
UpdatedAt: item.UpdatedAt ?? item.updatedAt ?? item.updated_at,
|
||
}
|
||
}
|
||
|
||
// ========== 本地主机组列表 ==========
|
||
const loadHostGroups = async () => {
|
||
if (!serviceId.value) return
|
||
loading.value = true
|
||
try {
|
||
const res = await getHostGroupList({ service_id: serviceId.value })
|
||
const body = res?.data
|
||
console.debug('[HostGroup] list response body:', JSON.stringify(body))
|
||
if (body?.code === 200 && body?.data) {
|
||
// data 可能是直接数组,或 { all_count, data: [...] } 格式
|
||
const items = Array.isArray(body.data) ? body.data : (body.data.data || body.data.list || [])
|
||
hostGroupList.value = items.map(normalizeHostGroup)
|
||
console.debug('[HostGroup] normalized list:', hostGroupList.value)
|
||
} else {
|
||
hostGroupList.value = []
|
||
if (body?.message) {
|
||
ElMessage.warning(body.message)
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('获取本地主机组列表失败:', error)
|
||
ElMessage.error('获取本地主机组列表失败')
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
// ========== 同步 ==========
|
||
const handleSync = async () => {
|
||
if (!serviceId.value) {
|
||
ElMessage.warning('缺少主控服务ID')
|
||
return
|
||
}
|
||
ElMessageBox.confirm('确定要从远程同步主机组数据到本地吗?', '同步确认', {
|
||
confirmButtonText: '确定同步',
|
||
cancelButtonText: '取消',
|
||
type: 'info'
|
||
}).then(async () => {
|
||
syncLoading.value = true
|
||
try {
|
||
const res = await syncHostGroup({ service_id: serviceId.value })
|
||
const body = res?.data
|
||
if (body?.code === 200) {
|
||
const synced = body.data
|
||
const count = Array.isArray(synced) ? synced.length : 0
|
||
ElMessage.success(`同步完成,共同步 ${count} 个主机组`)
|
||
} else {
|
||
ElMessage.warning(body?.message || '同步返回异常')
|
||
}
|
||
loadHostGroups()
|
||
} catch (error) {
|
||
ElMessage.error(extractApiError(error?.response?.data, '同步失败'))
|
||
} finally {
|
||
syncLoading.value = false
|
||
}
|
||
}).catch(() => {})
|
||
}
|
||
|
||
// ========== 编辑 ==========
|
||
const editDialogVisible = ref(false)
|
||
const editSubmitLoading = ref(false)
|
||
const editFormRef = ref(null)
|
||
|
||
const editForm = reactive({
|
||
id: undefined,
|
||
name: '',
|
||
note: ''
|
||
})
|
||
|
||
const editFormRules = {
|
||
name: [{ required: true, message: '请输入名称', trigger: 'blur' }]
|
||
}
|
||
|
||
const handleEditGroup = (row) => {
|
||
Object.assign(editForm, {
|
||
id: Number(row.Id ?? row.id),
|
||
name: row.Name ?? row.name,
|
||
note: row.Note ?? row.note ?? ''
|
||
})
|
||
editDialogVisible.value = true
|
||
}
|
||
|
||
const submitEdit = () => {
|
||
editFormRef.value?.validate(async (valid) => {
|
||
if (!valid) return
|
||
editSubmitLoading.value = true
|
||
try {
|
||
const res = await updateHostGroup({
|
||
id: editForm.id,
|
||
name: editForm.name,
|
||
note: editForm.note
|
||
})
|
||
const body = res?.data
|
||
if (body?.code === 200) {
|
||
ElMessage.success('修改成功')
|
||
editDialogVisible.value = false
|
||
loadHostGroups()
|
||
} else {
|
||
ElMessage.error(extractApiError(body, '修改失败'))
|
||
}
|
||
} catch (error) {
|
||
ElMessage.error(extractApiError(error?.response?.data, '修改失败'))
|
||
} finally {
|
||
editSubmitLoading.value = false
|
||
}
|
||
})
|
||
}
|
||
|
||
// ========== 绑定 ==========
|
||
const bindDialogVisible = ref(false)
|
||
const bindSubmitLoading = ref(false)
|
||
const bindFormRef = ref(null)
|
||
|
||
const bindForm = reactive({
|
||
id: undefined,
|
||
_name: '',
|
||
_groupName: '',
|
||
_goodName: '',
|
||
good_group_id: 0,
|
||
good_id: 0
|
||
})
|
||
|
||
// 选择器弹窗控制
|
||
const showGroupSelector = ref(false)
|
||
const showProductSelector = ref(false)
|
||
|
||
// 商品组选中回调
|
||
const handleGroupSelected = (group) => {
|
||
bindForm.good_group_id = group.id
|
||
bindForm._groupName = group.name || ''
|
||
}
|
||
|
||
// 商品选中回调
|
||
const handleProductSelected = (product) => {
|
||
bindForm.good_id = product.id
|
||
bindForm._goodName = product.name || ''
|
||
}
|
||
|
||
// 清除绑定
|
||
const clearBindGroup = () => {
|
||
bindForm.good_group_id = 0
|
||
bindForm._groupName = ''
|
||
}
|
||
|
||
const clearBindProduct = () => {
|
||
bindForm.good_id = 0
|
||
bindForm._goodName = ''
|
||
}
|
||
|
||
const handleBind = (row) => {
|
||
Object.assign(bindForm, {
|
||
id: Number(row.Id ?? row.id),
|
||
_name: row.Name ?? row.name,
|
||
_groupName: '',
|
||
_goodName: '',
|
||
good_group_id: row.goodGroupId ?? row.GoodGroupId ?? 0,
|
||
good_id: row.goodId ?? row.GoodId ?? 0
|
||
})
|
||
bindDialogVisible.value = true
|
||
}
|
||
|
||
const submitBind = async () => {
|
||
bindSubmitLoading.value = true
|
||
try {
|
||
const res = await bindHostGroup({
|
||
id: bindForm.id,
|
||
good_group_id: bindForm.good_group_id,
|
||
good_id: bindForm.good_id
|
||
})
|
||
const body = res?.data
|
||
if (body?.code === 200) {
|
||
ElMessage.success('绑定成功')
|
||
bindDialogVisible.value = false
|
||
loadHostGroups()
|
||
} else {
|
||
ElMessage.error(extractApiError(body, '绑定失败'))
|
||
}
|
||
} catch (error) {
|
||
ElMessage.error(extractApiError(error?.response?.data, '绑定失败'))
|
||
} finally {
|
||
bindSubmitLoading.value = false
|
||
}
|
||
}
|
||
|
||
// ========== 生成商品 ==========
|
||
const generateDialogVisible = ref(false)
|
||
const generateSubmitLoading = ref(false)
|
||
const generateFormRef = ref(null)
|
||
|
||
const generateForm = reactive({
|
||
id: undefined,
|
||
parent_group_id: 0,
|
||
_parentGroupName: '',
|
||
tag_id: 0,
|
||
_tagName: '',
|
||
table: 'kvm_service'
|
||
})
|
||
|
||
const generateFormRules = {
|
||
id: [{ required: true, message: '主机组ID不能为空', trigger: 'blur' }]
|
||
}
|
||
|
||
// 父级商品组选择器
|
||
const showGenerateGroupSelector = ref(false)
|
||
|
||
// 标签选择器
|
||
const showGenerateTagSelector = ref(false)
|
||
const tagOptions = ref([])
|
||
const tagLoading = ref(false)
|
||
const tagKeyword = ref('')
|
||
const selectedTagRow = ref(null)
|
||
const filteredTagOptions = computed(() =>
|
||
tagKeyword.value
|
||
? tagOptions.value.filter(t => t.name?.includes(tagKeyword.value))
|
||
: tagOptions.value
|
||
)
|
||
const filterTags = () => { /* computed 自动响应 */ }
|
||
const confirmTagSelect = () => {
|
||
if (!selectedTagRow.value) return
|
||
generateForm.tag_id = selectedTagRow.value.id
|
||
generateForm._tagName = selectedTagRow.value.name
|
||
showGenerateTagSelector.value = false
|
||
selectedTagRow.value = null
|
||
tagKeyword.value = ''
|
||
}
|
||
|
||
const loadTagOptions = async () => {
|
||
if (tagOptions.value.length) return
|
||
await fetchTagOptions()
|
||
}
|
||
|
||
const fetchTagOptions = async () => {
|
||
tagLoading.value = true
|
||
try {
|
||
const res = await getProductGroupTagList()
|
||
if (res?.data?.code === 200 && res?.data?.data) {
|
||
const inner = res.data.data
|
||
tagOptions.value = Array.isArray(inner) ? inner : (inner.data || inner.list || [])
|
||
}
|
||
} catch { /* */ } finally { tagLoading.value = false }
|
||
}
|
||
|
||
// 监听标签选择器打开时加载数据
|
||
watch(showGenerateTagSelector, (val) => { if (val) loadTagOptions() })
|
||
|
||
const handleGenerateGoods = (row) => {
|
||
Object.assign(generateForm, {
|
||
id: Number(row.Id ?? row.id),
|
||
parent_group_id: 0,
|
||
_parentGroupName: '',
|
||
tag_id: 0,
|
||
_tagName: '',
|
||
table: 'kvm_service'
|
||
})
|
||
generateDialogVisible.value = true
|
||
}
|
||
|
||
const submitGenerate = () => {
|
||
generateFormRef.value?.validate(async (valid) => {
|
||
if (!valid) return
|
||
|
||
ElMessageBox.confirm('此操作会自动生成商品分组和商品,确定继续吗?', '确认生成', {
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
type: 'warning'
|
||
}).then(async () => {
|
||
generateSubmitLoading.value = true
|
||
try {
|
||
const payload = { id: generateForm.id }
|
||
if (generateForm.parent_group_id) payload.parent_group_id = generateForm.parent_group_id
|
||
if (generateForm.tag_id) payload.tag_id = generateForm.tag_id
|
||
if (generateForm.table) payload.table = generateForm.table
|
||
|
||
const res = await generateGoodsByHostGroup(payload)
|
||
const body = res?.data
|
||
if (body?.code === 200) {
|
||
ElMessage.success('商品生成成功')
|
||
generateDialogVisible.value = false
|
||
loadHostGroups()
|
||
} else {
|
||
ElMessage.error(extractApiError(body, '商品生成失败'))
|
||
}
|
||
} catch (error) {
|
||
ElMessage.error(extractApiError(error?.response?.data, '生成失败'))
|
||
} finally {
|
||
generateSubmitLoading.value = false
|
||
}
|
||
}).catch(() => {})
|
||
})
|
||
}
|
||
|
||
// ========== 删除本地主机组 ==========
|
||
const handleDeleteGroup = (row) => {
|
||
const rawId = Number(row.Id ?? row.id)
|
||
if (!rawId) {
|
||
ElMessage.error('无法获取主机组ID')
|
||
return
|
||
}
|
||
ElMessageBox.confirm(`确定要删除本地主机组「${row.Name ?? row.name}」吗?删除后不可恢复。`, '删除确认', {
|
||
confirmButtonText: '确定删除',
|
||
cancelButtonText: '取消',
|
||
type: 'warning'
|
||
}).then(async () => {
|
||
try {
|
||
const res = await deleteHostGroup({ id: rawId })
|
||
const body = res?.data
|
||
if (body?.code === 200) {
|
||
ElMessage.success('删除成功')
|
||
loadHostGroups()
|
||
} else {
|
||
ElMessage.error(extractApiError(body, '删除失败'))
|
||
}
|
||
} catch (error) {
|
||
ElMessage.error(extractApiError(error?.response?.data, '删除失败'))
|
||
}
|
||
}).catch(() => {})
|
||
}
|
||
|
||
// ========== 返回 ==========
|
||
const goBack = () => {
|
||
router.push('/virtualization/kvm-service')
|
||
}
|
||
|
||
onMounted(() => {
|
||
if (!embedded) loadServiceOptions()
|
||
if (serviceId.value) {
|
||
loadHostGroups()
|
||
}
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
.host-group-mapping-container {
|
||
padding: 20px;
|
||
}
|
||
|
||
.page-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 20px;
|
||
padding-bottom: 16px;
|
||
border-bottom: 1px solid #ebeef5;
|
||
}
|
||
|
||
.header-left {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 16px;
|
||
}
|
||
|
||
.header-info h3 {
|
||
margin: 0;
|
||
font-size: 18px;
|
||
color: #303133;
|
||
}
|
||
|
||
.sub-info {
|
||
font-size: 13px;
|
||
color: #909399;
|
||
}
|
||
|
||
.header-right {
|
||
display: flex;
|
||
gap: 8px;
|
||
}
|
||
|
||
.embedded-toolbar {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.main-panel {
|
||
background: #fff;
|
||
border-radius: 8px;
|
||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
|
||
overflow: hidden;
|
||
}
|
||
|
||
.panel-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 14px 20px;
|
||
border-bottom: 1px solid #ebeef5;
|
||
background: #fafafa;
|
||
}
|
||
|
||
.panel-header h4 {
|
||
margin: 0;
|
||
font-size: 15px;
|
||
color: #303133;
|
||
}
|
||
|
||
.panel-body {
|
||
padding: 16px;
|
||
min-height: 300px;
|
||
}
|
||
|
||
|
||
</style>
|