فهرست منبع

Merge branch 'all-richText' into 'all'

系统通知发版

See merge request skyeye/skyeye_frontend/skyeye-admin!183
Fei Liu 1 سال پیش
والد
کامیت
d8448f78b9

+ 2 - 1
.env.development

@@ -19,7 +19,8 @@ VITE_DROP_CONSOLE = true
 # VITE_PROXY=[["/skyeye-admin-api","http://192.168.14.68/skyeye-admin-api"],[],["/eye_api_bak","http://192.168.14.68/eye_api"],["/push_stream_host","http://192.168.14.68/push_stream_host"],["/skyeye-login","http://192.168.14.68/skyeye-login"],["/ws_api_bak","ws://192.168.14.68/ws_api_bak"]]
 # VITE_PROXY=[["/skyeye-admin-api","http://192.168.13.68/skyeye-admin-api"],[],["/eye_api_bak","http://192.168.13.68/eye_api"],["/push_stream_host","http://192.168.13.68/push_stream_host"],["/skyeye-login","http://192.168.13.68/skyeye-login"],["/ws_api_bak","ws://192.168.13.68/ws_api_bak"]]
 # 中建材 staff
-VITE_PROXY=[["/skyeye-admin-api","http://192.168.13.68:70/skyeye-admin-api"],["/eye_api_bak","http://192.168.13.68:70/eye_api"],["/push_stream_host","http://192.168.13.68:70/push_stream_host"],["/skyeye-login","http://192.168.13.68:70/skyeye-login"],["/ws_api_bak","ws://192.168.13.68:70/ws_api_bak"]]
+# VITE_PROXY=[["/skyeye-admin-api","http://192.168.13.68:70/skyeye-admin-api"],["/eye_api_bak","http://192.168.13.68:70/eye_api"],["/push_stream_host","http://192.168.13.68:70/push_stream_host"],["/skyeye-login","http://192.168.13.68:70/skyeye-login"],["/ws_api_bak","ws://192.168.13.68:70/ws_api_bak"]]
+VITE_PROXY=[["/skyeye-admin-api","http://192.168.13.68:70/skyeye-admin-api"],["/eye_api_bak","http://192.168.22.233:8800/eye_api"],["/push_stream_host","http://192.168.13.68:70/push_stream_host"],["/skyeye-login","http://192.168.13.68:70/skyeye-login"],["/ws_api_bak","ws://192.168.13.68:70/ws_api_bak"]]
 # VITE_PROXY=[["/skyeye-admin-api","http://192.168.13.68/skyeye-admin-api"],[],["/eye_api_bak","http://192.168.13.68/eye_api"],["/push_stream_host","http://192.168.13.68/push_stream_host"],["/skyeye-login","http://192.168.13.68/skyeye-login"],["/ws_api_bak","ws://192.168.13.68/ws_api_bak"],["/skyeye-file-upload","http://192.168.13.68/skyeye-file-upload"]]
 # VITE_PROXY=[["/skyeye-admin-api","http://192.168.22.163:8800/api"],[],["/eye_api_bak","http://192.168.22.163:8800/api"],["/push_stream_host","http://192.168.13.68/push_stream_host"],["/skyeye-login","http://192.168.13.68/skyeye-login"],["/ws_api_bak","ws://192.168.13.68/ws_api_bak"]]
 #VITE_PROXY=[["/skyeye-admin-api","http://192.168.22.121:8800/api"],["/eye_api_bak","http://192.168.22.121:8800/api"],["/push_stream_host","http://192.168.13.68/push_stream_host"],["/skyeye-login","http://192.168.13.68/skyeye-login"],["/ws_api_bak","ws://192.168.13.68/ws_api_bak"],["/skyeye-file-upload","http://192.168.13.68/skyeye-file-upload"]]

+ 19 - 10
src/api/message/sysnotion-config.ts

@@ -1,14 +1,15 @@
 import { http } from '@/utils/http/axios';
