Эх сурвалжийг харах

feat: 类别管理完善
1. 类别新增关键字字段
2. 文章类别管理

kindring 1 жил өмнө
parent
commit
89e588c364

+ 8 - 9
README.md

@@ -51,20 +51,19 @@ pm2 list
 ### 文件库功能 file_lab
 > id , 图片名称 , 图片路径 , 图片tag , 上传日期
 
-| 字段 | 类型 | 可选值 | 默认值 | 备注 |
-| --- | --- | --- | --- | --- |
-| id | int | pk | pk | 文件id |
-| imgName | varchar | '' | '' | 图片名称 |
-| fileType | char | '0:other','1:video','2:img' | 0 | 文件类型 |
-| tags | varchar | '' | '' | 图片tag,用,隔开,用于查询关键字 |
-| filePath | varchar | '' | '' | 资源路径 |
-| uploadTime | varchar | '' | '' | 上传时间 |
+| 字段 | 类型 | 可选值                                 | 默认值 | 备注 |
+| --- | --- |-------------------------------------| --- | --- |
+| id | int | pk                                  | pk | 文件id |
+| imgName | varchar | ''                                  | '' | 图片名称 |
+| fileType | char | '0:other','1:video','2:img', 4: svg | 0 | 文件类型 |
+| tags | varchar | ''                                  | '' | 图片tag,用,隔开,用于查询关键字 |
+| filePath | varchar | ''                                  | '' | 资源路径 |
+| uploadTime | varchar | ''                                  | '' | 上传时间 |
 
 
 ### 轮播表
 > 用于存储轮播数据,可以选择站内产品或者新闻或者是特殊页面  
 
-
 | 字段            | 类型 | 可选值                                   | 默认值 | 备注     |
 |---------------| --- |---------------------------------------| --- |--------|
 | id            | int | pk                                    | pk | 轮播id   |

+ 13 - 12
components/IconSvg/iconSvg.vue

@@ -1,6 +1,9 @@
 <template>
-  <svg :class="svgClass" aria-hidden="true">
-    <use :xlink:href="iconName"/>
+  <svg class="svg-icon" aria-hidden="true">
+    <image v-if="svgHref" :xlink:href="svgHref" />
+
+    <use v-else :xlink:href="_iconName"/>
+<!--    使用 链接 加载 svg-->
   </svg>
 </template>
 
