Lin Qilong преди 3 месеца
родител
ревизия
c974e44901

+ 74 - 0
ruoyi-api-patform/src/main/java/com/ruoyi/interfaces/controller/BizDataShowConfigController.java

@@ -0,0 +1,74 @@
+package com.ruoyi.interfaces.controller;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.ruoyi.common.core.controller.BaseController;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.core.page.TableDataInfo;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.interfaces.domain.BizDataShowConfig;
+import com.ruoyi.interfaces.service.BizDataShowConfigService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiParam;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * @author lql
+ * @date 2025-9-17
+ */
+@Api(value = "业务数据展示配置", tags = "业务数据展示配置")
+@RestController
+@RequestMapping("/biz/data/show/config")
+public class BizDataShowConfigController extends BaseController {
+
+    @Autowired
+    private BizDataShowConfigService bizDataShowConfigService;
+
+
+    @ApiOperation(value = "新增/修改业务数据展示配置")
+    @PostMapping("")
+    public AjaxResult save(@ApiParam(name = "ptServiceAlarm", value = "BizDataShowConfig", required = true) @RequestBody BizDataShowConfig ptServiceAlarm) {
+        return AjaxResult.success(bizDataShowConfigService.saveOrUpdate(ptServiceAlarm));
+    }
+
+    @ApiOperation(value = "根据ID删除业务数据展示配置")
+    @DeleteMapping(value = "/{id}")
+    public AjaxResult delete(@ApiParam(name = "id", value = "id", required = true) @PathVariable String id) {
+        return AjaxResult.success(bizDataShowConfigService.removeById(id));
+    }
+
+    @ApiOperation(value = "根据ID获取业务数据展示配置(单表)")
+    @GetMapping("/{id}")
+    public AjaxResult get(@ApiParam(name = "id", value = "id", required = true) @PathVariable String id) {
+        return AjaxResult.success(bizDataShowConfigService.getById(id));
+    }
+
+    /**
+     * 查询分页列表
+     */
+    @GetMapping("/listOfPage")
+    public TableDataInfo listOfPage(BizDataShowConfig ptServiceAlarm) {
+        startPage();
+        QueryWrapper<BizDataShowConfig> queryWrapper = new QueryWrapper<>();
+        queryWrapper
+                .like(StringUtils.isNotBlank(ptServiceAlarm.getName()), "name", ptServiceAlarm.getName());
+        List<BizDataShowConfig> list = bizDataShowConfigService.list(queryWrapper);
+        return getDataTable(list);
+    }
+
+    /**
+     * 查询列表
+     */
+    @GetMapping("/list")
+    public AjaxResult list(BizDataShowConfig ptServiceAlarm) {
+        QueryWrapper<BizDataShowConfig> queryWrapper = new QueryWrapper<>();
+        queryWrapper
+                .like(StringUtils.isNotBlank(ptServiceAlarm.getName()), "name", ptServiceAlarm.getName());
+        List<BizDataShowConfig> list = bizDataShowConfigService.list(queryWrapper);
+        return AjaxResult.success(list);
+    }
+
+}

+ 47 - 0
ruoyi-api-patform/src/main/java/com/ruoyi/interfaces/domain/BizDataShowConfig.java

@@ -0,0 +1,47 @@
+package com.ruoyi.interfaces.domain;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.ruoyi.common.utils.JsonUtils;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.json.JSONObject;
+
+import java.util.Date;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class BizDataShowConfig {
+
+    private Long id;
+
+    private String name;
+
+    private String type;
+
+    private String queryOptions;
+
+    private String renderingOptions;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date createTime;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date updateTime;
+
+    public JSONObject getQueryOptionsData() {
+        if (queryOptions != null) {
+            return JsonUtils.jsonToPojo(queryOptions, JSONObject.class);
+        }
+        return null;
+    }
+
+    public JSONObject getRenderingOptionsData() {
+        if (renderingOptions != null) {
+            return JsonUtils.jsonToPojo(renderingOptions, JSONObject.class);
+        }
+        return null;
+    }
+
+}

+ 16 - 0
ruoyi-api-patform/src/main/java/com/ruoyi/interfaces/mapper/BizDataShowConfigMapper.java

@@ -0,0 +1,16 @@
+package com.ruoyi.interfaces.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.ruoyi.interfaces.domain.BizDataShowConfig;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 模型文件信息Mapper接口
+ *
+ * @author ruoyi
+ * @date 2025-07-15
+ */
+@Mapper
+public interface BizDataShowConfigMapper extends BaseMapper<BizDataShowConfig> {
+
+}

+ 8 - 0
ruoyi-api-patform/src/main/java/com/ruoyi/interfaces/service/BizDataShowConfigService.java

@@ -0,0 +1,8 @@
+package com.ruoyi.interfaces.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.ruoyi.interfaces.domain.BizDataShowConfig;
+
+public interface BizDataShowConfigService extends IService<BizDataShowConfig> {
+
+}

+ 26 - 0
ruoyi-api-patform/src/main/java/com/ruoyi/interfaces/service/impl/BizDataShowConfigServiceImpl.java

@@ -0,0 +1,26 @@
+package com.ruoyi.interfaces.service.impl;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.ruoyi.interfaces.domain.BizDataShowConfig;
+import com.ruoyi.interfaces.mapper.BizDataShowConfigMapper;
+import com.ruoyi.interfaces.service.BizDataShowConfigService;
+import org.springframework.stereotype.Service;
+
+import java.util.Date;
+
+@Service
+public class BizDataShowConfigServiceImpl extends ServiceImpl<BizDataShowConfigMapper, BizDataShowConfig> implements BizDataShowConfigService {
+
+    @Override
+    public boolean save(BizDataShowConfig entity) {
+        entity.setCreateTime(new Date());
+        return super.save(entity);
+    }
+
+    @Override
+    public boolean updateById(BizDataShowConfig entity) {
+        entity.setUpdateTime(new Date());
+        return super.updateById(entity);
+    }
+
+}

+ 1 - 0
ruoyi-ui/src/assets/icons/svg/alarm.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1760336207394" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8002" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M512 192A320 320 0 0 1 832 512v256c0 35.362133-28.672 64-64 64h-512A64 64 0 0 1 192 768V512A320 320 0 0 1 512 192z m0 512a192 192 0 1 0 0-384 192 192 0 0 0 0 384z m0-64a128 128 0 1 1 0-256 128 128 0 0 1 0 256zM479.982933 0h64.034134v128h-64.034134V0zM896 448H1024V512h-128v-64z m-896 0h128V512H0v-64z m120.32-306.756267l45.2608-45.226666 90.487467 90.487466L210.8416 231.765333 120.32 141.243733z m738.2016-45.226666l45.226667 45.226666-90.487467 90.5216L768 186.504533l90.5216-90.5216zM128 896h768a64 64 0 0 1 0 128H128a64 64 0 0 1 0-128z" fill="" p-id="8003"></path></svg>

Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
ruoyi-ui/src/assets/icons/svg/model-monitoring.svg


+ 1 - 0
ruoyi-ui/src/assets/icons/svg/monitoring-center.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1760335505830" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1774" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M819.2 716.8H204.8a68.267 68.267 0 0 1-68.267-68.267V204.8a68.267 68.267 0 0 1 68.267-68.267h614.4a68.267 68.267 0 0 1 68.267 68.267v443.733A68.267 68.267 0 0 1 819.2 716.8z m-34.133-307.2h-102.4L614.4 477.867l-136.533-204.8L341.333 409.6h-102.4v68.267h136.534l102.4-102.4 136.533 204.8 102.4-102.4h68.267V409.6zM358.4 785.067h307.2a51.2 51.2 0 0 1 0 102.4H358.4a51.2 51.2 0 1 1 0-102.4z" fill="" p-id="1775"></path></svg>

+ 4 - 1
ruoyi-ui/src/components/chart/GwEchart.vue

