瀏覽代碼

Merge branch 'wyf-alert' into 'dev'

报警配置页开发

See merge request skyeye/skyeye_frontend/skyeye-admin!106
楼航飞 1 年之前
父節點
當前提交
ceba69e698
共有 30 個文件被更改,包括 2103 次插入155 次删除
  1. 90 0
      src/api/message/message.ts
  2. 二進制
      src/assets/icons/arrow-right-grey.png
  3. 二進制
      src/assets/icons/arrow-right.png
  4. 二進制
      src/assets/icons/arrow.png
  5. 二進制
      src/assets/icons/arrow_bottom.png
  6. 二進制
      src/assets/icons/arrow_top.png
  7. 二進制
      src/assets/icons/link_icon.png
  8. 二進制
      src/assets/images/alarm-example-photo.png
  9. 二進制
      src/assets/images/alarm-example.png
  10. 66 0
      src/main.css
  11. 1 0
      src/views/message/SelectTree.vue
  12. 448 0
      src/views/message/alarm-config/AlarmConfig.vue
  13. 35 0
      src/views/message/alarm-config/components/AlarmExample.vue
  14. 185 0
      src/views/message/alarm-config/components/AlarmExampleMobile.vue
  15. 181 0
      src/views/message/alarm-config/components/AlarmExamplePlatform.vue
  16. 51 0
      src/views/message/alarm-config/components/PushOccasionsCard.vue
  17. 33 0
      src/views/message/alarm-config/types.ts
  18. 16 10
      src/views/message/alarmMessages/alarmMessages.vue
  19. 68 66
      src/views/message/alarmMessages/hook/index.ts
  20. 13 0
      src/views/message/api/index.ts
  21. 87 0
      src/views/message/components/Group.vue
  22. 285 0
      src/views/message/components/PushObject.vue
  23. 15 1
      src/views/message/constant.ts
  24. 1 1
      src/views/message/persongroup/components/GroupBoard.vue
  25. 42 61
      src/views/message/persongroup/components/SelectTree.vue
  26. 5 0
      src/views/message/persongroup/type.ts
  27. 0 1
      src/views/message/reportmessage/api/index.ts
  28. 0 13
      src/views/message/reportmessage/constant.ts
  29. 1 2
      src/views/message/reportmessage/overviewColumns.ts
  30. 480 0
      src/views/message/sysnotion-config/SysnotionConfig.vue

+ 90 - 0
src/api/message/message.ts

@@ -0,0 +1,90 @@
+import { http } from '@/utils/http/axios';
+
+export const getAlarmConfigDetail = (params) => {
+  return http.request<AlarmConfigForm>({
+    url: `/alarmMessage/getAlarmMessageDetail?id=${params}`,
+    method: 'get',
+  });
+};
+
+export interface AlarmConfigForm {
+  alarmMessageId?: number;
+  violationType?: number;
+  violationLevel: number | string;
+  violationName?: string;
+  pushChannel: string | number[];
+  pushOccasions?: number[];
+  pushPhaseVOList: PushPhaseVOList[];
+  creator?: string;
+}
+
+export interface PushPhaseVOList {
+  pushPhase: number;
+  recipientType: number | undefined;
+  userGroupList?: UserGroupVOItem[] | number[] | null;
+  customUserList?: CustomUserItem[] | number[] | null;
+  content?: string;
+}
+
+export interface UserGroupVOItem {
+  userGroupId: number;
+  total?: number | null;
+  operatorName?: string | null;
+  operationTime?: string | null;
+  name: string;
+  description?: string | null;
+}
+
+export interface CustomUserItem {
+  userId: number;
+  userLoginName: string;
+  userNickname: string;
+  userNumber: string;
+}
+
+export const getExistingAlarmType = () => {
+  return http.request<AvailableAlarmType[]>({
+    url: '/alarmMessage/queryExistingType',
+    method: 'get',
+  });
+};
+
+export interface AvailableAlarmType {
+  id: number;
+  name: string;
+  code?: string;
+  showName?: string;
+  remark?: string;
+  url?: string;
+  pushStatement?: string;
+  pushLinkPrompt?: string;
+  iconUrl?: string;
+  status?: number;
+  createdAt?: string;
+  updatedAt?: string;
+  isDeleted?: number;
+  extra?: string;
+}
+
+export const addAlarmConfig = (data: AlarmConfigForm) => {
+  return http.request({
+    url: '/alarmMessage/addAlarmMessage',
+    method: 'post',
+    data,
+  });
+};
+
+export const editAlarmConfig = (data: AlarmConfigForm) => {
+  return http.request({
+    url: '/alarmMessage/modifyAlarmMessage',
+    method: 'post',
+    data,
+  });
+};
+
+export const getDevMode = () => {
+  return http.request({
+    url: '/issue/getDevMode',
+    method: 'get',
+  });
+};

二進制
src/assets/icons/arrow-right-grey.png


二進制
src/assets/icons/arrow-right.png


二進制
src/assets/icons/arrow.png


二進制
src/assets/icons/arrow_bottom.png


二進制
src/assets/icons/arrow_top.png


二進制
src/assets/icons/link_icon.png


二進制
src/assets/images/alarm-example-photo.png


二進制
src/assets/images/alarm-example.png


+ 66 - 0
src/main.css

@@ -0,0 +1,66 @@
+.groupInfo {
+    padding: 0;
+    box-shadow: 0px 9px 28px 8px rgba(0, 0, 0, 0.05), 0px 6px 16px 0px rgba(0, 0, 0, 0.08), 0px 3px 6px -4px rgba(0, 0, 0, 0.12);
+    border-radius: 8px;
+
+    header {
+        width: 100%;
+        height: 63px;
+        padding: 20px 24px 20px 24px;
+        border-bottom: 1px solid #D9D9D9;
+
+        span {
+            font-weight: 500;
+            font-size: 16px;
+            color: rgba(0, 0, 0, 0.88);
+            line-height: 24px;
+        }
+
+        button {
+            margin-top: 13px;
+        }
+    }
+
+    .el-dialog__body {
+        display: flex;
+        flex-direction: column;
+        gap: 10px;
+        width: 100%;
+        height: calc(100% - 63px);
+        overflow-y: auto;
+
+        .group {
+            padding: 15px 20px 15px 10px;
+        }
+    }
+}
+
+.userInfo {
+    padding: 0;
+    box-shadow: 0px 9px 28px 8px rgba(0, 0, 0, 0.05), 0px 6px 16px 0px rgba(0, 0, 0, 0.08), 0px 3px 6px -4px rgba(0, 0, 0, 0.12);
+    border-radius: 8px;
+
+    header {
+        width: 100%;
+        height: 63px;
+        padding: 20px 24px 20px 24px;
+
+        span {
+            font-weight: 500;
+            font-size: 16px;
+            color: rgba(0, 0, 0, 0.88);
+            line-height: 24px;
+        }
+
+        button {
+            margin-top: 13px;
+        }
+    }
+
+    .el-dialog__body {
+        width: 100%;
+        padding: 16px;
+        height: calc(100% - 63px);
+        overflow-y: auto;
+    }
+}

+ 1 - 0
src/views/message/SelectTree.vue

@@ -56,6 +56,7 @@ watch(
 );
   onMounted(()=>{
     if(prop.form.customUserList.value.length > 0){
+      console.log(prop.form.customUserList.value)
       selectedUser.value = prop.form.customUserList.value
     }
   })

+ 448 - 0
src/views/message/alarm-config/AlarmConfig.vue

