Переглянути джерело

feat(admin): add EmotionAlert admin management page with list and resolve actions

- EmotionAlertManage.vue: admin page with filterable alert list and resolve button
- admin.js: add getEmotionAlertList and resolveEmotionAlert API endpoints
- router/index.js: add /emotion-alert route
- Layout.vue: add sidebar menu item for emotion alert management
Xiaogang Liao 2 місяців тому
батько
коміт
0dae403f70

+ 18 - 0
cfc-web/src/api/admin.js

@@ -760,3 +760,21 @@ export function searchUsers(params) {
     data: params
   })
 }
+
+// ========== 情绪告警管理 API ==========
+
+export function getEmotionAlertList(params) {
+  return request({
+    url: '/api/mind/alert/admin/list',
+    method: 'post',
+    data: params
+  })
+}
+
+export function resolveEmotionAlert(alertId) {
+  return request({
+    url: '/api/mind/alert/resolve',
+    method: 'post',
+    data: { alertId, source: 'checkin' }
+  })
+}

+ 6 - 0
cfc-web/src/router/index.js

@@ -346,6 +346,12 @@ const routes = [
         component: () => import('@/views/nutritionist/HealthCheckins.vue'),
         meta: { title: '健康打卡', perm: 'health:checkins' }
       },
+      {
+        path: 'emotion-alert',
+        name: 'EmotionAlertManage',
+        component: () => import('@/views/admin/EmotionAlertManage.vue'),
+        meta: { title: '情绪告警管理', perm: 'health:checkins' }
+      },
       {
         path: 'nutrition-mappings',
         name: 'NutritionMappings',

+ 1 - 0
cfc-web/src/views/Layout.vue

@@ -193,6 +193,7 @@ export default {
             { path: '/health-reports', label: '健康报告', icon: 'el-icon-document', perm: 'health:reports' },
             { path: '/health-indicators', label: '健康指标', icon: 'el-icon-data-line', perm: 'health:indicators' },
             { path: '/health-checkins', label: '健康打卡', icon: 'el-icon-s-order', perm: 'health:checkins' },
+            { path: '/emotion-alert', label: '情绪告警', icon: 'el-icon-warning', perm: 'health:checkins' },
             { path: '/nutrition-mappings', label: '营养素映射', icon: 'el-icon-connection', perm: 'health:mappings' },
             { path: '/energy-sandbox', label: '五维能量', icon: 'el-icon-data-line', perm: 'energy' },
             { path: '/energy-rule', label: '能量规则管理', icon: 'el-icon-setting', perm: 'energy' },

+ 167 - 0
cfc-web/src/views/admin/EmotionAlertManage.vue

@@ -0,0 +1,167 @@
+<template>
+  <div class="emotion-alert-manage">
+    <div class="header">
+      <h2>情绪告警管理</h2>
+      <el-radio-group v-model="resolvedFilter" @change="onFilterChange">
+        <el-radio-button label="">全部</el-radio-button>
+        <el-radio-button label="false">未处理</el-radio-button>
+        <el-radio-button label="true">已处理</el-radio-button>
+      </el-radio-group>
+    </div>
+
+    <el-table :data="list" v-loading="loading" border stripe>
+      <el-table-column prop="id" label="ID" width="70" />
+      <el-table-column prop="childId" label="孩子ID" width="90" />
+      <el-table-column label="告警类型" width="140">
+        <template slot-scope="{ row }">
+          {{ alertTypeLabel(row.alertType) }}
+        </template>
+      </el-table-column>
+      <el-table-column label="严重程度" width="100">
+        <template slot-scope="{ row }">
+          <el-tag :type="severityType(row.severity)">{{ severityLabel(row.severity) }}</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column prop="title" label="标题" width="180" show-overflow-tooltip />
+      <el-table-column prop="message" label="详情" min-width="220" show-overflow-tooltip />
+      <el-table-column label="状态" width="80">
+        <template slot-scope="{ row }">
+          <el-tag :type="row.resolved ? 'success' : 'danger'" size="small">
+            {{ row.resolved ? '已处理' : '未处理' }}
+          </el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="创建时间" width="160">
+        <template slot-scope="{ row }">
+          {{ formatTime(row.createdAt) }}
+        </template>
+      </el-table-column>
+      <el-table-column label="处理时间" width="160">
+        <template slot-scope="{ row }">
+          {{ row.resolvedAt ? formatTime(row.resolvedAt) : '-' }}
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" width="100" fixed="right">
+        <template slot-scope="{ row }">
+          <el-button
+            v-if="!row.resolved"
+            type="warning"
+            size="small"
+            @click="handleResolve(row)"
+          >标记已处理</el-button>
+          <span v-else class="resolved-text">-</span>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <el-pagination
+      @size-change="onPageChange"
+      @current-change="onPageChange"
+      :current-page="page"
+      :page-size="size"
+      :total="total"
+      layout="total, prev, pager, next"
+      style="margin-top: 20px; text-align: right;"
+    />
+  </div>
+</template>
+
+<script>
+import { getEmotionAlertList, resolveEmotionAlert } from '@/api/admin'
+
+export default {
+  data() {
+    return {
+      list: [],
+      loading: false,
+      page: 1,
+      size: 20,
+      total: 0,
+      resolvedFilter: ''
+    }
+  },
+  mounted() {
+    this.loadList()
+  },
+  methods: {
+    async loadList() {
+      this.loading = true
+      try {
+        var params = { page: this.page, size: this.size }
+        if (this.resolvedFilter !== '') {
+          params.resolved = this.resolvedFilter
+        }
+        var res = await getEmotionAlertList(params)
+        this.list = res.data && res.data.list ? res.data.list : []
+        this.total = res.data && res.data.total ? res.data.total : 0
+      } catch (e) {
+        this.$message.error('加载失败')
+      } finally {
+        this.loading = false
+      }
+    },
+    onFilterChange() {
+      this.page = 1
+      this.loadList()
+    },
+    onPageChange(val) {
+      this.page = val
+      this.loadList()
+    },
+    async handleResolve(row) {
+      try {
+        await this.$confirm('确认将该告警标记为已处理?', '提示', {
+          confirmButtonText: '确定',
+          cancelButtonText: '取消',
+          type: 'warning'
+        })
+        await resolveEmotionAlert(row.id)
+        this.$message.success('操作成功')
+        this.loadList()
+      } catch (e) {
+        if (e !== 'cancel') {
+          this.$message.error('操作失败')
+        }
+      }
+    },
+    alertTypeLabel(type) {
+      var map = {
+        consecutive_low_mood: '连续低情绪',
+        crisis_keyword: '危机关键词',
+        sharp_decline: '情绪锐降'
+      }
+      return map[type] || type
+    },
+    severityType(severity) {
+      var map = { high: 'danger', medium: 'warning', low: 'info' }
+      return map[severity] || 'info'
+    },
+    severityLabel(severity) {
+      var map = { high: '高', medium: '中', low: '低' }
+      return map[severity] || severity
+    },
+    formatTime(time) {
+      if (!time) return '-'
+      return new Date(time).toLocaleString()
+    }
+  }
+}
+</script>
+
+<style scoped>
+.emotion-alert-manage {
+  padding: 20px;
+}
+.header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 20px;
+}
+.header h2 {
+  margin: 0;
+}
+.resolved-text {
+  color: #909399;
+}
+</style>