jinx 1 рік тому
батько
коміт
4ef5c1e873
58 змінених файлів з 25538 додано та 40525 видалено
  1. 0 29975
      packages/admin/package-lock.json
  2. 3 3
      packages/admin/src/views/layout/index.vue
  3. 428 159
      packages/admin/src/views/tab5/index.vue
  4. 4537 2240
      packages/mobile/package-lock.json
  5. 3 0
      packages/mobile/package.json
  6. 2 1
      packages/mobile/public/index.html
  7. BIN
      packages/mobile/src/assets/img/frame.png
  8. 12 0
      packages/mobile/src/assets/img/frame.svg
  9. BIN
      packages/mobile/src/assets/img/icon_Service.png
  10. 7 0
      packages/mobile/src/assets/img/icon_Service.svg
  11. BIN
      packages/mobile/src/assets/img/icon_more.png
  12. 5 0
      packages/mobile/src/assets/img/icon_more.svg
  13. BIN
      packages/mobile/src/assets/img/icon_q&a.png
  14. 32 0
      packages/mobile/src/assets/img/icon_q&a.svg
  15. BIN
      packages/mobile/src/assets/img/icon_special topic.png
  16. 32 0
      packages/mobile/src/assets/img/icon_special topic.svg
  17. BIN
      packages/mobile/src/assets/img/icon_switch.png
  18. 5 0
      packages/mobile/src/assets/img/icon_switch.svg
  19. BIN
      packages/mobile/src/assets/img/icon_talk.png
  20. 10 0
      packages/mobile/src/assets/img/icon_talk.svg
  21. BIN
      packages/mobile/src/assets/img/icon_user.png
  22. 5 0
      packages/mobile/src/assets/img/icon_user.svg
  23. BIN
      packages/mobile/src/assets/img/img.png
  24. 11 0
      packages/mobile/src/assets/img/img.svg
  25. BIN
      packages/mobile/src/assets/img/lable.png
  26. 3 0
      packages/mobile/src/assets/img/lable.svg
  27. 21 14
      packages/mobile/src/main.js
  28. 84 31
      packages/mobile/src/utils/api.js
  29. 18 0
      packages/mobile/src/utils/index.js
  30. 34 39
      packages/mobile/src/views/Home.vue
  31. 633 0
      packages/mobile/src/views/qa/index.vue
  32. 172 0
      packages/mobile/src/views/topic/details.vue
  33. 269 0
      packages/mobile/src/views/topic/index.vue
  34. 7800 7715
      packages/mobile/yarn.lock
  35. BIN
      packages/pc/src/assets/img/icon_Service.png
  36. BIN
      packages/pc/src/assets/img/icon_more.png
  37. BIN
      packages/pc/src/assets/img/icon_switch.png
  38. BIN
      packages/pc/src/assets/img/icon_talk.png
  39. BIN
      packages/pc/src/assets/img/icon_user.png
  40. BIN
      packages/pc/src/assets/img/img_01.png
  41. BIN
      packages/pc/src/assets/img/img_02.png
  42. BIN
      packages/pc/src/assets/img/mobile/frame.png
  43. BIN
      packages/pc/src/assets/img/mobile/icon_Service.png
  44. BIN
      packages/pc/src/assets/img/mobile/icon_more.png
  45. BIN
      packages/pc/src/assets/img/mobile/icon_q&a.png
  46. BIN
      packages/pc/src/assets/img/mobile/icon_special topic.png
  47. BIN
      packages/pc/src/assets/img/mobile/icon_switch.png
  48. BIN
      packages/pc/src/assets/img/mobile/icon_talk.png
  49. BIN
      packages/pc/src/assets/img/mobile/icon_user.png
  50. BIN
      packages/pc/src/assets/img/mobile/img.png
  51. BIN
      packages/pc/src/assets/img/mobile/lable.png
  52. 93 71
      packages/pc/src/utils/api.js
  53. 18 0
      packages/pc/src/utils/index.js
  54. 71 120
      packages/pc/src/views/Home.vue
  55. 678 0
      packages/pc/src/views/qa/index.vue
  56. 206 0
      packages/pc/src/views/topic/details.vue
  57. 290 157
      packages/pc/src/views/topic/index.vue
  58. 10056 0
      yarn.lock

Різницю між файлами не показано, бо вона завелика
+ 0 - 29975
packages/admin/package-lock.json


+ 3 - 3
packages/admin/src/views/layout/index.vue

@@ -20,7 +20,7 @@
       <div class="left">
         <div
           class="biaoji el-icon-message"
-          :class="{ biaojiAc: $route.meta.myInd === 1 }"
+          :class="{ biaojiAc: [1, 5, 6, 7].includes($route.meta.myInd) }"
         >
           内容管理
         </div>
@@ -37,7 +37,7 @@
         <div
           v-if="userAdmin"
           class="biaoji el-icon-setting"
-          :class="{ biaojiAc: $route.meta.myInd > 1 }"
+          :class="{ biaojiAc: [2, 3, 4].includes($route.meta.myInd) }"
         >
           系统管理
         </div>
@@ -221,7 +221,7 @@ export default {
   activated () {} // 如果页面有keep-alive缓存功能,这个函数会触发
 }
 </script>
