Bläddra i källkod

增加导入word文档

罗永浩 5 månader sedan
förälder
incheckning
0ef62a680a

+ 103 - 27
addons/admin/package-lock.json

@@ -2101,6 +2101,11 @@
         "@xtuc/long": "4.2.2"
       }
     },
+    "@xmldom/xmldom": {
+      "version": "0.8.11",
+      "resolved": "https://registry.npmmirror.com/@xmldom/xmldom/-/xmldom-0.8.11.tgz",
+      "integrity": "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw=="
+    },
     "@xtuc/ieee754": {
       "version": "1.2.0",
       "resolved": "https://registry.npmmirror.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
@@ -2293,7 +2298,6 @@
       "version": "1.0.10",
       "resolved": "https://registry.npmmirror.com/argparse/-/argparse-1.0.10.tgz",
       "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
-      "dev": true,
       "requires": {
         "sprintf-js": "~1.0.2"
       }
@@ -2706,8 +2710,7 @@
     "base64-js": {
       "version": "1.5.1",
       "resolved": "https://registry.npmmirror.com/base64-js/-/base64-js-1.5.1.tgz",
-      "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
-      "dev": true
+      "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="
     },
     "batch": {
       "version": "0.6.1",
@@ -4119,8 +4122,7 @@
     "core-util-is": {
       "version": "1.0.2",
       "resolved": "https://registry.npmmirror.com/core-util-is/-/core-util-is-1.0.2.tgz",
-      "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==",
-      "dev": true
+      "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ=="
     },
     "cosmiconfig": {
       "version": "5.2.1",
@@ -4949,6 +4951,11 @@
         }
       }
     },
+    "dingbat-to-unicode": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/dingbat-to-unicode/-/dingbat-to-unicode-1.0.1.tgz",
+      "integrity": "sha512-98l0sW87ZT58pU4i61wa2OHwxbiYSbuxsCBozaVnYX2iCnr3bLM3fIes1/ej7h1YdOKuKt/MLs706TVnALA65w=="
+    },
     "dir-glob": {
       "version": "2.2.2",
       "resolved": "https://registry.npmmirror.com/dir-glob/-/dir-glob-2.2.2.tgz",
@@ -5099,6 +5106,14 @@
         "unidragger": "^2.4.0"
       }
     },
+    "duck": {
+      "version": "0.1.12",
+      "resolved": "https://registry.npmmirror.com/duck/-/duck-0.1.12.tgz",
+      "integrity": "sha512-wkctla1O6VfP89gQ+J/yDesM0S7B7XLXjKGzXxMDVFg7uEn706niAtyYovKbyq1oT9YwDcly721/iUWoc8MVRg==",
+      "requires": {
+        "underscore": "^1.13.1"
+      }
+    },
     "dunder-proto": {
       "version": "1.0.1",
       "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -7199,6 +7214,11 @@
       "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==",
       "dev": true
     },
+    "immediate": {
+      "version": "3.0.6",
+      "resolved": "https://registry.npmmirror.com/immediate/-/immediate-3.0.6.tgz",
+      "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="
+    },
     "import-cwd": {
       "version": "2.1.0",
       "resolved": "https://registry.npmmirror.com/import-cwd/-/import-cwd-2.1.0.tgz",
@@ -7325,8 +7345,7 @@
     "inherits": {
       "version": "2.0.4",
       "resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz",
-      "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
-      "dev": true
+      "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
     },
     "inquirer": {
       "version": "7.3.3",
@@ -7932,8 +7951,7 @@
     "isarray": {
       "version": "1.0.0",
       "resolved": "https://registry.npmmirror.com/isarray/-/isarray-1.0.0.tgz",
-      "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
-      "dev": true
+      "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="
     },
     "isexe": {
       "version": "2.0.0",
@@ -8096,6 +8114,17 @@
         "verror": "1.10.0"
       }
     },
+    "jszip": {
+      "version": "3.10.1",
+      "resolved": "https://registry.npmmirror.com/jszip/-/jszip-3.10.1.tgz",
+      "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
+      "requires": {
+        "lie": "~3.3.0",
+        "pako": "~1.0.2",
+        "readable-stream": "~2.3.6",
+        "setimmediate": "^1.0.5"
+      }
+    },
     "killable": {
       "version": "1.0.1",
       "resolved": "https://registry.npmmirror.com/killable/-/killable-1.0.1.tgz",
@@ -8137,6 +8166,14 @@
         "type-check": "~0.3.2"
       }
     },
+    "lie": {
+      "version": "3.3.0",
+      "resolved": "https://registry.npmmirror.com/lie/-/lie-3.3.0.tgz",
+      "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
+      "requires": {
+        "immediate": "~3.0.5"
+      }
+    },
     "lines-and-columns": {
       "version": "1.2.4",
       "resolved": "https://registry.npmmirror.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
@@ -8312,6 +8349,16 @@
       "integrity": "sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==",
       "dev": true
     },
+    "lop": {
+      "version": "0.4.2",
+      "resolved": "https://registry.npmmirror.com/lop/-/lop-0.4.2.tgz",
+      "integrity": "sha512-RefILVDQ4DKoRZsJ4Pj22TxE3omDO47yFpkIBoDKzkqPRISs5U1cnAdg/5583YPkWPaLIYHOKRMQSvjFsO26cw==",
+      "requires": {
+        "duck": "^0.1.12",
+        "option": "~0.2.1",
+        "underscore": "^1.13.1"
+      }
+    },
     "loud-rejection": {
       "version": "1.6.0",
       "resolved": "https://registry.npmmirror.com/loud-rejection/-/loud-rejection-1.6.0.tgz",
@@ -8355,6 +8402,30 @@
         "semver": "^6.0.0"
       }
     },
+    "mammoth": {
+      "version": "1.11.0",
+      "resolved": "https://registry.npmmirror.com/mammoth/-/mammoth-1.11.0.tgz",
+      "integrity": "sha512-BcEqqY/BOwIcI1iR5tqyVlqc3KIaMRa4egSoK83YAVrBf6+yqdAAbtUcFDCWX8Zef8/fgNZ6rl4VUv+vVX8ddQ==",
+      "requires": {
+        "@xmldom/xmldom": "^0.8.6",
+        "argparse": "~1.0.3",
+        "base64-js": "^1.5.1",
+        "bluebird": "~3.4.0",
+        "dingbat-to-unicode": "^1.0.1",
+        "jszip": "^3.7.1",
+        "lop": "^0.4.2",
+        "path-is-absolute": "^1.0.0",
+        "underscore": "^1.13.1",
+        "xmlbuilder": "^10.0.0"
+      },
+      "dependencies": {
+        "bluebird": {
+          "version": "3.4.7",
+          "resolved": "https://registry.npmmirror.com/bluebird/-/bluebird-3.4.7.tgz",
+          "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA=="
+        }
+      }
+    },
     "map-cache": {
       "version": "0.2.2",
       "resolved": "https://registry.npmmirror.com/map-cache/-/map-cache-0.2.2.tgz",
@@ -9331,6 +9402,11 @@
         "is-wsl": "^1.1.0"
       }
     },
+    "option": {
+      "version": "0.2.4",
+      "resolved": "https://registry.npmmirror.com/option/-/option-0.2.4.tgz",
+      "integrity": "sha512-pkEqbDyl8ou5cpq+VsnQbe/WlEy5qS7xPzMS1U55OCG9KPvwFD46zDbxQIj3egJSFc3D+XhYOPUzz49zQAVy7A=="
+    },
     "optionator": {
       "version": "0.8.3",
       "resolved": "https://registry.npmmirror.com/optionator/-/optionator-0.8.3.tgz",
@@ -9460,8 +9536,7 @@
     "pako": {
       "version": "1.0.11",
       "resolved": "https://registry.npmmirror.com/pako/-/pako-1.0.11.tgz",
-      "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
-      "dev": true
+      "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="
     },
     "parallel-transform": {
       "version": "1.2.0",
@@ -9582,8 +9657,7 @@
     "path-is-absolute": {
       "version": "1.0.1",
       "resolved": "https://registry.npmmirror.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
-      "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
-      "dev": true
+      "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="
     },
     "path-is-inside": {
       "version": "1.0.2",
@@ -10931,8 +11005,7 @@
     "process-nextick-args": {
       "version": "2.0.1",
       "resolved": "https://registry.npmmirror.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
-      "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
-      "dev": true
+      "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
     },
     "progress": {
       "version": "2.0.3",
@@ -11184,7 +11257,6 @@
       "version": "2.3.8",
       "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-2.3.8.tgz",
       "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
-      "dev": true,
       "requires": {
         "core-util-is": "~1.0.0",
         "inherits": "~2.0.3",
@@ -11198,8 +11270,7 @@
         "safe-buffer": {
           "version": "5.1.2",
           "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.1.2.tgz",
-          "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
-          "dev": true
+          "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
         }
       }
     },
@@ -12149,8 +12220,7 @@
     "setimmediate": {
       "version": "1.0.5",
       "resolved": "https://registry.npmmirror.com/setimmediate/-/setimmediate-1.0.5.tgz",
-      "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
-      "dev": true
+      "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="
     },
     "setprototypeof": {
       "version": "1.2.0",
@@ -12622,8 +12692,7 @@
     "sprintf-js": {
       "version": "1.0.3",
       "resolved": "https://registry.npmmirror.com/sprintf-js/-/sprintf-js-1.0.3.tgz",
-      "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
-      "dev": true
+      "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="
     },
     "ssf": {
       "version": "0.11.2",
@@ -12815,7 +12884,6 @@
       "version": "1.1.1",
       "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.1.1.tgz",
       "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
-      "dev": true,
       "requires": {
         "safe-buffer": "~5.1.0"
       },
@@ -12823,8 +12891,7 @@
         "safe-buffer": {
           "version": "5.1.2",
           "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.1.2.tgz",
-          "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
-          "dev": true
+          "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
         }
       }
     },
@@ -13542,6 +13609,11 @@
         "which-boxed-primitive": "^1.1.1"
       }
     },
+    "underscore": {
+      "version": "1.13.7",
+      "resolved": "https://registry.npmmirror.com/underscore/-/underscore-1.13.7.tgz",
+      "integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g=="
+    },
     "undici-types": {
       "version": "7.10.0",
       "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-7.10.0.tgz",
@@ -13824,8 +13896,7 @@
     "util-deprecate": {
       "version": "1.0.2",
       "resolved": "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz",
-      "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
-      "dev": true
+      "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
     },
     "util.promisify": {
       "version": "1.0.1",
@@ -15012,6 +15083,11 @@
         }
       }
     },
