houyaf vor 3 Jahren
Ursprung
Commit
d0da103ec2

+ 27 - 0
src/views/repo/access/index.vue

@@ -0,0 +1,27 @@
+<route lang="yaml">
+meta:
+  enabled: false
+</route>
+
+<script lang="ts" setup>
+const value = ref(true)
+const value1 = ref(true)
+const value2 = ref(true)
+const value3 = ref(false)
+</script>
+
+<template>
+  <div>
+    <page-header title="开关" />
+    <page-main title="基础用法" class="demo">
+      <el-switch v-model="value" active-color="#13ce66" inactive-color="#ff4949" />
+    </page-main>
+    <page-main title="文字描述" class="demo">
+      <el-switch v-model="value1" active-text="按月付费" inactive-text="按年付费" />
+    </page-main>
+    <page-main title="禁用状态" class="demo">
+      <el-switch v-model="value2" disabled style="margin-right: 10px;" />
+      <el-switch v-model="value3" disabled />
+    </page-main>
+  </div>
+</template>

+ 116 - 0
src/views/repo/cat/components/RepoCat.vue

@@ -0,0 +1,116 @@
+<route lang="yaml">
+meta:
+  enabled: false
+</route>
+
+<script lang="ts" setup name="repoCat">
+import { ElMessage } from 'element-plus'
+import type { FormInstance, FormRules } from 'element-plus'
+import { createRepoCat, getRepoCatById, updateRepoCat } from '@/api/repo/repoCatApi'
+const emit = defineEmits(['success'])
+
+const formRef = ref<FormInstance>()
+const formRules = ref<FormRules>({
+  repoCatTitle: [
+    { required: true, message: '请输入标题', trigger: 'blur' },
+  ],
+  repoCatDesc: [
+    { required: false, message: '请输入描述', trigger: 'blur' },
+  ],
+})
+
+const state = reactive({
+  title: '',
+  dialogVisible: false,
+  loading: false,
+  formData: {
+    repoTypeId: 0,
+    repoCatId: 0,
+    repoCatTitle: '',
+    repoCatDesc: '',
+  },
+  actionType: 1,
+})
+
+onMounted(() => {})
+
+const getData = async (repoTypeId: number, repoCatId: number) => {
+  state.loading = true
+  const { data } = await getRepoCatById(repoTypeId, repoCatId)
+  state.loading = false
+  state.formData = data
+}
+
+function onSubmit() {
+  formRef.value && formRef.value.validate(async (valid) => {
+    if (valid) {
+      if (state.actionType === 1) {
+        const { msg } = await createRepoCat(state.formData)
+        ElMessage.success(msg)
+        state.dialogVisible = false
+        emit('success')
+      }
+      else {
+        const { msg } = await updateRepoCat(state.formData)
+        ElMessage.success(msg)
+        state.dialogVisible = false
+        emit('success')
+      }
+    }
+  })
+}
+
+function onCancel() {
+  state.dialogVisible = false
+}
+
+function resetFormData() {
+  state.formData = {
+    repoTypeId: 0,
+    repoCatId: 0,
+    repoCatTitle: '',
+    repoCatDesc: '',
+  }
+}
+
+defineExpose({
+  showAddModel(repoTypeId: number) {
+    resetFormData()
+    state.title = '新增'
+    state.actionType = 1
+    state.formData.repoTypeId = repoTypeId
+    state.dialogVisible = true
+  },
+  showEditModel(repoTypeId: number, repoCatId: number) {
+    resetFormData()
+    state.title = '编辑'
+    state.actionType = 2
+    state.dialogVisible = true
+    getData(repoTypeId, repoCatId)
+  },
+
+})
+</script>
+
+<template>
+  <el-drawer v-model="state.dialogVisible" :title="state.title" width="600px" :close-on-click-modal="false" append-to-body destroy-on-close>
+    <el-form ref="formRef" :model="state.formData" :rules="formRules" label-width="120px" label-suffix=":">
+      <el-form-item label="分类名称" prop="repoCatTitle">
+        <el-input v-model="state.formData.repoCatTitle" placeholder="请输入标题" />
+      </el-form-item>
+
+      <el-form-item label="简要说明" prop="repoCatDesc">
+        <el-input v-model="state.formData.repoCatDesc" type="textarea" rows="10" placeholder="请输入简要说明" />
+      </el-form-item>
+    </el-form>
+
+    <template #footer>
+      <el-button size="large" @click="onCancel">
+        取消
+      </el-button>
+      <el-button type="primary" size="large" @click="onSubmit">
+        保存
+      </el-button>
+    </template>
+  </el-drawer>
+</template>

+ 280 - 0
src/views/repo/cat/index.vue

