houyaf %!s(int64=3) %!d(string=hai) anos
pai
achega
f996d2b58e

+ 151 - 0
src/views/app_center/apps/components/App.vue

@@ -0,0 +1,151 @@
+<route lang="yaml">
+meta:
+  enabled: false
+</route>
+
+<script lang="ts" setup>
+import { ElMessage } from 'element-plus'
+import type { FormInstance, FormRules } from 'element-plus'
+import { createApp, getAppById, updateApp } from '@/api/apps/appApi'
+const emit = defineEmits(['success'])
+
+const formRef = ref<FormInstance>()
+const formRules = ref<FormRules>({
+  appTitle: [{ required: true, message: '请输入appTitle', trigger: 'blur' }],
+  appCatTitle: [{ required: true, message: '请输入appCatTitle', trigger: 'blur' }],
+  appUrl: [{ required: true, message: '请输入appUrl', trigger: 'blur' }],
+  appCode: [{ required: true, message: '请输入app编码', trigger: 'blur' }],
+  appDesc: [{ required: true, message: '请输入描述', trigger: 'blur' }],
+})
+
+const state = reactive({
+  title: '',
+  dialogVisible: false,
+  loading: false,
+  formData: {
+    appId: 0,
+    appCode: '',
+    appTitle: '',
+    appCatTitle: '',
+    appCatId: 0,
+    appUrl: '',
+    appDesc: '',
+    appLogo: '',
+  },
+  actionType: 1,
+})
+
+onMounted(() => {})
+
+const getData = async (appId: number) => {
+  state.loading = true
+  const { data } = await getAppById(appId)
+  state.loading = false
+  state.formData = data
+}
+
+function onSubmit() {
+  formRef.value && formRef.value.validate(async (valid) => {
+    if (valid) {
+      if (state.actionType === 1) {
+        await createApp(state.formData)
+        state.dialogVisible = false
+        emit('success')
+      }
+      else {
+        await updateApp(state.formData)
+        state.dialogVisible = false
+        emit('success')
+      }
+      ElMessage.success({
+        message: '保存成功',
+        center: true,
+      })
+    }
+  })
+}
+
+function onCancel() {
+  state.dialogVisible = false
+}
+
+function resetFormData() {
+  state.formData = {
+    appId: 0,
+    appCode: '',
+    appTitle: '',
+    appCatTitle: '',
+    appCatId: 0,
+    appUrl: '',
+    appDesc: '',
+    appLogo: '',
+  }
+}
+
+defineExpose({
+  showAddModel() {
+    resetFormData()
+    state.title = '新增'
+    state.actionType = 1
+    state.dialogVisible = true
+  },
+  showEditModel(appId: number) {
+    resetFormData()
+    state.title = '编辑'
+    state.actionType = 2
+    state.dialogVisible = true
+    getData(appId)
+  },
+
+})
+</script>
+
+<template>
+  <div>
+    <el-dialog v-model="state.dialogVisible" :title="state.title" width="600px" :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="appTitle">
+            <el-input v-model="state.formData.appTitle" />
+          </el-form-item>
+
+          <el-form-item label="分类" prop="appCatTitle">
+            <el-input v-model="state.formData.appCatTitle" readonly />
+          </el-form-item>
+
+          <el-form-item label="编码" prop="appCode">
+            <el-input v-model="state.formData.appCode" />
+          </el-form-item>
+
+          <el-form-item label="地址" prop="appUrl">
+            <el-input v-model="state.formData.appUrl" />
+          </el-form-item>
+
+          <el-form-item label="LOGO">
+            <el-image
+              style="width: 50px; height: 50px;border-radius:50%;"
+              :src="state.formData.appLogo"
+              :preview-src-list="[state.formData.appLogo]"
+            />
+          </el-form-item>
+          <el-form-item label="参数" prop="appParam">
+            <el-input v-model="state.formData.appParam" />
+          </el-form-item>
+
+          <el-form-item label="描述">
+            <el-input v-model="state.formData.appDesc" type="textarea" />
+          </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-dialog>
+  </div>
+</template>

