From 9c91ad8087da1707b973173ebc6d7c3ac7d89195 Mon Sep 17 00:00:00 2001
From: dengzedong <dengzedong@email>
Date: 星期四, 12 九月 2024 15:51:22 +0800
Subject: [PATCH] mpk

---
 src/router/modules/remaining.ts             |   23 
 src/utils/dateUtil.ts                       |    5 
 public/template/模型参数导入模板.xlsx               |    0 
 src/api/mpk/mpk.ts                          |   46 +
 src/views/mpk/mpk.vue                       |  210 +++++++
 src/api/mpk/mpkHistory.ts                   |   10 
 src/views/mpk/ProjectPackageHistory.vue     |  155 +++++
 src/views/mpk/ProjectForm.vue               |  153 +++++
 src/views/mpk/ProjectPackageModelDialog.vue |  119 +++
 src/api/mpk/projectPackageHistory.ts        |   13 
 src/api/mpk/project.ts                      |   33 +
 src/views/mpk/MpkForm.vue                   |  278 +++++++++
 src/views/mpk/MpkGeneratorHistory.vue       |  109 +++
 src/views/mpk/ProjectPackage.vue            |   96 +++
 src/utils/dict.ts                           |    3 
 src/views/mpk/MpkGenerator.vue              |   49 +
 src/views/mpk/MpkRun.vue                    |  243 ++++++++
 src/views/mpk/project.vue                   |  228 +++++++
 18 files changed, 1,772 insertions(+), 1 deletions(-)

diff --git "a/public/template/\346\250\241\345\236\213\345\217\202\346\225\260\345\257\274\345\205\245\346\250\241\346\235\277.xlsx" "b/public/template/\346\250\241\345\236\213\345\217\202\346\225\260\345\257\274\345\205\245\346\250\241\346\235\277.xlsx"
new file mode 100644
index 0000000..cfe9487
--- /dev/null
+++ "b/public/template/\346\250\241\345\236\213\345\217\202\346\225\260\345\257\274\345\205\245\346\250\241\346\235\277.xlsx"
Binary files differ
diff --git a/src/api/mpk/mpk.ts b/src/api/mpk/mpk.ts
new file mode 100644
index 0000000..44800d0
--- /dev/null
+++ b/src/api/mpk/mpk.ts
@@ -0,0 +1,46 @@
+import request from '@/config/axios'
+
+export interface MpkVO {
+  id?: string
+  pyName: string
+  pkgName: string
+  pyType: string
+  className: string
+  dataLength: number
+  pyModule: string
+  remark?: string
+  modelMethods: object
+  filePath: string
+}
+
+export const getPage = async (params: PageParam) => {
+  return await request.get({ url: '/model/mpk/file/page', params })
+}
+
+export const getMpk = async (id: number) => {
+  return await request.get({ url: '/model/mpk/file/' + id })
+}
+
+export const createMpk = async (data: MpkVO) => {
+  return await request.post({ url: '/model/mpk/file', data: data })
+}
+
+export const updateMpk = async (params: MpkVO) => {
+  return await request.put({ url: '/model/mpk/file', data: params })
+}
+
+export const deleteMpk = async (id: number) => {
+  return await request.delete({ url: '/model/mpk/file?id=' + id })
+}
+
+export const generatorCode = (params) => {
+  return request.download({ url: '/model/mpk/file/generat', params })
+}
+
+export const modelRun = (params) => {
+  return request.post({ url: '/model/mpk/api/run', data: params })
+}
+
+export const list = () => {
+  return request.get({ url: '/model/mpk/file/list'})
+}
diff --git a/src/api/mpk/mpkHistory.ts b/src/api/mpk/mpkHistory.ts
new file mode 100644
index 0000000..a026f4e
--- /dev/null
+++ b/src/api/mpk/mpkHistory.ts
@@ -0,0 +1,10 @@
+import request from '@/config/axios'
+
+
+export const getPage = async (params: PageParam) => {
+  return await request.get({ url: '/model/mpk/generatorCodeHistory/page', params })
+}
+
+export const download = (params) => {
+  return request.download({ url: '/model/mpk/generatorCodeHistory/download', params })
+}
diff --git a/src/api/mpk/project.ts b/src/api/mpk/project.ts
new file mode 100644
index 0000000..514b05b
--- /dev/null
+++ b/src/api/mpk/project.ts
@@ -0,0 +1,33 @@
+import request from '@/config/axios'
+
+export const getPage = async (params: PageParam) => {
+  return await request.get({ url: '/model/mpk/project/page', params })
+}
+
+export const getProject = async (id: number) => {
+  return await request.get({ url: '/model/mpk/project/' + id })
+}
+
+export const createProject = async (data) => {
+  return await request.post({ url: '/model/mpk/project', data: data })
+}
+
+export const updateProject = async (params) => {
+  return await request.put({ url: '/model/mpk/project', data: params })
+}
+
+export const deleteProject = async (id: number) => {
+  return await request.delete({ url: '/model/mpk/project?id=' + id })
+}
+
+export const packageProject = (params) => {
+  return request.download({ url: '/model/mpk/file/packageModel', params })
+}
+
+export const list = () => {
+  return request.get({ url: '/model/mpk/project/list'})
+}
+
+export const getProjectModel = async (params: PageParam) => {
+  return await request.get({ url: '/model/mpk/project/getProjectModel', params })
+}
diff --git a/src/api/mpk/projectPackageHistory.ts b/src/api/mpk/projectPackageHistory.ts
new file mode 100644
index 0000000..aff5c30
--- /dev/null
+++ b/src/api/mpk/projectPackageHistory.ts
@@ -0,0 +1,13 @@
+import request from '@/config/axios'
+
+export const getPage = async (params: PageParam) => {
+  return await request.get({ url: '/model/mpk/projectPackageHistory/page', params })
+}
+
+export const download = (params) => {
+  return request.download({ url: '/model/mpk/projectPackageHistory/download', params })
+}
+
+export const getPackageModel = async (params: PageParam) => {
+  return await request.get({ url: '/model/mpk/projectPackageHistory/getPackageModel', params })
+}
diff --git a/src/router/modules/remaining.ts b/src/router/modules/remaining.ts
index 90fa84b..548ed4e 100644
--- a/src/router/modules/remaining.ts
+++ b/src/router/modules/remaining.ts
@@ -328,6 +328,29 @@
       }
     ]
   },
+  {
+    path: '/project',
+    component: Layout,
+    name: 'project',
+    meta: {
+      hidden: true
+    },
+    children: [
+      {
+        path: 'package/history/:projectId',
+        component: () => import('@/views/mpk/ProjectPackageHistory.vue'),
+        name: 'ProjectPackageHistory',
+        meta: {
+          title: '打包历史',
+          noCache: true,
+          hidden: true,
+          canTo: true,
+          icon: '',
+          activeMenu: '/model/project'
+        }
+      }
+    ]
+  },
 ]
 
 export default remainingRouter
diff --git a/src/utils/dateUtil.ts b/src/utils/dateUtil.ts
index 316b870..b83c3a5 100644
--- a/src/utils/dateUtil.ts
+++ b/src/utils/dateUtil.ts
@@ -6,6 +6,7 @@
 
 const DATE_TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'
 const DATE_FORMAT = 'YYYY-MM-DD'