@@ -0,0 +1,280 @@
+<script lang="ts" setup name="oc_cat_List">
+import { ElMessage, ElMessageBox } from 'element-plus'
+import RepoCatComponent from './components/RepoCat.vue'
+import { getRepoTypeByList } from '@/api/repo/repoTypeApi'
+import { deleteRepoCatById, getRepoCatListById } from '@/api/repo/repoCatApi'
+
+interface TreeNode {
+  label: string
+  isLeaf: boolean
+  repoTypeId: number
+  children?: TreeNode[]
+}
+
+interface Object {
+  repoTypeId: number
+  repoCatId: number
+  repoCatTitle: string
+  repoCatDesc: string
+}
+
+const state = reactive({
+  loading: false,
+  keywords: '',
+  treeData: [] as TreeNode[],
+  curRepoTypeId: 0,
+  repoCatList: [] as Object[],
+  dialogState: 1,
+  search: {
+    keywords: '',
+  },
+})
+
+const leftTreeRef = ref()
+const getRepoCatData = async () => {
+  const curRepoTypeId = state.curRepoTypeId
+  if (curRepoTypeId > 0) {
+    state.loading = true
+    const { data } = await getRepoCatListById(curRepoTypeId)
+    state.repoCatList = data
+    state.loading = false
+  }
+}
+
+const getRepoTypeData = async () => {
+  const { data } = await getRepoTypeByList({})
+  const children: TreeNode[] = []
+  data.forEach((item: any) => {
+    return children.push({
+      label: item.repoTypeTitle,
+      repoTypeId: item.repoTypeId,
+      isLeaf: true,
+    })
+  })
+  state.treeData = [
+    {
+      label: '数据仓库类型',
+      isLeaf: false,
+      repoTypeId: -1,
+      children,
+    },
+  ]
+
+  if (data && data.length > 0) {
+    state.curRepoTypeId = data[0].repoTypeId
+    nextTick(() => {
+      leftTreeRef.value.setCurrentKey(data[0].repoTypeId)
+    })
+    await getRepoCatData()
+  }
+}
+
+onMounted(() => {
+  getRepoTypeData()
+})
+
+// 模型选择
+const onTreeNodeClick = (node: TreeNode) => {
+  if (node.isLeaf) {
+    state.curRepoTypeId = node.repoTypeId
+    getRepoCatData()
+  }
+}
+
+const repoCatTableRef = ref()
+const repoCatRef = ref()
+
+// 检索对象
+const onSearch = () => {
+  getRepoCatData()
+}
+
+// 新增
+const onRepoCatAdd = () => {
+  const repoTypeId = state.curRepoTypeId
+  if (repoTypeId > 0) {
+    repoCatRef.value.showAddModel(repoTypeId)
+  }
+}
+
+// 编辑
+const onRepoCatEdit = (row: { repoCatId: number }) => {
+  const repoTypeId = state.curRepoTypeId
+  if (repoTypeId > 0) {
+    repoCatRef.value.showEditModel(repoTypeId, row.repoCatId)
+  }
+}
+
+// 删除对象
+const onRepoCatDelete = (row: any) => {
+  const { ocTypeId, ocCatId, ocCatTitle } = row
+  ElMessageBox.confirm(`确认删除「${ocCatTitle}」吗?`, '确认信息').then(async () => {
+    const { msg } = await deleteRepoCatById(ocTypeId, ocCatId)
+    ElMessage.success(msg)
+    await getRepoCatData()
+  })
+}
+</script>
+
+<template>
+  <div class="absolute-container">
+    <tool-header title="仓库分类" />
+
+    <div class="page-main">
+      <LayoutContainer left-side-width="250px" hide-left-side-toggle>
+        <template #leftSide>
+          <el-scrollbar class="tree">
+            <ElTree ref="leftTreeRef" :data="state.treeData" default-expand-all node-key="repoCatId" @node-click="onTreeNodeClick">
+              <template #default="{ node, data }">
+                <div class="custom-tree-node">
+                  <div class="label" :title="node.label">
+                    <el-icon v-if="data.isLeaf" size="16px" style="margin-right: 5px;">
+                      <svg-icon name="ep:office-building" />
+                    </el-icon>
+                    <div class="text">
+                      {{ data.label }}
+                    </div>
+                  </div>
+                </div>
+              </template>
+            </ElTree>
+          </el-scrollbar>
+        </template>
+
+        <div class="container">
+          <tool-bar>
+            <template #right>
+              <el-input v-model="state.search.keywords" placeholder="请输入名称,支持模糊查询" style="padding-right: 10px;" clearable />
+              <el-button type="primary" @click="onSearch">
+                <template #icon>
+                  <el-icon>
+                    <svg-icon name="i-ep:search" />
+                  </el-icon>
+                </template>
+              </el-button>
+
+              <el-button size="default" @click.stop="onRepoCatAdd">
+                <template #icon>
+                  <el-icon>
+                    <svg-icon name="i-ep:plus" />
+                  </el-icon>
+                </template>
+                数据仓库分类
+              </el-button>
+            </template>
+          </tool-bar>
+
+          <el-table ref="repoCatTableRef" v-loading="state.loading" class="list-table" :data="state.repoCatList" size="small" border stripe highlight-current-row height="100%">
+            <el-table-column type="index" label="序号" header-align="center" align="center" width="60" />
+
+            <el-table-column prop="repoCatTitle" label="名称" header-align="center" align="center" width="200">
+              <template #default="scope">
+                <span>{{ scope.row.repoCatTitle }}</span>
+              </template>
+            </el-table-column>
+
+            <el-table-column prop="repoCatDesc" label="说明" header-align="center" align="left">
+              <template #default="scope">
+                <span>{{ scope.row.repoCatDesc }}</span>
+              </template>
+            </el-table-column>
+
+            <el-table-column label="操作" header-align="center" align="center" width="220">
+              <template #default="scope">
+                <div>
+                  <el-button type="warning" size="small" plain @click="onRepoCatEdit(scope.row)">
+                    编辑
+                  </el-button>
+                  <el-button v-if="scope.row.isFixed <= 0" type="danger" size="small" plain @click="onRepoCatDelete(scope.row)">
+                    删除
+                  </el-button>
+                </div>
+              </template>
+            </el-table-column>
+          </el-table>
+        </div>
+      </LayoutContainer>
+    </div>
+    <RepoCatComponent ref="repoCatRef" @success="getRepoCatData" />
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.container {
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+}
+
+.absolute-container {
+  :deep(.left-side) {
+    display: flex;
+    flex-direction: column;
+    height: 100%;
+
+    .btns {
+      display: inline-flex;
+      width: 100%;
+      margin-bottom: var(--container-padding);
+
+      .add {
+        width: 100%;
+      }
+    }
+
+    .tree {
+      flex: 1;
+      overflow-y: auto;
+
+      .el-tree {
+        .el-tree-node__content {
+          height: 40px;
+        }
+
+        .is-current > .el-tree-node__content {
+          background-color: var(--el-color-primary-light-9);
+        }
+
+        .custom-tree-node {
+          flex: 1;
+          position: relative;
+          width: 0;
+          height: 100%;
+          display: flex;
+          flex-direction: column;
+          justify-content: center;
+
+          .label {
+            display: inline-flex;
+            align-items: center;
+            width: calc(100% - 10px);
+            color: var(--el-text-color-primary);
+
+            .text {
+              @include text-overflow;
+            }
+          }
+
+          &:hover {
+            .actions {
+              display: block;
+            }
+          }
+
+          .actions {
+            display: none;
+            position: absolute;
+            right: 10px;
+            top: 50%;
+            transform: translateY(-50%);
+
+            .el-button {
+              padding: 5px 8px;
+            }
+          }
+        }
+      }
+    }
+  }
+}
+</style>