+ 0 - 0
src/views/app_center/permit/components/Permit.vue


+ 528 - 0
src/views/app_center/permit/index.vue

@@ -0,0 +1,528 @@
+<route lang="yaml">
+meta:
+  enabled: false
+</route>
+
+<script lang="ts" setup name="AppRolePermit">
+import { ElMessage, ElMessageBox } from 'element-plus'
+import type Node from 'element-plus/es/components/tree/src/model/node'
+import dictionaryDialog from './components/dictionaryDialog/index.vue'
+import dictionaryItemDialog from './components/dictionaryItemDialog/index.vue'
+import api from '@/api'
+
+interface Dict {
+  id: string | number
+  label: string
+  code: string
+  children?: Dict[]
+}
+const dictionaryRef = ref()
+const dictionary = ref({
+  search: '',
+  tree: [] as Dict[],
+  currentNode: undefined as Node | undefined,
+  currentData: undefined as Dict | undefined,
+  dialog: {
+    visible: false,
+    parentId: '' as Dict['id'],
+    id: '' as Dict['id'],
+  },
+})
+
+const { pagination, getParams, onSizeChange, onCurrentChange, onSortChange } = usePagination()
+pagination.value.size = 20
+pagination.value.sizes = [20, 50, 100]
+const dictionaryItemRef = ref()
+const dictionaryItem = ref({
+  loading: false,
+  // 搜索
+  search: {
+    dictionaryId: '' as Dict['id'],
+    title: '',
+  },
+  // 列表数据
+  dataList: [],
+  selectionDataList: [],
+  dialog: {
+    visible: false,
+    id: '' as string | number,
+  },
+})
+
+const getDictionaryList = () => {
+  dictionaryItem.value.search.dictionaryId = ''
+  api.get('pages_example/dictionary/list', {
+    baseURL: '/mock/',
+  }).then((res) => {
+    dictionary.value.tree = res.data
+  })
+}
+onMounted(() => {
+  getDictionaryList()
+})
+watch(() => dictionary.value.search, (val) => {
+  dictionaryRef.value!.filter(val)
+})
+const dictionaryFilter = (value: string, data: Dict) => {
+  if (!value) {
+    return true
+  }
+  return data.label.includes(value)
+}
+const dictionaryAdd = (data?: Dict) => {
+  dictionary.value.currentData = data
+  dictionary.value.dialog.parentId = data?.id ?? ''
+  dictionary.value.dialog.id = ''
+  dictionary.value.dialog.visible = true
+}
+const dictionaryEdit = (node: Node, data: Dict) => {
+  dictionary.value.currentNode = node
+  dictionary.value.currentData = data
+  dictionary.value.dialog.parentId = node.parent.data.id ?? ''
+  dictionary.value.dialog.id = data.id
+  dictionary.value.dialog.visible = true
+}
+const dictionaryDelete = (node: Node, data: Dict) => {
+  ElMessageBox.confirm(`确认删除「${data.label}」吗?`, '确认信息').then(() => {
+    api.post('pages_example/dictionary/delete', {
+      id: data.id,
+    }, {
+      baseURL: '/mock/',
+    }).then(() => {
+      ElMessage.success({
+        message: '模拟删除成功',
+        center: true,
+      })
+      const parent = node.parent
+      const children: Dict[] = parent.data.children || parent.data
+      const index = children.findIndex(d => d.id === data.id)
+      children.splice(index, 1)
+      dictionary.value.tree = [...dictionary.value.tree]
+    })
+  })
+}
+// 新增成功后更新树
+const dictionaryAddNode = (data: Dict) => {
+  if (dictionary.value.currentData) {
+    if (!dictionary.value.currentData.children) {
+      dictionary.value.currentData.children = []
+    }
+    dictionary.value.currentData.children.push({
+      id: data.id,
+      label: data.label,
+      code: data.code,
+    })
+  }
+  else {
+    dictionary.value.tree.push({
+      id: data.id,
+      label: data.label,
+      code: data.code,
+    })
+  }
+}
+// 编辑成功后更新树
+const dictionaryEditNode = (data: Dict, parentId: string | number) => {
+  if (dictionary.value.currentNode && dictionary.value.currentData) {
+    if ((dictionary.value.currentNode.parent.data.id ?? '') === parentId) {
+      // 如果 parentId 一致说明节点位置没有变化,直接更新
+      dictionary.value.currentData.label = data.label
+      dictionary.value.currentData.code = data.code
+    }
+    else {
+      // 先更新原有节点信息
+      const parent = dictionary.value.currentNode.parent
+      const children: Dict[] = parent.data.children || parent.data
+      const index = children.findIndex(item => item.id === data.id)
+      children[index].label = data.label
+      children[index].code = data.code
+      // 然后找到需要移动到的父节点位置,并将原有节点移动过去
+      if (parentId) {
+        const findDictionary: any = (list: Dict[], parentId: number) => {
+          for (const i in list) {
+            if (list[i].id === parentId) {
+              return list[i]
+            }
+            else if (list[i].children) {
+              const temp = findDictionary(list[i].children, parentId)
+              if (temp) {
+                return temp
+              }
+            }
+          }
+        }
+        const targetNode = findDictionary(dictionary.value.tree, parentId)
+        if (!targetNode.children) {
+          targetNode.children = []
+        }
+        targetNode.children.push(children[index])
+      }
+      else {
+        dictionary.value.tree.push(children[index])
+      }
+      // 最后删除原节点
+      children.splice(index, 1)
+    }
+  }
+}
+const dictionaryClick = (data: Dict) => {
+  pagination.value.page = 1
+  dictionaryItem.value.search.dictionaryId = data.id
+}
+
+watch(() => dictionaryItem.value.search.dictionaryId, () => {
+  getDictionaryItemList()
+})
+
+function getDictionaryItemList() {
+  dictionaryItem.value.loading = true
+  const params = getParams()
+  dictionaryItem.value.search.dictionaryId && (params.dictionaryId = dictionaryItem.value.search.dictionaryId)
+  dictionaryItem.value.search.title && (params.title = dictionaryItem.value.search.title)
+  api.get('pages_example/dictionary/item/list', {
+    baseURL: '/mock/',
+    params,
+  }).then((res: any) => {
+    dictionaryItem.value.loading = false
+    dictionaryItem.value.dataList = res.data.list
+    dictionaryItem.value.dataList.forEach((item: any) => {
+      item.enableLoading = false
+    })
+    pagination.value.total = res.data.total
+  })
+}
+
+function onChangeEnable(row: any) {
+  return new Promise<boolean>((resolve) => {
+    ElMessageBox.confirm(`确认${!row.enable ? '启用' : '禁用'}「${row.name}」吗?`, '确认信息').then(() => {
+      row.enableLoading = true
+      api.post('pages_example/dictionary/item/change/enable', {
+        id: row.id,
+        enable: !row.enable,
+      }, {
+        baseURL: '/mock/',
+      }).then(() => {
+        row.enableLoading = false
+        ElMessage.success({
+          message: `模拟${!row.enable ? '启用' : '禁用'}成功`,
+          center: true,
+        })
+        return resolve(true)
+      }).catch(() => {
+        row.enableLoading = false
+        return resolve(false)
+      })
+    }).catch(() => {
+      return resolve(false)
+    })
+  })
+}
+
+// 每页数量切换
+function sizeChange(size: number) {
+  onSizeChange(size).then(() => getDictionaryItemList())
+}
+
+// 当前页码切换(翻页)
+function currentChange(page = 1) {
+  onCurrentChange(page).then(() => getDictionaryItemList())
+}
+
+// 字段排序
+function sortChange({ prop, order }: { prop: string; order: string }) {
+  onSortChange(prop, order).then(() => getDictionaryItemList())
+}
+
+function onCreate() {
+  dictionaryItem.value.dialog.id = ''
+  dictionaryItem.value.dialog.visible = true
+}
+
+function onEdit(row: any) {
+  dictionaryItem.value.dialog.id = row.id
+  dictionaryItem.value.dialog.visible = true
+}
+
+function onDelete(row: any) {
+  ElMessageBox.confirm(`确认删除「${row.name}」吗?`, '确认信息').then(() => {
+    api.post('pages_example/dictionary/item/delete', {
+      id: row.id,
+    }, {
+      baseURL: '/mock/',
+    }).then(() => {
+      getDictionaryItemList()
+      ElMessage.success({
+        message: '模拟删除成功',
+        center: true,
+      })
+    })
+  }).catch(() => {})
+}
+
+function onDeleteMulti(rows: any[]) {
+  ElMessageBox.confirm(`确认删除选中的 ${rows.length} 条数据吗?`, '确认信息').then(() => {
+    api.post('pages_example/dictionary/item/delete/multi', {
+      ids: rows.map(item => item.id),
+    }, {
+      baseURL: '/mock/',
+    }).then(() => {
+      getDictionaryItemList()
+      ElMessage.success({
+        message: '模拟删除成功',
+        center: true,
+      })
+    })
+  }).catch(() => {})
+}
+</script>
+
+<template>
+  <div class="absolute-container">
+    <page-header title="字典管理" content="页面数据为 Mock 示例数据,非真实数据。" />
+    <div class="page-main">
+      <LayoutContainer hide-left-side-toggle>
+        <template #leftSide>
+          <el-button-group class="btns">
+            <el-button type="primary" class="add" @click="dictionaryAdd()">
+              新增字典
+            </el-button>
+            <el-button @click="getDictionaryList">
+              <template #icon>
+                <el-icon>
+                  <svg-icon name="i-ep:refresh" />
+                </el-icon>
+              </template>
+            </el-button>
+          </el-button-group>
+          <el-input v-model="dictionary.search" placeholder="请输入关键词筛选字典" clearable class="search" />
+          <el-scrollbar class="tree">
+            <ElTree ref="dictionaryRef" :data="dictionary.tree" :filter-node-method="dictionaryFilter as any" default-expand-all @node-click="dictionaryClick">
+              <template #default="{ node, data }">
+                <div class="custom-tree-node">
+                  <div class="label" :title="node.label">
+                    {{ node.label }}
+                  </div>
+                  <div class="code">
+                    {{ data.code }}
+                  </div>
+                  <div class="actions">
+                    <el-button-group>
+                      <el-button type="primary" plain size="default" @click.stop="dictionaryAdd(data)">
+                        <template #icon>
+                          <el-icon>
+                            <svg-icon name="i-ep:plus" />
+                          </el-icon>
+                        </template>
+                      </el-button>
+                      <el-button type="info" plain size="default" @click.stop="dictionaryEdit(node, data)">
+                        <template #icon>
+                          <el-icon>
+                            <svg-icon name="i-ep:edit" />
+                          </el-icon>
+                        </template>
+                      </el-button>
+                      <el-button type="danger" plain size="default" @click.stop="dictionaryDelete(node, data)">
+                        <template #icon>
+                          <el-icon>
+                            <svg-icon name="i-ep:delete" />
+                          </el-icon>
+                        </template>
+                      </el-button>
+                    </el-button-group>
+                  </div>
+                </div>
+              </template>
+            </ElTree>
+          </el-scrollbar>
+        </template>
+        <div v-show="dictionaryItem.search.dictionaryId" class="container">
+          <el-space wrap>
+            <el-button type="primary" @click="onCreate">
+              <template #icon>
+                <el-icon>
+                  <svg-icon name="i-ep:plus" />
+                </el-icon>
+              </template>
+            </el-button>
+            <el-button type="danger" :disabled="!dictionaryItem.selectionDataList.length" @click="onDeleteMulti(dictionaryItem.selectionDataList)">
+              <template #icon>
+                <el-icon>
+                  <svg-icon name="i-ep:delete" />
+                </el-icon>
+              </template>
+            </el-button>
+            <el-input v-model="dictionaryItem.search.title" placeholder="请输入关键词筛选字典项" clearable style="width: 200px;" />
+            <el-button @click="getDictionaryItemList">
+              <template #icon>
+                <el-icon>
+                  <svg-icon name="i-ep:search" />
+                </el-icon>
+              </template>
+            </el-button>
+          </el-space>
+          <el-table ref="dictionaryItemRef" v-loading="dictionaryItem.loading" :data="dictionaryItem.dataList" border stripe highlight-current-row height="100%" @sort-change="sortChange" @selection-change="dictionaryItem.selectionDataList = $event">
+            <el-table-column type="selection" align="center" fixed />
+            <el-table-column prop="name" label="名称" />
+            <el-table-column label="键值" align="center" width="150">
+              <template #default="scope">
+                <el-tag type="info">
+                  {{ scope.row.value }}
+                </el-tag>
+              </template>
+            </el-table-column>
+            <el-table-column label="状态" width="100" align="center">
+              <template #default="scope">
+                <el-switch v-model="scope.row.enable" :loading="scope.row.enableLoading" inline-prompt active-text="启用" inactive-text="禁用" :before-change="() => onChangeEnable(scope.row)" />
+              </template>
+            </el-table-column>
+            <el-table-column label="操作" width="200" align="center">
+              <template #default="scope">
+                <el-button type="primary" size="small" plain @click="onEdit(scope.row)">
+                  编辑
+                </el-button>
+                <el-button type="danger" size="small" plain @click="onDelete(scope.row)">
+                  删除
+                </el-button>
+              </template>
+            </el-table-column>
+          </el-table>
+          <el-pagination :current-page="pagination.page" :total="pagination.total" :page-size="pagination.size" :page-sizes="pagination.sizes" :layout="pagination.layout" :hide-on-single-page="false" class="pagination" background @size-change="sizeChange" @current-change="currentChange" />
+        </div>
+        <div v-show="!dictionaryItem.search.dictionaryId" class="container">
+          <div class="empty">
+            请在左侧新增或选择一个字典
+          </div>
+        </div>
+      </LayoutContainer>
+      <dictionaryDialog v-if="dictionary.dialog.visible" :id="dictionary.dialog.id" v-model="dictionary.dialog.visible" :parent-id="dictionary.dialog.parentId" :tree="dictionary.tree" @add-node="dictionaryAddNode" @edit-node="dictionaryEditNode" />
+      <dictionaryItemDialog v-if="dictionaryItem.dialog.visible" :id="dictionaryItem.dialog.id" v-model="dictionaryItem.dialog.visible" :dictionary-id="dictionaryItem.search.dictionaryId" :tree="dictionary.tree" @success="getDictionaryItemList" />
+    </div>
+  </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;
+
+    .flex-container {
+      position: static;
+    }
+  }
+}
+
+.flex-container {
+  :deep(.left-side) {
+    display: flex;
+    flex-direction: column;
+    height: 100%;
+
+    .btns {
+      display: inline-flex;
+      width: 100%;
+
+      .add {
+        width: 100%;
+      }
+    }
+
+    .search {
+      margin: 15px 0;
+    }
+
+    .tree {
+      flex: 1;
+      overflow-y: auto;
+
+      .el-tree {
+        .el-tree-node__content {
+          height: 60px;
+        }
+
+        .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 {
+            width: calc(100% - 10px);
+            color: var(--el-text-color-primary);
+
+            @include text-overflow;
+          }
+
+          .code {
+            width: calc(100% - 10px);
+            color: var(--el-text-color-placeholder);
+
+            @include text-overflow;
+          }
+
+          &:hover {
+            .actions {
+              display: block;
+            }
+          }
+
+          .actions {
+            display: none;
+            position: absolute;
+            right: 10px;
+            top: 50%;
+            transform: translateY(-50%);
+
+            .el-button {
+              padding: 5px 8px;
+            }
+          }
+        }
+      }
+    }
+  }
+
+  :deep(.main) {
+    display: flex;
+    justify-content: center;
+  }
+
+  .container {
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    width: 100%;
+    height: 100%;
+
+    .empty {
+      text-align: center;
+      font-size: 32px;
+      color: var(--el-text-color-placeholder);
+    }
+
+    .el-table {
+      margin: 15px 0;
+    }
+  }
+}
+</style>