From 8bb2f8984e684b6baa3b4cc335ac034192f58ea1 Mon Sep 17 00:00:00 2001
From: 罗广辉 <guanghui.luo@foxmail.com>
Date: Wed, 01 Apr 2026 15:43:28 +0800
Subject: [PATCH] feat: 恢复资源配置

---
 applications/drone-command/src/views/resource/components/DrawPolygon.vue  |  312 +++++
 applications/drone-command/src/views/resource/oss.vue                     |  479 ++++++++
 applications/drone-command/src/views/resource/waylineFile.vue             |  235 ++++
 applications/drone-command/src/views/resource/sms.vue                     |  459 ++++++++
 applications/drone-command/src/views/resource/wayline.vue                 |  308 +++++
 applications/drone-command/src/views/resource/components/BoxSelect.vue    |  250 ++++
 applications/drone-command/src/views/resource/patchTypeManagement.vue     |  331 ++++++
 applications/drone-command/src/views/resource/attach.vue                  |  327 ++++++
 applications/drone-command/src/views/resource/components/FunButton.vue    |  151 ++
 applications/drone-command/src/views/resource/components/patchDetails.vue |   15 
 applications/drone-command/src/views/resource/media.vue                   |  361 ++++++
 11 files changed, 3,228 insertions(+), 0 deletions(-)