+ 107 - 0
src/views/repo/db/components/RepoObjectSelector.vue

@@ -0,0 +1,107 @@
+<route lang="yaml">
+meta:
+  enabled: false
+</route>
+
+<script lang="ts" setup name="repoObjectSelector">
+import { ElMessage } from 'element-plus'
+import { createRepoObject, getRepoObjectListByID } from '@/api/repo/repoObjectApi'
+const emit = defineEmits(['success'])
+
+const state = reactive({
+  title: '',
+  dialogVisible: false,
+  loading: false,
+  formData: {
+    repoId: 0,
+    modelId: 0,
+    objectId: 0,
+  },
+  actionType: 1,
+})
+
+onMounted(() => {})
+
+const getData = async () => {
+  state.loading = true
+  const { repoId } = state.formData
+  const { data } = await getRepoObjectListByID(repoId)
+  state.loading = false
+  state.formData = data
+}
+
+const onRepoObjectAdd = async (row: any) => {
+  state.formData.modelId = row.modelId
+  state.formData.objectId = row.objectId
+  const { msg } = await createRepoObject(state.formData)
+  ElMessage.success(msg)
+  emit('success')
+}
+
+function onCancel() {
+  state.dialogVisible = false
+}
+
+function resetFormData() {
+  state.formData = {
+    repoId: 0,
+    modelId: 0,
+    objectId: 0,
+  }
+}
+
+defineExpose({
+  showAddModel(repoId: number) {
+    resetFormData()
+    state.title = '新增'
+    state.actionType = 1
+    state.formData.repoId = repoId
+    state.dialogVisible = true
+  },
+})
+</script>
+
+<template>
+  <el-drawer v-model="state.dialogVisible" :title="state.title" width="800px" :close-on-click-modal="false" append-to-body destroy-on-close>
+    <el-table
+      ref="modelObjectTableRef"
+      v-loading="state.loading"
+      class="list-table"
+      :data="state.data"
+      row-key="permitId"
+      default-expand-all
+      size="small"
+      border stripe highlight-current-row
+      height="100%"
+    >
+      <el-table-column type="index" align="center" label="序号" width="70" />
+
+      <el-table-column label="模型名称" width="120">
+        <template #default="{ row }">
+          <span>{{ row.modelTitle }}</span>
+        </template>
+      </el-table-column>
+
+      <el-table-column label="数据对象" header-align="center" align="center">
+        <template #default="{ row }">
+          <span>{{ row.objectTitle }}</span>
+        </template>
+      </el-table-column>
+
+      <el-table-column label="操作" width="90" header-align="center" align="center">
+        <template #default="{ row }">
+          <div>
+            <el-button v-if="row.permitType === 0" size="small" type="primary" @click="onRepoObjectAdd(row)">
+              添加
+            </el-button>
+          </div>
+        </template>
+      </el-table-column>
+    </el-table>
+    <template #footer>
+      <el-button size="large" @click="onCancel">
+        取消
+      </el-button>
+    </template>
+  </el-drawer>
+</template>

+ 257 - 0
src/views/repo/db/index.vue