@@ -0,0 +1,448 @@
+<template>
+  <div class="alarm-config-page">
+    <div class="alarm-config-header">
+      <div class="alarm-config-rollback" @click="router.back()">
+        <img src="../reportmessage/img/rollback.png" /><span>返回</span>
+      </div>
+      <span>{{ formType }}报警配置</span>
+    </div>
+    <div class="alarm-config-content">
+      <div class="alarm-config-form">
+        <el-form
+          ref="formRef"
+          :model="alarmConfigForm"
+          :rules="rules"
+          label-width="auto"
+          :disabled="disableAll"
+        >
+          <el-form-item label="违规类型:" prop="violationType">
+            <el-select
+              class="alarm-config-input"
+              v-model="alarmConfigForm.violationType"
+              placeholder="请选择违规类型"
+              :disabled="disableType"
+            >
+              <el-option
+                v-for="item in AlarmTypes"
+                :key="item.id"
+                :label="item.name"
+                :value="item.id"
+            /></el-select>
+          </el-form-item>
+          <el-form-item label="报警等级:" prop="violationLevel">
+            <el-select
+              class="alarm-config-input"
+              v-model="alarmConfigForm.violationLevel"
+              placeholder="请选择报警等级"
+            >
+              <el-option
+                v-for="item in ALARMLEVEL_LIST"
+                :key="item.value"
+                :label="item.label"
+                :value="item.value"
+            /></el-select>
+          </el-form-item>
+          <el-form-item label="推送渠道:" prop="pushChannel">
+            <el-checkbox-group v-model="(alarmConfigForm.pushChannel as number[])">
+              <el-checkbox
+                v-for="channel in ALARMCHANNEL_LIST"
+                :key="channel.value"
+                :label="channel.label"
+                :value="channel.value"
+              >
+              </el-checkbox>
+            </el-checkbox-group>
+          </el-form-item>
+          <el-form-item label="推送阶段:" prop="pushOccasions">
+            <div class="alarm-config-checkbox-group">
+              <!-- v-model="alarmConfigForm.pushOccasions" -->
+              <div v-for="phase in ALARMPHASE_LIST" :key="phase.value">
+                <div
+                  v-if="
+                    !(phase.value === AlarmPhase.effects && isDevMode) &&
+                    alarmConfigForm.pushOccasions
+                  "
+                >
+                  <el-checkbox
+                    v-if="hasInit || route.query.operationType === '1'"
+                    :label="phase.label"
+                    :checked="alarmConfigForm.pushOccasions.includes(phase.value)"
+                    @change="handlePhaseChange(phase.value, $event)"
+                  >
+                  </el-checkbox>
+                  <div
+                    v-if="
+                      alarmConfigForm.pushPhaseVOList.some((it) => it.pushPhase === phase.value)
+                    "
+                  >
+                    <div class="push-occasions-card">
+                      <div style="height: 136px">
+                        <PushObject
+                          ref="pushObjectRef"
+                          :recipientType="alarmConfigForm.pushPhaseVOList.find((it) => it.pushPhase === phase.value)!.recipientType"
+                          :userGroupList="(alarmConfigForm.pushPhaseVOList.find((it) => it.pushPhase === phase.value)!.userGroupList as userGroupVOList[])"
+                          :customUserList="(alarmConfigForm.pushPhaseVOList.find((it) => it.pushPhase === phase.value)!.customUserList as customUserList[])"
+                          :disabled="disableAll"
+                          @submit-with-form="
+                            handlePhaseSubmit(
+                              submitConfigForm.pushPhaseVOList.find(
+                                (it) => it.pushPhase === phase.value,
+                              )!,
+                              $event,
+                            )
+                          "
+                        />
+                      </div>
+                      <el-form-item label="自定义内容:">
+                        <el-input
+                          v-model="
+                            alarmConfigForm.pushPhaseVOList.find((it) => it.pushPhase === phase.value)!
+                              .content
+                          "
+                          maxlength="200"
+                          resize="none"
+                          style="width: 357px"
+                          :autosize="{ minRows: 5, maxRows: 5 }"
+                          :placeholder="remark"
+                          show-word-limit
+                          type="textarea"
+                        />
+                      </el-form-item>
+                    </div>
+                  </div>
+                </div>
+              </div>
+            </div>
+          </el-form-item>
+          <el-form-item label="创建人:">
+            <el-input class="alarm-config-input" v-model="alarmConfigForm.creator" disabled />
+          </el-form-item>
+        </el-form>
+        <div style="text-align: right; margin-right: 32px">
+          <el-button :disabled="disableAll" @click="router.back()"> 取 消 </el-button>
+          <el-button :disabled="disableAll" type="primary" @click="debSubmitForm(formRef)">
+            确 定
+          </el-button>
+        </div>
+      </div>
+      <div class="alarm-config-example">
+        <AlarmExample />
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+  import PushObject from '../components/PushObject.vue';
+  import AlarmExample from './components/AlarmExample.vue';
+  import {
+    ElForm,
+    ElFormItem,
+    ElSelect,
+    ElCheckboxGroup,
+    ElCheckbox,
+    ElInput,
+    ElMessage,
+    FormInstance,
+    FormRules,
+  } from 'element-plus';
+  import { ref, watch, onMounted, computed } from 'vue';
+  import { useUserStore } from '@/store/modules/user';
+  import { useRouter, useRoute } from 'vue-router';
+  import {
+    getAlarmConfigDetail,
+    getExistingAlarmType,
+    getDevMode,
+    addAlarmConfig,
+    editAlarmConfig,
+    type AlarmConfigForm,
+    type PushPhaseVOList,
+    // type UserGroupVOItem,
+    // type CustomUserItem,
+    type AvailableAlarmType,
+  } from '@/api/message/message';
+  import {
+    // AlarmLevel,
+    ALARMLEVEL_LIST,
+    // AlarmChannel,
+    ALARMCHANNEL_LIST,
+    AlarmPhase,
+    ALARMPHASE_LIST,
+  } from './types';
+  import { type userGroupVOList, type customUserList } from '../components/PushObject.vue';
+  import _ from 'lodash-es';
+
+  const useUser = useUserStore();
+  const router = useRouter();
+  const route = useRoute();
+  const hasInit = ref(false);
+  const AlarmTypes = ref<AvailableAlarmType[]>([]);
+
+  const remark = computed(() => {
+    return (
+      '您好,智能检测到该车间发生【' +
+      AlarmTypes.value.find((it) => it.id === alarmConfigForm.value.violationType)?.name +
+      '】违规问题,请及时关注并前往处理!'
+    );
+  });
+
+  const alarmConfigForm = ref<AlarmConfigForm>({
+    violationType: undefined,
+    violationLevel: '',
+    pushChannel: [],
+    pushOccasions: [],
+    pushPhaseVOList: [],
+    creator: useUser.info.nickname,
+  });
+
+  const submitConfigForm = ref<AlarmConfigForm>({
+    alarmMessageId: undefined,
+    violationType: undefined,
+    violationLevel: '',
+    pushChannel: [],
+    pushPhaseVOList: [],
+  });
+
+  // 获取报警配置详情
+  const getAlarmDetail = (configId) => {
+    getAlarmConfigDetail(configId).then((res) => {
+      alarmConfigForm.value.violationType = res.violationType;
+      alarmConfigForm.value.violationLevel = res.violationLevel;
+      alarmConfigForm.value.pushChannel = JSON.parse(res.pushChannel as string);
+      alarmConfigForm.value.pushPhaseVOList = _.cloneDeep(res.pushPhaseVOList);
+      submitConfigForm.value.pushPhaseVOList = _.cloneDeep(res.pushPhaseVOList);
+      alarmConfigForm.value.pushOccasions = res.pushPhaseVOList.map((it) => it.pushPhase);
+      hasInit.value = true;
+
+      // 获取的配置填入违规类型
+      AlarmTypes.value.push({
+        id: res.violationType,
+        name: res.violationName,
+      } as AvailableAlarmType);
+    });
+  };
+
+  const pushObjectRef = ref();
+  const formType = ref('');
+  const disableType = ref(false);
+  const disableAll = ref(false);
+  const isDevMode = ref(false);
+
+  onMounted(() => {
+    getDevMode().then((res) => {
+      isDevMode.value = res;
+    });
+
+    if (!route.query.operationType) {
+      router.back();
+    }
+
+    switch (route.query.operationType) {
+      case '1':
+        formType.value = '新建';
+        getExistingAlarmType().then((res) => {
+          AlarmTypes.value = res;
+        });
+        break;
+      case '2':
+        formType.value = '编辑';
+        disableType.value = true;
+        getAlarmDetail(route.query.alarmConfigId);
+        break;
+      case '3':
+        formType.value = '查看';
+        disableAll.value = true;
+        getAlarmDetail(route.query.alarmConfigId);
+        break;
+    }
+  });
+
+  watch(
+    () => alarmConfigForm.value.pushOccasions,
+    () => {
+      if (!alarmConfigForm.value.pushOccasions) return;
+      alarmConfigForm.value.pushOccasions.forEach((occasion) => {
+        if (!alarmConfigForm.value.pushPhaseVOList.some((it) => it.pushPhase === occasion)) {
+          alarmConfigForm.value.pushPhaseVOList.push({
+            pushPhase: occasion,
+            recipientType: undefined,
+            content: '',
+          });
+          submitConfigForm.value.pushPhaseVOList.push({
+            pushPhase: occasion,
+            recipientType: undefined,
+            content: '',
+          });
+        }
+      });
+      alarmConfigForm.value.pushPhaseVOList.forEach((vo) => {
+        if (!alarmConfigForm.value.pushOccasions?.some((occ) => occ === vo.pushPhase)) {
+          alarmConfigForm.value.pushPhaseVOList.splice(
+            alarmConfigForm.value.pushPhaseVOList.findIndex((it) => it.pushPhase === vo.pushPhase),
+            1,
+          );
+          submitConfigForm.value.pushPhaseVOList.splice(
+            submitConfigForm.value.pushPhaseVOList.findIndex((it) => it.pushPhase === vo.pushPhase),
+            1,
+          );
+        }
+      });
+    },
+    {
+      deep: true,
+    },
+  );
+
+  const formRef = ref<FormInstance>();
+
+  const rules = ref<FormRules<AlarmConfigForm>>({
+    violationType: [{ required: true, message: '请选择违规类型', trigger: 'blur' }],
+    violationLevel: [{ required: true, message: '请选择违规等级', trigger: 'blur' }],
+    pushChannel: [{ required: true, message: '请选择推送渠道', trigger: 'change' }],
+    pushOccasions: [{ required: true, message: '请选择推送阶段', trigger: 'change' }],
+    pushPhaseVOList: [{ required: true, message: '请选择推送人员', trigger: 'change' }],
+  });
+
+  const handlePhaseChange = (occasion: number, e) => {
+    if (!alarmConfigForm.value.pushOccasions) return;
+
+    if (e) {
+      if (!alarmConfigForm.value.pushOccasions.includes(occasion)) {
+        alarmConfigForm.value.pushOccasions.push(occasion);
+      }
+    } else {
+      if (alarmConfigForm.value.pushOccasions.includes(occasion)) {
+        alarmConfigForm.value.pushOccasions.splice(
+          alarmConfigForm.value.pushOccasions.findIndex((it) => it === occasion),
+          1,
+        );
+      }
+    }
+  };
+
+  const handlePhaseSubmit = (phaseForm: PushPhaseVOList, childValue) => {
+    phaseForm.recipientType = childValue.recipientType;
+    switch (childValue.recipientType) {
+      case 1:
+        delete phaseForm.userGroupList;
+        delete phaseForm.customUserList;
+        break;
+      case 2:
+        delete phaseForm.customUserList;
+        phaseForm.userGroupList = childValue.userGroupList;
+        break;
+      case 3:
+        delete phaseForm.userGroupList;
+        phaseForm.customUserList = childValue.customUserList;
+        break;
+    }
+  };
+
+  // 提交表单
+  const submitForm = async (formEl: FormInstance | undefined) => {
+    if (!formEl) return;
+    formEl.validate((valid, fields) => {
+      if (valid) {
+        const fns = pushObjectRef.value.map((x) => {
+          return x.validateForm();
+        });
+        Promise.all(fns)
+          .then(() => {
+            submitConfigForm.value.violationLevel = alarmConfigForm.value.violationLevel;
+            submitConfigForm.value.pushChannel = alarmConfigForm.value.pushChannel;
+            alarmConfigForm.value.pushPhaseVOList.forEach((vo) => {
+              submitConfigForm.value.pushPhaseVOList.find(
+                (it) => it.pushPhase === vo.pushPhase,
+              )!.content = vo.content || remark.value;
+            });
+            switch (route.query.operationType) {
+              case '1':
+                delete submitConfigForm.value.alarmMessageId;
+                submitConfigForm.value.violationType = alarmConfigForm.value.violationType;
+                addAlarmConfig(submitConfigForm.value)
+                  .then(() => {
+                    ElMessage.success('新建成功');
+                    router.back();
+                  })
+                  .catch((e) => {
+                    ElMessage.error('新建失败', e);
+                  });
+                break;
+              case '2':
+                delete submitConfigForm.value.violationType;
+                submitConfigForm.value.alarmMessageId = +route.query.alarmConfigId!;
+                editAlarmConfig(submitConfigForm.value)
+                  .then(() => {
+                    ElMessage.success('编辑成功');
+                    router.back();
+                  })
+                  .catch((e) => {
+                    ElMessage.error('编辑失败', e);
+                  });
+                break;
+              default:
+                break;
+            }
+          })
+          .catch((e) => {
+            console.log('表单校验未通过', e);
+          });
+      } else {
+        console.log('表单校验未通过', fields);
+      }
+    });
+  };
+  const debSubmitForm = _.debounce(submitForm, 500);
+</script>
+
+<style scoped lang="scss">
+  .alarm-config-page {
+    background-color: rgba(255, 255, 255, 1);
+    .alarm-config-header {
+      display: flex;
+      align-items: center;
+      padding: 16px 0 15px 20px;
+      border-bottom: 1px solid #e9e9e9;
+      .alarm-config-rollback {
+        width: 60px;
+        display: flex;
+        align-items: center;
+        cursor: pointer;
+      }
+    }
+    .alarm-config-content {
+      display: flex;
+      height: calc(100vh - 137px);
+
+      .alarm-config-form {
+        display: flex;
+        flex-direction: column;
+        justify-content: space-between;
+        padding: 20px 0 32px 32px;
+        width: 803px;
+        border-right: 1px solid rgba(0, 0, 0, 0.06);
+        overflow-y: auto;
+
+        .alarm-config-input {
+          max-width: 446px;
+        }
+
+        .alarm-config-checkbox-group {
+          max-height: 556px;
+          overflow: auto;
+
+          .push-occasions-card {
+            padding: 10px 20px;
+            width: 572px;
+            height: 283px;
+            background-color: #fafafa;
+            font-size: 16px;
+          }
+        }
+      }
+      .alarm-config-example {
+        flex: 1;
+      }
+    }
+  }
+</style>