-<style lang='less' scoped>
+<style lang="less" scoped>
 .layout {
   .top {
     padding-left: 50px;

+ 428 - 159
packages/admin/src/views/tab5/index.vue

@@ -1,154 +1,324 @@
+<!-- eslint-disable vue/no-parsing-error -->
 <template>
-  <div class="tab5">
-    <div class="insideTop">专题管理</div>
-    <div class="obstruct"></div>
-    <div class="content">
-      <div class="search">
-        <div class="btns">
-          <div class="left">
-            <el-button v-for="tab in level1Data.filter(item => item.level===1)"
-                       :type="Number(activeTopId) ===tab.id?'primary':''" v-bind:key="tab.id"
-                       @click="handleLevelTopClick(tab.id)">{{ tab.name }}
-            </el-button>
-          </div>
-          <div class="right">
-            <el-button @click="level1Modal = true">一级专题管理</el-button>
-            <el-button
-              @click="(level2ModalFormVisible = true) && (handleAllReset() && (level2ModalForm.id = activeTopId))">
-              新增二级专题
-            </el-button>
+  <div style="height: 100%">
+    <div v-show="!secondId" class="tab5">
+      <div class="insideTop">专题管理</div>
+      <div class="obstruct"></div>
+      <div class="content">
+        <div class="search">
+          <div class="btns">
+            <div class="left">
+              <el-button
+                v-for="tab in level1Data.filter((item) => item.level === 1)"
+                :type="Number(activeTopId) === tab.id ? 'primary' : ''"
+                v-bind:key="tab.id"
+                @click="handleLevelTopClick(tab.id)"
+                >{{ tab.name }}
+              </el-button>
+            </div>
+            <div class="right">
+              <el-button @click="level1Modal = true">一级专题管理</el-button>
+              <el-button
+                @click="
+                  (level2ModalFormVisible = true) &&
+                    handleAllReset() &&
+                    (level2ModalForm.id = activeTopId)
+                "
+              >
+                新增二级专题
+              </el-button>
+            </div>
           </div>
         </div>
 
-      </div>
-
-      <div class="table">
-        <el-table :data="tableData" style="width: 100%" height="529px">
-          <el-table-column prop="name" label="二级专题"></el-table-column>
-          <el-table-column prop="thumb" label="封面">
-            <template #default="{ row }">
-              <img class="thumb" :src="getImageURL(row.thumb)" alt=""/>
-            </template>
-
-          </el-table-column>
-          <el-table-column prop="sort" label="排序值"></el-table-column>
-          <el-table-column label="操作">
-            <!--            <template #default="{ row }">-->
-            <template #default="{ row }">
-              <el-button type="text">村落设置</el-button>
-              <el-button type="text" @click="handleLevel2Edit(row.id)">编辑</el-button>
-              <el-popconfirm
-                title="确定删除吗?"
-                @confirm="handleLevel1Delete(row.id)"
-              >
-                <el-button slot="reference" type="text" style="padding: 0 5px">删除</el-button>
-              </el-popconfirm>
-            </template>
-          </el-table-column>
-        </el-table>
-      </div>
-      <el-dialog
-        :visible.sync="level1Modal"
-        width="50%"
-        destroy-on-close
-        center>
-
         <div class="table">
-          <el-table :data="level1Data" style="width: 100%" min-height="450px">
-            <el-table-column prop="name" label="专题名称"></el-table-column>
-            <el-table-column prop="name" label="排序值"></el-table-column>
+          <el-table :data="tableData" style="width: 100%" height="100%">
+            <el-table-column prop="name" label="二级专题"></el-table-column>
+            <el-table-column prop="thumb" label="封面">
+              <template #default="{ row }">
+                <img class="thumb" :src="getImageURL(row.thumb)" alt="" />
+              </template>
+            </el-table-column>
+            <el-table-column prop="sort" label="排序值"></el-table-column>
             <el-table-column label="操作">
+              <!--            <template #default="{ row }">-->
               <template #default="{ row }">
-                <el-button type="text" @click="handleLevel1Edit(row.id)">编辑</el-button>
+                <el-button type="text" @click="handleVillageSettings(row.id)"
+                  >村落设置</el-button
+                >
+                <el-button type="text" @click="handleLevel2Edit(row.id)"
+                  >编辑</el-button
+                >
                 <el-popconfirm
                   title="确定删除吗?"
                   @confirm="handleLevel1Delete(row.id)"
                 >
-                  <el-button slot="reference" type="text" style="padding: 0 5px">删除</el-button>
+                  <el-button slot="reference" type="text" style="padding: 0 5px"
+                    >删除</el-button
+                  >
                 </el-popconfirm>
-
               </template>
             </el-table-column>
           </el-table>
         </div>
-        <div slot="title" class="lv1-modal-header">
-          <span>一级专题</span>
-          <el-button type="primary" @click="level1ModalFormVisible = true">新增</el-button>
-        </div>
-        <span slot="footer" class="dialog-footer">
+        <el-dialog
+          :visible.sync="level1Modal"
+          width="50%"
+          destroy-on-close
+          center
+        >
+          <div class="table">
+            <el-table :data="level1Data" style="width: 100%" min-height="450px">
+              <el-table-column prop="name" label="专题名称"></el-table-column>
+              <el-table-column prop="name" label="排序值"></el-table-column>
+              <el-table-column label="操作">
+                <template #default="{ row }">
+                  <el-button type="text" @click="handleLevel1Edit(row.id)"
+                    >编辑</el-button
+                  >
+                  <el-popconfirm
+                    title="确定删除吗?"
+                    @confirm="handleLevel1Delete(row.id)"
+                  >
+                    <el-button
+                      slot="reference"
+                      type="text"
+                      style="padding: 0 5px"
+                      >删除</el-button
+                    >
+                  </el-popconfirm>
+                </template>
+              </el-table-column>
+            </el-table>
+          </div>
+          <div slot="title" class="lv1-modal-header">
+            <span>一级专题</span>
+            <el-button type="primary" @click="level1ModalFormVisible = true"
+              >新增</el-button
+            >
+          </div>
+          <span slot="footer" class="dialog-footer">
             <el-button @click="level1Modal = false">取 消</el-button>
-            <el-button type="primary" @click="level1Modal = false">确 定</el-button>
-        </span>
-      </el-dialog>
+            <el-button type="primary" @click="level1Modal = false"
+              >确 定</el-button
+            >
+          </span>
+        </el-dialog>
 
-      <el-dialog
-        :visible.sync="level1ModalFormVisible"
-        destroy-on-close
-        width="30%"
-        center>
-        <div slot="title" class="lv1-modal-header">
-          <span>新增</span>
-        </div>
-        <el-form ref="level1Form" label-position="right" label-width="80px" :model="level1ModalForm"
-                 :rules="level1ModalFormRules">
-          <el-form-item label="专题名称" prop="name">
-            <el-input v-model="level1ModalForm.name"></el-input>
-          </el-form-item>
-          <el-form-item label="排序值" prop="sort">
-            <el-input-number controls-position="right" placeholder="请输入1~999的数字。数字越小,排序越靠前。"
-                             v-model="level1ModalForm.sort"></el-input-number>
-          </el-form-item>
-        </el-form>
-        <span slot="footer" class="dialog-footer">
+        <el-dialog
+          :visible.sync="level1ModalFormVisible"
+          destroy-on-close
+          width="30%"
+          center
+        >
+          <div slot="title" class="lv1-modal-header">
+            <span>新增</span>
+          </div>
+          <el-form
+            ref="level1Form"
+            label-position="right"
+            label-width="80px"
+            :model="level1ModalForm"
+            :rules="level1ModalFormRules"
+          >
+            <el-form-item label="专题名称" prop="name">
+              <el-input v-model="level1ModalForm.name"></el-input>
+            </el-form-item>
+            <el-form-item label="排序值" prop="sort">
+              <el-input-number
+                controls-position="right"
+                placeholder="请输入1~999的数字。数字越小,排序越靠前。"
+                v-model="level1ModalForm.sort"
+              ></el-input-number>
+            </el-form-item>
+          </el-form>
+          <span slot="footer" class="dialog-footer">
             <el-button @click="level1ModalFormVisible = false">取 消</el-button>
-            <el-button type="primary" @click="handleLevel1Submit">确 定</el-button>
-        </span>
-      </el-dialog>
+            <el-button type="primary" @click="handleLevel1Submit"
+              >确 定</el-button
+            >
+          </span>
+        </el-dialog>
 
-      <!--    二级专题管理  -->
-      <el-dialog
-        destroy-on-close
-        :visible.sync="level2ModalFormVisible"
-        width="30%"
-        center>
-        <div slot="title" class="lv1-modal-header">
-          <span>{{ level2ModalForm.id ? "编辑" : "新增" }}二级专题</span>
-        </div>
-        <el-form ref="level2Form" label-position="right" label-width="80px" :model="level2ModalForm"
-                 :rules="level1ModalFormRules">
-          <el-form-item label="专题名称" prop="name" >
-            <el-input v-model="level2ModalForm.name" placeholder="请输入内容,最多20字" :max-length="20"></el-input>
-          </el-form-item>
-          <el-form-item label="封面" prop="thumb" >
-            <el-upload
-              action=""
-              :limit="1"
-              :http-request="handleFileUpload"
-              list-type="picture-card"
-              accept=".jpeg,.jpg,.png,.png"
-              :file-list="level2ModalForm.filelist"
-              :on-remove="handleFileRemove">
-              <i class="el-icon-plus"></i>
-              <div slot="tip" class="el-upload__tip">格式要求:支持png、jpg图片格式;最大支持5M,最多1张</div>
-            </el-upload>
-          </el-form-item>
-          <el-form-item label="排序值" prop="sort">
-            <el-input-number controls-position="right" placeholder="请输入1~999的数字。数字越小,排序越靠前。"
-                             v-model="level2ModalForm.sort"></el-input-number>
-          </el-form-item>
-        </el-form>
-        <span slot="footer" class="dialog-footer">
+        <!--    二级专题管理  -->
+        <el-dialog
+          destroy-on-close
+          :visible.sync="level2ModalFormVisible"
+          width="30%"
+          center
+        >
+          <div slot="title" class="lv1-modal-header">
+            <span>{{ level2ModalForm.id ? "编辑" : "新增" }}二级专题</span>
+          </div>
+          <el-form
+            ref="level2Form"
+            label-position="right"
+            label-width="80px"
+            :model="level2ModalForm"
+            :rules="level1ModalFormRules"
+          >
+            <el-form-item label="专题名称" prop="name">
+              <el-input
+                v-model="level2ModalForm.name"
+                placeholder="请输入内容,最多20字"
+                :max-length="20"
+              ></el-input>
+            </el-form-item>
+            <el-form-item label="封面" prop="thumb">
+              <el-upload
+                action=""
+                :limit="1"
+                :http-request="handleFileUpload"
+                list-type="picture-card"
+                accept=".jpeg,.jpg,.png,.png"
+                :file-list="level2ModalForm.filelist"
+                :on-remove="handleFileRemove"
+              >
+                <i class="el-icon-plus"></i>
+                <div slot="tip" class="el-upload__tip">
+                  格式要求:支持png、jpg图片格式;最大支持5M,最多1张
+                </div>
+              </el-upload>
+            </el-form-item>
+            <el-form-item label="排序值" prop="sort">
+              <el-input-number
+                controls-position="right"
+                placeholder="请输入1~999的数字。数字越小,排序越靠前。"
+                v-model="level2ModalForm.sort"
+              ></el-input-number>
+            </el-form-item>
+          </el-form>
+          <span slot="footer" class="dialog-footer">
             <el-button @click="level2ModalFormVisible = false">取 消</el-button>
-            <el-button type="primary" @click="handleLevel2Submit">确 定</el-button>
-        </span>
-      </el-dialog>
+            <el-button type="primary" @click="handleLevel2Submit"
+              >确 定</el-button
+            >
+          </span>
+        </el-dialog>
+      </div>
+    </div>
+
+    <div v-if="secondId" class="tab5">
+      <div class="insideTop">
+        <span class="back" @click="secondId = null">
+          <i class="el-icon-arrow-left"></i>返回 </span
+        >村落设置
+      </div>
+      <div class="obstruct"></div>
+      <div class="content">
+        <div class="search">
+          <div class="btns">
+            <div class="left"></div>
+            <div class="right">
+              <el-button type="primary" @click="editVillage('add')"
+                >新增</el-button
+              >
+            </div>
+          </div>
+        </div>
+        <div class="table">
+          <el-table :data="villageData" style="width: 100%" height="100%">
+            <el-table-column prop="name" label="村落名称"></el-table-column>
+            <el-table-column prop="thumb" label="封面">
+              <template #default="{ row }">
+                <img class="thumb" :src="getImageURL(row.thumb)" alt="" />
+              </template>
+            </el-table-column>
+            <el-table-column prop="remark" label="简介"></el-table-column>
+            <el-table-column prop="link" label="跳转链接"></el-table-column>
+            <el-table-column label="操作">
+              <!--            <template #default="{ row }">-->
+              <template #default="{ row }">
+                <!-- <el-button type="text" @click="handleVillageSettings(row.id)"
+                  >村落设置</el-button
+                >
+                <el-button type="text" @click="handleLevel2Edit(row.id)"
+                  >编辑</el-button
+                > -->
+                <el-popconfirm
+                  title="确定删除吗?"
+                  @confirm="handleVillageDelete(row.id)"
+                >
+                  <el-button slot="reference" type="text" style="padding: 0 5px"
+                    >删除</el-button
+                  >
+                </el-popconfirm>
+              </template>
+            </el-table-column>
+          </el-table>
+        </div>
 
+        <el-dialog
+          v-if="showVillageForm"
+          :visible.sync="showVillageForm"
+          destroy-on-close
+          width="30%"
+          center
+        >
+          <div slot="title" class="lv1-modal-header">
+            <span>新增</span>
+          </div>
+          <el-form
+            ref="villageForm"
+            label-position="right"
+            label-width="80px"
+            :model="villageForm"
+            :rules="villageFormRules"
+          >
+            <el-form-item label="村落名称" prop="name">
+              <el-input v-model="villageForm.name"></el-input>
+            </el-form-item>
+            <el-form-item label="封面" prop="thumb">
+              <el-upload
+                action=""
+                :limit="1"
+                :http-request="handleVillageFileUpload"
+                list-type="picture-card"
+                accept=".jpeg,.jpg,.png,.png"
+                :file-list="villageForm.filelist"
+                :on-remove="handleVillageFileRemove"
+              >
+                <i class="el-icon-plus"></i>
+                <div slot="tip" class="el-upload__tip">
+                  格式要求:支持png、jpg图片格式;最大支持5M,最多1张
+                </div>
+              </el-upload>
+            </el-form-item>
+            <el-form-item label="简介" prop="remark">
+              <el-input type="textarea" v-model="villageForm.remark"></el-input>
+            </el-form-item>
+            <el-form-item label="跳转链接" prop="link">
+              <el-input v-model="villageForm.link"></el-input>
+            </el-form-item>
+            <el-form-item label="排序值" prop="sort">
+              <el-input-number
+                controls-position="right"
+                placeholder="请输入1~999的数字。数字越小,排序越靠前。"
+                v-model="villageForm.sort"
+              ></el-input-number>
+            </el-form-item>
+          </el-form>
+          <span slot="footer" class="dialog-footer">
+            <el-button @click="showVillageForm = false">取 消</el-button>
+            <el-button type="primary" @click="handleVillageSubmit"
+              >确 定</el-button
+            >
+          </span>
+        </el-dialog>
+      </div>
     </div>
   </div>
 </template>
 
 <script>
-import { getTopList, getSecondList, addEntity, deleteEntity, getEntity, fileUpload } from '@/apis/tab5'
+import {
+  getTopList,
+  getSecondList,
+  addEntity,
+  deleteEntity,
+  getEntity,
+  fileUpload
+} from '@/apis/tab5'
 
 export default {
   name: 'tab5',
@@ -156,37 +326,43 @@ export default {
   data () {
     // 这里存放数据
     return {
+      secondId: null,
       activeTopId: '1',
       name: '',
+      villageData: [],
       tableData: [],
       level1Data: [],
       level1Modal: false,
       level1ModalFormVisible: false,
       level1ModalForm: {},
+      villageForm: null,
+      showVillageForm: false,
       level1ModalFormRules: {
-        name: [
-          { required: true, message: '请输入名称', trigger: 'change' }
-        ],
-        sort: [
-          { required: true, message: '请输入排序值', trigger: 'change' }
-        ],
-        thumb: [
-          { required: true, message: '请输入封面图' }
-        ]
-
+        name: [{ required: true, message: '请输入名称', trigger: 'change' }],
+        sort: [{ required: true, message: '请输入排序值', trigger: 'change' }],
+        thumb: [{ required: true, message: '请上传封面图' }]
+      },
+      villageFormRules: {
+        name: [{ required: true, message: '请输入名称', trigger: 'change' }],
+        remark: [{ required: true, message: '请输入简介', trigger: 'change' }],
+        link: [{ required: true, message: '请输入链接', trigger: 'change' }],
+        sort: [{ required: true, message: '请输入排序值', trigger: 'change' }],
+        thumb: [{ required: true, message: '请上传封面图' }]
       },
       level2ModalFormVisible: false,
       level2ModalForm: {
         filelist: []
       }
-
     }
   },
   // 监听属性 类似于data概念
   computed: {
     getImageURL: (url) => {
       return (url) => {
-        const domain = process.env.NODE_ENV === 'development' ? 'http://project.4dage.com:8016' : ''
+        const domain =
+          process.env.NODE_ENV === 'development'
+            ? 'http://project.4dage.com:8016'
+            : ''
         return domain + url
       }
     }
@@ -195,8 +371,62 @@ export default {
   watch: {},
   // 方法集合
   methods: {
-    handleClick () {
+    editVillage (add = false) {
+      if (add) {
+        this.villageForm = {
+          name: '',
+          thumb: '',
+          remark: '',
+          link: ''
+        }
+        setTimeout(() => {
+          this.showVillageForm = true
+        }, 100)
+      }
     },
+    async handleVillageSettings (id) {
+      this.secondId = id
+      this.villageData = []
+      this.getVillageList()
+    },
+    async getVillageList () {
+      const list = await getSecondList(this.secondId)
+      this.villageData = list.data
+    },
+    handleVillageSubmit () {
+      this.$refs.villageForm.validate(async (valid) => {
+        if (valid) {
+          const params = {
+            parentId: this.secondId,
+            level: 3,
+            ...this.villageForm
+          }
+
+          const res = await addEntity(params)
+          console.error(res)
+          if (res.code === 0) {
+            this.$message({
+              message: res.msg,
+              type: 'success'
+            })
+            this.villageForm = null
+            this.showVillageForm = false
+            await this.getVillageList()
+          } else {
+            this.$message({
+              message: res.msg,
+              type: 'error'
+            })
+            // this.villageForm = null
+            // this.showVillageForm = false
+          }
+        } else {
+          console.error('error submit!!')
+          return false
+        }
+      })
+    },
+    handleClick () {},
     handleLevelTopClick (id) {
       this.activeTopId = id
       this.handleRefreshList()
@@ -214,6 +444,7 @@ export default {
       // console.log('handleRefreshList', list)
       this.tableData = list.data
     },
+
     handleLevel1Submit () {
       this.$refs.level1Form.validate(async (valid) => {
         if (valid) {
@@ -260,6 +491,21 @@ export default {
       }
       await this.handleRefreshLevel1List()
     },
+    async handleVillageDelete (id) {
+      const res = await deleteEntity(id)
+      if (res.code === 0) {
+        this.$message({
+          message: '删除成功!',
+          type: 'success'
+        })
+      } else {
+        this.$message({
+          message: res.msg,
+          type: 'error'
+        })
+      }
+      await this.getVillageList()
+    },
     async handleLevel1Edit (id) {
       this.level1ModalFormVisible = true
       const res = await getEntity(id)
@@ -270,8 +516,7 @@ export default {
       }
       console.log('res', res)
     },
-    handleFileRemove () {
-    },
+    handleFileRemove () {},
     async handleFileUpload ({ file }) {
       const res = await fileUpload({
         file: file,
@@ -280,6 +525,17 @@ export default {
       console.log('res', res.data.filePath)
       this.level2ModalForm.thumb = res.data.filePath
     },
+    handleVillageFileRemove () {
+      this.villageForm.thumb = ''
+    },
+    async handleVillageFileUpload ({ file }) {
+      const res = await fileUpload({
+        file: file,
+        type: 'thumb'
+      })
+      console.log('res', res.data.filePath)
+      this.villageForm.thumb = res.data.filePath
+    },
     handleLevel2Submit () {
       this.$refs.level2Form.validate(async (valid) => {
         if (valid) {
@@ -314,11 +570,15 @@ export default {
         }
       })
     },
+
     async handleLevel2Edit (id) {
       console.log(id)
       this.level2ModalFormVisible = true
       const res = await getEntity(id)
-      const domain = process.env.NODE_ENV === 'development' ? 'http://project.4dage.com:8016' : ''
+      const domain =
+        process.env.NODE_ENV === 'development'
+          ? 'http://project.4dage.com:8016'
+          : ''
 
       this.level2ModalForm = {
         id: id,
@@ -352,35 +612,28 @@ export default {
   async mounted () {
     await this.handleRefreshLevel1List()
   },
-  beforeCreate () {
-  }, // 生命周期 - 创建之前
-  beforeMount () {
-  }, // 生命周期 - 挂载之前
-  beforeUpdate () {
-  }, // 生命周期 - 更新之前
-  updated () {
-  }, // 生命周期 - 更新之后
-  beforeDestroy () {
-  }, // 生命周期 - 销毁之前
-  destroyed () {
-  }, // 生命周期 - 销毁完成
-  activated () {
-  } // 如果页面有keep-alive缓存功能,这个函数会触发
+  beforeCreate () {}, // 生命周期 - 创建之前
+  beforeMount () {}, // 生命周期 - 挂载之前
+  beforeUpdate () {}, // 生命周期 - 更新之前
+  updated () {}, // 生命周期 - 更新之后
+  beforeDestroy () {}, // 生命周期 - 销毁之前
+  destroyed () {}, // 生命周期 - 销毁完成
+  activated () {} // 如果页面有keep-alive缓存功能,这个函数会触发
 }
 </script>
-<style lang='less' scoped>
+<style lang="less" scoped>
 .tab5 {
   width: 100%;
   height: 100%;
 
   .content {
     padding: 0 20px;
-
+    height: calc(100% - 52px);
     .search {
       height: 70px;
       display: flex;
       align-items: center;
-      border-bottom: 1px solid black;
+      // border-bottom: 1px solid black;
 
       .btns {
         display: flex;
@@ -400,6 +653,11 @@ export default {
     }
 
     .table {
+      height: calc(100% - 70px);
+
+      /deep/ .el-table {
+        overflow-y: auto;
+      }
       /deep/ .el-table .cell {
         padding: 3px 0;
       }
@@ -430,8 +688,19 @@ export default {
   }
 
   .thumb {
-    max-height: 100px;
-    width: auto;
+    // max-height: 100px;
+    // width: auto;
+
+    width: 100px; /* 最大宽度 */
+    height: 100px; /* 最大高度 */
+    object-fit: contain;
+    // width: auto; /* 宽度自适应 */
+    // height: auto; /* 高度自适应 */
+    display: block; /* 避免底部空白 */
   }
 }
+.back {
+  margin-right: 10px;
+  cursor: pointer;
+}
 </style>

Різницю між файлами не показано, бо вона завелика
+ 4537 - 2240
packages/mobile/package-lock.json


+ 3 - 0
packages/mobile/package.json

@@ -9,7 +9,10 @@
   "dependencies": {
     "axios": "^0.27.2",
     "core-js": "^3.6.5",
+    "lib-flexible": "^0.3.2",
     "mars3d": "^3.4.1",
+    "swiper": "^4.5.1",
+    "element-ui": "^2.15.8",
     "v-viewer": "^1.5.1",
     "vue": "^2.6.11",
     "vue-lazyload": "^1.3.3",

+ 2 - 1
packages/mobile/public/index.html

@@ -3,7 +3,8 @@
   <head>
     <meta charset="utf-8" />
     <meta http-equiv="X-UA-Compatible" content="IE=edge" />
-    <meta name="viewport" content="width=device-width,initial-scale=1.0" />
+    <meta name="viewport" content="width=device-width,initial-scale=1.0,user-scalable=no" />
+    
     <link rel="icon" href="<%= BASE_URL %>favicon.ico" />
     <!--引入cesium基础lib-->
     <link

BIN
packages/mobile/src/assets/img/frame.png


Різницю між файлами не показано, бо вона завелика
+ 12 - 0
packages/mobile/src/assets/img/frame.svg


BIN
packages/mobile/src/assets/img/icon_Service.png


Різницю між файлами не показано, бо вона завелика
+ 7 - 0
packages/mobile/src/assets/img/icon_Service.svg


BIN
packages/mobile/src/assets/img/icon_more.png


Різницю між файлами не показано, бо вона завелика
+ 5 - 0
packages/mobile/src/assets/img/icon_more.svg


BIN
packages/mobile/src/assets/img/icon_q&a.png


Різницю між файлами не показано, бо вона завелика
+ 32 - 0
packages/mobile/src/assets/img/icon_q&a.svg


BIN
packages/mobile/src/assets/img/icon_special topic.png


Різницю між файлами не показано, бо вона завелика
+ 32 - 0
packages/mobile/src/assets/img/icon_special topic.svg


BIN
packages/mobile/src/assets/img/icon_switch.png


Різницю між файлами не показано, бо вона завелика
+ 5 - 0
packages/mobile/src/assets/img/icon_switch.svg


BIN
packages/mobile/src/assets/img/icon_talk.png


Різницю між файлами не показано, бо вона завелика
+ 10 - 0
packages/mobile/src/assets/img/icon_talk.svg


BIN
packages/mobile/src/assets/img/icon_user.png


Різницю між файлами не показано, бо вона завелика
+ 5 - 0
packages/mobile/src/assets/img/icon_user.svg


BIN
packages/mobile/src/assets/img/img.png


Різницю між файлами не показано, бо вона завелика
+ 11 - 0
packages/mobile/src/assets/img/img.svg


BIN
packages/mobile/src/assets/img/lable.png


Різницю між файлами не показано, бо вона завелика
+ 3 - 0
packages/mobile/src/assets/img/lable.svg


+ 21 - 14
packages/mobile/src/main.js

@@ -1,18 +1,25 @@
-import Vue from 'vue'
-import App from './App.vue'
-import router from './router'
+import Vue from "vue";
+import App from "./App.vue";
+import router from "./router";
+import "lib-flexible/flexible";
+import ElementUI from "element-ui";
+import "element-ui/lib/theme-chalk/index.css";
+import "swiper/dist/css/swiper.css";
+import { Message } from "element-ui";
 // 图片懒加载
-import VueLazyLoad from 'vue-lazyload'
+import VueLazyLoad from "vue-lazyload";
 Vue.use(VueLazyLoad, {
-  error: require('@/assets/img/IMGerror.png'),
-  loading: require('@/assets/img/loading2.gif')
-})
-import 'viewerjs/dist/viewer.css'
-import Viewer from 'v-viewer'
-Vue.use(Viewer)
-Vue.config.productionTip = false
-import './assets/base.css'
+  error: require("@/assets/img/IMGerror.png"),
+  loading: require("@/assets/img/loading2.gif"),
+});
+import "viewerjs/dist/viewer.css";
+import Viewer from "v-viewer";
+Vue.use(ElementUI);
+Vue.use(Viewer);
+Vue.config.productionTip = false;
+Vue.prototype.$message = Message;
+import "./assets/base.css";
 new Vue({
   router,
-  render: h => h(App)
-}).$mount('#app')
+  render: (h) => h(App),
+}).$mount("#app");

+ 84 - 31
packages/mobile/src/utils/api.js

@@ -1,70 +1,123 @@
-import axios from './request'
+import axios from "./request";
 // 村庄保存点赞
 export const likeSaveApi = (villageId) => {
   return axios({
-    method: 'get',
+    method: "get",
     url: `/web/village/addStar/${villageId}`,
-  })
-}
+  });
+};
 // 村庄浏览量
 export const lookSaveApi = (villageId) => {
   return axios({
-    method: 'get',
+    method: "get",
     url: `/web/village/addVisit/${villageId}`,
-  })
-}
+  });
+};
 // 获取浏览量和点赞量
 export const getCunNumApi = (villageId) => {
   return axios({
-    method: 'get',
+    method: "get",
     url: `/web/village/getDetail/${villageId}`,
-  })
-}
+  });
+};
 
 // 获取菜单树
 export const getTreeMenuApi = () => {
   return axios({
-    method: 'get',
-    url: '/web/getTreeMenu',
-  })
-}
+    method: "get",
+    url: "/web/getTreeMenu",
+  });
+};
 
 // 获取内容列表
 export const getInfoApi = (villageId) => {
   return axios({
-    method: 'get',
+    method: "get",
     url: `/web/content/list/${villageId}`,
-  })
-}
+  });
+};
 
 // 村庄-统计-5分钟更新一次
 export const getStatistics = (villageId) => {
   return axios({
-    method: 'get',
-    url: '/web/village/getStatistics',
-  })
-}
+    method: "get",
+    url: "/web/village/getStatistics",
+  });
+};
 
 // 村庄-保存分享量
 export const addShareApi = (villageId) => {
   return axios({
-    method: 'get',
+    method: "get",
     url: `/web/village/addShare/${villageId}`,
-  })
-}
+  });
+};
 
 // 获取访问量
 export const sceneGetList = () => {
   return axios({
-    method: 'get',
-    url: '/web/scene/getList',
-  })
-}
+    method: "get",
+    url: "/web/scene/getList",
+  });
+};
 
 // 添加访问量
 export const addVisit = (sceneCode) => {
   return axios({
-    method: 'get',
+    method: "get",
     url: `/web/scene/addVisit/${sceneCode}`,
-  })
-}
+  });
+};
+
+export const getTopicList = () => {
+  return axios({
+    method: "get",
+    url: `/web/subject/getList`,
+  });
+};
+export const getTopicSecondaryList = (parentId) => {
+  return axios({
+    method: "get",
+    url: `/web/subject/getSon/${parentId}`,
+  });
+};
+
+export const getTopicDetail = (id) => {
+  return axios({
+    method: "get",
+    url: `/web/subject/detail/${id}`,
+  });
+};
+
+export const getMessageKeywords = (type = null) => {
+  return axios({
+    method: "get",
+    url: `/web/dict/getList?type=${type}`,
+  });
+};
+export const getAskList = (id = null) => {
+  return axios({
+    method: "get",
+    url: id ? `/web/ask/getList?dictId=${id}` : `/web/ask/getList`,
+  });
+};
+export const getSnCode = (id = null) => {
+  return axios({
+    method: "get",
+    url: `/web/getRandCode`,
+    responseType: "blob",
+  });
+};
+export const sendMsg = (param) => {
+  return axios({
+    method: "post",
+    url: `/web/message/save`,
+    data: param,
+  });
+};
+export const getAskDetails = (id) => {
+  return axios({
+    method: "get",
+    url: `/web/ask/detail/${id}`,
+  });
+};

