Jelajahi Sumber

公共数据源管理对接ing

threethousanddream 3 minggu lalu
induk
melakukan
a3d881c294

+ 1 - 1
package.json

@@ -26,7 +26,7 @@
     "echarts": "^5.4.2",
     "echarts-gl": "^2.0.9",
     "echarts-wordcloud": "^2.1.0",
-    "element-plus": "2.2.26",
+    "element-plus": "^2.7.7",
     "file-saver": "^2.0.5",
     "js-cookie": "3.0.1",
     "js-table2excel": "^1.0.3",

+ 22 - 0
src/api/ds.js

@@ -0,0 +1,22 @@
+import request from "@/utils/request";
+import config from "@/utils/config";
+const bigdataPrefix = config.bigdataPrefix
+
+export const dsList = params => request.get(bigdataPrefix + '/report/reportdatasource/publicDs', {params});
+export const dsSave = data => request.post(bigdataPrefix + '/report/reportdatasource/save', data);
+export const dsUpdate = data => request.post(bigdataPrefix + '/report/reportdatasource/update', data);
+export const dsDel = params => request.post(bigdataPrefix + '/report/reportdatasource/remove', {}, {params});
+export const dsTest = data => request.post(bigdataPrefix + '/report/reportdatasource/testDs', data);
+
+export const tableUpdate = data => request.post(bigdataPrefix + '/tablecategory/update/category-component', data)
+export const tableDel = data => request.post(bigdataPrefix + '/report/baseInfo/delTable', data)
+export const tableFields = params => request.get(bigdataPrefix + '/report/baseInfo/listColumnsByDatasource', {params});
+export const tablePreview = params => request.get(bigdataPrefix + '/cube/sqlview/data-browser', {params});
+export const getTable = params => request.get(bigdataPrefix + '/report/baseInfo/listViewsByDataSource', {params});
+export const clearCatch = data => request.post(bigdataPrefix + '/report/reportdatasource/refreshCache', data);
+export const batchImport = data => request.post(bigdataPrefix + '/cube/file/uploadAppend', data);
+
+export const categoryList = params => request.get(bigdataPrefix + '/tablecategory/list', {params});
+export const categoryDel = params => request.post(bigdataPrefix + '/tablecategory/remove', {}, {params});
+export const categorySave = data => request.post(bigdataPrefix + '/tablecategory/submit', data);
+export const categoryUpdate = data => request.post(bigdataPrefix + '/tablecategory/update', data);

+ 3 - 1
src/api/system.js

@@ -20,4 +20,6 @@ export const searchUserByName = params => request.get('/api/yx/iam/getUserByAcco
 export const searchAdmin = params => request.get('/api/yx/iam/manageGetByKeyName', {params})
 
 export const searchTenant = params => request.get('/api/yx/iam/tenantPage', {params})
-export const searchTenantByIds = data => request.post('/api/yx/iam/tenantNameList', data)
+export const searchTenantByIds = data => request.post('/api/yx/iam/tenantNameList', data)
+
+

src/views/temp/components/category.scss → src/assets/category.scss


+ 38 - 5
src/assets/myStyle.scss

@@ -43,7 +43,8 @@
     font-weight: 500;
     font-size: 14px;
   }
-  .items, .extra {
+  .items,
+  .extra {
     display: flex;
     align-items: center;
     background-color: #fff;
@@ -96,11 +97,13 @@
   background-color: #fff;
   border-color: #eb5a10;
 }
-.el-button:hover, .el-button:focus {
+.el-button:hover,
+.el-button:focus {
   background-color: transparent !important;
   border-color: #eb5a10 !important;
 }
-.el-button--primary:hover, .el-button--primary:focus {
+.el-button--primary:hover,
+.el-button--primary:focus {
   background-color: #eb5a10 !important;
   border-color: #eb5a10 !important;
 }
@@ -132,7 +135,8 @@
 .layout-backtop {
   flex: 1;
 }
-#chart-bar1, #chart-bar2 {
+#chart-bar1,
+#chart-bar2 {
   overflow: hidden;
 }
 .btn-split {
@@ -176,4 +180,33 @@
     font-size: 12px;
     color: #777;
   }
-}
+}
+.el-popover.my-popper {
+  margin-top: 3px;
+  padding: 0;
+  min-width: unset !important;
+  .popper__arrow {
+    display: none;
+  }
+  .my-popper-item {
+    height: 32px;
+    line-height: 32px;
+    font-size: 12px;
+    padding: 0 10px;
+    color: #444;
+    background-color: #fff;
+    cursor: pointer;
+    &:hover {
+      color: #EB5A10;
+      background-color: #fef5f1;
+    }
+    .icon {
+      margin-right: 5px;
+    }
+  }
+  &.center {
+    .my-popper-item {
+      text-align: center;
+    }
+  }
+}

+ 63 - 0
src/stores/modules/system.js

@@ -0,0 +1,63 @@
+import * as dsApi from "@/api/ds";
+import {deepCopy} from "@/utils";
+
+const useSystemStore = defineStore('system', () => {
+  const state = reactive({
+    tableCategoryList: [],
+    tableCategoryTree: [],
+    tableCategoryParents: [],
+  })
+  const getTableCategory = (dsId) => {
+    return new Promise((resolve, reject) => {
+      dsApi.categoryList({datasourceId: dsId}).then(res => {
+        // 设置平铺列表
+        state.tableCategoryList = res.data || []
+        // 设置分类树
+        let parents = state.tableCategoryList.filter(i => !i.parentId).sort((b, a) => b.orderIndex - a.orderIndex)
+        parents.forEach(p => {
+          let children = state.tableCategoryList.filter(i => i.parentId === p.id)
+          if (children) {
+            p.children = children.sort((b, a) => b.orderIndex - a.orderIndex)
+            children.forEach(pp => {
+              let children2 = state.tableCategoryList.filter(i => i.parentId === pp.id)
+              if (children2) pp.children = children2.sort((b, a) => b.orderIndex - a.orderIndex)
+            })
+          }
+        })
+        state.tableCategoryTree = parents
+        // 设置父级分类树
+        let categoryList = deepCopy(state.tableCategoryList)
+        categoryList.forEach(i => delete i.children)
+        let parents2 = categoryList.filter(i => !i.parentId).sort((b, a) => b.orderIndex - a.orderIndex)
+        parents2.forEach(p => {
+          let children = categoryList.filter(i => i.parentId === p.id)
+          if (children) p.children = children.sort((b, a) => b.orderIndex - a.orderIndex)
+        })
+        parents2.unshift({categoryName: '顶级分类', id: 0})
+        state.tableCategoryParents = parents2
+        resolve()
+      })
+    })
+  }
+  const getTableCategorySearched = key => {
+    let categoryTree2 = deepCopy(state.tableCategoryTree)
+    let res = [
+      {categoryName: '全部分类', id: 0, parentId: 0},
+    ]
+    if (!key) {
+      return [...res, ...categoryTree2]
+    } else {
+      let tableCategoryList = deepCopy(state.tableCategoryList)
+      let searched = tableCategoryList.filter(i => i.categoryName.includes(key))
+      searched.forEach(i => delete i.children)
+      return [...res, ...searched]
+    }
+  }
+  return {
+    state,
+    getTableCategory,
+    getTableCategorySearched,
+  }
+})
+
+export default useSystemStore