+ 35 - 0
src/views/message/alarm-config/components/AlarmExample.vue

@@ -0,0 +1,35 @@
+<template>
+  <div>
+    <div class="alarm-example">示例样式</div>
+    <el-tabs v-model="activeName" class="alarm-example-tabs">
+      <el-tab-pane label="平台侧" :name="0">
+        <AlarmExamplePlatform />
+      </el-tab-pane>
+      <el-tab-pane label="蓝信侧" :name="1">
+        <AlarmExampleMobile />
+      </el-tab-pane>
+    </el-tabs>
+  </div>
+</template>
+
+<script setup lang="ts">
+  import AlarmExamplePlatform from './AlarmExamplePlatform.vue';
+  import AlarmExampleMobile from './AlarmExampleMobile.vue';
+  import { ref } from 'vue';
+
+  const activeName = ref<number>(0);
+</script>
+
+<style lang="scss" scoped>
+  .alarm-example {
+    padding: 18px 0 0 20px;
+    font-weight: 500;
+    font-size: 14px;
+    color: rgba(0, 0, 0, 0.85);
+    line-height: 22px;
+  }
+
+  :deep(.el-tabs__nav-scroll) {
+    padding: 0 20px 0;
+  }
+</style>

+ 185 - 0
src/views/message/alarm-config/components/AlarmExampleMobile.vue

@@ -0,0 +1,185 @@
+<template>
+  <div style="width: 450px">
+    <div class="alarm-example-title"> <div class="alarm-example-block"></div>卡片页</div>
+    <div class="alarm-example-info-title">报警提醒</div>
+    <div class="alarm-example-info">
+      <div>【类型】安全帽违规</div>
+      <div>【时间】2024-09-09 10:00:00</div>
+      <div>【地点】A车间-250工位</div>
+      <div>智能检测到发生【违规】情况,请及时关注并前往处理!</div>
+    </div>
+    <div class="alarm-example-card">
+      <div class="alarm-example-card-content">
+        <img src="@/assets/images/alarm-example.png" alt="" />
+        <div class="alarm-example-card-info">【安全帽违规】</div>
+      </div>
+      <div class="alarm-example-card-footer">
+        <div class="alarm-example-card-footer-btn">点击查看详情</div>
+        <img src="@/assets/icons/arrow-right-grey.png" alt="" />
+      </div>
+    </div>
+    <div class="alarm-example-title"><div class="alarm-example-block"></div>详情页</div>
+    <div class="alarm-example-detail">
+      <table>
+        <tr v-for="it in ExampleData">
+          <th>{{ it.th }}</th>
+          <td>{{ it.td }}</td>
+        </tr>
+        <tr>
+          <th>问题照片</th>
+          <td>
+            <img src="@/assets/images/alarm-example-photo.png" alt="" />
+            <img src="@/assets/images/alarm-example-photo.png" alt="" />
+            <img src="@/assets/images/alarm-example-photo.png" alt="" />
+          </td>
+        </tr>
+      </table>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+  const ExampleData = [
+    {
+      th: '问题分类',
+      td: '安全帽违规',
+    },
+    {
+      th: '问题描述',
+      td: '关于问题类型的描述,大概300字以内',
+    },
+    {
+      th: '地点',
+      td: 'B26胶接车间-固化区',
+    },
+    {
+      th: '危险点负责人',
+      td: '李四',
+    },
+    {
+      th: '上报人',
+      td: '张三三',
+    },
+    {
+      th: '上报时间',
+      td: '20240315 12:00',
+    },
+  ];
+</script>
+
+<style scoped lang="scss">
+  .alarm-example-title {
+    padding-left: 20px;
+    font-size: 14px;
+    color: #303133;
+    line-height: 20px;
+    .alarm-example-block {
+      margin-right: 20px;
+      width: 4px;
+      height: 12px;
+      background: #1777ff;
+      border-radius: 3px;
+      display: inline-block;
+    }
+  }
+
+  .alarm-example-info-title {
+    padding: 10px 44px;
+    font-weight: 600;
+    font-size: 14px;
+    color: #303133;
+  }
+
+  .alarm-example-info {
+    padding: 0 44px;
+    font-weight: 400;
+    font-size: 12px;
+    color: #606266;
+    div:last-child {
+      padding-top: 10px;
+    }
+  }
+
+  .alarm-example-card {
+    margin: 10px 20px;
+    border: 1px solid rgba(0, 0, 0, 0.06);
+    border-radius: 4px;
+
+    .alarm-example-card-content {
+      border-bottom: 1px solid rgba(0, 0, 0, 0.06);
+      img {
+        width: 100%;
+      }
+
+      .alarm-example-card-info {
+        padding: 4px 8px;
+        font-weight: 600;
+        font-size: 17px;
+        color: #303133;
+      }
+    }
+    .alarm-example-card-footer {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      padding: 4px 16px;
+      .alarm-example-card-footer-btn {
+        font-weight: 400;
+        font-size: 14px;
+        color: rgba(0, 0, 0, 0.65);
+      }
+    }
+  }
+
+  .alarm-example-detail {
+    padding: 10px 20px 0;
+    table {
+      width: 410px;
+      border-collapse: separate; /* 分开边框 */
+      border-spacing: 0; /* 消除边框间距 */
+
+      th,
+      td {
+        border-top: 1px solid rgba(0, 0, 0, 0.06);
+        border-left: 1px solid rgba(0, 0, 0, 0.06);
+        padding: 8px; /* 内边距 */
+        text-align: left; /* 文字对齐方式 */
+      }
+
+      th {
+        font-weight: 400;
+        font-size: 12px;
+        color: #606266;
+      }
+
+      td {
+        border-right: 1px solid rgba(0, 0, 0, 0.06);
+        font-weight: 400;
+        font-size: 12px;
+        color: #303133;
+        img:first-child {
+          margin-left: 0;
+        }
+        img {
+          display: inline-block;
+          margin: 0 5px;
+        }
+      }
+
+      tr:first-child th {
+        border-top-left-radius: 8px; /* 左上角圆角 */
+      }
+      tr:first-child td {
+        border-top-right-radius: 8px; /* 右上角圆角 */
+      }
+      tr:last-child th {
+        border-bottom: 1px solid rgba(0, 0, 0, 0.06);
+        border-bottom-left-radius: 8px; /* 左下角圆角 */
+      }
+      tr:last-child td {
+        border-bottom: 1px solid rgba(0, 0, 0, 0.06);
+        border-bottom-right-radius: 8px; /* 右下角圆角 */
+      }
+    }
+  }
+</style>

+ 181 - 0
src/views/message/alarm-config/components/AlarmExamplePlatform.vue

