Jelajahi Sumber

feat: 组件数据流转支持免费用户订阅格式

zhangyuhan 2 minggu lalu
induk
melakukan
3e27f2960a

+ 2 - 2
apps/bigmember_pc/src/api/modules/subscribe.js

@@ -86,7 +86,7 @@ export function updateMatchType(data) {
 
 // 订阅地区、采购单位行业修改
 export function setAreaBuyer(data) {
-  data.vSwitch = 'm'
+  data.vSwitch = data.vSwitch || 'm'
   data = qs.stringify(data)
   return request({
     url: '/publicapply/subscribe/update',
@@ -97,7 +97,7 @@ export function setAreaBuyer(data) {
 
 // 订阅修改(换成超级订阅)
 export function setUserInfoVip(data) {
-  data.vSwitch = 'm'
+  data.vSwitch = data.vSwitch || 'm'
   data = qs.stringify(data)
   return request({
     url: '/publicapply/subscribe/setUserInfo',

+ 75 - 0
apps/bigmember_pc/src/assets/js/selector.js

@@ -241,6 +241,73 @@ export const infoTypeListExp = (function () {
   return arr
 })()
 
+
+export const infoTypeListExpOfOneLevel = (function() {
+  return [
+    {
+      name: '全部',
+      value: '',
+      level: 0,
+      children: []
+    },
+    {
+      name: '拟建项目',
+      value: '拟建',
+      showHelp: true,
+      level: 1,
+      children: []
+    },
+    {
+      name: '采购意向',
+      value: '采购意向',
+      showHelp: true,
+      level: 1,
+      children: []
+    },
+    {
+      name: '招标预告',
+      value: '预告',
+      level: 1,
+      children: []
+    },
+    {
+      name: '招标公告',
+      value: '招标',
+      level: 1,
+      children: []
+    },
+    {
+      name: '招标结果',
+      value: '结果',
+      level: 1,
+      children: []
+    },
+    {
+      name: '其他信息',
+      value: '其它',
+      level: 1,
+      children: []
+    },
+  ]
+})()
+
+export function getOneLevelNamesByValues(valueArr) {
+  // 统一转成数组
+  const values = Array.isArray(valueArr) ? valueArr : [valueArr];
+
+  // 建立 value -> name 的映射表(只关心 level === 1 的项)
+  const valueToNameMap = infoTypeListExpOfOneLevel.reduce((map, item) => {
+    if (item.level === 1) {
+      map[item.value] = item.name;
+    }
+    return map;
+  }, {});
+
+  // 按顺序返回对应的 name
+  return values.map(v => valueToNameMap[v] || '').filter(v => v);
+}
+
+
 export const infoTypeListMapExp = {
   拟建项目: ['拟建项目'],
   采购意向: ['采购意向'],
@@ -723,6 +790,14 @@ export const pushFunctionData = [
 ]
 
 // 关键词匹配方式
+export const keywordMatchTypeListOfFree = [
+  {
+    value: 'title',
+    label: '标题',
+    auth: false
+  }
+]
+
 export const keywordMatchTypeList = [
   {
     value: 'title',

+ 12 - 2
apps/bigmember_pc/src/components/selector-cascader/AreaCityCountryCascader.vue

@@ -71,7 +71,7 @@
                 </ul>
               </div>
             </div>
-            <div class="module-container city-container">
+            <div class="module-container city-container" v-if='showCity'>
               <header class="module-header">
                 <span :class="{ 'icon-have-vip': !vip }">城市</span>
               </header>
@@ -101,7 +101,7 @@
                 </ul>
               </div>
             </div>
-            <div class="module-container country-container">
+            <div class="module-container country-container" v-if='showCity && showCountry'>
               <header class="module-header">
                 <span :class="{ 'icon-have-vip': !vip }">区县</span>
               </header>
@@ -191,6 +191,16 @@ export default {
       type: Boolean,
       default: false
     },
+    // 是否显示城市
+    showCity: {
+      type: Boolean,
+      default: true
+    },
+    // 是否显示城市下区县
+    showCountry: {
+      type: Boolean,
+      default: true
+    },
     // 是否展示选择标签tag
     showTags: {
       type: Boolean,

+ 3 - 2
apps/bigmember_pc/src/components/selector/InfoTypeSelectorContent.vue

@@ -133,7 +133,7 @@
 
 <script>
 import { Popover } from 'element-ui'
-import { infoTypeListExp, infoTypeNotAdvancedList, infoTypeAdvancedList } from '@/assets/js/selector.js'
+import { infoTypeListExp, infoTypeNotAdvancedList, infoTypeAdvancedList, infoTypeListExpOfOneLevel } from '@/assets/js/selector.js'
 
 export default {
   name: 'info-type-selector-content',
@@ -179,7 +179,8 @@ export default {
       dataMap: {
         all: infoTypeListExp,
         base: infoTypeNotAdvancedList,
-        advance:  infoTypeAdvancedList
+        advance:  infoTypeAdvancedList,
+        oneLevel: infoTypeListExpOfOneLevel
       }
     }
   },

+ 9 - 0
apps/bigmember_pc/src/router/routers.js

@@ -128,6 +128,15 @@ export default [
     name: 'config',
     component: () => import('@/views/subscribe/Config.vue')
   },
+  {
+    path: '/free/help/set_subscribe',
+    name: 'config',
+    meta: {
+      title: '帮用户订阅(免费订阅)',
+      vt: 'f'
+    },
+    component: () => import('@/views/subscribe/FreeConfig.vue')
+  },
   // 采购单位画像-通用路由-seo
   {
     path: '/dw/:id.html', // seo数字id

+ 459 - 0
apps/bigmember_pc/src/views/subscribe/FreeConfig.vue

@@ -0,0 +1,459 @@
+<template>
+  <div class="free-config-setting-container">
+    <div class="config-title">
+      订阅设置
+      <div class="subscribe-manage-tip">
+        注:修改完订阅条件后,预计30分钟左右生效。
+      </div>
+    </div>
+    <!-- 订阅设置 -->
+    <free-sub-config :datas="setData" :setStatus="setStatus" @update="getUpdate"></free-sub-config>
+    <!-- 关键词设置 -->
+    <free-key-config id="setkey" :datas="setData" :setStatus="setStatus" @update="getUpdate"></free-key-config>
+    <!-- 关键词列表 -->
+    <key-list
+      v-if="setData.keyList.length > 0"
+      ref="keyConfigRef"
+      :datas="setData"
+      :setStatus="setStatus"
+    ></key-list>
+    <el-dialog
+      custom-class="small-dialog"
+      title="关键词升级提示"
+      :visible.sync="dialogUpdate"
+      width="380px"
+      center
+      :show-close="false"
+    >
+      <p class="dialog-update-black">
+        “附加词”已整合至“关键词”中,添加多个关键词用空格隔开即可,并可以灵活选择匹配模式(精准或模糊)
+      </p>
+      <p class="dialog-update-gray">
+        示例说明:<br />
+        调整前:关键词:软件 <br />
+        <em style="opacity: 0">占位符:</em>附加词:系统
+        <br />调整后:关键词:软件 系统 <br />
+        <em style="opacity: 0">占位符:</em
+        >匹配模式:精准(同时包含所有关键词才推送)
+      </p>
+      <span slot="footer" class="dialog-footer">
+        <el-button class="know-btn" type="primary" @click="updateTipFn"
+          >我知道了</el-button
+        >
+      </span>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { Dialog, Button } from 'element-ui'
+import FreeSubConfig from './components/key/FreeSubConfig'
+import FreeKeyConfig from './components/key/FreeKeyConfig'
+import KeyList from './components/key/List'
+import { getFreeUserSubscribeList, setUserInfoVip, updateKey } from '@/api/modules'
+import $bus from '@/utils/bus'
+import { mixinNoOpenSetMessage } from '@/utils/mixins/subscribe-setting-prompt.js'
+import { getOneLevelNamesByValues } from '@/assets/js/selector'
+export default {
+  name: 'free-config',
+  mixins: [mixinNoOpenSetMessage],
+  components: {
+    [Dialog.name]: Dialog,
+    [Button.name]: Button,
+    KeyList,
+    FreeSubConfig,
+    FreeKeyConfig
+  },
+  data() {
+    return {
+      getBigInfoCount: 0,
+      setData: {
+        title: '关键词设置',
+        // 区域字符串 渲染输入框
+        areaStr: '',
+        // 采购单位行业字符串 渲染输入框
+        buyClassStr: '',
+        // 信息类型字符串
+        infoTypeStr: '',
+        projectmatch: null,
+        buyclassmatch: null,
+        mathway: null, // 老字段(保留不用)
+        // 区域对象
+        areaObj: {},
+        areaCount: -1, // 大会员权限有几个省(大会员单省版使用) -1表示全国
+        // 采购单位行业数组
+        buyClassArr: [],
+        // 信息类型数组
+        infoTypeArr: [],
+        // 关键词
+        keyList: [],
+        maxCount: 0,
+        // 关键词匹配方式(新)
+        matchType: [],
+        amount: ''
+      },
+      // 子账号业务范围
+      scope: [],
+      dialogUpdate: false,
+      // 订阅设置开关状态
+      setStatus: true
+    }
+  },
+  computed: {
+    canSubmanager() {
+      return this.$route.query.phone
+      // return !this.$store.state.user.info?.isSubCount
+    }
+  },
+  created() {
+    if (!this.canSubmanager) {
+      history.back()
+    }
+    this.getBigInfo()
+  },
+  mounted() {
+    $bus.$on('updateKey', (data) => {
+      // this.updateKeyWordsApi(data)
+      this.getBigInfo()
+    })
+    setTimeout(this.scrollToId, 100)
+  },
+  methods: {
+    async setSwitchCallback() {
+      // 调用api开启/关闭
+      const { flag } = await setUserInfoVip({
+        pageType: 'i_switch',
+        matchtype: this.setStatus ? 0 : 1 // 取反
+      })
+      if (flag) {
+        this.setStatus = !this.setStatus
+        this.$forceUpdate()
+      } else {
+        this.$message({
+          message: '修改失败',
+          type: 'error'
+        })
+      }
+    },
+    // 订阅设置-开关
+    onSetChange() {
+      if (this.setStatus) {
+        this.closeSetPrompt()
+      } else {
+        this.setSwitchCallback()
+      }
+    },
+    scrollToId() {
+      const { scroll } = this.$route.query
+      if (scroll === 'setkey') {
+        const headerH = $('#public-nav').height() || 0
+        const offset = $('#setkey').offset()
+        if (offset) {
+          $('body,html').animate({ scrollTop: offset.top + headerH })
+        }
+      }
+    },
+    parentGetCurEdit() {
+      const t = this.$refs.keyConfigRef.getCurEdit()
+      return t
+    },
+    // 排序
+    sortData(arr) {
+      return arr.sort((a, b) => {
+        return b.updatetime - a.updatetime
+      })
+    },
+    async getBigInfo() {
+      this.getBigInfoCount += 1
+      // const provinceArr = []
+      // let areaArr = []
+      const data = await getFreeUserSubscribeList()
+      const res = data.data
+      if (res) {
+
+        const editKeyList = [
+          {
+            a_key: res.keys || [],
+            s_item: '未分类'
+          }
+        ]
+        editKeyList.forEach((v, index) => {
+          if (v) {
+            v.groupIndex = index
+            v.showForm = false
+            if (!v.updatetime) {
+              v.updatetime = 0
+            }
+            if (v.a_key && v.a_key.length > 0) {
+              v.a_key.forEach((s, j) => {
+                s.keyindex = j
+                s.showForm = false
+                if (!s.updatetime) {
+                  s.updatetime = 0
+                }
+              })
+            }
+          }
+        })
+
+
+        this.setData.keyList = editKeyList
+        this.setData.areaStr = '全国'
+        this.setData.infoTypeStr = '全部类型'
+        this.setData.matchType = ['title']
+
+        const areaInfo = {
+          list: [],
+          regionMap: {}
+        }
+
+        if (res.area) {
+          for (const areaInfoKey in res.area) {
+            areaInfo.list.push(areaInfoKey)
+            areaInfo.regionMap[areaInfoKey] = res.area[areaInfoKey]
+          }
+
+          this.setData.areaStr = areaInfo.list.join('、')
+          this.setData.areaObj = areaInfo.regionMap
+        }
+
+        if (res.infotype && res.infotype.length) {
+          // 免费用户格式不一样,需要转换
+          this.setData.infoTypeArr = res.infotype
+          this.setData.infoTypeStr = getOneLevelNamesByValues(res.infotype).join('、')
+        }
+
+        this.setData.maxCount = 10
+
+        if (!Array.isArray(res.keys) || res.keys.length === 0) {
+          setUserInfoVip({
+            pageType: 'keyWords',
+            actionType: 'SC',
+            classify_name: '未分类',
+            vSwitch: 'f'
+          }).then((res) => {
+            this.getBigInfo()
+          })
+        }
+
+      } else {
+        console.log('状态值为:' + res.status)
+      }
+    },
+    // 子组件通知父组件更新关键词接口(大会员为全量提交,关键词修改使用超级订阅关键词接口提交)
+    updateKeyWordsApi(data) {
+      updateKey({
+        a_items: data
+      }).then((res) => {
+        if (res.data.status === 1) {
+          this.$message({
+            type: 'success',
+            message: '操作成功'
+          })
+          this.getBigInfo()
+        } else {
+          this.$message({
+            type: 'error',
+            message: res.error_msg || '操作失败'
+          })
+          this.getBigInfo()
+        }
+      })
+    },
+    // 除地区、采购单位以外设置修改接口
+    setUserInfoFn(data) {
+      setUserInfoVip({
+        pageType: data
+      }).then((res) => {
+        if (res.data.status === 1) {
+          this.$message({
+            type: 'success',
+            message: '操作成功'
+          })
+          this.getBigInfo()
+        } else {
+          this.$message({
+            type: 'error',
+            message: res.error_msg || '操作失败'
+          })
+          this.getBigInfo()
+        }
+      })
+    },
+    getUpdate() {
+      this.getBigInfo()
+    },
+    // 关闭升级提示按钮
+    updateTipFn() {
+      setUserInfoVip({
+        pageType: 'keytip'
+      }).then((res) => {
+        this.dialogUpdate = false
+      })
+    }
+  }
+}
+</script>
+
+<style lang="scss">
+.buyer-dialog {
+  border-radius: 8px !important;
+}
+.sub-dialog {
+  border-radius: 5px;
+}
+.small-dialog {
+  border-radius: 8px!important;
+  .el-dialog__title {
+    color: #1d1d1d;
+  }
+  .dialog-text {
+    margin-top: 4px;
+    font-size: 14px;
+    color: #686868;
+    line-height: 22px;
+    text-align: center;
+  }
+  .know-btn {
+    background: #2cb7ca;
+    border-radius: 6px;
+    border: 0;
+  }
+  .dialog-update-black {
+    color: #1d1d1d;
+    line-height: 22px;
+    text-align: justify;
+  }
+  .dialog-update-gray {
+    margin-top: 20px;
+    color: #686868;
+    line-height: 22px;
+    text-align: justify;
+  }
+}
+.free-config-setting-container {
+  width: 1080px;
+  margin: 32px auto 56px;
+  padding: 32px 30px;
+  background: #fff;
+  border-radius: 5px;
+  box-shadow: 0px 4px 8px 0px rgba(0, 0, 0, 0.08);
+  &-title{
+    display: flex;
+    align-items: center;
+    padding-bottom: 8px;
+    font-size: 18px;
+    color: #1d1d1d;
+    line-height: 28px;
+    border-bottom: 1px solid #ececec;
+  }
+  .subscribe-manage-tip {
+    display: inline-block;
+    margin-left: 22px;
+    font-size: 14px;
+    color: #2ABED1;
+    line-height: 22px;
+  }
+  .reset-tips {
+    width: 100%;
+    height: 32px;
+    line-height: 32px;
+    color: #ff9f40;
+    font-size: 13px;
+    background: rgb(255, 159, 64, 0.1);
+    text-align: center;
+  }
+  .save-btn {
+    display: block;
+    width: 180px;
+    height: 46px;
+    margin: 20px auto;
+    line-height: 46px;
+    color: #fff;
+    background: #2cb7ca;
+    border-radius: 6px;
+    text-align: center;
+    cursor: pointer;
+    border: 0;
+  }
+  .sub-dialog .el-dialog__header,
+  .sub-dialog .el-dialog__body {
+    padding: 0;
+  }
+  .buyer-dialog {
+    .el-dialog__header {
+      padding: 20px 20px 10px;
+    }
+    .el-dialog__body {
+      padding: 25px 25px 30px;
+    }
+  }
+  .prevent-loading{
+    opacity: 0;
+  }
+  .reset-disabled-switch{
+    margin-left: 9px;
+    &.is-disabled {
+      opacity: 1;
+      .el-switch__core,
+      .el-switch__label{
+        cursor: pointer;
+      }
+    }
+  }
+  .el-switch {
+    position: relative;
+  }
+
+  .el-switch__label--left {
+    position: absolute;
+    left: 6px;
+    top: 0;
+    color: #fff!important;
+    z-index: 9;
+    font-size: 12px;
+  }
+
+  .el-switch__label--right {
+    position: absolute;
+    right: 6px;
+    top: 0;
+    color: #fff!important;
+    z-index: 9;
+    font-size: 12px;
+  }
+
+  .el-switch__label * {
+    font-size: 12px;
+  }
+
+  .el-switch,
+  .el-switch__core {
+    height: 22px;
+    line-height: 22px;
+  }
+
+  .el-switch__core:after {
+    width: 18px;
+    height: 18px;
+  }
+
+  .el-switch.is-checked .el-switch__core::after {
+    margin-left: -18px !important;
+  }
+
+
+  .classify-title {
+    .icon-edit,
+    .icon-delete {
+      display: none;
+    }
+  }
+
+  .edit-form {
+      .is-matchway-item {
+        display: none;
+      }
+    }
+}
+</style>

+ 15 - 3
apps/bigmember_pc/src/views/subscribe/components/key/Edit.vue

@@ -46,7 +46,7 @@
           </div>
         </div>
       </div>
-      <div class="item">
+      <div class="item is-matchway-item">
         <div class="item-label">匹配模式:</div>
         <div class="item-value">
           <el-radio-group
@@ -197,6 +197,9 @@ export default {
     // 匹配信息过少提示
     littleTip() {
       return this.pushCount < 30 && this.cur.key !== ''
+    },
+    isFreeSubCustom () {
+      return this.$route.meta?.vt === 'f'
     }
   },
   watch: {
@@ -240,6 +243,9 @@ export default {
     },
     // 关键词推荐数量查询
     keywordsInput() {
+      if (this.isFreeSubCustom) {
+        this.cur.key = this.cur.key.replace(/\s/g, '')
+      }
       if (this.cur.key) {
         setTimeout(() => {
           this.getRecommendFn()
@@ -251,12 +257,18 @@ export default {
     },
     // 关键词输入框失去焦点 查询推送数量
     keywordsBlur() {
+      if (this.isFreeSubCustom) {
+        this.cur.key = this.cur.key.replace(/\s/g, '')
+      }
       if (this.cur.key) {
         this.getPushCountFn()
       }
     },
     // 排除词输入框失去焦点 查询推送数量
     notKeyBlur() {
+      if (this.isFreeSubCustom) {
+        this.cur.notkey = this.cur.notkey.replace(/\s/g, '')
+      }
       if (this.cur.notkey) {
         this.getPushCountFn()
       }
@@ -340,8 +352,8 @@ export default {
         kws_index: keyIndex, // 关键词索引
         kws_name: this.cur.key, // 关键词名称
         not_kws: this.getNotKeyArr(), // 排除词
-        match_way: this.cur.matchway, // 关键词匹配
-        vSwitch: 'm'
+        match_way: this.isFreeSubCustom ? 0 : this.cur.matchway, // 关键词匹配
+        vSwitch: this.$route.meta?.vt || 'm'
       }
       // 用jq axios数组参数无法序列化
       $.ajax({

+ 314 - 0
apps/bigmember_pc/src/views/subscribe/components/key/FreeKeyConfig.vue

@@ -0,0 +1,314 @@
+<template>
+  <div class="free-keywords-container">
+    <div class="key-title">
+      <div>
+        <span>{{ datas.title }}</span>
+        <span style="font-size: 14px"
+          ><em style="color: #2cb7ca"> {{ keyCounts }}</em
+          >/{{ datas.maxCount }}</span
+        >
+        <span style="font-size: 14px; color: #2cb7ca; margin-left: 16px"
+          >注:任意1组关键词组匹配成功即推送相关信息</span
+        >
+        <img
+          @click="showSetKeyDialog = true"
+          src="@/assets/images/icon/help.png"
+          class="help-img"
+        />
+      </div>
+<!--      <div class="add-classfily" @click="addClassfilyFn()" v-loading="!setStatus" element-loading-custom-class="prevent-loading">-->
+<!--        <i class="el-icon-plus"></i> 新增分类-->
+<!--      </div>-->
+    </div>
+    <common-dialog
+      width="500px"
+      :visible="showSetKeyDialog"
+      :show-footer="false"
+    >
+      <img
+        @click="showSetKeyDialog = false"
+        class="setkey-img"
+        src="@/assets/images/setkey-dialog.png"
+        alt=""
+      />
+    </common-dialog>
+    <div class="key-content">
+      <!-- <div v-if="datas.keyList.length === 0">
+        <div class="classify-title">
+          <span class="title-text">{{defaultClass}}</span>
+        </div>
+        <Edit
+          title="新增"
+          :datas="datas.keyList"
+          :className="defaultClass"
+          :classIndex="0"
+          :keyIndex="0"
+        ></Edit>
+      </div> -->
+    </div>
+    <!-- 修改分类dialog -->
+    <el-dialog
+      custom-class="sub-dialog"
+      :visible.sync="add.dialog"
+      :close-on-click-modal="false"
+      :show-close="false"
+      center
+      width="800px"
+      v-if="add.dialog"
+    >
+      <KeyCard @onCancel="add.dialog = false" @onConfirm="confirmEditClassFn">
+        <div slot="header">新增关键词分类</div>
+        <div class="class-edit-content">
+          <div class="item">
+            <div class="item-label">关键词分类:</div>
+            <div class="item-value">
+              <el-input
+                class="custom-long-input"
+                v-model.trim="add.className"
+                maxlength="20"
+                placeholder="请输入关键词分类"
+              ></el-input>
+            </div>
+          </div>
+        </div>
+      </KeyCard>
+    </el-dialog>
+  </div>
+</template>
+<script>
+import { Input, Button, Dialog, RadioGroup, Radio } from 'element-ui'
+import KeyCard from '@/components/selector/SelectorCard'
+import { setUserInfoVip } from '@/api/modules'
+import commonDialog from '@/components/dialog/Dialog'
+export default {
+  name: 'key-config',
+  props: {
+    datas: {
+      title: String,
+      keyList: Array,
+      maxCount: Number
+    },
+    // 订阅设置开关
+    setStatus: {
+      type: Boolean,
+      default: false
+    }
+  },
+  components: {
+    [Input.name]: Input,
+    [Dialog.name]: Dialog,
+    [Button.name]: Button,
+    [RadioGroup.name]: RadioGroup,
+    [Radio.name]: Radio,
+    KeyCard,
+    commonDialog
+    // Edit
+  },
+  data() {
+    return {
+      add: {
+        dialog: false,
+        className: ''
+      },
+      showSetKeyDialog: false
+    }
+  },
+  computed: {
+    keyCounts() {
+      let count = 0
+      this.datas.keyList.forEach((v) => {
+        if (v && v.a_key) {
+          count += v.a_key.length
+        }
+      })
+      return count
+    }
+  },
+  mounted() {},
+  methods: {
+    // 新增关键词分类弹框
+    addClassfilyFn() {
+      const t = this.$parent.parentGetCurEdit()
+      console.log(t)
+      if (t) return this.$toast('请先保存或取消正在操作的关键词组')
+      this.add.className = ''
+      this.add.dialog = true
+    },
+    // 确认添加分类
+    confirmEditClassFn() {
+      const classArr = this.getClassArray()
+      if (classArr.indexOf(this.add.className) > -1) {
+        return this.$message({
+          type: 'warning',
+          message: '分类名不能重复'
+        })
+      } else if (this.add.className === '') {
+        return this.$message({
+          type: 'warning',
+          message: '分类名不能为空'
+        })
+      }
+      if (
+        this.datas.keyList.length === 1 &&
+        this.datas.keyList[0].s_item === '未分类'
+      ) {
+        setUserInfoVip({
+          pageType: 'keyWords',
+          classify_index: 0,
+          classify_name: this.add.className,
+          actionType: 'SC'
+        }).then((res) => {
+          if (res.flag) {
+            this.$emit('update')
+            this.add.dialog = false
+          }
+        })
+        return
+      }
+      setUserInfoVip({
+        pageType: 'keyWords',
+        actionType: 'SC',
+        classify_name: this.add.className
+      }).then((res) => {
+        if (res.flag) {
+          this.$emit('update')
+        } else {
+          this.$message({
+            message: '新增分类失败',
+            type: 'error'
+          })
+        }
+      })
+      this.add.dialog = false
+    },
+    // 获取所有分类名
+    getClassArray() {
+      const data = JSON.parse(JSON.stringify(this.datas.keyList))
+      const classArr = []
+      data.forEach((v) => {
+        if (v.s_item) {
+          classArr.push(v.s_item)
+        }
+      })
+      return classArr
+    }
+  }
+}
+</script>
+<style lang="scss" scoped>
+.free-keywords-container {
+  .key-title {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    padding-bottom: 8px;
+    font-size: 18px;
+    color: #1d1d1d;
+    line-height: 28px;
+    border-bottom: 1px solid #ececec;
+    .help-img {
+      display: inline-block;
+      width: 18px;
+      height: 18px;
+      margin: 0 7px 0 8px;
+      cursor: pointer;
+    }
+    .add-classfily {
+      width: 110px;
+      height: 38px;
+      line-height: 38px;
+      border: 1px solid #2cb7ca;
+      border-radius: 6px;
+      text-align: center;
+      color: #2cb7ca;
+      font-size: 14px;
+      cursor: pointer;
+    }
+  }
+  .key-content {
+    padding: 20px 0;
+    .classify-title {
+      display: flex;
+      align-items: center;
+      .title-text {
+        font-size: 16px;
+        color: #1d1d1d;
+      }
+      .icon-edit,
+      .icon-delete {
+        display: inline-block;
+        width: 16px;
+        height: 16px;
+        background-repeat: no-repeat;
+        background-position: center center;
+        cursor: pointer;
+        &::before {
+          content: '' !important;
+        }
+      }
+      .icon-edit {
+        margin: 0 10px;
+        background-image: url('~@/assets/images/icon-edit.png');
+        background-size: contain;
+      }
+      .icon-delete {
+        background-image: url('~@/assets/images/icon-delete.png');
+        background-size: contain;
+      }
+    }
+  }
+  .selector-card {
+    width: 800px;
+    height: auto;
+  }
+  .class-edit-content {
+    width: 740px;
+    padding: 30px 0;
+    height: auto;
+    max-height: 340px;
+    margin: 0 auto;
+    border: 1px solid #ececec;
+    overflow-y: scroll;
+    box-sizing: border-box;
+    .item {
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      margin-bottom: 10px;
+    }
+    .item-label {
+      margin-right: 8px;
+      min-width: 120px;
+      height: 40px;
+      color: #1d1d1d;
+      font-size: 14px;
+      line-height: 40px;
+      text-align: right;
+    }
+    .item-label-required:before {
+      content: '*';
+      color: #f56c6c;
+      margin-right: 2px;
+    }
+    .item-value {
+      width: 352px;
+    }
+    .custom-long-input {
+      width: 352px;
+    }
+  }
+  ::v-deep {
+    .el-dialog__header {
+      display: none;
+    }
+    .el-dialog__body {
+      padding: 0;
+    }
+  }
+}
+.setkey-img {
+  position: absolute;
+  cursor: pointer;
+  top: calc(100% + 100px);
+}
+</style>

+ 680 - 0
apps/bigmember_pc/src/views/subscribe/components/key/FreeSubConfig.vue

@@ -0,0 +1,680 @@
+<template>
+  <div class="free-subscribe-container">
+    <div class="sub-content">
+      <div class="item">
+        <div class="item-label item-label-required">区域:</div>
+        <div class="item-value"  v-loading="!setStatus" element-loading-custom-class="prevent-loading">
+          <AreaCityCountryCascader
+            ref="areaCityCountry"
+            showSelected
+            showCount
+            stopDropdown
+            :showCity='false'
+            :showCountry='false'
+            :areaCount="maxSelectAreaCount"
+            :initMap="areaData"
+            @hideSelect="onAreaHideSelect"
+            @onClick="onAreaSelectClick"
+            @change="onAreaCityCountryChange"
+            @exceed="onExceedChange"
+          >
+            <div slot="header"></div>
+          </AreaCityCountryCascader>
+        </div>
+      </div>
+
+      <div class="item">
+        <div class="item-label item-label-required">信息类型:</div>
+        <div class="item-value" @click="dialog.infoType = true" v-loading="!setStatus" element-loading-custom-class="prevent-loading">
+          <el-input
+            class="custom-long-input"
+            v-model="datas.infoTypeStr"
+            readonly
+            unselectable="on"
+          ></el-input>
+        </div>
+      </div>
+
+      <div class="item">
+        <div class="item-label">关键词匹配方式:</div>
+        <div class="item-value"  v-loading="!setStatus" element-loading-custom-class="prevent-loading">
+          <CheckboxGroupSelector :sourceList="keywordMatchTypeList" keepOne @change="onMatchTypeChange" v-model="datas.matchType"></CheckboxGroupSelector>
+        </div>
+      </div>
+    </div>
+    <!-- 区域选择dialog -->
+    <el-dialog
+      custom-class="sub-dialog"
+      :visible.sync="dialog.area"
+      :close-on-click-modal="false"
+      :show-close="false"
+      center
+      width="460px"
+    >
+      <AreaSelect
+        :initCityMap="datas.areaObj"
+        @onCancel="dialog.area = false"
+        @onConfirm="saveAreaData"
+        :showSelectResult="true"
+        :showSelectedDetail="false"
+        :areaCount="maxSelectAreaCount"
+      >
+        <template #update-tips>超出已购买省份数量,请联系客户成功经理进行升级</template>
+      </AreaSelect>
+    </el-dialog>
+    <!-- 采购单位行业dialog -->
+    <el-dialog
+      custom-class="sub-dialog"
+      :visible.sync="dialog.buyClass"
+      :close-on-click-modal="false"
+      :show-close="false"
+      center
+      width="460px"
+    >
+      <BuyClassSelect
+        :initCate="datas.buyClassArr"
+        :otherMatch="datas.buyclassmatch"
+        @onCancel="dialog.buyClass = false"
+        @onConfirm="saveBuyClassData"
+        @onOtherConfirm="saveOtherBuyClass"
+      >
+        <div slot="header">选择采购单位类型</div>
+      </BuyClassSelect>
+    </el-dialog>
+    <!-- 信息类型dialog -->
+    <el-dialog
+      custom-class="sub-dialog info-type-dialog"
+      :visible.sync="dialog.infoType"
+      :close-on-click-modal="false"
+      :show-close="false"
+      center
+      width="460px"
+    >
+      <InfoTypeSelect
+        :initInfoType="datas.infoTypeArr"
+        showDataType="oneLevel"
+        @onCancel="dialog.infoType = false"
+        @onConfirm="saveInfoTypeData"
+      >
+        <div slot="header">选择信息类型</div>
+      </InfoTypeSelect>
+    </el-dialog>
+    <el-dialog
+      :visible.sync="dialog.areaMap"
+      title="超出可选省份数量"
+      :show-close="false"
+      class="tip-dialog"
+      width="380px"
+      top="30vh"
+      center
+    >
+      <div>
+        可选:<span class="highlight-text">{{ maxSelectAreaCount }}个省</span>,如需增加省份数量,您可联系客服升级大会员。
+      </div>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click.stop="goConcatCustomer">联系客服</el-button>
+        <el-button @click="dialog.areaMap = false">取 消</el-button>
+      </div>
+    </el-dialog>
+    <popupBox
+      ref="popupBox"
+      text="如需修改区域,请联系企业管理员修改分发设置区域。"
+      title="不支持修改区域"
+    ></popupBox>
+  </div>
+</template>
+<script>
+import { Switch, Input, RadioGroup, Radio, Button, Dialog, Checkbox } from 'element-ui'
+import AreaSelect from '@/components/selector/AreaSelector'
+import BuyClassSelect from '@/components/selector/BuyerclassSelector'
+import InfoTypeSelect from '@/components/selector/InfoTypeSelector'
+import popupBox from '@/components/common/popupBox'
+import AreaCityCountryCascader from '@/components/selector-cascader/AreaCityCountryCascader'
+import CheckboxGroupSelector from '@/components/selector/CheckboxGroupSelector'
+import { keywordMatchTypeListOfFree } from '@/assets/js/selector.js'
+import AmountRangeSelector from '@/components/selector/AmountRangeSelector.vue'
+import {
+  setAreaBuyer,
+  setUserInfoVip,
+  userRule,
+  entBaseInfo
+} from '@/api/modules'
+export default {
+  name: 'free-sub-config',
+  props: {
+    datas: {
+      areaStr: String,
+      buyClassStr: String,
+      infoTypeStr: String,
+      projectmatch: Number,
+      buyclassmatch: Number,
+      mathway: Number,
+      areaObj: Object,
+      areaCount: Number, // 用户购买了几个省份(单省版本)
+      buyClassArr: Array,
+      infoTypeArr: Array,
+      matchType: Array,
+      amount: String
+    },
+    // 订阅设置开关
+    setStatus: {
+      type: Boolean,
+      default: false
+    }
+  },
+  components: {
+    [Switch.name]: Switch,
+    [Input.name]: Input,
+    [RadioGroup.name]: RadioGroup,
+    [Radio.name]: Radio,
+    [Button.name]: Button,
+    [Dialog.name]: Dialog,
+    AreaSelect,
+    BuyClassSelect,
+    InfoTypeSelect,
+    popupBox,
+    AreaCityCountryCascader,
+    CheckboxGroupSelector,
+    AmountRangeSelector
+  },
+  data() {
+    return {
+      projectSwitch: {
+        loading: false,
+        disabled: false
+      },
+      unitSwitch: {
+        loading: false,
+        disabled: false
+      },
+      dialog: {
+        area: false,
+        buyClass: false,
+        infoType: false,
+        areaMap: false
+      },
+      areaData: {},
+      buyerData: [],
+      timeout: null,
+      isSetArea: false,
+      keywordMatchTypeList:  keywordMatchTypeListOfFree,
+      switchMode: this.setStatus
+    }
+  },
+  computed: {
+    // -1表示可选范围为全国
+    maxSelectAreaCount() {
+      const { areaCount } = this.datas
+      return areaCount || -1
+    },
+    formatAreaMap() {
+      const original = this.areaData
+      // if (!original) return ''
+      // 原二级数据结构为{福建:['福州', '厦门', '宁德']}
+      // 现数据结构为三级 需进行处理
+      if (Object.keys(original).length > 0) {
+        const obj = {}
+        for (const province in original) {
+          let cityMap = {}
+          if (Array.isArray(original[province])) {
+            original[province].forEach((city) => {
+              cityMap[city] = []
+            })
+          } else {
+            cityMap = original[province]
+          }
+          obj[province] = cityMap
+        }
+        return obj
+      } else {
+        return original
+      }
+    }
+  },
+  watch: {
+    'datas.areaObj'(newVal) {
+      this.areaData = newVal
+      try {
+        this.$refs.areaCityCountry.setState('')
+        this.$refs.areaCityCountry.setState(newVal)
+      } catch (error) {}
+    },
+    'datas.buyClassArr'(newVal) {
+      this.buyerData = newVal
+    }
+  },
+  mounted() {},
+  methods: {
+    onAmountRangeChange(amount) {
+      const { value } = amount
+      this.commonSetApi({
+        pageType: 'amount',
+        matchtype: value,
+      })
+    },
+    // 关键词匹配方式选择结果回调(新)
+    onMatchTypeChange(data) {
+      this.commonSetApi({
+        pageType: 'matchmode',
+        matchtype: data.toString(),
+      })
+    },
+    debounce(fn, delay) {
+      if (this.timeout) {
+        clearTimeout(this.timeout)
+      }
+      this.timeout = setTimeout(() => {
+        fn()
+      }, delay)
+    },
+    clickErea() {
+      this.debounce(() => {
+        entBaseInfo({}).then((res) => {
+          if (
+            res.data.vip_power === 1 ||
+            res.data.member_power === 1 ||
+            res.data.user_power === 1
+          ) {
+            // 企业授权
+            userRule().then((res_) => {
+              if (res_.data) {
+                // 设置了区域
+                this.$refs.popupBox.isshow = true
+              } else {
+                this.dialog.area = true
+              }
+            })
+          } else {
+            // 非企业授权
+            this.dialog.area = true
+          }
+        })
+      }, 200)
+    },
+    // 切换项目匹配按钮
+    switchProjectMatch(event) {
+      this.projectSwitch.loading = true
+      this.projectSwitch.disabled = true
+      this.commonSetApi(
+        {
+          pageType: 'projectMatch',
+          pmindex: event
+        },
+        () => {
+          this.projectSwitch.loading = false
+          this.projectSwitch.disabled = false
+          this.$message({
+            message: event === 1 ? '项目匹配已开启' : '项目匹配已关闭',
+            type: 'success'
+          })
+        }
+      )
+    },
+    // 原关键词匹配方式(弃用)
+    async chooseMathWay(state) {
+      this.commonSetApi({
+        pageType: 'saveSeniorset',
+        matchtype: state
+      })
+    },
+    // 保存区域修改数据(已停用)
+    saveAreaData(data) {
+      setAreaBuyer({
+        vSwitch: 'm',
+        area: JSON.stringify(data),
+        industry: this.buyerData.toString()
+      }).then((res) => {
+        if (res.data.doSuccess) {
+          this.$emit('update')
+          this.dialog.area = false
+        } else {
+          this.$message({
+            message: '修改失败',
+            type: 'error'
+          })
+        }
+      })
+    },
+    // 保存采购单位行业数据及匹配未分类行业数据
+    saveBuyClassData(data) {
+    const {area, district} = this.$refs.areaCityCountry.formatProvinceAndCities(this.areaData)
+      setAreaBuyer({
+        vSwitch: 'm',
+        // area: JSON.stringify(this.areaData),
+        area: JSON.stringify(area),
+        district: JSON.stringify(district),
+        industry: data.toString()
+      }).then((res) => {
+        if (res.data.doSuccess) {
+          this.$emit('update')
+          this.dialog.buyClass = false
+        } else {
+          this.$message({
+            message: '修改失败',
+            type: 'error'
+          })
+        }
+      })
+    },
+    // 保存未匹配采购单位行业
+    saveOtherBuyClass(data) {
+      this.commonSetApi({
+        pageType: 'other_buyerclass',
+        other: data.selected ? 1 : 0
+      })
+    },
+    // 保存信息类型数据
+    saveInfoTypeData(data) {
+      this.commonSetApi(
+        {
+          pageType: 'infoType',
+          infoTypeArr: data.toString(),
+          vSwitch: 'f'
+        },
+        () => {
+          this.dialog.infoType = false
+        }
+      )
+    },
+    // 设置接口
+    commonSetApi(data, callback) {
+      setUserInfoVip(data).then((res) => {
+        if (res.flag) {
+          this.$emit('update')
+          callback && callback()
+        } else {
+          this.$message({
+            message: '修改失败',
+            type: 'error'
+          })
+        }
+      })
+    },
+    // 组件选择地区回调事件(场景:每次选择都需保存)
+    onAreaCityCountryChange(data) {
+      const { area, district } = data
+      console.log(area, district)
+    },
+    async onAreaSelectClick() {
+      const { data } = await entBaseInfo()
+      if (
+        data.vip_power === 1 ||
+        data.member_power === 1 ||
+        data.user_power === 1
+      ) {
+        // 企业授权
+        const { data: flag } = await userRule()
+        if (flag) {
+          this.$refs.popupBox.isshow = true
+        } else {
+          this.$refs.areaCityCountry.toggleSelect()
+        }
+      } else {
+        this.$refs.areaCityCountry.toggleSelect()
+      }
+    },
+    // 当下拉框隐藏时回调事件(应用场景:每次选择地区时不需要都调用接口,等所有选择完 下拉框隐藏时再调用)
+    onAreaHideSelect(data) {
+      const { area, district } = data
+      setAreaBuyer({
+        vSwitch: 'f',
+        area: JSON.stringify(area),
+        industry: this.buyerData.toString(),
+        district: JSON.stringify(district)
+      }).then((res) => {
+        if (res?.data?.doSuccess) {
+          this.$emit('update')
+          this.dialog.area = false
+        } else {
+          this.$message({
+            message: res.errMsg || '修改失败',
+            type: 'error'
+          })
+        }
+      })
+    },
+    // 区域选择超出冒泡事件
+    onExceedChange() {
+      this.dialog.areaMap = true
+    },
+    goConcatCustomer() {
+      this.contactCustomer(this)
+    }
+  }
+}
+</script>
+<style lang="scss" scoped>
+.free-subscribe-container {
+  .sub-content {
+    padding: 20px 0;
+  }
+
+  .item {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    margin-bottom: 10px;
+  }
+
+  .item-label {
+    margin-right: 8px;
+    min-width: 120px;
+    height: 40px;
+    color: #1d1d1d;
+    font-size: 14px;
+    line-height: 40px;
+    text-align: right;
+  }
+
+  .item-label-required:before {
+    content: '*';
+    color: #f56c6c;
+    margin-right: 2px;
+  }
+
+  .item-value {
+    position: relative;
+    width: 352px;
+    .item-mask {
+      position: absolute;
+      top: 0;
+      left: 0;
+      bottom: 0;
+      right: 0;
+      z-index: 10000;
+    }
+  }
+
+  .custom-long-input {
+    width: 352px;
+  }
+
+  .item-other {
+    display: flex;
+    justify-content: center;
+    margin-bottom: 10px;
+  }
+
+  .item-other-value {
+    margin-top: 8px;
+  }
+
+  .math-tips {
+    margin-top: 4px;
+    font-size: 12px;
+    color: #999999;
+    line-height: 20px;
+  }
+
+  .radio-item {
+    margin-bottom: 10px;
+  }
+
+  // element-ui样式修改
+  ::v-deep {
+    .custom-long-input .el-input__inner {
+      font-size: 14px;
+      color: #2cb7ca;
+      white-space: nowrap;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      border-color: #ececec;
+    }
+
+    .el-loading-spinner .circular {
+      width: 22px;
+      height: 22px;
+    }
+
+    .el-loading-spinner {
+      margin-top: -11px;
+    }
+
+    .el-input__inner {
+      background: #fff;
+      cursor: pointer;
+    }
+
+    .el-switch {
+      position: relative;
+    }
+
+    .el-switch__label--left {
+      position: absolute;
+      left: 6px;
+      top: 0;
+      color: #fff;
+      z-index: 9;
+      font-size: 12px;
+    }
+
+    .el-switch__label--right {
+      position: absolute;
+      right: 6px;
+      top: 0;
+      color: #fff;
+      z-index: 9;
+      font-size: 12px;
+    }
+
+    .el-switch__label * {
+      font-size: 12px;
+    }
+
+    .el-switch,
+    .el-switch__core {
+      height: 22px;
+      line-height: 22px;
+    }
+
+    .el-switch__core:after {
+      width: 18px;
+      height: 18px;
+    }
+
+    .el-switch.is-checked .el-switch__core::after {
+      margin-left: -18px !important;
+    }
+
+    .el-radio {
+      color: #1d1d1d;
+      font-size: 14px;
+    }
+
+    .el-radio__inner {
+      width: 20px;
+      height: 20px;
+    }
+
+    .el-radio__input.is-checked .el-radio__inner {
+      border: 0;
+      background: transparent;
+      width: 20px;
+      height: 20px;
+      background: url('~@/assets/images/icon-checked.png') no-repeat center
+        center;
+      background-size: contain;
+    }
+
+    .el-radio__inner::after {
+      background: transparent;
+    }
+
+    .el-radio__input.is-checked + .el-radio__label {
+      color: #1d1d1d;
+    }
+
+    .el-radio__inner:hover {
+      border-color: #ececec;
+    }
+    .select-region {
+      left: 0 !important;
+    }
+    .s-header {
+      display: none;
+    }
+    .el-select {
+      width: 100%;
+      .el-input__inner {
+        height: 40px;
+        line-height: 40px;
+      }
+      .select-prefix {
+        height: 38px;
+        line-height: 38px;
+      }
+    }
+    .tip-dialog {
+      .el-button--primary,
+      .el-button--primary:hover,
+      .el-button--primary:focus {
+        width: 132px;
+        height: 36px;
+        margin-right: 52px;
+        text-align: center;
+        background: #2cb7ca;
+        border-radius: 6px;
+        font-style: 16px;
+        color: #fff;
+        border: 0;
+      }
+      .el-dialog {
+        border-radius: 8px;
+      }
+      .el-dialog__header {
+        padding: 32px 0 0;
+      }
+      .el-dialog__body {
+        padding: 20px 32px 32px;
+        color: #686868;
+        font-size: 14px;
+        line-height: 22px;
+        text-align: center;
+      }
+      .el-dialog__body i {
+        color: #2cb7ca;
+      }
+      .el-button {
+        width: 132px;
+        height: 36px;
+        padding: 0;
+        text-align: center;
+        font-size: 16px;
+      }
+      .el-dialog__footer {
+        padding: 0 0 32px;
+      }
+    }
+    .el-select-dropdown.select-custom{
+      left: 0!important;
+    }
+
+    .module-container.province-container {
+      width: 100%;
+      .el-icon-arrow-right {
+        display: none;
+      }
+    }
+  }
+}
+</style>

+ 2 - 1
apps/bigmember_pc/src/views/subscribe/components/key/List.vue

@@ -539,7 +539,8 @@ export default {
       const datas = {
         pageType: 'keyWords',
         actionType: 'DK',
-        delete_key: JSON.stringify(obj)
+        delete_key: JSON.stringify(obj),
+        vSwitch: this.$route.meta?.vt
       }
       this.setInfoApi(datas, () => {
         this.dialog.delKey = false