diff --git a/stdiet-admin/src/main/java/com/stdiet/web/controller/custom/WebSocketController.java b/stdiet-admin/src/main/java/com/stdiet/web/controller/custom/WebSocketController.java
index 4ae0335ec..4c2ec561d 100644
--- a/stdiet-admin/src/main/java/com/stdiet/web/controller/custom/WebSocketController.java
+++ b/stdiet-admin/src/main/java/com/stdiet/web/controller/custom/WebSocketController.java
@@ -2,8 +2,8 @@ package com.stdiet.web.controller.custom;
 
 import com.alibaba.fastjson.JSONObject;
 import com.stdiet.common.core.controller.BaseController;
-import com.stdiet.custom.utils.WsUtils;
 import com.stdiet.custom.server.WebSocketServer;
+import com.stdiet.custom.utils.WsUtils;
 import org.springframework.scheduling.annotation.EnableScheduling;
 import org.springframework.scheduling.annotation.Scheduled;
 import org.springframework.stereotype.Controller;
@@ -38,16 +38,16 @@ public class WebSocketController extends BaseController {
 
     }
 
-//    @Scheduled(fixedRate = 30000)
-//    public void boardCast() {
-//        try {
-//            JSONObject heartBeat = new JSONObject();
-//            heartBeat.put("type", WsUtils.WS_TYPE_HEART_BEAT);
-//            heartBeat.put("msg", "ping");
-//
-//            WebSocketServer.sendInfo(heartBeat.toJSONString(), null);
-//        } catch (IOException e) {
-//            e.printStackTrace();
-//        }
-//    }
+    @Scheduled(fixedRate = 1800000)
+    public void boardCast() {
+        try {
+            JSONObject heartBeat = new JSONObject();
+            heartBeat.put("type", WsUtils.WS_TYPE_SYSTEM_MESSAGE_CLEAN);
+            heartBeat.put("msg", "clean");
+
+            WebSocketServer.sendInfo(heartBeat.toJSONString(), null);
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+    }
 }