+ 1 - 2
src/views/backup/list.vue

@@ -57,8 +57,7 @@
         </el-table-column>
         <el-table-column label="操作" width="120px">
           <template v-slot="scope">
-            <el-button size="small" type="text" @click="downloadBackups(scope.row)"
-                       v-auth="'1823265230537535511'">
+            <el-button type="text" @click="downloadBackups(scope.row)" v-auth="'1823265230537535511'">
               下载
             </el-button>
           </template>

+ 167 - 0
src/views/system/components/TableCategory.vue

@@ -0,0 +1,167 @@
+<template>
+  <div class="menu-wrapper">
+    <div class="menu-btns">
+      <el-button type="primary" class="ghost" @click="state.showCategory=true" size="small">
+        <el-icon>
+          <EditPen/>
+        </el-icon>
+        编辑分类
+      </el-button>
+      <el-button type="primary" @click="onEditCategory(null)" size="small">
+        <el-icon>
+          <DocumentAdd/>
+        </el-icon>
+        添加分类
+      </el-button>
+    </div>
+    <div class="input-box">
+      <el-input v-model="state.searchKey" placeholder="搜索分类" size="default" clearable>
+        <template #append>
+          <el-icon @click="onCategorySearch">
+            <search/>
+          </el-icon>
+        </template>
+      </el-input>
+    </div>
+    <el-tree
+      :data="state.categoryTreeSearch" :props="{label:'categoryName',children:'children',}"
+      :expand-on-click-node="false" :current-node-key="state.nowCategory"
+      @node-click="handleNodeClick" node-key="id" highlight-current default-expand-all ref="tree"
+    >
+      <template #default="{ node, data }">
+        <span style="display: flex;align-items: center">
+          <img v-if="!data.id" src="/icon/all-category.svg" alt="" style="margin-right: 3px;">
+          <span>{{ node.label }}</span>
+        </span>
+      </template>
+    </el-tree>
+  </div>
+  <el-dialog v-model="state.showCategoryEdit" append-to-body width="400px" title="分类编辑">
+    <el-form label-width="70px" size="default">
+      <el-form-item label="分类名称">
+        <el-input v-model="state.categoryForm.categoryName" placeholder="填写分类名称"></el-input>
+      </el-form-item>
+      <el-form-item label="分类排序">
+        <el-input-number v-model="state.categoryForm.orderIndex" :min="0"></el-input-number>
+      </el-form-item>
+      <el-form-item label="父级分类">
+        <el-cascader
+          v-model="state.categoryForm.parentId" :options="categoryParents"
+          :props="{emitPath:false,value:'id',label:'categoryName',checkStrictly:true}" style="width:100%"
+        ></el-cascader>
+      </el-form-item>
+    </el-form>
+    <div class="dialog-btns">
+      <el-button @click="state.showCategoryEdit=false" size="default">取消</el-button>
+      <el-button type="primary" @click="onEditCategoryConfirm" size="default" :loading="state.saveLoading">确定
+      </el-button>
+    </div>
+  </el-dialog>
+  <el-dialog v-model="state.showCategory" append-to-body width="500px" title="分类列表">
+    <el-table
+      :data="categoryTree" default-expand-all row-key="id" max-height="700px"
+      size="default" :header-row-style="{color:'#161616'}"
+    >
+      <el-table-column label="分类名称" prop="categoryName"></el-table-column>
+      <el-table-column label="分类排序" prop="orderIndex" width="80px" align="center"></el-table-column>
+      <el-table-column label="操作" width="160px" align="center">
+        <template v-slot="{row}">
+          <el-button type="text" @click="onEditCategory(row)">编辑</el-button>
+          <el-button type="text" @click="onDelCategory(row)">删除</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+  </el-dialog>
+</template>
+<script setup name="TableCategory">
+import useSystemStore from '@/stores/modules/system'
+import {ElMessage, ElMessageBox} from "element-plus";
+import * as api from "@/api/ds";
+const systemStore = useSystemStore()
+const emit = defineEmits(['change'])
+const tree = ref()
+const props = defineProps({
+  dsId: [Number, String]
+})
+const state = reactive({
+  categoryTreeSearch: [],
+  searchKey: '',
+  nowCategory: 0,
+  categoryForm: {},
+  showCategoryEdit: false,
+  showCategory: false,
+  saveLoading: false,
+})
+const categoryTree = computed(() => systemStore.state.tableCategoryTree)
+const categoryList = computed(() => systemStore.state.tableCategoryList)
+const categoryParents = computed(() => systemStore.state.tableCategoryParents)
+
+onMounted(() => {
+  getCategory()
+})
+// 删除分类
+const onDelCategory = cate => {
+  ElMessageBox.confirm(
+    `确定删除该数据表分类吗`,
+    '提示',
+    {confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning',}
+  ).then(() => {
+    api.categoryDel({ids: cate.id}).then(res => {
+      ElMessage({type: 'success', message: '已删除'})
+      getCategory()
+    })
+  })
+}
+// 提交分类编辑信息
+const onEditCategoryConfirm = () => {
+  if (state.saveLoading) return
+  if (!state.categoryForm.categoryName) {
+    return ElMessage({type: 'warning', message: '请输入分类名称'})
+  }
+  if (isNaN(state.categoryForm.orderIndex)) {
+    return ElMessage({type: 'warning', message: '请输入分类排序'})
+  }
+  state.saveLoading = true
+  let task = state.categoryForm.id ? api.categoryUpdate({...state.categoryForm}) : api.categorySave({...state.categoryForm})
+  task.then(res => {
+    ElMessage({type: 'success', message: '已保存'})
+    state.showCategoryEdit = false
+    getCategory()
+  }).finally(() => state.saveLoading = false)
+}
+// 新增或编辑分类
+const onEditCategory = cate => {
+  if (cate) {
+    state.categoryForm = {...cate}
+  } else {
+    state.categoryForm = {categoryName: '', orderIndex: 0, parentId: 0, datasourceId: props.dsId}
+  }
+  state.showCategoryEdit = true
+}
+// 获取分类数据
+const getCategory = () => new Promise((resolve, reject) => {
+  systemStore.getTableCategory(props.dsId).then(() => {
+    if (state.nowCategory !== 0) {
+      state.nowCategory = 0
+      emit('change', 0)
+    }
+    onCategorySearch()
+    resolve()
+  })
+})
+// 筛选分类
+const onCategorySearch = () => {
+  state.categoryTreeSearch = systemStore.getTableCategorySearched(state.searchKey)
+}
+// 点击分类时触发选择事件
+const handleNodeClick = node => {
+  state.nowCategory = node.id
+  emit('change', node.id)
+}
+defineExpose({
+  categoryId: () => state.nowCategory,
+})
+</script>
+<style lang="scss">
+@import "../../../assets/category";
+</style>

+ 299 - 0
src/views/system/components/UploadBox.vue

@@ -0,0 +1,299 @@
+<template>
+  <el-dialog
+    title="导入数据" :close-on-click-modal="false" v-model="visible" lock-scroll width="1000px"
+    class="JNPF-dialog JNPF-dialog_center JNPF-dialog-export"
+  >
+    <el-steps :space="330" :active="active" align-center finish-status="success" class="upload-step">
+      <el-step title="导入Excel文件"></el-step>
+      <el-step title="字段关联"></el-step>
+      <el-step title="完成导入"></el-step>
+    </el-steps>
+    <el-form label-position="top" style="margin-top: 20px" size="default">
+      <el-form-item v-show="active === 0">
+        <el-upload
+          class="upload-demo" ref="uploadFile" drag :on-success="onSuccess"
+          :before-upload="beforeUploadChange" accept=".xlsx,.xls" :action="baseUrl + '/api/file/uploadFile'"
+          :headers="{ Authorization: Authorization }"
+        >
+          <el-icon>
+            <UploadFilled/>
+          </el-icon>
+          <div class="el-upload__text"> 将文件拖到此处,或<em style="margin-left: 3px;">点击上传</em></div>
+          <div class="el-upload__tip" slot="tip">只能上传xlsx/xls文件</div>
+          <div v-if="uploadFlag" class="el-upload__tip" slot="tip">正在上传中,请稍后...</div>
+        </el-upload>
+      </el-form-item>
+      <el-form-item v-show="active === 1">
+        <el-table :data="fileList" height="500" size="default">
+          <el-table-column v-for="(item, index) in fileListCols" :key="index" :label="item" minWidth="120">
+            <template v-slot="scope">{{ scope.row[item] }}</template>
+          </el-table-column>
+        </el-table>
+      </el-form-item>
+      <el-form-item v-show="active === 2">
+        <el-tag type="info">文件名称:{{ fileName }}</el-tag>
+        <div style="display: flex; justify-content: space-between; margin: 10px 0">
+          <div>
+            是否导入表头:
+            <el-switch v-model="withHeader" :active-value="false" :inactive-value="true"></el-switch>
+          </div>
+          <div>
+            是否清空原有数据:
+            <el-switch v-model="coverData" :active-value="1" :inactive-value="0"></el-switch>
+          </div>
+        </div>
+        <el-table :data="fileList" border highlight-current-row height="500" size="default">
+          <el-table-column width="80">
+            <template v-slot="scope">
+              <div v-if="scope.$index === 0">字段绑定</div>
+              <div v-else>{{ scope.$index }}</div>
+            </template>
+          </el-table-column>
+          <el-table-column v-for="(item, index) in fileListCols" :key="index" :label="item" minWidth="120">
+            <template v-slot="scope">
+              <el-form-item v-if="scope.$index === 0">
+                <el-select
+                  v-model="associationLists[index].fieldId" placeholder="请选择关联字段" filterable clearable
+                  :popper-append-to-body="false" popper-class="upload-select"
+                >
+                  <el-option
+                    v-for="(item, k) in columnList" :key="k" :title="item.columnComments + ' ' + item.columnName"
+                    :disabled="getDisabledSelect(item.columnName)" :value="item.columnName"
+                    :label="item.columnComments ? item.columnComments + '  ' + item.columnName : item.columnName"
+                  ></el-option>
+                </el-select>
+              </el-form-item>
+              <span v-else>{{ scope.row[item] }}</span>
+            </template>
+          </el-table-column>
+        </el-table>
+      </el-form-item>
+    </el-form>
+    <div class="dialog-btns">
+      <el-button size="small" @click="closeClick">取 消</el-button>
+      <el-button size="small" v-if="active === 1" @click="removeClick()">上一步</el-button>
+      <el-button size="small" v-if="active === 1" @click="active = 2;fileList.unshift({});">
+        下一步
+      </el-button>
+      <el-button size="small" type="primary" :loading="btnLoading" :disabled="active !== 2" @click="downLoad">
+        导 入
+      </el-button>
+    </div>
+  </el-dialog>
+</template>
+<script>
+import {getToken} from '@/utils/auth';
+import {read, utils} from 'xlsx';
+import * as report from '@/api/ds';
+
+export default {
+  data() {
+    return {
+      visible: false,
+      active: 0,
+      btnLoading: false,
+      type: 0,
+      columns: [],
+      checkAll: true,
+      isIndeterminate: false,
+      columnList: [],
+      columnLists: [],
+      Authorization: 'Bearer ' + getToken(),
+      files: [],
+      fileList: [],
+      fileListCols: [],
+      fileName: '',
+      listValue: '',
+      associationLists: [],
+      coverData: 0,
+      withHeader: true,
+      fileContname: '',
+      tableViewName: '',
+      datasourceId: '',
+      uploadFlag: false,
+      baseUrl: window._CONFIG.APIHead,
+    };
+  },
+  methods: {
+    init(e) {
+      this.visible = true;
+      if (!e) return;
+      this.getCubeFieldList(e);
+      this.tableViewName = e.tableViewName;
+      this.datasourceId = e.datasourceId;
+    },
+    getCubeFieldList(e) {
+      report.tableFields(e).then(res => {
+        this.columnList = res.data.filter(p => {
+          return p.primaryKeyStatus !== 1;
+        });
+      });
+    },
+    getDisabledSelect(val) {
+      let res = false;
+      this.associationLists.filter((i, k) => {
+        if (i.fieldId && i.fieldId === val) {
+          res = true;
+        }
+      });
+      return res;
+    },
+    beforeUploadChange(file) {
+      this.files = {0: file};
+      const fileType = file.name.substring(file.name.lastIndexOf('.') + 1);
+      const extension = fileType === '';
+      const extension2 = fileType === 'xlsx';
+      const isLt2M = file.size / 1024 / 1024 < 100;
+      if (!extension && !extension2) {
+        this.$message.error('上传文件只能是 xls或xlsx 格式!');
+        return false;
+      }
+      if (!isLt2M) {
+        this.$message.error('上传文件大小不能超过 100MB!');
+      }
+      this.uploadFlag = true;
+      return (extension || extension2) && isLt2M;
+    },
+    onSuccess(res) {
+      if (res.code === 200) {
+        this.uploadFlag = true;
+        this.fileName = res.msg;
+        const fileReader = new FileReader();
+        fileReader.onload = ev => {
+          try {
+            const data = ev.target.result;
+            const workbook = read(data, {type: 'binary', cellDates: true, dateNF: 'yyyy-mm-dd'});
+            //取第一张表
+            const wsname = workbook.SheetNames[0];
+            const wss = workbook.Sheets[wsname];
+            //生成json表格
+            const ws = utils.sheet_to_row_object_array(workbook.Sheets[wsname], {
+              raw: false,
+            });
+            //截取前六位展示預覽
+            this.fileList = ws.slice(0, 6);
+            // this.fileList.unshift({})
+            //获取表头
+            this.fileListCols = this.getHeaderRow(wss);
+            this.associationLists = this.fileListCols.map((i, k) => {
+              return {fieldName: i, fieldId: null, fieldKey: k};
+            });
+            this.$nextTick(() => {
+              //如果字段和表头相同可以直接绑定
+              this.associationLists.forEach((i, k) => {
+                this.columnList.forEach((v, c) => {
+                  if (i.fieldName === v.columnComments) {
+                    i.fieldId = v.columnName;
+                  }
+                });
+              });
+              this.active = 1;
+            });
+          } catch (e) {
+            return false;
+          }
+        };
+        fileReader.readAsBinaryString(this.files[0]);
+      }
+    },
+    getHeaderRow(sheet) {
+      const headers = [];
+      /* sheet['!ref']表示所有单元格的范围,例如从A1到F8则记录为 A1:F8*/
+      const range = utils.decode_range(sheet['!ref']);
+      let C,
+        R = range.s.r; /* 从第一行开始 */
+      /* 按列进行数据遍历 */
+      for (C = range.s.c; C <= range.e.c; ++C) {
+        /* 查找第一行中的单元格 */
+        const cell = sheet[utils.encode_cell({c: C, r: R})];
+        let hdr = 'UNKNOWN ' + C; // <-- 进行默认值设置
+        if (cell && cell.t) hdr = utils.format_cell(cell);
+        headers.push(hdr);
+      }
+      return headers;
+    },
+    //取消
+    closeClick() {
+      if (this.active > 0) {
+        this.$confirm('此操作将删除已导入的文件, 是否继续?', '提示', {
+          confirmButtonText: '确定',
+          cancelButtonText: '取消',
+          type: 'warning',
+        }).then(() => {
+          this.fileList = [];
+          this.associationLists = [];
+          this.fileListCols = [];
+          this.uploadFlag = false;
+          this.$nextTick(() => {
+            this.$refs.uploadFile.clearFiles();
+            this.active = 0;
+            this.visible = false;
+          });
+        });
+      } else {
+        this.visible = false;
+      }
+    },
+    //返回上一步操作
+    removeClick() {
+      this.$confirm('此操作将删除已导入的文件, 是否继续?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning',
+      }).then(() => {
+        this.fileList = [];
+        this.associationLists = [];
+        this.fileListCols = [];
+        this.uploadFlag = false;
+        this.$nextTick(() => {
+          this.$refs.uploadFile.clearFiles();
+          this.active = 0;
+        });
+      });
+    },
+    downLoad() {
+      this.btnLoading = true;
+      let data = this.associationLists.map((o, i) => {
+        if (o.fieldId) {
+          return {columnName: o.fieldId, index: o.fieldKey};
+        }
+      }).filter(l => l !== undefined);
+      this.$emit('UploadChange', {
+        file: this.fileName,
+        list: data,
+        isTruncate: this.coverData,
+        tableName: this.tableViewName,
+        dsId: this.datasourceId,
+        withHeader: this.withHeader,
+      });
+    },
+  },
+};
+</script>
+<style lang="scss">
+.upload-step {
+  .el-step__icon-inner {
+    font-size: 16px !important;
+  }
+}
+.upload-demo {
+  width: 100%;
+  .el-upload {
+    display: flex;
+    .el-upload-dragger {
+      width: 360px !important;
+      height: 180px !important;
+      .el-icon {
+        font-size: 60px;
+        color: #c0c4cc;
+      }
+    }
+  }
+}
+.upload-select .el-select-dropdown__item {
+  max-width: 300px;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+</style>

+ 382 - 0
src/views/system/ds.vue

@@ -0,0 +1,382 @@
+<template>
+  <div class="page page-ds">
+    <div class="my-top">
+      <el-form inline size="default">
+        <el-form-item label="数据类型">
+          <el-select size="default" v-model="type" style="width: 120px" @change="changeType">
+            <el-option :value="-1" label="全部类型"></el-option>
+            <el-option :value="0" label="mysql"></el-option>
+            <el-option :value="1" label="oracle"></el-option>
+            <el-option :value="2" label="sqlserver"></el-option>
+            <el-option :value="3" label="postgresql"></el-option>
+            <el-option :value="4" label="clickhouse"></el-option>
+            <el-option :value="5" label="kingbase"></el-option>
+            <el-option :value="6" label="dameng"></el-option>
+            <el-option :value="9" label="sybase"></el-option>
+            <el-option :value="10" label="mongodb"></el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item label="数据库名称">
+          <el-input size="default" v-model="name" clearable placeholder="输入数据库名称"
+                    style="width: 160px"></el-input>
+        </el-form-item>
+      </el-form>
+      <el-button size="default" @click="getData" :loading="loading">查询</el-button>
+      <el-button size="default" @click="onReset" :loading="loading">重置</el-button>
+      <div style="flex: 1"></div>
+      <el-button size="default" type="primary" @click="onDsAdd">
+        <el-icon>
+          <Plus/>
+        </el-icon>
+        新增数据源
+      </el-button>
+    </div>
+    <div style="height: calc(100% - 95px);overflow-y: auto">
+      <el-table :data="list" height="100%" size="default" v-loading="loading">
+        <el-table-column label="数据库名" prop="dsDb"></el-table-column>
+        <el-table-column label="数据库类型">
+          <template v-slot="{ row }">{{ typeMap[row.dsType] }}</template>
+        </el-table-column>
+        <el-table-column label="IP地址" prop="dsIp"></el-table-column>
+        <el-table-column label="端口" prop="dsPort"></el-table-column>
+        <el-table-column label="连接用户" prop="dsUser"></el-table-column>
+        <el-table-column label="描述" prop="remark">
+          <template v-slot="{ row }">
+            <div :title="row.remark">{{ row.remark || '-' }}</div>
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" width="170px">
+          <template v-slot="{ row }">
+            <el-button type="text" @click="onDsEdit(row)">编辑</el-button>
+            <span class="btn-split">|</span>
+            <el-button type="text" @click="onGoDetail(row)">查看</el-button>
+            <span class="btn-split">|</span>
+            <el-button type="text" @click="onDsDel(row)">删除</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+    </div>
+    <el-pagination
+      :current-page.sync="page.current" :page-size="page.size" :page-sizes="[10, 20, 30, 40]" :total="page.total"
+      background class="my-page" layout="total, sizes, prev, pager, next, jumper" @size-change="sizeChange"
+      @current-change="currentChange" style="padding-bottom: 0;"
+    ></el-pagination>
+    <el-dialog
+      v-model="showEdit" append-to-body :title="`${editType === 'add' ? '新增' : '编辑'}数据源`" width="600px"
+    >
+      <el-form ref="form" :model="dsForm" :rules="rules" label-width="115px" size="default">
+        <el-form-item label="数据源类型">
+          <el-select v-model="dsForm.dsType" @change="changeType">
+            <el-option :value="0" label="mysql"></el-option>
+            <el-option :value="1" label="oracle"></el-option>
+            <el-option :value="2" label="sqlserver"></el-option>
+            <el-option :value="3" label="postgresql"></el-option>
+            <el-option :value="4" label="clickhouse"></el-option>
+            <el-option :value="5" label="kingbase"></el-option>
+            <el-option :value="6" label="dameng"></el-option>
+            <el-option :value="9" label="sybase"></el-option>
+            <el-option :value="10" label="mongodb"></el-option>
+          </el-select>
+          <el-tooltip
+            class="item" effect="dark" placement="right"
+            content="某些版本的oracle数据库,用户名需要大写才可查询,请自行检查"
+          >
+            <el-button style="margin-left: 10px; color: #999" type="text">
+              <el-icon>
+                <QuestionFilled/>
+              </el-icon>
+            </el-button>
+          </el-tooltip>
+        </el-form-item>
+        <template v-if="[1,2,3,5,6,8,9,10].includes(dsForm.dsType)">
+          <el-form-item label="模式(schema)" prop="dbSchema">
+            <el-input v-model="dsForm.dbSchema" clearable :style="{width:dsForm.dsType===10?'405px':'100%'}"></el-input>
+            <el-tooltip
+              v-if="dsForm.dsType===10" class="item" effect="dark" placement="right"
+              content="使用authSource属性值(指定应验证所提供档案的数据库)"
+            >
+              <el-button type="text" style="margin-left: 10px; color: #999">
+                <el-icon>
+                  <QuestionFilled/>
+                </el-icon>
+              </el-button>
+            </el-tooltip>
+          </el-form-item>
+        </template>
+        <el-form-item label="数据库名" prop="dsDb">
+          <el-input placeholder="请输入数据库名" v-model="dsForm.dsDb" clearable maxlength="100" show-word-limit
+          ></el-input>
+        </el-form-item>
+        <el-form-item label="连接名" prop="dsName">
+          <el-input placeholder="请输入连接名" v-model="dsForm.dsName" clearable maxlength="100" show-word-limit
+          ></el-input>
+        </el-form-item>
+        <el-form-item label="IP地址" prop="dsIp">
+          <el-input placeholder="请输入IP地址" v-model="dsForm.dsIp" clearable maxlength="100" show-word-limit
+          ></el-input>
+        </el-form-item>
+        <el-form-item label="端口" prop="dsPort">
+          <el-input
+            placeholder="请输入端口" v-model="dsForm.dsPort" clearable maxlength="5" show-word-limit type="number"
+            :min="1"
+          ></el-input>
+        </el-form-item>
+        <el-form-item label="连接用户" prop="dsUser" maxlength="100" show-word-limit>
+          <el-input
+            placeholder="请输入连接用户" v-model="dsForm.dsUser" autocomplete="new-password" clearable
+            maxlength="100" show-word-limit
+          ></el-input>
+        </el-form-item>
+        <el-form-item prop="dsPassword">
+          <template #label>
+            <span v-if="editType==='add'" style="color: #F56C6C;margin-right: 4px;">*</span>
+            连接密码
+          </template>
+          <el-input
+            :placeholder="editType==='add'?'请输入连接密码':'******'" v-model="dsForm.dsPassword" clearable
+            autocomplete="new-password" :show-password="editType === 'add'" maxlength="100" show-word-limit
+          ></el-input>
+        </el-form-item>
+        <el-form-item label="描述">
+          <el-input
+            placeholder="请输入描述" v-model="dsForm.remark" :rows="3" type="textarea" clearable
+            maxlength="100" show-word-limit
+          ></el-input>
+        </el-form-item>
+      </el-form>
+      <div style="margin-top: 30px; display: flex; justify-content: flex-end">
+        <el-button @click="showEdit = false">取消</el-button>
+        <el-button type="primary" @click="onDsEditConfirm" :loading="loadingEdit"> 确定</el-button>
+        <el-button type="primary" @click="onDsTest" :loading="loadingEdit">测试连接</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+<script>
+import {
+  dsList,
+  dsSave,
+  dsTest,
+  dsDel,
+  dsUpdate,
+} from '@/api/ds';
+import {Plus, QuestionFilled} from "@element-plus/icons-vue";
+import {ElMessage, ElMessageBox} from "element-plus";
+let editType = 'add'
+export default {
+  data() {
+    return {
+      page: {size: 20, current: 1, total: 0},
+      name: '',
+      type: -1,
+      list: [],
+      showEdit: false,
+      dsForm: {dbSchema: '',},
+      editType: 'add',
+      typeMap: {
+        0: 'mysql',
+        1: 'oracle',
+        2: 'sqlserver',
+        3: 'postgresql',
+        4: 'clickhouse',
+        5: 'kingbase',
+        6: 'dameng',
+        9: 'sybase',
+        10: 'mongodb',
+      },
+      rules: {
+        dsDb: [
+          {required: true, message: '请输入数据库名', trigger: 'blur'},
+          {
+            validator(rule, value, callback) {
+              if (value.trim() || !value) {
+                callback()
+              } else {
+                callback(new Error('请输入有效内容'));
+              }
+            }, trigger: 'change'
+          }
+        ],
+        dsName: [
+          {
+            validator(rule, value, callback) {
+              if (value.trim() || !value) {
+                callback()
+              } else {
+                callback(new Error('请输入有效内容'));
+              }
+            }, trigger: 'change'
+          }
+        ],
+        dsIp: [
+          {required: true, message: '请输入IP地址', trigger: 'blur'},
+          {
+            validator(rule, value, callback) {
+              if (/^[0123456789.]+$/g.test(value)) {
+                callback()
+              } else {
+                if ((value + '').length) {
+                  callback(new Error('只能输入数字和点'));
+                }
+              }
+            }, trigger: 'change'
+          }
+        ],
+        dbSchema: [{required: true, message: '请输入模式(schema)', trigger: 'blur'}],
+        dsPort: [{required: true, message: '请输入端口号', trigger: 'blur'}],
+        dsUser: [{required: true, message: '请输入连接用户', trigger: 'blur'}],
+        dsPassword: [
+          {
+            validator(rule, value, callback) {
+              if (editType === 'add') {
+                if (!value) {
+                  callback(new Error('请输入连接密码'));
+                } else {
+                  callback();
+                }
+              } else {
+                callback();
+              }
+            }, trigger: 'blur'
+          }
+        ],
+      },
+      loading: false,
+      loadingEdit: false,
+    };
+  },
+  created() {
+    this.getData();
+  },
+  methods: {
+    getChildren(arr, nodes) {
+      if (!nodes) return;
+      arr.push(...nodes);
+      nodes.forEach(item => {
+        this.getChildren(arr, item.children);
+      });
+    },
+    onDsTest() {
+      this.$refs.form.validate(valid => {
+        if (!valid) return
+        let data = {...this.dsForm};
+        if (data.driverClassName) delete data.driverClassName;
+        if (data.url) delete data.url;
+        this.loadingEdit = true
+        dsTest(data).then(res => {
+          if (res.code == 200) {
+            ElMessage({message: '连接成功', type: 'success'});
+          } else {
+            ElMessage.error('连接错误');
+          }
+        }).finally(() => this.loadingEdit = false);
+      })
+    },
+    onDsDel(ds) {
+      ElMessageBox.confirm('确定删除此数据源?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning',
+      }).then(() => {
+        dsDel({ids: ds.id}).then(res => {
+          ElMessage({type: 'success', message: '已删除'});
+          this.getData();
+        });
+      });
+    },
+    // 确认编辑数据源
+    onDsEditConfirm() {
+      this.$refs.form.validate(valid => {
+        if (!valid) return;
+        let data = {...this.dsForm};
+        this.loadingEdit = true
+        if (this.editType == 'add') {
+          dsSave(data).then(res => {
+            ElMessage({type: 'success', message: '已提交'});
+            this.showEdit = false;
+            this.getData();
+          }).finally(() => this.loadingEdit = false);
+        } else if (this.editType == 'edit') {
+          dsUpdate(data).then(res => {
+            ElMessage({type: 'success', message: '已提交'});
+            this.showEdit = false;
+            this.getData();
+          }).finally(() => this.loadingEdit = false);
+        }
+      });
+    },
+    onGoDetail(ds) {
+      this.$router.push({path: '/system/ds/detail', query: {id: ds.id, type: ds.dsType}});
+    },
+    // 编辑数据源
+    onDsEdit(ds) {
+      this.editType = 'edit';
+      editType = 'edit'
+      this.dsForm = {...ds};
+      this.showEdit = true;
+    },
+    // 添加数据源
+    onDsAdd() {
+      this.editType = 'add';
+      editType = 'add';
+      this.dsForm = {
+        dsDb: '', // 数据库名
+        dsName: '', // 数据库连接名
+        dbSchema: 'public', // 数据库schema
+        dsIp: '', // ip地址
+        dsPort: '', // 端口
+        dsUser: '', // 连接用户名
+        dsPassword: '', // 连接密码
+        dsType: 0, // 数据源类型 0-mysql 1-oracle 2-sqlserver 3-postgresql 4-clickhouse 5-kingbase 6-dameng
+        remark: '', // 描述
+        whetherPublic: 1 // 公共数据源
+      };
+      this.showEdit = true;
+      this.$nextTick(() => {
+        this.$refs.form.clearValidate();
+      });
+    },
+    changeType(e) {
+      if (e === 1) {
+        this.dsForm.dbSchema = '';
+      } else if (e === 2) {
+        this.dsForm.dbSchema = 'dbo';
+      } else if (e === 10) {
+        this.dsForm.dbSchema = 'admin';
+      } else {
+        this.dsForm.dbSchema = 'public';
+      }
+    },
+    getData() {
+      let {current, size} = this.page;
+      let data = {current, size};
+      if (this.name) data.dsName = this.name;
+      if (this.type !== -1) data.dsType = this.type;
+      this.loading = true
+      dsList(data).then(res => {
+        let {total, records} = res.data;
+        this.page.total = total;
+        this.list = records;
+      }).finally(() => {
+        this.loading = false
+      });
+    },
+    onReset() {
+      this.type = -1;
+      this.name = '';
+      this.page.current = 1;
+      this.getData();
+    },
+    sizeChange(size) {
+      this.page.size = size;
+      this.getData();
+    },
+    currentChange(val) {
+      this.page.current = val;
+      this.getData();
+    },
+  },
+};
+</script>
+<style lang="scss">
+</style>

