Browse Source

完善媒体管理

李富豪 1 year ago
parent
commit
b32031196a

+ 21 - 0
Web/package-lock.json

@@ -17,6 +17,7 @@
         "mitt": "^3.0.0",
         "moment": "^2.30.0",
         "mqtt": "^4.3.7",
+        "photo-sphere-viewer": "^4.8.1",
         "reconnecting-websocket": "^4.4.0",
         "vconsole": "^3.15.0",
         "vue": "3.2.26",
@@ -1833,6 +1834,16 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/photo-sphere-viewer": {
+      "version": "4.8.1",
+      "resolved": "https://registry.npmjs.org/photo-sphere-viewer/-/photo-sphere-viewer-4.8.1.tgz",
+      "integrity": "sha512-Yl1KZq1adtrajCOrf8Y79Qi4A35DfEu8atL779YOdA9XHoH2l2+sYovejnZlGgUM0hEbTyenRDoyXSy/MtioYg==",
+      "deprecated": "Use @photo-sphere-viewer/core instead, see https://photo-sphere-viewer.js.org/guide/migration.html",
+      "dependencies": {
+        "three": "^0.147.0",
+        "uevent": "^2.1.1"
+      }
+    },
     "node_modules/picocolors": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz",
@@ -2078,6 +2089,11 @@
         "safe-buffer": "~5.2.0"
       }
     },
+    "node_modules/three": {
+      "version": "0.147.0",
+      "resolved": "https://registry.npmjs.org/three/-/three-0.147.0.tgz",
+      "integrity": "sha512-LPTOslYQXFkmvceQjFTNnVVli2LaVF6C99Pv34fJypp8NbQLbTlu3KinZ0zURghS5zEehK+VQyvWuPZ/Sm8fzw=="
+    },
     "node_modules/to-regex-range": {
       "version": "5.0.1",
       "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@@ -2130,6 +2146,11 @@
         "node": "*"
       }
     },