+const DATE_TIME_PATTERN_STRING = 'YYYYMMDDHHmmss'
 
 export function formatToDateTime(date?: dayjs.ConfigType, format = DATE_TIME_FORMAT): string {
   return dayjs(date).format(format)
@@ -15,4 +16,8 @@
   return dayjs(date).format(format)
 }
 
+export function formatToDateString(date?: dayjs.ConfigType, format = DATE_TIME_PATTERN_STRING): string {
+  return dayjs(date).format(format)
+}
+
 export const dateUtil = dayjs
diff --git a/src/utils/dict.ts b/src/utils/dict.ts
index b5e740d..f37f82b 100644
--- a/src/utils/dict.ts
+++ b/src/utils/dict.ts
@@ -232,5 +232,6 @@
   SCHE_MODEL_TYPE = 'sche_model_type',
   SCHE_MODEL_INVOCATION = 'sche_model_invocation',
   SCHE_TRIGGER_METHOD = 'sche_trigger_method',
-  MODEL_PARAM_TYPE = 'model_param_type'
+  MODEL_PARAM_TYPE = 'model_param_type',
+  MODEL_METHOD = 'model_method',
 }
diff --git a/src/views/mpk/MpkForm.vue b/src/views/mpk/MpkForm.vue
new file mode 100644
index 0000000..ee6fb7e
--- /dev/null
+++ b/src/views/mpk/MpkForm.vue
@@ -0,0 +1,278 @@
+<template>
+  <Dialog v-model="dialogVisible" :title="dialogTitle">
+    <el-form
+      ref="formRef"
+      v-loading="formLoading"
+      :model="formData"
+      :rules="formRules"
+      label-width="80px"
+    >
+      <el-divider content-position="left">模型信息</el-divider>
+      <el-row :gutter="8">
+        <el-col :span="12">
+          <el-form-item label="模型名称" prop="pyName">
+            <el-input disabled v-model="formData.pyName" placeholder=""/>
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-upload
+            ref="uploadRef"
+            v-model:file-list="fileList"
+            :show-file-list="false"
+            :action="importUrl"
+            :auto-upload="true"
+            :disabled="uploadLoading"
+            :before-upload="beforeUpload"
+            :headers="uploadHeaders"
+            :on-error="submitFormError"
+            :on-success="submitFormSuccess"
+            accept=".pyd"
+          >
+            <el-button type="primary"><Icon icon="ep:upload" />模型上传</el-button>
+          </el-upload>
+        </el-col>
+      </el-row>
+      <el-row :gutter="8">
+        <el-col :span="12">
+          <el-form-item label="包名" prop="pkgName">
+            <el-input v-model="formData.pkgName" placeholder=""/>
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="模型路径" prop="pyModule">
+            <el-input v-model="formData.pyModule" placeholder=""/>
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-row :gutter="20">
+        <el-col :span="24">
+          <el-form-item label="备注" prop="remark">
+            <el-input v-model="formData.remark" placeholder="" type="textarea"/>
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-divider content-position="left">模型方法</el-divider>
+      <el-row :gutter="20">
+        <el-col :span="4">
+          <el-button type="primary" size="small" @click="addRow()">新增</el-button>
+        </el-col>
+      </el-row>
+      <el-table :data="formData.modelMethods" border>
+        <el-table-column
+          prop=""
+          label="方法名"
+          align="center"
+          width="250">
+          <template #default="scope">
+            <el-select size="small" v-model="scope.row.methodName">
+              <el-option
+                v-for="item in getDictOptions(DICT_TYPE.MODEL_METHOD)"
+                :key="item.value"
+                :label="item.label"
+                :value="item.value"
+                :disabled="methodSelectDisabled(item.value)"
+              />
+            </el-select>
+          </template>
+        </el-table-column>
+        <el-table-column
+          prop=""
+          label="输入个数"
+          align="center">
+          <template #default="scope">
+            <el-input-number size="small" v-model="scope.row.dataLength" :min="1" :max="50"/>
+          </template>
+        </el-table-column>
+        <el-table-column
+          prop=""
+          label="是否有model"
+          align="center">
+          <template #default="scope">
+            <el-switch size="small" v-model="scope.row.model" :active-value="1"
+                       :inactive-value="0"/>
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" fixed="right" header-align="center" align="center" width="100">
+          <template #default="scope">
+            <el-button
+              @click="deleteRow(scope.$index)"
+              key="danger"
+              type="danger"
+              link
+            >删除
+            </el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+    </el-form>
+    <template #footer>
+      <el-button type="primary" @click="submitForm">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+<script lang="ts" setup>
+  import {DICT_TYPE, getDictOptions} from '@/utils/dict'
+  import * as MpkApi from '@/api/mpk/mpk'
+  import {FormRules} from 'element-plus'
+  import {getAccessToken, getTenantId} from "@/utils/auth";
+
+
+  const {t} = useI18n() // 国际化
+  const message = useMessage() // 消息弹窗
+
+  const dialogVisible = ref(false) // 弹窗的是否展示
+  const dialogTitle = ref('') // 弹窗的标题
+  const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+  const formType = ref('') // 表单的类型:create - 新增;update - 修改
+  const formData = ref({
+    id: undefined,
+    pyName: undefined,
+    pkgName: undefined,
+    pyType: undefined,
+    className: undefined,
+    pyModule: undefined,
+    remark: undefined,
+    modelMethods: [],
+    filePath: undefined,
+  })
+
+  const formRules = reactive<FormRules>({
+    pyName: [
+      {required: true, message: '模型名称不能为空,请上传模型文件', trigger: 'blur'}
+    ],
+    pyType: [
+      {required: true, message: '模型类型不能为空', trigger: 'blur'}
+    ],
+    pkgName: [
+      {required: true, message: '包名不能为空', trigger: 'blur'}
+    ],
+    className: [
+      {required: true, message: '类名不能为空', trigger: 'blur'}
+    ],
+    pyModule: [
+      {required: true, message: '模型路径不能为空', trigger: 'blur'}
+    ],
+  })
+
+  const formRef = ref() // 表单 Ref
+
+  /** 打开弹窗 */
+  const open = async (type: string, id?: number) => {
+    dialogVisible.value = true
+    dialogTitle.value = t('action.' + type)
+    formType.value = type
+    resetForm()
+    // 修改时,设置数据
+    if (id) {
+      formLoading.value = true
+      try {
+        formData.value = await MpkApi.getMpk(id)
+      } finally {
+        formLoading.value = false
+      }
+    }
+  }
+  defineExpose({open}) // 提供 open 方法,用于打开弹窗
+
+  /** 提交表单 */
+  const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+  const submitForm = async () => {
+    // 校验表单
+    if (!formRef) return
+    const valid = await formRef.value.validate()
+    if (!valid) return
+    // 模型方法校验
+    if (formData.value.modelMethods?.length <= 0) {
+      message.error('模型方法为空')
+      return
+    }
+    // 模型方法名称校验
+    if (formData.value.modelMethods.some(e => e.methodName === undefined || e.methodName === '')) {
+      message.error('存在不合法模型方法名')
+      return
+    }
+    // 提交请求
+    formLoading.value = true
+    try {
+      const data = formData.value as unknown as MpkApi.MpkVO
+      if (formType.value === 'create') {
+        await MpkApi.createMpk(data)
+        message.success(t('common.createSuccess'))
+      } else {
+        await MpkApi.updateMpk(data)
+        message.success(t('common.updateSuccess'))
+      }
+      dialogVisible.value = false
+      // 发送操作成功的事件
+      emit('success')
+    } finally {
+      formLoading.value = false
+    }
+  }
+
+  /** 重置表单 */
+  const resetForm = () => {
+    formData.value = {
+      id: undefined,
+      pyName: undefined,
+      pkgName: undefined,
+      pyType: undefined,
+      className: undefined,
+      pyModule: undefined,
+      remark: undefined,
+      modelMethods: [],
+      filePath: undefined
+    }
+    formRef.value?.resetFields()
+  }
+
+  const addRow = function () {
+    formData.value.modelMethods.push({
+      methodName: undefined,
+      dataLength: 1,
+      model: 0
+    })
+  }
+  const deleteRow = function (index) {
+    formData.value.modelMethods.splice(index, 1)
+  }
+
+  // 模型方法下拉禁用
+  const methodSelectDisabled = (value) => {
+    if (formData.value.modelMethods.some(e => e.methodName === value)) {
+      return true;
+    }
+    return false;
+  }
+
+  const fileList = ref([]) // 文件列表
+  const importUrl = import.meta.env.VITE_BASE_URL + import.meta.env.VITE_API_URL + '/model/mpk/file/upload'
+  const uploadLoading = ref(false) // 表单的加载中
+  const uploadHeaders = ref() // 上传 Header 头
+  const beforeUpload = function (file) {
+    // 提交请求
+    uploadHeaders.value = {
+      Authorization: 'Bearer ' + getAccessToken(),
+      'tenant-id': getTenantId()
+    }
+    uploadLoading.value = true
+    return true;
+  }
+  const submitFormError = (): void => {
+    message.error('上传失败!')
+    uploadLoading.value = false
+  }
+  const submitFormSuccess = (response: any) => {
+    if (response.code !== 0) {
+      message.error(response.msg)
+      uploadLoading.value = false
+      return
+    }
+    const data = response.data;
+    formData.value.filePath = data.filePath
+    formData.value.pyName = data.fileName.replace('.pyd','')
+    message.success('上传成功')
+    uploadLoading.value = false
+  }
+</script>
diff --git a/src/views/mpk/MpkGenerator.vue b/src/views/mpk/MpkGenerator.vue
new file mode 100644
index 0000000..07ccef1
--- /dev/null
+++ b/src/views/mpk/MpkGenerator.vue
@@ -0,0 +1,49 @@
+<template>
+  <Dialog v-model="dialogVisible" :title="dialogTitle">
+    <el-input
+      type="textarea"
+      :rows="4"
+      placeholder="备注"
+      v-model="remark"/>
+    <div style="width: 100%;display: flex;flex-direction: row;justify-content: end;margin-top: 16px">
+      <el-button @click="generatorCode()" type="primary">生成</el-button>
+    </div>
+  </Dialog>
+</template>
+<script lang="ts" setup>
+  import * as MpkApi from '@/api/mpk/mpk'
+  import download from "@/utils/download";
+  import {formatToDateString} from "@/utils/dateUtil";
+
+
+  const { t } = useI18n() // 国际化
+  const message = useMessage() // 消息弹窗
+
+  const dialogVisible = ref(false) // 弹窗的是否展示
+  const dialogTitle = ref('生成代码') // 弹窗的标题
+
+  const remark = ref('')
+  const id = ref()
+  const zipFileName = ref()
+
+  /** 打开弹窗 */
+  const open = async (modelId: string,pyName: string) => {
+    dialogVisible.value = true
+    id.value = modelId;
+    zipFileName.value = pyName + '_' + formatToDateString(new Date()) + '.zip';
+    remark.value = "";
+  }
+  defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+  /** 提交表单 */
+  const generatorCode = async () => {
+    const param = {
+      'id': id.value,
+      'remark': remark.value,
+      'zipFileName': zipFileName.value
+    }
+    const data = await MpkApi.generatorCode(param)
+    download.zip(data, zipFileName.value)
+    dialogVisible.value = false
+  }
+</script>
diff --git a/src/views/mpk/MpkGeneratorHistory.vue b/src/views/mpk/MpkGeneratorHistory.vue
new file mode 100644
index 0000000..fb79acc
--- /dev/null
+++ b/src/views/mpk/MpkGeneratorHistory.vue
@@ -0,0 +1,109 @@
+<template>
+  <Dialog v-model="dialogVisible" :title="dialogTitle" width="80%">
+    <!-- 搜索工作栏 -->
+    <ContentWrap>
+      <el-form
+        class="-mb-15px"
+        :model="queryParams"
+        ref="queryFormRef"
+        :inline="true"
+        label-width="68px"
+      >
+        <el-form-item prop="startTime">
+          <el-date-picker
+            v-model="queryParams.startTime"
+            type="datetime"
+            value-format="YYYY-MM-DD HH:mm:ss"
+            placeholder="选择日期时间"/>
+        </el-form-item>
+        <el-form-item prop="endTime">
+          <el-date-picker
+            v-model="queryParams.endTime"
+            type="datetime"
+            value-format="YYYY-MM-DD HH:mm:ss"
+            placeholder="选择日期时间"/>
+        </el-form-item>
+        <el-form-item>
+          <el-button @click="getList"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
+        </el-form-item>
+      </el-form>
+    </ContentWrap>
+
+    <!-- 列表 -->
+    <ContentWrap>
+      <el-table
+        v-loading="loading"
+        :data="list"
+        row-key="id"
+        border
+      >
+        <el-table-column label="文件名" header-align="center" align="center">
+          <template #default="scope">
+            <a style="cursor: pointer;color: #409eff" @click="downloadHistory(scope.row.id,scope.row.fileName)">{{ scope.row.fileName }}</a>
+          </template>
+        </el-table-column>
+        <el-table-column prop="remark" label="备注"  header-align="center" align="center"/>
+        <el-table-column prop="createTime" label="生成时间" :formatter="dateFormatter" header-align="center" align="center" width="200"/>
+      </el-table>
+      <!-- 分页 -->
+      <Pagination
+        v-model:limit="queryParams.pageSize"
+        v-model:page="queryParams.page"
+        :total="total"
+        @pagination="getList"
+      />
+    </ContentWrap>
+  </Dialog>
+</template>
+<script lang="ts" setup>
+  import download from "@/utils/download";
+  import * as HistoryApi from "@/api/mpk/mpkHistory";
+  import { dateFormatter } from '@/utils/formatTime'
+
+
+  const { t } = useI18n() // 国际化
+  const message = useMessage() // 消息弹窗
+
+  const dialogVisible = ref(false) // 弹窗的是否展示
+  const dialogTitle = ref('生成历史') // 弹窗的标题
+
+  const loading = ref(true) // 列表的加载中
+  const total = ref(0) // 列表的总页数
+  const list = ref([]) // 字典表格数据
+  const queryParams = reactive({
+    page: 1,
+    pageSize: 10,
+    mdkId: '',
+    startTime: undefined,
+    endTime: undefined,
+  })
+
+  /** 打开弹窗 */
+  const open = async (mdkId: String) => {
+    dialogVisible.value = true
+    queryParams.mdkId = mdkId;
+    queryParams.startTime = undefined;
+    queryParams.endTime = undefined;
+    getList()
+  }
+  defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+  const getList = async () => {
+    loading.value = true
+    try {
+      const data = await HistoryApi.getPage(queryParams)
+      list.value = data.list
+      total.value = data.total
+    } finally {
+      loading.value = false
+    }
+  }
+
+  const downloadHistory = async (id,fileName) => {
+    const param = {
+      'id': id,
+    }
+    const data = await HistoryApi.download(param)
+    download.zip(data, fileName)
+  }
+</script>
diff --git a/src/views/mpk/MpkRun.vue b/src/views/mpk/MpkRun.vue
new file mode 100644
index 0000000..2a5f7f2
--- /dev/null
+++ b/src/views/mpk/MpkRun.vue
@@ -0,0 +1,243 @@
+<template>
+  <Dialog v-model="dialogVisible" :title="dialogTitle">
+    <el-form
+      class="-mb-15px"
+      :model="formData"
+      ref="formRef"
+      :inline="true"
+      :rules="formRules"
+      label-width="68px"
+      v-loading="formLoading"
+    >
+      <el-form-item style="width: 100%">
+        <el-divider content-position="left">模型信息</el-divider>
+      </el-form-item>
+      <el-form-item label="全类名" style="width: 90%" prop="className">
+        <el-input v-model="formData.className" placeholder=""/>
+      </el-form-item>
+      <el-form-item label="方法名" prop="methodName">
+        <el-select v-model="formData.methodName" @change="methodChange" style="width: 240px">
+          <el-option
+            v-for="item in methodList"
+            :key="item.id"
+            :label="item.methodName"
+            :value="item.methodName"
+          />
+        </el-select>
+      </el-form-item>
+      <el-divider content-position="left">模型参数信息</el-divider>
+      <el-row :gutter="20">
+        <el-col :span="2" style="margin-bottom: 10px;margin-left: 20px">
+          <el-button tag="a" href="/template/模型参数导入模板.xlsx" download="模型参数导入模板.xlsx" style="text-decoration: none;" type="primary" size="small" link>模板下载</el-button>
+        </el-col>
+        <el-col :span="2" style="margin-bottom: 10px;">
+          <el-upload
+            ref="uploadRef"
+            v-model:file-list="fileList"
+            :show-file-list="false"
+            :action="importUrl"
+            :auto-upload="true"
+            :disabled="formLoading"
+            :before-upload="beforeUpload"
+            :headers="uploadHeaders"
+            :on-error="submitFormError"
+            :on-success="submitFormSuccess"
+            accept=".xlsx"
+          >
+            <el-button type="primary" size="small" link>参数导入</el-button>
+          </el-upload>
+        </el-col>
+      </el-row>
+      <el-row v-for="(item,index) in formData.datas" :key="index" :gutter="20">
+        <el-col :span="20">
+          <el-form-item :label="'参数_' + (index)" required style="width: 100%">
+            <el-input v-model="formData.datas[index]" placeholder="" />
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-row v-if="hasModel" :gutter="20">
+        <el-col :span="20">
+          <el-form-item label="model" required style="width: 100%">
+            <el-input v-model="formData.model" placeholder="" />
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-divider content-position="left">模型设置信息</el-divider>
+      <el-row :gutter="20">
+        <el-col :span="4">
+          <el-button type="primary" size="small" @click="addRow()">新增</el-button>
+        </el-col>
+      </el-row>
+      <el-table :data="formData.modelSettings" border>
+        <el-table-column
+          prop=""
+          label="参数key"
+          align="center">
+          <template #default="scope">
+            <el-input size="small" v-model="scope.row.settingKey" maxlength="50" clearable />
+          </template>
+        </el-table-column>
+        <el-table-column
+          prop=""
+          label="参数value"
+          align="center">
+          <template #default="scope">
+            <el-input size="small" v-model="scope.row.settingValue" maxlength="50" clearable />
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" fixed="right" header-align="center" align="center" width="100">
+          <template #default="scope">
+            <el-button
+              @click="deleteRow(scope.$index)"
+              key="danger"
+              type="danger"
+              link
+            >删除</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+      <el-divider content-position="left">模型运行结果</el-divider>
+      <el-input v-model="modelRunResult" placeholder="" rows="4" type="textarea" />
+      <div style="display: flex;flex-direction: row;justify-content: end;margin-top: 16px">
+        <el-button :loading="modelRunloading" type="primary" @click="modelRun()">运行</el-button>
+      </div>
+    </el-form>
+  </Dialog>
+</template>
+<script lang="ts" setup>
+  import * as MpkApi from '@/api/mpk/mpk'
+  import {FormRules} from "element-plus";
+  import {getAccessToken, getTenantId} from "@/utils/auth";
+
+  const { t } = useI18n() // 国际化
+  const message = useMessage() // 消息弹窗
+
+  const dialogVisible = ref(false) // 弹窗的是否展示
+  const dialogTitle = ref('模型运行') // 弹窗的标题
+
+  const formData = reactive({
+    className: '',
+    methodName: '',
+    datas: [],
+    modelSettings: [],
+    model: undefined
+  })
+
+  // 模型方法下拉列表
+  const methodList = ref([])
+  const hasModel = ref(false)
+
+  /** 打开弹窗 */
+  const open = async (row) => {
+    dialogVisible.value = true
+    formData.className = row.pkgName + '.impl.' + row.pyName + 'Impl';
+    const mpk = await MpkApi.getMpk(row.id)
+    methodList.value = mpk.modelMethods
+    formData.methodName = mpk.modelMethods[0].methodName
+    formData.datas = []
+    for (let i = 0 ; i < mpk.modelMethods[0].dataLength ; i++) {
+     formData.datas[i] = '[[]]'
+    }
+    hasModel.value = mpk.modelMethods[0].model === 1
+  }
+  defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+  const formRules = reactive<FormRules>({
+    methodName: [
+      {required: true, message: '方法名不能为空', trigger: 'blur'}
+    ],
+    className: [
+      {required: true, message: '全类名不能为空', trigger: 'blur'}
+    ]
+  })
+
+  const addRow = function () {
+    formData.modelSettings.push({
+      settingKey: '',
+      settingValue: ''
+    })
+  }
+  const deleteRow = function (index) {
+    formData.modelSettings.splice(index, 1)
+  }
+  const methodChange = function (value) {
+    formData.datas = []
+    for (let i = 0 ; i < methodList.value.find(e => e.methodName === value)?.dataLength ; i++) {
+      formData.datas[i] = '[[]]'
+    }
+    hasModel.value = methodList.value.find(e => e.methodName === value)?.model === 1
+  }
+
+  const fileList = ref([]) // 文件列表
+  const importUrl =
+    import.meta.env.VITE_BASE_URL + import.meta.env.VITE_API_URL + '/model/mpk/api/import'
+  const formLoading = ref(false) // 表单的加载中
+  const uploadHeaders = ref() // 上传 Header 头
+  /** 上传错误提示 */
+  const submitFormError = (): void => {
+    message.error('导入失败,请检查导入文件!')
+    formLoading.value = false
+  }
+  const submitFormSuccess = (response: any) => {
+    if (response.code !== 0) {
+      message.error(response.msg)
+      formLoading.value = false
+      return
+    }
+    const datas = response.data;
+    for (let i=0;i<formData.datas.length;i++) {
+      formData.datas[i] = datas[i]
+    }
+    message.success('导入成功')
+    formLoading.value = false
+  }
+  const beforeUpload = function (file) {
+    // 提交请求
+    uploadHeaders.value = {
+      Authorization: 'Bearer ' + getAccessToken(),
+      'tenant-id': getTenantId()
+    }
+    formLoading.value = true
+    return true;
+  }
+
+  // 模型运行结果
+  const modelRunResult = ref('')
+  // 模型运行loading
+  const modelRunloading = ref(false)
+  // 表单 Ref
+  const formRef = ref()
+  // 运行
+  const modelRun = async () => {
+// 校验表单
+    if (!formRef) return
+    const valid = await formRef.value.validate()
+    if (!valid) return
+    // 提交请求
+    modelRunloading.value = true
+    try {
+      const data = {
+        ...formData
+      }
+
+      //处理modelSettings
+      let settingsPredict = {};
+      data.modelSettings.forEach(e => {
+        settingsPredict[e.settingKey] = e.settingValue;
+      })
+      data.modelSettings = settingsPredict
+      data.hasModel = hasModel.value
+      if (data.hasModel && data.model) {
+        data.model = {model_path:data.model}
+      }else {
+        data.model = undefined
+      }
+
+      modelRunResult.value = await MpkApi.modelRun(data)
+      modelRunloading.value = false
+      message.success('运行成功')
+    } finally {
+      modelRunloading.value = false
+    }
+  }
+</script>
diff --git a/src/views/mpk/ProjectForm.vue b/src/views/mpk/ProjectForm.vue
new file mode 100644
index 0000000..37fee70
--- /dev/null
+++ b/src/views/mpk/ProjectForm.vue
@@ -0,0 +1,153 @@
+<template>
+  <Dialog v-model="dialogVisible" :title="dialogTitle">
+    <el-form
+      ref="formRef"
+      v-loading="formLoading"
+      :model="formData"
+      :rules="formRules"
+      label-width="80px"
+    >
+      <el-row :gutter="20">
+        <el-col :span="20">
+          <el-form-item label="项目名称" prop="projectName" style="width: 100%">
+            <el-input v-model="formData.projectName" placeholder="" style="width: 100%"/>
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-row :gutter="20">
+        <el-col :span="20">
+          <el-form-item label="项目编码" prop="projectCode" style="width: 100%">
+            <el-input v-model="formData.projectCode" placeholder="" style="width: 100%"/>
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-row :gutter="20">
+        <el-col>
+          <el-form-item label="关联模型" prop="models">
+            <el-transfer :props="{key: 'id',label: 'pyName'}" :titles="['未选模型', '已选模型']" target-order="unshift" filterable :filter-method="filterMethod" v-model="formData.models" :data="modelList">
+              <template #default="{ option }">
+                <span :title="option.pyName">{{ option.pyName}}</span>
+              </template>
+            </el-transfer>
+          </el-form-item>
+        </el-col>
+      </el-row>
+    </el-form>
+    <template #footer>
+      <el-button type="primary" @click="submitForm">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+<script lang="ts" setup>
+  import * as ProjectApi from '@/api/mpk/project'
+  import * as MpkApi from '@/api/mpk/mpk'
+  import {FormRules} from 'element-plus'
+
+
+  const {t} = useI18n() // 国际化
+  const message = useMessage() // 消息弹窗
+
+  const dialogVisible = ref(false) // 弹窗的是否展示
+  const dialogTitle = ref('') // 弹窗的标题
+  const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+  const formType = ref('') // 表单的类型:create - 新增;update - 修改
+  const formData = ref({
+    id: undefined,
+    projectName: undefined,
+    projectCode: undefined,
+  })
+
+
+  const formRules = reactive<FormRules>({
+    projectName: [
+      {required: true, message: '项目名称不能为空', trigger: 'blur'},
+    ],
+    projectCode: [
+      {required: true, message: '项目编码不能为空', trigger: 'blur'},
+    ],
+  })
+
+  const formRef = ref() // 表单 Ref
+
+  /** 打开弹窗 */
+  const open = async (type: string, id?: number) => {
+    dialogVisible.value = true
+    dialogTitle.value = t('action.' + type)
+    formType.value = type
+    getModelList();
+    resetForm()
+    // 修改时,设置数据
+    if (id) {
+      formLoading.value = true
+      try {
+        const data = await ProjectApi.getProject(id)
+        data.models = data.models.map(e => e.id)
+        formData.value = {
+          ...data
+        }
+      } finally {
+        formLoading.value = false
+      }
+    }
+  }
+  defineExpose({open}) // 提供 open 方法,用于打开弹窗
+
+  /** 提交表单 */
+  const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+  const submitForm = async () => {
+    // 校验表单
+    if (!formRef) return
+    const valid = await formRef.value.validate()
+    if (!valid) return
+    // 提交请求
+    formLoading.value = true
+    try {
+      const data = {
+        ...formData.value
+      }
+      data.models = data.models.map(e => {
+        return {id: e}
+      })
+      if (formType.value === 'create') {
+        await ProjectApi.createProject(data)
+        message.success(t('common.createSuccess'))
+      } else {
+        await ProjectApi.updateProject(data)
+        message.success(t('common.updateSuccess'))
+      }
+      dialogVisible.value = false
+      // 发送操作成功的事件
+      emit('success')
+    } finally {
+      formLoading.value = false
+    }
+  }
+
+  /** 重置表单 */
+  const resetForm = () => {
+    formData.value = {
+      id: undefined,
+      projectName: undefined,
+      projectCode: undefined,
+    }
+    formRef.value?.resetFields()
+  }
+
+  // 所有模型列表
+  const modelList = ref([])
+  const getModelList = async () => {
+    modelList.value = await MpkApi.list()
+  }
+
+  // 模型筛选
+  const filterMethod = function (query, item) {
+    return item.pyName.toLowerCase().indexOf(query.toLowerCase()) !== -1
+  }
+</script>
+
+<style scoped>
+  :deep(.el-transfer-panel) {
+    width: 35%;
+  }
+</style>
diff --git a/src/views/mpk/ProjectPackage.vue b/src/views/mpk/ProjectPackage.vue
new file mode 100644
index 0000000..86f3d77
--- /dev/null
+++ b/src/views/mpk/ProjectPackage.vue
@@ -0,0 +1,96 @@
+<template>
+  <Dialog v-model="dialogVisible" :title="dialogTitle">
+    <el-form
+      ref="formRef"
+      v-loading="formLoading"
+      :model="formData"
+      :rules="formRules"
+      label-width="80px"
+    >
+      <el-row>
+        <el-col :span="8">
+          <el-form-item label="版本号" prop="version">
+            <el-input v-model="formData.version" placeholder="请输入版本号"/>
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-row>
+        <el-col :span="24">
+          <el-form-item label="更新日志" prop="log">
+            <el-input type="textarea" :rows="4" placeholder="请输入更新日志" v-model="formData.log"/>
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-row justify="end">
+        <el-col :span="3">
+          <el-button @click="packageProject()" type="primary">打包</el-button>
+        </el-col>
+      </el-row>
+    </el-form>
+  </Dialog>
+</template>
+<script lang="ts" setup>
+  import * as ProjectApi from '@/api/mpk/project'
+  import download from "@/utils/download";
+  import {FormRules} from "element-plus";
+  import {formatToDateString} from "@/utils/dateUtil";
+
+
+  const { t } = useI18n() // 国际化
+  const message = useMessage() // 消息弹窗
+
+  const dialogVisible = ref(false) // 弹窗的是否展示
+  const dialogTitle = ref('打包') // 弹窗的标题
+  const formLoading = ref(false) // 表单的加载中
+
+  const formRules = reactive<FormRules>({
+    version: [
+      {required: true, message: '版本号不能为空', trigger: 'blur'},
+    ],
+    log: [
+      {required: true, message: '更新日志不能为空', trigger: 'blur'},
+    ]
+  })
+
+  const formData = reactive({
+    log: undefined,
+    projectId: undefined,
+    projectName: undefined,
+    projectCode: undefined,
+    ids: undefined,
+    version: undefined,
+  })
+
+  /** 打开弹窗 */
+  const open = async (projectId,projectName,projectCode,ids) => {
+    dialogVisible.value = true
+    formData.projectId = projectId
+    formData.projectName = projectName
+    formData.projectCode = projectCode
+    formData.ids = ids
+    formData.log = undefined
+    formData.version = 'V'
+  }
+  defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+  const formRef = ref() // 表单 Ref
+  /** 提交表单 */
+  const packageProject = async () => {
+    // 校验表单
+    try {
+      if (!formRef) return
+      const valid = await formRef.value.validate()
+      if (!valid) return
+
+      formLoading.value = true
+      formData.zipFileName = 'IAILMPK.' + formData.projectCode + '.' + formatToDateString(new Date()) + '.zip'
+      const data = await ProjectApi.packageProject(formData)
+      debugger
+      download.zip(data, formData.zipFileName)
+      formLoading.value = false
+      dialogVisible.value = false
+    } catch (e) {
+      formLoading.value = false
+    }
+  }
+</script>
diff --git a/src/views/mpk/ProjectPackageHistory.vue b/src/views/mpk/ProjectPackageHistory.vue
new file mode 100644
index 0000000..8548540
--- /dev/null
+++ b/src/views/mpk/ProjectPackageHistory.vue
@@ -0,0 +1,155 @@
+<template>
+  <ContentWrap>
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="68px"
+    >
+      <el-form-item label="项目名称" prop="projectId">
+        <el-select v-model="queryParams.projectId" class="!w-240px">
+          <el-option
+            v-for="item in projectList"
+            :key="item.id"
+            :label="item.projectName"
+            :value="item.id"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="开始时间" prop="startTime">
+        <el-date-picker
+          v-model="queryParams.startTime"
+          type="datetime"
+          value-format="YYYY-MM-DD HH:mm:ss"
+          placeholder="选择日期时间"/>
+      </el-form-item>
+      <el-form-item label="结束时间" prop="endTime">
+        <el-date-picker
+          v-model="queryParams.endTime"
+          type="datetime"
+          value-format="YYYY-MM-DD HH:mm:ss"
+          placeholder="选择日期时间"/>
+      </el-form-item>
+      <el-form-item>
+        <el-button @click="handleQuery">
+          <Icon icon="ep:search" class="mr-5px"/>
+          搜索
+        </el-button>
+        <el-button @click="resetQuery">
+          <Icon icon="ep:refresh" class="mr-5px"/>
+          重置
+        </el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table v-loading="loading" :data="list">
+      <el-table-column label="文件名" header-align="center" align="center" width="400">
+        <template #default="scope">
+          <a style="cursor: pointer;color: #409eff"
+             @click="downloadHistory(scope.row.filePath,scope.row.fileName)">{{ scope.row.fileName
+            }}</a>
+        </template>
+      </el-table-column>
+      <el-table-column prop="version" label="版本号" header-align="center" align="center" width="200"/>
+      <el-table-column prop="log" label="更新日志" header-align="center" align="center"/>
+      <el-table-column prop="createTime" label="打包时间" :formatter="dateFormatter" header-align="center" align="center" width="200"/>
+      <el-table-column label="操作" align="center" width="230px">
+        <template #default="scope">
+          <el-button
+            link
+            type="primary"
+            @click="viewPackageModel(scope.row.id)"
+          >
+            <Icon icon="ep:link"/>
+            查看关联模型
+          </el-button>
+        </template>
+      </el-table-column>
+
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+
+  <PackageModel ref="packageModelRef"/>
+</template>
+<script lang="ts" setup>
+  import download from "@/utils/download";
+  import * as PackageHistoryApi from '@/api/mpk/projectPackageHistory'
+  import * as ProjectApi from '@/api/mpk/project'
+  import {dateFormatter} from '@/utils/formatTime'
+  import PackageModel from './ProjectPackageModelDialog.vue'
+
+  defineOptions({name: 'ProjectPackageHistory'})
+
+  const message = useMessage() // 消息弹窗
+  const {t} = useI18n() // 国际化
+  const route = useRoute() // 路由
+
+  const loading = ref(true) // 列表的加载中
+  const total = ref(0) // 列表的总页数
+  const list = ref([]) // 列表的数据
+  const queryParams = reactive({
+    pageNo: 1,
+    pageSize: 10,
+    projectId: route.params.projectId,
+    startTime: undefined,
+    endTime: undefined,
+  })
+  const queryFormRef = ref() // 搜索的表单
+  const projectList = ref() // 字典类型的列表
+
+  /** 查询列表 */
+  const getList = async () => {
+    loading.value = true
+    try {
+      const data = await PackageHistoryApi.getPage(queryParams)
+      list.value = data.list
+      total.value = data.total
+    } finally {
+      loading.value = false
+    }
+  }
+
+  /** 搜索按钮操作 */
+  const handleQuery = () => {
+    queryParams.pageNo = 1
+    getList()
+  }
+
+  /** 重置按钮操作 */
+  const resetQuery = () => {
+    queryFormRef.value.resetFields()
+    handleQuery()
+  }
+
+  const downloadHistory = async (filePath,fileName) => {
+    const param = {
+      filePath,
+      fileName,
+    }
+    const data = await PackageHistoryApi.download(param)
+    download.zip(data, fileName)
+  }
+
+  const packageModelRef = ref()
+  const viewPackageModel = (id) => {
+    packageModelRef.value.open('package',id)
+  }
+
+  /** 初始化 **/
+  onMounted(async () => {
+    await getList()
+    // 查询字典(精简)列表
+    projectList.value = await ProjectApi.list()
+  })
+</script>
diff --git a/src/views/mpk/ProjectPackageModelDialog.vue b/src/views/mpk/ProjectPackageModelDialog.vue
new file mode 100644
index 0000000..531b66f
--- /dev/null
+++ b/src/views/mpk/ProjectPackageModelDialog.vue
@@ -0,0 +1,119 @@
+<template>
+  <Dialog v-model="dialogVisible" :title="dialogTitle" width="80%">
+    <!-- 搜索工作栏 -->
+    <ContentWrap>
+      <el-form
+        class="-mb-15px"
+        :model="queryParams"
+        ref="queryFormRef"
+        :inline="true"
+        label-width="68px"
+      >
+        <el-form-item label="模型名称" prop="pyName">
+          <el-input
+            v-model="queryParams.pyName"
+            placeholder="请输入模型名称"
+            clearable
+            class="!w-240px"
+          />
+        </el-form-item>
+        <el-form-item>
+          <el-button @click="getList"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
+        </el-form-item>
+      </el-form>
+    </ContentWrap>
+
+    <!-- 列表 -->
+    <ContentWrap>
+      <el-table
+        v-loading="loading"
+        :data="list"
+        row-key="id"
+        border
+      >
+        <el-table-column prop="pyName" label="模型名称"/>
+        <el-table-column prop="pkgName" label="包名"/>
+        <el-table-column prop="pyModule" label="模型路径" width="300px"/>
+        <el-table-column prop="remark" label="备注" width="300px"/>
+        <el-table-column label="模型方法" type="expand" width="100px">
+          <template #default="props">
+            <el-table :data="props.row.modelMethods">
+              <el-table-column align="center" label="方法名" prop="methodName" />
+              <el-table-column align="center" label="参数长度" prop="dataLength" />
+              <el-table-column align="center" label="是否有model" prop="model" :formatter="(row,column,cellValue) => cellValue === 1 ? 'true' : 'false'" />
+            </el-table>
+          </template>
+        </el-table-column>
+      </el-table>
+      <!-- 分页 -->
+      <Pagination
+        v-model:limit="queryParams.pageSize"
+        v-model:page="queryParams.page"
+        :total="total"
+        @pagination="getList"
+      />
+    </ContentWrap>
+  </Dialog>
+</template>
+<script lang="ts" setup>
+  import download from "@/utils/download";
+  import * as projectHistoryApi from '@/api/mpk/projectPackageHistory'
+  import * as projectApi from '@/api/mpk/project'
+  import { dateFormatter } from '@/utils/formatTime'
+
+
+  const { t } = useI18n() // 国际化
+  const message = useMessage() // 消息弹窗
+
+  const dialogVisible = ref(false) // 弹窗的是否展示
+  const dialogTitle = ref('关联模型') // 弹窗的标题
+
+  const loading = ref(true) // 列表的加载中
+  const total = ref(0) // 列表的总页数
+  const list = ref([]) // 字典表格数据
+  const queryParams = reactive({
+    page: 1,
+    pageSize: 10,
+    packageHistoryId: undefined,
+    projectId: undefined,
+    pyName: undefined,
+  })
+
+  const dialogType = ref('')
+
+  /** 打开弹窗 */
+  const open = async (type: String,id: String) => {
+    dialogVisible.value = true
+    dialogType.value = type
+
+    if (dialogType.value === 'package') {
+      queryParams.packageHistoryId = id;
+    }else if (dialogType.value === 'project') {
+      queryParams.projectId = id;
+    }
+
+    queryParams.pyName = undefined;
+    getList()
+  }
+  defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+  const getList = async () => {
+    loading.value = true
+    try {
+      let data = undefined;
+      if (dialogType.value === 'package') {
+        data = await projectHistoryApi.getPackageModel(queryParams)
+        data.list.forEach(e => {
+          e.modelMethods = JSON.parse(e.methodInfo)
+        })
+      }else if (dialogType.value === 'project') {
+        data = await projectApi.getProjectModel(queryParams)
+      }
+
+      list.value = data.list
+      total.value = data.total
+    } finally {
+      loading.value = false
+    }
+  }
+</script>
diff --git a/src/views/mpk/mpk.vue b/src/views/mpk/mpk.vue
new file mode 100644
index 0000000..9354e60
--- /dev/null
+++ b/src/views/mpk/mpk.vue
@@ -0,0 +1,210 @@
+<template>
+  <!-- 搜索工作栏 -->
+  <ContentWrap>
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="68px"
+    >
+      <el-form-item label="模型名称" prop="pyName">
+        <el-input
+          v-model="queryParams.pyName"
+          placeholder="请输入模型名称"
+          clearable
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button @click="handleQuery">
+          <Icon icon="ep:search" class="mr-5px"/>
+          搜索
+        </el-button>
+        <el-button @click="resetQuery">
+          <Icon icon="ep:refresh" class="mr-5px"/>
+          重置
+        </el-button>
+        <el-button
+          type="primary"
+          plain
+          @click="openForm('create')"
+          v-hasPermi="['mpk:create']"
+        >
+          <Icon icon="ep:plus" class="mr-5px"/>
+          新增
+        </el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table
+      v-loading="loading"
+      :data="list"
+      row-key="id"
+    >
+      <el-table-column prop="pyName" label="模型名称"/>
+      <el-table-column prop="pkgName" label="包名"/>
+      <el-table-column prop="pyModule" label="模型路径" width="200px"/>
+      <el-table-column prop="remark" label="备注" width="300px"/>
+      <el-table-column prop="createDate" label="创建时间" :formatter="dateFormatter" width="170px"/>
+      <el-table-column label="操作" align="center" width="200px">
+        <template #default="scope">
+          <div class="flex items-center justify-center">
+            <el-button link type="primary" @click="openForm('update', scope.row.id)" v-hasPermi="['mpk:update']">
+              <Icon icon="ep:edit"/>修改
+            </el-button>
+            <el-button link type="danger" @click="handleDelete(scope.row.id)" v-hasPermi="['mpk:delete']">
+              <Icon icon="ep:delete"/>删除
+            </el-button>
+            <div class="pl-12px">
+              <el-dropdown @command="(command) => handleCommand(command, scope.row)" trigger="click">
+                <el-button type="primary" link>
+                  <Icon icon="ep:d-arrow-right" /> 更多
+                </el-button>
+                <template #dropdown>
+                  <el-dropdown-menu>
+                    <el-dropdown-item
+                      command="generatorCode"
+                    >
+                      生成代码
+                    </el-dropdown-item>
+                    <el-dropdown-item
+                      command="generatorHistory"
+                    >
+                      生成历史
+                    </el-dropdown-item>
+                    <el-dropdown-item
+                      command="mpkRunDialog"
+                    >
+                      运行
+                    </el-dropdown-item>
+                  </el-dropdown-menu>
+                </template>
+              </el-dropdown>
+            </div>
+          </div>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      v-model:limit="queryParams.pageSize"
+      v-model:page="queryParams.page"
+      :total="total"
+      @pagination="getList"
+    />
+  </ContentWrap>
+
+  <!-- 表单弹窗:添加/修改 -->
+  <MpkForm ref="formRef" @success="getList"/>
+  <MpkGenerator ref="mpkGenerator"/>
+  <GeneratorCodeHistory ref="generatorCodeHistory"/>
+  <MpkRun ref="mpkRun"/>
+</template>
+<script lang="ts" setup>
+  import {dateFormatter} from '@/utils/formatTime'
+  import * as MpkApi from '@/api/mpk/mpk'
+  import MpkForm from './MpkForm.vue'
+  import MpkGenerator from './MpkGenerator.vue'
+  import GeneratorCodeHistory from './MpkGeneratorHistory.vue'
+  import MpkRun from './MpkRun.vue'
+  import * as UserApi from "@/api/system/user";
+
+  defineOptions({name: 'Mpk'})
+
+  const message = useMessage() // 消息弹窗
+  const {t} = useI18n() // 国际化
+
+  const loading = ref(true) // 列表的加载中
+  const total = ref(0) // 列表的总页数
+  const list = ref([]) // 字典表格数据
+  const queryParams = reactive({
+    page: 1,
+    pageSize: 10,
+    pyName: ''
+  })
+  const queryFormRef = ref() // 搜索的表单
+
+  const getList = async () => {
+    loading.value = true
+    try {
+      const data = await MpkApi.getPage(queryParams)
+      list.value = data.list
+      total.value = data.total
+    } finally {
+      loading.value = false
+    }
+  }
+
+  /** 操作分发 */
+  const handleCommand = (command: string, row) => {
+    switch (command) {
+      case 'generatorCode':
+        generatorCode(row.id,row.pyName)
+        break
+      case 'generatorHistory':
+        generatorHistory(row.id)
+        break
+      case 'mpkRunDialog':
+        mpkRunDialog(row)
+        break
+      default:
+        break
+    }
+  }
+
+  const mpkGenerator = ref();
+  const generatorCode = (id,pyName) => {
+    mpkGenerator.value.open(id,pyName);
+  }
+
+  const generatorCodeHistory = ref();
+  const generatorHistory = (id) => {
+    generatorCodeHistory.value.open(id);
+  }
+
+  const mpkRun = ref();
+  const mpkRunDialog = (row) => {
+    mpkRun.value.open(row);
+  }
+
+  /** 搜索按钮操作 */
+  const handleQuery = () => {
+    getList()
+  }
+
+  /** 重置按钮操作 */
+  const resetQuery = () => {
+    queryParams.page = 1
+    queryFormRef.value.resetFields()
+    handleQuery()
+  }
+
+  /** 添加/修改操作 */
+  const formRef = ref()
+  const openForm = (type: string, id?: number) => {
+    formRef.value.open(type, id)
+  }
+
+  /** 删除按钮操作 */
+  const handleDelete = async (id: number) => {
+    try {
+      // 删除的二次确认
+      await message.delConfirm()
+      // 发起删除
+      await MpkApi.deleteMpk(id)
+      message.success(t('common.delSuccess'))
+      // 刷新列表
+      await getList()
+    } catch {
+    }
+  }
+
+  /** 初始化 **/
+  onMounted(async () => {
+    await getList()
+  })
+</script>
diff --git a/src/views/mpk/project.vue b/src/views/mpk/project.vue
new file mode 100644
index 0000000..cc4a287
--- /dev/null
+++ b/src/views/mpk/project.vue
@@ -0,0 +1,228 @@
+<template>
+  <!-- 搜索工作栏 -->
+  <ContentWrap>
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="68px"
+    >
+      <el-form-item label="项目名称" prop="projectName">
+        <el-input
+          v-model="queryParams.projectName"
+          placeholder="请输入项目名称"
+          clearable
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="项目编码" prop="projectCode">
+        <el-input
+          v-model="queryParams.projectCode"
+          placeholder="请输入项目编码"
+          clearable
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button @click="handleQuery">
+          <Icon icon="ep:search" class="mr-5px"/>
+          搜索
+        </el-button>
+        <el-button @click="resetQuery">
+          <Icon icon="ep:refresh" class="mr-5px"/>
+          重置
+        </el-button>
+        <el-button
+          type="primary"
+          plain
+          @click="openForm('create')"
+          v-hasPermi="['project:create']"
+        >
+          <Icon icon="ep:plus" class="mr-5px"/>
+          新增
+        </el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table
+      v-loading="loading"
+      :data="list"
+      row-key="id"
+    >
+      <el-table-column prop="projectName" label="项目名称"/>
+      <el-table-column prop="projectCode" label="项目编码"/>
+      <el-table-column prop="createTime" label="创建时间" :formatter="dateFormatter" width="300px"/>
+      <el-table-column label="操作" align="center" width="300px">
+        <template #default="scope">
+          <div class="flex items-center justify-center">
+            <el-button
+              link
+              type="primary"
+              @click="openForm('update', scope.row.id)"
+              v-hasPermi="['project:update']"
+            >
+              <Icon icon="ep:edit"/>
+              修改
+            </el-button>
+            <el-button
+              link
+              type="danger"
+              @click="handleDelete(scope.row.id)"
+              v-hasPermi="['project:delete']"
+            >
+              <Icon icon="ep:delete"/>
+              删除
+            </el-button>
+            <el-button
+              link
+              type="primary"
+              @click="viewRelevanceModel(scope.row.id)"
+            >
+              <Icon icon="ep:link"/>
+              查看关联模型
+            </el-button>
+            <div class="pl-12px">
+              <el-dropdown @command="(command) => handleCommand(command, scope.row)"
+                           trigger="click">
+                <el-button type="primary" link>
+                  <Icon icon="ep:d-arrow-right"/>
+                  更多
+                </el-button>
+                <template #dropdown>
+                  <el-dropdown-menu>
+                    <el-dropdown-item
+                      command="packageModel"
+                    >
+                      <el-button link>打包</el-button>
+                    </el-dropdown-item>
+                    <el-dropdown-item
+                    >
+                      <router-link :to="'/project/package/history/' + scope.row.id">
+                        <el-button link>打包历史</el-button>
+                      </router-link>
+                    </el-dropdown-item>
+                  </el-dropdown-menu>
+                </template>
+              </el-dropdown>
+            </div>
+          </div>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      v-model:limit="queryParams.pageSize"
+      v-model:page="queryParams.page"
+      :total="total"
+      @pagination="getList"
+    />
+  </ContentWrap>
+
+  <!-- 表单弹窗:添加/修改 -->
+  <ProjectForm ref="formRef" @success="getList"/>
+  <ProjectPackage ref="projectPackageRef"/>
+  <RelevanceModel ref="relevanceModelRef"/>
+</template>
+<script lang="ts" setup>
+  import {dateFormatter} from '@/utils/formatTime'
+  import * as ProjectApi from '@/api/mpk/project'
+  import ProjectForm from './ProjectForm.vue'
+  import ProjectPackage from './ProjectPackage.vue'
+  import RelevanceModel from './ProjectPackageModelDialog.vue'
+
+  defineOptions({name: 'Project'})
+
+  const message = useMessage() // 消息弹窗
+  const {t} = useI18n() // 国际化
+
+  const loading = ref(true) // 列表的加载中
+  const total = ref(0) // 列表的总页数
+  const list = ref([]) // 字典表格数据
+  const queryParams = reactive({
+    page: 1,
+    pageSize: 10,
+    projectName: '',
+    projectCode: ''
+  })
+  const queryFormRef = ref() // 搜索的表单
+
+  const getList = async () => {
+    loading.value = true
+    try {
+      const data = await ProjectApi.getPage(queryParams)
+      list.value = data.list
+      total.value = data.total
+    } finally {
+      loading.value = false
+    }
+  }
+
+  /** 操作分发 */
+  const handleCommand = (command: string, row) => {
+    switch (command) {
+      case 'packageModel':
+        packageModel(row.id, row.projectName, row.projectCode, row.models)
+        break
+      default:
+        break
+    }
+  }
+
+  //打包
+  const projectPackageRef = ref();
+  const packageModel = (projectId, projectName, projectCode, models) => {
+    let ids = models.map(e => e.id);
+    if (ids && ids.length > 0) {
+      projectPackageRef.value.open(projectId, projectName, projectCode, ids.join(","));
+    } else {
+      message.error("请先为项目添加模型!")
+    }
+  }
+
+  /** 搜索按钮操作 */
+  const handleQuery = () => {
+    getList()
+  }
+
+  /** 重置按钮操作 */
+  const resetQuery = () => {
+    queryParams.page = 1
+    queryFormRef.value.resetFields()
+    handleQuery()
+  }
+
+  /** 添加/修改操作 */
+  const formRef = ref()
+  const openForm = (type: string, id?: number) => {
+    formRef.value.open(type, id)
+  }
+
+  /** 删除按钮操作 */
+  const handleDelete = async (id: number) => {
+    try {
+      // 删除的二次确认
+      await message.delConfirm()
+      // 发起删除
+      await ProjectApi.deleteProject(id)
+      message.success(t('common.delSuccess'))
+      // 刷新列表
+      await getList()
+    } catch {
+    }
+  }
+
+  // 查看关联模型
+  const relevanceModelRef = ref()
+  const viewRelevanceModel = (id) => {
+    relevanceModelRef.value.open('project',id)
+  }
+
+  /** 初始化 **/
+  onMounted(async () => {
+    await getList()
+  })
+</script>

--
Gitblit v1.9.3