Преглед на файлове

perf: 添加设备管理页面

曾坤森 преди 2 месеца
родител
ревизия
3d0cac6563

+ 27 - 7
src/api/dashboard.ts

@@ -11,14 +11,15 @@ export interface RootObject {
   endTime: string;
   entityType: number;
 }
-export interface Data {
-  id: number;
-  entityType: number;
+export interface DataList {
+  id?: number;
+  entityType?: number | null;
+  deviceType?: number | null;
   name: string;
   address: string;
-  status: number;
-  time: string;
-  data: string;
+  status?: number;
+  time?: string;
+  data?: string;
 }
 
 export interface ExportUrl {
@@ -31,7 +32,7 @@ export interface DashboardList {
   success: boolean;
   message: string;
   code: string;
-  data: Data[];
+  data: DataList[];
   totalCount: number;
   totalPage: number;
 }
@@ -46,6 +47,12 @@ export interface DashboardParams {
   endTime: string | null;
   entityType: number | null;
 }
+export interface LoginRes {
+  success: boolean;
+  data: DataList;
+  message: string;
+  code: string;
+}
 export async function queryDashboardList(
   params: object
 ): Promise<DashboardList> {
@@ -56,3 +63,16 @@ export async function exportDashboardList(params: object): Promise<ExportUrl> {
   const res = await instance.post('/api/Author/Export', params);
   return res.data;
 }
+
+export async function getDeviceDetails(params: object): Promise<LoginRes> {
+  const res = await instance.post('/api/Author/GetDevice', params);
+  return res.data;
+}
+export async function saveDeviceDetails(params: object): Promise<LoginRes> {
+  const res = await instance.post('/api/Author/SaveDevice', params);
+  return res.data;
+}
+export async function deleteDeviceDetails(params: object): Promise<LoginRes> {
+  const res = await instance.post('/api/Author/DeleteDevice', params);
+  return res.data;
+}

+ 7 - 2
src/layout/default-layout.vue

@@ -20,15 +20,16 @@
           <div class="left-side">
             <a-space>
               <img
+                class="logo"
                 alt="logo"
                 src="//p3-armor.byteimg.com/tos-cn-i-49unhts6dw/dfdba5317c0c20ce20e64fac803d52bc.svg~tplv-49unhts6dw-image.image"
               />
               <a-typography-title
-                :style="{ margin: 0, fontSize: '18px' }"
+                :style="{ fontSize: '18px' }"
                 :heading="5"
                 v-if="!collapsed"
               >
-                smms-web
+                SMMS
               </a-typography-title>
             </a-space>
           </div>
@@ -172,6 +173,10 @@ onMounted(() => {
   height: 30px;
   padding-left: 10px;
   overflow: hidden;
+
+  .logo {
+    margin-right: 10px;
+  }
 }
 
 .menu-wrapper {

+ 1 - 0
src/locale/en-US.ts

@@ -26,6 +26,7 @@ export default {
   'searchTable.form.down': 'Down',
   'searchTable.form.add': 'Add',
   'searchTable.form.delete': 'Delete',
+  'searchTable.form.edit': 'Edit',
   'searchTable.form.confirm': 'Confirm',
   'searchTable.table.number': 'No.',
   'searchTable.table.optional': 'Optional',

+ 1 - 0
src/locale/zh-CN.ts

@@ -24,6 +24,7 @@ export default {
   'searchTable.form.search': '查询',
   'searchTable.form.reset': '重置',
   'searchTable.form.down': '下载',
+  'searchTable.form.edit': '编辑',
   'searchTable.form.delete': '删除',
   'searchTable.form.add': '新增',
   'searchTable.form.confirm': '确定',

+ 10 - 11
src/router/routes/modules/dashboard.ts

@@ -22,17 +22,16 @@ const DASHBOARD: AppRouteRecordRaw = {
         roles: ['*'],
       },
     },
-
-    // {
-    //   path: 'monitor',
-    //   name: 'Monitor',
-    //   component: () => import('@/views/dashboard/monitor/index.vue'),
-    //   meta: {
-    //     locale: 'menu.dashboard.monitor',
-    //     requiresAuth: true,
-    //     roles: ['admin'],
-    //   },
-    // },
+    {
+      path: 'device-manage',
+      name: 'DeviceManage',
+      component: () => import('@/views/dashboard/manage/index.vue'),
+      meta: {
+        locale: 'menu.dashboard.manage',
+        requiresAuth: true,
+        roles: ['*'],
+      },
+    },
   ],
 };
 

+ 9 - 0
src/utils/const.ts

@@ -11,6 +11,15 @@ export const privilegeList = [
   { value: 1, label: 'manage.permission.admin' },
   { value: 2, label: 'manage.permission.user' },
 ];
+export interface DeviceInfo {
+  label: string;
+  value: string;
+}
+export const entityTypeList = [
+  { value: 1, label: 'Camera' },
+  { value: 2, label: 'Server' },
+  { value: 3, label: 'Temperature sensor' },
+];
 // 将 rules 改为函数,接收 t 函数作为参数
 export const getRules = (t: (key: string) => string) => ({
   email: [

+ 125 - 0
src/views/dashboard/manage/edit.vue

@@ -0,0 +1,125 @@
+<template>
+  <div>
+    <a-modal
+      v-model:visible="visible"
+      width="auto"
+      :title="id ? $t('searchTable.form.edit') : $t('searchTable.form.add')"
+      @cancel="() => handleCancel()"
+      @before-ok="handleBeforeOk"
+    >
+      <a-form :model="form" auto-label-width ref="formRef">
+        <a-row :gutter="8">
+          <a-col :span="12">
+            <a-form-item
+              field="name"
+              :label="t('dashboard.form.name')"
+              :rules="getRules(t).required"
+            >
+              <a-input v-model="form.name" />
+            </a-form-item>
+          </a-col>
+          <a-col :span="12">
+            <a-form-item
+              field="address"
+              :label="t('dashboard.form.address')"
+              :rules="getRules(t).required"
+            >
+              <a-input v-model="form.address" />
+            </a-form-item>
+          </a-col>
+          <a-col :span="12">
+            <a-form-item
+              field="deviceType"
+              :label="t('dashboard.form.entityType')"
+              :rules="getRules(t).required"
+            >
+              <a-select
+                v-model="form.deviceType"
+                allow-clear
+                @clear="form.deviceType = null"
+              >
+                <a-option
+                  v-for="item of entityTypeList"
+                  :value="item.value"
+                  :label="t(item.label)"
+                />
+              </a-select>
+            </a-form-item>
+          </a-col>
+        </a-row>
+      </a-form>
+    </a-modal>
+  </div>
+</template>
+<script setup lang="ts" name="EditDialog">
+import { reactive, ref, shallowRef, watch, getCurrentInstance } from 'vue';
+import { getDeviceDetails, saveDeviceDetails } from '@/api/dashboard';
+import type { DataList } from '@/api/dashboard';
+import { privilegeList, entityTypeList } from '@/utils/const';
+import { useI18n } from 'vue-i18n';
+import { getRules } from '@/utils/const';
+const formRef = ref();
+const { t } = useI18n();
+
+const this_ = getCurrentInstance()?.appContext.config.globalProperties;
+
+interface EditDialogProps {
+  modelValue: boolean;
+  id: number | null;
+}
+interface EditDialogEmits {
+  (e: 'update:modelValue', value: boolean): void;
+  (e: 'update-list'): void;
+}
+const props = withDefaults(defineProps<EditDialogProps>(), {
+  modelValue: false,
+  id: null,
+});
+const emit = defineEmits<EditDialogEmits>();
+const visible = shallowRef<boolean>(false);
+
+watch(
+  () => props.modelValue,
+  value => {
+    visible.value = value;
+    if (value && props.id) {
+      getDeviceDetails({ id: props.id }).then(res => {
+        form.value = res.data;
+      });
+    }
+  }
+);
+const formModel = () => {
+  return {
+    id: 0,
+    name: '',
+    address: '',
+    deviceType: null,
+  } as DataList;
+};
+const form = ref<DataList>(formModel());
+
+const handleBeforeOk = (done: (closed: boolean) => void) => {
+  formRef.value.validate().then((data: DataList['data']) => {
+    if (!data) {
+      saveDeviceDetails(form.value)
+        .then(res => {
+          this_?.$message.success('操作成功');
+          done(true); // 关闭模态框
+          handleCancel();
+        })
+        .catch(() => {
+          done(false); // 不关闭模态框(例如提交失败时)
+        });
+    } else {
+      done(false); // 不关闭模态框(例如提交失败时)
+    }
+  });
+};
+const handleCancel = () => {
+  form.value = formModel();
+  visible.value = false;
+  emit('update:modelValue', false);
+  emit('update-list');
+};
+</script>

+ 398 - 0
src/views/dashboard/manage/index.vue

@@ -0,0 +1,398 @@
+<template>
+  <div class="container">
+    <a-card class="general-card">
+      <a-row>
+        <a-col :flex="1">
+          <a-form
+            :model="formModel"
+            :label-col-props="{ span: 4 }"
+            :wrapper-col-props="{ span: 18 }"
+            label-align="left"
+          >
+            <a-row :gutter="16">
+              <a-col :span="6">
+                <a-form-item
+                  field="address"
+                  :label="t('dashboard.form.address')"
+                  label-col-flex="50px"
+                >
+                  <a-input
+                    v-model="formModel.address"
+                    :placeholder="t('dashboard.form.address')"
+                    allow-clear
+                  />
+                </a-form-item>
+              </a-col>
+              <a-col :span="6">
+                <a-form-item
+                  field="name"
+                  :label="t('dashboard.form.name')"
+                  label-col-flex="50px"
+                >
+                  <a-input
+                    v-model="formModel.name"
+                    :placeholder="t('dashboard.form.name')"
+                    allow-clear
+                  />
+                </a-form-item>
+              </a-col>
+              <a-col :span="6">
+                <a-form-item
+                  field="entityType"
+                  :label="t('dashboard.form.entityType')"
+                  label-col-flex="60px"
+                >
+                  <a-select
+                    v-model="formModel.entityType"
+                    :placeholder="t('dashboard.form.entityType')"
+                    allow-clear
+                    @clear="formModel.entityType = null"
+                  >
+                    <a-option
+                      v-for="item of entityTypeList"
+                      :value="item.value"
+                      :label="item.label"
+                    />
+                  </a-select>
+                </a-form-item>
+              </a-col>
+              <a-col :span="6">
+                <a-form-item
+                  field="statusTypeList"
+                  :label="t('dashboard.form.status')"
+                  label-col-flex="60px"
+                >
+                  <a-select
+                    v-model="formModel.status"
+                    :placeholder="t('dashboard.form.status')"
+                    allow-clear
+                    @clear="formModel.status = null"
+                  >
+                    <a-option
+                      v-for="item of statusTypeList"
+                      :value="item.value"
+                      :label="item.label"
+                    />
+                  </a-select>
+                </a-form-item>
+              </a-col>
+              <a-col :span="6">
+                <a-form-item
+                  :label="t('dashboard.form.timeRange')"
+                  label-col-flex="60px"
+                >
+                  <a-range-picker v-model="formModel.time" />
+                </a-form-item>
+              </a-col>
+            </a-row>
+          </a-form>
+        </a-col>
+        <a-divider :style="{ height: '84px' }" direction="vertical" />
+        <a-col :flex="'86px'">
+          <a-space :size="10">
+            <a-button type="primary" @click="search">
+              <template #icon>
+                <icon-search />
+              </template>
+              {{ t('searchTable.form.search') }}
+            </a-button>
+            <a-button @click="reset">
+              <template #icon>
+                <icon-refresh />
+              </template>
+              {{ t('searchTable.form.reset') }}
+            </a-button>
+          </a-space>
+          <a-button
+            type="primary"
+            class="right-side"
+            @click="showEditDialog = true"
+          >
+            <template #icon>
+              <icon-plus />
+            </template>
+            {{ t('searchTable.form.add') }}
+          </a-button>
+        </a-col>
+      </a-row>
+      <a-table
+        class="table-list"
+        row-key="name"
+        :loading="loading"
+        :pagination="pagination"
+        :columns="(cloneColumns as TableColumnData[])"
+        :data="renderData"
+        :bordered="false"
+        :size="size"
+        :scrollbar="true"
+        @page-change="onPageChange"
+        @row-dblclick="handleClick"
+      >
+        <template #index="{ rowIndex }">
+          {{ rowIndex + 1 + (pagination.current - 1) * pagination.pageSize }}
+        </template>
+
+        <template #entityType="{ record }">
+          <span>
+            {{
+              entityTypeList.find(item => item.value === record.entityType)
+                ?.label
+            }}
+          </span>
+        </template>
+        <template #status="{ record }">
+          <BTag :status="record.status">
+            {{
+              statusTypeList.find(item => item.value === record.status)?.label
+            }}
+          </BTag>
+        </template>
+        <template #time="{ record }">
+          <span>{{
+            record.time && dayjs(record.time).format('YYYY-MM-DD HH:mm:ss')
+          }}</span>
+        </template>
+        <template #optional="{ record }">
+          <a-button
+            type="primary"
+            size="mini"
+            class="action-button"
+            @click="
+              () => {
+                (deviceId = record.id), (showEditDialog = true);
+              }
+            "
+          >
+            {{ t('searchTable.form.edit') }}
+          </a-button>
+          <a-button
+            status="danger"
+            size="mini"
+            @click="handleDeleteFun(record.id)"
+          >
+            {{ t('searchTable.form.delete') }}
+          </a-button>
+        </template>
+      </a-table>
+    </a-card>
+    <a-modal
+      v-model:visible="visible"
+      title-align="start"
+      @cancel="handleCancel"
+    >
+      <template #title>{{ t('dashboard.dialog.title') }}</template>
+      <div>
+        <a-descriptions :data="deviceInfo" bordered :column="2" />
+      </div>
+      <template #footer>
+        <a-button @click="handleCancel">取消</a-button>
+        <!-- 只保留取消按钮,确认按钮被隐藏 -->
+      </template>
+    </a-modal>
+    <EditDialog
+      v-model="showEditDialog"
+      :id="deviceId"
+      @updateList="updateListFun"
+    ></EditDialog>
+  </div>
+</template>
+
+<script lang="ts" setup name="Dashboard">
+import {
+  ref,
+  reactive,
+  shallowRef,
+  h,
+  getCurrentInstance,
+  computed,
+} from 'vue';
+import {
+  queryDashboardList,
+  exportDashboardList,
+  deleteDeviceDetails,
+} from '@/api/dashboard';
+import type { DashboardParams, DataList } from '@/api/dashboard';
+import { SizeProps, Pagination } from '@/types/global';
+import BTag from '@/components/business/b-tag/index.vue';
+import EditDialog from './edit.vue';
+import type { TableColumnData } from '@arco-design/web-vue';
+import useLoading from '@/hooks/loading';
+import { useI18n } from 'vue-i18n';
+import { statusTypeList } from '../workplace/conf';
+import dayjs from 'dayjs';
+import { downLoadFun, DeviceInfo, entityTypeList } from '@/utils/const';
+import { useIntervalFn } from '@vueuse/core';
+import { Modal } from '@arco-design/web-vue';
+
+const { t } = useI18n();
+
+const { loading, setLoading } = useLoading(true);
+const cloneColumns = computed(() => [
+  {
+    title: t('searchTable.table.number'),
+    dataIndex: 'index',
+    slotName: 'index',
+    ellipsis: true,
+    tooltip: true,
+    width: 70,
+  },
+  {
+    title: t('dashboard.table.time'),
+    dataIndex: 'time',
+    slotName: 'time',
+    ellipsis: true,
+  },
+  // {
+  //   title: t('dashboard.form.status'),
+  //   dataIndex: 'status',
+  //   slotName: 'status',
+  //   width: 120,
+  // },
+  {
+    title: t('dashboard.form.name'),
+    dataIndex: 'name',
+    slotName: 'name',
+  },
+  {
+    title: t('dashboard.form.entityType'),
+    dataIndex: 'entityType',
+    slotName: 'entityType',
+  },
+
+  {
+    title: t('dashboard.form.address'),
+    dataIndex: 'address',
+    ellipsis: true,
+    tooltip: true,
+  },
+  {
+    title: t('searchTable.table.optional'),
+    align: 'center',
+    slotName: 'optional',
+  },
+]);
+
+const basePagination: Pagination = {
+  current: 1,
+  pageSize: 20,
+};
+const pagination = reactive({
+  ...basePagination,
+});
+const generateFormModel = () => {
+  return {
+    pageIndex: 1,
+    pageSize: 20,
+    name: null,
+    address: null,
+    status: null,
+    startTime: null,
+    endTime: null,
+    time: ['', ''],
+    entityType: null,
+  } as DashboardParams;
+};
+const renderData = ref<DataList[]>([] as DataList[]);
+const size = ref<SizeProps>('medium');
+const formModel = ref<DashboardParams>(generateFormModel());
+const visible = shallowRef<boolean>(false);
+const showEditDialog = shallowRef<boolean>(false);
+const deviceId = ref<number | null>(null);
+const this_ = getCurrentInstance()?.appContext.config.globalProperties;
+const deviceInfo = ref<DeviceInfo[]>([] as DeviceInfo[]);
+function searchTable() {
+  // setLoading(true);
+  const [startTime, endTime] = formModel.value.time
+    ? formModel.value.time
+    : ['', ''];
+  formModel.value.startTime = startTime ? startTime : null;
+  formModel.value.endTime = endTime ? endTime : null;
+  queryDashboardList(formModel.value)
+    .then(res => {
+      pagination.current = formModel.value.pageIndex;
+      pagination.pageSize = pagination.pageSize;
+      pagination.total = res.totalCount;
+      renderData.value = res.data;
+    })
+    .finally(() => {
+      setLoading(false);
+    });
+}
+searchTable();
+
+const search = () => {
+  searchTable();
+};
+const reset = () => {
+  formModel.value = generateFormModel();
+};
+const onPageChange = (current: number) => {
+  formModel.value.pageIndex = current;
+  searchTable();
+};
+
+const handleClick = (value: DataList) => {
+  if (value.data) {
+    deviceInfo.value.length = 0;
+    const obj = JSON.parse(value.data);
+    for (const key in obj) {
+      deviceInfo.value.push({
+        label: key,
+        value: obj[key],
+      });
+    }
+    visible.value = true;
+  } else {
+    this_ &&
+      this_.$message.warning('No device information available at the moment');
+  }
+};
+const handleCancel = () => {
+  visible.value = false;
+};
+const updateListFun = () => {
+  deviceId.value = null;
+  searchTable();
+};
+const handleDeleteFun = (id: number) => {
+  Modal.warning({
+    title: t('modal.warning.title'),
+    content: t('modal.warning.content'),
+    okText: t('searchTable.form.confirm'),
+    onBeforeOk: (done: (closed: boolean) => void) => {
+      deleteDeviceDetails({ id })
+        .then(res => {
+          if (res.success) {
+            this_?.$message.success('操作成功');
+            searchTable();
+          }
+        })
+        .finally(() => {
+          done(true); // 确定关闭模态框
+        });
+    },
+  });
+};
+</script>
+
+<style lang="less" scoped>
+.container {
+  padding: 10px 10px 20px;
+
+  .general-card {
+    padding-top: 20px;
+
+    .right-side {
+      margin-top: 20px;
+    }
+  }
+
+  .table-list {
+    margin-top: 0;
+  }
+
+  .action-button {
+    margin-right: 10px;
+  }
+}
+</style>

+ 0 - 9
src/views/dashboard/workplace/conf.ts

@@ -1,12 +1,3 @@
-export interface DeviceInfo {
-  label: string;
-  value: string;
-}
-export const entityTypeList = [
-  { value: 1, label: 'Camera' },
-  { value: 2, label: 'Server' },
-  { value: 3, label: 'Temperature sensor' },
-];
 export const statusTypeList = [
   { value: -1, label: 'Offline', color: 'gray' },
   { value: 0, label: 'Normal', color: 'green' },

+ 6 - 5
src/views/dashboard/workplace/index.vue

@@ -179,13 +179,14 @@ import {
   computed,
 } from 'vue';
 import { queryDashboardList, exportDashboardList } from '@/api/dashboard';
-import type { DashboardParams, Data } from '@/api/dashboard';
+import type { DashboardParams, DataList } from '@/api/dashboard';
 import { SizeProps, Pagination } from '@/types/global';
 import BTag from '@/components/business/b-tag/index.vue';
 import type { TableColumnData } from '@arco-design/web-vue';
 import useLoading from '@/hooks/loading';
 import { useI18n } from 'vue-i18n';
-import { entityTypeList, statusTypeList, DeviceInfo } from './conf';
+import { statusTypeList } from './conf';
+import { entityTypeList, DeviceInfo } from '@/utils/const';
 import dayjs from 'dayjs';
 import { downLoadFun } from '@/utils/const';
 import { useIntervalFn } from '@vueuse/core';
@@ -200,7 +201,7 @@ const cloneColumns = computed(() => [
     slotName: 'index',
     ellipsis: true,
     tooltip: true,
-    width: 60,
+    width: 70,
   },
   {
     title: t('dashboard.table.time'),
@@ -253,7 +254,7 @@ const generateFormModel = () => {
     entityType: null,
   } as DashboardParams;
 };
-const renderData = ref<Data[]>([] as Data[]);
+const renderData = ref<DataList[]>([] as DataList[]);
 const size = ref<SizeProps>('medium');
 const formModel = ref<DashboardParams>(generateFormModel());
 const visible = shallowRef<boolean>(false);
@@ -316,7 +317,7 @@ const onPageChange = (current: number) => {
   searchTable();
 };
 
-const handleClick = (value: Data) => {
+const handleClick = (value: DataList) => {
   if (value.data) {
     deviceInfo.value.length = 0;
     const obj = JSON.parse(value.data);

+ 2 - 1
src/views/dashboard/workplace/locale/en-US.ts

@@ -1,5 +1,6 @@
 export default {
-  'menu.dashboard.workplace': 'System Status',
+  'menu.dashboard.workplace': 'Device Status',
+  'menu.dashboard.manage': 'Device Manage',
   'workplace.welcome': 'Welcome!',
   'dashboard.dialog.title': 'Device Information',
   'dashboard.form.address': 'Location',

+ 1 - 0
src/views/dashboard/workplace/locale/zh-CN.ts

@@ -1,5 +1,6 @@
 export default {
   'menu.dashboard.workplace': '设备状态',
+  'menu.dashboard.manage': '设备管理',
   'workplace.welcome': '欢迎回来!',
   'dashboard.dialog.title': '设备信息',
   'dashboard.form.address': 'IP地址',