@@ -0,0 +1,181 @@
+<template>
+  <div style="width: 450px">
+    <div class="alarm-example-title"> <div class="alarm-example-block"></div>卡片页</div>
+    <div class="alarm-example-card">
+      <div class="alarm-example-card-head">
+        <div class="alarm-example-card-head-title">报警信息</div>
+        <div style="display: flex; justify-content: center">
+          <div class="alarm-example-card-head-btn">去查看</div>
+          <img src="@/assets/icons/arrow-right.png" alt="" />
+        </div>
+      </div>
+      <div class="alarm-example-card-content">
+        <!-- <img src="@/assets/images/alarm-example.png" alt="" /> -->
+        <div class="alarm-example-card-content-time">2024年6月25日 16:45:06:342</div>
+        <div class="alarm-example-card-content-tips"
+          >您好,智能检测到该区域发现员工【安全帽违规】的情况,请及时关注!</div
+        >
+      </div>
+    </div>
+    <div class="alarm-example-title"><div class="alarm-example-block"></div>详情页</div>
+    <div class="alarm-example-detail">
+      <table>
+        <tr v-for="it in ExampleData">
+          <th>{{ it.th }}</th>
+          <td>{{ it.td }}</td>
+        </tr>
+        <tr>
+          <th>问题照片</th>
+          <td>
+            <img src="@/assets/images/alarm-example-photo.png" alt="" />
+            <img src="@/assets/images/alarm-example-photo.png" alt="" />
+            <img src="@/assets/images/alarm-example-photo.png" alt="" />
+          </td>
+        </tr>
+      </table>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+  const ExampleData = [
+    {
+      th: '问题分类',
+      td: '安全帽违规',
+    },
+    {
+      th: '问题描述',
+      td: '关于问题类型的描述,大概300字以内',
+    },
+    {
+      th: '地点',
+      td: 'B26胶接车间-固化区',
+    },
+    {
+      th: '危险点负责人',
+      td: '李四',
+    },
+    {
+      th: '上报人',
+      td: '张三三',
+    },
+    {
+      th: '上报时间',
+      td: '20240315 12:00',
+    },
+  ];
+</script>
+
+<style scoped lang="scss">
+  .alarm-example-title {
+    padding-left: 20px;
+    font-size: 14px;
+    color: #303133;
+    line-height: 20px;
+    .alarm-example-block {
+      margin-right: 20px;
+      width: 4px;
+      height: 12px;
+      background: #1777ff;
+      border-radius: 3px;
+      display: inline-block;
+    }
+  }
+
+  .alarm-example-card {
+    padding: 10px 0;
+    margin: 10px 20px;
+    border: 1px solid rgba(0, 0, 0, 0.06);
+    border-radius: 4px;
+
+    .alarm-example-card-head {
+      padding: 0 20px 10px;
+      border-bottom: 1px solid rgba(0, 0, 0, 0.06);
+      display: flex;
+      justify-content: space-between;
+      .alarm-example-card-head-title {
+        display: inline-block;
+        font-weight: 600;
+        font-size: 15px;
+        color: #646566;
+      }
+      .alarm-example-card-head-btn {
+        display: inline-block;
+        font-weight: 400;
+        font-size: 14px;
+        color: #1777ff;
+      }
+      img {
+        display: inline-block;
+        margin-left: 5px;
+        width: 18px;
+      }
+    }
+    .alarm-example-card-content {
+      padding: 3px 20px 10px;
+      .alarm-example-card-content-time {
+        padding: 10px 0;
+        font-weight: 400;
+        font-size: 12px;
+        color: #969799;
+      }
+      .alarm-example-card-content-tips {
+        font-weight: 400;
+        font-size: 13px;
+        color: #646566;
+      }
+    }
+  }
+
+  .alarm-example-detail {
+    padding: 10px 20px 0;
+    table {
+      width: 410px;
+      border-collapse: separate; /* 分开边框 */
+      border-spacing: 0; /* 消除边框间距 */
+
+      th,
+      td {
+        border-top: 1px solid rgba(0, 0, 0, 0.06);
+        border-left: 1px solid rgba(0, 0, 0, 0.06);
+        padding: 8px; /* 内边距 */
+        text-align: left; /* 文字对齐方式 */
+      }
+
+      th {
+        font-weight: 400;
+        font-size: 12px;
+        color: #606266;
+      }
+
+      td {
+        border-right: 1px solid rgba(0, 0, 0, 0.06);
+        font-weight: 400;
+        font-size: 12px;
+        color: #303133;
+        img:first-child {
+          margin-left: 0;
+        }
+        img {
+          display: inline-block;
+          margin: 0 5px;
+        }
+      }
+
+      tr:first-child th {
+        border-top-left-radius: 8px; /* 左上角圆角 */
+      }
+      tr:first-child td {
+        border-top-right-radius: 8px; /* 右上角圆角 */
+      }
+      tr:last-child th {
+        border-bottom: 1px solid rgba(0, 0, 0, 0.06);
+        border-bottom-left-radius: 8px; /* 左下角圆角 */
+      }
+      tr:last-child td {
+        border-bottom: 1px solid rgba(0, 0, 0, 0.06);
+        border-bottom-right-radius: 8px; /* 右下角圆角 */
+      }
+    }
+  }
+</style>

+ 51 - 0
src/views/message/alarm-config/components/PushOccasionsCard.vue

@@ -0,0 +1,51 @@
+<template>
+  <div class="push-occasions-card">
+    <!-- <el-form-item label="推送对象:">
+      <el-radio-group v-model="form.pushRecipients.type">
+        <el-radio v-for="item in PushRecipientType" :value="item.id">{{ item.label }}</el-radio>
+      </el-radio-group>
+    </el-form-item>
+    <el-form-item v-if="form.pushRecipients.type !== 1">asdad </el-form-item> -->
+    <!-- <el-form-item label="自定义内容:">
+        <el-input
+    v-model="form."
+    style="width: 240px"
+    :autosize="{ minRows: 2, maxRows: 4 }"
+    type="textarea"
+    :placeholder="holder"
+  />
+    </el-form-item> -->
+    <!-- <div>{{ '【' + (type || '') + '】' + form.pushPhase }}</div> -->
+  </div>
+</template>
+
+<script setup lang="ts">
+  // import { PushRecipients } from '../types';
+  // import {
+  //   ElFormItem,
+  //   ElInput,
+  //   FormInstance,
+  //   FormRules,
+  // } from 'element-plus';
+
+  // const props = defineProps<{
+  //   form: PushRecipients;
+  //   type?: string | undefined;
+  // }>();
+
+  // 推送人员
+  // const PushRecipientType = [
+  //   { id: 1, label: '全员' },
+  //   { id: 2, label: '分组' },
+  //   { id: 3, label: '自定义' },
+  // ];
+</script>
+
+<style scoped lang="scss">
+  .push-occasions-card {
+    width: 541px;
+    height: 248px;
+    background-color: #fafafa;
+    font-size: 16px;
+  }
+</style>

+ 33 - 0
src/views/message/alarm-config/types.ts

@@ -0,0 +1,33 @@
+export enum AlarmLevel {
+  normal = 1,
+  warning = 2,
+  danger = 3,
+}
+
+export const ALARMLEVEL_LIST = [
+  { label: '一般问题', value: AlarmLevel.normal },
+  { label: '中等问题', value: AlarmLevel.warning },
+  { label: '严重问题', value: AlarmLevel.danger },
+];
+
+export enum AlarmChannel {
+  platform = 1,
+  lanxin = 2,
+}
+
+export const ALARMCHANNEL_LIST = [
+  { label: '平台', value: AlarmChannel.platform },
+  { label: '蓝信', value: AlarmChannel.lanxin },
+];
+
+export enum AlarmPhase {
+  occurs = 1,
+  effects = 2,
+  approved = 3,
+}
+
+export const ALARMPHASE_LIST = [
+  { label: '问题发生时推送', value: AlarmPhase.occurs },
+  { label: '问题生效后推送', value: AlarmPhase.effects },
+  { label: '审核通过后推送', value: AlarmPhase.approved },
+];

+ 16 - 10
src/views/message/alarmMessages/alarmMessages.vue

@@ -55,10 +55,10 @@
         <template #default="scope">
           <div class="operation">
             <el-tooltip class="box-item" effect="light" content="查看" placement="bottom">
-              <img src="./img/view.png" @click="handleView(scope.$index, scope.row)" />
+              <img src="./img/view.png" @click="handleView(scope.row.id)" />
             </el-tooltip>
             <el-tooltip class="box-item" effect="light" content="编辑" placement="bottom">
-              <img src="./img/edit.png" @click="handleEdit(scope.$index, scope.row)" />
+              <img src="./img/edit.png" @click="handleEdit(scope.row.id)" />
             </el-tooltip>
             <el-tooltip class="box-item" effect="light" content="删除" placement="bottom">
               <img src="./img/delete.png" @click="handleDelete(scope.row.id)" />
@@ -70,7 +70,7 @@
       <template #empty>
         <div class="emptyDiv">
           <img src="./img/empty.png" class="emptyImg" />
-          <span class="emptySpan">目前暂无数据,请<a>新建</a>报警消息</span>
+          <span class="emptySpan">目前暂无数据,请新建报警消息</span>
         </div>
       </template>
     </el-table>
@@ -106,11 +106,13 @@
 </template>
 
 <script lang="ts" setup>
-  import { ref } from 'vue';
+  import { ref, onMounted } from 'vue';
   import SearchBar from './components/SearchBar.vue';
-  import { useAlarmDataList } from './hook/index.ts';
+  import { useAlarmDataList } from './hook/index';
   import { storeToRefs } from 'pinia';
+  import { useRouter } from 'vue-router';
 
+  const router = useRouter();
   const useAlarmDataListFun = useAlarmDataList();
   const { alarmDataList, currentPage, pageSize, totalRow } = storeToRefs(useAlarmDataListFun);
   const { getAlarmData, deleteAlarm, updateStatus } = useAlarmDataListFun;
@@ -118,13 +120,13 @@
   const deleteDialog = ref(false);
 
   const createAlarm = () => {
-    console.log(alarmDataList);
+    router.push(`/message/alarm-config?operationType=1`);
   };
