Преглед изворни кода

feat: 完善Web管理端页面和API集成

- 新增Families.vue和TaskTemplates.vue管理页面
- 完善Users, Children, Tasks, Rewards, Points页面功能
- 集成后台API接口,实现CRUD操作
- 优化Layout.vue菜单结构和路由配置
- 更新Login.vue登录逻辑
User пре 5 месеци
родитељ
комит
bc92166fe6

+ 273 - 0
zxyj-web/src/api/admin.js

@@ -0,0 +1,273 @@
+import request from '@/utils/request'
+
+// 用户管理
+export function getUserList(params) {
+  return request({
+    url: '/api/admin/users',
+    method: 'get',
+    params
+  })
+}
+
+export function getUserDetail(id) {
+  return request({
+    url: `/api/admin/users/${id}`,
+    method: 'get'
+  })
+}
+
+export function updateUser(id, data) {
+  return request({
+    url: `/api/admin/users/${id}`,
+    method: 'put',
+    data
+  })
+}
+
+export function deleteUser(id) {
+  return request({
+    url: `/api/admin/users/${id}`,
+    method: 'delete'
+  })
+}
+
+// 家庭管理
+export function getFamilyList(params) {
+  return request({
+    url: '/api/admin/families',
+    method: 'get',
+    params
+  })
+}
+
+export function getFamilyDetail(id) {
+  return request({
+    url: `/api/admin/families/${id}`,
+    method: 'get'
+  })
+}
+
+export function updateFamily(id, data) {
+  return request({
+    url: `/api/admin/families/${id}`,
+    method: 'put',
+    data
+  })
+}
+
+export function deleteFamily(id) {
+  return request({
+    url: `/api/admin/families/${id}`,
+    method: 'delete'
+  })
+}
+
+export function getFamilyMembers(id) {
+  return request({
+    url: `/api/admin/families/${id}/members`,
+    method: 'get'
+  })
+}
+
+// 任务模板管理
+export function getTaskTemplateList(params) {
+  return request({
+    url: '/api/admin/task-templates',
+    method: 'get',
+    params
+  })
+}
+
+export function createTaskTemplate(data) {
+  return request({
+    url: '/api/admin/task-templates',
+    method: 'post',
+    data
+  })
+}
+
+export function updateTaskTemplate(id, data) {
+  return request({
+    url: `/api/admin/task-templates/${id}`,
+    method: 'put',
+    data
+  })
+}
+
+export function deleteTaskTemplate(id) {
+  return request({
+    url: `/api/admin/task-templates/${id}`,
+    method: 'delete'
+  })
+}
+
+// 孩子管理
+export function getChildrenList(params) {
+  return request({
+    url: '/api/admin/children',
+    method: 'get',
+    params
+  })
+}
+
+export function getChildDetail(id) {
+  return request({
+    url: `/api/admin/children/${id}`,
+    method: 'get'
+  })
+}
+
+export function updateChild(id, data) {
+  return request({
+    url: `/api/admin/children/${id}`,
+    method: 'put',
+    data
+  })
+}
+
+export function deleteChild(id) {
+  return request({
+    url: `/api/admin/children/${id}`,
+    method: 'delete'
+  })
+}
+
+export function adjustChildPoints(id, data) {
+  return request({
+    url: `/api/admin/children/${id}/adjust-points`,
+    method: 'post',
+    data
+  })
+}
+
+// 任务管理
+export function getTasksList(params) {
+  return request({
+    url: '/api/admin/tasks',
+    method: 'get',
+    params
+  })
+}
+
+export function getTaskStats() {
+  return request({
+    url: '/api/admin/tasks/stats',
+    method: 'get'
+  })
+}
+
+// 奖励管理
+export function getRewardsList(params) {
+  return request({
+    url: '/api/admin/rewards',
+    method: 'get',
+    params
+  })
+}
+
+export function getRewardTemplates() {
+  return request({
+    url: '/api/admin/rewards/templates',
+    method: 'get'
+  })
+}
+
+export function createRewardTemplate(data) {
+  return request({
+    url: '/api/admin/rewards/templates',
+    method: 'post',
+    data
+  })
+}
+
+export function deleteRewardTemplate(id) {
+  return request({
+    url: `/api/admin/rewards/templates/${id}`,
+    method: 'delete'
+  })
+}
+
+export function updateRewardTemplate(id, data) {
+  return request({
+    url: `/api/admin/rewards/templates/${id}`,
+    method: 'put',
+    data
+  })
+}
+
+export function approveReward(id, data) {
+  return request({
+    url: `/api/admin/rewards/${id}/approve`,
+    method: 'post',
+    data
+  })
+}
+
+// 积分管理
+export function getPointsLogs(params) {
+  return request({
+    url: '/api/admin/points/logs',
+    method: 'get',
+    params
+  })
+}
+
+export function getPointsStats() {
+  return request({
+    url: '/api/admin/points/stats',
+    method: 'get'
+  })
+}
+
+// ========== 指导师相关 API ==========
+
+// 获取指导师关联的家庭列表
+export function getTeacherFamilies() {
+  return request({
+    url: '/api/admin/teacher/families',
+    method: 'get'
+  })
+}
+
+// 获取指导师发布的任务模板
+export function getTeacherTaskTemplates() {
+  return request({
+    url: '/api/admin/teacher/task-templates',
+    method: 'get'
+  })
+}
+
+// 指导师创建任务模板
+export function createTeacherTaskTemplate(data) {
+  return request({
+    url: '/api/admin/teacher/task-templates',
+    method: 'post',
+    data
+  })
+}
+
+// 指导师删除任务模板
+export function deleteTeacherTaskTemplate(id) {
+  return request({
+    url: `/api/admin/teacher/task-templates/${id}`,
+    method: 'delete'
+  })
+}
+
+// 获取指定家庭的任务列表
+export function getTeacherFamilyTasks(familyId, params) {
+  return request({
+    url: `/api/admin/teacher/families/${familyId}/tasks`,
+    method: 'get',
+    params
+  })
+}
+
+// 获取指定家庭的奖励列表
+export function getTeacherFamilyRewards(familyId, params) {
+  return request({
+    url: `/api/admin/teacher/families/${familyId}/rewards`,
+    method: 'get',
+    params
+  })
+}

+ 12 - 0
zxyj-web/src/router/index.js

@@ -27,6 +27,12 @@ const routes = [
         component: () => import('@/views/Users.vue'),
         meta: { title: '用户管理' }
       },
