Browse Source

完成分页组件,实现知识库模块

愿你天天开心 9 tháng trước cách đây
mục cha
commit
06f7856552

+ 22 - 0
package-lock.json

@@ -31,6 +31,7 @@
         "nprogress": "0.2.0",
         "pinia": "2.1.7",
         "screenfull": "6.0.2",
+        "uuid": "^10.0.0",
         "vue": "3.4.25",
         "vue-cropper": "1.1.1",
         "vue-i18n": "9.10.2",
@@ -46,6 +47,7 @@
         "@types/js-cookie": "3.0.6",
         "@types/node": "18.18.2",
         "@types/nprogress": "0.2.3",
+        "@types/uuid": "^10.0.0",
         "@typescript-eslint/eslint-plugin": "7.3.1",
         "@typescript-eslint/parser": "7.3.1",
         "@unocss/preset-attributify": "0.58.6",
@@ -1817,6 +1819,13 @@
         "@types/node": "*"
       }
     },
+    "node_modules/@types/uuid": {
+      "version": "10.0.0",
+      "resolved": "https://registry.npmmirror.com/@types/uuid/-/uuid-10.0.0.tgz",
+      "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/@types/web-bluetooth": {
       "version": "0.0.20",
       "resolved": "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz",
@@ -9963,6 +9972,19 @@
       "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
       "dev": true
     },
+    "node_modules/uuid": {
+      "version": "10.0.0",
+      "resolved": "https://registry.npmmirror.com/uuid/-/uuid-10.0.0.tgz",
+      "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==",
+      "funding": [
+        "https://github.com/sponsors/broofa",
+        "https://github.com/sponsors/ctavan"
+      ],
+      "license": "MIT",
+      "bin": {
+        "uuid": "dist/bin/uuid"
+      }
+    },
     "node_modules/vary": {
       "version": "1.1.2",
       "resolved": "https://registry.npmmirror.com/vary/-/vary-1.1.2.tgz",

+ 4 - 2
package.json

@@ -40,6 +40,7 @@
     "nprogress": "0.2.0",
     "pinia": "2.1.7",
     "screenfull": "6.0.2",
+    "uuid": "^10.0.0",
     "vue": "3.4.25",
     "vue-cropper": "1.1.1",
     "vue-i18n": "9.10.2",
@@ -55,6 +56,7 @@
     "@types/js-cookie": "3.0.6",
     "@types/node": "18.18.2",
     "@types/nprogress": "0.2.3",
+    "@types/uuid": "^10.0.0",
     "@typescript-eslint/eslint-plugin": "7.3.1",
     "@typescript-eslint/parser": "7.3.1",
     "@unocss/preset-attributify": "0.58.6",
@@ -66,10 +68,10 @@
     "eslint": "8.57.0",
     "eslint-config-prettier": "9.1.0",
     "eslint-define-config": "2.1.0",
+    "eslint-plugin-import": "2.29.1",
+    "eslint-plugin-node": "11.1.0",
     "eslint-plugin-prettier": "5.1.3",
     "eslint-plugin-promise": "6.1.1",
-    "eslint-plugin-node": "11.1.0",
-    "eslint-plugin-import": "2.29.1",
     "eslint-plugin-vue": "9.23.0",
     "fast-glob": "3.3.2",
     "postcss": "8.4.36",

+ 1 - 1
src/api/system/menu/index.ts

@@ -46,7 +46,7 @@ export const tenantPackageMenuTreeselect = (packageId: string | number): AxiosPr
 // 新增菜单
 export const addMenu = (data: MenuForm) => {
   return request({
-    url: '/system/menu',
+    url: '/system/menu/create',
     method: 'post',
     data: data
   });

+ 159 - 0
src/components/ChunkUpload/index.vue

@@ -0,0 +1,159 @@
+<template>
+  <div class="chunk-upload">
+    <el-button type="primary" @click="handleClick" :disabled="uploading">
+      {{ uploading ? '上传中...' : '选择并上传文件' }}
+    </el-button>
+    <div v-for="(progress, index) in uploadProgressList" :key="index" class="progress-item">
+      <p>File {{ index + 1 }}: {{ progress.fileName }}</p>
+      <el-progress :percentage="progress.percentage"></el-progress>
+    </div>
+    <input type="file" ref="fileInput" @change="handleFileChange" style="display: none;" multiple />
+  </div>
+</template>
+
+<script lang="ts">
+import { defineComponent, ref } from "vue";
+import { ElMessage } from "element-plus";
+import axios from "axios";
+import { v1 as uuidv1 } from "uuid";
+
+axios.defaults.baseURL = "http://10.181.7.236:9988";
+
+export default defineComponent({
+  name: "ChunkUpload",
+  props: {
+    maxFileSize: {
+      type: Number,
+      default: 10 * 1024 * 1024, // 默认10MB
+    },
+    maxFiles: {
+      type: Number,
+      default: 3, // 默认最大上传3个文件
+    },
+  },
+  setup(props) {
+    const fileInput = ref<HTMLInputElement | null>(null);
+    const uploading = ref(false);
+    const uploadProgressList = ref<{ fileName: string; percentage: number }[]>([]);
+    const CHUNK_SIZE = 1 * 1024 * 1024; // 每个分片的大小为 1MB
+
+    const handleClick = () => {
+      fileInput.value?.click();
+    };
+
+    const handleFileChange = async (event: Event) => {
+      const files = (event.target as HTMLInputElement).files;
+      if (!files || files.length === 0) {
+        return;
+      }
+
+      // 检查文件数量
+      if (files.length > props.maxFiles) {
+        ElMessage.error(`最多只能上传 ${props.maxFiles} 个文件`);
+        return;
+      }
+
+      uploading.value = true;
+      uploadProgressList.value = Array.from(files).map(file => ({
+        fileName: file.name,
+        percentage: 0,
+      }));
+
+      try {
+        for (let i = 0; i < files.length; i++) {
+          const file = files[i];
+          if (file.size > props.maxFileSize) {
+            ElMessage.error(`文件 ${file.name} 大小不能超过 ${props.maxFileSize / (1024 * 1024)} MB`);
+            continue; // 跳过这个文件继续上传下一个
+          }
+
+          const totalChunks = Math.ceil(file.size / CHUNK_SIZE);
+          const fileIdentifier = await generateFileIdentifier();
+
+          for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex++) {
+            const start = chunkIndex * CHUNK_SIZE;
+            const chunk = file.slice(start, start + CHUNK_SIZE);
+            await uploadChunk(chunk, chunkIndex, totalChunks, fileIdentifier);
+            uploadProgressList.value[i].percentage = Math.round(((chunkIndex + 1) / totalChunks) * 100);
+          }
+
+          // 文件分片上传完成后,调用合并接口
+          const uuidFilename = await mergeChunks(fileIdentifier, file.name);
+          console.log("上传成功的文件 UUID 名称:", uuidFilename);
+        }
+
+        ElMessage.success("文件上传完成!");
+      } catch (error) {
+        ElMessage.error("文件上传失败!");
+        console.error("Upload error:", error);
+      } finally {
+        uploading.value = false;
+      }
+    };
+
+    const uploadChunk = async (
+      chunk: Blob,
+      chunkNumber: number,
+      totalChunks: number,
+      fileIdentifier: string
+    ) => {
+      const formData = new FormData();
+      formData.append("file", chunk);
+
+      try {
+        await axios.post(`/api/file/upload/uploadfile`, formData, {
+          params: {
+            chunknumber: chunkNumber,
+            identifier: fileIdentifier,
+          },
+          headers: {
+            "Content-Type": "multipart/form-data",
+          },
+        });
+      } catch (error) {
+        throw new Error(`上传分片 ${chunkNumber} 失败`);
+      }
+    };
+
+    const mergeChunks = async (identifier: string, filename: string) => {
+      try {
+        const response = await axios.post("/api/file/upload/mergefile", null, {
+          params: {
+            identifier: identifier,
+            filename: filename,
+            chunkstar: 0,  // 假设所有分片的开始序号为0
+          },
+        });
+
+        if (response.status !== 200) {
+          throw new Error("文件合并失败");
+        }
+        return response.data.uuidFilename;
+      } catch (error) {
+        throw new Error("合并请求失败");
+      }
+    };
+
+    const generateFileIdentifier = async (): Promise<string> => {
+      return uuidv1();  // 生成一个固定的 UUID1
+    };
+
+    return {
+      fileInput,
+      uploading,
+      uploadProgressList,
+      handleClick,
+      handleFileChange,
+    };
+  },
+});
+</script>
+
+<style scoped>
+.chunk-upload {
+  padding: 20px;
+}
+.progress-item {
+  margin-top: 10px;
+}
+</style>

+ 14 - 0
src/types/components.d.ts

@@ -12,6 +12,7 @@ declare module 'vue' {
     BpmnView: typeof import('./../components/BpmnView/index.vue')['default']
     Breadcrumb: typeof import('./../components/Breadcrumb/index.vue')['default']
     BuildCode: typeof import('./../components/BuildCode/index.vue')['default']
+    ChunkUpload: typeof import('./../components/ChunkUpload/index.vue')['default']
     DictTag: typeof import('./../components/DictTag/index.vue')['default']
     Editor: typeof import('./../components/Editor/index.vue')['default']
     ElAutocomplete: typeof import('element-plus/es')['ElAutocomplete']
@@ -21,9 +22,13 @@ declare module 'vue' {
     ElButton: typeof import('element-plus/es')['ElButton']
     ElCard: typeof import('element-plus/es')['ElCard']
     ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
+    ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
     ElCol: typeof import('element-plus/es')['ElCol']
+    ElCollapse: typeof import('element-plus/es')['ElCollapse']
+    ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
     ElColorPicker: typeof import('element-plus/es')['ElColorPicker']
     ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
+    ElContainer: typeof import('element-plus/es')['ElContainer']
     ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
     ElDialog: typeof import('element-plus/es')['ElDialog']
     ElDivider: typeof import('element-plus/es')['ElDivider']
@@ -34,25 +39,34 @@ declare module 'vue' {
     ElEmpty: typeof import('element-plus/es')['ElEmpty']
     ElForm: typeof import('element-plus/es')['ElForm']
     ElFormItem: typeof import('element-plus/es')['ElFormItem']
+    ElHeader: typeof import('element-plus/es')['ElHeader']
     ElIcon: typeof import('element-plus/es')['ElIcon']
+    ElImage: typeof import('element-plus/es')['ElImage']
     ElInput: typeof import('element-plus/es')['ElInput']
     ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
     ElLink: typeof import('element-plus/es')['ElLink']
+    ElMain: typeof import('element-plus/es')['ElMain']
     ElMenu: typeof import('element-plus/es')['ElMenu']
     ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
     ElOption: typeof import('element-plus/es')['ElOption']
     ElPagination: typeof import('element-plus/es')['ElPagination']
     ElPopover: typeof import('element-plus/es')['ElPopover']
+    ElProgress: typeof import('element-plus/es')['ElProgress']
     ElRadio: typeof import('element-plus/es')['ElRadio']
+    ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
     ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
     ElRow: typeof import('element-plus/es')['ElRow']
     ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
     ElSelect: typeof import('element-plus/es')['ElSelect']
+    ElSpace: typeof import('element-plus/es')['ElSpace']
     ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
     ElSwitch: typeof import('element-plus/es')['ElSwitch']
     ElTable: typeof import('element-plus/es')['ElTable']
     ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
+    ElTabPane: typeof import('element-plus/es')['ElTabPane']
+    ElTabs: typeof import('element-plus/es')['ElTabs']
     ElTag: typeof import('element-plus/es')['ElTag']
+    ElText: typeof import('element-plus/es')['ElText']
     ElTooltip: typeof import('element-plus/es')['ElTooltip']
     ElTree: typeof import('element-plus/es')['ElTree']
     ElTreeSelect: typeof import('element-plus/es')['ElTreeSelect']

+ 312 - 0
src/views/knowledge/knowledge-management/index.vue

@@ -0,0 +1,312 @@
+<template>
+  <div class="p-2">
+    <transition name="fade">
+      <div v-show="showSearch" class="mb-[10px]">
+          <el-form ref="queryFormRef" :model="queryParams" :inline="true">
+            <el-form-item style="width: 200px" label="事件类型" prop="eventType">
+              <el-select v-model="queryParams.eventType" placeholder="全部" clearable>
+                <el-option label="全部" value=""></el-option>
+                <el-option
+                  v-for="item in data.eventTypeSelection"
+                  :key="item.dictValue"
+                  :label="item.dictLabel"
+                  :value="item.dictValue"
+                ></el-option>
+              </el-select>
+            </el-form-item>
+            <el-form-item label="发布日期" prop="publishDate">
+              <el-date-picker
+                v-model="queryParams.publishDate"
+                type="daterange"
+                range-separator="-"
+                start-placeholder="开始日期"
+                end-placeholder="结束日期"
+                value-format="yyyy-MM-dd"
+              ></el-date-picker>
+            </el-form-item>
+            <el-form-item>
+              <el-input v-model="queryParams.keyword" placeholder="请输入报告的关键词/主题词" clearable
+                        @keyup.enter="handleQuery"/>
+            </el-form-item>
+            <el-form-item>
+              <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
+            </el-form-item>
+            <el-form-item>
+              <el-button icon="Refresh" @click="resetQuery">重置</el-button>
+            </el-form-item>
+          </el-form>
+      </div>
+    </transition>
+        <el-row :gutter="10" class="mb8">
+          <el-col :span="1.5">
+            <el-button type="primary" plain icon="Plus" @click="handleAdd">新增</el-button>
+          </el-col>
+          <el-col :span="1.5">
+            <el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate(selectedRow)">修改
+            </el-button>
+          </el-col>
+          <el-col :span="1.5">
+            <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete(selectedRow)">删除
+            </el-button>
+          </el-col>
+          <el-col :span="1.5">
+            <el-button type="warning" plain icon="Download" @click="handleExport">导出</el-button>
+          </el-col>
+          <right-toolbar v-model:showSearch="showSearch" @query-table="getList"></right-toolbar>
+        </el-row>
+
+      <!--      表格组件-->
+      <el-table v-loading="loading" :data="demoList" @selection-change="handleSelectionChange">
+        <el-table-column type="selection" width="55" align="center"/>
+        <el-table-column label="报告编号" align="center" prop="reportNumber"/>
+        <el-table-column label="报告名称" align="center" prop="reportName"/>
+        <el-table-column label="主题词" align="center" prop="keyword"/>
+        <el-table-column label="事件类型" align="center" prop="eventType"/>
+        <el-table-column label="摘要" align="center" prop="summary"/>
+        <el-table-column label="来源单位" align="center" prop="sourceUnit"/>
+        <el-table-column label="发布日期" align="center" prop="publishDate"/>
+        <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+          <template #default="scope">
+            <el-tooltip content="修改" placement="top">
+              <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)"></el-button>
+            </el-tooltip>
+            <el-tooltip content="删除" placement="top">
+              <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)"></el-button>
+            </el-tooltip>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <pagination v-show="total > 0" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize"
+                  :total="total" @pagination="getList"/>
+
+    <!--    新增/修改弹窗-->
+    <el-dialog v-model="dialog.visible" :title="dialog.title" width="500px" append-to-body>
+      <el-form ref="demoFormRef" :model="form" :rules="rules" label-width="80px">
+        <el-form-item label="报告编号" prop="reportNumber">
+          <el-input v-model="form.reportNumber" placeholder="请输入报告编号"/>
+        </el-form-item>
+        <el-form-item label="报告名称" prop="reportName">
+          <el-input v-model="form.reportName" placeholder="请输入报告名称"/>
+        </el-form-item>
+        <el-form-item label="主题词" prop="keyword">
+          <el-input v-model="form.keyword" placeholder="请输入主题词"/>
+        </el-form-item>
+        <el-form-item label="事件类型" prop="eventType">
+          <el-select v-model="form.eventType" placeholder="请选择事件类型" clearable>
+            <el-option
+              v-for="item in data.eventTypeSelection"
+              :key="item.dictValue"
+              :label="item.dictLabel"
+              :value="item.dictValue"
+            ></el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item label="摘要" prop="summary">
+          <el-input v-model="form.summary" placeholder="请输入摘要"/>
+        </el-form-item>
+        <el-form-item label="来源单位" prop="sourceUnit">
+          <el-input v-model="form.sourceUnit" placeholder="请输入来源单位"/>
+        </el-form-item>
+        <el-form-item label="发布日期" prop="publishDate">
+          <el-date-picker
+            v-model="form.publishDate"
+            type="date"
+            placeholder="选择发布日期"
+            value-format="yyyy-MM-dd"
+          ></el-date-picker>
+        </el-form-item>
+        <el-col :span="1.5">
+          <!-- 使用分片上传组件,每个分片-->
+          <chunk-upload :max-file-size="50 * 1024 * 1024" :max-files="5" />
+        </el-col>
+      </el-form>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button :loading="buttonLoading" type="primary" @click="submitForm">确定</el-button>
+          <el-button @click="cancel">取消</el-button>
+        </div>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup lang="ts">
+import {ref, reactive, toRefs, onMounted} from 'vue';
+import {getDicts} from "@/api/system/dict/data";
+import {ElMessage} from 'element-plus';
+import ChunkUpload from '@/components/ChunkUpload/index.vue';
+
+const demoList = ref([]);
+const buttonLoading = 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 selectedRow = ref(null);
+
+const queryFormRef = ref();
+const demoFormRef = ref();
+
+// const fileList = ref([]);
+
+const dialog = reactive({
+  visible: false,
+  title: ''
+});
+
+const initFormData = {
+  reportNumber: '',
+  reportName: '',
+  keyword: '',
+  eventType: '',
+  summary: '',
+  sourceUnit: '',
+  publishDate: ''
+};
+
+const data = reactive({
+  form: {...initFormData},
+  queryParams: {
+    pageNum: 1,
+    pageSize: 10,
+    eventType: '',
+    publishDate: ['', ''],
+    keyword: ''
+  },
+  rules: {
+    reportNumber: [{required: true, message: '报告编号不能为空', trigger: 'blur'}],
+    reportName: [{required: true, message: '报告名称不能为空', trigger: 'blur'}],
+    keyword: [{required: true, message: '主题词不能为空', trigger: 'blur'}],
+    eventType: [{required: true, message: '事件类型不能为空', trigger: 'blur'}],
+    summary: [{required: true, message: '摘要不能为空', trigger: 'blur'}],
+    sourceUnit: [{required: true, message: '来源单位不能为空', trigger: 'blur'}],
+    publishDate: [{required: true, message: '发布日期不能为空', trigger: 'blur'}]
+  },
+  eventTypeSelection: []
+});
+
+const {queryParams, form, rules} = toRefs(data);
+
+const getList = async () => {
+  loading.value = true;
+  try {
+    const response = await fetchReports(queryParams.value);
+    const {data, total} = response;
+    demoList.value = data;
+    total.value = total;
+  } catch (error) {
+    ElMessage.error('获取数据失败');
+  } finally {
+    loading.value = false;
+  }
+};
+
+const fetchReports = async (params) => {
+  // 假设后端接口为 /api/reports
+  const response = await fetch('/api/reports', {
+    method: 'POST',
+    headers: {
+      'Content-Type': 'application/json',
+    },
+    body: JSON.stringify(params)
+  });
+  const result = await response.json();
+  if (response.ok) {
+    return result;
+  } else {
+    throw new Error(result.message || '获取报告列表失败');
+  }
+};
+
+const cancel = () => {
+  reset();
+  dialog.visible = false;
+};
+
+const reset = () => {
+  form.value = {...initFormData};
+  demoFormRef.value?.resetFields();
+};
+
+const handleQuery = () => {
+  queryParams.value.pageNum = 1;
+  getList();
+};
+
+const resetQuery = () => {
+  queryParams.value = {pageNum: 1, pageSize: 10, eventType: '', publishDate: ['', ''], keyword: ''};
+  handleQuery();
+};
+
+const handleSelectionChange = (selection) => {
+  ids.value = selection.map((item) => item.reportNumber);
+  selectedRow.value = selection.length === 1 ? selection[0] : null;
+  single.value = selection.length != 1;
+  multiple.value = !selection.length;
+};
+
+const handleAdd = () => {
+  reset();
+  dialog.visible = true;
+  dialog.title = '添加报告';
+};
+
+// 修改报告
+const handleUpdate = (row) => {
+  if (row) {
+    reset();
+    Object.assign(form.value, row);
+    dialog.visible = true;
+    dialog.title = '修改报告';
+  }
+};
+ // 提交表单
+const submitForm = () => {
+  demoFormRef.value?.validate((valid) => {
+    if (valid) {
+      buttonLoading.value = true;
+      setTimeout(() => {
+        if (form.value.reportNumber) {
+          // 更新逻辑
+        } else {
+          // 添加逻辑
+        }
+        buttonLoading.value = false;
+        dialog.visible = false;
+        getList();
+      }, 500);
+    }
+  });
+};
+
+const handleDelete = (row) => {
+  if (row) {
+    // 删除逻辑
+    setTimeout(() => {
+      demoList.value = demoList.value.filter((item) => item.reportNumber !== row.reportNumber);
+      getList();
+    }, 500);
+  }
+};
+
+const handleExport = () => {
+  // 导出逻辑
+  console.log('导出数据');
+};
+
+// 处理文件上传数量限制
+const handleExceed = () => {
+  ElMessage.warning('最多上传5个文件');
+};
+
+
+onMounted(() => {
+  getList();
+  getDicts("mm_event_type").then(res => {
+    data.eventTypeSelection = res.data;
+  });
+});
+</script>