diff --git a/applications/drone-command/src/views/resource/attach.vue b/applications/drone-command/src/views/resource/attach.vue
new file mode 100644
index 0000000..7fca00b
--- /dev/null
+++ b/applications/drone-command/src/views/resource/attach.vue
@@ -0,0 +1,327 @@
+<template>
+  <basic-container>
+    <avue-crud :option="option" :table-loading="loading" :data="data" v-model:page="page" :permission="permissionList"
+      :before-open="beforeOpen" v-model="form" ref="crud" @row-del="rowDel" @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" plain v-if="permission.attach_upload" icon="el-icon-upload" @click="handleUpload">上 传
+        </el-button>
+        <el-button type="danger" icon="el-icon-delete" plain v-if="permission.attach_delete" @click="handleDelete">删 除
+        </el-button>
+      </template>
+      <template #menu="scope">
+        <el-button type="primary" text icon="el-icon-download" v-if="permission.attach_download"
+          @click="handleDownload(scope.row)">下载
+        </el-button>
+      </template>
+      <template #attachSize="{ row }">
+        <el-tag>{{ `${func.bytesToKB(row.attachSize)} KB` }}</el-tag>
+      </template>
+    </avue-crud>
+    <el-dialog title="附件管理" append-to-body v-model="attachBox" width="555px">
+      <avue-form ref="form" :option="attachOption" v-model="attachForm" :upload-after="uploadAfter">
+      </avue-form>
+    </el-dialog>
+  </basic-container>
+</template>
+
+<script>
+import { getList, getDetail, removeAttachAndData } from '@/api/resource/attach'
+import { mapGetters } from 'vuex'
+import func from '@/utils/func'
+
+export default {
+  data () {
+    return {
+      form: {},
+      query: {},
+      loading: true,
+      page: {
+        pageSize: 10,
+        currentPage: 1,
+        total: 0,
+      },
+      attachBox: false,
+      selectionList: [],
+      option: {
+        emptyBtnText: '重置',
+        emptyBtnIcon: 'el-icon-refresh',
+        tip: false,
+        searchShow: true,
+        searchGutter: 30,
+        searchMenuPosition: 'left',
+        searchMenuSpan: 4,
+        border: true,
+        index: true,
+        viewBtn: true,
+        // grid: true,
+        selection: true,
+        dialogClickModal: false,
+
+        addBtn: false,
+
+        height: 'auto',
+        calcHeight: 20,
+        column: [
+          {
+            label: '附件地址',
+            prop: 'link',
+            rules: [
+              {
+                required: true,
+                message: '请输入附件地址',
+                trigger: 'blur',
+              },
+            ],
+          },
+          {
+            label: '附件域名',
+            prop: 'domainUrl',
+            rules: [
+              {
+                required: true,
+                message: '请输入附件域名',
+                trigger: 'blur',
+              },
+            ],
+          },
+          {
+            label: '附件名称',
+            prop: 'name',
+            search: true,
+            searchSpan: 4,
+            rules: [
+              {
+                required: true,
+                message: '请输入附件名称',
+                trigger: 'blur',
+              },
+            ],
+          },
+          {
+            label: '附件原名',
+            prop: 'originalName',
+            search: true,
+            searchSpan: 4,
+            rules: [
+              {
+                required: true,
+                message: '请输入附件原名',
+                trigger: 'blur',
+              },
+            ],
+          },
+          {
+            label: '附件拓展名',
+            prop: 'extension',
+            search: true,
+            searchSpan: 4,
+            searchLabelWidth: 95,
+            rules: [
+              {
+                required: true,
+                message: '请输入附件拓展名',
+                trigger: 'blur',
+              },
+            ],
+          },
+          {
+            label: '附件大小',
+            prop: 'attachSize',
+            slot: true,
+            rules: [
+              {
+                required: true,
+                message: '请输入附件大小',
+                trigger: 'blur',
+              },
+            ],
+          },
+          {
+            label: '创建日期',
+            prop: 'createTime'
+          },
+          {
+            label: '创建日期',
+            prop: 'daterange',
+            type: 'daterange',
+            search: true,
+            searchRange: true,
+            searchSpan: 8,
+            format: 'YYYY-MM-DD',
+            valueFormat: 'YYYY-MM-DD',
+            startPlaceholder: '开始时间',
+            endPlaceholder: '结束时间',
+            viewDisplay: false,
+            hide: true,
+            rules: [
+              {
+                required: true,
+                message: '请选择操作时间',
+                trigger: 'blur',
+              },
+            ],
+              change: (value) => {
+               this.searchChange({ ...this.query,daterange:value.value}, () => {});
+            }
+          },
+        ],
+      },
+      data: [],
+      attachForm: {},
+      attachOption: {
+        submitBtn: false,
+        emptyBtn: false,
+        column: [
+          {
+            label: '附件上传',
+            prop: 'attachFile',
+            type: 'upload',
+            drag: true,
+            loadText: '模板上传中,请稍等',
+            span: 24,
+            propsHttp: {
+              res: 'data',
+            },
+            action: '/blade-resource/oss/endpoint/put-file-attach',
+          },
+        ],
+      },
+    }
+  },
+  computed: {
+    func () {
+      return func
+    },
+    ...mapGetters(['permission']),
+    permissionList () {
+      return {
+        addBtn: false,
+        editBtn: false,
+        viewBtn: false,
+        delBtn: this.validData(this.permission.attach_delete, false),
+      }
+    },
+    ids () {
+      let ids = []
+      this.selectionList.forEach(ele => {
+        ids.push(ele.id)
+      })
+      return ids.join(',')
+    },
+  },
+  methods: {
+    handleUpload () {
+      this.attachBox = true
+    },
+    uploadAfter (res, done, loading, column) {
+      window.console.log(column)
+      this.attachBox = false
+      this.refreshChange()
+      done()
+    },
+    handleDownload (row) {
+      window.open(`${row.link}`)
+    },
+    rowDel (row) {
+      this.$confirm('确定将选择数据及对应的文件删除?', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning',
+      })
+        .then(() => {
+          return removeAttachAndData(row.id)
+        })
+        .then(() => {
+          this.onLoad(this.page)
+          this.$message({
+            type: 'success',
+            message: '操作成功!',
+          })
+        })
+    },
+    handleDelete () {
+      if (this.selectionList.length === 0) {
+        this.$message.warning('请选择至少一条数据')
+        return
+      }
+      this.$confirm('确定将选择数据及对应的文件删除?', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning',
+      })
+        .then(() => {
+          return removeAttachAndData(this.ids)
+        })
+        .then(() => {
+          this.onLoad(this.page)
+          this.$message({
+            type: 'success',
+            message: '操作成功!',
+          })
+          this.$refs.crud.toggleSelection()
+        })
+    },
+    beforeOpen (done, type) {
+      if (['edit', 'view'].includes(type)) {
+        getDetail(this.form.id).then(res => {
+          this.form = res.data.data
+        })
+      }
+      done()
+    },
+    searchReset () {
+      this.query = {}
+      this.onLoad(this.page)
+    },
+    searchChange (params, done) {
+      this.query = params
+      this.page.currentPage = 1
+      this.onLoad(this.page, params)
+      done()
+    },
+    selectionChange (list) {
+      this.selectionList = list
+    },
+    selectionClear () {
+      this.selectionList = []
+      this.$refs.crud.toggleSelection()
+    },
+    currentChange (currentPage) {
+      this.page.currentPage = currentPage
+    },
+    sizeChange (pageSize) {
+      this.page.pageSize = pageSize
+    },
+    refreshChange () {
+      this.onLoad(this.page, this.query)
+    },
+    onLoad (page, params = {}) {
+      const { daterange } = this.query
+      let values = {
+        ...params,
+        ...this.query,
+      }
+      if (daterange) {
+        values = {
+          ...values,
+          startTime: daterange[0],
+          endTime: daterange[1],
+        }
+        values.daterange = null
+      }
+      this.loading = true
+      getList(page.currentPage, page.pageSize, values).then(res => {
+        const data = res.data.data
+        this.page.total = data.total
+        this.data = data.records
+        this.loading = false
+        this.selectionClear()
+      })
+    },
+  },
+}
+</script>
+
+<style scoped lang="scss"></style>
diff --git a/applications/drone-command/src/views/resource/components/BoxSelect.vue b/applications/drone-command/src/views/resource/components/BoxSelect.vue
new file mode 100644
index 0000000..87a9858
--- /dev/null
+++ b/applications/drone-command/src/views/resource/components/BoxSelect.vue
@@ -0,0 +1,250 @@
+<template>
+	<div></div>
+</template>
+
+<script setup>
+import * as Cesium from 'cesium'
+import { ElMessage } from 'element-plus'
+
+let curPolygonPosition = []
+let savePolygonPosition = []
+let curPolygon = null
+let curPolygonEntity = null
+let curPolygonObj = null
+let menuPanel = null
+let saveMenuPanel = null
+
+const viewInstance = inject('viewInstance')
+const homeViewer = inject('homeViewer')
+
+const updateBoxSelect = inject('updateBoxSelect')
+
+function preventDefault(event) {
+	event.preventDefault()
+
+	return
+}
+
+function init() {
+	// draw.drawPlane();
+	curPolygon = new Cesium.PolygonHierarchy()
+	curPolygonEntity = new Cesium.Entity()
+
+	homeViewer.value.scene.globe.depthTestAgainstTerrain = true
+
+	viewInstance.value.addLeftClickEvent(null, addPlanerPointEvent, 'drawCustomPolygon')
+	viewInstance.value.addRightClickEvent(null, addPlanarMenuEvent, 'drawCustomPolygon')
+	viewInstance.value.addMouseHandler(null, mouseMoveEvent, 'drawCustomPolygon')
+}
+
+function mouseMoveEvent(movement, viewer) {
+	const mouseInfo = viewInstance.value.getMouseInfoAll(movement.endPosition)
+
+	const { x, y } = mouseInfo.windowPosition
+
+	if (!menuPanel) {
+		menuPanel = createMenuPanel()
+		viewer.container.appendChild(menuPanel)
+	}
+
+	menuPanel.style.transform = `translate3d(${x}px, ${y}px, 0)`
+
+	if (
+		curPolygonObj &&
+		mouseInfo.surfacePosition &&
+		mouseInfo.surfacePosition != null &&
+		curPolygonPosition.length >= 1
+	) {
+		curPolygonPosition.pop()
+		curPolygonPosition.push(mouseInfo.surfacePosition)
+
+		curPolygon.positions.pop()
+		curPolygon.positions.push(mouseInfo.surfacePosition)
+	}
+}
+
+function addPlanerPointEvent(click, pick) {
+	const { position } = click
+
+	const mouseInfo = viewInstance.value.getMouseInfoAll(position)
+
+	if (mouseInfo.surfacePosition && mouseInfo.surfacePosition != null) drawCustomPolygon(mouseInfo)
+}
+
+function addPlanarMenuEvent(click, pick, viewer) {
+	if (savePolygonPosition.length <= 2) {
+		ElMessage.warning('当前绘制无法形成面,请继续绘制或取消绘制!!')
+
+		return
+	}
+	const { position } = click
+
+	const mouseInfo = viewInstance.value.getMouseInfoAll(position)
+
+	remove()
+	removeMenuPanel()
+
+	if (mouseInfo.surfacePosition && mouseInfo.surfacePosition != null) drawCustomPolygon(mouseInfo)
+
+	const { x, y } = position
+	saveMenuPanel = createSaveMenuPanel()
+	saveMenuPanel.addEventListener('click', saveMenuClickEvent)
+	saveMenuPanel.style.transform = `translate3d(${x}px, ${y}px, 0)`
+	viewer.container.appendChild(saveMenuPanel)
+}
+
+function createSaveMenuPanel() {
+	const menuPanel = document.createElement('div')
+	menuPanel.className = 'planar-menu-panel'
+
+	const arr = [
+		{ title: '保存框选', class: 'save-draw' },
+		{ title: '清除框选', class: 'anew-draw' },
+	]
+
+	arr.forEach(item => {
+		const title = document.createElement('div')
+		title.innerText = item.title
+		title.className = item.class
+		menuPanel.appendChild(title)
+	})
+
+	return menuPanel
+}
+
+const emit = defineEmits(['upDateBoxSelect'])
+
+function saveMenuClickEvent(e) {
+	const className = e.target.className
+	if (className === 'save-draw') {
+		emit('upDateBoxSelect')
+		updateBoxSelect(savePolygonPosition)
+	}
+	if (className == 'anew-draw') {
+		cancel()
+	}
+
+	removeSaveMenuPanel()
+}
+
+function removeSaveMenuPanel() {
+	if (saveMenuPanel) {
+		homeViewer.value.container?.removeChild(saveMenuPanel)
+		saveMenuPanel.removeEventListener('click', saveMenuClickEvent)
+		saveMenuPanel = null
+	}
+}
+
+function createMenuPanel() {
+	const menuPanel = document.createElement('div')
+	menuPanel.className = 'planar-menu-panel'
+
+	const title = document.createElement('div')
+	title.innerText = '左击选择点位,右击结束'
+	title.className = 'proof-custom-add-polygon'
+	menuPanel.appendChild(title)
+
+	return menuPanel
+}
+
+function removeMenuPanel() {
+	if (menuPanel) {
+		homeViewer.value.container?.removeChild(menuPanel)
+		menuPanel = null
+	}
+}
+
+function drawCustomPolygon(point) {
+	if (curPolygonPosition.length == 0) {
+		curPolygonPosition.push(point.surfacePosition.clone())
+		curPolygon.positions.push(point.surfacePosition.clone())
+	}
+
+	savePolygonPosition.push(point.wgs84SurfacePosition)
+	curPolygonPosition.push(point.surfacePosition.clone())
+	curPolygon.positions.push(point.surfacePosition.clone())
+
+	if (!curPolygonObj) {
+		curPolygonEntity.polyline = {
+			width: 2,
+			material: Cesium.Color.WHITE,
+			clampToGround: false,
+		}
+
+		curPolygonEntity.polyline.positions = new Cesium.CallbackProperty(function () {
+			return curPolygonPosition
+		}, false)
+
+		curPolygonEntity.polygon = {
+			hierarchy: new Cesium.CallbackProperty(function () {
+				return curPolygon
+			}, false),
+
+			material: Cesium.Color.fromCssColorString('#00ffff').withAlpha(0.4),
+			clampToGround: false,
+		}
+		curPolygonEntity.name = 'customProofPolygon'
+
+		curPolygonEntity._id = 'customProofPolygon'
+		curPolygonObj = homeViewer.value.entities.add(curPolygonEntity)
+	}
+}
+
+function clearPlanar() {
+	homeViewer.value.entities.remove(curPolygonObj)
+	curPolygonObj = null
+	curPolygon = null
+	curPolygonEntity = null
+	curPolygonPosition = []
+	savePolygonPosition = []
+}
+
+function remove() {
+	viewInstance.value.removeLeftClickEvent('drawCustomPolygon')
+	viewInstance.value.removeRightClickEvent('drawCustomPolygon')
+	viewInstance.value.removeMouseHandler('drawCustomPolygon')
+}
+
+function cancel() {
+	remove()
+	removeMenuPanel()
+	clearPlanar()
+
+	init()
+}
+
+onMounted(() => {
+	document.addEventListener('contextmenu', preventDefault)
+
+	init()
+})
+
+onBeforeUnmount(() => {
+	document.removeEventListener('contextmenu', preventDefault)
+
+	remove()
+	removeMenuPanel()
+	removeSaveMenuPanel()
+	clearPlanar()
+	homeViewer.value.scene.globe.depthTestAgainstTerrain = false
+})
+</script>
+
+<style lang="scss" scoped>
+p {
+	text-indent: 2em;
+	color: #ffffff;
+	font-size: 18px;
+}
+
+.export-btns {
+	margin-top: 44px;
+	display: flex;
+	align-items: center;
+	justify-content: space-around;
+
+	.el-button {
+		background-color: transparent !important;
+	}
+}
+</style>
diff --git a/applications/drone-command/src/views/resource/components/DrawPolygon.vue b/applications/drone-command/src/views/resource/components/DrawPolygon.vue
new file mode 100644
index 0000000..59428dd
--- /dev/null
+++ b/applications/drone-command/src/views/resource/components/DrawPolygon.vue
@@ -0,0 +1,312 @@
+<template>
+  <div></div>
+</template>
+
+<script setup>
+import * as Cesium from 'cesium';
+
+import { ElMessage } from 'element-plus';
+import { sdfwUpdate } from '@/api/home/territory';
+
+let curPolygonPosition = [];
+let savePolygonPosition = [];
+let curPolygon = null;
+let curPolygonEntity = null;
+let curPolygonObj = null;
+
+let menuPanel = null;
+let saveMenuPanel = null;
+
+const selectionIds = inject('selectionIds');
+const clearSelect = inject('clearSelect');
+const getTableList = inject('getTableList')
+
+const viewInstance = inject('viewInstance');
+const homeViewer = inject('homeViewer');
+watch(
+  () => selectionIds.value,
+  (newValue, oldValue) => {
+    if (newValue && oldValue && newValue !== oldValue) {
+      cancel();
+    }
+  }
+);
+function preventDefault(event) {
+  event.preventDefault();
+
+  return;
+}
+
+function init() {
+  // let draw = new Draw(homeViewer.value);
+  // draw.drawPlane();
+  curPolygon = new Cesium.PolygonHierarchy();
+  curPolygonEntity = new Cesium.Entity();
+  homeViewer.value.scene.globe.depthTestAgainstTerrain = true;
+
+  viewInstance.value.addLeftClickEvent(null, addPlanerPointEvent, 'drawCustomPolygon');
+  viewInstance.value.addRightClickEvent(null, addPlanarMenuEvent, 'drawCustomPolygon');
+  viewInstance.value.addMouseHandler(null, mouseMoveEvent, 'drawCustomPolygon');
+}
+
+function mouseMoveEvent(movement, viewer) {
+  const mouseInfo = viewInstance.value.getMouseInfoAll(movement.endPosition);
+
+  const { x, y } = mouseInfo.windowPosition;
+
+  if (!menuPanel) {
+    menuPanel = createMenuPanel();
+    viewer.container.appendChild(menuPanel);
+  }
+
+  menuPanel.style.transform = `translate3d(${x}px, ${y}px, 0)`;
+
+  if (
+    curPolygonObj &&
+    mouseInfo.surfacePosition &&
+    mouseInfo.surfacePosition != null &&
+    curPolygonPosition.length >= 1
+  ) {
+    curPolygonPosition.pop();
+    curPolygonPosition.push(mouseInfo.surfacePosition);
+
+    curPolygon.positions.pop();
+    curPolygon.positions.push(mouseInfo.surfacePosition);
+  }
+}
+
+function addPlanerPointEvent(click, pick) {
+  const { position } = click;
+
+  const mouseInfo = viewInstance.value.getMouseInfoAll(position);
+
+  if (mouseInfo.surfacePosition && mouseInfo.surfacePosition != null) drawCustomPolygon(mouseInfo);
+}
+
+function addPlanarMenuEvent(click, pick, viewer) {
+  if (savePolygonPosition.length <= 2) {
+    ElMessage.warning('当前绘制无法形成面,请继续绘制或取消绘制!!');
+
+    return;
+  }
+  const { position } = click;
+
+  const mouseInfo = viewInstance.value.getMouseInfoAll(position);
+
+  remove();
+  removeMenuPanel();
+
+  if (mouseInfo.surfacePosition && mouseInfo.surfacePosition != null) drawCustomPolygon(mouseInfo);
+
+  const { x, y } = position;
+  saveMenuPanel = createSaveMenuPanel();
+  saveMenuPanel.addEventListener('click', saveMenuClickEvent);
+  saveMenuPanel.style.transform = `translate3d(${x}px, ${y}px, 0)`;
+  viewer.container.appendChild(saveMenuPanel);
+}
+
+function createSaveMenuPanel() {
+  const menuPanel = document.createElement('div');
+  menuPanel.className = 'planar-menu-panel';
+
+  const arr = [
+    { title: '提交绘制', class: 'save-draw' },
+    { title: '重新绘制', class: 'anew-draw' },
+  ];
+
+  arr.forEach(item => {
+    const title = document.createElement('div');
+    title.innerText = item.title;
+    title.className = item.class;
+    menuPanel.appendChild(title);
+  });
+
+  return menuPanel;
+}
+
+function saveMenuClickEvent(e) {
+  const className = e.target.className;
+  if (className === 'save-draw') {
+    updatePolygon();
+  }
+  if (className == 'anew-draw') {
+    cancel();
+  }
+
+  removeSaveMenuPanel();
+}
+
+function removeSaveMenuPanel() {
+  if (saveMenuPanel) {
+    homeViewer.value.container?.removeChild(saveMenuPanel);
+    saveMenuPanel.removeEventListener('click', saveMenuClickEvent);
+    saveMenuPanel = null;
+  }
+}
+
+function createMenuPanel() {
+  const menuPanel = document.createElement('div');
+  menuPanel.className = 'planar-menu-panel';
+
+  const title = document.createElement('div');
+  title.innerText = '左击选择点位,右击结束';
+  title.className = 'proof-custom-add-polygon';
+  menuPanel.appendChild(title);
+
+  return menuPanel;
+}
+
+function removeMenuPanel() {
+  if (menuPanel) {
+    homeViewer.value.container?.removeChild(menuPanel);
+    menuPanel = null;
+  }
+}
+
+function drawCustomPolygon(point) {
+  if (curPolygonPosition.length == 0) {
+    curPolygonPosition.push(point.surfacePosition.clone());
+    curPolygon.positions.push(point.surfacePosition.clone());
+  }
+
+  savePolygonPosition.push(point.wgs84SurfacePosition);
+  curPolygonPosition.push(point.surfacePosition.clone());
+  curPolygon.positions.push(point.surfacePosition.clone());
+
+  if (!curPolygonObj) {
+    curPolygonEntity.polyline = {
+      width: 2,
+      material: Cesium.Color.WHITE,
+      clampToGround: true,
+    };
+
+    curPolygonEntity.polyline.positions = new Cesium.CallbackProperty(function () {
+      return curPolygonPosition;
+    }, false);
+
+    curPolygonEntity.polygon = {
+      hierarchy: new Cesium.CallbackProperty(function () {
+        return curPolygon;
+      }, false),
+
+      material: Cesium.Color.fromCssColorString('#00ffff').withAlpha(0.4),
+      clampToGround: false,
+    };
+    curPolygonEntity.name = 'customProofPolygon';
+
+    curPolygonEntity._id = 'customProofPolygon';
+    curPolygonObj = homeViewer.value.entities.add(curPolygonEntity);
+  }
+}
+
+function clearPlanar() {
+  homeViewer.value.entities.remove(curPolygonObj);
+  curPolygonObj = null;
+  curPolygon = null;
+  curPolygonEntity = null;
+  curPolygonPosition = [];
+  savePolygonPosition = [];
+}
+
+function remove() {
+  viewInstance.value.removeLeftClickEvent('drawCustomPolygon');
+  viewInstance.value.removeRightClickEvent('drawCustomPolygon');
+  viewInstance.value.removeMouseHandler('drawCustomPolygon');
+}
+
+const emit = defineEmits(['upDateDrawState',]);
+
+function updatePolygon() {
+  let polygon = savePolygonPosition.reduce((pre, cur) => {
+    pre.push(`${cur.lng} ${cur.lat}`);
+    return pre;
+  }, []);
+  polygon.push(polygon[0]);
+
+  // 接口
+  sdfwUpdate({
+  	ids: selectionIds.value,
+  	sdfw: `POLYGON((${polygon.join(',')}))`,
+  })
+  	.then(res => {
+  		if (selectionIds.value) {
+  			emit('upDateDrawState')
+  			selectionIds.value = null
+  			clearSelect()
+  		}
+
+  		if (res.data.code !== 0) return ElMessage.error(res.message)
+  		ElMessage.success('重新绘制图斑成功!!')
+  		getTableList()
+  	})
+  	.catch(e => {
+  		cancel()
+  	})
+}
+
+
+function cancel() {
+  remove();
+  removeMenuPanel();
+  clearPlanar();
+  removeSaveMenuPanel();
+  init();
+}
+
+onMounted(() => {
+  document.addEventListener('contextmenu', preventDefault);
+
+  init();
+});
+
+onBeforeUnmount(() => {
+  document.removeEventListener('contextmenu', preventDefault);
+
+  remove();
+  removeMenuPanel();
+  removeSaveMenuPanel();
+  clearPlanar();
+
+  homeViewer.value.scene.globe.depthTestAgainstTerrain = false;
+});
+</script>
+
+<style lang="scss" scoped>
+p {
+  text-indent: 2em;
+  color: #ffffff;
+  font-size: 18px;
+}
+
+.export-btns {
+  margin-top: 44px;
+  display: flex;
+  align-items: center;
+  justify-content: space-around;
+
+  .el-button {
+    background-color: transparent !important;
+  }
+}
+.proof-custom-add-polygon {
+  position: absolute;
+  left: 20px;
+  background: rgba(0, 0, 0, 0.6);
+  white-space: nowrap;
+  border-radius: 4px;
+pointer-events: none;
+  &::after {
+    content: '';
+    position: absolute;
+    left: -3px;
+    top: calc(50% - 2px);
+    width: 0;
+    height: 0;
+    border-top: 4px solid rgba(0, 0, 0, 0.6);
+    border-left: 4px solid rgba(0, 0, 0, 0.6);
+    border-right: 4px solid transparent;
+    border-bottom: 4px solid transparent;
+    transform: rotate(-45deg);
+  }
+}
+</style>
diff --git a/applications/drone-command/src/views/resource/components/FunButton.vue b/applications/drone-command/src/views/resource/components/FunButton.vue
new file mode 100644
index 0000000..4a4c38e
--- /dev/null
+++ b/applications/drone-command/src/views/resource/components/FunButton.vue
@@ -0,0 +1,151 @@
+<template>
+	<div class="fun-button-container">
+		<template v-for="(item, index) in tooltipList" :key="index">
+			<el-popover
+				effect="dark"
+				placement="right"
+				:trigger="item.isClick ? 'click' : 'hover'"
+				popper-class="function-popover"
+				v-if="item.show"
+			>
+				<div>
+					<div>{{ item.label[Number(item.isSelected)] }}</div>
+				</div>
+				<template #reference v-if="item.show">
+					<div
+						:class="['currency-box', { 'actived-blue': item.isSelected && item.isSelectColor && item.showActive }]"
+						@click="item.event(item, index)"
+					>
+						<img :src="item.icon[Number(item.isSelected)]" alt="_icon" />
+					</div>
+				</template>
+			</el-popover>
+		</template>
+	</div>
+
+	<BoxSelect v-if="isShowBoxSelect" @upDateBoxSelect="upDateBoxSelect" />
+	<DrawPolygon v-if="isShowDrawPolygon" @upDateDrawState="upDateDrawState" />
+</template>
+
+<script setup>
+import { useStore } from 'vuex'
+import drawPng from '@/assets/images/home/territory/draw.png'
+import noDrawPng from '@/assets/images/home/territory/no-draw.png'
+import boxSelectPng from '@/assets/images/home/territory/box-select.png'
+
+import BoxSelect from './BoxSelect.vue'
+import DrawPolygon from './DrawPolygon.vue'
+
+const isBoxSelect = defineModel('isBoxSelect')
+const isDrawPolygon = defineModel('isDrawPolygon')
+const isShowBoxSelect = ref(false)
+const isShowDrawPolygon = ref(false)
+
+const isDrawPolygonSelect = ref(false)
+
+watch(isBoxSelect, newVal => {
+	if (newVal === false) {
+		isShowBoxSelect.value = newVal
+
+		setBoxSelect(null, 1, newVal)
+	}
+})
+
+watch(isDrawPolygon, newVal => {
+	if (newVal === false) {
+		isShowDrawPolygon.value = newVal
+
+		setDrawState(null, 0, newVal)
+	}
+})
+
+const tooltipList = ref([
+	{
+		showActive: true,
+		label: ['重新绘制', '取消绘制'],
+		isSelectColor: true,
+		isSelected: false,
+		icon: [drawPng, noDrawPng],
+		event: setDrawState,
+		show: isDrawPolygon,
+	},
+
+	{
+		showActive: true,
+		label: ['框选', '取消框选'],
+		isSelectColor: true,
+		isSelected: false,
+		icon: [boxSelectPng, boxSelectPng],
+		event: setBoxSelect,
+		show: isBoxSelect,
+	},
+])
+
+function setDrawState(value, index, show = null) {
+	if (show != null) {
+		tooltipList.value[index].isSelected = show
+	} else {
+		tooltipList.value[index].isSelected = !tooltipList.value[index].isSelected
+	}
+
+	isShowDrawPolygon.value = tooltipList.value[index].isSelected
+	isDrawPolygonSelect.value = tooltipList.value[index].isSelected
+}
+
+function upDateDrawState(show = false) {
+	setDrawState(null, 0, show)
+}
+
+function setBoxSelect(value, index, show = null) {
+	if (show != null) {
+		tooltipList.value[index].isSelected = show
+	} else {
+		tooltipList.value[index].isSelected = !tooltipList.value[index].isSelected
+	}
+
+	isShowBoxSelect.value = tooltipList.value[index].isSelected
+}
+
+function upDateBoxSelect(show = false) {
+	setBoxSelect(null, 1, show)
+}
+
+defineExpose({
+	upDateDrawState,
+	isDrawPolygonSelect,
+})
+</script>
+
+<style lang="scss" scoped>
+.fun-button-container {
+	position: absolute;
+	top: 50px;
+	left: 23%;
+	transform: translate(100%, 0);
+}
+
+.currency-box {
+	width: 45px;
+	height: 45px;
+	border-radius: 3px;
+	background: url('@/assets/images/home/territory/newcon_box.png');
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	overflow: hidden;
+	cursor: pointer;
+	margin-bottom: 8px;
+	pointer-events: all;
+
+	img {
+		width: 31px;
+		// height: 31px;
+	}
+}
+
+.actived-blue {
+	background-image: none;
+	// background-color: #409eff;
+	background-color: rgba(23, 124, 198, 0.7);
+}
+</style>
diff --git a/applications/drone-command/src/views/resource/components/patchDetails.vue b/applications/drone-command/src/views/resource/components/patchDetails.vue
new file mode 100644
index 0000000..343df2c
--- /dev/null
+++ b/applications/drone-command/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/applications/drone-command/src/views/resource/media.vue b/applications/drone-command/src/views/resource/media.vue
new file mode 100644
index 0000000..a49c4b4
--- /dev/null
+++ b/applications/drone-command/src/views/resource/media.vue
@@ -0,0 +1,361 @@
+<template>
+  <basic-container>
+    <avue-crud :option="option" :table-loading="loading" :data="data" v-model:page="page" :permission="permissionList"
+      v-model="form" ref="crud" @row-del="rowDel" @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-download" plain :loading="isDownloading" @click="handleZipDownload">{{
+          isDownloading ? '压缩下载中...' : '压缩下载' }}</el-button>
+      </template>
+      <template #menu="scope">
+        <el-button type="primary" text icon="el-icon-view" v-if="permission.attach_delete"
+          @click="handleRowView(scope.row)">查看
+        </el-button>
+        <el-button type="success" text icon="el-icon-download" v-if="permission.attach_delete"
+          @click="handleRowDownload(scope.row)">下载
+        </el-button>
+        <el-button type="danger" text icon="el-icon-delete" v-if="permission.attach_delete"
+          @click="handleRowDelete(scope.row)">删除
+        </el-button>
+      </template>
+    </avue-crud>
+    <el-dialog title="媒体文件" append-to-body v-model="taskBox" width="555px">
+      <avue-form ref="form" :option="taskOption" v-model="taskForm" :upload-after="uploadAfter">
+      </avue-form>
+    </el-dialog>
+  </basic-container>
+</template>
+
+<script>
+import JSZip from 'jszip'
+import { getMedia, deleteAllFile } from '@/api/resource/media'
+import { getAirportList } from '@/api/device/device'
+import { mapGetters } from 'vuex'
+import func from '@/utils/func'
+
+export default {
+  data () {
+    const endDate = new Date()
+    const startDate = new Date()
+    startDate.setMonth(startDate.getMonth() - 1)
+    const defaultStartDate = `${startDate.getFullYear()}-${String(startDate.getMonth() + 1).padStart(2, '0')}-${String(startDate.getDate()).padStart(2, '0')}`
+    const defaultEndDate = `${endDate.getFullYear()}-${String(endDate.getMonth() + 1).padStart(2, '0')}-${String(endDate.getDate()).padStart(2, '0')}`
+
+    return {
+      form: {},
+      query: {
+        daterange: [defaultStartDate, defaultEndDate]
+      },
+      loading: true,
+      page: {
+        pageSize: 10,
+        currentPage: 1,
+        total: 0,
+      },
+      taskBox: false,
+      selectionList: [],
+      selectedWorkspaceId: null,
+      option: {
+        tip: false,
+        searchShow: true,
+        searchGutter: 30,
+        searchMenuPosition: 'left',
+        searchMenuSpan: 4,
+        border: true,
+        index: true,
+        viewBtn: true,
+        selection: true,
+        dialogClickModal: false,
+        delBtn: false,
+        addBtn: false,
+
+        height: 'auto',
+        calcHeight: 20,
+        column: [
+          { label: '机场选择', prop: 'airportNickname', type: 'select', search: true, searchSpan: 4, dicData: [], props: { label: 'nickname', value: 'nickname' }, hide: true, change: (value) => {
+               this.searchChange({ ...this.query,airportNickname:value.value}, () => {});
+          } },
+          { label: '创建日期', prop: 'daterange', type: 'daterange', search: true, searchRange: true, searchSpan: 8, format: 'YYYY-MM-DD', valueFormat: 'YYYY-MM-DD', startPlaceholder: '开始时间', endPlaceholder: '结束时间', viewDisplay: false, hide: true, searchValue: [defaultStartDate, defaultEndDate]
+           , change: (value) => {
+               this.searchChange({ ...this.query,daterange:value.value}, () => {});
+          }
+          },
+          { label: '任务名称', prop: 'job_name' },
+          { label: '文件名称', prop: 'file_name' },
+          { label: '拍摄时间', prop: 'create_time' },
+          { label: '文件路径', prop: 'object_key' },
+          { label: '设备负载', prop: 'payload' },
+        ],
+      },
+      data: [],
+      taskForm: {},
+      taskOption: {
+        submitBtn: false,
+        emptyBtn: false,
+        column: [
+          { label: '任务上传', prop: 'taskFile', type: 'upload', drag: true, loadText: '任务上传中,请稍等', span: 24, propsHttp: { res: 'data' }, action: '/blade-resource/oss/endpoint/put-file-task' },
+        ],
+      },
+      domainUrl: '',
+      isDownloading: false, // 控制按钮加载状态
+    }
+  },
+  computed: {
+    func () { return func },
+    ...mapGetters(['permission']),
+    permissionList () {
+      return {
+        addBtn: false,
+        editBtn: false,
+        viewBtn: this.validData(this.permission.attach_view, false),
+        delBtn: this.validData(this.permission.attach_delete, false),
+      }
+    },
+    ids () { return this.selectionList.map(ele => ele.id).join(',') },
+  },
+  mounted () {
+    this.loadAirportList()
+    this.$refs.crud.setSearch({ daterange: this.query.daterange })
+    this.onLoad(this.page, this.query)
+  },
+  methods: {
+    loadAirportList () {
+      getAirportList().then(res => {
+        const airportData = res.data.data || []
+        const airportColumn = this.option.column.find(col => col.prop === 'airportNickname')
+        if (airportColumn) {
+          airportColumn.dicData = airportData.map(item => ({
+            nickname: item.nickname,
+            workspace_id: item.workspace_id,
+          }))
+        }
+      }).catch(error => {
+        console.error('加载机场列表时出错:', error)
+      })
+    },
+    handleUpload () {
+      this.taskBox = true
+    },
+    uploadAfter (res, done) {
+      this.taskBox = false
+      this.refreshChange()
+      done()
+    },
+    async handleRowDownload (row) {
+      const mediaUrl = this.domainUrl + row.object_key
+      try {
+        const response = await fetch(mediaUrl)
+        if (!response.ok) throw new Error('文件下载失败')
+        const blob = await response.blob()
+        const url = window.URL.createObjectURL(blob)
+        const link = document.createElement('a')
+        link.href = url
+        link.download = row.file_name || 'download'
+        document.body.appendChild(link)
+        link.click()
+        document.body.removeChild(link)
+        window.URL.revokeObjectURL(url)
+        this.$message({ type: 'success', message: '文件下载成功: ' + row.file_name })
+      } catch (error) {
+        console.error('下载文件时出错:', error)
+        this.$message({ type: 'error', message: '下载失败: ' + error.message })
+      }
+    },
+    handleRowDelete (row) {
+      this.$confirm('确定删除此文件?', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' })
+        .then(() => deleteAllFile(row.id))
+        .then(() => {
+          this.onLoad(this.page)
+          this.$message({ type: 'success', message: '删除成功!' })
+        })
+        .catch(error => {
+          console.error('删除文件时出错:', error)
+          this.$message({ type: 'error', message: '删除失败,请稍后重试!' })
+        })
+    },
+    rowDel (row) {
+      this.$confirm('确定将选择数据及对应的文件删除?', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' })
+        .then(() => deleteAllFile(row.id))
+        .then(() => {
+          this.onLoad(this.page)
+          this.$message({ type: 'success', message: '操作成功!' })
+        })
+    },
+    searchReset () {
+      const endDate = new Date()
+      const startDate = new Date()
+      startDate.setMonth(startDate.getMonth() - 1)
+      const defaultStartDate = `${startDate.getFullYear()}-${String(startDate.getMonth() + 1).padStart(2, '0')}-${String(startDate.getDate()).padStart(2, '0')}`
+      const defaultEndDate = `${endDate.getFullYear()}-${String(endDate.getMonth() + 1).padStart(2, '0')}-${String(endDate.getDate()).padStart(2, '0')}`
+      this.query = { daterange: [defaultStartDate, defaultEndDate] }
+      this.selectedWorkspaceId = null
+      this.$refs.crud.setSearch({ daterange: this.query.daterange })
+      this.onLoad(this.page, this.query)
+    },
+    searchChange (params, done) {
+      this.query = params
+      this.page.currentPage = 1
+      const airportColumn = this.option.column.find(col => col.prop === 'airportNickname')
+      const selectedAirport = airportColumn.dicData.find(item => item.nickname === params.airportNickname)
+      this.selectedWorkspaceId = selectedAirport ? selectedAirport.workspace_id : null
+      this.onLoad(this.page, params)
+      done()
+    },
+    selectionChange (list) {
+      this.selectionList = list
+    },
+    selectionClear () {
+      this.selectionList = []
+      this.$refs.crud.toggleSelection()
+    },
+    currentChange (currentPage) {
+      this.page.currentPage = currentPage
+      this.onLoad(this.page, this.query)
+    },
+    sizeChange (pageSize) {
+      this.page.pageSize = pageSize
+      this.onLoad(this.page, this.query)
+    },
+    refreshChange () {
+      this.onLoad(this.page, this.query)
+    },
+    onLoad (page, params = {}) {
+      this.loading = true
+      const { daterange } = params
+      let create_time = null
+      let end_time = null
+
+      if (daterange && Array.isArray(daterange) && daterange.length === 2) {
+        const startStr = daterange[0]
+        const endStr = daterange[1]
+        const startDate = new Date(startStr + ' 00:00:00').getTime()
+        const endDate = new Date(endStr + ' 23:59:59').getTime()
+        if (!isNaN(startDate) && !isNaN(endDate)) {
+          create_time = startDate
+          end_time = endDate
+        }
+      }
+
+      let values = { ...params, ...this.query }
+      if (daterange) values.daterange = null
+
+      getMedia(page.currentPage, page.pageSize, this.selectedWorkspaceId || null, create_time, end_time)
+        .then(res => {
+          const data = res.data
+          if (data && data.data && data.data.list) {
+            this.page.total = data.data.pagination.total
+            this.data = data.data.list.map(item => ({
+              job_name: item.job_name,
+              file_name: item.file_name,
+              create_time: this.formatCreateTime(item.create_time),
+              object_key: item.object_key,
+              payload: item.payload,
+              airportNickname: item.airportNickname,
+              id: item.id,
+            }))
+            this.domainUrl = data.data.list.length > 0 ? data.data.list[0].domain_url : 'http://localhost:2888/manage/resource'
+          } else {
+            this.data = []
+          }
+          this.loading = false
+          this.selectionClear()
+          this.$refs.crud.refreshTable()
+        })
+        .catch(error => {
+          this.loading = false
+          console.error('加载媒体数据时出错:', error)
+        })
+    },
+    formatCreateTime (createTime) {
+      if (typeof createTime !== 'number' || isNaN(createTime)) return ''
+      const timestamp = createTime < 10000000000 ? createTime * 1000 : createTime
+      const date = new Date(timestamp)
+      if (isNaN(date.getTime())) return ''
+      const year = date.getFullYear()
+      const month = String(date.getMonth() + 1).padStart(2, '0')
+      const day = String(date.getDate()).padStart(2, '0')
+      const hours = String(date.getHours()).padStart(2, '0')
+      const minutes = String(date.getMinutes()).padStart(2, '0')
+      const seconds = String(date.getSeconds()).padStart(2, '0')
+      return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
+    },
+    handleRowView (row) {
+      const mediaUrl = this.domainUrl + row.object_key
+      const mediaType = this.getMediaType(row.object_key)
+      if (mediaType === 'image' || mediaType === 'video') {
+        window.open(mediaUrl, '_blank')
+      }
+    },
+    getMediaType (objectKey) {
+      const extension = objectKey.split('.').pop().toLowerCase()
+      if (['jpg', 'jpeg', 'png', 'gif'].includes(extension)) return 'image'
+      if (['mp4', 'avi', 'mov'].includes(extension)) return 'video'
+      return 'unknown'
+    },
+    showVideo (url) {
+      this.taskBox = true
+      this.taskForm = { videoUrl: url }
+      this.taskOption = {
+        submitBtn: false,
+        emptyBtn: false,
+        column: [
+          { label: '视频播放', prop: 'videoUrl', type: 'video', span: 24, props: { controls: true, autoplay: false } },
+        ],
+      }
+    },
+    validData (val, def) {
+      return typeof val !== 'undefined' ? val : def
+    },
+    async handleZipDownload () {
+      if (this.selectionList.length === 0) {
+        this.$message({ type: 'warning', message: '请先选择需要下载的文件' })
+        return
+      }
+
+      this.isDownloading = true
+
+      try {
+        const zip = new JSZip()
+        const files = this.selectionList.map(item => ({
+          url: this.domainUrl + item.object_key,
+          name: item.file_name
+        }))
+
+        const fetchPromises = files.map(async file => {
+          const response = await fetch(file.url, { cache: 'force-cache' })
+          if (!response.ok) throw new Error(`下载文件 ${file.name} 失败`)
+          const blob = await response.blob()
+          zip.file(file.name, blob)
+        })
+
+        await Promise.all(fetchPromises)
+
+        const content = await zip.generateAsync({
+          type: 'blob',
+          compression: 'DEFLATE',
+          compressionOptions: { level: 1 }
+        })
+
+        const url = window.URL.createObjectURL(content)
+        const link = document.createElement('a')
+        link.href = url
+        link.download = `media_files_${new Date().toISOString().slice(0, 10)}_${new Date().getTime()}.zip`
+        document.body.appendChild(link)
+        link.click()
+        document.body.removeChild(link)
+        window.URL.revokeObjectURL(url)
+
+        this.$message({ type: 'success', message: `成功下载 ${this.selectionList.length} 个文件` })
+      } catch (error) {
+        console.error('压缩下载失败:', error)
+        this.$message({ type: 'error', message: '压缩下载失败: ' + error.message })
+      } finally {
+        this.isDownloading = false
+      }
+    },
+  },
+}
+</script>
+
+<style scoped lang="scss"></style>
diff --git a/applications/drone-command/src/views/resource/oss.vue b/applications/drone-command/src/views/resource/oss.vue
new file mode 100644
index 0000000..8e76346
--- /dev/null
+++ b/applications/drone-command/src/views/resource/oss.vue
@@ -0,0 +1,479 @@
+<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="danger" icon="el-icon-delete" plain v-if="permission.oss_delete" @click="handleDelete">删 除
+        </el-button>
+      </template>
+      <template #menu="scope">
+        <el-button type="primary" text icon="el-icon-video-play" v-if="userInfo.role_name.includes('admin')"
+          @click="handleDebug(scope.row)">调试
+        </el-button>
+        <el-button type="primary" text icon="el-icon-circle-check" v-if="scope.row.status == 1 && permission.oss_enable"
+          @click.stop="handleEnable(scope.row)">启用
+        </el-button>
+        <el-button type="primary" text icon="VideoPause" v-if="scope.row.status == 2 && permission.oss_enable"
+          @click.stop="handleDisable(scope.row)">禁用
+        </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>
+import { getListPage, getDetail, add, update, remove, enable, disable } from '@/api/resource/oss'
+import { mapGetters } from 'vuex'
+import func from '@/utils/func'
+
+export default {
+  data () {
+    return {
+      form: {},
+      query: {},
+      loading: true,
+      box: false,
+      page: {
+        pageSize: 10,
+        currentPage: 1,
+        total: 0,
+      },
+      selectionList: [],
+      option: {
+        emptyBtnText: '重置',
+        emptyBtnIcon: 'el-icon-refresh',
+        tip: false,
+        searchShow: true,
+        searchGutter: 30,
+        searchMenuPosition: 'left',
+        searchMenuSpan: 4,
+        border: true,
+        index: true,
+        viewBtn: true,
+        selection: true,
+        grid: false,
+        menuWidth: 350,
+        labelWidth: 100,
+        dialogWidth: 880,
+        dialogClickModal: false,
+
+        height: 'auto',
+        calcHeight: 20,
+        column: [
+          {
+            label: '存储分类',
+            type: 'radio',
+            value: 1,
+            span: 24,
+            width: 120,
+            // searchLabelWidth: 50,
+            searchSpan: 4,
+            row: true,
+            dicUrl: '/blade-system/dict/dictionary?code=oss',
+            props: {
+              label: 'dictValue',
+              value: 'dictKey',
+            },
+            dataType: 'number',
+            slot: true,
+            prop: 'category',
+            search: true,
+            rules: [
+              {
+                required: true,
+                message: '请选择分类',
+                trigger: 'blur',
+              },
+            ],
+               change: (value) => {
+               this.searchChange({ ...this.query,category:value.value}, () => {});
+            }
+          },
+          {
+            label: '存储名称',
+            prop: 'name',
+            span: 24,
+            search: true,
+            searchSpan: 4,
+            rules: [
+              {
+                required: true,
+                message: '请输入名称',
+                trigger: 'blur',
+              },
+            ],
+          },
+          {
+            label: '资源编号',
+            prop: 'ossCode',
+            span: 24,
+            width: 120,
+            // searchLabelWidth: 100,
+            search: true,
+            searchSpan: 4,
+            rules: [
+              {
+                required: true,
+                message: '请输入资源编号',
+                trigger: 'blur',
+              },
+            ],
+          },
+          {
+            label: '资源地址',
+            labelTip: '对象存储通用资源地址,可以是内网也可以是外网',
+            prop: 'endpoint',
+            span: 24,
+            rules: [
+              {
+                required: true,
+                message: '请输入资源地址',
+                trigger: 'blur',
+              },
+            ],
+          },
+          {
+            label: '外网地址',
+            labelTip: '资源地址设置为内网上传,则外部访问需要配置外网映射地址',
+            prop: 'transformEndpoint',
+            span: 24,
+          },
+          {
+            label: '空间名',
+            prop: 'bucketName',
+            span: 24,
+            width: 120,
+            rules: [
+              {
+                required: true,
+                message: '请输入空间名',
+                trigger: 'blur',
+              },
+            ],
+          },
+          {
+            label: 'accessKey',
+            prop: 'accessKey',
+            span: 24,
+            search: true,
+            searchSpan: 4,
+            // searchLabelWidth: 100,
+            overHidden: true,
+            rules: [
+              {
+                required: true,
+                message: '请输入accessKey',
+                trigger: 'blur',
+              },
+            ],
+          },
+          {
+            label: 'secretKey',
+            prop: 'secretKey',
+            span: 24,
+            overHidden: true,
+            rules: [
+              {
+                required: true,
+                message: '请输入secretKey',
+                trigger: 'blur',
+              },
+            ],
+          },
+          {
+            label: 'appId',
+            prop: 'appId',
+            span: 24,
+            hide: true,
+            display: false,
+          },
+          {
+            label: '过期时间',
+            prop: 'expire',
+            type: "number",
+            span: 24,
+            hide: true,
+            rules: [
+              {
+                required: true,
+                message: '请输入过期时间',
+                trigger: 'blur',
+              },
+            ],
+          },
+          {
+            label: '地域简称',
+            prop: 'region',
+            span: 24,
+            hide: true,
+            rules: [
+              {
+                required: true,
+                message: '请输入地域简称',
+                trigger: 'blur',
+              },
+            ],
+          },
+          {
+            label: '是否启用',
+            prop: 'status',
+            span: 24,
+            width: 85,
+            align: 'center',
+            slot: true,
+            addDisplay: false,
+            editDisplay: false,
+            viewDisplay: false,
+          },
+          {
+            label: '备注',
+            prop: 'remark',
+            span: 24,
+            hide: true,
+          },
+        ],
+      },
+      data: [],
+      debugForm: {
+        code: '',
+      },
+      debugOption: {
+        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: {
+    'form.category' () {
+      const category = func.toInt(this.form.category)
+      this.$refs.crud.option.column.filter(item => {
+        if (item.prop === 'appId') {
+          item.display = category === 4
+        }
+        // if (item.prop === 'region') {
+        //   item.display = category === 4 || category === 5;
+        // }
+      })
+    },
+    'debugForm.code' () {
+      const column = this.findObject(this.debugOption.column, 'backgroundUrl')
+      column.action = `/blade-resource/oss/endpoint/put-file?code=${this.debugForm.code}`
+    },
+  },
+  computed: {
+    ...mapGetters(['userInfo', 'permission']),
+    permissionList () {
+      return {
+        addBtn: this.validData(this.permission.oss_add),
+        viewBtn: this.validData(this.permission.oss_view),
+        delBtn: this.validData(this.permission.oss_delete),
+        editBtn: this.validData(this.permission.oss_edit),
+      }
+    },
+    ids () {
+      let ids = []
+      this.selectionList.forEach(ele => {
+        ids.push(ele.id)
+      })
+      return ids.join(',')
+    },
+  },
+  methods: {
+    rowSave (row, done, loading) {
+      add(row).then(
+        () => {
+          this.onLoad(this.page)
+          this.$message({
+            type: 'success',
+            message: '操作成功!',
+          })
+          done()
+        },
+        error => {
+          window.console.log(error)
+          loading()
+        }
+      )
+    },
+    rowUpdate (row, index, done, loading) {
+      update(row).then(
+        () => {
+          this.onLoad(this.page)
+          this.$message({
+            type: 'success',
+            message: '操作成功!',
+          })
+          done()
+        },
+        error => {
+          window.console.log(error)
+          loading()
+        }
+      )
+    },
+    rowDel (row) {
+      this.$confirm('确定将选择数据删除?', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning',
+      })
+        .then(() => {
+          return remove(row.id)
+        })
+        .then(() => {
+          this.onLoad(this.page)
+          this.$message({
+            type: 'success',
+            message: '操作成功!',
+          })
+        })
+    },
+    searchReset () {
+      this.query = {}
+      this.onLoad(this.page)
+    },
+    searchChange (params, done) {
+      this.query = params
+      this.page.currentPage = 1
+      this.onLoad(this.page, params)
+      done()
+    },
+    selectionChange (list) {
+      this.selectionList = list
+    },
+    selectionClear () {
+      this.selectionList = []
+      this.$refs.crud.toggleSelection()
+    },
+    handleEnable (row) {
+      this.$confirm('是否确定启用这条配置?', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning',
+      })
+        .then(() => {
+          return enable(row.id)
+        })
+        .then(() => {
+          this.onLoad(this.page)
+          this.$message({
+            type: 'success',
+            message: '操作成功!',
+          })
+          this.$refs.crud.toggleSelection()
+        })
+    },
+    handleDisable (row) {
+      this.$confirm('是否确定禁用用这条配置?', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning',
+      })
+        .then(() => {
+          return disable(row.id)
+        })
+        .then(() => {
+          this.onLoad(this.page)
+          this.$message({
+            type: 'success',
+            message: '操作成功!',
+          })
+          this.$refs.crud.toggleSelection()
+        })
+    },
+    handleDebug (row) {
+      this.box = true
+      this.debugForm.code = row.ossCode
+      this.debugForm.backgroundUrl = ''
+    },
+    handleSubmit (form, done) {
+      this.$message({
+        type: 'success',
+        message: `获取到图片地址:[${form.backgroundUrl}]`,
+      })
+      done()
+    },
+    handleDelete () {
+      if (this.selectionList.length === 0) {
+        this.$message.warning('请选择至少一条数据')
+        return
+      }
+      this.$confirm('确定将选择数据删除?', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning',
+      })
+        .then(() => {
+          return remove(this.ids)
+        })
+        .then(() => {
+          this.onLoad(this.page)
+          this.$message({
+            type: 'success',
+            message: '操作成功!',
+          })
+          this.$refs.crud.toggleSelection()
+        })
+    },
+    beforeOpen (done, type) {
+      if (['edit', 'view'].includes(type)) {
+        getDetail(this.form.id).then(res => {
+          this.form = res.data.data
+        })
+      }
+      done()
+    },
+    currentChange (currentPage) {
+      this.page.currentPage = currentPage
+    },
+    sizeChange (pageSize) {
+      this.page.pageSize = pageSize
+    },
+    refreshChange () {
+      this.onLoad(this.page, this.query)
+    },
+    onLoad (page, params = {}) {
+      this.loading = true
+      getListPage(page.currentPage, page.pageSize, Object.assign(params, this.query)).then(res => {
+        const data = res.data.data
+        this.page.total = data.total
+        this.data = data.records
+        this.loading = false
+        this.selectionClear()
+      })
+    },
+  },
+}
+</script>
+<style scoped lang="scss"></style>
\ No newline at end of file
diff --git a/applications/drone-command/src/views/resource/patchTypeManagement.vue b/applications/drone-command/src/views/resource/patchTypeManagement.vue
new file mode 100644
index 0000000..1ea4aa5
--- /dev/null
+++ b/applications/drone-command/src/views/resource/patchTypeManagement.vue
@@ -0,0 +1,331 @@
+<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" type="primary"> 返回 </el-button>
+      </template>
+      <template #status="{ row }">
+        <el-tag>{{ row.statusName }}</el-tag>
+      </template>
+      <template #category="{ row }">
+        <el-tag>{{ row.categoryName }}</el-tag>
+      </template>
+        <template #menu="{ row }">
+        <el-button 
+          :disabled="row.patches_type === '综合类'" 
+          type="primary" 
+          text 
+          icon="el-icon-delete" 
+          @click="rowDel(row)"
+        >
+          删除 
+        </el-button>
+      </template>
+    </avue-crud>
+  </basic-container>
+</template>
+
+<script setup>
+import {
+  searchManagementApi,
+  listOfSpotTypesApi,
+  spotTypesCreateApi,
+  editSpotTypeApi,
+  deleteSpotTypeApi,
+} from '@/api/patchManagement/index';
+import { ref, computed, watch } from 'vue';
+import { enable, disable } from '@/api/resource/oss';
+import { useStore } from 'vuex';
+import func from '@/utils/func';
+import { useRouter } from 'vue-router';
+import { ElMessage, ElMessageBox, ElLoading } from 'element-plus';
+const store = useStore();
+const router = useRouter();
+// ---------------- data ----------------
+const form = ref({});
+const query = ref({});
+const creatorOption = ref([]);
+const loading = ref(true);
+const page = ref({
+  pageSize: 20,
+  currentPage: 1,
+  total: 0,
+  lotValue: '',
+  userName: '',
+});
+const selectionList = ref([]);
+const option = ref({
+  emptyBtnText: '重置',
+  emptyBtnIcon: 'el-icon-refresh',
+  addBtn: true,
+  addBtnText: '新增类型',
+  tip: false,
+  searchShow: true,
+  searchGutter: 30,
+  searchMenuPosition: 'left',
+  searchMenuSpan: 4,
+  border: true,
+  index: true,
+  indexLabel: '序号',
+   indexWidth: 60,
+  selection: false,
+  grid: false,
+  menuWidth: 180,
+  labelWidth: 90,
+  dialogWidth: 600,
+  dialogClickModal: false,
+  height: 'auto',
+  calcHeight: 20,
+  refreshBtn: false,
+  gridBtn: false,
+  searchShowBtn: false,
+  columnBtn: false,
+  delBtn: false, 
+  column: [
+    {
+      label: '类型名称',
+      prop: 'patches_type',
+      search: true,
+      searchSpan: 4,
+      span: 24,
+      rules: [{ required: true, message: '请输入类型名称', trigger: 'blur' }],
+    },
+    {
+      label: '创建时间',
+      prop: 'create_time',
+      addDisplay: false,
+      editDisplay: false,
+      rules: [{ required: true, message: '请输入创建时间', trigger: 'blur' }],
+    },
+    {
+      label: '创建人',
+      prop: 'user_name',
+      search: true,
+      addDisplay: false,
+      editDisplay: false,
+      searchSpan: 4,
+      type: 'select',
+      dicData: creatorOption,
+      props: {
+        label: 'label',
+        value: 'value',
+      },
+      rules: [{ required: true, message: '请输入创建人', trigger: 'blur' }],
+    },
+  ],
+});
+
+const data = ref([]);
+// 获取搜索数据
+const getsearchManagementApi = () => {
+  searchManagementApi().then(res => {
+    const creatorOptionuniqueMap = new Map();
+    res.data.data.user_names.forEach(item => {
+      const [key, value] = Object.entries(item)[0];
+      if (!creatorOptionuniqueMap.has(key)) {
+        creatorOptionuniqueMap.set(key, value);
+      }
+    });
+    creatorOption.value = Array.from(creatorOptionuniqueMap).map(([key, value]) => ({
+      label: value,
+      value: value,
+    })); 
+  });
+};
+
+// ---------------- 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;
+      }
+    });
+  }
+);
+
+// ---------------- 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) => {
+  const createParams = {
+    patches_type: row.patches_type,
+  };
+  spotTypesCreateApi(createParams).then(
+    () => {
+      onLoad(page.value);
+      ElMessage.success('操作成功!');
+      done();
+    },
+    error => {
+      console.log(error);
+      loadingFn();
+    }
+  );
+};
+
+const rowUpdate = (row, index, done, loadingFn) => {
+  editSpotTypeApi(row).then(
+    res => {
+      onLoad(page.value);
+      ElMessage.success('操作成功!');
+      done();
+    },
+    error => {
+      console.log(error);
+      loadingFn();
+    }
+  );
+};
+
+const rowDel = row => {
+  ElMessageBox.confirm('确定将选择数据删除?', '提示', {
+    confirmButtonText: '确定',
+    cancelButtonText: '取消',
+    type: 'warning',
+  })
+    .then(() => deleteSpotTypeApi([row.id]))
+    .then(() => {
+      onLoad(page.value);
+      ElMessage.success('操作成功!');
+    });
+};
+
+const searchReset = () => {
+  page.value.userName = '';
+  page.value.lotValue = '';
+  page.value.currentPage = 1;
+  page.value.pageSize = 20;
+  onLoad(page.value);
+};
+
+const searchChange = (params, done) => {
+  page.value.currentPage = 1;
+  page.value.lotValue = params.patches_type;
+  page.value.userName = params.user_name;
+  onLoad(page.value);
+  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 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 = {}) => {
+  const searchparams = {
+    current: pageParam.currentPage,
+    size: pageParam.pageSize,
+    lotValue: pageParam.lotValue,
+    userName: pageParam.userName,
+  };
+  loading.value = true;
+  listOfSpotTypesApi(searchparams).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' });
+};
+
+// ---------------- utils ----------------
+function validData(value) {
+  return value !== undefined && value !== null && value !== false;
+}
+
+onMounted(() => {
+  getsearchManagementApi();
+});
+</script>
+
+<style scoped lang="scss"></style>
diff --git a/applications/drone-command/src/views/resource/sms.vue b/applications/drone-command/src/views/resource/sms.vue
new file mode 100644
index 0000000..3854e6b
--- /dev/null
+++ b/applications/drone-command/src/views/resource/sms.vue
@@ -0,0 +1,459 @@
+<template>
+  <basic-container>
+    <avue-crud
+      :option="option"
+      :table-loading="loading"
+      :data="data"
+      v-model:page="page"
+      :permission="permissionList"
+      :before-open="beforeOpen"
+      v-model="form"
+      ref="crud"
+      @row-update="rowUpdate"
+      @row-save="rowSave"
+      @row-del="rowDel"
+      @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="danger"
+          icon="el-icon-delete"
+          plain
+          v-if="permission.sms_delete"
+          @click="handleDelete"
+          >删 除
+        </el-button>
+      </template>
+      <template #menu="scope">
+        <el-button
+          type="primary"
+          text
+          icon="el-icon-video-play"
+          v-if="userInfo.role_name.includes('admin')"
+          @click="handleDebug(scope.row)"
+          >调试
+        </el-button>
+        <el-button
+          type="primary"
+          text
+          icon="el-icon-circle-check"
+          v-if="permission.sms_enable"
+          @click.stop="handleEnable(scope.row)"
+          >启用
+        </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 :option="debugOption" v-model="debugForm" @submit="handleSend" />
+    </el-dialog>
+  </basic-container>
+</template>
+
+<script>
+import { getList, getDetail, add, update, remove, enable, send } from '@/api/resource/sms';
+import { mapGetters } from 'vuex';
+import func from '@/utils/func';
+
+export default {
+  data() {
+    return {
+      form: {},
+      query: {},
+      loading: true,
+      box: false,
+      page: {
+        pageSize: 10,
+        currentPage: 1,
+        total: 0,
+      },
+      selectionList: [],
+      option: {
+        tip: false,
+        searchShow: true,
+        searchMenuSpan: 6,
+        border: true,
+        index: true,
+        viewBtn: true,
+        selection: true,
+        grid: true,
+        menuWidth: 350,
+        labelWidth: 100,
+        dialogWidth: 880,
+        dialogClickModal: false,
+        column: [
+          {
+            label: '分类',
+            type: 'radio',
+            value: 1,
+            span: 24,
+            width: 100,
+            searchLabelWidth: 50,
+            row: true,
+            dicUrl: '/blade-system/dict/dictionary?code=sms',
+            props: {
+              label: 'dictValue',
+              value: 'dictKey',
+            },
+            dataType: 'number',
+            slot: true,
+            prop: 'category',
+            search: true,
+            rules: [
+              {
+                required: true,
+                message: '请选择分类',
+                trigger: 'blur',
+              },
+            ],
+          },
+          {
+            label: '资源编号',
+            prop: 'smsCode',
+            span: 24,
+            width: 200,
+            search: true,
+            rules: [
+              {
+                required: true,
+                message: '请输入资源编号',
+                trigger: 'blur',
+              },
+            ],
+          },
+          {
+            label: '模版ID',
+            prop: 'templateId',
+            span: 24,
+            width: 200,
+            search: true,
+            rules: [
+              {
+                required: true,
+                message: '请输入模版ID',
+                trigger: 'blur',
+              },
+            ],
+          },
+          {
+            label: 'accessKey',
+            prop: 'accessKey',
+            span: 24,
+            overHidden: true,
+            rules: [
+              {
+                required: true,
+                message: '请输入accessKey',
+                trigger: 'blur',
+              },
+            ],
+          },
+          {
+            label: 'secretKey',
+            prop: 'secretKey',
+            span: 24,
+            overHidden: true,
+            display: true,
+            hide: true,
+            rules: [
+              {
+                required: true,
+                message: '请输入secretKey',
+                trigger: 'blur',
+              },
+            ],
+          },
+          {
+            label: 'regionId',
+            prop: 'regionId',
+            span: 24,
+            value: 'cn-hangzhou',
+            hide: true,
+            display: false,
+          },
+          {
+            label: '短信签名',
+            prop: 'signName',
+            span: 24,
+            width: 200,
+            rules: [
+              {
+                required: true,
+                message: '请输入短信签名',
+                trigger: 'blur',
+              },
+            ],
+          },
+          {
+            label: '是否启用',
+            prop: 'status',
+            span: 24,
+            width: 85,
+            align: 'center',
+            slot: true,
+            addDisplay: false,
+            editDisplay: false,
+            viewDisplay: false,
+          },
+          {
+            label: '备注',
+            prop: 'remark',
+            span: 24,
+            hide: true,
+          },
+        ],
+      },
+      data: [],
+      debugForm: {
+        code: '',
+      },
+      debugOption: {
+        submitText: '发送',
+        column: [
+          {
+            label: '资源编号',
+            prop: 'code',
+            disabled: true,
+            span: 24,
+          },
+          {
+            label: '发送手机',
+            prop: 'phones',
+            span: 24,
+          },
+          {
+            label: '发送参数',
+            prop: 'params',
+            span: 24,
+            placeholder: "例: {'code':2333,'title':'通知标题'}",
+          },
+        ],
+      },
+    };
+  },
+  watch: {
+    'form.category'() {
+      const category = func.toInt(this.form.category);
+      this.$refs.crud.option.column.filter(item => {
+        if (item.prop === 'templateId') {
+          if (category === 1) {
+            item.label = '模版内容';
+          } else {
+            item.label = '模版ID';
+          }
+        }
+        if (item.prop === 'accessKey') {
+          if (category === 1) {
+            item.label = 'apiKey';
+          } else if (category === 4) {
+            item.label = 'appId';
+          } else {
+            item.label = 'accessKey';
+          }
+        }
+        if (item.prop === 'secretKey') {
+          item.display = category !== 1;
+          if (category === 4) {
+            item.label = 'appKey';
+          } else {
+            item.label = 'secretKey';
+          }
+        }
+        if (item.prop === 'regionId') {
+          if (category === 3) {
+            item.display = true;
+            item.value = 'cn-hangzhou';
+          } else {
+            item.display = false;
+          }
+        }
+      });
+    },
+  },
+  computed: {
+    ...mapGetters(['userInfo', 'permission']),
+    permissionList() {
+      return {
+        addBtn: this.validData(this.permission.sms_add, false),
+        viewBtn: this.validData(this.permission.sms_view, false),
+        delBtn: this.validData(this.permission.sms_delete, false),
+        editBtn: this.validData(this.permission.sms_edit, false),
+      };
+    },
+    ids() {
+      let ids = [];
+      this.selectionList.forEach(ele => {
+        ids.push(ele.id);
+      });
+      return ids.join(',');
+    },
+  },
+  methods: {
+    rowSave(row, done, loading) {
+      add(row).then(
+        () => {
+          this.onLoad(this.page);
+          this.$message({
+            type: 'success',
+            message: '操作成功!',
+          });
+          done();
+        },
+        error => {
+          window.console.log(error);
+          loading();
+        }
+      );
+    },
+    rowUpdate(row, index, done, loading) {
+      update(row).then(
+        () => {
+          this.onLoad(this.page);
+          this.$message({
+            type: 'success',
+            message: '操作成功!',
+          });
+          done();
+        },
+        error => {
+          window.console.log(error);
+          loading();
+        }
+      );
+    },
+    rowDel(row) {
+      this.$confirm('确定将选择数据删除?', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning',
+      })
+        .then(() => {
+          return remove(row.id);
+        })
+        .then(() => {
+          this.onLoad(this.page);
+          this.$message({
+            type: 'success',
+            message: '操作成功!',
+          });
+        });
+    },
+    handleEnable(row) {
+      this.$confirm('是否确定启用这条配置?', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning',
+      })
+        .then(() => {
+          return enable(row.id);
+        })
+        .then(() => {
+          this.onLoad(this.page);
+          this.$message({
+            type: 'success',
+            message: '操作成功!',
+          });
+          this.$refs.crud.toggleSelection();
+        });
+    },
+    handleDebug(row) {
+      this.box = true;
+      this.debugForm.code = row.smsCode;
+    },
+    handleSend(form, done, loading) {
+      send(form.code, form.phones, form.params).then(
+        res => {
+          this.$message({
+            type: 'success',
+            message: '发送成功!',
+          });
+          done();
+          window.console.log(res);
+          this.box = false;
+        },
+        error => {
+          window.console.log(error);
+          loading();
+        }
+      );
+    },
+    handleDelete() {
+      if (this.selectionList.length === 0) {
+        this.$message.warning('请选择至少一条数据');
+        return;
+      }
+      this.$confirm('确定将选择数据删除?', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning',
+      })
+        .then(() => {
+          return remove(this.ids);
+        })
+        .then(() => {
+          this.onLoad(this.page);
+          this.$message({
+            type: 'success',
+            message: '操作成功!',
+          });
+          this.$refs.crud.toggleSelection();
+        });
+    },
+    beforeOpen(done, type) {
+      if (['edit', 'view'].includes(type)) {
+        getDetail(this.form.id).then(res => {
+          this.form = res.data.data;
+        });
+      }
+      done();
+    },
+    searchReset() {
+      this.query = {};
+      this.onLoad(this.page);
+    },
+    searchChange(params, done) {
+      this.query = params;
+      this.page.currentPage = 1;
+      this.onLoad(this.page, params);
+      done();
+    },
+    selectionChange(list) {
+      this.selectionList = list;
+    },
+    selectionClear() {
+      this.selectionList = [];
+      this.$refs.crud.toggleSelection();
+    },
+    currentChange(currentPage) {
+      this.page.currentPage = currentPage;
+    },
+    sizeChange(pageSize) {
+      this.page.pageSize = pageSize;
+    },
+    refreshChange() {
+      this.onLoad(this.page, this.query);
+    },
+    onLoad(page, params = {}) {
+      this.loading = true;
+      getList(page.currentPage, page.pageSize, Object.assign(params, this.query)).then(res => {
+        const data = res.data.data;
+        this.page.total = data.total;
+        this.data = data.records;
+        this.loading = false;
+        this.selectionClear();
+      });
+    },
+  },
+};
+</script>
diff --git a/applications/drone-command/src/views/resource/wayline.vue b/applications/drone-command/src/views/resource/wayline.vue
new file mode 100644
index 0000000..52ea83f
--- /dev/null
+++ b/applications/drone-command/src/views/resource/wayline.vue
@@ -0,0 +1,308 @@
+<template>
+  <basic-container>
+    <avue-crud :option="option" :table-loading="loading" :data="data" v-model:page="page" :permission="permissionList"
+      v-model="form" ref="crud" @search-change="searchChange" @search-reset="searchReset"
+      @selection-change="selectionChange" @current-change="currentChange" @size-change="sizeChange"
+      @refresh-change="refreshChange" @on-load="onLoad">
+    </avue-crud>
+  </basic-container>
+</template>
+
+<script>
+import { getJobsByUser } from '@/api/resource/wayline'
+import { getAirportList } from '@/api/device/device'
+import { mapGetters } from 'vuex'
+import func from '@/utils/func'
+
+export default {
+  data () {
+    // 计算默认的开始和结束日期
+    const endDate = new Date() // 当前日期
+    const startDate = new Date() // 一个月前的日期
+    startDate.setMonth(startDate.getMonth() - 1)
+
+    // 格式化为 YYYY-MM-DD 字符串
+    const defaultStartDate = `${startDate.getFullYear()}-${String(startDate.getMonth() + 1).padStart(2, '0')}-${String(startDate.getDate()).padStart(2, '0')}`
+    const defaultEndDate = `${endDate.getFullYear()}-${String(endDate.getMonth() + 1).padStart(2, '0')}-${String(endDate.getDate()).padStart(2, '0')}`
+
+    return {
+      form: {},
+      // 初始化 query,设置默认时间范围
+      query: {
+        daterange: [defaultStartDate, defaultEndDate]
+      },
+      loading: true,
+      page: {
+        pageSize: 10,
+        currentPage: 1,
+        total: 0,
+      },
+      selectionList: [],
+      selectedWorkspaceId: null,
+      option: {
+        tip: false,
+        searchShow: true,
+        searchGutter: 30,
+        searchMenuPosition: 'left',
+        searchMenuSpan: 4,
+        border: true,
+        index: true,
+        // selection: true,
+        dialogClickModal: false,
+        menu: false, // 禁用默认操作列
+
+        height: 'auto',
+        calcHeight: 20,
+        column: [
+          {
+            label: '机场选择',
+            prop: 'dock_name',
+            type: 'select',
+            search: true,
+            searchSpan: 4,
+            dicData: [],
+            props: {
+              label: 'nickname',
+              value: 'nickname',
+            },
+            hide: true,
+             change: (value) => {
+               this.searchChange({ ...this.query,dock_name:value.value}, () => {});
+          }
+          },
+          {
+            label: '任务名称',
+            prop: 'job_name',
+            search: true,
+            searchSpan: 4,
+          },
+          {
+            label: '时间范围',
+            prop: 'daterange',
+            type: 'daterange',
+            search: true,
+            searchRange: true,
+            searchSpan: 8,
+            format: 'YYYY-MM-DD', // 显示格式
+            valueFormat: 'YYYY-MM-DD', // 值格式
+            startPlaceholder: '开始时间',
+            endPlaceholder: '结束时间',
+            searchValue: [defaultStartDate, defaultEndDate], // 设置搜索表单默认值
+            hide: true ,// 隐藏在表格中,只保留在搜索框
+                change: (value) => {
+               this.searchChange({ ...this.query,daterange:value.value}, () => {});
+          }
+          },
+          {
+            label: '机场名称',
+            prop: 'dock_name',
+          },
+          {
+            label: '用户名',
+            prop: 'username',
+          },
+          {
+            label: '执行时间',
+            prop: 'begin_time',
+            formatter: (row) => this.formatTime(row.begin_time),
+          },
+          {
+            label: '完成时间',
+            prop: 'completed_time',
+            formatter: (row) => this.formatTime(row.completed_time),
+          },
+          {
+            label: '错误原因',
+            prop: 'reason',
+            formatter: (row) => {
+              return [4, 5].includes(row.status) ? row.reason || '未知' : ''
+            },
+          },
+        ],
+      },
+      data: [],
+    }
+  },
+  computed: {
+    ...mapGetters(['permission']),
+    permissionList () {
+      return {
+        addBtn: false,
+        editBtn: false,
+        viewBtn: this.validData(this.permission.attach_view, false),
+        delBtn: this.validData(this.permission.attach_delete, false),
+      }
+    },
+    ids () {
+      let ids = []
+      this.selectionList.forEach(ele => {
+        ids.push(ele.job_id)
+      })
+      return ids.join(',')
+    },
+  },
+  mounted () {
+    this.loadAirportList()
+    // 初始化时手动触发搜索,以确保默认值生效
+    this.$refs.crud.searchChange(this.query)
+    this.onLoad(this.page, this.query)
+  },
+  methods: {
+    loadAirportList () {
+      getAirportList().then(res => {
+        const airportData = res.data.data || []
+        const airportColumn = this.option.column.find(col => col.prop === 'dock_name')
+        if (airportColumn) {
+          airportColumn.dicData = airportData.map(item => ({
+            nickname: item.nickname,
+            workspace_id: item.workspace_id,
+          }))
+        }
+      }).catch(error => {
+        // 处理错误
+      })
+    },
+    searchReset () {
+      // 重置时恢复默认时间范围
+      const endDate = new Date()
+      const startDate = new Date()
+      startDate.setMonth(startDate.getMonth() - 1)
+      const defaultStartDate = `${startDate.getFullYear()}-${String(startDate.getMonth() + 1).padStart(2, '0')}-${String(startDate.getDate()).padStart(2, '0')}`
+      const defaultEndDate = `${endDate.getFullYear()}-${String(endDate.getMonth() + 1).padStart(2, '0')}-${String(endDate.getDate()).padStart(2, '0')}`
+      this.query = {
+        daterange: [defaultStartDate, defaultEndDate]
+      }
+      this.selectedWorkspaceId = null
+      // 更新 searchValue 以确保界面重置后显示默认值
+      const daterangeColumn = this.option.column.find(col => col.prop === 'daterange')
+      daterangeColumn.searchValue = [defaultStartDate, defaultEndDate]
+      this.onLoad(this.page, this.query)
+    },
+    searchChange (params, done) {
+      this.query = params
+      this.page.currentPage = 1
+
+      const airportColumn = this.option.column.find(col => col.prop === 'dock_name')
+      const selectedAirport = airportColumn.dicData.find(item => item.nickname === params.dock_name)
+      this.selectedWorkspaceId = selectedAirport ? selectedAirport.workspace_id : null
+
+      const { daterange } = params
+      let startTime = null
+      let endTime = null
+      if (daterange && Array.isArray(daterange) && daterange.length === 2) {
+        startTime = this.formatTime(daterange[0])
+        endTime = this.formatTime(daterange[1])
+      }
+
+      if (startTime && isNaN(new Date(startTime).getTime())) {
+        startTime = null
+      }
+      if (endTime && isNaN(new Date(endTime).getTime())) {
+        endTime = null
+      }
+
+      this.onLoad(this.page, params)
+      if (done) done()
+    },
+    selectionChange (list) {
+      this.selectionList = list
+    },
+    selectionClear () {
+      this.selectionList = []
+      this.$refs.crud.toggleSelection()
+    },
+    currentChange (currentPage) {
+      this.page.currentPage = currentPage
+      this.onLoad(this.page, this.query)
+    },
+    sizeChange (pageSize) {
+      this.page.pageSize = pageSize
+      this.onLoad(this.page, this.query)
+    },
+    refreshChange () {
+      this.onLoad(this.page, this.query)
+    },
+    onLoad (page, params = {}) {
+      this.loading = true
+
+      const workspaceId = this.selectedWorkspaceId || null
+      const { daterange } = params
+      let startTime = null
+      let endTime = null
+
+      if (daterange && Array.isArray(daterange) && daterange.length === 2) {
+        startTime = this.formatTime(daterange[0])
+        endTime = this.formatTime(daterange[1])
+      }
+
+      if (startTime && isNaN(new Date(startTime).getTime())) {
+        startTime = null
+      }
+      if (endTime && isNaN(new Date(endTime).getTime())) {
+        endTime = null
+      }
+
+      getJobsByUser(
+        workspaceId,
+        page.currentPage,
+        page.pageSize,
+        params.status || '',
+        startTime,
+        endTime,
+        params.job_name || null
+      ).then(res => {
+        const responseData = res.data
+
+        if (responseData && responseData.data && Array.isArray(responseData.data.list)) {
+          this.page.total = responseData.data.pagination.total || responseData.data.list.length
+          this.data = responseData.data.list.map(item => ({
+            job_id: item.job_id,
+            job_name: item.job_name,
+            dock_name: item.dock_name,
+            status: item.status,
+            username: item.username,
+            workspace_id: item.workspace_id,
+            reason: item.reason,
+            begin_time: item.begin_time,
+            completed_time: item.completed_time,
+          }))
+        } else {
+          this.data = []
+          this.page.total = 0
+        }
+
+        this.loading = false
+        this.selectionClear()
+      }).catch(error => {
+        this.loading = false
+        this.data = []
+        this.page.total = 0
+      })
+    },
+    formatTime (time) {
+      if (!time) return ''
+      let date
+      if (typeof time === 'number') {
+        date = new Date(time < 10000000000 ? time * 1000 : time)
+      } else if (typeof time === 'string') {
+        date = new Date(time)
+      }
+      if (isNaN(date.getTime())) {
+        return null
+      }
+      const year = date.getFullYear()
+      const month = String(date.getMonth() + 1).padStart(2, '0')
+      const day = String(date.getDate()).padStart(2, '0')
+      const hours = String(date.getHours()).padStart(2, '0')
+      const minutes = String(date.getMinutes()).padStart(2, '0')
+      const seconds = String(date.getSeconds()).padStart(2, '0')
+      return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
+    },
+    validData (val, def) {
+      return typeof val !== 'undefined' ? val : def
+    },
+  },
+}
+</script>
+
+<style scoped lang="scss"></style>
diff --git a/applications/drone-command/src/views/resource/waylineFile.vue b/applications/drone-command/src/views/resource/waylineFile.vue
new file mode 100644
index 0000000..ceeedd1
--- /dev/null
+++ b/applications/drone-command/src/views/resource/waylineFile.vue
@@ -0,0 +1,235 @@
+<template>
+  <basic-container>
+    <avue-crud :option="option" :table-loading="loading" :data="data" :page.sync="page" v-model="form" ref="crud"
+      @search-change="searchChange" @search-reset="searchReset" @current-change="currentChange"
+      @size-change="sizeChange" @refresh-change="refreshChange">
+    </avue-crud>
+  </basic-container>
+</template>
+
+<script>
+import { getWaylineFileByUser } from '@/api/resource/waylineFile'
+import { getAirportList } from '@/api/device/device'
+
+export default {
+  name: 'WaylineFile',
+  data () {
+    const { defaultStartDate, defaultEndDate } = this.getDefaultDates()
+    return {
+      form: {},
+      query: { name: '', dock_name: '', daterange: [defaultStartDate, defaultEndDate] },
+      loading: false,
+      page: { pageSize: 10, currentPage: 1, total: 0 },
+      selectedWorkspaceId: null,
+      data: [],
+      option: {
+        tip: false,
+        searchShow: true,
+        searchGutter: 30,
+        searchMenuPosition: 'left',
+        searchMenuSpan: 4,
+        border: true,
+        index: true,
+        menu: false,
+        page: true,
+
+        height: 'auto',
+        calcHeight: 20,
+        column: [
+          {
+            label: '机场选择',
+            prop: 'dock_name',
+            type: 'select',
+            search: true,
+            searchSpan: 4,
+            dicData: [],
+            props: { label: 'nickname', value: 'nickname' },
+            hide: true,
+             change: (value) => {
+               this.searchChange({ ...this.query,dock_name:value.value}, () => {});
+          }
+          },
+          {
+            label: '航线名称',
+            prop: 'name',
+            search: true,
+            searchSpan: 4,
+          },
+          {
+            label: '时间范围',
+            prop: 'daterange',
+            type: 'daterange',
+            search: true,
+            searchRange: true,
+            searchSpan: 8,
+            format: 'YYYY-MM-DD',
+            valueFormat: 'YYYY-MM-DD',
+            startPlaceholder: '开始时间',
+            endPlaceholder: '结束时间',
+            searchValue: [defaultStartDate, defaultEndDate],
+            hide: true,
+             change: (value) => {
+               this.searchChange({ ...this.query,daterange:value.value}, () => {});
+          }
+          },
+          { label: '航线名称', prop: 'name' },
+          {
+            label: '航线类型',
+            prop: 'wayline_type',
+            formatter: (row) => this.formatWaylineType(row.wayline_type),
+          },
+          { label: '用户名', prop: 'user_name' },
+          { label: '航线ID', prop: 'id' },
+          {
+            label: '更新时间',
+            prop: 'update_time',
+            formatter: (row) => this.formatTime(row.update_time),
+          },
+        ],
+      },
+    }
+  },
+  mounted () {
+    this.fetchAirports()
+    this.fetchData(this.page, this.query) // 初次加载包含默认时间范围
+  },
+  methods: {
+    getDefaultDates () {
+      const endDate = new Date()
+      const startDate = new Date()
+      startDate.setMonth(startDate.getMonth() - 1)
+      // 结束时间设定为当天 23:59:59
+      endDate.setHours(23, 59, 59, 999)
+
+      return {
+        defaultStartDate: `${startDate.getFullYear()}-${String(startDate.getMonth() + 1).padStart(2, '0')}-${String(startDate.getDate()).padStart(2, '0')}`,
+        defaultEndDate: `${endDate.getFullYear()}-${String(endDate.getMonth() + 1).padStart(2, '0')}-${String(endDate.getDate()).padStart(2, '0')}`,
+      }
+    },
+    async fetchAirports () {
+      try {
+        const res = await getAirportList()
+        const airportData = res.data.data || []
+        const airportColumn = this.option.column.find((col) => col.prop === 'dock_name')
+        if (airportColumn) {
+          airportColumn.dicData = airportData.map((item) => ({
+            nickname: item.nickname,
+            workspace_id: item.workspace_id,
+          }))
+        }
+      } catch (error) {
+        console.error('加载机场列表失败:', error)
+      }
+    },
+    async fetchData (page, params = {}) {
+      this.loading = true
+      const { name, dock_name, daterange } = params
+      const workspaceId = this.selectedWorkspaceId || null
+
+      // 时间范围处理
+      const startDate = new Date(daterange[0])
+      let endDate = new Date(daterange[1])
+      // 结束时间始终设定为当天 23:59:59
+      endDate.setHours(23, 59, 59, 999)
+
+      const startTime = isNaN(startDate.getTime()) ? null : startDate.getTime()
+      const endTime = isNaN(endDate.getTime()) ? null : endDate.getTime()
+
+      const requestParams = {
+        workspaceId,
+        waylineName: name || null,
+        startTime,
+        endTime,
+        page: page.currentPage,
+        pageSize: page.pageSize,
+      }
+
+      console.log('请求参数:', requestParams)
+
+      try {
+        const res = await getWaylineFileByUser(
+          requestParams.workspaceId,
+          requestParams.waylineName,
+          requestParams.startTime,
+          requestParams.endTime,
+          requestParams.page,
+          requestParams.pageSize
+        )
+        const responseData = res.data
+
+        if (responseData?.code === 0 && Array.isArray(responseData.data?.list)) {
+          this.data = responseData.data.list.map((item) => ({
+            id: item.id,
+            name: item.name,
+            wayline_type: item.wayline_type,
+            user_name: item.user_name,
+            update_time: item.update_time,
+          }))
+          this.page.total = responseData.data.pagination?.total || responseData.data.list.length
+          this.page.currentPage = page.currentPage
+          console.log('返回数据条数:', this.data.length)
+          console.log('当前页码:', this.page.currentPage)
+          console.log('总条数:', this.page.total)
+        } else {
+          this.data = []
+          this.page.total = 0
+        }
+      } catch (error) {
+        console.error('加载数据失败:', error)
+        this.data = []
+        this.page.total = 0
+      } finally {
+        this.loading = false
+      }
+    },
+    searchChange (params, done) {
+      this.query = { ...params }
+      const airportColumn = this.option.column.find((col) => col.prop === 'dock_name')
+      const selectedAirport = airportColumn.dicData.find((item) => item.nickname === params.dock_name)
+      this.selectedWorkspaceId = selectedAirport ? selectedAirport.workspace_id : null
+      this.page.currentPage = 1
+      this.fetchData(this.page, this.query)
+      done()
+    },
+    searchReset () {
+      const { defaultStartDate, defaultEndDate } = this.getDefaultDates()
+      this.query = { name: '', dock_name: '', daterange: [defaultStartDate, defaultEndDate] }
+      this.selectedWorkspaceId = null
+      const daterangeColumn = this.option.column.find((col) => col.prop === 'daterange')
+      daterangeColumn.searchValue = [defaultStartDate, defaultEndDate]
+      this.page.currentPage = 1
+      this.fetchData(this.page, this.query)
+    },
+    currentChange (currentPage) {
+      console.log('切换页码至:', currentPage)
+      this.page.currentPage = currentPage
+      this.fetchData(this.page, this.query)
+    },
+    sizeChange (pageSize) {
+      console.log('每页条数改为:', pageSize)
+      this.page.pageSize = pageSize
+      this.page.currentPage = 1
+      this.fetchData(this.page, this.query)
+    },
+    refreshChange () {
+      this.fetchData(this.page, this.query)
+    },
+    formatTime (time) {
+      if (!time) return ''
+      const date = new Date(time < 10000000000 ? time * 1000 : time)
+      if (isNaN(date.getTime())) return '无效时间'
+      return date.toLocaleString('zh-CN', { hour12: false }).replace(/\//g, '-')
+    },
+    formatWaylineType (type) {
+      const typeMap = { '0': '普通航线', '1': '图斑举证航线', '2': '航测航线', '3': '航点航线', '4': '正射举证航线' }
+      return typeMap[type] || '未知'
+    },
+  },
+}
+</script>
+
+<style scoped lang="scss">
+.avue-crud .el-table {
+  max-height: none !important;
+}
+</style>

--
Gitblit v1.9.3