Explorar el Código

1.试题弹窗修复

3. 弹窗取消点击空白关闭

4. 删除视频列表菜单,改成视频列表弹窗

5.通知公告那把来源去掉

6.vip管理,只给修改,只可以改名称和价格和备注

7.纠错添加标记处理情况,处理状态设置和备注,仅后台查看

8.会员新增或者设置vip时有效时间默认按当前开始一年加
弹窗里显示openid

"9.
1)首页统计的用户访问统计是lev_exam_access_logs,按日期,按类别显示统计,
2)用户答题排行榜了留着,
3)vip统计多一栏vip类型筛选根据不同vip统计
4 视频统计改成视频付费统计"
罗永浩 hace 6 meses
padre
commit
ace46771d2
Se han modificado 32 ficheros con 1179 adiciones y 318 borrados
  1. 2 2
      addons/admin/src/main.js
  2. 92 19
      addons/admin/src/views/complaint/complaint.vue
  3. 1 0
      addons/admin/src/views/dashboard/components/AnswerRanks.vue
  4. 153 0
      addons/admin/src/views/dashboard/components/ExamAccessRanks.vue
  5. 1 0
      addons/admin/src/views/dashboard/components/MemberList.vue
  6. 8 0
      addons/admin/src/views/dashboard/components/OrderList.vue
  7. 8 4
      addons/admin/src/views/dashboard/workplace.vue
  8. 2 1
      addons/admin/src/views/exam/component/PaperForm.vue
  9. 3 2
      addons/admin/src/views/exam/component/SubjectManager.vue
  10. 135 60
      addons/admin/src/views/exam/component/TopicManager.vue
  11. 7 7
      addons/admin/src/views/exam/examList.vue
  12. 19 5
      addons/admin/src/views/member/member/index.vue
  13. 5 4
      addons/admin/src/views/subject/subject.vue
  14. 4 4
      addons/admin/src/views/system/ad/components/list.vue
  15. 1 1
      addons/admin/src/views/system/ad/index.vue
  16. 5 0
      addons/admin/src/views/system/article/components/ArticleForm.vue
  17. 14 5
      addons/admin/src/views/system/article/components/list.vue
  18. 49 52
      addons/admin/src/views/system/notice/index.vue
  19. 260 0
      addons/admin/src/views/video/component/CoursesDialog.vue
  20. 24 11
      addons/admin/src/views/video/component/CoursesForm.vue
  21. 47 47
      addons/admin/src/views/video/courses.vue
  22. 66 2
      addons/admin/src/views/video/video.vue
  23. 7 6
      addons/admin/src/views/vip/vip.vue
  24. 1 1
      app/Http/Controllers/Admin/AccountController.php
  25. 12 0
      app/Http/Controllers/Admin/IndexController.php
  26. 15 12
      app/Models/MemberModel.php
  27. 27 41
      app/Services/Common/AccountService.php
  28. 0 2
      app/Services/Common/ArticleService.php
  29. 116 0
      app/Services/Common/ExamAccessLogsService.php
  30. 87 29
      app/Services/Common/MemberService.php
  31. 6 1
      app/Services/Exam/TopicService.php
  32. 2 0
      routes/web.php

+ 2 - 2
addons/admin/src/main.js

@@ -10,9 +10,9 @@ Vue.config.productionTip = false;
 import "./assets/css/common.css"; // 引入通用CSS
 import TextEllipsis from "./components/TextEllipsis.vue";
 import ImagePreview from "./components/ImagePreview.vue";
-import permissionMixin from "@/mixins/permissionMixin";
+// import permissionMixin from "@/mixins/permissionMixin";
 
-Vue.mixin(permissionMixin);
+// Vue.mixin(permissionMixin);
 // 全局注册组件
 Vue.component("TextEllipsis", TextEllipsis);
 Vue.component("ImagePreview", ImagePreview);

+ 92 - 19
addons/admin/src/views/complaint/complaint.vue

@@ -30,6 +30,14 @@
                             </el-select>
                         </el-form-item>
                     </el-col>
+                    <el-col :md="6" :sm="12">
+                        <el-form-item label="处理结果">
+                            <el-select v-model="table.where.process_result" placeholder="请选择处理结果" clearable>
+                                <el-option v-for="(label, key) in processResultOptions" :key="key" :label="label"
+                                    :value="Number(key)" />
+                            </el-select>
+                        </el-form-item>
+                    </el-col>
                     <el-col :md="24">
                         <div class="ele-form-actions">
                             <el-button type="primary" @click="$refs.table.reload()" icon="el-icon-search">查询</el-button>
@@ -42,7 +50,7 @@
             <!-- 操作按钮 -->
             <div class="ele-table-tool ele-table-tool-default">
                 <el-button type="danger" icon="el-icon-delete" @click="batchRemove()"
-                    v-if="hasPerm(permissionMap['delete'])">批量删除</el-button>
+                    v-if="permission.includes(permissionMap['delete'])">批量删除</el-button>
             </div>
 
             <!-- 数据表格 -->
@@ -64,21 +72,41 @@
                         </template>
                     </el-table-column>
                     <el-table-column prop="create_time" label="提交时间" width="180" />
-                    <el-table-column prop="status" label="状态" width="100">
-                        <template slot-scope="{ row }">
-                            <span>{{ row.status === 1 ? '待处理' : '已处理' }}</span>
-                        </template>
-                    </el-table-column><el-table-column prop="remark" label="处理备注" width="180">
-                        <template slot-scope="{ row }">
-                            <text-ellipsis :text="row.remark" :maxLength="10" />
-                        </template>
+                    <el-table-column prop="" align="center" label="对用户" min-width="280">
+                        <el-table-column prop="status" align="center" label="状态" width="100">
+                            <template slot-scope="{ row }">
+                                <el-tag :type="row.status === 1 ? 'warning' : 'success'" effect="light">
+                                    {{ statusOptions[row.status] || '未知' }}
+                                </el-tag>
+                            </template>
+                        </el-table-column><el-table-column align="center" prop="remark" label="处理备注" width="180">
+                            <template slot-scope="{ row }">
+                                <text-ellipsis :text="row.remark" :maxLength="10" />
+                            </template>
+                        </el-table-column>
+                    </el-table-column>
+                    <!-- 后台内部 -->
+                    <el-table-column prop="" align="center" label="后台内部" min-width="300">
+                        <el-table-column prop="process_result" align="center" label="处理结果" width="120">
+                            <template slot-scope="{ row }">
+                                <el-tag :type="getProcessTagType(row.process_result)" effect="light">
+                                    {{ processResultOptions[row.process_result] || '未知' }}
+                                </el-tag>
+                            </template>
+                        </el-table-column>
+
+                        <el-table-column prop="internal_note" align="center" label="内部备注" min-width="180">
+                            <template slot-scope="{ row }">
+                                <text-ellipsis :text="row.internal_note" :maxLength="10" />
+                            </template>
+                        </el-table-column>
                     </el-table-column>
                     <el-table-column label="操作" width="220" fixed="right">
                         <template slot-scope="{ row }">
                             <el-button type="text" size="mini" @click="remove(row)"
-                                v-if="hasPerm(permissionMap['delete'])">删除</el-button>
+                                v-if="permission.includes(permissionMap['delete'])">删除</el-button>
                             <el-button type="text" size="mini" @click="handleComplaint(row)"
-                                v-if="row.status === 1 && hasPerm(permissionMap['edit'])">处理</el-button>
+                                v-if="row.status === 1 && permission.includes(permissionMap['edit'])">处理</el-button>
                         </template>
                     </el-table-column>
                 </template>
@@ -86,11 +114,36 @@
         </el-card>
 
         <!-- 处理弹窗 -->
-        <el-dialog title="处理反馈" :visible.sync="handleVisible" width="500px">
-            <el-form :model="handleData" :rules="handleRules" ref="handleForm" label-width="120px">
-                <el-form-item label="处理备注" prop="remark">
-                    <el-input type="textarea" v-model="handleData.remark" placeholder="请输入处理备注" rows="4" />
+        <el-dialog title="处理反馈" :visible.sync="handleVisible" min-width="600px">
+            <el-form :model="handleData" :rules="handleRules" ref="handleForm" label-width="140px">
+                <el-divider class="mb-16">用户可见</el-divider>
+
+                <!-- 处理结果 -->
+                <el-form-item label="处理结果(对用户)" prop="status">
+                    <el-select v-model="handleData.status" placeholder="请选择状态" clearable>
+                        <el-option label="待处理" :value="1" />
+                        <el-option label="已处理" :value="2" />
+                    </el-select>
                 </el-form-item>
+                <!-- 对用户可见的备注 -->
+                <el-form-item label="处理备注(对用户)" prop="remark">
+                    <el-input type="textarea" v-model="handleData.remark" placeholder="请输入处理备注(用户可见)" rows="3" />
+                </el-form-item>
+                <el-divider class="mb-16">仅内部可见</el-divider>
+
+                <!-- 处理结果 -->
+                <el-form-item label="处理结果(仅后台)" prop="process_result">
+                    <el-select v-model="handleData.process_result" placeholder="请选择处理结果" clearable>
+                        <el-option v-for="(label, key) in processResultOptions" :key="key" :label="label"
+                            :value="Number(key)" />
+                    </el-select>
+                </el-form-item>
+
+                <!-- 内部备注 -->
+                <el-form-item label="内部备注(仅后台)">
+                    <el-input type="textarea" v-model="handleData.internal_note" placeholder="请输入内部备注,仅后台可见" rows="3" />
+                </el-form-item>
+
             </el-form>
             <div slot="footer" class="dialog-footer">
                 <el-button @click="handleVisible = false">取消</el-button>
@@ -110,10 +163,19 @@ export default {
             },
             choose: [],
             typeOptions: { 1: '功能BUG', 2: '题库问题', 3: '产品问题', 4: 'VIP问题', 5: '费用相关', 99: '其他' },
+            statusOptions: { 1: '待处理', 2: '已处理' },
+            processResultOptions: {
+                0: '未处理',
+                1: '已解决',
+                2: '无需处理',
+                3: '转交/升级'
+            },
             handleVisible: false,
-            handleData: { id: null, remark: '' },
+            handleData: { id: null, remark: '', internal_note: '', process_result: 1 },
             handleRules: {
-                remark: [{ required: true, message: '请输入处理备注', trigger: 'blur' }]
+                remark: [{ required: true, message: '请输入处理备注', trigger: 'blur' }],
+                process_result: [{ required: true, message: '请选择处理结果', trigger: 'change' }]
+
             },
             permissionMap: {
                 delete: "sys:complaint:delete",
@@ -123,7 +185,18 @@ export default {
             }
         }
     },