-  const handleView = (index: number, row) => {
-    console.log(index, row);
+  const handleEdit = (index: number) => {
+    router.push(`/message/alarm-config?alarmConfigId=${index}&operationType=2`);
   };
-  const handleEdit = (index: number, row) => {
-    console.log(index, row);
+  const handleView = (index: number) => {
+    router.push(`/message/alarm-config?alarmConfigId=${index}&operationType=3`);
   };
 
   const deleteId = ref<number>();
@@ -146,6 +148,10 @@
     currentPage.value = newCurrentPage;
     getAlarmData();
   };
+
+  onMounted(() => {
+    getAlarmData();
+  });
 </script>
 
 <style lang="scss" scoped>

+ 68 - 66
src/views/message/alarmMessages/hook/index.ts

@@ -1,74 +1,76 @@
-import { alarmTableData, alarmInfoRes, alarmLevelMapping, pushChannelMapping } from '../type'
-import {getAlarmMessageList, deleteAlarmMessage, updateAlarmStatus} from '../api/index'
+import { alarmTableData, alarmInfoRes, alarmLevelMapping, pushChannelMapping } from '../type';
+import { getAlarmMessageList, deleteAlarmMessage, updateAlarmStatus } from '../api/index';
 import { reactive, ref, onMounted } from 'vue';
-import { defineStore } from "pinia";
+import { defineStore } from 'pinia';
 
 export const useAlarmDataList = defineStore('useAlarmDataList', () => {
-    
-    const alarmDataList = reactive<alarmTableData[]>([]);
-    const searchContent = ref<string>('');
-    const pageSize = ref<number>(10);
-    const currentPage = ref<number>(1);
-    const totalPage = ref<number>(0);
-    const totalRow = ref<number>(0);
+  const alarmDataList = reactive<alarmTableData[]>([]);
+  const searchContent = ref<string>('');
+  const pageSize = ref<number>(10);
+  const currentPage = ref<number>(1);
+  const totalPage = ref<number>(0);
+  const totalRow = ref<number>(0);
 
-    function getAlarmData(){
-        getAlarmMessageList({content:searchContent.value, pageNumber:currentPage.value, pageSize:pageSize.value})
-        .then((res: alarmInfoRes) => {
-            console.log('res', res);
-            if(res === null){
-                currentPage.value = 1
-                totalPage.value = 0
-                totalRow.value = 0
-                alarmDataList.length = 0;
-                return
-            }
-            alarmDataList.length = 0;
-            totalPage.value = res.totalPage
-            totalRow.value = res.totalRow
-            for (let alarmMessage of res.records){
-                alarmDataList.push({
-                    id: alarmMessage.id,
-                    alarmType: alarmMessage.violationName,
-                    alarmLevel: alarmLevelMapping[alarmMessage.violationLevel],
-                    pushChannel: alarmMessage.pushChannel.split(',').map((channel)=>pushChannelMapping[Number(channel)]),
-                    status: Boolean(alarmMessage.status),
-                    operationTime: alarmMessage.updatedAt
-                })
-            }
-        })
-    }
-
-    function deleteAlarm(id: number){
-        deleteAlarmMessage(id)
-        .then((res) => {
-            getAlarmData();
-        })
-    }
+  function getAlarmData() {
+    getAlarmMessageList({
+      content: searchContent.value,
+      pageNumber: currentPage.value,
+      pageSize: pageSize.value,
+    }).then((res: alarmInfoRes) => {
+      console.log('res', res);
+      if (res === null) {
+        currentPage.value = 1;
+        totalPage.value = 0;
+        totalRow.value = 0;
+        alarmDataList.length = 0;
+        return;
+      }
+      alarmDataList.length = 0;
+      totalPage.value = res.totalPage;
+      totalRow.value = res.totalRow;
+      for (let alarmMessage of res.records) {
+        alarmDataList.push({
+          id: alarmMessage.id,
+          alarmType: alarmMessage.violationName,
+          alarmLevel: alarmLevelMapping[alarmMessage.violationLevel],
+          pushChannel: alarmMessage.pushChannel
+            .slice(1, -1)
+            .split(',')
+            .map((channel) => pushChannelMapping[Number(channel)]),
+          status: Boolean(alarmMessage.status),
+          operationTime: alarmMessage.updatedAt,
+        });
+      }
+    });
+  }
 
-    function updateStatus(id: number, rowStatus: boolean){
-        let status = Number(rowStatus)
-        updateAlarmStatus({id, status})
-        .then((res) => {
-            getAlarmData();
-        })
-    }
+  function deleteAlarm(id: number) {
+    deleteAlarmMessage(id).then((res) => {
+      getAlarmData();
+    });
+  }
 
-    onMounted(() => {
-        getAlarmData();
-        console.log("alarmDataList", alarmDataList);
-        
+  function updateStatus(id: number, rowStatus: boolean) {
+    let status = Number(rowStatus);
+    updateAlarmStatus({ id, status }).then((res) => {
+      getAlarmData();
     });
+  }
+
+  onMounted(() => {
+    getAlarmData();
+    console.log('alarmDataList', alarmDataList);
+  });
 
-    return {
-        alarmDataList,
-        searchContent,
-        currentPage,
-        pageSize,
-        totalPage,
-        totalRow,
-        getAlarmData,
-        deleteAlarm,
-        updateStatus
-      };
-})
+  return {
+    alarmDataList,
+    searchContent,
+    currentPage,
+    pageSize,
+    totalPage,
+    totalRow,
+    getAlarmData,
+    deleteAlarm,
+    updateStatus,
+  };
+});

+ 13 - 0
src/views/message/api/index.ts

@@ -0,0 +1,13 @@
+import { http } from '@/utils/http/axios';
+export function ToPushObjectqueryUserGroupList() {
+    return http.request({
+        url: '/userGroup/ToPushObjectqueryUserGroupList',
+        method: 'post',
+    });
+}
+export function queryUserGroupDetail(userGroupList: number[]) {
+    return http.request({
+        url: `/alarmMessage/queryUserGroupDetail?userGroupList=${userGroupList}`,
+        method: 'post',
+    });
+}

+ 87 - 0
src/views/message/components/Group.vue

@@ -0,0 +1,87 @@
+<template>
+  <div class="group" v-for="group in userGroupInfo" :key="group.userGroupId">
+    <div class="group-name">
+      <span
+        style="font-weight: 400; font-size: 16px; color: rgba(0, 0, 0, 0.88); line-height: 22px"
+      >
+        {{ group.name }}
+      </span>
+      <span
+        style="
+          margin-left: 4px;
+          font-weight: 400;
+          font-size: 12px;
+          color: rgba(0, 0, 0, 0.88);
+          line-height: 17px;
+        "
+      >
+        共
+        <span style="color: #1777ff">{{ group.total }}</span>
+        人
+      </span>
+      <div
+        class="user-info"
+        :class="{ expanded: group.isExpand }"
+        ref="userInfoRefs"
+        :style="
+          !group.isExpand ? 'max-height:86px;overflow-y:hidden;' : 'max-height:100%;overflow-y:auto'
+        "
+        style="display: flex"
+      >
+        <div
+          class="left"
+          style="display: flex; gap: 10px; margin-top: 20px; flex-wrap: wrap; flex: 1"
+        >
+          <el-tag type="primary" v-for="user in group.userList" :key="user.userId">
+            {{ user.loginName }}-{{ user.nickname }}
+          </el-tag>
+        </div>
+        <div class="right" style="width: 50px; margin-top: 20px" v-if="group.isHidden">
+          <span
+            @click="toggleExpand(group)"
+            style="display: flex; cursor: pointer; align-items: center"
+          >
+            {{ group.isExpand ? '收起' : '展开' }}
+            <img v-if="group.isExpand" src="@/assets/icons/arrow_top.png" />
+            <img v-else src="@/assets/icons/arrow_bottom.png" />
+          </span>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref, watch } from 'vue';
+import { GroupData } from '../persongroup/type';
+const userInfoRefs = ref({});
+const userGroupInfo = ref<GroupData[]>();
+const toggleExpand = (group) => {
+  group.isExpand = !group.isExpand;
+};
+const props = defineProps<{
+  userGroupInfo?: GroupData[];
+}>();
+watch(
+  () => props.userGroupInfo,
+  (newVal) => {
+    userGroupInfo.value = newVal;
+  },
+);
+watch(
+  () => userInfoRefs.value,
+  (newRefs) => {
+    if (newRefs) {
+      Object.keys(newRefs).forEach((key) => {
+        const el = newRefs[key];
+        if (el.offsetHeight >= 86) {
+          userGroupInfo.value![key].isHidden = true;
+        }
+      });
+    }
+  },
+);
+</script>
+
+<style lang="scss" scoped>
+</style>

+ 285 - 0
src/views/message/components/PushObject.vue