+ 309 - 0
src/views/system/dsDetail.vue

@@ -0,0 +1,309 @@
+<template>
+  <div class="page page-ds-detail">
+    <TableCategory v-if="datasourceId" @change="onSelectCategory" :dsId="datasourceId" ref="category"
+    ></TableCategory>
+    <div class="main-wrapper">
+      <div class="my-top">
+        <el-form inline size="default">
+          <el-form-item label="表名称">
+            <el-input
+              v-model="tableKey" clearable placeholder="请输入完整表名称" size="default" style="width: 200px"
+            ></el-input>
+          </el-form-item>
+        </el-form>
+        <el-button size="default" @click="getTableList">查询</el-button>
+        <el-button size="default" @click="onReset">重置</el-button>
+        <div style="flex: 1"></div>
+        <el-button size="default" type="primary" @click="showUpload = true" v-if="dsType!==10">
+          <el-icon>
+            <FolderAdd/>
+          </el-icon>
+          文件上传
+        </el-button>
+        <el-button size="default" @click="clear">清理数据表缓存</el-button>
+        <el-button size="default" @click="$router.go(-1)">返回</el-button>
+      </div>
+      <div style="height: calc(100% - 95px);overflow-y: auto">
+        <el-table :data="tableList" size="default" height="100%">
+          <el-table-column label="表名" prop="tableViewName"></el-table-column>
+          <el-table-column label="备注" prop="tableComments">
+            <template v-slot="{ row }">{{ row.tableComments || '-' }}</template>
+          </el-table-column>
+          <el-table-column label="分类" prop="categoryName"></el-table-column>
+          <el-table-column label="操作" width="200px">
+            <template v-slot="{ row }">
+              <el-button size="default" type="text" @click="onTableEdit(row)"> 编辑</el-button>
+              <span class="btn-split">|</span>
+              <el-button size="default" type="text" @click="tableFieldPreview(row)"> 字段预览</el-button>
+              <span class="btn-split">|</span>
+              <el-popover placement="bottom-end" trigger="hover" popper-class="my-popper" width="75px">
+                <template #reference>
+                  <el-button type="text" size="default">更多</el-button>
+                </template>
+                <div class="my-popper-item" @click="tablePreview(row)">数据预览</div>
+                <div class="my-popper-item" @click="batchUpload(row)">批量导入</div>
+                <div class="my-popper-item" @click="onDel(row)">删除</div>
+              </el-popover>
+            </template>
+          </el-table-column>
+        </el-table>
+      </div>
+      <el-pagination
+        :current-page.sync="page.current" :page-size="page.size" :page-sizes="[10, 20, 50, 100]" :total="page.total"
+        background class="my-page" layout="total, sizes, prev, pager, next, jumper" style="padding-bottom: 0;"
+        @size-change="sizeChange" @current-change="currentChange"
+      ></el-pagination>
+    </div>
+    <el-dialog
+      v-model="showFieldPreview" append-to-body class="table-field-preview-dialog" title="字段预览" width="600px"
+    >
+      <el-table :data="tableFieldData" height="100%" size="default">
+        <el-table-column label="字段名称" prop="columnName"></el-table-column>
+        <el-table-column label="字段类型" prop="dataType"></el-table-column>
+        <el-table-column label="字段长度" prop="columnSize"></el-table-column>
+        <el-table-column label="字段注释" prop="columnComments"></el-table-column>
+      </el-table>
+    </el-dialog>
+    <el-dialog v-model="showEdit" append-to-body title="表信息编辑" width="400px">
+      <el-form label-width="80px" size="default">
+        <el-form-item label="表名">
+          <el-input v-model="nowTable.tableViewName" class="my-disabled" disabled></el-input>
+        </el-form-item>
+        <el-form-item label="备注">
+          <el-input v-model="nowTable.tableComments"></el-input>
+        </el-form-item>
+        <el-form-item label="分类" required>
+          <el-cascader
+            v-model="nowTable.categoryId" :options="systemStore.state.tableCategoryTree" style="width: 100%"
+            :props="{ emitPath: false, value: 'id', label: 'categoryName', checkStrictly: true }"
+          ></el-cascader>
+        </el-form-item>
+      </el-form>
+      <div class="dialog-btns">
+        <el-button size="default" type="primary" @click="onEditConfirm">确定</el-button>
+        <el-button size="default" @click="showEdit = false">取消</el-button>
+      </div>
+    </el-dialog>
+    <el-dialog
+      :title="`表【${nowTable.tableViewName}】数据预览`" v-model="showDataPreview" append-to-body
+      class="table-data-preview-dialog" width="80%"
+    >
+      <el-table :data="tableDataData" border height="100%" size="default" stripe>
+        <el-table-column v-for="(item, i) of tableDataColumn" :key="i" :label="item" :prop="item"></el-table-column>
+      </el-table>
+    </el-dialog>
+    <UploadBox v-if="uploadBoxVisible" ref="UploadBox" @UploadChange="UploadChange"/>
+    <!-- 文件上传抽屉 -->
+    <!--    <dataset-file-upload-->
+    <!--      ref="upload"-->
+    <!--      v-model="showUpload"-->
+    <!--      :datasource-id="datasourceId"-->
+    <!--      direction="btt"-->
+    <!--      size="900px"-->
+    <!--      @closeDrawer="closeDrawer"-->
+    <!--      :category-id="nowCategoryId"-->
+    <!--      :showCategory="true"-->
+    <!--    ></dataset-file-upload>-->
+  </div>
+</template>
+<script>
+import * as report from '@/api/ds';
+import {ElMessage, ElMessageBox, ElLoading} from "element-plus";
+import TableCategory from './components/TableCategory.vue';
+import {Plus, QuestionFilled} from "@element-plus/icons-vue";
+import useSystemStore from '@/stores/modules/system'
+import UploadBox from './components/UploadBox.vue';
+// import DatasetFileUpload from '@/views/newbi/dataset/comps/datasetFileUpload.vue';
+
+export default {
+  // components: {DatasetFileUpload, UploadBox, TableCategory},
+  components: {TableCategory, UploadBox},
+  data() {
+    return {
+      datasourceId: 0,
+      dsType: 0,
+      categoryKey: '',
+      nowCategoryId: 0,
+      tableKey: '',
+      tableList: [],
+      page: {current: 1, size: 20, total: 0},
+      showFieldPreview: false,
+      tableFieldData: [],
+      showRelation: false,
+      table: {},
+      showEdit: false,
+      nowTable: {},
+      showDataPreview: false,
+      tableDataColumn: [],
+      tableDataData: [],
+      uploadBoxVisible: false,
+      showUpload: false,
+    };
+  },
+  computed: {
+    ...mapStores(useSystemStore),
+  },
+  created() {
+    this.datasourceId = this.$route.query.id;
+    this.dsType = parseInt(this.$route.query.type);
+    this.getTableList();
+  },
+  activated() {
+    this.datasourceId = this.$route.query.id;
+    this.getTableList();
+  },
+  methods: {
+    onDel(table) {
+      ElMessageBox.confirm('确定删除此表?请确保该数据表不在使用中', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning',
+      }).then(() => {
+        report.tableDel({dsId: table.datasourceId, tableName: table.tableViewName}).then(res => {
+          ElMessage({type: 'success', message: '已删除'})
+          this.getTableList()
+        })
+      }).catch(() => {
+      })
+    },
+    onReset() {
+      this.tableKey = '';
+      this.nowCategoryId = 0;
+      this.getTableList();
+    },
+    tablePreview(table) {
+      report.tableFields(table).then(res => {
+        this.tableFieldData = res.data;
+        this.nowTable = table;
+        this.showDataPreview = true;
+        this.getTableData();
+      });
+    },
+    getTableData() {
+      let data = {current: 1, size: 50, tableName: this.nowTable.tableViewName, datasourceId: this.datasourceId};
+      report.tablePreview(data).then(res => {
+        let {records, total} = res.data;
+        this.tableDataColumn = records.length ? Object.keys(records[0]) : [];
+        this.tableDataData = records;
+      });
+    },
+    onTableEdit(table) {
+      this.nowTable = {...table};
+      this.showEdit = true;
+    },
+    onEditConfirm() {
+      let data = {...this.nowTable};
+      if (!data.categoryId) return ElMessage({type: 'error', message: '请选择分类'})
+      delete data.categoryName;
+      report.tableUpdate(data).then(res => {
+        ElMessage.success('已保存');
+        this.getTableList();
+        this.showEdit = false;
+      });
+    },
+    // 字段预览
+    tableFieldPreview(e) {
+      report.tableFields(e).then(res => {
+        this.tableFieldData = res.data;
+        this.showFieldPreview = true;
+      });
+    },
+    // 表头中文获取
+    tableSetColumn(row) {
+      const find = this.tableFieldData.find(v => v.columnName === row);
+      return find?.columnComments ? find?.columnComments + ` (${row})` : row;
+    },
+    // 获取数据表列表
+    getTableList() {
+      let {current, size} = this.page;
+      let data = {datasourceId: this.datasourceId, current, size};
+      if (!data.datasourceId) return;
+      if (this.tableKey) data.tableName = this.tableKey;
+      if (this.nowCategoryId) data.classifyId = this.nowCategoryId;
+      report.getTable(data).then(res => {
+        let {records, total} = res.data;
+        let categoryList = this.systemStore.state.tableCategoryList
+        records.forEach(item => {
+          let now = categoryList.find(i => i.id == item.categoryId);
+          if (now) item.categoryName = now.categoryName;
+          else item.categoryName = '-';
+        });
+        this.page.total = total;
+        this.tableList = records;
+      });
+    },
+    clear() {
+      this.nowCategoryId = 0;
+      this.tableKey = '';
+      const loading = ElLoading.service({
+        lock: true,
+        text: '正在清理缓存',
+        background: 'rgba(0, 0, 0, 0.7)'
+      });
+      this.tableList = [];
+      report.clearCatch([this.datasourceId]).then(res => {
+        this.getTableList();
+        ElMessage.success('已清理');
+      }).finally(() => loading.close());
+    },
+    sizeChange(size) {
+      this.page.size = size;
+      this.getTableList();
+    },
+    currentChange(val) {
+      this.page.current = val;
+      this.getTableList();
+    },
+    onSelectCategory(id) {
+      this.nowCategoryId = id;
+      this.getTableList();
+    },
+    closeDrawer(e) {
+      this.showUpload = false;
+      this.getTableList();
+    },
+    batchUpload(e) {
+      this.uploadBoxVisible = true;
+      this.$nextTick(() => {
+        this.$refs.UploadBox.init(e);
+      });
+    },
+    UploadChange(data) {
+      let query = {...data};
+      report.batchImport(query).then(res => {
+        ElMessage({message: res.msg, type: 'success'});
+        this.$refs.UploadBox.visible = false;
+        this.$refs.UploadBox.btnLoading = false;
+        this.uploadBoxVisible = false;
+      }).catch(() => {
+        this.$refs.UploadBox.btnLoading = false;
+      });
+    },
+  },
+};
+</script>
+<style lang="scss">
+.page-ds-detail {
+  display: flex;
+  height: 100%;
+  overflow-x: auto;
+  overflow-y: hidden;
+  .main-wrapper {
+    flex: 1;
+    height: 100%;
+  }
+}
+.table-field-preview-dialog, .table-data-preview-dialog {
+  max-height: 80% !important;
+  display: flex;
+  flex-direction: column;
+  .el-dialog__header {
+    flex: 0 0 56px;
+    margin: 0;
+  }
+  .el-dialog__body {
+    flex: 1;
+    overflow-y: auto;
+  }
+}
+</style>

+ 1 - 1
src/views/temp/components/ChartTempCategory.vue

@@ -159,5 +159,5 @@ defineExpose({
 })
 </script>
 <style lang="scss">
-@import "./category.scss";
+@import "../../../assets/category";
 </style>

+ 1 - 1
src/views/temp/components/VisualTempCategory.vue

@@ -160,5 +160,5 @@ defineExpose({
 })
 </script>
 <style lang="scss">
-@import "./category.scss";
+@import "../../../assets/category";
 </style>