@@ -0,0 +1,257 @@
+<route lang="yaml">
+meta:
+  enabled: false
+</route>
+
+<script lang="ts" setup>
+import { getModelTreeData } from '@/api/model/modelApi'
+import { getTableDataByList, getTableFieldsByList } from '@/api/repo/repoDBApi'
+import useSettingsStore from '@/store/modules/settings'
+
+const settingsStore = useSettingsStore()
+const router = useRouter()
+const tabbar = useTabbar()
+const route = useRoute()
+
+const repoTypeId = parseInt(route.params.repoTypeId.toString())
+const repoId = parseInt(route.params.repoId.toString())
+
+interface TreeNode {
+  label: string
+  isLeaf: boolean
+  value: number
+  objectCode: string
+  children?: TreeNode[]
+}
+
+const state = reactive({
+  loading: false,
+  keywords: '',
+  treeData: [] as TreeNode[],
+  curModelId: 0,
+  curObjectId: 0,
+  curObjectCode: '',
+  dialogState: 1,
+  tableFields: [],
+  tableDataList: [],
+  search: {
+    keywords: '',
+  },
+})
+
+const leftTreeRef = ref()
+const getTreeData = async () => {
+  state.loading = true
+  const { data } = await getModelTreeData()
+  state.treeData = data
+  state.loading = false
+
+  if (data && data.length > 0) {
+    const firstModel = data[0]
+    state.curModelId = firstModel.value
+    if (firstModel.children && firstModel.children.size > 0) {
+      const firstObject = firstModel.children[0]
+      await nextTick(() => {
+        leftTreeRef.value.setCurrentKey(firstObject.value)
+      })
+    }
+  }
+}
+
+const getTableFields = async () => {
+  const tableName = state.curObjectCode
+  const res = await getTableFieldsByList(tableName)
+  state.tableFields = res.data
+}
+
+const getTableData = async () => {
+  const tableName = state.curObjectCode
+  const res = await getTableDataByList(tableName)
+  state.tableDataList = res.data
+}
+
+const getData = () => {
+  if (state.curObjectCode !== '') {
+    state.loading = true
+    getTableFields()
+    getTableData()
+    state.loading = false
+  }
+}
+
+onMounted(() => {
+  getTreeData()
+})
+
+// 对象选择
+const onTreeNodeClick = (node: TreeNode) => {
+  if (node.isLeaf) {
+    state.curObjectId = node.value
+    state.curObjectCode = node.objectCode
+    getTableFields()
+    getTableData()
+  }
+  else {
+    state.curModelId = node.value
+  }
+}
+
+const tableDataRef = ref()
+
+// 检索对象
+const onSearch = () => {
+  getData()
+}
+
+// 返回列表页
+function goBack() {
+  if (settingsStore.settings.tabbar.enable && settingsStore.settings.tabbar.mergeTabsBy !== 'activeMenu') {
+    tabbar.close({ name: 'repo_index' })
+  }
+  else {
+    router.push({ name: 'repo_index' })
+  }
+}
+</script>
+
+<template>
+  <div class="absolute-container">
+    <page-header title="数据仓库">
+      <el-button size="default" round @click="goBack">
+        <template #icon>
+          <el-icon>
+            <svg-icon name="i-ep:arrow-left" />
+          </el-icon>
+        </template>
+        返回
+      </el-button>
+    </page-header>
+
+    <div class="page-main">
+      <LayoutContainer left-side-width="250px" hide-left-side-toggle>
+        <template #leftSide>
+          <el-scrollbar class="tree">
+            <ElTree ref="leftTreeRef" :data="state.treeData" default-expand-all node-key="modelId" @node-click="onTreeNodeClick">
+              <template #default="{ node, data }">
+                <div class="custom-tree-node">
+                  <div class="label" :title="node.label">
+                    <el-icon v-if="data.isLeaf" size="16px" style="margin-right: 5px;">
+                      <svg-icon name="uim:box" />
+                    </el-icon>
+                    <el-icon v-else size="16px" style="margin-right: 5px;">
+                      <svg-icon name="bx:bxs-component" />
+                    </el-icon>
+                    <div class="text">
+                      {{ data.label }}
+                    </div>
+                  </div>
+                </div>
+              </template>
+            </ElTree>
+          </el-scrollbar>
+        </template>
+
+        <div class="container">
+          <tool-bar>
+            <template #right>
+              <el-input v-model="state.search.keywords" placeholder="请输入名称,支持模糊查询" style="padding-right: 10px;" clearable />
+              <el-button type="primary" @click="onSearch">
+                <template #icon>
+                  <el-icon>
+                    <svg-icon name="i-ep:search" />
+                  </el-icon>
+                </template>
+              </el-button>
+            </template>
+          </tool-bar>
+
+          <el-table ref="tableDataRef" v-loading="state.loading" class="list-table" :data="state.tableDataList" size="small" border stripe highlight-current-row height="100%">
+            <el-table-column type="index" label="序号" header-align="center" align="center" width="70" />
+
+            <template v-for="(field, index) in state.tableFields" :key="index">
+              <el-table-column :prop="field.attrCode" :label="field.attrCode" header-align="center" align="center" />
+            </template>
+          </el-table>
+        </div>
+      </LayoutContainer>
+    </div>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.absolute-container {
+  .page-header {
+    margin-bottom: 0;
+  }
+
+  :deep(.left-side) {
+    display: flex;
+    flex-direction: column;
+    height: 100%;
+
+    .btns {
+      display: inline-flex;
+      width: 100%;
+      margin-bottom: var(--container-padding);
+
+      .add {
+        width: 100%;
+      }
+    }
+
+    .tree {
+      flex: 1;
+      overflow-y: auto;
+
+      .el-tree {
+        .el-tree-node__content {
+          height: 40px;
+        }
+
+        .is-current > .el-tree-node__content {
+          background-color: var(--el-color-primary-light-9);
+        }
+
+        .custom-tree-node {
+          flex: 1;
+          position: relative;
+          width: 0;
+          height: 100%;
+          display: flex;
+          flex-direction: column;
+          justify-content: center;
+
+          .label {
+            display: inline-flex;
+            align-items: center;
+            width: calc(100% - 10px);
+            color: var(--el-text-color-primary);
+
+            .text {
+              @include text-overflow;
+            }
+          }
+
+          &:hover {
+            .actions {
+              display: block;
+            }
+          }
+
+          .actions {
+            display: none;
+            position: absolute;
+            right: 10px;
+            top: 50%;
+            transform: translateY(-50%);
+
+            .el-button {
+              padding: 5px 8px;
+            }
+          }
+        }
+      }
+    }
+  }
+}
+</style>