+    "xmlbuilder": {
+      "version": "10.1.1",
+      "resolved": "https://registry.npmmirror.com/xmlbuilder/-/xmlbuilder-10.1.1.tgz",
+      "integrity": "sha512-OyzrcFLL/nb6fMGHbiRDuPup9ljBycsdCypwuyg5AAHvyWzGfChJpCXMG88AGTIMFhGZ9RccFN1e6lhg3hkwKg=="
+    },
     "xtend": {
       "version": "4.0.2",
       "resolved": "https://registry.npmmirror.com/xtend/-/xtend-4.0.2.tgz",

+ 1 - 0
addons/admin/package.json

@@ -19,6 +19,7 @@
     "echarts": "^4.9.0",
     "echarts-wordcloud": "^1.1.3",
     "element-ui": "^2.13.2",
+    "mammoth": "1.11.0",
     "mathjax": "^3.2.2",
     "nprogress": "^0.2.0",
     "tinymce": "^5.4.0",

+ 252 - 41
addons/admin/src/views/exam/articleList.vue

@@ -6,18 +6,16 @@
                 @keyup.enter.native="$refs.table.reload()" @submit.native.prevent>
                 <el-row :gutter="15">
                     <el-col :md="6" :sm="12">
-                        <el-form-item label="试卷类型:">
-                            <el-select v-model="table.where.paper_type" placeholder="请选择试卷类型" clearable
-                                class="ele-fluid">
-                                <el-option label="单招题" :value="1" />
-                                <el-option label="对口题" :value="2" />
-                                <el-option label="专升本题" :value="3" />
+                        <el-form-item label="分类:">
+                            <el-select v-model="table.where.cate_id" placeholder="请选择分类" clearable class="ele-fluid">
+                                <el-option v-for="category in categoryOptions" :key="category.id" :label="category.name"
+                                    :value="category.id" />
                             </el-select>
                         </el-form-item>
                     </el-col>
                     <el-col :md="6" :sm="12">
-                        <el-form-item label="文章标题:">
-                            <el-input v-model="table.where.keyword" placeholder="请输入文章标题" clearable />
+                        <el-form-item label="标题:">
+                            <el-input v-model="table.where.keyword" placeholder="请输入标题" clearable />
                         </el-form-item>
                     </el-col>
                     <el-col :md="6" :sm="12">
@@ -36,6 +34,8 @@
                     size="small" v-if="permission.includes(permissionMap['add'])">新增复习资料</el-button>
                 <el-button @click="openImport()" type="success" icon="el-icon-upload2" class="ele-btn-icon mr-10"
                     size="small" v-if="permission.includes(permissionMap['add'])">导入Excel</el-button>
+                <el-button v-if="permission.includes(permissionMap['edit'])" @click="openCategoryManager()"
+                    type="warning" icon="el-icon-menu" class="ele-btn-icon mr-10" size="small">分类管理</el-button>
                 <el-button @click="remove()" type="danger" icon="el-icon-delete" class="ele-btn-icon mr-10" size="small"
                     v-if="permission.includes(permissionMap['delete'])">批量删除</el-button>
             </div>
@@ -46,23 +46,24 @@
                 <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 label="文章标题" width="300">
+                    <el-table-column label="标题" width="300">
                         <template slot-scope="{ row }">
                             <text-ellipsis :text="row.title" :max-length="20" />
                         </template>
                     </el-table-column>
-                    <el-table-column label="试卷类型" width="120">
+                    <el-table-column label="分类" width="120">
                         <template slot-scope="{ row }">
-                            <el-tag :type="getPaperTypeTagType(row.paper_type)">
-                                {{ getPaperTypeText(row.paper_type) }}
+                            <el-tag type="primary">
+                                {{ getCategoryName(row.cate_id) }}
                             </el-tag>
                         </template>
                     </el-table-column>
                     <el-table-column label="内容" min-width="200">
                         <template slot-scope="{ row }">
-                            <el-button type="text" @click="showContentDialog(row)" size="mini">
-                                查看内容
-                            </el-button>
+                            <div class="content-preview" @click="openForm(row)">
+                                <div v-html="getContentPreview(row.content)" class="content-text"></div>
+                                <el-button type="text" size="mini" class="view-more-btn">查看全部</el-button>
+                            </div>
                         </template>
                     </el-table-column>
                     <el-table-column label="操作" width="160" fixed="right">
@@ -80,18 +81,25 @@
         </el-card>
 
         <article-form :visible.sync="formDialogVisible" :is-edit="isEdit" :article-id="formData.id" :default-type="21"
-            :default-paper-type="paperType" @saved="$refs.table.reload()" />
+            :article_cate_type="article_cate_type" @saved="$refs.table.reload()" />
 
         <!-- Excel导入弹窗 -->
-        <article-excel-import :visible.sync="importDialogVisible" @imported="$refs.table.reload()" />
+        <article-excel-import :visible.sync="importDialogVisible" :article_cate_type="article_cate_type"
+            @imported="$refs.table.reload()" />
 
-        <!-- 内容查看弹窗 -->
-        <el-dialog title="内容预览" :visible.sync="contentDialogVisible" width="800px" append-to-body>
-            <div v-html="currentContent" class="content-preview"></div>
+        <!-- 内容预览弹窗 -->
+        <el-dialog title="内容预览" :visible.sync="contentDialogVisible" width="900px" append-to-body>
+            <div class="content-preview-container">
+                <tinymce-editor v-model="currentContent" :init="previewEditorConfig" />
+            </div>
             <div slot="footer">
                 <el-button @click="contentDialogVisible = false">关闭</el-button>
             </div>
         </el-dialog>
+
+        <!-- 分类管理弹窗 -->
+        <CategoryManager :visible.sync="categoryManagerVisible" :article_cate_type="article_cate_type"
+            @updated="onCategoryUpdated" />
     </div>
 </template>
 
@@ -99,15 +107,17 @@
 import { mapGetters } from "vuex";
 import ArticleForm from "./component/ArticleForm.vue"
 import ArticleExcelImport from "./component/ArticleExcelImport.vue"
+import CategoryManager from "./component/CategoryManager.vue"
+import TinymceEditor from '@/components/TinymceEditor/index.vue';
 
 export default {
     props: {
         type: { type: Number, default: 21 }, // 复习资料类型
-        paperType: { type: Number, default: 1 }, // 试卷类型:1-单招题,2-对口题,3-专升本题
+        article_cate_type: { type: Number, default: 1 }, // 分类类型
         permissionMap: { type: Object, default: null },
     },
     name: "ArticleList",
-    components: { ArticleForm, ArticleExcelImport },
+    components: { ArticleForm, ArticleExcelImport, CategoryManager, TinymceEditor },
     data() {
         return {
             table: {
@@ -115,15 +125,103 @@ export default {
                 where: {
                     keyword: '',
                     type: 21, // 写死为复习资料类型
-                    paper_type: this.paperType
+                    cate_id: null,
+                    article_cate_type: this.article_cate_type // 添加分类类型筛选
                 }
             },
+            categoryOptions: [],
             choose: [],
             formDialogVisible: false,
             isEdit: false,
             importDialogVisible: false,
             contentDialogVisible: false,
             currentContent: '',
+            categoryManagerVisible: false,
+            // 预览编辑器配置
+            previewEditorConfig: {
+                height: 400,
+                branding: false,
+                language: 'zh_CN',
+                plugins: 'code print preview fullscreen paste searchreplace save autosave link autolink image imagetools media table codesample lists advlist hr charmap emoticons anchor directionality pagebreak quickbars nonbreaking visualblocks visualchars wordcount',
+                toolbar: 'fullscreen preview code | undo redo | forecolor backcolor | bold italic underline strikethrough | alignleft aligncenter alignright alignjustify | outdent indent | numlist bullist | formatselect fontselect fontsizeselect | link image media emoticons charmap anchor pagebreak codesample | ltr rtl',
+                toolbar_drawer: 'sliding',
+
+                // 图片处理配置
+                convert_urls: false,                    // 不转换URL
+                relative_urls: false,                    // 不使用相对URL
+                remove_script_host: false,               // 不移除脚本主机
+                document_base_url: window.location.origin + '/', // 设置基础URL
+
+                // HTML内容处理
+                valid_elements: '*[*]',                  // 允许所有元素
+                valid_children: '+body[style]',         // 允许body样式
+                extended_valid_elements: 'img[src|alt|width|height|style|class|data-*],div[class|style|data-*],p[style|data-*],h1[style|data-*],h2[style|data-*],h3[style|data-*],hr[style|data-*]',
+
+                // 样式和格式保持
+                keep_styles: true,                       // 保持样式
+                allow_script_urls: true,                 // 允许脚本URL
+                allow_html_data_urls: true,              // 允许data URL
+                preserve_cdata: true,                   // 保持CDATA
+                allow_conditional_comments: true,        // 允许条件注释
+
+                // HTML清理控制
+                cleanup: false,                          // 不清理HTML
+                cleanup_on_startup: false,               // 启动时不清理
+                verify_html: false,                       // 不验证HTML
+
+                // 其他配置
+                auto_focus: false,                       // 不自动聚焦
+                convert_newlines_to_brs: true,          // 转换换行为br
+
+                // 图片处理回调
+                setup: (editor) => {
+                    // 编辑器初始化完成后的回调
+                    editor.on('init', () => {
+                        console.log('预览编辑器初始化完成');
+                        // 处理图片URL,包括base64图片
+                        const images = editor.dom.select('img');
+                        images.forEach(img => {
+                            if (img.src) {
+                                // 如果是相对路径,转换为完整URL
+                                if (img.src.startsWith('/uploads/')) {
+                                    img.src = window.location.origin + img.src;
+                                }
+                                // 如果是base64图片,确保格式正确
+                                else if (img.src.startsWith('data:image/')) {
+                                    // base64图片直接使用,不需要转换
+                                    console.log('发现base64图片:', img.src.substring(0, 50) + '...');
+                                }
+                                // 如果是其他相对路径
+                                else if (!img.src.startsWith('http') && !img.src.startsWith('data:')) {
+                                    img.src = window.location.origin + '/' + img.src;
+                                }
+                            }
+                        });
+                    });
+
+                    // 监听内容变化,处理新添加的图片
+                    editor.on('NodeChange', (e) => {
+                        const images = editor.dom.select('img');
+                        images.forEach(img => {
+                            if (img.src) {
+                                // 如果是相对路径,转换为完整URL
+                                if (img.src.startsWith('/uploads/')) {
+                                    img.src = window.location.origin + img.src;
+                                }
+                                // 如果是base64图片,确保格式正确
+                                else if (img.src.startsWith('data:image/')) {
+                                    // base64图片直接使用,不需要转换
+                                    console.log('发现base64图片:', img.src.substring(0, 50) + '...');
+                                }
+                                // 如果是其他相对路径
+                                else if (!img.src.startsWith('http') && !img.src.startsWith('data:')) {
+                                    img.src = window.location.origin + '/' + img.src;
+                                }
+                            }
+                        });
+                    });
+                }
+            },
             formData: {
                 id: null,
                 title: "",
@@ -134,53 +232,98 @@ export default {
         };
     },
     computed: { ...mapGetters(["permission"]) },
+    watch: {
+        categoryManagerVisible(val) {
+            console.log('articleList categoryManagerVisible changed:', val);
+        }
+    },
     methods: {
         openForm(row) {
+            console.log('openForm called, categoryManagerVisible:', this.categoryManagerVisible);
+            // 确保分类管理弹窗关闭
+            this.categoryManagerVisible = false;
             this.isEdit = !!row;
             this.formData = row ? {
                 id: row.id,
                 title: row.title,
                 type: 21,
-                paper_type: row.paper_type,
+                cate_id: row.cate_id,
                 content: row.content
             } : {
                 id: null,
                 title: "",
                 type: 21,
-                paper_type: this.paperType,
+                cate_id: null,
                 content: ""
             };
             this.formDialogVisible = true;
         },
         openImport() {
+            // 确保分类管理弹窗关闭
+            this.categoryManagerVisible = false;
             this.importDialogVisible = true;
         },
         resetSearch() {
             this.table.where = {
                 keyword: '',
                 type: 21,
-                paper_type: this.paperType
+                cate_id: null,
+                article_cate_type: this.article_cate_type
             };
             this.$refs.table.reload();
         },
-        getPaperTypeText(paperType) {
-            const types = {
-                1: '单招题',
-                2: '对口题',
-                3: '专升本题'
-            };
-            return types[paperType] || '未知';
+        getCategoryName(cateId) {
+            if (!cateId) return '未分类';
+            const category = this.categoryOptions.find(cat => cat.id === cateId);
+            return category ? category.name : '未知分类';
         },
-        getPaperTypeTagType(paperType) {
-            const types = {
-                1: 'primary',
-                2: 'success',
-                3: 'warning'
-            };
-            return types[paperType] || '';
+        getContentPreview(content) {
+            if (!content) return '暂无内容';
+            // 移除HTML标签,获取纯文本
+            const textContent = content.replace(/<[^>]*>/g, '');
+            // 截取前100个字符
+            const preview = textContent.length > 100 ? textContent.substring(0, 100) + '...' : textContent;
+            return preview;
+        },
+        // 处理内容中的图片URL
+        processImageUrls(content) {
+            if (!content) return content;
+
+            // 获取当前域名
+            const baseUrl = window.location.origin;
+
+            // 处理相对路径的图片URL
+            content = content.replace(/src="\/uploads\//g, `src="${baseUrl}/uploads/`);
+            content = content.replace(/src='\/uploads\//g, `src='${baseUrl}/uploads/`);
+
+            // 处理可能存在的其他相对路径(但不处理base64图片)
+            content = content.replace(/src="(?!http|data:|\/\/)([^"]+)"/g, (match, p1) => {
+                // 如果是base64图片,不进行转换
+                if (p1.startsWith('data:image/')) {
+                    return match;
+                }
+                // 其他相对路径进行转换
+                return `src="${baseUrl}/${p1}"`;
+            });
+            content = content.replace(/src='(?!http|data:|\/\/)([^']+)'/g, (match, p1) => {
+                // 如果是base64图片,不进行转换
+                if (p1.startsWith('data:image/')) {
+                    return match;
+                }
+                // 其他相对路径进行转换
+                return `src='${baseUrl}/${p1}'`;
+            });
+
+            console.log('处理后的内容:', content);
+            return content;
         },
         showContentDialog(row) {
-            this.currentContent = row.content || '暂无内容';
+            // 处理内容中的图片URL
+            let content = row.content || '暂无内容';
+            if (content && content !== '暂无内容') {
+                content = this.processImageUrls(content);
+            }
+            this.currentContent = content;
             this.contentDialogVisible = true;
         },
         editStatus(row) {
@@ -201,8 +344,42 @@ export default {
                     } else this.$message.error(res.data.msg);
                 });
             });