+      {
+        path: 'families',
+        name: 'Families',
+        component: () => import('@/views/Families.vue'),
+        meta: { title: '家庭管理' }
+      },
       {
         path: 'children',
         name: 'Children',
@@ -39,6 +45,12 @@ const routes = [
         component: () => import('@/views/Tasks.vue'),
         meta: { title: '任务管理' }
       },
+      {
+        path: 'task-templates',
+        name: 'TaskTemplates',
+        component: () => import('@/views/TaskTemplates.vue'),
+        meta: { title: '任务模板' }
+      },
       {
         path: 'rewards',
         name: 'Rewards',

+ 261 - 23
zxyj-web/src/views/Children.vue

@@ -3,69 +3,307 @@
     <el-card>
       <div slot="header">
         <span>孩子管理</span>
+        <el-button
+          type="primary"
+          size="small"
+          style="float: right"
+          @click="handleRefresh"
+          :loading="loading"
+        >
+          刷新
+        </el-button>
       </div>
-      <el-table :data="tableData" border>
-        <el-table-column prop="id" label="ID" width="60"></el-table-column>
-        <el-table-column prop="nickname" label="昵称"></el-table-column>
-        <el-table-column prop="age" label="年龄"></el-table-column>
-        <el-table-column prop="danLevel" label="DAN等级"></el-table-column>
-        <el-table-column prop="totalPoints" label="当前积分"></el-table-column>
-        <el-table-column prop="streakDays" label="连续打卡"></el-table-column>
-        <el-table-column prop="penaltyEnabled" label="扣分">
+
+      <!-- 搜索筛选 -->
+      <el-form :inline="true" :model="searchForm" style="margin-bottom: 20px">
+        <el-form-item label="昵称">
+          <el-input
+            v-model="searchForm.nickname"
+            placeholder="请输入昵称"
+            clearable
+            @keyup.enter.native="handleSearch"
+          ></el-input>
+        </el-form-item>
+        <el-form-item label="家庭ID">
+          <el-input
+            v-model="searchForm.familyId"
+            placeholder="请输入家庭ID"
+            clearable
+            @keyup.enter.native="handleSearch"
+          ></el-input>
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" @click="handleSearch">搜索</el-button>
+          <el-button @click="handleReset">重置</el-button>
+        </el-form-item>
+      </el-form>
+
+      <!-- 孩子列表 -->
+      <el-table :data="tableData" border v-loading="loading" style="width: 100%">
+        <el-table-column prop="id" label="ID" width="80"></el-table-column>
+        <el-table-column prop="nickname" label="昵称" width="150"></el-table-column>
+        <el-table-column prop="age" label="年龄" width="80"></el-table-column>
+        <el-table-column prop="danLevel" label="DAN等级" width="100">
+          <template slot-scope="scope">
+            <el-tag v-if="scope.row.danLevel" type="success" size="small">
+              {{ scope.row.danLevel }}
+            </el-tag>
+            <span v-else>-</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="totalPoints" label="当前积分" width="120">
+          <template slot-scope="scope">
+            <el-tag type="success">{{ scope.row.totalPoints || 0 }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column prop="streakDays" label="连续打卡" width="100">
+          <template slot-scope="scope">
+            <span>{{ scope.row.streakDays || 0 }}天</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="familyId" label="家庭ID" width="100"></el-table-column>
+        <el-table-column prop="penaltyEnabled" label="扣分" width="80">
           <template slot-scope="scope">
             <el-tag :type="scope.row.penaltyEnabled ? 'danger' : 'info'" size="small">
               {{ scope.row.penaltyEnabled ? '开启' : '关闭' }}
             </el-tag>
           </template>
         </el-table-column>
-        <el-table-column label="操作" width="180">
+        <el-table-column prop="createdAt" label="创建时间" width="180">
+          <template slot-scope="scope">
+            {{ formatDate(scope.row.createdAt) }}
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" width="180" fixed="right">
           <template slot-scope="scope">
-            <el-button type="text" size="small" @click="handleAdjustPoints(scope.row)">调分</el-button>
-            <el-button type="text" size="small">编辑</el-button>
+            <el-button type="text" size="small" @click="handleAdjustPoints(scope.row)">
+              调分
+            </el-button>
+            <el-button type="text" size="small" @click="handleEdit(scope.row)">
+              编辑
+            </el-button>
           </template>
         </el-table-column>
       </el-table>
+
+      <!-- 分页 -->
+      <el-pagination
+        style="margin-top: 20px; text-align: right"
+        @size-change="handleSizeChange"
+        @current-change="handleCurrentChange"
+        :current-page="pagination.current"
+        :page-sizes="[10, 20, 50, 100]"
+        :page-size="pagination.size"
+        :total="pagination.total"
+        layout="total, sizes, prev, pager, next, jumper"
+      ></el-pagination>
     </el-card>
 
+    <!-- 调整积分对话框 -->
     <el-dialog title="调整积分" :visible.sync="dialogVisible" width="400px">
-      <el-form :model="pointsForm">
-        <el-form-item label="调整积分">
-          <el-input-number v-model="pointsForm.amount" :min="-100" :max="100"></el-input-number>
+      <el-form :model="pointsForm" :rules="pointsRules" ref="pointsForm" label-width="100px">
+        <el-form-item label="当前积分">
+          <span>{{ currentChild ? currentChild.totalPoints : 0 }}</span>
+        </el-form-item>
+        <el-form-item label="调整积分" prop="amount">
+          <el-input-number
+            v-model="pointsForm.amount"
+            :min="-1000"
+            :max="1000"
+            placeholder="正数为加分,负数为扣分"
+          ></el-input-number>
         </el-form-item>
-        <el-form-item label="原因">
-          <el-input v-model="pointsForm.reason" placeholder="请输入原因"></el-input>
+        <el-form-item label="原因" prop="reason">
+          <el-input
+            type="textarea"
+            v-model="pointsForm.reason"
+            placeholder="请输入调整原因"
+            :rows="3"
+          ></el-input>
         </el-form-item>
       </el-form>
       <div slot="footer">
         <el-button @click="dialogVisible = false">取消</el-button>
-        <el-button type="primary" @click="submitPoints">确定</el-button>
+        <el-button type="primary" @click="submitPoints" :loading="submitting">确定</el-button>
+      </div>
+    </el-dialog>
+
+    <!-- 编辑孩子对话框 -->
+    <el-dialog title="编辑孩子" :visible.sync="editDialogVisible" width="500px">
+      <el-form :model="editForm" :rules="editRules" ref="editForm" label-width="100px">
+        <el-form-item label="昵称" prop="nickname">
+          <el-input v-model="editForm.nickname" placeholder="请输入昵称"></el-input>
+        </el-form-item>
+        <el-form-item label="年龄" prop="age">
+          <el-input-number v-model="editForm.age" :min="0" :max="18"></el-input-number>
+        </el-form-item>
+        <el-form-item label="扣分开关">
+          <el-switch v-model="editForm.penaltyEnabled" :active-value="1" :inactive-value="0"></el-switch>
+        </el-form-item>
+      </el-form>
+      <div slot="footer">
+        <el-button @click="editDialogVisible = false">取消</el-button>
+        <el-button type="primary" @click="submitEdit" :loading="submitting">确定</el-button>
       </div>
     </el-dialog>
   </div>
 </template>
 
 <script>
+import { getChildrenList, adjustChildPoints, updateChild } from '@/api/admin'
+
 export default {
+  name: 'Children',
   data() {
     return {
-      tableData: [
-        { id: 1, nickname: '小明', age: 6, danLevel: 'B1', totalPoints: 150, streakDays: 5, penaltyEnabled: 1 }
-      ],
+      loading: false,
+      submitting: false,
+      tableData: [],
+      searchForm: {
+        nickname: '',
+        familyId: ''
+      },
+      pagination: {
+        current: 1,
+        size: 10,
+        total: 0
+      },
       dialogVisible: false,
+      editDialogVisible: false,
+      currentChild: null,
       pointsForm: {
         amount: 0,
         reason: ''
+      },
+      pointsRules: {
+        amount: [
+          { required: true, message: '请输入调整积分', trigger: 'blur' }
+        ],
+        reason: [
+          { required: true, message: '请输入调整原因', trigger: 'blur' }
+        ]
+      },
+      editForm: {
+        id: null,
+        nickname: '',
+        age: 0,
+        penaltyEnabled: 1
+      },
+      editRules: {
+        nickname: [
+          { required: true, message: '请输入昵称', trigger: 'blur' }
+        ],
+        age: [
+          { required: true, message: '请输入年龄', trigger: 'blur' }
+        ]
       }
     }
   },
+  mounted() {
+    this.loadChildren()
+  },
   methods: {
+    async loadChildren() {
+      this.loading = true
+      try {
+        const params = {
+          page: this.pagination.current,
+          size: this.pagination.size,
+          ...this.searchForm
+        }
+        const res = await getChildrenList(params)
+        this.tableData = res.data.records || res.data || []
+        this.pagination.total = res.data.total || this.tableData.length
+      } catch (error) {
+        this.$message.error(error.message || '加载孩子列表失败')
+      } finally {
+        this.loading = false
+      }
+    },
+    handleSearch() {
+      this.pagination.current = 1
+      this.loadChildren()
+    },
+    handleReset() {
+      this.searchForm = {
+        nickname: '',
+        familyId: ''
+      }
+      this.handleSearch()
+    },
+    handleRefresh() {
+      this.loadChildren()
+    },
+    handleSizeChange(val) {
+      this.pagination.size = val
+      this.loadChildren()
+    },
+    handleCurrentChange(val) {
+      this.pagination.current = val
+      this.loadChildren()
+    },
     handleAdjustPoints(row) {
+      this.currentChild = row
+      this.pointsForm = {
+        amount: 0,
+        reason: ''
+      }
       this.dialogVisible = true
     },
-    submitPoints() {
-      this.$message.success('积分调整成功')
-      this.dialogVisible = false
+    async submitPoints() {
+      this.$refs.pointsForm.validate(async (valid) => {
+        if (valid) {
+          this.submitting = true
+          try {
+            await adjustChildPoints(this.currentChild.id, this.pointsForm)
+            this.$message.success('积分调整成功')
+            this.dialogVisible = false
+            this.loadChildren()
+          } catch (error) {
+            this.$message.error(error.message || '积分调整失败')
+          } finally {
+            this.submitting = false
+          }
+        }
+      })
+    },
+    handleEdit(row) {
+      this.editForm = {
+        id: row.id,
+        nickname: row.nickname,
+        age: row.age,
+        penaltyEnabled: row.penaltyEnabled
+      }
+      this.editDialogVisible = true
+    },
+    async submitEdit() {
+      this.$refs.editForm.validate(async (valid) => {
+        if (valid) {
+          this.submitting = true
+          try {
+            await updateChild(this.editForm.id, this.editForm)
+            this.$message.success('更新成功')
+            this.editDialogVisible = false
+            this.loadChildren()
+          } catch (error) {
+            this.$message.error(error.message || '更新失败')
+          } finally {
+            this.submitting = false
+          }
+        }
+      })
+    },
+    formatDate(dateStr) {
+      if (!dateStr) return '-'
+      const date = new Date(dateStr)
+      return date.toLocaleString('zh-CN')
     }
   }
 }
 </script>
+
+<style scoped>
+.page-container {
+  padding: 20px;
+}
+</style>

+ 102 - 27
zxyj-web/src/views/Dashboard.vue

@@ -1,5 +1,6 @@
 <template>
   <div class="dashboard">
+    <!-- 统计卡片 -->
     <el-row :gutter="20">
       <el-col :span="6">
         <el-card class="stat-card">
@@ -7,7 +8,7 @@
             <i class="el-icon-user"></i>
           </div>
           <div class="stat-info">
-            <div class="stat-value">{{ stats.totalUsers }}</div>
+            <div class="stat-value">{{ stats.totalUsers || 0 }}</div>
             <div class="stat-label">用户总数</div>
           </div>
         </el-card>
@@ -18,7 +19,7 @@
             <i class="el-icon-s-custom"></i>
           </div>
           <div class="stat-info">
-            <div class="stat-value">{{ stats.totalChildren }}</div>
+            <div class="stat-value">{{ stats.totalChildren || 0 }}</div>
             <div class="stat-label">孩子数量</div>
           </div>
         </el-card>
@@ -29,8 +30,8 @@
             <i class="el-icon-s-order"></i>
           </div>
           <div class="stat-info">
-            <div class="stat-value">{{ stats.todayTasks }}</div>
-            <div class="stat-label">今日任务</div>
+            <div class="stat-value">{{ taskStats.total || 0 }}</div>
+            <div class="stat-label">总任务数</div>
           </div>
         </el-card>
       </el-col>
@@ -40,37 +41,61 @@
             <i class="el-icon-s-goods"></i>
           </div>
           <div class="stat-info">
-            <div class="stat-value">{{ stats.pendingRewards }}</div>
+            <div class="stat-value">{{ stats.pendingRewards || 0 }}</div>
             <div class="stat-label">待审批奖励</div>
           </div>
         </el-card>
       </el-col>
     </el-row>
 
+    <!-- 内容区域 -->
     <el-row :gutter="20" style="margin-top: 20px">
       <el-col :span="12">
         <el-card>
-          <div slot="header">最近任务</div>
-          <el-table :data="recentTasks" size="small">
-            <el-table-column prop="title" label="任务名称"></el-table-column>
-            <el-table-column prop="status" label="状态">
+          <div slot="header">
+            <span>最近任务</span>
+            <el-button type="text" size="small" style="float: right" @click="$router.push('/tasks')">
+              查看全部
+            </el-button>
+          </div>
+          <el-table :data="recentTasks" size="small" v-loading="loadingTasks">
+            <el-table-column prop="title" label="任务名称" min-width="150"></el-table-column>
+            <el-table-column prop="status" label="状态" width="100">
               <template slot-scope="scope">
-                <el-tag :type="scope.row.status === 'completed' ? 'success' : 'warning'" size="small">
+                <el-tag
+                  :type="scope.row.status === 'completed' ? 'success' : 'warning'"
+                  size="small"
+                >
                   {{ scope.row.status === 'completed' ? '已完成' : '进行中' }}
                 </el-tag>
               </template>
             </el-table-column>
-            <el-table-column prop="points" label="积分"></el-table-column>
+            <el-table-column prop="points" label="积分" width="80">
+              <template slot-scope="scope">
+                <el-tag type="success" size="small">{{ scope.row.points }}</el-tag>
+              </template>
+            </el-table-column>
           </el-table>
         </el-card>
       </el-col>
       <el-col :span="12">
         <el-card>
-          <div slot="header">系统公告</div>
-          <el-alert title="系统运行正常" type="success" :closable="false"></el-alert>
+          <div slot="header">系统信息</div>
+          <el-alert
+            title="系统运行正常"
+            type="success"
+            :closable="false"
+            style="margin-bottom: 20px"
+          ></el-alert>
           <el-divider></el-divider>
-          <p>欢迎使用心知家庭管理后台</p>
-          <p style="color: #999; font-size: 12px">版本: 1.0.0</p>
+          <p><strong>系统名称:</strong>知心益家管理后台</p>
+          <p><strong>版本:</strong>1.0.0</p>
+          <p><strong>技术栈:</strong>Vue 2.6 + Element UI 2.15 + Spring Boot 2.7</p>
+          <el-divider></el-divider>
+          <p style="color: #909399; font-size: 14px">
+            <i class="el-icon-info"></i>
+            管理员可以在此管理用户、家庭、孩子、任务和奖励等数据。
+          </p>
         </el-card>
       </el-col>
     </el-row>
@@ -78,31 +103,81 @@
 </template>
 
 <script>
+import { getUserList, getChildrenList, getRewardsList, getTasksList, getTaskStats } from '@/api/admin'
+
 export default {
+  name: 'Dashboard',
   data() {
     return {
+      loadingTasks: false,
       stats: {
         totalUsers: 0,
         totalChildren: 0,
-        todayTasks: 0,
         pendingRewards: 0
       },
+      taskStats: {
+        total: 0,
+        today: 0
+      },
       recentTasks: []
     }
   },
   mounted() {
-    // 模拟数据
-    this.stats = {
-      totalUsers: 10,
-      totalChildren: 12,
-      todayTasks: 25,
-      pendingRewards: 3
+    this.loadDashboardData()
+  },
+  methods: {
+    async loadDashboardData() {
+      await Promise.all([
+        this.loadUserStats(),
+        this.loadChildrenStats(),
+        this.loadRewardsStats(),
+        this.loadTaskStats(),
+        this.loadRecentTasks()
+      ])
+    },
+    async loadUserStats() {
+      try {
+        const res = await getUserList({ page: 1, size: 1 })
+        this.stats.totalUsers = res.data.total || 0
+      } catch (error) {
+        console.error('加载用户统计失败:', error)
+      }
+    },
+    async loadChildrenStats() {
+      try {
+        const res = await getChildrenList({ page: 1, size: 1 })
+        this.stats.totalChildren = res.data.total || 0
+      } catch (error) {
+        console.error('加载孩子统计失败:', error)
+      }
+    },
+    async loadRewardsStats() {
+      try {
+        const res = await getRewardsList({ page: 1, size: 1, status: 'pending' })
+        this.stats.pendingRewards = res.data.total || 0
+      } catch (error) {
+        console.error('加载奖励统计失败:', error)
+      }
+    },
+    async loadTaskStats() {
+      try {
+        const res = await getTaskStats()
+        this.taskStats = res.data || { total: 0, today: 0 }
+      } catch (error) {
+        console.error('加载任务统计失败:', error)
+      }
+    },
+    async loadRecentTasks() {
+      this.loadingTasks = true
+      try {
+        const res = await getTasksList({ page: 1, size: 5 })
+        this.recentTasks = (res.data.records || res.data || []).slice(0, 5)
+      } catch (error) {
+        console.error('加载最近任务失败:', error)
+      } finally {
+        this.loadingTasks = false
+      }
     }
-    this.recentTasks = [
-      { title: '完成作业', status: 'completed', points: 5 },
-      { title: '收拾玩具', status: 'pending', points: 3 },
-      { title: '阅读绘本', status: 'completed', points: 4 }
-    ]
   }
 }
 </script>

+ 312 - 0
zxyj-web/src/views/Families.vue

@@ -0,0 +1,312 @@
+<template>
+  <div class="page-container">
+    <el-card>
+      <div slot="header">
+        <span>家庭管理</span>
+        <el-button
+          type="primary"
+          size="small"
+          style="float: right"
+          @click="handleRefresh"
+          :loading="loading"
+        >
+          刷新
+        </el-button>
+      </div>
+
+      <!-- 搜索 -->
+      <el-form :inline="true" :model="searchForm" style="margin-bottom: 20px">
+        <el-form-item label="家庭名称">
+          <el-input
+            v-model="searchForm.name"
+            placeholder="请输入家庭名称"
+            clearable
+            @keyup.enter.native="handleSearch"
+          ></el-input>
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" @click="handleSearch">搜索</el-button>
+          <el-button @click="handleReset">重置</el-button>
+        </el-form-item>
+      </el-form>
+
+      <!-- 家庭列表 -->
+      <el-table
+        :data="tableData"
+        border
+        v-loading="loading"
+        style="width: 100%"
+      >
+        <el-table-column prop="id" label="ID" width="80"></el-table-column>
+        <el-table-column prop="name" label="家庭名称" width="200"></el-table-column>
+        <el-table-column prop="inviteCode" label="邀请码" width="150"></el-table-column>
+        <el-table-column prop="createdAt" label="创建时间" width="180">
+          <template slot-scope="scope">
+            {{ formatDate(scope.row.createdAt) }}
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" width="250" fixed="right">
+          <template slot-scope="scope">
+            <el-button
+              type="text"
+              size="small"
+              @click="handleViewMembers(scope.row)"
+            >
+              查看成员
+            </el-button>
+            <el-button
+              type="text"
+              size="small"
+              @click="handleEdit(scope.row)"
+            >
+              编辑
+            </el-button>
+            <el-button
+              type="text"
+              size="small"
+              style="color: #F56C6C"
+              @click="handleDelete(scope.row)"
+            >
+              删除
+            </el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <!-- 分页 -->
+      <el-pagination
+        style="margin-top: 20px; text-align: right"
+        @size-change="handleSizeChange"
+        @current-change="handleCurrentChange"
+        :current-page="pagination.current"
+        :page-sizes="[10, 20, 50, 100]"
+        :page-size="pagination.size"
+        :total="pagination.total"
+        layout="total, sizes, prev, pager, next, jumper"
+      ></el-pagination>
+    </el-card>
+
+    <!-- 编辑家庭弹窗 -->
+    <el-dialog
+      title="编辑家庭"
+      :visible.sync="editDialogVisible"
+      width="500px"
+    >
+      <el-form :model="editForm" :rules="editRules" ref="editForm" label-width="100px">
+        <el-form-item label="家庭名称" prop="name">
+          <el-input v-model="editForm.name" placeholder="请输入家庭名称"></el-input>
+        </el-form-item>
+      </el-form>
+      <div slot="footer">
+        <el-button @click="editDialogVisible = false">取消</el-button>
+        <el-button type="primary" @click="submitEdit" :loading="submitting">确定</el-button>
+      </div>
+    </el-dialog>
+
+    <!-- 查看成员弹窗 -->
+    <el-dialog
+      title="家庭成员"
+      :visible.sync="membersDialogVisible"
+      width="800px"
+    >
+      <div v-if="membersData.family">
+        <el-descriptions :column="2" border style="margin-bottom: 20px">
+          <el-descriptions-item label="家庭名称">{{ membersData.family.name }}</el-descriptions-item>
+          <el-descriptions-item label="邀请码">{{ membersData.family.inviteCode }}</el-descriptions-item>
+        </el-descriptions>
+
+        <el-divider content-position="left">家长</el-divider>
+        <el-table
+          :data="membersData.parents"
+          border
+          style="width: 100%; margin-bottom: 20px"
+          v-if="membersData.parents && membersData.parents.length > 0"
+        >
+          <el-table-column prop="id" label="ID" width="80"></el-table-column>
+          <el-table-column prop="nickname" label="昵称" width="150"></el-table-column>
+          <el-table-column prop="realName" label="真实姓名" width="120"></el-table-column>
+          <el-table-column prop="phone" label="手机号" width="150"></el-table-column>
+        </el-table>
+        <el-empty v-else description="暂无家长" :image-size="60"></el-empty>
+
+        <el-divider content-position="left">孩子</el-divider>
+        <el-table
+          :data="membersData.children"
+          border
+          style="width: 100%"
+          v-if="membersData.children && membersData.children.length > 0"
+        >
+          <el-table-column prop="id" label="ID" width="80"></el-table-column>
+          <el-table-column prop="nickname" label="昵称" width="150"></el-table-column>
+          <el-table-column prop="totalPoints" label="总积分" width="100">
+            <template slot-scope="scope">
+              <el-tag type="success">{{ scope.row.totalPoints || 0 }}</el-tag>
+            </template>
+          </el-table-column>
+          <el-table-column prop="createdAt" label="创建时间">
+            <template slot-scope="scope">
+              {{ formatDate(scope.row.createdAt) }}
+            </template>
+          </el-table-column>
+        </el-table>
+        <el-empty v-else description="暂无孩子" :image-size="60"></el-empty>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { getFamilyList, getFamilyMembers, updateFamily, deleteFamily } from '@/api/admin'
+
+export default {
+  name: 'Families',
+  data() {
+    return {
+      loading: false,
+      submitting: false,
+      tableData: [],
+      searchForm: {
+        name: ''
+      },
+      pagination: {
+        current: 1,
+        size: 10,
+        total: 0
+      },
+      editDialogVisible: false,
+      editForm: {
+        id: null,
+        name: ''
+      },
+      editRules: {
+        name: [
+          { required: true, message: '请输入家庭名称', trigger: 'blur' }
+        ]
+      },
+      membersDialogVisible: false,
+      membersData: {
+        family: null,
+        parents: [],
+        children: []
+      }
+    }
+  },
+  mounted() {
+    // 检查是否有从用户管理跳转过来的familyId参数
+    const familyId = this.$route.query.familyId
+    if (familyId) {
+      this.handleViewMembersById(parseInt(familyId))
+    }
+    this.loadFamilies()
+  },
+  methods: {
+    async loadFamilies() {
+      this.loading = true
+      try {
+        const params = {
+          page: this.pagination.current,
+          size: this.pagination.size,
+          ...this.searchForm
+        }
+        const res = await getFamilyList(params)
+        this.tableData = res.data.records || res.data || []
+        this.pagination.total = res.data.total || this.tableData.length
+      } catch (error) {
+        this.$message.error(error.message || '加载家庭列表失败')
+      } finally {
+        this.loading = false
+      }
+    },
+    handleSearch() {
+      this.pagination.current = 1
+      this.loadFamilies()
+    },
+    handleReset() {
+      this.searchForm = {
+        name: ''
+      }
+      this.handleSearch()
+    },
+    handleRefresh() {
+      this.loadFamilies()
+    },
+    handleSizeChange(val) {
+      this.pagination.size = val
+      this.loadFamilies()
+    },
+    handleCurrentChange(val) {
+      this.pagination.current = val
+      this.loadFamilies()
+    },
+    handleEdit(row) {
+      this.editForm = {
+        id: row.id,
+        name: row.name
+      }
+      this.editDialogVisible = true
+    },
+    async submitEdit() {
+      this.$refs.editForm.validate(async (valid) => {
+        if (valid) {
+          this.submitting = true
+          try {
+            await updateFamily(this.editForm.id, this.editForm)
+            this.$message.success('更新成功')
+            this.editDialogVisible = false
+            this.loadFamilies()
+          } catch (error) {
+            this.$message.error(error.message || '更新失败')
+          } finally {
+            this.submitting = false
+          }
+        }
+      })
+    },
+    handleDelete(row) {
+      this.$confirm(`确定要删除家庭 "${row.name}" 吗?`, '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(async () => {
+        try {
+          await deleteFamily(row.id)
+          this.$message.success('删除成功')
+          this.loadFamilies()
+        } catch (error) {
+          this.$message.error(error.message || '删除失败')
+        }
+      }).catch(() => {})
+    },
+    async handleViewMembers(row) {
+      await this.handleViewMembersById(row.id)
+    },
+    async handleViewMembersById(familyId) {
+      this.loading = true
+      try {
+        const res = await getFamilyMembers(familyId)
+        this.membersData = {
+          family: res.data.family,
+          parents: res.data.parents || [],
+          children: res.data.children || []
+        }
+        this.membersDialogVisible = true
+      } catch (error) {
+        this.$message.error(error.message || '加载家庭成员失败')
+      } finally {
+        this.loading = false
+      }
+    },
+    formatDate(dateStr) {
+      if (!dateStr) return '-'
+      const date = new Date(dateStr)
+      return date.toLocaleString('zh-CN')
+    }
+  }
+}
+</script>
+
+<style scoped>
+.page-container {
+  padding: 20px;
+}
+</style>

+ 55 - 33
zxyj-web/src/views/Layout.vue

@@ -1,39 +1,61 @@
 <template>
   <el-container class="layout-container">
     <el-aside width="200px">
-      <div class="logo">心知家庭</div>
-      <el-menu
-        :default-active="activeMenu"
-        router
-        background-color="#304156"
-        text-color="#bfcbd9"
-        active-text-color="#409EFF"
-      >
-        <el-menu-item index="/dashboard">
-          <i class="el-icon-s-home"></i>
-          <span slot="title">首页</span>
-        </el-menu-item>
-        <el-menu-item index="/users">
-          <i class="el-icon-user"></i>
-          <span slot="title">用户管理</span>
-        </el-menu-item>
-        <el-menu-item index="/children">
-          <i class="el-icon-s-custom"></i>
-          <span slot="title">孩子管理</span>
-        </el-menu-item>
-        <el-menu-item index="/tasks">
-          <i class="el-icon-s-order"></i>
-          <span slot="title">任务管理</span>
-        </el-menu-item>
-        <el-menu-item index="/rewards">
-          <i class="el-icon-s-goods"></i>
-          <span slot="title">奖励管理</span>
-        </el-menu-item>
-        <el-menu-item index="/points">
-          <i class="el-icon-s-money"></i>
-          <span slot="title">积分管理</span>
-        </el-menu-item>
-      </el-menu>
+      <div class="logo">知行益家</div>
+        <el-menu
+          :default-active="activeMenu"
+          router
+          background-color="#304156"
+          text-color="#bfcbd9"
+          active-text-color="#409EFF"
+        >
+          <el-menu-item index="/dashboard">
+            <i class="el-icon-s-home"></i>
+            <span slot="title">首页</span>
+          </el-menu-item>
+          
+          <el-submenu index="basic">
+            <template slot="title">
+              <i class="el-icon-s-data"></i>
+              <span>基础数据</span>
+            </template>
+            <el-menu-item index="/users">
+              <i class="el-icon-user"></i>
+              <span>用户管理</span>
+            </el-menu-item>
+            <el-menu-item index="/families">
+              <i class="el-icon-s-home"></i>
+              <span>家庭管理</span>
+            </el-menu-item>
+            <el-menu-item index="/children">
+              <i class="el-icon-s-custom"></i>
+              <span>孩子管理</span>
+            </el-menu-item>
+          </el-submenu>
+          
+          <el-submenu index="business">
+            <template slot="title">
+              <i class="el-icon-s-platform"></i>
+              <span>业务管理</span>
+            </template>
+            <el-menu-item index="/tasks">
+              <i class="el-icon-s-order"></i>
+              <span>任务管理</span>
+            </el-menu-item>
+            <el-menu-item index="/task-templates">
+              <i class="el-icon-document"></i>
+              <span>任务模板</span>
+            </el-menu-item>
+            <el-menu-item index="/rewards">
+              <i class="el-icon-s-goods"></i>
+              <span>奖励管理</span>
+            </el-menu-item>
+            <el-menu-item index="/points">
+              <i class="el-icon-s-money"></i>
+              <span>积分管理</span>
+            </el-menu-item>
+          </el-submenu>
+        </el-menu>
     </el-aside>
     <el-container>
       <el-header>

+ 25 - 73
zxyj-web/src/views/Login.vue

@@ -1,32 +1,23 @@
 <template>
   <div class="login-container">
     <el-card class="login-card">
-      <h2 class="title">知家管理后台</h2>
+      <h2 class="title">知行益家管理后台</h2>
       <el-form :model="loginForm" :rules="rules" ref="loginForm">
-        <el-form-item prop="phone">
+        <el-form-item prop="username">
           <el-input 
-            v-model="loginForm.phone" 
-            placeholder="请输入手机号" 
-            prefix-icon="el-icon-phone"
+            v-model="loginForm.username" 
+            placeholder="请输入用户名" 
+            prefix-icon="el-icon-user"
           ></el-input>
         </el-form-item>
-        <el-form-item prop="code">
-          <div class="code-input-wrapper">
-            <el-input 
-              v-model="loginForm.code" 
-              placeholder="请输入验证码" 
-              prefix-icon="el-icon-lock"
-              class="code-input"
-            ></el-input>
-            <el-button 
-              type="primary" 
-              @click="sendCode" 
-              :disabled="countdown > 0"
-              class="send-code-btn"
-            >
-              {{ countdown > 0 ? countdown + 's' : '获取验证码' }}
-            </el-button>
-          </div>
+        <el-form-item prop="password">
+          <el-input 
+            v-model="loginForm.password" 
+            type="password"
+            placeholder="请输入密码" 
+            prefix-icon="el-icon-lock"
+            @keyup.enter.native="handleLogin"
+          ></el-input>
         </el-form-item>
         <el-form-item>
           <el-button 
@@ -44,63 +35,37 @@
 </template>
 
 <script>
-import { sendCode, adminLogin } from '@/api/auth'
+import { adminLogin } from '@/api/auth'
 
 export default {
   name: 'Login',
   data() {
     return {
       loginForm: {
-        phone: '',
-        code: ''
+        username: '',
+        password: ''
       },
       rules: {
-        phone: [
-          { required: true, message: '请输入手机号', trigger: 'blur' },
-          { pattern: /^1[3-9]\d{9}$/, message: '手机号格式不正确', trigger: 'blur' }
+        username: [
+          { required: true, message: '请输入用户名', trigger: 'blur' }
         ],
-        code: [
-          { required: true, message: '请输入验证码', trigger: 'blur' },
-          { len: 6, message: '验证码为6位数字', trigger: 'blur' }
+        password: [
+          { required: true, message: '请输入密码', trigger: 'blur' }
         ]
       },
-      loading: false,
-      countdown: 0
+      loading: false
     }
   },
   methods: {
-    async sendCode() {
-      if (!this.loginForm.phone) {
-        this.$message.error('请先输入手机号')
-        return
-      }
-      if (!/^1[3-9]\d{9}$/.test(this.loginForm.phone)) {
-        this.$message.error('手机号格式不正确')
-        return
-      }
-
-      try {
-        await sendCode({ phone: this.loginForm.phone, type: 'admin_login' })
-        this.$message.success('验证码已发送')
-        
-        // 开始倒计时
-        this.countdown = 60
-        const timer = setInterval(() => {
-          this.countdown--
-          if (this.countdown <= 0) {
-            clearInterval(timer)
-          }
-        }, 1000)
-      } catch (e) {
-        this.$message.error(e.message || '发送失败')
-      }
-    },
     handleLogin() {
       this.$refs.loginForm.validate(async valid => {
         if (valid) {
           this.loading = true
           try {
-            const res = await adminLogin(this.loginForm)
+            const res = await adminLogin({
+              phone: this.loginForm.username,
+              password: this.loginForm.password
+            })
             localStorage.setItem('token', res.data.token)
             localStorage.setItem('adminId', res.data.adminId)
             localStorage.setItem('adminName', res.data.realName)
@@ -141,17 +106,4 @@ export default {
 .login-btn {
   width: 100%;
 }
-
-.code-input-wrapper {
-  display: flex;
-  gap: 10px;
-}
-
-.code-input {
-  flex: 1;
-}
-
-.send-code-btn {
-  width: 120px;
-}
 </style>

+ 131 - 29
zxyj-web/src/views/Points.vue

@@ -3,85 +3,187 @@
     <el-card>
       <div slot="header">
         <span>积分管理</span>
+        <el-button
+          type="primary"
+          size="small"
+          style="float: right"
+          @click="handleRefresh"
+          :loading="loading"
+        >
+          刷新
+        </el-button>
       </div>
-      <el-form inline>
-        <el-form-item label="孩子">
-          <el-select v-model="searchForm.childId" placeholder="请选择" style="width: 150px">
-            <el-option label="全部" value=""></el-option>
-            <el-option label="小明" :value="1"></el-option>
-          </el-select>
+
+      <!-- 搜索筛选 -->
+      <el-form :inline="true" :model="searchForm" style="margin-bottom: 20px">
+        <el-form-item label="孩子ID">
+          <el-input
+            v-model="searchForm.childId"
+            placeholder="请输入孩子ID"
+            clearable
+            @keyup.enter.native="handleSearch"
+          ></el-input>
         </el-form-item>
         <el-form-item label="类型">
-          <el-select v-model="searchForm.type" placeholder="请选择" style="width: 150px">
-            <el-option label="全部" value=""></el-option>
+          <el-select v-model="searchForm.type" placeholder="全部类型" clearable @change="handleSearch">
             <el-option label="获得" value="earn"></el-option>
             <el-option label="消耗" value="spend"></el-option>
             <el-option label="奖励" value="bonus"></el-option>
             <el-option label="扣罚" value="penalty"></el-option>
+            <el-option label="调整" value="adjust"></el-option>
+            <el-option label="退还" value="refund"></el-option>
           </el-select>
         </el-form-item>
         <el-form-item>
-          <el-button type="primary" @click="handleSearch">查询</el-button>
+          <el-button type="primary" @click="handleSearch">搜索</el-button>
+          <el-button @click="handleReset">重置</el-button>
         </el-form-item>
       </el-form>
-      <el-table :data="tableData" border>
-        <el-table-column prop="id" label="ID" width="60"></el-table-column>
-        <el-table-column prop="childId" label="孩子ID" width="80"></el-table-column>
-        <el-table-column prop="amount" label="积分变动">
+
+      <!-- 积分流水列表 -->
+      <el-table :data="tableData" border v-loading="loading" style="width: 100%">
+        <el-table-column prop="id" label="ID" width="80"></el-table-column>
+        <el-table-column prop="childId" label="孩子ID" width="100"></el-table-column>
+        <el-table-column prop="amount" label="积分变动" width="120">
           <template slot-scope="scope">
-            <span :style="{ color: scope.row.amount > 0 ? '#67C23A' : '#F56C6C' }">
+            <span :style="{ color: scope.row.amount > 0 ? '#67C23A' : '#F56C6C', fontWeight: 'bold' }">
               {{ scope.row.amount > 0 ? '+' : '' }}{{ scope.row.amount }}
             </span>
           </template>
         </el-table-column>
-        <el-table-column prop="type" label="类型">
+        <el-table-column prop="type" label="类型" width="100">
           <template slot-scope="scope">
             <el-tag :type="getTypeTag(scope.row.type)" size="small">
               {{ getTypeText(scope.row.type) }}
             </el-tag>
           </template>
         </el-table-column>
-        <el-table-column prop="description" label="描述"></el-table-column>
-        <el-table-column prop="createdAt" label="时间"></el-table-column>
+        <el-table-column prop="description" label="描述" min-width="200"></el-table-column>
+        <el-table-column prop="taskId" label="任务ID" width="100">
+          <template slot-scope="scope">
+            <span>{{ scope.row.taskId || '-' }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="createdAt" label="时间" width="180">
+          <template slot-scope="scope">
+            {{ formatDate(scope.row.createdAt) }}
+          </template>
+        </el-table-column>
       </el-table>
+
+      <!-- 分页 -->
       <el-pagination
         style="margin-top: 20px; text-align: right"
-        :current-page="1"
-        :page-size="10"
-        :total="100"
-        layout="total, prev, pager, next"
+        @size-change="handleSizeChange"
+        @current-change="handleCurrentChange"
+        :current-page="pagination.current"
+        :page-sizes="[10, 20, 50, 100]"
+        :page-size="pagination.size"
+        :total="pagination.total"
+        layout="total, sizes, prev, pager, next, jumper"
       ></el-pagination>
     </el-card>
   </div>
 </template>
 
 <script>
+import { getPointsLogs } from '@/api/admin'
+
 export default {
+  name: 'Points',
   data() {
     return {
+      loading: false,
+      tableData: [],
       searchForm: {
         childId: '',
         type: ''
       },
-      tableData: [
-        { id: 1, childId: 1, amount: 5, type: 'earn', description: '完成任务: 完成作业', createdAt: '2026-03-30 10:00:00' },
-        { id: 2, childId: 1, amount: -30, type: 'spend', description: '兑换奖励: 看动画片', createdAt: '2026-03-30 09:30:00' },
-        { id: 3, childId: 1, amount: 1, type: 'bonus', description: '提前完成奖励', createdAt: '2026-03-30 09:00:00' }
-      ]
+      pagination: {
+        current: 1,
+        size: 10,
+        total: 0
+      }
     }
   },
+  mounted() {
+    this.loadPointsLogs()
+  },
   methods: {
+    async loadPointsLogs() {
+      this.loading = true
+      try {
+        const params = {
+          page: this.pagination.current,
+          size: this.pagination.size,
+          ...this.searchForm
+        }
+        const res = await getPointsLogs(params)
+        this.tableData = res.data.records || res.data || []
+        this.pagination.total = res.data.total || this.tableData.length
+      } catch (error) {
+        this.$message.error(error.message || '加载积分流水失败')
+      } finally {
+        this.loading = false
+      }
+    },
     handleSearch() {
-      this.$message.success('查询成功')
+      this.pagination.current = 1
+      this.loadPointsLogs()
+    },
+    handleReset() {
+      this.searchForm = {
+        childId: '',
+        type: ''
+      }
+      this.handleSearch()
+    },
+    handleRefresh() {
+      this.loadPointsLogs()
+    },
+    handleSizeChange(val) {
+      this.pagination.size = val
+      this.loadPointsLogs()
+    },
+    handleCurrentChange(val) {
+      this.pagination.current = val
+      this.loadPointsLogs()
     },
     getTypeTag(type) {
-      const tags = { earn: 'success', spend: 'warning', bonus: 'success', penalty: 'danger' }
+      const tags = {
+        earn: 'success',
+        spend: 'warning',
+        bonus: 'success',
+        penalty: 'danger',
+        adjust: 'info',
+        reset: 'warning',
+        refund: 'success'
+      }
       return tags[type] || 'info'
     },
     getTypeText(type) {
-      const texts = { earn: '获得', spend: '消耗', bonus: '奖励', penalty: '扣罚', adjust: '调整', reset: '清零', refund: '退还' }
+      const texts = {
+        earn: '获得',
+        spend: '消耗',
+        bonus: '奖励',
+        penalty: '扣罚',
+        adjust: '调整',
+        reset: '清零',
+        refund: '退还'
+      }
       return texts[type] || type
+    },
+    formatDate(dateStr) {
+      if (!dateStr) return '-'
+      const date = new Date(dateStr)
+      return date.toLocaleString('zh-CN')
     }
   }
 }
 </script>
+
+<style scoped>
+.page-container {
+  padding: 20px;
+}
+</style>

+ 295 - 32
zxyj-web/src/views/Rewards.vue

@@ -3,77 +3,340 @@
     <el-card>
       <div slot="header">
         <span>奖励管理</span>
-        <el-button type="primary" size="small" style="float: right">添加奖励模板</el-button>
+        <el-button
+          type="primary"
+          size="small"
+          style="float: right"
+          @click="handleRefresh"
+          :loading="loading"
+        >
+          刷新
+        </el-button>
       </div>
-      <el-tabs v-model="activeTab">
+
+      <el-tabs v-model="activeTab" @tab-click="handleTabClick">
+        <!-- 奖励列表 -->
         <el-tab-pane label="奖励列表" name="list">
-          <el-table :data="tableData" border>
-            <el-table-column prop="id" label="ID" width="60"></el-table-column>
-            <el-table-column prop="title" label="奖励名称"></el-table-column>
-            <el-table-column prop="pointsRequired" label="所需积分"></el-table-column>
-            <el-table-column prop="category" label="分类"></el-table-column>
-            <el-table-column prop="status" label="状态">
+          <!-- 搜索筛选 -->
+          <el-form :inline="true" :model="searchForm" style="margin-bottom: 20px">
+            <el-form-item label="状态">
+              <el-select v-model="searchForm.status" placeholder="全部状态" clearable @change="handleSearch">
+                <el-option label="可用" value="available"></el-option>
+                <el-option label="待审批" value="pending"></el-option>
+                <el-option label="已兑换" value="exchanged"></el-option>
+                <el-option label="已拒绝" value="rejected"></el-option>
+              </el-select>
+            </el-form-item>
+            <el-form-item>
+              <el-button type="primary" @click="handleSearch">搜索</el-button>
+              <el-button @click="handleReset">重置</el-button>
+            </el-form-item>
+          </el-form>
+
+          <el-table :data="tableData" border v-loading="loading" style="width: 100%">
+            <el-table-column prop="id" label="ID" width="80"></el-table-column>
+            <el-table-column prop="title" label="奖励名称" min-width="200"></el-table-column>
+            <el-table-column prop="pointsRequired" label="所需积分" width="120">
+              <template slot-scope="scope">
+                <el-tag type="warning">{{ scope.row.pointsRequired }}</el-tag>
+              </template>
+            </el-table-column>
+            <el-table-column prop="category" label="分类" width="100"></el-table-column>
+            <el-table-column prop="childId" label="孩子ID" width="80"></el-table-column>
+            <el-table-column prop="status" label="状态" width="100">
               <template slot-scope="scope">
                 <el-tag :type="getStatusType(scope.row.status)" size="small">
                   {{ getStatusText(scope.row.status) }}
                 </el-tag>
               </template>
             </el-table-column>
-            <el-table-column label="操作" width="200">
+            <el-table-column prop="createdAt" label="创建时间" width="180">
               <template slot-scope="scope">
-                <el-button v-if="scope.row.status === 'pending'" type="text" size="small" @click="handleApprove(scope.row, true)">通过</el-button>
-                <el-button v-if="scope.row.status === 'pending'" type="text" size="small" style="color: #F56C6C" @click="handleApprove(scope.row, false)">拒绝</el-button>
-                <el-button type="text" size="small">编辑</el-button>
+                {{ formatDate(scope.row.createdAt) }}
+              </template>
+            </el-table-column>
+            <el-table-column label="操作" width="200" fixed="right">
+              <template slot-scope="scope">
+                <el-button
+                  v-if="scope.row.status === 'pending'"
+                  type="text"
+                  size="small"
+                  @click="handleApprove(scope.row, true)"
+                >
+                  通过
+                </el-button>
+                <el-button
+                  v-if="scope.row.status === 'pending'"
+                  type="text"
+                  size="small"
+                  style="color: #F56C6C"
+                  @click="handleApprove(scope.row, false)"
+                >
+                  拒绝
+                </el-button>
               </template>
             </el-table-column>
           </el-table>
+
+          <el-pagination
+            style="margin-top: 20px; text-align: right"
+            @size-change="handleSizeChange"
+            @current-change="handleCurrentChange"
+            :current-page="pagination.current"
+            :page-sizes="[10, 20, 50, 100]"
+            :page-size="pagination.size"
+            :total="pagination.total"
+            layout="total, sizes, prev, pager, next, jumper"
+          ></el-pagination>
         </el-tab-pane>
+
+        <!-- 奖励模板 -->
         <el-tab-pane label="奖励模板" name="templates">
-          <el-table :data="templates" border>
-            <el-table-column prop="id" label="ID" width="60"></el-table-column>
-            <el-table-column prop="title" label="模板名称"></el-table-column>
-            <el-table-column prop="pointsRequired" label="所需积分"></el-table-column>
-            <el-table-column prop="category" label="分类"></el-table-column>
-            <el-table-column label="操作" width="120">
+          <el-button type="primary" size="small" style="margin-bottom: 15px" @click="showCreateTemplate">
+            新建模板
+          </el-button>
+          <el-table :data="templates" border v-loading="loadingTemplates" style="width: 100%">
+            <el-table-column prop="id" label="ID" width="80"></el-table-column>
+            <el-table-column prop="title" label="模板名称" min-width="200"></el-table-column>
+            <el-table-column prop="pointsRequired" label="所需积分" width="120">
+              <template slot-scope="scope">
+                <el-tag type="warning">{{ scope.row.pointsRequired }}</el-tag>
+              </template>
+            </el-table-column>
+            <el-table-column prop="category" label="分类" width="100"></el-table-column>
+            <el-table-column prop="createdAt" label="创建时间" width="180">
               <template slot-scope="scope">
-                <el-button type="text" size="small">编辑</el-button>
-                <el-button type="text" size="small" style="color: #F56C6C">删除</el-button>
+                {{ formatDate(scope.row.createdAt) }}
+              </template>
+            </el-table-column>
+            <el-table-column label="操作" width="150" fixed="right">
+              <template slot-scope="scope">
+                <el-button
+                  type="text"
+                  size="small"
+                  style="color: #F56C6C"
+                  @click="handleDeleteTemplate(scope.row)"
+                >
+                  删除
+                </el-button>
               </template>
             </el-table-column>
           </el-table>
         </el-tab-pane>
       </el-tabs>
     </el-card>
+
+    <!-- 创建奖励模板对话框 -->
+    <el-dialog title="新建奖励模板" :visible.sync="dialogVisible" width="500px">
+      <el-form :model="templateForm" label-width="100px">
+        <el-form-item label="奖励名称">
+          <el-input v-model="templateForm.title" placeholder="请输入奖励名称"></el-input>
+        </el-form-item>
+        <el-form-item label="所需积分">
+          <el-input-number v-model="templateForm.pointsRequired" :min="1" :max="10000"></el-input-number>
+        </el-form-item>
+        <el-form-item label="分类">
+          <el-select v-model="templateForm.category" placeholder="请选择分类">
+            <el-option label="玩具" value="玩具"></el-option>
+            <el-option label="图书" value="图书"></el-option>
+            <el-option label="美食" value="美食"></el-option>
+            <el-option label="娱乐" value="娱乐"></el-option>
+            <el-option label="其他" value="其他"></el-option>
+          </el-select>
+        </el-form-item>
+      </el-form>
+      <div slot="footer">
+        <el-button @click="dialogVisible = false">取消</el-button>
+        <el-button type="primary" @click="handleCreateTemplate" :loading="creating">确定</el-button>
+      </div>
+    </el-dialog>
   </div>
 </template>
 
 <script>
+import { getRewardsList, getRewardTemplates, createRewardTemplate, deleteRewardTemplate, approveReward } from '@/api/admin'
+
 export default {
+  name: 'Rewards',
   data() {
     return {
       activeTab: 'list',
-      tableData: [
-        { id: 1, title: '看动画片30分钟', pointsRequired: 30, category: '精神类', status: 'pending' },
-        { id: 2, title: '去游乐场', pointsRequired: 100, category: '物质类', status: 'exchanged' }
-      ],
-      templates: [
-        { id: 1, title: '看动画片30分钟', pointsRequired: 30, category: '精神类' },
-        { id: 2, title: '玩平板游戏30分钟', pointsRequired: 40, category: '特权类' }
-      ]
+      loading: false,
+      loadingTemplates: false,
+      tableData: [],
+      templates: [],
+      searchForm: {
+        status: ''
+      },
+      pagination: {
+        current: 1,
+        size: 10,
+        total: 0
+      },
+      // 创建模板对话框
+      dialogVisible: false,
+      creating: false,
+      templateForm: {
+        title: '',
+        pointsRequired: 100,
+        category: ''
+      }
     }
   },
+  mounted() {
+    this.loadRewards()
+  },
   methods: {
+    async loadRewards() {
+      this.loading = true
+      try {
+        const params = {
+          page: this.pagination.current,
+          size: this.pagination.size,
+          ...this.searchForm
+        }
+        const res = await getRewardsList(params)
+        this.tableData = res.data.records || res.data || []
+        this.pagination.total = res.data.total || this.tableData.length
+      } catch (error) {
+        this.$message.error(error.message || '加载奖励列表失败')
+      } finally {
+        this.loading = false
+      }
+    },
+    async loadTemplates() {
+      this.loadingTemplates = true
+      try {
+        const res = await getRewardTemplates()
+        this.templates = res.data || []
+      } catch (error) {
+        this.$message.error(error.message || '加载模板失败')
+      } finally {
+        this.loadingTemplates = false
+      }
+    },
+    handleTabClick(tab) {
+      if (tab.name === 'templates' && this.templates.length === 0) {
+        this.loadTemplates()
+      }
+    },
+    handleSearch() {
+      this.pagination.current = 1
+      this.loadRewards()
+    },
+    handleReset() {
+      this.searchForm = {
+        status: ''
+      }
+      this.handleSearch()
+    },
+    handleRefresh() {
+      if (this.activeTab === 'list') {
+        this.loadRewards()
+      } else {
+        this.loadTemplates()
+      }
+    },
+    handleSizeChange(val) {
+      this.pagination.size = val
+      this.loadRewards()
+    },
+    handleCurrentChange(val) {
+      this.pagination.current = val
+      this.loadRewards()
+    },
+    async handleApprove(row, approved) {
+      this.$confirm(
+        approved ? `确定通过 "${row.title}" 的兑换申请吗?` : `确定拒绝 "${row.title}" 的兑换申请吗?`,
+        '提示',
+        {
+          confirmButtonText: '确定',
+          cancelButtonText: '取消',
+          type: 'warning'
+        }
+      ).then(async () => {
+        try {
+          await approveReward(row.id, { approved })
+          this.$message.success(approved ? '已通过' : '已拒绝')
+          this.loadRewards()
+        } catch (error) {
+          this.$message.error(error.message || '操作失败')
+        }
+      }).catch(() => {})
+    },
     getStatusType(status) {
-      const types = { available: 'success', pending: 'warning', exchanged: 'info', rejected: 'danger' }
+      const types = {
+        available: 'success',
+        pending: 'warning',
+        exchanged: 'info',
+        rejected: 'danger'
+      }
       return types[status] || 'info'
     },
     getStatusText(status) {
-      const texts = { available: '可用', pending: '待审批', exchanged: '已兑换', rejected: '已拒绝' }
+      const texts = {
+        available: '可用',
+        pending: '待审批',
+        exchanged: '已兑换',
+        rejected: '已拒绝'
+      }
       return texts[status] || status
     },
-    handleApprove(row, approved) {
-      this.$message.success(approved ? '已通过' : '已拒绝')
+    showCreateTemplate() {
+      this.templateForm = {
+        title: '',
+        pointsRequired: 100,
+        category: ''
+      }
+      this.dialogVisible = true
+    },
+    async handleCreateTemplate() {
+      if (!this.templateForm.title) {
+        this.$message.warning('请输入奖励名称')
+        return
+      }
+      if (!this.templateForm.category) {
+        this.$message.warning('请选择分类')
+        return
+      }
+      this.creating = true
+      try {
+        await createRewardTemplate(this.templateForm)
+        this.$message.success('创建成功')
+        this.dialogVisible = false
+        this.loadTemplates()
+      } catch (error) {
+        this.$message.error(error.message || '创建失败')
+      } finally {
+        this.creating = false
+      }
+    },
+    handleDeleteTemplate(row) {
+      this.$confirm('确定要删除该模板吗?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(async () => {
+        try {
+          await deleteRewardTemplate(row.id)
+          this.$message.success('删除成功')
+          this.loadTemplates()
+        } catch (error) {
+          this.$message.error(error.message || '删除失败')
+        }
+      }).catch(() => {})
+    },
+    formatDate(dateStr) {
+      if (!dateStr) return '-'
+      const date = new Date(dateStr)
+      return date.toLocaleString('zh-CN')
     }
   }
 }
 </script>
+
+<style scoped>
+.page-container {
+  padding: 20px;
+}
+</style>

+ 429 - 0
zxyj-web/src/views/TaskTemplates.vue

@@ -0,0 +1,429 @@
+<template>
+  <div class="page-container">
+    <el-card>
+      <div slot="header">
+        <span>任务模板管理</span>
+        <div style="float: right">
+          <el-button
+            type="primary"
+            size="small"
+            icon="el-icon-plus"
+            @click="handleCreate"
+          >
+            新建模板
+          </el-button>
+          <el-button
+            size="small"
+            @click="handleRefresh"
+            :loading="loading"
+          >
+            刷新
+          </el-button>
+        </div>
+      </div>
+
+      <!-- 搜索和筛选 -->
+      <el-form :inline="true" :model="searchForm" style="margin-bottom: 20px">
+        <el-form-item label="分类">
+          <el-select v-model="searchForm.category" placeholder="全部分类" clearable @change="handleSearch">
+            <el-option label="学习" value="学习"></el-option>
+            <el-option label="家务" value="家务"></el-option>
+            <el-option label="运动" value="运动"></el-option>
+            <el-option label="阅读" value="阅读"></el-option>
+            <el-option label="生活习惯" value="生活习惯"></el-option>
+            <el-option label="其他" value="其他"></el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item label="难度">
+          <el-select v-model="searchForm.difficulty" placeholder="全部难度" clearable @change="handleSearch">
+            <el-option label="简单" value="easy"></el-option>
+            <el-option label="中等" value="medium"></el-option>
+            <el-option label="困难" value="hard"></el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item label="状态">
+          <el-select v-model="searchForm.isActive" placeholder="全部状态" clearable @change="handleSearch">
+            <el-option label="启用" :value="1"></el-option>
+            <el-option label="禁用" :value="0"></el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" @click="handleSearch">搜索</el-button>
+          <el-button @click="handleReset">重置</el-button>
+        </el-form-item>
+      </el-form>
+
+      <!-- 模板列表 -->
+      <el-table
+        :data="tableData"
+        border
+        v-loading="loading"
+        style="width: 100%"
+      >
+        <el-table-column prop="id" label="ID" width="80"></el-table-column>
+        <el-table-column prop="title" label="模板名称" min-width="200"></el-table-column>
+        <el-table-column prop="category" label="分类" width="100">
+          <template slot-scope="scope">
+            <el-tag :type="getCategoryType(scope.row.category)" size="small">
+              {{ scope.row.category }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column prop="points" label="积分" width="80">
+          <template slot-scope="scope">
+            <el-tag type="success">{{ scope.row.points }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column prop="difficulty" label="难度" width="100">
+          <template slot-scope="scope">
+            <el-tag :type="getDifficultyType(scope.row.difficulty)" size="small">
+              {{ getDifficultyText(scope.row.difficulty) }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column prop="recommendedAge" label="推荐年龄" width="100">
+          <template slot-scope="scope">
+            <span v-if="scope.row.recommendedAge">{{ scope.row.recommendedAge }}岁+</span>
+            <span v-else>-</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="needReview" label="需审核" width="80">
+          <template slot-scope="scope">
+            <el-tag v-if="scope.row.needReview === 1" type="warning" size="small">是</el-tag>
+            <el-tag v-else type="info" size="small">否</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column prop="isActive" label="状态" width="80">
+          <template slot-scope="scope">
+            <el-switch
+              v-model="scope.row.isActive"
+              :active-value="1"
+              :inactive-value="0"
+              @change="handleStatusChange(scope.row)"
+            ></el-switch>
+          </template>
+        </el-table-column>
+        <el-table-column prop="createdAt" label="创建时间" width="180">
+          <template slot-scope="scope">
+            {{ formatDate(scope.row.createdAt) }}
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" width="180" fixed="right">
+          <template slot-scope="scope">
+            <el-button
+              type="text"
+              size="small"
+              @click="handleEdit(scope.row)"
+            >
+              编辑
+            </el-button>
+            <el-button
+              type="text"
+              size="small"
+              style="color: #F56C6C"
+              @click="handleDelete(scope.row)"
+            >
+              删除
+            </el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <!-- 分页 -->
+      <el-pagination
+        style="margin-top: 20px; text-align: right"
+        @size-change="handleSizeChange"
+        @current-change="handleCurrentChange"
+        :current-page="pagination.current"
+        :page-sizes="[10, 20, 50, 100]"
+        :page-size="pagination.size"
+        :total="pagination.total"
+        layout="total, sizes, prev, pager, next, jumper"
+      ></el-pagination>
+    </el-card>
+
+    <!-- 创建/编辑模板弹窗 -->
+    <el-dialog
+      :title="dialogTitle"
+      :visible.sync="editDialogVisible"
+      width="600px"
+      @close="handleDialogClose"
+    >
+      <el-form :model="editForm" :rules="editRules" ref="editForm" label-width="120px">
+        <el-form-item label="模板名称" prop="title">
+          <el-input v-model="editForm.title" placeholder="请输入模板名称"></el-input>
+        </el-form-item>
+        <el-form-item label="描述" prop="description">
+          <el-input
+            type="textarea"
+            v-model="editForm.description"
+            placeholder="请输入任务描述"
+            :rows="3"
+          ></el-input>
+        </el-form-item>
+        <el-form-item label="分类" prop="category">
+          <el-select v-model="editForm.category" placeholder="请选择分类">
+            <el-option label="学习" value="学习"></el-option>
+            <el-option label="家务" value="家务"></el-option>
+            <el-option label="运动" value="运动"></el-option>
+            <el-option label="阅读" value="阅读"></el-option>
+            <el-option label="生活习惯" value="生活习惯"></el-option>
+            <el-option label="其他" value="其他"></el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item label="积分" prop="points">
+          <el-input-number v-model="editForm.points" :min="1" :max="1000" placeholder="完成奖励积分"></el-input-number>
+        </el-form-item>
+        <el-form-item label="难度" prop="difficulty">
+          <el-select v-model="editForm.difficulty" placeholder="请选择难度">
+            <el-option label="简单" value="easy"></el-option>
+            <el-option label="中等" value="medium"></el-option>
+            <el-option label="困难" value="hard"></el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item label="推荐年龄" prop="recommendedAge">
+          <el-input-number v-model="editForm.recommendedAge" :min="0" :max="18" placeholder="推荐年龄"></el-input-number>
+          <span style="margin-left: 10px; color: #909399">岁及以上</span>
+        </el-form-item>
+        <el-form-item label="需要审核" prop="needReview">
+          <el-switch v-model="editForm.needReview" :active-value="1" :inactive-value="0"></el-switch>
+          <span style="margin-left: 10px; color: #909399; font-size: 12px">
+            开启后,孩子完成任务需要家长审核
+          </span>
+        </el-form-item>
+        <el-form-item label="按分类审核" prop="reviewByCategory" v-if="editForm.needReview === 1">
+          <el-switch v-model="editForm.reviewByCategory" :active-value="1" :inactive-value="0"></el-switch>
+          <span style="margin-left: 10px; color: #909399; font-size: 12px">
+            开启后,将按照分类自动分配审核人
+          </span>
+        </el-form-item>
+      </el-form>
+      <div slot="footer">
+        <el-button @click="editDialogVisible = false">取消</el-button>
+        <el-button type="primary" @click="submitEdit" :loading="submitting">确定</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { getTaskTemplateList, createTaskTemplate, updateTaskTemplate, deleteTaskTemplate } from '@/api/admin'
+
+export default {
+  name: 'TaskTemplates',
+  data() {
+    return {
+      loading: false,
+      submitting: false,
+      tableData: [],
+      searchForm: {
+        category: '',
+        difficulty: '',
+        isActive: null
+      },
+      pagination: {
+        current: 1,
+        size: 10,
+        total: 0
+      },
+      editDialogVisible: false,
+      isCreate: false,
+      editForm: {
+        id: null,
+        title: '',
+        description: '',
+        category: '',
+        points: 10,
+        difficulty: 'medium',
+        recommendedAge: null,
+        needReview: 1,
+        reviewByCategory: 0,
+        isActive: 1
+      },
+      editRules: {
+        title: [
+          { required: true, message: '请输入模板名称', trigger: 'blur' }
+        ],
+        category: [
+          { required: true, message: '请选择分类', trigger: 'change' }
+        ],
+        points: [
+          { required: true, message: '请输入积分', trigger: 'blur' }
+        ],
+        difficulty: [
+          { required: true, message: '请选择难度', trigger: 'change' }
+        ]
+      }
+    }
+  },
+  computed: {
+    dialogTitle() {
+      return this.isCreate ? '新建任务模板' : '编辑任务模板'
+    }
+  },
+  mounted() {
+    this.loadTemplates()
+  },
+  methods: {
+    async loadTemplates() {
+      this.loading = true
+      try {
+        const params = {
+          page: this.pagination.current,
+          size: this.pagination.size,
+          ...this.searchForm
+        }
+        const res = await getTaskTemplateList(params)
+        this.tableData = res.data.records || res.data || []
+        this.pagination.total = res.data.total || this.tableData.length
+      } catch (error) {
+        this.$message.error(error.message || '加载模板列表失败')
+      } finally {
+        this.loading = false
+      }
+    },
+    handleSearch() {
+      this.pagination.current = 1
+      this.loadTemplates()
+    },
+    handleReset() {
+      this.searchForm = {
+        category: '',
+        difficulty: '',
+        isActive: null
+      }
+      this.handleSearch()
+    },
+    handleRefresh() {
+      this.loadTemplates()
+    },
+    handleSizeChange(val) {
+      this.pagination.size = val
+      this.loadTemplates()
+    },
+    handleCurrentChange(val) {
+      this.pagination.current = val
+      this.loadTemplates()
+    },
+    handleCreate() {
+      this.isCreate = true
+      this.editForm = {
+        id: null,
+        title: '',
+        description: '',
+        category: '',
+        points: 10,
+        difficulty: 'medium',
+        recommendedAge: null,
+        needReview: 1,
+        reviewByCategory: 0,
+        isActive: 1
+      }
+      this.editDialogVisible = true
+    },
+    handleEdit(row) {
+      this.isCreate = false
+      this.editForm = {
+        id: row.id,
+        title: row.title,
+        description: row.description || '',
+        category: row.category,
+        points: row.points,
+        difficulty: row.difficulty,
+        recommendedAge: row.recommendedAge,
+        needReview: row.needReview,
+        reviewByCategory: row.reviewByCategory || 0,
+        isActive: row.isActive
+      }
+      this.editDialogVisible = true
+    },
+    handleDialogClose() {
+      this.$refs.editForm.resetFields()
+    },
+    async submitEdit() {
+      this.$refs.editForm.validate(async (valid) => {
+        if (valid) {
+          this.submitting = true
+          try {
+            if (this.isCreate) {
+              await createTaskTemplate(this.editForm)
+              this.$message.success('创建成功')
+            } else {
+              await updateTaskTemplate(this.editForm.id, this.editForm)
+              this.$message.success('更新成功')
+            }
+            this.editDialogVisible = false
+            this.loadTemplates()
+          } catch (error) {
+            this.$message.error(error.message || (this.isCreate ? '创建失败' : '更新失败'))
+          } finally {
+            this.submitting = false
+          }
+        }
+      })
+    },
+    async handleStatusChange(row) {
+      try {
+        await updateTaskTemplate(row.id, { isActive: row.isActive })
+        this.$message.success(row.isActive === 1 ? '已启用' : '已禁用')
+      } catch (error) {
+        this.$message.error(error.message || '状态更新失败')
+        // 恢复原状态
+        row.isActive = row.isActive === 1 ? 0 : 1
+      }
+    },
+    handleDelete(row) {
+      this.$confirm(`确定要删除模板 "${row.title}" 吗?`, '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(async () => {
+        try {
+          await deleteTaskTemplate(row.id)
+          this.$message.success('删除成功')
+          this.loadTemplates()
+        } catch (error) {
+          this.$message.error(error.message || '删除失败')
+        }
+      }).catch(() => {})
+    },
+    getCategoryType(category) {
+      const types = {
+        '学习': 'primary',
+        '家务': 'success',
+        '运动': 'warning',
+        '阅读': 'info',
+        '生活习惯': 'danger'
+      }
+      return types[category] || ''
+    },
+    getDifficultyType(difficulty) {
+      const types = {
+        easy: 'success',
+        medium: 'warning',
+        hard: 'danger'
+      }
+      return types[difficulty] || 'info'
+    },
+    getDifficultyText(difficulty) {
+      const texts = {
+        easy: '简单',
+        medium: '中等',
+        hard: '困难'
+      }
+      return texts[difficulty] || difficulty
+    },
+    formatDate(dateStr) {
+      if (!dateStr) return '-'
+      const date = new Date(dateStr)
+      return date.toLocaleString('zh-CN')
+    }
+  }
+}
+</script>
+
+<style scoped>
+.page-container {
+  padding: 20px;
+}
+</style>

+ 198 - 23
zxyj-web/src/views/Tasks.vue

@@ -3,66 +3,241 @@
     <el-card>
       <div slot="header">
         <span>任务管理</span>
-        <el-input v-model="search" placeholder="搜索任务" style="width: 200px; float: right" size="small"></el-input>
+        <el-button
+          type="primary"
+          size="small"
+          style="float: right"
+          @click="handleRefresh"
+          :loading="loading"
+        >
+          刷新
+        </el-button>
       </div>
-      <el-table :data="tableData" border>
-        <el-table-column prop="id" label="ID" width="60"></el-table-column>
-        <el-table-column prop="title" label="任务名称"></el-table-column>
-        <el-table-column prop="points" label="积分" width="80"></el-table-column>
-        <el-table-column prop="category" label="分类"></el-table-column>
-        <el-table-column prop="deadline" label="截止时间"></el-table-column>
-        <el-table-column prop="status" label="状态">
+
+      <!-- 搜索筛选 -->
+      <el-form :inline="true" :model="searchForm" style="margin-bottom: 20px">
+        <el-form-item label="任务标题">
+          <el-input
+            v-model="searchForm.title"
+            placeholder="请输入任务标题"
+            clearable
+            @keyup.enter.native="handleSearch"
+          ></el-input>
+        </el-form-item>
+        <el-form-item label="状态">
+          <el-select v-model="searchForm.status" placeholder="全部状态" clearable @change="handleSearch">
+            <el-option label="进行中" value="pending"></el-option>
+            <el-option label="已完成" value="completed"></el-option>
+            <el-option label="已逾期" value="overdue"></el-option>
+            <el-option label="已取消" value="cancelled"></el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item label="孩子ID">
+          <el-input
+            v-model="searchForm.childId"
+            placeholder="请输入孩子ID"
+            clearable
+            @keyup.enter.native="handleSearch"
+          ></el-input>
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" @click="handleSearch">搜索</el-button>
+          <el-button @click="handleReset">重置</el-button>
+        </el-form-item>
+      </el-form>
+
+      <!-- 统计卡片 -->
+      <el-row :gutter="20" style="margin-bottom: 20px">
+        <el-col :span="6">
+          <el-card class="stat-card">
+            <div class="stat-label">总任务数</div>
+            <div class="stat-value">{{ stats.total || 0 }}</div>
+          </el-card>
+        </el-col>
+        <el-col :span="6">
+          <el-card class="stat-card">
+            <div class="stat-label">今日任务</div>
+            <div class="stat-value">{{ stats.today || 0 }}</div>
+          </el-card>
+        </el-col>
+      </el-row>
+
+      <!-- 任务列表 -->
+      <el-table :data="tableData" border v-loading="loading" style="width: 100%">
+        <el-table-column prop="id" label="ID" width="80"></el-table-column>
+        <el-table-column prop="title" label="任务名称" min-width="200"></el-table-column>
+        <el-table-column prop="points" label="积分" width="80">
+          <template slot-scope="scope">
+            <el-tag type="success">{{ scope.row.points }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column prop="category" label="分类" width="100"></el-table-column>
+        <el-table-column prop="deadline" label="截止时间" width="180">
+          <template slot-scope="scope">
+            {{ formatDate(scope.row.deadline) }}
+          </template>
+        </el-table-column>
+        <el-table-column prop="status" label="状态" width="100">
           <template slot-scope="scope">
             <el-tag :type="getStatusType(scope.row.status)" size="small">
               {{ getStatusText(scope.row.status) }}
             </el-tag>
           </template>
         </el-table-column>
-        <el-table-column prop="needReview" label="需审核">
+        <el-table-column prop="needReview" label="需审核" width="80">
           <template slot-scope="scope">
             <el-tag :type="scope.row.needReview ? 'warning' : 'info'" size="small">
               {{ scope.row.needReview ? '是' : '否' }}
             </el-tag>
           </template>
         </el-table-column>
-        <el-table-column label="操作" width="120">
+        <el-table-column prop="childId" label="孩子ID" width="80"></el-table-column>
+        <el-table-column prop="createdAt" label="创建时间" width="180">
           <template slot-scope="scope">
-            <el-button type="text" size="small">编辑</el-button>
-            <el-button type="text" size="small" style="color: #F56C6C">删除</el-button>
+            {{ formatDate(scope.row.createdAt) }}
           </template>
         </el-table-column>
       </el-table>
+
+      <!-- 分页 -->
       <el-pagination
         style="margin-top: 20px; text-align: right"
-        :current-page="1"
-        :page-size="10"
-        :total="100"
-        layout="total, prev, pager, next"
+        @size-change="handleSizeChange"
+        @current-change="handleCurrentChange"
+        :current-page="pagination.current"
+        :page-sizes="[10, 20, 50, 100]"
+        :page-size="pagination.size"
+        :total="pagination.total"
+        layout="total, sizes, prev, pager, next, jumper"
       ></el-pagination>
     </el-card>
   </div>
 </template>
 
 <script>
+import { getTasksList, getTaskStats } from '@/api/admin'
+
 export default {
+  name: 'Tasks',
   data() {
     return {
-      search: '',
-      tableData: [
-        { id: 1, title: '完成数学作业', points: 5, category: '学习类', deadline: '2026-03-30 20:00', status: 'pending', needReview: 0 },
-        { id: 2, title: '收拾玩具', points: 3, category: '生活类', deadline: '2026-03-30 18:00', status: 'completed', needReview: 1 }
-      ]
+      loading: false,
+      tableData: [],
+      stats: {
+        total: 0,
+        today: 0
+      },
+      searchForm: {
+        title: '',
+        status: '',
+        childId: ''
+      },
+      pagination: {
+        current: 1,
+        size: 10,
+        total: 0
+      }
     }
   },
+  mounted() {
+    this.loadTasks()
+    this.loadStats()
+  },
   methods: {
+    async loadTasks() {
+      this.loading = true
+      try {
+        const params = {
+          page: this.pagination.current,
+          size: this.pagination.size,
+          ...this.searchForm
+        }
+        const res = await getTasksList(params)
+        this.tableData = res.data.records || res.data || []
+        this.pagination.total = res.data.total || this.tableData.length
+      } catch (error) {
+        this.$message.error(error.message || '加载任务列表失败')
+      } finally {
+        this.loading = false
+      }
+    },
+    async loadStats() {
+      try {
+        const res = await getTaskStats()
+        this.stats = res.data || { total: 0, today: 0 }
+      } catch (error) {
+        console.error('加载统计失败:', error)
+      }
+    },
+    handleSearch() {
+      this.pagination.current = 1
+      this.loadTasks()
+    },
+    handleReset() {
+      this.searchForm = {
+        title: '',
+        status: '',
+        childId: ''
+      }
+      this.handleSearch()
+    },
+    handleRefresh() {
+      this.loadTasks()
+      this.loadStats()
+    },
+    handleSizeChange(val) {
+      this.pagination.size = val
+      this.loadTasks()
+    },
+    handleCurrentChange(val) {
+      this.pagination.current = val
+      this.loadTasks()
+    },
     getStatusType(status) {
-      const types = { pending: 'warning', completed: 'success', overdue: 'danger', cancelled: 'info' }
+      const types = {
+        pending: 'warning',
+        completed: 'success',
+        overdue: 'danger',
+        cancelled: 'info'
+      }
       return types[status] || 'info'
     },
     getStatusText(status) {
-      const texts = { pending: '进行中', completed: '已完成', overdue: '已逾期', cancelled: '已取消' }
+      const texts = {
+        pending: '进行中',
+        completed: '已完成',
+        overdue: '已逾期',
+        cancelled: '已取消'
+      }
       return texts[status] || status
+    },
+    formatDate(dateStr) {
+      if (!dateStr) return '-'
+      const date = new Date(dateStr)
+      return date.toLocaleString('zh-CN')
     }
   }
 }
 </script>
+
+<style scoped>
+.page-container {
+  padding: 20px;
+}
+
+.stat-card {
+  text-align: center;
+}
+
+.stat-label {
+  color: #909399;
+  font-size: 14px;
+  margin-bottom: 10px;
+}
+
+.stat-value {
+  font-size: 28px;
+  font-weight: bold;
+  color: #409EFF;
+}
+</style>

+ 269 - 19
zxyj-web/src/views/Users.vue

@@ -3,46 +3,296 @@
     <el-card>
       <div slot="header">
         <span>用户管理</span>
+        <el-button 
+          type="primary" 
+          size="small" 
+          style="float: right"
+          @click="handleRefresh"
+          :loading="loading"
+        >
+          刷新
+        </el-button>
       </div>
-      <el-table :data="tableData" border>
-        <el-table-column prop="id" label="ID" width="60"></el-table-column>
-        <el-table-column prop="nickname" label="昵称"></el-table-column>
-        <el-table-column prop="role" label="角色">
+
+      <!-- 搜索和筛选 -->
+      <el-form :inline="true" :model="searchForm" style="margin-bottom: 20px">
+        <el-form-item label="角色">
+          <el-select v-model="searchForm.role" placeholder="全部角色" clearable @change="handleSearch">
+            <el-option label="家长" value="parent"></el-option>
+            <el-option label="孩子" value="child"></el-option>
+            <el-option label="教师" value="teacher"></el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item label="昵称">
+          <el-input 
+            v-model="searchForm.nickname" 
+            placeholder="请输入昵称" 
+            clearable
+            @keyup.enter.native="handleSearch"
+          ></el-input>
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" @click="handleSearch">搜索</el-button>
+          <el-button @click="handleReset">重置</el-button>
+        </el-form-item>
+      </el-form>
+
+      <!-- 用户列表 -->
+      <el-table 
+        :data="tableData" 
+        border 
+        v-loading="loading"
+        style="width: 100%"
+      >
+        <el-table-column prop="id" label="ID" width="80"></el-table-column>
+        <el-table-column prop="nickname" label="昵称" width="150"></el-table-column>
+        <el-table-column prop="realName" label="真实姓名" width="120"></el-table-column>
+        <el-table-column prop="phone" label="手机号" width="120"></el-table-column>
+        <el-table-column prop="role" label="角色" width="100">
           <template slot-scope="scope">
-            <el-tag :type="scope.row.role === 'parent' ? 'success' : 'warning'" size="small">
-              {{ scope.row.role === 'parent' ? '家长' : '孩子' }}
+            <el-tag 
+              :type="getRoleType(scope.row.role)" 
+              size="small"
+            >
+              {{ getRoleText(scope.row.role) }}
             </el-tag>
           </template>
         </el-table-column>
-        <el-table-column prop="familyId" label="家庭ID"></el-table-column>
-        <el-table-column prop="createdAt" label="创建时间"></el-table-column>
-        <el-table-column label="操作" width="120">
+        <el-table-column prop="familyId" label="家庭ID" width="100"></el-table-column>
+        <el-table-column prop="createdAt" label="创建时间" width="180">
+          <template slot-scope="scope">
+            {{ formatDate(scope.row.createdAt) }}
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" width="200" fixed="right">
           <template slot-scope="scope">
-            <el-button type="text" size="small">编辑</el-button>
-            <el-button type="text" size="small" style="color: #F56C6C">删除</el-button>
+            <el-button 
+              type="text" 
+              size="small" 
+              @click="handleEdit(scope.row)"
+            >
+              编辑
+            </el-button>
+            <el-button 
+              type="text" 
+              size="small"
+              @click="handleViewFamily(scope.row)"
+              v-if="scope.row.familyId"
+            >
+              查看家庭
+            </el-button>
+            <el-button 
+              type="text" 
+              size="small" 
+              style="color: #F56C6C"
+              @click="handleDelete(scope.row)"
+            >
+              删除
+            </el-button>
           </template>
         </el-table-column>
       </el-table>
+
+      <!-- 分页 -->
       <el-pagination
         style="margin-top: 20px; text-align: right"
-        :current-page="1"
-        :page-size="10"
-        :total="50"
-        layout="total, prev, pager, next"
+        @size-change="handleSizeChange"
+        @current-change="handleCurrentChange"
+        :current-page="pagination.current"
+        :page-sizes="[10, 20, 50, 100]"
+        :page-size="pagination.size"
+        :total="pagination.total"
+        layout="total, sizes, prev, pager, next, jumper"
       ></el-pagination>
     </el-card>
+
+    <!-- 编辑用户弹窗 -->
+    <el-dialog 
+      title="编辑用户" 
+      :visible.sync="editDialogVisible" 
+      width="500px"
+    >
+      <el-form :model="editForm" :rules="editRules" ref="editForm" label-width="100px">
+        <el-form-item label="昵称" prop="nickname">
+          <el-input v-model="editForm.nickname" placeholder="请输入昵称"></el-input>
+        </el-form-item>
+        <el-form-item label="真实姓名" prop="realName">
+          <el-input v-model="editForm.realName" placeholder="请输入真实姓名"></el-input>
+        </el-form-item>
+        <el-form-item label="手机号" prop="phone">
+          <el-input v-model="editForm.phone" placeholder="请输入手机号"></el-input>
+        </el-form-item>
+        <el-form-item label="角色" prop="role">
+          <el-select v-model="editForm.role" placeholder="请选择角色">
+            <el-option label="家长" value="parent"></el-option>
+            <el-option label="孩子" value="child"></el-option>
+            <el-option label="教师" value="teacher"></el-option>
+          </el-select>
+        </el-form-item>
+      </el-form>
+      <div slot="footer">
+        <el-button @click="editDialogVisible = false">取消</el-button>
+        <el-button type="primary" @click="submitEdit" :loading="submitting">确定</el-button>
+      </div>
+    </el-dialog>
   </div>
 </template>
 
 <script>
+import { getUserList, updateUser, deleteUser } from '@/api/admin'
+
 export default {
+  name: 'Users',
   data() {
     return {
-      tableData: [
-        { id: 1, nickname: '张三', role: 'parent', familyId: 1, createdAt: '2026-03-30 10:00:00' },
-        { id: 2, nickname: '小明', role: 'child', familyId: 1, createdAt: '2026-03-30 10:00:00' }
-      ]
+      loading: false,
+      submitting: false,
+      tableData: [],
+      searchForm: {
+        role: '',
+        nickname: ''
+      },
+      pagination: {
+        current: 1,
+        size: 10,
+        total: 0
+      },
+      editDialogVisible: false,
+      editForm: {
+        id: null,
+        nickname: '',
+        realName: '',
+        phone: '',
+        role: ''
+      },
+      editRules: {
+        nickname: [
+          { required: true, message: '请输入昵称', trigger: 'blur' }
+        ],
+        role: [
+          { required: true, message: '请选择角色', trigger: 'change' }
+        ]
+      }
+    }
+  },
+  mounted() {
+    this.loadUsers()
+  },
+  methods: {
+    async loadUsers() {
+      this.loading = true
+      try {
+        const params = {
+          page: this.pagination.current,
+          size: this.pagination.size,
+          ...this.searchForm
+        }
+        const res = await getUserList(params)
+        this.tableData = res.data.records || res.data || []
+        this.pagination.total = res.data.total || this.tableData.length
+      } catch (error) {
+        this.$message.error(error.message || '加载用户列表失败')
+      } finally {
+        this.loading = false
+      }
+    },
+    handleSearch() {
+      this.pagination.current = 1
+      this.loadUsers()
+    },
+    handleReset() {
+      this.searchForm = {
+        role: '',
+        nickname: ''
+      }
+      this.handleSearch()
+    },
+    handleRefresh() {
+      this.loadUsers()
+    },
+    handleSizeChange(val) {
+      this.pagination.size = val
+      this.loadUsers()
+    },
+    handleCurrentChange(val) {
+      this.pagination.current = val
+      this.loadUsers()
+    },
+    handleEdit(row) {
+      this.editForm = {
+        id: row.id,
+        nickname: row.nickname,
+        realName: row.realName || '',
+        phone: row.phone || '',
+        role: row.role
+      }
+      this.editDialogVisible = true
+    },
+    async submitEdit() {
+      this.$refs.editForm.validate(async (valid) => {
+        if (valid) {
+          this.submitting = true
+          try {
+            await updateUser(this.editForm.id, this.editForm)
+            this.$message.success('更新成功')
+            this.editDialogVisible = false
+            this.loadUsers()
+          } catch (error) {
+            this.$message.error(error.message || '更新失败')
+          } finally {
+            this.submitting = false
+          }
+        }
+      })
+    },
+    handleDelete(row) {
+      this.$confirm(`确定要删除用户 "${row.nickname}" 吗?`, '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(async () => {
+        try {
+          await deleteUser(row.id)
+          this.$message.success('删除成功')
+          this.loadUsers()
+        } catch (error) {
+          this.$message.error(error.message || '删除失败')
+        }
+      }).catch(() => {})
+    },
+    handleViewFamily(row) {
+      this.$router.push({
+        path: '/families',
+        query: { familyId: row.familyId }
+      })
+    },
+    getRoleType(role) {
+      const types = {
+        parent: 'success',
+        child: 'warning',
+        teacher: 'primary'
+      }
+      return types[role] || 'info'
+    },
+    getRoleText(role) {
+      const texts = {
+        parent: '家长',
+        child: '孩子',
+        teacher: '教师'
+      }
+      return texts[role] || role
+    },
+    formatDate(dateStr) {
+      if (!dateStr) return '-'
+      const date = new Date(dateStr)
+      return date.toLocaleString('zh-CN')
     }
   }
 }
 </script>
+
+<style scoped>
+.page-container {
+  padding: 20px;
+}
+</style>