+ 189 - 0
src/views/repo/repo/components/Repo.vue

@@ -0,0 +1,189 @@
+<route lang="yaml">
+meta:
+  enabled: false
+</route>
+
+<script lang="ts" setup>
+import { ElMessage } from 'element-plus'
+import type { FormInstance, FormRules } from 'element-plus'
+import { createRepo, getRepoById, updateRepo } from '@/api/repo/repoApi'
+import RepoCatSelector from '@/views/repository/repo/components/RepoCatSelector.vue'
+
+const emit = defineEmits(['success'])
+
+const formRef = ref<FormInstance>()
+const formRules = ref<FormRules>({
+  repoTitle: [
+    { required: true, message: '请输入标题', trigger: 'blur' },
+  ],
+  repoDesc: [
+    { required: false, message: '请输入描述', trigger: 'blur' },
+  ],
+})
+
+const state = reactive({
+  title: '',
+  dialogVisible: false,
+  loading: false,
+  search: {
+    repoTypeId: 0,
+    repoId: 0,
+  },
+  formData: {
+    repoId: 0,
+    repoTitle: '',
+    repoTypeId: 0,
+    repoCatId: 0,
+    repoHost: '',
+    repoPort: 0,
+    repoAccount: '',
+    repoPassword: '',
+    repoAccessKey: '',
+    repoSecretKey: '',
+    repoTarget: '',
+    repoDesc: '',
+  },
+  actionType: 1,
+})
+
+onMounted(() => {})
+
+const getData = async () => {
+  const { repoTypeId, repoId } = state.search
+  state.loading = true
+  const { data } = await getRepoById(repoTypeId, repoId)
+  state.loading = false
+  state.formData = data
+}
+
+function onSubmit() {
+  formRef.value && formRef.value.validate(async (valid) => {
+    if (valid) {
+      if (state.actionType === 1) {
+        const { msg } = await createRepo(state.formData)
+        ElMessage.success(msg)
+        state.dialogVisible = false
+        emit('success')
+      }
+      else {
+        const { msg } = await updateRepo(state.formData)
+        ElMessage.success(msg)
+        state.dialogVisible = false
+        emit('success')
+      }
+    }
+  })
+}
+
+function onCancel() {
+  state.dialogVisible = false
+}
+
+function resetFormData() {
+  state.formData = {
+    repoId: 0,
+    repoTitle: '',
+    repoTypeId: 0,
+    repoCatId: 0,
+    repoHost: '',
+    repoPort: 0,
+    repoAccount: '',
+    repoPassword: '',
+    repoAccessKey: '',
+    repoSecretKey: '',
+    repoTarget: '',
+    repoDesc: '',
+  }
+}
+
+defineExpose({
+  showAddModel(repoTypeId: number) {
+    resetFormData()
+    if (repoTypeId === 1) {
+      state.title = '新增数据库'
+    }
+    else if (repoTypeId === 2) {
+      state.title = '新增文件服务器'
+    }
+    else {
+      state.title = '新增高速缓冲'
+    }
+    state.actionType = 1
+    state.search.repoTypeId = repoTypeId
+    state.formData.repoTypeId = repoTypeId
+    state.dialogVisible = true
+  },
+
+  showEditModel(repoTypeId: number, repoId: number) {
+    resetFormData()
+    state.title = '编辑'
+    state.actionType = 2
+    state.search.repoTypeId = repoTypeId
+    state.search.repoId = repoId
+    state.dialogVisible = true
+    getData()
+  },
+
+})
+</script>
+
+<template>
+  <el-drawer v-model="state.dialogVisible" :title="state.title" width="700px" :close-on-click-modal="false" append-to-body destroy-on-close>
+    <div v-loading="state.loading">
+      <el-form ref="formRef" :model="state.formData" :rules="formRules" label-width="120px" label-suffix=":">
+        <el-form-item label="标题" prop="repoTitle">
+          <el-input v-model="state.formData.repoTitle" placeholder="标题" />
+        </el-form-item>
+
+        <el-form-item label="类别" prop="dbTypeTitle">
+          <RepoCatSelector v-model="state.formData.repoCatId" :repo-type-id="state.formData.repoTypeId" />
+        </el-form-item>
+
+        <el-form-item label="主机" prop="repoHost">
+          <el-col :span="16">
+            <el-input v-model="state.formData.repoHost" placeholder="请输入主机" />
+          </el-col>
+          <el-col :span="1">
+            :
+          </el-col>
+          <el-col :span="7">
+            <el-input v-model="state.formData.repoPort" type="number" placeholder="请输入端口" />
+          </el-col>
+        </el-form-item>
+
+        <el-form-item label="账号" prop="repoAccount">
+          <el-input v-model="state.formData.repoAccount" placeholder="请输入账号" />
+        </el-form-item>
+
+        <el-form-item label="密码" prop="repoPassword">
+          <el-input v-model="state.formData.repoPassword" placeholder="请输入密码" />
+        </el-form-item>
+
+        <el-form-item label="Access Key" prop="repoAccessKey">
+          <el-input v-model="state.formData.repoAccessKey" placeholder="请输入Access Key" />
+        </el-form-item>
+
+        <el-form-item label="Secret key" prop="repoSecretKey">
+          <el-input v-model="state.formData.repoSecretKey" placeholder="请输入Secret Key" />
+        </el-form-item>
+
+        <el-form-item label="目标库" prop="repoTarget">
+          <el-input v-model="state.formData.repoTarget" placeholder="请输入Target" />
+        </el-form-item>
+
+        <el-form-item label="简要说明" prop="repoDesc">
+          <el-input v-model="state.formData.repoDesc" type="textarea" rows="5" placeholder="请输入简要说明" />
+        </el-form-item>
+      </el-form>
+    </div>
+
+    <template #footer>
+      <el-button size="large" @click="onCancel">
+        取消
+      </el-button>
+      <el-button type="primary" size="large" @click="onSubmit">
+        保存
+      </el-button>
+    </template>
+  </el-drawer>
+</template>

