Ver código fonte

完成推送对象组件

chauncey 1 ano atrás
pai
commit
843dd828a9

BIN
src/assets/icons/arrow.png


BIN
src/assets/icons/arrow_bottom.png


BIN
src/assets/icons/arrow_top.png


BIN
src/assets/icons/link_icon.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
     }
   })

+ 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',
+    });
+}

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

@@ -0,0 +1,277 @@
+<template>
+  <el-form :model="ruleForm" ref="ruleFormRef">
+    <el-form-item
+      label="推送对象"
+      prop="recipientType"
+      :rules="[{ required: true, message: '请选择推送对象' }]"
+    >
+      <el-radio-group v-model="ruleForm.recipientType">
+        <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"
+        >
+          <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"
+        >
+          <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"
+  >
+    <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 }"
+          :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">
+            <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>
+  </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 { 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);
+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?: number[];
+  customUserList?: UserList[];
+}>();
+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,
+    }));
+  });
+};
+const submitForm = () => {
+  return ruleFormRef.value!.validate(() => {});
+};
+const getChildValue = () => {
+  return ruleForm;
+};
+const toggleExpand = (group) => {
+  group.isExpand = !group.isExpand;
+};
+const handleCancle = () => {
+  userInfo.value = false;
+};
+const handleSubmit = (selectedData: UserList[]) => {
+  selectedUser.value = selectedData;
+  ruleForm.customUserList = selectedUser.value;
+  userInfo.value = false;
+};
+defineExpose({
+  submitForm,
+  getChildValue,
+});
+onMounted(() => {
+  ToPushObjectqueryUserGroupList().then((res) => {
+    options.value = res.groupVOList;
+  });
+});
+watchEffect(() => {
+  if (props.recipientType) {
+    ruleForm.recipientType = props.recipientType;
+  }
+  if (props.userGroupList) {
+    ruleForm.userGroupList = props.userGroupList;
+  }
+  if (props.customUserList) {
+    ruleForm.customUserList = props.customUserList;
+    selectedUser.value = props.customUserList;
+  }
+});
+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>

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

@@ -12,4 +12,17 @@ export const statisticTypeName = [
 ];
 export const messageTypeName = [
     { value: 1, 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"

+ 39 - 59
src/views/message/persongroup/components/SelectTree.vue

@@ -7,16 +7,18 @@
         :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"
       />
@@ -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>

+ 4 - 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,7 @@ export interface FormData {
     total: number;
     userGroupId: number;
     userList: FromUserList[];
+}
+export interface GroupData extends FormData {
+    isExpand: 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();

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

@@ -0,0 +1,469 @@
+<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 {
+  id: string;
+  name: string;
+  userId: number;
+}
+interface ObjectFrom {
+  recipientType?: number;
+  userGroupList?: number[];
+  customUserList?: UserList[];
+}
+interface RuleForm {
+  title: string;
+  content: string;
+  channel: number[];
+  object: ObjectFrom;
+  operator: string;
+}
+const ruleForm = reactive<RuleForm>({
+  title: '',
+  content: '',
+  channel: [],
+  object: {},
+  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>