-export interface queryReportConfigListParams {
-  content?: string;
-  title?: string;
-  pushChannel?: number[];
-  recipientType?: number;
-  userGroupList?: number[];
-  customUserList?: number[];
-}
-export function queryReportConfigList(params: queryReportConfigListParams) {
-  return http.request({
+import { RuleFormAdd } from '@/views/message/sysnotion-config/type';
+// export interface queryReportConfigListParams {
+//   content?: string;
+//   title?: string;
+//   pushChannel?: number[];
+//   recipientType?: number;
+//   userGroupList?: number[];
+//   customUserList?: number[];
+// }
+export function addSystemMessage(params: RuleFormAdd) {
+  return http.request<number>({
     url: '/systemMessage/addSystemMessage',
     method: 'post',
     params,
@@ -26,3 +27,11 @@ export function viewSystemMessage(id: number) {
     method: 'get',
   });
 }
+
+export function updateSystemMessage(params: RuleFormAdd) {
+  return http.request({
+    url: '/systemMessage/updateSystemMessage',
+    method: 'post',
+    params,
+  })
+}

+ 9 - 9
src/views/message/components/PushObject.vue

@@ -217,11 +217,11 @@
     userInfo.value = false;
   };
   const formatCustomUserList = (customList: customUserList[]): SelectedFilterPersonInfo[] => {
-    return customList.map((item) => ({
-      id: item.userId,
-      staffNo: item.userNumber,
-      nickname: item.userNickname,
-    }));
+     return customList.map((item) => ({
+        id: item.userId,
+        staffNo: item.userNumber,
+        nickname: item.userNickname,
+      }));
   };
   defineExpose({
     submitForm,
@@ -235,17 +235,17 @@
     });
   });
   watchEffect(() => {
-    if (props.recipientType) {
+    if (props.recipientType != null) {
       ruleForm.recipientType = props.recipientType;
     }
-    if (props.userGroupList) {
+    if (props.userGroupList != null) {
       ruleForm.userGroupList = props.userGroupList.map((item) => item.userGroupId);
     }
-    if (props.customUserList) {
+    if (props.customUserList != null) {
       ruleForm.customUserList = formatCustomUserList(props.customUserList);
       selectedUser.value = formatCustomUserList(props.customUserList);
     }
-    if (props.disabled) {
+    if (props.disabled != null) {
       disabled.value = props.disabled;
     }
   });

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

@@ -3,6 +3,7 @@ export const typeName = [
   { value: 2, label: '平台访问统计' },
   { value: 3, label: '人员访问数据' },
 ];
+
 export const statisticTypeName = [
   { value: 1, label: '周报' },
   { value: 2, label: '月报' },
@@ -10,21 +11,42 @@ export const statisticTypeName = [
   { value: 4, label: '年报' },
   { value: 5, label: '自定义' },
 ];
+
 export const messageTypeName = [
   { value: 1, label: '报表消息' },
   { value: 2, label: '报警消息' },
   { value: 3, label: '系统消息' },
 ];
+
+// 推送渠道:1-蓝信 2-平台 4-APP主页 5-PC主页
 export const pushChannelName = [
+  { value: 4, label: 'APP主页' },
+  { value: 5, label: 'PC主页' },
   { 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: '未推送' },
 ];
+
+export const messageTypeOptions = [
+  {value: 1, label: 'Banner样式'},
+  {value: 2, label: '文本样式'},
+]
+
+export const contentTypeOptinos = [
+  {value: 1, label: '富文本'},
+  {value: 2, label: '链接跳转'},
+]
+
+export const title = '本系统进行了重大升级,请查看详细内容';
+
+export const content = '尊敬的用户:\n    我们计划于2024年9月5日进行平台系统升级,以提升服务性能和用户体验,升级期间,平台将暂时不可用,预计停机时间为4小时,从上午2:00至6:00。请您提前做好相关安排,以避免不便,感谢您的理解与支持。如有疑问,请联系客服支持团队。\n敬请留意。\n天眼团队';

+ 142 - 421
src/views/message/sysnotion-config/SysnotionConfig.vue

@@ -9,233 +9,68 @@
     </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
-              :disabled="isDisabled"
-            />
-          </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
-              :disabled="isDisabled"
-            />
-          </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"
-              :disabled="isDisabled"
-            />
-          </el-form-item>
-          <PushObject
-            ref="childFromRef"
-            :recipientType="ruleForm.object.recipientType"
-            :userGroupList="ruleForm.object.userGroupList"
-            :customUserList="ruleForm.object.customUserList"
-            :disabled="isDisabled"
-          />
-          <el-form-item label="操作人" prop="operator" class="transprant">
-            <el-input v-model="ruleForm.operator" :disabled="true" />
-          </el-form-item>
-        </el-form>
+        <!-- 基本配置 -->
+        <BasicInfo ref="basicInfoRef" :data-soure="ruleForm" :is-disabled="isDisabled" />
+        <!-- 内容配置区域 -->
+        <ContentConfig ref="contentConfigRef" :data-soure="ruleForm" :is-disabled="isDisabled" />
+        <!-- 按钮区域 -->
         <div class="btns" v-if="!isDisabled">
           <div style="position: absolute; right: 0; bottom: 0">
-            <el-button @click="refresh()">重置</el-button>
-            <el-button type="primary" @click="submitForm()">确定</el-button>
+            <el-button @click="onCancel">取消</el-button>
+            <el-button @click="onSave">暂存</el-button>
+            <el-button type="primary" @click="submitForm">确定</el-button>
           </div>
         </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>
+      <!-- 实时预览 -->
+      <RightCard :rule-form="ruleForm" />
     </div>
   </div>
 </template>
 
 <script lang="ts" setup>
   import { useRoute, useRouter } from 'vue-router';
-  import { ref, reactive, onMounted } from 'vue';
+  import { ref, reactive, onMounted, computed } from 'vue';
   import { storeToRefs } from 'pinia';
-  import { debounce } from 'lodash-es';
   import { ElMessage } from 'element-plus';
   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';
   import {
-    queryReportConfigListParams,
-    queryReportConfigList,
+    addSystemMessage,
     confirmReportConfig,
+    updateSystemMessage,
     viewSystemMessage,
   } from '@/api/message/sysnotion-config';
-  import { ObjectFrom } from './type';
-  const title = ref<string>('本系统进行了重大升级,请查看详细内容');
-  const content = ref<string>(
-    '尊敬的用户:\n    我们计划于2024年9月5日进行平台系统升级,以提升服务性能和用户体验,升级期间,平台将暂时不可用,预计停机时间为4小时,从上午2:00至6:00。请您提前做好相关安排,以避免不便,感谢您的理解与支持。如有疑问,请联系客服支持团队。\n敬请留意。\n天眼团队',
-  );
+  import { RuleFormView, MessageTypeEnum, ContentTypeEnum, RuleFormAdd } from './type';
+  import BasicInfo from './compontents/BasicInfo.vue';
+  import ContentConfig from './compontents/ContentConfig.vue';
+  import RightCard from './compontents/RightCard.vue';
+
   const isDisabled = ref<boolean>(false);
-  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 RuleForm {
-    title: string;
-    content: string;
-    channel: number[];
-    object: ObjectFrom;
-    operator: string;
-  }
-  const ruleForm = reactive<RuleForm>({
+  const basicInfoRef = ref<InstanceType<typeof BasicInfo>>();
+  const contentConfigRef = ref<InstanceType<typeof ContentConfig>>();
+  const ruleForm = reactive<RuleFormView>({
+    messageType: MessageTypeEnum.BANNER,
     title: '',
+    bannerUrl: '',
+    pushChannel: [],
+    expirationTime: '',
+    recipientType: 1, // 全员
+    userGroupList: [],
+    customUserList: [],
+    introduction: '',
+    contentType: ContentTypeEnum.RICHTEXT,
     content: '',
-    channel: [],
-    object: {},
+    contentUrl: '',
     operator: info.value.nickname,
   });
-  const activeName = ref('platform');
-  const debounceEmit = debounce((params) => {
-    queryReportConfigList(params)
-      .then((res) => {
-        confirmReportConfig(res).then(() => {
-          ElMessage({
-            message: '下发成功!',
-            type: 'success',
-          });
-          router.back();
-        });
-      })
-      .catch((e) => console.error(e));
-  }, 500);
-  const submitForm = () => {
-    const childValue = childFromRef.value!.getChildValue();
-    childFromRef.value!.submitForm().then((res) => {
-      validate.value = res;
-    });
-    ruleFormRef.value!.validate((valid) => {
-      if (validate.value && valid) {
-        const params: queryReportConfigListParams = {
-          content: ruleForm.content,
-          title: ruleForm.title,
-          pushChannel: ruleForm.channel.map((item) => item),
-          recipientType: childValue.recipientType,
-          userGroupList: childValue.userGroupList,
-          customUserList: childValue.customUserList,
-        };
-        debounceEmit(params);
-      } else {
-        console.log('下发失败');
-      }
-    });
-  };
-  const refresh = () => {
-    if (!ruleFormRef.value) return;
-    ruleFormRef.value.resetFields();
-    childFromRef.value.refreshForm();
+
+  // 原始数据副本
+  let originalData: RuleFormView = {
+    ...ruleForm,
   };
+
   const router = useRouter();
   const rollback = () => {
     router.back();
@@ -246,19 +81,119 @@
     if (sysId) {
       isDisabled.value = true;
       viewSystemMessage(Number(sysId)).then((res) => {
+        if (!res?.status) {
+          isDisabled.value = false;
+        }
+        ruleForm.id = Number(sysId);
         ruleForm.title = res.title;
+        ruleForm.introduction = res.introduction ? res.introduction : '';
+        ruleForm.messageType = res.messageType;
         ruleForm.content = res.content ? res.content : ' ';
-        ruleForm.channel = res.pushChannel;
-        ruleForm.object.recipientType = res.recipientType;
+        ruleForm.pushChannel = res.pushChannel;
+        ruleForm.recipientType = res.recipientType;
+
         if (res.recipientType === 2) {
-          ruleForm.object.userGroupList = res.userGroupList;
-        }
-        if (res.recipientType === 3) {
-          ruleForm.object.customUserList = res.customUserList;
+          // 若选择分组
+          ruleForm.userGroupList = res.userGroupList;
+        } else if (res.recipientType === 3) {
+          // 若选择自定义
+          ruleForm.customUserList = res.customUserList;
         }
       });
     }
   });
+
+  // 取消
+  const onCancel = () => {
+    // 比对数据
+    const changes = compareData(ruleForm, originalData);
+    console.log('Object.keys(changes): ', Object.keys(changes));
+
+    // ElMessageBox.confirm(
+    //   '您对系统通知的额操作尚未保存,请问是否暂存?',
+    //   '提示',
+    //   {
+    //     confirmButtonText: '暂存',
+    //     cancelButtonText: '取消',
+    //     type: 'warning',
+    //   }
+    // )
+    //   .then(() => {
+    //     ElMessage({
+    //       type: 'success',
+    //       message: '暂存成功',
+    //     })
+    //   })
+    //   .catch(() => {
+    //     ElMessage({
+    //       type: 'info',
+    //       message: '取消暂存',
+    //     })
+    //   })
+  };
+
+  // 暂存
+  const onSave = async () => {
+    // to save dada
+    const baseInfoData = await basicInfoRef.value?.validate();
+    const contentConfigData = contentConfigRef.value?.buildFormdata();
+    console.log('baseInfoData', baseInfoData);
+    console.log('contentConfigData', contentConfigData);
+
+    const params: RuleFormAdd = {
+      ...baseInfoData!,
+      ...contentConfigData,
+    };
+    delete params.operator;
+
+
+    if (!sysId) {
+      addSystemMessage(params).then((res) => {
+        if (res) {
+          ruleForm.id = res;
+          ElMessage({
+            message: '暂存成功!',
+            type: 'success',
+          });
+        }
+      });
+    } else {
+      updateSystemMessage(params).then(() => {
+        ElMessage({
+            message: '编辑成功!',
+            type: 'success',
+        });
+      });
+    }
+  };
+
+  const submitForm = () => {
+    if (!ruleForm.id) {
+      ElMessage({
+        message: '暂存后才能下发!',
+        type: 'warning',
+      });
+      return;
+    }
+    confirmReportConfig(ruleForm.id).then(() => {
+      ElMessage({
+        message: '下发成功!',
+        type: 'success',
+      });
+      router.back();
+    });
+  };
+
+  // 比对方法
+  const compareData = (newData: RuleFormView, oldData: RuleFormView) => {
+    const diff: Partial<Omit<RuleFormView, 'content'>> = {};
+    for (const key in newData) {
+      if (newData[key as keyof RuleFormView] !== oldData[key as keyof RuleFormView]) {
+        diff[key as keyof RuleFormView] = newData[key as keyof RuleFormView];
+      }
+    }
+    return diff;
+  };
 </script>
 
 <style lang="scss" scoped>
@@ -318,220 +253,6 @@
           position: relative;
         }
       }
-      .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;
-          overflow-y: auto;
-          :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;
-                align-items: center;
-                margin-bottom: 12px;
-                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 {
-                    font-size: 14px;
-                    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 - 55px);
-                  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;
-                align-items: center;
-                margin-bottom: 12px;
-                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 - 50px);
-                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>

+ 209 - 0
src/views/message/sysnotion-config/compontents/BasicInfo.vue

@@ -0,0 +1,209 @@
+<!--
+ * @since: 2024-12-30
+ * BasicInfo.vue
+-->
+<template>
+  <CardLayout title="基础配置" :isShowWraning="false" :mandatory="false">
+    <el-form
+      ref="ruleFormRef"
+      style="max-width: 600px"
+      label-width="auto"
+      :model="ruleForm"
+      :rules="formRules"
+      :label-position="labelPosition"
+      class="el-form-outer"
+    >
+      <el-form-item label="消息样式: " prop="messageType">
+        <el-radio-group v-model="ruleForm.messageType" :disabled="isDisabled">
+          <el-radio :value="item.value" v-for="item in messageTypeOptions" :key="item.value"
+            >{{ item.label }}
+          </el-radio>
+        </el-radio-group>
+      </el-form-item>
+      <el-form-item label="消息标题: " prop="title">
+        <el-input
+          v-model="ruleForm.title"
+          placeholder="请输入20字以内的消息标题"
+          maxlength="20"
+          show-word-limit
+          :disabled="isDisabled"
+        />
+      </el-form-item>
+      <el-form-item
+        label="Banner图片: "
+        prop="bannerUrl"
+        v-if="ruleForm.messageType === MessageTypeEnum.BANNER"
+      >
+        <el-upload
+          class="avatar-uploader"
+          :action="actionUrl"
+          :show-file-list="false"
+          :on-success="handleAvatarSuccess"
+          :before-upload="beforeAvatarUpload"
+        >
+          <img v-if="ruleForm.bannerUrl" :src="ruleForm.bannerUrl" class="avatar" />
+          <el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>
+        </el-upload>
+      </el-form-item>
+      <el-form-item label="推送渠道: " prop="pushChannel">
+        <el-checkbox-group v-model="ruleForm.pushChannel">
+          <el-checkbox
+            v-for="item in pushChannelName"
+            :key="item.value"
+            :value="item.value"
+            :label="item.label"
+            :disabled="isDisabled"
+          />
+        </el-checkbox-group>
+      </el-form-item>
+      <el-form-item label="失效时间: " prop="expirationTime">
+        <el-date-picker
+          v-model="ruleForm.expirationTime"
+          type="datetime"
+          placeholder="请选择失效时间"
+          format="YYYY/MM/DD hh:mm:ss"
+          value-format="YYYY-MM-DD hh:mm:ss"
+          :disabled="isDisabled"
+        />
+      </el-form-item>
+      <PushObject
+        ref="childFromRef"
+        :recipientType="ruleForm.recipientType"
+        :userGroupList="ruleForm.userGroupList"
+        :customUserList="ruleForm.customUserList"
+        :disabled="isDisabled"
+      />
+    </el-form>
+  </CardLayout>
+</template>
+
+<script setup lang="ts">
+  import { ref, watch, computed, unref } from 'vue';
+  import type { FormInstance, FormProps, FormRules, UploadProps } from 'element-plus';
+  import { ElMessage } from 'element-plus';
+  import { Plus } from '@element-plus/icons-vue';
+  import PushObject from '../../components/PushObject.vue';
+  import CardLayout from './CardLayout.vue';
+  import { RuleFormView, MessageTypeEnum, RuleFormAdd } from '../type';
+  import { messageTypeOptions, pushChannelName } from '../../constant';
+  import urlJoin from 'url-join';
+  import { useGlobSetting } from '@/hooks/setting';
+  const { urlPrefix } = useGlobSetting();
+  interface Props {
+    dataSoure: RuleFormView;
+    isDisabled: boolean;
+  }
+  const props = defineProps<Props>();
+  const labelPosition = ref<FormProps['labelPosition']>('left');
+  const childFromRef = ref<InstanceType<typeof PushObject>>();
+  const actionUrl = computed(() => {
+    return urlJoin(urlPrefix!, `/skyeye-file-upload/upload`);
+  });
+
+  /**
+   * 表单相关操作
+   */
+  type Rule = Omit<RuleFormView, 'introduction' | 'contentType' | 'content' | 'contentUrl' | 'operator'>;
+  const formRules: FormRules<Rule> = {
+    messageType: [{ required: true, trigger: 'change', message: '请选择消息样式' }],
+    title: [{ required: true, trigger: 'change', message: '请输入消息标题' }],
+    pushChannel: [{ required: true, trigger: 'change', message: '请选择推送渠道' }],
+    recipientType: [{ required: true, trigger: 'change', message: '请选择推送对象' }],
+    bannerUrl: [{ required: true, trigger: 'change', message: '请选择banner图片' }],
+  };
+  const ruleForm = ref<Rule>({
+    messageType: MessageTypeEnum.BANNER,
+    title: '',
+    bannerUrl:'',
+    pushChannel: [],
+    expirationTime: '',
+    recipientType: 1, // 默认全员
+    userGroupList:[],
+    customUserList: [],
+  });
+
+  watch(
+    () => props.dataSoure,
+    (value) => {
+      if (value) {
+        ruleForm.value = { ...value };
+      }
+    },
+    {
+      immediate: true,
+      deep: true,
+    },
+  );
+
+  const ruleFormRef = ref<FormInstance>();
+  const isValidate = ref<boolean>();
+  const validate = async () => {
+    if (!ruleFormRef.value) return;
+    try {
+      const isSuccess = await ruleFormRef.value.validate();
+      if (isSuccess) {
+        const childValue = childFromRef.value!.getChildValue();
+        childFromRef.value!.submitForm().then((res) => {
+          isValidate.value = res;
+        });
+
+        const basicInfo = { ...unref(ruleForm) } as RuleFormAdd;
+
+        if (childValue) {
+          basicInfo.recipientType = childValue.recipientType!;
+          basicInfo.userGroupList = childValue.userGroupList!;
+          basicInfo.customUserList = childValue.customUserList!;
+        } 
+        return basicInfo;
+      }
+    } catch (error) {
+      ElMessage.error('请完善信息填写');
+      throw error;
+    }
+  };
+
+  const handleAvatarSuccess: UploadProps['onSuccess'] = (response, uploadFile) => {
+    ruleForm.value.bannerUrl = URL.createObjectURL(uploadFile.raw!);
+  };
+
+  const beforeAvatarUpload: UploadProps['beforeUpload'] = (rawFile) => {
+    if (!['image/jpeg', 'image/png'].includes(rawFile.type)) {
+      ElMessage.error('Avatar picture must be JPG format!');
+      return false;
+    } else if (rawFile.size / 1024 / 1024 > 5) {
+      ElMessage.error('Avatar picture size can not exceed 2MB!');
+      return false;
+    }
+    return true;
+  };
+
+  defineExpose({ validate });
+</script>
+
+<style scoped lang="scss">
+.avatar-uploader .avatar {
+  width: 58px;
+  height: 58px;
+  display: block;
+}
+.avatar-uploader .el-upload {
+  border: 1px dashed var(--el-border-color);
+  border-radius: 6px;
+  cursor: pointer;
+  position: relative;
+  overflow: hidden;
+  transition: var(--el-transition-duration-fast);
+}
+
+.avatar-uploader .el-upload:hover {
+  border-color: var(--el-color-primary);
+}
+
+.el-icon.avatar-uploader-icon {
+  font-size: 28px;
+  color: #8c939d;
+  width: 58px;
+  height: 58px;
+  text-align: center;
+}
+</style>

+ 92 - 0
src/views/message/sysnotion-config/compontents/CardLayout.vue

@@ -0,0 +1,92 @@
+<!--
+ * @since: 2024-12-30
+ * CardLayout.vue
+-->
+<template>
+  <div class="layout">
+    <section class="header" v-if="title">
+      <header class="card-title">
+        {{ title }}
+        <span class="mandatory" v-if="mandatory">*</span>
+      </header>
+      <slot name="actions"></slot>
+    </section>
+    <slot></slot>
+
+    <div class="flex items-center cursor-pointer new-action w-[70px]" v-if="!isViewMode && showAddNewButton" @click="$emit('handleAddNew')">
+      <el-button type="primary" :icon="Plus" circle size="small" />
+      <span style="margin-left: 10px; padding-top: 2px">新增</span>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { inject } from 'vue';
+  import { Plus } from '@element-plus/icons-vue';
+
+  interface LayoutSetting {
+    title?: string;
+    mandatory?: boolean;
+    showAddNewButton?: boolean;
+    isShowWraning?: boolean;
+  }
+
+  withDefaults(defineProps<LayoutSetting>(), {
+    mandatory: false,
+    showAddNewButton: false,
+    isShowWraning: false,
+  });
+
+  const isViewMode = inject('isViewMode', false);
+
+  defineEmits<{
+    (event: 'handleAddNew'): void;
+  }>();
+</script>
+
+<style scoped lang="scss">
+  .layout {
+    margin-bottom: 20px;
+    border-bottom: 4px solid #d3eafe;
+  }
+
+  .layout:last-child {
+    border: none;
+  }
+
+  .header {
+    margin-bottom: 20px;
+    padding-right: 20px;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+
+    .card-title {
+      height: 20px;
+      line-height: 20px;
+      font-size: 16px;
+      font-weight: bold;
+
+      &::before {
+        content: ' ';
+        display: inline;
+        padding: 2px;
+        margin-right: 10px;
+        background-color: #409eff;
+        height: 80%;
+      }
+    }
+  }
+
+  .mandatory {
+    color: var(--el-color-danger);
+  }
+
+  .new-action {
+    margin-bottom: 20px;
+  }
+
+  .warningTip {
+    margin-left: 10px;
+  }
+</style>

+ 240 - 0
src/views/message/sysnotion-config/compontents/ContentConfig.vue

@@ -0,0 +1,240 @@
+<!--
+ * @since: 2024-12-30
+ * ContentConfig.vue
+-->
+<template>
+  <div class="container"></div>
+  <card-layout title="内容配置" :isShowWraning="false" :mandatory="false">
+    <el-form
+      ref="ruleFormRef"
+      label-width="auto"
+      :model="ruleForm"
+      :label-position="labelPosition"
+      class="el-form-outer"
+    >
+      <el-form-item label="简介内容" prop="introduction" class="transprant">
+        <el-input
+          v-model="ruleForm.introduction"
+          placeholder="请输入500字以内的简介内容"
+          type="textarea"
+          :rows="5"
+          maxlength="500"
+          show-word-limit
+          :disabled="isDisabled"
+        />
+      </el-form-item>
+
+      <el-form-item label="详情内容" prop="contentType" class="transprant">
+        <el-radio-group v-model="ruleForm.contentType" :disabled="isDisabled">
+          <el-radio :value="item.value" v-for="item in contentTypeOptinos" :key="item.value">{{
+            item.label
+          }}</el-radio>
+        </el-radio-group>
+      </el-form-item>
+      <el-form-item
+        label=" "
+        prop="contentUrl"
+        class="transprant"
+        v-if="ruleForm.contentType === ContentTypeEnum.LINK"
+      >
+        <el-input
+          v-model="ruleForm.contentUrl"
+          placeholder="请将链接地址粘贴到此处"
+          :disabled="isDisabled"
+        />
+      </el-form-item>
+      <el-form-item
+        label="  "
+        prop="content"
+        v-if="ruleForm.contentType === ContentTypeEnum.RICHTEXT"
+      >
+        <div style="border: 1px solid #ccc">
+          <Toolbar
+            style="border-bottom: 1px solid #ccc"
+            :editor="editorRef"
+            :defaultConfig="toolbarConfig"
+          />
+          <Editor
+            style="height: 500px; overflow-y: hidden"
+            v-model="valueHtml"
+            :defaultConfig="editorConfig"
+            @onCreated="handleCreated"
+            @onChange="handleChange"
+            @onDestroyed="handleDestroyed"
+            @onFocus="handleFocus"
+            @onBlur="handleBlur"
+          />
+        </div>
+      </el-form-item>
+      <el-form-item label="操作人" prop="operator" class="transprant">
+        <el-input v-model="ruleForm.operator" :disabled="true" />
+      </el-form-item>
+    </el-form>
+  </card-layout>
+</template>
+
+<script setup lang="ts">
+  import { onBeforeUnmount, ref, shallowRef, watch } from 'vue';
+  import type { FormProps } from 'element-plus';
+  import '@wangeditor/editor/dist/css/style.css'; // 引入 css
+  import { Editor, Toolbar } from '@wangeditor/editor-for-vue';
+  import type { IEditorConfig } from '@wangeditor/editor';
+  import { RuleFormView, ContentTypeEnum } from './../type';
+  import { contentTypeOptinos } from '../../constant';
+  import CardLayout from './CardLayout.vue';
+  import { useUserStore } from '@/store/modules/user';
+
+  interface Props {
+    dataSoure: RuleFormView;
+    isDisabled: boolean;
+  }
+  const props = defineProps<Props>();
+  const labelPosition = ref<FormProps['labelPosition']>('left');
+  const userStore = useUserStore();
+
+  /**
+   * 表单相关操作
+   */
+  type Rule = Pick<RuleFormView, 'introduction' | 'contentType' | 'content' | 'operator' | 'contentUrl'>;
+  const ruleForm = ref<Rule>({
+    introduction: '',
+    contentType: ContentTypeEnum.RICHTEXT,
+    content: '',
+    operator: '',
+    contentUrl: '',
+  });
+
+  watch(
+    () => props.dataSoure,
+    (value) => {
+      if (value) {
+        const { introduction, contentType, content, operator, contentUrl } = value;
+        ruleForm.value = {
+          introduction,
+          contentType,
+          content,
+          operator,
+          contentUrl,
+        };
+        if (content) {
+          valueHtml.value = content;
+        }
+      }
+    },
+    {
+      immediate: true,
+      deep: true,
+    },
+  );
+
+  /********************* 富文本区域配置与方法 **********************/
+  // 编辑器实例,必须用 shallowRef
+  const editorRef = shallowRef();
+  // 内容 HTML
+  const valueHtml = ref();
+  // 排除工具栏选项
+  const toolbarConfig = {
+    excludeKeys: [
+      'insertTable', // 插入表格
+      'deleteTable', // 删除表格
+      'insertVideo',
+      'uploadVideo',
+      'codeBlock', // 代码块
+      'emotion', // 表情
+    ],
+  };
+  const editorConfig: Partial<IEditorConfig> = {
+    placeholder: '请输入内容...',
+    MENU_CONF: {},
+  };
+  
+  editorConfig.MENU_CONF!['uploadImage'] = {
+    // 上传图片的配置
+    server: '/eye_api_bak/api/systemMessage/uploadPicture', // form-data fieldName ,默认值 'wangeditor-uploaded-image'
+    fieldName: 'file',
+    // 单个文件的最大体积限制,默认为 2M
+    maxFileSize: 5 * 1024 * 1024, // 1M
+    // 最多可上传几个文件,默认为 100
+    maxNumberOfFiles: 10,
+    // 选择文件时的类型限制,默认为 ['image/*'] 。如不想限制,则设置为 []
+    allowedFileTypes: ['image/*'],
+    // 自定义上传参数,例如传递验证的 token 等。参数会被添加到 formData 中,一起上传到服务端。
+    meta: {
+      // type: 1,
+      // targetId: 5,
+      bizType: 'ALGO',
+    },
+    // 将 meta 拼接到 url 参数中,默认 false
+    metaWithUrl: false,
+    // 自定义增加 http  header
+    headers: {
+      Accept: 'application/json, text/plain, */*',
+      // ContentType: 'application/json;charset=UTF-8'
+      Satoken: userStore.getToken,
+      Tenantid: userStore.getTenantId,
+    },
+    // 跨域是否传递 cookie ,默认为 false
+    withCredentials: true,
+    // 超时时间,默认为 10 秒
+    timeout: 10 * 1000, // 5 秒
+    // 上传之前触发
+    onBeforeUpload(file) {
+      console.log('file: ', file); 
+      return file;
+      // 可以 return
+      // 1. return file 或者 new 一个 file ,接下来将上传
+      // 2. return false ,不上传这个 file
+    },
+    // 上传进度的回调函数
+    onProgress(progress: number) {
+      // progress 是 0-100 的数字
+      console.log('progress', progress);
+    },
+    // 单个文件上传成功之后
+    onSuccess(file: File, res: any) {
+      console.log(`${file.name} 上传成功`, res);
+    },
+    // 单个文件上传失败
+    onFailed(file: File, res: any) {
+      console.log(`${file.name} 上传失败`, res);
+    },
+    // 上传错误,或者触发 timeout 超时
+    onError(file: File, err: any, res: any) {
+      console.log(`${file.name} 上传出错`, err, res);
+    },
+  };
+
+  // 执行 createEditor 组件销毁时,也及时销毁编辑器
+  onBeforeUnmount(() => {
+    const editor = editorRef.value;
+    if (editor == null) return;
+    editor.destroy();
+  });
+
+  const handleCreated = (editor) => {
+    editorRef.value = editor;
+  };
+  const handleChange = (editor) => {
+    // console.log('change:', editor.children)
+  };
+  const handleDestroyed = (editor) => {
+    console.log('destroyed', editor);
+  };
+  const handleFocus = (editor) => {
+    // console.log('focus', editor)
+  };
+  const handleBlur = (editor) => {
+    console.log('blur', editor);
+  };
+
+  const buildFormdata = () => {
+    return {
+      ...ruleForm.value,
+      content: valueHtml.value,
+    };
+  };
+
+  defineExpose({ buildFormdata });
+</script>
+
+<style scoped lang="scss"></style>

+ 311 - 0
src/views/message/sysnotion-config/compontents/RightCard.vue

@@ -0,0 +1,311 @@
+<!--
+ * @since: 2024-12-30
+ * RightCard.vue
+-->
+<template>
+   <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.introductio || 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.introductio || content }}</span>
+                  </div>
+                </div>
+              </div>
+            </el-tab-pane>
+          </el-tabs>
+        </div>
+      </div>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive, onMounted } from 'vue';
+import { RuleFormProps } from '../type';
+import { title, content } from '../../constant';
+
+const activeName = ref('platform');
+interface Props {
+  ruleForm: RuleFormProps
+}
+defineProps<Props>()
+</script>
+
+<style scoped lang="scss">
+.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;
+          overflow-y: auto;
+          :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;
+                align-items: center;
+                margin-bottom: 12px;
+                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 {
+                    font-size: 14px;
+                    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 - 55px);
+                  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;
+                align-items: center;
+                margin-bottom: 12px;
+                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 - 50px);
+                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>

+ 75 - 14
src/views/message/sysnotion-config/type.ts

@@ -1,21 +1,82 @@
+import { Group } from "konva/lib/Group";
+
 export interface UserList {
-    userId: number;
-    userLoginName: string;
-    userNickname: string;
-    userNumber: string;
+  userId: number;
+  userLoginName: string;
+  userNickname: string;
+  userNumber: string;
 }
 
 export interface GroupList {
-    userGroupId: number;
-    total: number;
-    operatorName: string;
-    operationTime: string;
-    name: string;
-    description: string;
+  userGroupId: number;
+  total: number;
+  operatorName: string;
+  operationTime: string;
+  name: string;
+  description: string;
 }
 
 export interface ObjectFrom {
-    recipientType?: number;
-    userGroupList?: GroupList[];
-    customUserList?: UserList[];
-}
+  recipientType?: number;
+  userGroupList?: GroupList[];
+  customUserList?: UserList[];
+}
+
+export interface RuleFormBase {
+  id?: number;
+  /**
+   * 基础信息配置
+   */
+  // 消息样式
+  messageType: MessageTypeEnum;
+  // 消息标题
+  title: string;
+  // 如果消息类型选择 banner,需提供上传banner图片
+  bannerUrl?: string;
+  // 推送渠道
+  pushChannel: number[];
+  // 失效时间
+  expirationTime: string;
+  // 推送对象 -- 类型 
+  recipientType: number;
+
+  /**
+   * 内容配置部分
+   */
+  // 内容简介
+  introduction?: string;
+  // 内容详情类型
+  contentType?: ContentTypeEnum;
+  // 内容详情内容,如果填写富文本(二选一)
+  content?: string;
+  // 内容详情内容,如果填写链接(二选一)
+  contentUrl?: string;
+  // 操作人
+  operator?: string;
+}
+
+export interface RuleFormView extends RuleFormBase {
+  // 推送对象,如果选择 分组类型
+  userGroupList?: GroupList[];
+  // 推送对象,如果选择 自定义类型
+  customUserList?: UserList[];
+}
+
+
+export interface RuleFormAdd extends RuleFormBase {
+  // 推送对象,如果选择 分组类型
+  userGroupList?: number[];
+  // 推送对象,如果选择 自定义类型
+  customUserList?: number[];
+}
+
+
+export enum ContentTypeEnum {
+  RICHTEXT = 1,
+  LINK = 2,
+}
+
+export enum MessageTypeEnum {
+  BANNER = 1,
+  TEXT = 2,
+}