+        },
+
+        // 打开分类管理弹窗
+        openCategoryManager() {
+            console.log('openCategoryManager called, current categoryManagerVisible:', this.categoryManagerVisible);
+            // 确保其他弹窗关闭
+            this.formDialogVisible = false;
+            this.importDialogVisible = false;
+            // 强制重置状态
+            this.categoryManagerVisible = false;
+            this.$nextTick(() => {
+                this.categoryManagerVisible = true;
+                console.log('categoryManagerVisible set to true');
+            });
+        },
+        // 分类更新后的回调
+        onCategoryUpdated() {
+        },
+        async loadCategoryOptions() {
+            try {
+                const res = await this.$http.get('/articleCategory/options', {
+                    params: {
+                        type: this.article_cate_type
+                    }
+                });
+                if (res.data.code === 0) {
+                    this.categoryOptions = res.data.data || [];
+                }
+            } catch (error) {
+                console.error('加载分类选项失败:', error);
+            }
         }
     },
+    mounted() {
+        this.loadCategoryOptions();
+    },
 };
 </script>
 
@@ -215,4 +392,38 @@ export default {
     border-radius: 4px;
     background-color: #fafafa;
 }
+
+/* 表格中的内容预览样式 */
+.el-table .content-preview {
+    max-height: 60px;
+    padding: 8px;
+    cursor: pointer;
+    border: 1px solid #e4e7ed;
+    border-radius: 4px;
+    background-color: #f8f9fa;
+    transition: all 0.3s ease;
+}
+
+.el-table .content-preview:hover {
+    background-color: #e9ecef;
+    border-color: #409eff;
+}
+
+.content-text {
+    line-height: 1.4;
+    color: #606266;
+    font-size: 12px;
+    margin-bottom: 4px;
+}
+
+.view-more-btn {
+    color: #409eff;
+    font-size: 11px;
+    padding: 0;
+    margin: 0;
+}
+
+.view-more-btn:hover {
+    color: #66b1ff;
+}
 </style>