+ 56 - 0
src/views/repo/repo/components/RepoCatSelector.vue

@@ -0,0 +1,56 @@
+<script lang="ts" setup name="RepoCatSelector">
+import { getRepoCatListById } from '@/api/repo/repoCatApi'
+const props = withDefaults(defineProps<{
+  modelValue: number
+  repoTypeId: number
+}>(), {
+  modelValue: 0,
+  repoTypeId: 0,
+})
+
+const emit = defineEmits(['update:modelValue'])
+interface RepoCat {
+  repoTypeId: number
+  repoCatId: number
+  repoCatTitle: string
+}
+
+const repoCatId = computed({
+  get: () => {
+    return props.modelValue
+  },
+  set: (val) => {
+    emit('update:modelValue', val)
+  },
+})
+
+const state = reactive({
+  loading: false,
+  dataList: [] as RepoCat[],
+})
+
+const getData = async () => {
+  const repoTypeId = props.repoTypeId
+  if (repoTypeId > 0) {
+    state.loading = true
+    const { data } = await getRepoCatListById(repoTypeId)
+    state.loading = false
+    state.dataList = data
+  }
+}
+onMounted(() => {
+  getData ()
+})
+</script>
+
+<template>
+  <el-select v-model="repoCatId" v-loading="state.loading" style="width: 100%" filterable placeholder="请选择" value-key="repoCatId">
+    <el-option key="0" label="请选择" :value="0" disabled />
+    <el-option
+      v-for="item in state.dataList"
+      :key="item.repoCatId"
+      :label="item.repoCatTitle"
+      :value="item.repoCatId"
+    />
+  </el-select>
+</template>

+ 242 - 0
src/views/repo/repo/index.vue