+ 10 - 17
src/views/message/systemNotifications/components/systemNotificationTable.vue

@@ -13,13 +13,11 @@
     :data="sysNotionData"
     stripe
     height="calc(100vh - 400px)"
-    :cell-style="{ textAlign: 'center' }"
-    :header-cell-style="{ 'text-align': 'center' }"
     style="width: 100%; margin-top: 16px; --el-table-border-color: none"
   >
-    <el-table-column width="380" prop="title" label="消息标题" />
+    <el-table-column prop="title" label="消息标题" />
 
-    <el-table-column width="200" prop="pushChannel" label="推送渠道">
+    <el-table-column width="300" prop="pushChannel" label="推送渠道">
       <template #default="scope">
         <div>
           {{ scope.row.pushChannel.map((channel) => pushChannelMapping[channel]).join(',') }}
@@ -37,22 +35,20 @@
 
     <el-table-column width="135" prop="status" label="推送状态">
       <template #default="scope">
-        <div class="pushstatus" v-if="scope.row.status === 0"
-          ><img src="../img//prepareToPush.png" alt=""
-        /></div>
-        <div class="pushstatus" v-else><img src="../img/completed.png" alt="" /></div>
+        <img src="../img//prepareToPush.png" alt="" v-if="scope.row.status === 0" />
+        <img src="../img/completed.png" alt="" v-else />
       </template>
     </el-table-column>
 