+
+    computed: { ...mapGetters(["permission"]) },
     methods: {
+        getProcessTagType(value) {
+            switch (value) {
+                case 0: return 'info';     // 未处理
+                case 1: return 'success';  // 已解决
+                case 2: return 'warning';  // 无需处理
+                case 3: return 'danger';   // 转交/升级
+                default: return '';
+            }
+        },
         resetSearch() {
             this.table.where = { title: '', user_name: '', type: null, status: null };
             this.$refs.table.reload();
@@ -154,14 +227,14 @@ export default {
             });
         },
         handleComplaint(row) {
-            this.handleData = { id: row.id, remark: '' };
+            this.handleData = row;
             this.handleVisible = true;
         },
         saveHandle() {
             this.$refs.handleForm.validate(async valid => {
                 if (!valid) return;
                 try {
-                    const res = await this.$http.post("/complaint/edit", { ...this.handleData, status: 2 });
+                    const res = await this.$http.post("/complaint/edit", { ...this.handleData });
                     if (res.data.code === 0) {
                         this.$message.success("处理成功");
                         this.handleVisible = false;

+ 1 - 0
addons/admin/src/views/dashboard/components/AnswerRanks.vue

@@ -93,6 +93,7 @@ export default {
             }
         },
         datePickerType() {
+            return "date";
             switch (this.query.dateType) {
                 case "year":
                     return "year";

+ 153 - 0
addons/admin/src/views/dashboard/components/ExamAccessRanks.vue

@@ -0,0 +1,153 @@
+<template>
+    <div class="mt-10">
+        <!-- 筛选表单 -->
+        <el-form :model="query" inline label-width="100px" size="small" @submit.native.prevent>
+            <el-form-item label="时间类型">
+                <el-select v-model="query.dateType" placeholder="请选择">
+                    <el-option label="日" value="day" />
+                    <el-option label="周" value="week" />
+                    <el-option label="月" value="month" />
+                </el-select>
+            </el-form-item>
+
+            <el-form-item label="开始时间">
+                <el-date-picker v-model="query.start_time" :type="datePickerType" placeholder="选择开始时间"
+                    value-format="yyyy-MM-dd" />
+            </el-form-item>
+
+            <el-form-item label="结束时间">
+                <el-date-picker v-model="query.end_time" :type="datePickerType" placeholder="选择结束时间"
+                    value-format="yyyy-MM-dd" />
+            </el-form-item>
+
+            <el-form-item>
+                <el-button type="primary" @click="onSearch">查询</el-button>
+                <el-button @click="resetQuery">重置</el-button>
+            </el-form-item>
+        </el-form>
+
+        <!-- 表格 -->
+        <el-table :data="list" border style="width: 100%; margin-top: 10px;">
+            <el-table-column prop="stat_date" :label="dateLabel" fixed="left" />
+
+            <el-table-column prop="type" label="模块类型" width="120">
+                <template #default="{ row }">
+                    <el-tag>
+                        {{ getModuleLabel(row.type) }}
+                    </el-tag>
+                </template>
+            </el-table-column>
+
+            <el-table-column prop="scene_count1" label="每日一练" width="120" />
+            <el-table-column prop="scene_count2" label="历年真题" width="120" />
+            <el-table-column prop="scene_count3" label="职业适应性模拟" width="160" />
+            <el-table-column prop="scene_count4" label="模拟题" width="120" />
+            <el-table-column prop="scene_count5" label="复习资料" width="120" />
+            <el-table-column prop="scene_count6" label="公共基础课测试" width="160" />
+            <el-table-column prop="scene_count7" label="专业基础课测试" width="160" />
+            <el-table-column prop="scene_count20" label="视频课程" width="120" />
+        </el-table>
+
+        <!-- 分页 -->
+        <el-pagination v-model:current-page="query.page" v-model:page-size="query.limit"
+            layout="total, sizes, prev, pager, next, jumper" :total="total" :page-sizes="[10, 20, 50, 100]"
+            style="margin-top: 10px; text-align: right;" @current-change="onPageChange" @size-change="onSizeChange" />
+    </div>
+</template>
+
+<script>
+export default {
+    name: "ExamAccessRanks",
+    data() {
+        return {
+            query: {
+                dateType: "day",
+                start_time: "",
+                end_time: "",
+                page: 1,
+                limit: 10,
+            },
+            list: [],
+            total: 0,
+        };
+    },
+    computed: {
+        dateLabel() {
+            switch (this.query.dateType) {
+                case "day":
+                    return "日期";
+                case "week":
+                    return "周数";
+                case "month":
+                    return "月份";
+                default:
+                    return "时间";
+            }
+        },
+        datePickerType() {
+            return "date";
+            switch (this.query.dateType) {
+                case "week":
+                    return "week";
+                case "month":
+                    return "month";
+                default:
+                    return "date";
+            }
+        }
+    },
+    created() {
+        this.loadList();
+    },
+    methods: {
+        getModuleLabel(type) {
+            switch (type) {
+                case 1: return "职高单招";
+                case 2: return "职高对口";
+                case 3: return "专升本";
+                default: return "未知模块";
+            }
+        },
+        loadList() {
+            const params = { ...this.query };
+            this.$http.post("/index/examAccessStats", params).then(res => {
+                if (res.data.code === 0) {
+                    this.list = res.data.data.list;
+                    this.total = res.data.data.total;
+                } else {
+                    this.$message.error(res.data.msg || "查询失败");
+                }
+            });
+        },
+        onSearch() {
+            this.query.page = 1;
+            this.loadList();
+        },
+        resetQuery() {
+            this.query = {
+                dateType: "day",
+                start_time: "",
+                end_time: "",
+                page: 1,
+                limit: 10,
+            };
+            this.loadList();
+        },
+        onPageChange(newPage) {
+            this.query.page = newPage;
+            this.loadList();
+        },
+        onSizeChange(newSize) {
+            this.query.limit = newSize;
+            this.query.page = 1;
+            this.loadList();
+        }
+    }
+};
+</script>
+
+<style scoped>
+.mt-10 {
+    margin-top: 10px;
+}
+</style>

+ 1 - 0
addons/admin/src/views/dashboard/components/MemberList.vue

@@ -82,6 +82,7 @@ export default {
             }
         },
         datePickerType() {
+            return "date";
             switch (this.query.dateType) {
                 case "year":
                     return "year";

+ 8 - 0
addons/admin/src/views/dashboard/components/OrderList.vue

@@ -20,6 +20,13 @@
                     value-format="yyyy-MM-dd" />
             </el-form-item>
 
+            <el-form-item label="VIP类型" v-if="defaultType == 1">
+                <el-select v-model="query.vip_type" placeholder="请选择" clearable>
+                    <el-option label="职高VIP" value="zg_vip" />
+                    <el-option label="专升本VIP" value="zsb_vip" />
+                    <el-option label="视频VIP" value="video_vip" />
+                </el-select>
+            </el-form-item>
 
             <el-form-item>
                 <el-button type="primary" @click="onSearch">查询</el-button>
@@ -103,6 +110,7 @@ export default {
             }
         },
         datePickerType() {
+            return "date";
             switch (this.query.dateType) {
                 case "year":
                     return "year";

+ 8 - 4
addons/admin/src/views/dashboard/workplace.vue

@@ -59,10 +59,13 @@
     <!-- Tab 切换 -->
     <el-card shadow="never" style="margin-top: 20px;">
       <el-tabs v-model="activeTab">
-        <el-tab-pane label="用户访问统计" name="answer_ranks">
+        <el-tab-pane label="用户访问统计" name="exam_access_ranks">
+          <ExamAccessRanks v-if="activeTab === 'exam_access_ranks'" />
+        </el-tab-pane>
+        <el-tab-pane label="用户答题排行榜" name="answer_ranks">
           <AnswerRanks v-if="activeTab === 'answer_ranks'" />
         </el-tab-pane>
-        <el-tab-pane label="视频统计" name="order">
+        <el-tab-pane label="视频付费统计" name="order">
           <OrderList v-if="activeTab === 'order'" defaultType="2" />
         </el-tab-pane>
         <el-tab-pane label="VIP统计" name="vip">
@@ -80,17 +83,18 @@
 import OrderList from "./components/OrderList.vue";
 import MemberList from "./components/MemberList.vue";
 import AnswerRanks from "./components/AnswerRanks.vue";
+import ExamAccessRanks from "./components/ExamAccessRanks.vue";
 import { mapGetters } from "vuex";
 
 export default {
   name: "Workplace",
-  components: { OrderList, MemberList, AnswerRanks },
+  components: { OrderList, MemberList, AnswerRanks, ExamAccessRanks },
   computed: {
     ...mapGetters(["permission"]),
   },
   data() {
     return {
-      activeTab: "answer_ranks", // 默认营业额
+      activeTab: "vip", // 默认营业额
       datas: {
         // 用户数据
         users: {

+ 2 - 1
addons/admin/src/views/exam/component/PaperForm.vue

@@ -1,5 +1,6 @@
 <template>
-    <el-dialog :title="isEdit ? '编辑试卷' : '添加试卷'" :visible.sync="visibleInternal" width="700px">
+    <el-dialog :close-on-click-modal="false" :title="isEdit ? '编辑试卷' : '添加试卷'" :visible.sync="visibleInternal"
+        min-width="1100px">
         <div v-if="loaded">
             <el-form :model="formData" :rules="formRules" ref="formRef" label-width="120px">
                 <el-form-item label="试卷名称" prop="name">

+ 3 - 2
addons/admin/src/views/exam/component/SubjectManager.vue

@@ -1,7 +1,7 @@
 <template>
     <div>
         <!-- 科目管理弹窗 -->
-        <el-dialog title="科目分类管理" :visible.sync="localVisible" width="800px">
+        <el-dialog :close-on-click-modal="false" title="科目分类管理" :visible.sync="localVisible" width="800px">
             <ele-data-table ref="subjectTable" :config="subjectTable" highlight-current-row border stripe>
                 <template slot-scope="{ row }">
                     <el-table-column prop="id" label="ID" width="60" align="center" />
@@ -29,7 +29,8 @@
         </el-dialog>
 
         <!-- 新增/编辑弹窗 -->
-        <el-dialog :title="isEdit ? '编辑科目' : '新增科目'" :visible.sync="formVisible" width="500px" @close="resetForm">
+        <el-dialog :close-on-click-modal="false" :title="isEdit ? '编辑科目' : '新增科目'" :visible.sync="formVisible"
+            width="500px" @close="resetForm">
             <el-form :model="formData" :rules="formRules" ref="formRef" label-width="120px">
                 <el-form-item label="课程名称" prop="subject_name">
                     <el-input v-model="formData.subject_name" placeholder="请输入科目名称" />

+ 135 - 60
addons/admin/src/views/exam/component/TopicManager.vue

@@ -1,6 +1,6 @@
 <template>
-    <el-dialog :visible="visible" :title="'试题管理 - 试卷ID: ' + paperId" width="90%" top="5vh" @close="closeDialog"
-        custom-class="topic-manager-dialog">
+    <el-dialog :close-on-click-modal="false" :visible="visible" :title="'试题管理 - 试卷ID: ' + paperId" width="90%" top="5vh"
+        @close="closeDialog" custom-class="topic-manager-dialog">
         <div class="topic-manager-container">
             <!-- 试题列表 -->
             <div class="topic-list-container">
@@ -18,37 +18,35 @@
                         </template>
                     </el-table-column>
                     <!-- <el-table-column label="ID" prop="id" width="60" /> -->
-                    <el-table-column label="题目内容" min-width="300">
+                    <el-table-column label="题目内容" min-width="150">
                         <template slot-scope="{ row }">
                             <div v-if="row.show_type === 1" class="topic-content">
                                 {{ row.topic_name }}
                             </div>
                             <div v-else class="topic-content">
-                                <image-preview size="xs" :images="row.topic_name" />
+                                <image-preview size="xs" :images="[row.topic_name]" />
                             </div>
                         </template>
                     </el-table-column>
-                    <el-table-column label="题型" width="120">
+                    <el-table-column label="题型" min-width="40">
                         <template slot-scope="{ row }">
                             <el-tag :type="getTopicTypeTag(row.topic_type)">
                                 {{ row.topic_type }}
                             </el-tag>
                         </template>
                     </el-table-column>
-                    <el-table-column label="分数" prop="score" width="80" />
-                    <el-table-column label="答案" width="120">
+                    <el-table-column label="分数" prop="score" min-width="40" />
+                    <el-table-column label="正确答案" min-width="80">
                         <template slot-scope="{ row }">
-                            <template slot-scope="{ row }">
-                                <div v-if="row.show_type === 1" class="topic-content">
-                                    <text-ellipsis :text="row.correct_answer" :max-length="10" />
-                                </div>
-                                <div v-else class="topic-content">
-                                    <image-preview size="xs" :images="row.correct_answer" />
-                                </div>
-                            </template>
+                            <div v-if="row.answer_type === 1" class="topic-content">
+                                <text-ellipsis :text="row.correct_answer" :max-length="10" />
+                            </div>
+                            <div v-else class="topic-content">
+                                <image-preview size="xs" :images="row.correct_answer" />
+                            </div>
                         </template>
                     </el-table-column>
-                    <el-table-column label="解析" width="120">
+                    <el-table-column label="解析" min-width="80">
                         <template slot-scope="{ row }">
                             <div v-if="row.show_type === 1" class="topic-content">
                                 <text-ellipsis :text="row.topic_analysis" :max-length="10" />
@@ -58,7 +56,7 @@
                             </div>
                         </template>
                     </el-table-column>
-                    <el-table-column label="操作" width="180" fixed="right">
+                    <el-table-column label="操作" min-width="60" fixed="right">
                         <template slot-scope="{ row, $index }">
                             <el-button size="mini" @click="editTopic(row)">编辑</el-button>
                             <el-button size="mini" type="danger" @click="deleteTopic(row, $index)">删除</el-button>
@@ -107,37 +105,66 @@
                         <el-input-number v-model="form.score" :min="1" :max="100"></el-input-number>
                     </el-form-item>
 
-                    <el-form-item label="允许提交图片答案" prop="answer_type" v-if="form.show_type === 2">
+                    <el-form-item label="图片选项/答案" prop="answer_type" v-if="form.show_type === 2">
                         <el-radio-group v-model="form.answer_type">
                             <el-radio :label="1">是</el-radio>
                             <el-radio :label="2">否</el-radio>
                         </el-radio-group>
                     </el-form-item>
 
-                    <!-- 根据题型显示不同的答案输入区域 -->
+                    <!-- 单选题、多选题 -->
                     <div v-if="['单选题', '多选题'].includes(form.topic_type)">
-                        <el-form-item v-for="(option, index) in options" :key="index"
-                            :label="'选项 ' + String.fromCharCode(65 + index)"
-                            :prop="'answer_' + String.fromCharCode(65 + index)">
-                            <el-input v-model="options[index]"
-                                :placeholder="'请输入选项' + String.fromCharCode(65 + index) + '的内容'"></el-input>
-                        </el-form-item>
+                        <template v-if="form.answer_type == 1">
+                            <!-- 选项:图片方式 -->
+                            <el-form-item v-for="(option, index) in options" :key="index"
+                                :label="'选项 ' + String.fromCharCode(65 + index)"
+                                :prop="'answer_' + String.fromCharCode(65 + index)">
+                                <uploadImage :limit="1" v-model="options[index]" />
+                            </el-form-item>
+
+                            <!-- 正确答案:选择对应选项 -->
+                            <el-form-item label="正确答案" prop="correct_answer">
+                                <el-select v-model="form.correct_answer" placeholder="请选择正确答案" multiple
+                                    v-if="form.topic_type === '多选题'">
+                                    <el-option v-for="(option, index) in options" :key="index"
+                                        :label="'选项 ' + String.fromCharCode(65 + index)"
+                                        :value="String.fromCharCode(65 + index)" />
+                                </el-select>
+                                <el-select v-model="form.correct_answer" placeholder="请选择正确答案" v-else>
+                                    <el-option v-for="(option, index) in options" :key="index"
+                                        :label="'选项 ' + String.fromCharCode(65 + index)"
+                                        :value="String.fromCharCode(65 + index)" />
+                                </el-select>
+                            </el-form-item>
+                        </template>
 
-                        <el-form-item label="正确答案" prop="correct_answer">
-                            <el-select v-model="form.correct_answer" placeholder="请选择正确答案" multiple
-                                v-if="form.topic_type === '多选题'">
-                                <el-option v-for="(option, index) in options" :key="index"
-                                    :label="String.fromCharCode(65 + index)"
-                                    :value="String.fromCharCode(65 + index)"></el-option>
-                            </el-select>
-                            <el-select v-model="form.correct_answer" placeholder="请选择正确答案" v-else>
-                                <el-option v-for="(option, index) in options" :key="index"
-                                    :label="String.fromCharCode(65 + index)"
-                                    :value="String.fromCharCode(65 + index)"></el-option>
-                            </el-select>
-                        </el-form-item>
+                        <template v-else>
+                            <!-- 选项:文本方式 -->
+                            <el-form-item v-for="(option, index) in options" :key="index"
+                                :label="'选项 ' + String.fromCharCode(65 + index)"
+                                :prop="'answer_' + String.fromCharCode(65 + index)">
+                                <el-input v-model="options[index]"
+                                    :placeholder="'请输入选项' + String.fromCharCode(65 + index) + '的内容'" />
+                            </el-form-item>
+
+                            <!-- 正确答案:选择对应选项 -->
+                            <el-form-item label="正确答案" prop="correct_answer">
+                                <el-select v-model="form.correct_answer" placeholder="请选择正确答案" multiple
+                                    v-if="form.topic_type === '多选题'">
+                                    <el-option v-for="(option, index) in options" :key="index"
+                                        :label="String.fromCharCode(65 + index)"
+                                        :value="String.fromCharCode(65 + index)" />
+                                </el-select>
+                                <el-select v-model="form.correct_answer" placeholder="请选择正确答案" v-else>
+                                    <el-option v-for="(option, index) in options" :key="index"
+                                        :label="String.fromCharCode(65 + index)"
+                                        :value="String.fromCharCode(65 + index)" />
+                                </el-select>
+                            </el-form-item>
+                        </template>
                     </div>
 
+
                     <div v-else-if="form.topic_type === '判断题'">
                         <el-form-item label="正确答案" prop="correct_answer">
                             <el-radio-group v-model="form.correct_answer">
@@ -145,13 +172,31 @@
                                 <el-radio label="错误">错误</el-radio>
                             </el-radio-group>
                         </el-form-item>
+                        <!-- 更详细的调试信息 -->
+                        <div style="color: red; font-size: 12px; background: #f0f0f0; padding: 5px; margin-top: 10px;">
+                            <div>调试信息 - 判断题:</div>
+                            <div>当前值: "{{ form.correct_answer }}"</div>
+                            <div>值类型: {{ typeof form.correct_answer }}</div>
+                            <div>值长度: {{ form.correct_answer ? form.correct_answer.length : 0 }}</div>
+                            <div>是否等于'正确': {{ form.correct_answer === '正确' }}</div>
+                            <div>是否等于'错误': {{ form.correct_answer === '错误' }}</div>
+                            <div>原始数据: {{ currentTopic ? currentTopic.correct_answer : '无' }}</div>
+                        </div>
                     </div>
 
+                    <!-- 填空题,简答题 -->
                     <div v-else>
-                        <el-form-item label="参考答案" prop="correct_answer">
-                            <el-input type="textarea" :rows="2" v-model="form.correct_answer"
-                                placeholder="请输入参考答案"></el-input>
-                        </el-form-item>
+                        <div v-if="form.answer_type === 1">
+                            <el-form-item label="正确答案" prop="correct_answer">
+                                <uploadImage :limit="1" v-model="form.correct_answer"></uploadImage>
+                            </el-form-item>
+                        </div>
+                        <div v-else>
+                            <el-form-item label="正确答案" prop="correct_answer">
+                                <el-input type="textarea" :rows="2" v-model="form.correct_answer"
+                                    placeholder="请输入正确答案"></el-input>
+                            </el-form-item>
+                        </div>
                     </div>
                     <el-form-item label="答案解析" prop="topic_analysis" class="mt-16">
                         <div v-if="form.show_type === 1">
@@ -239,12 +284,6 @@ export default {
                 this.resetForm();
             }
         },
-        'form.topic_type'(newVal) {
-            // 重置选项和答案
-            if (!['单选题', '多选题'].includes(newVal)) {
-                this.form.correct_answer = '';
-            }
-        }
     },
     methods: {
         async loadTopics() {
@@ -324,7 +363,24 @@ export default {
         editTopic(topic) {
             this.isEdit = true;
             this.currentTopic = { ...topic };
-            this.form = { ...topic };
+
+            // 先处理判断题答案,再整体赋值
+            let correctAnswer = topic.correct_answer;
+
+            // 专门处理判断题答案
+            if (topic.topic_type === '判断题') {
+                correctAnswer = this.formatJudgmentAnswer(topic.correct_answer);
+                console.log('判断题答案处理:', topic.correct_answer, '->', correctAnswer);
+            } else if (topic.topic_type === '多选题' && typeof topic.correct_answer === 'string') {
+                correctAnswer = topic.correct_answer.split(',');
+            }
+
+            // 整体赋值form
+            this.form = {
+                ...topic,
+                correct_answer: correctAnswer
+            };
+
             this.editingIndex = this.topics.findIndex(item => item.id === topic.id);
 
             // 设置选项
@@ -333,18 +389,39 @@ export default {
                 topic.answer_B || '',
                 topic.answer_C || '',
                 topic.answer_D || '',
-                topic.answer_E || '',
-                topic.answer_F || ''
             ];
 
-            // 处理多选题答案
-            if (topic.topic_type === '多选题' && typeof topic.correct_answer === 'string') {
-                this.form.correct_answer = topic.correct_answer.split(',');
-            }
+            console.log('编辑后form数据:', this.form);
+            console.log('correct_answer值:', this.form.correct_answer);
 
             this.showForm = true;
         },
+        // 添加判断题答案格式化方法
+        formatJudgmentAnswer(value) {
+            console.log('格式化判断题答案,输入值:', value, '类型:', typeof value);
 
+            if (value === '正确' || value === '错误') {
+                return value; // 已经是正确格式,直接返回
+            }
+
+            // 处理各种可能的数据格式
+            const mapping = {
+                '正确': '正确',
+                '错误': '错误',
+                'true': '正确',
+                'false': '错误',
+                '1': '正确',
+                '0': '错误',
+                '对的': '正确',
+                '错的': '错误',
+                '对': '正确',
+                '错': '错误'
+            };
+
+            const result = mapping[value] || '正确'; // 默认值
+            console.log('格式化结果:', result);
+            return result;
+        },
         async deleteTopic(topic, index) {
             try {
                 await this.$confirm('确定要删除这道试题吗?', '提示', {
@@ -383,7 +460,7 @@ export default {
 
                 try {
                     // 保存选项
-                    ['A', 'B', 'C', 'D', 'E', 'F'].forEach((letter, index) => {
+                    ['A', 'B', 'C', 'D'].forEach((letter, index) => {
                         this.form[`answer_${letter}`] = this.options[index] || '';
                     });
 
@@ -436,9 +513,7 @@ export default {
                 answer_A: '',
                 answer_B: '',
                 answer_C: '',
-                answer_D: '',
-                answer_E: '',
-                answer_F: ''
+                answer_D: ''
             };
             this.options = ['', '', '', ''];
             this.editingIndex = -1;
@@ -502,7 +577,7 @@ export default {
 }
 
 .topic-form-container {
-    width: 750px;
+    width: 400px;
     padding-left: 20px;
     overflow-y: auto;
     border-left: 1px solid #eee;

+ 7 - 7
addons/admin/src/views/exam/examList.vue

@@ -48,13 +48,13 @@
             <!-- 操作按钮 -->
             <div class="ele-table-tool ele-table-tool-default">
                 <el-button @click="openForm()" type="primary" icon="el-icon-plus" class="ele-btn-icon mr-10"
-                    size="small" v-if="hasPerm(permissionMap['add'])">新增试卷</el-button>
+                    size="small" v-if="permission.includes(permissionMap['add'])">新增试卷</el-button>
                 <el-button @click="excelDialogVisible = true" class="ele-btn-icon mr-10" size="small"
-                    icon="el-icon-upload2" v-if="hasPerm(permissionMap['edit'])">导入试题</el-button>
+                    icon="el-icon-upload2" v-if="permission.includes(permissionMap['edit'])">导入试题</el-button>
                 <el-button @click="remove()" type="danger" icon="el-icon-delete" class="ele-btn-icon mr-10" size="small"
-                    v-if="hasPerm(permissionMap['delete'])">批量删除</el-button>
+                    v-if="permission.includes(permissionMap['delete'])">批量删除</el-button>
                 <el-button class="ele-btn-icon mr-10" size="small" icon="el-icon-setting"
-                    @click="subjectDialogVisible = true" v-if="hasPerm(permissionMap['edit'])">分类管理</el-button>
+                    @click="subjectDialogVisible = true" v-if="permission.includes(permissionMap['edit'])">分类管理</el-button>
             </div>
 
             <!-- 数据表格 -->
@@ -93,14 +93,14 @@
                     <el-table-column label="操作" width="240" fixed="right">
                         <template slot-scope="{ row }">
                             <el-link @click="openForm(row)" icon="el-icon-edit" type="primary" :underline="false"
-                                v-if="hasPerm(permissionMap['edit'])">修改</el-link>
+                                v-if="permission.includes(permissionMap['edit'])">修改</el-link>
                             <el-popconfirm title="确定要删除此试卷吗?" @confirm="remove(row)" class="ele-action">
                                 <el-link slot="reference" icon="el-icon-delete" type="danger" :underline="false"
-                                    v-if="hasPerm(permissionMap['delete'])">删除</el-link>
+                                    v-if="permission.includes(permissionMap['delete'])">删除</el-link>
                             </el-popconfirm>
                             <el-link @click="openTopicManager(row)" icon="el-icon-document-add" type="success"
                                 :underline="false" class="ele-action"
-                                v-if="hasPerm(permissionMap['edit'])">编辑试题</el-link>
+                                v-if="permission.includes(permissionMap['edit'])">编辑试题</el-link>
                         </template>
                     </el-table-column>
                 </template>

+ 19 - 5
addons/admin/src/views/member/member/index.vue

@@ -62,6 +62,7 @@
             </template>
           </el-table-column>
           <el-table-column prop="nickname" label="昵称" min-width="120" />
+          <!-- <el-table-column prop="openid" label="OpenID" min-width="200" /> -->
           <el-table-column prop="mobile" label="手机号" min-width="120" />
           <el-table-column prop="create_time" label="注册时间" min-width="160" />
           <el-table-column label="VIP类型" min-width="180">
@@ -95,10 +96,14 @@
     </el-card>
 
     <!-- 编辑弹窗 -->
-    <el-dialog :title="editForm.id ? '修改会员' : '新增会员'" :visible.sync="showEdit" width="600px"
+    <el-dialog :title="editForm.id ? '修改会员' : '新增会员'" :visible.sync="showEdit" min-width="600px"
       @closed="editForm = { status: 1 }" :destroy-on-close="true" top="20px" :close-on-click-modal="false"
       custom-class="ele-dialog-form" :lock-scroll="false">
-      <el-form :model="editForm" ref="editForm" label-width="120px" :rules="editRules">
+      <el-form :model="editForm" ref="editForm" label-width="150px" :rules="editRules">
+        <el-form-item label="OpenID:">
+          <el-input v-model="editForm.openid" disabled />
+        </el-form-item>
+
         <el-form-item label="头像:">
           <uploadImage v-if="showEdit" :limit="1" v-model="editForm.avatar" />
         </el-form-item>
@@ -109,13 +114,22 @@
           <el-input v-model="editForm.nickname" placeholder="请输入昵称" />
         </el-form-item>
         <el-form-item label="职高VIP:">
-          <el-switch v-model="editForm.is_zg_vip" active-value="1" inactive-value="2" />
+          <el-switch v-model="editForm.is_zg_vip" :active-value="1" :inactive-value="2" />
+        </el-form-item>
+        <el-form-item label="职高VIP到期时间:" v-if="editForm.is_zg_vip == 1">
+          <el-input v-model="editForm.zg_vip_expired" disabled />
         </el-form-item>
         <el-form-item label="专升本VIP:">
-          <el-switch v-model="editForm.is_zsb_vip" active-value="1" inactive-value="2" />
+          <el-switch v-model="editForm.is_zsb_vip" :active-value="1" :inactive-value="2" />
+        </el-form-item>
+        <el-form-item label="专升本VIP到期时间:" v-if="editForm.is_zsb_vip == 1">
+          <el-input v-model="editForm.zsb_vip_expired" disabled />
         </el-form-item>
         <el-form-item label="视频VIP:">
-          <el-switch v-model="editForm.is_video_vip" active-value="1" inactive-value="2" />
+          <el-switch v-model="editForm.is_video_vip" :active-value="1" :inactive-value="2" />
+        </el-form-item>
+        <el-form-item label="视频VIP到期时间:" v-if="editForm.is_video_vip == 1">
+          <el-input v-model="editForm.video_vip_expired" disabled />
         </el-form-item>
         <el-form-item label="刷题类型:" prop="entry_type">
           <el-select v-model="editForm.entry_type" placeholder="请选择刷题类型">

+ 5 - 4
addons/admin/src/views/subject/subject.vue

@@ -37,11 +37,11 @@
 
             <!-- 操作按钮 -->
             <div class="ele-table-tool ele-table-tool-default">
-                <el-button type="primary" icon="el-icon-plus" @click="openForm()" v-if="hasPerm(permissionMap['edit'])">
+                <el-button type="primary" icon="el-icon-plus" @click="openForm()" v-if="permission.includes(permissionMap['edit'])">
                     新增科目
                 </el-button>
                 <el-button type="danger" icon="el-icon-delete" @click="batchRemove()"
-                    v-if="hasPerm(permissionMap['delete'])">
+                    v-if="permission.includes(permissionMap['delete'])">
                     批量删除
                 </el-button>
             </div>
@@ -68,9 +68,9 @@
                     <el-table-column label="操作" width="160" fixed="right">
                         <template slot-scope="{ row }">
                             <el-button type="text" size="mini" @click="openForm(row)"
-                                v-if="hasPerm(permissionMap['edit'])">编辑</el-button>
+                                v-if="permission.includes(permissionMap['edit'])">编辑</el-button>
                             <el-button type="text" size="mini" @click="remove(row)"
-                                v-if="hasPerm(permissionMap['delete'])">删除</el-button>
+                                v-if="permission.includes(permissionMap['delete'])">删除</el-button>
                         </template>
                     </el-table-column>
                 </template>
@@ -129,6 +129,7 @@ export default {
             }
         };
     },
+    computed: { ...mapGetters(["permission"]) },
     methods: {
         resetSearch() {
             this.table.where = { keyword: "", type: null, status: null };

+ 4 - 4
addons/admin/src/views/system/ad/components/list.vue

@@ -22,9 +22,9 @@
       <!-- 操作按钮 -->
       <div class="ele-table-tool ele-table-tool-default">
         <el-button @click="showEdit = true" type="primary" icon="el-icon-plus" class="ele-btn-icon" size="small"
-          v-if="hasPerm(permissionMap['add'])">添加</el-button>
+          v-if="permission.includes(permissionMap['add'])">添加</el-button>
         <el-button @click="remove()" type="danger" icon="el-icon-delete" class="ele-btn-icon" size="small"
-          v-if="hasPerm(permissionMap['delete'])">批量删除</el-button>
+          v-if="permission.includes(permissionMap['delete'])">批量删除</el-button>
       </div>
 
       <!-- 数据表格 -->
@@ -57,10 +57,10 @@
           <el-table-column label="操作" width="130px" align="center" :resizable="false" fixed="right">
             <template slot-scope="{row}">
               <el-link @click="edit(row)" icon="el-icon-edit" type="primary" :underline="false"
-                v-if="hasPerm(permissionMap['edit'])">修改</el-link>
+                v-if="permission.includes(permissionMap['edit'])">修改</el-link>
               <el-popconfirm title="确定要删除此吗?" @confirm="remove(row)" class="ele-action">
                 <el-link slot="reference" icon="el-icon-delete" type="danger" :underline="false"
-                  v-if="hasPerm(permissionMap['delete'])">删除</el-link>
+                  v-if="permission.includes(permissionMap['delete'])">删除</el-link>
               </el-popconfirm>
             </template>
           </el-table-column>

+ 1 - 1
addons/admin/src/views/system/ad/index.vue

@@ -21,7 +21,7 @@
       </el-form>
       <!-- 操作按钮 -->
       <div class="ele-table-tool ele-table-tool-default">
-        <el-button @click="showEdit=true" type="primary" icon="el-icon-plus" class="ele-btn-icon" size="small" v-if="hasPerm('sys:ad:add')">添加
+        <el-button @click="showEdit=true" type="primary" icon="el-icon-plus" class="ele-btn-icon" size="small" v-if="permission.includes('sys:ad:add')">添加
         </el-button>
         <el-button @click="remove()" type="danger" icon="el-icon-delete" class="ele-btn-icon" size="small" v-if="permission.includes('sys:ad:dall')">批量删除
         </el-button>

+ 5 - 0
addons/admin/src/views/system/article/components/ArticleForm.vue

@@ -12,6 +12,9 @@
                     <el-form-item label="标题:" prop="title">
                         <el-input v-model="formData.title" placeholder="请输入标题" clearable />
                     </el-form-item>
+                    <el-form-item label="作者:" prop="author">
+                        <el-input v-model="formData.author" placeholder="请输入作者" clearable />
+                    </el-form-item>
                     <el-form-item label="状态:" prop="status">
                         <el-radio-group v-model="formData.status">
                             <el-radio :label="1">发布</el-radio>
@@ -57,6 +60,7 @@ export default {
             ],
             rules: {
                 title: [{ required: true, message: '请输入标题', trigger: 'blur' }],
+                author: [{ required: true, message: '请输入标题', trigger: 'blur' }],
                 type: [{ required: true, message: '请选择类型', trigger: 'blur' }],
                 content: [{ required: true, message: '内容不能为空', trigger: 'blur' }],
             },
@@ -78,6 +82,7 @@ export default {
         },
     },
     methods: {
+        
         resetForm() {
             this.$refs.formRef && this.$refs.formRef.resetFields()
             this.formData = Object.assign({}, this.defaultData)

+ 14 - 5
addons/admin/src/views/system/article/components/list.vue

@@ -34,12 +34,12 @@
               <el-button @click="reloadTable()">
                 重置
               </el-button>
-              <el-button v-if="hasPerm(permissionMap['add'])" type="primary" icon="el-icon-plus" size="small"
+              <el-button v-if="permission.includes(permissionMap['add'])" type="primary" icon="el-icon-plus" size="small"
                 @click="openCreate">
                 创建
               </el-button>
               <!-- 导入按钮,仅在 defaultType 为 9 时可见 -->
-              <el-button v-if="defaultType == 9 && hasPerm(permissionMap['add'])" type="success" icon="el-icon-upload2"
+              <el-button v-if="defaultType == 9 && permission.includes(permissionMap['add'])" type="success" icon="el-icon-upload2"
                 size="small" @click="openImportDialog">
                 导入
               </el-button>
@@ -53,9 +53,14 @@
           <el-table-column type="selection" width="45" align="center" fixed="left" />
           <el-table-column prop="id" label="ID" width="60" align="center" fixed="left" show-overflow-tooltip />
           <el-table-column prop="title" label="标题" show-overflow-tooltip min-width="200" />
-          <el-table-column prop="type_name" label="类型" show-overflow-tooltip min-width="100">
+          <el-table-column v-if="defaultType == 1" prop="author" label="作者" show-overflow-tooltip min-width="100">
             <template slot-scope="{row}">
-              <span class="ele-text-primary">{{ row.type_name }}</span>
+              <span>{{ row.author }}</span>
+            </template>
+          </el-table-column>
+          <el-table-column v-if="defaultType != 1" prop="type_name" label="类型" show-overflow-tooltip min-width="100">
+            <template slot-scope="{row}">
+              <span class="ele-text-primary">{{ getTypeName(row.type) }}</span>
             </template>
           </el-table-column>
           <el-table-column prop="status" label="发布" :resizable="false" min-width="120">
@@ -66,7 +71,7 @@
           <el-table-column label="操作" width="130px" align="center" :resizable="false" fixed="right">
             <template slot-scope="{row}">
               <el-link @click="edit(row)" icon="el-icon-edit" type="primary" :underline="false"
-                v-if="hasPerm(permissionMap['edit'])">修改</el-link>
+                v-if="permission.includes(permissionMap['edit'])">修改</el-link>
             </template>
           </el-table-column>
         </template>
@@ -171,6 +176,10 @@ export default {
     }
   },
   methods: {
+    getTypeName(typeId) {
+      const item = this.typeArr.find(t => t.id === typeId)
+      return item ? item.name : '未知类型'
+    },
     openCreate() {
       this.editArticle = { status: 1, type: this.defaultType ? Number(this.defaultType) : 1 }
       this.showArticleForm = true

+ 49 - 52
addons/admin/src/views/system/notice/index.vue

@@ -3,57 +3,58 @@
     <el-card shadow="never">
       <!-- 搜索表单 -->
       <el-form :model="table.where" label-width="77px" class="ele-form-search"
-               @keyup.enter.native="$refs.table.reload()" @submit.native.prevent>
+        @keyup.enter.native="$refs.table.reload()" @submit.native.prevent>
         <el-row :gutter="15">
           <el-col :md="6" :sm="12">
             <el-form-item label="通知标题:">
-              <el-input v-model="table.where.title" placeholder="请输入通知标题" clearable/>
+              <el-input v-model="table.where.title" placeholder="请输入通知标题" clearable />
             </el-form-item>
           </el-col>
           <el-col :md="6" :sm="12">
             <div class="ele-form-actions">
               <el-button type="primary" @click="$refs.table.reload()" icon="el-icon-search" class="ele-btn-icon">查询
               </el-button>
-              <el-button @click="(table.where={})&&$refs.table.reload()">重置</el-button>
+              <el-button @click="(table.where = {}) && $refs.table.reload()">重置</el-button>
             </div>
           </el-col>
         </el-row>
       </el-form>
       <!-- 操作按钮 -->
       <div class="ele-table-tool ele-table-tool-default">
-        <el-button @click="showEdit=true" type="primary" icon="el-icon-plus" class="ele-btn-icon" size="small" v-if="permission.includes('sys:notice:add')">添加
+        <el-button @click="showEdit = true" type="primary" icon="el-icon-plus" class="ele-btn-icon" size="small"
+          v-if="permission.includes('sys:notice:add')">添加
         </el-button>
-        <el-button @click="remove()" type="danger" icon="el-icon-delete" class="ele-btn-icon" size="small" v-if="permission.includes('sys:notice:dall')">批量删除
+        <el-button @click="remove()" type="danger" icon="el-icon-delete" class="ele-btn-icon" size="small"
+          v-if="permission.includes('sys:notice:dall')">批量删除
         </el-button>
       </div>
       <!-- 数据表格 -->
-      <ele-data-table ref="table" :config="table" :choose.sync="choose" height="calc(100vh - 315px)" highlight-current-row>
+      <ele-data-table ref="table" :config="table" :choose.sync="choose" height="calc(100vh - 315px)"
+        highlight-current-row>
         <template slot-scope="{index}">
-          <el-table-column type="selection" width="45" align="center" fixed="left"/>
-          <el-table-column type="index" :index="index" label="编号" width="60" align="center" fixed="left" show-overflow-tooltip/>
-          <el-table-column prop="title" label="通知标题" sortable="custom" show-overflow-tooltip min-width="250"/>
-          <el-table-column label="通知来源" min-width="100" align="center">
-            <template slot-scope="{row}">
-              <el-tag :type="['primary','success'][row.source]" size="mini">{{ ['内部通知', '外部新闻'][row.source] }}</el-tag>
-            </template>
-          </el-table-column>
+          <el-table-column type="selection" width="45" align="center" fixed="left" />
+          <el-table-column type="index" :index="index" label="编号" width="60" align="center" fixed="left"
+            show-overflow-tooltip />
+          <el-table-column prop="title" label="通知标题" sortable="custom" show-overflow-tooltip min-width="250" />
           <el-table-column prop="status" label="通知状态" sortable="custom" :resizable="false" min-width="120">
             <template slot-scope="{row}">
-              <el-switch v-model="row.status" @change="editStatus(row)" :active-value="1" :inactive-value="2"/>
+              <el-switch v-model="row.status" @change="editStatus(row)" :active-value="1" :inactive-value="2" />
             </template>
           </el-table-column>
-<!--          <el-table-column prop="browse" label="浏览量" sortable="custom" show-overflow-tooltip min-width="100"/>-->
+          <!--          <el-table-column prop="browse" label="浏览量" sortable="custom" show-overflow-tooltip min-width="100"/>-->
           <el-table-column label="创建时间" sortable="custom" show-overflow-tooltip min-width="160">
             <template slot-scope="{row}">{{ row.create_time | toDateString }}</template>
           </el-table-column>
           <el-table-column label="更新时间" sortable="custom" show-overflow-tooltip min-width="160">
             <template slot-scope="{row}">{{ row.update_time | toDateString }}</template>
           </el-table-column>
-          <el-table-column label="操作" width="130px" align="center" :resizable="false"  fixed="right">
+          <el-table-column label="操作" width="130px" align="center" :resizable="false" fixed="right">
             <template slot-scope="{row}">
-              <el-link @click="edit(row)" icon="el-icon-edit" type="primary" :underline="false" v-if="permission.includes('sys:notice:edit')">修改</el-link>
+              <el-link @click="edit(row)" icon="el-icon-edit" type="primary" :underline="false"
+                v-if="permission.includes('sys:notice:edit')">修改</el-link>
               <el-popconfirm title="确定要删除此通知吗?" @confirm="remove(row)" class="ele-action">
-                <el-link slot="reference" icon="el-icon-delete" type="danger" :underline="false" v-if="permission.includes('sys:notice:delete')">删除</el-link>
+                <el-link slot="reference" icon="el-icon-delete" type="danger" :underline="false"
+                  v-if="permission.includes('sys:notice:delete')">删除</el-link>
               </el-popconfirm>
             </template>
           </el-table-column>
@@ -61,13 +62,14 @@
       </ele-data-table>
     </el-card>
     <!-- 编辑弹窗 -->
-    <el-dialog :title="editForm.id?'修改通知':'修改通知'" :visible.sync="showEdit" width="600px"
-               @closed="editForm={source:1,status:1}" :destroy-on-close="true" custom-class="ele-dialog-form" :lock-scroll="false">
+    <el-dialog :title="editForm.id ? '修改通知' : '修改通知'" :visible.sync="showEdit" width="600px"
+      @closed="editForm = { source: 1, status: 1 }" :destroy-on-close="true" custom-class="ele-dialog-form"
+      :lock-scroll="false">
       <el-form :model="editForm" ref="editForm" :rules="editRules" label-width="82px">
         <el-row :gutter="15">
           <el-col :sm="12">
             <el-form-item label="通知标题:" prop="title">
-              <el-input v-model="editForm.title" placeholder="请输入通知标题" clearable/>
+              <el-input v-model="editForm.title" placeholder="请输入通知标题" clearable />
             </el-form-item>
             <el-form-item label="通知状态:">
               <el-radio-group v-model="editForm.status">
@@ -75,19 +77,13 @@
                 <el-radio :label="2">禁用</el-radio>
               </el-radio-group>
             </el-form-item>
-            <el-form-item label="通知来源:">
-              <el-radio-group v-model="editForm.source">
-                <el-radio :label="1">内部通知</el-radio>
-                <el-radio :label="2">外部新闻</el-radio>
-              </el-radio-group>
-            </el-form-item>
           </el-col>
         </el-row>
         <!-- 富文本编辑器 -->
-        <tinymce-editor v-model="editForm.content" :init="editContent"/>
+        <tinymce-editor v-model="editForm.content" :init="editContent" />
       </el-form>
       <div slot="footer">
-        <el-button @click="showEdit=false">取消</el-button>
+        <el-button @click="showEdit = false">取消</el-button>
         <el-button type="primary" @click="save">保存</el-button>
       </div>
     </el-dialog>
@@ -99,22 +95,22 @@ import TinymceEditor from '@/components/TinymceEditor'
 import { mapGetters } from "vuex";
 export default {
   name: "SysNotice",
-  components: {TinymceEditor},
+  components: { TinymceEditor },
   data() {
     return {
-      table: {url: '/notice/index', where: {}},  // 表格配置
+      table: { url: '/notice/index', where: {} },  // 表格配置
       choose: [],  // 表格选中数据
       showEdit: false,  // 是否显示表单弹窗
-      editForm: {source:1,status:1},  // 表单数据
+      editForm: { source: 1, status: 1 },  // 表单数据
       editRules: {  // 表单验证规则
         title: [
-          {required: true, message: '请输入通知标题', trigger: 'blur'}
+          { required: true, message: '请输入通知标题', trigger: 'blur' }
         ],
         status: [
-          {required: true, message: '请输入选择通知状态', trigger: 'blur'}
+          { required: true, message: '请输入选择通知状态', trigger: 'blur' }
         ],
         source: [
-          {required: true, message: '请输入选择通知来源', trigger: 'blur'}
+          { required: true, message: '请输入选择通知来源', trigger: 'blur' }
         ],
       },
       // 自定义文件上传(这里使用把选择的文件转成blob演示)
@@ -131,7 +127,7 @@ export default {
           let file = input.files[0];
           let reader = new FileReader();
           reader.onload = (e) => {
-            let blob = new Blob([e.target.result], {type: file.type});
+            let blob = new Blob([e.target.result], { type: file.type });
             callback(URL.createObjectURL(blob));
           };
           reader.readAsArrayBuffer(file);
@@ -142,7 +138,7 @@ export default {
   },
   computed: {
     ...mapGetters(["permission"]),
-    
+
     editContent() {
       return {
         menubar: false,
@@ -165,12 +161,12 @@ export default {
       this.$message.closeAll();
       this.$refs['editForm'].validate((valid) => {
         if (valid) {
-          const loading = this.$loading({lock: true});
+          const loading = this.$loading({ lock: true });
           this.$http.post('/notice/edit', this.editForm).then(res => {
             loading.close();
             if (res.data.code === 0) {
               this.showEdit = false;
-              this.$message({type: 'success', message: res.data.msg});
+              this.$message({ type: 'success', message: res.data.msg });
               this.$refs.table.reload();
             } else {
               this.$message.error(res.data.msg);
@@ -190,12 +186,12 @@ export default {
       if (!row) {  // 批量删除
         if (this.choose.length === 0) return this.$message.error('请至少选择一条数据');
         let ids = this.choose.map(d => d.id);
-        this.$confirm('确定要删除选中的通知吗?', '提示', {type: 'warning'}).then(() => {
-          const loading = this.$loading({lock: true});
-          this.$http.post('/notice/delete', {id: ids}).then(res => {
+        this.$confirm('确定要删除选中的通知吗?', '提示', { type: 'warning' }).then(() => {
+          const loading = this.$loading({ lock: true });
+          this.$http.post('/notice/delete', { id: ids }).then(res => {
             loading.close();
             if (res.data.code === 0) {
-              this.$message({type: 'success', message: res.data.msg});
+              this.$message({ type: 'success', message: res.data.msg });
               this.$refs.table.reload();
             } else {
               this.$message.error(res.data.msg);
@@ -206,11 +202,11 @@ export default {
           });
         }).catch(() => 0);
       } else {  // 单个删除
-        const loading = this.$loading({lock: true});
-        this.$http.post('/notice/delete', {id:row.id}).then(res => {
+        const loading = this.$loading({ lock: true });
+        this.$http.post('/notice/delete', { id: row.id }).then(res => {
           loading.close();
           if (res.data.code === 0) {
-            this.$message({type: 'success', message: res.data.msg});
+            this.$message({ type: 'success', message: res.data.msg });
             this.$refs.table.reload();
           } else {
             this.$message.error(res.data.msg);
@@ -224,12 +220,12 @@ export default {
     /* 更改状态 */
     editStatus(row) {
       this.$message.closeAll();
-      const loading = this.$loading({lock: true});
+      const loading = this.$loading({ lock: true });
       let params = Object.assign({}, row);
       this.$http.post('/notice/status', params).then(res => {
         loading.close();
         if (res.data.code === 0) {
-          this.$message({type: 'success', message: res.data.msg});
+          this.$message({ type: 'success', message: res.data.msg });
         } else {
           row.status = !row.status ? 2 : 1;
           this.$message.error(res.data.msg);
@@ -241,12 +237,12 @@ export default {
     },
     /* 是否置顶 */
     editIsTop(row) {
-      const loading = this.$loading({lock: true});
+      const loading = this.$loading({ lock: true });
       let params = Object.assign({}, row);
       this.$http.post('/notice/setIsTop', params).then(res => {
         loading.close();
         if (res.data.code === 0) {
-          this.$message({type: 'success', message: res.data.msg});
+          this.$message({ type: 'success', message: res.data.msg });
         } else {
           row.isTop = !row.isTop ? 2 : 1;
           this.$message.error(res.data.msg);
@@ -261,7 +257,8 @@ export default {
 </script>
 
 <style scoped>
-.ele-block >>> .el-upload, .ele-block >>> .el-upload-dragger {
+.ele-block>>>.el-upload,
+.ele-block>>>.el-upload-dragger {
   width: 100%;
 }
 </style>

+ 260 - 0
addons/admin/src/views/video/component/CoursesDialog.vue

@@ -0,0 +1,260 @@
+<template>
+    <el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="90%" top="5vh" :close-on-click-modal="false"
+        custom-class="courses-main-dialog" :close-on-press-escape="false" @closed="handleClosed">
+        <div class="courses-dialog-content">
+            <!-- 搜索表单 -->
+            <el-form :model="tableCourses.where" label-width="90px" class="ele-form-search"
+                @keyup.enter.native="$refs.tableCourses.reload()" @submit.native.prevent>
+                <el-row :gutter="15">
+                    <el-col :md="6" :sm="12">
+                        <el-form-item label="课程名称:">
+                            <el-input v-model="tableCourses.where.keyword" placeholder="请输入课程名称" clearable />
+                        </el-form-item>
+                    </el-col>
+                    <el-col :md="6" :sm="12">
+                        <el-form-item label="视频课名称:">
+                            <el-input v-model="tableCourses.where.video_name" placeholder="请输入视频课名称" clearable />
+                        </el-form-item>
+                    </el-col>
+                    <el-col :md="6" :sm="12">
+                        <el-form-item label="状态:">
+                            <el-select v-model="tableCourses.where.status" placeholder="请选择状态" clearable
+                                class="ele-fluid">
+                                <el-option label="发布" :value="1" />
+                                <el-option label="待发布" :value="2" />
+                            </el-select>
+                        </el-form-item>
+                    </el-col>
+                    <el-col :md="6" :sm="12">
+                        <div class="ele-form-actions">
+                            <el-button type="primary" @click="$refs.tableCourses.reload()" icon="el-icon-search"
+                                class="ele-btn-icon">查询</el-button>
+                            <el-button @click="resetSearchCourses">重置</el-button>
+                        </div>
+                    </el-col>
+                </el-row>
+            </el-form>
+
+            <!-- 操作按钮 -->
+            <div class="ele-table-tool ele-table-tool-default">
+                <el-button @click="removeCourses()" type="danger" icon="el-icon-delete" class="ele-btn-icon mr-10"
+                    v-if="permission.includes('sys:courses:delete')" size="small">批量删除</el-button>
+                <el-button @click="openFormCourses()" type="primary" icon="el-icon-plus" class="ele-btn-icon mr-10"
+                    v-if="permission.includes('sys:courses:add')" size="small">新增视频</el-button>
+            </div>
+
+            <!-- 数据表格 -->
+            <ele-data-table ref="tableCourses" :config="tableCourses" :choose.sync="chooseCourses" height="400px"
+                highlight-current-row>
+                <template slot-scope="{ index }">
+                    <el-table-column type="selection" width="45" align="center" fixed="left" />
+                    <el-table-column type="index" :index="index" label="编号" width="60" align="center" fixed="left" />
+                    <el-table-column prop="course_name" label="课程名称" width="240" />
+                    <el-table-column label="所属视频课名称" min-width="180">
+                        <template slot-scope="{ row }">
+                            <span>{{ row.video_name }}</span>
+                        </template>
+                    </el-table-column>
+                    <el-table-column prop="fee" label="费用" width="100" />
+                    <el-table-column prop="poster" label="封面" width="120">
+                        <template slot-scope="{ row }">
+                            <el-image :src="row.poster" style="width:60px;height:40px;" fit="cover" />
+                        </template>
+                    </el-table-column>
+                    <el-table-column label="状态" width="80">
+                        <template slot-scope="{ row }">
+                            <el-switch v-model="row.status" @change="editStatusCourses(row)" :active-value="1"
+                                :inactive-value="2" />
+                        </template>
+                    </el-table-column>
+                    <el-table-column label="操作" width="200" fixed="right">
+                        <template slot-scope="{ row }">
+                            <el-link @click="openFormCourses(row)" icon="el-icon-edit" type="primary" :underline="false"
+                                v-if="permission.includes('sys:courses:edit')" class="ele-action">修改</el-link>
+                            <el-popconfirm title="确定要删除吗?" @confirm="removeCourses(row)" class="ele-action">
+                                <el-link slot="reference" icon="el-icon-delete" type="danger" :underline="false"
+                                    v-if="permission.includes('sys:courses:delete')">删除</el-link>
+                            </el-popconfirm>
+                        </template>
+                    </el-table-column>
+                </template>
+            </ele-data-table>
+        </div>
+
+        <div slot="footer" class="dialog-footer">
+            <el-button @click="dialogVisible = false">关闭</el-button>
+        </div>
+
+        <!-- 编辑/新增表单 -->
+        <edit-form :visible.sync="formDialogVisibleCourses" :is-edit="isEditCourses" :course-id="formDataCourses.id"
+            :video-id="videoId" @saved="handleFormSaved" />
+    </el-dialog>
+</template>
+
+<script>
+import EditForm from "./CoursesForm.vue";
+import { mapGetters } from "vuex";
+
+export default {
+    name: "CoursesDialog",
+    components: { EditForm },
+    props: {
+        // 控制弹窗显示
+        visible: {
+            type: Boolean,
+            default: false
+        },
+        // 视频集ID,用于筛选该视频集下的课程
+        videoId: {
+            type: [Number, String],
+            default: null
+        },
+        // 视频集名称,用于显示
+        videoName: {
+            type: String,
+            default: ''
+        }
+    },
+    computed: {
+        ...mapGetters(["permission"]),
+        dialogTitle() {
+            return this.videoName ? `视频课程列表 - ${this.videoName}` : '视频课程列表';
+        }
+    },
+    data() {
+        return {
+            tableCourses: {
+                url: "/videosCourses/index",
+                where: { video_id: null, video_name: null, status: null, keyword: null }
+            },
+            chooseCourses: [],
+            formDialogVisibleCourses: false,
+            isEditCourses: false,
+            formDataCourses: { id: null },
+            dialogVisible: false
+        };
+    },
+    watch: {
+        visible: {
+            immediate: true,
+            handler(newVal) {
+                this.dialogVisible = newVal;
+                if (newVal) {
+                    this.$nextTick(() => {
+                        this.initData();
+                    });
+                }
+            }
+        },
+        dialogVisible(newVal) {
+            this.$emit('update:visible', newVal);
+        },
+        videoId: {
+            immediate: true,
+            handler(newVal) {
+                if (newVal) {
+                    this.tableCourses.where.video_id = newVal;
+                }
+            }
+        }
+    },
+    methods: {
+        initData() {
+            // 初始化搜索条件
+            if (this.videoId) {
+                this.tableCourses.where.video_id = this.videoId;
+            }
+            // 重新加载表格数据
+            this.$nextTick(() => {
+                if (this.$refs.tableCourses) {
+                    this.$refs.tableCourses.reload();
+                }
+            });
+        },
+        openFormCourses(row) {
+            this.isEditCourses = !!row;
+            this.formDataCourses = row ? {
+                id: row.id,
+                course_name: row.course_name,
+                video_id: row.video_id,
+                fee: row.fee,
+                poster: row.poster,
+                status: row.status
+            } : { video_id: this.videoId };
+            this.formDialogVisibleCourses = true;
+        },
+        resetSearchCourses() {
+            this.tableCourses.where = { video_id: this.videoId };
+            this.$refs.tableCourses.reload();
+        },
+        editStatusCourses(row) {
+            this.$http.post("/videosCourses/status", { id: row.id, status: row.status });
+        },
+        removeCourses(row) {
+            let ids = row ? [row.id] : this.chooseCourses.map(d => d.id);
+            if (!ids.length) return this.$message.warning("请选择数据");
+            this.$confirm("确定要删除选中课程吗?", "提示", { type: "warning" }).then(() => {
+                this.$http.post("/videosCourses/delete", { id: ids })
+                    .then(res => {
+                        if (res.data.code === 0) {
+                            this.$message.success("删除成功");
+                            this.$refs.tableCourses.reload();
+                        } else {
+                            this.$message.error(res.data.msg);
+                        }
+                    });
+            });
+        },
+        handleFormSaved() {
+            this.$refs.tableCourses.reload();
+        },
+        handleClosed() {
+            // 弹窗关闭时重置数据
+            this.chooseCourses = [];
+            this.formDialogVisibleCourses = false;
+            this.isEditCourses = false;
+            this.formDataCourses = { id: null };
+            this.$emit('closed');
+        }
+    }
+};
+</script>
+
+<style scoped>
+.courses-dialog-content {
+    padding: 10px 0;
+}
+
+.ele-action {
+    margin-right: 12px;
+}
+
+.dialog-footer {
+    text-align: right;
+}
+
+/* 确保弹窗层级正确 */
+.courses-main-dialog {
+    z-index: 2001 !important;
+}
+
+/* 内层弹窗的样式 */
+.courses-main-dialog .el-dialog__wrapper {
+    z-index: 2002 !important;
+}
+
+/* 确保蒙版层级正确 */
+.courses-main-dialog .v-modal {
+    z-index: 2000 !important;
+}
+
+/* 内层弹窗的蒙版 */
+.courses-main-dialog~.el-dialog__wrapper .v-modal {
+    z-index: 2001 !important;
+}
+
+/* 内层弹窗 */
+.courses-main-dialog~.el-dialog__wrapper .el-dialog {
+    z-index: 2002 !important;
+}
+</style>

+ 24 - 11
addons/admin/src/views/video/component/CoursesForm.vue

@@ -1,9 +1,10 @@
 <template>
-    <el-dialog :title="isEdit ? '编辑课程' : '新增课程'" :visible.sync="visibleInternal" width="700px">
+    <el-dialog :title="isEdit ? '编辑课程视频' : '新增课程视频'" append-to-body :visible.sync="visibleInternal" width="700px"
+        custom-class="courses-form-dialog">
         <div v-if="loaded">
             <el-form :model="formData" :rules="formRules" ref="formRef" label-width="120px">
-                <!-- 课程名称 -->
-                <el-form-item label="课程名称" prop="course_name">
+                <!-- 视频名称 -->
+                <el-form-item label="视频名称" prop="course_name">
                     <el-input v-model="formData.course_name" />
                 </el-form-item>
 
@@ -15,8 +16,8 @@
                     </el-select>
                 </el-form-item>
 
-                <!-- 课程链接 -->
-                <el-form-item label="课程链接" prop="course_url">
+                <!-- 课程视频链接 -->
+                <el-form-item label="课程视频链接" prop="course_url">
                     <el-input v-model="formData.course_url" />
                 </el-form-item>
 
@@ -61,7 +62,8 @@ export default {
     props: {
         visible: { type: Boolean, default: false },
         isEdit: { type: Boolean, default: false },
-        videoId: { type: [Number, null], default: null }
+        videoId: { type: [Number, null], default: null },
+        courseId: { type: [Number, null], default: null }
     },
     data() {
         return {
@@ -80,9 +82,9 @@ export default {
             saving: false,
             videoOptions: [],  // 存储视频课选项
             formRules: {
-                course_name: [{ required: true, message: "请输入课程名称", trigger: "blur" }],
+                course_name: [{ required: true, message: "请输入视频名称", trigger: "blur" }],
                 video_id: [{ required: true, message: "请选择所属视频课", trigger: "change" }],
-                course_url: [{ required: true, message: "请输入课程链接", trigger: "blur" }]
+                course_url: [{ required: true, message: "请输入课程视频链接", trigger: "blur" }]
             }
         };
     },
@@ -94,13 +96,13 @@ export default {
             this.$emit("update:visible", val);
             if (val) {
                 this.loaded = false;
-                if (this.isEdit && this.videoId) {
+                if (this.isEdit && this.courseId) {
                     this.loadDetail();
                 } else {
                     this.formData = {
                         id: null,
                         course_name: "",
-                        video_id: null,
+                        video_id: this.videoId,
                         course_url: "",
                         fee: 0,
                         poster: "",
@@ -124,7 +126,7 @@ export default {
             }
         },
         async loadDetail() {
-            const res = await this.$http.get(`/videosCourses/info?id=${this.videoId}`);
+            const res = await this.$http.get(`/videosCourses/info?id=${this.courseId}`);
             if (res.data.code === 0) {
                 this.formData = res.data.data;
             }
@@ -151,3 +153,14 @@ export default {
     }
 };
 </script>
+<style>
+/* 内层弹窗 */
+.courses-form-dialog {
+    z-index: 3002 !important;
+}
+
+/* 内层弹窗的遮罩层 */
+.courses-form-dialog~.v-modal {
+    z-index: 3001 !important;
+}
+</style>

+ 47 - 47
addons/admin/src/views/video/courses.vue

@@ -2,22 +2,23 @@
     <div class="ele-body">
         <el-card shadow="never">
             <!-- 搜索表单 -->
-            <el-form :model="table.where" label-width="90px" class="ele-form-search"
-                @keyup.enter.native="$refs.table.reload()" @submit.native.prevent>
+            <el-form :model="tableCourses.where" label-width="90px" class="ele-form-search"
+                @keyup.enter.native="$refs.tableCourses.reload()" @submit.native.prevent>
                 <el-row :gutter="15">
                     <el-col :md="6" :sm="12">
                         <el-form-item label="课程名称:">
-                            <el-input v-model="table.where.keyword" placeholder="请输入课程名称" clearable />
+                            <el-input v-model="tableCourses.where.keyword" placeholder="请输入课程名称" clearable />
                         </el-form-item>
                     </el-col>
                     <el-col :md="6" :sm="12">
                         <el-form-item label="视频课名称:">
-                            <el-input v-model="table.where.video_name" placeholder="请输入视频课名称" clearable />
+                            <el-input v-model="tableCourses.where.video_name" placeholder="请输入视频课名称" clearable />
                         </el-form-item>
                     </el-col>
                     <el-col :md="6" :sm="12">
                         <el-form-item label="状态:">
-                            <el-select v-model="table.where.status" placeholder="请选择状态" clearable class="ele-fluid">
+                            <el-select v-model="tableCourses.where.status" placeholder="请选择状态" clearable
+                                class="ele-fluid">
                                 <el-option label="发布" :value="1" />
                                 <el-option label="待发布" :value="2" />
                             </el-select>
@@ -25,63 +26,62 @@
                     </el-col>
                     <el-col :md="6" :sm="12">
                         <div class="ele-form-actions">
-                            <el-button type="primary" @click="$refs.table.reload()" icon="el-icon-search"
+                            <el-button type="primary" @click="$refs.tableCourses.reload()" icon="el-icon-search"
                                 class="ele-btn-icon">查询</el-button>
-                            <el-button @click="resetSearch">重置</el-button>
+                            <el-button @click="resetSearchCourses">重置</el-button>
                         </div>
                     </el-col>
                 </el-row>
             </el-form>
 
             <!-- 操作按钮 -->
-            <div class="ele-table-tool ele-table-tool-default">
-                <el-button @click="openForm()" type="primary" icon="el-icon-plus" class="ele-btn-icon mr-10"
-                    v-if="permission.includes('sys:courses:add')" size="small">新增课程</el-button>
-                <el-button @click="remove()" type="danger" icon="el-icon-delete" class="ele-btn-icon mr-10"
+            <div class="ele-tableCourses-tool ele-tableCourses-tool-default">
+                <el-button @click="removeCourses()" type="danger" icon="el-icon-delete" class="ele-btn-icon mr-10"
                     v-if="permission.includes('sys:courses:delete')" size="small">批量删除</el-button>
             </div>
 
             <!-- 数据表格 -->
-            <ele-data-table ref="table" :config="table" :choose.sync="choose" height="calc(100vh - 315px)"
-                highlight-current-row>
+            <ele-data-tableCourses ref="tableCourses" :config="tableCourses" :chooseCourses.sync="chooseCourses"
+                height="calc(100vh - 315px)" highlight-current-row>
                 <template slot-scope="{ index }">
-                    <el-table-column type="selection" width="45" align="center" fixed="left" />
-                    <el-table-column type="index" :index="index" label="编号" width="60" align="center" fixed="left" />
-                    <el-table-column prop="course_name" label="课程名称" width="240" />
-                    <el-table-column label="所属视频课名称" width="180">
+                    <el-tableCourses-column type="selection" width="45" align="center" fixed="left" />
+                    <el-tableCourses-column type="index" :index="index" label="编号" width="60" align="center"
+                        fixed="left" />
+                    <el-tableCourses-column prop="course_name" label="课程名称" width="240" />
+                    <el-tableCourses-column label="所属视频课名称" width="180">
                         <template slot-scope="{ row }">
                             <span>{{ row.video_name }}</span> <!-- 显示视频课名称 -->
                         </template>
-                    </el-table-column>
-                    <el-table-column prop="fee" label="费用" width="100" />
-                    <el-table-column prop="poster" label="封面" width="120">
+                    </el-tableCourses-column>
+                    <el-tableCourses-column prop="fee" label="费用" width="100" />
+                    <el-tableCourses-column prop="poster" label="封面" width="120">
                         <template slot-scope="{ row }">
                             <el-image :src="row.poster" style="width:60px;height:40px;" fit="cover" />
                         </template>
-                    </el-table-column>
-                    <el-table-column label="状态" width="80">
+                    </el-tableCourses-column>
+                    <el-tableCourses-column label="状态" width="80">
                         <template slot-scope="{ row }">
-                            <el-switch v-model="row.status" @change="editStatus(row)" :active-value="1"
+                            <el-switch v-model="row.status" @change="editStatusCourses(row)" :active-value="1"
                                 :inactive-value="2" />
                         </template>
-                    </el-table-column>
-                    <el-table-column label="操作" width="200" fixed="right">
+                    </el-tableCourses-column>
+                    <el-tableCourses-column label="操作" width="200" fixed="right">
                         <template slot-scope="{ row }">
-                            <el-link @click="openForm(row)" icon="el-icon-edit" type="primary" :underline="false"
+                            <el-link @click="openFormCourses(row)" icon="el-icon-edit" type="primary" :underline="false"
                                 v-if="permission.includes('sys:courses:edit')">修改</el-link>
-                            <el-popconfirm title="确定要删除吗?" @confirm="remove(row)" class="ele-action">
+                            <el-popconfirm title="确定要删除吗?" @confirm="removeCourses(row)" class="ele-action">
                                 <el-link slot="reference" icon="el-icon-delete" type="danger" :underline="false"
                                     v-if="permission.includes('sys:courses:delete')">删除</el-link>
                             </el-popconfirm>
                         </template>
-                    </el-table-column>
+                    </el-tableCourses-column>
                 </template>
-            </ele-data-table>
+            </ele-data-tableCourses>
         </el-card>
 
         <!-- 编辑/新增表单 -->
-        <edit-form :visible.sync="formDialogVisible" :is-edit="isEdit" :course-id="formData.id"
-            @saved="$refs.table.reload()" />
+        <edit-form :visible.sync="formDialogVisibleCourses" :is-edit="isEditCourses" :course-id="formDataCourses.id"
+            @saved="$refs.tableCourses.reload()" />
     </div>
 </template>
 
@@ -97,40 +97,40 @@ export default {
     },
     data() {
         return {
-            table: {
+            tableCourses: {
                 url: "/videosCourses/index",  // 更新后的接口路径
                 where: { video_id: null, video_name: null, status: null, keyword: null }
             },
-            choose: [],
-            formDialogVisible: false,
-            isEdit: false,
-            formData: { id: null },
+            chooseCourses: [],
+            formDialogVisibleCourses: false,
+            isEditCourses: false,
+            formDataCourses: { id: null },
         };
     },
     mounted() {
     },
     methods: {
-        openForm(row) {
-            this.isEdit = !!row;
-            this.formData = row ? { id: row.id, course_name: row.course_name, video_id: row.video_id, fee: row.fee, poster: row.poster, status: row.status } : {};
-            this.formDialogVisible = true;
+        openFormCourses(row) {
+            this.isEditCourses = !!row;
+            this.formDataCourses = row ? { id: row.id, course_name: row.course_name, video_id: row.video_id, fee: row.fee, poster: row.poster, status: row.status } : {};
+            this.formDialogVisibleCourses = true;
         },
-        resetSearch() {
-            this.table.where = {};
-            this.$refs.table.reload();
+        resetSearchCourses() {
+            this.tableCourses.where = {};
+            this.$refs.tableCourses.reload();
         },
-        editStatus(row) {
+        editStatusCourses(row) {
             this.$http.post("/videosCourses/status", { id: row.id, status: row.status });  // 更新后的接口路径
         },
-        remove(row) {
-            let ids = row ? [row.id] : this.choose.map(d => d.id);
+        removeCourses(row) {
+            let ids = row ? [row.id] : this.chooseCourses.map(d => d.id);
             if (!ids.length) return this.$message.warning("请选择数据");
             this.$confirm("确定要删除选中课程吗?", "提示", { type: "warning" }).then(() => {
                 this.$http.post("/videosCourses/delete", { id: ids })  // 更新后的接口路径
                     .then(res => {
                         if (res.data.code === 0) {
                             this.$message.success("删除成功");
-                            this.$refs.table.reload();
+                            this.$refs.tableCourses.reload();
                         } else this.$message.error(res.data.msg);
                     });
             });

+ 66 - 2
addons/admin/src/views/video/video.vue

@@ -43,6 +43,10 @@
                     v-if="permission.includes('sys:video:add')" size="small">新增课程</el-button>
                 <el-button @click="remove()" type="danger" icon="el-icon-delete" class="ele-btn-icon mr-10"
                     v-if="permission.includes('sys:video:dall')" size="small">批量删除</el-button>
+
+                <!-- 操作按钮 -->
+                <el-button @click="openFormCourses()" type="primary" icon="el-icon-plus" class="ele-btn-icon mr-10"
+                    v-if="permission.includes('sys:courses:add')" size="small">新增视频</el-button>
             </div>
 
             <!-- 数据表格 -->
@@ -73,6 +77,8 @@
                                 <el-link slot="reference" icon="el-icon-delete" type="danger" :underline="false"
                                     v-if="permission.includes('sys:video:delete')">删除</el-link>
                             </el-popconfirm>
+                            <el-link @click="openCoursesDialog(row)" icon="el-icon-list" type="primary"
+                                :underline="false" class="ele-action">视频列表</el-link>
                         </template>
                     </el-table-column>
                 </template>
@@ -87,16 +93,27 @@
         <!-- 编辑/新增表单 -->
         <video-form :visible.sync="formDialogVisible" :is-edit="isEdit" :video-id="formData.id"
             :category-options="categoryOptions" @saved="$refs.table.reload()" />
+
+
+        <!-- 编辑/新增表单 -->
+        <edit-form :visible.sync="formDialogVisibleCourses" :is-edit="isEditCourses" :course-id="formDataCourses.id"
+            @saved="$refs.tableCourses.reload()" />
+
+        <!-- 视频课程列表弹窗 -->
+        <courses-dialog :visible.sync="coursesDialogVisible" :video-id="currentVideoId" :video-name="currentVideoName"
+            @closed="handleCoursesDialogClosed" />
     </div>
 </template>
 
 <script>
 import VideoForm from "./component/VideoForm.vue";
+import EditForm from "./component/CoursesForm.vue";
 import { mapGetters } from "vuex";
+import CoursesDialog from "./component/CoursesDialog.vue";
 
 export default {
     name: "VideoList",
-    components: { VideoForm },
+    components: { VideoForm, EditForm, CoursesDialog },
     computed: {
         ...mapGetters(["permission"]),
     },
@@ -114,7 +131,18 @@ export default {
             formDialogVisible: false,
             isEdit: false,
             formData: { id: null },
-            categoryDialogVisible: false
+            categoryDialogVisible: false,
+
+            chooseCourses: [],
+            formDialogVisibleCourses: false,
+            isEditCourses: false,
+            formDataCourses: { id: null },
+
+            // 视频课程弹窗相关
+            coursesDialogVisible: false,
+            currentVideoId: null,
+            currentVideoName: ''
+
         };
     },
     mounted() {
@@ -168,6 +196,42 @@ export default {
                     }
                 });
             });
+        },
+        openFormCourses(row) {
+            this.isEditCourses = !!row;
+            this.formDataCourses = row ? { id: row.id, course_name: row.course_name, video_id: row.video_id, fee: row.fee, poster: row.poster, status: row.status } : {};
+            this.formDialogVisibleCourses = true;
+        },
+        resetSearchCourses() {
+            this.tableCourses.where = {};
+            this.$refs.tableCourses.reload();
+        },
+        editStatusCourses(row) {
+            this.$http.post("/videosCourses/status", { id: row.id, status: row.status });  // 更新后的接口路径
+        },
+        removeCourses(row) {
+            let ids = row ? [row.id] : this.chooseCourses.map(d => d.id);
+            if (!ids.length) return this.$message.warning("请选择数据");
+            this.$confirm("确定要删除选中课程吗?", "提示", { type: "warning" }).then(() => {
+                this.$http.post("/videosCourses/delete", { id: ids })  // 更新后的接口路径
+                    .then(res => {
+                        if (res.data.code === 0) {
+                            this.$message.success("删除成功");
+                            this.$refs.tableCourses.reload();
+                        } else this.$message.error(res.data.msg);
+                    });
+            });
+        },
+        // 打开视频课程弹窗
+        openCoursesDialog(row) {
+            this.currentVideoId = row.id;
+            this.currentVideoName = row.video_name;
+            this.coursesDialogVisible = true;
+        },
+        // 视频课程弹窗关闭回调
+        handleCoursesDialogClosed() {
+            this.currentVideoId = null;
+            this.currentVideoName = '';
         }
     }
 };

+ 7 - 6
addons/admin/src/views/vip/vip.vue

@@ -37,13 +37,13 @@
 
             <!-- 操作按钮 -->
             <div class="ele-table-tool ele-table-tool-default">
-                <el-button type="primary" v-if="hasPerm(permissionMap['add'])" icon="el-icon-plus" @click="openForm()">
+                <!-- <el-button type="primary" v-if="permission.includes(permissionMap['add'])" icon="el-icon-plus" @click="openForm()">
                     新增VIP类型
                 </el-button>
-                <el-button type="danger" v-if="hasPerm(permissionMap['delete'])" icon="el-icon-delete"
+                <el-button type="danger" v-if="permission.includes(permissionMap['delete'])" icon="el-icon-delete"
                     @click="batchRemove()">
                     批量删除
-                </el-button>
+                </el-button> -->
                 <!-- <el-button type="primary" icon="el-icon-delete" @click="openBatchVipDialog()">
                     开通VIP
                 </el-button> -->
@@ -62,7 +62,7 @@
                         </template>
                     </el-table-column>
                     <el-table-column prop="price" label="VIP价格" width="120" />
-                    <el-table-column prop="day" label="VIP时长(天数)" width="120" />
+                    <el-table-column prop="day" label="VIP时长/天" width="120" />
                     <el-table-column label="状态" width="80">
                         <template slot-scope="{ row }">
                             <el-tag :type="row.status === 1 ? 'success' : 'info'">
@@ -72,9 +72,9 @@
                     </el-table-column>
                     <el-table-column label="操作" width="160" fixed="right">
                         <template slot-scope="{ row }">
-                            <el-button v-if="hasPerm(permissionMap['edit'])" type="text" size="mini"
+                            <el-button v-if="permission.includes(permissionMap['edit'])" type="text" size="mini"
                                 @click="openForm(row)">编辑</el-button>
-                            <el-button v-if="hasPerm(permissionMap['delete'])" type="text" size="mini"
+                            <el-button v-if="permission.includes(permissionMap['delete'])" type="text" size="mini"
                                 @click="remove(row)">删除</el-button>
                         </template>
                     </el-table-column>
@@ -177,6 +177,7 @@ export default {
             }
         };
     },
+    computed: { ...mapGetters(["permission"]) },
     methods: {
         resetSearch() {
             this.table.where = { keyword: "", type: null, status: null };

+ 1 - 1
app/Http/Controllers/Admin/AccountController.php

@@ -68,7 +68,7 @@ class AccountController extends Backend
      */
     public function stats(Request $request)
     {
-        $params = $request->only(['dateType', 'type', 'status', 'start_time', 'end_time', 'page', 'limit']);
+        $params = $request->only(['dateType', 'type', 'status', 'start_time', 'end_time', 'page', 'limit', 'vip_type']);
         $data = $this->service->getRevenueStats($params);
 
         return response()->json([

+ 12 - 0
app/Http/Controllers/Admin/IndexController.php

@@ -15,6 +15,7 @@ use App\Services\Common\AccountService;
 use App\Services\Common\AnswerRanksService;
 use App\Services\Common\BalanceLogService;
 use App\Services\Common\DepositService;
+use App\Services\Common\ExamAccessLogsService;
 use App\Services\Common\GoodsService;
 use App\Services\Common\MemberService;
 use App\Services\Common\MenuService;
@@ -164,4 +165,15 @@ class IndexController extends Backend
             'data' => $data
         ]);
     }
+    public function examAccessStats()
+    {
+        $params = Request()->only(['dateType', 'start_time', 'end_time', 'page', 'limit']);
+
+        $service = ExamAccessLogsService::make();
+        $data = $service->examAccessStats($params);
+        return response()->json([
+            'code' => 0,
+            'data' => $data
+        ]);
+    }
 }

+ 15 - 12
app/Models/MemberModel.php

@@ -23,15 +23,18 @@ class MemberModel extends BaseModel
     // 设置数据表
     protected $table = 'member';
 
+    protected $guarded = ['id']; // 除了 id 之外,其余字段都允许批量更新
+
+
     /**
      * 仓库
      * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
      */
     public function stock()
     {
-        return $this->belongsTo(StockModel::class, 'stock_id','stock_id')
-            ->where(['mark'=>1])
-            ->select(['stock_id','stock_name','status']);
+        return $this->belongsTo(StockModel::class, 'stock_id', 'stock_id')
+            ->where(['mark' => 1])
+            ->select(['stock_id', 'stock_name', 'status']);
     }
 
     /**
@@ -40,9 +43,9 @@ class MemberModel extends BaseModel
      */
     public function line()
     {
-        return $this->belongsTo(LineModel::class, 'line_id','line_id')
-            ->where(['mark'=>1])
-            ->select(['line_id','line_name','status']);
+        return $this->belongsTo(LineModel::class, 'line_id', 'line_id')
+            ->where(['mark' => 1])
+            ->select(['line_id', 'line_name', 'status']);
     }
 
     /**
@@ -50,8 +53,8 @@ class MemberModel extends BaseModel
      */
     public function parent()
     {
-        return $this->hasOne(MemberModel::class, 'id','parent_id')
-            ->where(['status'=>1,'mark'=>1])
+        return $this->hasOne(MemberModel::class, 'id', 'parent_id')
+            ->where(['status' => 1, 'mark' => 1])
             ->select(['id', 'nickname', 'username', 'mobile', 'status']);
     }
 
@@ -61,8 +64,8 @@ class MemberModel extends BaseModel
      */
     public function order1()
     {
-        return $this->hasMany(OrderModel::class, 'user_id','id')
-            ->where(['mark'=>1])->where('status','>=', 1);
+        return $this->hasMany(OrderModel::class, 'user_id', 'id')
+            ->where(['mark' => 1])->where('status', '>=', 1);
     }
 
     /**
@@ -71,8 +74,8 @@ class MemberModel extends BaseModel
      */
     public function order2()
     {
-        return $this->hasMany(OrderModel::class, 'user_id','id')
-            ->where(['mark'=>1])->where('status', 2);
+        return $this->hasMany(OrderModel::class, 'user_id', 'id')
+            ->where(['mark' => 1])->where('status', 2);
     }
 
 

+ 27 - 41
app/Services/Common/AccountService.php

@@ -240,84 +240,69 @@ class AccountService extends BaseService
         ')->first();
     }
 
-    /**
-     * 营业额统计(分页)
-     *
-     * @param array $params
-     *   - dateType: year|month|day
-     *   - type: 交易类型 1-开通VIP,2-视频课付费
-     *   - status: 状态 1-已完成,2-待处理,3-失败/取消
-     *   - start_time, end_time: 时间范围
-     *   - page, limit: 分页
-     * @return array
-     */
     public function getRevenueStats(array $params)
     {
         $dateType = $params['dateType'] ?? 'day';
-        $type = $params['type'] ?? null;
+        $vipType = $params['vip_type'] ?? null; // zg_vip | zsb_vip | video_vip
         $status = $params['status'] ?? null;
+        $type = $params['type'] ?? null;
         $page = max(1, (int) ($params['page'] ?? 1));
         $limit = max(1, (int) ($params['limit'] ?? 10));
 
-        // 时间转换
-        $startTimestamp = !empty($params['start_time'])
-            ? strtotime($params['start_time'])
-            : strtotime(date('2000-01-01 00:00:00'));
-        $endTimestamp = !empty($params['end_time'])
-            ? strtotime($params['end_time'])
-            : time();
+        $startTimestamp = !empty($params['start_time']) ? strtotime($params['start_time']) : strtotime('2000-01-01');
+        $endTimestamp = !empty($params['end_time']) ? strtotime($params['end_time']) : time();
 
         if ($endTimestamp < $startTimestamp) {
             $endTimestamp = $startTimestamp;
         }
 
-        // 分组日期格式
+        // 日期分组
         switch ($dateType) {
             case 'year':
-                $format = '%Y';
-                $groupRaw = "FROM_UNIXTIME(create_time, '{$format}')";
+                $groupRaw = "FROM_UNIXTIME(lev_l.create_time,'%Y')";
                 break;
             case 'month':
-                $format = '%Y-%m';
-                $groupRaw = "FROM_UNIXTIME(create_time, '{$format}')";
+                $groupRaw = "FROM_UNIXTIME(lev_l.create_time,'%Y-%m')";
                 break;
             case 'week':
-                // 按周统计,显示格式 YYYY-WW
-                $groupRaw = "CONCAT(YEAR(FROM_UNIXTIME(create_time)), '-', LPAD(WEEK(FROM_UNIXTIME(create_time), 1), 2, '0'))";
+                $groupRaw = "CONCAT(YEAR(FROM_UNIXTIME(lev_l.create_time)),'-',LPAD(WEEK(FROM_UNIXTIME(lev_l.create_time),1),2,'0'))";
                 break;
             case 'day':
             default:
-                $format = '%Y-%m-%d';
-                $groupRaw = "FROM_UNIXTIME(create_time, '{$format}')";
+                $groupRaw = "FROM_UNIXTIME(lev_l.create_time,'%Y-%m-%d')";
                 break;
         }
-
-        // 基础查询
-        $query = DB::table('account_logs')
+        $query = DB::table('account_logs as l')
+            ->join('member as m', 'l.user_id', '=', 'm.id')
             ->selectRaw("
             {$groupRaw} as stat_date,
             COUNT(*) as total_count,
-            SUM(money) as total_money
+            SUM(lev_l.money) as total_money
         ")
-            ->whereBetween('create_time', [$startTimestamp, $endTimestamp]);
+            ->whereBetween('l.create_time', [$startTimestamp, $endTimestamp]);
 
-        // 条件过滤
-        if ($type !== null) {
-            $query->where('type', $type);
-        }
         if ($status !== null) {
-            $query->where('status', $status);
+            $query->where('l.status', $status);
         }
 
-        $query->groupBy('stat_date')
-            ->orderBy('stat_date', 'desc');
+        // VIP类型筛选
+        if ($vipType == 'zg_vip') {
+            $query->where('m.is_zg_vip', 1);
+        } elseif ($vipType == 'zsb_vip') {
+            $query->where('m.is_zsb_vip', 1);
+        } elseif ($vipType == 'video_vip') {
+            $query->where('m.is_video_vip', 1);
+        }
+        if ($type) {
+            $query->where('l.type', $type);
+        }
 
+        $query->groupBy('stat_date')->orderBy('stat_date', 'desc');
         // 总条数
         $total = DB::table(DB::raw("({$query->toSql()}) as t"))
             ->mergeBindings($query)
             ->count();
 
-        // 分页
         $list = $query->forPage($page, $limit)->get();
 
         return [
@@ -327,4 +312,5 @@ class AccountService extends BaseService
             'limit' => $limit,
         ];
     }
+
 }

+ 0 - 2
app/Services/Common/ArticleService.php

@@ -82,12 +82,10 @@ class ArticleService extends BaseService
             ->paginate($pageSize > 0 ? $pageSize : 9999999);
         $list = $list ? $list->toArray() : [];
         if ($list) {
-            $typrArr = ['', '充值协议', '注册协议', '隐私政策', '', '', '', '', '', '智能问答'];
             foreach ($list['data'] as &$item) {
                 $item['create_time'] = $item['create_time'] ? datetime($item['create_time'], 'Y-m-d H.i.s') : '';
                 $item['cover'] = $item['cover'] ? get_image_url($item['cover']) : '';
                 $item['content'] = $item['content'] ? get_format_content($item['content']) : '';
-                $item['type_name'] = isset($typrArr[$item['type']]) ? $typrArr[$item['type']] : '其他';
             }
         }
 

+ 116 - 0
app/Services/Common/ExamAccessLogsService.php

@@ -0,0 +1,116 @@
+<?php
+// +----------------------------------------------------------------------
+// | LARAVEL8.0 框架 [ LARAVEL ][ RXThinkCMF ]
+// +----------------------------------------------------------------------
+// | 版权所有 2017~2021 LARAVEL研发中心
+// +----------------------------------------------------------------------
+// | 官方网站: http://www.laravel.cn
+// +----------------------------------------------------------------------
+// | Author: laravel开发员 <laravel.qq.com>
+// +----------------------------------------------------------------------
+
+namespace App\Services\Common;
+
+use App\Models\UserModel;
+use App\Services\BaseService;
+use App\Services\ConfigService;
+use Illuminate\Support\Facades\DB;
+
+/**
+ * 服务类
+ * @author laravel开发员
+ * @since 2020/11/11
+ * Class UserService
+ * @package App\Services\Common
+ */
+class ExamAccessLogsService extends BaseService
+{
+    /**
+     * 构造函数
+     * @author laravel开发员
+     * @since 2020/11/11
+     * UserService constructor.
+     */
+    public function __construct()
+    {
+    }
+
+    /**
+     * 静态入口
+     */
+    public static function make()
+    {
+        if (!self::$instance) {
+            self::$instance = new static();
+        }
+        return self::$instance;
+    }
+
+
+    public function examAccessStats(array $params)
+    {
+        $dateType = $params['dateType'] ?? 'day';
+        $page = max(1, (int) ($params['page'] ?? 1));
+        $limit = max(1, (int) ($params['limit'] ?? 10));
+
+        // 时间范围
+        $startDate = !empty($params['start_time']) ? $params['start_time'] : '2000-01-01';
+        $endDate = !empty($params['end_time']) ? $params['end_time'] : date('Y-m-d');
+        if ($endDate < $startDate) {
+            $endDate = $startDate;
+        }
+
+        // 分组字段
+        switch ($dateType) {
+            case 'month':
+                $groupRaw = "DATE_FORMAT(date, '%Y-%m')";
+                break;
+            case 'year':
+                $groupRaw = "DATE_FORMAT(date, '%Y')";
+                break;
+            case 'week':
+                $groupRaw = "CONCAT(YEAR(date), '-', LPAD(WEEK(date, 1), 2, '0'))";
+                break;
+            case 'day':
+            default:
+                $groupRaw = "DATE_FORMAT(date, '%Y-%m-%d')";
+                break;
+        }
+
+        // 基础查询
+        $query = DB::table('exam_access_logs')
+            ->selectRaw("
+                {$groupRaw} as stat_date,
+                type,
+                SUM(scene_count1) as scene_count1,
+                SUM(scene_count2) as scene_count2,
+                SUM(scene_count3) as scene_count3,
+                SUM(scene_count4) as scene_count4,
+                SUM(scene_count5) as scene_count5,
+                SUM(scene_count6) as scene_count6,
+                SUM(scene_count7) as scene_count7,
+                SUM(scene_count20) as scene_count20
+            ")
+            ->whereBetween('date', [$startDate, $endDate])
+            ->where('status', 1)
+            ->where('mark', 1)
+            ->groupBy('stat_date', 'type')
+            ->orderBy('stat_date', 'desc');
+
+        // 总条数
+        $total = DB::table(DB::raw("({$query->toSql()}) as t"))
+            ->mergeBindings($query)
+            ->count();
+
+        // 分页
+        $list = $query->forPage($page, $limit)->get();
+
+        return [
+            'list' => $list,
+            'total' => $total,
+            'page' => $page,
+            'limit' => $limit,
+        ];
+    }
+
+}

+ 87 - 29
app/Services/Common/MemberService.php

@@ -255,6 +255,25 @@ class MemberService extends BaseService
     {
         $data = request()->all();
 
+        // 允许更新的字段(对应前端 dialog)
+        $allowedFields = [
+            'id',
+            'avatar',
+            'mobile',
+            'nickname',
+            'is_zg_vip',
+            'zg_vip_expired',
+            'is_zsb_vip',
+            'zsb_vip_expired',
+            'is_video_vip',
+            'video_vip_expired',
+            'entry_type',
+            'need_paper'
+        ];
+
+        // 只保留允许字段
+        $data = array_intersect_key($data, array_flip($allowedFields));
+
         // 头像与证件图片处理
         foreach (['avatar', 'driving_license', 'drivers_license'] as $field) {
             if (!empty($data[$field])) {
@@ -278,21 +297,25 @@ class MemberService extends BaseService
 
         // VIP字段处理(可选)
         $vipFields = ['is_zg_vip', 'is_zsb_vip', 'is_video_vip'];
+        $vipMapping = [
+            'is_zg_vip' => 'zg_vip_expired',
+            'is_zsb_vip' => 'zsb_vip_expired',
+            'is_video_vip' => 'video_vip_expired'
+        ];
+
         foreach ($vipFields as $vipField) {
             if (isset($data[$vipField])) {
                 $data[$vipField] = intval($data[$vipField]);
-                if ($data[$vipField] === 1) {
-                    // 开通VIP,设置到期时间(默认一年)
-                    $expireField = $vipField . '_expired';
-                    $data[$expireField] = date('Y-m-d H:i:s', time() + 365 * 24 * 3600);
-                } elseif ($data[$vipField] === 2) {
-                    // 取消VIP,清除到期时间
-                    $expireField = $vipField . '_expired';
-                    $data[$expireField] = null;
-                }
+                $expireField = $vipMapping[$vipField];
+
+                // 获取当前数据库状态
+                $oldStatus = $this->model->where('id', $id)->value($vipField);
+                $oldExpire = $this->model->where('id', $id)->value($expireField);
+
+                // 调用公共方法计算新过期时间
+                $data[$expireField] = $this->calcVipExpire($oldStatus, $data[$vipField], $oldExpire);
             }
         }
-
         // 日志记录
         ActionLogModel::setRecord(
             session('userId'),
@@ -308,8 +331,16 @@ class MemberService extends BaseService
         // 清理缓存
         RedisService::keyDel("caches:members:count*");
 
-        // 调用父类保存
-        return parent::edit($data);
+        try {
+            if ($id) {
+                DB::table('member')->where('id', $id)->update($data);
+            } else {
+                DB::table('member')->insert($data);
+            }
+            return message("操作成功", true, );
+        } catch (\Exception $e) {
+            return message("操作失败:" . $e->getMessage(), false);
+        }
     }
 
     /**
@@ -375,10 +406,6 @@ class MemberService extends BaseService
         }
 
         $ids = $params['ids'];
-        $vipFields = ['is_zg_vip', 'is_zsb_vip', 'is_video_vip'];
-        $updateData = ['update_time' => time()];
-
-        // 定义VIP字段对应的过期字段
         $vipMapping = [
             'is_zg_vip' => 'zg_vip_expired',
             'is_zsb_vip' => 'zsb_vip_expired',
@@ -393,28 +420,36 @@ class MemberService extends BaseService
                 $status = intval($params[$vipField]);
                 $updateData[$vipField] = $status;
 
-                if ($status === 1) {
-                    // 开通VIP,设置过期时间为一年后
-                    $updateData[$expireField] = date('Y-m-d H:i:s', time() + 365 * 24 * 3600);
-                } else {
-                    // 取消VIP,过期时间设为NULL
-                    $updateData[$expireField] = null;
+                // 查出这些会员当前的VIP状态和过期时间
+                $members = DB::table('member')
+                    ->whereIn('id', $ids)
+                    ->select('id', $vipField, $expireField)
+                    ->get()
+                    ->keyBy('id');
+
+                foreach ($members as $memberId => $member) {
+                    $newExpire = $this->calcVipExpire(intval($member->$vipField), $status, $member->$expireField);
+
+                    DB::table('member')
+                        ->where('id', $memberId)
+                        ->update([
+                            $vipField => $status,
+                            $expireField => $newExpire,
+                            'update_time' => time(),
+                        ]);
                 }
+
                 $hasVipField = true;
             }
         }
+
         if (!$hasVipField) {
             return message("请指定要操作的VIP类型", false);
         }
-
-        try {
-            DB::table('member')->whereIn('id', $ids)->update($updateData);
-            return message("批量设置VIP成功", true, $updateData);
-        } catch (\Exception $e) {
-            return message("操作失败:" . $e->getMessage(), false);
-        }
+        return message("批量设置VIP成功", true);
     }
 
+
     public function getMemberStats(array $params)
     {
         $dateType = $params['dateType'] ?? 'day';
@@ -481,4 +516,27 @@ class MemberService extends BaseService
             'limit' => $limit,
         ];
     }
+
+    /**
+     * 根据旧状态和新状态计算VIP过期时间
+     *
+     * @param int|null $oldStatus 当前VIP状态 (0/1/2)
+     * @param int $newStatus 新状态 (0/1/2)
+     * @param string|null $oldExpire 当前过期时间
+     * @return string|null 返回新的过期时间
+     */
+    public function calcVipExpire(?int $oldStatus, int $newStatus, ?string $oldExpire): ?string
+    {
+        if ($newStatus === 1) {
+            // 从非VIP → VIP,才设置一年过期时间
+            if ($oldStatus !== 1) {
+                return date('Y-m-d H:i:s', time() + 365 * 24 * 3600);
+            }
+            // 已是VIP,保持原过期时间
+            return $oldExpire;
+        } else {
+            // 0 或 2 → 清空过期时间
+            return null;
+        }
+    }
 }

+ 6 - 1
app/Services/Exam/TopicService.php

@@ -46,11 +46,16 @@ class TopicService extends BaseService
         $list = $query->orderBy('sort', 'desc')->paginate($pageSize > 0 ? $pageSize : 9999999);
         $list = $list ? $list->toArray() : [];
         // 要处理的字段
-        $fields = ['poster', 'topic_analysis', 'topic_name', 'correct_answer'];
+        $fields = ['poster', 'topic_analysis', 'topic_name'];
 
         $items = isset($list['data']) ? $list['data'] : [];
 
         foreach ($items as &$item) {
+            // 1-图片 2-文字
+            if ($item['answer_type'] == 1 && in_array($item['topic_type'], ['填空题', '简单题'])) {
+                $fields[] = 'correct_answer';
+            }
+
             if (!empty($item['show_type']) && (int) $item['show_type'] === 2) {
                 // 图片型 → 转为完整图片路径(支持多图)
                 foreach ($fields as $field) {

+ 2 - 0
routes/web.php

@@ -68,6 +68,8 @@ Route::post('/index/updateUserInfo', [IndexController::class, 'updateUserInfo'])
 Route::post('/index/updatePwd', [IndexController::class, 'updatePwd']);
 Route::post('/index/statistics', [IndexController::class, 'statistics']);
 Route::post('/index/answerRanksStat', [IndexController::class, 'answerRanksStat']);
+Route::post('index/examAccessStats', [IndexController::class, 'examAccessStats']);
+
 
 // 用户管理
 Route::get('/user/index', [UserController::class, 'index']);