From cd0c904b3c3099ff74658c616179916bad69a052 Mon Sep 17 00:00:00 2001
From: shuishen <1109946754@qq.com>
Date: Thu, 21 Aug 2025 16:13:24 +0800
Subject: [PATCH] feat:图斑管理页面基础

---
 src/views/resource/components/patchDetails.vue |   15 +
 src/router/views/index.js                      |   31 ++
 src/views/resource/patchManagement.vue         |  368 ++++++++++++++++++++++++++++
 src/views/resource/patchTypeManagement.vue     |  317 ++++++++++++++++++++++++
 pnpm-lock.yaml                                 |   22 +
 5 files changed, 750 insertions(+), 3 deletions(-)

diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 90201ae..38f3287 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -41,6 +41,12 @@
   dayjs:
     specifier: ^1.11.13
     version: 1.11.13
+  decimal:
+    specifier: ^0.0.2
+    version: 0.0.2
+  decimal.js:
+    specifier: ^10.4.3
+    version: 10.4.3
   echarts:
     specifier: ^5.6.0
     version: 5.6.0
@@ -71,6 +77,9 @@
   postcss-pxtorem:
     specifier: ^6.1.0
     version: 6.1.0(postcss@8.4.38)
+  qrcodejs2-fix:
+    specifier: ^0.0.1
+    version: 0.0.1
   reconnecting-websocket:
     specifier: ^4.4.0
     version: 4.4.0
@@ -2458,6 +2467,15 @@
       ms: 2.1.2
     dev: true
 
