feat(打包): 打包部署

master
tangqk 2025-08-13 16:45:06 +08:00
commit 5840992c97
53 changed files with 852 additions and 265 deletions

View File

@ -4,7 +4,7 @@ services:
image: nex-be:1.0.3 image: nex-be:1.0.3
container_name: nex-be container_name: nex-be
ports: ports:
- "8113:8112" - "8113:8113"
volumes: volumes:
- /var/lib/vdi/:/var/lib/vdi/ - /var/lib/vdi/:/var/lib/vdi/
environment: environment:

View File

@ -86,6 +86,13 @@
<artifactId>lombok</artifactId> <artifactId>lombok</artifactId>
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
<!-- Apache HttpClient 依赖 -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.14</version>
</dependency>
<dependency> <dependency>

View File

@ -2,6 +2,7 @@ package com.unisinsight.project.config;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate; import org.springframework.web.client.RestTemplate;
/** /**
@ -15,6 +16,10 @@ public class RestTemplateConfig {
@Bean @Bean
public RestTemplate restTemplate() { public RestTemplate restTemplate() {
return new RestTemplate(); HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
factory.setConnectTimeout(600000);
factory.setReadTimeout(600000);
factory.setConnectionRequestTimeout(600000);
return new RestTemplate(factory);
} }
} }

View File

@ -14,6 +14,7 @@ import com.unisinsight.project.service.DeviceImageMappingService;
import io.swagger.annotations.Api; import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ObjectUtils;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
@ -47,10 +48,20 @@ public class DeviceImageMappingController {
} }
log.info("终端镜像映射新增请求参数为:{}", JSONUtil.toJsonStr(deviceImageMappingReq)); log.info("终端镜像映射新增请求参数为:{}", JSONUtil.toJsonStr(deviceImageMappingReq));
QueryWrapper<DeviceImageMapping> wrapper = new QueryWrapper<>();
if (CollectionUtil.isEmpty(deviceImageMappingReq.getData()) && ObjectUtils.isNotEmpty(deviceImageMappingReq.getDeviceId())) {
wrapper.lambda().eq(DeviceImageMapping::getDeviceId, deviceImageMappingReq.getDeviceId());
List<DeviceImageMapping> list = deviceImageMappingService.list(wrapper);
List<Long> collect = list.stream().map(DeviceImageMapping::getId).filter(Objects::nonNull).distinct().collect(Collectors.toList());
if (CollectionUtil.isNotEmpty(collect)) {
boolean removedByIds = deviceImageMappingService.removeByIds(collect);
log.info("终端镜像映射新增接口删除了 {} 条旧数据ID列表: {}", removedByIds, collect);
}
return Result.successResult();
}
List<DeviceImageMappingReq> reqData = deviceImageMappingReq.getData(); List<DeviceImageMappingReq> reqData = deviceImageMappingReq.getData();
List<DeviceImageMappingReq> addList = reqData.stream().distinct().filter(e -> Objects.isNull(e.getId())).collect(Collectors.toList()); List<DeviceImageMappingReq> addList = reqData.stream().distinct().filter(e -> Objects.isNull(e.getId())).collect(Collectors.toList());
QueryWrapper<DeviceImageMapping> wrapper = new QueryWrapper<>();
wrapper.lambda().eq(DeviceImageMapping::getDeviceId, reqData.get(0).getDeviceId()); wrapper.lambda().eq(DeviceImageMapping::getDeviceId, reqData.get(0).getDeviceId());
List<DeviceImageMapping> list = deviceImageMappingService.list(wrapper); List<DeviceImageMapping> list = deviceImageMappingService.list(wrapper);

View File

@ -14,6 +14,7 @@ import com.unisinsight.project.service.DeviceUserMappingService;
import io.swagger.annotations.Api; import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ObjectUtils;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
@ -46,10 +47,20 @@ public class DeviceUserMappingController {
return Result.errorResult(BaseErrorCode.PARAMETERS_INVALID); return Result.errorResult(BaseErrorCode.PARAMETERS_INVALID);
} }
log.info("终端用户映射新增请求参数为:{}", JSONUtil.toJsonStr(deviceUserMappingReq)); log.info("终端用户映射新增请求参数为:{}", JSONUtil.toJsonStr(deviceUserMappingReq));
QueryWrapper<DeviceUserMapping> wrapper = new QueryWrapper<>();
if (CollectionUtil.isEmpty(deviceUserMappingReq.getData()) && ObjectUtils.isNotEmpty(deviceUserMappingReq.getDeviceId())) {
wrapper.lambda().eq(DeviceUserMapping::getDeviceId, deviceUserMappingReq.getDeviceId());
List<DeviceUserMapping> list = deviceUserMappingService.list(wrapper);
List<Long> collect = list.stream().map(DeviceUserMapping::getId).filter(Objects::nonNull).distinct().collect(Collectors.toList());
if (CollectionUtil.isNotEmpty(collect)) {
boolean removedByIds = deviceUserMappingService.removeByIds(collect);
log.info("终端用户映射新增接口删除了 {} 条旧数据ID列表: {}", removedByIds, collect);
}
return Result.successResult();
}
List<DeviceUserMappingReq> userMappingReqData = deviceUserMappingReq.getData(); List<DeviceUserMappingReq> userMappingReqData = deviceUserMappingReq.getData();
List<DeviceUserMappingReq> addList = userMappingReqData.stream().distinct().filter(e -> Objects.isNull(e.getId())).collect(Collectors.toList()); List<DeviceUserMappingReq> addList = userMappingReqData.stream().distinct().filter(e -> Objects.isNull(e.getId())).collect(Collectors.toList());
QueryWrapper<DeviceUserMapping> wrapper = new QueryWrapper<>();
wrapper.lambda().eq(DeviceUserMapping::getDeviceId, userMappingReqData.get(0).getDeviceId()); wrapper.lambda().eq(DeviceUserMapping::getDeviceId, userMappingReqData.get(0).getDeviceId());
List<DeviceUserMapping> list = deviceUserMappingService.list(wrapper); List<DeviceUserMapping> list = deviceUserMappingService.list(wrapper);

View File

@ -161,13 +161,13 @@ public class FileChunkController {
// 异步执行创建和做种操作 // 异步执行创建和做种操作
CompletableFuture.runAsync(() -> { CompletableFuture.runAsync(() -> {
try { try {
String url = btUrl + "/test/start?sourceFile=%s&torrentFile=%s"; String url = btUrl + "/vdi/start?sourceFile=%s&torrentFile=%s";
url = String.format(url, finalFilePath, finalFilePath + ".torrent"); url = String.format(url, finalFilePath, finalFilePath + ".torrent");
log.info("请求bt创建接口参数: {}", url); log.info("请求bt创建接口参数: {}", url);
ResponseEntity<Boolean> responseEntity = restTemplate.exchange(url, HttpMethod.GET, null, Boolean.class); ResponseEntity<Boolean> responseEntity = restTemplate.exchange(url, HttpMethod.GET, null, Boolean.class);
log.info("请求bt创建接口返回: {}", JSONUtil.toJsonStr(responseEntity)); log.info("请求bt创建接口返回: {}", JSONUtil.toJsonStr(responseEntity));
HttpStatus statusCode = responseEntity.getStatusCode(); HttpStatus statusCode = responseEntity.getStatusCode();
if (statusCode != HttpStatus.OK) { if (statusCode == HttpStatus.OK) {
boolean result = Boolean.TRUE.equals(responseEntity.getBody()); boolean result = Boolean.TRUE.equals(responseEntity.getBody());
if (result) { if (result) {
log.info("请求bt创建接口成功"); log.info("请求bt创建接口成功");

View File

@ -3,11 +3,13 @@ package com.unisinsight.project.controller;
import com.unisinsight.project.util.BtTorrentUtils; import com.unisinsight.project.util.BtTorrentUtils;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
@RestController("test") @RestController
@Slf4j @Slf4j
@RequestMapping("/vdi")
public class TestController { public class TestController {
@GetMapping("/start") @GetMapping("/start")
@ -20,11 +22,11 @@ public class TestController {
} }
@GetMapping("/stop") @GetMapping("/stop")
public String stop(@RequestParam("sourceFile") String sourceFile) { public boolean stop(@RequestParam("sourceFile") String sourceFile) {
// 测试停止 // 测试停止
System.out.println("停止做种..."); System.out.println("停止做种...");
boolean stopResult = BtTorrentUtils.stopSeeding(sourceFile); boolean stopResult = BtTorrentUtils.stopSeeding(sourceFile);
System.out.println("停止结果: " + (stopResult ? "成功" : "失败")); System.out.println("停止结果: " + (stopResult ? "成功" : "失败"));
return "success"; return stopResult;
} }
} }

View File

@ -7,6 +7,7 @@ import io.swagger.annotations.ApiModelProperty;
import lombok.Data; import lombok.Data;
import java.io.Serializable; import java.io.Serializable;
import java.util.Date;
/** /**
* @TableName image * @TableName image
@ -70,6 +71,11 @@ public class ImageRes implements Serializable {
@ApiModelProperty("镜像存储路径") @ApiModelProperty("镜像存储路径")
@JsonProperty("storage_path") @JsonProperty("storage_path")
private String storagePath; private String storagePath;
/**
*
*/
@ApiModelProperty("创建时间")
@JsonProperty("create_time")
private Date createTime;
} }

