|
|
@@ -1,127 +1,106 @@
|
|
|
<template>
|
|
|
- <div class="health-plan-manage admin-page">
|
|
|
- <el-card>
|
|
|
- <div slot="header" class="admin-page-header">
|
|
|
- <span class="admin-page-title">健康方案管理</span>
|
|
|
- <div class="flex items-center gap-sm">
|
|
|
- <el-input
|
|
|
- v-model="keyword"
|
|
|
- placeholder="搜索成员/目标"
|
|
|
- prefix-icon="el-icon-search"
|
|
|
- clearable
|
|
|
- style="width:200px"
|
|
|
- @keyup.enter.native="handleSearch"
|
|
|
- @clear="handleSearch"
|
|
|
- />
|
|
|
- <el-select v-model="statusFilter" placeholder="状态" clearable style="width:130px" @change="handleSearch">
|
|
|
- <el-option label="草稿" value="draft" />
|
|
|
- <el-option label="待审核" value="pending_review" />
|
|
|
- <el-option label="已发布" value="published" />
|
|
|
- <el-option label="已驳回" value="rejected" />
|
|
|
- </el-select>
|
|
|
- <el-input
|
|
|
- v-model="familyIdFilter"
|
|
|
- placeholder="家庭ID"
|
|
|
- clearable
|
|
|
- style="width:120px"
|
|
|
- @keyup.enter.native="handleSearch"
|
|
|
- @clear="handleSearch"
|
|
|
- />
|
|
|
- <el-button type="primary" size="mini" @click="handleSearch">查询</el-button>
|
|
|
- </div>
|
|
|
+ <div class="health-plan-manage">
|
|
|
+ <div class="page-header">
|
|
|
+ <h2>健康方案管理</h2>
|
|
|
+ <div class="filters">
|
|
|
+ <el-input v-model="keyword" placeholder="搜索成员名/目标" style="width:200px" clearable @keyup.enter="loadPlans" />
|
|
|
+ <el-select v-model="statusFilter" placeholder="状态筛选" clearable style="width:140px;margin-left:10px" @change="loadPlans">
|
|
|
+ <el-option label="草稿" value="draft" />
|
|
|
+ <el-option label="待审核" value="pending_review" />
|
|
|
+ <el-option label="已发布" value="published" />
|
|
|
+ <el-option label="已驳回" value="rejected" />
|
|
|
+ </el-select>
|
|
|
+ <el-button type="primary" @click="loadPlans" style="margin-left:10px">查询</el-button>
|
|
|
</div>
|
|
|
+ </div>
|
|
|
|
|
|
- <div class="table-scroll-wrap">
|
|
|
- <el-table :data="list" v-loading="loading" border stripe :max-height="tableHeight">
|
|
|
- <el-table-column prop="id" label="ID" width="70" />
|
|
|
- <el-table-column label="家庭" min-width="120" show-overflow-tooltip>
|
|
|
- <template slot-scope="{ row }">
|
|
|
- {{ row.familyName || ('家庭' + (row.familyId || '-')) }}
|
|
|
- </template>
|
|
|
- </el-table-column>
|
|
|
- <el-table-column label="成员" width="140" show-overflow-tooltip>
|
|
|
- <template slot-scope="{ row }">{{ row.memberName || row.memberIds || '-' }}</template>
|
|
|
- </el-table-column>
|
|
|
- <el-table-column label="目标" min-width="180" show-overflow-tooltip>
|
|
|
- <template slot-scope="{ row }">{{ row.goal || '-' }}</template>
|
|
|
- </el-table-column>
|
|
|
- <el-table-column label="维度" width="140">
|
|
|
- <template slot-scope="{ row }">
|
|
|
- <el-tag
|
|
|
- v-for="(d, i) in splitDimensions(row.dimensions)"
|
|
|
- :key="i"
|
|
|
- size="mini"
|
|
|
- style="margin-right:4px"
|
|
|
- >{{ d }}</el-tag>
|
|
|
- </template>
|
|
|
- </el-table-column>
|
|
|
- <el-table-column label="状态" width="100">
|
|
|
- <template slot-scope="{ row }">
|
|
|
- <el-tag :type="statusType(row.status)" size="mini">{{ statusLabel(row.status) }}</el-tag>
|
|
|
- </template>
|
|
|
- </el-table-column>
|
|
|
- <el-table-column label="创建时间" width="170">
|
|
|
- <template slot-scope="{ row }">{{ formatTime(row.createdAt) }}</template>
|
|
|
- </el-table-column>
|
|
|
- <el-table-column label="操作" width="100" fixed="right">
|
|
|
- <template slot-scope="{ row }">
|
|
|
- <el-button size="mini" type="text" @click="showDetail(row)">详情</el-button>
|
|
|
- </template>
|
|
|
- </el-table-column>
|
|
|
- </el-table>
|
|
|
- </div>
|
|
|
+ <el-table :data="plans" border stripe style="width:100%" v-loading="loading" :max-height="tableHeight">
|
|
|
+ <el-table-column prop="id" label="方案ID" width="80" />
|
|
|
+ <el-table-column prop="familyName" label="家庭" width="140">
|
|
|
+ <template slot-scope="{ row }">{{ row.familyName || row.familyId }}</template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="memberName" label="成员" width="120">
|
|
|
+ <template slot-scope="{ row }">{{ row.memberName || row.memberIds || '-' }}</template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="维度" width="160">
|
|
|
+ <template slot-scope="{ row }">
|
|
|
+ <el-tag v-for="(d, i) in (row.dimensions || '').split(',')" :key="i" size="mini" style="margin-right:3px">{{ d }}</el-tag>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="目标" min-width="200">
|
|
|
+ <template slot-scope="{ row }">
|
|
|
+ {{ row.goal ? (row.goal.length > 50 ? row.goal.substring(0, 50) + '...' : row.goal) : '-' }}
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="状态" width="100">
|
|
|
+ <template slot-scope="{ row }">
|
|
|
+ <el-tag :type="statusType(row.status)">{{ statusLabel(row.status) }}</el-tag>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="reviewComment" label="审核意见" width="150">
|
|
|
+ <template slot-scope="{ row }">{{ row.reviewComment || '-' }}</template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="createdAt" label="创建时间" width="160">
|
|
|
+ <template slot-scope="{ row }">{{ formatTime(row.createdAt) }}</template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="操作" width="100">
|
|
|
+ <template slot-scope="{ row }">
|
|
|
+ <el-button size="mini" type="primary" @click="showDetail(row)">详情</el-button>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
|
|
|
- <el-pagination
|
|
|
- style="margin-top:16px;text-align:right"
|
|
|
- layout="total, prev, pager, next, sizes"
|
|
|
- :current-page.sync="page"
|
|
|
- :page-size.sync="size"
|
|
|
- :page-sizes="[10, 20, 50]"
|
|
|
- :total="total"
|
|
|
- @current-change="loadList"
|
|
|
- @size-change="handleSizeChange"
|
|
|
- />
|
|
|
- </el-card>
|
|
|
+ <el-pagination
|
|
|
+ style="margin-top:16px;text-align:right"
|
|
|
+ background
|
|
|
+ layout="total, prev, pager, next"
|
|
|
+ :total="total"
|
|
|
+ :page-size="pageSize"
|
|
|
+ :current-page="pageNum"
|
|
|
+ @current-change="handlePageChange"
|
|
|
+ />
|
|
|
|
|
|
- <el-dialog title="健康方案详情" :visible.sync="detailVisible" width="720px" destroy-on-close>
|
|
|
- <div v-loading="detailLoading" v-if="currentPlan">
|
|
|
- <el-descriptions :column="2" border size="small">
|
|
|
+ <!-- 详情弹窗 -->
|
|
|
+ <el-dialog title="健康方案详情" :visible.sync="detailVisible" width="800px" destroy-on-close>
|
|
|
+ <div v-if="currentPlan" v-loading="detailLoading">
|
|
|
+ <el-descriptions :column="2" border>
|
|
|
<el-descriptions-item label="方案ID">{{ currentPlan.id }}</el-descriptions-item>
|
|
|
- <el-descriptions-item label="状态">
|
|
|
- <el-tag :type="statusType(currentPlan.status)" size="mini">{{ statusLabel(currentPlan.status) }}</el-tag>
|
|
|
- </el-descriptions-item>
|
|
|
- <el-descriptions-item label="家庭">
|
|
|
- {{ currentPlan.familyName || ('家庭' + (currentPlan.familyId || '-')) }}
|
|
|
- <span v-if="currentPlan.familyId" style="color:#999;margin-left:6px">(#{{ currentPlan.familyId }})</span>
|
|
|
- </el-descriptions-item>
|
|
|
+ <el-descriptions-item label="家庭">{{ currentPlan.familyName || currentPlan.familyId }}</el-descriptions-item>
|
|
|
<el-descriptions-item label="成员">{{ currentPlan.memberName || currentPlan.memberIds || '-' }}</el-descriptions-item>
|
|
|
- <el-descriptions-item label="维度" :span="2">{{ currentPlan.dimensions || '-' }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="维度">{{ currentPlan.dimensions || '-' }}</el-descriptions-item>
|
|
|
<el-descriptions-item label="目标" :span="2">{{ currentPlan.goal || '-' }}</el-descriptions-item>
|
|
|
- <el-descriptions-item label="创建时间">{{ formatTime(currentPlan.createdAt) }}</el-descriptions-item>
|
|
|
- <el-descriptions-item label="更新时间">{{ formatTime(currentPlan.updatedAt) }}</el-descriptions-item>
|
|
|
- <el-descriptions-item v-if="currentPlan.reviewComment" label="审核意见" :span="2">
|
|
|
- {{ currentPlan.reviewComment }}
|
|
|
+ <el-descriptions-item label="状态">
|
|
|
+ <el-tag :type="statusType(currentPlan.status)">{{ statusLabel(currentPlan.status) }}</el-tag>
|
|
|
</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="规划师ID">{{ currentPlan.teacherId || '-' }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="审核人ID">{{ currentPlan.reviewedBy || '-' }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="审核时间">{{ formatTime(currentPlan.reviewedAt) }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="审核意见">{{ currentPlan.reviewComment || '-' }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="创建时间" :span="2">{{ formatTime(currentPlan.createdAt) }}</el-descriptions-item>
|
|
|
</el-descriptions>
|
|
|
|
|
|
- <div class="detail-section-title">方案内容</div>
|
|
|
- <pre class="plan-content">{{ currentPlan.planContent || '暂无内容' }}</pre>
|
|
|
+ <el-divider content-position="left">方案内容</el-divider>
|
|
|
+ <div class="plan-content" v-html="renderPlanContent(currentPlan.planContent)"></div>
|
|
|
|
|
|
- <div v-if="currentPlan.tasks && currentPlan.tasks.length" class="detail-section-title">
|
|
|
- 分解任务({{ currentPlan.tasks.length }})
|
|
|
- </div>
|
|
|
- <el-table
|
|
|
- v-if="currentPlan.tasks && currentPlan.tasks.length"
|
|
|
- :data="currentPlan.tasks"
|
|
|
- border
|
|
|
- size="mini"
|
|
|
- style="margin-top:8px"
|
|
|
- >
|
|
|
- <el-table-column prop="id" label="ID" width="70" />
|
|
|
- <el-table-column prop="title" label="任务" min-width="180" show-overflow-tooltip />
|
|
|
- <el-table-column prop="status" label="状态" width="100" />
|
|
|
- <el-table-column label="截止时间" width="160">
|
|
|
- <template slot-scope="{ row }">{{ formatTime(row.deadline) }}</template>
|
|
|
+ <el-divider content-position="left">分解任务</el-divider>
|
|
|
+ <el-table :data="tasks" border stripe size="small" v-loading="tasksLoading">
|
|
|
+ <el-table-column prop="title" label="任务标题" min-width="180" />
|
|
|
+ <el-table-column prop="actionType" label="类型" width="100">
|
|
|
+ <template slot-scope="{ row }">{{ actionTypeLabel(row.actionType) }}</template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="dimension" label="维度" width="80">
|
|
|
+ <template slot-scope="{ row }">
|
|
|
+ <el-tag size="mini">{{ dimensionLabel(row.dimension) }}</el-tag>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="frequency" label="频率" width="80">
|
|
|
+ <template slot-scope="{ row }">{{ frequencyLabel(row.frequency) }}</template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="notes" label="备注" width="150" />
|
|
|
+ <el-table-column prop="status" label="状态" width="80">
|
|
|
+ <template slot-scope="{ row }">
|
|
|
+ <el-tag :type="taskStatusType(row.status)" size="mini">{{ taskStatusLabel(row.status) }}</el-tag>
|
|
|
+ </template>
|
|
|
</el-table-column>
|
|
|
</el-table>
|
|
|
</div>
|
|
|
@@ -139,69 +118,41 @@ export default {
|
|
|
name: 'HealthPlanManage',
|
|
|
computed: {
|
|
|
tableHeight() {
|
|
|
- return window.innerHeight - 320
|
|
|
+ return window.innerHeight - 280
|
|
|
}
|
|
|
},
|
|
|
data() {
|
|
|
return {
|
|
|
- loading: false,
|
|
|
- detailLoading: false,
|
|
|
- list: [],
|
|
|
+ plans: [],
|
|
|
total: 0,
|
|
|
- page: 1,
|
|
|
- size: 20,
|
|
|
+ pageNum: 1,
|
|
|
+ pageSize: 20,
|
|
|
keyword: '',
|
|
|
statusFilter: '',
|
|
|
- familyIdFilter: '',
|
|
|
+ loading: false,
|
|
|
detailVisible: false,
|
|
|
- currentPlan: null
|
|
|
+ currentPlan: null,
|
|
|
+ detailLoading: false,
|
|
|
+ tasks: [],
|
|
|
+ tasksLoading: false
|
|
|
}
|
|
|
},
|
|
|
created() {
|
|
|
- this.loadList()
|
|
|
+ this.loadPlans()
|
|
|
},
|
|
|
methods: {
|
|
|
- formatTime(t) {
|
|
|
- return t ? String(t).replace('T', ' ').substring(0, 19) : '-'
|
|
|
- },
|
|
|
- splitDimensions(dims) {
|
|
|
- if (!dims) return []
|
|
|
- return String(dims).split(',').map(function (s) { return s.trim() }).filter(Boolean)
|
|
|
- },
|
|
|
- statusType(s) {
|
|
|
- var map = { pending_review: 'warning', draft: 'info', published: 'success', rejected: 'danger' }
|
|
|
- return map[s] || 'info'
|
|
|
- },
|
|
|
- statusLabel(s) {
|
|
|
- var map = { pending_review: '待审核', draft: '草稿', published: '已发布', rejected: '已驳回' }
|
|
|
- return map[s] || s || '-'
|
|
|
- },
|
|
|
- handleSearch() {
|
|
|
- this.page = 1
|
|
|
- this.loadList()
|
|
|
- },
|
|
|
- handleSizeChange() {
|
|
|
- this.page = 1
|
|
|
- this.loadList()
|
|
|
- },
|
|
|
- async loadList() {
|
|
|
+ async loadPlans() {
|
|
|
this.loading = true
|
|
|
try {
|
|
|
- var familyId = this.familyIdFilter ? Number(this.familyIdFilter) : null
|
|
|
- if (familyId !== null && isNaN(familyId)) familyId = null
|
|
|
- var res = await adminListHealthPlans({
|
|
|
- page: this.page,
|
|
|
- size: this.size,
|
|
|
+ const res = await adminListHealthPlans({
|
|
|
+ page: this.pageNum,
|
|
|
+ size: this.pageSize,
|
|
|
keyword: this.keyword || null,
|
|
|
- status: this.statusFilter || null,
|
|
|
- familyId: familyId
|
|
|
+ status: this.statusFilter || null
|
|
|
})
|
|
|
if (res.code === 200) {
|
|
|
- var data = res.data || {}
|
|
|
- this.list = data.records || []
|
|
|
- this.total = data.total || 0
|
|
|
- } else {
|
|
|
- this.$message.error(res.message || '加载失败')
|
|
|
+ this.plans = res.data?.records || []
|
|
|
+ this.total = res.data?.total || 0
|
|
|
}
|
|
|
} catch (e) {
|
|
|
this.$message.error('加载失败')
|
|
|
@@ -209,44 +160,85 @@ export default {
|
|
|
this.loading = false
|
|
|
}
|
|
|
},
|
|
|
+ handlePageChange(page) {
|
|
|
+ this.pageNum = page
|
|
|
+ this.loadPlans()
|
|
|
+ },
|
|
|
async showDetail(row) {
|
|
|
this.detailVisible = true
|
|
|
- this.currentPlan = row
|
|
|
this.detailLoading = true
|
|
|
+ this.tasksLoading = true
|
|
|
+ this.currentPlan = row
|
|
|
+ this.tasks = []
|
|
|
try {
|
|
|
- var res = await adminGetHealthPlanDetail({ planId: row.id })
|
|
|
+ const res = await adminGetHealthPlanDetail({ planId: row.id })
|
|
|
if (res.code === 200) {
|
|
|
this.currentPlan = res.data
|
|
|
- } else {
|
|
|
- this.$message.error(res.message || '加载详情失败')
|
|
|
+ this.tasks = res.data.tasks || []
|
|
|
}
|
|
|
} catch (e) {
|
|
|
this.$message.error('加载详情失败')
|
|
|
} finally {
|
|
|
this.detailLoading = false
|
|
|
+ this.tasksLoading = false
|
|
|
}
|
|
|
+ },
|
|
|
+ formatTime(t) {
|
|
|
+ if (!t) return '-'
|
|
|
+ return t.replace('T', ' ').substring(0, 19)
|
|
|
+ },
|
|
|
+ statusType(s) {
|
|
|
+ const map = { draft: 'info', pending_review: 'warning', published: 'success', rejected: 'danger' }
|
|
|
+ return map[s] || 'info'
|
|
|
+ },
|
|
|
+ statusLabel(s) {
|
|
|
+ const map = { draft: '草稿', pending_review: '待审核', published: '已发布', rejected: '已驳回' }
|
|
|
+ return map[s] || s
|
|
|
+ },
|
|
|
+ actionTypeLabel(type) {
|
|
|
+ const map = { buy: '购买', read: '阅读', exercise: '运动', checkin: '打卡', diet: '饮食', activity: '活动' }
|
|
|
+ return map[type] || type || '-'
|
|
|
+ },
|
|
|
+ dimensionLabel(d) {
|
|
|
+ const map = { body: '身', mind: '心', wisdom: '智', action: '行', wealth: '富' }
|
|
|
+ return map[d] || d || '-'
|
|
|
+ },
|
|
|
+ frequencyLabel(f) {
|
|
|
+ const map = { once: '一次性', daily: '每日' }
|
|
|
+ return map[f] || f || '-'
|
|
|
+ },
|
|
|
+ taskStatusType(s) {
|
|
|
+ const map = { pending: 'warning', completed: 'success', failed: 'danger' }
|
|
|
+ return map[s] || 'info'
|
|
|
+ },
|
|
|
+ taskStatusLabel(s) {
|
|
|
+ const map = { pending: '待完成', completed: '已完成', failed: '未完成' }
|
|
|
+ return map[s] || s || '-'
|
|
|
+ },
|
|
|
+ renderPlanContent(content) {
|
|
|
+ if (!content) return '<span style="color:#999">暂无内容</span>'
|
|
|
+ return content
|
|
|
+ .replace(/&/g, '&')
|
|
|
+ .replace(/</g, '<')
|
|
|
+ .replace(/>/g, '>')
|
|
|
+ .replace(/\n/g, '<br>')
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
</script>
|
|
|
|
|
|
<style scoped>
|
|
|
-.detail-section-title {
|
|
|
- margin: 16px 0 8px;
|
|
|
- font-weight: 600;
|
|
|
- color: #303133;
|
|
|
-}
|
|
|
+.health-plan-manage { padding: 20px; }
|
|
|
+.page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
|
|
|
+.page-header h2 { margin: 0; font-size: 20px; }
|
|
|
+.filters { display: flex; align-items: center; }
|
|
|
.plan-content {
|
|
|
- white-space: pre-wrap;
|
|
|
- word-break: break-word;
|
|
|
background: #f5f7fa;
|
|
|
- padding: 12px;
|
|
|
- border-radius: 6px;
|
|
|
- max-height: 360px;
|
|
|
- overflow: auto;
|
|
|
- margin: 0;
|
|
|
- font-size: 13px;
|
|
|
- line-height: 1.6;
|
|
|
- color: #303133;
|
|
|
+ border-radius: 4px;
|
|
|
+ padding: 16px;
|
|
|
+ min-height: 80px;
|
|
|
+ line-height: 1.8;
|
|
|
+ white-space: pre-wrap;
|
|
|
+ font-size: 14px;
|
|
|
}
|
|
|
</style>
|