|
|
@@ -0,0 +1,388 @@
|
|
|
+<template>
|
|
|
+ <div class="page-container">
|
|
|
+ <div class="admin-page-header">
|
|
|
+ <div>
|
|
|
+ <span class="admin-page-title">家庭工作台</span>
|
|
|
+ <span class="admin-page-subtitle">{{ family.name }}({{ family.inviteCode }})</span>
|
|
|
+ </div>
|
|
|
+ <el-button size="mini" @click="goBack">返回家庭列表</el-button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <el-row :gutter="16">
|
|
|
+ <el-col v-for="(card, idx) in cards" :key="card.key" :span="8" class="wb-col">
|
|
|
+ <div class="wb-card" :class="cardStaticClass(card.key)" @click="openModule(card.key)">
|
|
|
+ <div class="wb-card-label">{{ card.label }}</div>
|
|
|
+ <div class="wb-card-total">{{ card.total }}</div>
|
|
|
+ <div class="wb-card-chart" :ref="chartRef(idx)"></div>
|
|
|
+ </div>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+
|
|
|
+ <!-- 任务抽屉 -->
|
|
|
+ <el-drawer title="任务管理" :visible.sync="taskDrawerVisible" size="65%">
|
|
|
+ <div class="mb-12">
|
|
|
+ <el-button type="primary" size="mini" @click="openTaskForm()">新增任务</el-button>
|
|
|
+ </div>
|
|
|
+ <el-table :data="taskList" size="small" border>
|
|
|
+ <el-table-column prop="title" label="标题" min-width="140"></el-table-column>
|
|
|
+ <el-table-column prop="status" label="状态" width="90"></el-table-column>
|
|
|
+ <el-table-column prop="points" label="积分" width="70"></el-table-column>
|
|
|
+ <el-table-column label="操作" width="90">
|
|
|
+ <template slot-scope="{ row }">
|
|
|
+ <el-button size="mini" type="text" @click="openTaskForm(row)">编辑</el-button>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+ <el-pagination class="pagination-right-mt12" small background layout="prev, pager, next, total"
|
|
|
+ :total="taskTotal" :page-size="taskPageSize" :current-page="taskPage" @current-change="loadTasks"></el-pagination>
|
|
|
+ </el-drawer>
|
|
|
+
|
|
|
+ <!-- 方案抽屉 -->
|
|
|
+ <el-drawer title="方案管理" :visible.sync="planDrawerVisible" size="65%">
|
|
|
+ <div class="mb-12">
|
|
|
+ <el-button type="primary" size="mini" @click="openPlanForm()">新增方案</el-button>
|
|
|
+ </div>
|
|
|
+ <el-table :data="planList" size="small" border>
|
|
|
+ <el-table-column prop="planType" label="类型" width="90"></el-table-column>
|
|
|
+ <el-table-column prop="memberName" label="成员" width="90"></el-table-column>
|
|
|
+ <el-table-column prop="goal" label="目标" min-width="140"></el-table-column>
|
|
|
+ <el-table-column prop="status" label="状态" width="90"></el-table-column>
|
|
|
+ <el-table-column label="操作" width="90">
|
|
|
+ <template slot-scope="{ row }">
|
|
|
+ <el-button size="mini" type="text" @click="openPlanForm(row)">编辑</el-button>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+ </el-drawer>
|
|
|
+
|
|
|
+ <!-- 报告抽屉 -->
|
|
|
+ <el-drawer title="报告(查看 / 手动新增)" :visible.sync="reportDrawerVisible" size="65%">
|
|
|
+ <div class="mb-12">
|
|
|
+ <el-button type="primary" size="mini" @click="reportFormVisible = true">手动新增报告</el-button>
|
|
|
+ </div>
|
|
|
+ <el-table :data="reportList" size="small" border>
|
|
|
+ <el-table-column prop="reportDate" label="日期" width="110"></el-table-column>
|
|
|
+ <el-table-column prop="personName" label="成员" width="90"></el-table-column>
|
|
|
+ <el-table-column prop="reportType" label="类型" width="110"></el-table-column>
|
|
|
+ <el-table-column prop="overallScore" label="综合分" width="80"></el-table-column>
|
|
|
+ <el-table-column prop="status" label="状态" width="80"></el-table-column>
|
|
|
+ </el-table>
|
|
|
+ </el-drawer>
|
|
|
+
|
|
|
+ <!-- 消息抽屉 -->
|
|
|
+ <el-drawer title="消息(只读)" :visible.sync="messageDrawerVisible" size="65%">
|
|
|
+ <el-button v-if="activeConversationId" size="mini" class="mb-12" @click="messageBack">返回会话列表</el-button>
|
|
|
+ <div v-if="!activeConversationId">
|
|
|
+ <el-table :data="conversationList" size="small" border @row-click="openConversation">
|
|
|
+ <el-table-column prop="title" label="会话" min-width="140"></el-table-column>
|
|
|
+ <el-table-column prop="assistantType" label="类型" width="90"></el-table-column>
|
|
|
+ <el-table-column prop="messageCount" label="消息数" width="80"></el-table-column>
|
|
|
+ <el-table-column prop="lastMessagePreview" label="最后一条" min-width="160"></el-table-column>
|
|
|
+ </el-table>
|
|
|
+ <el-pagination class="pagination-right-mt12" small background layout="prev, pager, next, total"
|
|
|
+ :total="conversationTotal" :page-size="10" :current-page="conversationPage" @current-change="loadConversations"></el-pagination>
|
|
|
+ </div>
|
|
|
+ <div v-else class="message-timeline">
|
|
|
+ <div v-for="m in messageList" :key="m.id" class="message-item">
|
|
|
+ <div class="message-meta">{{ m.role }} · {{ m.createdAt }}</div>
|
|
|
+ <div class="message-content">{{ m.content }}</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-drawer>
|
|
|
+
|
|
|
+ <!-- 任务新增/编辑 -->
|
|
|
+ <el-dialog title="任务" :visible.sync="taskFormVisible" width="520px">
|
|
|
+ <el-form :model="taskForm" label-width="90px" size="small">
|
|
|
+ <el-form-item label="标题"><el-input v-model="taskForm.title"></el-input></el-form-item>
|
|
|
+ <el-form-item label="积分"><el-input-number v-model="taskForm.points" :min="0"></el-input-number></el-form-item>
|
|
|
+ <el-form-item label="分类"><el-input v-model="taskForm.category"></el-input></el-form-item>
|
|
|
+ <el-form-item label="类型">
|
|
|
+ <el-select v-model="taskForm.taskType" placeholder="任务类型">
|
|
|
+ <el-option label="一次性" value="once"></el-option>
|
|
|
+ <el-option label="循环" value="recurring"></el-option>
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="描述"><el-input type="textarea" v-model="taskForm.description"></el-input></el-form-item>
|
|
|
+ </el-form>
|
|
|
+ <div slot="footer">
|
|
|
+ <el-button size="small" @click="taskFormVisible = false">取消</el-button>
|
|
|
+ <el-button size="small" type="primary" @click="submitTaskForm">保存</el-button>
|
|
|
+ </div>
|
|
|
+ </el-dialog>
|
|
|
+
|
|
|
+ <!-- 方案新增/编辑 -->
|
|
|
+ <el-dialog title="方案" :visible.sync="planFormVisible" width="520px">
|
|
|
+ <el-form :model="planForm" label-width="90px" size="small">
|
|
|
+ <el-form-item label="类型"><el-input v-model="planForm.planType"></el-input></el-form-item>
|
|
|
+ <el-form-item label="成员"><el-input v-model="planForm.memberName"></el-input></el-form-item>
|
|
|
+ <el-form-item label="目标"><el-input v-model="planForm.goal"></el-input></el-form-item>
|
|
|
+ <el-form-item label="天数"><el-input-number v-model="planForm.totalDays" :min="0"></el-input-number></el-form-item>
|
|
|
+ </el-form>
|
|
|
+ <div slot="footer">
|
|
|
+ <el-button size="small" @click="planFormVisible = false">取消</el-button>
|
|
|
+ <el-button size="small" type="primary" @click="submitPlanForm">保存</el-button>
|
|
|
+ </div>
|
|
|
+ </el-dialog>
|
|
|
+
|
|
|
+ <!-- 报告手动新增 -->
|
|
|
+ <el-dialog title="手动新增报告" :visible.sync="reportFormVisible" width="520px">
|
|
|
+ <el-form :model="reportForm" label-width="100px" size="small">
|
|
|
+ <el-form-item label="成员姓名"><el-input v-model="reportForm.personName"></el-input></el-form-item>
|
|
|
+ <el-form-item label="报告类型">
|
|
|
+ <el-select v-model="reportForm.reportType" placeholder="报告类型">
|
|
|
+ <el-option label="体检" value="physical_exam"></el-option>
|
|
|
+ <el-option label="肠道菌群" value="gut_flora"></el-option>
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="报告日期"><el-date-picker v-model="reportForm.reportDate" type="date" value-format="yyyy-MM-dd"></el-date-picker></el-form-item>
|
|
|
+ <el-form-item label="综合分"><el-input-number v-model="reportForm.overallScore" :min="0"></el-input-number></el-form-item>
|
|
|
+ <el-form-item label="备注"><el-input type="textarea" v-model="reportForm.remark"></el-input></el-form-item>
|
|
|
+ </el-form>
|
|
|
+ <div slot="footer">
|
|
|
+ <el-button size="small" @click="reportFormVisible = false">取消</el-button>
|
|
|
+ <el-button size="small" type="primary" @click="submitReportForm">保存</el-button>
|
|
|
+ </div>
|
|
|
+ </el-dialog>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+import * as echarts from "echarts"
|
|
|
+import {
|
|
|
+ getFamilyWorkbench,
|
|
|
+ getFamilyMessages,
|
|
|
+ getConversationMessages,
|
|
|
+ createFamilyReport,
|
|
|
+ createFamilyTask,
|
|
|
+ updateFamilyTask,
|
|
|
+ createFamilyPlan,
|
|
|
+ updateFamilyPlan
|
|
|
+} from "@/api/familyWorkbench"
|
|
|
+import { getFamilyTasks, getFamilyOverview } from "@/api/admin"
|
|
|
+
|
|
|
+export default {
|
|
|
+ name: "FamilyWorkbench",
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ familyId: Number(this.$route.params.id),
|
|
|
+ family: {},
|
|
|
+ loading: false,
|
|
|
+ cards: [],
|
|
|
+ chartInstances: [],
|
|
|
+ activeModule: "",
|
|
|
+ taskDrawerVisible: false,
|
|
|
+ taskList: [],
|
|
|
+ taskPage: 1,
|
|
|
+ taskPageSize: 10,
|
|
|
+ taskTotal: 0,
|
|
|
+ taskFormVisible: false,
|
|
|
+ taskForm: {},
|
|
|
+ planDrawerVisible: false,
|
|
|
+ planList: [],
|
|
|
+ planFormVisible: false,
|
|
|
+ planForm: {},
|
|
|
+ reportDrawerVisible: false,
|
|
|
+ reportList: [],
|
|
|
+ reportFormVisible: false,
|
|
|
+ reportForm: {},
|
|
|
+ messageDrawerVisible: false,
|
|
|
+ conversationList: [],
|
|
|
+ conversationPage: 1,
|
|
|
+ conversationTotal: 0,
|
|
|
+ activeConversationId: null,
|
|
|
+ messageList: []
|
|
|
+ }
|
|
|
+ },
|
|
|
+ mounted() {
|
|
|
+ this.loadWorkbench()
|
|
|
+ },
|
|
|
+ beforeDestroy() {
|
|
|
+ this.chartInstances.forEach((c) => c && c.dispose())
|
|
|
+ this.chartInstances = []
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ goBack() {
|
|
|
+ this.$router.push("/families")
|
|
|
+ },
|
|
|
+ async loadWorkbench() {
|
|
|
+ this.loading = true
|
|
|
+ try {
|
|
|
+ const res = await getFamilyWorkbench(this.familyId)
|
|
|
+ const data = res.data || {}
|
|
|
+ this.family = data.family || {}
|
|
|
+ this.cards = data.cards || []
|
|
|
+ await this.$nextTick()
|
|
|
+ this.renderCharts()
|
|
|
+ } catch (error) {
|
|
|
+ this.$message.error(error.message || "加载工作台失败")
|
|
|
+ } finally {
|
|
|
+ this.loading = false
|
|
|
+ }
|
|
|
+ },
|
|
|
+ cardStaticClass(key) {
|
|
|
+ return key === "cf" ? "wb-card-static" : ""
|
|
|
+ },
|
|
|
+ cardClass(key) {
|
|
|
+ return "wb-card-" + key
|
|
|
+ },
|
|
|
+ chartRef(idx) {
|
|
|
+ return "chart-" + idx
|
|
|
+ },
|
|
|
+ renderCharts() {
|
|
|
+ this.chartInstances.forEach((c) => c && c.dispose())
|
|
|
+ this.chartInstances = []
|
|
|
+ this.cards.forEach((card, idx) => {
|
|
|
+ const el = this.$refs["chart-" + idx]
|
|
|
+ if (!el) return
|
|
|
+ const chart = echarts.init(el)
|
|
|
+ const series = (card.series || []).map((p) => ({ date: p.date, value: p.value || 0 }))
|
|
|
+ chart.setOption({
|
|
|
+ grid: { left: 4, right: 4, top: 8, bottom: 4, containLabel: true },
|
|
|
+ xAxis: { type: "category", data: series.map((p) => p.date), show: false },
|
|
|
+ yAxis: { type: "value", show: false },
|
|
|
+ series: [{ type: "line", data: series.map((p) => p.value), smooth: true, symbol: "none", lineStyle: { width: 1.5 } }]
|
|
|
+ })
|
|
|
+ this.chartInstances.push(chart)
|
|
|
+ })
|
|
|
+ },
|
|
|
+ openModule(key) {
|
|
|
+ if (key === "cf") return
|
|
|
+ this.activeModule = key
|
|
|
+ if (key === "tasks") {
|
|
|
+ this.taskDrawerVisible = true
|
|
|
+ this.loadTasks(1)
|
|
|
+ } else if (key === "plans") {
|
|
|
+ this.planDrawerVisible = true
|
|
|
+ this.loadOverview()
|
|
|
+ } else if (key === "reports") {
|
|
|
+ this.reportDrawerVisible = true
|
|
|
+ this.loadOverview()
|
|
|
+ } else if (key === "messages") {
|
|
|
+ this.messageDrawerVisible = true
|
|
|
+ this.loadConversations(1)
|
|
|
+ } else if (key === "coupons") {
|
|
|
+ this.$router.push({ path: "/admin/family-coupon", query: { familyId: this.familyId } })
|
|
|
+ }
|
|
|
+ },
|
|
|
+ async loadTasks(page) {
|
|
|
+ this.taskPage = page
|
|
|
+ try {
|
|
|
+ const res = await getFamilyTasks(this.familyId, { page, size: this.taskPageSize })
|
|
|
+ this.taskList = res.data.records || []
|
|
|
+ this.taskTotal = res.data.total || 0
|
|
|
+ } catch (error) {
|
|
|
+ this.$message.error(error.message || "加载任务失败")
|
|
|
+ }
|
|
|
+ },
|
|
|
+ async loadOverview() {
|
|
|
+ try {
|
|
|
+ const res = await getFamilyOverview(this.familyId)
|
|
|
+ this.reportList = res.data.reports || []
|
|
|
+ this.planList = res.data.plans || []
|
|
|
+ } catch (error) {
|
|
|
+ this.$message.error(error.message || "加载明细失败")
|
|
|
+ }
|
|
|
+ },
|
|
|
+ openTaskForm(row) {
|
|
|
+ this.taskForm = row ? { ...row } : { title: "", category: "", points: 0, taskType: "once", description: "" }
|
|
|
+ this.taskFormVisible = true
|
|
|
+ },
|
|
|
+ async submitTaskForm() {
|
|
|
+ try {
|
|
|
+ if (this.taskForm.id) {
|
|
|
+ await updateFamilyTask(this.familyId, { ...this.taskForm, taskId: this.taskForm.id })
|
|
|
+ } else {
|
|
|
+ await createFamilyTask(this.familyId, this.taskForm)
|
|
|
+ }
|
|
|
+ this.$message.success("保存成功")
|
|
|
+ this.taskFormVisible = false
|
|
|
+ this.loadTasks(this.taskPage)
|
|
|
+ } catch (error) {
|
|
|
+ this.$message.error(error.message || "保存失败")
|
|
|
+ }
|
|
|
+ },
|
|
|
+ openPlanForm(row) {
|
|
|
+ this.planForm = row ? { ...row } : { planType: "gut", memberName: "", goal: "", totalDays: 30 }
|
|
|
+ this.planFormVisible = true
|
|
|
+ },
|
|
|
+ async submitPlanForm() {
|
|
|
+ try {
|
|
|
+ if (this.planForm.id) {
|
|
|
+ await updateFamilyPlan(this.familyId, { ...this.planForm, planId: this.planForm.id })
|
|
|
+ } else {
|
|
|
+ await createFamilyPlan(this.familyId, this.planForm)
|
|
|
+ }
|
|
|
+ this.$message.success("保存成功")
|
|
|
+ this.planFormVisible = false
|
|
|
+ this.loadOverview()
|
|
|
+ } catch (error) {
|
|
|
+ this.$message.error(error.message || "保存失败")
|
|
|
+ }
|
|
|
+ },
|
|
|
+ async submitReportForm() {
|
|
|
+ try {
|
|
|
+ const data = {
|
|
|
+ personName: this.reportForm.personName,
|
|
|
+ reportType: this.reportForm.reportType,
|
|
|
+ overallScore: this.reportForm.overallScore,
|
|
|
+ remark: this.reportForm.remark
|
|
|
+ }
|
|
|
+ if (this.reportForm.reportDate) {
|
|
|
+ data.reportDate = new Date(this.reportForm.reportDate + "T00:00:00").toISOString()
|
|
|
+ }
|
|
|
+ await createFamilyReport(this.familyId, data)
|
|
|
+ this.$message.success("报告已创建")
|
|
|
+ this.reportFormVisible = false
|
|
|
+ this.loadOverview()
|
|
|
+ this.loadWorkbench()
|
|
|
+ } catch (error) {
|
|
|
+ this.$message.error(error.message || "创建失败")
|
|
|
+ }
|
|
|
+ },
|
|
|
+ async loadConversations(page) {
|
|
|
+ this.conversationPage = page
|
|
|
+ try {
|
|
|
+ const res = await getFamilyMessages(this.familyId, { page, size: 10 })
|
|
|
+ this.conversationList = res.data.records || []
|
|
|
+ this.conversationTotal = res.data.total || 0
|
|
|
+ } catch (error) {
|
|
|
+ this.$message.error(error.message || "加载会话失败")
|
|
|
+ }
|
|
|
+ },
|
|
|
+ async openConversation(row) {
|
|
|
+ this.activeConversationId = row.id
|
|
|
+ try {
|
|
|
+ const res = await getConversationMessages(this.familyId, row.id, { page: 1, size: 50 })
|
|
|
+ this.messageList = res.data.records || []
|
|
|
+ } catch (error) {
|
|
|
+ this.$message.error(error.message || "加载消息失败")
|
|
|
+ }
|
|
|
+ },
|
|
|
+ messageBack() {
|
|
|
+ this.activeConversationId = null
|
|
|
+ this.messageList = []
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.page-container { padding: 24px; }
|
|
|
+.mb-12 { margin-bottom: 12px; }
|
|
|
+.admin-page-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; flex-shrink: 0; }
|
|
|
+.admin-page-title { font-size: 20px; font-weight: 600; color: var(--adm-text-88); }
|
|
|
+.admin-page-subtitle { font-size: 12px; color: var(--adm-text-45); margin-left: 12px; font-weight: 400; }
|
|
|
+.wb-col { margin-bottom: 0; }
|
|
|
+.wb-card { background: #fff; border-radius: 8px; padding: 14px 16px 8px; margin-bottom: 16px; cursor: pointer; border: 1px solid #ebeef5; transition: box-shadow 0.2s; }
|
|
|
+.wb-card:hover { box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08); }
|
|
|
+.wb-card-static { cursor: default; }
|
|
|
+.wb-card-static:hover { box-shadow: none; }
|
|
|
+.wb-card-label { font-size: 13px; color: var(--adm-text-65); }
|
|
|
+.wb-card-total { font-size: 28px; font-weight: 600; color: var(--adm-text-88); margin: 4px 0 6px; }
|
|
|
+.wb-card-chart { height: 48px; }
|
|
|
+.message-timeline { max-height: 60vh; overflow: auto; }
|
|
|
+.message-item { padding: 8px 0; border-bottom: 1px dashed #ebeef5; }
|
|
|
+.message-meta { font-size: 12px; color: var(--adm-text-45); }
|
|
|
+.message-content { margin-top: 4px; white-space: pre-wrap; word-break: break-all; }
|
|
|
+.pagination-right-mt12 { margin-top: 12px; text-align: right; }
|
|
|
+</style>
|