@@ -0,0 +1,285 @@
+<template>
+  <el-form :model="ruleForm" ref="ruleFormRef">
+    <el-form-item
+      label="推送对象"
+      prop="recipientType"
+      :rules="[{ required: true, message: '请选择推送对象' }]"
+    >
+      <el-radio-group v-model="ruleForm.recipientType" :disabled="disabled">
+        <el-radio
+          v-for="item in recipientTypeName"
+          :key="item.value"
+          :value="item.value"
+          :label="item.label"
+        />
+      </el-radio-group>
+    </el-form-item>
+    <div class="userGroupList" v-if="ruleForm.recipientType === 2">
+      <el-form-item
+        label="选择分组"
+        prop="userGroupList"
+        :rules="[{ required: true, message: '请选择分组' }]"
+      >
+        <el-select
+          v-model="ruleForm.userGroupList"
+          multiple
+          placeholder="请选择分组"
+          style="width: 300px"
+          :disabled="disabled"
+        >
+          <el-option
+            v-for="item in options"
+            :key="item.userGroupId"
+            :value="item.userGroupId"
+            :label="item.name"
+          />
+        </el-select>
+      </el-form-item>
+      <span
+        v-if="ruleForm.userGroupList.length > 0"
+        @click="queryGroupInfo(ruleForm.userGroupList)"
+      >
+        人员详情
+      </span>
+    </div>
+    <div class="customUserList" v-if="ruleForm.recipientType === 3">
+      <el-form-item
+        label="选择人员"
+        prop="customUserList"
+        :rules="[{ required: true, message: '请选择人员' }]"
+      >
+        <el-select
+          v-model="ruleForm.customUserList"
+          value-key="id"
+          multiple
+          placeholder="请选择人员"
+          style="width: 300px"
+          @click="userInfo = true"
+          :disabled="disabled"
+        >
+          <el-option v-for="user in selectedUser" :key="user.id" :label="user.name" :value="user">
+          </el-option>
+        </el-select>
+      </el-form-item>
+    </div>
+  </el-form>
+  <el-dialog
+    v-model="groupInfo"
+    title="人员详情"
+    align-center
+    :close-on-click-modal="false"
+    style="height: 583px"
+    :width="731"
+    :destroy-on-close="true"
+    class="groupInfo"
+  >
+    <Group :userGroupInfo="userGroupInfo" />
+  </el-dialog>
+  <el-dialog
+    v-model="userInfo"
+    class="userInfo"
+    title="添加组内成员"
+    align-center
+    :close-on-click-modal="false"
+    style="height: 583px"
+    :width="731"
+    :destroy-on-close="true"
+  >
+    <SelectTree @cancel="handleCancle" @submit="handleSubmit" :selectedUser="selectedUser" />
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+  import { onMounted, ref, reactive, watch, watchEffect } from 'vue';
+  import SelectTree from '../persongroup/components/SelectTree.vue';
+  import Group from './Group.vue';
+  import { recipientTypeName } from '../constant';
+  import { ToPushObjectqueryUserGroupList, queryUserGroupDetail } from '../api/index';
+  import type { FormInstance } from 'element-plus';
+  import { GroupData } from '../persongroup/type';
+  const ruleFormRef = ref<FormInstance>();
+  const groupInfo = ref<boolean>(false);
+  const userInfo = ref<boolean>(false);
+  const disabled = ref<boolean>(false);
+  export interface customUserList {
+    userId: number;
+    userLoginName: string;
+    userNickname: string;
+    userNumber: string;
+  }
+  export interface userGroupVOList {
+    userGroupId: number;
+    total: number;
+    operatorName: string;
+    operationTime: string;
+    name: string;
+    description: string;
+  }
+  interface UserList {
+    id: string;
+    name: string;
+    userId: number;
+  }
+  const selectedUser = ref<UserList[]>([]);
+  interface RuleForm {
+    recipientType?: number;
+    userGroupList: number[];
+    customUserList: UserList[];
+  }
+  const ruleForm = reactive<RuleForm>({
+    userGroupList: [],
+    customUserList: [],
+  });
+  const props = defineProps<{
+    recipientType?: number;
+    userGroupList?: userGroupVOList[];
+    customUserList?: customUserList[];
+    disabled?: boolean;
+  }>();
+  const emit = defineEmits(['submitWithForm']);
+  interface Options {
+    userGroupId?: number;
+    name?: string;
+    description: string;
+    total: number;
+    operatorName: string;
+    operationTime: string;
+  }
+  const options = ref<Options[]>([]);
+  const userGroupInfo = ref<GroupData[]>();
+  const queryGroupInfo = (groupList) => {
+    groupInfo.value = true;
+    queryUserGroupDetail(groupList).then((res) => {
+      userGroupInfo.value = res.map((group) => ({
+        ...group,
+        isExpand: false,
+        isHidden: false,
+      }));
+    });
+  };
+  const submitForm = () => {
+    return ruleFormRef.value!.validate(() => {});
+  };
+  const validateForm = () => {
+    return new Promise((resolve, reject) => {
+      ruleFormRef
+        .value!.validate(() => {})
+        .then((valid) => {
+          if (valid) {
+            const res = getChildValue();
+            emit('submitWithForm', res);
+            resolve(() => {});
+          } else {
+            reject(new Error('error submit!'));
+          }
+        });
+    });
+  };
+  const getChildValue = () => {
+    return {
+      recipientType: ruleForm.recipientType,
+      userGroupList:
+        ruleForm.recipientType === 3
+          ? []
+          : ruleForm.recipientType === 1
+          ? []
+          : ruleForm.userGroupList.map((item) => item),
+      customUserList:
+        ruleForm.recipientType === 2
+          ? []
+          : ruleForm.recipientType === 1
+          ? []
+          : ruleForm.customUserList.map((item) => item.userId),
+    };
+  };
+  const handleCancle = () => {
+    userInfo.value = false;
+  };
+  const handleSubmit = (selectedData: UserList[]) => {
+    selectedUser.value = selectedData;
+    ruleForm.customUserList = selectedUser.value;
+    userInfo.value = false;
+  };
+  const formatCustomUserList = (customList: customUserList[]): UserList[] => {
+    return customList.map((item) => ({
+      id: `u${item.userId}`,
+      userId: item.userId,
+      name: `${item.userLoginName}-${item.userNickname}`,
+    }));
+  };
+  defineExpose({
+    submitForm,
+    validateForm,
+    getChildValue,
+  });
+  onMounted(() => {
+    ToPushObjectqueryUserGroupList().then((res) => {
+      options.value = res.groupVOList;
+    });
+  });
+  watchEffect(() => {
+    if (props.recipientType) {
+      ruleForm.recipientType = props.recipientType;
+    }
+    if (props.userGroupList) {
+      ruleForm.userGroupList = props.userGroupList.map((item) => item.userGroupId);
+    }
+    if (props.customUserList) {
+      ruleForm.customUserList = formatCustomUserList(props.customUserList);
+      selectedUser.value = formatCustomUserList(props.customUserList);
+    }
+    if (props.disabled) {
+      disabled.value = props.disabled;
+    }
+  });
+  watch(
+    () => ruleForm.customUserList,
+    (newSelected) => {
+      selectedUser.value = newSelected;
+    },
+    { immediate: true },
+  );
+</script>
+
+<style lang="scss" scoped>
+  .userGroupList {
+    margin-left: 12%;
+    width: 88%;
+    max-height: 120px;
+    padding: 12px 17px 12px 12px;
+    background: #fafafa;
+    border-radius: 4px;
+    :deep(.el-form-item) {
+      margin-bottom: 12px !important;
+    }
+    ::v-deep .el-select__selection {
+      min-height: 25px;
+      max-height: 60px;
+      overflow-y: auto;
+    }
+    span {
+      cursor: pointer;
+      margin-left: 80px;
+      font-weight: 400;
+      font-size: 10px;
+      color: #1777ff;
+      line-height: 14px;
+    }
+  }
+  .customUserList {
+    margin-left: 12%;
+    width: 88%;
+    max-height: 120px;
+    padding: 12px 17px 12px 12px;
+    background: #fafafa;
+    border-radius: 4px;
+    :deep(.el-form-item) {
+      margin-bottom: 12px !important;
+    }
+    ::v-deep .el-select__selection {
+      min-height: 25px;
+      max-height: 60px;
+      overflow-y: auto;
+    }
+  }
+</style>

+ 15 - 1
src/views/message/constant.ts

@@ -11,5 +11,19 @@ export const statisticTypeName = [
     { value: 5, label: "自定义" },
 ];
 export const messageTypeName = [
-    { value: 1, label: "报表消息" }
+    { value: 1, label: "报表消息" },
+    { value: 2, label: "报警消息" },
+]
+export const pushChannelName = [
+    { value: 1, label: "蓝信" },
+    { value: 2, label: "平台" },
+]
+export const recipientTypeName = [
+    { value: 1, label: "全员" },
+    { value: 2, label: "分组" },
+    { value: 3, label: "自定义" }
+]
+export const statusName = [
+    { value: 0, label: "已推送" },
+    { value: 1, label: "未推送" },
 ]

+ 1 - 1
src/views/message/persongroup/components/GroupBoard.vue

@@ -111,7 +111,7 @@
     </el-form>
     <el-dialog
       v-model="dialogVisible"
-      title="添加组内成员"
+      title="添加员"
       align-center
       :close-on-click-modal="false"
       style="height: 583px"

+ 42 - 61
src/views/message/persongroup/components/SelectTree.vue

@@ -7,21 +7,23 @@
         :style="{ width: '300px', height: '30px' }"
         placeholder="请输入搜索内容"
         :prefix-icon="Search"
-        @input="onSearch"
         clearable
       /></el-form-item>
       <el-tree
+        v-loading="loading"
         ref="treeRef"
-        :data="filterData"
+        :data="nodeData"
         show-checkbox
         node-key="id"
         :props="defaultProps"
-        :default-expand-all="true"
+        :render-after-expand="false"
+        :filter-node-method="filterNode"
+        accordion
         @node-click="handleNodeClick"
         @check-change="handleCheckChange"
       />
     </div>
-    <el-divider direction="vertical" style="height: auto" />
+    <!-- <el-divider direction="vertical" style="height: 100%;flex:1" /> -->
     <div class="right" style="margin-left: 16px">
       <div class="head" style="margin-bottom: 22px">
         <span
@@ -60,11 +62,13 @@
 <script lang="ts" setup>
 import { onMounted, ref, watch } from 'vue';
 import { Search } from '@element-plus/icons-vue';
-import type { ElTree } from 'element-plus';
+import { ElTree } from 'element-plus';
 import { treeSelected, TreeNode, FormattedNode } from '../type';
 import { countLeafNodes, formatTree } from '../hook/index';
 import { queryUserTree } from '../api/index';
 import { cloneDeep } from 'lodash-es';