diff --git a/stdiet-custom/src/main/java/com/stdiet/custom/utils/WsUtils.java b/stdiet-custom/src/main/java/com/stdiet/custom/utils/WsUtils.java
index a1b2286bf..fb76c3526 100644
--- a/stdiet-custom/src/main/java/com/stdiet/custom/utils/WsUtils.java
+++ b/stdiet-custom/src/main/java/com/stdiet/custom/utils/WsUtils.java
@@ -6,6 +6,8 @@ public class WsUtils {
 
     public static final String WS_TYPE_SYSTEM_MESSAGE = "WS_TYPE_SYSTEM_MESSAGE";
 
+    public static final String WS_TYPE_SYSTEM_MESSAGE_CLEAN = "WS_TYPE_SYSTEM_MESSAGE_CLEAN";
+
     public static final String WS_TYPE_MESSAGE_COUNT = "WS_TYPE_MESSAGE_COUNT";
 
     public static final String WS_TYPE_NEW_CUSTOMER_REPLY = "WS_TYPE_NEW_CUSTOMER_REPLY";
diff --git a/stdiet-ui/src/components/RecipesPlanDrawer/index.vue b/stdiet-ui/src/components/RecipesPlanDrawer/index.vue
index 60e7b60b1..c024ae6e4 100644
--- a/stdiet-ui/src/components/RecipesPlanDrawer/index.vue
+++ b/stdiet-ui/src/components/RecipesPlanDrawer/index.vue
@@ -192,7 +192,7 @@ export default {
   },
   methods: {
     showDrawer(data) {
-      // console.log(data);
+      console.log(data);
       this.data = data;
       if (!this.data) {
         return;
diff --git a/stdiet-ui/src/store/modules/message.js b/stdiet-ui/src/store/modules/message.js
index e3643e1c5..168bf4cbb 100644
--- a/stdiet-ui/src/store/modules/message.js
+++ b/stdiet-ui/src/store/modules/message.js
@@ -1,5 +1,9 @@
 import { getCustomerPhysicalSignsByCusId } from "@/api/custom/customer";
 import { dealHealthy } from "@/utils/healthyData";
+import {
+  listRecipesPlanByCusId,
+  updateRecipesPlan
+} from "@/api/custom/recipesPlan";
 
 import {
   fetchTopicList,
@@ -16,7 +20,10 @@ const oriState = {
   healthyData: {},
   healthDataLoading: false,
   healthyDataType: 0,
-  avoidFoodIds: []
+  avoidFoodIds: [],
+  //
+  planList: [],
+  planListLoading: false
 };
 
 const mutations = {
@@ -71,12 +78,17 @@ const actions = {
   },
   async fetchTopicDetailActions({ commit, dispatch, state }, payload) {
     const { topicId, id, uid } = payload;
-    const { healthyData } = state;
+    const { healthyData, planList } = state;
     commit("save", { selTopicId: topicId });
     // 客户信息
     if (healthyData.customerId !== parseInt(uid)) {
       dispatch("getHealthyData", { cusId: uid, callback: payload.callback });
     }
+    // 食谱计划
+    if (!planList.length || planList[0].cusId !== parseInt(uid)) {
+      dispatch("getRecipesPlanActions", { cusId: uid });
+    }
+
     //
     const result = await fetchTopicDetail({ topicId, id });
     if (result.code === 200) {
@@ -130,13 +142,39 @@ const actions = {
           obj => obj.id
         );
       }
-    } else {
-      throw new Error(healthyDataResult.msg);
     }
     commit("save", {
       healthDataLoading: false,
       ...newState
     });
+  },
+  async getRecipesPlanActions({ commit }, payload) {
+    commit("save", { planListLoading: true, planList: [] });
+    const result = await listRecipesPlanByCusId(payload.cusId);
+    let planList = [];
+    if (result.code === 200) {
+      planList = result.data;
+    }
+    commit("save", {
+      planList,
+      planListLoading: false
+    });
+  },
+  async updateRecipesPlanActions({ commit, state }, payload) {
+    const { id, sendFlag, callback } = payload;
+    const { planList } = state;
+    const result = await updateRecipesPlan({ id, sendFlag });
+    if (result.code === 200) {
+      callback && callback("success", result.msg);
+      const newPlanList = JSON.parse(JSON.stringify(planList));
+      const tarPlan = newPlanList.find(obj => obj.id === id);
+      if (tarPlan) {
+        tarPlan.sendFlag = sendFlag;
+      }
+      commit("save", {
+        planList: newPlanList
+      });
+    }
   }
 };
 
diff --git a/stdiet-ui/src/views/custom/message/messageBrowser/Comment.vue b/stdiet-ui/src/views/custom/message/messageBrowser/Comment.vue
index 94a390e71..e60aaa6f4 100644
--- a/stdiet-ui/src/views/custom/message/messageBrowser/Comment.vue
+++ b/stdiet-ui/src/views/custom/message/messageBrowser/Comment.vue
@@ -31,6 +31,7 @@ export default {
         dietician: "主任营养师",
         after_sale: "售后营养师",
         dietician_assistant: "营养师助理",
+        manager: "总经理",
       },
     };
   },
diff --git a/stdiet-ui/src/views/custom/message/messageBrowser/index.vue b/stdiet-ui/src/views/custom/message/messageBrowser/index.vue
index dac7a0d1b..f90b80429 100644
--- a/stdiet-ui/src/views/custom/message/messageBrowser/index.vue
+++ b/stdiet-ui/src/views/custom/message/messageBrowser/index.vue
@@ -353,6 +353,13 @@ export default {
       .topic_detail_title {
         display: flex;
         cursor: pointer;
+
+        & > :nth-child(1) {
+          flex: 0 0 40px;
+        }
+        & > :nth-child(2) {
+          flex: 1 0 0;
+        }
       }
 
       .comment_reply_item {
diff --git a/stdiet-ui/src/views/custom/message/userInfo/index.vue b/stdiet-ui/src/views/custom/message/userInfo/index.vue
index ba447aeb3..304299003 100644
--- a/stdiet-ui/src/views/custom/message/userInfo/index.vue
+++ b/stdiet-ui/src/views/custom/message/userInfo/index.vue
@@ -1,38 +1,61 @@
 <template>
-  <div v-loading="healthDataLoading">
-    <HealthyView
-      dev
-      :data="healthyDataType === 0 ? healthyData : {}"
-      v-show="healthyDataType === 0"
-    />
-    <BodySignView
-      dev
-      :data="healthyDataType === 1 ? healthyData : {}"
-      v-show="healthyDataType === 1"
-    />
-  </div>
+  <el-tabs v-model="activeName">
+    <el-tab-pane label="客户信息" name="health">
+      <div
+        v-loading="healthDataLoading"
+        :style="{ height: getTabContentHeight(), overflow: 'auto' }"
+      >
+        <HealthyView
+          dev
+          :data="healthyDataType === 0 ? healthyData : {}"
+          v-show="healthyDataType === 0"
+        />
+        <BodySignView
+          dev
+          :data="healthyDataType === 1 ? healthyData : {}"
+          v-show="healthyDataType === 1"
+        />
+      </div>
+    </el-tab-pane>
+    <el-tab-pane label="食谱计划" name="plan">
+      <div :style="{ height: getTabContentHeight(), overflow: 'auto' }">
+        <RecipesPlan />
+      </div>
+    </el-tab-pane>
+  </el-tabs>
 </template>
 <script>
 import { createNamespacedHelpers } from "vuex";
 import HealthyView from "@/components/HealthyView";
 import BodySignView from "@/components/BodySignView";
-const {
-  mapActions,
-  mapState,
-  mapMutations,
-  mapGetters,
-} = createNamespacedHelpers("message");
+import RecipesPlan from "./recipesPlan";
+const { mapActions, mapState, mapMutations, mapGetters } =
+  createNamespacedHelpers("message");
 export default {
   name: "SignUserInfo",
   components: {
     HealthyView,
     BodySignView,
+    RecipesPlan,
   },
   data() {
-    return {};
+    return {
+      activeName: "health",
+    };
+  },
+  methods: {
+    getTabContentHeight() {
+      const tabPanelElm = document.querySelector(".el-tabs");
+      if (tabPanelElm) {
+        return `${tabPanelElm.clientHeight - 68}px`;
+      }
+      return "";
+    },
   },
   computed: {
     ...mapState(["healthyData", "healthyDataType", "healthDataLoading"]),
   },
 };
 </script>
+<style lang="scss" scoped>
+</style>
diff --git a/stdiet-ui/src/views/custom/message/userInfo/recipesPlan.vue b/stdiet-ui/src/views/custom/message/userInfo/recipesPlan.vue
new file mode 100644
index 000000000..1a4bfc80b
--- /dev/null
+++ b/stdiet-ui/src/views/custom/message/userInfo/recipesPlan.vue
@@ -0,0 +1,220 @@
+<template>
+  <div class="recipes_plan_wrapper">
+    <div class="header">
+      <section>
+        <el-button
+          v-if="cusOutId"
+          type="primary"
+          icon="el-icon-share"
+          size="mini"
+          class="copyBtn"
+          :data-clipboard-text="copyValue"
+          @click="handleOnRecipesLinkClick"
+          >食谱链接
+        </el-button>
+        <el-popover
+          placement="top"
+          trigger="click"
+          v-if="cusOutId"
+          style="margin: 0 12px"
+        >
+          <VueQr :text="copyValue" :logoSrc="logo" :size="256" />
+          <el-button
+            slot="reference"
+            size="mini"
+            icon="el-icon-picture-outline"
+            type="primary"
+            @click="handleCopy(scope.row.path)"
+            >二维码</el-button
+          >
+        </el-popover>
+        <!-- <el-button icon="el-icon-view" size="mini" @click="handleInnerOpen"
+          >查看暂停记录
+        </el-button> -->
+      </section>
+      <section>
+        <el-button
+          icon="el-icon-refresh"
+          size="mini"
+          @click="getRecipesPlanActions({ cusId })"
+          circle
+        />
+      </section>
+    </div>
+
+    <el-table :data="planList" v-loading="planListLoading">
+      <el-table-column label="审核状态" align="center">
+        <template slot-scope="scope">
+          <el-tag :type="getReviewType(scope.row.reviewStatus)">
+            {{ getReviewStatusName(scope.row.reviewStatus) }}
+          </el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="计划" align="center" width="130">
+        <template slot-scope="scope">
+          {{ `第${scope.row.startNumDay} 至 ${scope.row.endNumDay}天` }}
+        </template>
+      </el-table-column>
+      <el-table-column label="日期" align="center" width="200">
+        <template slot-scope="scope">
+          {{ `${scope.row.startDate} 至 ${scope.row.endDate}` }}
+        </template>
+      </el-table-column>
+      <el-table-column label="订阅情况" align="center">
+        <template slot-scope="scope">
+          <el-tag :type="scope.row.subscribed ? 'success' : 'danger'">
+            {{ scope.row.subscribed ? "已订阅" : "未订阅" }}
+          </el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="发送" align="center">
+        <template slot-scope="scope">
+          <el-switch
+            v-model="!!scope.row.sendFlag"
+            @change="(val) => handleOnSendChange(val, scope.row)"
+          />
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" align="center">
+        <template slot-scope="scope">
+          <el-button
+            type="text"
+            :icon="
+              scope.row.recipesId ? 'el-icon-edit' : 'el-icon-edit-outline'
+            "
+            @click="handleOnRecipesEditClick(scope.row)"
+          >
+            {{ `${scope.row.recipesId ? "编辑" : "制作"}` }}
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <!-- 暂停记录抽屉 -->
+    <!-- <PlanPauseDrawer ref="planPauseRef" /> -->
+  </div>
+</template>
+<script>
+import Clipboard from "clipboard";
+import { createNamespacedHelpers } from "vuex";
+const { mapActions, mapState, mapMutations, mapGetters } =
+  createNamespacedHelpers("message");
+// import PlanPauseDrawer from "./PlanPauseDrawer";
+import VueQr from "vue-qr";
+const logo = require("@/assets/logo/logo_b.png");
+export default {
+  name: "RecipesPlanDrawer",
+  components: {
+    // PlanPauseDrawer,
+    VueQr,
+  },
+  data() {
+    return {
+      logo,
+      title: "",
+      cusOutId: "",
+      copyValue: "",
+      cusId: "",
+    };
+  },
+  watch: {
+    planList(val, newVal) {
+      console.log({ val, newVal });
+      this.cusOutId = val.reduce((str, cur) => {
+        if (!str && cur.recipesId && cur.reviewStatus === 2) {
+          str = cur.outId;
+          this.cusId = cur.cusId;
+        }
+        return str;
+      }, "");
+      if (this.cusOutId) {
+        this.copyValue =
+          window.location.origin.replace("manage", "sign") +
+          "/recipes/detail/" +
+          this.cusOutId;
+      }
+    },
+  },
+  computed: {
+    ...mapState(["planList", "planListLoading", "healthyData"]),
+  },
+  methods: {
+    getReviewStatusName(status) {
+      switch (status) {
+        case 1:
+          return "未审核";
+        case 2:
+          return "已审核";
+        case 3:
+          return "制作中";
+        case 0:
+        default:
+          return "未制作";
+      }
+    },
+    getReviewType(status) {
+      switch (status) {
+        case 1:
+          return "danger";
+        case 2:
+          return "success";
+        case 3:
+          return "";
+        case 0:
+        default:
+          return "info";
+      }
+    },
+    // handleInnerOpen() {
+    //   this.$refs["planPauseRef"].showDrawer(this.data);
+    //   this.innerVisible = true;
+    //   this.innerTitle = `「${this.data.name}」暂停记录`;
+    // },
+    handleOnRecipesLinkClick() {
+      new Clipboard(".copyBtn");
+      this.$message({
+        message: "拷贝成功",
+        type: "success",
+      });
+    },
+    handleOnRecipesEditClick(data) {
+      window.open(
+        "/recipes/build/" + this.healthyData.name + "/" + data.id,
+        "_blank"
+      );
+    },
+    handleOnSendChange(val, data) {
+      console.log({ val, data });
+      const { id } = data;
+      if (data.reviewStatus === 2) {
+        this.updateRecipesPlanActions({
+          id,
+          sendFlag: val ? 1 : 0,
+          callback: (type, msg) => {
+            this.$message[type](msg);
+          },
+        });
+      } else {
+        this.$message.error("未审核的食谱不能发送");
+      }
+    },
+    ...mapActions(["getRecipesPlanActions", "updateRecipesPlanActions"]),
+  },
+};
+</script>
+<style lang="scss" scoped>
+/deep/ :focus {
+  outline: 0;
+}
+
+.recipes_plan_wrapper {
+  // height: calc(100vh - 77px);
+
+  .header {
+    margin-bottom: 8px;
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+  }
+}
+</style>