+    "node_modules/uevent": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/uevent/-/uevent-2.2.0.tgz",
+      "integrity": "sha512-48s5LF/c6R1fUmctGib/dWKhZjZLd4aK/85dwVAbwgHNBSO0k0UNp0ZKZpkSbU6633qYhgykYQPakTSuOxZopA=="
+    },
     "node_modules/undici-types": {
       "version": "5.26.5",
       "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",

+ 1 - 0
Web/package.json

@@ -18,6 +18,7 @@
     "mitt": "^3.0.0",
     "moment": "^2.30.0",
     "mqtt": "^4.3.7",
+    "photo-sphere-viewer": "^4.8.1",
     "reconnecting-websocket": "^4.4.0",
     "vconsole": "^3.15.0",
     "vue": "3.2.26",

+ 8 - 0
Web/src/api/custom/index.ts

@@ -33,6 +33,7 @@ export type FetchFeedbackRecordListApi = (data: FetchFeedbackRecordListApiParams
 export type FetchChangeRecordListApi = (params: FetchFeedbackRecordListApiParams) => Promise<any>;
 export type FetchProjectListApi = () => Promise<any>;
 export type FetchMediaFileListApi = (params: FetchMediaFileListApiParams) => Promise<any>;
+export type DownloadMediaFileApi = (dirId: string) => Promise<any>;
 export type FetchFileListByFolderApi = (dirId: string, params: FetchFileListByFolderApiParams) => Promise<any>;
 export type FetchFileDetailApi = (fileId: string) => Promise<any>;
 
@@ -60,6 +61,12 @@ const fetchMediaFileListApi: FetchMediaFileListApi = async (params) => {
     return res.data;
 };
 
+// 下载文件夹
+const downloadMediaFileApi: DownloadMediaFileApi = async (dirId) => {
+    const res = await request.get(`/media/api/v1/files/${workspaceId}/fileDownList/${dirId}`);
+    return res.data;
+};
+
 // 获取文件夹下所有文件
 const fetchFileListByFolderApi: FetchFileListByFolderApi = async (dirId, params) => {
     const res = await request.get(`/media/api/v1/files/${workspaceId}/files/${dirId}`, { params: params });
@@ -77,6 +84,7 @@ export const apis = {
     fetchChangeRecordList: fetchChangeRecordListApi,
     fetchProjectList: fetchProjectListApi,
     fetchMediaFileList: fetchMediaFileListApi,
+    downloadMediaFile: downloadMediaFileApi,
     fetchFileListByFolder: fetchFileListByFolderApi,
     fetchFileDetail: fetchFileDetailApi,
 };

+ 1 - 1
Web/src/components/common/nav.vue

@@ -25,7 +25,7 @@
           轨迹回放
         </span>
       </a-menu-item>
-      <a-menu-item :key="'/' + ERouterName.MEMBERS">
+      <a-menu-item :key="'/' + ERouterName.MEMBER">
         <TeamOutlined />
         <span>
           成员管理

+ 42 - 0
Web/src/components/panoramic/index.vue

@@ -0,0 +1,42 @@
+<template>
+    <div id="viewer"></div>
+</template>
+
+<script lang="ts" setup>
+import { onMounted } from 'vue';
+import { Viewer } from 'photo-sphere-viewer';
+import 'photo-sphere-viewer/dist/photo-sphere-viewer.css';
+
+interface Props {
+    src: string,
+};
+
+const props = withDefaults(defineProps<Props>(), {
+
+});
+
+onMounted(() => {
+    new Viewer({
+        container: document.querySelector('#viewer') as any,
+        panorama: props.src,
+        size: {
+            width: '100%' as any,
+            height: '100%' as any,
+        },
+        navbar: [
+            'zoom',
+            'move',
+            'caption',
+            'fullscreen',
+        ],
+        lang: {
+            zoomOut: '',
+            zoomIn: '',
+            move: '',
+            fullscreen: '',
+        }
+    });
+})
+</script>
+
+<style lang="scss" scoped></style>

+ 5 - 10
Web/src/hooks/use-g-map.ts

@@ -15,11 +15,10 @@ export function useGMapManage() {
     }).then((AMap) => {
       state.aMap = AMap
       state.map = new AMap.Map(container, {
-        center: [113.943225499, 22.577673716],
-        zoom: 20
+        center: [121.48, 31.22],
+        zoom: 12
       })
       state.mouseTool = new AMap.MouseTool(state.map)
-
       // 挂在到全局
       app.config.globalProperties.$aMap = state.aMap
       app.config.globalProperties.$map = state.map
@@ -33,18 +32,14 @@ export function useGMapManage() {
     initMap('g-container', app)
   }
 
-  async function newMap(containerId: string, config: any) {
-    AMapLoader.load({
+  async function asyncInitMap() {
+    return AMapLoader.load({
       ...AMapConfig
-    }).then((AMap) => {
-      new AMap.Map(containerId, config)
-    }).catch(e => {
-      console.log(e)
     })
   }
 
   return {
     globalPropertiesConfig,
-    newMap,
+    asyncInitMap,
   }
 }

+ 59 - 9
Web/src/pages/page-web/projects/media/detail/components/FileInfo.vue

@@ -4,7 +4,11 @@
       <a-row>
         <a-col flex="auto">
           <div class="fileInfo-detail-left">
-            <a-image width="100%" height="100%" :src="state.info.url" />
+            <div class="fileInfo-detail-left-background" v-if="state.imgLoading">
+              <a-spin tip="加载中..." />
+            </div>
+            <a-image width="100%" height="100%" :src="state.info.url" v-else-if="1" />
+            <Panoramic :src="state.info.url" v-else-if="2" />
           </div>
         </a-col>
         <a-col flex="420px">
@@ -110,6 +114,14 @@
               </div>
               <div class="fileInfo-detail-info-map-content">
                 <div id="photoPositionMap" :style="{ width: '100%', height: '100%' }"></div>
+                <div class="fileInfo-detail-info-map-content-title">
+                  <span>
+                    {{ state.info.latitude }}° N
+                  </span>
+                  <span>
+                    {{ state.info.longitude }}° E
+                  </span>
+                </div>
               </div>
             </div>
           </div>
@@ -135,6 +147,7 @@
 <script lang="ts" setup>
 import { reactive, onMounted } from 'vue';
 import { CloseOutlined, EnvironmentOutlined, DownloadOutlined, DeleteOutlined } from '@ant-design/icons-vue';
+import Panoramic from '/@/components/panoramic/index.vue';
 import { useGMapManage } from '/@/hooks/use-g-map';
 import { apis } from '/@/api/custom';
 
@@ -148,6 +161,7 @@ const props = withDefaults(defineProps<Props>(), {
 });
 
 const state = reactive({
+  imgLoading: true,// 图片加载
   info: {
     url: '',
     file_name: '',
@@ -173,16 +187,20 @@ onMounted(async () => {
   try {
     const res = await apis.fetchFileDetail(props.fileId);
     state.info = res.data;
+    const img = new Image();
+    img.src = state.info.url;
+    img.onload = () => {
+      state.imgLoading = false;
+    };
     if (state.info.longitude && state.info.latitude) {
-      AmapHook.newMap('photoPositionMap', {
-        pitch: 50, // 地图俯仰角度,有效范围 0 度- 83 度
-        viewMode: '3D', // 地图模式
-        rotateEnable: true, // 是否开启地图旋转交互 鼠标右键 + 鼠标画圈移动 或 键盘Ctrl + 鼠标左键画圈移动
-        pitchEnable: true, // 是否开启地图倾斜交互 鼠标右键 + 鼠标上下移动或键盘Ctrl + 鼠标左键上下移动
+      const AMap = await AmapHook.asyncInitMap();
+      new AMap.Map('photoPositionMap', {
+        layers: [new AMap.TileLayer.Satellite()],// 卫星图-图层
+        viewMode: '3D',// 3D地图
+        rotateEnable: true,// 开启地图旋转交互
+        pitchEnable: true,// 开启地图倾斜交互
         zoom: 17, // 初始化地图层级
-        rotation: -15, // 初始地图顺时针旋转的角度
-        zooms: [2, 20], // 地图显示的缩放级别范围
-        center: [state.info.longitude, state.info.latitude],
+        center: [state.info.longitude, state.info.latitude],// 中心点
       })
     }
   } catch (e) {
@@ -220,6 +238,15 @@ onMounted(async () => {
       width: 100%;
       height: calc(100vh - 120px);
       overflow: hidden;
+
+      &-background {
+        width: 100%;
+        height: calc(100vh - 120px);
+        padding: 100px;
+        display: flex;
+        justify-content: center;
+        align-items: center;
+      }
     }
 
     &-info {
@@ -284,6 +311,21 @@ onMounted(async () => {
           background: #dddddd;
           border-radius: 4px;
           margin-top: 10px;
+          position: relative;
+          overflow: hidden;
+
+          &-title {
+            width: 100%;
+            height: 28px;
+            padding-left: 10px;
+            background: rgba(0, 0, 0, .5);
+            line-height: 28px;
+            font-size: 14px;
+            color: #fff;
+            position: absolute;
+            bottom: 0;
+            left: 0;
+          }
         }
       }
     }
@@ -319,4 +361,12 @@ onMounted(async () => {
     }
   }
 }
+
+.amap-logo {
+  display: none !important;
+}
+
+.amap-copyright {
+  display: none !important;
+}
 </style>

+ 4 - 1
Web/src/pages/page-web/projects/media/index/index.vue

@@ -90,6 +90,7 @@ import Search from './components/Search.vue';
 import fileSrc from '/@/assets/media/file.svg';
 import { apis } from '/@/api/custom';
 import router from '/@/router/index';
+import { downloadFile } from '/@/utils/common';
 
 interface State {
   listLoading: boolean,
@@ -241,7 +242,9 @@ const onSelectChange = (selectedRowKeys: string[]) => {
 const onClickDownload = async (record: any) => {
   state.listLoading = true;
   try {
-
+    const bold = await apis.downloadMediaFile(record.id);
+    const data = new Blob([bold], { type: 'application/zip' });
+    downloadFile(data, record.dir_name)
   } catch (e) {
     console.error(e);
   } finally {

+ 66 - 0
Web/src/pages/page-web/projects/member/components/Search.vue

@@ -0,0 +1,66 @@
+<template>
+  <a-row style="margin-bottom: 20px;" justify="end">
+    <a-col>
+      <a-form ref="formRef" layout="inline" :model="formModel" :colon="false">
+        <a-form-item name="device_name">
+          <a-select style="width: 200px;" placeholder="请选择项目" v-model:value="formModel.device_name">
+            <a-select-option value="1">
+              项目编号 0001
+            </a-select-option>
+          </a-select>
+        </a-form-item>
+        <a-form-item name="keyword">
+          <a-input style="width: 200px;" placeholder="用户名称" v-model:value="formModel.keyword" />
+        </a-form-item>
+        <a-form-item>
+          <a-button style="margin-right: 10px;" @click="handleClicSekarch">
+            <template #icon>
+              <SearchOutlined />
+            </template>
+          </a-button>
+          <a-button @click="handleClickReset">
+            <template #icon>
+              <ReloadOutlined />
+            </template>
+          </a-button>
+        </a-form-item>
+      </a-form>
+    </a-col>
+  </a-row>
+</template>
+
+<script lang="ts" setup>
+import { ref, reactive } from 'vue';
+import { SearchOutlined, ReloadOutlined } from '@ant-design/icons-vue';
+
+interface Props {
+  onClickSearch: (query: any) => Promise<any>,
+  onClickReset: (query: any) => Promise<any>,
+};
+
+const props = withDefaults(defineProps<Props>(), {
+
+});
+
+const formRef = ref();
+
+const formModel = reactive({
+  device_name: undefined,
+  keyword: '',
+})
+
+// 点击查询
+const handleClicSekarch = async () => {
+  const values = formRef.value?.getFieldsValue();
+  await props.onClickSearch(values);
+}
+
+// 点击重置
+const handleClickReset = async () => {
+  formRef.value?.resetFields();
+  const values = formRef.value?.getFieldsValue();
+  await props.onClickReset(values);
+}
+</script>
+
+<style lang="scss" scoped></style>

+ 193 - 0
Web/src/pages/page-web/projects/member/index.vue

@@ -0,0 +1,193 @@
+<template>
+  <div class="mediaList">
+    <Search :onClickSearch="onClickSearch" :onClickReset="onClickReset" />
+    <div class="mediaList-table">
+      <a-table :columns="columns" :data-source="data.member" :pagination="paginationConfig" @change="refreshData"
+        row-key="user_id" :rowClassName="(record, index) => ((index % 2) === 0 ? 'table-striped' : null)"
+        :scroll="{ x: '100%', y: 600 }">
+        <template v-for="col in ['mqtt_username', 'mqtt_password']" #[col]="{ text, record }" :key="col">
+          <div>
+            <a-input v-if="editableData[record.user_id]" v-model:value="editableData[record.user_id][col]"
+              style="margin: -5px 0" />
+            <template v-else>
+              {{ text }}
+            </template>
+          </div>
+        </template>
+        <template #action="{ record }">
+          <div class="editable-row-operations">
+            <span v-if="editableData[record.user_id]">
+              <a-tooltip title="确定">
+                <span @click="save(record)" style="color: #28d445;">
+                  <CheckOutlined />
+                </span>
+              </a-tooltip>
+              <a-tooltip title="取消">
+                <span @click="() => delete editableData[record.user_id]" class="ml15" style="color: #e70102;">
+                  <CloseOutlined />
+                </span>
+              </a-tooltip>
+            </span>
+            <span v-else class="fz18 flex-align-center flex-row" style="color: #2d8cf0">
+              <a-tooltip title="编辑">
+                <EditOutlined @click="edit(record)" />
+              </a-tooltip>
+            </span>
+          </div>
+        </template>
+      </a-table>
+    </div>
+  </div>
+</template>
+<script lang="ts" setup>
+import { message } from 'ant-design-vue'
+import { TableState } from 'ant-design-vue/lib/table/interface'
+import { onMounted, reactive, UnwrapRef } from 'vue'
+import Search from './components/Search.vue';
+import { IPage } from '/@/api/http/type'
+import { getAllUsersInfo, updateUserInfo } from '/@/api/manage'
+import { ELocalStorageKey } from '/@/types'
+import { EditOutlined, CheckOutlined, CloseOutlined } from '@ant-design/icons-vue'
+
+export interface Member {
+  user_id: string
+  username: string
+  user_type: string
+  workspace_name: string
+  create_time: string
+  mqtt_username: string
+  mqtt_password: string
+}
+
+interface MemberData {
+  member: Member[]
+}
+
+const columns = [
+  {
+    title: '用户名称',
+    dataIndex: 'username',
+    width: 150,
+    sorter: (a: Member, b: Member) => a.username.localeCompare(b.username),
+  },
+  {
+    title: '用户类型',
+    dataIndex: 'user_type',
+    width: 150,
+  },
+  {
+    title: '项目名称',
+    dataIndex: 'workspace_name',
+    width: 150,
+  },
+  {
+    title: 'Mqtt 用户',
+    dataIndex: 'mqtt_username',
+    width: 150,
+    slots: { customRender: 'mqtt_username' },
+  },
+  {
+    title: 'Mqtt 密码',
+    dataIndex: 'mqtt_password',
+    width: 150,
+    slots: { customRender: 'mqtt_password' },
+  },
+  {
+    title: '创建时间',
+    dataIndex: 'create_time',
+    width: 150,
+    sorter: (a: Member, b: Member) => a.create_time.localeCompare(b.create_time),
+  },
+  {
+    title: '操作',
+    dataIndex: 'action',
+    width: 80,
+    slots: { customRender: 'action' },
+  }
+]
+
+const data = reactive<MemberData>({
+  member: []
+})
+
+const editableData: UnwrapRef<Record<string, Member>> = reactive({})
+
+const paginationConfig = reactive({
+  pageSizeOptions: ['20', '50', '100'],
+  showQuickJumper: true,
+  showSizeChanger: true,
+  pageSize: 20,
+  current: 1,
+  total: 0
+})
+
+type Pagination = TableState['pagination']
+
+const body: IPage = {
+  page: 1,
+  total: 0,
+  page_size: 50
+}
+const workspaceId: string = localStorage.getItem(ELocalStorageKey.WorkspaceId)!
+
+onMounted(() => {
+  getAllUsers(workspaceId, body)
+})
+
+function refreshData(page: Pagination) {
+  body.page = page?.current!
+  body.page_size = page?.pageSize!
+  getAllUsers(workspaceId, body)
+}
+
+function getAllUsers(workspaceId: string, page: IPage) {
+  getAllUsersInfo(workspaceId, page).then(res => {
+    const userList: Member[] = res.data.list
+    data.member = userList
+    paginationConfig.total = res.data.pagination.total
+    paginationConfig.current = res.data.pagination.page
+  })
+}
+
+function edit(record: Member) {
+  editableData[record.user_id] = record
+}
+
+function save(record: Member) {
+  delete editableData[record.user_id]
+  updateUserInfo(workspaceId, record.user_id, record).then(res => {
+    if (res.code !== 0) {
+      message.error(res.message)
+    }
+  })
+}
+
+// 点击搜索
+const onClickSearch = async () => {
+  console.log('点击搜索');
+}
+
+// 点击重置
+const onClickReset = async () => {
+  console.log('点击重置');
+}
+</script>
+
+<style lang="scss">
+.mediaList {
+  padding: 20px;
+}
+
+.ant-table {
+  border-top: 1px solid rgb(0, 0, 0, 0.06);
+  border-bottom: 1px solid rgb(0, 0, 0, 0.06);
+}
+
+.ant-table-tbody tr td {
+  border: 0;
+}
+
+.table-striped {
+  background-color: #f7f9fa;
+}
+</style>

+ 0 - 174
Web/src/pages/page-web/projects/members.vue

@@ -1,174 +0,0 @@
-<template>
-  <div class="table flex-display flex-column">
-    <a-table :columns="columns" :data-source="data.member" :pagination="paginationProp" @change="refreshData"
-      row-key="user_id" :row-selection="rowSelection"
-      :rowClassName="(record, index) => ((index % 2) === 0 ? 'table-striped' : null)" :scroll="{ x: '100%', y: 600 }">
-      <template v-for="col in ['mqtt_username', 'mqtt_password']" #[col]="{ text, record }" :key="col">
-        <div>
-          <a-input v-if="editableData[record.user_id]" v-model:value="editableData[record.user_id][col]"
-            style="margin: -5px 0" />
-          <template v-else>
-            {{ text }}
-          </template>
-        </div>
-      </template>
-      <template #action="{ record }">
-        <div class="editable-row-operations">
-          <span v-if="editableData[record.user_id]">
-            <a-tooltip title="Confirm changes">
-              <span @click="save(record)" style="color: #28d445;">
-                <CheckOutlined />
-              </span>
-            </a-tooltip>
-            <a-tooltip title="Modification canceled">
-              <span @click="() => delete editableData[record.user_id]" class="ml15" style="color: #e70102;">
-                <CloseOutlined />
-              </span>
-            </a-tooltip>
-          </span>
-          <span v-else class="fz18 flex-align-center flex-row" style="color: #2d8cf0">
-            <EditOutlined @click="edit(record)" />
-          </span>
-        </div>
-      </template>
-    </a-table>
-  </div>
-</template>
-<script lang="ts" setup>
-import { message } from 'ant-design-vue'
-import { TableState } from 'ant-design-vue/lib/table/interface'
-import { onMounted, reactive, Ref, ref, UnwrapRef } from 'vue'
-import { IPage } from '/@/api/http/type'
-import { getAllUsersInfo, updateUserInfo } from '/@/api/manage'
-import { ELocalStorageKey } from '/@/types'
-import { EditOutlined, CheckOutlined, CloseOutlined } from '@ant-design/icons-vue'
-
-export interface Member {
-  user_id: string
-  username: string
-  user_type: string
-  workspace_name: string
-  create_time: string
-  mqtt_username: string
-  mqtt_password: string
-}
-
-interface MemberData {
-  member: Member[]
-}
-const columns = [
-  { title: '用户名称', dataIndex: 'username', width: 150, sorter: (a: Member, b: Member) => a.username.localeCompare(b.username), className: 'titleStyle' },
-  { title: '用户类型', dataIndex: 'user_type', width: 150, className: 'titleStyle' },
-  { title: '项目名称', dataIndex: 'workspace_name', width: 150, className: 'titleStyle' },
-  { title: 'Mqtt 用户', dataIndex: 'mqtt_username', width: 150, className: 'titleStyle', slots: { customRender: 'mqtt_username' } },
-  { title: 'Mqtt 密码', dataIndex: 'mqtt_password', width: 150, className: 'titleStyle', slots: { customRender: 'mqtt_password' } },
-  { title: '创建时间', dataIndex: 'create_time', width: 150, sorter: (a: Member, b: Member) => a.create_time.localeCompare(b.create_time), className: 'titleStyle' },
-  { title: '操作', dataIndex: 'action', width: 100, className: 'titleStyle', slots: { customRender: 'action' } },
-]
-
-const data = reactive<MemberData>({
-  member: []
-})
-
-const editableData: UnwrapRef<Record<string, Member>> = reactive({})
-
-const paginationProp = reactive({
-  pageSizeOptions: ['20', '50', '100'],
-  showQuickJumper: true,
-  showSizeChanger: true,
-  pageSize: 50,
-  current: 1,
-  total: 0
-})
-
-const rowSelection = {
-  onChange: (selectedRowKeys: (string | number)[], selectedRows: []) => {
-    console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows)
-  },
-  onSelect: (record: any, selected: boolean, selectedRows: []) => {
-    console.log(record, selected, selectedRows)
-  },
-  onSelectAll: (selected: boolean, selectedRows: [], changeRows: []) => {
-    console.log(selected, selectedRows, changeRows)
-  },
-}
-type Pagination = TableState['pagination']
-
-const body: IPage = {
-  page: 1,
-  total: 0,
-  page_size: 50
-}
-const workspaceId: string = localStorage.getItem(ELocalStorageKey.WorkspaceId)!
-
-onMounted(() => {
-  getAllUsers(workspaceId, body)
-})
-
-function refreshData(page: Pagination) {
-  body.page = page?.current!
-  body.page_size = page?.pageSize!
-  getAllUsers(workspaceId, body)
-}
-
-function getAllUsers(workspaceId: string, page: IPage) {
-  getAllUsersInfo(workspaceId, page).then(res => {
-    const userList: Member[] = res.data.list
-    data.member = userList
-    paginationProp.total = res.data.pagination.total
-    paginationProp.current = res.data.pagination.page
-  })
-}
-
-function edit(record: Member) {
-  editableData[record.user_id] = record
-}
-
-function save(record: Member) {
-  delete editableData[record.user_id]
-  updateUserInfo(workspaceId, record.user_id, record).then(res => {
-    if (res.code !== 0) {
-      message.error(res.message)
-    }
-  })
-}
-
-</script>
-<style>
-.table {
-  background-color: white;
-  margin: 20px;
-  padding: 20px;
-  height: 88vh;
-}
-
-.table-striped {
-  background-color: #f7f9fa;
-}
-
-.ant-table {
-  border-top: 1px solid rgb(0, 0, 0, 0.06);
-  border-bottom: 1px solid rgb(0, 0, 0, 0.06);
-}
-
-.ant-table-tbody tr td {
-  border: 0;
-}
-
-.ant-table td {
-  white-space: nowrap;
-}
-
-.ant-table-thead tr th {
-  background: white !important;
-  border: 0;
-}
-
-th.ant-table-selection-column {
-  background-color: white !important;
-}
-
-.ant-table-header {
-  background-color: white !important;
-}
-</style>

+ 0 - 11
Web/src/pages/page-web/projects/trajectory/index.vue

@@ -1,11 +0,0 @@
-<template>
-  <div>
-    轨迹回放
-  </div>
-</template>
-
-<script lang="ts" setup>
-
-</script>
-
-<style lang="scss" scoped></style>

+ 70 - 0
Web/src/pages/page-web/projects/trajectory/index/components/Search.vue

@@ -0,0 +1,70 @@
+<template>
+  <a-row style="margin-bottom: 20px;" justify="end">
+    <a-col>
+      <a-form ref="formRef" layout="inline" :model="formModel" :colon="false">
+        <a-form-item name="date">
+          <a-range-picker style="width: 250px;" v-model:value="formModel.date" />
+        </a-form-item>
+        <a-form-item name="device_name">
+          <a-select style="width: 200px;" placeholder="请选择拍摄负载" v-model:value="formModel.device_name">
+            <a-select-option value="1">
+              Mavic 3E Camera
+            </a-select-option>
+          </a-select>
+        </a-form-item>
+        <a-form-item name="keyword">
+          <a-input style="width: 200px;" placeholder="文件夹名称、计划名称" v-model:value="formModel.keyword" />
+        </a-form-item>
+        <a-form-item>
+          <a-button style="margin-right: 10px;" @click="handleClicSekarch">
+            <template #icon>
+              <SearchOutlined />
+            </template>
+          </a-button>
+          <a-button @click="handleClickReset">
+            <template #icon>
+              <ReloadOutlined />
+            </template>
+          </a-button>
+        </a-form-item>
+      </a-form>
+    </a-col>
+  </a-row>
+</template>
+
+<script lang="ts" setup>
+import { ref, reactive } from 'vue';
+import { SearchOutlined, ReloadOutlined } from '@ant-design/icons-vue';
+
+interface Props {
+  onClickSearch: (query: any) => Promise<any>,
+  onClickReset: (query: any) => Promise<any>,
+};
+
+const props = withDefaults(defineProps<Props>(), {
+
+});
+
+const formRef = ref();
+
+const formModel = reactive({
+  date: [],
+  device_name: undefined,
+  keyword: '',
+})
+
+// 点击查询
+const handleClicSekarch = async () => {
+  const values = formRef.value?.getFieldsValue();
+  await props.onClickSearch(values);
+}
+
+// 点击重置
+const handleClickReset = async () => {
+  formRef.value?.resetFields();
+  const values = formRef.value?.getFieldsValue();
+  await props.onClickReset(values);
+}
+</script>
+
+<style lang="scss" scoped></style>

+ 322 - 0
Web/src/pages/page-web/projects/trajectory/index/index.vue

@@ -0,0 +1,322 @@
+<template>
+  <div class="mediaList">
+    <Search :mode="state.mode" :selectedRowKeys="state.selectedRowKeys" :onClickDownload="onClickBatchDownload"
+      :onClickSearch="onClickSearch" :onClickReset="onClickReset" />
+    <div style="background: #FFFFFF;padding: 20px;">
+      <div class="mediaList-info">
+        <div>
+          轨迹回放文件
+        </div>
+        <div class="mediaList-info-right">
+          <div class="mediaList-info-right-text">
+            <div>
+              已选/全部:
+            </div>
+            <div>
+              {{ state.selectedRowKeys.length }}/{{ paginationConfig.total }}
+            </div>
+          </div>
+          <a-button style="padding:0 5px;" :type="state.mode === 'table' ? 'primary' : 'default'"
+            :ghost="state.mode === 'table'" size="small" @click="state.mode = 'table'">
+            <MenuOutlined />
+          </a-button>
+          <a-button style="padding:0 5px;" :type="state.mode === 'list' ? 'primary' : 'default'"
+            :ghost="state.mode === 'list'" size="small" @click="state.mode = 'list'">
+            <AppstoreOutlined />
+          </a-button>
+        </div>
+      </div>
+      <div v-if="false">
+        <div class="mediaList-table" v-if="state.mode === 'table'">
+          <a-table :scroll="{ x: '100%', y: 500 }" :childrenColumnName="null" rowKey="id" :loading="state.listLoading"
+            :columns="columns" :dataSource="state.list" :rowClassName="rowClassName" :pagination="paginationConfig"
+            :rowSelection="{ selectedRowKeys: state.selectedRowKeys, onChange: onSelectChange }">
+            <!-- 文件夹名称 -->
+            <template #dir_name="{ record }">
+              <a-tooltip :title="record.dir_name">
+                <div class="fileName" @click="onClickFile(record)">
+                  <img :src="fileSrc">
+                  <div>
+                    {{ record.dir_name }}
+                  </div>
+                </div>
+              </a-tooltip>
+            </template>
+            <!-- 操作 -->
+            <template #action="{ record }">
+              <div class="flex-align-center flex-row" style="color: #2d8cf0">
+                <a-tooltip title="压缩下载">
+                  <DownloadOutlined style="margin-right: 10px;" @click="onClickDownload(record)" />
+                </a-tooltip>
+                <a-tooltip title="轨迹回放">
+                  <EnvironmentOutlined @click="onClickTrajectory" />
+                </a-tooltip>
+              </div>
+            </template>
+          </a-table>
+        </div>
+        <div class="mediaList-list" v-else>
+          <a-list :grid="{ gutter: 16, xs: 1, sm: 2, md: 3, lg: 4, xl: 8, xxl: 12 }" :loading="state.listLoading"
+            :dataSource="state.list" :pagination="paginationConfig">
+            <template #renderItem="{ item }">
+              <a-list-item>
+                <a-card hoverable @click="onClickFile(item)">
+                  <template #cover>
+                    <div style="display: flex;justify-content:center;align-items: center;">
+                      <img style="width: 70%;" :src="fileSrc" />
+                    </div>
+                  </template>
+                  <a-card-meta>
+                    <template #description>
+                      <a-tooltip placement="bottom" :title="item.dir_name">
+                        <div class="mediaList-list-name">
+                          {{ item.dir_name }}
+                        </div>
+                      </a-tooltip>
+                    </template>
+                  </a-card-meta>
+                </a-card>
+              </a-list-item>
+            </template>
+          </a-list>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { reactive, onMounted } from 'vue';
+import { MenuOutlined, AppstoreOutlined, DownloadOutlined, EnvironmentOutlined } from '@ant-design/icons-vue';
+import Search from './components/Search.vue';
+import fileSrc from '/@/assets/media/file.svg';
+import { apis } from '/@/api/custom';
+import router from '/@/router/index';
+import { downloadFile } from '/@/utils/common';
+
+interface State {
+  listLoading: boolean,
+  list: any[],
+  selectedRowKeys: string[],
+  mode: 'table' | 'list',
+};
+
+const state: State = reactive({
+  listLoading: false,
+  list: [],
+  selectedRowKeys: [],
+  mode: 'table',
+})
+
+const paginationConfig = reactive({
+  pageSizeOptions: ['20', '50', '100'],
+  showQuickJumper: true,
+  showSizeChanger: true,
+  pageSize: 20,
+  current: 1,
+  total: 0,
+  onChange: async (current: number) => {
+    paginationConfig.current = current;
+    await fetchList();
+  },
+  onShowSizeChange: async (current: number, pageSize: number) => {
+    paginationConfig.pageSize = pageSize;
+    await fetchList();
+  }
+})
+
+const fetchList = async () => {
+  state.listLoading = true;
+  try {
+    const res = await apis.fetchMediaFileList({ page: paginationConfig.current, page_size: paginationConfig.pageSize });
+    state.list = res.data.list;
+    paginationConfig.current = res.data.pagination.page;
+    paginationConfig.pageSize = res.data.pagination.page_size;
+    paginationConfig.total = res.data.pagination.total;
+  } catch (e) {
+    console.error(e);
+  } finally {
+    state.listLoading = false;
+  }
+}
+
+onMounted(async () => {
+  // await fetchList();
+})
+
+const columns = [
+  {
+    title: '文件夹名称',
+    dataIndex: 'dir_name',
+    width: 250,
+    ellipsis: true,
+    sorter: (a: any, b: any) => a.dir_name.localeCompare(b.dir_name),
+    slots: { customRender: 'dir_name' }
+  },
+  {
+    title: '设备型号',
+    dataIndex: 'device_name',
+    width: 150,
+    ellipsis: true,
+  },
+  {
+    title: '拍摄负载',
+    dataIndex: 'payload',
+    width: 150,
+    ellipsis: true,
+  },
+  {
+    title: '容量大小',
+    dataIndex: 'is_original',
+    width: 150,
+    ellipsis: true,
+    customRender: ({ text }: any) => {
+      return text || '--';
+    }
+  },
+  {
+    title: '创建时间',
+    dataIndex: 'create_time',
+    width: 200,
+    sorter: (a: any, b: any) => a.create_time.localeCompare(b.create_time),
+  },
+  {
+    title: '航线名称',
+    dataIndex: 'wayline_name',
+    width: 150,
+  },
+  {
+    title: '任务类型',
+    dataIndex: 'template_type',
+    width: 150,
+    customRender: ({ text }: any) => {
+      return text || '--';
+    }
+  },
+  {
+    title: '创建人',
+    dataIndex: 'username',
+    width: 150,
+  },
+  {
+    title: '操作',
+    dataIndex: 'actions',
+    fixed: 'right',
+    width: 80,
+    slots: { customRender: 'action' },
+  },
+]
+
+const rowClassName = (record: any, index: number) => {
+  const className = []
+  if ((index & 1) === 0) {
+    className.push('table-striped')
+  }
+  return className.toString().replaceAll(',', ' ')
+}
+
+// 点击批量下载
+const onClickBatchDownload = async () => {
+  console.log(state.selectedRowKeys, '点击批量下载');
+}
+
+// 点击搜索
+const onClickSearch = async () => {
+  console.log('点击搜索');
+}
+
+// 点击重置
+const onClickReset = async () => {
+  console.log('点击重置');
+}
+
+// 点击文件夹
+const onClickFile = (record: any) => {
+  router.push({ path: `/media/${record.id}` })
+}
+
+// 勾选
+const onSelectChange = (selectedRowKeys: string[]) => {
+  state.selectedRowKeys = selectedRowKeys;
+}
+
+// 点击下载
+const onClickDownload = async (record: any) => {
+  state.listLoading = true;
+  try {
+    const bold = await apis.downloadMediaFile(record.id);
+    const data = new Blob([bold], { type: 'application/zip' });
+    downloadFile(data, record.dir_name)
+  } catch (e) {
+    console.error(e);
+  } finally {
+    state.listLoading = false;
+  }
+}
+
+// 点击轨迹回放
+const onClickTrajectory = () => {
+  router.push({ path: '/trajectory' })
+}
+</script>
+
+<style lang="scss">
+.mediaList {
+  padding: 20px;
+
+  &-info {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 20px;
+
+    &-right {
+      display: flex;
+      align-items: center;
+
+      &-text {
+        display: flex;
+        align-items: center;
+        margin-right: 20px;
+      }
+    }
+  }
+
+  .fileName {
+    display: flex;
+    align-items: center;
+    cursor: pointer;
+
+    img {
+      width: 36px;
+      height: 36px;
+      margin-right: 5px;
+    }
+  }
+
+  &-list {
+    &-name {
+      overflow: hidden;
+      text-overflow: ellipsis;
+      white-space: nowrap;
+    }
+  }
+}
+
+.ant-table {
+  border-top: 1px solid rgb(0, 0, 0, 0.06);
+  border-bottom: 1px solid rgb(0, 0, 0, 0.06);
+}
+
+.ant-table-tbody tr td {
+  border: 0;
+}
+
+.table-striped {
+  background-color: #f7f9fa;
+}
+
+.ant-card-body {
+  padding: 0 10px 10px;
+}
+</style>

+ 20 - 26
Web/src/router/index.ts

@@ -20,36 +20,11 @@ const routes: Array<RouteRecordRaw> = [
     name: ERouterName.HOME,
     component: () => import('/@/pages/page-web/home.vue'),
     children: [
-      {
-        path: '/' + ERouterName.MEMBERS,
-        name: ERouterName.MEMBERS,
-        component: () => import('/@/pages/page-web/projects/members.vue')
-      },
       {
         path: '/' + ERouterName.DEVICES,
         name: ERouterName.DEVICES,
         component: () => import('/@/pages/page-web/projects/devices.vue')
       },
-      {
-        path: '/' + ERouterName.FIRMWARES,
-        name: ERouterName.FIRMWARES,
-        component: () => import('../pages/page-web/projects/Firmwares.vue')
-      },
-      {
-        path: '/' + ERouterName.MEDIA,
-        name: ERouterName.MEDIA,
-        component: () => import('/@/pages/page-web/projects/media/index/index.vue')
-      },
-      {
-        path: '/' + ERouterName.MEDIA + '/:id',
-        name: ERouterName.MEDIA + '/:id',
-        component: () => import('/@/pages/page-web/projects/media/detail/index.vue')
-      },
-      {
-        path: '/' + ERouterName.TRAJECTORY,
-        name: ERouterName.TRAJECTORY,
-        component: () => import('/@/pages/page-web/projects/trajectory/index.vue')
-      },
       {
         path: '/' + ERouterName.TASK,
         name: ERouterName.TASK,
@@ -70,9 +45,28 @@ const routes: Array<RouteRecordRaw> = [
               }
             ]
           }
-
         ]
       },
+      {
+        path: '/' + ERouterName.MEDIA,
+        name: ERouterName.MEDIA,
+        component: () => import('/@/pages/page-web/projects/media/index/index.vue')
+      },
+      {
+        path: '/' + ERouterName.MEDIA + '/:id',
+        name: ERouterName.MEDIA + '/:id',
+        component: () => import('/@/pages/page-web/projects/media/detail/index.vue')
+      },
+      {
+        path: '/' + ERouterName.TRAJECTORY,
+        name: ERouterName.TRAJECTORY,
+        component: () => import('/@/pages/page-web/projects/trajectory/index/index.vue')
+      },
+      {
+        path: '/' + ERouterName.MEMBER,
+        name: ERouterName.MEMBER,
+        component: () => import('/@/pages/page-web/projects/member/index.vue')
+      },
     ]
   },
   {

+ 1 - 1
Web/src/types/enums.ts

@@ -4,7 +4,7 @@ export enum ERouterName {
     TASK = 'task',
     MEDIA = 'media',
     TRAJECTORY = 'trajectory',
-    MEMBERS = 'members',
+    MEMBER = 'member',
 
     ELEMENT = 'element',
     PROJECT = 'project',