+  /decimal.js@10.4.3:
+    resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==}
+    dev: false
+
+  /decimal@0.0.2:
+    resolution: {integrity: sha512-puX1+D5GlpXYH0kDejnu8F2jsGLKRePc7eOznNjqMbA2otl7AzvubrvvZVmOvRCHhsqeAPKNcAxHHbevsH/1qg==}
+    engines: {node: 0.4.x}
+    dev: false
+
   /deep-equal@1.1.2:
     resolution: {integrity: sha512-5tdhKF6DbU7iIzrIOa1AOUt39ZRm13cmL1cGEh//aqR8x9+tNfbywRf0n5FD/18OKMdo7DNEtrX2t22ZAkI+eg==}
     engines: {node: '>= 0.4'}
@@ -3418,6 +3436,10 @@
       long: 5.3.2
     dev: false
 
+  /qrcodejs2-fix@0.0.1:
+    resolution: {integrity: sha512-1R6jB87k1PUwjX65hh24FftDjrghPvryYfsajV3chCp//dQb8QUeL446DVLj2SIeotP8ro9rSYri0wOp7O+7Hg==}
+    dev: false
+
   /queue-microtask@1.2.3:
     resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
     dev: true
diff --git a/src/router/views/index.js b/src/router/views/index.js
index 568b2c8..9b208fb 100644
--- a/src/router/views/index.js
+++ b/src/router/views/index.js
@@ -1,5 +1,5 @@
-import Layout from '@/page/index/index.vue';
-import Store from '@/store/';
+import Layout from '@/page/index/index.vue'
+import Store from '@/store/'
 
 export default [
   {
@@ -157,4 +157,29 @@
       },
     ],
   },
-];
+
+  {
+    path: '/resource',
+    component: Layout,
+    redirect: '/resource/patchManagement',
+    children: [
+      {
+        path: 'patchManagement',
+        name: '图斑管理',
+        meta: {
+          i18n: 'info',
+        },
+        component: () => import(/* webpackChunkName: "views" */ '@/views/resource/patchManagement.vue'),
+      },
+
+      {
+        path: 'patchTypeManagement',
+        name: '图斑类型管理',
+        meta: {
+          i18n: 'info',
+        },
+        component: () => import(/* webpackChunkName: "views" */ '@/views/resource/patchTypeManagement.vue'),
+      },
+    ],
+  },
+]
diff --git a/src/views/resource/components/patchDetails.vue b/src/views/resource/components/patchDetails.vue
new file mode 100644
index 0000000..343df2c
--- /dev/null
+++ b/src/views/resource/components/patchDetails.vue
@@ -0,0 +1,15 @@
+<template>
+	<el-dialog append-to-body v-model="patchDetailsShow">
+		<el-tabs v-model="activeTab" type="border-card">
+			<el-tab-pane label="图斑详情" name="first">图斑详情</el-tab-pane>
+			<el-tab-pane label="图斑类型" name="second">图斑类型</el-tab-pane>
+			<el-tab-pane label="图斑区域" name="third">图斑区域</el-tab-pane>
+		</el-tabs>
+	</el-dialog>
+</template>
+
+<script setup>
+const patchDetailsShow = ref(true)
+</script>
+
+<style lang="scss" scoped></style>
\ No newline at end of file
diff --git a/src/views/resource/patchManagement.vue b/src/views/resource/patchManagement.vue
new file mode 100644
index 0000000..19bc2ab
--- /dev/null
+++ b/src/views/resource/patchManagement.vue
@@ -0,0 +1,368 @@
+<template>
+  <basic-container>
+    <avue-crud :option="option" :table-loading="loading" :data="data" v-model:page="page" :permission="permissionList"
+      v-model="form" ref="crud" @row-update="rowUpdate" @row-save="rowSave" @row-del="rowDel" :before-open="beforeOpen"
+      @search-change="searchChange" @search-reset="searchReset" @selection-change="selectionChange"
+      @current-change="currentChange" @size-change="sizeChange" @refresh-change="refreshChange" @on-load="onLoad">
+      <template #menu-left>
+        <el-button type="primary" icon="el-icon-upload" @click="uploadPatch">
+          上传图斑
+        </el-button>
+        <el-button type="primary" icon="el-icon-setting" @click="goTypeManagement">
+          类型管理
+        </el-button>
+        <el-button type="success" icon="el-icon-download" @click="downloadPatch">
+          导出
+        </el-button>
+      </template>
+
+      <template #menu="scope">
+        <el-button type="primary" text icon="el-icon-video-play" @click="handleDebug(scope.row)">详情
+        </el-button>
+        <el-button type="primary" text icon="el-icon-video-play" @click="handleDebug(scope.row)">编辑
+        </el-button>
+        <el-button type="primary" text icon="el-icon-video-play" @click="handleDebug(scope.row)">删除
+        </el-button>
+      </template>
+    </avue-crud>
+
+    <el-dialog title="对象存储上传调试" append-to-body v-model="box" width="550px">
+      <avue-form ref="form" :option="debugOption" v-model="debugForm" @submit="handleSubmit" />
+    </el-dialog>
+
+    <patchDetails ref="patchDetails" />
+  </basic-container>
+</template>
+
+<script setup>
+import { ref, computed, watch } from 'vue'
+import { useStore } from 'vuex'
+import { useRouter } from 'vue-router'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { getListPage, getDetail, add, update, remove, enable, disable } from '@/api/resource/oss'
+import func from '@/utils/func'
+import patchDetails from '@/views/resource/components/patchDetails.vue'
+
+const store = useStore()
+const router = useRouter()
+
+// ===== state =====
+const form = ref({})
+const query = ref({})
+const loading = ref(true)
+const box = ref(false)
+
+const page = ref({
+  pageSize: 10,
+  currentPage: 1,
+  total: 0,
+})
+
+const selectionList = ref([])
+
+const option = ref({
+  addBtn: false,
+  tip: false,
+  searchShow: true,
+  searchMenuSpan: 8,
+  searchMenuPosition: 'right',
+  border: true,
+  index: true,
+  selection: true,
+  grid: false,
+  menuWidth: 240,
+  labelWidth: 100,
+  dialogWidth: 880,
+  dialogClickModal: false,
+  height: 'auto',
+  calcHeight: 20,
+  refreshBtn: false,
+  gridBtn: false,
+  searchShowBtn: false,
+  columnBtn: false,
+  viewBtn: false,
+  editBtn: false,
+  delBtn: false,
+  column: [
+    {
+      label: '文件名称',
+      prop: 'name',
+      span: 24,
+      search: true,
+      searchSpan: 4,
+      rules: [{ required: true, message: '请输入文件名称', trigger: 'blur' }],
+    },
+    {
+      label: '图斑类型',
+      prop: 'ossCode',
+      span: 24,
+      searchLabelWidth: 100,
+      search: true,
+      searchSpan: 4,
+      rules: [{ required: true, message: '请输入图斑类型', trigger: 'blur' }],
+    },
+    {
+      label: '图斑数量',
+      prop: 'ossCode',
+      span: 24,
+      rules: [{ required: true, message: '请输入图斑数量', trigger: 'blur' }],
+    },
+    {
+      label: '异常图斑数量',
+      prop: 'ossCode',
+      span: 24,
+      rules: [{ required: true, message: '请输入异常图斑数量', trigger: 'blur' }],
+    },
+    {
+      label: '行政区划',
+      prop: 'ossCode',
+      span: 24,
+      searchLabelWidth: 100,
+      search: true,
+      searchSpan: 4,
+      rules: [{ required: true, message: '请输入行政区划', trigger: 'blur' }],
+    },
+    {
+      label: '数据来源',
+      prop: 'ossCode',
+      span: 24,
+      rules: [{ required: true, message: '请输入数据来源', trigger: 'blur' }],
+    },
+    {
+      label: '创建时间',
+      prop: 'ossCode',
+      span: 24,
+      rules: [{ required: true, message: '请输入创建时间', trigger: 'blur' }],
+    },
+    {
+      label: '创建人',
+      prop: 'bucketName',
+      span: 24,
+      searchLabelWidth: 100,
+      search: true,
+      searchSpan: 4,
+      rules: [{ required: true, message: '请输入创建人', trigger: 'blur' }],
+    },
+  ],
+})
+
+const data = ref([])
+
+const debugForm = ref({
+  code: '',
+  backgroundUrl: '',
+})
+
+const debugOption = ref({
+  submitText: '提交',
+  column: [
+    {
+      label: '资源编号',
+      prop: 'code',
+      disabled: true,
+      span: 24,
+    },
+    {
+      label: '上传背景',
+      prop: 'backgroundUrl',
+      type: 'upload',
+      listType: 'picture-img',
+      dataType: 'string',
+      action: '/blade-resource/oss/endpoint/put-file',
+      propsHttp: { res: 'data', url: 'link' },
+      span: 24,
+    },
+  ],
+})
+
+// ===== refs =====
+const crudRef = ref(null)
+const uploadPatchDialog = ref(null)
+
+// ===== computed =====
+const userInfo = computed(() => store.getters.userInfo)
+const permission = computed(() => store.getters.permission)
+
+const permissionList = computed(() => ({
+  addBtn: !!permission.value.oss_add,
+  viewBtn: !!permission.value.oss_view,
+  delBtn: !!permission.value.oss_delete,
+  editBtn: !!permission.value.oss_edit,
+}))
+
+const ids = computed(() => selectionList.value.map(ele => ele.id).join(','))
+
+// ===== watch =====
+watch(
+  () => form.value.category,
+  () => {
+    const category = func.toInt(form.value.category)
+    option.value.column.forEach(item => {
+      if (item.prop === 'appId') {
+        item.display = category === 4
+      }
+    })
+  }
+)
+
+watch(
+  () => debugForm.value.code,
+  () => {
+    const column = debugOption.value.column.find(item => item.prop === 'backgroundUrl')
+    if (column) {
+      column.action = `/blade-resource/oss/endpoint/put-file?code=${debugForm.value.code}`
+    }
+  }
+)
+
+// ===== methods =====
+const rowSave = (row, done, loading) => {
+  add(row).then(
+    () => {
+      onLoad(page.value)
+      ElMessage.success('操作成功!')
+      done()
+    },
+    error => {
+      console.log(error)
+      loading()
+    }
+  )
+}
+
+const rowUpdate = (row, index, done, loading) => {
+  update(row).then(
+    () => {
+      onLoad(page.value)
+      ElMessage.success('操作成功!')
+      done()
+    },
+    error => {
+      console.log(error)
+      loading()
+    }
+  )
+}
+
+const rowDel = row => {
+  ElMessageBox.confirm('确定将选择数据删除?', '提示', {
+    confirmButtonText: '确定',
+    cancelButtonText: '取消',
+    type: 'warning',
+  })
+    .then(() => remove(row.id))
+    .then(() => {
+      onLoad(page.value)
+      ElMessage.success('操作成功!')
+    })
+}
+
+const searchReset = () => {
+  query.value = {}
+  onLoad(page.value)
+}
+
+const searchChange = (params, done) => {
+  query.value = params
+  page.value.currentPage = 1
+  onLoad(page.value, params)
+  done()
+}
+
+const selectionChange = list => {
+  selectionList.value = list
+}
+
+const selectionClear = () => {
+  selectionList.value = []
+  crudRef.value?.toggleSelection()
+}
+
+const handleEnable = row => {
+  ElMessageBox.confirm('是否确定启用这条配置?', '提示', {
+    confirmButtonText: '确定',
+    cancelButtonText: '取消',
+    type: 'warning',
+  })
+    .then(() => enable(row.id))
+    .then(() => {
+      onLoad(page.value)
+      ElMessage.success('操作成功!')
+      crudRef.value?.toggleSelection()
+    })
+}
+
+const handleDisable = row => {
+  ElMessageBox.confirm('是否确定禁用用这条配置?', '提示', {
+    confirmButtonText: '确定',
+    cancelButtonText: '取消',
+    type: 'warning',
+  })
+    .then(() => disable(row.id))
+    .then(() => {
+      onLoad(page.value)
+      ElMessage.success('操作成功!')
+      crudRef.value?.toggleSelection()
+    })
+}
+
+const handleDebug = row => {
+  box.value = true
+  debugForm.value.code = row.ossCode
+  debugForm.value.backgroundUrl = ''
+}
+
+const handleSubmit = (formData, done) => {
+  ElMessage.success(`获取到图片地址:[${formData.backgroundUrl}]`)
+  done()
+}
+
+const beforeOpen = (done, type) => {
+  if (['edit', 'view'].includes(type)) {
+    getDetail(form.value.id).then(res => {
+      form.value = res.data.data
+    })
+  }
+  done()
+}
+
+const currentChange = currentPage => {
+  page.value.currentPage = currentPage
+}
+
+const sizeChange = pageSize => {
+  page.value.pageSize = pageSize
+}
+
+const refreshChange = () => {
+  onLoad(page.value, query.value)
+}
+
+const onLoad = (pageInfo, params = {}) => {
+  loading.value = true
+  getListPage(pageInfo.currentPage, pageInfo.pageSize, Object.assign({}, params, query.value)).then(res => {
+    const d = res.data.data
+    page.value.total = d.total
+    data.value = d.records
+    loading.value = false
+    selectionClear()
+  })
+}
+
+// 上传图斑
+const uploadPatch = () => {
+  uploadPatchDialog.value.open()
+}
+
+// 跳转至图斑类型管理页面
+const goTypeManagement = () => {
+  router.push({ path: '/resource/patchTypeManagement' })
+}
+
+// 下载图斑
+const downloadPatch = () => {
+  crudRef.value?.download()
+}
+</script>
+
+<style scoped lang="scss"></style>
\ No newline at end of file
diff --git a/src/views/resource/patchTypeManagement.vue b/src/views/resource/patchTypeManagement.vue
new file mode 100644
index 0000000..893e486
--- /dev/null
+++ b/src/views/resource/patchTypeManagement.vue
@@ -0,0 +1,317 @@
+<template>
+  <basic-container>
+    <avue-crud :option="option" :table-loading="loading" :data="data" v-model:page="page" :permission="permissionList"
+      v-model="form" ref="crud" @row-update="rowUpdate" @row-save="rowSave" @row-del="rowDel" :before-open="beforeOpen"
+      @search-change="searchChange" @search-reset="searchReset" @selection-change="selectionChange"
+      @current-change="currentChange" @size-change="sizeChange" @refresh-change="refreshChange" @on-load="onLoad">
+      <template #search-menu="{ row, size }">
+        <el-button icon="el-icon-back" @click="goPatchManagement">
+          返回
+        </el-button>
+      </template>
+
+      <template #status="{ row }">
+        <el-tag>{{ row.statusName }}</el-tag>
+      </template>
+      <template #category="{ row }">
+        <el-tag>{{ row.categoryName }}</el-tag>
+      </template>
+    </avue-crud>
+    <el-dialog title="对象存储上传调试" append-to-body v-model="box" width="550px">
+      <avue-form ref="form" :option="debugOption" v-model="debugForm" @submit="handleSubmit" />
+    </el-dialog>
+  </basic-container>
+</template>
+
+<script setup>
+import { ref, computed, watch } from 'vue'
+import { getListPage, getDetail, add, update, remove, enable, disable } from '@/api/resource/oss'
+import { useStore } from 'vuex'
+import func from '@/utils/func'
+import { useRouter } from 'vue-router'
+
+const store = useStore()
+const router = useRouter()
+
+// ---------------- data ----------------
+const form = ref({})
+const query = ref({})
+const loading = ref(true)
+const box = ref(false)
+
+const page = ref({
+  pageSize: 10,
+  currentPage: 1,
+  total: 0,
+})
+
+const selectionList = ref([])
+
+const option = ref({
+  addBtn: true,
+  addBtnText: '新增类型',
+  tip: false,
+  searchShow: true,
+  searchMenuSpan: 16,
+  searchMenuPosition: 'right',
+  border: true,
+  index: true,
+  selection: true,
+  grid: false,
+  menuWidth: 180,
+  labelWidth: 100,
+  dialogWidth: 880,
+  dialogClickModal: false,
+  height: 'auto',
+  calcHeight: 20,
+  refreshBtn: false,
+  gridBtn: false,
+  searchShowBtn: false,
+  columnBtn: false,
+  column: [
+    {
+      label: '类型名称',
+      prop: 'name',
+      search: true,
+      searchSpan: 4,
+      rules: [
+        { required: true, message: '请输入类型名称', trigger: 'blur' },
+      ],
+    },
+    {
+      label: '创建时间',
+      prop: 'ossCode',
+      rules: [
+        { required: true, message: '请输入创建时间', trigger: 'blur' },
+      ],
+    },
+    {
+      label: '创建人',
+      prop: 'ossCode',
+      search: true,
+      searchSpan: 4,
+      rules: [
+        { required: true, message: '请输入创建人', trigger: 'blur' },
+      ],
+    },
+  ],
+})
+
+const data = ref([])
+
+const debugForm = ref({
+  code: '',
+})
+
+const debugOption = ref({
+  submitText: '提交',
+  column: [
+    {
+      label: '资源编号',
+      prop: 'code',
+      disabled: true,
+      span: 24,
+    },
+    {
+      label: '上传背景',
+      prop: 'backgroundUrl',
+      type: 'upload',
+      listType: 'picture-img',
+      dataType: 'string',
+      action: '/blade-resource/oss/endpoint/put-file',
+      propsHttp: { res: 'data', url: 'link' },
+      span: 24,
+    },
+  ],
+})
+
+// ---------------- watch ----------------
+watch(
+  () => form.value.category,
+  () => {
+    const category = func.toInt(form.value.category)
+    option.value.column.filter(item => {
+      if (item.prop === 'appId') {
+        item.display = category === 4
+      }
+    })
+  }
+)
+
+watch(
+  () => debugForm.value.code,
+  () => {
+    const column = findObject(debugOption.value.column, 'backgroundUrl')
+    column.action = `/blade-resource/oss/endpoint/put-file?code=${debugForm.value.code}`
+  }
+)
+
+// ---------------- computed ----------------
+const permission = computed(() => store.getters.permission)
+const permissionList = computed(() => ({
+  addBtn: validData(permission.value.oss_add),
+  viewBtn: validData(permission.value.oss_view),
+  delBtn: validData(permission.value.oss_delete),
+  editBtn: validData(permission.value.oss_edit),
+}))
+
+const ids = computed(() => {
+  return selectionList.value.map(ele => ele.id).join(',')
+})
+
+// ---------------- methods ----------------
+const rowSave = (row, done, loadingFn) => {
+  add(row).then(
+    () => {
+      onLoad(page.value)
+      ElMessage.success('操作成功!')
+      done()
+    },
+    error => {
+      console.log(error)
+      loadingFn()
+    }
+  )
+}
+
+const rowUpdate = (row, index, done, loadingFn) => {
+  update(row).then(
+    () => {
+      onLoad(page.value)
+      ElMessage.success('操作成功!')
+      done()
+    },
+    error => {
+      console.log(error)
+      loadingFn()
+    }
+  )
+}
+
+const rowDel = row => {
+  ElMessageBox.confirm('确定将选择数据删除?', '提示', {
+    confirmButtonText: '确定',
+    cancelButtonText: '取消',
+    type: 'warning',
+  })
+    .then(() => remove(row.id))
+    .then(() => {
+      onLoad(page.value)
+      ElMessage.success('操作成功!')
+    })
+}
+
+const searchReset = () => {
+  query.value = {}
+  onLoad(page.value)
+}
+
+const searchChange = (params, done) => {
+  query.value = params
+  page.value.currentPage = 1
+  onLoad(page.value, params)
+  done()
+}
+
+const selectionChange = list => {
+  selectionList.value = list
+}
+
+const selectionClear = () => {
+  selectionList.value = []
+  crudRef.value.toggleSelection()
+}
+
+const handleEnable = row => {
+  ElMessageBox.confirm('是否确定启用这条配置?', '提示', {
+    confirmButtonText: '确定',
+    cancelButtonText: '取消',
+    type: 'warning',
+  })
+    .then(() => enable(row.id))
+    .then(() => {
+      onLoad(page.value)
+      ElMessage.success('操作成功!')
+      crudRef.value.toggleSelection()
+    })
+}
+
+const handleDisable = row => {
+  ElMessageBox.confirm('是否确定禁用这条配置?', '提示', {
+    confirmButtonText: '确定',
+    cancelButtonText: '取消',
+    type: 'warning',
+  })
+    .then(() => disable(row.id))
+    .then(() => {
+      onLoad(page.value)
+      ElMessage.success('操作成功!')
+      crudRef.value.toggleSelection()
+    })
+}
+
+const handleDebug = row => {
+  box.value = true
+  debugForm.value.code = row.ossCode
+  debugForm.value.backgroundUrl = ''
+}
+
+const handleSubmit = (form, done) => {
+  ElMessage.success(`获取到图片地址:[${form.backgroundUrl}]`)
+  done()
+}
+
+const beforeOpen = (done, type) => {
+  if (['edit', 'view'].includes(type)) {
+    getDetail(form.value.id).then(res => {
+      form.value = res.data.data
+    })
+  }
+  done()
+}
+
+const currentChange = currentPage => {
+  page.value.currentPage = currentPage
+}
+
+const sizeChange = pageSize => {
+  page.value.pageSize = pageSize
+}
+
+const refreshChange = () => {
+  onLoad(page.value, query.value)
+}
+
+const onLoad = (pageParam, params = {}) => {
+  loading.value = true
+  getListPage(pageParam.currentPage, pageParam.pageSize, Object.assign(params, query.value)).then(res => {
+    const resData = res.data.data
+    page.value.total = resData.total
+    data.value = resData.records
+    loading.value = false
+    selectionClear()
+  })
+}
+
+// ---------------- extra ----------------
+const goPatchManagement = () => {
+  router.push({ path: '/resource/patchManagement' })
+}
+
+const addPatchType = () => {
+  // TODO: 新增图斑类型逻辑
+}
+
+// ---------------- utils ----------------
+function validData (value) {
+  return value !== undefined && value !== null && value !== false
+}
+
+function findObject (list, prop) {
+  return list.find(item => item.prop === prop)
+}
+
+const crudRef = ref(null)
+</script>
+
+<style scoped lang="scss"></style>
\ No newline at end of file

--
Gitblit v1.9.3