@@ -0,0 +1,242 @@
+<script lang="ts" setup name="DBTypeIndex">
+import { ElMessage, ElMessageBox } from 'element-plus'
+import RepoComponent from './components/Repo.vue'
+import { deleteRepoById, getRepoListByType } from '@/api/repo/repoApi'
+import useSettingsStore from '@/store/modules/settings'
+
+const settingsStore = useSettingsStore()
+const router = useRouter()
+const tabbar = useTabbar()
+
+const state = reactive({
+  loading: false,
+  // 表格是否自适应高度
+  tableAutoHeight: false,
+  // 搜索
+  search: {
+    keywords: '',
+  },
+  // 列表数据
+  repoDBList: [],
+  repoFileList: [],
+})
+
+const repoRef = ref()
+
+const getRepoList = async (repoTypeId: number) => {
+  state.loading = true
+  const { data } = await getRepoListByType(repoTypeId)
+  if (repoTypeId === 1) {
+    state.repoDBList = data
+  }
+  else {
+    state.repoFileList = data
+  }
+  state.loading = false
+}
+
+const getRepoData = async () => {
+  await getRepoList(1)
+  await getRepoList(2)
+}
+
+onMounted(() => {
+  getRepoData()
+})
+
+function onRepoAdd(repoTypeId: number) {
+  repoRef.value.showAddModel(repoTypeId)
+}
+
+function onRepoEdit(row: any) {
+  const { repoTypeId, repoId } = row
+  repoRef.value.showEditModel(repoTypeId, repoId)
+}
+
+function onRepoDel(row: any) {
+  const { repoId, repoTypeId, repoTitle } = row
+  ElMessageBox.confirm(`确认删除「${repoTitle}」吗?`, '确认信息').then(async () => {
+    const { msg } = await deleteRepoById(repoTypeId, repoId)
+    ElMessage.success(msg)
+    await getRepoList(repoTypeId)
+  }).catch(() => {})
+}
+
+// 编辑对象
+const onRepoDetails = (row: { repoTypeId: number; repoId: number }) => {
+  if (settingsStore.settings.tabbar.enable && settingsStore.settings.tabbar.mergeTabsBy !== 'activeMenu') {
+    tabbar.open({
+      name: 'repoDBDetails',
+      params: {
+        repoTypeId: row.repoTypeId,
+        repoId: row.repoId,
+      },
+    })
+  }
+  else {
+    router.push({
+      name: 'repoDBDetails',
+      params: {
+        repoTypeId: row.repoTypeId,
+        repoId: row.repoId,
+      },
+    })
+  }
+}
+</script>
+
+<template>
+  <div class="absolute-container">
+    <page-header title="数据仓库">
+      <template #content>
+        <div>
+          用于管理各种类型的数据仓库
+        </div>
+      </template>
+    </page-header>
+
+    <el-row :gutter="20" style="margin: -10px 10px;">
+      <el-col :md="16">
+        <page-main style="margin: 10px 0;">
+          <tool-bar>
+            <template #left>
+              <h3>数据库</h3>
+            </template>
+            <template #right>
+              <el-button type="success" plain @click="onRepoAdd(1)">
+                <template #icon>
+                  <el-icon>
+                    <svg-icon name="i-ep:plus" />
+                  </el-icon>
+                </template>
+              </el-button>
+            </template>
+          </tool-bar>
+
+          <el-table v-loading="state.loading" class="list-table" :data="state.repoDBList" :border="false" :stripe="false" :show-header="false" height="100%">
+            <el-table-column type="index" width="50" fixed="left" />
+
+            <el-table-column prop="repoTitle" label="名称" header-align="center" align="center" width="150">
+              <template #default="scope">
+                <el-icon size="16px" style="margin-right: 5px;">
+                  <svg-icon name="ep:coin" />
+                </el-icon>
+                <span>{{ scope.row.repoTitle }}</span>
+              </template>
+            </el-table-column>
+
+            <el-table-column prop="repoCatTitle" label="类型" header-align="center" align="center" width="150">
+              <template #default="scope">
+                <el-tag type="success">
+                  {{ scope.row.repoCatTitle }}
+                </el-tag>
+              </template>
+            </el-table-column>
+
+            <el-table-column prop="repoDesc" label="说明" header-align="left" align="left">
+              <template #default="scope">
+                <span>{{ scope.row.repoDesc }}</span>
+              </template>
+            </el-table-column>
+
+            <el-table-column label="操作" width="220" header-align="center" align="right" fixed="right">
+              <template #default="scope">
+                <div>
+                  <el-button type="primary" size="small" plain @click="onRepoDetails(scope.row)">
+                    详情
+                  </el-button>
+                  <el-button type="warning" size="small" plain @click="onRepoEdit(scope.row)">
+                    编辑
+                  </el-button>
+                  <el-button v-if="!scope.row.isFixed" type="danger" size="small" plain @click="onRepoDel(scope.row)">
+                    删除
+                  </el-button>
+                </div>
+              </template>
+            </el-table-column>
+          </el-table>
+        </page-main>
+      </el-col>
+
+      <el-col :md="8">
+        <page-main style="margin: 10px 0;">
+          <tool-bar>
+            <template #left>
+              <h3>文件服务器</h3>
+            </template>
+            <template #right>
+              <el-button type="success" plain @click="onRepoAdd(2)">
+                <template #icon>
+                  <el-icon>
+                    <svg-icon name="i-ep:plus" />
+                  </el-icon>
+                </template>
+              </el-button>
+            </template>
+          </tool-bar>
+
+          <el-table v-loading="state.loading" class="list-table" :data="state.repoFileList" :border="false" :stripe="false" :show-header="false" height="100%">
+            <el-table-column type="index" label="序号" width="50" header-align="center" align="center" />
+
+            <el-table-column prop="repoTitle" label="名称" header-align="center" align="center" width="100">
+              <template #default="scope">
+                <el-icon size="16px" style="margin-right: 5px;">
+                  <svg-icon name="ep:files" />
+                </el-icon>
+                <span>{{ scope.row.repoTitle }}</span>
+              </template>
+            </el-table-column>
+
+            <el-table-column prop="repoTitle" label="名称" header-align="center" align="center">
+              <template #default="scope">
+                <el-tag type="warning">
+                  {{ scope.row.repoCatTitle }}
+                </el-tag>
+              </template>
+            </el-table-column>
+
+            <el-table-column label="操作" width="150" align="center" fixed="right">
+              <template #default="scope">
+                <el-button type="warning" size="small" plain @click="onRepoEdit(scope.row)">
+                  编辑
+                </el-button>
+                <el-button v-if="!scope.row.isFixed" type="danger" size="small" plain @click="onRepoDel(scope.row)">
+                  删除
+                </el-button>
+              </template>
+            </el-table-column>
+          </el-table>
+        </page-main>
+      </el-col>
+    </el-row>
+    <RepoComponent ref="repoRef" @success="getRepoData" />
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.absolute-container {
+  position: absolute;
+  width: 100%;
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+
+  .page-main {
+    // 让 page-main 的高度自适应
+    flex: 1;
+    overflow: auto;
+    display: flex;
+    flex-direction: column;
+
+    .tool-bar {
+      h3{
+        color: #757d87;
+      }
+    }
+
+    .search-container {
+      margin-bottom: 0;
+    }
+  }
+}
+</style>

+ 113 - 0
src/views/repo/type/components/DbType.vue