+ 18 - 0
packages/mobile/src/utils/index.js

@@ -0,0 +1,18 @@
+export const getDate = () => {
+  // 补零
+  let addZero = (t) => {
+      return t < 10 ? '0' + t : t;
+  }
+  let time = new Date();
+  let Y = time.getFullYear(), // 年
+      M = time.getMonth() + 1, // 月
+      D = time.getDate(), // 日
+      h = time.getHours(), // 时
+      m = time.getMinutes(), // 分
+      s = time.getSeconds(); // 秒
+  if (M > 12) {
+      // 注: new Date()的年月日的拼接,在月份为12月时,会出现 获取月份+1 后,为13月的bug,需要特殊处理。用moment第三方插件获取时间也可避免此问题。
+      M = M - 12;
+  }
+  return `${Y}-${addZero(M)}-${addZero(D)} ${addZero(h)}:${addZero(m)}:${addZero(s)}`;
+}

+ 34 - 39
packages/mobile/src/views/Home.vue

@@ -14,12 +14,7 @@
     <!-- 顶部 -->
     <div class="top" :class="{ topOpen: cutArrows }">
       <!-- 箭头 -->
-      <img
-        class="arrowsImg"
-        @click="cutArrows = !cutArrows"
-        src="../assets/img/arrows.png"
-        alt=""
-      />
+      <img class="arrowsImg" @click="cutArrows = !cutArrows" src="../assets/img/arrows.png" alt="" />
       <div class="arrows" @click="cutArrows = !cutArrows">
         <div class="arrowsLeft"></div>
         <div class="arrowsRight"></div>
@@ -44,12 +39,7 @@
         </div>
         <div class="browse browse2">详情统计</div>
         <div class="detailsNum">
-          <div
-            class="row"
-            v-for="item in numData"
-            :key="item.id"
-            @click="toCun(item.id)"
-          >
+          <div class="row" v-for="item in numData" :key="item.id" @click="toCun(item.id)">
             <div class="rowLeft">{{ item.name }}</div>
             <div class="rowRight">
               <div class="plan">
@@ -66,14 +56,23 @@
 
     <!-- 加载中 -->
     <div class="homeLoading" :class="{ homeLoadingNone: isLoding }">
-      <img src="../assets/img/loading.gif" alt="">
+      <img src="../assets/img/loading.gif" alt="" />
     </div>
+
+    <div class="btn-list">
+      <div @click="showTopic = true" class="btn topic-btn"><img src="../assets/img/icon_special topic.svg" alt="" /></div>
+      <div @click="showQA = true" class="btn qa-btn"><img src="../assets/img/icon_q&a.svg" alt="" /></div>
+    </div>
+    <Topic v-if="showTopic" @close="showTopic = false" />
+    <QA v-if="showQA" @close="showQA = false" />
   </div>
 </template>
 
 <script>
 import { getStatistics, lookSaveApi } from "../utils/api";
 import Search from "./Search.vue";
+import Topic from "./topic";
+import QA from "./qa";
 // import "mars3d/dist/mars3d.css";
 // import * as mars3d from "mars3d";
 // mapIns存储初始化的地图map实例,如果map实例放在vue的data中会导致帧率下降严重