+ 54 - 58
addons/admin/src/views/exam/component/ArticleExcelImport.vue

@@ -25,8 +25,8 @@
                 <div class="el-upload__tip" slot="tip">
                     <p>只能上传xlsx/xls文件,且不超过10MB</p>
                     <p><strong>Excel格式要求:</strong></p>
-                    <p>• 第一行必须是标题行:名称、类型、内容</p>
-                    <p>• 类型支持:1/单招/单招题、2/对口/对口题、3/专升本/专升本题</p>
+                    <p>• 第一行必须是标题行:名称、分类名称、内容</p>
+                    <p>• 分类名称:请填写对应的分类名称</p>
                     <p>• 内容支持富文本格式</p>
                 </div>
             </el-upload>
@@ -38,14 +38,7 @@
             <el-table :data="excelData" border stripe max-height="400" style="width: 100%">
                 <el-table-column type="index" label="序号" width="60" />
                 <el-table-column prop="name" label="名称" width="200" show-overflow-tooltip />
-                <el-table-column prop="type" label="类型(原始)" width="150" show-overflow-tooltip />
-                <el-table-column prop="paper_type" label="试卷类型" width="120">
-                    <template slot-scope="{ row }">
-                        <el-tag :type="getPaperTypeTagType(row.paper_type)">
-                            {{ getPaperTypeText(row.paper_type) }}
-                        </el-tag>
-                    </template>
-                </el-table-column>
+                <el-table-column prop="category_name" label="分类名称" width="120" show-overflow-tooltip />
                 <el-table-column prop="content" label="内容" min-width="200" show-overflow-tooltip />
                 <el-table-column label="操作" width="100">
                     <template slot-scope="{ row, $index }">