@@ -0,0 +1,113 @@
+<route lang="yaml">
+meta:
+  enabled: false
+</route>
+
+<script lang="ts" setup>
+import { ElMessage } from 'element-plus'
+import type { FormInstance, FormRules } from 'element-plus'
+import { createRepoType, getRepoTypeById, updateRepoType } from '@/api/repo/repoTypeApi'
+const emit = defineEmits(['success'])
+
+const formRef = ref<FormInstance>()
+const formRules = ref<FormRules>({
+  dbTypeTitle: [
+    { required: true, message: '请输入标题', trigger: 'blur' },
+  ],
+  dbTypeDesc: [
+    { required: false, message: '请输入描述', trigger: 'blur' },
+  ],
+})
+
+const state = reactive({
+  title: '',
+  dialogVisible: false,
+  loading: false,
+  formData: {
+    dbTypeId: 0,
+    dbTypeTitle: '',
+    dbTypeDesc: '',
+  },
+  actionType: 1,
+})
+
+onMounted(() => {})
+
+const getData = async (dbTypeId: number) => {
+  state.loading = true
+  const { data } = await getRepoTypeById(dbTypeId)
+  state.loading = false
+  state.formData = data
+}
+
+function onSubmit() {
+  formRef.value && formRef.value.validate(async (valid) => {
+    if (valid) {
+      if (state.actionType === 1) {
+        const { msg } = await createRepoType(state.formData)
+        ElMessage.success(msg)
+        state.dialogVisible = false
+        emit('success')
+      }
+      else {
+        const { msg } = await updateRepoType(state.formData)
+        ElMessage.success(msg)
+        state.dialogVisible = false
+        emit('success')
+      }
+    }
+  })
+}
+
+function onCancel() {
+  state.dialogVisible = false
+}
+
+function resetFormData() {
+  state.formData = {
+    dbTypeId: 0,
+    dbTypeTitle: '',
+    dbTypeDesc: '',
+  }
+}
+
+defineExpose({
+  showAddModel() {
+    resetFormData()
+    state.title = '新增'
+    state.actionType = 1
+    state.dialogVisible = true
+  },
+  showEditModel(dbTypeId: number) {
+    resetFormData()
+    state.title = '编辑'
+    state.actionType = 2
+    state.dialogVisible = true
+    getData(dbTypeId)
+  },
+
+})
+</script>
+
+<template>
+  <el-drawer v-model="state.dialogVisible" :title="state.title" width="600px" :close-on-click-modal="false" append-to-body destroy-on-close>
+    <el-form ref="formRef" :model="state.formData" :rules="formRules" label-width="120px" label-suffix=":">
+      <el-form-item label="分类名称" prop="dbTypeTitle">
+        <el-input v-model="state.formData.dbTypeTitle" placeholder="请输入标题" />
+      </el-form-item>
+
+      <el-form-item label="简要说明" prop="dbTypeDesc">
+        <el-input v-model="state.formData.dbTypeDesc" type="textarea" rows="10" placeholder="请输入简要说明" />
+      </el-form-item>
+    </el-form>
+
+    <template #footer>
+      <el-button size="large" @click="onCancel">
+        取消
+      </el-button>
+      <el-button type="primary" size="large" @click="onSubmit">
+        保存
+      </el-button>
+    </template>
+  </el-drawer>
+</template>

+ 128 - 0
src/views/repo/type/index.vue

@@ -0,0 +1,128 @@
+<script lang="ts" setup name="DbTypeIndex">
+import { ElMessage, ElMessageBox } from 'element-plus'
+import DbType from './components/DbType.vue'
+import { deleteRepoTypeById, getRepoTypeByList } from '@/api/repo/repoTypeApi'
+
+const state = reactive({
+  loading: false,
+  // 表格是否自适应高度
+  tableAutoHeight: false,
+  // 搜索
+  search: {
+    keywords: '',
+  },
+  // 列表数据
+  dataList: [],
+})
+
+const dbTypeRef = ref()
+
+const getDataList = async () => {
+  state.loading = true
+  const { data } = await getRepoTypeByList({})
+  state.dataList = data
+  state.loading = false
+}
+
+onMounted(() => {
+  getDataList()
+})
+
+function onDbTypeAdd() {
+  dbTypeRef.value.showAddModel()
+}
+
+function onDbTypeEdit(row: any) {
+  const dbTypeId = row.dbTypeId
+  dbTypeRef.value.showEditModel(dbTypeId)
+}
+
+function onDbTypeDel(row: any) {
+  ElMessageBox.confirm(`确认删除「${row.dbTypeTitle}」吗?`, '确认信息').then(async () => {
+    const { msg } = await deleteRepoTypeById(row.dbTypeId)
+    ElMessage.success(msg)
+    await getDataList()
+  }).catch(() => {})
+}
+</script>
+
+<template>
+  <div :class="{ 'absolute-container': state.tableAutoHeight }">
+    <page-header title="数据库类型设定">
+      <template #content>
+        <div>
+          系统支持多种数据库类型,用于设定接入参数
+        </div>
+      </template>
+
+      <el-button-group>
+        <el-button type="primary" size="large" @click="onDbTypeAdd">
+          <template #icon>
+            <el-icon>
+              <svg-icon name="ep:plus" />
+            </el-icon>
+          </template>
+          新增
+        </el-button>
+      </el-button-group>
+    </page-header>
+
+    <page-main>
+      <el-table v-loading="state.loading" class="list-table" :data="state.dataList" border stripe highlight-current-row height="100%">
+        <el-table-column type="index" label="序号" width="60" header-align="center" align="center" />
+
+        <el-table-column prop="dbTypeTitle" label="名称" header-align="center" align="center" width="200">
+          <template #default="scope">
+            <span> {{ scope.row.dbTypeTitle }}</span>
+          </template>
+        </el-table-column>
+
+        <el-table-column prop="dbTypeDesc" label="说明">
+          <template #default="scope">
+            <span>{{ scope.row.dbTypeDesc }}</span>
+          </template>
+        </el-table-column>
+
+        <el-table-column label="操作" width="250" align="center" fixed="right">
+          <template #default="scope">
+            <div>
+              <el-button type="primary" size="small" plain @click="onDbTypeEdit(scope.row)">
+                编辑
+              </el-button>
+              <el-button v-if="!scope.row.isFixed" type="danger" size="small" plain @click="onDbTypeDel(scope.row)">
+                删除
+              </el-button>
+            </div>
+          </template>
+        </el-table-column>
+      </el-table>
+    </page-main>
+    <DbType ref="dbTypeRef" @success="getDataList" />
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.absolute-container {
+  position: absolute;
+  width: 100%;
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+
+  .page-header {
+    margin-bottom: 0;
+  }
+
+  .page-main {
+    // 让 page-main 的高度自适应
+    flex: 1;
+    overflow: auto;
+    display: flex;
+    flex-direction: column;
+
+    .search-container {
+      margin-bottom: 0;
+    }
+  }
+}
+</style>