-    <el-table-column prop="pushAt" label="推送时间" />
+    <el-table-column prop="pushAt" label="推送时间" width="200px" />
 
-    <el-table-column label="操作" fixed="right">
+    <el-table-column label="操作" fixed="right" width="120px">
       <template #default="scope">
         <div class="operation">
-          <el-tooltip class="box-item" effect="light" content="查看" placement="bottom">
+          <el-tooltip effect="light" content="查看" placement="bottom">
             <img src="../img/view.png" @click="handleView(scope.row.id)" />
           </el-tooltip>
-          <el-tooltip class="box-item" effect="light" content="删除" placement="bottom">
+          <el-tooltip effect="light" content="删除" placement="bottom">
             <img src="../img/delete.png" @click="handleDelete(scope.row.id)" />
           </el-tooltip>
         </div>
@@ -179,11 +175,8 @@
 
   .operation {
     display: flex;
-    justify-content: center;
-    /* :first-child {
-      margin-right: 20px;
-    } */
-    :nth-child(2) {
+    
+    img + img {
       margin-left: 20px;
     }
   }

+ 4 - 0
src/views/message/systemNotifications/type.ts

@@ -15,11 +15,15 @@ export interface systemTableData {
 export enum pushChannelEnum {
   platform = 2,
   lanxin = 1,
+  app = 4,
+  pc = 5,
 }
 
 export const pushChannelMapping = {
   [pushChannelEnum.platform]: '平台',
   [pushChannelEnum.lanxin]: '蓝信',
+  [pushChannelEnum.app]: 'APP主页',
+  [pushChannelEnum.pc]: 'PC主页',
 };
 
 export enum recipientTypeEnum {

+ 1 - 1
utils/devProxy/local/proxy.ts

@@ -3,7 +3,7 @@ import path from 'path';
 
 // 连后端本机时的代理staff环境
 const proxyStaff: PROXY_TYPE = {
-  serverHost: 'http://192.168.13.68/eye_api_bak/',
+  serverHost: 'http://192.168.22.233:8800/',
   loginHost: 'http://192.168.13.68/skyeye-login/',
   skyeyeFileUploadHost: 'http://192.168.13.68/skyeye-file-upload',
   nvrDownload: 'http://192.168.13.68/nvr_download',