@@ -74,6 +67,10 @@ export default {
         visible: {
             type: Boolean,
             default: false
+        },
+        article_cate_type: {
+            type: Number,
+            default: 1
         }
     },
     data() {
@@ -81,7 +78,8 @@ export default {
             isFullScreen: false,
             importing: false,
             excelData: [],
-            currentFile: null
+            currentFile: null,
+            categoryOptions: []
         }
     },
     methods: {
@@ -89,10 +87,10 @@ export default {
         downloadTemplate() {
             // 创建模板数据
             const templateData = [
-                ['名称', '类型', '内容'],
-                ['示例复习资料1', '1', '这是单招题的复习内容示例'],
-                ['示例复习资料2', '对口', '这是对口题的复习内容示例'],
-                ['示例复习资料3', '专升本题', '这是专升本题的复习内容示例']
+                ['名称', '分类名称', '内容'],
+                ['示例复习资料1', '对口', '这是分类1的复习内容示例'],
+                ['示例复习资料2', '单招', '这是分类2的复习内容示例'],
+                ['示例复习资料3', '专升本', '这是分类3的复习内容示例']
             ];
 
             // 创建工作簿
@@ -163,17 +161,17 @@ export default {
         // 处理Excel数据
         processExcelData(jsonData) {
             const headers = jsonData[0];
+            console.log(headers);
             const dataRows = jsonData.slice(1);
 
             // 验证表头
-            const requiredHeaders = ['名称', '类型', '内容'];
+            const requiredHeaders = ['名称', '分类名称', '内容'];
             const headerMap = {};
             headers.forEach((header, index) => {
                 if (requiredHeaders.includes(header)) {
                     headerMap[header] = index;
                 }
             });
-
             if (Object.keys(headerMap).length < requiredHeaders.length) {
                 this.$message.error(`Excel表头必须包含: ${requiredHeaders.join(', ')}`);
                 return;
@@ -182,57 +180,32 @@ export default {
             // 处理数据行
             this.excelData = dataRows.map((row, index) => {
                 const name = row[headerMap['名称']] || '';
-                const type = row[headerMap['类型']] || '';
+                const categoryName = row[headerMap['分类名称']] || '';
                 const content = row[headerMap['内容']] || '';
 
-                // 处理类型转换
-                let paper_type = null;
-                if (type) {
-                    const typeStr = String(type).trim();
-                    if (typeStr === '1' || typeStr === '单招' || typeStr === '单招题') {
-                        paper_type = 1;
-                    } else if (typeStr === '2' || typeStr === '对口' || typeStr === '对口题') {
-                        paper_type = 2;
-                    } else if (typeStr === '3' || typeStr === '专升本' || typeStr === '专升本题') {
-                        paper_type = 3;
-                    }
+                // 根据分类名称匹配分类ID
+                const categories = this.categoryOptions.filter(cat => cat.name === categoryName.trim());
+                if (categories.length === 0) {
+                    this.$message.error(`第${index + 2}行:找不到分类名称"${categoryName}",请检查分类名称是否正确`);
+                    return null;
                 }
 
+                // 如果能匹配到多个分类名称,则取第一个
+                const category = categories[0];
+
                 return {
                     name: String(name).trim(),
-                    type: String(type).trim(),
-                    paper_type: paper_type,
+                    category_name: String(categoryName).trim(),
+                    cate_id: category.id,
                     content: String(content).trim()
                 };
-            }).filter(item => item.name && item.content); // 过滤空数据
+            }).filter(item => item !== null && item.name && item.content); // 过滤空数据和错误数据
 
             if (this.excelData.length === 0) {
                 this.$message.warning('没有找到有效的数据行');
-            } else {
-                this.$message.success(`成功读取 ${this.excelData.length} 条数据`);
             }
         },
 
-        // 获取试卷类型文本
-        getPaperTypeText(paperType) {
-            const types = {
-                1: '单招题',
-                2: '对口题',
-                3: '专升本题'
-            };
-            return types[paperType] || '未知';
-        },
-
-        // 获取试卷类型标签类型
-        getPaperTypeTagType(paperType) {
-            const types = {
-                1: 'primary',
-                2: 'success',
-                3: 'warning'
-            };
-            return types[paperType] || '';
-        },
-
         // 删除行
         removeRow(index) {
             this.excelData.splice(index, 1);
@@ -246,9 +219,9 @@ export default {
             }
 
             // 验证数据
-            const invalidRows = this.excelData.filter(item => !item.name || !item.content || !item.paper_type);
+            const invalidRows = this.excelData.filter(item => !item.name || !item.content || !item.cate_id);
             if (invalidRows.length > 0) {
-                this.$message.error('存在无效数据,请检查名称、内容和试卷类型是否完整');
+                this.$message.error('存在无效数据,请检查名称、内容和分类名称是否完整');
                 return;
             }
 
@@ -258,7 +231,7 @@ export default {
                 const importData = {
                     articles: this.excelData.map(item => ({
                         title: item.name,
-                        paper_type: item.paper_type,
+                        cate_id: item.cate_id,
                         content: item.content,
                         type: 21 // 固定为复习资料类型
                     }))
@@ -296,8 +269,31 @@ export default {
             if (this.$refs.upload) {
                 this.$refs.upload.clearFiles();
             }
+        },
+
+        // 加载分类选项
+        async loadCategoryOptions() {
+            try {
+                const res = await this.$http.get('/articleCategory/options', {
+                    params: {
+                        // type: this.article_cate_type
+                    }
+                });
+                if (res.data.code === 0) {
+                    this.categoryOptions = res.data.data || [];
+                }
+            } catch (error) {
+                console.error('加载分类选项失败:', error);
+            }
         }
-    }
+    },
+    watch: {
+        visible(val) {
+            if (val) {
+                this.loadCategoryOptions();
+            }
+        }
+    },
 }
 </script>
 

+ 376 - 26
addons/admin/src/views/exam/component/ArticleForm.vue

@@ -1,22 +1,50 @@
 <template>
-    <el-dialog :title="isEdit ? '编辑复习资料' : '新增复习资料'" append-to-body :visible.sync="visibleInternal" width="800px"
+    <el-dialog :title="isEdit ? '编辑复习资料' : '新增复习资料'" append-to-body :visible.sync="visibleInternal" min-width="800px"
         custom-class="article-form-dialog">
         <div v-if="loaded">
             <el-form :model="formData" :rules="formRules" ref="formRef" label-width="120px">
-                <el-form-item label="文章标题" prop="title">
-                    <el-input v-model="formData.title" placeholder="请输入文章标题" />
+                <el-form-item label="标题" prop="title">
+                    <el-input v-model="formData.title" placeholder="请输入标题" />
                 </el-form-item>
 
-                <el-form-item label="试卷类型" prop="paper_type">
-                    <el-select v-model="formData.paper_type" placeholder="请选择试卷类型" class="ele-fluid">
-                        <el-option label="单招题" :value="1" />
-                        <el-option label="对口题" :value="2" />
-                        <el-option label="专升本题" :value="3" />
+                <el-form-item label="分类" prop="cate_id">
+                    <el-select v-model="formData.cate_id" placeholder="请选择分类" class="ele-fluid" clearable>
+                        <el-option v-for="category in categoryOptions" :key="category.id" :label="category.name"
+                            :value="category.id" />
                     </el-select>
                 </el-form-item>
 
-                <el-form-item label="文章内容" prop="content">
-                    <tinymce-editor v-model="formData.content" :init="editorConfig" />
+                <el-form-item label="内容类型" prop="content_type">
+                    <el-radio-group v-model="formData.content_type" @change="onContentTypeChange">
+                        <el-radio :label="1">富文本编辑</el-radio>
+                        <el-radio :label="2">Word文档</el-radio>
+                        <el-radio :label="3">图片</el-radio>
+                    </el-radio-group>
+                </el-form-item>
+
+                <el-form-item label="内容" prop="content">
+                    <!-- 富文本编辑器 -->
+                    <tinymce-editor v-if="formData.content_type === 1" v-model="formData.content"
+                        :init="editorConfig" />
+
+                    <!-- Word文档上传 -->
+                    <div v-else-if="formData.content_type === 2" class="word-upload-container">
+                        <el-upload class="word-uploader" :show-file-list="false" :before-upload="beforeWordUpload"
+                            :auto-upload="false" :on-change="onWordFileChange" accept=".doc,.docx">
+                            <el-button type="primary" icon="el-icon-upload">选择Word文档</el-button>
+                        </el-upload>
+                        <div v-if="formData.word_file" class="word-file-info">
+                            <i class="el-icon-document"></i>
+                            <span>{{ formData.word_file_name }}</span>
+                            <el-button type="text" @click="removeWordFile" size="mini">删除</el-button>
+                        </div>
+                    </div>
+
+                    <!-- 图片上传 -->
+                    <div v-else-if="formData.content_type === 3" class="image-upload-container">
+                        <uploadImage :limit="1" v-model="formData.image_url" @upload-success="onImageUploadSuccess">
+                        </uploadImage>
+                    </div>
                 </el-form-item>
             </el-form>
 
@@ -31,18 +59,20 @@
 
 <script>
 import TinymceEditor from '@/components/TinymceEditor/index.vue'
+import uploadImage from '@/components/uploadImage.vue'
 
 export default {
     name: "ArticleForm",
     components: {
-        TinymceEditor
+        TinymceEditor,
+        uploadImage
     },
     props: {
         visible: { type: Boolean, default: false },
         isEdit: { type: Boolean, default: false },
         articleId: { type: [Number, null], default: null },
         defaultType: { type: Number, default: 21 },
-        defaultPaperType: { type: Number, default: 1 }
+        article_cate_type: { type: Number, default: 1 }
     },
     data() {
         return {
@@ -53,13 +83,19 @@ export default {
                 id: null,
                 title: "",
                 type: 21, // 固定为复习资料类型
-                paper_type: this.defaultPaperType,
-                content: ""
+                cate_id: null,
+                content: "",
+                content_type: 1, // 内容类型:1-富文本,2-Word文档,3-图片
+                word_file: null, // Word文档文件对象
+                word_file_name: "", // Word文档名称
+                image_url: "" // 图片URL
             },
+            categoryOptions: [],
             formRules: {
-                title: [{ required: true, message: "请输入文章标题", trigger: "blur" }],
-                paper_type: [{ required: true, message: "请选择试卷类型", trigger: "change" }],
-                content: [{ required: true, message: "请输入文章内容", trigger: "blur" }]
+                title: [{ required: true, message: "请输入标题", trigger: "blur" }],
+                cate_id: [{ required: true, message: "请选择分类", trigger: "change" }],
+                content_type: [{ required: true, message: "请选择内容类型", trigger: "change" }],
+                content: [{ required: true, message: "请输入内容", trigger: "blur" }]
             },
             editorConfig: {
                 height: 400,
@@ -67,7 +103,41 @@ export default {
                 language: 'zh_CN',
                 plugins: 'code print preview fullscreen paste searchreplace save autosave link autolink image imagetools media table codesample lists advlist hr charmap emoticons anchor directionality pagebreak quickbars nonbreaking visualblocks visualchars wordcount',
                 toolbar: 'fullscreen preview code | undo redo | forecolor backcolor | bold italic underline strikethrough | alignleft aligncenter alignright alignjustify | outdent indent | numlist bullist | formatselect fontselect fontsizeselect | link image media emoticons charmap anchor pagebreak codesample | ltr rtl',
-                toolbar_drawer: 'sliding'
+                toolbar_drawer: 'sliding',
+                // 确保HTML内容能正确显示
+                valid_elements: '*[*]',
+                valid_children: '+body[style]',
+                // 允许所有HTML标签
+                extended_valid_elements: 'img[src|alt|width|height|style|class],div[class|style],p[style],h1[style],h2[style],h3[style],hr[style]',
+                // 确保图片能正确显示
+                convert_urls: false,
+                // 允许内联样式
+                allow_script_urls: true,
+                // 保持HTML格式
+                keep_styles: true,
+                // 不清理HTML
+                cleanup: false,
+                cleanup_on_startup: false,
+                verify_html: false,
+                // 允许所有属性
+                allow_html_data_urls: true,
+                // 不自动转换URL
+                relative_urls: false,
+                remove_script_host: false,
+                // 保持原始HTML结构
+                preserve_cdata: true,
+                // 允许所有CSS样式
+                allow_conditional_comments: true,
+                // 不自动格式化
+                auto_focus: false,
+                // 保持换行
+                convert_newlines_to_brs: true,
+                // 不自动清理
+                cleanup_on_startup: false,
+                // 允许所有标签
+                valid_elements: '*[*]',
+                // 不验证HTML
+                verify_html: false
             }
         };
     },
@@ -86,10 +156,14 @@ export default {
                         id: null,
                         title: "",
                         type: 21,
-                        paper_type: this.defaultPaperType,
-                        content: ""
+                        cate_id: null,
+                        content: "",
+                        content_type: 1,
+                        word_file: null,
+                        word_file_name: "",
+                        image_url: ""
                     };
-                    this.loaded = true;
+                    this.loadCategories();
                 }
             }
         }
@@ -97,17 +171,29 @@ export default {
     methods: {
         async loadDetail() {
             const res = await this.$http.get(`/article/info?id=${this.articleId}`);
+            console.log('Article info response:', res.data);
             if (res.data.code === 0) {
                 const data = res.data.data;
+                console.log('Article data:', data);
+                // 先加载分类选项
+                await this.loadCategories();
+                console.log('Category options loaded:', this.categoryOptions);
+                // 然后设置表单数据
                 this.formData = {
                     id: data.id,
                     title: data.title,
                     type: 21,
-                    paper_type: data.paper_type || this.defaultPaperType,
-                    content: data.content
+                    cate_id: data.cate_id || null,
+                    content: data.content || "",
+                    content_type: data.content_type || 1,
+                    word_file: null, // 编辑时不加载文件对象
+                    word_file_name: data.word_file_name || "",
+                    image_url: data.image_url || ""
                 };
+                console.log('Form data set:', this.formData);
+            } else {
+                await this.loadCategories();
             }
-            this.loaded = true;
         },
         async saveForm() {
             this.$refs.formRef.validate(async valid => {
@@ -119,7 +205,7 @@ export default {
                         id: this.formData.id,
                         title: this.formData.title,
                         type: 21,
-                        paper_type: this.formData.paper_type,
+                        cate_id: this.formData.cate_id,
                         content: this.formData.content,
                         status: 1, // 默认发布状态
                         sort: 0, // 默认排序
@@ -127,7 +213,6 @@ export default {
                         tags: '', // 默认标签
                         cover: '', // 默认封面
                         description: '', // 默认描述
-                        cate_id: 0 // 默认分类
                     };
 
                     const res = await this.$http.post("/article/edit", submitData);
@@ -142,6 +227,196 @@ export default {
                     this.saving = false;
                 }
             });
+        },
+        async loadCategories() {
+            try {
+                const res = await this.$http.get('/articleCategory/options', {
+                    params: {
+                        type: this.article_cate_type
+                    }
+                });
+                if (res.data.code === 0) {
+                    this.categoryOptions = res.data.data || [];
+                }
+                this.loaded = true;
+            } catch (error) {
+                console.error('加载分类选项失败:', error);
+                this.loaded = true;
+            }
+        },
+
+        // 内容类型变化处理
+        onContentTypeChange(type) {
+            // 切换内容类型时清空其他类型的数据,但保留已解析的内容
+            if (type === 1) {
+                // 富文本编辑 - 不清空content,保留已解析的内容
+                this.formData.word_file = null;
+                this.formData.word_file_name = '';
+                this.formData.image_url = '';
+            } else if (type === 2) {
+                // Word文档 - 如果还没有解析内容,则清空content
+                if (!this.formData.word_file) {
+                    this.formData.content = '';
+                }
+                this.formData.image_url = '';
+            } else if (type === 3) {
+                // 图片 - 如果还没有解析内容,则清空content
+                if (!this.formData.image_url) {
+                    this.formData.content = '';
+                }
+                this.formData.word_file = null;
+                this.formData.word_file_name = '';
+            }
+        },
+
+        // Word文档上传前验证
+        beforeWordUpload(file) {
+            const isWord = file.type === 'application/msword' ||
+                file.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';
+            if (!isWord) {
+                this.$message.error('只能上传Word文档!');
+                return false;
+            }
+            const isLt10M = file.size / 1024 / 1024 < 10;
+            if (!isLt10M) {
+                this.$message.error('Word文档大小不能超过 10MB!');
+                return false;
+            }
+            // 返回false阻止自动上传,我们使用on-change处理
+            return false;
+        },
+
+        // Word文档文件选择变化
+        onWordFileChange(file, fileList) {
+            if (file.raw) {
+                this.formData.word_file = file.raw;
+                this.formData.word_file_name = file.name;
+                this.$message.success('Word文档选择成功');
+
+                // 直接解析Word文档内容
+                this.parseWordContentFromFile(file.raw);
+            }
+        },
+
+        // 删除Word文档
+        removeWordFile() {
+            this.formData.word_file = null;
+            this.formData.word_file_name = '';
+        },
+
+        // 图片上传前验证
+        beforeImageUpload(file) {
+            const isImage = file.type.startsWith('image/');
+            if (!isImage) {
+                this.$message.error('只能上传图片!');
+                return false;
+            }
+            const isLt2M = file.size / 1024 / 1024 < 2;
+            if (!isLt2M) {
+                this.$message.error('图片大小不能超过 2MB!');
+                return false;
+            }
+            return true;
+        },
+
+        // 图片上传成功
+        onImageUploadSuccess(uploadData) {
+            this.$message.success('图片上传成功');
+
+            // 将图片内容显示在富文本编辑器中
+            this.parseImageContent(uploadData.url, uploadData.fileName || '上传的图片');
+        },
+
+        // 图片上传失败
+        onImageUploadError(error) {
+            this.$message.error('图片上传失败');
+        },
+
+        // 删除图片
+        removeImage() {
+            this.formData.image_url = '';
+        },
+
+        // 解析Word文档内容(从文件对象)
+        async parseWordContentFromFile(file) {
+            try {
+                // 切换到富文本编辑模式
+                this.formData.content_type = 1;
+
+                // 使用前端解析Word文档
+                const content = await this.parseWordWithFrontendFromFile(file);
+
+                // 将解析的内容设置到富文本编辑器中
+                this.formData.content = content;
+                this.$message.success('Word文档内容解析成功,已加载到编辑器中');
+            } catch (error) {
+                console.error('解析Word文档失败:', error);
+                // 解析失败时的备用内容
+                this.formData.content = `<div class="word-document">
+                    <h3>Word文档:${file.name}</h3>
+                    <p>文件大小:${(file.size / 1024 / 1024).toFixed(2)} MB</p>
+                    <p>上传时间:${new Date().toLocaleString()}</p>
+                    <p>注意:Word文档内容解析失败,请手动编辑内容</p>
+                </div>`;
+                this.$message.warning('Word文档内容解析失败,请手动编辑内容');
+            }
+        },
+
+        // 前端解析Word文档(从文件对象)
+        async parseWordWithFrontendFromFile(file) {
+            try {
+                // 导入mammoth
+                const mammoth = require('mammoth');
+
+                // 读取文件内容
+                const arrayBuffer = await file.arrayBuffer();
+
+                // 使用mammoth解析Word文档
+                const result = await mammoth.convertToHtml({ arrayBuffer: arrayBuffer });
+
+                let content = '';
+
+                if (result.value) {
+                    // 获取解析后的HTML内容
+                    content = '<div class="word-content">' + result.value + '</div>';
+
+                    // 如果有警告信息,在控制台显示
+                    if (result.messages && result.messages.length > 0) {
+                        console.log('Word文档解析警告:', result.messages);
+                    }
+
+                    this.$message.success('Word文档解析成功');
+                } else {
+                    content = '<div class="word-content"><p>Word文档解析失败,请手动输入或粘贴内容。</p></div>';
+                    this.$message.warning('Word文档解析失败,请手动输入内容');
+                }
+
+                return content;
+            } catch (error) {
+                console.error('前端解析Word文档失败:', error);
+                this.$message.error('Word文档解析失败: ' + error.message);
+                return '<div class="word-content"><p>Word文档解析失败,请手动输入内容。</p></div>';
+            }
+        },
+
+        // HTML转义函数
+        escapeHtml(text) {
+            const div = document.createElement('div');
+            div.textContent = text;
+            return div.innerHTML;
+        },
+
+        // 解析图片内容
+        parseImageContent(url, fileName) {
+            // 切换到富文本编辑模式
+            this.formData.content_type = 1;
+
+            // 将图片内容设置到富文本编辑器中
+            this.formData.content = `<div class="image-content">
+                <img src="${url}" alt="${fileName}" style="max-width: 100%; height: auto; border: 1px solid #ddd; border-radius: 4px; margin: 10px 0;">
+            </div>`;
+
+            this.$message.success('图片已加载到编辑器中,您可以继续编辑');
         }
     }
 };
@@ -156,4 +431,79 @@ export default {
 .article-form-dialog~.v-modal {
     z-index: 3001 !important;
 }
+
+/* Word文档上传样式 */
+.word-upload-container {
+    border: 1px dashed #d9d9d9;
+    border-radius: 6px;
+    padding: 20px;
+    text-align: center;
+    background-color: #fafafa;
+}
+
+.word-file-info {
+    margin-top: 10px;
+    padding: 10px;
+    background-color: #f0f9ff;
+    border: 1px solid #bae6fd;
+    border-radius: 4px;
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+}
+
+.word-file-info i {
+    color: #0369a1;
+    margin-right: 8px;
+}
+
+.word-file-info span {
+    flex: 1;
+    color: #0369a1;
+    font-size: 14px;
+}
+
+/* 图片上传样式 */
+.image-upload-container {
+    border: 1px dashed #d9d9d9;
+    border-radius: 6px;
+    padding: 20px;
+    text-align: center;
+    background-color: #fafafa;
+}
+
+.image-uploader {
+    display: inline-block;
+}
+
+.uploaded-image {
+    width: 200px;
+    height: 200px;
+    object-fit: cover;
+    border-radius: 6px;
+    border: 1px solid #d9d9d9;
+}
+
+.image-uploader-icon {
+    font-size: 28px;
+    color: #8c939d;
+    width: 200px;
+    height: 200px;
+    line-height: 200px;
+    text-align: center;
+    border: 1px dashed #d9d9d9;
+    border-radius: 6px;
+    background-color: #fafafa;
+    cursor: pointer;
+    transition: all 0.3s;
+}
+
+.image-uploader-icon:hover {
+    border-color: #409eff;
+    color: #409eff;
+}
+
+.image-actions {
+    margin-top: 10px;
+}
 </style>

+ 271 - 0
addons/admin/src/views/exam/component/CategoryManager.vue

@@ -0,0 +1,271 @@
+<template>
+    <el-dialog :visible.sync="visible" title="分类管理" width="800px" :close-on-click-modal="false">
+        <div class="category-manager">
+            <!-- 操作按钮 -->
+            <div class="category-actions">
+                <el-button type="primary" icon="el-icon-plus" @click="showAddForm">添加分类</el-button>
+            </div>
+
+            <!-- 分类列表 -->
+            <el-table :data="categories" v-loading="loading" :border="true" :stripe="true">
+                <el-table-column label="排序" width="80" align="center">
+                    <template slot-scope="{ row }">
+                        <i class="el-icon-sort"></i>
+                    </template>
+                </el-table-column>
+                <el-table-column label="分类名称" prop="name" min-width="150" />
+                <el-table-column label="图标" width="80" align="center">
+                    <template slot-scope="{ row }">
+                        <image-preview v-if="row.icon" size="xs" :images="[row.icon]" />
+                        <span v-else>-</span>
+                    </template>
+                </el-table-column>
+                <el-table-column label="描述" prop="description" min-width="200">
+                    <template slot-scope="{ row }">
+                        <text-ellipsis :text="row.description" :max-length="30" />
+                    </template>
+                </el-table-column>
+                <el-table-column label="状态" width="80" align="center">
+                    <template slot-scope="{ row }">
+                        <el-tag :type="row.status === 1 ? 'success' : 'danger'">
+                            {{ row.status === 1 ? '有效' : '无效' }}
+                        </el-tag>
+                    </template>
+                </el-table-column>
+                <el-table-column label="操作" width="140" align="center" fixed="right">
+                    <template slot-scope="{ row, $index }">
+                        <el-button size="mini" type="primary" plain @click="editCategory(row)">编辑</el-button>
+                        <el-button size="mini" type="danger" plain @click="deleteCategory(row, $index)">删除</el-button>
+                    </template>
+                </el-table-column>
+            </el-table>
+        </div>
+
+        <!-- 添加/编辑分类弹窗 -->
+        <el-dialog :visible.sync="showForm" :title="isEdit ? '编辑分类' : '添加分类'" width="500px"
+            :close-on-click-modal="false" append-to-body>
+            <el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
+                <el-form-item label="分类名称" prop="name">
+                    <el-input v-model="form.name" placeholder="请输入分类名称" />
+                </el-form-item>
+
+                <el-form-item label="图标" prop="icon">
+                    <uploadImage :limit="1" v-model="form.icon" />
+                </el-form-item>
+
+                <el-form-item label="描述" prop="description">
+                    <el-input type="textarea" :rows="3" v-model="form.description" placeholder="请输入分类描述" />
+                </el-form-item>
+
+                <el-form-item label="排序" prop="sort">
+                    <el-input-number v-model="form.sort" :min="0" :max="999" />
+                </el-form-item>
+
+                <el-form-item label="状态" prop="status">
+                    <el-radio-group v-model="form.status">
+                        <el-radio :label="1">有效</el-radio>
+                        <el-radio :label="2">无效</el-radio>
+                    </el-radio-group>
+                </el-form-item>
+            </el-form>
+
+            <div slot="footer" class="dialog-footer">
+                <el-button @click="cancelForm">取消</el-button>
+                <el-button type="primary" @click="submitForm">保存</el-button>
+            </div>
+        </el-dialog>
+    </el-dialog>
+</template>
+
+<script>
+import uploadImage from '../../../components/uploadImage'
+
+export default {
+    name: 'CategoryManager',
+    components: {
+        uploadImage
+    },
+    props: {
+        visible: {
+            type: Boolean,
+            default: false
+        },
+        article_cate_type: {
+            type: Number,
+            default: 1
+        }
+    },
+    data() {
+        return {
+            loading: false,
+            categories: [],
+            showForm: false,
+            isEdit: false,
+            form: {
+                id: null,
+                name: '',
+                icon: '',
+                type: 1,
+                sort: 0,
+                description: '',
+                status: 1
+            },
+            rules: {
+                name: [
+                    { required: true, message: '请输入分类名称', trigger: 'blur' }
+                ],
+                status: [
+                    { required: true, message: '请选择状态', trigger: 'change' }
+                ]
+            }
+        }
+    },
+    watch: {
+        visible(val) {
+            console.log('CategoryManager visible changed:', val);
+            if (val) {
+                this.loadCategories();
+            } else {
+                // 弹窗关闭时,确保内部弹窗也关闭
+                this.showForm = false;
+                this.resetForm();
+            }
+        }
+    },
+    methods: {
+        // 加载分类列表
+        async loadCategories() {
+            this.loading = true;
+            try {
+                const res = await this.$http.get('/articleCategory/index', {
+                    params: {
+                        type: this.article_cate_type,
+                        limit: 999
+                    }
+                });
+
+                if (res.data.code === 0) {
+                    this.categories = res.data.data || [];
+                } else {
+                    this.$message.error(res.data.msg);
+                }
+            } catch (error) {
+                this.$message.error('加载分类失败');
+                console.error(error);
+            } finally {
+                this.loading = false;
+            }
+        },
+
+        // 显示添加表单
+        showAddForm() {
+            this.isEdit = false;
+            this.resetForm();
+            this.showForm = true;
+        },
+
+        // 编辑分类
+        editCategory(category) {
+            this.isEdit = true;
+            this.form = { ...category, type: this.article_cate_type };
+            this.showForm = true;
+        },
+
+        // 删除分类
+        async deleteCategory(category, index) {
+            try {
+                await this.$confirm('确定要删除这个分类吗?', '提示', {
+                    type: 'warning'
+                });
+
+                const res = await this.$http.post('/articleCategory/delete', {
+                    id: category.id
+                });
+
+                if (res.data.code === 0) {
+                    this.$message.success('删除成功');
+                    this.categories.splice(index, 1);
+                } else {
+                    this.$message.error(res.data.msg);
+                }
+            } catch (error) {
+                if (error !== 'cancel') {
+                    this.$message.error('删除失败');
+                    console.error(error);
+                }
+            }
+        },
+
+        // 提交表单
+        async submitForm() {
+            this.$refs.formRef.validate(async (valid) => {
+                if (!valid) return;
+
+                try {
+                    const formData = {
+                        ...this.form,
+                        type: this.article_cate_type
+                    };
+
+                    const res = await this.$http.post("/articleCategory/edit", formData);
+
+                    if (res.data.code === 0) {
+                        this.$message.success(this.isEdit ? '更新成功' : '添加成功');
+                        this.loadCategories();
+                        this.cancelForm();
+                        this.$emit('updated');
+                    } else {
+                        this.$message.error(res.data.msg);
+                    }
+                } catch (error) {
+                    this.$message.error(this.isEdit ? '更新失败' : '添加失败');
+                    console.error(error);
+                }
+            });
+        },
+
+        // 取消表单
+        cancelForm() {
+            this.showForm = false;
+            this.resetForm();
+        },
+
+        // 重置表单
+        resetForm() {
+            this.form = {
+                id: null,
+                name: '',
+                icon: '',
+                type: this.article_cate_type,
+                sort: 0,
+                description: '',
+                status: 1
+            };
+            this.isEdit = false;
+
+            if (this.$refs.formRef) {
+                this.$refs.formRef.clearValidate();
+            }
+        },
+
+        // 关闭弹窗
+        handleClose() {
+            // 确保内部弹窗也关闭
+            this.showForm = false;
+            this.resetForm();
+            this.$emit('update:visible', false);
+        }
+    }
+};
+</script>
+
+<style scoped>
+.category-manager {
+    padding: 20px 0;
+}
+
+.category-actions {
+    margin-bottom: 20px;
+    text-align: right;
+}
+</style>

+ 3 - 3
addons/admin/src/views/exam/index1-5.vue

@@ -1,14 +1,14 @@
 <template>
-    <examList :paperType=1 :type=21 :permissionMap="permissionMap" />
+    <articleList :type=21 :article_cate_type=1 :permissionMap="permissionMap" />
 </template>
 
 <script>
-import examList from "./articleList"
+import articleList from "./articleList"
 
 export default {
     name: "index1-5",
 
-    components: { examList },
+    components: { articleList },
     data() {
         return {
             permissionMap: {

+ 3 - 3
addons/admin/src/views/exam/index2-5.vue

@@ -1,14 +1,14 @@
 <template>
-    <examList :paperType=2 :type=21 :permissionMap="permissionMap" />
+    <articleList :type=21 :article_cate_type=2 :permissionMap="permissionMap" />
 </template>
 
 <script>
-import examList from "./articleList"
+import articleList from "./articleList"
 
 export default {
     name: "index2-5",
 
-    components: { examList },
+    components: { articleList },
     data() {
         return {
             permissionMap: {

+ 3 - 3
addons/admin/src/views/exam/index3-5.vue

@@ -1,14 +1,14 @@
 <template>
-    <examList :paperType=3 :type=21 :permissionMap="permissionMap" />
+    <articleList :type=21 :article_cate_type=3 :permissionMap="permissionMap" />
 </template>
 
 <script>
-import examList from "./articleList"
+import articleList from "./articleList"
 
 export default {
     name: "index3-5",
 
-    components: { examList },
+    components: { articleList },
     data() {
         return {
             permissionMap: {

+ 57 - 0
app/Http/Controllers/Admin/ArticleCategoryController.php

@@ -24,4 +24,61 @@ class ArticleCategoryController extends Backend
         parent::__construct();
         $this->service = new ArticleCategoryService();
     }
+
+    /**
+     * 获取分类列表
+     */
+    public function index()
+    {
+        $pageSize = request()->get('limit', 10);
+        $list = $this->service->getDataList(request()->all(), $pageSize);
+        $message = array(
+            "msg" => '操作成功',
+            "code" => 0,
+            "data" => isset($list['list']) ? $list['list'] : [],
+            "count" => isset($list['total']) ? $list['total'] : 0,
+        );
+        return $message;
+    }
+
+    /**
+     * 获取分类选项
+     */
+    public function options()
+    {
+        $list = $this->service->getOptions();
+        return response()->json(['code' => 0, 'msg' => '操作成功', 'data' => $list]);
+    }
+
+    /**
+     * 获取分类详情
+     */
+    public function info()
+    {
+        return $this->service->info();
+    }
+
+    /**
+     * 添加或编辑分类
+     */
+    public function edit()
+    {
+        return $this->service->edit();
+    }
+
+    /**
+     * 删除分类
+     */
+    public function delete()
+    {
+        return $this->service->delete();
+    }
+
+    /**
+     * 设置分类状态
+     */
+    public function status()
+    {
+        return $this->service->status();
+    }
 }

+ 33 - 0
app/Http/Controllers/Admin/ArticleController.php

@@ -63,4 +63,37 @@ class ArticleController extends Backend
 
         return response()->json($result);
     }
+
+    /**
+     * 解析Word文档内容
+     * @return \Illuminate\Http\JsonResponse
+     */
+    public function parseWord()
+    {
+        $url = request()->input('url');
+        $fileName = request()->input('fileName');
+
+        if (!$url) {
+            return response()->json(['code' => 1, 'msg' => '文档URL不能为空']);
+        }
+
+        try {
+            // 这里可以集成Word文档解析库,如PhpOffice\PhpWord
+            // 暂时返回模拟的解析结果
+            $content = $this->service->parseWordDocument($url, $fileName);
+
+            return response()->json([
+                'code' => 0,
+                'msg' => '解析成功',
+                'data' => [
+                    'content' => $content
+                ]
+            ]);
+        } catch (\Exception $e) {
+            return response()->json([
+                'code' => 1,
+                'msg' => '解析失败: ' . $e->getMessage()
+            ]);
+        }
+    }
 }

+ 78 - 3
app/Services/Common/ArticleCategoryService.php

@@ -26,17 +26,92 @@ class ArticleCategoryService extends BaseService
     }
 
     /**
+     * 获取分类数据列表
+     */
+    public function getDataList($params, $pageSize = 15)
+    {
+        $query = $this->model->where('mark', 1);
+
+        // 按类型过滤
+        if (!empty($params['type'])) {
+            $query->where('type', $params['type']);
+        }
+
+        // 按状态过滤
+        if (isset($params['status']) && $params['status'] !== '') {
+            $query->where('status', $params['status']);
+        }
+
+        // 按名称搜索
+        if (!empty($params['keyword'])) {
+            $query->where('name', 'like', "%{$params['keyword']}%");
+        }
+
+        $paginator = $query->orderBy('sort', 'desc')
+            ->orderBy('id', 'desc')
+            ->paginate($pageSize > 0 ? $pageSize : 9999999);
+
+        $list = $paginator ? $paginator->toArray() : [];
+
+        // 处理图片字段
+        $items = isset($list['data']) ? $list['data'] : [];
+        foreach ($items as &$item) {
+            if (!empty($item['icon'])) {
+                $item['icon'] = get_image_url($item['icon']);
+            }
+            $item['create_time'] = $item['create_time'] ? datetime($item['create_time'], 'Y-m-d H:i:s') : '';
+            $item['update_time'] = $item['update_time'] ? datetime($item['update_time'], 'Y-m-d H:i:s') : '';
+        }
+
+        return [
+            'pageSize' => $pageSize,
+            'total' => isset($list['total']) ? $list['total'] : 0,
+            'list' => $items
+        ];
+    }
+
+    /**
      * 获取分类选项列表
      * @return array
      */
-    public function options()
+    public function getOptions()
     {
-        $datas = $this->model->where(['status' => 1, 'mark' => 1])
-            ->select(['id', 'name'])
+        $query = $this->model->where(['status' => 1, 'mark' => 1]);
+
+        // 按类型过滤
+        $type = request()->get('type');
+        if ($type) {
+            $query->where('type', $type);
+        }
+
+        $datas = $query->select(['id', 'name'])
             ->orderBy('sort', 'desc')
             ->orderBy('id', 'desc')
             ->get();
 
         return $datas ? $datas->toArray() : [];
     }
+
+    /**
+     * 添加或编辑分类
+     */
+    public function edit()
+    {
+        $data = request()->all();
+
+        // 处理图标字段
+        if (!empty($data['icon'])) {
+            $data['icon'] = get_image_path(trim($data['icon']));
+        }
+
+        $id = $data['id'] ?? 0;
+
+        // 默认字段处理
+        $data['update_time'] = time();
+        if (!$id) {
+            $data['create_time'] = time();
+        }
+
+        return parent::edit($data);
+    }
 }

+ 74 - 9
app/Services/Common/ArticleService.php

@@ -59,12 +59,15 @@ class ArticleService extends BaseService
         $where = ['a.mark' => 1];
         $status = isset($params['status']) ? $params['status'] : 0;
         $type = isset($params['type']) ? $params['type'] : 0;
-        $paperType = isset($params['paper_type']) ? $params['paper_type'] : 0;
+        $cateId = isset($params['cate_id']) ? $params['cate_id'] : 0;
+        $articleCateType = isset($params['article_cate_type']) ? $params['article_cate_type'] : 0;
 
         if ($status > 0) {
             $where['a.status'] = $status;
         }
-        $list = $this->model->from('article as a')
+
+        $query = $this->model->from('article as a')
+            ->leftJoin('article_cates as ac', 'a.cate_id', '=', 'ac.id')
             ->where($where)
             ->where(function ($query) use ($type) {
                 if ($type && is_array($type)) {
@@ -73,9 +76,14 @@ class ArticleService extends BaseService
                     $query->where('a.type', $type);
                 }
             })
-            ->where(function ($query) use ($paperType) {
-                if ($paperType) {
-                    $query->where('a.paper_type', $paperType);
+            ->where(function ($query) use ($cateId) {
+                if ($cateId) {
+                    $query->where('a.cate_id', $cateId);
+                }
+            })
+            ->where(function ($query) use ($articleCateType) {
+                if ($articleCateType) {
+                    $query->where('ac.type', $articleCateType);
                 }
             })
             ->where(function ($query) use ($params) {
@@ -85,8 +93,9 @@ class ArticleService extends BaseService
                 }
             })
             ->select(['a.*'])
-            ->orderBy('a.create_time', 'desc')
-            ->paginate($pageSize > 0 ? $pageSize : 9999999);
+            ->orderBy('a.create_time', 'desc');
+
+        $list = $query->paginate($pageSize > 0 ? $pageSize : 9999999);
         $list = $list ? $list->toArray() : [];
         if ($list) {
             foreach ($list['data'] as &$item) {
@@ -174,16 +183,17 @@ class ArticleService extends BaseService
                     throw new Exception("Title or content missing in import data");
                 }
 
+                // 格式化内容
+                $item['content'] = set_format_content($item['content']);
                 DB::table('article')->insert([
                     'title' => $item['title'],
                     'content' => $item['content'],
-                    'paper_type' => $item['paper_type'],
+                    'cate_id' => $item['cate_id'] ?? 0,
                     'type' => 21, // 复习资料类型
                     'author' => '',
                     'tags' => '',
                     'cover' => '',
                     'description' => '',
-                    'cate_id' => 0,
                     'sort' => 0,
                     'view_num' => 0,
                     'status' => 1,
@@ -202,6 +212,27 @@ class ArticleService extends BaseService
     }
 
     /**
+     * 获取记录详情
+     * @return array
+     * @since 2020/11/11
+     * @author laravel开发员
+     */
+    public function info()
+    {
+        // 记录ID
+        $id = request()->input("id", 0);
+        $info = [];
+        if ($id) {
+            $info = $this->model->getInfo($id);
+            // 确保内容不被格式化,保持原始HTML格式供TinyMCE编辑器使用
+            if (isset($info['content'])) {
+                $info['content'] = $info['content'] ? get_format_content($info['content']) : '';
+            }
+        }
+        return message(MESSAGE_OK, true, $info);
+    }
+
+    /**
      * 删除七天之前标记软删除的数据
      */
     public function delete()
@@ -212,4 +243,38 @@ class ArticleService extends BaseService
         $this->model->where('mark', 0)->where('update_time', '<=', time() - 7 * 86400)->delete();
         return parent::delete();
     }
+
+    /**
+     * 解析Word文档内容
+     * @param string $url 文档URL
+     * @param string $fileName 文件名
+     * @return string 解析后的HTML内容
+     */
+    public function parseWordDocument($url, $fileName)
+    {
+        try {
+            // 这里可以集成Word文档解析库,如PhpOffice\PhpWord
+            // 暂时返回模拟的解析结果
+
+            // 模拟解析Word文档内容
+            $content = '<div class="word-document">';
+            $content .= '<h2>Word文档内容解析</h2>';
+            $content .= '<h3>文档信息</h3>';
+            $content .= '<p><strong>文件名:</strong>' . htmlspecialchars($fileName) . '</p>';
+            $content .= '<p><strong>文档路径:</strong>' . htmlspecialchars($url) . '</p>';
+            $content .= '<p><strong>解析时间:</strong>' . date('Y-m-d H:i:s') . '</p>';
+            $content .= '<hr>';
+            $content .= '<h3>文档内容</h3>';
+            $content .= '<p>这里是Word文档的解析内容...</p>';
+            $content .= '<p>实际项目中,这里会显示从Word文档中提取的文本内容。</p>';
+            $content .= '<p>您可以继续在富文本编辑器中编辑这些内容。</p>';
+            $content .= '<hr>';
+            $content .= '<p><em>注意:这是模拟的解析结果。实际项目中需要集成Word文档解析库。</em></p>';
+            $content .= '</div>';
+
+            return $content;
+        } catch (\Exception $e) {
+            throw new \Exception('Word文档解析失败: ' . $e->getMessage());
+        }
+    }
 }

+ 6 - 0
routes/web.php

@@ -130,6 +130,12 @@ Route::post('/article/import', [\App\Http\Controllers\Admin\ArticleController::c
 Route::post('/article/importReviewMaterials', [\App\Http\Controllers\Admin\ArticleController::class, 'importReviewMaterials']);
 
 // 文章分类管理
+// ArticleCategory
+Route::get('/articleCategory/index', [\App\Http\Controllers\Admin\ArticleCategoryController::class, 'index']);
+Route::get('/articleCategory/info', [\App\Http\Controllers\Admin\ArticleCategoryController::class, 'info']);
+Route::post('/articleCategory/edit', [\App\Http\Controllers\Admin\ArticleCategoryController::class, 'edit']);
+Route::post('/articleCategory/delete', [\App\Http\Controllers\Admin\ArticleCategoryController::class, 'delete']);
+Route::post('/articleCategory/status', [\App\Http\Controllers\Admin\ArticleCategoryController::class, 'status']);
 Route::get('/articleCategory/options', [\App\Http\Controllers\Admin\ArticleCategoryController::class, 'options']);
 
 // 广告管理