@@ -10,24 +13,17 @@ export default {
   props: {
     iconClass: {
       type: String,
-      required: true,
+      default: '',
     },
-    className: {
+    svgHref: {
       type: String,
       default: '',
     },
   },
   computed: {
-    iconName () {
+    _iconName () {
       return `#icon-${this.iconClass}`
     },
-    svgClass () {
-      if (this.className) {
-        return 'svg-icon ' + this.className
-      } else {
-        return 'svg-icon'
-      }
-    },
   },
 }
 </script>
@@ -40,4 +36,9 @@ export default {
   fill: currentColor;
   overflow: hidden;
 }
+.svg-icon image {
+  width: 100%;
+  height: 100%;
+  fill: currentColor;
+}
 </style>

+ 3 - 1
components/header/menuDrops/menuDrop.vue

@@ -69,7 +69,9 @@ export default {
     pHref: {default:''}
   },
   computed:{
-      products(){return this.$store.getters.productTypes},
+      products(){
+        return this.$store.getters.productTypes.filter(item=>item.typeKey!=='all')
+      },
       solutions(){return this.$store.getters.solutionTypes},
   },
   // 使用vuex getter 获取数据

+ 1 - 0
components/productList.vue

@@ -43,6 +43,7 @@ export default {
   data(){
     return {
       langType: langMap.lang,
+
     }
   },
   methods: {

+ 13 - 7
components/productTypes.vue

@@ -1,7 +1,7 @@
 <template>
   <div class="w-screen pad:w-full">
     <big-title>{{lang===langType.cn?"产品类别":getAbbrText("产品类别")}}</big-title>
-    <div class="conBox product-type">
+    <div class="conBox container product-type">
       <p
         v-for="(item,i) in types"
         :key="item.key"
@@ -9,9 +9,9 @@
         @click="selectType(item.key)"
       >
         <span class="icon-box">
-          <svg-icon :icon-class="item.icon"/>
+          <svg-icon :svgHref="item.icon"/>
         </span>
-        <span class="type-name">{{lang===langType.cn?item.text:getAbbrText(item.text)}}</span>
+        <span class="type-name">{{lang===langType.cn?item.text:item.en_text}}</span>
       </p>
     </div>
   </div>
@@ -31,12 +31,18 @@ export default {
     },
     type:{
       default: 'all'
+    },
+    types: {
+      type: Array,
+      required: true
+    },
+    title: {
+      default: '产品类别'
     }
   },
   data(){
     return {
       langType: langMap.lang,
-      types: pTypes
     }
   },
   methods:{
@@ -51,7 +57,7 @@ export default {
       if (this.type === nextType){
         return 0;
       }
-      this.$root.$emit('changeProductType',nextType);
+      this.$root.$emit('changeType', nextType);
     }
   }
 }
@@ -67,8 +73,8 @@ export default {
 }
 .product-type{
   height: 120px;
-  display: grid;
-  grid-template-columns: repeat(6,1fr);
+  display: flex;
+  justify-content: space-around;
 }
 
 .product-type .type-item{

+ 23 - 5
components/public/imageTable.vue

@@ -14,11 +14,16 @@
           <div
               v-for="item in images"
               :key="item.path"
-              :class="`img-viewItem ${item.filePath === imgUrl?'img-viewItem-select bg-red-300':'bg-gray-400'}`"
+              :class="`img-viewItem
+              ${item.filePath === imgUrl?'img-viewItem-select bg-red-300':'bg-gray-400'}
+              ${fileType === fileTypes.svg?'svgImage':''}
+              `"
               :style="imgItemStyle"
               @click="selectImg(item)"
           >
-            <img class="w-auto h-full" :src="item.filePath" alt="item.fileName">
+            <img v-if="fileType === fileTypes.image" class="w-auto h-full" :src="item.filePath" alt="item.fileName">
+<!--            icon 类型-->
+            <svg-icon v-else-if="fileType === fileTypes.svg" :svgHref="item.filePath"></svg-icon>
           </div>
         </div>
         <template v-slot:loadFail>
@@ -35,7 +40,7 @@
         <div class="w-full h-32 rounded
         border overflow-hidden flex-shrink">
           <upload-file
-            :type="1"
+            :type="fileType"
             :multiple="true"
             @change="uploadChangeHandle"
           > </upload-file>
@@ -123,18 +128,23 @@ export default {
       type: Number,
       default: 5
     },
+    // 文件类型 1图片 2视频 3音频 4svg 文件
+    fileType: {
+      type: Number,
+      default: db_base.fileType.image
+    }
   },
   data(){
     return {
-      uploadUrl: `/api/base/fileUp?type=${db_base.fileType.image}`,
+      uploadUrl: `/api/base/fileUp`,
       tabKey: 1,
       imgUrl:'',
       fileData: {},
       loadingState: 0,
       loadingMessage: '',
       searchKey: '',
-      fileType: db_base.fileType.image,
       fileState: fileState,
+      fileTypes: db_base.fileType,
       fileList: [],
       images: [],
       imgBoxStyle: '',
@@ -260,6 +270,7 @@ export default {
       file.showInfo_type = 'info';
       let form = new FormData();
       form.append('file',file.file);
+      this.uploadUrl += `?type=${this.fileType}`;
       let [err,res] = await handle(this.$axios.post(this.uploadUrl,form,{
         onUploadProgress: (progressEvent) => {
           file.percent = Math.round((progressEvent.loaded * 100) / progressEvent.total) || 0;
@@ -326,6 +337,13 @@ export default {
   justify-content: flex-start;
   overflow: auto;
 }
+.svgImage{
+  display: flex;
+  color: #1a1a1a;
+  font-size: 65px;
+  justify-content: center;
+  align-items: center;
+}
 .img-viewItem{
   cursor: pointer;
   flex-shrink: 0;

+ 3 - 1
components/public/popCard.vue

@@ -4,7 +4,7 @@
 
 <template>
   <div class="w-8/12 h-5/6 p-2 rounded flex flex-col justify-center border bg-white">
-    <div class="p-header mx-1.5 h-16  text-2xl flex border-b items-center">
+    <div class="p-header mx-1.5 h-16  text-2xl flex border-b items-center py-1.5">
       <div class="icon-btn-group">
         <slot name="close-group"></slot>
       </div>
@@ -33,5 +33,7 @@
   right: 0;
   top: 0;
   height: 100%;
+  display: flex;
+  align-content: center;
 }
 </style>

+ 2 - 0
components/public/uploadFile.vue

@@ -4,6 +4,8 @@ const acceptMap = {
   1: 'image/*',
   2: 'video/*',
   3: 'audio/*',
+  // svg
+  4: 'image/svg+xml',
 };
 export default {
   name: 'uploadFile',

+ 163 - 13
components/typeEdit.vue

@@ -4,9 +4,13 @@ import {rCode} from "../map/rcodeMap_esm";
 import {handle} from "../until/handle";
 import InputRow from "./public/form/inputRow.vue";
 import RoundedTitle from "./public/roundedTitle.vue";
-import dbField_esm from "../map/dbField_esm";
+import dbField_esm, {db_base} from "../map/dbField_esm";
 import {FormVerify} from "kind-form-verify";
 import {fieldCheck} from "../until/form/fieldVerify";
+import Pop from "./public/pop.vue";
+import PopCard from "./public/popCard.vue";
+import ImageTable from "./public/imageTable.vue";
+import {urlAddParam} from "@/until/url";
 
 const emptyType = {
   type_name: '',
@@ -17,7 +21,7 @@ const emptyType = {
 let formVerify = null;
 export default {
   name: 'typeEdit',
-  components: {RoundedTitle, InputRow},
+  components: {ImageTable, PopCard, Pop, RoundedTitle, InputRow},
   props: {
   },
   data () {
@@ -34,11 +38,11 @@ export default {
           state: 0,
         },
         type_logo: {
-          val: "",
-          oldVal: "",
-          init: "",
-          msg: "",
-          state: 0,
+          val: '',
+          init: '',
+          msg: '',
+          reCheckField: 'fileData',
+          state: 0
         },
         type_sort: {
           val: 0,
@@ -54,9 +58,37 @@ export default {
           msg: "",
           state: 0,
           reCheckField: 'type_name',
+        },
+        seo_key: {
+          val: "",
+          oldVal: "",
+          init: "",
+          msg: "",
+          state: 0,
         }
       },
-      api: ''
+      api: '',
+      queryParams : {},
+      showLogoDialog: false,
+      fileType: db_base.fileType.svg,
+      loading: false,
+    }
+  },
+  computed: {
+    tags () {
+      // 拆分tags
+      let tags = this.formType.seo_key.val
+      if (!tags) {
+        return [];
+      }
+      // 去除空字符串
+      tags = tags.replace(/\s/g,'')
+      let tagArr = tags.split(',')
+      // 标签去重
+      tagArr = [...new Set(tagArr)]
+      // 移除空标签
+      tagArr = tagArr.filter(item => item)
+      return tagArr
     }
   },
   mounted() {
@@ -74,9 +106,10 @@ export default {
      * @param api
      * @param object
      */
-    init (title, api, object) {
+    init (title, api, object, queryParams) {
 
       this.api = api
+      this.queryParams = queryParams ? queryParams : {}
       if (!api){
         this.$message.error('请设置api地址')
         throw new Error('请设置api地址')
@@ -96,6 +129,7 @@ export default {
       this.formType.type_logo.init = type.type_logo
       this.formType.type_sort.init = type.type_sort
       this.formType.type_key.init = type.type_key
+      this.formType.seo_key.init = type.seo_key
       this.isInitialized = true
       formVerify = new FormVerify(
         this.formType,
@@ -106,6 +140,9 @@ export default {
         console.log(msg);
       };
     },
+    updateQueryParams(params){
+      this.queryParams = params
+    },
     initForm() {
 
     },
@@ -116,26 +153,57 @@ export default {
     handleSave () {
       this.executeSubmitData()
     },
+    deleteTag(tag){
+      let tagArr = this.tags
+      let index = tagArr.indexOf(tag)
+      tagArr.splice(index, 1)
+      this.formType.seo_key.val = tagArr.join(',')
+      this.formType.seo_key.msg = ''
+    },
     handleCancel () {
       this.close()
     },
     handleReset() {
       this.initForm()
     },
+    openLogoHandle() {
+      this.showLogoDialog = true
+    },
+    closeLogoHandle() {
+      this.showLogoDialog = false
+    },
+    selectImageHandle(fileData){
+      console.log('okHandle');
+      console.log(fileData);
+      this.showLogoDialog = false;
+      // 选择封面图片
+      this.formType.type_logo.val = fileData.filePath;
+      this.formType.type_logo.init = fileData.filePath;
+      this.formType.type_logo.msg = '';
+      this.formType.type_logo.state = 0;
+      this.formType.type_logo.showText = fileData.filePath;
+    },
     async executeSubmitData(){
       if(!this.isInitialized)
       {
         throw new Error('请先初始化类别编辑器')
       }
       let url = this.api;
+      // 拼接param参数
       if(this.typeId){
-        url = url + '?id=' + this.typeId
+         this.queryParams.id = this.typeId
       }
+      url = urlAddParam(url, this.queryParams)
+      console.log(url)
       // 检测参数
       if(!formVerify.check()) {
           return this.$message.error('参数错误')
       }
       let params = formVerify.getFormData()
+      params = {
+        ...params,
+        ...this.queryParams,
+      }
       let [err, res] = await handle(axios.post(url, params))
       if(err){
         this.$message.error(`[${this.title}] 失败:${err.message}`)
@@ -161,13 +229,26 @@ export default {
     <rounded-title class="text-xl">
       <span>{{ title }}</span>
     </rounded-title>
+
     <input-row class="mt-2" :msg="formType.type_name.msg"
                label="类型名称">
       <a-input v-model="formType.type_name.val" placeholder="类型名称"></a-input>
     </input-row>
     <input-row class="mt-2" :msg="formType.type_logo.msg"
                label="类型logo">
-      <a-input v-model="formType.type_logo.val" placeholder="类型logo"></a-input>
+      <div class="svg-upload">
+        <svg-icon  :svgHref="formType.type_logo.val"></svg-icon>
+        <div v-if="!formType.type_logo.val" class="null-box">
+          选择图标
+        </div>
+        <div class="absolute w-full h-full left-0 top-0
+              justify-center text-white bg-gray-400
+              items-center text-2xl flex opacity-0 hover:opacity-70"
+             @click="openLogoHandle"
+        >
+          点击选择图标文件
+        </div>
+      </div>
     </input-row>
     <input-row class="mt-2" :msg="formType.type_sort.msg"
                label="排序">
@@ -176,7 +257,38 @@ export default {
     </input-row>
     <input-row class="mt-2" :msg="formType.type_key.msg"
                label="类型关键字key">
-      <a-input v-model="formType.type_key.val" placeholder="类型关键字key"></a-input>
+      <a-tooltip>
+        <template slot="title">
+          用于浏览器的搜索关键字, 优化用户访问体验, 禁止重复
+        </template>
+        <a-input v-model="formType.type_key.val" placeholder="类型关键字key"></a-input>
+      </a-tooltip>
+    </input-row>
+
+
+    <input-row class="mt-2" :msg="formType.seo_key.msg"
+               label="seo关键字">
+      <a-tooltip>
+        <template slot="title">
+          搜索引擎关注的关键字,多个用英文逗号隔开
+        </template>
+        <a-input v-model="formType.seo_key.val" placeholder="seo关键字"></a-input>
+      </a-tooltip>
+
+    </input-row>
+
+    <input-row class="mt-2"
+               label="">
+      <div class="tags">
+        <a-tag
+          v-for="tag in tags"
+          :key="tag"
+          color="#2db7f5"
+          closable @close="deleteTag"
+        >
+          {{ tag }}
+        </a-tag>
+      </div>
     </input-row>
 
     <div class="mt-2">
@@ -184,9 +296,47 @@ export default {
       <a-button type="primary" @click="handleSave">确认</a-button>
     </div>
 
+    <pop :show="showLogoDialog" :loading="loading">
+      <pop-card>
+        <template slot="close-group">
+          <a-button type="danger" @click="closeLogoHandle">关闭</a-button>
+        </template>
+        <image-table
+          class="w-full h-full"
+          :fileType="fileType"
+          @cancel="closeLogoHandle"
+          @ok="selectImageHandle">
+        </image-table>
+      </pop-card>
+    </pop>
   </div>
 </template>
 
 <style scoped>
-
+.svg-upload{
+  width: 90px;
+  height: 90px;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  position: relative;
+  border: 1px solid #ccc;
+  border-radius: 5px;
+  font-size: 45px;
+}
+.svg-upload .null-box{
+  width: 100%;
+  height: 100%;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  font-size: 20px;
+}
+.tags{
+  width: 100%;
+  display: flex;
+  flex-wrap: wrap;
+  justify-content: flex-start;
+  align-items: center;
+}
 </style>

+ 2 - 2
map/adminSideBar.js

@@ -62,8 +62,8 @@ export const adminMenus = [
       },
       {
         key: 'productType',
-        title: '产品类别管理',
-        path: '/manger/product/type'
+        title: '文章类别管理',
+        path: '/manger/news/type'
       }
     ],
   }

+ 15 - 0
map/apiMap.js

@@ -31,9 +31,24 @@ export const apiMap = {
   productEditType: {
     path: `/api/product/type/edit`
   },
+  productDeleteType: {
+    path: `/api/product/type/del`
+  },
   searchSolution: {
     path: `/api/solution/search`,
   },
+  newsTypes: {
+    path: `/api/news/types`
+  },
+  addNewsType: {
+    path: `/api/news/type/add`
+  },
+  editNewsType: {
+    path: `/api/news/type/edit`
+  },
+  deleteNewsType: {
+    path: `/api/news/type/del`
+  },
   solutionInfo: {
     path: `/api/solution`
   },

+ 2 - 0
map/dbField_esm.js

@@ -22,6 +22,8 @@ export const db_base = {
     other: 0,
     image: 1,
     video: 2,
+    audio: 3,
+    svg: 4
   },
   // 轮播类型
   carouselType: {

+ 6 - 1
nuxt.config.js

@@ -173,7 +173,12 @@ let seoOption = {
 }
 export default {
     // Global page headers (https://go.nuxtjs.dev/config-head)
-    head: {
+  alias: {
+    '@': path.resolve(__dirname),
+    '~': path.resolve(__dirname),
+  },
+
+  head: {
         title: '深圳市合方圆科技',
         meta: [
           { charset: 'utf-8' },

+ 1 - 1
pages/manger/index.vue

@@ -1,7 +1,7 @@
 <script>
 
 
-import AdminLayout from "~/components/layout/adminLayout.vue";
+import AdminLayout from "@/components/layout/adminLayout.vue";
 import {adminMenus} from "~/map/adminSideBar";
 import Vue from 'vue'
 import antd from 'ant-design-vue'

+ 263 - 0
pages/manger/news/type.vue

@@ -0,0 +1,263 @@
+<script>
+
+import RoundedTitle from "../../../components/public/roundedTitle.vue";
+import PopCard from "../../../components/public/popCard.vue";
+import TypeEdit from "../../../components/typeEdit.vue";
+import Pop from "../../../components/public/pop.vue";
+import ImageTable from "../../../components/public/imageTable.vue";
+import ImageViewer from "../../../components/public/imageViewer.vue";
+import TableSelect from "../../../components/public/tableSelect.vue";
+import {handle} from "../../../until/handle";
+import axios from "axios";
+import {apiMap} from "../../../map/apiMap";
+import {rCode} from "../../../map/rcodeMap_esm";
+import dbField_esm from "@/map/dbField_esm";
+
+const typeColumns = [
+  {
+    title: 'ID',
+    dataIndex: 'type_id',
+    width: '5%',
+  },
+  {
+    title: '图标',
+    dataIndex: 'type_logo',
+    width: '10%',
+    scopedSlots: {customRender: 'type_logo'},
+  },
+  {
+    title: '类型名称',
+    dataIndex: 'type_name',
+    width: '15%',
+  },
+  {
+    title: '类型关键字',
+    dataIndex: 'type_key',
+    width: '20%',
+  },
+  {
+    title: '搜索关键字',
+    dataIndex: 'seo_key',
+    width: '30%',
+    scopedSlots: {customRender: 'seo_key'},
+  },
+  {
+    title: '操作',
+    scopedSlots: {customRender: 'operation'},
+  }
+];
+export default {
+  name: 'newsType',
+  components: {ImageTable, TypeEdit, PopCard, Pop, ImageViewer, TableSelect, RoundedTitle},
+  data() {
+    return {
+      typeColumns,
+      loading: false,
+      types: [],
+      page: 1,
+      pageSize: 10,
+      typeEditShow: false,
+      parentType: dbField_esm.db_base.newsType.solution,
+      parentTypes: [
+        {
+          key: dbField_esm.db_base.newsType.solution,
+          value: '解决方案',
+        },
+        {
+          key: dbField_esm.db_base.newsType.news,
+          value: '新闻',
+        },
+
+      ]
+    }
+  },
+  beforeMount() {
+    this.getTypes();
+  },
+  computed: {
+    showTypes() {
+        return this.types.filter(item =>
+          item.parent_type === this.parentType);
+    }
+  },
+  methods: {
+    // 获取产品类型列表
+    async getTypes() {
+      this.loading = true;
+      let [err, res] = await handle(axios.get(apiMap.newsTypes.path));
+      this.loading = false;
+      if (err) {
+        this.$message.error(err);
+        return {};
+      }
+      let result = res.data;
+      if (result.code !== rCode.OK) {
+        this.$message.error(result.msg);
+        return {};
+      }
+      console.log(result.data)
+      this.types = result.data;
+    },
+    parseTags(tagArrStr){
+      let tags = tagArrStr
+      if (!tags) {
+        return [];
+      }
+      // 去除空字符串
+      tags = tags.replace(/\s/g,'')
+      let tagArr = tags.split(',')
+      // 标签去重
+      tagArr = [...new Set(tagArr)]
+      // 移除空标签
+      tagArr = tagArr.filter(item => item)
+      return tagArr
+    },
+    handleAddType() {
+      this.opentypeEdit();
+      this.$nextTick(() => {
+        this.$refs.type_edit.init("文章类型", apiMap.addNewsType.path,
+          {
+            parent_type: this.parentType,
+          })
+      })
+    },
+    parentTypeChange(value) {
+      if (!this.typeEditShow )
+      {
+        return;
+      }
+      this.$nextTick(() => {
+        this.$refs.type_edit.updateQueryParams({
+          parent_type: this.parentType,
+        });
+      })
+    },
+    handleEditType(item){
+      //
+      this.opentypeEdit();
+      this.$nextTick(() => {
+        this.$refs.type_edit.init("文章类型",
+          apiMap.editNewsType.path, item,
+          {
+            parent_type: this.parentType,
+          }
+          );
+      })
+    },
+    handleDeleteType(item){
+      // 询问是否要删除
+      this.$confirm({
+        content: '确定要删除吗?',
+        okText: '确定',
+        cancelText: '取消',
+        onOk: () => {
+          this.executeDeleteType(item);
+        }});
+    },
+    handleCloseEdit(){
+      this.closeTypeEdit();
+    },
+    opentypeEdit(){
+      this.typeEditShow = true;
+    },
+    closeTypeEdit(){
+      this.typeEditShow = false;
+      this.getTypes();
+    },
+    async executeDeleteType(item){
+      let url = apiMap.productDeleteType.path;
+      url += `?id=${item.type_id}`
+      let [err,res] = await handle(axios.delete(url, {id: item.type_id}));
+      if(err){
+        this.$message.error(`[删除类型] 失败:${err.message}`)
+        console.log(err)
+        return err
+      }
+      let result = res.data
+      if(result.code !== rCode.OK) {
+        this.$message.error(`[移除类型] 失败:${result.msg}`)
+        return result
+      }
+      this.$message.success(`[移除类型] 成功`)
+      this.closeTypeEdit();
+    }
+  }
+}
+
+</script>
+
+<template>
+  <div class="w-full p-2 h-full" >
+    <rounded-title class="text-xl">
+      <span>文章分类管理</span>
+      <a href="/manger/news"
+         class="inline-flex px-10 h-full ml-5 rounded bg-blue-400 text-white cursor-pointer
+         hover:text-orange-500 ">文章中心</a>
+      <a-button
+        class="ml-2 flex items-center"
+        type="primary"
+        @click="handleAddType"
+      >新增
+      </a-button>
+    </rounded-title>
+    <div class="w-full rounded border mt-2 bg-white p-2">
+<!--      tab 选择 -->
+      <a-radio-group
+        v-model="parentType"
+        @change="parentTypeChange"
+      >
+        <a-radio-button
+          v-for="item in parentTypes"
+          :key="item.key"
+          :value="item.key"
+        >
+          {{ item.value }}
+        </a-radio-button>
+      </a-radio-group>
+      <!--      一级分类 -->
+      <a-table
+        :columns="typeColumns"
+        :loading="loading"
+        :row-key="record => record.id"
+        :pagination="false"
+        :data-source="showTypes"
+      >
+        <template class="logo_view" slot="type_logo" slot-scope="text,record">
+          <svg-icon :class="''" :svgHref="text"></svg-icon>
+        </template>
+        <template class="tags" slot="seo_key" slot-scope="text,record">
+          <a-tag
+            v-for="tag in parseTags(text)"
+            :key="tag"
+            color="#2db7f5"
+          >
+            {{ tag }}
+          </a-tag>
+        </template>
+        <template class="flex justify-center" slot="operation" slot-scope="text,record">
+          <a-button
+            type="primary"
+            @click="handleEditType(record)"
+          >编辑
+          </a-button>
+          <a-button type="danger" @click="handleDeleteType(record)">移除</a-button>
+        </template>
+      </a-table>
+    </div>
+
+
+
+    <pop :show="typeEditShow" :loading="loading">
+      <pop-card>
+        <template slot="close-group">
+          <a-button type="danger" @click="handleCloseEdit">关闭</a-button>
+        </template>
+        <type-edit ref="type_edit" @close="handleCloseEdit"></type-edit>
+      </pop-card>
+    </pop>
+  </div>
+</template>
+
+<style scoped>
+
+</style>

+ 80 - 7
pages/manger/product/type.vue

@@ -1,7 +1,7 @@
 
 
 <template>
-  <div class="w-full p-2 h-full">
+  <div class="w-full p-2 h-full" >
     <rounded-title class="text-xl">
       <span>产品分类管理</span>
       <a href="/manger/product" class="inline-flex px-10 h-full ml-5 rounded bg-blue-400 text-white cursor-pointer hover:text-orange-500 ">产品中心</a>
@@ -13,6 +13,7 @@
       </a-button>
     </rounded-title>
 
+
     <div class="w-full rounded border mt-2 bg-white p-2">
 <!--      产品类型列表 -->
       <a-table
@@ -22,20 +23,34 @@
         :pagination="false"
         :data-source="productTypes"
       >
+        <template class="logo_view" slot="type_logo" slot-scope="text,record">
+          <svg-icon :class="''" :svgHref="text"></svg-icon>
+        </template>
+        <template class="tags" slot="seo_key" slot-scope="text,record">
+          <a-tag
+            v-for="tag in parseTags(text)"
+            :key="tag"
+            color="#2db7f5"
+          >
+            {{ tag }}
+          </a-tag>
+        </template>
         <template class="flex justify-center" slot="operation" slot-scope="text,record">
           <a-button
             type="primary"
             @click="handleEditType(record)"
           >编辑
           </a-button>
+          <a-button type="danger" @click="handleDeleteType(record)">移除</a-button>
 
         </template>
       </a-table>
     </div>
+
     <pop :show="typeEditShow" :loading="loading">
       <pop-card>
         <template slot="close-group">
-          <a-button type="danager" @click="handleCloseEdit">关闭</a-button>
+          <a-button type="danger" @click="handleCloseEdit">关闭</a-button>
         </template>
         <type-edit ref="type_edit" @close="handleCloseEdit"></type-edit>
       </pop-card>
@@ -56,6 +71,8 @@ import {rCode} from "../../../map/rcodeMap_esm";
 import Pop from "../../../components/public/pop.vue";
 import PopCard from "../../../components/public/popCard.vue";
 import TypeEdit from "../../../components/typeEdit.vue";
+import ImageTable from "../../../components/public/imageTable.vue";
+import {db_base} from "../../../map/dbField_esm";
 
 const productTypeColumns = [
   {
@@ -67,6 +84,7 @@ const productTypeColumns = [
     title: '图标',
     dataIndex: 'type_logo',
     width: '10%',
+    scopedSlots: {customRender: 'type_logo'},
   },
   {
     title: '类型名称',
@@ -78,6 +96,12 @@ const productTypeColumns = [
     dataIndex: 'type_key',
     width: '20%',
   },
+  {
+    title: '搜索关键字',
+    dataIndex: 'seo_key',
+    width: '30%',
+    scopedSlots: {customRender: 'seo_key'},
+  },
   {
     title: '操作',
     scopedSlots: {customRender: 'operation'},
@@ -85,7 +109,7 @@ const productTypeColumns = [
 ];
 export default {
   name: "productType",
-  components: {TypeEdit, PopCard, Pop, ImageViewer, TableSelect, RoundedTitle},
+  components: {ImageTable, TypeEdit, PopCard, Pop, ImageViewer, TableSelect, RoundedTitle},
   data(){
     return {
       productTypeColumns,
@@ -94,6 +118,7 @@ export default {
       page: 1,
       pageSize: 10,
       typeEditShow: false,
+
     }
   },
   beforeMount() {
@@ -121,8 +146,21 @@ export default {
       this.$nextTick(() => {
         this.$refs.type_edit.init("产品类型", apiMap.productAddType.path)
       })
-
-
+    },
+    parseTags(tagArrStr){
+      console.log(tagArrStr)
+      let tags = tagArrStr
+      if (!tags) {
+        return [];
+      }
+      // 去除空字符串
+      tags = tags.replace(/\s/g,'')
+      let tagArr = tags.split(',')
+      // 标签去重
+      tagArr = [...new Set(tagArr)]
+      // 移除空标签
+      tagArr = tagArr.filter(item => item)
+      return tagArr
     },
     handleEditType(item){
       //
@@ -131,6 +169,16 @@ export default {
         this.$refs.type_edit.init("产品类型", apiMap.productEditType.path, item);
       })
     },
+    handleDeleteType(item){
+      // 询问是否要删除
+      this.$confirm({
+        content: '确定要删除吗?',
+        okText: '确定',
+        cancelText: '取消',
+        onOk: () => {
+          this.executeDeleteType(item);
+        }});
+    },
     handleCloseEdit(){
       this.closeTypeEdit();
     },
@@ -140,10 +188,35 @@ export default {
     closeTypeEdit(){
       this.typeEditShow = false;
       this.getProductTypes();
+    },
+    async executeDeleteType(item){
+      let url = apiMap.productDeleteType.path;
+      url += `?id=${item.type_id}`
+      let [err,res] = await handle(axios.delete(url, {id: item.type_id}));
+      if(err){
+        this.$message.error(`[删除类型] 失败:${err.message}`)
+        console.log(err)
+        return err
+      }
+      let result = res.data
+      if(result.code !== rCode.OK) {
+        this.$message.error(`[移除类型] 失败:${result.msg}`)
+        return result
+      }
+      this.$message.success(`[移除类型] 成功`)
+      this.closeTypeEdit();
     }
-  }
+}
 }
 </script>
 <style scoped>
-
+.logo_view{
+  width: 70px;
+  height: 70px;
+  border-radius: 50%;
+  font-size: 50px;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}
 </style>

+ 9 - 6
pages/product/index.vue

@@ -79,7 +79,8 @@ export default {
       products: this.pProduct?this.pProduct:[],
       basePageData: {},
       pageSave: {},
-      isPhone: false
+      isPhone: false,
+      types: this.$store.getters.productTypes
     }
   },
   beforeMount() {
@@ -95,10 +96,8 @@ export default {
     this.isPhone = isMediaView(0,1024);
     this.$root.$on('changeLang',this.switchLang);
     this.$root.$on('searchProductKey',this.changeProductKeyHandle);
-    this.$root.$on('changeProductType',this.selectType);
+    this.$root.$on('changeType',this.selectType);
     this.$root.$on('changePage',this.changePageHandle);
-
-
     // this.loadData();
   },
   methods:{
@@ -134,10 +133,10 @@ export default {
     async searchProduct(){
       // 获取数据
       let url = apiMap.searchProduct.path;
+
       url += `?key=${this.key}&type=${this.type}&p=${this.page}`
       let [err,res] = await handle(axios.get(url));
       if(err){ console.log(err); return null; }
-
       let result = res.data;
       if(result.code === 1){
         this.products = result.data;
@@ -170,6 +169,10 @@ export default {
       let nowCount = pageData.total;
       this.nowTotal = nowTotal;
       this.nowCount = nowCount;
+    },
+    changeTypeHandle(nextType){
+      console.log(nextType)
+      this.selectType(nextType);
     }
   }
 }
@@ -181,7 +184,7 @@ export default {
     <!--    推荐广告-->
     <product-banner :lang="lang" />
     <!--    产品类别 -->
-    <product-types :lang="lang" :type="type"></product-types>
+    <product-types :lang="lang" :type="type" :types="types" @changeType="changeTypeHandle"></product-types>
     <!--    产品列表 -->
     <product-list :lang="lang" :product-list="products"></product-list>
     <page-select :page="page" :count="nowCount" :total="nowTotal"></page-select>

+ 98 - 1
server/control/c_news.js

@@ -6,6 +6,7 @@ const log = require("../logger").logger("c_solution","info");
 const d_news = require("../database/d_news");
 const d_product = require("../database/d_product");
 const {filePathToUrl} = require("../tools/filePathTool");
+const dbField = require("../map/dbField");
 
 async function addReadNum(newId){
   let [err,res] = await handle(d_news.addReadNum(newId));
@@ -102,9 +103,105 @@ async function editArticle(article){
   return [null, res];
 }
 
+async function getArticleTypes(){
+  [err, res] = await handle(d_news.loadTypes());
+  if(err){
+    log.error(`[获取文章类型] 获取文章类型 ${err.message}`);
+    return [{
+      code: codeMap.ServerError,
+      message: `[服务器异常] 获取文章类型失败`
+    }, null];
+  }
+  return [err, res]
+}
+
+
+async function editType( id, articleType)
+{
+  let [err,res] = await handle(d_news.getTypeByKey(articleType.type_key))
+  if(err){
+    return [err,null];
+  }
+  if(res.length){
+    return [
+      {
+        eCode:codeMap.DataRepeat,
+        eMsg:`类型关键字重复`
+      },null]
+  }
+  [err,res] = await handle(d_news.editArticleType(id, articleType));
+  if(err){
+    return [err,null];
+  }
+  return [null,res];
+}
+
+async function addType( id, articleType)
+{
+  // 数据校验
+  let parentType = articleType.parentType;
+  if(parentType !== dbField.db_base.newsType.solution ||
+    parentType !== dbField.db_base.newsType.news){
+    return [
+      {
+        eCode: codeMap.NotPermission,
+        eMsg: `不受支持的文章父类型, 目前支持 解决方案与文章`
+      }, null];
+  }
+  // 判断类别关键字是否重复
+  let [err,res] = await handle(d_news.getTypeByKey(articleType.type_key))
+  if(err){
+    return [err,null];
+  }
+  if(res.length){
+    return [
+      {
+        eCode:codeMap.DataRepeat,
+        eMsg:`类型关键字重复`
+      },null]
+  }
+
+  [err,res] = await handle(d_news.addArticleType(id, articleType, parentType));
+  if(err){
+    return [err,null];
+  }
+  return [null,res];
+}
+
+// 删除产品类型
+async function deleteType(id) {
+  let err, res;
+
+  // 检索产品类型下是否有产品
+  [err, res] = await handle(d_news.getAriticleByTypeId(id));
+  if (err) {
+    // 删除失败
+    log.error('[移除文章分类] 寻找文章失败' + err.message);
+    return [err, null];
+  }
+  if (res.length > 0) {
+    // 存在产品
+    log.warn('[移除文章分类] 存在文章,无法删除');
+    return [
+      {
+        eCode: codeMap.NotPermission,
+        eMsg: `该类型下存在文章,无法删除, 请先移除对应文章`
+      }, null];
+  }
+  [err, res] = await handle(d_product.deleteProductType(id));
+  if (err) {
+    log.error('删除失败' + err.message);
+    return [err, null];
+  }
+  return [null,res];
+}
 module.exports = {
   addReadNum,
   searchNewsByMini,
   addArticle,
-  editArticle
+  editArticle,
+  getArticleTypes,
+  editType,
+  addType,
+  deleteType
 }

+ 15 - 15
server/control/c_solution.js

@@ -62,17 +62,17 @@ async function searchSolution(type, key, p, l)
   }
 
 
-  return await searchHandle(
+  return searchHandle(
     '搜索产品失败',
     d_solution.searchSolution,
     _params,
     null,
     p,
     l,
-    (item)=>{
-      if(item.coverPath){
-        item.coverPath = filePathToUrl(item.coverType,item.coverPath);
-      }else{
+    (item) => {
+      if (item.coverPath) {
+        item.coverPath = filePathToUrl(item.coverType, item.coverPath);
+      } else {
         item.coverPath = '';
       }
 
@@ -95,7 +95,7 @@ async function searchNews(type, key, sortKey = '', sortType = '', p, l)
   }
 
 
-  return await searchHandle(
+  return searchHandle(
     '搜索新闻失败',
     d_solution.searchSolution,
     _params,
@@ -105,10 +105,10 @@ async function searchNews(type, key, sortKey = '', sortType = '', p, l)
     },
     p,
     l,
-    (item)=>{
-      if(item.coverPath){
-        item.coverPath = filePathToUrl(item.coverType,item.coverPath);
-      }else{
+    (item) => {
+      if (item.coverPath) {
+        item.coverPath = filePathToUrl(item.coverType, item.coverPath);
+      } else {
         item.coverPath = '';
       }
 
@@ -141,17 +141,17 @@ async function searchAllNews(pType,type, key, p, l){
   }
 
 
-  return await searchHandle(
+  return searchHandle(
     '搜索文章失败',
     d_solution.searchSolution,
     _params,
     null,
     p,
     l,
-    (item)=>{
-      if(item.coverPath){
-        item.coverPath = filePathToUrl(item.coverType,item.coverPath);
-      }else{
+    (item) => {
+      if (item.coverPath) {
+        item.coverPath = filePathToUrl(item.coverType, item.coverPath);
+      } else {
         item.coverPath = '';
       }
 

+ 28 - 3
server/control/product.js

@@ -104,7 +104,19 @@ async function getProductTypes()
 
 async function editType( id, productType)
 {
-  let [err,res] = await handle(d_product.editProductType(id, productType));
+  // 判断类别关键字是否重复
+  let [err,res] = await handle(d_product.getTypeByKey(productType.type_key))
+  if(err){
+    return [err,null];
+  }
+  if(res.length){
+    return [
+      {
+        eCode:codeMap.DataRepeat,
+        eMsg:`产品类型关键字重复`
+      },null]
+  }
+  [err,res] = await handle(d_product.editProductType(id, productType));
   if(err){
     return [err,null];
   }
@@ -113,8 +125,20 @@ async function editType( id, productType)
 
 async function addType( id, productType)
 {
+  // 判断类别关键字是否重复
+  let [err,res] = await handle(d_product.getTypeByKey(productType.type_key))
+  if(err){
+    return [err,null];
+  }
+  if(res.length){
+    return [
+      {
+        eCode:codeMap.DataRepeat,
+        eMsg:`产品类型关键字重复`
+      },null]
+  }
   // 数据校验
-  let [err,res] = await handle(d_product.addProductType(id, productType));
+  [err,res] = await handle(d_product.addProductType(id, productType));
   if(err){
     return [err,null];
   }
@@ -129,11 +153,12 @@ async function deleteType(id) {
   [err, res] = await handle(d_product.getProductByTypeId(id));
   if (err) {
     // 删除失败
-    log.error('无法找到对应产品' + err.message);
+    log.error('[移除产品分类] 无法找到产品' + err.message);
     return [err, null];
   }
   if (res.length > 0) {
     // 存在产品
+    log.warn('[移除产品分类] 存在产品,无法删除');
     return [
       {
         eCode: codeMap.NotPermission,

+ 3 - 0
server/database/d_base.js

@@ -144,6 +144,9 @@ function deleteFile(fileId){
   let sql = `delete from hfy_files where fileId = ? limit 1`;
   return mysql.pq(sql,[fileId]);
 }
+
+
+
 module.exports = {
   getCarousel,
   getCarouselById,

+ 65 - 2
server/database/d_news.js

@@ -1,6 +1,7 @@
 const mysql = require('./mysql');
 const {searchSql,limitSql} = require("../tools/searchSql");
 const {getUnixTimeStamp} = require("../tools/time_cjs");
+const time = require("../tools/time_cjs");
 const log = require("../logger").logger("d_news","info");
 
 function addReadNum(id){
@@ -10,7 +11,7 @@ function addReadNum(id){
 }
 
 function loadTypes() {
-  let sql = `SELECT * FROM hfy_news_type`;
+  let sql = `SELECT * FROM hfy_news_type ORDER BY type_sort DESC`;
   return mysql.pq(sql, []);
 }
 
@@ -19,6 +20,8 @@ function getTypeByKey(key){
   return mysql.pq(sql,[key]);
 }
 
+
+
 // 轻量搜索接口
 function searchAllNewsMini(type='array',searchParam,sort,page,limit){
   let sql;
@@ -83,6 +86,63 @@ function editArticle(article, typeId){
   return mysql.pq(sql,values);
 }
 
+
+
+// 编辑产品类型
+function editArticleType(id, typeChange) {
+  let sql = ``
+  let values = [];
+  sql += `UPDATE hfy_news_type SET date_time = ?`;
+  values.push(time.getUnixTimeStamp());
+  if(typeChange.type_name){
+    sql += `,type_name = ?`;
+    values.push(typeChange.type_name);
+  }
+  if(typeChange.type_key){
+    sql += `,type_key = ?`;
+    values.push(typeChange.type_key);
+  }
+  if(typeChange.type_sort){
+    sql += `,type_sort = ?`;
+    values.push(typeChange.type_sort);
+  }
+  if(typeChange.type_logo){
+    sql += `,type_logo = ?`;
+    values.push(typeChange.type_logo);
+  }
+  if(typeChange.seo_key){
+    sql += `,seo_key = ?`;
+    values.push(typeChange.seo_key);
+  }
+  sql += ` WHERE type_id = ?`;
+  values.push(id);
+  return mysql.pq(sql, values);
+}
+
+// 删除产品类型
+function deleteArticleType(id) {
+  let sql = `DELETE FROM hfy_news_type WHERE type_id = ?`;
+  return mysql.pq(sql, [id]);
+}
+
+// 新增产品类型
+function addArticleType(type, parentId) {
+  let sql =
+    `INSERT INTO hfy_news_type (
+date_time, type_name, type_key, type_sort, type_logo, seo_key, parent_id)
+VALUES (?, ?, ?, ?, ?, ?, ?)`;
+  let values = [];
+  values.push(time.getUnixTimeStamp());
+  values.push(type.type_name);
+  values.push(type.type_key);
+  values.push(type.type_sort);
+  values.push(type.type_logo);
+  values.push(type.seo_key);
+  values.push(parentId);
+
+  return mysql.pq(sql,values);
+}
+
 module.exports = {
   addReadNum,
   loadTypes,
@@ -90,5 +150,8 @@ module.exports = {
   getNewsById,
   getTypeByKey,
   addArticle,
-  editArticle
+  editArticle,
+  editArticleType,
+  deleteArticleType,
+  addArticleType,
 }

+ 24 - 4
server/database/d_product.js

@@ -120,7 +120,7 @@ function getProductByTypeId(id){
 
 // 获取产品类型列表
 function getProductTypeList() {
-  let sql = `SELECT * FROM hfy_product_type`;
+  let sql = `SELECT * FROM hfy_product_type ORDER BY type_sort DESC`;
   return mysql.pq(sql, []);
 }
 
@@ -146,6 +146,10 @@ function editProductType(id, typeChange) {
     sql += `,type_logo = ?`;
     values.push(typeChange.type_logo);
   }
+  if(typeChange.seo_key){
+    sql += `,seo_key = ?`;
+    values.push(typeChange.seo_key);
+  }
   sql += ` WHERE type_id = ?`;
   values.push(id);
   return mysql.pq(sql, values);
@@ -158,8 +162,23 @@ function deleteProductType(id) {
 
 // 新增产品类型
 function addProductType(type) {
-  let sql = `INSERT INTO hfy_product_type (date_time, type_name, type_key, type_sort, type_logo) VALUES (?, ?, ?, ?, ?)`;
-  return mysql.pq(sql, [time.getUnixTimeStamp(), type.type_name, type.type_key, type.type_sort, type.type_logo]);
+  let sql = `INSERT INTO hfy_product_type
+(date_time, type_name, type_key, type_sort, type_logo, seo_key)
+VALUES (?, ?, ?, ?, ?, ?)`;
+  let values = [];
+  values.push(time.getUnixTimeStamp());
+  values.push(type.type_name);
+  values.push(type.type_key);
+  values.push(type.type_sort);
+  values.push(type.type_logo);
+  values.push(type.seo_key);
+  return mysql.pq(sql, values);
+}
+
+// 获取类型
+function getTypeByKey(key) {
+  let sql = `SELECT * FROM hfy_product_type WHERE type_key = ?`;
+  return mysql.pq(sql, [key]);
 }
 
 module.exports = {
@@ -173,5 +192,6 @@ module.exports = {
   getProductTypeList,
   editProductType,
   deleteProductType,
-  addProductType
+  addProductType,
+  getTypeByKey
 }

+ 1 - 1
server/index.js

@@ -22,7 +22,7 @@ app.use(
   session({
     secret: 'hfy',
     name: 'session', //这里的name值得是cookie的name,默认cookie的name是:connect.sid
-    cookie: { maxAge: 1800000 }, //过期时间半小时
+    cookie: { maxAge: 2800000 }, //过期时间半小时
     keys: ['owner', 'captcha'], // 用户登陆信息,验证码字段
     resave: true,
     saveUninitialized: true,

+ 2 - 0
server/map/dbField.js

@@ -20,6 +20,8 @@ const db_base = {
     other: 0,
     image: 1,
     video: 2,
+    audio: 3,
+    svg: 4
   },
   carouselType: {
     href: 0,

+ 67 - 0
server/router/r_news.js

@@ -4,6 +4,8 @@ const c_solution = require("../control/c_solution");
 const typeTool = require("../tools/typeTool_cjs");
 const c = require("../control/c_news");
 const {isEmpty} = require("../tools/typeTool_cjs");
+const checkLogin = require("../middleware/checkSession");
+const progressField = require("../map/progressField");
 const log = require("../logger").logger("r_news","info");
 
 router.get(
@@ -133,4 +135,69 @@ router.post('/edit',async(req,res)=>{
   }
 });
 
+// 获取文章分类
+router.get('/types', async (req, res) => {
+  try{
+    let err, result;
+    [err, result] = await c.getArticleTypes();
+    if(err){ return controlError(res, err, null);}
+    success(res, result);
+  }catch (e) {
+    ServerError(res, null, e.message);
+  }
+});
+
+// 删除
+
+router.post('/type/edit',
+  checkLogin(progressField.session_hfy),
+  async (req, res) => {
+  try {
+    let err, result;
+    const id = req.query.id;
+    const body = req.body;
+    log.info(`[编辑文章类型] id=${id}, body=${JSON.stringify(body)}`);
+    [err, result] = await c.editType(id, body);
+    if(err){ return controlError(res, err, null);}
+    success(res, result);
+  }catch (e) {
+    ServerError(res, null, e.message);
+  }
+});
+
+
+router.post('/type/add',
+  checkLogin(progressField.session_hfy),
+  async (req, res) => {
+  try {
+    let err, result;
+    const body = req.body;
+    log.info(`[新增文章类型] body=${JSON.stringify(body)}`);
+    [err, result] = await c.addType(body);
+    if(err){ return controlError(res, err, null);}
+    success(res, result);
+  }catch (e) {
+    ServerError(res, null, e.message);
+  }
+});
+
+router.delete('/type/del',
+  checkLogin(progressField.session_hfy),
+  async (req, res) => {
+  try {
+    let err, result;
+    const id = req.query.id;
+    log.info(`[移除文章类型] id=${id} `);
+    if(!id ){
+      paramFail(res, "id is must be two ");
+      return;
+    }
+    [err, result] = await c.deleteType(id);
+    if(err){ return controlError(res, err, null);}
+    success(res, result);
+  }catch (e) {
+    ServerError(res, null, e.message);
+  }
+});
+
 module.exports = router;

+ 8 - 7
server/router/r_product.js

@@ -3,6 +3,8 @@ const {paramFail, ServerError, success, controlError, searchSuccess} = require("
 const c = require('../control/product');
 const log = require("../logger").logger("r_product","info")
 const typeTool = require('../tools/typeTool_cjs');
+const checkLogin = require("../middleware/checkSession");
+const progressField = require("../map/progressField");
 
 /**
  * 加载产品列表,根据类型
@@ -97,7 +99,7 @@ router.get('/types', async (req, res) => {
 
 // 编辑产品类型
 
-router.post('/type/edit', async (req, res) => {
+router.post('/type/edit', checkLogin(progressField.session_hfy), async (req, res) => {
   try {
     let err, result;
     const id = req.query.id;
@@ -112,7 +114,7 @@ router.post('/type/edit', async (req, res) => {
 });
 
 
-router.post('/type/add', async (req, res) => {
+router.post('/type/add', checkLogin(progressField.session_hfy),async (req, res) => {
   try {
     let err, result;
     const body = req.body;
@@ -125,17 +127,16 @@ router.post('/type/add', async (req, res) => {
   }
 });
 
-router.post('/type/del', async (req, res) => {
+router.delete('/type/del', checkLogin(progressField.session_hfy), async (req, res) => {
   try {
     let err, result;
     const id = req.query.id;
-    const body = req.body;
-    log.info(`[移除产品类型] id=${id} body=${JSON.stringify(body)}`);
-    if(!id || !body || id !== body.id){
+    log.info(`[移除产品类型] id=${id} `);
+    if(!id ){
       paramFail(res, "id is must be two ");
       return;
     }
-    [err, result] = await c.addType(body);
+    [err, result] = await c.deleteType(id);
     if(err){ return controlError(res, err, null);}
     success(res, result);
   }catch (e) {

+ 1 - 0
static/image/all.svg

@@ -0,0 +1 @@
+<svg t="1681465373129" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2628" width="200" height="200"><path d="M384 896c-17.664 0-32-14.304-32-32L352 160c0-17.664 14.336-32 32-32s32 14.336 32 32l0 704C416 881.696 401.664 896 384 896z"  p-id="2629"></path><path d="M641.056 896.128c-17.696 0-32-14.304-32-32l0-704c0-17.664 14.304-32 32-32s32 14.336 32 32l0 704C673.056 881.824 658.752 896.128 641.056 896.128z"  p-id="2630"></path><path d="M864 736c-17.696 0-32-14.304-32-32L832 320c0-17.664 14.304-32 32-32s32 14.336 32 32l0 384C896 721.696 881.696 736 864 736z"  p-id="2631"></path><path d="M160 736c-17.664 0-32-14.304-32-32L128 320c0-17.664 14.336-32 32-32s32 14.336 32 32l0 384C192 721.696 177.664 736 160 736z"  p-id="2632"></path></svg>

+ 12 - 2
store/index.js

@@ -8,6 +8,8 @@ import dbField_esm from "../map/dbField_esm";
 
 function _transform(arr){
   arr = arr.map(item => {
+    item.en_text = item.type_name;
+    item.icon = item.type_logo;
     item.text = item.type_name;
     item.key = item.type_key;
     item.typeKey = item.type_key;
@@ -64,7 +66,6 @@ export const modules = {
           commit(indexTypes.mutations.setNewsTypes, data.nType);
           commit(indexTypes.mutations.setCarousel, data.carousel);
         }
-
       },
     },
     getters: {
@@ -73,7 +74,16 @@ export const modules = {
       carousel: state => state.carousel,
       productTypes: state => {
         let arr = state.pTypes;
-        return _transform(arr);
+        let res = _transform(arr);
+        // 首位插入一个空选项
+        res.unshift({
+          en_text : "全部类型",
+          icon : "/image/all.svg",
+          text : "全部类型",
+          key : "all",
+          typeKey : "all",
+        })
+        return res
       },
       allNewsTypes: state => {
         let arr = state.nTypes;

+ 27 - 0
until/form/rules.js

@@ -57,6 +57,7 @@ const hrefRule = [
     message: '链接格式不正确'
   },
 ];
+
 const numberRule = [
   {
     type: 'number',
@@ -67,6 +68,8 @@ const numberRule = [
 ]
 
 
+
+
 const requireIdRule = [
   requiredRuleItem,
 ]
@@ -136,6 +139,26 @@ const cardRule = [
     return checkID(val)?'':'身份证验证失败';
   }]
 
+/**
+ * 验证标签类型, 多个标签使用逗号隔开
+ * @param val
+ * @return {boolean|string}
+ */
+function tagsRule(val) {
+  // 移除空格
+  val = val.replace(/\s+/g, '');
+  let tags = val.split(',');
+  if(tags.length > 10) {
+    return '最多支持10个标签';
+  }
+  for (var i=0;i<tags.length;i++) {
+    if(!/^[a-zA-Z\u4e00-\u9fa5]+$/.test(tags[i])) {
+      return '标签名称必须为中文或英文, 多个标签用英文逗号隔开 例: 新闻,测试';
+    }
+  }
+  return true;
+}
+
 export const paramsRules = [
   {
     name: "用户名验证规则",
@@ -186,6 +209,10 @@ export const paramsRules = [
   {
     checkFields: ['sort', 'type_sort'],
     rules: numberRule
+  },
+  {
+    checkFields: ['tags', 'seo_tags'],
+    rules: [tagsRule]
   }
 ]
 

+ 13 - 22
until/time.js

@@ -41,28 +41,19 @@ export function timeFormat(time, format) {
   let tf = function(i) {
     return (i < 10 ? '0' : '') + i
   };
-  return format.replace(/yyyy|MM|dd|HH|mm|ss/g, function(a) {
-    switch (a) {
-      case 'yyyy':
-        return tf(t.getFullYear());
-        break;
-      case 'MM':
-        return tf(t.getMonth() + 1);
-        break;
-      case 'mm':
-        return tf(t.getMinutes());
-        break;
-      case 'dd':
-        return tf(t.getDate());
-        break;
-      case 'HH':
-        return tf(t.getHours());
-        break;
-      case 'ss':
-        return tf(t.getSeconds());
-        break;
-    }
-  })
+
+  return format
+    .replace(/yyyy/,tf(t.getFullYear()))
+    .replace(/MM/,tf(t.getMonth() + 1))
+    .replace(/M/,String(t.getMonth() + 1))
+    .replace(/dd/,tf(t.getDate()))
+    .replace(/d/,String(t.getDate()))
+    .replace(/HH/,tf(t.getHours()))
+    .replace(/H/,String(t.getHours()))
+    .replace(/mm/,tf(t.getMinutes()))
+    .replace(/m/,String(t.getMinutes()))
+    .replace(/ss/,tf(t.getSeconds()))
+    .replace(/s/,String(t.getSeconds()));
 }
 
 export function getUnixTimeStamp(){

+ 22 - 0
until/url.js

@@ -0,0 +1,22 @@
+// 拼接url params 参数
+
+export function urlAddParam(url, params) {
+  console.log(url)
+  // 判断url中是否含有?
+  if (url.indexOf('?') === -1) {
+    url += '?'
+  } else {
+    url += '&'
+  }
+  for (const key in params) {
+    if (params.hasOwnProperty(key)) {
+      const value = params[key]
+      // 判断value是否存在,如果存在拼接参数
+      if (value !== null && value !== undefined) {
+        url += `&${key}=${value}`
+      }
+    }
+  }
+  console.log(url)
+  return url
+}