View File

@ -20,4 +20,12 @@ public class ListReq<T> {
@JsonProperty("data") @JsonProperty("data")
private List<T> data; private List<T> data;
/**
*
*/
@ApiModelProperty("序列号")
@JsonProperty("device_id")
private String deviceId;
} }

View File

@ -7,16 +7,17 @@ import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.unisinsight.project.entity.dao.DeviceImageMapping; import com.unisinsight.project.entity.dao.DeviceImageMapping;
import com.unisinsight.project.entity.dao.Image; import com.unisinsight.project.entity.dao.Image;
import com.unisinsight.project.entity.res.ImageRes; import com.unisinsight.project.entity.res.ImageRes;
import com.unisinsight.project.exception.BaseErrorCode;
import com.unisinsight.project.exception.Result; import com.unisinsight.project.exception.Result;
import com.unisinsight.project.mapper.DeviceImageMappingMapper; import com.unisinsight.project.mapper.DeviceImageMappingMapper;
import com.unisinsight.project.mapper.ImageMapper; import com.unisinsight.project.mapper.ImageMapper;
import com.unisinsight.project.service.ClientService; import com.unisinsight.project.service.ClientService;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import javax.annotation.Resource; import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
@ -37,15 +38,19 @@ public class ClientServiceImpl implements ClientService {
@Resource @Resource
private DeviceImageMappingMapper deviceImageMappingMapper; private DeviceImageMappingMapper deviceImageMappingMapper;
// 请求bt配置
@Value("${file.upload.bt-url}")
private String btUrl;
@Override @Override
public Result<?> getImageList(String deviceId, String token) { public Result<?> getImageList(String deviceId, String token) {
HashMap<String, Object> hashMap = new HashMap<>();
QueryWrapper<DeviceImageMapping> deviceImageMappingQueryWrapper = new QueryWrapper<>(); QueryWrapper<DeviceImageMapping> deviceImageMappingQueryWrapper = new QueryWrapper<>();
deviceImageMappingQueryWrapper.lambda().eq(DeviceImageMapping::getDeviceId, deviceId); deviceImageMappingQueryWrapper.lambda().eq(DeviceImageMapping::getDeviceId, deviceId);
List<DeviceImageMapping> deviceImageMappings = deviceImageMappingMapper.selectList(deviceImageMappingQueryWrapper); List<DeviceImageMapping> deviceImageMappings = deviceImageMappingMapper.selectList(deviceImageMappingQueryWrapper);
if (CollectionUtil.isEmpty(deviceImageMappings)) { if (CollectionUtil.isEmpty(deviceImageMappings)) {
return Result.errorResultMessage(BaseErrorCode.PARAMS_CHK_ERROR, "请先配置终端镜像"); hashMap.put("list", new ArrayList<>());
return Result.successResult(hashMap);
} }
List<Long> imageIdList = deviceImageMappings.stream().map(DeviceImageMapping::getImageId).filter(Objects::nonNull).distinct().collect(Collectors.toList()); List<Long> imageIdList = deviceImageMappings.stream().map(DeviceImageMapping::getImageId).filter(Objects::nonNull).distinct().collect(Collectors.toList());
if (CollectionUtil.isNotEmpty(imageIdList)) { if (CollectionUtil.isNotEmpty(imageIdList)) {
@ -55,20 +60,28 @@ public class ClientServiceImpl implements ClientService {
log.info("用户登录查询镜像结果:{}", JSONUtil.toJsonStr(images)); log.info("用户登录查询镜像结果:{}", JSONUtil.toJsonStr(images));
List<ImageRes> imageRes = BeanUtil.copyToList(images, ImageRes.class); List<ImageRes> imageRes = BeanUtil.copyToList(images, ImageRes.class);
List<HashMap<String, Object>> collect = imageRes.stream().distinct().map(e -> { List<HashMap<String, Object>> collect = imageRes.stream().distinct().map(e -> {
HashMap<String, Object> hashMap = new HashMap<>(); HashMap<String, Object> map = new HashMap<>();
if (StringUtils.isNotBlank(e.getImageName())) { if (StringUtils.isNotBlank(e.getImageName())) {
hashMap.put("name", e.getImageName()); map.put("name", e.getImageName());
} }
if (StringUtils.isNotBlank(e.getBtPath())) { if (StringUtils.isNotBlank(e.getBtPath())) {
hashMap.put("torrent", e.getBtPath()); if (e.getBtPath().contains("http://") || e.getBtPath().contains("https://")) {
map.put("torrent", e.getBtPath());
} else {
String fileName = e.getBtPath().substring(Math.max(e.getBtPath().lastIndexOf("\\"), e.getBtPath().lastIndexOf("/")) + 1);
map.put("torrent", btUrl + "/api/vdi/file/down/" + fileName);
}
} }
return hashMap; return map;
}).collect(Collectors.toList()); }).collect(Collectors.toList());
HashMap<String, Object> hashMap = new HashMap<>();
hashMap.put("list", collect); if (CollectionUtil.isNotEmpty(collect)) {
return Result.successResult(hashMap); hashMap.put("list", collect);
} else {
hashMap.put("list", new ArrayList<>());
}
} }
return Result.successResult(); return Result.successResult(hashMap);
} }
} }

View File

