generated from Leo_Ding/web-template
Merge branch 'master' of https://gitlab.guxuan.icu/Leo_Ding/GPU_Admin
This commit is contained in:
commit
ff9383d79f
@ -1,13 +1,7 @@
|
|||||||
import request from '@/utils/request'
|
import request from '@/utils/request'
|
||||||
|
|
||||||
//获取banner列表
|
//获取审核列表
|
||||||
export const getCertificationsList = (params) => request.basic.get('/api/v1/certifications', params)
|
export const getCertificationsList = (params) => request.basic.get('/api/v1/certifications', params)
|
||||||
|
|
||||||
//获取单个banner
|
// 审核接口
|
||||||
export const getBanner = (id) => request.basic.get(`/api/v1/banners/${id}`)
|
export const updateCertifications = (id, data) => request.basic.put(`/api/v1/certifications/${id}`, data)
|
||||||
//创建banner
|
|
||||||
export const createBanner = (data) => request.basic.post('/api/v1/banners', data)
|
|
||||||
//更新banner
|
|
||||||
export const updateBanner = (id, data) => request.basic.put(`/api/v1/banners/${id}`, data)
|
|
||||||
//删除banner
|
|
||||||
export const deleteBanner = (id) => request.basic.delete(`/api/v1/banners/${id}`)
|
|
||||||
|
|||||||
30
src/apis/modules/resource.js
Normal file
30
src/apis/modules/resource.js
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
//获取计算卡列表
|
||||||
|
export const getCardsList = (params) => request.basic.get('/api/v1/autoDl-dl-cards', params)
|
||||||
|
|
||||||
|
//获取主机资源列表
|
||||||
|
export const getHostsList = (params) => request.basic.get('/api/v1/autoDl-dl-hosts', params)
|
||||||
|
|
||||||
|
// 获取实例卡列表
|
||||||
|
export const getInstancesList = (params) => request.basic.get('/api/v1/auto-dl-instances', params)
|
||||||
|
|
||||||
|
// 黑名单列表
|
||||||
|
export const getBlackCustomersList = (params) => request.basic.get('/api/v1/blackCustomers', params)
|
||||||
|
|
||||||
|
// 移除黑名单
|
||||||
|
export const deleteBlackCustomers = (id) => request.basic.delete(`/api/v1/blackCustomers/${id}`)
|
||||||
|
|
||||||
|
|
||||||
|
// 拉黑用户
|
||||||
|
export const updateCustomers = (id, data) => request.basic.put(`/api/v1/customers/${id}`, data)
|
||||||
|
|
||||||
|
|
||||||
|
//获取单个banner
|
||||||
|
export const getBanner = (id) => request.basic.get(`/api/v1/banners/${id}`)
|
||||||
|
//创建banner
|
||||||
|
export const createBanner = (data) => request.basic.post('/api/v1/banners', data)
|
||||||
|
//更新banner
|
||||||
|
export const updateBanner = (id, data) => request.basic.put(`/api/v1/banners/${id}`, data)
|
||||||
|
//删除banner
|
||||||
|
export const deleteBanner = (id) => request.basic.delete(`/api/v1/banners/${id}`)
|
||||||
@ -1,11 +1,18 @@
|
|||||||
import request from '@/utils/request'
|
import request from '@/utils/request'
|
||||||
|
|
||||||
//获取banner列表
|
//获取用户列表
|
||||||
export const getCustomersList = (params) => request.basic.get('/api/v1/customers', params)
|
export const getCustomersList = (params) => request.basic.get('/api/v1/customers', params)
|
||||||
|
|
||||||
// 黑名单列表
|
// 黑名单列表
|
||||||
export const getBlackCustomersList = (params) => request.basic.get('/api/v1/blackCustomers', params)
|
export const getBlackCustomersList = (params) => request.basic.get('/api/v1/blackCustomers', params)
|
||||||
|
|
||||||
|
// 移除黑名单
|
||||||
|
export const deleteBlackCustomers = (id) => request.basic.delete(`/api/v1/blackCustomers/${id}`)
|
||||||
|
|
||||||
|
|
||||||
|
// 拉黑用户
|
||||||
|
export const updateCustomers = (id, data) => request.basic.put(`/api/v1/customers/${id}`, data)
|
||||||
|
|
||||||
|
|
||||||
//获取单个banner
|
//获取单个banner
|
||||||
export const getBanner = (id) => request.basic.get(`/api/v1/banners/${id}`)
|
export const getBanner = (id) => request.basic.get(`/api/v1/banners/${id}`)
|
||||||
|
|||||||
@ -36,7 +36,9 @@ export default {
|
|||||||
'account.trigger': '触发报错',
|
'account.trigger': '触发报错',
|
||||||
'account.logout': '退出登录',
|
'account.logout': '退出登录',
|
||||||
resource: 'GPU资源管理',
|
resource: 'GPU资源管理',
|
||||||
resourceAdmin: '资源池管理',
|
resourceAdmin: '主机资源列表',
|
||||||
|
resourceCard: '计算卡列表',
|
||||||
|
resourceInstance: '实例卡列表',
|
||||||
resourceStatistics: '资源池统计信息',
|
resourceStatistics: '资源池统计信息',
|
||||||
userControl: '用户管理',
|
userControl: '用户管理',
|
||||||
userList: '用户列表',
|
userList: '用户列表',
|
||||||
|
|||||||
@ -18,12 +18,35 @@ export default [
|
|||||||
name: 'resourceAdmin',
|
name: 'resourceAdmin',
|
||||||
component: 'resource/resourceAdmin/index.vue',
|
component: 'resource/resourceAdmin/index.vue',
|
||||||
meta: {
|
meta: {
|
||||||
title: '资源池管理 ',
|
title: '主机资源列表 ',
|
||||||
isMenu: true,
|
isMenu: true,
|
||||||
keepAlive: true,
|
keepAlive: true,
|
||||||
permission: '*',
|
permission: '*',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'resourceCard',
|
||||||
|
name: 'resourceCard',
|
||||||
|
component: 'resource/resourceCard/index.vue',
|
||||||
|
meta: {
|
||||||
|
title: '计算卡列表 ',
|
||||||
|
isMenu: true,
|
||||||
|
keepAlive: true,
|
||||||
|
permission: '*',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'resourceInstance',
|
||||||
|
name: 'resourceInstance',
|
||||||
|
component: 'resource/resourceInstance/index.vue',
|
||||||
|
meta: {
|
||||||
|
title: '实例卡列表 ',
|
||||||
|
isMenu: true,
|
||||||
|
keepAlive: true,
|
||||||
|
permission: '*',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
path: 'resourceStatistics',
|
path: 'resourceStatistics',
|
||||||
name: 'resourceStatistics',
|
name: 'resourceStatistics',
|
||||||
|
|||||||
@ -0,0 +1,402 @@
|
|||||||
|
<template>
|
||||||
|
<div class="certification-table">
|
||||||
|
<a-table
|
||||||
|
:columns="columns"
|
||||||
|
:data-source="data"
|
||||||
|
:loading="loading"
|
||||||
|
:pagination="pagination"
|
||||||
|
:scroll="{ x: getTableWidth() }"
|
||||||
|
@change="handleTableChange"
|
||||||
|
:row-key="record => record.id"
|
||||||
|
>
|
||||||
|
<template #bodyCell="{ column, record }">
|
||||||
|
<!-- 认证类型 -->
|
||||||
|
<template v-if="column.key === 'certificationType'">
|
||||||
|
<a-tag :color="record.certificationType === 'COMPANY' ? 'orange' : 'green'">
|
||||||
|
{{ getCertificationTypeLabel(record.certificationType) }}
|
||||||
|
</a-tag>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 认证状态 -->
|
||||||
|
<template v-if="column.key === 'certificationStatus'">
|
||||||
|
<div class="status-cell">
|
||||||
|
<a-tag
|
||||||
|
:color="getStatusColor(record.certificationStatus)"
|
||||||
|
class="status-tag"
|
||||||
|
>
|
||||||
|
{{ getStatusLabel(record.certificationStatus) }}
|
||||||
|
</a-tag>
|
||||||
|
<!-- 显示失败原因 -->
|
||||||
|
<div v-if="record.certificationStatus === 'CERTIFICATION_FAILED' && record.fail_reason"
|
||||||
|
class="fail-reason">
|
||||||
|
<a-tooltip :title="record.fail_reason">
|
||||||
|
<info-circle-outlined style="color: #ff4d4f; margin-left: 4px;" />
|
||||||
|
</a-tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 个人认证字段 -->
|
||||||
|
<template v-if="record.certificationType === 'USER'">
|
||||||
|
<template v-if="column.key === 'name'">
|
||||||
|
{{ record.name }}
|
||||||
|
</template>
|
||||||
|
<template v-if="column.key === 'idCard'">
|
||||||
|
{{ record.idCard }}
|
||||||
|
</template>
|
||||||
|
<template v-if="column.key === 'certificationPeriod'">
|
||||||
|
{{ record.certificationStart }} - {{ record.certificationEnd }}
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 企业认证字段 -->
|
||||||
|
<template v-if="record.certificationType === 'COMPANY'">
|
||||||
|
<template v-if="column.key === 'companyName'">
|
||||||
|
<div class="text-truncate" :title="record.companyName">
|
||||||
|
{{ record.companyName }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-if="column.key === 'unifiedSocialCreditCode'">
|
||||||
|
{{ record.unifiedSocialCreditCode }}
|
||||||
|
</template>
|
||||||
|
<template v-if="column.key === 'companyContact'">
|
||||||
|
<div v-if="record.identity === '企业法人'">
|
||||||
|
{{ record.name }}(法人)
|
||||||
|
</div>
|
||||||
|
<div v-else-if="record.identity === '被授权人'">
|
||||||
|
{{ record.name }}(被授权人)
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
{{ record.name }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 操作列 -->
|
||||||
|
<template v-if="column.key === 'action'">
|
||||||
|
<a-space :size="8">
|
||||||
|
<a-button
|
||||||
|
type="link"
|
||||||
|
size="small"
|
||||||
|
@click="$emit('edit', record)"
|
||||||
|
:disabled="record.certificationStatus !== 'CERTIFICATION_DFFILED'"
|
||||||
|
>
|
||||||
|
审核
|
||||||
|
</a-button>
|
||||||
|
<a-button
|
||||||
|
type="link"
|
||||||
|
size="small"
|
||||||
|
@click="$emit('view', record)"
|
||||||
|
>
|
||||||
|
详情
|
||||||
|
</a-button>
|
||||||
|
</a-space>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #emptyText>
|
||||||
|
<div class="empty-content">
|
||||||
|
<a-empty description="暂无认证数据" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</a-table>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { defineProps, defineEmits, computed } from 'vue'
|
||||||
|
import { Empty, Tooltip } from 'ant-design-vue'
|
||||||
|
import { InfoCircleOutlined } from '@ant-design/icons-vue'
|
||||||
|
import { authenticationDict, authenticationTypeDict } from '@/enums/dict'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
data: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
loading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
pagination: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
showType: {
|
||||||
|
type: String,
|
||||||
|
default: 'all', // all, personal, company
|
||||||
|
validator: (value) => ['all', 'personal', 'company'].includes(value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['table-change', 'edit', 'view'])
|
||||||
|
|
||||||
|
// 状态颜色映射
|
||||||
|
const statusColorMap = {
|
||||||
|
PENDING_CERTIFICATION: 'orange',
|
||||||
|
CERTIFICATION_DFFILED: 'blue',
|
||||||
|
CERTIFICATION_PASSED: 'green',
|
||||||
|
CERTIFICATION_FAILED: 'red',
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据显示类型动态计算列
|
||||||
|
const columns = computed(() => {
|
||||||
|
const baseColumns = [
|
||||||
|
{
|
||||||
|
title: '用户名',
|
||||||
|
dataIndex: 'userName',
|
||||||
|
width: 120,
|
||||||
|
ellipsis: true,
|
||||||
|
fixed: 'left'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '手机号',
|
||||||
|
dataIndex: 'phone',
|
||||||
|
width: 130,
|
||||||
|
ellipsis: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '认证类型',
|
||||||
|
dataIndex: 'certificationType',
|
||||||
|
key: 'certificationType',
|
||||||
|
width: 100,
|
||||||
|
filters: authenticationTypeDict.options.map(item => ({
|
||||||
|
text: item.label,
|
||||||
|
value: item.value
|
||||||
|
})),
|
||||||
|
onFilter: (value, record) => record.certificationType === value,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '认证状态',
|
||||||
|
dataIndex: 'certificationStatus',
|
||||||
|
key: 'certificationStatus',
|
||||||
|
width: 140,
|
||||||
|
filters: authenticationDict.options.map(item => ({
|
||||||
|
text: item.label,
|
||||||
|
value: item.value
|
||||||
|
})),
|
||||||
|
onFilter: (value, record) => record.certificationStatus === value,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
// 根据显示类型添加不同的列
|
||||||
|
if (props.showType === 'all') {
|
||||||
|
// 全部认证:显示所有列,用条件渲染控制显示内容
|
||||||
|
return [
|
||||||
|
...baseColumns,
|
||||||
|
{
|
||||||
|
title: '真实姓名/企业名称',
|
||||||
|
dataIndex: 'name',
|
||||||
|
key: 'name',
|
||||||
|
width: 150,
|
||||||
|
ellipsis: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '证件号码/信用代码',
|
||||||
|
dataIndex: 'idCard',
|
||||||
|
key: 'idCard',
|
||||||
|
width: 180,
|
||||||
|
ellipsis: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '有效期/身份',
|
||||||
|
dataIndex: 'certificationPeriod',
|
||||||
|
key: 'certificationPeriod',
|
||||||
|
width: 150,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
key: 'action',
|
||||||
|
fixed: 'right',
|
||||||
|
width: 120,
|
||||||
|
align: 'center'
|
||||||
|
},
|
||||||
|
]
|
||||||
|
} else if (props.showType === 'personal') {
|
||||||
|
// 个人认证:显示个人相关字段
|
||||||
|
return [
|
||||||
|
...baseColumns,
|
||||||
|
{
|
||||||
|
title: '真实姓名',
|
||||||
|
dataIndex: 'name',
|
||||||
|
key: 'name',
|
||||||
|
width: 120,
|
||||||
|
ellipsis: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '身份证号',
|
||||||
|
dataIndex: 'idCard',
|
||||||
|
key: 'idCard',
|
||||||
|
width: 180,
|
||||||
|
ellipsis: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '证件有效期',
|
||||||
|
dataIndex: 'certificationPeriod',
|
||||||
|
key: 'certificationPeriod',
|
||||||
|
width: 180,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '证件类型',
|
||||||
|
dataIndex: 'documentType',
|
||||||
|
key: 'documentType',
|
||||||
|
width: 100,
|
||||||
|
customRender: ({ text }) => text === '身份证' ? text : '其他',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
key: 'action',
|
||||||
|
fixed: 'right',
|
||||||
|
width: 120,
|
||||||
|
align: 'center'
|
||||||
|
},
|
||||||
|
]
|
||||||
|
} else if (props.showType === 'company') {
|
||||||
|
// 企业认证:显示企业相关字段
|
||||||
|
return [
|
||||||
|
...baseColumns,
|
||||||
|
{
|
||||||
|
title: '企业名称',
|
||||||
|
dataIndex: 'companyName',
|
||||||
|
key: 'companyName',
|
||||||
|
width: 200,
|
||||||
|
ellipsis: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '统一信用代码',
|
||||||
|
dataIndex: 'unifiedSocialCreditCode',
|
||||||
|
key: 'unifiedSocialCreditCode',
|
||||||
|
width: 180,
|
||||||
|
ellipsis: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '联系人身份',
|
||||||
|
dataIndex: 'companyContact',
|
||||||
|
key: 'companyContact',
|
||||||
|
width: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '联系人姓名',
|
||||||
|
dataIndex: 'name',
|
||||||
|
key: 'name',
|
||||||
|
width: 120,
|
||||||
|
ellipsis: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '证件类型',
|
||||||
|
dataIndex: 'companyDocumentType',
|
||||||
|
key: 'companyDocumentType',
|
||||||
|
width: 120,
|
||||||
|
customRender: ({ text }) => text === '营业执照' ? text : '其他',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
key: 'action',
|
||||||
|
fixed: 'right',
|
||||||
|
width: 120,
|
||||||
|
align: 'center'
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
return baseColumns
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取表格宽度
|
||||||
|
const getTableWidth = () => {
|
||||||
|
if (props.showType === 'personal') return 900
|
||||||
|
if (props.showType === 'company') return 1100
|
||||||
|
return 800
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取认证类型标签
|
||||||
|
const getCertificationTypeLabel = (type) => {
|
||||||
|
return authenticationTypeDict.getLabel(type) || type
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取状态标签
|
||||||
|
const getStatusLabel = (status) => {
|
||||||
|
return authenticationDict.getLabel(status) || status
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取状态颜色
|
||||||
|
const getStatusColor = (status) => {
|
||||||
|
return statusColorMap[status] || 'default'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 表格变化
|
||||||
|
const handleTableChange = (pagination, filters, sorter) => {
|
||||||
|
emit('table-change', pagination)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.certification-table {
|
||||||
|
:deep(.ant-table) {
|
||||||
|
background: #fff;
|
||||||
|
|
||||||
|
.ant-table-thead > tr > th {
|
||||||
|
background: #fafafa;
|
||||||
|
font-weight: 600;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-tbody > tr > td {
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-tbody > tr:hover > td {
|
||||||
|
background: #fafafa;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-cell {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.status-tag {
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 20px;
|
||||||
|
padding: 0 10px;
|
||||||
|
font-weight: 500;
|
||||||
|
border: none;
|
||||||
|
|
||||||
|
&.ant-tag-orange {
|
||||||
|
background: #fff7e6;
|
||||||
|
color: #fa8c16;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.ant-tag-blue {
|
||||||
|
background: #e6f4ff;
|
||||||
|
color: #1677ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.ant-tag-green {
|
||||||
|
background: #f6ffed;
|
||||||
|
color: #52c41a;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.ant-tag-red {
|
||||||
|
background: #fff2f0;
|
||||||
|
color: #ff4d4f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fail-reason {
|
||||||
|
margin-left: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-truncate {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-content {
|
||||||
|
padding: 48px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -5,32 +5,16 @@
|
|||||||
<a-card class="mb-8-2">
|
<a-card class="mb-8-2">
|
||||||
<a-row :gutter="24">
|
<a-row :gutter="24">
|
||||||
<a-col :span="24">
|
<a-col :span="24">
|
||||||
<a-form-item :label="$t('pages.system.role.form.name')" name="name">
|
<a-form-item label="审核结果" name="check">
|
||||||
<a-input v-model:value="formData.name"></a-input>
|
<a-radio-group v-model:value="formData.check" :options="[
|
||||||
</a-form-item>
|
{ label: '通过', value: true },
|
||||||
</a-col>
|
{ label: '不通过', value: false },
|
||||||
|
]" />
|
||||||
<a-col :span="24">
|
|
||||||
<a-form-item :label="$t('pages.system.role.form.sequence')" name="sequence">
|
|
||||||
<a-input :defaultValue="0" type="number" v-model:value="formData.sequence"></a-input>
|
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :span="24">
|
<a-col :span="24">
|
||||||
<a-form-item :label="$t('pages.system.role.form.status')" name="status">
|
<a-form-item label="审核不通过理由" name="fail_reason">
|
||||||
<a-radio-group v-model:value="formData.status" :options="[
|
<a-textarea v-model:value="formData.fail_reason" :disabled="formData.check === true" />
|
||||||
{ label: $t('pages.system.role.form.status.enabled'), value: 'enabled' },
|
|
||||||
{ label: $t('pages.system.role.form.status.disabled'), value: 'disabled' },
|
|
||||||
]"></a-radio-group>
|
|
||||||
</a-form-item>
|
|
||||||
</a-col>
|
|
||||||
<a-col :span="24">
|
|
||||||
<a-form-item :label="'描述'">
|
|
||||||
<a-textarea v-model:value="formData.description"></a-textarea>
|
|
||||||
</a-form-item>
|
|
||||||
</a-col>
|
|
||||||
<a-col :span="24">
|
|
||||||
<a-form-item :label="'上传图片'" name="permissions">
|
|
||||||
<GxUpload :fileNumber="1" />
|
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
</a-row>
|
</a-row>
|
||||||
@ -40,119 +24,103 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { cloneDeep } from 'lodash-es'
|
|
||||||
import { message } from 'ant-design-vue'
|
import { message } from 'ant-design-vue'
|
||||||
import { ref, watch } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import { config } from '@/config'
|
|
||||||
import apis from '@/apis'
|
import apis from '@/apis'
|
||||||
import { useForm, useModal } from '@/hooks'
|
import { useForm, useModal } from '@/hooks'
|
||||||
import GxUpload from '@/components/GxUpload/index.vue'
|
|
||||||
const emit = defineEmits(['ok'])
|
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
const emit = defineEmits(['ok'])
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
const { modal, showModal, hideModal, showLoading, hideLoading } = useModal()
|
const { modal, showModal, hideModal, showLoading, hideLoading } = useModal()
|
||||||
const { formRecord, formData, formRef, formRules, resetForm } = useForm()
|
const { formData, formRef, resetForm } = useForm()
|
||||||
const { t } = useI18n() // 解构出t方法
|
|
||||||
const cancelText = ref(t('button.cancel'))
|
const cancelText = ref(t('button.cancel'))
|
||||||
const okText = ref(t('button.confirm'))
|
const okText = ref(t('button.confirm'))
|
||||||
formData.value.enabled='enabled'
|
|
||||||
formRules.value = {
|
// 初始化表单数据
|
||||||
name: { required: true, message: t('pages.system.role.form.name.placeholder') },
|
formData.value = {
|
||||||
code: { required: true, message: t('pages.system.role.form.code.placeholder') },
|
check: true,
|
||||||
status: { required: true, message: t('pages.system.role.form.status.placeholder') },
|
fail_reason: '',
|
||||||
|
id: null,
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// 动态校验规则:使用 validator
|
||||||
* 新建
|
const validateFailReason = (rule, value, callback) => {
|
||||||
*/
|
if (formData.value.check === false) {
|
||||||
function handleCreate() {
|
if (!value || value.trim() === '') {
|
||||||
showModal({
|
callback(new Error('请选择不通过时,必须填写不通过理由'))
|
||||||
type: 'create',
|
} else {
|
||||||
title: t('pages.system.role.add'),
|
callback()
|
||||||
})
|
}
|
||||||
|
} else {
|
||||||
|
// 通过时不需要校验
|
||||||
|
callback()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 表单规则
|
||||||
|
const formRules = computed(() => ({
|
||||||
|
check: [{ required: true, message: '请选择审核结果' }],
|
||||||
|
fail_reason: [{ validator: validateFailReason, trigger: 'blur' }],
|
||||||
|
}))
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 编辑
|
* 编辑(审核)
|
||||||
*/
|
*/
|
||||||
async function handleEdit(record = {}) {
|
async function handleEdit(id) {
|
||||||
showModal({
|
showModal({
|
||||||
type: 'edit',
|
type: 'edit',
|
||||||
title: t('pages.system.role.edit'),
|
title: '用户审核',
|
||||||
})
|
})
|
||||||
|
formData.value.id = id
|
||||||
const { data, success } = await apis.role.getRole(record.id).catch()
|
// 可选:如果需要回显原数据,可在此处调用 API 获取详情并赋值
|
||||||
if (!success) {
|
// 比如:formData.value = await fetchCertificationDetail(id)
|
||||||
message.error(t('component.message.error.save'))
|
|
||||||
hideModal()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let menus = []
|
|
||||||
if (data.menus) {
|
|
||||||
for (let item of data.menus) {
|
|
||||||
menus.push(item.menu_id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
checkedKeys.value = menus
|
|
||||||
formRecord.value = data
|
|
||||||
formData.value = cloneDeep(data)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 确定
|
* 确定
|
||||||
*/
|
*/
|
||||||
function handleOk() {
|
async function handleOk() {
|
||||||
formRef.value
|
|
||||||
.validateFields()
|
|
||||||
.then(async (values) => {
|
|
||||||
try {
|
try {
|
||||||
|
await formRef.value.validateFields()
|
||||||
showLoading()
|
showLoading()
|
||||||
const params = {...values}
|
|
||||||
let result = null
|
const params = {
|
||||||
switch (modal.value.type) {
|
check: formData.value.check,
|
||||||
case 'create':
|
fail_reason: formData.value.check ? null : formData.value.fail_reason?.trim(),
|
||||||
result = await apis.banner.createBanner(params).catch(() => {
|
|
||||||
throw new Error()
|
|
||||||
})
|
|
||||||
break
|
|
||||||
case 'edit':
|
|
||||||
result = await apis.banner.updateBanner(formData.value.id, params).catch(() => {
|
|
||||||
throw new Error()
|
|
||||||
})
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const result = await apis.certifications.updateCertifications(formData.value.id, params)
|
||||||
|
|
||||||
hideLoading()
|
hideLoading()
|
||||||
if (config('http.code.success') === result?.success) {
|
if (result?.success) {
|
||||||
|
message.success('操作成功')
|
||||||
hideModal()
|
hideModal()
|
||||||
emit('ok')
|
emit('ok')
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
hideLoading()
|
hideLoading()
|
||||||
|
// validateFields 失败会抛出异常,但已被 catch,无需额外处理
|
||||||
}
|
}
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
hideLoading()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 取消
|
|
||||||
*/
|
|
||||||
function handleCancel() {
|
function handleCancel() {
|
||||||
hideModal()
|
hideModal()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 关闭后
|
|
||||||
*/
|
|
||||||
function onAfterClose() {
|
function onAfterClose() {
|
||||||
resetForm()
|
resetForm()
|
||||||
hideLoading()
|
// 重置表单数据(可选)
|
||||||
|
formData.value = {
|
||||||
|
check: true,
|
||||||
|
fail_reason: '',
|
||||||
|
id: null,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
handleCreate,
|
|
||||||
handleEdit,
|
handleEdit,
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less" scoped></style>
|
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
|
<div class="certification-management">
|
||||||
<x-search-bar class="mb-8-2">
|
<x-search-bar class="mb-8-2">
|
||||||
<template #default="{ gutter, colSpan }">
|
<template #default="{ gutter, colSpan }">
|
||||||
<a-form :model="searchFormData" layout="inline" :label-col="{ style: { width: '100px' } }">
|
<a-form :model="searchFormData" layout="inline" :label-col="{ style: { width: '100px' } }">
|
||||||
<a-row :gutter="gutter" :wrap="false">
|
<a-row :gutter="gutter" :wrap="false">
|
||||||
<!-- 每个字段占 6 栅格,共 4 个 -->
|
|
||||||
<a-col :span="6">
|
<a-col :span="6">
|
||||||
<a-form-item label="用户姓名" name="userName">
|
<a-form-item label="用户姓名" name="userName">
|
||||||
<a-input v-model:value="searchFormData.userName" placeholder="请输入用户姓名" />
|
<a-input v-model:value="searchFormData.userName" placeholder="请输入用户姓名" />
|
||||||
@ -11,20 +11,9 @@
|
|||||||
</a-col>
|
</a-col>
|
||||||
|
|
||||||
<a-col :span="6">
|
<a-col :span="6">
|
||||||
<a-form-item label="认证状态" name="status">
|
<a-form-item label="认证类型" name="certificationType">
|
||||||
|
<a-select v-model:value="searchFormData.certificationType" placeholder="请选择认证类型">
|
||||||
<a-select v-model:value="searchFormData.status" placeholder="请选择认证状态">
|
<a-select-option value="">全部类型</a-select-option>
|
||||||
<a-select-option v-for="item in authenticationDict.options" :key="item.value"
|
|
||||||
:value="item.value">
|
|
||||||
{{ item.label }}
|
|
||||||
</a-select-option>
|
|
||||||
</a-select>
|
|
||||||
</a-form-item>
|
|
||||||
</a-col>
|
|
||||||
|
|
||||||
<a-col :span="6">
|
|
||||||
<a-form-item label="认证类型" name="ctfType">
|
|
||||||
<a-select v-model:value="searchFormData.ctfType" placeholder="请选择认证类型">
|
|
||||||
<a-select-option v-for="item in authenticationTypeDict.options" :key="item.value"
|
<a-select-option v-for="item in authenticationTypeDict.options" :key="item.value"
|
||||||
:value="item.value">
|
:value="item.value">
|
||||||
{{ item.label }}
|
{{ item.label }}
|
||||||
@ -33,7 +22,20 @@
|
|||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
|
|
||||||
<!-- 操作按钮列:右对齐,占 6 栅格 -->
|
<a-col :span="6">
|
||||||
|
<a-form-item label="认证状态" name="status">
|
||||||
|
<a-select v-model:value="searchFormData.status" placeholder="请选择认证状态">
|
||||||
|
<a-select-option value="">全部状态</a-select-option>
|
||||||
|
<a-select-option v-for="item in authenticationDict.options" :key="item.value"
|
||||||
|
:value="item.value">
|
||||||
|
<a-tag :color="getStatusColor(item.value)" style="margin: 0; border: none;">
|
||||||
|
{{ item.label }}
|
||||||
|
</a-tag>
|
||||||
|
</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
|
||||||
<a-col :span="6" class="align-right">
|
<a-col :span="6" class="align-right">
|
||||||
<a-space>
|
<a-space>
|
||||||
<a-button @click="handleResetSearch">{{ $t('button.reset') }}</a-button>
|
<a-button @click="handleResetSearch">{{ $t('button.reset') }}</a-button>
|
||||||
@ -47,91 +49,97 @@
|
|||||||
</template>
|
</template>
|
||||||
</x-search-bar>
|
</x-search-bar>
|
||||||
|
|
||||||
<a-row :gutter="8" :wrap="false">
|
<!-- 普通横线标签页 -->
|
||||||
<a-col flex="auto">
|
<div class="table-container">
|
||||||
<a-card type="flex">
|
<a-tabs v-model:activeKey="activeTab" type="line" @change="handleTabChange">
|
||||||
<x-action-bar class="mb-8-2">
|
<a-tab-pane key="all" tab="全部认证">
|
||||||
<!-- <a-button v-action="'add'" type="primary" @click="$refs.editDialogRef.handleCreate()">
|
<CertificationTable
|
||||||
<template #icon>
|
:data="allData"
|
||||||
<plus-outlined></plus-outlined>
|
:loading="loading"
|
||||||
</template>
|
:pagination="paginationState"
|
||||||
添加图片
|
@table-change="onTableChange"
|
||||||
</a-button> -->
|
@edit="handleEdit"
|
||||||
</x-action-bar>
|
@view="handleViewDetail"
|
||||||
<a-table :columns="columns" :data-source="listData" :loading="loading" :pagination="paginationState"
|
:show-type="'all'"
|
||||||
:scroll="{ x: 1000 }" @change="onTableChange">
|
/>
|
||||||
<template #bodyCell="{ column, record }">
|
</a-tab-pane>
|
||||||
<!-- <template v-if="'action' === column.key">
|
|
||||||
<x-action-button @click="$refs.editDialogRef.handleEdit(record)">
|
<a-tab-pane key="USER" tab="个人认证">
|
||||||
<a-tooltip>
|
<CertificationTable
|
||||||
<template #title> {{ $t('pages.system.role.edit') }}</template>
|
:data="personalData"
|
||||||
<edit-outlined />
|
:loading="loading"
|
||||||
</a-tooltip>
|
:pagination="paginationState"
|
||||||
</x-action-button>
|
@table-change="onTableChange"
|
||||||
<x-action-button @click="handleRemove(record)">
|
@edit="handleEdit"
|
||||||
<a-tooltip>
|
@view="handleViewDetail"
|
||||||
<template #title> {{ $t('pages.system.delete') }}</template>
|
:show-type="'personal'"
|
||||||
<delete-outlined style="color: #ff4d4f" />
|
/>
|
||||||
</a-tooltip>
|
</a-tab-pane>
|
||||||
</x-action-button>
|
|
||||||
</template> -->
|
<a-tab-pane key="COMPANY" tab="企业认证">
|
||||||
</template>
|
<CertificationTable
|
||||||
</a-table>
|
:data="companyData"
|
||||||
</a-card>
|
:loading="loading"
|
||||||
</a-col>
|
:pagination="paginationState"
|
||||||
</a-row>
|
@table-change="onTableChange"
|
||||||
|
@edit="handleEdit"
|
||||||
|
@view="handleViewDetail"
|
||||||
|
:show-type="'company'"
|
||||||
|
/>
|
||||||
|
</a-tab-pane>
|
||||||
|
</a-tabs>
|
||||||
|
</div>
|
||||||
|
|
||||||
<edit-dialog ref="editDialogRef" @ok="onOk"></edit-dialog>
|
<edit-dialog ref="editDialogRef" @ok="onOk"></edit-dialog>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { message, Modal } from 'ant-design-vue'
|
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
import { message, Tabs } from 'ant-design-vue'
|
||||||
import apis from '@/apis'
|
import apis from '@/apis'
|
||||||
import { formatUtcDateTime } from '@/utils/util'
|
import { usePagination } from '@/hooks'
|
||||||
import { config } from '@/config'
|
|
||||||
import { statusTypeEnum } from '@/enums/system'
|
|
||||||
import { usePagination, useForm } from '@/hooks'
|
|
||||||
import EditDialog from './components/EditDialog.vue'
|
import EditDialog from './components/EditDialog.vue'
|
||||||
import { PlusOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons-vue'
|
import CertificationTable from './components/CertificationTable.vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { authenticationDict, authenticationTypeDict } from '@/enums/dict'
|
import { authenticationDict, authenticationTypeDict } from '@/enums/dict'
|
||||||
|
|
||||||
defineOptions({
|
const ATabs = Tabs
|
||||||
name: 'systemRole',
|
const ATabPane = Tabs.TabPane
|
||||||
})
|
|
||||||
const { t } = useI18n() // 解构出t方法
|
|
||||||
const columns = [
|
|
||||||
// { title: '序号', dataIndex: 'id', width: 200 },
|
|
||||||
{ title: '用户名', dataIndex: 'userName', width: 150 },
|
|
||||||
{ title: '手机号', dataIndex: 'phone', key: 'phone', width: 150 },
|
|
||||||
{
|
|
||||||
title: '认证类型',
|
|
||||||
dataIndex: 'certificationType',
|
|
||||||
key: 'certificationType',
|
|
||||||
width: 120,
|
|
||||||
customRender: ({ text }) => authenticationTypeDict.getLabel(text) || text,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '认证状态',
|
|
||||||
dataIndex: 'certificationStatus',
|
|
||||||
key: 'certificationStatus',
|
|
||||||
width: 120,
|
|
||||||
customRender: ({ text }) => authenticationDict.getLabel(text) || text,
|
|
||||||
},
|
|
||||||
{ title: t('button.action'), key: 'action', fixed: 'right', width: 120 },
|
|
||||||
]
|
|
||||||
|
|
||||||
const { listData, loading, showLoading, hideLoading, paginationState, searchFormData, resetPagination } =
|
defineOptions({
|
||||||
usePagination()
|
name: 'certificationManagement',
|
||||||
const { resetForm } = useForm()
|
})
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
// 状态颜色映射
|
||||||
|
const statusColorMap = {
|
||||||
|
PENDING_CERTIFICATION: 'orange',
|
||||||
|
CERTIFICATION_DFFILED: 'blue',
|
||||||
|
CERTIFICATION_PASSED: 'green',
|
||||||
|
CERTIFICATION_FAILED: 'red',
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应式数据
|
||||||
|
const activeTab = ref('all')
|
||||||
|
const allData = ref([])
|
||||||
|
const personalData = ref([])
|
||||||
|
const companyData = ref([])
|
||||||
const editDialogRef = ref()
|
const editDialogRef = ref()
|
||||||
|
|
||||||
|
const { listData, loading, showLoading, hideLoading, paginationState, searchFormData, resetPagination } = usePagination()
|
||||||
|
|
||||||
|
// 获取状态颜色
|
||||||
|
const getStatusColor = (status) => {
|
||||||
|
return statusColorMap[status] || 'default'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始加载
|
||||||
getPageList()
|
getPageList()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取用户列表
|
* 获取用户列表
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
*/
|
||||||
async function getPageList() {
|
async function getPageList() {
|
||||||
try {
|
try {
|
||||||
@ -148,41 +156,47 @@ async function getPageList() {
|
|||||||
})
|
})
|
||||||
hideLoading()
|
hideLoading()
|
||||||
if (data.length > 0) {
|
if (data.length > 0) {
|
||||||
listData.value = data
|
allData.value = data
|
||||||
|
// 按认证类型分组
|
||||||
|
personalData.value = data.filter(item => item.certificationType === 'USER')
|
||||||
|
companyData.value = data.filter(item => item.certificationType === 'COMPANY')
|
||||||
paginationState.total = total
|
paginationState.total = total
|
||||||
|
} else {
|
||||||
|
allData.value = []
|
||||||
|
personalData.value = []
|
||||||
|
companyData.value = []
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
hideLoading()
|
hideLoading()
|
||||||
|
console.error('获取认证列表失败:', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 移除
|
* 标签页切换
|
||||||
*/
|
*/
|
||||||
function handleRemove({ id }) {
|
function handleTabChange(activeKey) {
|
||||||
Modal.confirm({
|
activeTab.value = activeKey
|
||||||
title: t('pages.system.role.delTip'),
|
}
|
||||||
content: t('button.confirm'),
|
|
||||||
okText: t('button.confirm'),
|
/**
|
||||||
onOk: () => {
|
* 编辑按钮点击
|
||||||
return new Promise((resolve, reject) => {
|
*/
|
||||||
; (async () => {
|
function handleEdit(record) {
|
||||||
try {
|
if (record.certificationStatus !== 'CERTIFICATION_DFFILED') {
|
||||||
const { success } = await apis.role.delRole(id).catch(() => {
|
message.warning('该用户认证状态不是已提交,不能操作')
|
||||||
throw new Error()
|
return
|
||||||
})
|
|
||||||
if (config('http.code.success') === success) {
|
|
||||||
resolve()
|
|
||||||
message.success(t('component.message.success.delete'))
|
|
||||||
await getPageList()
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
editDialogRef.value?.handleEdit(record.id)
|
||||||
reject()
|
}
|
||||||
}
|
|
||||||
})()
|
/**
|
||||||
})
|
* 查看详情
|
||||||
},
|
*/
|
||||||
})
|
function handleViewDetail(record) {
|
||||||
|
// 可以弹出详情模态框,展示完整的认证信息
|
||||||
|
console.log('查看详情:', record)
|
||||||
|
// 这里可以调用详情对话框组件
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -207,8 +221,7 @@ function handleResetSearch() {
|
|||||||
* 搜索
|
* 搜索
|
||||||
*/
|
*/
|
||||||
function handleSearch() {
|
function handleSearch() {
|
||||||
// resetForm()
|
resetPagination()
|
||||||
// resetPagination()
|
|
||||||
getPageList()
|
getPageList()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -220,4 +233,63 @@ async function onOk() {
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less" scoped></style>
|
<style lang="less" scoped>
|
||||||
|
.certification-management {
|
||||||
|
.table-container {
|
||||||
|
background: #fff;
|
||||||
|
padding: 16px 24px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.03),
|
||||||
|
0 1px 6px -1px rgba(0, 0, 0, 0.02),
|
||||||
|
0 2px 4px 0 rgba(0, 0, 0, 0.02);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 标签页样式
|
||||||
|
:deep(.ant-tabs) {
|
||||||
|
.ant-tabs-nav {
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-tabs-tab {
|
||||||
|
padding: 12px 0;
|
||||||
|
margin: 0 32px 0 0;
|
||||||
|
font-size: 14px;
|
||||||
|
color: rgba(0, 0, 0, 0.65);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #1890ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.ant-tabs-tab-active .ant-tabs-tab-btn {
|
||||||
|
color: #1890ff;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-tabs-ink-bar {
|
||||||
|
background: #1890ff;
|
||||||
|
height: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-tabs-content {
|
||||||
|
min-height: 400px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.align-right {
|
||||||
|
text-align: right;
|
||||||
|
|
||||||
|
.ant-space {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mb-8-2 {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -1,249 +1,356 @@
|
|||||||
<template>
|
<template>
|
||||||
<x-search-bar class="mb-8-2">
|
<x-search-bar class="mb-8-2">
|
||||||
<template #default="{ gutter, colSpan }">
|
<template #default="{ gutter, colSpan }">
|
||||||
<a-form
|
<a-form :model="searchFormData" layout="inline">
|
||||||
:label-col="{ style: { width: '100px' } }"
|
|
||||||
:model="searchFormData"
|
|
||||||
layout="inline">
|
|
||||||
<a-row :gutter="gutter">
|
<a-row :gutter="gutter">
|
||||||
<a-col v-bind="colSpan">
|
<a-col v-bind="colSpan">
|
||||||
<a-form-item name="title">
|
<a-form-item label="主机名称/ID" name="machine_name">
|
||||||
<template #label>
|
<a-input placeholder="请输入主机名称/ID" v-model:value="searchFormData.machine_name"></a-input>
|
||||||
规则名称
|
|
||||||
<a-tooltip title="规则名称是唯一的 key">
|
|
||||||
<question-circle-outlined class="ml-4-1 color-placeholder" />
|
|
||||||
</a-tooltip>
|
|
||||||
</template>
|
|
||||||
<a-input v-model:value="searchFormData.title"></a-input>
|
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col v-bind="colSpan">
|
<a-col v-bind="colSpan">
|
||||||
<a-form-item label="描述">
|
<a-form-item label="内网IP" name="internal_ip">
|
||||||
<a-input></a-input>
|
<a-input placeholder="请输入内网IP" v-model:value="searchFormData.internal_ip"></a-input>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
<template v-if="searchBarExpand">
|
<a-col class="align-right" v-bind="colSpan">
|
||||||
<a-col v-bind="colSpan">
|
|
||||||
<a-form-item label="服务调用次数">
|
|
||||||
<a-input></a-input>
|
|
||||||
</a-form-item>
|
|
||||||
</a-col>
|
|
||||||
<a-col v-bind="colSpan">
|
|
||||||
<a-form-item label="状态">
|
|
||||||
<a-select></a-select>
|
|
||||||
</a-form-item>
|
|
||||||
</a-col>
|
|
||||||
<a-col v-bind="colSpan">
|
|
||||||
<a-form-item label="上次调度时间">
|
|
||||||
<a-date-picker placeholder=""></a-date-picker>
|
|
||||||
</a-form-item>
|
|
||||||
</a-col>
|
|
||||||
</template>
|
|
||||||
<a-col
|
|
||||||
class="align-right"
|
|
||||||
v-bind="colSpan">
|
|
||||||
<a-space>
|
<a-space>
|
||||||
<a-button>重置</a-button>
|
<a-button @click="handleResetSearch">重置</a-button>
|
||||||
<a-button
|
<a-button ghost type="primary" @click="handleSearch">
|
||||||
ghost
|
|
||||||
type="primary"
|
|
||||||
@click="handleSearch">
|
|
||||||
搜索
|
搜索
|
||||||
</a-button>
|
</a-button>
|
||||||
<a @click="() => (searchBarExpand = !searchBarExpand)">
|
|
||||||
展开
|
|
||||||
<template v-if="searchBarExpand">
|
|
||||||
<up-outlined :style="{ fontSize: '12px' }"></up-outlined>
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<down-outlined :style="{ fontSize: '12px' }"></down-outlined>
|
|
||||||
</template>
|
|
||||||
</a>
|
|
||||||
</a-space>
|
</a-space>
|
||||||
</a-col>
|
</a-col>
|
||||||
</a-row>
|
</a-row>
|
||||||
</a-form>
|
</a-form>
|
||||||
</template>
|
</template>
|
||||||
</x-search-bar>
|
</x-search-bar>
|
||||||
<a-card>
|
<a-row :gutter="8" :wrap="false">
|
||||||
<x-action-bar class="mb-8-2">
|
<a-col flex="auto">
|
||||||
<a-button
|
<a-card type="flex">
|
||||||
type="primary"
|
|
||||||
@click="$refs.editDialogRef.handleCreate()">
|
|
||||||
<template #icon>
|
|
||||||
<plus-outlined></plus-outlined>
|
|
||||||
</template>
|
|
||||||
新建
|
|
||||||
</a-button>
|
|
||||||
<template #extra>
|
|
||||||
<a-space>
|
|
||||||
<a-tooltip title="刷新">
|
|
||||||
<a-button
|
|
||||||
type="text"
|
|
||||||
@click="handleSearch">
|
|
||||||
<template #icon>
|
|
||||||
<reload-outlined></reload-outlined>
|
|
||||||
</template>
|
|
||||||
</a-button>
|
|
||||||
</a-tooltip>
|
|
||||||
<a-dropdown>
|
|
||||||
<a-tooltip title="密度">
|
|
||||||
<a-button type="text">
|
|
||||||
<template #icon>
|
|
||||||
<column-height-outlined></column-height-outlined>
|
|
||||||
</template>
|
|
||||||
</a-button>
|
|
||||||
</a-tooltip>
|
|
||||||
<template #overlay>
|
|
||||||
<a-menu
|
|
||||||
:selectedKeys="[size]"
|
|
||||||
@click="handleSize">
|
|
||||||
<a-menu-item key="default">默认</a-menu-item>
|
|
||||||
<a-menu-item key="middle">中等</a-menu-item>
|
|
||||||
<a-menu-item key="small">紧凑</a-menu-item>
|
|
||||||
</a-menu>
|
|
||||||
</template>
|
|
||||||
</a-dropdown>
|
|
||||||
<a-tooltip title="设置">
|
|
||||||
<a-button type="text">
|
|
||||||
<template #icon>
|
|
||||||
<setting-outlined></setting-outlined>
|
|
||||||
</template>
|
|
||||||
</a-button>
|
|
||||||
</a-tooltip>
|
|
||||||
</a-space>
|
|
||||||
</template>
|
|
||||||
</x-action-bar>
|
|
||||||
<a-table
|
<a-table
|
||||||
:columns="columns"
|
:columns="columns" :data-source="listData" :loading="loading" :pagination="paginationState"
|
||||||
:data-source="listData"
|
:scroll="{ x: 1400 }"
|
||||||
:loading="loading"
|
|
||||||
:pagination="paginationState"
|
|
||||||
:size="size"
|
|
||||||
row-key="id"
|
|
||||||
@change="onTableChange">
|
@change="onTableChange">
|
||||||
<template #bodyCell="{ column, record }">
|
<template #bodyCell="{ column, record }">
|
||||||
<template v-if="'action' === column.key">
|
<!-- 健康状态 -->
|
||||||
<x-action-button @click="$refs.editDialogRef.handleEdit(record)">编辑</x-action-button>
|
<template v-if="'health_status' === column.key">
|
||||||
<x-action-button @click="handleDelete(record)">删除</x-action-button>
|
<a-tag :color="getHealthStatusColor(record.health_status)">
|
||||||
<x-action-button>
|
{{ getHealthStatusText(record.health_status) }}
|
||||||
<a-dropdown :trigger="['click']">
|
</a-tag>
|
||||||
<more-outlined></more-outlined>
|
|
||||||
<template #overlay>
|
|
||||||
<a-menu>
|
|
||||||
<a-menu-item>菜单1</a-menu-item>
|
|
||||||
<a-menu-item>菜单2</a-menu-item>
|
|
||||||
<a-menu-item>菜单3</a-menu-item>
|
|
||||||
</a-menu>
|
|
||||||
</template>
|
</template>
|
||||||
</a-dropdown>
|
|
||||||
</x-action-button>
|
<!-- 状态 -->
|
||||||
|
<template v-if="'online_status' === column.key">
|
||||||
|
<a-tag :color="getOnlineStatusColor(record.online_status)">
|
||||||
|
{{ getOnlineStatusText(record.online_status) }}
|
||||||
|
</a-tag>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 主机配置 -->
|
||||||
|
<template v-if="'cpu_config' === column.key">
|
||||||
|
<div>{{ record.cpu_num }}核</div>
|
||||||
|
<div>{{ formatMemory(record.memory) }}</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 磁盘空间 -->
|
||||||
|
<template v-if="'disk_space' === column.key">
|
||||||
|
<div v-if="record.disks && record.disks.length > 0">
|
||||||
|
<div v-for="disk in record.disks" :key="disk.disk_path">
|
||||||
|
{{ disk.disk_path }}: {{ formatDiskSpace(disk.idle) }}/{{ formatDiskSpace(disk.total) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else>无磁盘信息</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 硬件平台 -->
|
||||||
|
<template v-if="'hardware_platform' === column.key">
|
||||||
|
<div>{{ record.chip_corp?.toUpperCase() || 'NVIDIA' }}</div>
|
||||||
|
<div>{{ record.cpu_arch?.toUpperCase() || 'x86' }}</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 算力型号 -->
|
||||||
|
<template v-if="'gpu_info' === column.key">
|
||||||
|
<div>{{ record.gpu_name }}</div>
|
||||||
|
<div>driver={{ record.driver_version }}</div>
|
||||||
|
<div>cuda={{ record.cuda_version }}</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 空闲算力 -->
|
||||||
|
<template v-if="'gpu_idle' === column.key">
|
||||||
|
<div>{{ record.gpu?.idle || 0 }}/{{ record.gpu?.total || 0 }}</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- CPU内存分配 -->
|
||||||
|
<template v-if="'cpu_mem_per_gpu' === column.key">
|
||||||
|
<div>{{ record.cpu_per_gpu }}核/GPU</div>
|
||||||
|
<div>{{ formatMemory(record.mem_per_gpu) }}/GPU</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 实例数量 -->
|
||||||
|
<template v-if="'instance_count' === column.key">
|
||||||
|
<div>{{ record.binding_instance_num || 0 }}</div>
|
||||||
|
<div v-if="record.machine_instance_limit > 0">
|
||||||
|
限额: {{ record.machine_instance_limit }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 录入时间 -->
|
||||||
|
<template v-if="'created_at' === column.key">
|
||||||
|
{{ formatDateTime(record.created_at) }}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 可见性 -->
|
||||||
|
<template v-if="'visibility' === column.key">
|
||||||
|
<a-tag :color="record.user_visible_limit ? 'red' : 'green'">
|
||||||
|
{{ record.user_visible_limit ? '受限' : '无限制' }}
|
||||||
|
</a-tag>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 操作 -->
|
||||||
|
<template v-if="'action' === column.key">
|
||||||
|
<a-space>
|
||||||
|
<a-button type="link" size="small" @click="$refs.editDialogRef.handleEdit(record)">
|
||||||
|
编辑
|
||||||
|
</a-button>
|
||||||
|
<a-button type="link" size="small" danger @click="handleDownShelf(record)">
|
||||||
|
下架
|
||||||
|
</a-button>
|
||||||
|
<a-button type="link" size="small" danger @click="handleRemove(record)">
|
||||||
|
删除
|
||||||
|
</a-button>
|
||||||
|
</a-space>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</a-table>
|
</a-table>
|
||||||
</a-card>
|
</a-card>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
|
||||||
<edit-dialog
|
<edit-dialog ref="editDialogRef" @ok="onOk"></edit-dialog>
|
||||||
ref="editDialogRef"
|
|
||||||
@ok="onOk" />
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { message, Modal } from 'ant-design-vue'
|
import { message, Modal } from 'ant-design-vue'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import {
|
|
||||||
ColumnHeightOutlined,
|
|
||||||
DownOutlined,
|
|
||||||
QuestionCircleOutlined,
|
|
||||||
ReloadOutlined,
|
|
||||||
SettingOutlined,
|
|
||||||
UpOutlined,
|
|
||||||
PlusOutlined,
|
|
||||||
MoreOutlined,
|
|
||||||
} from '@ant-design/icons-vue'
|
|
||||||
import apis from '@/apis'
|
import apis from '@/apis'
|
||||||
|
import { formatUtcDateTime } from '@/utils/util'
|
||||||
import { config } from '@/config'
|
import { config } from '@/config'
|
||||||
import { usePagination } from '@/hooks'
|
import { usePagination, useForm } from '@/hooks'
|
||||||
import EditDialog from './components/EditDialog.vue'
|
import EditDialog from './components/EditDialog.vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'listTable',
|
name: 'hostManagement',
|
||||||
})
|
})
|
||||||
|
|
||||||
const columns = [
|
const { t } = useI18n()
|
||||||
{ title: '区域ID', dataIndex: 'hostId' },
|
|
||||||
{ title: '区域名称', dataIndex: 'hostName' },
|
|
||||||
{ title: '区域编码', dataIndex: 'regionId' },
|
|
||||||
{ title: '数据中心地址', dataIndex: 'cpu' },
|
|
||||||
{ title: '状态', dataIndex: 'memory' },
|
|
||||||
{ title: '部署主机数量(主机数量)', dataIndex: 'storage' },
|
|
||||||
{ title: '可用计算卡总数', dataIndex: 'ip' },
|
|
||||||
{ title: '操作', key: 'action', width: 160 },
|
|
||||||
]
|
|
||||||
// const { listData, paginationState, loading, showLoading, hideLoading, resetPagination, searchFormData } =
|
|
||||||
// usePagination()
|
|
||||||
const listData = ref([])
|
|
||||||
const editDialogRef = ref()
|
|
||||||
const searchBarExpand = ref(false)
|
|
||||||
const size = ref('default')
|
|
||||||
|
|
||||||
// getPageList()
|
// 表格列定义
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: '主机名称/ID',
|
||||||
|
dataIndex: 'machine_name',
|
||||||
|
key: 'machine_name',
|
||||||
|
width: 140,
|
||||||
|
fixed: 'left'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '内网IP',
|
||||||
|
dataIndex: 'internal_ip',
|
||||||
|
key: 'internal_ip',
|
||||||
|
width: 110
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '健康状态',
|
||||||
|
key: 'health_status',
|
||||||
|
width: 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '状态',
|
||||||
|
key: 'online_status',
|
||||||
|
width: 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '主机配置',
|
||||||
|
key: 'cpu_config',
|
||||||
|
width: 120
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '磁盘空间(GB)',
|
||||||
|
key: 'disk_space',
|
||||||
|
width: 150
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '硬件平台',
|
||||||
|
key: 'hardware_platform',
|
||||||
|
width: 120
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '算力型号',
|
||||||
|
key: 'gpu_info',
|
||||||
|
width: 200
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '空闲算力',
|
||||||
|
key: 'gpu_idle',
|
||||||
|
width: 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'CPU内存分配',
|
||||||
|
key: 'cpu_mem_per_gpu',
|
||||||
|
width: 120
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '实例数量',
|
||||||
|
key: 'instance_count',
|
||||||
|
width: 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '录入时间',
|
||||||
|
key: 'created_at',
|
||||||
|
width: 160
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '可见性',
|
||||||
|
key: 'visibility',
|
||||||
|
width: 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
key: 'action',
|
||||||
|
fixed: 'right',
|
||||||
|
width: 180
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
// 健康状态映射
|
||||||
|
const healthStatusMap = {
|
||||||
|
0: { text: '正常', color: 'green' },
|
||||||
|
1: { text: '警告', color: 'orange' },
|
||||||
|
2: { text: '异常', color: 'red' },
|
||||||
|
// 可根据实际情况添加更多状态
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在线状态映射(根据图片中的"已上架"状态)
|
||||||
|
const onlineStatusMap = {
|
||||||
|
0: { text: '未上架', color: 'default' },
|
||||||
|
1: { text: '上架中', color: 'processing' },
|
||||||
|
2: { text: '已上架', color: 'green' },
|
||||||
|
3: { text: '下架中', color: 'orange' },
|
||||||
|
4: { text: '已下架', color: 'red' },
|
||||||
|
}
|
||||||
|
|
||||||
|
// 工具函数
|
||||||
|
function getHealthStatusText(status) {
|
||||||
|
return healthStatusMap[status]?.text || '未知'
|
||||||
|
}
|
||||||
|
|
||||||
|
function getHealthStatusColor(status) {
|
||||||
|
return healthStatusMap[status]?.color || 'default'
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOnlineStatusText(status) {
|
||||||
|
return onlineStatusMap[status]?.text || '未知'
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOnlineStatusColor(status) {
|
||||||
|
return onlineStatusMap[status]?.color || 'default'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化内存(字节转GB)
|
||||||
|
function formatMemory(bytes) {
|
||||||
|
if (!bytes) return '0G'
|
||||||
|
const gb = bytes / (1024 * 1024 * 1024)
|
||||||
|
return `${Math.round(gb)}G`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化磁盘空间(字节转GB)
|
||||||
|
function formatDiskSpace(bytes) {
|
||||||
|
if (!bytes) return '0GB'
|
||||||
|
const gb = bytes / (1024 * 1024 * 1024)
|
||||||
|
return `${Math.round(gb)}GB`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化日期时间
|
||||||
|
function formatDateTime(dateTimeStr) {
|
||||||
|
if (!dateTimeStr) return ''
|
||||||
|
try {
|
||||||
|
const date = new Date(dateTimeStr)
|
||||||
|
return date.toLocaleString('zh-CN', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: '2-digit',
|
||||||
|
hour12: false
|
||||||
|
}).replace(/\//g, '-')
|
||||||
|
} catch (e) {
|
||||||
|
return dateTimeStr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用分页和数据获取钩子
|
||||||
|
const { listData, loading, showLoading, hideLoading, paginationState, searchFormData, resetPagination } =
|
||||||
|
usePagination()
|
||||||
|
const { resetForm } = useForm()
|
||||||
|
const editDialogRef = ref()
|
||||||
|
|
||||||
|
getPageList()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取分页列表
|
* 获取主机列表
|
||||||
*/
|
*/
|
||||||
async function getPageList() {
|
async function getPageList() {
|
||||||
try {
|
try {
|
||||||
// showLoading()
|
showLoading()
|
||||||
// const { pageSize, current } = paginationState
|
const { pageSize, current } = paginationState
|
||||||
// const { code, data } = await apis.common
|
const { success, data, total } = await apis.resource
|
||||||
// .getPageList({
|
.getHostsList({
|
||||||
// pageSize,
|
pageSize,
|
||||||
// current: current,
|
current: current,
|
||||||
// })
|
...searchFormData.value,
|
||||||
// .catch(() => {
|
})
|
||||||
// throw new Error()
|
.catch(() => {
|
||||||
// })
|
throw new Error()
|
||||||
// hideLoading()
|
})
|
||||||
// if (config('http.code.success') === code) {
|
hideLoading()
|
||||||
// const { records, pagination } = data
|
if (config('http.code.success') === success) {
|
||||||
// listData.value = records
|
listData.value = data
|
||||||
// paginationState.total = pagination.total
|
paginationState.total = total
|
||||||
// }
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// hideLoading()
|
hideLoading()
|
||||||
|
message.error('获取主机列表失败')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 搜索
|
* 移除/删除主机
|
||||||
*/
|
*/
|
||||||
function handleSearch() {
|
function handleRemove({ id, machine_name }) {
|
||||||
// resetPagination()
|
|
||||||
// getPageList()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 删除
|
|
||||||
*/
|
|
||||||
function handleDelete({ id }) {
|
|
||||||
Modal.confirm({
|
Modal.confirm({
|
||||||
title: '删除提示',
|
title: '确认删除',
|
||||||
content: '确认删除?',
|
content: `确定要删除主机 ${machine_name} 吗?此操作不可恢复。`,
|
||||||
|
okText: '确认删除',
|
||||||
|
cancelText: '取消',
|
||||||
|
okType: 'danger',
|
||||||
onOk: () => {
|
onOk: () => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
;(async () => {
|
; (async () => {
|
||||||
try {
|
try {
|
||||||
const { code } = await apis.common.del(id).catch(() => {
|
const { success } = await apis.resource.deleteHost(id).catch(() => {
|
||||||
throw new Error()
|
throw new Error()
|
||||||
})
|
})
|
||||||
if (config('http.code.success') === code) {
|
if (config('http.code.success') === success) {
|
||||||
resolve()
|
resolve()
|
||||||
message.success('删除成功')
|
message.success('删除成功')
|
||||||
await getPageList()
|
await getPageList()
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
reject()
|
reject()
|
||||||
|
message.error('删除失败')
|
||||||
}
|
}
|
||||||
})()
|
})()
|
||||||
})
|
})
|
||||||
@ -252,30 +359,77 @@ function handleDelete({ id }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 密度
|
* 下架主机
|
||||||
* @param {string} key
|
|
||||||
*/
|
*/
|
||||||
function handleSize({ key }) {
|
function handleDownShelf({ id, machine_name }) {
|
||||||
size.value = key
|
Modal.confirm({
|
||||||
|
title: '确认下架',
|
||||||
|
content: `确定要下架主机 ${machine_name} 吗?`,
|
||||||
|
okText: '确认下架',
|
||||||
|
cancelText: '取消',
|
||||||
|
onOk: () => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
; (async () => {
|
||||||
|
try {
|
||||||
|
const { success } = await apis.resource.downShelfHost(id).catch(() => {
|
||||||
|
throw new Error()
|
||||||
|
})
|
||||||
|
if (config('http.code.success') === success) {
|
||||||
|
resolve()
|
||||||
|
message.success('下架成功')
|
||||||
|
await getPageList()
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
reject()
|
||||||
|
message.error('下架失败')
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 表格发生改变
|
* 分页变化
|
||||||
* @param current
|
|
||||||
* @param pageSize
|
|
||||||
*/
|
*/
|
||||||
function onTableChange({ current, pageSize }) {
|
function onTableChange({ current, pageSize }) {
|
||||||
// paginationState.current = current
|
paginationState.current = current
|
||||||
// paginationState.pageSize = pageSize
|
paginationState.pageSize = pageSize
|
||||||
// getPageList()
|
getPageList()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 完成
|
* 重置搜索
|
||||||
*/
|
*/
|
||||||
function onOk() {
|
function handleResetSearch() {
|
||||||
// getPageList()
|
searchFormData.value = {}
|
||||||
|
resetPagination()
|
||||||
|
getPageList()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 搜索
|
||||||
|
*/
|
||||||
|
function handleSearch() {
|
||||||
|
resetForm()
|
||||||
|
resetPagination()
|
||||||
|
getPageList()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编辑完成
|
||||||
|
*/
|
||||||
|
async function onOk() {
|
||||||
|
await getPageList()
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less" scoped></style>
|
<style lang="less" scoped>
|
||||||
|
.align-right {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mb-8-2 {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
202
src/views/resource/resourceCard/index.vue
Normal file
202
src/views/resource/resourceCard/index.vue
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
<template>
|
||||||
|
<x-search-bar class="mb-8-2">
|
||||||
|
<template #default="{ gutter, colSpan }">
|
||||||
|
<a-form :model="searchFormData" layout="inline">
|
||||||
|
<a-row :gutter="gutter">
|
||||||
|
<a-col v-bind="colSpan">
|
||||||
|
<a-form-item :label="$t('pages.system.role.form.name')" name="name">
|
||||||
|
<a-input :placeholder="$t('pages.system.role.form.code.placeholder')"
|
||||||
|
v-model:value="searchFormData.name"></a-input>
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col class="align-right" v-bind="colSpan">
|
||||||
|
<a-space>
|
||||||
|
<a-button @click="handleResetSearch">{{ $t('button.reset') }}</a-button>
|
||||||
|
<a-button ghost type="primary" @click="handleSearch">
|
||||||
|
{{ $t('button.search') }}
|
||||||
|
</a-button>
|
||||||
|
</a-space>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
</a-form>
|
||||||
|
</template>
|
||||||
|
</x-search-bar>
|
||||||
|
<a-row :gutter="8" :wrap="false">
|
||||||
|
<a-col flex="auto">
|
||||||
|
<a-card type="flex">
|
||||||
|
<!-- <x-action-bar class="mb-8-2">
|
||||||
|
<a-button v-action="'add'" type="primary" @click="$refs.editDialogRef.handleCreate()">
|
||||||
|
<template #icon>
|
||||||
|
<plus-outlined></plus-outlined>
|
||||||
|
</template>
|
||||||
|
添加图片
|
||||||
|
</a-button>
|
||||||
|
</x-action-bar> -->
|
||||||
|
<a-table
|
||||||
|
:columns="columns" :data-source="listData" :loading="loading" :pagination="paginationState"
|
||||||
|
:scroll="{ x: 1000 }"
|
||||||
|
@change="onTableChange">
|
||||||
|
<template #bodyCell="{ column, record }">
|
||||||
|
<template v-if="'reserved' === column.key">
|
||||||
|
<!--状态-->
|
||||||
|
<a-tag v-if="record.reserved == true" color="red">
|
||||||
|
是
|
||||||
|
</a-tag>
|
||||||
|
<!--状态-->
|
||||||
|
<a-tag v-else color="processing">
|
||||||
|
否
|
||||||
|
</a-tag>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- <template v-if="'createAt' === column.key">
|
||||||
|
{{ formatUtcDateTime(record.created_at) }}
|
||||||
|
</template> -->
|
||||||
|
|
||||||
|
<!-- <template v-if="'action' === column.key">
|
||||||
|
<x-action-button @click="$refs.editDialogRef.handleEdit(record)">
|
||||||
|
<a-tooltip>
|
||||||
|
<template #title> {{ $t('pages.system.role.edit') }}</template>
|
||||||
|
<edit-outlined />
|
||||||
|
</a-tooltip>
|
||||||
|
</x-action-button>
|
||||||
|
<x-action-button @click="handleRemove(record)">
|
||||||
|
<a-tooltip>
|
||||||
|
<template #title> {{ $t('pages.system.delete') }}</template>
|
||||||
|
<delete-outlined style="color: #ff4d4f" />
|
||||||
|
</a-tooltip>
|
||||||
|
</x-action-button>
|
||||||
|
</template> -->
|
||||||
|
</template>
|
||||||
|
</a-table>
|
||||||
|
</a-card>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
|
||||||
|
<edit-dialog ref="editDialogRef" @ok="onOk"></edit-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { message, Modal } from 'ant-design-vue'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import apis from '@/apis'
|
||||||
|
import { formatUtcDateTime } from '@/utils/util'
|
||||||
|
import { config } from '@/config'
|
||||||
|
import { statusTypeEnum } from '@/enums/system'
|
||||||
|
import { usePagination, useForm } from '@/hooks'
|
||||||
|
import EditDialog from './components/EditDialog.vue'
|
||||||
|
import { PlusOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons-vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'systemRole',
|
||||||
|
})
|
||||||
|
const { t } = useI18n() // 解构出t方法
|
||||||
|
const columns = [
|
||||||
|
{ title: '主机ID', dataIndex: 'machine_id', width: 120 },
|
||||||
|
{ title: 'GPU名称', dataIndex: 'gpu_name', width: 120 },
|
||||||
|
{ title: 'GPU UUID', dataIndex: 'gpu_uuid', key: 'gpu_uuid', width: 240 },
|
||||||
|
{ title: '实例ID', dataIndex: 'instance_uuid', width: 120 },
|
||||||
|
{ title: '是否被占用', dataIndex: 'reserved', key: 'reserved', width: 120 },
|
||||||
|
{ title: '开始占用时间', dataIndex: 'reserve_time', key: 'reserve_time', width: 120 },
|
||||||
|
// { title: t('button.action'), key: 'action', fixed: 'right', width: 120 },
|
||||||
|
]
|
||||||
|
|
||||||
|
const { listData, loading, showLoading, hideLoading, paginationState, searchFormData, resetPagination } =
|
||||||
|
usePagination()
|
||||||
|
const { resetForm } = useForm()
|
||||||
|
const editDialogRef = ref()
|
||||||
|
|
||||||
|
getPageList()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户列表
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async function getPageList() {
|
||||||
|
try {
|
||||||
|
showLoading()
|
||||||
|
const { pageSize, current } = paginationState
|
||||||
|
const { success, data, total } = await apis.resource
|
||||||
|
.getCardsList({
|
||||||
|
pageSize,
|
||||||
|
current: current,
|
||||||
|
...searchFormData.value,
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
throw new Error()
|
||||||
|
})
|
||||||
|
hideLoading()
|
||||||
|
if (config('http.code.success') === success) {
|
||||||
|
listData.value = data
|
||||||
|
paginationState.total = total
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
hideLoading()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除
|
||||||
|
*/
|
||||||
|
function handleRemove({ id }) {
|
||||||
|
Modal.confirm({
|
||||||
|
title: t('pages.system.role.delTip'),
|
||||||
|
content: t('button.confirm'),
|
||||||
|
okText: t('button.confirm'),
|
||||||
|
onOk: () => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
; (async () => {
|
||||||
|
try {
|
||||||
|
const { success } = await apis.role.delRole(id).catch(() => {
|
||||||
|
throw new Error()
|
||||||
|
})
|
||||||
|
if (config('http.code.success') === success) {
|
||||||
|
resolve()
|
||||||
|
message.success(t('component.message.success.delete'))
|
||||||
|
await getPageList()
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
reject()
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页
|
||||||
|
*/
|
||||||
|
function onTableChange({ current, pageSize }) {
|
||||||
|
paginationState.current = current
|
||||||
|
paginationState.pageSize = pageSize
|
||||||
|
getPageList()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重置
|
||||||
|
*/
|
||||||
|
function handleResetSearch() {
|
||||||
|
searchFormData.value = {}
|
||||||
|
resetPagination()
|
||||||
|
getPageList()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 搜索
|
||||||
|
*/
|
||||||
|
function handleSearch() {
|
||||||
|
resetForm()
|
||||||
|
resetPagination()
|
||||||
|
getPageList()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编辑完成
|
||||||
|
*/
|
||||||
|
async function onOk() {
|
||||||
|
await getPageList()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped></style>
|
||||||
815
src/views/resource/resourceInstance/index.vue
Normal file
815
src/views/resource/resourceInstance/index.vue
Normal file
@ -0,0 +1,815 @@
|
|||||||
|
<template>
|
||||||
|
<x-search-bar class="mb-8-2">
|
||||||
|
<template #default="{ gutter, colSpan }">
|
||||||
|
<a-form :model="searchFormData" layout="inline">
|
||||||
|
<a-row :gutter="gutter">
|
||||||
|
<a-col v-bind="colSpan">
|
||||||
|
<a-form-item label="用户ID" name="user_id">
|
||||||
|
<a-input placeholder="请输入用户ID" v-model:value="searchFormData.user_id"></a-input>
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col v-bind="colSpan">
|
||||||
|
<a-form-item label="实例ID" name="instance_id">
|
||||||
|
<a-input placeholder="请输入实例ID" v-model:value="searchFormData.instance_id"></a-input>
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col class="align-right" v-bind="colSpan">
|
||||||
|
<a-space>
|
||||||
|
<a-button @click="handleResetSearch">重置</a-button>
|
||||||
|
<a-button ghost type="primary" @click="handleSearch">
|
||||||
|
搜索
|
||||||
|
</a-button>
|
||||||
|
</a-space>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
</a-form>
|
||||||
|
</template>
|
||||||
|
</x-search-bar>
|
||||||
|
<a-row :gutter="8" :wrap="false">
|
||||||
|
<a-col flex="auto">
|
||||||
|
<a-card type="flex">
|
||||||
|
<!-- <x-action-bar class="mb-8-2">
|
||||||
|
<a-button type="primary" @click="handleExportInstances">
|
||||||
|
<template #icon>
|
||||||
|
<ExportOutlined />
|
||||||
|
</template>
|
||||||
|
导出实例列表
|
||||||
|
</a-button>
|
||||||
|
</x-action-bar> -->
|
||||||
|
<a-table :columns="columns" :data-source="listData" :loading="loading" :pagination="paginationState"
|
||||||
|
:scroll="{ x: 1600 }" rowKey="id" @change="onTableChange">
|
||||||
|
<template #bodyCell="{ column, record }">
|
||||||
|
<!-- 用户名/用户ID -->
|
||||||
|
<template v-if="'user_info' === column.key">
|
||||||
|
<div class="user-info-cell">
|
||||||
|
<div>自己</div>
|
||||||
|
<div class="text-secondary">{{ record.id?.substring(0, 12) || '--' }}</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 实例ID/名称 -->
|
||||||
|
<template v-if="'instance_info' === column.key">
|
||||||
|
<div class="instance-info-cell">
|
||||||
|
<div>{{ record.machine_name || '--' }}</div>
|
||||||
|
<div class="text-secondary">{{ record.instance_uuid || '--' }}</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 规格详情 -->
|
||||||
|
<template v-if="'spec_detail' === column.key">
|
||||||
|
<div class="spec-detail-cell">
|
||||||
|
<div>{{ formatGPUSpec(record) }}</div>
|
||||||
|
<a-button type="link" size="small" @click="handleViewSpecDetail(record)">
|
||||||
|
查看详情
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 状态 -->
|
||||||
|
<template v-if="'status' === column.key">
|
||||||
|
<a-tag :color="getStatusColor(record.status)">
|
||||||
|
{{ getStatusText(record.status) }}
|
||||||
|
</a-tag>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 磁盘使用 -->
|
||||||
|
<template v-if="'disk_usage' === column.key">
|
||||||
|
<div class="disk-usage-cell">
|
||||||
|
<div>系统盘 {{ formatDiskUsage(record) }}</div>
|
||||||
|
<div>数据盘 {{ formatDiskUsage(record) }}</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 健康状态 -->
|
||||||
|
<template v-if="'health_status' === column.key">
|
||||||
|
<a-tag :color="getDiskHealthStatusColor(record.disk_health_status)">
|
||||||
|
{{ getDiskHealthStatusText(record.disk_health_status) }}
|
||||||
|
</a-tag>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 创建时间 -->
|
||||||
|
<template v-if="'created_at' === column.key">
|
||||||
|
{{ formatDateTime(record.created_at) }}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- SSH登录 -->
|
||||||
|
<template v-if="'ssh_login' === column.key">
|
||||||
|
<div class="ssh-login-cell">
|
||||||
|
<a-space direction="vertical" size="small">
|
||||||
|
<a-button type="link" size="small" @click="handleOpenJupyterLab(record)">
|
||||||
|
JupyterLab
|
||||||
|
</a-button>
|
||||||
|
<a-button type="link" size="small" @click="handleOpenAutoPanel(record)">
|
||||||
|
AutoPanel
|
||||||
|
</a-button>
|
||||||
|
<a-button type="link" size="small" @click="handleOpenInstanceMonitor(record)">
|
||||||
|
实例监控
|
||||||
|
</a-button>
|
||||||
|
</a-space>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 快捷工具 -->
|
||||||
|
<template v-if="'quick_tools' === column.key">
|
||||||
|
<div class="quick-tools-cell">
|
||||||
|
<a-space direction="vertical" size="small">
|
||||||
|
<a-button type="link" size="small" danger @click="handleShutdownInstance(record)"
|
||||||
|
:disabled="record.status !== 'running'">
|
||||||
|
关机
|
||||||
|
</a-button>
|
||||||
|
</a-space>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 操作 -->
|
||||||
|
<!-- <template v-if="'action' === column.key">
|
||||||
|
<div class="action-cell">
|
||||||
|
<a-space>
|
||||||
|
<a-button type="link" size="small" @click="handleStartInstance(record)"
|
||||||
|
:disabled="record.status === 'running'">
|
||||||
|
启动
|
||||||
|
</a-button>
|
||||||
|
<a-button type="link" size="small" @click="handleRestartInstance(record)"
|
||||||
|
:disabled="record.status !== 'running'">
|
||||||
|
重启
|
||||||
|
</a-button>
|
||||||
|
<a-button type="link" size="small" danger @click="handleStopInstance(record)"
|
||||||
|
:disabled="record.status !== 'running'">
|
||||||
|
停止
|
||||||
|
</a-button>
|
||||||
|
<a-dropdown>
|
||||||
|
<template #overlay>
|
||||||
|
<a-menu>
|
||||||
|
<a-menu-item @click="handleViewLogs(record)">
|
||||||
|
查看日志
|
||||||
|
</a-menu-item>
|
||||||
|
<a-menu-item @click="handleTerminal(record)">
|
||||||
|
终端连接
|
||||||
|
</a-menu-item>
|
||||||
|
<a-menu-item danger @click="handleDeleteInstance(record)">
|
||||||
|
删除实例
|
||||||
|
</a-menu-item>
|
||||||
|
</a-menu>
|
||||||
|
</template>
|
||||||
|
<a-button type="link" size="small">
|
||||||
|
更多
|
||||||
|
<DownOutlined />
|
||||||
|
</a-button>
|
||||||
|
</a-dropdown>
|
||||||
|
</a-space>
|
||||||
|
</div>
|
||||||
|
</template> -->
|
||||||
|
</template>
|
||||||
|
</a-table>
|
||||||
|
</a-card>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
<a-modal v-model:visible="specModalVisible" title="实例规格详情" width="700px" :footer="null"
|
||||||
|
@cancel="specModalVisible = false">
|
||||||
|
<div v-html="specDetailContent"></div>
|
||||||
|
</a-modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { message, Modal } from 'ant-design-vue'
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import apis from '@/apis'
|
||||||
|
import { formatUtcDateTime } from '@/utils/util'
|
||||||
|
import { config } from '@/config'
|
||||||
|
import { usePagination, useForm } from '@/hooks'
|
||||||
|
import { ExportOutlined, DownOutlined } from '@ant-design/icons-vue'
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'instanceManagement',
|
||||||
|
})
|
||||||
|
|
||||||
|
// 表格列定义
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: '用户名/用户ID',
|
||||||
|
key: 'user_info',
|
||||||
|
width: 150,
|
||||||
|
fixed: 'left'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '实例ID/名称',
|
||||||
|
key: 'instance_info',
|
||||||
|
width: 180
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '规格详情',
|
||||||
|
key: 'spec_detail',
|
||||||
|
width: 180
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '状态',
|
||||||
|
key: 'status',
|
||||||
|
width: 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '磁盘使用',
|
||||||
|
key: 'disk_usage',
|
||||||
|
width: 140
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '健康状态',
|
||||||
|
key: 'health_status',
|
||||||
|
width: 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '创建时间',
|
||||||
|
key: 'created_at',
|
||||||
|
width: 160
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'SSH登录',
|
||||||
|
key: 'ssh_login',
|
||||||
|
width: 140
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '快捷工具',
|
||||||
|
key: 'quick_tools',
|
||||||
|
width: 100
|
||||||
|
},
|
||||||
|
// {
|
||||||
|
// title: '操作',
|
||||||
|
// key: 'action',
|
||||||
|
// fixed: 'right',
|
||||||
|
// width: 240
|
||||||
|
// },
|
||||||
|
]
|
||||||
|
|
||||||
|
// 状态映射
|
||||||
|
const statusMap = {
|
||||||
|
'running': { text: '运行中', color: 'green' },
|
||||||
|
'stopped': { text: '已停止', color: 'default' },
|
||||||
|
'starting': { text: '启动中', color: 'processing' },
|
||||||
|
'stopping': { text: '停止中', color: 'orange' },
|
||||||
|
'error': { text: '异常', color: 'red' },
|
||||||
|
'creating': { text: '创建中', color: 'processing' },
|
||||||
|
'deleting': { text: '删除中', color: 'orange' },
|
||||||
|
'paused': { text: '已暂停', color: 'warning' },
|
||||||
|
'exited': { text: '已退出', color: 'default' },
|
||||||
|
}
|
||||||
|
|
||||||
|
// 磁盘健康状态映射
|
||||||
|
const diskHealthStatusMap = {
|
||||||
|
'normal': { text: '正常', color: 'green' },
|
||||||
|
'warning': { text: '警告', color: 'orange' },
|
||||||
|
'error': { text: '异常', color: 'red' },
|
||||||
|
'abnormal': { text: '异常', color: 'red' },
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用分页和数据获取钩子
|
||||||
|
const { listData, loading, showLoading, hideLoading, paginationState, searchFormData, resetPagination } =
|
||||||
|
usePagination()
|
||||||
|
const { resetForm } = useForm()
|
||||||
|
|
||||||
|
// 弹窗相关
|
||||||
|
const specModalVisible = ref(false)
|
||||||
|
const currentRecord = ref(null)
|
||||||
|
|
||||||
|
// 计算规格详情内容
|
||||||
|
const specDetailContent = computed(() => {
|
||||||
|
if (!currentRecord.value) return ''
|
||||||
|
return getSpecDetailContent(currentRecord.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 工具函数
|
||||||
|
function getStatusText(status) {
|
||||||
|
return statusMap[status]?.text || status || '未知'
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStatusColor(status) {
|
||||||
|
return statusMap[status]?.color || 'default'
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDiskHealthStatusText(status) {
|
||||||
|
return diskHealthStatusMap[status]?.text || status || '正常'
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDiskHealthStatusColor(status) {
|
||||||
|
return diskHealthStatusMap[status]?.color || 'green'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化日期时间
|
||||||
|
function formatDateTime(dateTimeStr) {
|
||||||
|
if (!dateTimeStr) return ''
|
||||||
|
try {
|
||||||
|
const date = new Date(dateTimeStr)
|
||||||
|
return date.toLocaleString('zh-CN', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: '2-digit',
|
||||||
|
hour12: false
|
||||||
|
}).replace(/\//g, '-')
|
||||||
|
} catch (e) {
|
||||||
|
return dateTimeStr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化资源大小(字节转GB)
|
||||||
|
function formatBytesToGB(bytes) {
|
||||||
|
if (!bytes && bytes !== 0) return '0GB'
|
||||||
|
const gb = bytes / (1024 * 1024 * 1024)
|
||||||
|
return `${gb.toFixed(2)}GB`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化百分比
|
||||||
|
function formatPercent(decimal) {
|
||||||
|
if (decimal === undefined || decimal === null) return '0.00%'
|
||||||
|
return `${(decimal * 100).toFixed(2)}%`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 规格详情相关函数
|
||||||
|
function formatGPUSpec(record) {
|
||||||
|
if (!record.gpu_name) return '--'
|
||||||
|
|
||||||
|
const gpuName = record.gpu_name
|
||||||
|
const gpuAmount = record.req_gpu_amount || 1
|
||||||
|
return `${gpuName} * ${gpuAmount}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 磁盘使用格式化
|
||||||
|
function formatDiskUsage(record) {
|
||||||
|
const usedRate = record.root_fs_used_rate || 0
|
||||||
|
return `${formatPercent(usedRate)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取规格详情内容HTML
|
||||||
|
function getSpecDetailContent(record) {
|
||||||
|
const cpuUsage = record.cpu_usage_percent || 0
|
||||||
|
const memUsage = record.mem_usage_percent || 0
|
||||||
|
const memUsageBytes = record.mem_usage || 0
|
||||||
|
const memLimitBytes = record.mem_limit_in_byte || record.mem_limit || 0
|
||||||
|
|
||||||
|
// 计算CPU核心数和内存
|
||||||
|
const cpuCores = record.cpu_limit || 0
|
||||||
|
const memoryGB = (memLimitBytes / (1024 * 1024 * 1024)).toFixed(1)
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div style="margin-top: 16px; line-height: 1.8;">
|
||||||
|
|
||||||
|
<div style="background: #f6f8fa; padding: 12px; border-radius: 4px; margin-bottom: 16px;">
|
||||||
|
<h5 style="margin: 0 0 8px 0; color: #666;">GPU配置</h5>
|
||||||
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px 16px;">
|
||||||
|
<div><strong>GPU型号:</strong>${record.gpu_name || '--'}</div>
|
||||||
|
<div><strong>GPU数量:</strong>${record.req_gpu_amount || 1}</div>
|
||||||
|
${record.gpu_all_num ? `<div><strong>主机总GPU:</strong>${record.gpu_all_num}</div>` : ''}
|
||||||
|
${record.gpu_idle_num !== undefined ? `<div><strong>主机空闲GPU:</strong>${record.gpu_idle_num}</div>` : ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="background: #f6f8fa; padding: 12px; border-radius: 4px; margin-bottom: 16px;">
|
||||||
|
<h5 style="margin: 0 0 8px 0; color: #666;">CPU配置</h5>
|
||||||
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px 16px;">
|
||||||
|
<div><strong>CPU核心数:</strong>${cpuCores}核</div>
|
||||||
|
<div><strong>CPU使用率:</strong>${formatPercent(cpuUsage)}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="background: #f6f8fa; padding: 12px; border-radius: 4px; margin-bottom: 16px;">
|
||||||
|
<h5 style="margin: 0 0 8px 0; color: #666;">内存配置</h5>
|
||||||
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px 16px;">
|
||||||
|
<div><strong>内存大小:</strong>${formatBytesToGB(memLimitBytes)}</div>
|
||||||
|
<div><strong>内存使用率:</strong>${formatPercent(memUsage)}</div>
|
||||||
|
<div><strong>已用内存:</strong>${formatBytesToGB(memUsageBytes)}</div>
|
||||||
|
<div><strong>剩余内存:</strong>${formatBytesToGB(memLimitBytes - memUsageBytes)}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="background: #f6f8fa; padding: 12px; border-radius: 4px; margin-bottom: 16px;">
|
||||||
|
<h5 style="margin: 0 0 8px 0; color: #666;">磁盘配置</h5>
|
||||||
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px 16px;">
|
||||||
|
<div><strong>系统盘大小:</strong>${formatBytesToGB(record.root_fs_total_size)}</div>
|
||||||
|
<div><strong>系统盘使用率:</strong>${formatPercent(record.root_fs_used_rate)}</div>
|
||||||
|
<div><strong>系统盘已用:</strong>${formatBytesToGB(record.root_fs_used_size)}</div>
|
||||||
|
<div><strong>系统盘剩余:</strong>${formatBytesToGB(record.root_fs_total_size - record.root_fs_used_size)}</div>
|
||||||
|
${record.usage_info?.data_disk_total_size ? `
|
||||||
|
<div><strong>数据盘大小:</strong>${formatBytesToGB(record.usage_info.data_disk_total_size)}</div>
|
||||||
|
<div><strong>数据盘使用率:</strong>${formatPercent(record.root_fs_used_rate)}</div>
|
||||||
|
<div><strong>数据盘已用:</strong>${formatBytesToGB(record.usage_info.data_disk_used_size)}</div>
|
||||||
|
<div><strong>数据盘剩余:</strong>${formatBytesToGB(record.usage_info.data_disk_total_size - record.usage_info.data_disk_used_size)}</div>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="background: #f6f8fa; padding: 12px; border-radius: 4px;">
|
||||||
|
<h5 style="margin: 0 0 8px 0; color: #666;">其他信息</h5>
|
||||||
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px 16px;">
|
||||||
|
<div><strong>镜像:</strong>${record.image || '--'}</div>
|
||||||
|
<div><strong>启动模式:</strong>${record.start_mode || '--'}</div>
|
||||||
|
<div><strong>共享内存:</strong>${formatBytesToGB(record.shm_size)}</div>
|
||||||
|
<div><strong>启动时间:</strong>${record.started_at?.Valid ? formatDateTime(record.started_at.Time) : '--'}</div>
|
||||||
|
${record.machine_name ? `<div><strong>所在主机:</strong>${record.machine_name}</div>` : ''}
|
||||||
|
${record.machine_id ? `<div><strong>主机ID:</strong>${record.machine_id}</div>` : ''}
|
||||||
|
${record.ssh_command ? `<div style="grid-column: 1 / -1;"><strong>SSH命令:</strong><br><code style="background: #f0f0f0; padding: 4px 8px; border-radius: 3px; font-family: monospace; word-break: break-all;">${record.ssh_command}</code></div>` : ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化加载数据
|
||||||
|
getPageList()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取实例列表
|
||||||
|
*/
|
||||||
|
async function getPageList() {
|
||||||
|
try {
|
||||||
|
showLoading()
|
||||||
|
const { pageSize, current } = paginationState
|
||||||
|
const { success, data, total } = await apis.resource
|
||||||
|
.getInstancesList({
|
||||||
|
pageSize,
|
||||||
|
current: current,
|
||||||
|
...searchFormData.value,
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
throw new Error()
|
||||||
|
})
|
||||||
|
hideLoading()
|
||||||
|
if (config('http.code.success') === success) {
|
||||||
|
listData.value = data
|
||||||
|
paginationState.total = total
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
hideLoading()
|
||||||
|
console.error('获取实例列表失败:', error)
|
||||||
|
message.error('获取实例列表失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查看规格详情
|
||||||
|
*/
|
||||||
|
function handleViewSpecDetail(record) {
|
||||||
|
currentRecord.value = record
|
||||||
|
specModalVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打开JupyterLab
|
||||||
|
*/
|
||||||
|
function handleOpenJupyterLab(record) {
|
||||||
|
if (record.jupyter_port && record.jupyter_token) {
|
||||||
|
const jupyterUrl = `http://${record.proxy_host}:${record.jupyter_port}/?token=${record.jupyter_token}`
|
||||||
|
window.open(jupyterUrl, '_blank')
|
||||||
|
message.success('正在打开JupyterLab...')
|
||||||
|
} else {
|
||||||
|
message.warning('JupyterLab未启用或配置不完整')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打开AutoPanel
|
||||||
|
*/
|
||||||
|
function handleOpenAutoPanel(record) {
|
||||||
|
if (record.proxy_host) {
|
||||||
|
const autoPanelUrl = `http://${record.proxy_host}/autopanel/${record.instance_uuid}`
|
||||||
|
window.open(autoPanelUrl, '_blank')
|
||||||
|
message.success('正在打开AutoPanel...')
|
||||||
|
} else {
|
||||||
|
message.warning('AutoPanel未配置')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打开实例监控
|
||||||
|
*/
|
||||||
|
function handleOpenInstanceMonitor(record) {
|
||||||
|
if (record.instance_uuid) {
|
||||||
|
const monitorUrl = `/monitor/instance/${record.instance_uuid}`
|
||||||
|
window.open(monitorUrl, '_blank')
|
||||||
|
message.success('正在打开实例监控...')
|
||||||
|
} else {
|
||||||
|
message.warning('实例ID不存在')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关机实例
|
||||||
|
*/
|
||||||
|
function handleShutdownInstance(record) {
|
||||||
|
Modal.confirm({
|
||||||
|
title: '确认关机',
|
||||||
|
content: `确定要关机实例 ${record.instance_uuid || record.machine_name} 吗?关机后实例将停止运行。`,
|
||||||
|
okText: '确认关机',
|
||||||
|
cancelText: '取消',
|
||||||
|
okType: 'danger',
|
||||||
|
onOk: () => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
; (async () => {
|
||||||
|
try {
|
||||||
|
const { success, message: msg } = await apis.instance.shutdownInstance(record.id).catch(() => {
|
||||||
|
throw new Error('API调用失败')
|
||||||
|
})
|
||||||
|
if (config('http.code.success') === success) {
|
||||||
|
resolve()
|
||||||
|
message.success(msg || '关机命令已发送')
|
||||||
|
setTimeout(() => {
|
||||||
|
getPageList()
|
||||||
|
}, 2000)
|
||||||
|
} else {
|
||||||
|
reject(new Error(msg || '关机失败'))
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
reject(error)
|
||||||
|
message.error(error.message || '关机失败')
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启动实例
|
||||||
|
*/
|
||||||
|
function handleStartInstance(record) {
|
||||||
|
Modal.confirm({
|
||||||
|
title: '确认启动',
|
||||||
|
content: `确定要启动实例 ${record.instance_uuid || record.machine_name} 吗?`,
|
||||||
|
okText: '确认启动',
|
||||||
|
cancelText: '取消',
|
||||||
|
onOk: () => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
; (async () => {
|
||||||
|
try {
|
||||||
|
const { success, message: msg } = await apis.instance.startInstance(record.id).catch(() => {
|
||||||
|
throw new Error('API调用失败')
|
||||||
|
})
|
||||||
|
if (config('http.code.success') === success) {
|
||||||
|
resolve()
|
||||||
|
message.success(msg || '启动命令已发送')
|
||||||
|
setTimeout(() => {
|
||||||
|
getPageList()
|
||||||
|
}, 2000)
|
||||||
|
} else {
|
||||||
|
reject(new Error(msg || '启动失败'))
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
reject(error)
|
||||||
|
message.error(error.message || '启动失败')
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重启实例
|
||||||
|
*/
|
||||||
|
function handleRestartInstance(record) {
|
||||||
|
Modal.confirm({
|
||||||
|
title: '确认重启',
|
||||||
|
content: `确定要重启实例 ${record.instance_uuid || record.machine_name} 吗?重启期间实例将暂时不可用。`,
|
||||||
|
okText: '确认重启',
|
||||||
|
cancelText: '取消',
|
||||||
|
onOk: () => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
; (async () => {
|
||||||
|
try {
|
||||||
|
const { success, message: msg } = await apis.instance.restartInstance(record.id).catch(() => {
|
||||||
|
throw new Error('API调用失败')
|
||||||
|
})
|
||||||
|
if (config('http.code.success') === success) {
|
||||||
|
resolve()
|
||||||
|
message.success(msg || '重启命令已发送')
|
||||||
|
setTimeout(() => {
|
||||||
|
getPageList()
|
||||||
|
}, 2000)
|
||||||
|
} else {
|
||||||
|
reject(new Error(msg || '重启失败'))
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
reject(error)
|
||||||
|
message.error(error.message || '重启失败')
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 停止实例
|
||||||
|
*/
|
||||||
|
function handleStopInstance(record) {
|
||||||
|
Modal.confirm({
|
||||||
|
title: '确认停止',
|
||||||
|
content: `确定要停止实例 ${record.instance_uuid || record.machine_name} 吗?停止后实例将立即终止运行。`,
|
||||||
|
okText: '确认停止',
|
||||||
|
cancelText: '取消',
|
||||||
|
okType: 'danger',
|
||||||
|
onOk: () => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
; (async () => {
|
||||||
|
try {
|
||||||
|
const { success, message: msg } = await apis.instance.stopInstance(record.id).catch(() => {
|
||||||
|
throw new Error('API调用失败')
|
||||||
|
})
|
||||||
|
if (config('http.code.success') === success) {
|
||||||
|
resolve()
|
||||||
|
message.success(msg || '停止命令已发送')
|
||||||
|
setTimeout(() => {
|
||||||
|
getPageList()
|
||||||
|
}, 2000)
|
||||||
|
} else {
|
||||||
|
reject(new Error(msg || '停止失败'))
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
reject(error)
|
||||||
|
message.error(error.message || '停止失败')
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除实例
|
||||||
|
*/
|
||||||
|
function handleDeleteInstance(record) {
|
||||||
|
Modal.confirm({
|
||||||
|
title: '确认删除',
|
||||||
|
content: `确定要删除实例 ${record.instance_uuid || record.machine_name} 吗?此操作将永久删除实例及相关数据,不可恢复!`,
|
||||||
|
okText: '确认删除',
|
||||||
|
cancelText: '取消',
|
||||||
|
okType: 'danger',
|
||||||
|
onOk: () => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
; (async () => {
|
||||||
|
try {
|
||||||
|
const { success, message: msg } = await apis.instance.deleteInstance(record.id).catch(() => {
|
||||||
|
throw new Error('API调用失败')
|
||||||
|
})
|
||||||
|
if (config('http.code.success') === success) {
|
||||||
|
resolve()
|
||||||
|
message.success(msg || '删除成功')
|
||||||
|
await getPageList()
|
||||||
|
} else {
|
||||||
|
reject(new Error(msg || '删除失败'))
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
reject(error)
|
||||||
|
message.error(error.message || '删除失败')
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查看日志
|
||||||
|
*/
|
||||||
|
function handleViewLogs(record) {
|
||||||
|
if (record.instance_uuid) {
|
||||||
|
const logsUrl = `/logs/instance/${record.instance_uuid}`
|
||||||
|
window.open(logsUrl, '_blank')
|
||||||
|
message.success('正在打开日志页面...')
|
||||||
|
} else {
|
||||||
|
message.warning('实例ID不存在')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 终端连接
|
||||||
|
*/
|
||||||
|
function handleTerminal(record) {
|
||||||
|
if (record.ssh_command) {
|
||||||
|
navigator.clipboard.writeText(record.ssh_command).then(() => {
|
||||||
|
message.success('SSH命令已复制到剪贴板')
|
||||||
|
}).catch(() => {
|
||||||
|
Modal.info({
|
||||||
|
title: 'SSH连接命令',
|
||||||
|
content: `
|
||||||
|
<div style="margin: 16px 0;">
|
||||||
|
<p>请复制以下命令到终端中使用:</p>
|
||||||
|
<code style="background: #f0f0f0; padding: 8px; border-radius: 4px; display: block; font-family: monospace; word-break: break-all;">
|
||||||
|
${record.ssh_command}
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
okText: '关闭',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
message.warning('SSH连接信息不存在')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出实例列表
|
||||||
|
*/
|
||||||
|
function handleExportInstances() {
|
||||||
|
Modal.confirm({
|
||||||
|
title: '导出实例列表',
|
||||||
|
content: '确定要导出当前实例列表吗?导出的文件将包含所有实例的详细信息。',
|
||||||
|
okText: '确认导出',
|
||||||
|
cancelText: '取消',
|
||||||
|
onOk: () => {
|
||||||
|
message.loading('正在准备导出数据...', 0)
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
message.destroy()
|
||||||
|
message.success('导出成功!文件已开始下载')
|
||||||
|
}, 1500)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页变化
|
||||||
|
*/
|
||||||
|
function onTableChange({ current, pageSize }) {
|
||||||
|
paginationState.current = current
|
||||||
|
paginationState.pageSize = pageSize
|
||||||
|
getPageList()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重置搜索
|
||||||
|
*/
|
||||||
|
function handleResetSearch() {
|
||||||
|
searchFormData.value = {}
|
||||||
|
resetPagination()
|
||||||
|
getPageList()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 搜索
|
||||||
|
*/
|
||||||
|
function handleSearch() {
|
||||||
|
resetForm()
|
||||||
|
resetPagination()
|
||||||
|
getPageList()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.align-right {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mb-8-2 {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-secondary {
|
||||||
|
color: rgba(0, 0, 0, 0.45);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info-cell,
|
||||||
|
.instance-info-cell,
|
||||||
|
.spec-detail-cell,
|
||||||
|
.disk-usage-cell,
|
||||||
|
.ssh-login-cell,
|
||||||
|
.quick-tools-cell,
|
||||||
|
.action-cell {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spec-detail-cell {
|
||||||
|
.ant-btn-link {
|
||||||
|
padding: 0;
|
||||||
|
height: auto;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-table-cell) {
|
||||||
|
.ant-btn-link {
|
||||||
|
padding: 0;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-dropdown-trigger {
|
||||||
|
padding: 0;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-tag) {
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 弹窗样式
|
||||||
|
:deep(.ant-modal-body) {
|
||||||
|
max-height: 70vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -40,26 +40,15 @@
|
|||||||
<a-table :columns="columns" :data-source="listData" :loading="loading" :pagination="paginationState"
|
<a-table :columns="columns" :data-source="listData" :loading="loading" :pagination="paginationState"
|
||||||
:scroll="{ x: 1000 }" @change="onTableChange">
|
:scroll="{ x: 1000 }" @change="onTableChange">
|
||||||
<template #bodyCell="{ column, record }">
|
<template #bodyCell="{ column, record }">
|
||||||
<template v-if="'banner_type' === column.key">
|
<template v-if="'status' === column.key">
|
||||||
<!--状态-->
|
<!--状态-->
|
||||||
<a-tag v-if="record.banner_type == 1" color="processing">
|
<a-tag v-if="record.status == 'DISABLED'" color="red">
|
||||||
首页轮播图
|
|
||||||
</a-tag>
|
|
||||||
<!--状态-->
|
|
||||||
<a-tag v-else color="processing">
|
|
||||||
营销活动图
|
|
||||||
</a-tag>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template v-if="'is_recommend' === column.key">
|
|
||||||
<!--状态-->
|
|
||||||
<a-tag v-if="record.banner_type == true" color="processing">
|
|
||||||
启用
|
|
||||||
</a-tag>
|
|
||||||
<!--状态-->
|
|
||||||
<a-tag v-else color="processing">
|
|
||||||
禁用
|
禁用
|
||||||
</a-tag>
|
</a-tag>
|
||||||
|
<!--状态-->
|
||||||
|
<a-tag v-else color="processing">
|
||||||
|
启用
|
||||||
|
</a-tag>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
||||||
@ -68,15 +57,9 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-if="'action' === column.key">
|
<template v-if="'action' === column.key">
|
||||||
<x-action-button @click="$refs.editDialogRef.handleEdit(record)">
|
|
||||||
<a-tooltip>
|
|
||||||
<template #title> {{ $t('pages.system.role.edit') }}</template>
|
|
||||||
<edit-outlined />
|
|
||||||
</a-tooltip>
|
|
||||||
</x-action-button>
|
|
||||||
<x-action-button @click="handleRemove(record)">
|
<x-action-button @click="handleRemove(record)">
|
||||||
<a-tooltip>
|
<a-tooltip>
|
||||||
<template #title> {{ $t('pages.system.delete') }}</template>
|
<template #title> 移出黑名单</template>
|
||||||
<delete-outlined style="color: #ff4d4f" />
|
<delete-outlined style="color: #ff4d4f" />
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
</x-action-button>
|
</x-action-button>
|
||||||
@ -86,8 +69,6 @@
|
|||||||
</a-card>
|
</a-card>
|
||||||
</a-col>
|
</a-col>
|
||||||
</a-row>
|
</a-row>
|
||||||
|
|
||||||
<edit-dialog ref="editDialogRef" @ok="onOk"></edit-dialog>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
@ -98,7 +79,7 @@ import { formatUtcDateTime } from '@/utils/util'
|
|||||||
import { config } from '@/config'
|
import { config } from '@/config'
|
||||||
import { statusTypeEnum } from '@/enums/system'
|
import { statusTypeEnum } from '@/enums/system'
|
||||||
import { usePagination, useForm } from '@/hooks'
|
import { usePagination, useForm } from '@/hooks'
|
||||||
import EditDialog from './components/EditDialog.vue'
|
|
||||||
import { PlusOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons-vue'
|
import { PlusOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons-vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { disabledDict, authenticationTypeDict } from '@/enums/dict'
|
import { disabledDict, authenticationTypeDict } from '@/enums/dict'
|
||||||
@ -133,7 +114,7 @@ const columns = [
|
|||||||
const { listData, loading, showLoading, hideLoading, paginationState, searchFormData, resetPagination } =
|
const { listData, loading, showLoading, hideLoading, paginationState, searchFormData, resetPagination } =
|
||||||
usePagination()
|
usePagination()
|
||||||
const { resetForm } = useForm()
|
const { resetForm } = useForm()
|
||||||
const editDialogRef = ref()
|
|
||||||
|
|
||||||
getPageList()
|
getPageList()
|
||||||
|
|
||||||
@ -158,6 +139,9 @@ async function getPageList() {
|
|||||||
if (data.length > 0) {
|
if (data.length > 0) {
|
||||||
listData.value = data
|
listData.value = data
|
||||||
paginationState.total = total
|
paginationState.total = total
|
||||||
|
}else{
|
||||||
|
listData.value = []
|
||||||
|
paginationState.total = 0
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
hideLoading()
|
hideLoading()
|
||||||
@ -169,14 +153,14 @@ async function getPageList() {
|
|||||||
*/
|
*/
|
||||||
function handleRemove({ id }) {
|
function handleRemove({ id }) {
|
||||||
Modal.confirm({
|
Modal.confirm({
|
||||||
title: t('pages.system.role.delTip'),
|
title: '提示',
|
||||||
content: t('button.confirm'),
|
content: '确定移出黑名单吗?',
|
||||||
okText: t('button.confirm'),
|
okText: t('button.confirm'),
|
||||||
onOk: () => {
|
onOk: () => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
; (async () => {
|
; (async () => {
|
||||||
try {
|
try {
|
||||||
const { success } = await apis.role.delRole(id).catch(() => {
|
const { success } = await apis.userControl.deleteBlackCustomers(id).catch(() => {
|
||||||
throw new Error()
|
throw new Error()
|
||||||
})
|
})
|
||||||
if (config('http.code.success') === success) {
|
if (config('http.code.success') === success) {
|
||||||
|
|||||||
@ -1,21 +1,16 @@
|
|||||||
<template>
|
<template>
|
||||||
<a-modal
|
<a-modal :open="modal.open" :title="modal.title" :confirm-loading="modal.confirmLoading" :after-close="onAfterClose"
|
||||||
:open="modal.open"
|
@ok="handleOk" @cancel="handleCancel">
|
||||||
:title="modal.title"
|
<a-form ref="formRef" scroll-to-first-error :model="formData" :rules="formRules"
|
||||||
:confirm-loading="modal.confirmLoading"
|
|
||||||
:after-close="onAfterClose"
|
|
||||||
@ok="handleOk"
|
|
||||||
@cancel="handleCancel">
|
|
||||||
<a-form
|
|
||||||
ref="formRef"
|
|
||||||
scroll-to-first-error
|
|
||||||
:model="formData"
|
|
||||||
:rules="formRules"
|
|
||||||
:label-col="{ style: { width: '80px' } }">
|
:label-col="{ style: { width: '80px' } }">
|
||||||
<a-form-item
|
<a-form-item label="封禁原因" name="blackReason">
|
||||||
label="标题"
|
<a-textarea v-model:value="formData.blackReason"></a-textarea>
|
||||||
name="title">
|
</a-form-item>
|
||||||
<a-input v-model:value="formData.title"></a-input>
|
<a-form-item label="封禁天数" name="blackDays">
|
||||||
|
<a-input :defaultValue="0" type="number" v-model:value="formData.blackDays"></a-input>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="备注" name="remark">
|
||||||
|
<a-textarea v-model:value="formData.remark"></a-textarea>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-form>
|
</a-form>
|
||||||
</a-modal>
|
</a-modal>
|
||||||
@ -25,6 +20,7 @@
|
|||||||
import { cloneDeep } from 'lodash-es'
|
import { cloneDeep } from 'lodash-es'
|
||||||
import apis from '@/apis'
|
import apis from '@/apis'
|
||||||
import { useForm, useModal } from '@/hooks'
|
import { useForm, useModal } from '@/hooks'
|
||||||
|
import { config } from '@/config'
|
||||||
|
|
||||||
const emit = defineEmits(['ok'])
|
const emit = defineEmits(['ok'])
|
||||||
|
|
||||||
@ -32,7 +28,8 @@ const { modal, showModal, hideModal, showLoading, hideLoading } = useModal()
|
|||||||
const { formRef, formRules, formRecord, formData, resetForm } = useForm()
|
const { formRef, formRules, formRecord, formData, resetForm } = useForm()
|
||||||
|
|
||||||
formRules.value = {
|
formRules.value = {
|
||||||
title: { required: true, message: '请输入标题' },
|
blackReason: { required: true, message: '请输入理由' },
|
||||||
|
blackDays: { required: true, message: '请输入封禁天数' }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -51,10 +48,9 @@ function handleCreate() {
|
|||||||
function handleEdit(record = {}) {
|
function handleEdit(record = {}) {
|
||||||
showModal({
|
showModal({
|
||||||
type: 'edit',
|
type: 'edit',
|
||||||
title: '编辑',
|
title: '用户封禁',
|
||||||
})
|
})
|
||||||
formRecord.value = record
|
formRecord.value = record
|
||||||
formData.value = cloneDeep(record)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -67,7 +63,9 @@ function handleOk() {
|
|||||||
try {
|
try {
|
||||||
showLoading()
|
showLoading()
|
||||||
const params = {
|
const params = {
|
||||||
...values,
|
blackDays: Number(values.blackDays),
|
||||||
|
blackReason: values.blackReason,
|
||||||
|
remark: values.remark
|
||||||
}
|
}
|
||||||
let result = null
|
let result = null
|
||||||
switch (modal.value.type) {
|
switch (modal.value.type) {
|
||||||
@ -77,13 +75,13 @@ function handleOk() {
|
|||||||
})
|
})
|
||||||
break
|
break
|
||||||
case 'edit':
|
case 'edit':
|
||||||
result = await apis.common.update(formRecord.value.id, params).catch(() => {
|
result = await apis.userControl.updateCustomers(formRecord.value, params).catch(() => {
|
||||||
throw new Error()
|
throw new Error()
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
hideLoading()
|
hideLoading()
|
||||||
if (200 === result?.code) {
|
if (config('http.code.success') === result?.success) {
|
||||||
hideModal()
|
hideModal()
|
||||||
emit('ok')
|
emit('ok')
|
||||||
}
|
}
|
||||||
|
|||||||
@ -78,7 +78,7 @@
|
|||||||
</x-action-button>
|
</x-action-button>
|
||||||
<x-action-button @click="handleRemove(record)">
|
<x-action-button @click="handleRemove(record)">
|
||||||
<a-tooltip>
|
<a-tooltip>
|
||||||
<template #title> {{ $t('pages.system.delete') }}</template>
|
<template #title> 拉黑用户</template>
|
||||||
<delete-outlined style="color: #ff4d4f" />
|
<delete-outlined style="color: #ff4d4f" />
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
</x-action-button>
|
</x-action-button>
|
||||||
@ -168,28 +168,18 @@ async function getPageList() {
|
|||||||
/**
|
/**
|
||||||
* 移除
|
* 移除
|
||||||
*/
|
*/
|
||||||
function handleRemove({ id }) {
|
function handleRemove({ id, status }) {
|
||||||
|
console.log(id, status)
|
||||||
|
if( status == 'DISABLED' ){
|
||||||
|
message.error('该用户已被禁用,请勿重复操作')
|
||||||
|
return
|
||||||
|
}
|
||||||
Modal.confirm({
|
Modal.confirm({
|
||||||
title: t('pages.system.role.delTip'),
|
title: '确认操作',
|
||||||
content: t('button.confirm'),
|
content: '确认拉黑该用户?',
|
||||||
okText: t('button.confirm'),
|
okText: t('button.confirm'),
|
||||||
onOk: () => {
|
onOk: () => {
|
||||||
return new Promise((resolve, reject) => {
|
editDialogRef.value?.handleEdit(id)
|
||||||
; (async () => {
|
|
||||||
try {
|
|
||||||
const { success } = await apis.role.delRole(id).catch(() => {
|
|
||||||
throw new Error()
|
|
||||||
})
|
|
||||||
if (config('http.code.success') === success) {
|
|
||||||
resolve()
|
|
||||||
message.success(t('component.message.success.delete'))
|
|
||||||
await getPageList()
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
reject()
|
|
||||||
}
|
|
||||||
})()
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user