|
@@ -0,0 +1,515 @@
|
|
|
|
|
+<template>
|
|
|
|
|
+ <div class="page-container">
|
|
|
|
|
+ <!-- 页头:返回 + 家庭信息 -->
|
|
|
|
|
+ <div class="admin-page-header">
|
|
|
|
|
+ <div style="display:flex;align-items:center;gap:12px;">
|
|
|
|
|
+ <el-button icon="el-icon-arrow-left" size="small" @click="$router.back()">返回列表</el-button>
|
|
|
|
|
+ <span class="admin-page-title">{{ familyName }}</span>
|
|
|
|
|
+ <span v-if="familyData.inviteCode" class="adm-tag">邀请码 {{ familyData.inviteCode }}</span>
|
|
|
|
|
+ <span v-if="familyData.createdAt" class="adm-sub">{{ formatDate(familyData.createdAt) }}</span>
|
|
|
|
|
+ <span v-if="memberCount != null" class="adm-sub">共 {{ memberCount }} 位成员</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="admin-page-actions">
|
|
|
|
|
+ <el-button type="primary" size="small" @click="refresh" :loading="loading">刷新</el-button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 统计卡片带(点击切换 tab) -->
|
|
|
|
|
+ <div class="stat-cards">
|
|
|
|
|
+ <div
|
|
|
|
|
+ v-for="c in cardList"
|
|
|
|
|
+ :key="c.key"
|
|
|
|
|
+ class="stat-card stat-card--clickable"
|
|
|
|
|
+ :class="{ 'is-active': activeTab === c.key }"
|
|
|
|
|
+ @click="activeTab = c.key"
|
|
|
|
|
+ >
|
|
|
|
|
+ <div class="stat-label">{{ c.label }}</div>
|
|
|
|
|
+ <div class="stat-value adm-num">{{ cardData[c.key] }}</div>
|
|
|
|
|
+ <span v-if="c.badge && cardBadge[c.key] != null" class="badge">{{ cardBadge[c.key] }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- ECharts 指标曲线区 -->
|
|
|
|
|
+ <el-card class="mb-16">
|
|
|
|
|
+ <div slot="header" class="adm-card-header">指标趋势</div>
|
|
|
|
|
+ <div ref="reportChart" class="chart-wrap"></div>
|
|
|
|
|
+ <div ref="cfChart" class="chart-wrap chart-wrap--mt"></div>
|
|
|
|
|
+ </el-card>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- Tab 明细区 -->
|
|
|
|
|
+ <el-card>
|
|
|
|
|
+ <div slot="header" class="adm-card-header">
|
|
|
|
|
+ <span>{{ tabTitle }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <!-- 报告 tab -->
|
|
|
|
|
+ <template v-if="activeTab === 'reports'">
|
|
|
|
|
+ <div class="mb-12"><el-button type="primary" size="mini" @click="goProxyReportUpload">代传报告</el-button></div>
|
|
|
|
|
+ <el-table :data="reports" border class="full-width" max-height="420" v-loading="tableLoading">
|
|
|
|
|
+ <el-table-column label="类型"><template slot-scope="{ row }"><StatusTag :options="reportTypeOptions" :value="row.reportType" /></template></el-table-column>
|
|
|
|
|
+ <el-table-column prop="reportNumber" label="报告编号" width="140" />
|
|
|
|
|
+ <el-table-column label="日期"><template slot-scope="{ row }">{{ formatDate(row.reportDate) }}</template></el-table-column>
|
|
|
|
|
+ <el-table-column prop="personName" label="姓名" />
|
|
|
|
|
+ <el-table-column label="状态"><template slot-scope="{ row }"><StatusTag :options="reportStatusOptions" :value="row.status" /></template></el-table-column>
|
|
|
|
|
+ <el-table-column prop="overallScore" label="总分" width="70" />
|
|
|
|
|
+ <el-table-column prop="gutHealthScore" label="肠道分" width="80" />
|
|
|
|
|
+ </el-table>
|
|
|
|
|
+ <el-empty v-if="!tableLoading && reports.length === 0" description="暂无报告" :image-size="60" />
|
|
|
|
|
+ </template>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 方案 tab -->
|
|
|
|
|
+ <template v-if="activeTab === 'plans'">
|
|
|
|
|
+ <div class="mb-12"><el-button type="primary" size="mini" @click="goPlanManage">方案管理/审核</el-button></div>
|
|
|
|
|
+ <el-table :data="plans" border class="full-width" max-height="420" v-loading="tableLoading">
|
|
|
|
|
+ <el-table-column label="类型"><template slot-scope="{ row }"><StatusTag :options="planTypeOptions" :value="row.planType" /></template></el-table-column>
|
|
|
|
|
+ <el-table-column prop="memberName" label="成员" />
|
|
|
|
|
+ <el-table-column prop="goal" label="目标" show-overflow-tooltip />
|
|
|
|
|
+ <el-table-column label="状态"><template slot-scope="{ row }"><StatusTag :options="planStatusOptions" :value="row.status" /></template></el-table-column>
|
|
|
|
|
+ <el-table-column prop="totalDays" label="天数" width="70" />
|
|
|
|
|
+ <el-table-column label="创建时间"><template slot-scope="{ row }">{{ formatDate(row.createdAt) }}</template></el-table-column>
|
|
|
|
|
+ </el-table>
|
|
|
|
|
+ <el-empty v-if="!tableLoading && plans.length === 0" description="暂无方案" :image-size="60" />
|
|
|
|
|
+ </template>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 任务 tab -->
|
|
|
|
|
+ <template v-if="activeTab === 'tasks'">
|
|
|
|
|
+ <div class="mb-12"><el-button type="primary" size="mini" @click="goTasks">创建任务</el-button></div>
|
|
|
|
|
+ <el-table :data="tasks" border class="full-width" max-height="420" v-loading="tableLoading">
|
|
|
|
|
+ <el-table-column prop="title" label="标题" show-overflow-tooltip />
|
|
|
|
|
+ <el-table-column label="状态"><template slot-scope="{ row }"><StatusTag :options="taskStatusOptions" :value="row.status" /></template></el-table-column>
|
|
|
|
|
+ <el-table-column prop="category" label="分类" width="110" />
|
|
|
|
|
+ <el-table-column prop="points" label="积分" width="70" />
|
|
|
|
|
+ <el-table-column label="截止"><template slot-scope="{ row }">{{ formatDate(row.deadline) }}</template></el-table-column>
|
|
|
|
|
+ </el-table>
|
|
|
|
|
+ <el-pagination class="pagination-right-mt12" @current-change="(p) => loadTasks(p)" :current-page="taskPage" :page-size="taskSize" :total="taskTotal" layout="total, prev, pager, next" />
|
|
|
|
|
+ <el-empty v-if="!tableLoading && tasks.length === 0" description="暂无任务" :image-size="60" />
|
|
|
|
|
+ </template>
|
|
|
|
|
+ <!-- CF值 tab -->
|
|
|
|
|
+ <template v-if="activeTab === 'cf'">
|
|
|
|
|
+ <div class="cf-summary mb-12" v-loading="tableLoading">
|
|
|
|
|
+ <div class="cf-member-row" v-for="m in cfMembers" :key="m.memberId">
|
|
|
|
|
+ <span class="name">{{ m.name }}</span>
|
|
|
|
|
+ <span class="num">可用 {{ m.available || 0 }}</span>
|
|
|
|
|
+ <span class="num frozen">冻结 {{ m.frozen || 0 }}</span>
|
|
|
|
|
+ <span class="num earned">累计 {{ m.totalEarned || 0 }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="cf-total">
|
|
|
|
|
+ <strong>合计</strong> 可用 {{ cfTotalAvailable }} · 冻结 {{ cfTotalFrozen }}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="cf-logs-header">最近流水</div>
|
|
|
|
|
+ <el-table :data="cfLogs" border class="full-width" max-height="300">
|
|
|
|
|
+ <el-table-column prop="userName" label="用户" width="120" />
|
|
|
|
|
+ <el-table-column label="类型" width="90"><template slot-scope="{ row }">{{ cfLogTypeLabel(row.type) }}</template></el-table-column>
|
|
|
|
|
+ <el-table-column label="变动"><template slot-scope="{ row }"><span :class="row.amount >= 0 ? 'pos' : 'neg'">{{ row.amount > 0 ? '+' : '' }}{{ row.amount }}</span></template></el-table-column>
|
|
|
|
|
+ <el-table-column prop="balanceAfter" label="变动后余额" width="110" />
|
|
|
|
|
+ <el-table-column prop="remark" label="备注" show-overflow-tooltip />
|
|
|
|
|
+ <el-table-column label="时间"><template slot-scope="{ row }">{{ formatTime(row.createdAt) }}</template></el-table-column>
|
|
|
|
|
+ </el-table>
|
|
|
|
|
+ <el-pagination class="pagination-right-mt12" @current-change="(p) => loadCfSummary(p)" :current-page="cfLogPage" :page-size="cfLogSize" :total="cfLogTotal" layout="total, prev, pager, next" />
|
|
|
|
|
+ <el-empty v-if="!tableLoading && cfLogs.length === 0" description="暂无流水" :image-size="60" />
|
|
|
|
|
+ </template>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 奖励 tab -->
|
|
|
|
|
+ <template v-if="activeTab === 'rewards'">
|
|
|
|
|
+ <div class="mb-12"><el-button type="primary" size="mini" @click="goRewards">去奖励管理</el-button></div>
|
|
|
|
|
+ <el-table :data="rewards" border class="full-width" max-height="420" v-loading="tableLoading">
|
|
|
|
|
+ <el-table-column prop="title" label="标题" show-overflow-tooltip />
|
|
|
|
|
+ <el-table-column label="状态"><template slot-scope="{ row }"><StatusTag :options="rewardStatusOptions" :value="row.status" /></template></el-table-column>
|
|
|
|
|
+ <el-table-column prop="points" label="积分" width="80" />
|
|
|
|
|
+ <el-table-column label="创建时间"><template slot-scope="{ row }">{{ formatTime(row.createdAt) }}</template></el-table-column>
|
|
|
|
|
+ </el-table>
|
|
|
|
|
+ <el-pagination class="pagination-right-mt12" @current-change="(p) => loadRewards(p)" :current-page="rewardPage" :page-size="rewardSize" :total="rewardTotal" layout="total, prev, pager, next" />
|
|
|
|
|
+ <el-empty v-if="!tableLoading && rewards.length === 0" description="暂无奖励" :image-size="60" />
|
|
|
|
|
+ </template>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 心愿 tab -->
|
|
|
|
|
+ <template v-if="activeTab === 'wishes'">
|
|
|
|
|
+ <div class="mb-12"><el-button type="primary" size="mini" @click="goWishes">去心愿管理</el-button></div>
|
|
|
|
|
+ <el-table :data="wishes" border class="full-width" max-height="420" v-loading="tableLoading">
|
|
|
|
|
+ <el-table-column prop="title" label="标题" show-overflow-tooltip />
|
|
|
|
|
+ <el-table-column label="状态"><template slot-scope="{ row }"><StatusTag :options="wishStatusOptions" :value="row.status" /></template></el-table-column>
|
|
|
|
|
+ <el-table-column prop="pointsRequired" label="所需积分" width="100" />
|
|
|
|
|
+ <el-table-column label="创建时间"><template slot-scope="{ row }">{{ formatTime(row.createdAt) }}</template></el-table-column>
|
|
|
|
|
+ </el-table>
|
|
|
|
|
+ <el-pagination class="pagination-right-mt12" @current-change="(p) => loadWishes(p)" :current-page="wishPage" :page-size="wishSize" :total="wishTotal" layout="total, prev, pager, next" />
|
|
|
|
|
+ <el-empty v-if="!tableLoading && wishes.length === 0" description="暂无心愿" :image-size="60" />
|
|
|
|
|
+ </template>
|
|
|
|
|
+<script>
|
|
|
|
|
+import * as echarts from 'echarts'
|
|
|
|
|
+import { getFamilyOverview, getFamilyTasks, getFamilyMembers, setMemberAdminRole, getFamilyCfSummary, getFamilyMessages, getRewardsList, getWishesList, getPendingWishes } from '@/api/admin'
|
|
|
|
|
+
|
|
|
|
|
+export default {
|
|
|
|
|
+ name: 'FamilyWorkbench',
|
|
|
|
|
+ data() {
|
|
|
|
|
+ return {
|
|
|
|
|
+ loading: false,
|
|
|
|
|
+ tableLoading: false,
|
|
|
|
|
+ familyId: null,
|
|
|
|
|
+ familyData: {},
|
|
|
|
|
+ memberCount: null,
|
|
|
|
|
+ activeTab: 'reports',
|
|
|
|
|
+ reports: [],
|
|
|
|
|
+ plans: [],
|
|
|
|
|
+ tasks: [],
|
|
|
|
|
+ taskPage: 1,
|
|
|
|
|
+ taskSize: 20,
|
|
|
|
|
+ taskTotal: 0,
|
|
|
|
|
+ cfMembers: [],
|
|
|
|
|
+ cfLogs: [],
|
|
|
|
|
+ cfLogPage: 1,
|
|
|
|
|
+ cfLogSize: 20,
|
|
|
|
|
+ cfLogTotal: 0,
|
|
|
|
|
+ rewards: [],
|
|
|
|
|
+ rewardPage: 1,
|
|
|
|
|
+ rewardSize: 20,
|
|
|
|
|
+ rewardTotal: 0,
|
|
|
|
|
+ wishes: [],
|
|
|
|
|
+ wishPage: 1,
|
|
|
|
|
+ wishSize: 20,
|
|
|
|
|
+ wishTotal: 0,
|
|
|
|
|
+ messages: [],
|
|
|
|
|
+ msgPage: 1,
|
|
|
|
|
+ msgSize: 10,
|
|
|
|
|
+ msgTotal: 0,
|
|
|
|
|
+ members: [],
|
|
|
|
|
+ reportChart: null,
|
|
|
|
|
+ cfChart: null
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ computed: {
|
|
|
|
|
+ familyName() {
|
|
|
|
|
+ return this.familyData.name || '家庭工作台'
|
|
|
|
|
+ },
|
|
|
|
|
+ cardList() {
|
|
|
|
|
+ return [
|
|
|
|
|
+ { key: 'members', label: '成员数', badge: null },
|
|
|
|
|
+ { key: 'reports', label: '报告数', badge: 'pending' },
|
|
|
|
|
+ { key: 'plans', label: '方案数', badge: 'pending_review' },
|
|
|
|
|
+ { key: 'tasks', label: '任务数', badge: 'in_progress' },
|
|
|
|
|
+ { key: 'cf', label: 'CF值合计' },
|
|
|
|
|
+ { key: 'rewards', label: '奖励' },
|
|
|
|
|
+ { key: 'wishes', label: '心愿' },
|
|
|
|
|
+ { key: 'messages', label: '咨询消息' }
|
|
|
|
|
+ ]
|
|
|
|
|
+ },
|
|
|
|
|
+ cardData() {
|
|
|
|
|
+ const c = {}
|
|
|
|
|
+ c.members = this.memberCount != null ? this.memberCount : 0
|
|
|
|
|
+ c.reports = this.reports.length
|
|
|
|
|
+ c.plans = this.plans.length
|
|
|
|
|
+ c.tasks = this.taskTotal
|
|
|
|
|
+ if (this.cfTotalAvailable != null) c.cf = this.cfTotalAvailable
|
|
|
|
|
+ else c.cf = 0
|
|
|
|
|
+ c.rewards = this.rewardTotal
|
|
|
|
|
+ c.wishes = this.wishTotal
|
|
|
|
|
+ c.messages = this.msgTotal
|
|
|
|
|
+ return c
|
|
|
|
|
+ },
|
|
|
|
|
+ cardBadge() {
|
|
|
|
|
+ const b = {}
|
|
|
|
|
+ // 待审核方案角标
|
|
|
|
|
+ b.plans = this.plans.filter(p => p.status === 'pending_review').length || null
|
|
|
|
|
+ // 待办进行中任务角标
|
|
|
|
|
+ b.tasks = this.tasks.filter(t => t.status === 'in_progress' || t.status === 'pending').length || null
|
|
|
|
|
+ return b
|
|
|
|
|
+ },
|
|
|
|
|
+ tabTitle() {
|
|
|
|
|
+ const map = {
|
|
|
|
|
+ reports: '报告明细', plans: '方案明细', tasks: '任务明细',
|
|
|
|
|
+ cf: 'CF值汇总', rewards: '奖励列表', wishes: '心愿列表',
|
|
|
|
|
+ messages: '咨询消息', members: '成员列表'
|
|
|
|
|
+ }
|
|
|
|
|
+ return map[this.activeTab] || ''
|
|
|
|
|
+ },
|
|
|
|
|
+ cfTotalAvailable() {
|
|
|
|
|
+ if (!this.cfMembers.length) return null
|
|
|
|
|
+ return this.cfMembers.reduce((s, m) => s + (m.available || 0), 0)
|
|
|
|
|
+ },
|
|
|
|
|
+ cfTotalFrozen() {
|
|
|
|
|
+ if (!this.cfMembers.length) return null
|
|
|
|
|
+ return this.cfMembers.reduce((s, m) => s + (m.frozen || 0), 0)
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ mounted() {
|
|
|
|
|
+ this.familyId = this.$route.query.familyId
|
|
|
|
|
+ if (!this.familyId) { this.$message.error('缺少 familyId'); return }
|
|
|
|
|
+ this.loadAll()
|
|
|
|
|
+ },
|
|
|
|
|
+ methods: {
|
|
|
|
|
+ async refresh() { await this.loadAll() },
|
|
|
|
|
+ <!-- 咨询消息 tab -->
|
|
|
|
|
+ <template v-if="activeTab === 'messages'">
|
|
|
|
|
+ <div class="mb-12"><el-button type="primary" size="mini" @click="goMessages">去消息管理</el-button></div>
|
|
|
|
|
+ <el-table :data="messages" border class="full-width" max-height="420" v-loading="tableLoading">
|
|
|
|
|
+ <el-table-column label="家长" width="120"><template slot-scope="{ row }">{{ row.parentName || row.parentId || '-' }}</template></el-table-column>
|
|
|
|
|
+ <el-table-column label="规划师" width="120"><template slot-scope="{ row }">{{ row.teacherName || row.teacherId || '-' }}</template></el-table-column>
|
|
|
|
|
+ <el-table-column prop="content" label="内容" show-overflow-tooltip />
|
|
|
|
|
+ <el-table-column label="状态" width="90"><template slot-scope="{ row }"><StatusTag :value="row.status" :options="messageStatusOptions" /></template></el-table-column>
|
|
|
|
|
+ <el-table-column label="时间" width="160"><template slot-scope="{ row }">{{ formatTime(row.createdAt) }}</template></el-table-column>
|
|
|
|
|
+ </el-table>
|
|
|
|
|
+ <el-pagination class="pagination-right-mt12" @current-change="(p) => loadMessages(p)" :current-page="msgPage" :page-size="msgSize" :total="msgTotal" layout="total, prev, pager, next" />
|
|
|
|
|
+ <el-empty v-if="!tableLoading && messages.length === 0" description="暂无咨询" :image-size="60" />
|
|
|
|
|
+ </template>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 成员 tab -->
|
|
|
|
|
+ <template v-if="activeTab === 'members'">
|
|
|
|
|
+ <el-table :data="members" border class="full-width" max-height="420" v-loading="tableLoading">
|
|
|
|
|
+ <el-table-column prop="nickname" label="昵称" width="150" />
|
|
|
|
|
+ <el-table-column prop="phone" label="手机号" width="150" />
|
|
|
|
|
+ <el-table-column prop="gender" label="性别" width="80"><template slot-scope="{ row }">{{ row.gender === 'male' ? '男' : row.gender === 'female' ? '女' : '-' }}</template></el-table-column>
|
|
|
|
|
+ <el-table-column prop="age" label="年龄" width="80"><template slot-scope="{ row }">{{ row.age || '-' }}</template></el-table-column>
|
|
|
|
|
+ <el-table-column prop="totalPoints" label="总积分" width="90"><template slot-scope="{ row }"><StatusTag type="success" :text="String(row.totalPoints || 0)" /></template></el-table-column>
|
|
|
|
|
+ <el-table-column label="角色" width="100"><template slot-scope="{ row }"><StatusTag size="small" :type="row.role === 'admin' ? 'danger' : 'info'" :text="row.role === 'admin' ? '管理员' : '成员'" /></template></el-table-column>
|
|
|
|
|
+ <el-table-column label="操作" width="140"><template slot-scope="{ row }"><el-button size="mini" :type="row.role !== 'admin' ? 'primary' : ''" @click="toggleAdmin(row)">{{ row.role === 'admin' ? '撤销管理员' : '设为管理员' }}</el-button></template></el-table-column>
|
|
|
|
|
+ </el-table>
|
|
|
|
|
+ <el-empty v-if="!tableLoading && members.length === 0" description="暂无成员" :image-size="60" />
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-card>
|
|
|
|
|
+ </div>
|
|
|
|
|
+</template>
|
|
|
|
|
+ async loadAll() {
|
|
|
|
|
+ this.loading = true
|
|
|
|
|
+ try {
|
|
|
|
|
+ const [ov, membersRes] = await Promise.all([
|
|
|
|
|
+ getFamilyOverview(this.familyId),
|
|
|
|
|
+ getFamilyMembers(this.familyId)
|
|
|
|
|
+ ])
|
|
|
|
|
+ this.familyData = ov.data.family || {}
|
|
|
|
|
+ const members = (membersRes.data && membersRes.data.members) || []
|
|
|
|
|
+ this.members = members
|
|
|
|
|
+ this.memberCount = members.length
|
|
|
|
|
+ this.reports = (ov.data.reports) || []
|
|
|
|
|
+ this.plans = (ov.data.plans) || []
|
|
|
|
|
+ await Promise.all([
|
|
|
|
|
+ this.loadTasks(1),
|
|
|
|
|
+ this.loadCfSummary(1),
|
|
|
|
|
+ this.loadRewards(1),
|
|
|
|
|
+ this.loadWishes(1),
|
|
|
|
|
+ this.loadMessages(1)
|
|
|
|
|
+ ])
|
|
|
|
|
+ this.renderReportChart()
|
|
|
|
|
+ this.renderCfChart()
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ this.$message.error(e.message || '加载失败')
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ this.loading = false
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ async loadTasks(page) {
|
|
|
|
|
+ this.taskPage = page
|
|
|
|
|
+ this.tableLoading = true
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await getFamilyTasks(this.familyId, { page, size: this.taskSize })
|
|
|
|
|
+ this.tasks = res.data.records || []
|
|
|
|
|
+ this.taskTotal = res.data.total || 0
|
|
|
|
|
+ } catch (e) { this.$message.error(e.message || '加载任务失败') } finally { this.tableLoading = false }
|
|
|
|
|
+ },
|
|
|
|
|
+ async loadCfSummary(page) {
|
|
|
|
|
+ this.cfLogPage = page
|
|
|
|
|
+ this.tableLoading = true
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await getFamilyCfSummary(this.familyId, { page, size: this.cfLogSize })
|
|
|
|
|
+ const data = res.data || {}
|
|
|
|
|
+ this.cfMembers = data.members || []
|
|
|
|
|
+ this.cfLogs = data.logs || []
|
|
|
|
|
+ this.cfLogTotal = data.logTotal != null ? data.logTotal : this.cfLogs.length
|
|
|
|
|
+ } catch (e) { this.$message.error(e.message || '加载CF汇总失败') } finally { this.tableLoading = false }
|
|
|
|
|
+ },
|
|
|
|
|
+ async loadRewards(page) {
|
|
|
|
|
+ this.rewardPage = page
|
|
|
|
|
+ this.tableLoading = true
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await getRewardsList({ familyId: this.familyId, page, size: this.rewardSize })
|
|
|
|
|
+ this.rewards = res.data.records || []
|
|
|
|
|
+ this.rewardTotal = res.data.total || 0
|
|
|
|
|
+ } catch (e) { this.$message.error(e.message || '加载奖励失败') } finally { this.tableLoading = false }
|
|
|
|
|
+ },
|
|
|
|
|
+ async loadWishes(page) {
|
|
|
|
|
+ this.wishPage = page
|
|
|
|
|
+ this.tableLoading = true
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await getWishesList({ familyId: this.familyId, page, size: this.wishSize })
|
|
|
|
|
+ this.wishes = res.data.records || []
|
|
|
|
|
+ this.wishTotal = res.data.total || 0
|
|
|
|
|
+ // 待处理心愿数(用于角标,可选)
|
|
|
|
|
+ try {
|
|
|
|
|
+ const p = await getPendingWishes(this.familyId)
|
|
|
|
|
+ this._pendingWishes = p.data
|
|
|
|
|
+ } catch (_) {}
|
|
|
|
|
+ } catch (e) { this.$message.error(e.message || '加载心愿失败') } finally { this.tableLoading = false }
|
|
|
|
|
+ },
|
|
|
|
|
+ async loadMessages(page) {
|
|
|
|
|
+ this.msgPage = page
|
|
|
|
|
+ this.tableLoading = true
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await getFamilyMessages(this.familyId, { page, size: this.msgSize })
|
|
|
|
|
+ this.messages = res.data.records || []
|
|
|
|
|
+ this.msgTotal = res.data.total || 0
|
|
|
|
|
+ } catch (e) { this.$message.error(e.message || '加载消息失败') } finally { this.tableLoading = false }
|
|
|
|
|
+ },
|
|
|
|
|
+ cfLogTypeLabel(t) {
|
|
|
|
|
+ const map = { earn: '赚取', spend: '消费', freeze: '冻结', unfreeze: '解冻', withdraw: '提现', adjust: '调整', skip: '跳过' }
|
|
|
|
|
+ return map[t] || t || '-'
|
|
|
|
|
+ },
|
|
|
|
|
+ toggleAdmin(row) {
|
|
|
|
|
+ const targetIsAdmin = row.role !== 'admin'
|
|
|
|
|
+ setMemberAdminRole(this.familyId, row.id, targetIsAdmin).then(() => {
|
|
|
|
|
+ this.$message.success(targetIsAdmin ? '已设为管理员' : '已撤销管理员')
|
|
|
|
|
+ this.loadAll()
|
|
|
|
|
+ }).catch(e => { this.$message.error(e.message || '操作失败') })
|
|
|
|
|
+ },
|
|
|
|
|
+ goProxyReportUpload() { this.$router.push({ path: '/report-proxy-upload', query: { familyId: this.familyId } }) },
|
|
|
|
|
+ goPlanManage() { this.$router.push({ path: '/health-plan-manage', query: { familyId: this.familyId } }) },
|
|
|
|
|
+ goTasks() { this.$router.push({ path: '/tasks', query: { familyId: this.familyId } }) },
|
|
|
|
|
+ goRewards() { this.$router.push('/rewards') },
|
|
|
|
|
+ goWishes() { this.$router.push('/wishes') },
|
|
|
|
|
+ goMessages() { this.$router.push('/teacher-messages') },
|
|
|
|
|
+
|
|
|
|
|
+ renderReportChart() {
|
|
|
|
|
+ if (!this.$refs.reportChart) return
|
|
|
|
|
+ // 按 reportDate 聚合数量
|
|
|
|
|
+ const dateMap = {}
|
|
|
|
|
+ ;(this.reports || []).forEach(r => {
|
|
|
|
|
+ const d = r.reportDate || ''
|
|
|
|
|
+ dateMap[d] = (dateMap[d] || 0) + 1
|
|
|
|
|
+ })
|
|
|
|
|
+ const dates = Object.keys(dateMap).sort()
|
|
|
|
|
+ const values = dates.map(d => dateMap[d])
|
|
|
|
|
+ if (!this.reportChart) this.reportChart = echarts.init(this.$refs.reportChart)
|
|
|
|
|
+ this.reportChart.setOption({
|
|
|
|
|
+ title: { text: '报告趋势', left: 'center', textStyle: { fontSize: 13, color: '#666' } },
|
|
|
|
|
+ tooltip: { trigger: 'axis' },
|
|
|
|
|
+ grid: { left: 40, right: 20, top: 50, bottom: 30 },
|
|
|
|
|
+ xAxis: { type: 'category', data: dates, axisLabel: { rotate: 30, fontSize: 10 } },
|
|
|
|
|
+ yAxis: { type: 'value', minInterval: 1 },
|
|
|
|
|
+ series: [{ type: 'line', smooth: true, data: values, areaStyle: {}, itemStyle: { color: '#409eff' } }]
|
|
|
|
|
+ })
|
|
|
|
|
+ },
|
|
|
|
|
+ renderCfChart() {
|
|
|
|
|
+ if (!this.$refs.cfChart) return
|
|
|
|
|
+ const logs = (this.cfLogs || [])
|
|
|
|
|
+ if (!logs.length) {
|
|
|
|
|
+ if (this.cfChart) this.cfChart.clear()
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ const sorted = [...logs].sort((a, b) => {
|
|
|
|
|
+ const da = a.createdAt ? new Date(a.createdAt).getTime() : 0
|
|
|
|
|
+ const db = b.createdAt ? new Date(b.createdAt).getTime() : 0
|
|
|
|
|
+ return da - db
|
|
|
|
|
+ })
|
|
|
|
|
+ const timestamps = sorted.map(l => l.createdAt)
|
|
|
|
|
+ let cum = 0
|
|
|
|
|
+ const cumulative = sorted.map(l => { cum += (l.amount || 0); return cum })
|
|
|
|
|
+ if (!this.cfChart) this.cfChart = echarts.init(this.$refs.cfChart)
|
|
|
|
|
+ this.cfChart.setOption({
|
|
|
|
|
+ title: { text: 'CF 值变动趋势', left: 'center', textStyle: { fontSize: 13, color: '#666' } },
|
|
|
|
|
+ tooltip: { trigger: 'axis' },
|
|
|
|
|
+ grid: { left: 40, right: 20, top: 50, bottom: 30 },
|
|
|
|
|
+ xAxis: { type: 'category', data: timestamps, axisLabel: { rotate: 30, fontSize: 10, formatter: v => (v || '').slice(5, 10) } },
|
|
|
|
|
+ yAxis: { type: 'value' },
|
|
|
|
|
+ series: [{ type: 'line', smooth: true, data: cumulative, areaStyle: {}, itemStyle: { color: '#67c23a' } }]
|
|
|
|
|
+ })
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ formatTime(t) { return t ? t.replace('T', ' ').substring(0, 19) : '-' },
|
|
|
|
|
+ formatDate(d) { return d ? d.substring(0, 10) : '-' },
|
|
|
|
|
+
|
|
|
|
|
+ reportTypeOptions: [
|
|
|
|
|
+ { value: 'gut_flora', label: '菌群检测', type: 'success' },
|
|
|
|
|
+ { value: 'physical_exam', label: '体检报告', type: 'primary' },
|
|
|
|
|
+ { value: 'dan', label: 'DAN测评', type: 'warning' },
|
|
|
|
|
+ { value: 'brain_status', label: '脑状态', type: 'primary' },
|
|
|
|
|
+ { value: 'aptitude', label: '先天潜能', type: 'primary' },
|
|
|
|
|
+ { value: 'insurance', label: '保单', type: 'info' },
|
|
|
|
|
+ { value: 'fortune', label: '家庭周报', type: 'danger' }
|
|
|
|
|
+ ],
|
|
|
|
|
+ reportStatusOptions: [
|
|
|
|
|
+ { value: 'active', label: '生效', type: 'success' },
|
|
|
|
|
+ { value: 'archived', label: '已归档', type: 'warning' },
|
|
|
|
|
+ { value: 'deleted', label: '已删除', type: 'info' }
|
|
|
|
|
+ ],
|
|
|
|
|
+ planTypeOptions: [
|
|
|
|
|
+ { value: 'health_plan', label: '健康方案', type: 'primary' },
|
|
|
|
|
+ { value: 'family_attribution_plan', label: '家庭归因方案', type: 'warning' }
|
|
|
|
|
+ ],
|
|
|
|
|
+ planStatusOptions: [
|
|
|
|
|
+ { value: 'draft', label: '草稿', type: 'info' },
|
|
|
|
|
+ { value: 'pending_review', label: '待审核', type: 'warning' },
|
|
|
|
|
+ { value: 'published', label: '已发布', type: 'success' },
|
|
|
|
|
+ { value: 'rejected', label: '已驳回', type: 'danger' }
|
|
|
|
|
+ ],
|
|
|
|
|
+ taskStatusOptions: [
|
|
|
|
|
+ { value: 'pending', label: '进行中', type: 'warning' },
|
|
|
|
|
+ { value: 'in_progress', label: '进行中', type: 'warning' },
|
|
|
|
|
+ { value: 'completed', label: '已完成', type: 'success' },
|
|
|
|
|
+ { value: 'locked', label: '前置锁定', type: 'info' },
|
|
|
|
|
+ { value: 'assigned', label: '待接受', type: 'info' },
|
|
|
|
|
+ { value: 'rejected', label: '已拒绝', type: 'danger' },
|
|
|
|
|
+ { value: 'overdue', label: '已过期', type: 'danger' },
|
|
|
|
|
+ { value: 'cancelled', label: '已取消', type: 'info' }
|
|
|
|
|
+ ],
|
|
|
|
|
+ rewardStatusOptions: [
|
|
|
|
|
+ { value: 'pending', label: '待发放', type: 'warning' },
|
|
|
|
|
+ { value: 'sent', label: '已发放', type: 'success' },
|
|
|
|
|
+ { value: 'expired', label: '已过期', type: 'info' },
|
|
|
|
|
+ { value: 'rejected', label: '已驳回', type: 'danger' }
|
|
|
|
|
+ ],
|
|
|
|
|
+ wishStatusOptions: [
|
|
|
|
|
+ { value: 'pending', label: '待审批', type: 'warning' },
|
|
|
|
|
+ { value: 'approved', label: '已通过', type: 'success' },
|
|
|
|
|
+ { value: 'rejected', label: '已拒绝', type: 'danger' },
|
|
|
|
|
+ { value: 'fulfilled', label: '已兑换', type: 'primary' },
|
|
|
|
|
+ { value: 'cancelled', label: '已取消', type: 'info' }
|
|
|
|
|
+ ],
|
|
|
|
|
+ messageStatusOptions: [
|
|
|
|
|
+ { value: 'pending', label: '待回复', type: 'warning' },
|
|
|
|
|
+ { value: 'replied', label: '已回复', type: 'success' }
|
|
|
|
|
+ ]
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+</script>
|
|
|
|
|
+<style scoped>
|
|
|
|
|
+.page-container { padding: 24px; }
|
|
|
|
|
+.mb-12 { margin-bottom: 12px; }
|
|
|
|
|
+.mb-16 { margin-bottom: 16px; }
|
|
|
|
|
+.full-width { width: 100%; }
|
|
|
|
|
+.pagination-right-mt12 { margin-top: 12px; text-align: right; }
|
|
|
|
|
+
|
|
|
|
|
+/* 页头 */
|
|
|
|
|
+.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; line-height: 1.4; color: var(--adm-text-88); }
|
|
|
|
|
+.adm-sub { font-size: 12px; color: var(--adm-text-45); margin-left: 8px; }
|
|
|
|
|
+.adm-tag { display: inline-block; font-size: 11px; color: #409eff; background: var(--adm-info-bg); border: 1px solid var(--adm-info-border); border-radius: 4px; padding: 1px 6px; margin-left: 8px; }
|
|
|
|
|
+.adm-card-header { font-weight: 500; color: var(--adm-text-88); display: flex; align-items: center; justify-content: space-between; }
|
|
|
|
|
+.tab-action { font-size: 12px; color: var(--adm-text-45); }
|
|
|
|
|
+
|
|
|
|
|
+/* 统计卡片带 */
|
|
|
|
|
+.stat-cards { display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 16px; flex-shrink: 0; }
|
|
|
|
|
+.stat-card {
|
|
|
|
|
+ flex: 1 1 130px; min-width: 130px;
|
|
|
|
|
+ background: var(--color-bg-card); border: 1px solid var(--adm-border-split);
|
|
|
|
|
+ border-radius: var(--adm-radius-card); padding: 14px 16px; position: relative;
|
|
|
|
|
+}
|
|
|
|
|
+.stat-card--clickable { cursor: pointer; transition: border-color .15s, box-shadow .15s; }
|
|
|
|
|
+.stat-card--clickable:hover { border-color: var(--adm-primary); box-shadow: var(--adm-shadow-popup); }
|
|
|
|
|
+.stat-card.is-active { border-color: var(--adm-primary); background: var(--adm-info-bg); }
|
|
|
|
|
+.stat-label { font-size: 12px; color: var(--adm-text-45); margin-bottom: 8px; }
|
|
|
|
|
+.stat-value { font-size: 22px; font-weight: 600; color: var(--adm-text-88); }
|
|
|
|
|
+.badge { position: absolute; top: 8px; right: 8px; font-size: 11px; background: #f56c6c; color: #fff; border-radius: 10px; padding: 0 6px; line-height: 16px; }
|
|
|
|
|
+
|
|
|
|
|
+/* ECharts 图表 */
|
|
|
|
|
+.chart-wrap { width: 100%; height: 220px; }
|
|
|
|
|
+.chart-wrap--mt { margin-top: 16px; }
|
|
|
|
|
+
|
|
|
|
|
+/* CF 汇总 */
|
|
|
|
|
+.cf-summary { margin-bottom: 12px; }
|
|
|
|
|
+.cf-member-row { display: flex; align-items: center; gap: 16px; padding: 6px 0; border-bottom: 1px solid var(--adm-border-split); }
|
|
|
|
|
+.cf-member-row .name { flex: 1; font-size: 13px; color: var(--adm-text-88); }
|
|
|
|
|
+.cf-member-row .num { font-size: 13px; color: var(--adm-text-65); }
|
|
|
|
|
+.cf-member-row .num.frozen { color: #e6a23c; }
|
|
|
|
|
+.cf-member-row .num.earned { color: var(--adm-success); }
|
|
|
|
|
+.cf-total { font-size: 13px; color: var(--adm-text-88); padding: 8px 0; }
|
|
|
|
|
+.cf-logs-header { font-size: 13px; color: var(--adm-text-45); margin-bottom: 8px; }
|
|
|
|
|
+.pos { color: var(--adm-success); }
|
|
|
|
|
+.neg { color: #f56c6c; }
|
|
|
|
|
+</style>
|