@@ -82,10 +81,12 @@ var zoneLayer = null;
 var graphicLayer = null;
 
 export default {
-  components: { Search },
+  components: { Search, Topic, QA },
   data() {
     //这里存放数据
     return {
+      showTopic: false,
+      showQA: false,
       // 数据加载中
       isLoding: false,
 
@@ -272,11 +273,7 @@ export default {
       const acInfo = this.villagePos.find((v) => v.id === id) || {};
 
       const graphic = new mars3d.graphic.DivGraphic({
-        position: new mars3d.LngLatPoint(
-          acInfo.position[0],
-          acInfo.position[1],
-          100
-        ),
+        position: new mars3d.LngLatPoint(acInfo.position[0], acInfo.position[1], 100),
         style: {
           html: `<div class="lableContent" _id=${id} >
   <div class="lableIconArea">
@@ -288,12 +285,7 @@ export default {
           horizontalOrigin: mars3d.Cesium.HorizontalOrigin.LEFT,
           verticalOrigin: mars3d.Cesium.VerticalOrigin.BOTTOM,
           // distanceDisplayCondition: new mars3d.Cesium.DistanceDisplayCondition(0, 30000), // 按视距距离显示
-          scaleByDistance: new mars3d.Cesium.NearFarScalar(
-            1000,
-            1.5,
-            12000,
-            0.8
-          ),
+          scaleByDistance: new mars3d.Cesium.NearFarScalar(1000, 1.5, 12000, 0.8),
           clampToGround: true,
         },
         attr: {
@@ -328,12 +320,9 @@ export default {
       this.map.addLayer(this.tilesetLayer);
 
       // 加载的事件 只执行一次
-      this.tilesetLayer.on(
-        mars3d.EventType.initialTilesLoaded,
-        function (event) {
-          console.log("触发initialTilesLoaded事件", event);
-        }
-      );
+      this.tilesetLayer.on(mars3d.EventType.initialTilesLoaded, function (event) {
+        console.log("触发initialTilesLoaded事件", event);
+      });
     },
     // 加载区域边界
     loadZoneLayer(_map, _type, _color, _repeat) {
@@ -436,11 +425,7 @@ export default {
   //生命周期 - 创建完成(可以访问当前this实例)
   async created() {
     // 移动端和pc端的切换
-    if (
-      window.navigator.userAgent.match(
-        /(phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone)/i
-      )
-    ) {
+    if (window.navigator.userAgent.match(/(phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone)/i)) {
       // 移动端
     } else {
       // PC端
@@ -505,7 +490,17 @@ export default {
   margin: 0 auto;
   position: relative;
   overflow: hidden;
-
+  .btn-list {
+    position: absolute;
+    bottom: 0.6133rem;
+    right: 0;
+    .btn {
+      img {
+        width: 2.4rem;
+        height: 2.4rem;
+      }
+    }
+  }
   .homeLoading {
     position: absolute;
     top: 0;
@@ -518,7 +513,7 @@ export default {
     display: flex;
     justify-content: center;
     align-items: center;
-    img{
+    img {
       width: 40px;
     }
   }
@@ -549,7 +544,7 @@ export default {
     position: absolute;
     top: 0;
     left: 0;
-    z-index: 9999;
+    z-index: 99;
     width: 100%;
     height: 70px;
     overflow: hidden;

+ 633 - 0
packages/mobile/src/views/qa/index.vue

@@ -0,0 +1,633 @@
+<!--  -->
+<template>
+  <div class="qa-layout">
+    <div class="qa-header">
+      <div class="back-btn" @click="onBack">
+        <i class="el-icon-arrow-left"></i>
+      </div>
+      <p>问答咨询</p>
+    </div>
+    <div class="qa-content">
+      <div class="msg-box">
+        <div class="control-box">
+          <div class="info-box" ref="scrollRef">
+            <div class="qa-list">
+              <div class="qaList-header">
+                <div class="left"><i></i><span>猜你想问</span></div>
+                <div class="right"><span>换一批</span><i @click="changeWord"></i></div>
+              </div>
+
+              <div ref="mySwiper" class="swiper-container-word key-word">
+                <div class="word-list swiper-wrapper">
+                  <div class="swiper-slide" v-for="(item, index) in words" v-if="index < 5">
+                    <div class="word-item" @click="handleWord(item)" :class="{ active: wordId == item.id }">{{ item.name }}</div>
+                  </div>
+                </div>
+              </div>
+              <div class="question-list">
+                <div class="question-item" @click="sendMessage(i)" v-for="(i, index) in questions" v-if="index < 5">{{ i.question }}</div>
+              </div>
+            </div>
+            <div v-for="i in messageList">
+              <div class="send-box" v-if="i.type == 1">
+                <div class="message-box">
+                  <p class="text">{{ i.text }}</p>
+                  <span class="time">{{ i.time }}</span>
+                </div>
+                <div class="avatar"></div>
+              </div>
+              <div class="reply-box" v-if="i.type == 2">
+                <div class="avatar"></div>
+                <div class="message-box">
+                  <p class="text">{{ i.text }}</p>
+                  <span class="time">{{ i.time }}</span>
+                </div>
+              </div>
+            </div>
+          </div>
+          <!-- <div class="tips">每天最多可咨询10个问题</div> -->
+          <div class="input-box">
+            <div class="input-content">
+              <div class="message-top">
+                <el-input type="textarea" :rows="2" maxLength="200" placeholder="请输入您想咨询的问题" v-model="text"> </el-input>
+              </div>
+              <div class="message-bottom">
+                <div class="top">
+                  <el-input type="input" maxLength="20" placeholder="请输入您的联系方式,不超过20字" v-model="phoneNum"> </el-input>
+                </div>
+                <div class="bottom">
+                  <div class="bottom-box">
+                    <el-input class="code-ipt" type="input" placeholder="请输入验证码" v-model="snCode"> </el-input>
+                    <div class="sn-code" @click.stop="getCode()">
+                      <img v-if="identifyCode" :src="identifyCode" alt="" />
+                    </div>
+                  </div>
+
+                  <div class="send-btn" :class="{ disabled }" @click="sendMessage()">发送</div>
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import Swiper from "swiper";
+import { getDate } from "@/utils";
+import { getMessageKeywords, getAskList, getSnCode, sendMsg, getAskDetails } from "@/utils/api";
+export default {
+  //import引入的组件需要注入到对象中才能使用
+  components: {},
+  data() {
+    //这里存放数据
+    return {
+      words: [],
+      questions: [],
+      swiperOptions: {
+        slidesPerView: "auto",
+        observer: true, //修改swiper自己或子元素时,自动初始化swiper
+        observeParents: true, //修改swiper的父元素时,自动初始化swiper
+      },
+      swiperInstance: null,
+      text: "",
+      phoneNum: "",
+      snCode: "",
+      identifyCode: "",
+      src: "",
+      showSnCode: false,
+      wordId: null,
+      messageList: [],
+      disabled: false,
+    };
+  },
+  //监听属性 类似于data概念
+  computed: {},
+  emits: ["close"],
+  //监控data中的数据变化
+  watch: {},
+  //方法集合
+  methods: {
+    handleWord(i) {
+      if (i.id == this.wordId) retrun;
+      this.wordId = i.id;
+      this.getQuestionList();
+    },
+    async getWordList() {
+      let res = await getMessageKeywords("ask");
+
+      this.words = res.data;
+      this.initSwiper();
+      this.getQuestionList();
+    },
+    async getQuestionList() {
+      let res = await getAskList(this.wordId);
+      this.questions = res.data;
+    },
+    async getCode() {
+      this.showSnCode = false;
+      let res = await getSnCode();
+      this.identifyCode = URL.createObjectURL(res);
+      this.showSnCode = true;
+    },
+    changeWord() {
+      this.wordId = null;
+      this.getWordList();
+    },
+    onBack() {
+      this.$emit("close");
+    },
+
+    async sendMessage(data) {
+      let msg, res;
+      if (data) {
+        res = await getAskDetails(data.id);
+        if (res.code == 0) {
+          msg = { type: 1, text: data.question, time: getDate() };
+          this.addMessage(msg);
+          setTimeout(() => {
+            msg = { type: 2, text: res.data.answer, time: res.timestamp };
+            this.addMessage(msg);
+          }, 0);
+        } else {
+          this.$message({
+            message: res.msg,
+            type: "error",
+          });
+        }
+      } else {
+        this.disabled = true;
+        if (!this.text) {
+          this.disabled = false;
+          return this.$message({
+            message: "请输入问题",
+            type: "error",
+          });
+        }
+        if (!this.phoneNum) {
+          this.disabled = false;
+          return this.$message({
+            message: "请输入联系方式",
+            type: "error",
+          });
+        }
+        if (!this.snCode) {
+          this.disabled = false;
+          return this.$message({
+            message: "请输入验证码",
+            type: "error",
+          });
+        }
+
+        let params = { contactWay: this.phoneNum, question: this.text, randCode: this.snCode };
+        res = await sendMsg(params);
+        if (res.code == 0) {
+          msg = { type: 1, text: this.text, time: getDate() };
+          this.addMessage(msg);
+          setTimeout(() => {
+            msg = { type: 2, text: res.data, time: res.timestamp };
+            this.addMessage(msg);
+            this.getCode();
+            this.clearInput();
+          }, 0);
+        } else {
+          // this.getCode();
+          // this.clearInput();
+          this.$message({
+            message: res.msg,
+            type: "error",
+          });
+        }
+      }
+      this.disabled = false;
+    },
+    autoScroll() {
+      setTimeout(() => {
+        this.$refs.scrollRef.scrollTo({
+          top: 999999,
+          behavior: "smooth",
+        });
+      }, 0);
+    },
+    addMessage(item) {
+      this.messageList.push(item);
+      this.autoScroll();
+    },
+    clearInput() {
+      this.text = "";
+      (this.phoneNum = ""), (this.snCode = "");
+    },
+    initSwiper() {
+      setTimeout(() => {
+        if (!this.swiperInstance) {
+          this.swiperInstance = new Swiper(".swiper-container-word", this.swiperOptions);
+        } else {
+          this.swiperInstance.update();
+        }
+      }, 0);
+    },
+  },
+  //生命周期 - 创建完成(可以访问当前this实例)
+  created() {},
+  //生命周期 - 挂载完成(可以访问DOM元素)
+  mounted() {
+    this.getWordList();
+    this.getCode();
+  },
+  beforeCreate() {}, //生命周期 - 创建之前
+  beforeMount() {}, //生命周期 - 挂载之前
+  beforeUpdate() {}, //生命周期 - 更新之前
+  updated() {}, //生命周期 - 更新之后
+  beforeDestroy() {}, //生命周期 - 销毁之前
+  destroyed() {}, //生命周期 - 销毁完成
+  activated() {}, //如果页面有keep-alive缓存功能,这个函数会触发
+};
+</script>
+<style lang="less" scoped>
+//@import url(); 引入公共css类
+.qa-layout {
+  position: fixed;
+  width: 100%;
+  height: 100%;
+  max-width: 500px;
+  top: 0;
+  left: 50%;
+  transform: translateX(-50%);
+  background: rgba(204, 191, 171, 1);
+  overflow: hidden;
+  z-index: 100;
+  padding: 0 0.2667rem;
+  pointer-events: none;
+  .qa-header {
+    width: 100%;
+    height: 1.2533rem;
+    border-bottom: 1px solid #fff;
+    pointer-events: auto;
+    font-size: 0.3733rem;
+    color: #fff;
+    display: flex;
+    align-items: center;
+    justify-content: flex-start;
+
+    .back-btn {
+      width: 40px;
+      height: 100%;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      position: relative;
+      z-index: 2;
+      i {
+        font-size: 0.4267rem;
+        font-weight: bold;
+      }
+    }
+  }
+  .qa-content {
+    flex: 1;
+    width: 100%;
+    height: calc(100vh - 1.2533rem);
+    display: flex;
+    align-items: center;
+    justify-content: flex-start;
+    pointer-events: auto;
+    .msg-box {
+      width: 100%;
+      height: 100%;
+      // padding: 10px;
+      position: relative;
+
+      .control-box {
+        width: 100%;
+        height: 100%;
+        z-index: 2;
+        position: relative;
+        // padding: 30px 50px 20px 60px;
+        display: flex;
+        align-items: flex-start;
+        justify-content: flex-start;
+        flex-direction: column;
+        .info-box {
+          height: 100%;
+          flex: 1;
+          width: 100%;
+          overflow-y: scroll;
+          padding: 0.2667rem;
+          -webkit-overflow-scrolling: touch;
+
+          &::-webkit-scrollbar {
+            width: 7px;
+          }
+          &::-webkit-scrollbar-thumb {
+            border-radius: 5px;
+            background: rgba(217, 200, 169, 0.5);
+          }
+          &::-webkit-scrollbar-track {
+            border-radius: 0;
+            background: transparent;
+          }
+          .qa-list {
+            width: 6.9333rem;
+            background: #fff;
+            padding: 0.2667rem;
+            border-radius: 4px 4px 4px 0;
+            .qaList-header {
+              width: 100%;
+              display: flex;
+              align-items: center;
+              justify-content: space-between;
+              border-bottom: 1px solid rgba(243, 243, 243, 1);
+              padding-bottom: 0.2667rem;
+              .left {
+                display: flex;
+                align-items: center;
+                justify-content: space-between;
+                i {
+                  width: 0.5333rem;
+                  height: 0.5333rem;
+                  background: url(../../assets/img/icon_talk.svg) no-repeat;
+                  background-size: 100%;
+                  margin-right: 10px;
+                }
+                span {
+                  font-size: 0.3733rem;
+                  font-weight: bold;
+                  color: rgba(197, 172, 135, 1);
+                }
+              }
+              .right {
+                display: flex;
+                align-items: center;
+                justify-content: space-between;
+                i {
+                  width: 0.4rem;
+                  height: 0.4rem;
+                  background: url(../../assets/img/icon_switch.svg) no-repeat;
+                  background-size: 100%;
+                  margin-left: 0.24rem;
+                  cursor: pointer;
+                }
+                span {
+                  font-size: 0.32rem;
+                  color: #999;
+                }
+              }
+            }
+            .key-word {
+              // display: flex;
+              // align-items: flex-start;
+              // justify-content: flex-start;
+              width: 100%;
+              overflow: hidden;
+              margin: 0.1867rem auto;
+              .swiper-slide {
+                width: auto;
+                margin-right: 0.1333rem;
+                .word-item {
+                  font-size: 0.2667rem;
+                  color: #c5ac87;
+                  line-height: 0.5867rem;
+                  border-radius: 50px 50px 50px 50px;
+                  border: 1px solid #c5ac87;
+                  padding: 0.1067rem 0.4rem;
+                  cursor: pointer;
+                  &.active {
+                    background: #c5ac87;
+                    color: #fff;
+                  }
+                }
+              }
+            }
+            .question-list {
+              margin-top: 0.2667rem;
+              .question-item {
+                font-size: 0.32rem;
+                color: #54b3ef;
+                line-height: 0.5067rem;
+                text-align: left;
+                font-style: normal;
+                text-transform: none;
+                width: 100%;
+                text-overflow: ellipsis;
+                overflow: hidden;
+                white-space: nowrap;
+                margin-bottom: 0.16rem;
+                cursor: pointer;
+                &:last-of-type {
+                  margin-bottom: 0;
+                }
+              }
+            }
+          }
+          .send-box {
+            display: flex;
+            align-items: flex-start;
+            justify-content: flex-end;
+            margin-top: 0.3467rem;
+            .message-box {
+              display: flex;
+              align-items: flex-end;
+              justify-content: flex-start;
+              flex-direction: column;
+              max-width: 80%;
+              word-break: break-all;
+              .text {
+                font-size: 0.32rem;
+                color: #303030;
+                line-height: 0.5733rem;
+                text-align: left;
+                padding: 0.2133rem 0.32rem;
+                background: #fff;
+                border-radius: 4px 4px 0 4px;
+                max-width: 100%;
+              }
+              .time {
+                font-size: 0.2667rem;
+                color: #ffffff;
+                width: 100%;
+              }
+            }
+            .avatar {
+              width: 1.0667rem;
+              height: 1.0667rem;
+              background: url("../../assets/img/icon_user.svg") #87adc5 no-repeat;
+              background-size: 100%;
+              border-radius: 0px 0px 0px 0px;
+              border: 0.0267rem solid #ffffff;
+              border-radius: 50%;
+              margin-left: 0.2133rem;
+            }
+          }
+          .reply-box {
+            display: flex;
+            align-items: flex-start;
+            justify-content: flex-start;
+            margin-top: 0.3467rem;
+            .message-box {
+              display: flex;
+              align-items: flex-start;
+              justify-content: flex-start;
+              flex-direction: column;
+              max-width: 40%;
+              .text {
+                font-size: 0.32rem;
+                color: #303030;
+                line-height: 0.5733rem;
+                text-align: left;
+                padding: 0.2133rem 0.32rem;
+                background: #fff;
+                border-radius: 4px 4px 4px 0;
+                max-width: 100%;
+              }
+              .time {
+                font-size: 0.2667rem;
+                color: #ffffff;
+                width: 100%;
+                text-align: right;
+              }
+            }
+            .avatar {
+              width: 1.0667rem;
+              height: 1.0667rem;
+              background: url("../../assets/img/icon_Service.svg") #c5ac87 no-repeat;
+              background-size: 100%;
+              border-radius: 0px 0px 0px 0px;
+              border: 0.0267rem solid #ffffff;
+              border-radius: 50%;
+              margin-right: 0.2133rem;
+            }
+          }
+        }
+        .tips {
+          font-size: 14px;
+          color: #9f171c;
+          line-height: 26px;
+          text-align: left;
+          padding: 5px 20px;
+        }
+        .input-box {
+          // height: 270px;
+          width: 100%;
+          padding: 0 0 0.4rem 0;
+          .input-content {
+            height: 100%;
+            width: 100%;
+            display: flex;
+            align-items: flex-start;
+            justify-content: flex-start;
+            flex-direction: column;
+            .message-top {
+              width: 100%;
+              height: 2.8533rem;
+              background: #ffffff;
+              border-radius: 0.1333rem;
+              border: 0.0267rem solid #cecece;
+              margin-bottom: 0.1333rem;
+              /deep/ .el-textarea {
+                width: 100%;
+                height: 100%;
+                padding: 0.2667rem 0;
+                textarea {
+                  width: 100%;
+                  height: 100%;
+                  resize: none !important;
+                  border: none !important;
+                  background: none !important;
+                }
+              }
+            }
+            .message-bottom {
+              width: 100%;
+              // height: 1.0667rem;
+              display: flex;
+              align-items: center;
+              justify-content: space-between;
+              flex-direction: column;
+              .top {
+                display: flex;
+                width: 100%;
+                margin-bottom: 0.1333rem;
+                /deep/ input {
+                  width: 100%;
+                  height: 1.0667rem;
+                }
+              }
+              .bottom {
+                display: flex;
+                width: 100%;
+                .bottom-box {
+                  display: flex;
+                  background: #fff;
+                  align-items: center;
+                  justify-content: flex-start;
+                  flex: 1;
+
+                  .code-ipt {
+                    height: 1.0667rem;
+                    /deep/ input {
+                      height: 1.0667rem;
+                      background: none;
+                    }
+                  }
+                  .sn-code {
+                    width: 2.4533rem;
+                    height: 0.8267rem;
+
+                    background: #f2f2f2;
+                    margin: 0 10px;
+                    img {
+                      width: 100%;
+                      height: 100%;
+                    }
+                  }
+                }
+
+                .send-btn {
+                  height: 1.0667rem;
+                  width: 3.28rem;
+                  background: #c5ac87;
+                  border: none;
+                  display: flex;
+                  align-items: center;
+                  justify-content: center;
+                  font-weight: bold;
+                  font-size: 20px;
+                  color: #ffffff;
+                  cursor: pointer;
+                  margin-left: 0.2667rem;
+                  &.disabled {
+                    opacity: 0.5;
+                    pointer-events: none;
+                  }
+                  &:active {
+                    background: rgba(217, 200, 169, 0.9);
+                  }
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
+
+/deep/ input {
+  border: none !important;
+  font-size: 0.32rem;
+  color: #000;
+  &::placeholder {
+    color: rgba(122, 122, 120, 1);
+  }
+}
+/deep/ textarea {
+  // display: none;
+  font-size: 0.32rem;
+  color: #000;
+  &::placeholder {
+    color: rgba(122, 122, 120, 1);
+  }
+}
+</style>

+ 172 - 0
packages/mobile/src/views/topic/details.vue

@@ -0,0 +1,172 @@
+<!--  -->
+<template>
+  <div class="details-box">
+    <div class="nav-header">
+      <div class="back-btn" @click="onBack">
+        <i class="el-icon-arrow-left"></i>
+      </div>
+      <p>{{ title }}</p>
+    </div>
+    <div class="details-content">
+      <div class="details-list">
+        <div class="details-item" v-for="(i, index) in secondaryList">
+          <div class="cover-image" :style="`background-image:url(${getImageURL(i.thumb)});`"><img src="../../assets/img/lable.svg" alt="" /></div>
+          <p class="title">{{ i.name }}</p>
+          <p class="desc">{{ i.remark }}</p>
+          <div class="more">
+            <div class="icon"></div>
+            <span @click="gotoLink(i)">查看更多</span>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { getTopicList, getTopicSecondaryList } from "@/utils/api";
+export default {
+  //import引入的组件需要注入到对象中才能使用
+  components: {},
+  emits: ["back"],
+  props: ["secondId", "title"],
+  data() {
+    //这里存放数据
+    return { secondaryList: [] };
+  },
+  //监听属性 类似于data概念
+  computed: {
+    getImageURL: (url) => {
+      return (url) => {
+        const domain = process.env.NODE_ENV === "development" ? "http://project.4dage.com:8016" : "";
+        return domain + url;
+      };
+    },
+  },
+  //监控data中的数据变化
+  watch: {},
+  //方法集合
+  methods: {
+    gotoLink(i) {
+      window.open(i.link);
+    },
+    onBack() {
+      this.$emit("back");
+    },
+    async handleSecondaryList() {
+      // const res = await getTopicSecondaryList(this.secondId);
+      const res = await getTopicSecondaryList(15);
+      console.log("res", res.data);
+      this.secondaryList = res.data || [];
+    },
+  },
+  //生命周期 - 创建完成(可以访问当前this实例)
+  created() {
+    this.handleSecondaryList();
+  },
+  //生命周期 - 挂载完成(可以访问DOM元素)
+  mounted() {},
+  beforeCreate() {}, //生命周期 - 创建之前
+  beforeMount() {}, //生命周期 - 挂载之前
+  beforeUpdate() {}, //生命周期 - 更新之前
+  updated() {}, //生命周期 - 更新之后
+  beforeDestroy() {}, //生命周期 - 销毁之前
+  destroyed() {}, //生命周期 - 销毁完成
+  activated() {}, //如果页面有keep-alive缓存功能,这个函数会触发
+};
+</script>
+<style lang="less" scoped>
+//@import url(); 引入公共css类
+.details-box {
+  position: fixed;
+  width: 100%;
+  height: 100%;
+  max-width: 500px;
+  top: 0;
+  left: 50%;
+  transform: translateX(-50%);
+  z-index: 100;
+  background: rgba(72, 70, 66, 1);
+  .nav-header {
+    width: 100%;
+    height: 1.1733rem;
+    background: #b29d86;
+    font-size: 0.3733rem;
+    color: #fff;
+    display: flex;
+    align-items: center;
+    justify-content: flex-start;
+    // padding: 0 0.4267rem;
+    .back-btn {
+      width: 40px;
+      height: 100%;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      background: #b29d86;
+      position: relative;
+      z-index: 2;
+      i {
+        font-size: 0.4267rem;
+        font-weight: bold;
+      }
+    }
+  }
+  .details-content {
+    width: 100%;
+    height: calc(100% - 1.1733rem);
+    overflow-y: auto;
+    padding: 0.8rem 0.48rem 0;
+    .details-list {
+      width: 100%;
+      .details-item {
+        margin-bottom: 0.88rem;
+        word-break: break-all;
+        .cover-image {
+          width: 100%;
+          height: 4.8267rem;
+          background-repeat: no-repeat;
+          background-size: cover;
+          position: relative;
+          margin-bottom: 0.2667rem;
+          padding: 0.1867rem 0.2133rem;
+          img {
+            width: 100%;
+            height: 100%;
+          }
+        }
+        .title {
+          font-size: 0.5333rem;
+          color: #ffffff;
+          line-height: 1.0667rem;
+          font-weight: bold;
+        }
+        .desc {
+          font-size: 0.3733rem;
+          color: #ffffff;
+          line-height: 0.56rem;
+          display: -webkit-box;
+          overflow: hidden;
+          text-overflow: ellipsis;
+          -webkit-line-clamp: 5;
+          -webkit-box-orient: vertical;
+        }
+        .more {
+          display: flex;
+          align-items: center;
+          justify-content: flex-end;
+          font-size: 0.3733rem;
+          color: #d9c8a9;
+          margin-top: 0.5333rem;
+          .icon {
+            width: 0.8533rem;
+            height: 0.8533rem;
+            background: url(../../assets/img/icon_more.svg) no-repeat;
+            background-size: 100% 100%;
+          }
+        }
+      }
+    }
+  }
+}
+</style>

+ 269 - 0
packages/mobile/src/views/topic/index.vue

@@ -0,0 +1,269 @@
+<!--  -->
+<template>
+  <div class="topic-box">
+    <div class="nav-header">
+      <div class="back-btn" @click="onClose">
+        <i class="el-icon-arrow-left"></i>
+      </div>
+
+      <div class="nav-swiper">
+        <div class="tab" v-if="primaryList.length">
+          <div class="swiper-container-tab">
+            <div class="tab-list swiper-wrapper">
+              <div class="swiper-slide" v-for="item in primaryList">
+                <div :class="{ active: activeName == String(item.id) }" class="tab-name" @click="handleClick(item.id)">{{ item.name }}</div>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <div class="topic-content">
+      <div class="topic-list">
+        <div class="list-item" v-for="item in secondaryList" @click="gotoItem(item)">
+          <div class="cover"></div>
+          <div class="inner">
+            <img :src="getImageURL(item.thumb)" alt="" />
+            <p class="title">{{ item.name }}</p>
+          </div>
+        </div>
+      </div>
+    </div>
+    <Details :secondId="secondId" :title="secondName" v-if="secondId" @back="secondId = null" />
+  </div>
+</template>
+
+<script>
+import Details from "./details.vue";
+import { getTopicList, getTopicSecondaryList } from "@/utils/api";
+import Swiper from "swiper";
+export default {
+  //import引入的组件需要注入到对象中才能使用
+  components: { Details },
+  emits: ["close"],
+  data() {
+    //这里存放数据
+    return {
+      secondId: null,
+      secondName: "",
+      primaryList: [],
+      secondaryList: [],
+      tabW: 0,
+      activeName: "",
+      swiperOptions: {
+        slidesPerView: "auto",
+      },
+    };
+  },
+  //监听属性 类似于data概念
+  computed: {
+    getImageURL: (url) => {
+      return (url) => {
+        const domain = process.env.NODE_ENV === "development" ? "http://project.4dage.com:8016" : "";
+        return domain + url;
+      };
+    },
+  },
+  //监控data中的数据变化
+  watch: {},
+  //方法集合
+  methods: {
+    onClose() {
+      this.$emit("close");
+    },
+    gotoItem(i) {
+      this.secondId = i.id;
+      this.secondName = i.name;
+      console.error(this.secondName);
+    },
+    handleClick(id) {
+      this.activeName = String(id);
+      this.handleSecondaryList();
+    },
+    async handleDefaultList() {
+      const res = await getTopicList();
+      this.primaryList = res.data || [];
+      if (res.data.length > 0) {
+        this.activeName = String(res.data[0].id);
+        this.initTabSwiper();
+        await this.handleSecondaryList();
+      }
+    },
+    initTabSwiper() {
+      this.$nextTick(() => {
+        setTimeout(() => {
+          new Swiper(".swiper-container-tab", this.swiperOptions);
+        }, 0);
+      });
+    },
+    async handleSecondaryList() {
+      const res = await getTopicSecondaryList(this.activeName);
+      console.log("res", res.data);
+      this.secondaryList = res.data || [];
+    },
+  },
+  //生命周期 - 创建完成(可以访问当前this实例)
+  created() {
+    this.handleDefaultList();
+  },
+  //生命周期 - 挂载完成(可以访问DOM元素)
+  mounted() {},
+  beforeCreate() {}, //生命周期 - 创建之前
+  beforeMount() {}, //生命周期 - 挂载之前
+  beforeUpdate() {}, //生命周期 - 更新之前
+  updated() {}, //生命周期 - 更新之后
+  beforeDestroy() {}, //生命周期 - 销毁之前
+  destroyed() {}, //生命周期 - 销毁完成
+  activated() {}, //如果页面有keep-alive缓存功能,这个函数会触发
+};
+</script>
+<style lang="less" scoped>
+//@import url(); 引入公共css类
+.topic-box {
+  position: fixed;
+  width: 100%;
+  height: 100%;
+  max-width: 500px;
+  overflow: hidden;
+  top: 0;
+  left: 50%;
+  transform: translateX(-50%);
+  z-index: 100;
+  background: rgba(72, 70, 66, 1);
+
+  .nav-header {
+    width: 100%;
+    height: 1.1733rem;
+    background: #b29d86;
+    font-size: 0.3733rem;
+    color: #fff;
+    display: flex;
+    align-items: center;
+    justify-content: flex-start;
+
+    .back-btn {
+      width: 40px;
+      height: 100%;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      background: #b29d86;
+      position: relative;
+      z-index: 2;
+      i {
+        font-size: 0.4267rem;
+        font-weight: bold;
+      }
+    }
+
+    .nav-swiper {
+      flex: 1;
+      width: calc(100% - 40px);
+      height: 100%;
+      position: relative;
+      z-index: 1;
+      .tab {
+        width: 100%;
+        // overflow: hidden;
+        height: 100%;
+        .swiper-container-tab {
+          display: flex;
+          align-items: center;
+          justify-content: center;
+          height: 100%;
+          .tab-list {
+            .swiper-slide {
+              width: auto;
+              margin: 0 0.6667rem;
+
+              .tab-name {
+                color: #fff;
+                font-size: 0.3733rem;
+                cursor: pointer;
+                height: 100%;
+                display: flex;
+                align-items: center;
+                justify-content: center;
+                &.active {
+                  color: #9f171c;
+                  position: relative;
+                  font-weight: bold;
+                  &::after {
+                    content: "";
+                    position: absolute;
+                    width: 100%;
+                    height: 0.0533rem;
+                    background: #9f171c;
+                    border-radius: 0px 0px 0px 0px;
+                    bottom: -0.0267rem;
+                    left: 50%;
+                    transform: translateX(-50%);
+                  }
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+  .topic-content {
+    width: 100%;
+    height: calc(100% - 1.1733rem);
+    overflow-y: auto;
+    .topic-list {
+      padding: 0.8rem 0.3467rem 0;
+      .list-item {
+        width: 100%;
+        height: 5.0933rem;
+        background: url(../../assets/img/frame.svg) no-repeat;
+        background-size: 100% 100%;
+        position: relative;
+        padding: 21;
+        margin-bottom: 0.8rem;
+        padding: 0 0.4rem;
+        .cover {
+          width: 100%;
+          height: 100%;
+          position: absolute;
+          top: 0;
+          left: 0;
+          background: url(../../assets/img/frame.svg) no-repeat;
+          background-size: 100% 100%;
+          z-index: 100;
+        }
+        .inner {
+          width: 100%;
+          height: 100%;
+          background: rgba(128, 109, 82, 1);
+          padding: 0.3467rem;
+          position: relative;
+          img {
+            width: 100%;
+            height: 100%;
+            object-fit: cover;
+          }
+          .title {
+            width: 90%;
+            font-weight: bold;
+            font-size: 0.5333rem;
+            color: #ffffff;
+            line-height: 60px;
+            letter-spacing: 12px;
+            position: absolute;
+            top: 50%;
+            left: 50%;
+            text-align: center;
+            transform: translate(-50%, -50%);
+            text-shadow: 2px 2px 2px rgba(0, 0, 0, 0.3);
+            white-space: nowrap;
+            text-overflow: ellipsis;
+            overflow: hidden;
+          }
+        }
+      }
+    }
+  }
+}
+</style>

Різницю між файлами не показано, бо вона завелика
+ 7800 - 7715
packages/mobile/yarn.lock


BIN
packages/pc/src/assets/img/icon_Service.png


BIN
packages/pc/src/assets/img/icon_more.png


BIN
packages/pc/src/assets/img/icon_switch.png


BIN
packages/pc/src/assets/img/icon_talk.png


BIN
packages/pc/src/assets/img/icon_user.png


BIN
packages/pc/src/assets/img/img_01.png


BIN
packages/pc/src/assets/img/img_02.png


BIN
packages/pc/src/assets/img/mobile/frame.png


BIN
packages/pc/src/assets/img/mobile/icon_Service.png


BIN
packages/pc/src/assets/img/mobile/icon_more.png


BIN
packages/pc/src/assets/img/mobile/icon_q&a.png


BIN
packages/pc/src/assets/img/mobile/icon_special topic.png


BIN
packages/pc/src/assets/img/mobile/icon_switch.png


BIN
packages/pc/src/assets/img/mobile/icon_talk.png


BIN
packages/pc/src/assets/img/mobile/icon_user.png


BIN
packages/pc/src/assets/img/mobile/img.png


BIN
packages/pc/src/assets/img/mobile/lable.png


+ 93 - 71
packages/pc/src/utils/api.js

@@ -1,101 +1,123 @@
-import axios from './request'
+import axios from "./request";
 // 村庄保存点赞
 export const likeSaveApi = (villageId) => {
-    return axios({
-        method: 'get',
-        url: `/web/village/addStar/${villageId}`,
-    })
-}
+  return axios({
+    method: "get",
+    url: `/web/village/addStar/${villageId}`,
+  });
+};
 // 村庄浏览量
 export const lookSaveApi = (villageId) => {
-    return axios({
-        method: 'get',
-        url: `/web/village/addVisit/${villageId}`,
-    })
-}
+  return axios({
+    method: "get",
+    url: `/web/village/addVisit/${villageId}`,
+  });
+};
 // 获取浏览量和点赞量
 export const getCunNumApi = (villageId) => {
-    return axios({
-        method: 'get',
-        url: `/web/village/getDetail/${villageId}`,
-    })
-}
+  return axios({
+    method: "get",
+    url: `/web/village/getDetail/${villageId}`,
+  });
+};
 
 // 获取菜单树
 export const getTreeMenuApi = () => {
-    return axios({
-        method: 'get',
-        url: '/web/getTreeMenu',
-    })
-}
+  return axios({
+    method: "get",
+    url: "/web/getTreeMenu",
+  });
+};
 
 // 获取内容列表
 export const getInfoApi = (villageId) => {
-    return axios({
-        method: 'get',
-        url: `/web/content/list/${villageId}`,
-    })
-}
+  return axios({
+    method: "get",
+    url: `/web/content/list/${villageId}`,
+  });
+};
 
 // 村庄-统计-5分钟更新一次
 export const getStatistics = (villageId) => {
-    return axios({
-        method: 'get',
-        url: '/web/village/getStatistics',
-    })
-}
+  return axios({
+    method: "get",
+    url: "/web/village/getStatistics",
+  });
+};
 
 // 村庄-保存分享量
 export const addShareApi = (villageId) => {
-    return axios({
-        method: 'get',
-        url: `/web/village/addShare/${villageId}`,
-    })
-}
+  return axios({
+    method: "get",
+    url: `/web/village/addShare/${villageId}`,
+  });
+};
 
 // 获取访问量
 export const sceneGetList = () => {
-    return axios({
-        method: 'get',
-        url: '/web/scene/getList',
-    })
-}
+  return axios({
+    method: "get",
+    url: "/web/scene/getList",
+  });
+};
 
 // 添加访问量
 export const addVisit = (sceneCode) => {
-    return axios({
-        method: 'get',
-        url: `/web/scene/addVisit/${sceneCode}`,
-    })
-}
-
+  return axios({
+    method: "get",
+    url: `/web/scene/addVisit/${sceneCode}`,
+  });
+};
 
 export const getTopicList = () => {
-    return axios({
-        method: 'get',
-        url: `/web/subject/getList`,
-    })
-}
+  return axios({
+    method: "get",
+    url: `/web/subject/getList`,
+  });
+};
 export const getTopicSecondaryList = (parentId) => {
-    return axios({
-        method: 'get',
-        url: `/web/subject/getSon/${parentId}`,
-    })
-}
+  return axios({
+    method: "get",
+    url: `/web/subject/getSon/${parentId}`,
+  });
+};
 
 export const getTopicDetail = (id) => {
-    return axios({
-        method: 'get',
-        url: `/web/subject/detail/${id}`,
-    })
-}
-
-export const getMessageKeywords = (id) => {
-    return axios({
-        method: 'get',
-        url: `/web/dict/getList`,
-
-    })
-}
-
+  return axios({
+    method: "get",
+    url: `/web/subject/detail/${id}`,
+  });
+};
 
+export const getMessageKeywords = (type = null) => {
+  return axios({
+    method: "get",
+    url: `/web/dict/getList?type=${type}`,
+  });
+};
+export const getAskList = (id = null) => {
+  return axios({
+    method: "get",
+    url: id ? `/web/ask/getList?dictId=${id}` : `/web/ask/getList`,
+  });
+};
+export const getSnCode = (id = null) => {
+  return axios({
+    method: "get",
+    url: `/web/getRandCode`,
+    responseType: "blob",
+  });
+};
+export const sendMsg = (param) => {
+  return axios({
+    method: "post",
+    url: `/web/message/save`,
+    data: param,
+  });
+};
+export const getAskDetails = (id) => {
+  return axios({
+    method: "get",
+    url: `/web/ask/detail/${id}`,
+  });
+};

+ 18 - 0
packages/pc/src/utils/index.js

@@ -0,0 +1,18 @@
+export const getDate = () => {
+  // 补零
+  let addZero = (t) => {
+      return t < 10 ? '0' + t : t;
+  }
+  let time = new Date();
+  let Y = time.getFullYear(), // 年
+      M = time.getMonth() + 1, // 月
+      D = time.getDate(), // 日
+      h = time.getHours(), // 时
+      m = time.getMinutes(), // 分
+      s = time.getSeconds(); // 秒
+  if (M > 12) {
+      // 注: new Date()的年月日的拼接,在月份为12月时,会出现 获取月份+1 后,为13月的bug,需要特殊处理。用moment第三方插件获取时间也可避免此问题。
+      M = M - 12;
+  }
+  return `${Y}-${addZero(M)}-${addZero(D)} ${addZero(h)}:${addZero(m)}:${addZero(s)}`;
+}

+ 71 - 120
packages/pc/src/views/Home.vue

@@ -3,7 +3,7 @@
   <div class="home">
     <div id="mars3dContainer" class="mars3d-container"></div>
     <div id="zoneControl" @click="showZoneSelect">
-      <img src="data/layer.png"/>
+      <img src="data/layer.png" />
     </div>
     <div id="zoneSelect">
       <div @click="cityZone">市</div>
@@ -27,22 +27,13 @@
       <!-- 搜索 -->
       <div class="search" @keyup.enter="mySearch">
         <div class="searchBtn" @click="mySearch"></div>
-        <el-input
-            placeholder="搜索村落名称..."
-            suffix-icon="el-icon-search"
-            v-model="name"
-        >
-        </el-input>
+        <el-input placeholder="搜索村落名称..." suffix-icon="el-icon-search" v-model="name"> </el-input>
       </div>
       <!-- 下面内容 -->
       <div class="main">
         <div class="mainTop">
-          <div :class="{ active: mainInd === 0 }" @click="mainInd = 0">
-            浏览统计
-          </div>
-          <div :class="{ active: mainInd === 1 }" @click="mainInd = 1">
-            区域筛选
-          </div>
+          <div :class="{ active: mainInd === 0 }" @click="mainInd = 0">浏览统计</div>
+          <div :class="{ active: mainInd === 1 }" @click="mainInd = 1">区域筛选</div>
         </div>
         <!-- 浏览统计盒子 -->
         <div class="mainBox1" v-show="mainInd === 0">
@@ -64,12 +55,7 @@
           <div class="details">
             <h3>详情统计</h3>
             <div class="detailsNum">
-              <div
-                  class="row"
-                  @click.stop="toCun(item.id)"
-                  v-for="item in numData"
-                  :key="item.id"
-              >
+              <div class="row" @click.stop="toCun(item.id)" v-for="item in numData" :key="item.id">
                 <div class="rowLeft">{{ item.name }}</div>
                 <div class="rowRight">
                   <div class="plan">
@@ -88,47 +74,31 @@
               <div>{{ item.name }}</div>
               <div class="bs">{{ item.son.length }}</div>
             </div>
-            <div
-                @click="cutInd(item.id)"
-                class="rr"
-                :class="
-                item.id === mapDataInd
-                  ? 'el-icon-arrow-up'
-                  : 'el-icon-arrow-down'
-              "
-            ></div>
+            <div @click="cutInd(item.id)" class="rr" :class="item.id === mapDataInd ? 'el-icon-arrow-up' : 'el-icon-arrow-down'"></div>
             <div class="sonBox">
-              <div
-                  @click.stop="toCun(val.id)"
-                  :class="{ active: item.id === mapDataInd }"
-                  v-for="val in item.son"
-                  :key="val.id"
-              >
-                · {{ val.name }}
-              </div>
+              <div @click.stop="toCun(val.id)" :class="{ active: item.id === mapDataInd }" v-for="val in item.son" :key="val.id">· {{ val.name }}</div>
             </div>
           </div>
         </div>
       </div>
 
       <div class="special_topic">
-        <img src="../assets/img/icon_q&a.png" alt="" />
-        <img src="../assets/img/icon_special_topic.png" alt="" @click="topicModalVisible =true"/>
+        <img src="../assets/img/icon_q&a.png" @click="showQA = true" alt="" />
+        <img src="../assets/img/icon_special_topic.png" alt="" @click="topicModalVisible = true" />
       </div>
     </div>
     <!-- 加载中 -->
-    <div
-        class="homeLoading"
-        :class="{ homeLoadingNone: isLoading }"
-        v-loading="true"
-    ></div>
-    <topic :value="topicModalVisible" @close="topicModalVisible =false"/>
+    <div class="homeLoading" :class="{ homeLoadingNone: isLoading }" v-loading="true"></div>
+    <topic :value="topicModalVisible" @close="topicModalVisible = false" />
+    <QA v-if="showQA" @closeQA="showQA = false"></QA>
   </div>
 </template>
 
 <script>
-import {getStatistics, lookSaveApi} from "../utils/api";
-import Topic from './topic/index.vue'
+import { getStatistics, lookSaveApi } from "../utils/api";
+import Topic from "./topic/index.vue";
+import QA from "./qa/index.vue";
+
 // mapIns存储初始化的地图map实例,如果map实例放在vue的data中会导致帧率下降严重
 var mapIns = null;
 var zoneLayer = null;
@@ -140,11 +110,13 @@ import "mars3d/dist/mars3d.css";
 export default {
   name: "home",
   components: {
-    topic: Topic
+    topic: Topic,
+    QA,
   },
   data() {
     //这里存放数据
     return {
+      showQA: false,
       topicModalVisible: false,
       // 数据加载中
       isLoading: false,
@@ -163,41 +135,41 @@ export default {
           id: 1000,
           name: "蓬江区",
           son: [
-            {id: 3, name: "卢边村"},
-            {id: 2, name: "良溪村"},
+            { id: 3, name: "卢边村" },
+            { id: 2, name: "良溪村" },
           ],
         },
         {
           id: 5000,
           name: "开平市",
           son: [
-            {id: 5, name: "仓前村"},
-            {id: 9, name: "马降龙村"},
-            {id: 8, name: "自力村"},
+            { id: 5, name: "仓前村" },
+            { id: 9, name: "马降龙村" },
+            { id: 8, name: "自力村" },
           ],
         },
         {
           id: 6000,
           name: "台山市",
           son: [
-            {id: 1, name: "东宁村"},
-            {id: 10, name: "浮石村"},
-            {id: 11, name: "浮月村"},
-            {id: 12, name: "横江村"},
+            { id: 1, name: "东宁村" },
+            { id: 10, name: "浮石村" },
+            { id: 11, name: "浮月村" },
+            { id: 12, name: "横江村" },
           ],
         },
         {
           id: 4000,
           name: "鹤山市",
           son: [
-            {id: 4, name: "田心村"},
-            {id: 6, name: "霄南村"},
+            { id: 4, name: "田心村" },
+            { id: 6, name: "霄南村" },
           ],
         },
         {
           id: 7000,
           name: "恩平市",
-          son: [{id: 7, name: "歇马村"}],
+          son: [{ id: 7, name: "歇马村" }],
         },
       ],
       // 地图实例
@@ -301,7 +273,7 @@ export default {
     // 初始化地图
     async initMap() {
       // 读取 config.json 配置文件
-      const json = await mars3d.Util.fetchJson({url: "config/config.json"});
+      const json = await mars3d.Util.fetchJson({ url: "config/config.json" });
       console.log("读取 config.json 配置文件完成", json); // 打印测试信息
       // 创建三维地球场景
       const mapOptions = json.map3d;
@@ -353,11 +325,7 @@ export default {
       const acInfo = this.villagePos.find((v) => v.id === id) || {};
 
       const graphic = new mars3d.graphic.DivGraphic({
-        position: new mars3d.LngLatPoint(
-            acInfo.position[0],
-            acInfo.position[1],
-            100
-        ),
+        position: new mars3d.LngLatPoint(acInfo.position[0], acInfo.position[1], 100),
         style: {
           html: `
           <div class="lableContent" _id=${id} >
@@ -371,12 +339,7 @@ export default {
           horizontalOrigin: mars3d.Cesium.HorizontalOrigin.LEFT,
           verticalOrigin: mars3d.Cesium.VerticalOrigin.BOTTOM,
           // distanceDisplayCondition: new mars3d.Cesium.DistanceDisplayCondition(0, 30000), // 按视距距离显示
-          scaleByDistance: new mars3d.Cesium.NearFarScalar(
-              1000,
-              1.5,
-              12000,
-              0.8
-          ),
+          scaleByDistance: new mars3d.Cesium.NearFarScalar(1000, 1.5, 12000, 0.8),
           clampToGround: true,
         },
         attr: {
@@ -403,7 +366,7 @@ export default {
     },
     mySearch() {
       console.log("点击了搜索", this.name);
-      mapIns.flyToPoint(this.getFlyPos(this.name), {radius: 3000});
+      mapIns.flyToPoint(this.getFlyPos(this.name), { radius: 3000 });
     },
     // 跳转
     async toCun(id) {
@@ -537,11 +500,7 @@ export default {
   //生命周期 - 创建完成(可以访问当前this实例)
   async created() {
     // 移动端和pc端的切换
-    if (
-        window.navigator.userAgent.match(
-            /(phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone)/i
-        )
-    ) {
+    if (window.navigator.userAgent.match(/(phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone)/i)) {
       // 移动端
       if (window.location.href.includes("web")) {
         window.location.href = window.location.href.replace("web", "webM");
@@ -560,50 +519,43 @@ export default {
   mounted() {
     // 初始化地图实例
     this.initMap().then(
-        (map) => {
-          // 加载模型
-          // this.loadModel();
-          // 村名lable图层
-          let graphicLayer = new mars3d.layer.GraphicLayer();
-          // 行政边界图层
-          zoneLayer = new mars3d.layer.GeoJsonLayer();
-          // 绑定标注的点击事件
-          graphicLayer.on("click", (e) => {
-            const attr = e.graphic.attr || {};
-            // console.log("name", attr.name);
-            // console.log("id", attr.id);
-            this.toCun(attr.id);
-          });
-          map.addLayer(graphicLayer);
-
-          // 添加村名label
-
-          this.villagePos.forEach((v) => {
-            this.drawVillageLabel(graphicLayer, v.id);
-          });
-
-          // 加载区边界线
-          this.loadZoneLayer(mapIns, "jmq", "#00ffff", 8);
-        },
-        (error) => {
-          console.log("地图初始化失败", error);
-        }
+      (map) => {
+        // 加载模型
+        // this.loadModel();
+        // 村名lable图层
+        let graphicLayer = new mars3d.layer.GraphicLayer();
+        // 行政边界图层
+        zoneLayer = new mars3d.layer.GeoJsonLayer();
+        // 绑定标注的点击事件
+        graphicLayer.on("click", (e) => {
+          const attr = e.graphic.attr || {};
+          // console.log("name", attr.name);
+          // console.log("id", attr.id);
+          this.toCun(attr.id);
+        });
+        map.addLayer(graphicLayer);
+
+        // 添加村名label
+
+        this.villagePos.forEach((v) => {
+          this.drawVillageLabel(graphicLayer, v.id);
+        });
+
+        // 加载区边界线
+        this.loadZoneLayer(mapIns, "jmq", "#00ffff", 8);
+      },
+      (error) => {
+        console.log("地图初始化失败", error);
+      }
     );
   },
-  beforeCreate() {
-  }, //生命周期 - 创建之前
-  beforeMount() {
-  }, //生命周期 - 挂载之前
-  beforeUpdate() {
-  }, //生命周期 - 更新之前
-  updated() {
-  }, //生命周期 - 更新之后
-  beforeDestroy() {
-  }, //生命周期 - 销毁之前
-  destroyed() {
-  }, //生命周期 - 销毁完成
-  activated() {
-  }, //如果页面有keep-alive缓存功能,这个函数会触发
+  beforeCreate() {}, //生命周期 - 创建之前
+  beforeMount() {}, //生命周期 - 挂载之前
+  beforeUpdate() {}, //生命周期 - 更新之前
+  updated() {}, //生命周期 - 更新之后
+  beforeDestroy() {}, //生命周期 - 销毁之前
+  destroyed() {}, //生命周期 - 销毁完成
+  activated() {}, //如果页面有keep-alive缓存功能,这个函数会触发
 };
 </script>
 <style lang="less" scoped>
@@ -977,7 +929,6 @@ export default {
       img {
         cursor: pointer;
       }
-
     }
   }
 

+ 678 - 0
packages/pc/src/views/qa/index.vue

@@ -0,0 +1,678 @@
+<!--  -->
+<template>
+  <div class="qa-layout">
+    <div class="header">
+      <div class="back-btn" @click="onBack"><i class="el-icon-arrow-left"></i>返回</div>
+      <span>问答咨询</span>
+    </div>
+    <!-- <div class="pic bg1"></div>
+    <div class="pic bg2"></div> -->
+    <div class="qa-content">
+      <div class="msg-box">
+        <div class="line"><i></i><i></i><i></i><i></i></div>
+        <div class="control-box">
+          <div class="info-box" ref="scrollRef">
+            <div class="qa-list">
+              <div class="qaList-header">
+                <div class="left"><i></i><span>猜你想问</span></div>
+                <div class="right"><span>换一批</span><i @click="changeWord"></i></div>
+              </div>
+
+              <div ref="mySwiper" class="swiper-container-word key-word">
+                <div class="word-list swiper-wrapper">
+                  <div class="swiper-slide" v-for="(item, index) in words" v-if="index < 5">
+                    <div class="word-item" @click="handleWord(item)" :class="{ active: wordId == item.id }">{{ item.name }}</div>
+                  </div>
+                </div>
+              </div>
+              <div class="question-list">
+                <div class="question-item" @click="sendMessage(i)" v-for="(i, index) in questions" v-if="index < 5">{{ i.question }}</div>
+              </div>
+            </div>
+            <div v-for="i in messageList">
+              <div class="send-box" v-if="i.type == 1">
+                <div class="message-box">
+                  <p class="text">{{ i.text }}</p>
+                  <span class="time">{{ i.time }}</span>
+                </div>
+                <div class="avatar"></div>
+              </div>
+              <div class="reply-box" v-if="i.type == 2">
+                <div class="avatar"></div>
+                <div class="message-box">
+                  <p class="text">{{ i.text }}</p>
+                  <span class="time">{{ i.time }}</span>
+                </div>
+              </div>
+            </div>
+          </div>
+          <div class="tips">每天最多可咨询10个问题</div>
+          <div class="input-box">
+            <div class="input-content">
+              <div class="message-top">
+                <el-input type="textarea" :rows="2" maxLength="200" placeholder="请输入您想咨询的问题,不超过200字" v-model="text"> </el-input>
+              </div>
+              <div class="message-bottom">
+                <div class="left">
+                  <el-input type="input" maxLength="20" placeholder="请输入您的联系方式,不超过20字" v-model="phoneNum"> </el-input>
+                </div>
+                <div class="right">
+                  <el-input class="code-ipt" type="input" placeholder="请输入验证码" v-model="snCode"> </el-input>
+                  <div class="sn-code" @click.stop="getCode()">
+                    <!-- <PictureIdentify v-if="showSnCode" :identifyCode="identifyCode" :contentWidth="156" :contentHeight="65" /> -->
+                    <img :src="identifyCode" alt="" />
+                  </div>
+                  <div class="send-btn" :class="{ disabled }" @click="sendMessage()">发送</div>
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import Swiper from "swiper";
+import { getDate } from "@/utils";
+import { getMessageKeywords, getAskList, getSnCode, sendMsg, getAskDetails } from "@/utils/api";
+export default {
+  //import引入的组件需要注入到对象中才能使用
+  components: {},
+  data() {
+    //这里存放数据
+    return {
+      words: [],
+      questions: [],
+      swiperOptions: {
+        slidesPerView: "auto",
+        observer: true, //修改swiper自己或子元素时,自动初始化swiper
+        observeParents: true, //修改swiper的父元素时,自动初始化swiper
+      },
+      swiperInstance: null,
+      text: "",
+      phoneNum: "",
+      snCode: "",
+      identifyCode: "",
+      src: "",
+      showSnCode: false,
+      wordId: null,
+      messageList: [],
+      disabled: false,
+    };
+  },
+  //监听属性 类似于data概念
+  computed: {},
+  emits: ["closeQA"],
+  //监控data中的数据变化
+  watch: {},
+  //方法集合
+  methods: {
+    handleWord(i) {
+      if (i.id == this.wordId) retrun;
+      this.wordId = i.id;
+      this.getQuestionList();
+    },
+    async getWordList() {
+      let res = await getMessageKeywords("ask");
+
+      this.words = res.data;
+      this.initSwiper();
+      this.getQuestionList();
+    },
+    async getQuestionList() {
+      let res = await getAskList(this.wordId);
+      this.questions = res.data;
+    },
+    async getCode() {
+      this.showSnCode = false;
+      let res = await getSnCode();
+      this.identifyCode = URL.createObjectURL(res);
+      this.showSnCode = true;
+    },
+    changeWord() {
+      this.wordId = null;
+      this.getWordList();
+    },
+    onBack() {
+      this.$emit("closeQA");
+    },
+
+    async sendMessage(data) {
+      this.disabled = true;
+
+      let msg, res;
+      if (data) {
+        res = await getAskDetails(data.id);
+        if (res.code == 0) {
+          msg = { type: 1, text: data.question, time: getDate() };
+          this.addMessage(msg);
+          setTimeout(() => {
+            msg = { type: 2, text: res.data.answer, time: res.timestamp };
+            this.addMessage(msg);
+          }, 0);
+        } else {
+          this.$message({
+            message: res.msg,
+            type: "error",
+          });
+        }
+      } else {
+        if (!this.text) {
+          this.disabled = false;
+          return this.$message({
+            message: "请输入问题",
+            type: "error",
+          });
+        }
+        if (!this.phoneNum) {
+          this.disabled = false;
+          return this.$message({
+            message: "请输入联系方式",
+            type: "error",
+          });
+        }
+        if (!this.snCode) {
+          this.disabled = false;
+          return this.$message({
+            message: "请输入验证码",
+            type: "error",
+          });
+        }
+
+        let params = { contactWay: this.phoneNum, question: this.text, randCode: this.snCode };
+        res = await sendMsg(params);
+        if (res.code == 0) {
+          msg = { type: 1, text: this.text, time: getDate() };
+          this.addMessage(msg);
+          setTimeout(() => {
+            msg = { type: 2, text: res.data, time: res.timestamp };
+            this.addMessage(msg);
+          }, 0);
+
+          this.getCode();
+          this.clearInput();
+        } else {
+          // this.getCode();
+          // this.clearInput();
+          this.$message({
+            message: res.msg,
+            type: "error",
+          });
+        }
+      }
+      this.disabled = false;
+    },
+    autoScroll() {
+      setTimeout(() => {
+        this.$refs.scrollRef.scrollTo({
+          top: 999999,
+          behavior: "smooth",
+        });
+      }, 0);
+    },
+    addMessage(item) {
+      this.messageList.push(item);
+      this.autoScroll();
+    },
+    clearInput() {
+      this.text = "";
+      (this.phoneNum = ""), (this.snCode = "");
+    },
+    initSwiper() {
+      setTimeout(() => {
+        if (!this.swiperInstance) {
+          this.swiperInstance = new Swiper(".swiper-container-word", this.swiperOptions);
+        } else {
+          this.swiperInstance.update();
+        }
+      }, 0);
+    },
+  },
+  //生命周期 - 创建完成(可以访问当前this实例)
+  created() {},
+  //生命周期 - 挂载完成(可以访问DOM元素)
+  mounted() {
+    this.getWordList();
+    this.getCode();
+  },
+  beforeCreate() {}, //生命周期 - 创建之前
+  beforeMount() {}, //生命周期 - 挂载之前
+  beforeUpdate() {}, //生命周期 - 更新之前
+  updated() {}, //生命周期 - 更新之后
+  beforeDestroy() {}, //生命周期 - 销毁之前
+  destroyed() {}, //生命周期 - 销毁完成
+  activated() {}, //如果页面有keep-alive缓存功能,这个函数会触发
+};
+</script>
+<style lang="less" scoped>
+//@import url(); 引入公共css类
+.qa-layout {
+  width: 100%;
+  height: 100%;
+  position: fixed;
+  z-index: 1000;
+  top: 0;
+  left: 0;
+  background: rgba(33, 27, 18, 0.5);
+  backdrop-filter: blur(20px);
+  font-family: "思源宋体";
+  padding: 0 94px;
+  display: flex;
+  align-items: flex-start;
+  justify-content: flex-start;
+  flex-direction: column;
+  .header {
+    width: 100%;
+    height: 100px;
+    border-bottom: 1px solid rgba(255, 255, 255, 0.5);
+    color: #fff;
+    font-size: 24px;
+    display: flex;
+    align-items: center;
+    justify-content: flex-start;
+    .back-btn {
+      margin-right: 50px;
+      cursor: pointer;
+    }
+  }
+  .pic {
+    position: absolute;
+    pointer-events: none;
+    z-index: 10;
+    &.bg1 {
+      width: 457px;
+      height: 187px;
+      background: url(../../assets/img/img_01.png) no-repeat;
+      background-size: 100%;
+      left: 0;
+      bottom: 0;
+    }
+    &.bg2 {
+      width: 457px;
+      height: 187px;
+      background: url(../../assets/img/img_02.png) no-repeat;
+      background-size: 100%;
+
+      right: 0;
+      top: 112px;
+    }
+  }
+  .qa-content {
+    flex: 1;
+    width: 100%;
+    height: calc(100vh - 100px);
+    display: flex;
+    align-items: center;
+    justify-content: flex-start;
+    .msg-box {
+      width: 100%;
+      height: 92.8%;
+      padding: 10px;
+      position: relative;
+      .line {
+        position: absolute;
+        z-index: 1;
+        width: 100%;
+        height: 100%;
+        left: 0;
+        top: 0;
+        i {
+          position: absolute;
+          &:nth-of-type(1) {
+            width: calc(100% - 60px);
+            height: 1px;
+            background: rgba(217, 200, 169, 1);
+            top: 0;
+            left: 30px;
+          }
+          &:nth-of-type(2) {
+            width: calc(100% - 60px);
+            height: 1px;
+            background: rgba(217, 200, 169, 1);
+            bottom: 0;
+            left: 30px;
+          }
+          &:nth-of-type(3) {
+            height: calc(100% - 60px);
+            width: 1px;
+            background: rgba(217, 200, 169, 1);
+            bottom: 30px;
+            right: 0;
+          }
+          &:nth-of-type(4) {
+            height: calc(100% - 60px);
+            width: 1px;
+            background: rgba(217, 200, 169, 1);
+            bottom: 30px;
+            left: 0;
+          }
+        }
+      }
+      .control-box {
+        width: 100%;
+        height: 100%;
+        background: rgba(193, 193, 178, 1);
+        border-radius: 4px;
+        z-index: 2;
+        position: relative;
+        padding: 30px 50px 20px 60px;
+        display: flex;
+        align-items: flex-start;
+        justify-content: flex-start;
+        flex-direction: column;
+        .info-box {
+          height: 100%;
+          flex: 1;
+          width: 100%;
+          overflow-y: auto;
+          padding-right: 10px;
+          padding-bottom: 20px;
+
+          &::-webkit-scrollbar {
+            width: 7px;
+          }
+          &::-webkit-scrollbar-thumb {
+            border-radius: 5px;
+            background: rgba(217, 200, 169, 0.5);
+          }
+          &::-webkit-scrollbar-track {
+            border-radius: 0;
+            background: transparent;
+          }
+          .qa-list {
+            width: 520px;
+            background: #fff;
+            padding: 20px;
+            border-radius: 4px;
+            .qaList-header {
+              width: 100%;
+              display: flex;
+              align-items: center;
+              justify-content: space-between;
+              border-bottom: 1px solid rgba(243, 243, 243, 1);
+              padding-bottom: 10px;
+              .left {
+                display: flex;
+                align-items: center;
+                justify-content: space-between;
+                i {
+                  width: 24px;
+                  height: 24px;
+                  background: url(../../assets/img/icon_talk.png) no-repeat;
+                  background-size: 100%;
+                  margin-right: 10px;
+                }
+                span {
+                  font-size: 20px;
+                  font-weight: bold;
+                  color: rgba(197, 172, 135, 1);
+                }
+              }
+              .right {
+                display: flex;
+                align-items: center;
+                justify-content: space-between;
+                i {
+                  width: 20px;
+                  height: 20px;
+                  background: url(../../assets/img/icon_switch.png) no-repeat;
+                  background-size: 100%;
+                  margin-left: 10px;
+                  cursor: pointer;
+                }
+                span {
+                  font-size: 14px;
+                  color: #999;
+                }
+              }
+            }
+            .key-word {
+              // display: flex;
+              // align-items: flex-start;
+              // justify-content: flex-start;
+              width: 100%;
+              overflow: hidden;
+              margin: 10px auto 10px;
+              .swiper-slide {
+                width: auto;
+                margin-right: 10px;
+                .word-item {
+                  font-size: 14px;
+                  color: #c5ac87;
+                  line-height: 26px;
+                  border-radius: 50px 50px 50px 50px;
+                  border: 1px solid #c5ac87;
+                  padding: 8px 18px;
+                  cursor: pointer;
+                  &.active {
+                    background: #c5ac87;
+                    color: #fff;
+                  }
+                }
+              }
+            }
+            .question-list {
+              .question-item {
+                font-size: 16px;
+                color: #54b3ef;
+                line-height: 26px;
+                text-align: left;
+                font-style: normal;
+                text-transform: none;
+                width: 100%;
+                text-overflow: ellipsis;
+                overflow: hidden;
+                white-space: nowrap;
+                margin-bottom: 5px;
+                cursor: pointer;
+                &:last-of-type {
+                  margin-bottom: 0;
+                }
+              }
+            }
+          }
+          .send-box {
+            display: flex;
+            align-items: flex-start;
+            justify-content: flex-end;
+            margin-top: 10px;
+            .message-box {
+              display: flex;
+              align-items: flex-end;
+              justify-content: flex-start;
+              flex-direction: column;
+              max-width: 40%;
+              word-break: break-all;
+              .text {
+                font-size: 16px;
+                color: #303030;
+                line-height: 26px;
+                text-align: left;
+                padding: 10px 20px;
+                background: #fff;
+                border-radius: 4px 4px 0 4px;
+                max-width: 100%;
+              }
+              .time {
+                font-size: 13px;
+                color: #ffffff;
+                line-height: 26px;
+                width: 100%;
+              }
+            }
+            .avatar {
+              width: 48px;
+              height: 48px;
+              background: url("../../assets/img/icon_user.png") #87adc5 no-repeat;
+              background-size: 100%;
+              border-radius: 0px 0px 0px 0px;
+              border: 2px solid #ffffff;
+              border-radius: 50%;
+              margin-left: 10px;
+            }
+          }
+          .reply-box {
+            display: flex;
+            align-items: flex-start;
+            justify-content: flex-start;
+            margin-top: 10px;
+            .message-box {
+              display: flex;
+              align-items: flex-start;
+              justify-content: flex-start;
+              flex-direction: column;
+              max-width: 40%;
+              .text {
+                font-size: 16px;
+                color: #303030;
+                line-height: 26px;
+                text-align: left;
+                padding: 10px 20px;
+                background: #fff;
+                border-radius: 4px 4px 4px 0;
+                max-width: 100%;
+              }
+              .time {
+                font-size: 13px;
+                color: #ffffff;
+                line-height: 26px;
+                width: 100%;
+                text-align: right;
+              }
+            }
+            .avatar {
+              width: 48px;
+              height: 48px;
+              background: url("../../assets/img/icon_Service.png") #c5ac87 no-repeat;
+              background-size: 100%;
+              border-radius: 0px 0px 0px 0px;
+              border: 2px solid #ffffff;
+              border-radius: 50%;
+              margin-right: 10px;
+            }
+          }
+        }
+        .tips {
+          font-size: 14px;
+          color: #9f171c;
+          line-height: 26px;
+          text-align: left;
+          padding: 5px 20px;
+        }
+        .input-box {
+          padding-right: 10px;
+          height: 270px;
+          width: 100%;
+
+          .input-content {
+            height: 100%;
+            width: 100%;
+            border-radius: 4px;
+            background: rgba(232, 231, 225, 1);
+            padding: 16px 14px;
+            display: flex;
+            align-items: flex-start;
+            justify-content: flex-start;
+            flex-direction: column;
+            .message-top {
+              flex: 1;
+              width: 100%;
+              /deep/ .el-textarea {
+                width: 100%;
+                height: 100%;
+
+                textarea {
+                  width: 100%;
+                  height: 100%;
+                  resize: none !important;
+                  border: none !important;
+                  background: none !important;
+                }
+              }
+            }
+            .message-bottom {
+              width: 100%;
+              height: 65px;
+              display: flex;
+              align-items: center;
+              justify-content: space-between;
+              .left {
+                width: 30%;
+                /deep/ input {
+                  // width: 465px;
+                  height: 65px;
+                }
+              }
+              .right {
+                display: flex;
+                max-width: 50%;
+
+                .code-ipt {
+                  // width: 245px;
+                  display: flex;
+                  align-items: center;
+                  justify-content: flex-end;
+                  height: 65px;
+                  /deep/ input {
+                    // width: 245px;
+                    height: 65px;
+                  }
+                }
+                .sn-code {
+                  width: 156px;
+                  height: 65px;
+                  background: #f2f2f2;
+                  margin: 0 10px;
+                  img {
+                    width: 100%;
+                    height: 100%;
+                  }
+                }
+                .send-btn {
+                  height: 65px;
+                  width: 174px;
+                  background: rgba(217, 200, 169, 1);
+                  border: none;
+                  display: flex;
+                  align-items: center;
+                  justify-content: center;
+                  font-weight: bold;
+                  font-size: 20px;
+                  color: #ffffff;
+                  cursor: pointer;
+                  &.disabled {
+                    opacity: 0.5;
+                    pointer-events: none;
+                  }
+                  &:active {
+                    background: rgba(217, 200, 169, 0.9);
+                  }
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
+/deep/ input {
+  border: 1px solid #d9d9d9 !important;
+  background: rgba(255, 255, 255, 0.5) !important;
+  font-size: 16px;
+  color: #000;
+  &::placeholder {
+    color: rgba(122, 122, 120, 1);
+  }
+}
+/deep/ textarea {
+  // display: none;
+  font-size: 16px;
+  color: #000;
+  &::placeholder {
+    color: rgba(122, 122, 120, 1);
+  }
+}
+</style>

+ 206 - 0
packages/pc/src/views/topic/details.vue

@@ -0,0 +1,206 @@
+<!--  -->
+<template>
+  <div class="details" v-if="show">
+    <div class="header">
+      <div class="back-btn" @click="onBack"><i class="el-icon-arrow-left"></i>返回</div>
+      <span>{{title}}</span>
+    </div>
+    <div class="pic bg1"></div>
+    <div class="pic bg2"></div>
+    <div class="details-content">
+      <div class="detail-list">
+        <div class="detail-item" v-for="(i, index) in secondaryList" :key="i.id">
+          <div class="item-left">
+            <img :src="getImageURL(i.thumb)" alt="" />
+          </div>
+          <div class="item-right">
+            <p class="title">{{ i.name }}</p>
+            <p class="desc">{{ i.remark }}</p>
+            <div class="more">
+              <i></i>
+              <span @click="gotoLink(i)">查看更多</span>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { getTopicList, getTopicSecondaryList } from "@/utils/api";
+export default {
+  //import引入的组件需要注入到对象中才能使用
+  components: {},
+  emits: ["back"],
+  props: ["secondId","title"],
+  data() {
+    //这里存放数据
+    return { secondaryList: [], show: true };
+  },
+  //监听属性 类似于data概念
+  computed: {
+    getImageURL: (url) => {
+      return (url) => {
+        const domain = process.env.NODE_ENV === "development" ? "http://project.4dage.com:8016" : "";
+        return domain + url;
+      };
+    },
+  },
+  //监控data中的数据变化
+  watch: {},
+  //方法集合
+  methods: {
+    gotoLink(i) {
+      console.error(i);
+      window.open(i.link);
+    },
+    onBack() {
+      this.$emit("back");
+    },
+    async handleSecondaryList() {
+      const res = await getTopicSecondaryList(this.secondId);
+      console.log("res", res.data);
+      this.secondaryList = res.data || [];
+    },
+  },
+  //生命周期 - 创建完成(可以访问当前this实例)
+  created() {
+    this.handleSecondaryList();
+  },
+  //生命周期 - 挂载完成(可以访问DOM元素)
+  mounted() {},
+  beforeCreate() {}, //生命周期 - 创建之前
+  beforeMount() {}, //生命周期 - 挂载之前
+  beforeUpdate() {}, //生命周期 - 更新之前
+  updated() {}, //生命周期 - 更新之后
+  beforeDestroy() {}, //生命周期 - 销毁之前
+  destroyed() {}, //生命周期 - 销毁完成
+  activated() {}, //如果页面有keep-alive缓存功能,这个函数会触发
+};
+</script>
+<style lang="less" scoped>
+//@import url(); 引入公共css类
+.details {
+  width: 100%;
+  height: 100%;
+  position: fixed;
+  z-index: 1000;
+  top: 0;
+  left: 0;
+  background: rgba(33, 27, 18, 0.5);
+  backdrop-filter: blur(20px);
+  font-family: "思源宋体";
+  padding: 0 94px;
+  .header {
+    height: 100px;
+    border-bottom: 1px solid rgba(255, 255, 255, 0.5);
+    color: #fff;
+    font-size: 24px;
+    display: flex;
+    align-items: center;
+    justify-content: flex-start;
+    .back-btn {
+      margin-right: 50px;
+      cursor: pointer;
+    }
+  }
+  .pic {
+    position: absolute;
+    pointer-events: none;
+    z-index: 10;
+    &.bg1 {
+      width: 457px;
+      height: 187px;
+      background: url(../../assets/img/img_01.png) no-repeat;
+      background-size: 100%;
+      left: 0;
+      bottom: 0;
+    }
+    &.bg2 {
+      width: 457px;
+      height: 187px;
+      background: url(../../assets/img/img_02.png) no-repeat;
+      background-size: 100%;
+
+      right: 0;
+      top: 112px;
+    }
+  }
+  .details-content {
+    width: 100%;
+    padding: 50px 30px 0px 70px;
+    height: calc(100% - 100px);
+    .detail-list {
+      height: 100%;
+      overflow-y: auto;
+
+      &::-webkit-scrollbar {
+        width: 7px;
+      }
+      &::-webkit-scrollbar-thumb {
+        border-radius: 5px;
+        background: rgba(217, 200, 169, 0.5);
+      }
+      &::-webkit-scrollbar-track {
+        border-radius: 0;
+        background: transparent;
+      }
+
+      .detail-item {
+        margin-bottom: 65px;
+        display: flex;
+        align-items: center;
+        justify-content: flex-start;
+        .item-left {
+          width: 509px;
+          height: 272px;
+          margin-right: 102px;
+          img {
+            width: 100%;
+            height: 100%;
+            object-fit: cover;
+          }
+        }
+        .item-right {
+          word-break: break-all;
+          color: #fff;
+          flex: 1;
+          .title {
+            font-size: 30px;
+          }
+          .desc {
+            font-size: 20px;
+            line-height: 30px;
+            display: -webkit-box;
+            overflow: hidden;
+            text-overflow: ellipsis;
+            -webkit-line-clamp: 4;
+            -webkit-box-orient: vertical;
+            margin: 17px 0 34px;
+          }
+          .more {
+            display: flex;
+            align-items: center;
+            justify-content: flex-start;
+            i {
+              width: 40px;
+              height: 40px;
+              display: inline-block;
+              background-image: url(../../assets/img/icon_more.png);
+              background-repeat: no-repeat;
+              background-size: 100%;
+              margin-right: 10px;
+            }
+            font-size: 20px;
+            color: rgba(217, 200, 169, 1);
+            span {
+              cursor: pointer;
+            }
+          }
+        }
+      }
+    }
+  }
+}
+</style>

+ 290 - 157
packages/pc/src/views/topic/index.vue

@@ -1,109 +1,171 @@
 <template>
   <div class="topic" v-if="visible">
-    <div class="close-btn" @click="handleModalClose"></div>
-    <el-tabs v-model="activeName" @tab-click="handleClick">
-
-      <el-tab-pane :label="tab.name" :name="String(tab.id)" v-for="tab in primaryList">
-        <div class="swiper-container">
+    <div class="info-box" :class="{ hide: secondId }">
+      <div class="close-btn" @click="handleModalClose"></div>
+      <div class="get-width" ref="tabRef">
+        <div v-for="item in primaryList">{{ item.name }}</div>
+      </div>
+      <div class="tab" v-if="primaryList.length" :style="`width:${tabW}px;`">
+        <div class="swiper-container-tab">
+          <div class="tab-list swiper-wrapper">
+            <div class="swiper-slide" v-for="item in primaryList">
+              <div :class="{ active: activeName == String(item.id) }" class="tab-name" @click="handleClick(item.id)">{{ item.name }}</div>
+            </div>
+          </div>
+        </div>
+      </div>
+      <!-- <el-tabs v-model="activeName" @tab-click="handleClick">
+      <el-tab-pane :label="tab.name" :name="String(tab.id)" v-for="tab in primaryList"> -->
+      <div class="card" :style="`width:${cardListW}px;`">
+        <div class="swiper-container-card">
           <div class="card-list swiper-wrapper" v-show="isLoaded">
-            <div class="card-item swiper-slide" v-for="item in secondaryList">
-              <div class="inner_img" :style="{
-              backgroundImage:`url(${getImageURL(item.thumb)})`,
-            }"></div>
+            <div class="card-item swiper-slide" v-for="item in secondaryList" @click="gotoItem(item)">
+              <div
+                class="inner_img"
+                :style="{
+                  backgroundImage: `url(${getImageURL(item.thumb)})`,
+                }"
+              ></div>
               <div class="inner_title">{{ item.name }}</div>
               <div class="bg"></div>
             </div>
           </div>
-
         </div>
+      </div>
 
-      </el-tab-pane>
+      <!-- </el-tab-pane>
+    </el-tabs> -->
+    </div>
 
-    </el-tabs>
+    <Details :secondId="secondId" :title="secondName" v-if="secondId" @back="secondId = null" />
   </div>
 </template>
 
 <script>
-import {getTopicList, getTopicSecondaryList} from "@/utils/api"
+import { getTopicList, getTopicSecondaryList } from "@/utils/api";
 import Swiper from "swiper";
+import Details from "./details.vue";
 
 export default {
   computed: {
     getImageURL: (url) => {
       return (url) => {
-        const domain = process.env.NODE_ENV === 'development' ? 'http://project.4dage.com:8016' : ''
-        return domain + url
+        const domain = process.env.NODE_ENV === "development" ? "http://project.4dage.com:8016" : "";
+        return domain + url;
+      };
+    },
+    cardListW: function () {
+      console.error(this);
+      let w = this.secondaryList.length * (423 + 50);
+      console.error(window.innerWidth);
+      if (w > window.innerWidth) {
+        w = window.innerWidth;
       }
-    }
+      return w;
+    },
+  },
+  components: {
+    Details,
   },
   props: {
     value: {
       type: Boolean,
-      default: false
-    }
-
+      default: false,
+    },
   },
   watch: {
     value(val) {
-      this.visible = val
+      this.visible = val;
       if (this.visible) {
         this.handleDefaultList();
         this.handleInitSwiper();
-
       }
-    }
+    },
   },
   data() {
-
     return {
+      secondId: null,
+      secondName: '',
+      tabW: 0,
       activeName: "",
       primaryList: [],
       secondaryList: [],
       visible: false,
-      isLoaded: false
-    }
+      isLoaded: true,
+      swiperOptions: {
+        slidesPerView: "auto",
+        // centeredSlides: true,
+        // slideToClickedSlide: true,
+        // spaceBetween: 10,
+        // slidesPerView: 3,
+        // centerInsufficientSlides: true,
+        // centeredSlidesBounds: true,
+        // freeMode: {
+        //   enabled: true,
+        //   sticky: false,
+        //   momentumBounce: false,
+        //   // momentumVelocityRatio: 0.5,
+        // },
+      },
+      cardOptions: {
+        freeMode: true,
+        direction: "horizontal",
+        observer: true,
+        observeParents: true,
+        slidesPerView: "auto",
+      },
+    };
   },
 
   methods: {
-    handleClick() {
+    gotoItem(i) {
+      this.secondId = i.id;
+      this.secondName = i.name;
+      console.error(this.secondName);
+    },
+    handleClick(id) {
+      this.activeName = String(id);
       this.handleSecondaryList();
     },
     async handleDefaultList() {
       const res = await getTopicList();
-      this.primaryList = res.data || []
+      this.primaryList = res.data || [];
       if (res.data.length > 0) {
-        this.activeName = String(res.data[0].id)
+        this.activeName = String(res.data[0].id);
+        this.initTabSwiper();
         await this.handleSecondaryList();
       }
     },
+    initTabSwiper() {
+      this.$nextTick(() => {
+        this.tabW = this.$refs.tabRef.getBoundingClientRect().width;
+        if (this.tabW > window.innerWidth) {
+          this.tabW = window.innerWidth;
+        }
+        setTimeout(() => {
+          new Swiper(".swiper-container-tab", this.swiperOptions);
+        }, 0);
+      });
+    },
     async handleSecondaryList() {
       const res = await getTopicSecondaryList(this.activeName);
-      console.log('res', res.data)
-      this.secondaryList = res.data || []
+      console.log("res", res.data);
+      this.secondaryList = res.data || [];
     },
     handleModalClose() {
-      this.$emit('close')
+      this.$emit("close");
     },
     handleInitSwiper() {
       this.$nextTick(() => {
         setTimeout(() => {
-          new Swiper(".swiper-container", {
-            freeMode: true,
-            direction: 'horizontal',
-            observer: true,
-            observeParents: true,
-            slidesPerView: 'auto'
-          });
+          new Swiper(".swiper-container-card", this.cardOptions);
           this.isLoaded = true;
         }, 1000);
       });
-    }
+    },
   },
-  mounted() {
-
-  }
-
-}
+  mounted() {},
+};
 </script>
 
 <style scoped lang="less">
@@ -117,138 +179,209 @@ export default {
   background: rgba(33, 27, 18, 0.5);
   backdrop-filter: blur(20px);
   font-family: "思源宋体";
-
-  .close-btn {
-    cursor: pointer;
-    top: -7px;
-    right: 104px;
-    position: absolute;
-    width: 66px;
-    height: 105px;
-    z-index: 10;
-    background: url("../../assets/img/close.png");
-    background-size: 100% 100%;
-  }
-
-  /deep/ .el-tabs {
-    display: flex;
-    flex-direction: column;
-    height: 100%;
+  .info-box {
     width: 100%;
+    height: 100%;
+    &.hide {
+      opacity: 0;
+      pointer-events: none;
+    }
+    .close-btn {
+      cursor: pointer;
+      top: -7px;
+      right: 104px;
+      position: absolute;
+      width: 66px;
+      height: 105px;
+      z-index: 10;
+      background: url("../../assets/img/close.png");
+      background-size: 100% 100%;
+    }
 
-    .el-tabs__header {
-      //height: 44px;
-      height: 150px;
+    /deep/ .el-tabs {
       display: flex;
-      align-items: center;
-      justify-content: center;
-
-      .el-tabs__nav-wrap::after {
-        height: 0;
-      }
+      flex-direction: column;
+      height: 100%;
+      width: 100%;
+
+      .el-tabs__header {
+        //height: 44px;
+        height: 150px;
+        display: flex;
+        align-items: flex-end;
+        padding: 0 0 10px 0;
+        justify-content: center;
+
+        .el-tabs__nav-wrap::after {
+          height: 0;
+        }
 
-      .el-tabs__item {
-        padding: 5px 80px;
-        height: auto;
-        box-sizing: border-box;
-        display: inline-block;
-        list-style: none;
-        font-size: 36px;
-        font-weight: 500;
-        color: #ffffff;
-        position: relative;
-      }
+        .el-tabs__item {
+          padding: 5px 80px;
+          height: auto;
+          box-sizing: border-box;
+          display: inline-block;
+          list-style: none;
+          font-size: 36px;
+          font-weight: 500;
+          color: #ffffff;
+          position: relative;
+        }
 
-      .el-tabs__active-bar {
-        height: 9px;
-        background-color: transparent;
+        .el-tabs__active-bar {
+          height: 9px;
+          background-color: transparent;
 
-        &:before {
-          content: '';
-          position: absolute;
-          width: 50%;
-          transform: translateX(50%);
-          height: 100%;
+          &:before {
+            content: "";
+            position: absolute;
+            width: 50%;
+            transform: translateX(50%);
+            height: 100%;
 
-          left: 0;
-          background-color: #D9C8A9;
+            left: 0;
+            background-color: #d9c8a9;
+          }
         }
       }
-    }
 
-    .el-tab-pane, .el-tabs__content {
-      flex: 1;
-      display: flex;
+      .el-tab-pane,
+      .el-tabs__content {
+        // flex: 1;
+        display: flex;
+      }
     }
-  }
-
-  .swiper-container {
-    height: 100%;
-    width: 100%;
-  }
-
-  .card-list {
-    user-select: none;
-    display: flex;
-    justify-content: center;
-    height: 100%;
-    width: 100%;
-    gap: 0 80px;
-
-    .card-item {
-      width: 423px;
-      height: 681px;
-      position: relative;
-      z-index: 1;
-      cursor: pointer;
-
-      &:after {
-        content: "";
-        width: calc(100% - 30px);
-        height: calc(100% + 20px);
-        position: absolute;
-        top: 0;
-        left: 15px;
-        z-index: -1;
-        background: #806D52;
+    .card {
+      height: calc(100% - 155px);
+      display: flex;
+      align-items: center;
+      margin: 0 auto;
+      .swiper-container-card {
+        width: 100%;
+        margin-top: -30px;
+        .swiper-slide {
+          margin: 0 25px;
+        }
       }
+    }
 
-      .inner_img {
-        width: calc(100% - 60px);
-        height: 100%;
-        margin: 10px auto 0 auto;
-        background-repeat: no-repeat;
-        background-size: cover;
-        background-position: center center;
-        object-fit: scale-down;
+    .swiper-container {
+      height: 100%;
+      width: 100%;
+    }
+    .tab {
+      width: auto;
+      // overflow: hidden;
+      margin: 105px auto 0;
+      height: 50px;
+      .swiper-container-tab {
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        .tab-list {
+          .swiper-slide {
+            width: auto;
+            margin: 0 75px;
+            .tab-name {
+              color: #fff;
+              font-size: 36px;
+              cursor: pointer;
+
+              &.active {
+                color: rgba(217, 200, 169, 1);
+                position: relative;
+                &::after {
+                  content: "";
+                  position: absolute;
+                  width: 97px;
+                  height: 9px;
+                  background: #d9c8a9;
+                  border-radius: 0px 0px 0px 0px;
+                  bottom: -3px;
+                  left: 50%;
+                  transform: translateX(-50%);
+                }
+              }
+            }
+          }
+        }
       }
-
-      .inner_title {
-        width: 100%;
-        font-weight: bold;
-        font-size: 24px;
-        color: #FFFFFF;
-        line-height: 30px;
-        letter-spacing: 12px;
-        text-align: center;
-        font-style: normal;
-        position: absolute;
-        bottom: 40px;
+    }
+    .get-width {
+      display: inline-block;
+      position: fixed;
+      opacity: 0;
+      pointer-events: none;
+      > div {
+        color: #fff;
+        font-size: 36px;
+        width: auto;
+        margin: 0 75px;
+        display: inline-block;
       }
+    }
+    .card-list {
+      user-select: none;
+
+      // display: flex;
+      // justify-content: center;
+      // height: 100%;
+      // width: 100%;
+      // gap: 0 80px;
+
+      .card-item {
+        width: 423px;
+        height: 681px;
+        position: relative;
+        z-index: 1;
+        cursor: pointer;
 
-      .bg {
-        width: 100%;
-        height: 100%;
-        position: absolute;
-        top: 10px;
-        left: 0;
-        background: url("../../assets/img/topic_frame.png");
-        z-index: 2;
+        &:after {
+          content: "";
+          width: calc(100% - 30px);
+          height: calc(100% + 20px);
+          position: absolute;
+          top: 0;
+          left: 15px;
+          z-index: -1;
+          background: #806d52;
+        }
 
-      }
+        .inner_img {
+          width: calc(100% - 60px);
+          height: 100%;
+          margin: 10px auto 0 auto;
+          background-repeat: no-repeat;
+          background-size: cover;
+          background-position: center center;
+          object-fit: scale-down;
+        }
 
+        .inner_title {
+          width: 100%;
+          font-weight: bold;
+          font-size: 24px;
+          color: #ffffff;
+          line-height: 30px;
+          letter-spacing: 12px;
+          text-align: center;
+          font-style: normal;
+          position: absolute;
+          bottom: 40px;
+          text-shadow: 2px 2px 2px rgba(0, 0, 0, 0.3);
+        }
 
+        .bg {
+          width: 100%;
+          height: 100%;
+          position: absolute;
+          top: 10px;
+          left: 0;
+          background: url("../../assets/img/topic_frame.png");
+          z-index: 2;
+        }
+      }
     }
   }
 }
-</style>
+</style>

Різницю між файлами не показано, бо вона завелика
+ 10056 - 0
yarn.lock