+import { TreeKey } from 'element-plus/es/components/tree/src/tree.type';
+const loading = ref(true);
 const queryStr = ref<string>('');
 const defaultProps = {
   children: 'children',
@@ -72,45 +76,34 @@ const defaultProps = {
 };
 const treeData = ref<TreeNode[]>();
 const nodeData = ref<FormattedNode[]>();
-const filterData = ref<FormattedNode[]>();
-
-const filterTree = (nodes: FormattedNode[], keyword: string): FormattedNode[] => {
-  const filteredNodes: FormattedNode[] = [];
-  const traverse = (node) => {
-    let includeNode = false;
-    if (node.name.includes(keyword)) {
-      includeNode = true;
-    }
-    if (node.children) {
-      const filteredChildren = node.children
-        .map((child) => traverse(child))
-        .filter((child) => child !== null);
-      if (filteredChildren.length > 0) {
-        includeNode = true;
-        node = { ...node, children: filteredChildren };
-      }
-    }
-    return includeNode ? node : null;
-  };
-  nodes.forEach((node) => {
-    const result = traverse(node);
-    if (result !== null) {
-      filteredNodes.push(result);
-    }
-  });
-  return filteredNodes;
-};
-
-const onSearch = () => {
-  if (queryStr.value) {
-    filterData.value = filterTree(nodeData.value!, queryStr.value);
-  } else {
-    filterData.value = nodeData.value;
+const filterNode = (query: string, nodeData: FormattedNode, node: any) => {
+  if (!query) return true;
+  nodeData.filter = nodeData.name!.includes(query);
+  if (!nodeData.filter && node.level > 1) {
+    nodeData.filter = node.parent.data.filter;
   }
+  return nodeData.filter!;
 };
+
+const treeRef = ref<InstanceType<typeof ElTree>>();
 const total = ref<number>(0);
 const selected = ref<number>(0);
 const selectedPeople = ref<treeSelected[]>([]);
+const handleTagClose = (id) => {
+  const index = selectedPeople.value.findIndex((item) => item.id === id);
+  if (index !== -1) {
+    selectedPeople.value.splice(index, 1);
+    selected.value = selectedPeople.value.length;
+    treeRef.value!.setChecked(id, false, true);
+  }
+};
+const emit = defineEmits(['cancel', 'submit']);
+const handleCancle = () => {
+  emit('cancel');
+};
+const handleSubmit = () => {
+  emit('submit', selectedPeople.value);
+};
 const handleCheckChange = (node, checked) => {
   if (!node.children || (node.children.length === 0 && node.userId)) {
     if (checked) {
@@ -128,25 +121,9 @@ const handleCheckChange = (node, checked) => {
     selected.value = selectedPeople.value.length;
   }
 };
-const treeRef = ref<InstanceType<typeof ElTree>>();
-const handleTagClose = (id) => {
-  const index = selectedPeople.value.findIndex((item) => item.id === id);
-  if (index !== -1) {
-    selectedPeople.value.splice(index, 1);
-    selected.value = selectedPeople.value.length;
-    treeRef.value!.setChecked(id, false, true);
-  }
-};
-const emit = defineEmits(['cancel', 'submit']);
-const handleCancle = () => {
-  emit('cancel');
-};
-const handleSubmit = () => {
-  emit('submit', selectedPeople.value);
-};
-const handleNodeClick = (node) => {
-  const isChecked = treeRef.value!.getCheckedKeys().includes(node.id);
-  treeRef.value!.setChecked(node.id,!isChecked,true);
+const handleNodeClick = (node, checked) => {
+  handleCheckChange(node, checked.checked);
+  treeRef.value!.setChecked(node.id, !checked.checked, true);
 };
 const props = defineProps<{
   selectedUser: treeSelected[];
@@ -155,10 +132,10 @@ onMounted(() => {
   queryUserTree().then((res) => {
     treeData.value = res;
     nodeData.value = formatTree(treeData.value!);
-    filterData.value = nodeData.value;
     total.value = countLeafNodes(nodeData.value);
-    const selectedIds: string[] = selectedPeople.value.map((item) => item.id as string);
+    const selectedIds: TreeKey[] = selectedPeople.value.map((item) => item.id as string);
     treeRef.value!.setCheckedKeys(selectedIds);
+    loading.value = false;
   });
 });
 watch(
@@ -169,6 +146,9 @@ watch(
   },
   { immediate: true },
 );
+watch(queryStr, (query) => {
+  treeRef.value!.filter(query);
+});
 </script>
 
 <style lang="scss" scoped>
@@ -181,6 +161,7 @@ watch(
     flex-direction: column;
     width: 50%;
     height: 100%;
+    border-right: 1px solid rgba(0,0,0,0.06);
     .el-tree {
       width: 100%;
       margin-top: 20px;
@@ -195,7 +176,7 @@ watch(
   .right {
     display: flex;
     flex-direction: column;
-    width: 50%;
+    flex: 1;
     height: 100%;
     position: relative;
     .head {

+ 5 - 0
src/views/message/persongroup/type.ts

@@ -42,6 +42,7 @@ export interface FormattedNode {
     name: string | null;
     userId: number | null;
     children: FormattedNode[];
+    filter?: boolean;
 }
 export interface FromUserList {
     userId: number;
@@ -55,4 +56,8 @@ export interface FormData {
     total: number;
     userGroupId: number;
     userList: FromUserList[];
+}
+export interface GroupData extends FormData {
+    isExpand: boolean;
+    isHidden: boolean;
 }

+ 0 - 1
src/views/message/reportmessage/api/index.ts

@@ -1,5 +1,4 @@
 import { http } from '@/utils/http/axios';
-import qs from 'qs'
 export function queryReportConfigList(type: number) {
     return http.request({
         url: '/reportMessage/queryReportConfigList',

+ 0 - 13
src/views/message/reportmessage/constant.ts

@@ -1,13 +0,0 @@
-export const recipientTypeName = [
-    { value: 1, label: "全员" },
-    { value: 2, label: "分组" },
-    { value: 3, label: "自定义" }
-]
-export const pushChannelName = [
-    { value: 1, label: "蓝信" },
-    { value: 2, label: "平台" },
-]
-export const statusName = [
-    { value: 0, label: "已推送" },
-    { value: 1, label: "未推送" },
-]

+ 1 - 2
src/views/message/reportmessage/overviewColumns.ts

@@ -1,8 +1,7 @@
 import { h } from 'vue';
 import type { BasicColumn } from '@/components/Table';
 import { ElSwitch } from 'element-plus';
-import { pushChannelName, recipientTypeName, statusName } from './constant'
-import { statisticTypeName } from '@/views/message/constant'
+import { pushChannelName, recipientTypeName, statusName,statisticTypeName } from '../constant'
 import { storeToRefs } from 'pinia';
 import useFormList from './store/useFormList';
 const formStore = useFormList();

+ 480 - 0
src/views/message/sysnotion-config/SysnotionConfig.vue

@@ -0,0 +1,480 @@
+<template>
+  <div class="sysnotion-config">
+    <div class="tophead">
+      <div><img src="@/views/message/reportmessage/img/rollback.png" />返回</div>
+      <span>新建系统通知</span>
+    </div>
+    <div class="content">
+      <div class="left">
+        <el-form
+          ref="ruleFormRef"
+          style="max-width: 600px"
+          label-width="auto"
+          :model="ruleForm"
+          :label-position="labelPosition"
+          class="el-form-outer"
+        >
+          <el-form-item
+            label="消息标题"
+            prop="title"
+            :rules="[{ required: true, message: '请输入消息标题' }]"
+          >
+            <el-input
+              v-model="ruleForm.title"
+              placeholder="请输入20字以内的消息标题"
+              maxlength="20"
+              show-word-limit
+            />
+          </el-form-item>
+          <el-form-item label="消息内容" prop="content" class="transprant">
+            <el-input
+              v-model="ruleForm.content"
+              placeholder="请输入500字以内的消息内容"
+              type="textarea"
+              :rows="5"
+              maxlength="500"
+              show-word-limit
+            />
+          </el-form-item>
+          <el-form-item
+            label="推送渠道"
+            prop="channel"
+            :rules="[{ required: true, message: '请选择推送渠道' }]"
+          >
+            <el-checkbox
+              v-model="ruleForm.channel"
+              v-for="item in pushChannelName"
+              :key="item.value"
+              :value="item.value"
+              :label="item.label"
+            />
+          </el-form-item>
+          <PushObject
+            ref="childFromRef"
+            :recipientType="ruleForm.object.recipientType"
+            :userGroupList="ruleForm.object.userGroupList"
+            :customUserList="ruleForm.object.customUserList"
+          />
+          <el-form-item label="操作人" prop="operator" class="transprant">
+            <el-input v-model="ruleForm.operator" :disabled="true" />
+          </el-form-item>
+        </el-form>
+        <div class="btns">
+          <el-button>重置</el-button>
+          <el-button>暂存</el-button>
+          <el-button type="primary" @click="submitForm()">确定</el-button>
+        </div>
+      </div>
+      <div class="right">
+        <div class="top-head">
+          实时效果
+          <el-tooltip
+            effect="dark"
+            content="实例样式仅供参考,最终展示以线上为准"
+            placement="top-start"
+          >
+            <img src="@/assets/icons/info.png" />
+          </el-tooltip>
+        </div>
+        <div class="tabs">
+          <el-tabs v-model="activeName">
+            <el-tab-pane label="平台侧" name="platform" class="platform">
+              <div class="tabs-content">
+                <div class="title">
+                  <div class="vertical"></div>
+                  <span>卡片页:</span>
+                </div>
+                <div class="card">
+                  <div class="card-title"
+                    >系统公告<a>去查看<img src="@/assets/icons/arrow.png" /></a
+                  ></div>
+                  <div class="card-content">
+                    <p style="font-size: 12px; color: #969799">2024年6月25日 16:45:06</p>
+                    <p style="margin-top: 5px; font-size: 13px; color: #646566">
+                      {{ ruleForm.title || title }}
+                    </p>
+                  </div>
+                </div>
+              </div>
+              <div class="tabs-content">
+                <div class="title">
+                  <div class="vertical"></div>
+                  <span>详情页:</span>
+                </div>
+                <div class="info">
+                  <div class="info-title"> {{ ruleForm.title || title }} </div>
+                  <div class="info-content">
+                    <span>{{ ruleForm.content || content }}</span>
+                  </div>
+                </div>
+              </div>
+            </el-tab-pane>
+            <el-tab-pane label="蓝信侧" name="lanxin" class="lanxin">
+              <div class="tabs-content">
+                <div class="title">
+                  <div class="vertical"></div>
+                  <span>卡片页:</span>
+                </div>
+                <div class="card">
+                  <div class="card-title">系统公告</div>
+                  <div class="card-content">
+                    <p style="margin-top: 5px; font-size: 13px; color: #646566">
+                      {{ ruleForm.title || title }}
+                    </p>
+                  </div>
+                  <img src="@/assets/icons/link_icon.png" />
+                </div>
+              </div>
+              <div class="tabs-content">
+                <div class="title">
+                  <div class="vertical"></div>
+                  <span>详情页:</span>
+                </div>
+                <div class="info">
+                  <div class="info-title"> {{ ruleForm.title || title }} </div>
+                  <div class="info-content">
+                    <span>{{ ruleForm.content || content }}</span>
+                  </div>
+                </div>
+              </div>
+            </el-tab-pane>
+          </el-tabs>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref, reactive } from 'vue';
+import { storeToRefs } from 'pinia';
+import { useUserStore } from '@/store/modules/user';
+import { pushChannelName } from '../constant';
+import type { FormProps } from 'element-plus';
+import PushObject from '../components/PushObject.vue';
+import type { FormInstance } from 'element-plus';
+const title = ref<string>('本系统进行了重大升级,请查看详细内容');
+const content = ref<string>(
+  '尊敬的用户:\n    我们计划于2024年9月5日进行平台系统升级,以提升服务性能和用户体验,升级期间,平台将暂时不可用,预计停机时间为4小时,从上午2:00至6:00。请您提前做好相关安排,以避免不便,感谢您的理解与支持。如有疑问,请联系客服支持团队。\n敬请留意。\n天眼团队',
+);
+const ruleFormRef = ref<FormInstance>();
+const childFromRef = ref();
+const validate = ref<boolean>();
+const useUser = useUserStore();
+const { info } = storeToRefs(useUser);
+const labelPosition = ref<FormProps['labelPosition']>('left');
+interface UserList {
+  userId: number;
+  userLoginName:string;
+  userNickname:string;
+  userNumber:string;
+}
+interface GroupList {
+  userGroupId:number;
+  total:number;
+  operatorName:string;
+  operationTime:string;
+  name:string;
+  description:string;
+}
+interface ObjectFrom {
+  recipientType?: number;
+  userGroupList?: GroupList[];
+  customUserList?: UserList[];
+}
+interface RuleForm {
+  title: string;
+  content: string;
+  channel: number[];
+  object: ObjectFrom;
+  operator: string;
+}
+const ruleForm = reactive<RuleForm>({
+  title: '',
+  content: '',
+  channel: [],
+  object: {
+    userGroupList:[]
+  },
+  operator: info.value.nickname,
+});
+const activeName = ref('platform');
+const submitForm = () => {
+  console.log(childFromRef.value!.getChildValue())
+  childFromRef.value!.submitForm().then((res) => {
+    validate.value = res;
+  });
+  ruleFormRef.value!.validate((valid) => {
+    if (validate.value && valid) {
+      console.log(ruleForm);
+    } else {
+      console.log('下发失败');
+    }
+  });
+};
+</script>
+
+<style lang="scss" scoped>
+.sysnotion-config {
+  position: relative;
+  height: calc(100vh - 64px - 18px);
+  background-color: rgba(255, 255, 255, 1);
+  box-sizing: border-box !important;
+  .tophead {
+    display: flex;
+    gap: 20px;
+    width: 100%;
+    height: 50px;
+    padding: 16px 0 14px 21px;
+    border-bottom: 1px solid rgba(0, 0, 0, 0.06);
+    div {
+      display: flex;
+      align-items: center;
+      font-weight: 400;
+      font-size: 14px;
+      color: #303133;
+      line-height: 22px;
+      cursor: pointer;
+      img {
+        margin-right: 4px;
+      }
+    }
+  }
+  .content {
+    display: flex;
+    width: 100%;
+    height: calc(100vh - 64px - 18px - 50px);
+    padding: 0 30px 0 0;
+    .left {
+      flex: 1;
+      position: relative;
+      padding: 21px;
+      border-right: 1px solid rgba(0, 0, 0, 0.06);
+      .el-form-outer {
+        display: flex;
+        flex-direction: column;
+        gap: 32px;
+      }
+      .transprant {
+        :deep(.el-form-item__label::before) {
+          content: '**';
+          opacity: 0;
+        }
+      }
+      .btns {
+        position: absolute;
+        right: 27px;
+        bottom: 21px;
+      }
+    }
+    .right {
+      width: 380px;
+      height: 100%;
+      padding: 20px 9px 0 20px;
+      .top-head {
+        display: flex;
+        gap: 7px;
+        align-items: center;
+        width: 100%;
+        height: 22px;
+        font-weight: 600;
+        font-size: 14px;
+        color: rgba(0, 0, 0, 0.85);
+        line-height: 22px;
+        img {
+          cursor: pointer;
+        }
+      }
+      .tabs {
+        margin-top: 14px;
+        :deep(.el-tabs__header) {
+          margin: 0 0 22px;
+        }
+        :deep(.is-top) {
+          font-weight: 550;
+          font-size: 14px;
+          line-height: 22px;
+        }
+        :deep(.el-tabs__nav-wrap::after) {
+          height: 0px;
+        }
+        .platform {
+          display: flex;
+          flex-direction: column;
+          gap: 24px;
+          .tabs-content {
+            width: 100%;
+            height: auto;
+            max-height: calc(100vh - 450px);
+            .title {
+              display: flex;
+              gap: 18px;
+              width: 100%;
+              height: 31px;
+              .vertical {
+                width: 4px;
+                height: 12px;
+                background: #1777ff;
+                border-radius: 3px;
+              }
+              span {
+                font-weight: 400;
+                font-size: 14px;
+                color: #303133;
+                line-height: 20px;
+              }
+            }
+            .card {
+              width: 100%;
+              height: 124px;
+              padding: 10px 14px 0 12px;
+              background: #ffffff;
+              border-radius: 4px;
+              border: 1px solid rgba(0, 0, 0, 0.06);
+              .card-title {
+                display: flex;
+                justify-content: space-between;
+                font-weight: 600;
+                font-size: 15px;
+                color: #646566;
+                line-height: 20px;
+                a {
+                  display: flex;
+                  gap: 8px;
+                  cursor: default;
+                }
+              }
+              .card-content {
+                margin-top: 15px;
+                font-weight: 400;
+                line-height: 20px;
+              }
+            }
+            .info {
+              width: 100%;
+              max-height: calc(100vh - 450px - 32px);
+              background: #ffffff;
+              border-radius: 4px;
+              border: 1px solid rgba(0, 0, 0, 0.06);
+              .info-title {
+                display: flex;
+                justify-content: center;
+                align-items: center;
+                width: 100%;
+                height: 41px;
+                font-weight: 600;
+                font-size: 14px;
+                color: rgba(0, 0, 0, 0.85);
+                line-height: 22px;
+                border-bottom: 1px solid rgba(0, 0, 0, 0.06);
+              }
+              .info-content {
+                width: 100%;
+                max-height: calc(100vh - 450px - 32px - 41px);
+                overflow-y: auto;
+                padding: 12px 7px 12px 12px;
+                font-weight: 400;
+                font-size: 14px;
+                color: #646566;
+                line-height: 22px;
+                word-wrap: break-word;
+                white-space: pre-wrap;
+              }
+            }
+          }
+        }
+        .lanxin {
+          display: flex;
+          flex-direction: column;
+          gap: 24px;
+          .tabs-content {
+            width: 100%;
+            height: auto;
+            max-height: calc(100vh - 400px);
+            .title {
+              display: flex;
+              gap: 18px;
+              width: 100%;
+              height: 31px;
+              .vertical {
+                width: 4px;
+                height: 12px;
+                background: #1777ff;
+                border-radius: 3px;
+              }
+              span {
+                font-weight: 400;
+                font-size: 14px;
+                color: #303133;
+                line-height: 20px;
+              }
+            }
+            .card {
+              width: 100%;
+              height: 79px;
+              padding: 10px 14px 0 12px;
+              background: #ffffff;
+              border-radius: 4px;
+              border: 1px solid rgba(0, 0, 0, 0.06);
+              position: relative;
+              .card-title {
+                display: flex;
+                justify-content: space-between;
+                font-weight: 600;
+                font-size: 15px;
+                color: #646566;
+                line-height: 20px;
+                a {
+                  display: flex;
+                  gap: 8px;
+                }
+              }
+              .card-content {
+                margin-top: 15px;
+                font-weight: 400;
+                line-height: 20px;
+              }
+              img {
+                position: absolute;
+                top: 10px;
+                right: 17px;
+              }
+            }
+            .info {
+              width: 100%;
+              max-height: calc(100vh - 400px - 32px);
+              background: #ffffff;
+              border-radius: 4px;
+              border: 1px solid rgba(0, 0, 0, 0.06);
+              .info-title {
+                display: flex;
+                justify-content: center;
+                align-items: center;
+                width: 100%;
+                height: 41px;
+                font-weight: 600;
+                font-size: 14px;
+                color: rgba(0, 0, 0, 0.85);
+                line-height: 22px;
+                border-bottom: 1px solid rgba(0, 0, 0, 0.06);
+              }
+              .info-content {
+                width: 100%;
+                max-height: calc(100vh - 400px - 32px - 41px);
+                overflow-y: auto;
+                padding: 12px 7px 12px 12px;
+                font-weight: 400;
+                font-size: 14px;
+                color: #646566;
+                line-height: 22px;
+                word-wrap: break-word;
+                white-space: pre-wrap;
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
+</style>