@@ -17,7 +17,9 @@ onMounted(() => {
 })
 
 onUnmounted(() => {
-  gwChartRef.value.removeEventListener("resize", reloadChart);
+  if (gwChartRef.value) {
+    gwChartRef.value.removeEventListener("resize", reloadChart);
+  }
 })
 
 function loadChart(option) {
@@ -78,6 +80,7 @@ function carousel(timeout = 5000, yAxisChange = false) {
     });
   }
 }
+
 </script>
 <style lang="scss" scoped>
 .gw-chart-wrapper {

+ 404 - 0
ruoyi-ui/src/views/container/containerDetail.vue

@@ -0,0 +1,404 @@
+<template>
+  <div class="app-container">
+    <el-row justify="space-between">
+      <el-col :span="12">
+        <el-form :model="queryParams" ref="queryRef" :inline="true" label-width="68px">
+          <el-form-item label="容器名称">
+            <el-input v-model="queryParams.containerName" placeholder="请输入容器名称" clearable style="width: 240px"
+                      @keyup.enter="getModelListTable"/>
+          </el-form-item>
+          <el-form-item>
+            <el-button type="primary" icon="Search" @click="getModelListTable">查询</el-button>
+          </el-form-item>
+        </el-form>
+      </el-col>
+      <el-button type="primary" @click="reg" icon="Plus">创建容器</el-button>
+    </el-row>
+    <el-table
+        :data="tableData"
+        height="69vh"
+        :cell-style="{ padding: '5px' }"
+        :header-cell-style="{ height: heightAll * 0.01 + 'px' }"
+        :row-style="{ fontSize: '1rem', textAlign:'center' }"
+        border>
+      <el-table-column type="index" label="序号" width="80">
+        <template #default="{ $index }">
+          <div style="text-align: center;">{{ $index + 1 }}</div>
+        </template>
+      </el-table-column>
+      <el-table-column prop="containerName" label="模型名称"/>
+      <el-table-column prop="name" label="容器名称"/>
+      <el-table-column prop="imageName" label="容器镜像" width="220"/>
+      <el-table-column prop="publishedPorts" label="开放端口" width="180">
+        <template v-slot:header>
+          开放端口
+          <el-popover title="开放端口" content="开放端口:内置端口" placement="top-start">
+            <template #reference>
+              <QuestionFilled style="width: 1em; height: 1em;"></QuestionFilled>
+            </template>
+          </el-popover>
+        </template>
+      </el-table-column>
+      <el-table-column prop="status" align="center" label="模型状态" width="120">
+        <template #default="scope">
+          <el-tag v-if="scope.row.status==='RUNNING'">
+            {{ getContainerStatus(scope.row.status) }}
+          </el-tag>
+          <el-tag v-if="scope.row.status==='STOPPED'" type="danger">
+            {{ getContainerStatus(scope.row.status) }}
+          </el-tag>
+          <el-tag v-if="scope.row.status==='EXITED'" type="warning">
+            {{ getContainerStatus(scope.row.status) }}
+          </el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column align="center" label="操作" width="300">
+        <template #default="scope">
+          <div style="display: flex;justify-content: space-between;">
+            <el-button type="primary" @click="handleEdit(scope.row)" size="small" text>编辑</el-button>
+            <el-button v-if="scope.row.status!=='RUNNING'" @click="startContainers(scope.row.id)" type="primary"
+                       text
+                       size="small">运行
+            </el-button>
+            <el-button v-if="scope.row.status==='RUNNING'" @click="stopContainers(scope.row.id)" type="danger" text
+                       size="small">停止
+            </el-button>
+            <el-button v-if="scope.row.status==='RUNNING'" @click="restartContainers(scope.row.id)" type="warning"
+                       text
+                       size="small">重启
+            </el-button>
+            <el-button type="danger" @click="handleDelete(scope.row)" text size="small">删除</el-button>
+          </div>
+        </template>
+      </el-table-column>
+    </el-table>
+    <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum"
+                v-model:limit="queryParams.pageSize"/>
+
+    <el-dialog v-model="dialogVisible" :title="dialogTitle" @close="clearForm" destroy-on-close>
+      <el-form :model="form" label-position="right" ref="formRef" label-width="120px" :rules="rulesJi">
+        <el-form-item label="容器名称:" prop="containerName">
+          <el-input v-model="form.containerName"/>
+        </el-form-item>
+        <el-form-item label="容器英文名称:" prop="name">
+          <el-input v-model="form.name" :disabled="form.id"/>
+        </el-form-item>
+        <el-form-item label="镜像:" prop="imageName">
+          <el-select v-model="form.imageName" :disabled="form.id">
+            <el-option v-for="item in imageList" :key="item.id" :label="item.imageName + ':' + item.tag"
+                       :value="item.imageName + ':' + item.tag"></el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item>
+          <template v-slot:label>
+            <div style="display: flex;align-items: center;">
+              开放端口
+              <el-popover title="开放端口" content="开放端口:内置端口" placement="top-start">
+                <template #reference>
+                  <QuestionFilled style="width: 1em; height: 1em; margin: 0 5px;"></QuestionFilled>
+                </template>
+              </el-popover>
+              :
+            </div>
+          </template>
+          <el-input v-model="form.publishedPorts" :disabled="form.id"/>
+        </el-form-item>
+        <el-form-item label="容器说明">
+          <el-input v-model="form.remark" type="textarea" placeholder="请输入容器说明"/>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="dialogVisible = false">取消</el-button>
+        <el-button type="primary" @click="submit">提交</el-button>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+<script setup>
+import {nextTick, onMounted, reactive, ref} from 'vue';
+import {getModelContainerById, getModelContainerPageList, saveModelContainer} from "@/api/container/modelContainer.js";
+import {deleteContainer, restartContainer, startContainer, stopContainer} from "@/api/container/containerOperation.js";
+import Pagination from "@/components/Pagination/index.vue";
+import {getModelContainerImageList} from "@/api/container/modelContainerImage.js";
+
+const {proxy} = getCurrentInstance();
+
+const queryParams = ref({
+  pageNum: 1,
+  pageSize: 20,
+  containerName: '',
+})
+let tableData = ref([])
+const total = ref(0)
+
+function getContainerStatus(status) {
+  switch (status) {
+    case 'RUNNING':
+      return '运行中'
+    case 'STOPPED':
+      return '已停止'
+    case 'EXITED':
+      return '审核未通过'
+    default:
+      return ''
+  }
+}
+
+function startContainers(id) {
+  startContainer(id).then(() => {
+    proxy.$message.success('启动成功')
+    getModelListTable()
+  })
+}
+
+function stopContainers(id) {
+  stopContainer(id).then(() => {
+    proxy.$message.success('关闭成功')
+    getModelListTable()
+  })
+}
+
+function restartContainers(id) {
+  restartContainer(id).then(() => {
+    proxy.$message.success('重启成功')
+    getModelListTable()
+  })
+}
+
+async function handleEdit(row) {
+  dialogVisible.value = true
+  dialogTitle.value = `编辑【${row.containerName}】容器`
+  await nextTick()
+  getModelContainerById(row.id).then(res => {
+    form.value = res.data
+  })
+}
+
+function handleDelete(row) {
+  proxy.$modal.confirm('是否确认删除?').then(function () {
+    return deleteContainer(row.id);
+  }).then(() => {
+    getModelListTable();
+    proxy.$modal.msgSuccess("删除成功");
+  }).catch(() => {
+  });
+}
+
+const dialogVisible = ref(false)
+const dialogTitle = ref('新建容器')
+const imageList = ref([])
+
+const formRef = ref();
+const form = ref({
+  name: '',
+  imageName: '',
+  publishedPorts: '',
+});
+const rulesJi = reactive({
+  name: [
+    {required: true, message: '请输入容器名称', trigger: 'blur'},
+    {
+      pattern: /^[a-z][a-z0-9_-]{3,49}$/,
+      message: '名称格式不正确,以字母开头,允许使用小写字母、数字、下划线和连字符。',
+      trigger: 'blur'
+    },
+  ],
+  imageName: [{required: true, message: '必填', trigger: 'blur'}],
+});
+
+const heightAll = window.innerHeight
+
+async function submit() {
+  formRef.value.validate((valid) => {
+    if (valid) {
+      if (!form.value.id) {
+        let [imageName, tag] = form.value.imageName.split(':');
+        let image = imageList.value.find(item => item.imageName === imageName && item.tag === tag);
+        if (image) {
+          form.value.imageId = image.imageId
+        }
+      }
+      saveModelContainer(form.value).then(() => {
+        proxy.$modal.msgSuccess("保存成功");
+        dialogVisible.value = false
+        getModelListTable()
+      })
+    }
+  })
+}
+
+function clearForm() {
+  form.value = {
+    name: '',
+    imageName: '',
+    publishedPorts: ''
+  }
+}
+
+function reg() {
+  dialogVisible.value = true
+}
+
+function getModelListTable() {
+  getModelContainerPageList(queryParams.value).then(res => {
+    tableData.value = res.rows
+    total.value = res.total
+  })
+}
+
+function getImageList() {
+  getModelContainerImageList().then(res => {
+    imageList.value = res.data
+  })
+}
+
+getImageList()
+onMounted(() => {
+  getModelListTable()
+});
+</script>
+<style scoped>
+.pagination-container {
+  margin-top: 15px;
+}
+
+.type-container {
+  padding-right: 10px;
+
+  .type-item {
+    padding: 10px;
+    border-radius: 10px;
+    text-align: center;
+    background-color: #fff;
+    color: #409EFF;
+    cursor: pointer;
+
+    &.active {
+      background-color: #409EFF;
+      color: #fff;
+    }
+
+  }
+}
+
+:deep(.treeLeft) .el-tree-node__content {
+  display: flex !important;
+  height: 28px; /* 按设计稿调整高度 */
+  align-items: center;
+  padding-top: 0 !important;
+}
+
+:deep(.treeLeft) .el-tree-node__content:hover {
+  background-color: #e9e9eb;
+}
+
+:deep(.treeLeft) .el-tree-node__content:active {
+  background-color: rgka(69, 157, 255, 0.1) !important;
+}
+
+/* 选中态(Active) */
+:deep(.treeLeft) .el-tree-node.is-current > .el-tree-node__content {
+  background-color: #c6e2ff !important;
+}
+
+.tabs-wrapper {
+  position: relative; /* 确保内容层在伪元素上方 */
+  z-index: 1; /* 关键:高于背景图 */
+}
+
+.tabs-wrapper :deep(.el-tabs),
+.tabs-wrapper :deep(.el-tabs__content) {
+  background-color: red !important;
+}
+
+:deep(.el-tabs) {
+  background-color: transparent !important;
+}
+
+:deep(.el-tabs__content) {
+  background-color: transparent !important;
+}
+
+:deep(.custom-dialog-bg) {
+  z-index: 1000;
+  background-image: url('@/assets/images/backDia.jpg') !important;
+  background-position-x: left;
+  background-position-y: bottom;
+  background-size: initial;
+  background-repeat: repeat-x;
+  background-attachment: initial;
+  background-origin: initial;
+  background-clip: initial;
+  background-color: rgb(255, 255, 255);
+}
+
+:deep(.custom-dialog-bg .el-dialog__header) {
+
+}
+
+:deep(.custom-dialog-bg .el-dialog__body) {
+
+  color: #ecf0f1 !important;; /* 内容文字颜色 */
+}
+
+/* 横向排列单选框标签和输入框 */
+.custom-input-wrapper {
+  display: flex;
+  align-items: center;
+  gap: 10px; /* 调整间距 */
+}
+
+/* 输入框仅显示底部横线 */
+.underline-input :deep(.el-input__wrapper) {
+  padding: 0;
+  box-shadow: none !important;
+  border-bottom: 1px solid #dcdfe6; /* 横线颜色 */
+  border-radius: 0;
+  background: transparent;
+}
+
+.underline-input :deep(.el-input__inner) {
+  height: 24px;
+  padding: 0 5px;
+}
+
+:deep(.el-table__body tr:hover > td) {
+  background-color: #eaf7ff !important;
+}
+
+.drag-handle {
+  cursor: move;
+}
+
+.ghost {
+  opacity: 0.5;
+  background: #c8ebfb;
+}
+
+/* 防止文字选中 */
+:deep(.el-table__row) {
+  user-select: none;
+  -webkit-user-select: none;
+}
+</style>
+<style scoped lang="scss">
+
+
+.el-table .el-table__row td {
+  height: 60px !important; /* 行高 */
+}
+
+.custom-tree-node {
+  display: flex; /* 启用 Flex 布局 */
+  align-items: center; /* 垂直居中 */
+  gap: 8px; /* 图标与文字间距 */
+}
+
+:deep(.svg-icon) {
+  outline: none;
+}
+
+:deep(.svg-icon svg) {
+  stroke: none;
+}
+</style>

+ 149 - 0
ruoyi-ui/src/views/container/containerImage.vue

@@ -0,0 +1,149 @@
+<template>
+  <div class="app-container">
+    <el-row justify="space-between">
+      <el-col :span="12">
+        <el-form :model="queryParams" ref="queryRef" :inline="true" label-width="68px">
+          <el-form-item label="镜像名称">
+            <el-input v-model="queryParams.imageName" placeholder="请输入镜像名称" clearable style="width: 240px"
+                      @keyup.enter="getModelListTable"/>
+          </el-form-item>
+          <el-form-item>
+            <el-button type="primary" icon="Search" @click="getModelListTable">查询</el-button>
+          </el-form-item>
+        </el-form>
+      </el-col>
+      <el-button type="primary" @click="reg" icon="Plus">创建镜像</el-button>
+    </el-row>
+    <el-table
+        :data="tableData"
+        height="69vh"
+        :cell-style="{ padding: '5px' }"
+        :header-cell-style="{ height: heightAll * 0.01 + 'px' }"
+        :row-style="{ fontSize: '1rem', textAlign:'center' }"
+        border>
+      <el-table-column type="index" label="序号" width="80">
+        <template #default="{ $index }">
+          <div style="text-align: center;">{{ $index + 1 }}</div>
+        </template>
+      </el-table-column>
+      <el-table-column prop="imageName" label="镜像名称"/>
+      <el-table-column prop="tag" label="版本" width="160"/>
+      <el-table-column show-overflow-tooltip prop="imageId" label="镜像ID" width="160">
+        <template #default="scope">
+          {{ scope.row.imageId }}
+        </template>
+      </el-table-column>
+      <el-table-column prop="createTime" align="center" label="创建时间" width="220">
+        <template #default="scope">
+          {{ scope.row.createTime }}
+        </template>
+      </el-table-column>
+      <el-table-column prop="size" align="center" label="镜像大小" width="120">
+        <template #default="scope">
+          {{ formatDiskSize(scope.row.size) }}
+        </template>
+      </el-table-column>
+      <el-table-column align="center" label="操作" width="200">
+        <template #default="scope">
+          <div style="display: flex;justify-content: space-between;">
+            <el-button type="danger" @click="handleDelete(scope.row)" text size="small">删除</el-button>
+          </div>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <el-dialog v-model="dialogVisible" :title="dialogTitle" @close="clearForm" destroy-on-close>
+      <el-form :model="form" label-position="right" ref="formRef" label-width="120px" :rules="rules">
+        <el-form-item label="镜像名称:" prop="imageName">
+          <el-input v-model="form.imageName"/>
+        </el-form-item>
+        <el-form-item label="镜像版本:" prop="tag">
+          <el-input v-model="form.tag"/>
+        </el-form-item>
+        <el-form-item label="镜像文件:">
+          <file-upload v-model="form.modelFilePath" :limit="1" :file-type="['zip']" :file-size="20"></file-upload>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="dialogVisible = false">取消</el-button>
+        <el-button type="primary" @click="submit">提交</el-button>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+<script setup>
+import {onMounted, reactive, ref} from 'vue';
+import {
+  deleteModelContainerImage,
+  getModelContainerImageList,
+  saveModelContainerImage
+} from "@/api/container/modelContainerImage.js";
+import {formatDiskSize} from "@/utils/index.js";
+import FileUpload from "@/components/FileUpload/index.vue";
+
+const {proxy} = getCurrentInstance();
+
+const queryParams = ref({
+  imageName: '',
+})
+let tableData = ref([])
+
+function handleDelete(row) {
+  proxy.$modal.confirm('是否确认删除?').then(function () {
+    return deleteModelContainerImage(row.imageId);
+  }).then(() => {
+    getModelListTable();
+    proxy.$modal.msgSuccess("删除成功");
+  }).catch(() => {
+  });
+}
+
+const dialogVisible = ref(false)
+const dialogTitle = ref('新建镜像')
+
+const formRef = ref();
+const form = ref({
+  imageName: '',
+  tag: '',
+  modelFilePath: '',
+});
+const rules = reactive({
+  imageName: [{required: true, message: '必填', trigger: 'blur'}],
+});
+
+const heightAll = window.innerHeight
+
+async function submit() {
+  formRef.value.validate((valid) => {
+    if (valid) {
+      saveModelContainerImage(form.value).then(() => {
+        proxy.$modal.msgSuccess("开始创建镜像");
+        dialogVisible.value = false
+        getModelListTable()
+      })
+    }
+  })
+}
+
+function clearForm() {
+  form.value = {
+    imageName: '',
+    tag: '',
+    modelFilePath: '',
+  }
+}
+
+function reg() {
+  dialogVisible.value = true
+}
+
+function getModelListTable() {
+  getModelContainerImageList(queryParams.value).then(res => {
+    tableData.value = res.data
+  })
+}
+
+onMounted(() => {
+  getModelListTable()
+});
+</script>

+ 17 - 394
ruoyi-ui/src/views/container/index.vue

@@ -1,404 +1,27 @@
 <template>
-  <div class="app-container">
-    <el-row justify="space-between">
-      <el-col :span="12">
-        <el-form :model="queryParams" ref="queryRef" :inline="true" label-width="68px">
-          <el-form-item label="容器名称">
-            <el-input v-model="queryParams.containerName" placeholder="请输入容器名称" clearable style="width: 240px"
-                      @keyup.enter="getModelListTable"/>
-          </el-form-item>
-          <el-form-item>
-            <el-button type="primary" icon="Search" @click="getModelListTable">查询</el-button>
-          </el-form-item>
-        </el-form>
-      </el-col>
-      <el-button type="primary" @click="reg" icon="Plus">创建容器</el-button>
-    </el-row>
-    <el-table
-        :data="tableData"
-        height="69vh"
-        :cell-style="{ padding: '5px' }"
-        :header-cell-style="{ height: heightAll * 0.01 + 'px' }"
-        :row-style="{ fontSize: '1rem', textAlign:'center' }"
-        border>
-      <el-table-column type="index" label="序号" width="80">
-        <template #default="{ $index }">
-          <div style="text-align: center;">{{ $index + 1 }}</div>
-        </template>
-      </el-table-column>
-      <el-table-column prop="containerName" label="模型名称"/>
-      <el-table-column prop="name" label="容器名称"/>
-      <el-table-column prop="imageName" label="容器镜像" width="220"/>
-      <el-table-column prop="publishedPorts" label="开放端口" width="180">
-        <template v-slot:header>
-          开放端口
-          <el-popover title="开放端口" content="开放端口:内置端口" placement="top-start">
-            <template #reference>
-              <QuestionFilled style="width: 1em; height: 1em;"></QuestionFilled>
-            </template>
-          </el-popover>
-        </template>
-      </el-table-column>
-      <el-table-column prop="status" align="center" label="模型状态" width="120">
-        <template #default="scope">
-          <el-tag v-if="scope.row.status==='RUNNING'">
-            {{ getContainerStatus(scope.row.status) }}
-          </el-tag>
-          <el-tag v-if="scope.row.status==='STOPPED'" type="danger">
-            {{ getContainerStatus(scope.row.status) }}
-          </el-tag>
-          <el-tag v-if="scope.row.status==='EXITED'" type="warning">
-            {{ getContainerStatus(scope.row.status) }}
-          </el-tag>
-        </template>
-      </el-table-column>
-      <el-table-column align="center" label="操作" width="300">
-        <template #default="scope">
-          <div style="display: flex;justify-content: space-between;">
-            <el-button type="primary" @click="handleEdit(scope.row)" size="small" text>编辑</el-button>
-            <el-button v-if="scope.row.status!=='RUNNING'" @click="startContainers(scope.row.id)" type="primary"
-                       text
-                       size="small">运行
-            </el-button>
-            <el-button v-if="scope.row.status==='RUNNING'" @click="stopContainers(scope.row.id)" type="danger" text
-                       size="small">停止
-            </el-button>
-            <el-button v-if="scope.row.status==='RUNNING'" @click="restartContainers(scope.row.id)" type="warning"
-                       text
-                       size="small">重启
-            </el-button>
-            <el-button type="danger" @click="handleDelete(scope.row)" text size="small">删除</el-button>
-          </div>
-        </template>
-      </el-table-column>
-    </el-table>
-    <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum"
-                v-model:limit="queryParams.pageSize"/>
-
-    <el-dialog v-model="dialogVisible" :title="dialogTitle" @close="clearForm" destroy-on-close>
-      <el-form :model="form" label-position="right" ref="formRef" label-width="120px" :rules="rulesJi">
-        <el-form-item label="容器名称:" prop="containerName">
-          <el-input v-model="form.containerName"/>
-        </el-form-item>
-        <el-form-item label="容器英文名称:" prop="name">
-          <el-input v-model="form.name"/>
-        </el-form-item>
-        <el-form-item label="镜像:" prop="imageName">
-          <el-select v-model="form.imageName" :disabled="form.id">
-            <el-option v-for="item in imageList" :key="item.id" :label="item.imageName + ':' + item.tag"
-                       :value="item.imageName + ':' + item.tag"></el-option>
-          </el-select>
-        </el-form-item>
-        <el-form-item>
-          <template v-slot:label>
-            <div style="display: flex;align-items: center;">
-              开放端口
-              <el-popover title="开放端口" content="开放端口:内置端口" placement="top-start">
-                <template #reference>
-                  <QuestionFilled style="width: 1em; height: 1em; margin: 0 5px;"></QuestionFilled>
-                </template>
-              </el-popover>
-              :
-            </div>
-          </template>
-          <el-input v-model="form.publishedPorts"/>
-        </el-form-item>
-        <el-form-item label="容器说明">
-          <el-input v-model="form.remark" type="textarea" placeholder="请输入容器说明"/>
-        </el-form-item>
-      </el-form>
-      <template #footer>
-        <el-button @click="dialogVisible = false">取消</el-button>
-        <el-button type="primary" @click="submit">提交</el-button>
-      </template>
-    </el-dialog>
-  </div>
+  <el-tabs tab-position="left" style="height: 100%;padding: 10px 0;">
+    <el-tab-pane label="容器管理">
+      <container-detail></container-detail>
+    </el-tab-pane>
+    <el-tab-pane label="镜像管理">
+      <container-image></container-image>
+    </el-tab-pane>
+    <el-tab-pane label="Docker服务管理">
+      <docker-server></docker-server>
+    </el-tab-pane>
+  </el-tabs>
 </template>
 <script setup>
-import {nextTick, onMounted, reactive, ref} from 'vue';
-import {getModelContainerById, getModelContainerPageList, saveModelContainer} from "@/api/container/modelContainer.js";
-import {deleteContainer, restartContainer, startContainer, stopContainer} from "@/api/container/containerOperation.js";
-import Pagination from "@/components/Pagination/index.vue";
-import {getModelContainerImageList} from "@/api/container/modelContainerImage.js";
-
-const {proxy} = getCurrentInstance();
-
-const queryParams = ref({
-  pageNum: 1,
-  pageSize: 20,
-  containerName: '',
-})
-let tableData = ref([])
-const total = ref(0)
-
-function getContainerStatus(status) {
-  switch (status) {
-    case 'RUNNING':
-      return '运行中'
-    case 'STOPPED':
-      return '已停止'
-    case 'EXITED':
-      return '审核未通过'
-    default:
-      return ''
-  }
-}
-
-function startContainers(id) {
-  startContainer(id).then(() => {
-    proxy.$message.success('启动成功')
-    getModelListTable()
-  })
-}
-
-function stopContainers(id) {
-  stopContainer(id).then(() => {
-    proxy.$message.success('关闭成功')
-    getModelListTable()
-  })
-}
-
-function restartContainers(id) {
-  restartContainer(id).then(() => {
-    proxy.$message.success('重启成功')
-    getModelListTable()
-  })
-}
-
-async function handleEdit(row) {
-  dialogVisible.value = true
-  dialogTitle.value = `编辑【${row.containerName}】容器`
-  await nextTick()
-  getModelContainerById(row.id).then(res => {
-    form.value = res.data
-  })
-}
-
-function handleDelete(row) {
-  proxy.$modal.confirm('是否确认删除?').then(function () {
-    return deleteContainer(row.id);
-  }).then(() => {
-    getModelListTable();
-    proxy.$modal.msgSuccess("删除成功");
-  }).catch(() => {
-  });
-}
-
-const dialogVisible = ref(false)
-const dialogTitle = ref('新建容器')
-const imageList = ref([])
-
-const formRef = ref();
-const form = ref({
-  name: '',
-  imageName: '',
-  publishedPorts: '',
-});
-const rulesJi = reactive({
-  name: [
-    {required: true, message: '请输入容器名称', trigger: 'blur'},
-    {
-      pattern: /^[a-z][a-z0-9_-]{3,49}$/,
-      message: '名称格式不正确,以字母开头,允许使用小写字母、数字、下划线和连字符。',
-      trigger: 'blur'
-    },
-  ],
-  imageName: [{required: true, message: '必填', trigger: 'blur'}],
-});
-
-const heightAll = window.innerHeight
-
-async function submit() {
-  formRef.value.validate((valid) => {
-    if (valid) {
-      if (!form.value.id) {
-        let [imageName, tag] = form.value.imageName.split(':');
-        let image = imageList.value.find(item => item.imageName === imageName && item.tag === tag);
-        if (image) {
-          form.value.imageId = image.imageId
-        }
-      }
-      saveModelContainer(form.value).then(() => {
-        proxy.$modal.msgSuccess("保存成功");
-        dialogVisible.value = false
-        getModelListTable()
-      })
-    }
-  })
-}
-
-function clearForm() {
-  form.value = {
-    name: '',
-    imageName: '',
-    publishedPorts: ''
-  }
-}
-
-function reg() {
-  dialogVisible.value = true
-}
-
-function getModelListTable() {
-  getModelContainerPageList(queryParams.value).then(res => {
-    tableData.value = res.rows
-    total.value = res.total
-  })
-}
-
-function getImageList() {
-  getModelContainerImageList().then(res => {
-    imageList.value = res.data
-  })
-}
-
-getImageList()
-onMounted(() => {
-  getModelListTable()
-});
+import ContainerDetail from "@/views/container/containerDetail.vue";
+import ContainerImage from "@/views/container/containerImage.vue";
+import DockerServer from "@/views/docker/index.vue";
 </script>
 <style scoped>
-.pagination-container {
-  margin-top: 15px;
-}
-
-.type-container {
-  padding-right: 10px;
-
-  .type-item {
-    padding: 10px;
-    border-radius: 10px;
-    text-align: center;
-    background-color: #fff;
-    color: #409EFF;
-    cursor: pointer;
-
-    &.active {
-      background-color: #409EFF;
-      color: #fff;
-    }
-
-  }
-}
-
-:deep(.treeLeft) .el-tree-node__content {
-  display: flex !important;
-  height: 28px; /* 按设计稿调整高度 */
-  align-items: center;
-  padding-top: 0 !important;
-}
-
-:deep(.treeLeft) .el-tree-node__content:hover {
-  background-color: #e9e9eb;
-}
-
-:deep(.treeLeft) .el-tree-node__content:active {
-  background-color: rgka(69, 157, 255, 0.1) !important;
-}
-
-/* 选中态(Active) */
-:deep(.treeLeft) .el-tree-node.is-current > .el-tree-node__content {
-  background-color: #c6e2ff !important;
-}
-
-.tabs-wrapper {
-  position: relative; /* 确保内容层在伪元素上方 */
-  z-index: 1; /* 关键:高于背景图 */
-}
-
-.tabs-wrapper :deep(.el-tabs),
-.tabs-wrapper :deep(.el-tabs__content) {
-  background-color: red !important;
-}
-
-:deep(.el-tabs) {
-  background-color: transparent !important;
-}
-
 :deep(.el-tabs__content) {
-  background-color: transparent !important;
-}
-
-:deep(.custom-dialog-bg) {
-  z-index: 1000;
-  background-image: url('@/assets/images/backDia.jpg') !important;
-  background-position-x: left;
-  background-position-y: bottom;
-  background-size: initial;
-  background-repeat: repeat-x;
-  background-attachment: initial;
-  background-origin: initial;
-  background-clip: initial;
-  background-color: rgb(255, 255, 255);
-}
-
-:deep(.custom-dialog-bg .el-dialog__header) {
-
-}
-
-:deep(.custom-dialog-bg .el-dialog__body) {
-
-  color: #ecf0f1 !important;; /* 内容文字颜色 */
-}
-
-/* 横向排列单选框标签和输入框 */
-.custom-input-wrapper {
-  display: flex;
-  align-items: center;
-  gap: 10px; /* 调整间距 */
-}
-
-/* 输入框仅显示底部横线 */
-.underline-input :deep(.el-input__wrapper) {
-  padding: 0;
-  box-shadow: none !important;
-  border-bottom: 1px solid #dcdfe6; /* 横线颜色 */
-  border-radius: 0;
-  background: transparent;
-}
-
-.underline-input :deep(.el-input__inner) {
-  height: 24px;
-  padding: 0 5px;
-}
-
-:deep(.el-table__body tr:hover > td) {
-  background-color: #eaf7ff !important;
-}
-
-.drag-handle {
-  cursor: move;
-}
-
-.ghost {
-  opacity: 0.5;
-  background: #c8ebfb;
-}
-
-/* 防止文字选中 */
-:deep(.el-table__row) {
-  user-select: none;
-  -webkit-user-select: none;
-}
-</style>
-<style scoped lang="scss">
-
-
-.el-table .el-table__row td {
-  height: 60px !important; /* 行高 */
-}
-
-.custom-tree-node {
-  display: flex; /* 启用 Flex 布局 */
-  align-items: center; /* 垂直居中 */
-  gap: 8px; /* 图标与文字间距 */
-}
-
-:deep(.svg-icon) {
-  outline: none;
+  height: 100%;
 }
 
-:deep(.svg-icon svg) {
-  stroke: none;
+:deep(.el-tab-pane) {
+  height: 100%;
 }
 </style>

+ 0 - 421
ruoyi-ui/src/views/monitor/modelOperation/index.vue

@@ -1,421 +0,0 @@
-<template>
-  <div class="app-container" style="background-color: #F7F7F7;height: 100%;overflow: auto;">
-    <div class="gw-statistic-row" style="height: 12vh;">
-      <gw-statistic v-for="item in statisticData" :key="item.name" :name="item.name" :value="item.value"
-                    :color="item.color"></gw-statistic>
-    </div>
-    <div class="gw-statistic-row" style="height: 30vh;">
-      <gw-card style="width: 50%;">
-        <template v-slot:title>
-          <span>模型服务接口统计</span>
-        </template>
-        <gw-echart ref="top9Ref"></gw-echart>
-      </gw-card>
-      <!--      <gw-card style="width: 33%;">-->
-      <!--        <template v-slot:title>-->
-      <!--          <span>模型计算结果统计</span>-->
-      <!--        </template>-->
-      <!--        <gw-echart ref="top10Ref"></gw-echart>-->
-      <!--      </gw-card>-->
-      <gw-card style="width: 50%;">
-        <template v-slot:title>
-          <div style="display: flex;align-items: center;justify-content: space-between;">
-            <div>
-              <span>模型计算成功统计</span>
-            </div>
-            <el-segmented v-model="dateType" :options="dateTypeOptions" @change="initChartTop11"/>
-          </div>
-        </template>
-        <gw-echart ref="top11Ref"></gw-echart>
-      </gw-card>
-    </div>
-    <div class="gw-statistic-row" style="height: 30vh;">
-      <gw-card style="width: 50%;">
-        <template v-slot:title>
-          <div style="display: flex;align-items: center;justify-content: space-between;">
-            <div>模型调用次数</div>
-            <el-segmented v-model="dateType" :options="dateTypeOptions" @change="initChartTop3"/>
-          </div>
-        </template>
-        <gw-echart ref="top3Ref"></gw-echart>
-      </gw-card>
-      <gw-card style="width: 50%;">
-        <template v-slot:title>
-          <div style="display: flex;align-items: center;justify-content: space-between;">
-            <div>
-              <span>应用调用统计</span>
-              <span style="color: #79bbff;">&nbsp;{{ todayModelCallCount }}&nbsp;</span>
-              <span>次</span>
-            </div>
-            <el-segmented v-model="dateType" :options="dateTypeOptions" @change="initChartTop1"/>
-          </div>
-        </template>
-        <gw-echart ref="top1Ref"></gw-echart>
-      </gw-card>
-      <!--      <gw-card style="width: 30%;">-->
-      <!--        <template v-slot:title>-->
-      <!--          <div style="display: flex;align-items: center;font-weight: bold;width: 100%;">-->
-      <!--            <div style="">模型服务调用次数</div>-->
-      <!--            <div style="width: 50%;margin-left: 20%;">-->
-      <!--              <el-select v-model="userId" class="m-2" placeholder="选则用户" style="width: 100%;"-->
-      <!--                         @change="initChartTop2">-->
-      <!--                <el-option v-for="item in userOptions" :key="item.value" :label="item.label" :value="item.value"/>-->
-      <!--              </el-select>-->
-      <!--            </div>-->
-      <!--          </div>-->
-      <!--        </template>-->
-      <!--        <gw-echart ref="top2Ref"></gw-echart>-->
-      <!--      </gw-card>-->
-    </div>
-
-  </div>
-</template>
-<script setup>
-import {onMounted} from 'vue';
-import * as echarts from 'echarts';
-import GwStatistic from "@/views/monitor/service/GwStatistic.vue";
-import {
-  getModelCallCount,
-  getModelServiceCount,
-  getModelServiceSuccessCount,
-  getModelTypeCallCount,
-  getStatisticData,
-  getUserModelCallCount,
-  getViewNumByCity
-} from "@/api/monitor/server.js";
-import {initEchartMap} from "@/utils/echarts/chinaMap.js";
-import GwCard from "@/views/monitor/service/GwCard.vue";
-import GwEchart from "@/components/chart/GwEchart.vue";
-import {listUser} from "@/api/system/user.js";
-import {parseTime} from "@/utils/ruoyi.js";
-import {getAlarmList} from "@/api/service/alarm.js";
-
-const statisticData = ref([
-  {name: '当前服务总数', value: 0, color: '#477ACF'},
-  {name: '服务在线比率', value: '0%', color: '#40B0D7'},
-  {name: '模型服务支撑健康指数', value: '无', color: '#2DBEA2'},
-  {name: '当月热点服务', value: '无', color: '#487ACF'},
-  {name: '累计调用次数', value: '0', color: '#4BBA9B'},
-])
-const todayModelCallCount = ref(0)
-const userId = ref(null)
-const userOptions = ref([])
-listUser().then(res => {
-  userOptions.value = res.rows.map(item => {
-    return {
-      label: item.nickName,
-      value: item.userId
-    }
-  })
-
-})
-const top1Ref = ref(null)
-const top2Ref = ref(null)
-const top3Ref = ref(null)
-const top5Ref = ref(null)
-const top9Ref = ref(null)
-const top11Ref = ref(null)
-const modelTypeCallCount = ref(0)
-
-const dateType = ref('5')
-const dateTypeOptions = ref([
-  {label: '今日', value: '1'},
-  {label: '近三日', value: '2'},
-  {label: '近一周', value: '3'},
-  {label: '近一个月', value: '4'},
-  {label: '全部', value: '5'},
-])
-const bt1Ref = ref(null)
-const echartMapData = ref([])
-const alarmTableData = ref([
-  {tm: '09-12 11:12', type: '格式异常', modelName: '上海沿海风暴潮预报模型', content: '调用返回异常'},
-  {tm: '09-11 11:12', type: '请求异常', modelName: '上海沿海风暴潮预报模型', content: '模型连接失败'},
-  {tm: '09-10 11:12', type: '服务器未响应', modelName: '上海沿海风暴潮预报模型', content: '服务器报错'},
-])
-
-function getTimes(dateType) {
-  let startTime = null, endTime = null
-  const date = new Date()
-  switch (dateType) {
-    case '1':
-      startTime = parseTime(new Date(), '{y}-{m}-{d}') + ' 00:00:00'
-      date.setDate(date.getDate() + 1)
-      endTime = parseTime(date, '{y}-{m}-{d}') + ' 00:00:00'
-      break;
-    case '2':
-      endTime = parseTime(new Date())
-      date.setDate(date.getDate() - 3)
-      startTime = parseTime(date)
-      break;
-    case '3':
-      endTime = parseTime(new Date())
-      date.setDate(date.getDate() - 7)
-      startTime = parseTime(date)
-      break;
-    case '4':
-      endTime = parseTime(new Date())
-      date.setDate(date.getDate() - 30)
-      startTime = parseTime(date)
-      break;
-    default:
-  }
-  return {startTime, endTime}
-}
-
-function initChartTop1() {
-  const params = {}
-  const {startTime, endTime} = getTimes(dateType.value)
-  params.startTime = startTime
-  params.endTime = endTime
-  getModelCallCount(params).then(res => {
-    let chartData = res.data
-    todayModelCallCount.value = chartData.map(item => item.total).reduce((acc, current) => acc + current, 0);
-    const option = {
-      tooltip: {},
-      grid: {
-        left: '5%',
-        right: '5%',
-        bottom: '0%',
-        top: '10%',
-        containLabel: true
-      },
-      xAxis: {
-        // splitLine: {show: false},
-        axisLabel: {
-          rotate: 10 // 设置标签旋转45度
-        },
-        data: chartData.map(item => item.appName),
-      },
-      yAxis: {
-        type: 'value',
-        name: '次',
-        // splitLine: {show: false}
-      },
-      series: [
-        {
-          data: chartData.map(item => item.total),
-          type: 'bar',
-          label: {
-            show: true,   // 启用标签
-            position: 'top' // 位置:顶部(可选 'inside'、'bottom' 等)
-          }
-        }
-      ]
-    };
-    top1Ref.value.loadChart(option);
-  })
-}
-
-function initChartTop3() {
-  const params = {}
-  const {startTime, endTime} = getTimes(dateType.value)
-  params.startTime = startTime
-  params.endTime = endTime
-  getModelTypeCallCount(params).then(res => {
-    modelTypeCallCount.value = res.data.map(item => item.total).reduce((acc, current) => acc + current, 0);
-    let chartData = res.data.map(item => {
-      return {
-        name: item.name,
-        value: item.total
-      }
-    })
-    const option = {
-      tooltip: {
-        trigger: 'item'
-      },
-      grid: {
-        left: '5%',
-        right: '5%',
-        bottom: '0%',
-        top: '10%',
-        containLabel: true
-      },
-      legend: {
-        top: '90%',
-        left: 'center'
-      },
-      series: [
-        {
-          type: 'pie',
-          radius: ['50%', '70%'],
-          avoidLabelOverlap: false,
-          label: {
-            show: true,
-            position: 'outside',
-            formatter: '{c}',
-          },
-          labelLine: {
-            show: true
-          },
-          color: ['#529b2e', '#95d475', '#b3e19d', '#d1edc4'],
-          data: chartData
-        }
-      ]
-    };
-    top3Ref.value.loadChart(option)
-  })
-}
-
-function initChartTop9() {
-  getModelServiceCount().then(res => {
-    modelTypeCallCount.value = res.data.map(item => item.total).reduce((acc, current) => acc + current, 0);
-    let chartData = res.data.map(item => {
-      return {
-        name: item.name,
-        value: item.total
-      }
-    })
-    const option = {
-      tooltip: {
-        trigger: 'item'
-      },
-      grid: {
-        left: '5%',
-        right: '5%',
-        bottom: '0%',
-        top: '10%',
-        containLabel: true
-      },
-      legend: {
-        top: '90%',
-        left: 'center'
-      },
-      series: [
-        {
-          type: 'pie',
-          radius: ['50%', '70%'],
-          avoidLabelOverlap: false,
-          label: {
-            show: true,
-            position: 'outside',
-            formatter: '{c}',
-          },
-          labelLine: {
-            show: true
-          },
-          color: ['#529b2e', '#95d475', '#b3e19d', '#d1edc4'],
-          data: chartData
-        }
-      ]
-    };
-    top9Ref.value.loadChart(option)
-  })
-}
-
-function initChartTop11() {
-  const params = {}
-  const {startTime, endTime} = getTimes(dateType.value)
-  params.startTime = startTime
-  params.endTime = endTime
-  getModelServiceSuccessCount(params).then(res => {
-    let rawData = [
-      res.data.map(item => item.success),
-      res.data.map(item => item.fail)
-    ]
-    const totalData = [];
-    for (let i = 0; i < rawData[0].length; ++i) {
-      let sum = 0;
-      for (let j = 0; j < rawData.length; ++j) {
-        sum += rawData[j][i];
-      }
-      totalData.push(sum);
-    }
-    const series = ['成功', '失败'].map((name, sid) => {
-      return {
-        name,
-        type: 'bar',
-        stack: 'total',
-        barWidth: '60%',
-        label: {
-          show: true,
-          formatter: (params) => Math.round(params.value * 1000) / 10 + '%'
-        },
-        data: rawData[sid].map((d, did) =>
-            totalData[did] <= 0 ? 0 : d / totalData[did]
-        )
-      };
-    });
-    const option = {
-      legend: {
-        selectedMode: false
-      },
-      tooltip: {
-        trigger: 'item'
-      },
-      grid: {
-        left: '5%',
-        right: '5%',
-        bottom: '0%',
-        top: '20%',
-        containLabel: true
-      },
-      yAxis: {
-        type: 'value'
-      },
-      xAxis: {
-        type: 'category',
-        axisLabel: {
-          rotate: 10 // 设置标签旋转45度
-        },
-        data: res.data.map(item => item.name)
-      },
-      series
-    };
-    top11Ref.value.loadChart(option)
-  })
-}
-
-/** 获取统计数据 */
-function initStatisticData() {
-  getStatisticData().then((r) => {
-    statisticData.value = r.data
-  })
-}
-
-onMounted(() => {
-  initStatisticData()
-
-  initChartTop1()
-  initChartTop3()
-  initChartTop9()
-  initChartTop11()
-});
-</script>
-<style scoped lang="scss">
-.boxShadow {
-  box-shadow: -8px 0 15px -10px rgba(0, 0, 0, 0.1), /* 左侧阴影 */
-  8px 0 15px -10px rgba(0, 0, 0, 0.1); /* 右侧阴影 */
-  transition: box-shadow 0.3s ease;
-
-}
-
-.gw-statistic-row {
-  display: flex;
-  align-content: center;
-  gap: 10px;
-  margin-bottom: 10px;
-
-  .gw-statistic-body {
-    box-shadow: 0px 0px 12px rgba(0, 0, 0, 0.12);
-    width: 100%;
-    height: 100%;
-    border-radius: 8px;
-    background: #fff;
-    padding: 15px 20px;
-
-    .title {
-      font-weight: bold;
-      margin: 0;
-    }
-
-
-  }
-
-}
-
-.chart_container {
-  width: 100%;
-  height: 100%;
-  overflow: hidden;
-}
-</style>

+ 45 - 103
ruoyi-ui/src/views/monitor/taskMonitor/index.vue

@@ -4,40 +4,34 @@
       <gw-statistic v-for="item in statisticData" :key="item.name" :name="item.name" :value="item.value"
                     :color="item.color"></gw-statistic>
     </div>
-    <div class="gw-statistic-row" style="height: 30vh;">
-      <gw-card>
-        <template v-slot:title>
-          <div style="display: flex;align-items: center;justify-content: space-between;">
-            <div>
-              <span>任务统计</span>
-            </div>
-            <el-segmented v-model="task" :options="taskOptions" @change="initChartTop5"/>
-          </div>
-        </template>
-        <gw-echart ref="top5Ref"></gw-echart>
-      </gw-card>
-    </div>
+    <el-card>
+      <el-table
+          :data="tableData"
+          height="59vh"
+          :cell-style="{ padding: '5px' }"
+          :header-cell-style="{ height: heightAll * 0.01 + 'px' }"
+          :row-style="{ fontSize: '1rem', textAlign:'center' }"
+          border>
+        <el-table-column align="center" width="200" prop="name" label="任务名称"></el-table-column>
+        <el-table-column align="center" width="240" prop="modelName" label="关联模型"/>
+        <el-table-column align="center" width="240" prop="time" label="执行时间"/>
+        <el-table-column align="center" prop="status" label="执行状态" width="80">
+          <template #default="scope">
+            <el-tag v-if="scope.row.status==1">成功</el-tag>
+            <el-tag v-else type="danger">失败</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column align="center" prop="remark" label="执行说明"/>
+      </el-table>
+      <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum"
+                  v-model:limit="queryParams.pageSize"/>
+    </el-card>
   </div>
 </template>
 <script setup>
 import {onMounted} from 'vue';
-import * as echarts from 'echarts';
 import GwStatistic from "@/views/monitor/service/GwStatistic.vue";
-import {
-  getModelCallCount,
-  getModelServiceCount,
-  getModelServiceSuccessCount,
-  getModelTypeCallCount,
-  getStatisticData,
-  getUserModelCallCount,
-  getViewNumByCity
-} from "@/api/monitor/server.js";
-import {initEchartMap} from "@/utils/echarts/chinaMap.js";
-import GwCard from "@/views/monitor/service/GwCard.vue";
-import GwEchart from "@/components/chart/GwEchart.vue";
-import {listUser} from "@/api/system/user.js";
-import {parseTime} from "@/utils/ruoyi.js";
-import {getAlarmList} from "@/api/service/alarm.js";
+import Pagination from "@/components/Pagination/index.vue";
 
 const statisticData = ref([
   {name: '任务数据总数', value: 10, color: '#477ACF'},
@@ -45,82 +39,36 @@ const statisticData = ref([
   {name: '日执行量', value: 10, color: '#2DBEA2'},
   {name: '月执行量', value: 145, color: '#487ACF'},
 ])
-const top5Ref = ref(null)
-
-const task = ref('1')
-const taskOptions = ref([
-  {label: '日执行量', value: '1'},
-  {label: '月执行量', value: '2'},
-])
+const queryParams = ref({
+  pageNum: 1,
+  pageSize: 20
+})
+const tableData = ref([])
+const total = ref(0)
 
-function initChartTop5() {
-  const option = {
-    legend: {},
-    grid: {
-      left: '5%',
-      right: '5%',
-      bottom: '10%',
-      top: '15%',
-      containLabel: true
+function getTaskData() {
+  tableData.value = [
+    {
+      time: '2023-05-05 09:09:09',
+      name: '防洪预演',
+      modelName: '模型1',
+      alertContent: '模型1的报警内容'
     },
-    tooltip: {
-      trigger: 'axis',
-      showContent: false
-    },
-    dataset: {
-      source: [
-        ['模型', '09-07', '09-08', '09-09', '09-10', '09-11', '09-12'],
-        ['上海沿海风暴潮预报模型', 56.5, 82.1, 88.7, 70.1, 53.4, 85.1],
-        ['马斯京根法', 51.1, 51.4, 55.1, 53.3, 73.8, 68.7],
-        ['三水源新安江产流模型', 40.1, 62.2, 69.5, 36.4, 45.2, 32.5],
-        ['上海市中心城区排水系统模型', 25.2, 37.1, 41.2, 18, 33.9, 49.1]
-      ]
-    },
-    xAxis: {type: 'category'},
-    yAxis: {gridIndex: 0},
-    series: [
-      {
-        type: 'line',
-        smooth: true,
-        seriesLayoutBy: 'row',
-        emphasis: {focus: 'series'}
-      },
-      {
-        type: 'line',
-        smooth: true,
-        seriesLayoutBy: 'row',
-        emphasis: {focus: 'series'}
-      },
-      {
-        type: 'line',
-        smooth: true,
-        seriesLayoutBy: 'row',
-        emphasis: {focus: 'series'}
-      },
-      {
-        type: 'line',
-        smooth: true,
-        seriesLayoutBy: 'row',
-        emphasis: {focus: 'series'}
-      }
-    ]
-  };
-  top5Ref.value.loadChart(option);
+    {
+      time: '2023-05-05 09:09:09',
+      name: '风暴潮预演',
+      modelName: '模型2',
+      alertContent: '模型2的报警内容'
+    }
+  ]
+  total.value = 2
 }
 
-
 onMounted(() => {
-  initChartTop5()
+  getTaskData()
 });
 </script>
 <style scoped lang="scss">
-.boxShadow {
-  box-shadow: -8px 0 15px -10px rgba(0, 0, 0, 0.1), /* 左侧阴影 */
-  8px 0 15px -10px rgba(0, 0, 0, 0.1); /* 右侧阴影 */
-  transition: box-shadow 0.3s ease;
-
-}
-
 .gw-statistic-row {
   display: flex;
   align-content: center;
@@ -144,10 +92,4 @@ onMounted(() => {
   }
 
 }
-
-.chart_container {
-  width: 100%;
-  height: 100%;
-  overflow: hidden;
-}
 </style>

+ 32 - 37
ruoyi-ui/src/views/service/gateway/index.vue

@@ -64,10 +64,10 @@
           ></el-switch>
         </template>
       </el-table-column>
-      <el-table-column label="操作" align="center" width="150" class-name="small-padding fixed-width">
+      <el-table-column label="操作" align="center" width="160" class-name="small-padding fixed-width">
         <template #default="scope">
-          <el-button type="primary" @click="handleUpdate(scope.row)" size="small" text>编辑</el-button>
-          <el-button type="danger" @click="handleDelete(scope.row)" size="small" text>删除</el-button>
+          <el-button type="primary" @click="handleUpdate(scope.row)" text>编辑</el-button>
+          <el-button type="danger" @click="handleDelete(scope.row)" text>删除</el-button>
         </template>
       </el-table-column>
     </el-table>
@@ -115,7 +115,28 @@
           <el-col :span="12">
             <el-form-item label="状态">
               <el-radio-group v-model="form.status">
-                <el-radio v-for="dict in sys_normal_disable" :key="dict.value" :value="dict.value">
+                <el-radio v-for="dict in [{value: '0', label: '启用'}, {value: '1', label: '停用'}]" :key="dict.value"
+                          :value="dict.value">
+                  {{ dict.label }}
+                </el-radio>
+              </el-radio-group>
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row>
+          <el-col :span="12">
+            <el-form-item label="是否鉴权" prop="filters">
+              <el-radio-group v-model="form.filters">
+                <el-radio value="1">是</el-radio>
+                <el-radio value="0">否</el-radio>
+              </el-radio-group>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="状态">
+              <el-radio-group v-model="form.status">
+                <el-radio v-for="dict in [{value: '0', label: '启用'}, {value: '1', label: '停用'}]" :key="dict.value"
+                          :value="dict.value">
                   {{ dict.label }}
                 </el-radio>
               </el-radio-group>
@@ -141,19 +162,9 @@ const gatewayList = ref([]);
 const open = ref(false);
 const loading = ref(true);
 const showSearch = ref(true);
-const ids = ref([]);
-const single = ref(true);
-const multiple = ref(true);
 const total = ref(0);
 const title = ref("");
 
-// 列显隐信息
-const columns = ref([
-  {key: 0, label: `服务名称`, visible: true},
-  {key: 1, label: `服务地址`, visible: true},
-  {key: 2, label: `状态`, visible: true}
-]);
-
 const data = reactive({
   form: {},
   queryParams: {
@@ -187,15 +198,7 @@ function getList() {
 function handleQuery() {
   queryParams.value.pageNum = 1;
   getList();
-};
-
-/** 重置按钮操作 */
-function resetQuery() {
-  proxy.resetForm("queryRef");
-  queryParams.value.deptId = undefined;
-  proxy.$refs.deptTreeRef.setCurrentKey(null);
-  handleQuery();
-};
+}
 
 /** 删除按钮操作 */
 function handleDelete(row) {
@@ -219,15 +222,7 @@ function handleStatusChange(row) {
   }).catch(function () {
     row.status = row.status === "0" ? "1" : "0";
   });
-};
-
-/** 选择条数  */
-function handleSelectionChange(selection) {
-  ids.value = selection.map(item => item.id);
-  single.value = selection.length != 1;
-  multiple.value = !selection.length;
-};
-
+}
 
 /** 重置操作表单 */
 function reset() {
@@ -245,19 +240,19 @@ function reset() {
     status: "0"
   };
   proxy.resetForm("userRef");
-};
+}
 
 /** 取消按钮 */
 function cancel() {
   open.value = false;
   reset();
-};
+}
 
 /** 新增按钮操作 */
 function handleAdd() {
   reset();
   open.value = true;
-  title.value = "添加服务";
+  title.value = "新建网关";
 }
 
 /** 修改按钮操作 */
@@ -275,7 +270,7 @@ function handleUpdate(row) {
 
   form.value = row;
   open.value = true;
-  title.value = "修改服务";
+  title.value = `编辑【${row.serviceName}】网关`;
 }
 
 /** 提交按钮 */
@@ -290,7 +285,7 @@ function submitForm() {
       });
     }
   });
-};
+}
 
 getList();
 </script>

+ 7 - 1
ruoyi-ui/src/views/service/log/index.vue

@@ -54,6 +54,12 @@
         <el-table-column show-overflow-tooltip align="center" width="100" prop="userName" label="请求用户"/>
         <el-table-column show-overflow-tooltip header-align="center" align="left" width="220" prop="appName"
                          label="请求应用"/>
+        <el-table-column show-overflow-tooltip align="center" width="100" prop="statusCode" label="请求状态">
+          <template #default="scope">
+            <el-tag v-if="scope.row.statusCode==200">成功</el-tag>
+            <el-tag v-else type="danger">失败</el-tag>
+          </template>
+        </el-table-column>
       </el-table>
     </el-row>
     <pagination
@@ -76,7 +82,7 @@ import useClipboard from 'vue-clipboard3'
 
 const {toClipboard} = useClipboard()
 const {proxy} = getCurrentInstance();
-const tableHeight = ref(window.innerHeight - 300)
+const tableHeight = ref(window.innerHeight - 280)
 const queryParams = reactive({
   pageNum: 1,
   pageSize: 20,

Някои файлове не бяха показани, защото твърде много файлове са промени