@ -119,12 +119,20 @@ public class DeviceUserMappingServiceImpl extends ServiceImpl<DeviceUserMappingM
@Override @Override
public Result<?> loginUser(DeviceUserReq deviceUserReq) { public Result<?> loginUser(DeviceUserReq deviceUserReq) {
QueryWrapper<User> userQueryWrapper = new QueryWrapper<>();
userQueryWrapper.lambda().in(User::getUserName, deviceUserReq.getUserName());
List<User> userList1 = userMapper.selectList(userQueryWrapper);
log.info("登录查询用户结果:{}", JSONUtil.toJsonStr(userList1));
if (CollectionUtil.isEmpty(userList1)) {
return Result.errorResultMessage(BaseErrorCode.PARAMS_CHK_ERROR, "用户不存在");
}
QueryWrapper<DeviceUserMapping> wrapper = new QueryWrapper<>(); QueryWrapper<DeviceUserMapping> wrapper = new QueryWrapper<>();
wrapper.lambda().eq(DeviceUserMapping::getDeviceId, deviceUserReq.getDeviceId()); wrapper.lambda().eq(DeviceUserMapping::getDeviceId, deviceUserReq.getDeviceId());
List<DeviceUserMapping> deviceUserMappings = deviceUserMappingMapper.selectList(wrapper); List<DeviceUserMapping> deviceUserMappings = deviceUserMappingMapper.selectList(wrapper);
log.info("用户登录查询映射结果:{}", JSONUtil.toJsonStr(deviceUserMappings)); log.info("用户登录查询映射结果:{}", JSONUtil.toJsonStr(deviceUserMappings));
List<User> users = new ArrayList<>();
List<User> users = new ArrayList<>();
List<Long> userIdList = deviceUserMappings.stream().map(DeviceUserMapping::getUserId).filter(Objects::nonNull).distinct().collect(Collectors.toList()); List<Long> userIdList = deviceUserMappings.stream().map(DeviceUserMapping::getUserId).filter(Objects::nonNull).distinct().collect(Collectors.toList());
log.info("用户登录查询用户id结果:{}", JSONUtil.toJsonStr(userIdList)); log.info("用户登录查询用户id结果:{}", JSONUtil.toJsonStr(userIdList));
if (CollectionUtil.isNotEmpty(userIdList)) { if (CollectionUtil.isNotEmpty(userIdList)) {
@ -152,11 +160,11 @@ public class DeviceUserMappingServiceImpl extends ServiceImpl<DeviceUserMappingM
} }
log.info("用户登录查询结果:{}", JSONUtil.toJsonStr(users)); log.info("用户登录查询结果:{}", JSONUtil.toJsonStr(users));
if (CollectionUtil.isEmpty(users)) { if (CollectionUtil.isEmpty(users)) {
return Result.errorResultMessage(BaseErrorCode.PARAMS_CHK_ERROR, "用户不存在,请重试"); return Result.errorResultMessage(BaseErrorCode.PARAMS_CHK_ERROR, "当前用户无权限登录终端,请联系管理员");
} }
users = users.stream().filter(Objects::nonNull).distinct().collect(Collectors.toList()); users = users.stream().filter(Objects::nonNull).distinct().collect(Collectors.toList());
if (CollectionUtil.isEmpty(users)) { if (CollectionUtil.isEmpty(users)) {
return Result.errorResultMessage(BaseErrorCode.PARAMS_CHK_ERROR, "用户不存在,请重试"); return Result.errorResultMessage(BaseErrorCode.PARAMS_CHK_ERROR, "当前用户无权限登录终端,请联系管理员");
} }
if (!users.get(0).getPassword().equals(deviceUserReq.getPassword())) { if (!users.get(0).getPassword().equals(deviceUserReq.getPassword())) {
return Result.errorResultMessage(BaseErrorCode.PARAMS_CHK_ERROR, "密码错误,请重新输入"); return Result.errorResultMessage(BaseErrorCode.PARAMS_CHK_ERROR, "密码错误,请重新输入");

View File

@ -30,6 +30,8 @@ import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.util.List;
import java.util.stream.Collectors;
/** /**
* @author rdpnr_puzhi * @author rdpnr_puzhi
@ -71,6 +73,16 @@ public class ImageServiceImpl extends ServiceImpl<ImageMapper, Image>
return Result.successResult(); return Result.successResult();
} else { } else {
PageResult<ImageRes> convert = PageResult.convertIPage(imageIPage, ImageRes.class); PageResult<ImageRes> convert = PageResult.convertIPage(imageIPage, ImageRes.class);
List<ImageRes> data = convert.getData();
List<ImageRes> collect = data.stream().distinct().peek(e -> {
if (StringUtils.isNotBlank(e.getBtPath())) {
if (!e.getBtPath().contains("http://") || !e.getBtPath().contains("https://")) {
String fileName = e.getBtPath().substring(Math.max(e.getBtPath().lastIndexOf("\\"), e.getBtPath().lastIndexOf("/")) + 1);
e.setBtPath(btUrl + "/api/vdi/file/down/" + fileName);
}
}
}).collect(Collectors.toList());
convert.setData(collect);
return Result.successResult(convert); return Result.successResult(convert);
} }
} }
@ -81,7 +93,7 @@ public class ImageServiceImpl extends ServiceImpl<ImageMapper, Image>
if (ObjectUtils.isNotEmpty(image)) { if (ObjectUtils.isNotEmpty(image)) {
try { try {
try { try {
String url = btUrl + "/test/stop?sourceFile=%s"; String url = btUrl + "/vdi/stop?sourceFile=%s";
url = String.format(url, image.getStoragePath()); url = String.format(url, image.getStoragePath());
log.info("请求bt停止接口参数: {}", url); log.info("请求bt停止接口参数: {}", url);
ResponseEntity<Boolean> responseEntity = restTemplate.exchange(url, HttpMethod.GET, null, Boolean.class); ResponseEntity<Boolean> responseEntity = restTemplate.exchange(url, HttpMethod.GET, null, Boolean.class);
@ -107,8 +119,13 @@ public class ImageServiceImpl extends ServiceImpl<ImageMapper, Image>
return Result.successResult(); return Result.successResult();
} }
} else { } else {
int deleted = imageMapper.deleteById(deleteIdReq.getId());
log.info("文件不存在,镜像删除insert:{}", deleted);
if (deleted == 1) {
return Result.successResult();
}
log.warn("文件不存在,无需删除: {}", filePath); log.warn("文件不存在,无需删除: {}", filePath);
return Result.errorResultMessage(BaseErrorCode.HTTP_ERROR_CODE_500, "文件不存在,无需删除"); return Result.successResult();
} }
} catch (IOException e) { } catch (IOException e) {
log.error("删除文件失败: {}", e.getMessage(), e); log.error("删除文件失败: {}", e.getMessage(), e);

View File

@ -1,6 +1,6 @@
# application.yml # application.yml
server: server:
port: 8112 port: 8113
file: file:
upload: upload:
temp-dir: /var/lib/vdi/tmp/chunked-uploads temp-dir: /var/lib/vdi/tmp/chunked-uploads

View File

@ -45,11 +45,11 @@ export default defineConfig({
npmClient: 'pnpm', npmClient: 'pnpm',
proxy: { proxy: {
'/api/nex/v1/': { '/api/nex/v1/': {
target: 'http://10.100.51.85:8112', target: 'http://10.100.51.85:8113',
// changeOrigin: true, // changeOrigin: true,
}, },
'/api/files': { '/api/files': {
target: 'http://10.100.51.85:8112', target: 'http://10.100.51.85:8113',
}, },
}, },
}); });

22
web-fe/serve/dist/106.async.js vendored 100644

File diff suppressed because one or more lines are too long

27
web-fe/serve/dist/306.async.js vendored 100644

File diff suppressed because one or more lines are too long

44
web-fe/serve/dist/35.async.js vendored 100644

File diff suppressed because one or more lines are too long

58
web-fe/serve/dist/402.async.js vendored 100644

File diff suppressed because one or more lines are too long

30
web-fe/serve/dist/489.async.js vendored 100644

File diff suppressed because one or more lines are too long

7
web-fe/serve/dist/67.async.js vendored 100644

File diff suppressed because one or more lines are too long

57
web-fe/serve/dist/687.async.js vendored 100644

File diff suppressed because one or more lines are too long

2
web-fe/serve/dist/732.async.js vendored 100644

File diff suppressed because one or more lines are too long

3
web-fe/serve/dist/818.async.js vendored 100644

File diff suppressed because one or more lines are too long

9
web-fe/serve/dist/850.async.js vendored 100644

File diff suppressed because one or more lines are too long

25
web-fe/serve/dist/929.async.js vendored 100644

File diff suppressed because one or more lines are too long

70
web-fe/serve/dist/966.async.js vendored 100644

File diff suppressed because one or more lines are too long

10
web-fe/serve/dist/983.async.js vendored 100644

File diff suppressed because one or more lines are too long

13
web-fe/serve/dist/index.html vendored 100644
View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<link rel="stylesheet" href="/umi.css">
</head>
<body>
<div id="root"></div>
<script src="/umi.js"></script>
</body>
</html>

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
.main-layout{min-height:100vh}.main-sider .logo{height:64px;display:flex;align-items:center;justify-content:center;color:#fff;font-size:18px;font-weight:700;border-bottom:1px solid #303030}.main-header{background:#fff;padding:0 24px;display:flex;align-items:center;justify-content:space-between;box-shadow:0 2px 8px #0000001a}.main-header .trigger{font-size:18px;color:#666}.main-header .trigger:hover{color:#1890ff}.main-header .header-right{display:flex;align-items:center;gap:16px}.main-header .header-right .welcome-text{color:#666;font-size:14px}.main-header .header-right .user-avatar{cursor:pointer;background:#1890ff}.main-header .header-right .user-avatar:hover{opacity:.8}.main-content{background:#fff;border-radius:8px;box-shadow:0 2px 8px #0000001a;min-height:calc(100vh - 112px)}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
.main-layout{min-height:100vh}.main-sider .logo{height:64px;display:flex;align-items:center;justify-content:center;color:#fff;font-size:18px;font-weight:700;border-bottom:1px solid #303030}.main-header{background:#fff;padding:0 24px;display:flex;align-items:center;justify-content:space-between;box-shadow:0 2px 8px #0000001a}.main-header .trigger{font-size:18px;color:#666}.main-header .trigger:hover{color:#1890ff}.main-header .header-right{display:flex;align-items:center;gap:16px}.main-header .header-right .welcome-text{color:#666;font-size:14px}.main-header .header-right .user-avatar{cursor:pointer;background:#1890ff}.main-header .header-right .user-avatar:hover{opacity:.8}.main-content{background:#fff;border-radius:8px;box-shadow:0 2px 8px #0000001a;min-height:calc(100vh - 112px)}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
.page-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:24px}.page-header h2{margin:0;font-size:24px;font-weight:600;color:#333}.image-list{height:100%;display:flex;flex-direction:column;padding:16px;box-sizing:border-box}.image-list .search-box{margin-bottom:16px;display:flex;justify-content:space-between}.image-list .search-box .search-input{display:flex;gap:8px;align-items:center}.image-list .images-list-container,.image-list .images-list-container .images-list-table{flex:1 1;display:flex;flex-direction:column;overflow:hidden}.image-list .images-list-container .images-list-table .ant-table-wrapper{display:flex;flex-direction:column;flex:1 1;overflow:hidden}.image-list .images-list-container .images-list-table .ant-table-wrapper .ant-spin-nested-loading,.image-list .images-list-container .images-list-table .ant-table-wrapper .ant-spin-nested-loading .ant-spin-container{flex:1 1;display:flex;flex-direction:column;overflow:hidden}.image-list .images-list-container .images-list-table .ant-table-wrapper .ant-spin-nested-loading .ant-spin-container .ant-table,.image-list .images-list-container .images-list-table .ant-table-wrapper .ant-spin-nested-loading .ant-spin-container .ant-table .ant-table-container{display:flex;flex-direction:column;flex:1 1;overflow:hidden}.image-list .images-list-container .images-list-table .ant-table-wrapper .ant-spin-nested-loading .ant-spin-container .ant-table .ant-table-container .ant-table-header{flex-shrink:0}.image-list .images-list-container .images-list-table .ant-table-wrapper .ant-spin-nested-loading .ant-spin-container .ant-table .ant-table-container .ant-table-body{flex:1 1;overflow:auto!important}.image-list .images-list-container .images-list-table .ant-table-wrapper .ant-spin-nested-loading .ant-spin-container .ant-table .ant-table-pagination{flex-shrink:0;position:relative;z-index:1}.image-list .image-detail .detail-item{margin-bottom:16px}.image-list .image-detail .detail-item label{font-weight:600;color:#333;display:inline-block;width:100px}.image-list .image-detail .detail-item span{color:#666}.image-list .image-detail .detail-item p{margin:8px 0 0 100px;color:#666;line-height:1.6}.profile-page .profile-content .profile-header{display:flex;align-items:center;gap:16px;margin-bottom:16px}.profile-page .profile-content .profile-header .profile-info h3{margin:0 0 4px;font-size:20px;font-weight:600;color:#333}.profile-page .profile-content .profile-header .profile-info p{margin:0;color:#666;font-size:14px}.profile-page .profile-content .quick-actions{display:flex;gap:12px;flex-wrap:wrap}@media (max-width: 768px){.page-header{flex-direction:column;align-items:flex-start;gap:12px}.profile-content .profile-header{flex-direction:column;text-align:center}.profile-content .quick-actions{justify-content:center}}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
.login-container{display:flex;min-height:100vh;background:linear-gradient(135deg,#1890ff,#722ed1)}.login-container:before{content:"";position:absolute;top:0;left:0;right:0;bottom:0;background:url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><pattern id="grid" width="10" height="10" patternUnits="userSpaceOnUse"><path d="M 10 0 L 0 0 0 10" fill="none" stroke="rgba%28255,255,255,0.1%29" stroke-width="0.5"/></pattern></defs><rect width="100" height="100" fill="url%28%23grid%29"/></svg>');opacity:.3}.login-left{flex:1 1;display:flex;align-items:center;justify-content:center;position:relative;overflow:hidden}.brand-content{text-align:center;color:#fff;z-index:1;position:relative;max-width:400px;padding:40px}.brand-logo{margin-bottom:30px}.brand-logo .logo-icon{font-size:60px;margin-bottom:20px;display:block;color:#ffffffe6}.brand-logo .brand-title{font-size:36px;font-weight:700;margin:0;color:#fff;text-shadow:0 2px 4px rgba(0,0,0,.3)}.brand-subtitle{font-size:20px;font-weight:500;margin-bottom:30px;color:#ffffffe6}.brand-description{margin-bottom:40px}.brand-description p{font-size:16px;line-height:1.6;margin:8px 0;color:#fffc}.brand-features{display:flex;justify-content:space-around;flex-wrap:wrap;gap:20px}.brand-features .feature-item{display:flex;flex-direction:column;align-items:center;gap:8px}.brand-features .feature-item .feature-icon{font-size:24px}.brand-features .feature-item span:last-child{font-size:14px;color:#fffc}.login-right{flex:1 1;display:flex;align-items:center;justify-content:center;padding:40px}.login-form-container{width:100%;max-width:400px}.login-header{text-align:center;margin-bottom:40px}.login-header .login-title{font-size:28px;font-weight:600;color:#fff;margin:0 0 8px}.login-header .login-subtitle{font-size:16px;color:#fff;margin:0}.login-form .ant-form-item{margin-bottom:24px}.login-input{height:48px;border-radius:8px;border:1px solid #d9d9d9;font-size:16px}.login-input:hover,.login-input:focus,.login-input.ant-input-focused{border-color:#1890ff;box-shadow:0 0 0 2px #1890ff33}.login-input .input-icon{color:#bfbfbf;font-size:16px}.login-button{height:48px;border-radius:8px;font-size:16px;font-weight:500;background:linear-gradient(135deg,#1890ff,#722ed1);border:none;margin-top:8px}.login-button:hover{background:linear-gradient(135deg,#40a9ff,#9254de);transform:translateY(-1px);box-shadow:0 4px 12px #1890ff4d}.login-button:active{transform:translateY(0)}.login-tips{text-align:center;margin-top:24px;padding:16px;background-color:#f6f8fa;border-radius:8px;border:1px solid #e8e8e8}.login-tips p{margin:0;color:#595959;font-size:14px}.login-footer{text-align:center;margin-top:40px}.login-footer p{color:#8c8c8c;font-size:12px;margin:0}@media (max-width: 768px){.login-container{flex-direction:column}.login-left{flex:none;height:200px;padding:20px}.brand-content{padding:20px}.brand-logo .logo-icon{font-size:40px}.brand-title{font-size:24px}.brand-subtitle{font-size:16px}.brand-description p{font-size:14px}.login-right{flex:1 1;padding:20px}}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
.page-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:24px}.page-header h2{margin:0;font-size:24px;font-weight:600;color:#333}.image-list .image-detail .detail-item{margin-bottom:16px}.image-list .image-detail .detail-item label{font-weight:600;color:#333;display:inline-block;width:100px}.image-list .image-detail .detail-item span{color:#666}.image-list .image-detail .detail-item p{margin:8px 0 0 100px;color:#666;line-height:1.6}.profile-page .profile-content .profile-header{display:flex;align-items:center;gap:16px;margin-bottom:16px}.profile-page .profile-content .profile-header .profile-info h3{margin:0 0 4px;font-size:20px;font-weight:600;color:#333}.profile-page .profile-content .profile-header .profile-info p{margin:0;color:#666;font-size:14px}.profile-page .profile-content .quick-actions{display:flex;gap:12px;flex-wrap:wrap}@media (max-width: 768px){.page-header{flex-direction:column;align-items:flex-start;gap:12px}.profile-content .profile-header{flex-direction:column;text-align:center}.profile-content .quick-actions{justify-content:center}}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
.user_content___NVrSf{display:flex;width:100%;height:100%;background-color:#f7f8fa}.user_content___NVrSf .left_content___k_v4w{width:400px;height:100%;padding:8px;background-color:#fff}.user_content___NVrSf .left_content___k_v4w .search___mN53j{width:100%;height:70px}.user_content___NVrSf .left_content___k_v4w .tree_box___HLlDc{width:100%;height:calc(100% - 70px);overflow:auto;padding-top:10px}.user_content___NVrSf .right_content___NTJte{width:calc(100% - 400px);height:100%;padding-left:10px}.user_content___NVrSf .right_content___NTJte .teble_content___yJ7lW{width:100%;height:100%;background-color:#fff;padding:8px}.user_content___NVrSf .right_content___NTJte .teble_content___yJ7lW .teble_box___YE1no{display:flex;flex-direction:column;width:100%;height:calc(100% - 50px);overflow:hidden}.user_content___NVrSf :where(.css-dev-only-do-not-override-1vjf2v5).ant-pagination .ant-pagination-total-text{position:absolute;left:5px}.user_content___NVrSf .images-list-table{flex:1 1;display:flex;flex-direction:column;overflow:hidden}.user_content___NVrSf .images-list-table .ant-table-wrapper{display:flex;flex-direction:column;flex:1 1;overflow:hidden}.user_content___NVrSf .images-list-table .ant-table-wrapper .ant-spin-nested-loading,.user_content___NVrSf .images-list-table .ant-table-wrapper .ant-spin-nested-loading .ant-spin-container{flex:1 1;display:flex;flex-direction:column;overflow:hidden}.user_content___NVrSf .images-list-table .ant-table-wrapper .ant-spin-nested-loading .ant-spin-container .ant-table,.user_content___NVrSf .images-list-table .ant-table-wrapper .ant-spin-nested-loading .ant-spin-container .ant-table .ant-table-container{display:flex;flex-direction:column;flex:1 1;overflow:hidden}.user_content___NVrSf .images-list-table .ant-table-wrapper .ant-spin-nested-loading .ant-spin-container .ant-table .ant-table-container .ant-table-header{flex-shrink:0}.user_content___NVrSf .images-list-table .ant-table-wrapper .ant-spin-nested-loading .ant-spin-container .ant-table .ant-table-container .ant-table-body{flex:1 1;overflow:auto!important}.user_content___NVrSf .images-list-table .ant-table-wrapper .ant-spin-nested-loading .ant-spin-container .ant-table .ant-table-pagination{flex-shrink:0;position:relative;z-index:1}.content_wrap___IzyVq{width:100%;height:100%}.content_wrap___IzyVq .search_wrap___vyvi6{display:flex;justify-content:flex-end;margin-bottom:5px;padding-bottom:5px}.content_wrap___IzyVq :where(.css-dev-only-do-not-override-1vjf2v5).ant-pagination .ant-pagination-total-text{position:absolute;left:5px}.model_content___MsF87{width:100%;height:650px}.content_wrap___t2j0e{width:100%;height:100%}.content_wrap___t2j0e .search_wrap___MKaSL{display:flex;justify-content:flex-end;margin-bottom:5px;padding-bottom:5px}.content_wrap___t2j0e :where(.css-dev-only-do-not-override-1vjf2v5).ant-pagination .ant-pagination-total-text{position:absolute;left:5px}.model_content___DaYce{width:100%;height:650px}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
.user_content___n6dbD{display:flex;width:100%;height:100%;background-color:#f7f8fa}.user_content___n6dbD .left_content___CkwkA{width:400px;height:100%;padding:8px;background-color:#fff}.user_content___n6dbD .left_content___CkwkA .search___HoQbf{width:100%;height:70px}.user_content___n6dbD .left_content___CkwkA .tree_box___x56Nb{width:100%;height:calc(100% - 70px);overflow:auto;padding-top:10px}.user_content___n6dbD .right_content___JtLdU{width:calc(100% - 400px);height:100%;padding-left:10px}.user_content___n6dbD .right_content___JtLdU .teble_content___kLIoM{width:100%;height:100%;background-color:#fff;padding:8px}.user_content___n6dbD .right_content___JtLdU .teble_content___kLIoM .teble_box___Gk7lM{display:flex;flex-direction:column;width:100%;height:calc(100% - 50px);overflow:hidden}.user_content___n6dbD :where(.css-dev-only-do-not-override-1vjf2v5).ant-pagination .ant-pagination-total-text{position:absolute;left:5px}.user_content___n6dbD .images-list-table{flex:1 1;display:flex;flex-direction:column;overflow:hidden}.user_content___n6dbD .images-list-table .ant-table-wrapper{display:flex;flex-direction:column;flex:1 1;overflow:hidden}.user_content___n6dbD .images-list-table .ant-table-wrapper .ant-spin-nested-loading,.user_content___n6dbD .images-list-table .ant-table-wrapper .ant-spin-nested-loading .ant-spin-container{flex:1 1;display:flex;flex-direction:column;overflow:hidden}.user_content___n6dbD .images-list-table .ant-table-wrapper .ant-spin-nested-loading .ant-spin-container .ant-table,.user_content___n6dbD .images-list-table .ant-table-wrapper .ant-spin-nested-loading .ant-spin-container .ant-table .ant-table-container{display:flex;flex-direction:column;flex:1 1;overflow:hidden}.user_content___n6dbD .images-list-table .ant-table-wrapper .ant-spin-nested-loading .ant-spin-container .ant-table .ant-table-container .ant-table-header{flex-shrink:0}.user_content___n6dbD .images-list-table .ant-table-wrapper .ant-spin-nested-loading .ant-spin-container .ant-table .ant-table-container .ant-table-body{flex:1 1;overflow:auto!important}.user_content___n6dbD .images-list-table .ant-table-wrapper .ant-spin-nested-loading .ant-spin-container .ant-table .ant-table-pagination{flex-shrink:0;position:relative;z-index:1}

1
web-fe/serve/dist/umi.css vendored 100644
View File

@ -0,0 +1 @@
html,body{width:100%;height:100%}input::-ms-clear,input::-ms-reveal{display:none}*,*:before,*:after{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;-ms-overflow-style:scrollbar;-webkit-tap-highlight-color:rgba(0,0,0,0)}body{margin:0}[tabindex="-1"]:focus{outline:none}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5em;font-weight:500}p{margin-top:0;margin-bottom:1em}abbr[title],abbr[data-original-title]{text-decoration:underline dotted;border-bottom:0;cursor:help}address{margin-bottom:1em;font-style:normal;line-height:inherit}input[type=text],input[type=password],input[type=number],textarea{-webkit-appearance:none}ol,ul,dl{margin-top:0;margin-bottom:1em}ol ol,ul ul,ol ul,ul ol{margin-bottom:0}dt{font-weight:500}dd{margin-bottom:.5em;margin-left:0}blockquote{margin:0 0 1em}dfn{font-style:italic}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}pre,code,kbd,samp{font-size:1em;font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,Courier,monospace}pre{margin-top:0;margin-bottom:1em;overflow:auto}figure{margin:0 0 1em}img{vertical-align:middle;border-style:none}a,area,button,[role=button],input:not([type=range]),label,select,summary,textarea{touch-action:manipulation}table{border-collapse:collapse}caption{padding-top:.75em;padding-bottom:.3em;text-align:left;caption-side:bottom}input,button,select,optgroup,textarea{margin:0;color:inherit;font-size:inherit;font-family:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}button,html [type=button],[type=reset],[type=submit]{-webkit-appearance:button}button::-moz-focus-inner,[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner{padding:0;border-style:none}input[type=radio],input[type=checkbox]{box-sizing:border-box;padding:0}input[type=date],input[type=time],input[type=datetime-local],input[type=month]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;margin:0;padding:0;border:0}legend{display:block;width:100%;max-width:100%;margin-bottom:.5em;padding:0;color:inherit;font-size:1.5em;line-height:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item}template{display:none}[hidden]{display:none!important}mark{padding:.2em;background-color:#feffe6}

84
web-fe/serve/dist/umi.js vendored 100644

File diff suppressed because one or more lines are too long

View File

@ -80,60 +80,60 @@ const MainLayout: React.FC = () => {
return ( return (
<ConfigProvider locale={zhCN}> <ConfigProvider locale={zhCN}>
<Layout className="main-layout"> <Layout className="main-layout">
<Sider <Sider
trigger={null} trigger={null}
collapsible collapsible
collapsed={collapsed} collapsed={collapsed}
className="main-sider" className="main-sider"
>
<div className="logo">{!collapsed && <span>VDI</span>}</div>
<Menu
theme="dark"
mode="inline"
selectedKeys={[selectedKey]}
onClick={handleMenuClick}
> >
<Menu.Item key="userList" icon={<AppstoreOutlined />}> <div className="logo">{!collapsed && <span>Nex</span>}</div>
<Menu
</Menu.Item> theme="dark"
<Menu.Item key="terminal" icon={<AppstoreOutlined />}> mode="inline"
selectedKeys={[selectedKey]}
</Menu.Item> onClick={handleMenuClick}
<Menu.Item key="images" icon={<AppstoreOutlined />}>
</Menu.Item>
<Menu.Item key="profile" icon={<UserOutlined />}>
</Menu.Item>
</Menu>
</Sider>
<Layout>
<Header className="main-header">
<Button
type="text"
icon={collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
onClick={() => setCollapsed(!collapsed)}
className="trigger"
/>
<div className="header-right">
<span className="welcome-text">{username}</span>
<Dropdown overlay={userMenu} placement="bottomRight">
<Avatar icon={<UserOutlined />} className="user-avatar" />
</Dropdown>
</div>
</Header>
<Content
className="main-content"
style={{ height: 'calc(100vh - 64px)', overflow: 'auto' }}
> >
<Outlet /> <Menu.Item key="terminal" icon={<AppstoreOutlined />}>
</Content>
</Menu.Item>
<Menu.Item key="userList" icon={<AppstoreOutlined />}>
</Menu.Item>
<Menu.Item key="images" icon={<AppstoreOutlined />}>
</Menu.Item>
<Menu.Item key="profile" icon={<UserOutlined />}>
</Menu.Item>
</Menu>
</Sider>
<Layout>
<Header className="main-header">
<Button
type="text"
icon={collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
onClick={() => setCollapsed(!collapsed)}
className="trigger"
/>
<div className="header-right">
<span className="welcome-text">{username}</span>
<Dropdown overlay={userMenu} placement="bottomRight">
<Avatar icon={<UserOutlined />} className="user-avatar" />
</Dropdown>
</div>
</Header>
<Content
className="main-content"
style={{ height: 'calc(100vh - 64px)', overflow: 'auto' }}
>
<Outlet />
</Content>
</Layout>
</Layout> </Layout>
</Layout>
</ConfigProvider> </ConfigProvider>
); );
}; };

View File

@ -76,37 +76,25 @@ const ImportModal: React.FC<IMAGES.ImportModalProps> = ({
// 上传镜像时间相关 // 上传镜像时间相关
const [elapsedTime, setElapsedTime] = useState<number>(0); // 上传时间 const [elapsedTime, setElapsedTime] = useState<number>(0); // 上传时间
const timerRef = useRef<NodeJS.Timeout | null>(null); const timerRef = useRef<NodeJS.Timeout | null>(null);
// 上传完成状态
// const [uploadCompleted, _setUploadCompleted] = useState(false); const uploadCompletedRef = useRef(false);
// const uploadCompletedRef = useRef(false); // 手动取消状态
const isManualCancel = useRef(false); // 是否手动取消上传
// const setUploadCompleted = (value: boolean) => {
// uploadCompletedRef.current = value;
// _setUploadCompleted(value);
// };
// 处理页面刷新/关闭 // 处理页面刷新/关闭
// useEffect(() => { useEffect(() => {
// const handleBeforeUnload = (e: BeforeUnloadEvent) => { const handleBeforeUnload = (e: BeforeUnloadEvent) => {
// if (isUploading && !uploadCompletedRef.current) { if (isUploading && !uploadCompletedRef.current) {
// e.preventDefault(); e.preventDefault();
// e.returnValue = '镜像正在上传中,确定要离开吗?'; e.returnValue = '镜像正在上传中,确定要离开吗?';
return e.returnValue;
}
};
// // 使用 sendBeacon 发送取消请求 window.addEventListener('beforeunload', handleBeforeUnload);
// const params = new URLSearchParams(); return () => window.removeEventListener('beforeunload', handleBeforeUnload);
// params.append('file_id', fileId.current); }, [isUploading]);
// const blob = new Blob([params.toString()], {
// type: 'application/x-www-form-urlencoded',
// });
// navigator.sendBeacon('/api/cancel-upload', blob);
// return e.returnValue;
// }
// };
// window.addEventListener('beforeunload', handleBeforeUnload);
// return () => window.removeEventListener('beforeunload', handleBeforeUnload);
// }, [isUploading]);
// 计时器清理(仅组件卸载时执行) // 计时器清理(仅组件卸载时执行)
useEffect(() => { useEffect(() => {
@ -117,30 +105,14 @@ const ImportModal: React.FC<IMAGES.ImportModalProps> = ({
}; };
}, []); }, []);
// // 上传取消逻辑(依赖 isUploading 和 uploadCompletedRef
// useEffect(() => {
// return () => {
// if (isUploading && !uploadCompletedRef.current && fileId.current) {
// const params = new URLSearchParams();
// params.append('file_id', fileId.current);
// cancelUploadImagesAPI(params).then((res) => {
// if (res.code === CODE) {
// message.success('上传已取消');
// } else {
// message.error('取消上传失败');
// }
// });
// }
// };
// }, [isUploading]); // 保留原有依赖
// 添加重置状态函数 // 添加重置状态函数
const resetState = () => { const resetState = () => {
// setUploadCompleted(false); // 重置上传完成状态
setUploadProgress(0); setUploadProgress(0);
setIsUploading(false); setIsUploading(false);
setUploadStatus(READY); setUploadStatus(READY);
setUploadMessage(''); setUploadMessage('');
uploadCompletedRef.current = false; // 重置上传完成状态
isManualCancel.current = false; // 重置手动取消状态
completedChunks.current = 0; completedChunks.current = 0;
totalChunks.current = 0; totalChunks.current = 0;
fileId.current = ''; fileId.current = '';
@ -162,13 +134,6 @@ const ImportModal: React.FC<IMAGES.ImportModalProps> = ({
} }
}; };
// 当弹窗关闭时重置状态
useEffect(() => {
if (!visible) {
resetState();
}
}, [visible]);
// 4. 上传单个分片 // 4. 上传单个分片
const uploadChunk = async ( const uploadChunk = async (
chunk: Blob, chunk: Blob,
@ -206,8 +171,9 @@ const ImportModal: React.FC<IMAGES.ImportModalProps> = ({
// 根据后端返回的状态进行判断 // 根据后端返回的状态进行判断
if (response.success) { if (response.success) {
if (response.status === 'completed') { if (response.status === 'completed') {
uploadCompletedRef.current = true; // 设置上传完成状态
isManualCancel.current = false; // 重置手动取消状态
// 文件上传完成设置进度为100% // 文件上传完成设置进度为100%
// setUploadCompleted(true); // 标记上传完成
setUploadProgress(100); // 这里已经正确设置了100% setUploadProgress(100); // 这里已经正确设置了100%
setIsUploading(false); setIsUploading(false);
setUploadStatus(SUCCESS); setUploadStatus(SUCCESS);
@ -297,8 +263,11 @@ const ImportModal: React.FC<IMAGES.ImportModalProps> = ({
hasError = true; // 设置错误标记 hasError = true; // 设置错误标记
setIsUploading(false); setIsUploading(false);
setUploadStatus(ERROR); setUploadStatus(ERROR);
setUploadMessage(result.message || '文件上传失败,请重试'); // 只有当不是用户取消时才显示错误消息
message.error(result.message ||'文件上传失败'); if (!isManualCancel.current) {
setUploadMessage(result.message || '文件上传失败');
message.error(result.message || '文件上传失败');
}
// 中止其他正在进行的上传 // 中止其他正在进行的上传
if (abortController.current) { if (abortController.current) {
@ -326,6 +295,8 @@ const ImportModal: React.FC<IMAGES.ImportModalProps> = ({
// 2. 开始上传 // 2. 开始上传
const startUpload = async (file: File) => { const startUpload = async (file: File) => {
try { try {
isManualCancel.current=false;
uploadCompletedRef.current = false;
setIsUploading(true); setIsUploading(true);
setUploadStatus(UPLOADING); setUploadStatus(UPLOADING);
setUploadMessage('正在准备上传...'); setUploadMessage('正在准备上传...');
@ -421,12 +392,17 @@ const ImportModal: React.FC<IMAGES.ImportModalProps> = ({
// 取消上传 // 取消上传
const cancelUpload = async () => { const cancelUpload = async () => {
// 先更新 ref 再更新 state // 1. 立即标记状态
// uploadCompletedRef.current = true; isManualCancel.current = true;
// _setUploadCompleted(true); uploadCompletedRef.current = true; // 标记完成(阻止 beforeunload
if (abortController.current) {
abortController.current.abort(); // 2. 清空上传队列(阻止新请求)
abortController.current = null; uploadQueue.current = [];
// 3. 中止所有进行中的请求
const oldController = abortController.current;
abortController.current = new AbortController(); // 新建控制器
if (oldController) {
oldController.abort(); // 中止旧请求
} }
// 如果有正在上传的文件调用后端取消上传API // 如果有正在上传的文件调用后端取消上传API
if (fileId.current) { if (fileId.current) {
@ -458,11 +434,10 @@ const ImportModal: React.FC<IMAGES.ImportModalProps> = ({
<Alert <Alert
message="重要提示" message="重要提示"
description={ description={
<div style={{ color: "rgb(237, 41, 31)" }}> <div style={{ color: 'rgb(237, 41, 31)' }}>
<div>1. </div> <div>1. </div>
<div> <div>2. </div>
2. <div>3. </div>
</div>
</div> </div>
} }
type="warning" type="warning"
@ -530,31 +505,25 @@ const ImportModal: React.FC<IMAGES.ImportModalProps> = ({
</div> </div>
); );
const handleCancel = () => {
if (isUploading) {
cancelUpload().finally(() => {
resetState(); // 确保取消完成后再重置
onCancel();
});
} else {
resetState();
onCancel();
}
};
return ( return (
<Modal <Modal
title="导入镜像" title="导入镜像"
open={visible} open={visible}
onCancel={() => { onCancel={handleCancel}
if (isUploading) {
cancelUpload();
} else {
// 如果不是上传状态,直接重置状态
resetState();
}
onCancel();
}}
footer={[ footer={[
<Button <Button key="close" onClick={handleCancel}>
key="close"
onClick={() => {
if (isUploading) {
cancelUpload();
} else {
resetState();
}
onCancel();
}}
>
</Button>, </Button>,
]} ]}

View File

@ -195,7 +195,7 @@ const ImageList: React.FC = () => {
key: 'bt_path', key: 'bt_path',
title: 'BT路径', title: 'BT路径',
dataIndex: 'bt_path', dataIndex: 'bt_path',
width: 250, width: 180,
defaultVisible: true, defaultVisible: true,
ellipsis: true, ellipsis: true,
render: (text: string) => text ? <Tooltip title={text} placement="topLeft">{text}</Tooltip>:'--' render: (text: string) => text ? <Tooltip title={text} placement="topLeft">{text}</Tooltip>:'--'
@ -224,7 +224,7 @@ const ImageList: React.FC = () => {
key: 'image_status', key: 'image_status',
title: '镜像状态', title: '镜像状态',
dataIndex: 'image_status', dataIndex: 'image_status',
width: 80, width: 90,
render: (text: number) => (text ? getStatusTag(text) : '--'), render: (text: number) => (text ? getStatusTag(text) : '--'),
defaultVisible: true, defaultVisible: true,
}, },
@ -232,7 +232,7 @@ const ImageList: React.FC = () => {
key: 'create_time', key: 'create_time',
title: '创建时间', title: '创建时间',
dataIndex: 'create_time', dataIndex: 'create_time',
width: 180, width: 160,
render: (text: string) => render: (text: string) =>
text ? ( text ? (
<Tooltip title={dayjs(text).format('YYYY-MM-DD HH:mm:ss')}> <Tooltip title={dayjs(text).format('YYYY-MM-DD HH:mm:ss')}>
@ -247,7 +247,7 @@ const ImageList: React.FC = () => {
{ {
key: 'action', key: 'action',
title: '操作', title: '操作',
width: 100, width: 90,
fixed: 'right' as 'right', fixed: 'right' as 'right',
render: (_: any, record: IMAGES.ImageItem) => ( render: (_: any, record: IMAGES.ImageItem) => (
<Space size="small"> <Space size="small">

View File

@ -42,7 +42,7 @@ const LoginPage: React.FC = () => {
<SafetyCertificateOutlined className="logo-icon" /> <SafetyCertificateOutlined className="logo-icon" />
<h1 className="brand-title"></h1> <h1 className="brand-title"></h1>
</div> </div>
<div className="brand-subtitle">VDI </div> <div className="brand-subtitle">Nex</div>
<div className="brand-description"> <div className="brand-description">
<p></p> <p></p>
<p></p> <p></p>
@ -69,7 +69,7 @@ const LoginPage: React.FC = () => {
<div className="login-form-container"> <div className="login-form-container">
<div className="login-header"> <div className="login-header">
<h2 className="login-title"></h2> <h2 className="login-title"></h2>
<p className="login-subtitle">使VDI</p> <p className="login-subtitle">使Nex</p>
</div> </div>
<Form <Form
@ -113,7 +113,7 @@ const LoginPage: React.FC = () => {
</Button> </Button>
</Form.Item> </Form.Item>
</Form> </Form>
<div className="login-tips"> <div className="login-tips">
<p>admin / 123456</p> <p>admin / 123456</p>
</div> </div>

View File

@ -7,9 +7,9 @@ import { deleteUserGroup, getGroupTree } from '@/services/userList';
import { import {
DeleteOutlined, DeleteOutlined,
DownOutlined, DownOutlined,
GoldOutlined,
PlusOutlined, PlusOutlined,
RedoOutlined, RedoOutlined,
GoldOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import { import {
Button, Button,
@ -387,19 +387,41 @@ const UserListPage: React.FC = () => {
}; };
const onDeleteGroup = async () => { const onDeleteGroup = async () => {
if (selectedOrg) { if (selectedOrg) {
try { const params: any = {
const params = { page_size: pageSize,
id: selectedOrg, page_num: currentPage,
}; };
const res = await deleteUserGroup(params); if (selectedOrg) {
const { code } = res || {}; params.device_group_id = selectedOrg;
if (code === ERROR_CODE) {
message.success('分组删除成功');
getGroupList();
}
} catch (error) {
message.error('分组删除失败');
} }
try {
const result: any = await getTerminalList(params);
const { data } = result || {};
const { total = 0 } = data || {};
if (total > 0) {
message.info("该分组下有终端,请先删除该分组下的所有终端");
} else {
onDeleteGroupSave();
}
} catch (err) {
console.log(err);
}
}
};
const onDeleteGroupSave = async () => {
try {
const params = {
id: selectedOrg,
};
const res = await deleteUserGroup(params);
const { code } = res || {};
if (code === ERROR_CODE) {
message.success('分组删除成功');
getGroupList();
}
} catch (error) {
message.error('分组删除失败');
} }
}; };
@ -486,7 +508,7 @@ const UserListPage: React.FC = () => {
showIcon={true} showIcon={true}
selectedKeys={selectedOrg ? [selectedOrg] : []} selectedKeys={selectedOrg ? [selectedOrg] : []}
// switcherIcon={<TeamOutlined style={{ fontSize: '15px' }}/>} // switcherIcon={<TeamOutlined style={{ fontSize: '15px' }}/>}
icon={<GoldOutlined style={{ fontSize: '15px' }} />} icon={<GoldOutlined style={{ fontSize: '15px' }} />}
/> />
</Spin> </Spin>
</div> </div>

View File

@ -61,30 +61,27 @@ const BindUserModal: React.FC<UserEditModalProps> = ({
const handleOk = async () => { const handleOk = async () => {
try { try {
const values = await form.validateFields(); const values = await form.validateFields();
const { image_list } = values || {}; const { image_list = [] } = values || {};
console.log('image_list=====', image_list); console.log('image_list=====', image_list);
if (image_list && image_list.length > 0) { const list: any[] = [];
const list: any[] = []; image_list.forEach((item: any) => {
image_list.forEach((item: any) => { const obj: any = {
const obj: any = { device_id: device_id,
device_id: device_id, image_id: item.id,
image_id: item.id,
};
const newData = dataSource.filter(
(record) => record.image_id === item.id,
);
if (newData && newData.length === 1) {
obj.id = newData[0].id;
}
list.push({ ...obj });
});
const payload: any = {
data: list,
}; };
onBind(payload); const newData = dataSource.filter(
} else { (record) => record.image_id === item.id,
message.info('请先选择绑定的镜像'); );
} if (newData && newData.length === 1) {
obj.id = newData[0].id;
}
list.push({ ...obj });
});
const payload: any = {
data: list,
device_id:device_id,
};
onBind(payload);
} catch (error) { } catch (error) {
message.error('请检查表单字段'); message.error('请检查表单字段');
} }
@ -131,7 +128,7 @@ const BindUserModal: React.FC<UserEditModalProps> = ({
<Form.Item <Form.Item
name="image_list" name="image_list"
label="选择镜像" label="选择镜像"
rules={[{ required: true, message: '请输入终端型号' }]} rules={[{ required: false, message: '请输入终端型号' }]}
> >
<SelectedTable /> <SelectedTable />
</Form.Item> </Form.Item>

View File

@ -112,46 +112,45 @@ const BindUserModal: React.FC<UserEditModalProps> = ({
const values = await form.validateFields(); const values = await form.validateFields();
const { user_list = [] } = values || {}; const { user_list = [] } = values || {};
const list: any[] = []; const list: any[] = [];
if (user_list && user_list.length > 0) { user_list.forEach((item: any) => {
user_list.forEach((item: any) => { const { type, id } = item || {};
const { type, id } = item || {}; if (type === 1) {
if (type === 1) { // 用户
// 用户 const obj: any = {
const obj: any = { device_id,
device_id, device_group_id,
device_group_id, type: type,
type: type, user_id: id,
user_id: id, };
}; const newData = userDataSource.filter(
const newData = userDataSource.filter( (record) => record.user_id === item.id && record.type === 1,
(record) => record.user_id === item.id && record.type === 1, );
); if (newData && newData.length === 1) {
if (newData && newData.length === 1) { obj.id = newData[0].id;
obj.id = newData[0].id;
}
list.push(obj);
} else {
//用户分组
const obj: any = {
device_id,
device_group_id,
type: type,
user_group_id: id,
};
const newData = userDataSource.filter(
(record) => record.user_group_id === item.id && record.type === 2,
);
if (newData && newData.length === 1) {
obj.id = newData[0].id;
}
list.push(obj);
} }
}); list.push(obj);
const payload = { } else {
data: list, //用户分组
}; const obj: any = {
onBind(payload); device_id,
} device_group_id,
type: type,
user_group_id: id,
};
const newData = userDataSource.filter(
(record) => record.user_group_id === item.id && record.type === 2,
);
if (newData && newData.length === 1) {
obj.id = newData[0].id;
}
list.push(obj);
}
});
const payload = {
data: list,
device_id:device_id,
};
onBind(payload);
} catch (error) { } catch (error) {
message.error('请检查表单字段'); message.error('请检查表单字段');
} }
@ -198,7 +197,7 @@ const BindUserModal: React.FC<UserEditModalProps> = ({
<Form.Item <Form.Item
name="user_list" name="user_list"
label="选择用户" label="选择用户"
rules={[{ required: true, message: '请选择绑定用户' }]} rules={[{ required: false, message: '请选择绑定用户' }]}
> >
<SelectedTable orgTreeData={orgTreeData} /> <SelectedTable orgTreeData={orgTreeData} />
</Form.Item> </Form.Item>

View File

@ -394,20 +394,54 @@ const UserListPage: React.FC = () => {
const onDeleteGroup = async () => { const onDeleteGroup = async () => {
if (selectedOrg) { if (selectedOrg) {
try { // try {
const params = { // const params = {
id: selectedOrg, // id: selectedOrg,
}; // };
const res = await deleteUserGroup(params); // const res = await deleteUserGroup(params);
const { code } = res || {}; // const { code } = res || {};
if (code === ERROR_CODE) { // if (code === ERROR_CODE) {
message.success('分组删除成功'); // message.success('分组删除成功');
setSelectedOrg(null); // setSelectedOrg(null);
getUserGroupList(); // getUserGroupList();
} // }
} catch (error) { // } catch (error) {
message.error('分组删除失败'); // message.error('分组删除失败');
// }
const params: any = {
page_size: pageSize,
page_num: currentPage,
};
if (selectedOrg) {
params.user_group_id = selectedOrg;
} }
try {
const result = await getUserList(params);
const { data = {} } = result || {};
const { total = 0 } = data || {};
if (total > 0) {
message.info('当前分组下有用户,请先删除用户', 5);
} else {
ondeleteCroupSave();
}
} catch (err) {}
}
};
const ondeleteCroupSave = async () => {
try {
const params = {
id: selectedOrg,
};
const res = await deleteUserGroup(params);
const { code } = res || {};
if (code === ERROR_CODE) {
message.success('分组删除成功');
setSelectedOrg(null);
getUserGroupList();
}
} catch (error) {
message.error('分组删除失败');
} }
}; };

View File

@ -6,7 +6,7 @@ const BASE_URL = '/api/nex/v1';
// 查询镜像列表 // 查询镜像列表
export async function getImagesList(params:any) { export async function getImagesList(params:any) {
// return request<IMAGES.Images_ListInfo>(`${BASE_URL}/queryimagesList`, { // return request<IMAGES.Images_ListInfo>(`${BASE_URL}/queryimagesList`, {
return request<IMAGES.Images_ListInfo>(`${BASE_URL}/image/select/page`, { return request<IMAGES.Images_ListInfo>(`${BASE_URL}/image/select/page`, {
method: 'POST', method: 'POST',
data: params, data: params,
}); });