This commit is contained in:
qiuyuan 2026-01-28 17:58:02 +08:00
parent dd577b4868
commit efb064280b
10 changed files with 1585 additions and 248 deletions

View 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}`)

View File

@ -1,6 +1,6 @@
import request from '@/utils/request'
//获取banner列表
//获取用户列表
export const getCustomersList = (params) => request.basic.get('/api/v1/customers', params)
// 黑名单列表
@ -10,6 +10,10 @@ export const getBlackCustomersList = (params) => request.basic.get('/api/v1/blac
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

View File

@ -36,7 +36,9 @@ export default {
'account.trigger': '触发报错',
'account.logout': '退出登录',
resource: 'GPU资源管理',
resourceAdmin: '资源池管理',
resourceAdmin: '主机资源列表',
resourceCard: '计算卡列表',
resourceInstance: '实例卡列表',
resourceStatistics: '资源池统计信息',
userControl: '用户管理',
userList: '用户列表',

View File

@ -18,12 +18,35 @@ export default [
name: 'resourceAdmin',
component: 'resource/resourceAdmin/index.vue',
meta: {
title: '资源池管理 ',
title: '主机资源列表 ',
isMenu: true,
keepAlive: true,
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',
name: 'resourceStatistics',

View File

@ -1,249 +1,356 @@
<template>
<x-search-bar class="mb-8-2">
<template #default="{ gutter, colSpan }">
<a-form
:label-col="{ style: { width: '100px' } }"
:model="searchFormData"
layout="inline">
<a-form :model="searchFormData" layout="inline">
<a-row :gutter="gutter">
<a-col v-bind="colSpan">
<a-form-item name="title">
<template #label>
规则名称
<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 label="主机名称/ID" name="machine_name">
<a-input placeholder="请输入主机名称/ID" v-model:value="searchFormData.machine_name"></a-input>
</a-form-item>
</a-col>
<a-col v-bind="colSpan">
<a-form-item label="描述">
<a-input></a-input>
<a-form-item label="内网IP" name="internal_ip">
<a-input placeholder="请输入内网IP" v-model:value="searchFormData.internal_ip"></a-input>
</a-form-item>
</a-col>
<template v-if="searchBarExpand">
<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-col class="align-right" v-bind="colSpan">
<a-space>
<a-button>重置</a-button>
<a-button
ghost
type="primary"
@click="handleSearch">
<a-button @click="handleResetSearch">重置</a-button>
<a-button ghost type="primary" @click="handleSearch">
搜索
</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-col>
</a-row>
</a-form>
</template>
</x-search-bar>
<a-card>
<x-action-bar class="mb-8-2">
<a-button
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-row :gutter="8" :wrap="false">
<a-col flex="auto">
<a-card type="flex">
<a-table
:columns="columns"
:data-source="listData"
:loading="loading"
:pagination="paginationState"
:size="size"
row-key="id"
:columns="columns" :data-source="listData" :loading="loading" :pagination="paginationState"
:scroll="{ x: 1400 }"
@change="onTableChange">
<template #bodyCell="{ column, record }">
<template v-if="'action' === column.key">
<x-action-button @click="$refs.editDialogRef.handleEdit(record)">编辑</x-action-button>
<x-action-button @click="handleDelete(record)">删除</x-action-button>
<x-action-button>
<a-dropdown :trigger="['click']">
<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 v-if="'health_status' === column.key">
<a-tag :color="getHealthStatusColor(record.health_status)">
{{ getHealthStatusText(record.health_status) }}
</a-tag>
</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>
</a-table>
</a-card>
</a-col>
</a-row>
<edit-dialog
ref="editDialogRef"
@ok="onOk" />
<edit-dialog ref="editDialogRef" @ok="onOk"></edit-dialog>
</template>
<script setup>
import { message, Modal } from 'ant-design-vue'
import { ref } from 'vue'
import {
ColumnHeightOutlined,
DownOutlined,
QuestionCircleOutlined,
ReloadOutlined,
SettingOutlined,
UpOutlined,
PlusOutlined,
MoreOutlined,
} from '@ant-design/icons-vue'
import apis from '@/apis'
import { formatUtcDateTime } from '@/utils/util'
import { config } from '@/config'
import { usePagination } from '@/hooks'
import { usePagination, useForm } from '@/hooks'
import EditDialog from './components/EditDialog.vue'
import { useI18n } from 'vue-i18n'
defineOptions({
name: 'listTable',
name: 'hostManagement',
})
const columns = [
{ 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')
const { t } = useI18n()
// 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() {
try {
// showLoading()
// const { pageSize, current } = paginationState
// const { code, data } = await apis.common
// .getPageList({
// pageSize,
// current: current,
// })
// .catch(() => {
// throw new Error()
// })
// hideLoading()
// if (config('http.code.success') === code) {
// const { records, pagination } = data
// listData.value = records
// paginationState.total = pagination.total
// }
showLoading()
const { pageSize, current } = paginationState
const { success, data, total } = await apis.resource
.getHostsList({
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()
hideLoading()
message.error('获取主机列表失败')
}
}
/**
* 搜索
* 移除/删除主机
*/
function handleSearch() {
// resetPagination()
// getPageList()
}
/**
* 删除
*/
function handleDelete({ id }) {
function handleRemove({ id, machine_name }) {
Modal.confirm({
title: '删除提示',
content: '确认删除?',
title: '确认删除',
content: `确定要删除主机 ${machine_name} 吗?此操作不可恢复。`,
okText: '确认删除',
cancelText: '取消',
okType: 'danger',
onOk: () => {
return new Promise((resolve, reject) => {
;(async () => {
; (async () => {
try {
const { code } = await apis.common.del(id).catch(() => {
const { success } = await apis.resource.deleteHost(id).catch(() => {
throw new Error()
})
if (config('http.code.success') === code) {
if (config('http.code.success') === success) {
resolve()
message.success('删除成功')
await getPageList()
}
} catch (error) {
reject()
message.error('删除失败')
}
})()
})
@ -252,30 +359,77 @@ function handleDelete({ id }) {
}
/**
* 密度
* @param {string} key
* 下架主机
*/
function handleSize({ key }) {
size.value = key
function handleDownShelf({ id, machine_name }) {
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 }) {
// paginationState.current = current
// paginationState.pageSize = pageSize
// getPageList()
paginationState.current = current
paginationState.pageSize = pageSize
getPageList()
}
/**
* 完成
* 重置搜索
*/
function onOk() {
// getPageList()
function handleResetSearch() {
searchFormData.value = {}
resetPagination()
getPageList()
}
/**
* 搜索
*/
function handleSearch() {
resetForm()
resetPagination()
getPageList()
}
/**
* 编辑完成
*/
async function onOk() {
await getPageList()
}
</script>
<style lang="less" scoped></style>
<style lang="less" scoped>
.align-right {
text-align: right;
}
.mb-8-2 {
margin-bottom: 8px;
}
</style>

View File

@ -0,0 +1,119 @@
<template>
<a-modal
:open="modal.open"
:title="modal.title"
: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' } }">
<a-form-item
label="标题"
name="title">
<a-input v-model:value="formData.title"></a-input>
</a-form-item>
</a-form>
</a-modal>
</template>
<script setup>
import { cloneDeep } from 'lodash-es'
import apis from '@/apis'
import { useForm, useModal } from '@/hooks'
const emit = defineEmits(['ok'])
const { modal, showModal, hideModal, showLoading, hideLoading } = useModal()
const { formRef, formRules, formRecord, formData, resetForm } = useForm()
formRules.value = {
title: { required: true, message: '请输入标题' },
}
/**
* 新建
*/
function handleCreate() {
showModal({
type: 'create',
title: '新建',
})
}
/**
* 编辑
*/
function handleEdit(record = {}) {
showModal({
type: 'edit',
title: '编辑',
})
formRecord.value = record
formData.value = cloneDeep(record)
}
/**
* 确定
*/
function handleOk() {
formRef.value
.validateFields()
.then(async (values) => {
try {
showLoading()
const params = {
...values,
}
let result = null
switch (modal.value.type) {
case 'create':
result = await apis.common.create(params).catch(() => {
throw new Error()
})
break
case 'edit':
result = await apis.common.update(formRecord.value.id, params).catch(() => {
throw new Error()
})
break
}
hideLoading()
if (200 === result?.code) {
hideModal()
emit('ok')
}
} catch (error) {
hideLoading()
}
})
.catch(() => {
hideLoading()
})
}
/**
* 取消
*/
function handleCancel() {
hideModal()
}
/**
* 关闭后
*/
function onAfterClose() {
resetForm()
}
defineExpose({
handleCreate,
handleEdit,
})
</script>
<style lang="less" scoped></style>

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

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

View File

@ -1,21 +1,16 @@
<template>
<a-modal
:open="modal.open"
:title="modal.title"
:confirm-loading="modal.confirmLoading"
:after-close="onAfterClose"
@ok="handleOk"
@cancel="handleCancel">
<a-form
ref="formRef"
scroll-to-first-error
:model="formData"
:rules="formRules"
<a-modal :open="modal.open" :title="modal.title" :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' } }">
<a-form-item
label="标题"
name="title">
<a-input v-model:value="formData.title"></a-input>
<a-form-item label="封禁原因" name="blackReason">
<a-textarea v-model:value="formData.blackReason"></a-textarea>
</a-form-item>
<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>
</a-modal>
@ -25,6 +20,7 @@
import { cloneDeep } from 'lodash-es'
import apis from '@/apis'
import { useForm, useModal } from '@/hooks'
import { config } from '@/config'
const emit = defineEmits(['ok'])
@ -32,7 +28,8 @@ const { modal, showModal, hideModal, showLoading, hideLoading } = useModal()
const { formRef, formRules, formRecord, formData, resetForm } = useForm()
formRules.value = {
title: { required: true, message: '请输入标题' },
blackReason: { required: true, message: '请输入理由' },
blackDays: { required: true, message: '请输入封禁天数' }
}
/**
@ -51,10 +48,9 @@ function handleCreate() {
function handleEdit(record = {}) {
showModal({
type: 'edit',
title: '编辑',
title: '用户封禁',
})
formRecord.value = record
formData.value = cloneDeep(record)
}
/**
@ -67,7 +63,9 @@ function handleOk() {
try {
showLoading()
const params = {
...values,
blackDays: Number(values.blackDays),
blackReason: values.blackReason,
remark: values.remark
}
let result = null
switch (modal.value.type) {
@ -77,13 +75,13 @@ function handleOk() {
})
break
case 'edit':
result = await apis.common.update(formRecord.value.id, params).catch(() => {
result = await apis.userControl.updateCustomers(formRecord.value, params).catch(() => {
throw new Error()
})
break
}
hideLoading()
if (200 === result?.code) {
if (config('http.code.success') === result?.success) {
hideModal()
emit('ok')
}

View File

@ -78,7 +78,7 @@
</x-action-button>
<x-action-button @click="handleRemove(record)">
<a-tooltip>
<template #title> {{ $t('pages.system.delete') }}</template>
<template #title> 拉黑用户</template>
<delete-outlined style="color: #ff4d4f" />
</a-tooltip>
</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({
title: t('pages.system.role.delTip'),
content: t('button.confirm'),
title: '确认操作',
content: '确认拉黑该用户?',
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()
}
})()
})
editDialogRef.value?.handleEdit(id)
},
})
}