瀏覽代碼

Merge branch 'feature/v1.0.45' of https://jygit.jydev.jianyu360.cn/jianyu/web into dev/v1.0.45_wmh

wenmenghao 1 年之前
父節點
當前提交
87d967f310
共有 100 個文件被更改,包括 11668 次插入169 次删除
  1. 1 0
      apps/bigmember_pc/.env.development
  2. 1 0
      apps/bigmember_pc/.env.production
  3. 3 4
      apps/bigmember_pc/index.html
  4. 1 0
      apps/bigmember_pc/package.json
  5. 1 0
      apps/bigmember_pc/src/App.vue
  6. 26 0
      apps/bigmember_pc/src/api/modules/bi.js
  7. 17 0
      apps/bigmember_pc/src/api/modules/common.js
  8. 18 0
      apps/bigmember_pc/src/api/modules/crmApplication.js
  9. 11 0
      apps/bigmember_pc/src/api/modules/dataExport.js
  10. 3 0
      apps/bigmember_pc/src/api/modules/index.js
  11. 12 0
      apps/bigmember_pc/src/api/modules/nzj.js
  12. 61 0
      apps/bigmember_pc/src/api/modules/search.js
  13. 二進制
      apps/bigmember_pc/src/assets/images/empty/jy-loading.gif
  14. 二進制
      apps/bigmember_pc/src/assets/images/icon/select-down-icon.png
  15. 二進制
      apps/bigmember_pc/src/assets/images/icon/tip2.png
  16. 316 6
      apps/bigmember_pc/src/assets/js/selector.js
  17. 85 0
      apps/bigmember_pc/src/assets/js/selector/scope.js
  18. 71 1
      apps/bigmember_pc/src/assets/style/common.scss
  19. 48 4
      apps/bigmember_pc/src/assets/style/pic-icon.scss
  20. 134 34
      apps/bigmember_pc/src/components/article-item/ArticleItem.vue
  21. 2 1
      apps/bigmember_pc/src/components/collect-info/CollectInfo.vue
  22. 16 4
      apps/bigmember_pc/src/components/crm-info/crmAction.vue
  23. 312 0
      apps/bigmember_pc/src/components/filter-items/AmountRangeSelector.vue
  24. 146 0
      apps/bigmember_pc/src/components/filter-items/AttachmentSelector.vue
  25. 89 0
      apps/bigmember_pc/src/components/filter-items/BasePowerLayout.vue
  26. 71 0
      apps/bigmember_pc/src/components/filter-items/BuyerTypeSelector.vue
  27. 416 0
      apps/bigmember_pc/src/components/filter-items/CascadeContent.vue
  28. 177 0
      apps/bigmember_pc/src/components/filter-items/ChangeHandsDropdown.vue
  29. 119 0
      apps/bigmember_pc/src/components/filter-items/CheckboxGroupSelector.vue
  30. 134 0
      apps/bigmember_pc/src/components/filter-items/CommonCheckboxSelector.vue
  31. 148 0
      apps/bigmember_pc/src/components/filter-items/CommonSingleChoice.vue
  32. 152 0
      apps/bigmember_pc/src/components/filter-items/ContactSelector.vue
  33. 311 0
      apps/bigmember_pc/src/components/filter-items/EntamountRangeData.vue
  34. 338 0
      apps/bigmember_pc/src/components/filter-items/EstablishTimeSelector.vue
  35. 71 0
      apps/bigmember_pc/src/components/filter-items/IndustrySelector.vue
  36. 103 0
      apps/bigmember_pc/src/components/filter-items/InfoTypeDropdown.vue
  37. 76 0
      apps/bigmember_pc/src/components/filter-items/KeywordSelector.vue
  38. 107 0
      apps/bigmember_pc/src/components/filter-items/KeywordTagsSelector.vue
  39. 189 0
      apps/bigmember_pc/src/components/filter-items/KeywordTagsSelectorContent.vue
  40. 176 0
      apps/bigmember_pc/src/components/filter-items/Layout.vue
  41. 290 0
      apps/bigmember_pc/src/components/filter-items/OnecascadeContent.vue
  42. 800 0
      apps/bigmember_pc/src/components/filter-items/RegionCollapseSelector.vue
  43. 995 0
      apps/bigmember_pc/src/components/filter-items/RegionSelector.vue
  44. 160 0
      apps/bigmember_pc/src/components/filter-items/SearchRangeDropdown.vue
  45. 130 0
      apps/bigmember_pc/src/components/filter-items/SearchScopeSelector.vue
  46. 101 0
      apps/bigmember_pc/src/components/filter-items/SelectorWithBasePower.vue
  47. 165 0
      apps/bigmember_pc/src/components/search-input/SearchInput.vue
  48. 45 3
      apps/bigmember_pc/src/components/selector/InfoTypeSelector.vue
  49. 55 22
      apps/bigmember_pc/src/components/selector/InfoTypeSelectorContent.vue
  50. 229 0
      apps/bigmember_pc/src/components/selector/SearchTimeScopeSelector.vue
  51. 3 2
      apps/bigmember_pc/src/components/selector/SelectorCard.vue
  52. 753 0
      apps/bigmember_pc/src/components/selector/SelectorCascader.vue
  53. 11 7
      apps/bigmember_pc/src/components/selector/TagSelector.vue
  54. 24 1
      apps/bigmember_pc/src/components/selector/TimeSelector.vue
  55. 255 25
      apps/bigmember_pc/src/components/selector/TimeSelectorContent.vue
  56. 303 0
      apps/bigmember_pc/src/components/selector/basicDropdown.vue
  57. 504 0
      apps/bigmember_pc/src/components/selector/timeDropdown.vue
  58. 1 0
      apps/bigmember_pc/src/components/subscribe-manager/KeyConfig.vue
  59. 0 2
      apps/bigmember_pc/src/components/subscribe-manager/powerPerson.vue
  60. 1 1
      apps/bigmember_pc/src/components/toast/index.js
  61. 2 0
      apps/bigmember_pc/src/main.js
  62. 45 5
      apps/bigmember_pc/src/router/modules/order.js
  63. 42 0
      apps/bigmember_pc/src/router/modules/search.js
  64. 7 1
      apps/bigmember_pc/src/router/router-interceptors.js
  65. 2 0
      apps/bigmember_pc/src/router/router.js
  66. 3 2
      apps/bigmember_pc/src/router/routers.js
  67. 15 1
      apps/bigmember_pc/src/store/user.js
  68. 23 0
      apps/bigmember_pc/src/utils/brace/index.js
  69. 10 0
      apps/bigmember_pc/src/utils/common.js
  70. 1 0
      apps/bigmember_pc/src/utils/directive/index.js
  71. 52 0
      apps/bigmember_pc/src/utils/directive/modules/visited.js
  72. 116 0
      apps/bigmember_pc/src/utils/format/date.js
  73. 26 0
      apps/bigmember_pc/src/utils/format/info-type-transform.js
  74. 1058 0
      apps/bigmember_pc/src/utils/format/search-bid-filter.js
  75. 39 1
      apps/bigmember_pc/src/utils/globalFunctions.js
  76. 2 0
      apps/bigmember_pc/src/utils/index.js
  77. 80 0
      apps/bigmember_pc/src/utils/mixins/visited-setup.js
  78. 1 0
      apps/bigmember_pc/src/utils/prototype/index.js
  79. 4 0
      apps/bigmember_pc/src/utils/prototype/modules/visited.js
  80. 83 0
      apps/bigmember_pc/src/utils/visited/index.js
  81. 53 0
      apps/bigmember_pc/src/utils/visited/transform.js
  82. 22 0
      apps/bigmember_pc/src/utils/whichContainer.js
  83. 4 2
      apps/bigmember_pc/src/views/BidrenewalDialog/index.vue
  84. 24 2
      apps/bigmember_pc/src/views/article-content/components/ContentBIActions.vue
  85. 1 0
      apps/bigmember_pc/src/views/article-content/components/ContentHeader.vue
  86. 22 6
      apps/bigmember_pc/src/views/article-content/components/Nps.vue
  87. 2 2
      apps/bigmember_pc/src/views/article-content/pages/Article.vue
  88. 20 2
      apps/bigmember_pc/src/views/medical-field/Credentials.vue
  89. 21 0
      apps/bigmember_pc/src/views/order/components/big-member/buy-tip.vue
  90. 5 6
      apps/bigmember_pc/src/views/order/components/data-export/buy-tip.vue
  91. 22 0
      apps/bigmember_pc/src/views/order/components/doc-member/buy-tip.vue
  92. 3 3
      apps/bigmember_pc/src/views/order/components/doc-member/info.vue
  93. 1 0
      apps/bigmember_pc/src/views/order/components/resource-pack/buy-tip.vue
  94. 21 0
      apps/bigmember_pc/src/views/order/components/vipsubscribe/buy-tip.vue
  95. 36 17
      apps/bigmember_pc/src/views/portrayal/components/DataExportTip.vue
  96. 2 2
      apps/bigmember_pc/src/views/project/AttendBiddingList.vue
  97. 0 0
      apps/bigmember_pc/src/views/search/Layout.vue
  98. 218 0
      apps/bigmember_pc/src/views/search/bidding/components/history-filter-dialog.vue
  99. 612 0
      apps/bigmember_pc/src/views/search/bidding/components/recommend-card.vue
  100. 181 0
      apps/bigmember_pc/src/views/search/bidding/components/save-filter-dialog.vue

+ 1 - 0
apps/bigmember_pc/.env.development

@@ -2,3 +2,4 @@ NODE_ENV=development
 VITE_APP_BASE_API='/dev-api'
 VITE_APP_BASE_URL='/swordfish/page_big_pc'
 VITE_APP_BASE_PUBLIC='/'
+VITE_APP_WORK_DESKTOP_URL='/page_workDesktop/work-bench/app'

+ 1 - 0
apps/bigmember_pc/.env.production

@@ -2,3 +2,4 @@ NODE_ENV=production
 VITE_APP_BASE_API=''
 VITE_APP_BASE_URL='/page_big_pc'
 VITE_APP_BASE_PUBLIC='/page_big_pc/'
+VITE_APP_WORK_DESKTOP_URL='/page_workDesktop/work-bench/app/big'

+ 3 - 4
apps/bigmember_pc/index.html

@@ -24,22 +24,21 @@
       <link ignore href='https://jybx2-webtest.jydev.jianyu360.com/css/pc.css?v=6302' rel="stylesheet"/>
       <link ignore href='https://jybx2-webtest.jydev.jianyu360.com/pccss/public-nav-1200.css?v=6302' rel="stylesheet" type="text/css"/>
 
-      <script src=//cdn-common.jianyu360.com/cdn/lib/jquery/3.6.0/jquery.min.js></script>
+      <link ignore rel="stylesheet" href="https://at.alicdn.com/t/c/font_624651_o2us2uwpt6b.css">
     <% } %>
 
-      <!-- 使用CDN的CSS文件 -->
-      <link ignore rel="stylesheet" href="https://cdn-common.jianyu360.com/cdn/assets/iconfont/pc/24.2.21/iconfont.css">
-
       <!-- 使用CDN的CSS文件 -->
       <% for (var i in cdn && cdn.css) { %>
       <link rel="stylesheet" href="<%= cdn.css[i] %>" />
       <% } %>
       <!-- 使用CDN的JS文件 -->
+      <script ignore src=//cdn-common.jianyu360.com/cdn/lib/jquery/3.6.0/jquery.min.js></script>
       <% for (var i in cdn && cdn.js) { %>
       <script type="text/javascript" src="<%= cdn.js[i] %>"></script>
       <% } %>
 
       <% if (!isDev) { %>
+      <link ignore rel="stylesheet" href="https://cdn-common.jianyu360.com/cdn/assets/iconfont/pc/24.5.6/iconfont.css">
       <link href='/css/reset.css?v=6302' rel="stylesheet" type="text/css"/>
       <link href='/pccss/reset_pc.css' rel="stylesheet" type="text/css"/>
       <script src="/antiRes/js/mainHook.js"></script>

+ 1 - 0
apps/bigmember_pc/package.json

@@ -12,6 +12,7 @@
   "dependencies": {
     "@jy/util": "workspace:^",
     "@jy/data-models": "workspace:^",
+    "@jy/pc-ui": "workspace:^",
     "@jianyu/easy-fix-sub-app": "^0.0.2",
     "@jianyu/easy-inject-qiankun": "^0.1.11",
     "@jianyu/icon": "^0.1.7",

+ 1 - 0
apps/bigmember_pc/src/App.vue

@@ -33,6 +33,7 @@ export default {
 <style lang="scss">
 @import '~@/assets/style/common.scss';
 @import '~@/assets/style/uno.common.scss';
+@import '~@/assets/style/pic-icon.scss';
 @import '~@/assets/style/reset-ele.scss';
 @import '~@/assets/style/reset-qiankun.scss';
 

+ 26 - 0
apps/bigmember_pc/src/api/modules/bi.js

@@ -0,0 +1,26 @@
+import request from '@/api'
+
+// 获取已添加的信息id数组
+export function ajaxGetInfoIds() {
+  return request({
+    url: '/jyapi/biService/getInfoId',
+    method: 'post'
+  })
+}
+
+// 添加信息
+export function ajaxSetInfoId(data) {
+  return request({
+    url: '/jyapi/biService/addProject',
+    method: 'post',
+    data
+  })
+}
+
+// 获取物业专版的筛选条件
+export function getPropertyFilters() {
+  return request({
+    url: '/jyapi/jybx/core/property/searchCriteria',
+    method: 'post'
+  })
+}

+ 17 - 0
apps/bigmember_pc/src/api/modules/common.js

@@ -0,0 +1,17 @@
+import request from '@/api'
+import qs from 'qs'
+
+/**
+ * 获取客服二维码
+ * @param data { type: "kf" } 只获取客服的二维码,
+ * 不传参根据用户权限判断是否返回客成二维码
+ * @returns {*}
+ */
+export function getCustomInfo (data) {
+  data = qs.stringify(data)
+  return request({
+    url: '/bigmember/use/getCustom',
+    method: 'POST',
+    data
+  })
+}

+ 18 - 0
apps/bigmember_pc/src/api/modules/crmApplication.js

@@ -18,6 +18,24 @@ export function ajaxEmployOperate(data) {
   })
 }
 
+// 判断情报是否创建
+export function ajaxOptOperateExist (data) {
+  return request({
+    url: '/jyNewApi/property/information/exist',
+    method: 'post',
+    data
+  })
+}
+
+// 物业专版批量收录
+export function ajaxOptEmployOperate(data) {
+  return request({
+    url: '/jyNewApi/property/information/batch/create',
+    method: 'post',
+    data
+  })
+}
+
 // 收录情况
 export function ajaxEmployInfo(data) {
   return request({

+ 11 - 0
apps/bigmember_pc/src/api/modules/dataExport.js

@@ -104,3 +104,14 @@ export function ajaxGetDontPromptAgain(data) {
     data
   })
 }
+
+
+// 数据导出-标讯搜索数据导出逻辑
+export function searchIndexDataExport(data) {
+  data = qs.stringify(data)
+  return request({
+    url: '/front/wx_dataExport/searchExport',
+    method: 'post',
+    data
+  })
+}

+ 3 - 0
apps/bigmember_pc/src/api/modules/index.js

@@ -30,3 +30,6 @@ export * from './pay'
 export * from './message'
 export * from './business'
 export * from './docs'
+export * from './search'
+export * from './nzj'
+export * from './common'

+ 12 - 0
apps/bigmember_pc/src/api/modules/nzj.js

@@ -0,0 +1,12 @@
+import request from '@/api'
+import qs from 'qs'
+
+// 拟在建筛选条件
+export function ajaxGetSearchNzjCondition(data) {
+  data = qs.stringify(data)
+  return request({
+    url: '/front/project/nzj/conditions',
+    method: 'post',
+    data
+  })
+}

+ 61 - 0
apps/bigmember_pc/src/api/modules/search.js

@@ -0,0 +1,61 @@
+import request from '@/api'
+
+// 检查已存筛选是否pass
+export function checkBiddingFilterPass(data) {
+  return request({
+    url: '/jyapi/jybx/base/checkSearchScreen',
+    method: 'POST',
+    data: data
+  })
+}
+// 添加筛选条件
+export function addBiddingFilter(data) {
+  return request({
+    url: '/jyapi/jybx/base/addSearchScreen',
+    method: 'post',
+    data
+  })
+}
+
+// 获取已存筛选列表
+export function getBiddingFilterList() {
+  return request({
+    url: '/jyapi/jybx/base/showSearchScreen',
+    method: 'post',
+    noToast: true
+  })
+}
+
+// 删除/批量删除已存筛选
+export function deleteBiddingFilter(data) {
+  return request({
+    url: '/jyapi/jybx/base/delSearchScreen',
+    method: 'post',
+    data
+  })
+}
+
+// 获取中国移动融创
+export function getCMCustomInfo () {
+  return request({
+    url: '/jylab/supsearch/searchPower',
+    method: 'post'
+  })
+}
+
+/**
+ * 检测当前账号是否在反爬虫白名单中
+ * 如果在反爬白名单,则空搜索刷新搜索结果(即允许空搜索)
+ * 不在,则不允许空搜索(此处空搜索指的是主搜索框是否为空)
+ *
+ * 该接口也返回一些校验关键词输入规范的正则,
+ * 需要在搜索前进行校验是否可以进行搜索
+ * @returns {*}
+ */
+
+export function getInAntiSpiderWhiteList () {
+  return request({
+    url: '/publicapply/userbase/whitelist',
+    method: 'post'
+  })
+}

二進制
apps/bigmember_pc/src/assets/images/empty/jy-loading.gif


二進制
apps/bigmember_pc/src/assets/images/icon/select-down-icon.png


二進制
apps/bigmember_pc/src/assets/images/icon/tip2.png


+ 316 - 6
apps/bigmember_pc/src/assets/js/selector.js

@@ -1,3 +1,5 @@
+export * from './selector/scope'
+
 /* eslint-disable */
 // 城市选择/筛选 - 省份与字母IndexBar对照数据
 // 在[@/components/selector/AreaSelectorCard.vue]中使用
@@ -66,7 +68,8 @@ export const cateListMapExp = {
 
 // 信息类型数据
 // 在[@/components/selector/InfoTypeSelectorCard.vue]中使用
-export const infoTypeListExp = [
+// 信息类型筛选数据拆分(仅超前项目:采购意向,拟建项目)
+export const infoTypeAdvancedList = [
   {
     name: '全部',
     value: '',
@@ -86,10 +89,20 @@ export const infoTypeListExp = [
     showHelp: true,
     level: 1,
     children: []
+  }
+]
+
+// 信息类型筛选数据拆分(移除超前项目)
+export const infoTypeNotAdvancedList = [
+  {
+    name: '全部',
+    value: '',
+    level: 0,
+    children: []
   },
   {
     name: '招标预告',
-    value: '',
+    value: '招标预告',
     level: 1,
     children: [
       {
@@ -121,7 +134,7 @@ export const infoTypeListExp = [
   },
   {
     name: '招标公告',
-    value: '',
+    value: '招标公告',
     level: 1,
     children: [
       {
@@ -168,7 +181,7 @@ export const infoTypeListExp = [
   },
   {
     name: '招标结果',
-    value: '',
+    value: '招标结果',
     level: 1,
     children: [
       {
@@ -195,7 +208,7 @@ export const infoTypeListExp = [
   },
   {
     name: '招标信用信息',
-    value: '',
+    value: '招标信用信息',
     level: 1,
     children: [
       {
@@ -217,6 +230,26 @@ export const infoTypeListExp = [
   }
 ]
 
+// 信息类型筛选数据(包含全部信息类型的完整数据)
+export const infoTypeListExp = (function () {
+  const arr = [...infoTypeAdvancedList]
+  infoTypeNotAdvancedList.forEach((item) => {
+    if (item.level !== 0 && item.value) {
+      arr.push(item)
+    }
+  })
+  return arr
+})()
+
+export const infoTypeListMapExp = {
+  拟建项目: ['拟建项目'],
+  采购意向: ['采购意向'],
+  招标预告: ['预告', '预审', '预审结果', '论证意见', '需求公示'],
+  招标公告: ['招标', '邀标', '询价', '竞谈', '单一', '竞价', '变更'],
+  招标结果: ['中标', '成交', '废标', '流标'],
+  招标信用信息: ['合同', '验收', '违规'],
+}
+
 // 行业分类数据
 // 在[@/components/selector/IndustrySelectorCard.vue]中使用
 export const industryListMapExp = {
@@ -712,7 +745,7 @@ export const keywordMatchTypeList = [
   }
 ]
 
-// 金额区间
+// 筛选项-金额区间
 export const amountRangeData = [
   {
     value: '',
@@ -756,3 +789,280 @@ export const amountRangeData = [
     disabled: true
   }
 ]
+//成立时间
+export const timeRangeData = [
+  {
+    label: '全部',
+    value: ''
+  },
+  {
+    label: '近1年内',
+    value: '-1y'
+  },
+  {
+    label: '1-3年',
+    value: '1y-3y'
+  },
+  {
+    label: '3-5年',
+    value: '3y-5y'
+  },
+  {
+    label: '5-10年',
+    value: '5y-10y'
+  },
+  {
+    label: '10年以上',
+    value: '10y-'
+  },
+  {
+    value: '0',
+    label: '自定义',
+    disabled: true
+  }
+]
+
+// 筛选项-附件
+export const attachmentData = [
+  {
+    value: '0',
+    label: '全部'
+  },
+  {
+    value: '1',
+    label: '有附件'
+  },
+  {
+    value: '-1',
+    label: '无附件'
+  }
+]
+
+// 筛选项-信息来源
+export const infoSource = [
+  {
+    value: '',
+    label: '全部'
+  },
+  {
+    value: '1',
+    label: '个人订阅'
+  },
+  {
+    value: '2',
+    label: '企业自动分发'
+  },
+  {
+    value: '3',
+    label: '企业手动分发'
+  }
+]
+
+// 筛选项-查看状态
+export const viewStatusData = [
+  {
+    value: '',
+    label: '全部'
+  },
+  {
+    value: '0',
+    label: '未查看'
+  },
+  {
+    value: '1',
+    label: '已查看'
+  }
+]
+
+// 筛选项-中标企业联系方式
+export const winnerContactData = [
+  {
+    label: '不限',
+    value: ''
+  },
+  {
+    label: '有中标企业联系方式',
+    value: 'y'
+  }
+]
+
+// 筛选项-中标企业联系方式
+export const buyerContactData = [
+  {
+    label: '不限',
+    value: ''
+  },
+  {
+    label: '有采购单位联系方式',
+    value: 'y'
+  }
+]
+// 搜索模式列表
+export const searchModeList = [
+  {
+    label: '精准搜索',
+    key: '0'
+  },
+  {
+    label: '模糊搜索',
+    key: '1'
+  }
+]
+
+// 搜索接口参数表
+export const biddingSearchListType = [
+  'fType', // 免费用户
+  'pType', // 付费用户
+  'vType', // 超级订阅用户
+  'mType', // 大会员用户
+  'eType' // 商机管理用户
+]
+
+// 招标搜索范围
+export const biddingSearchScope = [
+  {
+    label: '标题搜索',
+    key: 'title'
+  },
+  {
+    label: '正文搜索',
+    key: 'content'
+  },
+  {
+    label: '附件',
+    key: 'file'
+  },
+  {
+    label: '项目名称/标的物',
+    key: 'ppa',
+    needPower: true
+  },
+  {
+    label: '采购单位',
+    key: 'buyer',
+    needPower: true
+  },
+  {
+    label: '中标企业',
+    key: 'winner',
+    needPower: true
+  },
+  {
+    label: '招标代理机构',
+    key: 'agency',
+    needPower: true
+  }
+]
+
+export const biddingSearchTime = [
+  {
+    label: '最近7天',
+    key: 'lately-7'
+  },
+  {
+    label: '最近30天',
+    key: 'lately-30'
+  },
+  {
+    label: '最近1年',
+    key: 'thisyear'
+  },
+  {
+    label: '最近3年',
+    key: 'threeyear',
+    needPower: true
+  },
+  {
+    label: '最近5年',
+    key: 'fiveyear',
+    needPower: true
+  },
+  {
+    label: '自定义',
+    key: 'exact',
+    needPower: true
+  }
+]
+
+// 是否有联系方式
+export const biddingSearchConcat = [
+  {
+    label: '不限',
+    key: ''
+  },
+  {
+    label: '有联系方式',
+    key: 'y'
+  }
+]
+
+// 附件
+export const biddingSearchFileExists = [
+  {
+    label: '全部',
+    key: '0'
+  },
+  {
+    label: '有附件',
+    key: '1'
+  },
+  {
+    label: '无附件',
+    key: '-1'
+  }
+]
+
+// 查看状态
+export const biddingSearchViewStatus = [
+  {
+    label: '全部',
+    key: ''
+  },
+  {
+    label: '未查看',
+    key: '0'
+  },
+  {
+    label: '已查看',
+    key: '1'
+  }
+]
+
+// 信息来源
+export const biddingSearchInfoSource = [
+  {
+    label: '全部',
+    key: '0'
+  },
+  {
+    label: '企业手动分发',
+    key: '3'
+  },
+  {
+    label: '企业自动分发',
+    key: '2'
+  },
+  {
+    label: '个人订阅',
+    key: '1'
+  }
+]
+
+// 关键词匹配模式
+export const wordsModeList = [
+  {
+    label: '包含所有关键词',
+    key: '0'
+  },
+  {
+    label: '包含任意关键词',
+    key: '1'
+  }
+]
+
+// 热门省份
+export const hotAndAllProvinceList = {
+  // '#': ['全国'],
+  hot: ['全国', '北京', '广东', '山东', '河南', '浙江', '江苏', '陕西', '上海', '四川', '湖北', '福建', '河北', '安徽', '湖南', '辽宁', '江西', '山西'],
+  other: ['云南', '新疆', '重庆', '广西', '吉林', '贵州', '天津', '甘肃', '黑龙江', '内蒙古', '宁夏', '海南', '青海', '西藏', '香港', '澳门', '台湾']
+}

+ 85 - 0
apps/bigmember_pc/src/assets/js/selector/scope.js

@@ -0,0 +1,85 @@
+import { difference } from 'lodash'
+// 筛选项-搜索范围
+export const searchScopeData = [
+  {
+    label: '标题',
+    value: 'title'
+  },
+  {
+    label: '正文',
+    value: 'content'
+  },
+  {
+    label: '附件',
+    value: 'file'
+  },
+  {
+    label: '项目名称/标的物',
+    value: 'ppa'
+  },
+  {
+    label: '采购单位',
+    value: 'buyer'
+  },
+  {
+    label: '中标企业',
+    value: 'winner'
+  },
+  {
+    label: '招标代理机构',
+    value: 'agency'
+  }
+]
+
+// 免费用户展示的key,其他key需要添加power,并用power分组
+const orderFreeKey = ['title', 'content', 'file']
+const orderFreeOldUserKey = ['title', 'content', 'file', 'winner']
+export function calcSearchScope(conf = {}) {
+  if (!conf) {
+    return searchScopeData
+  }
+
+  // 是否是老用户
+  const oldUser = conf.oldUser
+  // 是否是付费用户
+  const isVipUser = conf.vipUser
+
+  if (isVipUser) {
+    return searchScopeData
+  }
+
+  const arr = []
+  if (oldUser) {
+    // 老用户专项
+    const cacheArr = []
+    const allKeyArr = searchScopeData.map((s) => s.value)
+    const vipKeyArr = difference(allKeyArr, orderFreeOldUserKey)
+    // 1. 排序
+    const sortedAllKeys = [...orderFreeOldUserKey, ...vipKeyArr]
+    sortedAllKeys.forEach((s) => {
+      const t = searchScopeData.find((t) => t.value === s)
+      if (t) {
+        cacheArr.push(t)
+      }
+    })
+    // 2. 判断权限
+    cacheArr.forEach((s) => {
+      if (s.value === 'winner') {
+        s.oldUserPower = 1
+      } else if (vipKeyArr.includes(s.value)) {
+        s.power = 1
+      }
+      arr.push(s)
+    })
+  } else {
+    searchScopeData.forEach((s) => {
+      if (orderFreeKey.includes(s.value)) {
+        // do something
+      } else {
+        s.power = 1
+      }
+      arr.push(s)
+    })
+  }
+  return arr
+}

+ 71 - 1
apps/bigmember_pc/src/assets/style/common.scss

@@ -194,4 +194,74 @@ input[type='number'] {
 
 a{
   user-select: text!important;
-}
+}
+
+.use-badge {
+  position: relative;
+  &::after {
+    content: attr(data-badge);
+    position: absolute;
+    top: 0;
+    right: -24px;
+    display: inline-block;
+    font-size: 12px;
+    line-height: 12px;
+    color: #fff;
+    padding: 2px 6px;
+    background-color: #FF3A20;
+    border: 1px solid #fff;
+    border-radius: 12px;
+    border-bottom-left-radius: 0;
+  }
+
+  // 扩展
+  &.el-button::after {
+    top: 0;
+    right: -11px;
+    transform: translate3d(0, -50%, 0);
+  }
+}
+
+/* 删除筛选提示框 */
+.filter-delete-messagebox{
+  width: 420px;
+  border-radius: 8px;
+  padding: 32px;
+  .el-message-box__title{
+    color: #1D1D1D;
+  }
+  .el-message-box__header{
+    padding: 0!important;
+  }
+  .el-message-box__content{
+    padding: 20px 27px 32px;
+  }
+  .el-message-box__message p{
+    font-size: 14px;
+    color: #686868;
+  }
+  .el-message-box__btns{
+    display: flex;
+    flex-direction: row-reverse;
+    justify-content: space-between;
+  }
+  .btn-group.confirm-btn{
+    background: #2cb7ca;
+    margin-right: 52px;
+    border: 0;
+    color: #fff;
+  }
+  .btn-group{
+    width: 132px;
+    height: 36px;
+    padding: 0;
+    border-radius: 6px;
+    font-size: 16px;
+  }
+  .btn-group.confirm-btn:focus{
+    color: #fff;
+  }
+  .btn-group.confirm-btn:hover {
+    color: #fff;
+  }
+}

+ 48 - 4
apps/bigmember_pc/src/assets/style/pic-icon.scss

@@ -3,16 +3,60 @@
   width: 20px;
   height: 20px;
 }
+.wh24 {
+  width: 24px;
+  height: 24px;
+}
 .j-icon-base {
+  background-color: transparent;
   background-repeat: no-repeat;
+  background-position: center;
   background-size: contain;
 }
 
-.wh24 {
-  width: 24px;
-  height: 24px;
-}
+// 选择器前的 checkbox,需要添加 j-icon 基类
+// .checkbox {
+//   border: 1px solid #ddd;
+//   border-radius: 50%;
+//   -webkit-appearance: none;
+//   background: #fff;
+//   &:checked,
+//   &.checked {
+//     border: 0;
+//     background: url(~@/assets/image/icon/checkbox-checked.png) no-repeat center;
+//     background-size: 100% 100%;
+//     &[disabled] {
+//       border: 0;
+//       background: url(~@/assets/image/icon/checkbox-disabled.png) no-repeat center;
+//       background-size: 100% 100%;
+//     }
+//   }
+
+//   &.half {
+//     border: 0;
+//     background: url(~@/assets/image/icon/checkbox-checked-half.png) no-repeat center;
+//     background-size: 100% 100%;
+//     &[disabled] {
+//       background: url(~@/assets/image/icon/checkbox-checked-half-disabled.png) no-repeat center;
+//     }
+//   }
+  
+//   &.transparent {
+//     &:checked,
+//     &.checked {
+//       border: 0;
+//       background: url(~@/assets/image/icon/checkbox-transparent-checked.png) no-repeat center;
+//       background-size: 100% 100%;
+//     }
+//   }
+// }
 
+.icon-vip-mark-img {
+  background-image: url(~@/assets/images/icon/vip.png);
+}
+.icon-help-img {
+  background-image: url(~@/assets/images/icon/help.png);
+}
 .icon-img-close {
   background-image: url(@/assets/images/icon/close-icon2x.png);
 }

+ 134 - 34
apps/bigmember_pc/src/components/article-item/ArticleItem.vue

@@ -4,7 +4,8 @@
     :class="{
       'style-for-gray': config.gray,
       'style-for-table': config.table,
-      'style-for-push': config.push
+      'style-for-push': config.push,
+      'style-for-bidding': config.bidding
     }"
   >
     <div class="article-item-header">
@@ -22,7 +23,7 @@
           <div
             v-html="calcTitle"
             class="a-i-left visited-hd"
-            :class="config.push ? 'ellipsis-3' : 'ellipsis'"
+            :class="(config.push || config.bidding) ? 'ellipsis-3' : 'ellipsis'"
             @click="onClick"
           ></div>
         </div>
@@ -30,14 +31,14 @@
           <span class="el-icon-jy-time" v-if="!config.gray"></span>
           <span class="time-text">
             <slot name="right-time">{{
-              dateFromNow(article.publishtime * 1000)
+              dateFromNow(article.publishtime * 1000) || dateFromNow(article.publishTime * 1000)
             }}</slot>
           </span>
         </div>
       </div>
     </div>
     <div
-      v-if="config.push && article.detail && calcDetail"
+      v-if="config.detail && article.detail && calcDetail"
       class="a-i-detail ellipsis"
       v-html="calcDetail"
     ></div>
@@ -60,19 +61,26 @@
         <span class="tag tag-ent" v-if="buySubject && article.source === 3"
           >企业手动分发</span
         >
-        <span class="tag" v-if="article.area">{{ article.area }}</span>
-        <span class="tag orange" v-if="article.type || article.subtype">{{
-          article.type || article.subtype
-        }}</span>
+        <span class="tag"
+              v-if="article.area"
+              :class="{'tag-handle': tagClickList.includes('area')}"
+              @click.stop="tagClick('area')">
+          {{ article.area }}</span>
+        <span class="tag orange"
+              :class="{'tag-handle': tagClickList.includes('subtype')}"
+              v-if="article.type || article.subtype"
+              @click.stop="tagClick('subtype')">
+          {{article.type || article.subtype }}
+        </span>
         <span
           class="tag green"
-          v-if="article.buyerclass || article.buyerClass"
+          v-if="calcBuyerclass"
           >{{ article.buyerclass || article.buyerClass }}</span
         >
-        <span class="tag dpink" v-if="calcBudget && calcBudget !== '0元'">{{
-          calcBudget
-        }}</span>
-        <span v-if="config.gray && article.ca_fileExists" class="haveFile"
+        <span class="tag dpink" v-if="calcBudget && calcBudget !== '0元'">
+          {{ calcBudget }}
+        </span>
+        <span v-if="config.gray && (article.ca_fileExists || article.fileExists)" class="haveFile"
           >有附件</span
         >
       </div>
@@ -129,6 +137,21 @@
           <span slot="reference" class="view-status">查看状态</span>
         </el-popover>
         <slot name="right-handle-container"></slot>
+        <!-- 参标-->
+        <div
+          v-if="config.joinBid && ('joinBid' in article)"
+          class="join-bid"
+          @click.prevent.stop="joinBidChange"
+        >
+            <i
+              class="j-self-icon"
+              :class="
+                article.joinBid ? 'icon-canbiao-img-active' : 'icon-canbiao-img'
+              "
+            ></i>
+            <span>{{ article.joinBid ? '终止参标' : '参标' }}</span>
+        </div>
+        <!-- 收藏-->
         <div
           class="right-actions"
           v-if="config.collect"
@@ -214,8 +237,9 @@
           <em v-if="article.winnerPerson">,</em>
           {{ article.winnerTel }}
           <em
-            v-if="article.winnerInfo && article.winnerInfo.length > 1"
-            @click="goPortrayal('entDesc', w.winnerId, 'contact')"
+            class="more-tel"
+            v-if="article.winnerInfo && article.winnerInfo.length === 1"
+            @click="goPortrayal('entDesc', article.winnerInfo[0].winnerId, 'contact')"
             >获取更多</em
           >
         </span>
@@ -226,15 +250,15 @@
       </p>
       <p
         class="l-d-item"
-        v-if="article.signendTime || article.bidendTime || article.bidOpenTime"
+        v-if="article.signendTime || article.bidendTime || article.bidEndTime || article.bidOpenTime"
       >
         <span v-if="article.signendTime">
           <i class="l-d-item-label">报名截止日期:</i>
           {{ dateFromNow(article.signendTime * 1000) }}
         </span>
-        <span v-if="article.bidendTime">
+        <span v-if="article.bidendTime || article.bidEndTime">
           <i class="l-d-item-label">投标截止日期:</i>
-          {{ dateFromNow(article.bidendTime * 1000) }}
+          {{ dateFromNow(article.bidendTime * 1000) || dateFromNow(article.bidEndTime * 1000) }}
         </span>
         <span v-if="article.bidOpenTime">
           <i class="l-d-item-label">开标日期:</i>
@@ -287,7 +311,9 @@ export default {
           gray: false,
           table: false,
           collect: false,
-          push: false
+          push: false,
+          detail: false, // 是否展示详情数据
+          bidding: false // 招标采购搜索
         }
       }
     },
@@ -312,6 +338,14 @@ export default {
     model: {
       type: String,
       default: 'S'
+    },
+    matchKeys: {
+      type: Array,
+      default: () => []
+    },
+    tagClickList: {
+      type: Array,
+      default: () => []
     }
   },
   computed: {
@@ -329,18 +363,31 @@ export default {
       return vipPower === 1 || memberPower === 1 || entPower === 1
     },
     calcBudget() {
-      if (this.article.budget) {
-        return moneyUnit(this.article.budget)
-      } else if (this.article.bidAmount) {
-        return moneyUnit(this.article.bidAmount)
+      // 先展示中标金额
+      if (this.article.bidAmount) {
+        if(isNaN(this.article.bidAmount) && this.article.bidAmount.indexOf('登录') > -1) {
+          return this.article.bidAmount
+        } else {
+          return moneyUnit(this.article.bidAmount)
+        }
+      } else if (this.article.budget) {
+        // 无中标金额展示预算金额
+        if(isNaN(this.article.budget) && this.article.budget.indexOf('登录') > -1) {
+          return this.article.budget
+        } else {
+          return moneyUnit(this.article.budget)
+        }
       } else {
         return this.article.budget
       }
     },
+    getMatchKeys() {
+      return this.matchKeys.concat(this.article.matchKeys || [])
+    },
     calcTitle() {
       const hightLightedTitle = replaceKeyword(
         this.article.title,
-        this.article.matchKeys,
+        this.getMatchKeys,
         ['<span class="highlight-text">', '</span>']
       )
       if (this.article.filetext_search) {
@@ -353,10 +400,10 @@ export default {
     calcDetail() {
       const extractDetail = extractKeywords(
         this.article.detail,
-        this.article.matchKeys
+        this.getMatchKeys,
       )
       if (extractDetail) {
-        return replaceKeyword(extractDetail, this.article.matchKeys, [
+        return replaceKeyword(extractDetail, this.getMatchKeys, [
           '<span class="highlight-text">',
           '</span>'
         ])
@@ -367,14 +414,11 @@ export default {
     // 处理关键词在附件中
     calcFileText() {
       const inFile = this.article.filetext_search
-      const keywords = this.article.matchKeys
+      const keywords = this.getMatchKeys
       if (inFile) {
         const keyword = keywords[0]
-        if (keywords.length > 3) {
-          return `(<span class="highlight-text">${keyword.substring(
-            0,
-            3
-          )}</span>...在附件中)`
+        if (keyword?.length > 3) {
+          return `(<span class="highlight-text">${keyword.substring(0, 3)}</span>...在附件中)`
         } else {
           return `(<span class="highlight-text">${keyword}</span>在附件中)`
         }
@@ -386,9 +430,9 @@ export default {
     calcFiletext_search() {
       const extractFiletext = extractKeywords(
         this.article.filetext_search,
-        this.article.matchKeys
+        this.getMatchKeys
       )
-      return replaceKeyword(extractFiletext, this.article.matchKeys, [
+      return replaceKeyword(extractFiletext, this.getMatchKeys, [
         '<span class="highlight-text">',
         '</span>'
       ])
@@ -407,6 +451,11 @@ export default {
       } else {
         return this.model === 'D' && !subtypeFlag
       }
+    },
+    // 处理采购单位类型是否展示
+    calcBuyerclass () {
+      const buyerClass = this.article.buyerclass || this.article.buyerClass
+      return buyerClass && buyerClass !== '其它' && buyerClass.indexOf("登录")<0
     }
   },
   data() {
@@ -451,6 +500,10 @@ export default {
         event: e
       })
     },
+    // 参标
+    joinBidChange () {
+      this.$emit('onJoinBid', this.article)
+    },
     setShow() {
       this.$emit('setShow')
     },
@@ -464,6 +517,12 @@ export default {
       if (!data) return []
       const arr = data.split('、')
       return arr
+    },
+    // 标签点击事件
+    tagClick (label) {
+      if(this.tagClickList?.includes(label)) {
+        this.$emit('tag-click', label)
+      }
     }
   }
 }
@@ -717,6 +776,20 @@ $border-color: #ececec;
       align-items: unset;
     }
   }
+  &.style-for-bidding{
+    .a-i-right {
+      padding-left:0;
+    }
+    .tag-handle {
+      cursor: pointer;
+      &:hover{
+        text-decoration: underline;
+      }
+    }
+    .list-detail{
+      padding-left:0;
+    }
+  }
 }
 .right-actions {
   display: flex;
@@ -757,4 +830,31 @@ $border-color: #ececec;
     cursor: pointer;
   }
 }
+
+.join-bid {
+  display: inline-block;
+  cursor: pointer;
+  font-size: 14px;
+  color: #1d1d1d;
+  margin-right: 10px;
+
+  .j-self-icon {
+    display: inline-block;
+    width: 20px;
+    height: 20px;
+    background-position: center;
+    vertical-align: text-bottom;
+    margin-right: 10px;
+  }
+
+  .icon-canbiao-img {
+    background: url(~@/assets/images/icon/canbiao.png) no-repeat center;
+    background-size: contain;
+  }
+
+  .icon-canbiao-img-active {
+    background: url(~@/assets/images/icon/canbiao-active.png) no-repeat center;
+    background-size: contain;
+  }
+}
 </style>

+ 2 - 1
apps/bigmember_pc/src/components/collect-info/CollectInfo.vue

@@ -565,6 +565,7 @@ export default {
         member_attach:
           '请留下联系方式,我们会尽快联系您!体验附件下载特权,挖掘更多项目情报!',
         member_freeuse: '请留下联系方式,我们会尽快联系您体验大会员全部功能!',
+        jylab_see500_plus: '请留下联系方式,我们会尽快联系您!开通大会员可查看更多招标项目,实时监控更多潜在商机!'
       },
       sourceDescMap: {
         pc_buyer_monitor_more: '采购单位画像页-超级订阅用户申请监控更多业主',
@@ -881,7 +882,7 @@ export default {
           ]
           var isCollect = sourceList.includes(source)
           _this.source = source
-          _this.isForce = res.data.fource
+          _this.isForce = res.data?.fource
           if (result) {
             if (isCollect) {
               callback && callback()

+ 16 - 4
apps/bigmember_pc/src/components/crm-info/crmAction.vue

@@ -30,6 +30,7 @@
 
 <script>
 import iframeDialog from '@/components/crm-info/IframeDialog.vue'
+import { getParam } from '@/utils/'
 import {
   ajaxIgnoreOperate,
   ajaxEmployOperate,
@@ -91,14 +92,17 @@ export default {
       employInfo: [],
       setShowDialog: false,
       iframeSrc: '',
-      lock: false
+      lock: false,
+      property: getParam('property') === 'BIProperty',
+      employId: getParam('employId')
     }
   },
   computed: {
     getList() {
-      if (this.list[0].active === 0) {
+      if (this.list[0].active === 0 && !this.property) {
         return this.list.slice(0, 1)
       } else {
+        let arrList = []
         if (this.entInfo.niche_dis === 1 || this.entInfo.niche_dis === 2) {
           const arr = []
           this.list.forEach((v) => {
@@ -106,10 +110,15 @@ export default {
               arr.push(v)
             }
           })
-          return arr
+          arrList = arr
         } else {
-          return this.list
+          arrList = this.list
         }
+        // 如果是物业版,则不显示收录
+        if (this.property) {
+          arrList = arrList.filter((v) => v.class !== 'employ')
+        }
+        return arrList
       }
     }
   },
@@ -149,6 +158,9 @@ export default {
       if (employInfoItem) {
         employId = employInfoItem.employId
       }
+      if (this.property) {
+        employId = this.employId
+      }
       switch (item.class) {
         case 'employ':
           this.setEmployEvent(item)

+ 312 - 0
apps/bigmember_pc/src/components/filter-items/AmountRangeSelector.vue

@@ -0,0 +1,312 @@
+<template>
+  <Layout
+    ref="layoutRef"
+    :type="type"
+    :placeholder="placeholder"
+    :trigger="trigger"
+    :value="activeLabel"
+    @visible="onVisibleChange"
+  >
+    <div class="filter-list" slot="empty">
+      <div
+        class="filter-item"
+        :class="{'active': item.value === activeValue, 'highlight': item.disabled && isCustom }"
+        v-for="item in options"
+        :key="item.label"
+        :label="item.label"
+        :value="item.value"
+        @click="handleChange(item)"
+      >
+        <el-popover
+          v-if="item.disabled"
+          class="custom-popover amount-range-popover"
+          :append-to-body="false"
+          placement="right-end"
+          :trigger="popoverTrigger"
+          :offset="12"
+          v-model="showPopover"
+          ref="customPricePopover"
+        >
+          <div class="custom-money">
+            <div class="custom-money-item">
+              从<el-input class="price-input" :class="{'focus': price.min}" v-model="price.min" oninput="value=value.replace(/^\D*([0-9]\d*\.?\d{0,2})?.*$/,'$1')" maxlength="9"></el-input>万
+            </div>
+            <div class="custom-money-item">
+              至<el-input class="price-input" :class="{'focus': price.max}" v-model="price.max" oninput="value=value.replace(/^\D*([0-9]\d*\.?\d{0,2})?.*$/,'$1')" maxlength="9"></el-input>万
+            </div>
+            <div class="custom-money-button">
+              <el-button type="primary" @click.stop="onSubmitPrice">确定</el-button>
+            </div>
+          </div>
+          <div slot="reference" class="custom-label">
+            <span>{{ item.label }}</span>
+            <i class="el-icon-arrow-right"></i>
+          </div>
+        </el-popover>
+        <span v-else>{{ item.label }}</span>
+      </div>
+    </div>
+  </Layout>
+</template>
+
+<script>
+import { Popover, Button, Input } from 'element-ui'
+import { amountRangeData } from '@/assets/js/selector.js'
+import Layout from '@/components/filter-items/Layout.vue'
+
+export default {
+  name: 'SelectContainer',
+  components: {
+    [Popover.name]: Popover,
+    [Button.name]: Button,
+    [Input.name]: Input,
+    Layout
+  },
+  props: {
+    type: {
+      type: String,
+      default: 'dropdown'
+    },
+    trigger: {
+      type: String,
+      default: 'hover'
+    },
+    placeholder: {
+      type: String,
+      default: '金额区间'
+    },
+    popoverTrigger: {
+      type: String,
+      default: 'hover'
+    },
+    value: {
+      type: [String, Object],
+      default: null
+    }
+  },
+  model: {
+    prop: 'value',
+    event: 'change'
+  },
+  data () {
+    return {
+      options: amountRangeData,
+      activeValue: this.value,
+      isCustom: false, // 当前是否是自定义选项
+      price: {
+        min: '',
+        max: ''
+      },
+      showPopover: false
+    }
+  },
+  computed: {
+    activeLabel () {
+      const price = this.activeValue
+      if (price) {
+        const priceArr = price.split('-')
+        if (priceArr.length > 1) {
+          const min = priceArr[0]
+          const max = priceArr[1]
+          if (min && max) {
+            return `${min}-${max}万`
+          } else if (!min) {
+            return `${max}万以下`
+          } else if (!max) {
+            return `${min}万以上`
+          } else {
+            return ''
+          }
+        } else {
+          return ''
+        }
+      } else {
+        return ''
+      }
+    }
+  },
+  watch: {
+    value: {
+      immediate: true,
+      handler(val) {
+        this.setState(val)
+      }
+    }
+  },
+  methods: {
+    onVisibleChange (flag) {
+      this.isFocus = flag
+      if (flag) {
+        this.setState(this.activeValue)
+        this.$nextTick(() => {
+          if (this.showPopover) {
+            setTimeout(() => {
+              // popover在下拉框展示时需要重新计算位置,通过先将popover弹框透明度将为0等位置计算完成后再恢复
+              this.$refs.customPricePopover[0].updatePopper()
+              const $popover = this.$root.$el.querySelector('.amount-range-popover > .el-popover')
+              $popover.style.opacity = '1'
+            }, 300)
+          }
+        })
+      }
+    },
+    compareMinMax () {
+      const { min, max } = this.price
+      const hasMinAndMax = String(min).length && String(max).length
+      if (hasMinAndMax && Number(min) > Number(max)) {
+        this.price.max = min
+        this.price.min = max
+      }
+    },
+    onSubmitPrice () {
+      this.compareMinMax()
+      const { min, max } = this.price
+      if (!min && !max) return
+      this.activeValue = `${min}-${max}`
+      this.options.forEach(item => {
+        if (item.label === '自定义') {
+          item.value = `${min}-${max}`
+        }
+      })
+      this.isCustom = true
+      this.$refs.layoutRef.$refs.dropdownRef.hide()
+      this.$refs.customPricePopover[0].doClose()
+      this.$emit('change', this.activeValue)
+    },
+    handleChange (item) {
+      if (item.label !== '自定义') {
+        this.activeValue = item.value
+        this.isCustom = false
+        this.price.min = ''
+        this.price.max = ''
+        this.$refs.layoutRef.$refs.dropdownRef.hide()
+        this.$refs.customPricePopover[0].doClose()
+        this.$emit('change', this.activeValue)
+      } else {
+        this.isCustom = true
+      }
+    },
+    getState () {
+      return {
+        label: this.activeLabel,
+        value: this.activeValue,
+        isCustom: this.isCustom
+      }
+    },
+    setState (data) {
+      this.isCustom = false
+      if (data) {
+        const valueArr = this.options.filter(v => !v.disabled).map(t => t.value)
+        if (valueArr.includes(data)) {
+          this.activeValue = data
+          this.showPopover = false
+        } else {
+          const priceArr = data.split('-')
+          const min = priceArr[0]
+          const max = priceArr[1]
+          this.isCustom = true
+          this.price.min = min
+          this.price.max = max
+          this.activeValue = data
+          this.showPopover = true
+          // this.$nextTick(() => {
+          //   const $popover = this.$root.$el.querySelector('.amount-range-popover > .el-popover')
+          //   $popover.style.opacity = '0'
+          // })
+        }
+      } else {
+        this.price.min = ''
+        this.price.max = ''
+        this.showPopover = false
+        this.activeValue = data
+      }
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.filter-list{
+  min-width: 140px;
+  padding: 8px 0;
+  border: 1px solid $color_main;
+  background: #fff;
+  border-radius: 5px;
+  margin-top: 2px;
+  .filter-item{
+    padding: 4px 16px;
+    font-size: 14px;
+    line-height: 22px;
+    color: #1d1d1d;
+    text-align: left;
+    cursor: pointer;
+    &:hover{
+      background: #ECECEC;
+    }
+    &.active{
+      background: #ECECEC;
+    }
+    &.highlight {
+      color: $color_main;
+    }
+    span{
+      display: inline-block;
+      width: 100%;
+    }
+  }
+  .custom-label{
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    .el-icon-arrow-right{
+      margin-right: -8px;
+    }
+  }
+  .custom-popover{
+    .custom-money{
+      padding: 12px;
+      margin-left: 4px;
+      border: 1px solid $color_main;
+      background: #fff;
+      border-radius: 4px;
+      &-item{
+        display: flex;
+        align-items: center;
+        margin-bottom: 12px;
+      }
+      &-button{
+        display: flex;
+        justify-content: flex-end;
+        .el-button{
+          width: 60px;
+          height: 28px;
+          padding: 0;
+        }
+      }
+    }
+    ::v-deep{
+      .el-popover{
+        margin-left: 16px;
+        border-color: $color_main;
+        padding: 0;
+        border: 0;
+        background: transparent;
+      }
+      .price-input{
+        width: 88px;
+        height: 24px;
+        margin: 0 4px;
+        .el-input__inner{
+          height: 100%;
+          padding: 0 8px;
+        }
+        &.focus{
+          .el-input__inner{
+            border-color: $color_main;
+          }
+        }
+      }
+    }
+  }
+}
+</style>

+ 146 - 0
apps/bigmember_pc/src/components/filter-items/AttachmentSelector.vue

@@ -0,0 +1,146 @@
+<template>
+  <Layout
+    ref="layoutRef"
+    :type="type"
+    :placeholder="placeholder"
+    :trigger="trigger"
+    :value="computedVal"
+  >
+   <div slot="empty" class="filter-list">
+      <div class="filter-item"
+        :class="{'active': selected.value === item.value}"
+        v-for="item in options"
+        :key="item.label"
+        :label="item.label"
+        :value="item.value"
+        @click="handleChange(item)"
+      >
+        {{ item.label }}
+      </div>
+    </div>
+  </Layout>
+</template>
+
+<script>
+import Layout from '@/components/filter-items/Layout.vue'
+import { attachmentData } from '@/assets/js/selector.js'
+export default {
+  name: 'Attachment',
+  props: {
+    type: {
+      type: String,
+      default: 'dropdown'
+    },
+    trigger: {
+      type: String,
+      default: 'hover'
+    },
+    placeholder: {
+      type: String,
+      default: '附件'
+    },
+    value: {
+      type: [String, Number, Object],
+      default: null
+    },
+    beforeChange: Function
+  },
+  components: {
+    Layout
+  },
+  data () {
+    return {
+      options: attachmentData,
+      selected: {
+        label: '',
+        value: this.value
+      }
+    }
+  },
+  computed: {
+    computedVal () {
+      if (this.selected.value && this.selected.value !== '0') {
+        return this.selected.label
+      } else {
+        return ''
+      }
+    }
+  },
+  model: {
+    prop: 'value',
+    event: 'change'
+  },
+  watch: {
+    value: {
+      immediate: true,
+      handler(n) {
+        this.setState(n)
+      }
+    }
+  },
+  methods: {
+    handleChange (item) {
+      const beforeChange = this.beforeChange
+      if (beforeChange) {
+        const pass = beforeChange(item)
+        if (pass) {
+          this.onChange(item)
+        }
+      } else {
+        this.onChange(item)
+      }
+    },
+    onChange (item) {
+      const { layoutRef } = this.$refs
+      try {
+        layoutRef.$refs.dropdownRef.hide()
+      } catch (error) {}
+      this.$emit('change', item.value)
+      this.selected.value = item.value
+      this.selected.label = item.label
+    },
+    getState () {
+      return this.selected.value
+    },
+    setState (value = this.selected.value) {
+      this.selected.value = value
+      this.options.forEach(option => {
+        if (option.value === value) {
+          this.selected.label = option.label
+        }
+      })
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.filter-list{
+  min-width: 140px;
+  padding: 8px 0;
+  border: 1px solid $color_main;
+  background: #fff;
+  border-radius: 5px;
+  margin-top: 2px;
+  .filter-item{
+    padding: 4px 16px;
+    font-size: 14px;
+    line-height: 22px;
+    color: #1d1d1d;
+    text-align: left;
+    cursor: pointer;
+    white-space: nowrap;
+    &:hover{
+      background: #ECECEC;
+    }
+    &.active{
+      // color: $color_main;
+      background: #ECECEC;
+    }
+    span{
+      display: inline-block;
+      width: 100%;
+    }
+  }
+}
+</style>

+ 89 - 0
apps/bigmember_pc/src/components/filter-items/BasePowerLayout.vue

@@ -0,0 +1,89 @@
+<template>
+  <section class="base-power-layout">
+    <div class="base-power-module default-module">
+      <slot name="default"></slot>
+      <div v-if="baseMaskShow" class="base-mask" @click="clickBaseMask"></div>
+    </div>
+    <div class="base-power-module vip-module" v-if="vipModuleShow">
+      <slot name="vip"></slot>
+      <div
+        v-if="vipMaskShow"
+        class="vip-mask pointer"
+        @click="clickVipMask"
+      ></div>
+    </div>
+  </section>
+</template>
+
+<script>
+export default {
+  name: 'BasePowerLayout',
+  props: {
+    baseMaskShow: {
+      type: Boolean,
+      default: false
+    },
+    vipMaskShow: {
+      type: Boolean,
+      default: false
+    },
+    vipModuleShow: {
+      type: Boolean,
+      default: false
+    }
+  },
+  methods: {
+    clickBaseMask() {
+      this.$emit('clickBaseMask')
+    },
+    clickVipMask() {
+      this.$emit('clickVipMask')
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+$gold: #c98f37;
+
+.base-power-layout {
+  display: flex;
+  align-items: center;
+  flex-wrap: wrap;
+}
+.base-power-module {
+  position: relative;
+  display: flex;
+  align-items: center;
+  border-radius: 4px;
+}
+.vip-module {
+  border: 1px dashed $gold;
+  padding: 1px 12px 1px 8px;
+  height: 32px;
+  background: linear-gradient(90deg, #fff7dC 0%, rgba(255, 247, 220, 0) 100%);
+  &::after {
+    content: '';
+    position: absolute;
+    top: 50%;
+    right: -48px;
+    transform: translate(0, -50%);
+    display: inline-block;
+    width: 38px;
+    height: 20px;
+    background: url(~@/assets/images/icon/vip.png) no-repeat center;
+    background-size: contain;
+  }
+}
+.base-mask,
+.vip-mask {
+  position: absolute;
+  left: 0;
+  right: 0;
+  top: 0;
+  bottom: 0;
+  z-index: 2;
+  height: 32px;
+  cursor: pointer;
+}
+</style>

+ 71 - 0
apps/bigmember_pc/src/components/filter-items/BuyerTypeSelector.vue

@@ -0,0 +1,71 @@
+<template>
+  <Layout
+    :type="type"
+    :placeholder="placeholder"
+    :trigger="trigger"
+    :value="computedVal"
+  >
+   <CascadeContent
+      ref="buyerContent"
+      slot="empty"
+      :options="options"
+      :value="value"
+      @change="onChange"
+    >
+    </CascadeContent>
+  </Layout>
+</template>
+
+<script>
+
+import Layout from '@/components/filter-items/Layout.vue'
+import CascadeContent from '@/components/filter-items/CascadeContent.vue'
+import { buyerclassListMapExp } from '@/assets/js/selector.js'
+export default {
+  name: 'IndustrySelector',
+  props: {
+    type: {
+      type: String,
+      default: 'dropdown'
+    },
+    trigger: {
+      type: String,
+      default: 'hover'
+    },
+    placeholder: {
+      type: String,
+      default: '采购单位类型'
+    },
+    value: {
+      type: Object,
+      default: () => {}
+    }
+  },
+  components: {
+    Layout,
+    CascadeContent
+  },
+  data () {
+    return {
+      options: buyerclassListMapExp,
+      selectedVal: []
+    }
+  },
+  watch: {
+    value (val) {
+      this.selectedVal = this.$refs.buyerContent.levelMapToArray(val)
+      this.$refs.buyerContent.setState(val)
+    }
+  },
+  computed: {
+    computedVal () {
+      return this.selectedVal.length ? `${this.placeholder}${this.selectedVal.length}个` : ''
+    }
+  },
+  methods: {
+    onChange (value) {
+      this.$emit('change', value)
+    }
+  }
+}
+</script>

+ 416 - 0
apps/bigmember_pc/src/components/filter-items/CascadeContent.vue

@@ -0,0 +1,416 @@
+<template>
+   <div class="cascade-content">
+    <div class="cascade-content-module">
+      <header class="module-header">一级分类</header>
+      <div class="module-main">
+        <ul>
+          <li
+            class="module-item"
+            @mouseover="onFistMouseOver(first, fIndex)"
+            @mouseout="onFirstMouseOut($event)"
+            :class="{'active': fActive === fIndex}"
+            v-for="(first, fIndex) in firstList"
+            :key="first.name"
+          >
+            <el-checkbox
+              v-model="first.checked"
+              :indeterminate="first.indeterminate"
+              @change="onFirstChange($event, first, fIndex)"
+            >
+            </el-checkbox>
+            <span class="item-name" @click.self="onOpenSecond(first, fIndex)">
+              {{ first.name }}
+              <slot name="expend-first" :first="first"></slot>
+            </span>
+            <i class="el-icon-arrow-right"></i>
+          </li>
+        </ul>
+      </div>
+    </div>
+    <div class="cascade-content-module">
+      <header class="module-header"><span>二级分类</span></header>
+      <div class="module-main">
+        <ul>
+          <li
+            class="module-item"
+            :class="{'active': sActive === sIndex}"
+            v-for="(second, sIndex) in secondList"
+            :key="second.name"
+          >
+            <el-checkbox
+              v-model="second.checked"
+              :disabled="second.disabled"
+              :indeterminate="second.indeterminate"
+              @change="onSecondChange($event, second, sIndex)"
+            >
+            </el-checkbox>
+            <span class="item-name">{{ second.name }}</span>
+          </li>
+        </ul>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { Checkbox } from 'element-ui'
+export default {
+  name: 'CascadeContent',
+  components: {
+    [Checkbox.name]: Checkbox
+  },
+  props: {
+    options: {
+      type: Object,
+      default: () => {}
+    },
+    value: {
+      type: Object,
+      default: () => {
+        return null
+      }
+    },
+
+  },
+  data () {
+    return {
+      firstList: [],
+      secondList: [],
+      fActive: -1,
+      sActive: -1,
+      fTimer: null,
+      sTimer: null
+    }
+  },
+  model: {
+    prop: 'value',
+    event: 'change'
+  },
+  watch: {
+    value (val) {
+      this.setState(val)
+    }
+  },
+  mounted () {
+    this.initData()
+  },
+  methods: {
+    getMapToArray () {
+      const options = this.options
+      const toArray = []
+      for (const key in options) {
+        toArray.push({
+          name: key,
+          checked: false,
+          indeterminate: false,
+          disabled: false,
+          children: options[key].map(v => {
+            return {
+              name: v,
+              checked: false,
+              indeterminate: false,
+              disabled: false
+            }
+          })
+        })
+      }
+      return toArray
+    },
+    initData () {
+      const sourceList = this.getMapToArray()
+      sourceList.unshift({
+        name: '全部',
+        checked: false,
+        disabled: false,
+        indeterminate: false,
+        all: true,
+        children: []
+      })
+      sourceList.forEach((first) => {
+        first.children.unshift({
+          name: '全部',
+          checked: false,
+          disabled: first.children.length === 0,
+          indeterminate: first.indeterminate,
+          all: true
+        })
+      })
+      this.firstList = sourceList
+      this.secondList = sourceList[0].children
+    },
+    onFistMouseOver (first, fIndex) {
+      if (!this.fTimer) {
+        this.fTimer = setTimeout(() => {
+          this.onOpenSecond(first, fIndex)
+        }, 150)
+      }
+    },
+    onFirstMouseOut () {
+      clearTimeout(this.fTimer)
+      this.fTimer = null
+    },
+    onOpenSecond (first, fIndex) {
+      this.fActive = fIndex
+      this.secondList = first.children
+    },
+    onFirstChange (checked, first, fIndex) {
+      this.onOpenSecond(first, fIndex)
+      first.checked = checked
+      this.setCommonChange(checked, first)
+      if (first.all) {
+        this.$emit('change', first.checked ? this.getState() : null)
+      } else {
+        this.$emit('change', this.getState())
+      }
+    },
+    onSecondChange (checked, second) {
+      second.checked = checked
+      if (second.all) {
+        this.secondList.forEach(item => {
+          item.checked = checked
+          item.indeterminate = false
+        })
+      } else {
+        second.indeterminate = false
+        second.checked = checked
+      }
+      const currentFirst = this.firstList[this.fActive]
+      const allSecondList = currentFirst.children.filter(item => !item.all)
+      const selectedSecondList = currentFirst.children.filter(item => !item.all && item.checked)
+      if (allSecondList.length === selectedSecondList.length) {
+        this.secondList[0].checked = true
+        this.secondList[0].indeterminate = false
+        currentFirst.checked = true
+        currentFirst.indeterminate = false
+      } else {
+        this.secondList[0].checked = false
+        this.secondList[0].indeterminate = selectedSecondList.length > 0
+        currentFirst.indeterminate = selectedSecondList.length > 0
+        currentFirst.checked = false
+      }
+      this.checkFirstAllStatus()
+      this.$emit('change', this.getState())
+    },
+    setCommonChange (checked, item) {
+      if (item.all) {
+        this.firstList.forEach(first => {
+          first.checked = checked
+          first.indeterminate = false
+          first.children.forEach((second) => {
+            second.checked = checked
+            second.indeterminate = false
+          })
+        })
+      } else {
+        item.indeterminate = false
+        item.children.forEach((second) => {
+          second.checked = checked
+          second.indeterminate = false
+        })
+      }
+      this.checkFirstAllStatus()
+    },
+    checkFirstAllStatus () {
+      const allFistList = this.firstList.filter(item => !item.all)
+      const selectedFirstList = this.firstList.filter(item => !item.all && item.checked)
+      const allHalfSelected = this.firstList.filter(item => !item.all && item.indeterminate)
+      if (allFistList.length === selectedFirstList.length) {
+        this.firstList[0].checked = true
+        this.firstList[0].indeterminate = false
+        this.firstList[0].children[0].checked = true
+        this.firstList[0].children[0].indeterminate = false
+      } else {
+        this.firstList[0].checked = false
+        this.firstList[0].indeterminate = selectedFirstList.length > 0 || allHalfSelected.length > 0
+        this.firstList[0].children[0].checked = false
+        this.firstList[0].children[0].indeterminate = selectedFirstList.length > 0 || allHalfSelected.length > 0
+      }
+    },
+    checkSecondAllStatus () {
+      const list = this.firstList
+      list.forEach(item => {
+        item.children[0].checked = item.checked
+        item.children[0].indeterminate = item.indeterminate
+      })
+    },
+    objectToArray (data = {}) {
+      const level1AndLevel2 = []
+      if (Object.keys(data).length) {
+        for (const key in data) {
+          data[key].forEach(v => {
+            level1AndLevel2.push(`${key}_${v}`)
+          })
+        }
+      }
+      return level1AndLevel2
+    },
+    restState () {
+      this.firstList.forEach(item => {
+        item.checked = false
+        item.indeterminate = false
+        item.children.forEach(second => {
+          second.checked = false
+          second.indeterminate = false
+        })
+      })
+    },
+    // {{一级分类: [二级分类]} to 一级分类_二级分类
+    levelMapToArray (levelMap) {
+      if (!levelMap) return []
+      const levelArray = []
+      this.firstList.forEach(item => {
+        for (const key in levelMap) {
+          if (key === item.name) {
+            if (levelMap[key].length === 0) {
+              item.children.filter(n => !n.all).forEach(v => {
+                levelArray.push(`${key}_${v}`)
+              })
+            } else {
+              levelMap[key].forEach(v => {
+                levelArray.push(`${key}_${v}`)
+              })
+            }
+          }
+        }
+      })
+
+      return levelArray
+    },
+    // 一级分类_二级分类 to {一级分类: [二级分类]}
+    levelArrayToMap (levelArray) {
+      if (!levelArray) return
+      let levelMap = {}
+      levelArray.forEach(item => {
+        const [category, subCategory] = item.split('_')
+        if (!levelMap[category]) {
+          levelMap[category] = []
+        }
+        levelMap[category].push(subCategory)
+      })
+      return levelMap
+    },
+    getState () {
+      const list = JSON.parse(JSON.stringify(this.firstList))
+      const allChecked = list.some(v => v.all && v.checked)
+      const hasSelectedList = list.filter(v => !v.all && (v.checked || v.indeterminate))
+      let levelMap = {}
+      if (allChecked) {
+        levelMap = {}
+      } else {
+        if (hasSelectedList.length) {
+          hasSelectedList.forEach(item => {
+            if (item.checked || item.indeterminate) {
+              levelMap[item.name] = item.children.filter(v => !v.all && v.checked).map(v => v.name)
+            }
+          })
+        } else {
+          levelMap = null
+        }
+      }
+      return levelMap // {一级分类: [二级分类]}
+    },
+    setState (value) {
+      this.restState()
+      if (!value) {
+        this.firstList.forEach(item => {
+          item.checked = false
+          item.indeterminate = false
+          item.children.forEach(second => {
+            second.checked = false
+            second.indeterminate = false
+          })
+        })
+        return
+      }
+      if (Object.keys(value).length === 0) {
+        this.firstList.forEach(item => {
+          item.checked = true
+          item.indeterminate = false
+          item.children.forEach(second => {
+            second.checked = true
+            second.indeterminate = false
+          })
+        })
+      } else {
+        this.firstList.forEach(item => {
+          item.children.forEach(second => {
+            for (const key in value) {
+              if (key === item.name) {
+                if (value[key].length === 0) {
+                  second.checked = true
+                } else {
+                  second.checked = value[key].includes(second.name)
+                }
+                second.indeterminate = false
+              }
+            }
+          })
+          const allChild = item.children.filter(child => !child.all)
+          const checkedChild = item.children.filter(child => !child.all && child.checked)
+          item.checked = allChild.length === checkedChild.length
+          item.indeterminate = checkedChild.length > 0 && allChild.length !== checkedChild.length
+        })
+        this.checkFirstAllStatus()
+        this.checkSecondAllStatus()
+      }
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.cascade-content{
+  display: flex;
+  align-items: center;
+  min-width: 360px;
+  height: 360px;
+  margin-top: 2px;
+  border-radius: 5px;
+  background: #fff;
+  overflow: hidden;
+  border: 1px solid $color_main;
+  &-module{
+    flex: 1;
+    height: 100%;
+    display: flex;
+    flex-direction: column;
+    &:not(:last-child){
+      border-right: 1px solid #ececec;
+    }
+    .module-header{
+      padding: 12px 11px;
+      color:#999999;
+      font-size: 14px;
+      line-height: 22px;
+    }
+    .module-main{
+      flex: 1;
+      overflow-y: scroll;
+    }
+    .module-item{
+      position: relative;
+      display: flex;
+      align-items: center;
+      padding: 0 8px;
+      height: 30px;
+      font-size: 14px;
+      line-height: 22px;
+      color: #1d1d1d;
+      &.active,
+      &:hover{
+        background: #ececec;
+        cursor: pointer;
+      }
+      .item-name{
+        flex: 1;
+        margin-left: 4px;
+        display: flex;
+        align-items: center;
+      }
+    }
+    .module-main::-webkit-scrollbar{
+      width: 4px;
+    }
+  }
+}
+</style>

+ 177 - 0
apps/bigmember_pc/src/components/filter-items/ChangeHandsDropdown.vue

@@ -0,0 +1,177 @@
+<template>
+  <div class="change-hands-dropdown">
+    <Layout
+      ref="layoutRef"
+      :type="type"
+      :placeholder="placeholder"
+      :trigger="trigger"
+      :value="computedVal"
+    >
+      <div slot="empty" class="filter-list">
+        <div class="filter-item"
+             :class="{'active': selected.value === item.value}"
+             v-for="item in options"
+             :key="item.label"
+             :label="item.label"
+             :value="item.value"
+             @click="handleChange(item)"
+        >
+          {{ item.label }}
+        </div>
+      </div>
+    </Layout>
+    <el-tooltip  effect="dark"  placement="top">
+      <div slot="content" style="width:300px; font-size: 14px;line-height: 24px;">
+        <span>"换手率说明:采购单位的历史物业项目中标单位更换频次统计。</span>
+      </div>
+      <i class="icon-help-img" ></i>
+    </el-tooltip>
+  </div>
+</template>
+
+<script>
+import Layout from '@/components/filter-items/Layout.vue'
+export default {
+    name: 'ChangeHands',
+  props: {
+    type: {
+      type: String,
+      default: 'dropdown'
+    },
+    trigger: {
+      type: String,
+      default: 'hover'
+    },
+    placeholder: {
+      type: String,
+      default: '换手率'
+    },
+    value: {
+      type: [String, Number],
+      default: null
+    },
+    beforeChange: Function
+  },
+  components: {
+    Layout
+  },
+  data () {
+    return {
+      options: [
+        {
+          label: "不限",
+          value: 0
+        },
+        {
+          label: "到期换手率高",
+          value: 1
+        }
+      ],
+      selected: {
+        label: '',
+        value: this.value
+      }
+    }
+  },
+  computed: {
+    computedVal () {
+      if (this.selected.value) {
+        return this.selected.label
+      } else {
+        return ''
+      }
+    }
+  },
+  model: {
+    prop: 'value',
+    event: 'change'
+  },
+  watch: {
+    value: {
+      immediate: true,
+      handler(n) {
+        this.setState(n)
+      }
+    }
+  },
+  methods: {
+    handleChange (item) {
+      const beforeChange = this.beforeChange
+      if (beforeChange) {
+        const pass = beforeChange(item)
+        if (pass) {
+          this.onChange(item)
+        }
+      } else {
+        this.onChange(item)
+      }
+    },
+    onChange (item) {
+      const { layoutRef } = this.$refs
+      try {
+        layoutRef.$refs.dropdownRef.hide()
+      } catch (error) {}
+      this.$emit('change', item.value)
+      this.selected.value = item.value
+      this.selected.label = item.label
+    },
+    getState () {
+      return this.selected.value
+    },
+    setState (value = this.selected.value) {
+      this.selected.value = value
+      this.options.forEach(option => {
+        if (option.value === value) {
+          this.selected.label = option.label
+        }
+      })
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.change-hands-dropdown{
+  min-width:96px;
+  display: flex;
+  height: 24px;
+}
+.filter-list{
+  min-width: 140px;
+  padding: 8px 0;
+  border: 1px solid $color_main;
+  background: #fff;
+  border-radius: 5px;
+  margin-top: 2px;
+  .filter-item{
+    padding: 4px 16px;
+    font-size: 14px;
+    line-height: 22px;
+    color: #1d1d1d;
+    text-align: left;
+    cursor: pointer;
+    white-space: nowrap;
+    &:hover{
+      background: #ECECEC;
+    }
+    &.active{
+      // color: $color_main;
+      background: #ECECEC;
+    }
+    span{
+      display: inline-block;
+      width: 100%;
+    }
+  }
+}
+.icon-help-img {
+  display: inline-block;
+  width: 16px;
+  height: 16px;
+  margin-top:6px;
+  margin-left: 6px;
+  background: url("~@/assets/images/icon/help.png") no-repeat center;
+  background-size: contain;
+  cursor: pointer;
+}
+</style>

+ 119 - 0
apps/bigmember_pc/src/components/filter-items/CheckboxGroupSelector.vue

@@ -0,0 +1,119 @@
+<template>
+  <div class="checkbox-group-selector">
+    <div class="s-container">
+      <div class="checkbox-item"
+        v-for="(item, index) in options"
+        :key="index"
+        @click="onClick(item)"
+      >
+        <span class="j-checkbox checkbox-item-icon" :class="{checked: value.includes(item.value), gold: item.power}"></span>
+        <span class="checkbox-item-label">{{ item.label }}</span>
+        <slot name="tips" :prop="item"></slot>
+      </div>
+      <slot name="endOther"></slot>
+    </div>
+  </div>
+</template>
+
+<script>
+/**
+ * @description: 多选框组选择器 已适用场景:关键词匹配方式选择器
+ * @param {Array} options 多选框组数据源
+ * @param {Function} beforeChange change 前置钩子函数,用于自定义校验
+ * @param {Array} value 当前选中的值
+ * @param {Boolean} keepOne 是否保留一个选项
+ * beforeChange 父组件使用样例 <child :beforeChange="onBeforeChange"></child>
+ * onBeforeChange(data) {
+ *    const vip = false
+ *    if (!vip && data.auth) {
+ *      this.$toast('需是付费用户')
+ *      return false
+ *    } else {
+ *      return true
+ *    }
+ *  }
+*/
+export default {
+  name: 'checkbox-group-selector-component',
+  props: {
+    options: {
+      type: Array,
+      default: () => []
+    },
+    value: {
+      type: Array,
+      default: () => []
+    },
+    keepOne: {
+      type: Boolean,
+      default: false
+    },
+    beforeChange: Function
+  },
+  model: {
+    prop: 'value',
+    event: 'change'
+  },
+  methods: {
+    onClick (item) {
+      const beforeChange = this.beforeChange
+      if (beforeChange) {
+        const pass = beforeChange(item)
+        if (pass) {
+          this.onChange(item)
+        }
+      } else {
+        this.onChange(item)
+      }
+    },
+    onChange (next) {
+      if (this.value.includes(next.value)) {
+        if (this.keepOne && this.value.length === 1) {
+          this.$toast('至少选择一个')
+        } else {
+          this.value.splice(this.value.indexOf(next.value), 1)
+        }
+      } else {
+        this.value.push(next.value)
+      }
+      this.$emit('change', this.value)
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.checkbox-group-selector{
+  display: flex;
+  align-items: center;
+  .s-header{
+    min-width: 120px;
+    margin-right: 8px;
+    font-size: 14px;
+    line-height: 40px;
+    text-align: right;
+  }
+  .s-container{
+    flex: 1;
+    display: flex;
+    align-items: center;
+    min-width: 352px;
+  }
+  .checkbox-item{
+    display: flex;
+    align-items: center;
+    margin-right: 10px;
+    color: #1d1d1d;
+    cursor: pointer;
+    &-icon{
+      width: 16px;
+      height: 16px;
+      margin-right: 10px;
+    }
+    &-label {
+      font-size: 14px;
+      line-height: 22px;
+    }
+  }
+}
+</style>

+ 134 - 0
apps/bigmember_pc/src/components/filter-items/CommonCheckboxSelector.vue

@@ -0,0 +1,134 @@
+<template>
+  <div class="common-checkbox-selector">
+    <el-checkbox-group
+      class="j-checkbox-group"
+      :value="list"
+      @input="onIssueStateChange"
+    >
+      <el-checkbox-button class="j-checkbox-button" label="全部"
+        >全部</el-checkbox-button
+      >
+      <el-checkbox-button
+        class="j-checkbox-button"
+        v-for="(state, index) in options.value"
+        :key="index"
+        :label="setSelectValue(state)"
+      >
+        {{ field ? state.label : state }}
+      </el-checkbox-button>
+    </el-checkbox-group>
+  </div>
+</template>
+
+<script setup>
+import { onMounted, ref } from 'vue'
+const props = defineProps({
+  // 是否单选
+  single: {
+    type: Boolean,
+    default: false
+  },
+  // field: 空代表字符串数组
+  // 其他: options是对象数组,field是对象数组的某个字段
+  field: {
+    type: String,
+    default: ''
+  },
+  options: {
+    type: Object,
+    default: () => {}
+  },
+  value: {
+    type: Array,
+    default: () => []
+  }
+})
+let list = ref([])
+onMounted(() => {
+  if (!props.value.length) {
+    list.value = ['全部']
+  } else {
+    list.value = props.value
+  }
+})
+function setSelectValue(state) {
+  return props.field === '' ? state : state[props.field];
+}
+const emit = defineEmits(['input'])
+function onIssueStateChange(value) {
+  const isAllBtn = value.filter((item, i) => item === '全部' && i !== 0)
+  if (isAllBtn.length > 0) {
+    value = ['全部']
+  } else {
+    // 如果是单选,则只取最新一个
+    if (props.single) {
+      const nonAllValues = value.filter((item) => item !== '全部')
+      value = nonAllValues.length
+        ? [nonAllValues[nonAllValues.length - 1]]
+        : ['全部']
+    } else {
+      value = value.filter((item) => item !== '全部')
+      if (value.length === 0) {
+        value = ['全部']
+      }
+    }
+  }
+  list.value = value
+  if (value.length === 1 && value[0] === '全部') {
+    emit('input', [])
+  } else {
+    emit('input', value)
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.common-checkbox-selector {
+  // checked-button
+  .j-checkbox-button {
+    margin: 0 10px 10px 0;
+    ::v-deep{
+      &.is-checked {
+        .el-checkbox-button__inner{
+          background: #2abed1;
+          color: #fff
+        }
+      }
+      .el-checkbox-button__inner {
+        line-height: 22px;
+        border: none;
+        border-color: transparent;
+        border-radius: 4px;
+        padding: 2px 6px;
+        color:#1d1d1d;
+        &:hover{
+          background: #2abed1;
+          color: #fff
+        }
+      }
+    }
+
+    &.is-active {
+      .el-checkbox-button__inner {
+        border-color: #2abed1;
+      }
+    }
+
+
+  }
+  .j-checkbox-button:first-child,
+  .j-checkbox-button:last-child {
+    ::v-deep{
+      .el-checkbox-button__inner {
+        border-color: transparent;
+        border-radius: 4px;
+        padding: 2px 6px;
+      }
+    }
+  }
+  .el-checkbox-button.is-focus .el-checkbox-button__inner {
+    border: none;
+    border-color: transparent;
+  }
+}
+</style>

+ 148 - 0
apps/bigmember_pc/src/components/filter-items/CommonSingleChoice.vue

@@ -0,0 +1,148 @@
+<template>
+  <Layout
+    ref="layoutRef"
+    :type="type"
+    :placeholder="placeholder"
+    :trigger="trigger"
+    :value="computedVal"
+  >
+    <div slot="empty" class="filter-list">
+      <div
+        class="filter-item"
+        :class="{ active: selected.value === item.value }"
+        v-for="item in options"
+        :key="item.label"
+        :label="item.label"
+        :value="item.value"
+        @click="handleChange(item)"
+      >
+        {{ item.label }}
+      </div>
+    </div>
+  </Layout>
+</template>
+
+<script>
+import Layout from '@/components/filter-items/Layout.vue'
+export default {
+  name: 'CommonSingleChoice',
+  props: {
+    type: {
+      type: String,
+      default: 'dropdown'
+    },
+    trigger: {
+      type: String,
+      default: 'hover'
+    },
+    placeholder: {
+      type: String,
+      default: '标签'
+    },
+    value: {
+      type: [String, Number, Object],
+      default: null
+    },
+    beforeChange: Function,
+    options: {
+      type: Array,
+      default: () => []
+    }
+  },
+  components: {
+    Layout
+  },
+  data() {
+    return {
+      selected: {
+        label: '',
+        value: this.value
+      }
+    }
+  },
+  computed: {
+    computedVal() {
+      if (this.selected.value) {
+        return this.selected.label
+      } else {
+        return this.selected.value === '' ? '全部' : ''
+      }
+    }
+  },
+  model: {
+    prop: 'value',
+    event: 'change'
+  },
+  watch: {
+    value: {
+      immediate: true,
+      handler(n) {
+        this.setState(n)
+      }
+    }
+  },
+  methods: {
+    handleChange(item) {
+      const beforeChange = this.beforeChange
+      if (beforeChange) {
+        const pass = beforeChange(item)
+        if (pass) {
+          this.onChange(item)
+        }
+      } else {
+        this.onChange(item)
+      }
+    },
+    onChange(item) {
+      const { layoutRef } = this.$refs
+      try {
+        layoutRef.$refs.dropdownRef.hide()
+      } catch (error) {}
+      this.$emit('change', item.value)
+      this.selected.value = item.value
+      this.selected.label = item.label
+    },
+    getState() {
+      return this.selected.value
+    },
+    setState(value = this.selected.value) {
+      this.selected.value = value
+      this.options.forEach((option) => {
+        if (option.value === value) {
+          this.selected.label = option.label
+        }
+      })
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.filter-list {
+  min-width: 140px;
+  padding: 8px 0;
+  border: 1px solid $color_main;
+  background: #fff;
+  border-radius: 5px;
+  margin-top: 2px;
+  .filter-item {
+    padding: 4px 16px;
+    font-size: 14px;
+    line-height: 22px;
+    color: #1d1d1d;
+    text-align: left;
+    cursor: pointer;
+    white-space: nowrap;
+    &:hover {
+      background: #ececec;
+    }
+    &.active {
+      color: $color_main;
+    }
+    span {
+      display: inline-block;
+      width: 100%;
+    }
+  }
+}
+</style>

+ 152 - 0
apps/bigmember_pc/src/components/filter-items/ContactSelector.vue

@@ -0,0 +1,152 @@
+<template>
+  <Layout
+    ref="layoutRef"
+    :type="type"
+    :placeholder="calcPlaceholder"
+    :value="computedVal"
+    :trigger="trigger"
+  >
+    <div class="filter-list" slot="empty">
+      <div class="filter-item"
+        :class="{'active': value === item.value}"
+        v-for="item in calcOptions"
+        :key="item.label"
+        :label="item.label"
+        :value="item.value"
+        @click="handleChange(item)"
+      >
+        {{ item.label }}
+      </div>
+    </div>
+  </Layout>
+</template>
+
+<script>
+import Layout from '@/components/filter-items/Layout.vue'
+import { winnerContactData, buyerContactData } from '@/assets/js/selector.js'
+export default {
+  name: 'ContactSelector',
+  props: {
+    type: {
+      type: String,
+      default: 'dropdown'
+    },
+    trigger: {
+      type: String,
+      default: 'hover'
+    },
+    placeholder: {
+      type: String,
+      default: ''
+    },
+    source: {
+      type: String,
+      default: 'winner'
+    },
+    options: {
+      type: Array,
+      default: () => []
+    },
+    value: {
+      type: [String, Object],
+      default: null
+    },
+    beforeChange: Function
+  },
+  components: {
+    Layout
+  },
+  model: {
+    prop: 'value',
+    event: 'change'
+  },
+  watch: {
+    value: {
+      immediate: true,
+      handler(val) {
+        this.setState(val)
+      }
+    }
+  },
+  computed: {
+    computedVal () {
+      if (this.value) {
+        const item = this.calcOptions.find(v => v.value === this.value)
+        return item.label
+      } else {
+        return ''
+      }
+    },
+    calcOptions () {
+      if (this.source === 'winner') {
+        return winnerContactData
+      } else {
+        return buyerContactData
+      }
+    },
+    calcPlaceholder () {
+      if (this.source === 'winner') {
+        return '中标企业联系方式'
+      } else {
+        return '采购单位联系方式'
+      }
+    }
+  },
+  methods: {
+    handleChange (item) {
+      const beforeChange = this.beforeChange
+      if (beforeChange) {
+        const pass = beforeChange(item)
+        if (pass) {
+          this.onChange(item)
+        }
+      } else {
+        this.onChange(item)
+      }
+    },
+    onChange (item) {
+      const { layoutRef } = this.$refs
+      this.$emit('change', item.value)
+      try {
+        layoutRef.$refs.dropdownRef.hide()
+      } catch (error) {}
+    },
+    getState () {
+      return this.value
+    },
+    setState (value) {
+      this.value = value
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.filter-list{
+  min-width: 140px;
+  padding: 8px 0;
+  border: 1px solid $color_main;
+  background: #fff;
+  border-radius: 5px;
+  margin-top: 2px;
+  .filter-item{
+    padding: 4px 16px;
+    font-size: 14px;
+    line-height: 22px;
+    color: #1d1d1d;
+    text-align: left;
+    cursor: pointer;
+    white-space: nowrap;
+    &:hover{
+      background: #ECECEC;
+    }
+    &.active{
+      color: $color_main;
+    }
+    span{
+      display: inline-block;
+      width: 100%;
+    }
+  }
+}
+</style>

+ 311 - 0
apps/bigmember_pc/src/components/filter-items/EntamountRangeData.vue

@@ -0,0 +1,311 @@
+<template>
+  <Layout ref="layoutRef" :type="type" :placeholder="placeholder" :trigger="trigger" :value="activeLabel"
+    @visible="onVisibleChange">
+    <div class="filter-list" slot="empty">
+      <div class="filter-item" :class="{ 'active': item.value === activeValue, 'highlight': item.disabled && isCustom }"
+        v-for="item in options" :key="item.label" :label="item.label" :value="item.value" @click="handleChange(item)">
+        <el-popover v-if="item.disabled" class="custom-popover" :append-to-body="false" placement="right-end"
+          :trigger="popoverTrigger" :offset="12" v-model="showPopover" ref="customPricePopover">
+          <div class="custom-money">
+            <div class="custom-money-item">
+              从<el-input class="price-input" :class="{ 'focus': price.min }" v-model="price.min"
+                oninput="value=value.replace(/^\D*([0-9]\d*\.?\d{0,2})?.*$/,'$1')" maxlength="9"></el-input>万
+            </div>
+            <div class="custom-money-item">
+              至<el-input class="price-input" :class="{ 'focus': price.max }" v-model="price.max"
+                oninput="value=value.replace(/^\D*([0-9]\d*\.?\d{0,2})?.*$/,'$1')" maxlength="9"></el-input>万
+            </div>
+            <div class="custom-money-button">
+              <el-button type="primary" @click.stop="onSubmitPrice">确定</el-button>
+            </div>
+          </div>
+          <div slot="reference" class="custom-label">
+            <span>{{ item.label }}</span>
+            <i class="el-icon-arrow-right"></i>
+          </div>
+        </el-popover>
+        <span v-else>{{ item.label }}</span>
+      </div>
+    </div>
+  </Layout>
+</template>
+
+<script>
+import { Popover, Button, Input } from 'element-ui'
+import { amountRangeData } from '@/assets/js/selector.js'
+import Layout from '@/components/filter-items/Layout.vue'
+
+export default {
+  name: 'SelectContainer',
+  components: {
+    [Popover.name]: Popover,
+    [Button.name]: Button,
+    [Input.name]: Input,
+    Layout
+  },
+  props: {
+    type: {
+      type: String,
+      default: 'dropdown'
+    },
+    trigger: {
+      type: String,
+      default: 'hover'
+    },
+    placeholder: {
+      type: String,
+      default: '金额区间'
+    },
+    popoverTrigger: {
+      type: String,
+      default: 'hover'
+    },
+    value: {
+      type: [String, Object],
+      default: null
+    },
+    options: {
+      type: Array,
+      default: () => []
+    }
+  },
+  model: {
+    prop: 'value',
+    event: 'change'
+  },
+  data() {
+    return {
+      activeValue: this.value,
+      isCustom: false, // 当前是否是自定义选项
+      price: {
+        min: '',
+        max: ''
+      },
+      showPopover: false
+    }
+  },
+  computed: {
+    activeLabel() {
+      const price = this.activeValue
+      if (price) {
+        const priceArr = price.split('-')
+        if (priceArr.length > 1) {
+          const min = priceArr[0]
+          const max = priceArr[1]
+          if (min && max) {
+            if (min === '0') {
+              return `${max}万以下`
+            } else {
+              return `${min}-${max}万`
+            }
+          } else if (!min) {
+            return `${max}万以下`
+          } else if (!max) {
+            return `${min}万以上`
+          } else {
+            return ''
+          }
+        } else {
+          return ''
+        }
+      } else {
+        return ''
+      }
+    }
+  },
+  watch: {
+    value: {
+      immediate: true,
+      handler(val) {
+        this.setState(val)
+      }
+    }
+  },
+  methods: {
+    onVisibleChange(flag) {
+      this.isFocus = flag
+      if (flag) {
+        this.setState(this.activeValue)
+        this.$nextTick(() => {
+          if (this.showPopover) {
+            setTimeout(() => {
+              // popover在下拉框展示时需要重新计算位置,通过先将popover弹框透明度将为0等位置计算完成后再恢复
+              this.$refs.customPricePopover[0].updatePopper()
+              const $popover = this.$root.$el.querySelector('.custom-popover > .el-popover')
+              $popover.style.opacity = '1'
+            }, 300)
+          }
+        })
+      }
+    },
+    compareMinMax() {
+      const { min, max } = this.price
+      const hasMinAndMax = String(min).length && String(max).length
+      if (hasMinAndMax && Number(min) > Number(max)) {
+        this.price.max = min
+        this.price.min = max
+      }
+    },
+    onSubmitPrice() {
+      this.compareMinMax()
+      const { min, max } = this.price
+      if (!min && !max) return
+      this.activeValue = `${min}-${max}`
+      this.options.forEach(item => {
+        if (item.label === '自定义') {
+          item.value = `${min}-${max}`
+        }
+      })
+      this.isCustom = true
+      this.$refs.layoutRef.$refs.dropdownRef.hide()
+      this.$refs.customPricePopover[0].doClose()
+      this.$emit('change', this.activeValue)
+    },
+    handleChange(item) {
+      if (item.label !== '自定义') {
+        this.activeValue = item.value
+        this.isCustom = false
+        this.price.min = ''
+        this.price.max = ''
+        this.$refs.layoutRef.$refs.dropdownRef.hide()
+        this.$refs.customPricePopover[0].doClose()
+        this.$emit('change', this.activeValue)
+      } else {
+        this.isCustom = true
+      }
+    },
+    getState() {
+      return {
+        label: this.activeLabel,
+        value: this.activeValue,
+        isCustom: this.isCustom
+      }
+    },
+    setState(data) {
+      this.isCustom = false
+      if (data) {
+        const valueArr = this.options.filter(v => !v.disabled).map(t => t.value)
+        if (valueArr.includes(data)) {
+          this.activeValue = data
+        } else {
+          const priceArr = data.split('-')
+          const min = priceArr[0]
+          const max = priceArr[1]
+          this.isCustom = true
+          this.price.min = min
+          this.price.max = max
+          this.activeValue = data
+          this.showPopover = true
+          this.$nextTick(() => {
+            const $popover = this.$root.$el.querySelector('.custom-popover > .el-popover')
+            $popover.style.opacity = '0'
+          })
+        }
+      } else {
+        this.activeValue = data
+      }
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.filter-list {
+  min-width: 140px;
+  padding: 8px 0;
+  border: 1px solid $color_main;
+  background: #fff;
+  border-radius: 5px;
+  margin-top: 2px;
+
+  .filter-item {
+    padding: 4px 16px;
+    font-size: 14px;
+    line-height: 22px;
+    color: #1d1d1d;
+    text-align: left;
+    cursor: pointer;
+
+    &:hover {
+      background: #ECECEC;
+    }
+
+    &.active {
+      background: #ECECEC;
+    }
+
+    &.highlight {
+      color: $color_main;
+    }
+
+    span {
+      display: inline-block;
+      width: 100%;
+    }
+  }
+
+  .custom-label {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+
+    .el-icon-arrow-right {
+      margin-right: -8px;
+    }
+  }
+
+  .custom-popover {
+    .custom-money {
+      padding: 12px;
+      margin-left: 4px;
+      border: 1px solid $color_main;
+      background: #fff;
+      border-radius: 4px;
+
+      &-item {
+        display: flex;
+        align-items: center;
+        margin-bottom: 12px;
+      }
+
+      &-button {
+        display: flex;
+        justify-content: flex-end;
+
+        .el-button {
+          width: 60px;
+          height: 28px;
+          padding: 0;
+        }
+      }
+    }
+
+    ::v-deep {
+      .el-popover {
+        margin-left: 16px;
+        border-color: $color_main;
+        padding: 0;
+        border: 0;
+        background: transparent;
+      }
+
+      .price-input {
+        width: 88px;
+        height: 24px;
+        margin: 0 4px;
+
+        .el-input__inner {
+          height: 100%;
+          padding: 0 8px;
+        }
+
+        &.focus {
+          .el-input__inner {
+            border-color: $color_main;
+          }
+        }
+      }
+    }
+  }
+}
+</style>

+ 338 - 0
apps/bigmember_pc/src/components/filter-items/EstablishTimeSelector.vue

@@ -0,0 +1,338 @@
+<template>
+  <Layout ref="layoutRef" :type="type" :placeholder="placeholder" :trigger="trigger" :value="activeLabel"
+    @visible="onVisibleChange">
+    <div class="filter-list timepicker_" slot="empty">
+      <div class="filter-item" :class="{ 'active': item.value === activeValue, 'highlight': item.disabled && isCustom }"
+        v-for="item in options" :key="item.label" :label="item.label" :value="item.value" @click="handleChange(item)">
+        <el-popover v-if="item.disabled" class="custom-popover" :append-to-body="false" placement="right-end"
+          :trigger="popoverTrigger" :offset="12" v-model="showPopover" @show="popShow" @hide="popHide"
+          ref="customPopover">
+          <div class="custom-time">
+            <el-date-picker ref="timePick" class="time-pick" :append-to-body="false" v-model="p_time" type="daterange"
+              range-separator="-" start-placeholder="开始日期" end-placeholder="结束日期" value-format="timestamp"
+              align="center" @change="dateChange">
+            </el-date-picker>
+          </div>
+          <div slot="reference" class="custom-label">
+            <span>{{ item.label }}</span>
+            <i class="el-icon-arrow-right"></i>
+          </div>
+        </el-popover>
+        <span v-else>{{ item.label }}</span>
+      </div>
+    </div>
+  </Layout>
+</template>
+
+<script>
+import { Popover, Button, Input, DatePicker } from 'element-ui'
+import { timeRangeData } from '@/assets/js/selector.js'
+import Layout from '@/components/filter-items/Layout.vue'
+import { dateFormatter } from '@/utils/'
+export default {
+  name: 'SelectContainer',
+  components: {
+    [Popover.name]: Popover,
+    [Button.name]: Button,
+    [Input.name]: Input,
+    [DatePicker.name]: DatePicker,
+    Layout
+  },
+  props: {
+    type: {
+      type: String,
+      default: 'dropdown'
+    },
+    trigger: {
+      type: String,
+      default: 'hover'
+    },
+    placeholder: {
+      type: String,
+      default: '成立时间'
+    },
+    popoverTrigger: {
+      type: String,
+      default: 'hover'
+    },
+    value: {
+      type: [String, Object],
+      default: null
+    }
+  },
+  model: {
+    prop: 'value',
+    event: 'change'
+  },
+  data() {
+    return {
+      options: timeRangeData,
+      activeValue: this.value,
+      isCustom: false, // 当前是否是自定义选项
+      isFocus: false,
+      p_time: '',
+      time: {
+        start: '',
+        end: ''
+      },
+      showPopover: false,
+      timer: null
+    }
+  },
+  computed: {
+    activeLabel() {
+      const time = this.activeValue
+      if (time) {
+        if (this.options.find(v => v.value === time)) {
+          let label = this.options.find(v => v.value === time).label
+          if (label === '自定义') {
+            let start = time.split('-')[0]
+            let end = time.split('-')[1]
+            return dateFormatter(Number(start), 'yyyy-MM-dd') + '-' + dateFormatter(Number(end), 'yyyy-MM-dd')
+          } else {
+            return label || ''
+          }
+        } else {
+          let start = time.split('-')[0]
+          let end = time.split('-')[1]
+          return dateFormatter(Number(start), 'yyyy-MM-dd') + '-' + dateFormatter(Number(end), 'yyyy-MM-dd')
+        }
+      } else {
+        return ''
+      }
+
+    }
+  },
+  watch: {
+    value: {
+      immediate: true,
+      handler(val) {
+        this.setState(val)
+      }
+    }
+  },
+  methods: {
+    popShow() {
+      if (this.showPopover && this.$refs.timePick) {
+        setTimeout(() => {
+          this.$refs.timePick[0].focus()
+        }, 200)
+
+      }
+    },
+    popHide() {
+      // if (this.$refs.timePick) {
+      //   this.$refs.timePick[0].blur()
+      // }
+    },
+    onVisibleChange(flag) {
+      this.isFocus = flag
+      if (flag) {
+        this.setState(this.activeValue)
+        this.$nextTick(() => {
+          if (this.showPopover) {
+            setTimeout(() => {
+              // popover在下拉框展示时需要重新计算位置,通过先将popover弹框透明度将为0等位置计算完成后再恢复
+              this.$refs.customPopover[0].updatePopper()
+              const $popover = this.$root.$el.querySelector('.custom-popover > .el-popover')
+              $popover.style.opacity = '1'
+            }, 100)
+          }
+        })
+      }
+    },
+    dateChange(val) {
+      if (!val || val.length === 0) return
+      let start = val[0]
+      let end = val[1]
+      this.activeValue = `${start}-${end}`
+      this.time.start = start
+      this.time.end = end
+      this.options.forEach(item => {
+        if (item.label === '自定义') {
+          item.value = `${start}-${end}`
+        }
+      })
+      this.isCustom = true
+      this.$refs.layoutRef.$refs.dropdownRef.hide()
+      this.$refs.customPopover[0].doClose()
+      this.$emit('change', this.getState())
+    },
+    handleChange(item) {
+      if (item.label !== '自定义') {
+        this.activeValue = item.value
+        this.isCustom = false
+        this.p_time = ''
+        this.time.start = ''
+        this.time.end = ''
+        this.$refs.layoutRef.$refs.dropdownRef.hide()
+        this.$refs.customPopover[0].doClose()
+        this.$emit('change', this.getState())
+      } else {
+        this.isCustom = true
+      }
+    },
+    getState() {
+      return this.activeValue
+    },
+    getAllstate() {
+      return {
+        label: this.activeLabel,
+        value: this.activeValue,
+        isCustom: this.isCustom
+      }
+    },
+    setState(data) {
+      this.isCustom = false
+      if (data) {
+        const valueArr = this.options.filter(v => !v.disabled).map(t => t.value)
+        if (valueArr.includes(data)) {
+          this.activeValue = data
+        } else {
+          const timeArr = data.split('-')
+          const start = Number(timeArr[0])
+          const end = Number(timeArr[1])
+          this.isCustom = true
+          this.time.start = start
+          this.time.end = end
+          this.activeValue = data
+          this.p_time = [start, end]
+          this.showPopover = true
+          this.$nextTick(() => {
+            const $popover = this.$root.$el.querySelector('.custom-popover > .el-popover')
+            $popover.style.opacity = '0'
+          })
+        }
+      } else {
+        this.activeValue = data
+      }
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.filter-list.timepicker_ {
+  min-width: 140px;
+  padding: 8px 0;
+  border: 1px solid $color_main;
+  background: #fff;
+  border-radius: 5px;
+  margin-top: 2px;
+
+  .el-date-editor.time-pick {
+    color: transparent !important;
+    border-color: transparent !important;
+    background-color: transparent !important;
+  }
+
+  .time-pick {
+    ::v-deep {
+
+      .el-range-input,
+      .el-input__icon,
+      .el-range-separator {
+        display: none;
+      }
+
+      .el-picker-panel.el-date-range-picker.el-popper {
+        // position: relative !important;
+        border: 1px solid $color_main;
+        top: -48px !important;
+        margin-left: 4px;
+      }
+    }
+  }
+
+  .filter-item {
+    padding: 4px 16px;
+    font-size: 14px;
+    line-height: 22px;
+    color: #1d1d1d;
+    text-align: left;
+    cursor: pointer;
+
+    &:hover {
+      background: #ECECEC;
+    }
+
+    &.active {
+      background: #ECECEC;
+    }
+
+    &.highlight {
+      color: $color_main;
+    }
+
+    span {
+      display: inline-block;
+      width: 100%;
+    }
+  }
+
+  .custom-label {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+
+    .el-icon-arrow-right {
+      margin-right: -8px;
+    }
+  }
+
+  .custom-popover {
+    .custom-money {
+      padding: 12px;
+      margin-left: 4px;
+      border: 1px solid $color_main;
+      background: #fff;
+      border-radius: 4px;
+
+      &-item {
+        display: flex;
+        align-items: center;
+        margin-bottom: 12px;
+      }
+
+      &-button {
+        display: flex;
+        justify-content: flex-end;
+
+        .el-button {
+          width: 60px;
+          height: 28px;
+          padding: 0;
+        }
+      }
+    }
+
+    ::v-deep {
+      .el-popover {
+        margin-left: 16px;
+        border-color: $color_main;
+        padding: 0;
+        border: 0;
+        background: transparent;
+        box-shadow: 0px 0px 0px transparent;
+      }
+
+      .price-input {
+        width: 88px;
+        height: 24px;
+        margin: 0 4px;
+
+        .el-input__inner {
+          height: 100%;
+          padding: 0 8px;
+        }
+
+        &.focus {
+          .el-input__inner {
+            border-color: $color_main;
+          }
+        }
+      }
+    }
+  }
+}
+</style>

+ 71 - 0
apps/bigmember_pc/src/components/filter-items/IndustrySelector.vue

@@ -0,0 +1,71 @@
+<template>
+  <Layout
+    :type="type"
+    :placeholder="placeholder"
+    :trigger="trigger"
+    :value="computedVal"
+  >
+   <CascadeContent
+      ref="industryContent"
+      slot="empty"
+      :options="options"
+      :value="value"
+      @change="onChange"
+    >
+    </CascadeContent>
+  </Layout>
+</template>
+
+<script>
+
+import Layout from '@/components/filter-items/Layout.vue'
+import CascadeContent from '@/components/filter-items/CascadeContent.vue'
+import { industryListMapExp } from '@/assets/js/selector.js'
+export default {
+  name: 'IndustrySelector',
+  props: {
+    type: {
+      type: String,
+      default: 'dropdown'
+    },
+    trigger: {
+      type: String,
+      default: 'hover'
+    },
+    placeholder: {
+      type: String,
+      default: '行业'
+    },
+    value: {
+      type: Object,
+      default: () => {}
+    }
+  },
+  components: {
+    Layout,
+    CascadeContent
+  },
+  watch: {
+    value (val) {
+      this.selectedVal = this.$refs.industryContent.levelMapToArray(val)
+      this.$refs.industryContent.setState(val)
+    }
+  },
+  data () {
+    return {
+      options: industryListMapExp,
+      selectedVal: []
+    }
+  },
+  computed: {
+    computedVal () {
+      return this.selectedVal.length ? `${this.placeholder}${this.selectedVal.length}个` : ''
+    }
+  },
+  methods: {
+    onChange (value) {
+      this.$emit('change', value)
+    }
+  }
+}
+</script>

+ 103 - 0
apps/bigmember_pc/src/components/filter-items/InfoTypeDropdown.vue

@@ -0,0 +1,103 @@
+<template>
+  <Layout
+    :type="type"
+    :placeholder="placeholder"
+    :trigger="trigger"
+    :value="computedVal"
+  >
+    <CascadeContent
+      ref="infoTypeDropdown"
+      slot="empty"
+      :options="dataMap"
+      :value="value"
+      @change="onChange"
+    >
+      <template #expend-first="{ first }" >
+        <el-tooltip  effect="dark"  placement="right" v-if="first.name === '采购意向'">
+          <div slot="content" style="width:300px; font-size: 14px;line-height: 24px;">
+            <span>"采购意向"是指提供未发布招标公告前1-3个月,政府单位的采购意向信息,包含采购内容、预算金额、预计采购时间、采购联系人及联系方式等相关信息。</span>
+          </div>
+          <i class="icon-help-img" ></i>
+        </el-tooltip>
+      </template>
+    </CascadeContent>
+  </Layout>
+</template>
+
+<script>
+import { Tooltip } from 'element-ui'
+import Layout from '@/components/filter-items/Layout.vue'
+import CascadeContent from '@/components/filter-items/CascadeContent.vue'
+import { infoTypeListMapExp } from '@/assets/js/selector.js'
+export default {
+  name: 'InfoTypeDropdown',
+  props: {
+    type: {
+      type: String,
+      default: 'dropdown'
+    },
+    trigger: {
+      type: String,
+      default: 'hover'
+    },
+    placeholder: {
+      type: String,
+      default: '信息类型'
+    },
+    value: {
+      type: Object,
+      default: () => {}
+    },
+    // 不需要拟建筛选条件
+    notNJ:  {
+      type: Boolean,
+      default: false
+    }
+  },
+  components: {
+    [Tooltip.name]: Tooltip,
+    Layout,
+    CascadeContent
+  },
+  watch: {
+    value (val) {
+      this.selectedVal = this.$refs.infoTypeDropdown.levelMapToArray(val)
+      this.$refs.infoTypeDropdown.setState(val)
+    }
+  },
+  data () {
+    return {
+      options: infoTypeListMapExp,
+      selectedVal: []
+    }
+  },
+  computed: {
+    computedVal () {
+      return this.selectedVal.length ? `${this.placeholder}${this.selectedVal.length}个` : ''
+    },
+    dataMap () {
+      const map = infoTypeListMapExp
+      if(this.notNJ) {
+        delete map['拟建项目']
+      }
+      return map
+    }
+  },
+  methods: {
+    onChange (value) {
+      this.$emit('change', value)
+    }
+  }
+}
+</script>
+<style lang="scss" scoped>
+.icon-help-img {
+  display: inline-block;
+  width: 18px;
+  height: 18px;
+  margin-left: 6px;
+  background: url("~@/assets/images/icon/help.png") no-repeat center;
+  background-size: contain;
+  cursor: pointer;
+}
+</style>

+ 76 - 0
apps/bigmember_pc/src/components/filter-items/KeywordSelector.vue

@@ -0,0 +1,76 @@
+<template>
+  <Layout
+    :type="type"
+    :placeholder="placeholder"
+    :trigger="trigger"
+    :value="computedVal"
+  >
+    <CascadeContent
+      ref="keywordContent"
+      slot="empty"
+      :options="options"
+      :value="value"
+      @change="onChange"
+    >
+    </CascadeContent>
+  </Layout>
+</template>
+
+<script>
+
+import Layout from '@/components/filter-items/Layout.vue'
+import CascadeContent from '@/components/filter-items/CascadeContent.vue'
+import { industryListMapExp } from '@/assets/js/selector.js'
+export default {
+  name: 'IndustrySelector',
+  props: {
+    type: {
+      type: String,
+      default: 'dropdown'
+    },
+    trigger: {
+      type: String,
+      default: 'hover'
+    },
+    placeholder: {
+      type: String,
+      default: '关键词'
+    },
+    value: {
+      type: Object,
+      default: () => {}
+    },
+    options: {
+      type: Object,
+      default: () => {
+        return {}
+      }
+    }
+  },
+  components: {
+    Layout,
+    CascadeContent
+  },
+  watch: {
+    value (val) {
+      this.selectedVal = this.$refs.industryContent.levelMapToArray(val)
+      this.$refs.industryContent.setState(val)
+    }
+  },
+  data() {
+    return {
+      selectedVal: []
+    }
+  },
+  computed: {
+    computedVal () {
+      return this.selectedVal.length ? `${this.placeholder}${this.selectedVal.length}个` : ''
+    }
+  },
+  methods: {
+    onChange (value) {
+      this.$emit('change', value)
+    }
+  }
+}
+</script>

+ 107 - 0
apps/bigmember_pc/src/components/filter-items/KeywordTagsSelector.vue

@@ -0,0 +1,107 @@
+<template>
+  <Layout
+    :value="computedVal"
+    :type="type"
+    trigger="hover"
+    :placeholder="placeholder"
+    placement="bottom"
+  >
+    <KeywordTagsSelectorContent
+      slot="empty"
+      :list="value"
+      :placeholder="inputPlaceholder"
+      :max-tip="maxTip"
+      :disabled="disabled"
+      :max-list-length="maxListLength"
+      :max-length="maxLength"
+      @change="keywordTagsChange">
+    </KeywordTagsSelectorContent>
+  </Layout>
+</template>
+
+<script>
+import  Layout from '@/components/filter-items/Layout.vue'
+import KeywordTagsSelectorContent from './KeywordTagsSelectorContent'
+
+export default {
+  components: {
+    Layout,
+    KeywordTagsSelectorContent
+  },
+  props: {
+    type: {
+      type: String,
+      default: 'dropdown'
+    },
+    placeholder: {
+      type: String,
+      default: '排除词'
+    },
+    trigger: {
+      type: String,
+      default: 'hover'
+    },
+    inputPlaceholder: {
+      type: String,
+      default: '请输入关键词'
+    },
+    maxTip: {
+      type: String,
+      default: '输入个数已达上限'
+    },
+    // 输入框可输入最大长度
+    maxLength: {
+      type: Number,
+      default: 15
+    },
+    // 数组最大长度
+    maxListLength: {
+      type: Number,
+      default: 5
+    },
+    // 输入框是否禁用
+    disabled: {
+      type: Boolean,
+      default: false
+    },
+    value: {
+      type: Array,
+      default: () => []
+    }
+  },
+  model: {
+    prop: 'value',
+    event: 'change'
+  },
+  data () {
+    return {}
+  },
+  computed: {
+    computedVal () {
+      return  this.value.length ? `${this.placeholder} ${this.value.length || 0}/${this.maxListLength}` : ''
+    }
+  },
+  created() {},
+  methods: {
+    keywordTagsChange (list) {
+      this.$emit('change', list)
+    }
+  }
+}
+</script>
+
+<style lang='scss' scoped>
+    ::v-deep {
+      .el-dropdown-menu {
+        width: 440px;
+        min-height: 157px;
+        box-sizing: border-box;
+        border: 1px solid #2cb7ca !important;
+        padding: 0;
+      }
+      .popper__arrow{
+        display: none;
+      }
+    }
+
+</style>

+ 189 - 0
apps/bigmember_pc/src/components/filter-items/KeywordTagsSelectorContent.vue

@@ -0,0 +1,189 @@
+<template>
+  <div class='add-keyword-container'>
+    <div style="margin-top:6px;">
+      <el-input
+        class="add-keyword-input"
+        type="text"
+        :placeholder="list.length >= maxListLength ? maxTip : placeholder"
+        v-model.trim="addKeywordVal"
+        :maxlength="maxLength"
+        :disabled="disabled || list.length >= maxListLength"
+        @keyup.native="onKeyup"
+        @focus="onKeydown"
+        @keydown.native="onKeydown"
+      ></el-input>
+      <span class="add-keyword-btn" :class="{'focus': addKeywordVal}" @click="addKeyTags">添加</span>
+    </div>
+    <div class="add-keyword-tags">
+      <el-tag
+        :key="'keyword_tag_' + index"
+        v-for="(tag, index) in list"
+        closable
+        :disable-transitions="false"
+        @close="removeTag(tag)">
+        {{tag}}
+      </el-tag>
+    </div>
+    <slot name="radio"></slot>
+  </div>
+</template>
+
+<script>
+import { Input, Tag } from 'element-ui'
+
+export default {
+  components: {
+    [Input.name]: Input,
+    [Tag.name]: Tag
+  },
+  props: {
+    placeholder: {
+      type: String,
+      default: '请输入关键词'
+    },
+    maxTip: {
+      type: String,
+      default: '输入个数已达上限'
+    },
+    // 输入框可输入最大长度
+    maxLength: {
+      type: Number,
+      default: 15
+    },
+    list: {
+      type: Array,
+      default: () => {
+        return []
+      }
+    },
+    // 数组最大长度
+    maxListLength: {
+      type: Number,
+      default: 5
+    },
+    // 输入框是否禁用
+    disabled: {
+      type: Boolean,
+      default: false
+    }
+  },
+  data () {
+    return {
+      addKeywordVal: ''
+    }
+  },
+  methods: {
+    addKeyTags() {
+      if (!this.addKeywordVal) return
+      this.list.push(this.addKeywordVal.replace(/,/g, ' ').replace(/\s*$/g,''))
+      this.$emit('change', this.list)
+      this.addKeywordVal = ''
+    },
+    onKeyup() {
+      if(this.$parent.$parent) {
+        this.$parent.$parent.visible = true
+      }
+      this.addKeywordVal = this.addKeywordVal.replace(/,/g, ' ').trim().replace(/^\s*/g, '')
+    },
+    removeTag (tag) {
+      this.list.splice(this.list.indexOf(tag), 1)
+      this.$emit('change', this.list)
+    },
+    // dropdown的坑点,微软输入法,input输入的时候会失去焦,dropdown会收起,暂且如此处理
+    onKeydown () {
+      if(this.$parent.$parent) {
+        this.$parent.$parent.visible = true
+      }
+    }
+  }
+}
+</script>
+
+<style lang='scss' scoped>
+.add-keyword-container{
+  display: flex;
+  align-items: center;
+  flex-wrap: wrap;
+  padding-top: 12px;
+  margin-left: 16px;
+  box-sizing:border-box;
+  ::v-deep {
+    .add-keyword-input{
+      width:auto;
+      .el-input__inner{
+        width: 368px;
+        height: 36px;
+        line-height: 30px;
+        font-size: 14px !important;
+        border: 1px solid #E0E0E0;
+        border-radius: 4px;
+        color: #1D1D1D !important;
+      }
+    }
+    .el-tag {
+      .el-tag__close {
+        color: #aaa;
+        font-weight: 700;
+        background-color:rgba(255, 255, 255, 0);
+      }
+      &:hover {
+        .el-tag__close {
+          background-color: transparent;
+          color: #2CB7CA;
+        }
+      }
+    }
+  }
+  .el-tag {
+    margin-top: 6px;
+    height: 24px;
+    line-height: 22px;
+    padding: 0 8px;
+    background: #F5F6F7;
+    border: 1px solid #ECECEC;
+    border-radius: 4px;
+    margin-right: 16px;
+    color: #1D1D1D;
+    font-size: 14px;
+    margin-left: 0;
+    &:hover{
+      color: #2CB7CA;
+      border: 1px solid #2ABED1;
+      background: #fff;
+    }
+    .el-icon-close{
+     background: transparent;
+      text-align: center;
+      position: relative;
+      cursor: pointer;
+      font-size: 12px;
+      height: 16px;
+      width: 16px;
+      line-height: 16px;
+      vertical-align: middle;
+      top: -1px;
+      right: -5px;
+    }
+    .el-icon-close::before {
+      display: block;
+      content: "\e6db";
+    }
+  }
+  .el-icon-close:before {
+    content: "\e6db";
+  }
+  .add-keyword-tags{
+    max-width:440px;
+    width: 100%;
+    margin-top: 10px;
+    margin-bottom: 16px;
+  }
+  .add-keyword-btn{
+    margin-left: 8px;
+    font-size: 14px;
+    line-height: 22px;
+    color: #999999;
+    cursor: pointer;
+  }
+}
+</style>

+ 176 - 0
apps/bigmember_pc/src/components/filter-items/Layout.vue

@@ -0,0 +1,176 @@
+<template>
+  <div ref="layout" class="filter-layout" :class="{'filter-input-layout': type === 'select'}">
+    <el-dropdown
+      :trigger="trigger"
+      :placement="placement"
+      @visible-change="onVisibleChange"
+      ref="dropdownRef"
+    >
+      <div v-if="type === 'select'" class="select-input-prefix" @click="$emit('onClick')">
+        <span class="select-prefix-value" v-if="value" v-html="value"></span>
+        <span class="select-prefix-value" v-else>{{ placeholder }}</span>
+        <i class="el-icon-arrow-down" :class="{'is-reverse': isFocus}"></i>
+      </div>
+      <div v-else class="select-prefix" @click="$emit('onClick')">
+        <span class="select-prefix-value highlight-text" v-if="value">{{ value }}</span>
+        <span class="select-prefix-value" v-else>{{ placeholder }}</span>
+        <i class="iconfont icon-xiala" :class="{ 'is-reverse': isFocus, 'highlight-text': value }"></i>
+      </div>
+      <el-dropdown-menu v-if="dropdownMenu" :class="onlyBottom ? 'fixed-dropdown' : ''" slot="dropdown" :append-to-body="false" ref="dropdownMenu">
+        <slot name="empty"></slot>
+      </el-dropdown-menu>
+    </el-dropdown>
+  </div>
+</template>
+
+<script>
+import { Dropdown, DropdownMenu } from 'element-ui'
+export default {
+  name: 'DropdownLayout',
+  components: {
+    [Dropdown.name]: Dropdown,
+    [DropdownMenu.name]: DropdownMenu
+  },
+  props: {
+    // 展示类型:'dropdown', 'select'两种
+    type: {
+      type: String,
+      default: 'dropdown'
+    },
+    trigger: {
+      type: String,
+      default: 'hover'
+    },
+    placeholder: {
+      type: String,
+      default: ''
+    },
+    // 下拉框弹出位置
+    placement: {
+      type: String,
+      default: 'bottom-start'
+    },
+    // 是否展示下拉框
+    dropdownMenu: {
+      type: Boolean,
+      default: true
+    },
+    value: {
+      type: String,
+      default: ''
+    },
+    // dropdown-menu仅在dropdown下方
+    onlyBottom: {
+      type: Boolean,
+      default: true
+    }
+  },
+  data () {
+    return {
+      isFocus: false
+    }
+  },
+  mounted () {
+    // 修改dropdown挂载位置
+    this.$nextTick(() => {
+      if (this.dropdownMenu) {
+        this.$refs.layout.appendChild(this.$refs?.dropdownMenu.popperElm)
+      }
+    })
+  },
+  methods: {
+    onVisibleChange (visible) {
+      this.isFocus = visible
+      this.$emit('visible', visible)
+
+    },
+    // 手动触发dropdown展开
+    show () {
+      this.$refs.dropdownRef.show()
+    },
+    // 手动触发dropdown收起
+    hide () {
+      this.$refs.dropdownRef.hide()
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+::v-deep .el-dropdown-menu.fixed-dropdown{
+    top: 30px !important;
+}
+.filter-layout{
+  position: relative;
+  ::v-deep{
+    .el-popper{
+      min-width: 140px;
+      margin-top: 0;
+      border: 0;
+      padding: 0;
+      .popper__arrow{
+        display: none!important;
+      }
+    }
+
+
+  }
+  .is-reverse{
+    transform: rotate(180deg);
+  }
+  &.filter-input-layout{
+    .select-input-prefix{
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+      padding: 0 10px;
+      min-width: 140px;
+      height: 40px;
+      border: 1px solid #ececec;
+      border-radius: 5px;
+      &:focus{
+        border-color: $color_main;
+      }
+    }
+    ::v-deep{
+      .el-popper{
+        margin-top: 2px;
+      }
+    }
+  }
+  .select-prefix{
+    display: flex;
+    align-items: center;
+    width: 100%;
+    padding: 0 0 0 10px;
+    height: 28px;
+    line-height: 28px;
+    background: #fff;
+    color: #1d1d1d;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+    overflow: hidden;
+    text-align: left;
+    cursor: pointer;
+    .select-prefix-value{
+      display: inline-block;
+      margin-right: 2px;
+      flex: 1;
+      text-overflow: ellipsis;
+      overflow: hidden;
+      white-space: nowrap;
+    }
+    .icon-xiala{
+      color: #686868;
+      display: inline-block;
+      font-size: 16px;
+      flex-shrink: 0;
+      transform: rotate(0deg);
+      transition: transform .5s;
+      &.is-reverse{
+        transform: rotate(180deg);
+      }
+    }
+  }
+}
+</style>

+ 290 - 0
apps/bigmember_pc/src/components/filter-items/OnecascadeContent.vue

@@ -0,0 +1,290 @@
+<template>
+  <Layout :type="type" :placeholder="placeholder" :trigger="trigger" :value="computedVal">
+    <div class="cascade-content" slot="empty">
+      <div class="cascade-content-module">
+        <header class="module-header" v-if="showHeader">{{ placeholder }}</header>
+        <div class="module-main">
+          <ul>
+            <li class="module-item" :class="{ 'active': fActive === fIndex }" v-for="(first, fIndex) in firstList"
+              :key="first.name">
+              <el-checkbox v-model="first.checked" :indeterminate="first.indeterminate"
+                @change="onFirstChange($event, first, fIndex)">
+              </el-checkbox>
+              <span class="item-name">{{ first.label }}</span>
+            </li>
+          </ul>
+        </div>
+      </div>
+    </div>
+  </Layout>
+</template>
+
+<script>
+import Layout from '@/components/filter-items/Layout.vue'
+import { Checkbox } from 'element-ui'
+export default {
+  name: 'CascadeContent',
+  components: {
+    [Checkbox.name]: Checkbox,
+    Layout
+  },
+  props: {
+    type: {
+      type: String,
+      default: 'dropdown'
+    },
+    trigger: {
+      type: String,
+      default: 'hover'
+    },
+    placeholder: {
+      type: String,
+      default: '企业类型'
+    },
+    options: {
+      type: Array,
+      default: () => []
+    },
+    value: {
+      type: [Array,Object],
+      default: () => {
+        return null
+      }
+    },
+    showHeader: {
+      type: Boolean,
+      default: false
+    }
+  },
+  data() {
+    return {
+      firstList: [],
+      fActive: -1,
+      sActive: -1,
+      fTimer: null,
+      sTimer: null
+    }
+  },
+  model: {
+    prop: 'value',
+    event: 'change'
+  },
+  computed: {
+    // 源数据是否带有【全部】选项
+    hadAll () {
+      return this.options.find(item => item.label === '全部') || false
+    },
+    // 源数据带有选项【全部】选项的值
+    hadAllValue () {
+      let result = ''
+      if(this.hadAll) {
+        this.options.forEach(item => {
+          if( item.label === '全部') {
+            result = item.value
+          }
+        })
+      }
+      return result
+    },
+    computedVal() {
+      let result = ''
+      if(Array.isArray(this.value) && this.value.length) {
+        if(this.hadAllValue && this.value.includes(this.hadAllValue)) {
+          result = ''
+        } else {
+          result = `${this.placeholder}${this.value.length}个`
+        }
+      }
+      return result
+    }
+  },
+  watch: {
+    value(val) {
+      this.setState(val)
+    }
+  },
+  mounted() {
+    this.initData()
+  },
+  methods: {
+    getArray() {
+      const options = this.options
+      const toArray = options.map(item => {
+        const resultObj = {
+          value: item.value,
+          label: item.label,
+          checked: false,
+          indeterminate: false,
+          disabled: false
+        }
+        if(this.hadAll && item.label === '全部') {
+          resultObj.all = true
+        }
+        return  resultObj
+      })
+      return toArray
+    },
+    initData() {
+      const sourceList = this.getArray()
+      // 数据源无全部的时候,手动在数据头部插入一组全部数据
+      if(!this.hadAll) {
+        sourceList.unshift({
+          label: '全部',
+          value: '全部',
+          checked: false,
+          disabled: false,
+          indeterminate: false,
+          all: true
+        })
+      }
+      this.firstList = sourceList
+      if(!this.value || this.value?.length === 0) {
+        this.setState(null)
+      } else {
+        this.setState(this.value)
+      }
+    },
+    onFirstChange(checked, first, fIndex) {
+      if (first.all) {
+        this.firstList.forEach(item => {
+          item.checked = checked
+          item.indeterminate = false
+        })
+      } else {
+        first.checked = checked
+      }
+      this.checkFirstAllStatus()
+
+      // 会返回null和array两种数据类型!!!!!!
+      if (first.all) {
+        this.$emit('change', first.checked ? this.getState() : null)
+      } else {
+        this.$emit('change', this.getState())
+      }
+    },
+    restState() {
+      this.firstList.forEach(item => {
+        item.checked = false
+        item.indeterminate = false
+      })
+    },
+    checkFirstAllStatus() {
+      const allFistList = this.firstList.filter(item => !item.all)
+      const selectedFirstList = this.firstList.filter(item => !item.all && item.checked)
+      const allHalfSelected = this.firstList.filter(item => !item.all && item.indeterminate)
+      if (allFistList.length === selectedFirstList.length) {
+        this.firstList[0].checked = true
+        this.firstList[0].indeterminate = false
+      } else {
+        this.firstList[0].checked = false
+        this.firstList[0].indeterminate = selectedFirstList.length > 0 || allHalfSelected.length > 0
+      }
+    },
+    getState() {
+      const list = JSON.parse(JSON.stringify(this.firstList))
+      let arr = []
+      list.forEach(item => {
+        if (item.checked) {
+          arr.push(item.value)
+        }
+      })
+      if(this.hadAll && arr.includes(this.hadAllValue)){
+        return [this.hadAllValue]
+      } else if (arr.includes('全部')) {
+        return []
+      } else {
+        return arr
+      }
+    },
+    setState(value) {
+      this.restState()
+      if (!value) {
+        this.firstList.forEach(item => {
+          item.checked = false
+          item.indeterminate = false
+        })
+        return
+      }
+      if(Array.isArray(value)) {
+        if(value.length === 0 || (value.length  === 1 && value[0] === this.hadAllValue)) {
+          this.firstList.forEach(item => {
+            item.checked = true
+            item.indeterminate = false
+          })
+        } else {
+          this.firstList.forEach(item => {
+            if (value.includes(item.value)) {
+              item.checked = true
+              item.indeterminate = false
+            }
+          })
+        }
+      }
+      this.checkFirstAllStatus()
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.cascade-content {
+  display: flex;
+  align-items: center;
+  min-width: 210px;
+  height: 240px;
+  margin-top: 2px;
+  border-radius: 5px;
+  background: #fff;
+  overflow: hidden;
+  border: 1px solid $color_main;
+
+  &-module {
+    flex: 1;
+    height: 100%;
+    display: flex;
+    flex-direction: column;
+
+    &:not(:last-child) {
+      border-right: 1px solid #ececec;
+    }
+
+    .module-header {
+      padding: 12px 11px;
+      color: #999999;
+      font-size: 14px;
+      line-height: 22px;
+    }
+
+    .module-main {
+      flex: 1;
+      overflow-y: scroll;
+    }
+
+    .module-item {
+      position: relative;
+      display: flex;
+      align-items: center;
+      padding: 0 8px;
+      height: 30px;
+      font-size: 14px;
+      line-height: 22px;
+      color: #1d1d1d;
+
+      &.active,
+      &:hover {
+        background: #ececec;
+        cursor: pointer;
+      }
+
+      .item-name {
+        flex: 1;
+        margin-left: 4px;
+      }
+    }
+
+    .module-main::-webkit-scrollbar {
+      width: 4px;
+    }
+  }
+}
+</style>

+ 800 - 0
apps/bigmember_pc/src/components/filter-items/RegionCollapseSelector.vue

@@ -0,0 +1,800 @@
+<template>
+  <div
+    class="selector-content region-selector-content"
+    key="s-content"
+  >
+    <div class="selected-list" v-if="showSelectedList">
+      <el-tag
+        type="plain"
+        :closable="!singleChoice"
+        v-for="tag in selectedTagList"
+        :key="tag"
+        @close="tagClose(tag)"
+        >{{ tag }}</el-tag
+      >
+    </div>
+    <div class="select-list" ref="selectList">
+      <div
+        class="index-item"
+        :data-index="key"
+        :ref="'index-item-' + key"
+        v-for="(item, key) in provinceListMap"
+        :key="key"
+      >
+        <div
+          class="j-button-item"
+          v-for="(province, ii) in item"
+          :class="{
+            bgc: onlyProvince,
+            expand: province.expanded && province.canExpanded,
+            active: provinceButtonActive(province),
+            country: province.name === '全国',
+            hidden: !moreStatus && key === 'other',
+            [province.selectedState]: !showSelectedList
+          }"
+          :key="ii * 2"
+          @click="changeExpandStateForLine($event, province)"
+        >
+          {{ province.name }}
+        </div>
+      </div>
+      <div class="city-list" ref="cityList" v-show="expandedCitiesShow">
+        <div class="city-list-content">
+          <div
+            v-if="!onlyCity"
+            class="city-item province"
+            :class="{
+              active: expandedProvince.selectedState === 'checked'
+            }"
+            @click="clickProvinceInCityListForLine(expandedProvince)"
+          >
+            {{ expandedProvince.name }}
+          </div>
+          <div
+            class="city-item city"
+            :class="{
+              active: city.selected
+            }"
+            v-for="(city, iii) in expandedProvince.children"
+            :key="iii"
+            @click="changeCityStateForLine(expandedProvince, city)"
+          >
+            {{ city.city }}
+          </div>
+        </div>
+        <div class="city-list-footer">
+          <button class="confirm" @click="confirmCitySelected">确定</button>
+          <button class="cancel" @click="cancelCitySelected">取消</button>
+        </div>
+      </div>
+      <div slot="expand" class="is-expand" @click="toggleMoreStatus">
+        <span>{{ moreStatus ? '收起' : '更多' }}</span>
+        <i class="el-icon-arrow-down" :class="{'is-reverse': moreStatus}"></i>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { Tag } from 'element-ui'
+import chinaMapJSON from '@/assets/js/china_area.js'
+import { hotAndAllProvinceList } from '@/assets/js/selector.js'
+import { getRandomString } from '@/utils/'
+export default {
+  name: 'RegionLineSelector',
+  props: {
+    onlyProvince: {
+      type: Boolean,
+      default: false
+    },
+    // 是否仅能选择城市
+    onlyCity: {
+      type: Boolean,
+      default: false
+    },
+    // 是否单选
+    singleChoice: {
+      type: Boolean,
+      default: false
+    },
+    beforeTabClick: Function,
+    // 是否显示选择结果
+    showSelectedList: {
+      type: Boolean,
+      default: true
+    },
+    // 刚进入页面需要被选中的城市数据
+    value: {
+      type: Object,
+      default() {
+        return {}
+      }
+    }
+  },
+  components: {
+    [Tag.name]: Tag,
+  },
+  model: {
+    prop: 'value',
+    event: 'change'
+  },
+  data() {
+    return {
+      // 省份与字母IndexBar对照表
+      provinceListMapExp: hotAndAllProvinceList,
+      provinceListMap: {},
+      // indexBar数据
+      indexList: [],
+      provinceExp: {
+        name: '',
+        // 展开状态
+        expanded: false,
+        // 是否可以展开
+        canExpanded: false,
+        // 选中状态: half(半选)、checked(全选)、''(未选中)、checkeddisabled(全选不能点击)、nonedisabled(未选不能点击)
+        selectedState: '',
+        children: [],
+        id: ''
+      },
+      // line状态下,当前被展开省的省份列表
+      expandedProvince: {
+        children: []
+      },
+      selectedCity: {},
+      selectedTagList: [],
+      moreStatus: false
+    }
+  },
+  computed: {
+    expandedCitiesShow() {
+      if (!this.expandedProvince) return false
+      return this.expandedProvince.children.length
+    }
+  },
+  watch: {
+    value(newVal, oldVal) {
+      this.setState(newVal)
+    }
+  },
+  created() {
+    this.initIndexBarAndAreaMap()
+    this.setState(this.value)
+  },
+  methods: {
+    toggleMoreStatus() {
+      this.moreStatus = !this.moreStatus
+      for (const key in this.provinceListMap) {
+        this.provinceListMap[key].forEach((item) => {
+          item.expanded = false
+        })
+      }
+      this.expandedProvince = {
+        children: []
+      }
+    },
+    getAllItem() {
+      var p = {}
+      if (this.provinceListMap['hot']) {
+        p = this.provinceListMap['hot'][0]
+      }
+      return p
+    },
+    // 整理城市数据列表(并初始化indexBar数据)
+    initIndexBarAndAreaMap(provinceMap = {}) {
+      // 整理数据得到indexListMap(),同时获得indexList
+      const provinceListMap = {}
+      const indexList = []
+      const pMap =
+        Object.keys(provinceMap).length === 0
+          ? this.provinceListMapExp
+          : provinceMap
+      if (Object.keys(provinceMap).length !== 0) {
+        this.provinceListMapExp = provinceMap
+      }
+      for (const key in pMap) {
+        const areaArr = []
+        indexList.push(key)
+        pMap[key].forEach((pName) => {
+          const provinceExp = JSON.parse(JSON.stringify(this.provinceExp))
+
+          provinceExp.name = pName
+          provinceExp.id = `ap-${getRandomString(8).toLowerCase()}`
+
+          if (pName !== '全国') {
+            let cities = []
+            if (!this.onlyProvince) {
+              cities = this.getCitiesFromJSONMap(pName)
+            }
+            // 筛选掉直辖市和特别行政区(台湾省也不不需要展开)
+            if (cities.ProRemark === '省份' || cities.ProRemark === '自治区') {
+              if (cities.ProID === 32) {
+                provinceExp.children = []
+                provinceExp.canExpanded = false
+              } else {
+                cities.city.forEach((c) => {
+                  // 将市区重组成一个新的对象
+                  return provinceExp.children.push({
+                    city: c.name,
+                    selected: false,
+                    canSelected: true,
+                    id: `ac-${getRandomString(8).toLowerCase()}`
+                  })
+                })
+              }
+            } else {
+              provinceExp.children = []
+              provinceExp.canExpanded = false
+            }
+          }
+
+          provinceExp.canExpanded = provinceExp.children.length !== 0
+          areaArr.push(provinceExp)
+        })
+
+        provinceListMap[key] = areaArr
+      }
+
+      this.provinceListMap = provinceListMap
+      this.indexList = indexList
+
+      // 给provinceListMap赋值
+      for (const k in provinceListMap) {
+        this.$set(this.provinceListMap, k, provinceListMap[k])
+      }
+      this.getAllItem().selectedState = 'checked'
+    },
+    // 循环chinaMapJSON,找到对应省下面对应的市
+    getCitiesFromJSONMap(provinceName) {
+      return chinaMapJSON.find((item) => item.name.indexOf(provinceName) !== -1)
+    },
+    // 控制城市盒子展开和收起(card)
+    changeExpandState(e, province) {
+      if (!province.canExpanded) return
+      province.expanded = !province.expanded
+    },
+    // 控制城市盒子展开和收起(line)
+    changeExpandStateForLine(e, province) {
+      const beforeTabClick = this.beforeTabClick
+      if (beforeTabClick) {
+        const pass = beforeTabClick(e, province)
+        if (!pass) {
+          return
+        }
+      }
+      if (province.name === this.expandedProvince.name) return
+      // 循环,将其他全部置为false
+      for (const key in this.provinceListMap) {
+        this.provinceListMap[key].forEach((item) => {
+          item.expanded = false
+        })
+      }
+      this.expandedProvince = {
+        children: []
+      }
+      province.expanded = true
+      // 省份数据与原数据分离(点击确定覆盖原数据,点击取消则不保存数据)
+      this.expandedProvince = JSON.parse(JSON.stringify(province))
+      // 如果直接点击直辖市
+      if (province.children.length === 0) {
+        if (province.name === '全国') {
+          this.setState()
+        } else {
+          if (this.singleChoice) {
+            this.setState()
+          }
+          this.getAllItem().selectedState = ''
+          if (this.showSelectedList) {
+            this.provinceListMap['hot'][0].selectedState = ''
+            this.expandedProvince.selectedState = 'checked'
+          } else {
+            var checked = this.expandedProvince.selectedState
+            if (checked === 'checked') {
+              this.expandedProvince.selectedState = ''
+            } else {
+              this.expandedProvince.selectedState = 'checked'
+            }
+          }
+        }
+        this.confirmCitySelected()
+      } else {
+        var hasCitySelected = this.expandedProvince.children.some(function (v) {
+          return v.selected
+        })
+        if (!hasCitySelected) {
+          this.expandedProvince.selectedState = 'checked'
+        }
+        this.moveTheCityContainer(e)
+      }
+    },
+    moveTheCityContainer(e) {
+      const selectList = this.$refs.selectList
+      const cityList = this.$refs.cityList
+      const { lineFirstDom, clickLine } = this.getDomInfo(e.target)
+
+      if (clickLine >= lineFirstDom.length) {
+        selectList.appendChild(cityList) // 往列表末尾插入元素
+      } else if (clickLine <= 1) {
+        selectList.insertBefore(cityList, lineFirstDom[1])
+      } else {
+        selectList.insertBefore(cityList, lineFirstDom[clickLine])
+      }
+    },
+    getDomInfo(dom) {
+      const indexDOMList = [] // 所有索引项dom数组(索引项下有每个省份的按钮)
+      const indexTopList = [] // 每个元素距离顶部高度数组
+      const tolerance = [] // 行间距差值
+      const lineFirstDom = [
+        // 每行的第一个dom
+        ...this.$refs[`index-item-${this.indexList[0]}`]
+      ]
+      this.indexList.forEach((item) => {
+        const ref = this.$refs[`index-item-${item}`]
+        if (ref && ref[0]) {
+          indexDOMList.push(ref[0])
+          indexTopList.push(parseInt(ref[0].getBoundingClientRect().top))
+        }
+      })
+      for (let i = 0; i < indexTopList.length; i++) {
+        if (indexTopList[i + 1] > indexTopList[i]) {
+          tolerance.push(indexTopList[i + 1] - indexTopList[i])
+          lineFirstDom.push(indexDOMList[i + 1])
+        }
+      }
+
+      // 求平均值,谁的值大于平均值,就在哪一行
+      let insetedLine = 0
+      const avg =
+        tolerance.length > 0
+          ? tolerance.reduce((prev, item) => prev + item) / tolerance.length
+          : 0
+      for (let j = 0; j < tolerance.length; j++) {
+        if (tolerance[j] > avg) {
+          insetedLine = j + 1
+        }
+      }
+
+      // clickDOMIndex: 点击dom在indexDOMList的索引
+      const clickDOMIndex = indexDOMList.findIndex(
+        (item) => item === dom.parentNode
+      ) // dom是点击的按钮
+      const indexTopSet = Array.from(new Set(indexTopList))
+      const clickLine =
+        indexTopSet.findIndex((item) => item === indexTopList[clickDOMIndex]) +
+        1
+
+      return {
+        lineFirstDom, // 每行的第一个dom
+        indexTopList, // 每个元素距离顶部高度数组
+        indexTopSet, // indexTopList去重后的数组(里面是每一行距离顶部的高度)
+        tolerance, // 行间距差值
+        clickLine, // 通过传入的dom计算出的当前点击的哪一行
+        insetedLine // 点击按钮前cityList在哪一行
+      }
+    },
+    // 城市选择按钮点击事件(line)
+    // 根据城市的选择情况判断省份的选择情况
+    changeCityStateForLine(province, city) {
+      if (this.singleChoice) {
+        this.setState()
+      }
+
+      this.getAllItem().selectedState = ''
+      province.selectedState = ''
+
+      if (this.singleChoice) {
+        // 单选情况下,需要先将其他选项取消掉
+        province.children.forEach((item) => (item.selected = false))
+      }
+      city.selected = !city.selected
+      // 判断省份的选择状态
+      let count = 0
+      const cityLength = province.children.length
+      if (cityLength) {
+        province.children.forEach((v) => {
+          // 前提是可点击的
+          if (v.canSelected && v.selected) {
+            count++
+          }
+        })
+      }
+      if (count === cityLength) {
+        // line状态下 ,城市全部选中,则只选中省份即可
+        province.selectedState = 'checked'
+        province.children.forEach((item) => (item.selected = false))
+      } else if(count < cityLength) {
+        province.selectedState = 'half'
+      }
+    },
+    // 省份点击事件(城市列表中的省份按钮)(line)
+    clickProvinceInCityListForLine(province) {
+      if (this.singleChoice) {
+        this.setState()
+        this.getAllItem().selectedState = ''
+      }
+      const state = province.selectedState
+      province.children.forEach((v) => (v.selected = false))
+      if (state === 'checked') {
+        province.selectedState = ''
+      } else {
+        province.selectedState = 'checked'
+      }
+    },
+    // 检查是否所有省份按钮被全选中
+    // 全部被全选->返回true
+    checkAllProvinceState() {
+      const stateArr = []
+      for (const key in this.provinceListMap) {
+        this.provinceListMap[key].forEach((item) => {
+          if (item.name !== '全国') {
+            if (item.selectedState === '') {
+              stateArr.push('unchecked')
+            } else if (item.selectedState === 'checked') {
+              stateArr.push('checked')
+            } else {
+              stateArr.push('other')
+            }
+          }
+        })
+      }
+      // 统计不同状态的个数
+      const counter = {
+        checked: 0,
+        unchecked: 0,
+        other: 0
+      }
+      for (let i = 0; i < stateArr.length; i++) {
+        const k = stateArr[i]
+        if (counter[k]) {
+          counter[k] += 1
+        } else {
+          counter[k] = 1
+        }
+      }
+      return {
+        state: stateArr,
+        allSelected: counter.checked === stateArr.length,
+        noSelected: counter.unchecked === stateArr.length
+      }
+    },
+    setAllNoSelected() {
+      for (const key in this.provinceListMap) {
+        this.provinceListMap[key].forEach((item) => {
+          item.selectedState = ''
+          item.children.forEach((iitem) => {
+            iitem.selected = false
+          })
+        })
+      }
+    },
+    // 初始化选中城市数据(card/line共用)
+    setState(data = {}) {
+      // 设置全国
+      if (!data || Object.keys(data).length === 0) {
+        // 其他全部设置不选中,全国设置选中
+        for (const key in this.provinceListMap) {
+          this.provinceListMap[key].forEach((item) => {
+            item.selectedState = ''
+            item.children.forEach((iitem) => {
+              iitem.selected = false
+            })
+            if (item.name === '全国') {
+              item.selectedState = 'checked'
+            }
+          })
+        }
+      } else {
+        // 先将所有城市选择取消
+        this.setState()
+        // 设置某几个省份被选中
+        for (const key in this.provinceListMap) {
+          this.provinceListMap[key].forEach((item) => {
+            const selectCityArr = data[item.name]
+            if (Array.isArray(selectCityArr)) {
+              if (selectCityArr.length === 0) {
+                // 全省被选中
+                item.children.forEach((iitem) => {
+                  iitem.selected = false
+                })
+
+                item.selectedState = 'checked'
+              } else {
+                // 省份中的某些市被选中
+                item.children.forEach((iitem) => {
+                  if (selectCityArr.indexOf(iitem.city) !== -1) {
+                    iitem.selected = true
+                  }
+                })
+                item.selectedState = 'half'
+              }
+            }
+
+            if (item.name === '全国') {
+              item.selectedState = ''
+            }
+          })
+        }
+      }
+      this.getSelectedTagList(data)
+    },
+    // 获取当前选中城市数据
+    getState() {
+      const counter = {}
+      // 判断是否全国被选中
+      if (this.getAllItem().selectedState === 'checked') {
+        return counter
+      }
+
+      // 全国没有被选中,排除循环全国
+      for (const key in this.provinceListMap) {
+        // if (key === '#') continue
+        this.provinceListMap[key].forEach((item) => {
+          // 当前省份下被选中的城市数量
+          const selectedCityArr = []
+          const cityTotalCount = item.children.length
+          item.children.forEach((iitem) => {
+            if (iitem.selected && iitem.canSelected) {
+              selectedCityArr.push(iitem.city)
+            }
+          })
+
+          // 先看是否有城市被选,再看是否省份被选
+          if (selectedCityArr.length) {
+            counter[item.name] = selectedCityArr
+          } else {
+            if (item.selectedState === 'checked') {
+              counter[item.name] = []
+            }
+          }
+
+        })
+      }
+      return counter
+    },
+    provinceButtonActive(province) {
+      if (this.onlyProvince) {
+        return province.selectedState === 'checked'
+      } else {
+        return province.selectedState === 'checked' && province.name === '全国'
+      }
+    },
+    confirmCitySelected() {
+      // 统计时候有城市被选中了
+      const cityLength = this.expandedProvince.children.length
+      let count = 0
+      if (cityLength) {
+        const selectedCityArr = this.expandedProvince.children.filter((v) => {
+          return v.canSelected && v.selected
+        })
+        count = selectedCityArr.length
+        // 判断是否仅能够选中城市
+        if (this.onlyCity) {
+          if (count === 0) {
+            return this.$toast('还未选择城市,请选择')
+          }
+        }
+      }
+      if (this.showSelectedList) {
+        if (
+          this.expandedProvince.selectedState !== 'checked' &&
+          cityLength !== 0 &&
+          count === 0
+        ) {
+          return
+        }
+      }
+
+      var beforeChange = this.beforeChange
+      if (beforeChange) {
+        var pass = beforeChange(this.expandedProvince)
+        if (!pass) {
+          return
+        }
+      }
+      // 替换赋值
+      for (const key in this.provinceListMap) {
+        // if (key === '#') continue
+        const res = this.provinceListMap[key].find((item) => {
+          if (item.name === this.expandedProvince.name) {
+            Object.assign(item, this.expandedProvince)
+            this.getAllItem().selectedState = ''
+          }
+          return item.name === this.expandedProvince.name
+        })
+        if (res) {
+          break
+        }
+      }
+      this.selectedCity = this.getState()
+
+      this.getSelectedTagList(this.selectedCity)
+      this.cancelCitySelected()
+      this.$emit('change', this.selectedCity)
+    },
+    cancelCitySelected() {
+      const selected = this.getState()
+      if (selected) {
+        this.setState(selected)
+      }
+      for (const key in this.provinceListMap) {
+        this.provinceListMap[key].forEach((item) => {
+          item.expanded = false
+        })
+      }
+      this.expandedProvince = {
+        children: []
+      }
+    },
+    getSelectedTagList(v) {
+      const privinceArr = []
+      let cityArr = []
+      for (const key in v) {
+        const item = v[key]
+        if (Array.isArray(item)) {
+          if (item.length === 0) {
+            privinceArr.push(key)
+          } else {
+            cityArr = cityArr.concat(item)
+          }
+        }
+      }
+      this.selectedTagList = privinceArr.concat(cityArr)
+    },
+    tagClose(name) {
+      this.cancelCitySelected()
+      if (name === '全国') {
+        this.selectedTagList = []
+        return
+      }
+      for (const key in this.selectedCity) {
+        const index = this.selectedCity[key].indexOf(name)
+        if (name === key) {
+          delete this.selectedCity[key]
+          break
+        } else if (index !== -1) {
+          this.selectedCity[key].splice(index, 1)
+          if (this.selectedCity[key].length === 0) {
+            delete this.selectedCity[key]
+          }
+        }
+      }
+      this.setState(this.selectedCity)
+      this.confirmCitySelected()
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.region-selector-content {
+  padding-right: 32px;
+  .el-tag--plain {
+    color: $color-text--highlight;
+    border-color: $color-text--highlight;
+    .el-tag__close {
+      color: $color-text--highlight;
+      &:hover {
+        color: #fff;
+        background-color: $color-text--highlight;
+      }
+    }
+  }
+  .el-tag {
+    margin: 4px 6px;
+    height: 28px;
+    line-height: 26px;
+  }
+  .select-list {
+    position: relative;
+    display: flex;
+    flex-direction: column;
+    padding-right: 72px;
+    .index-item {
+      display: flex;
+      flex-wrap: wrap;
+      .index-bar {
+        margin-left: 10px;
+        margin-right: 5px;
+        color: #999;
+      }
+    }
+    .j-button-item {
+      display: inline-block;
+      margin: 2px 5px;
+      padding: 1px 6px;
+      line-height: 20px;
+      border-radius: 4px;
+      font-size: 14px;
+      text-align: center;
+      background-color: #fff;
+      cursor: pointer;
+      box-sizing: border-box;
+      &:hover {
+        color: $color-text--highlight;
+      }
+      &.active {
+        color: #fff;
+        background-color: $color-text--highlight;
+        border: 1px solid $color-text--highlight;
+      }
+      &.expand {
+        background-color: #f5f5fb;
+        border: 1px solid #e0e0e0;
+        border-bottom-color: transparent;
+        border-bottom-left-radius: 0;
+        border-bottom-right-radius: 0;
+        position: relative;
+        z-index: 2;
+      }
+      &.checked {
+        color: #fff;
+        background-color: $color-text--highlight;
+        border: 1px solid $color-text--highlight;
+      }
+      &.half {
+        color: #2abed1;
+        border: 1px dashed #2abed1;
+      }
+    }
+  }
+  .city-list {
+    margin-top: -4px;
+    padding: 12px 20px;
+    width: 100%;
+    background-color: #f5f5fb;
+    border-radius: 4px;
+    border: 1px solid #e0e0e0;
+    .city-item {
+      font-size: 14px;
+      display: inline-block;
+      margin: 0 4px 4px;
+      padding: 4px 8px;
+      border-radius: 4px;
+      cursor: pointer;
+      &.active {
+        color: #fff;
+        background-color: $color-text--highlight;
+      }
+    }
+    .city-list-footer {
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      margin-top: 12px;
+      button {
+        padding: 4px 16px;
+        font-size: 14px;
+        line-height: 18px;
+        color: #1d1d1d;
+        background-color: #fff;
+        cursor: pointer;
+        border-radius: 4px;
+        border: 1px solid #e0e0e0;
+        &.confirm {
+          margin-right: 15px;
+          color: #fff;
+          background-color: #2cb7ca;
+          border-color: #2cb7ca;
+        }
+      }
+    }
+  }
+  .is-expand{
+    position: absolute;
+    top: 2px;
+    right: 0;
+    display: inline-block;
+    font-size: 14px;
+    line-height: 22px;
+    color: #686868;
+    cursor: pointer;
+    i{
+      margin-left: 2px;
+      transform: rotate(0deg);
+      transition: transform .3s;
+    }
+    .is-reverse{
+      transform: rotate(180deg);
+    }
+  }
+}
+</style>

+ 995 - 0
apps/bigmember_pc/src/components/filter-items/RegionSelector.vue

@@ -0,0 +1,995 @@
+<template>
+  <Layout
+    ref="layoutRef"
+    :type="type"
+    :placeholder="placeholder"
+    :trigger="trigger"
+    :value="computedVal"
+    @visible="onVisibleChange"
+  >
+    <div slot="empty" class="region--wrap">
+      <div class="select--count" v-if="showCount">
+        <div class="count-label">可选:<span v-html="canSelectedCountText"></span>,已选:<span v-html="selectedCountText"></span></div>
+      </div>
+      <div class="select--result" v-if="showTags && tags && tags.length !== 0">
+        <el-tag
+          v-for="(tag, index) in tags"
+          :key="tag.name + index"
+          closable
+          type="info"
+          @close="onCloseTags(tag)"
+        >
+          {{tag.name}}
+        </el-tag>
+      </div>
+      <div class="select--container">
+        <div class="module-container province-container">
+          <header class="module-header">省份</header>
+          <div class="module-main">
+            <ul>
+              <li class="module-item" @mouseover="onProvinceMouseOver(province, pIndex)" @mouseout="onProvinceMouseOut($event)" :class="{'active': pActive === pIndex}" v-for="(province, pIndex) in provinceList" :key="province.name">
+                <el-checkbox v-model="province.checked" :indeterminate="province.indeterminate" @change="onProvinceChange($event, province, pIndex)"></el-checkbox>
+                <span class="item-name" @click.self="onOpenCity(province, pIndex)">{{ province.name }}</span>
+                <i class="el-icon-arrow-right"></i>
+              </li>
+            </ul>
+          </div>
+        </div>
+        <div class="module-container city-container">
+          <header class="module-header"><span :class="{'icon-have-vip': !vip}">城市</span></header>
+          <div class="module-main">
+            <ul>
+              <li class="module-item" @mouseover="onCityMouseOver(city, cIndex)" @mouseout="onCityMouseOut($event)" :class="{'active': cActive === cIndex}" v-for="(city, cIndex) in citiesList" :key="city.name">
+                <el-checkbox v-model="city.checked" :disabled="city.disabled" :indeterminate="city.indeterminate" @change="onCitiesChange($event, city, cIndex)"></el-checkbox>
+                <span class="item-name" @click.self="onOpenCountry(city, cIndex)">{{ city.name }}</span>
+                <i class="el-icon-arrow-right" v-show="showCounty"></i>
+              </li>
+            </ul>
+          </div>
+        </div>
+        <div class="module-container country-container" v-show="showCounty">
+          <header class="module-header"><span :class="{'icon-have-vip': !vip}">区县</span></header>
+          <div class="module-main">
+            <ul>
+              <li class="module-item" v-for="(country, index) in countryList" :key="country + index">
+                <el-checkbox v-model="country.checked" :disabled="country.disabled" :indeterminate="country.indeterminate" @change="onCountryChange($event, country)"></el-checkbox>
+                <span class="item-name">{{ country.name }}</span>
+              </li>
+            </ul>
+          </div>
+        </div>
+      </div>
+    </div>
+  </Layout>
+</template>
+
+<script>
+import { Tag, Checkbox } from 'element-ui'
+import Layout from '@/components/filter-items/Layout.vue'
+import chinaMapJSON from '@/assets/js/china_area.js'
+export default {
+  name: 'RegionSelector',
+  components: {
+    [Tag.name]: Tag,
+    [Checkbox.name]: Checkbox,
+    Layout
+  },
+  props:{
+    type: {
+      type: String,
+      default: 'dropdown'
+    },
+    trigger: {
+      type: String,
+      default: 'hover'
+    },
+    placeholder: {
+      type: String,
+      default: '项目地区'
+    },
+    // 初始化选中的地区('': 不选, {}:全国)
+    initMap: {
+      type: [Object, String],
+      default () {
+        return ''
+      }
+    },
+    // 是否有权限
+    vip: {
+      type: Boolean,
+      default: true
+    },
+    // 是否展示全国
+    isHaveAll: {
+      type: Boolean,
+      default: true
+    },
+    // 是否显示左侧文案
+    showLabel: {
+      type: Boolean,
+      default: true
+    },
+    // 是否展示已选可选计数
+    showCount: {
+      type: Boolean,
+      default: true
+    },
+    // 是否展示选择标签tag
+    showTags: {
+      type: Boolean,
+      default: true
+    },
+    // 可选省份 买了几个省份,-1代表全国
+    areaCount: {
+      type: Number,
+      default: -1
+    },
+    // 是否显示区县
+    showCounty: {
+      type: Boolean,
+      default: true
+
+    },
+    value: {
+      type: Object,
+      default: () => {}
+    }
+  },
+  model: {
+    prop: 'value',
+    event: 'change'
+  },
+  data () {
+    return {
+      tags: [],
+      pActive: 0,
+      cActive: 0,
+      provinceList: [],
+      citiesList: [],
+      countryList: [],
+      pTimer: null,
+      cTimer: null
+    }
+  },
+  computed: {
+    computedVal () {
+      if (this.tags.length) {
+        return `${this.placeholder}${this.tags.length}个`
+      } else {
+        return ''
+      }
+    },
+     // 可选多少个省
+    canSelectedCountText () {
+      if (this.areaCount === -1) {
+        return '<span class="count-value">全国</span>'
+      } else {
+        return `<span class="highlight-text">${this.areaCount}</span><span class="count-value">个省</span>`
+      }
+    },
+    // 已选多少个省
+    selectedProvinceCount () {
+      const regionMap = this.getState()
+      return regionMap ? Object.keys(regionMap).length : 0
+    },
+    // 已选省文案
+    selectedCountText () {
+      const allChecked = this.allChecked
+      const len = this.selectedProvinceCount
+      if (allChecked) {
+        return '<span class="count-value">全国</span>'
+      } else {
+        return `<span class="highlight-text">${len}</span><span class="count-value">个省</span>`
+      }
+    },
+    // 已选择省是否超出可选省数量
+    isExceed () {
+      const allChecked = this.allChecked
+      const len = this.selectedProvinceCount
+      if (this.areaCount === -1) {
+        return false
+      } else {
+        return allChecked || this.areaCount < len
+      }
+    },
+    // 全国是否选中
+    allChecked () {
+      return this.provinceList.some(v => v.name === '全国' && v.checked)
+    }
+  },
+  watch: {
+    value (val) {
+      this.setState(val)
+    }
+  },
+  mounted () {
+    this.initAreaMap()
+  },
+  methods: {
+    // 获取选中的地区tag
+    getRegionTags () {
+      const tagsArr = []
+      const regionMap  = this.getState()
+      if (regionMap) {
+        const obj = regionMap
+        for (const key in obj) {
+          if (Object.keys(obj[key]).length === 0) {
+            tagsArr.push({
+              type: '省',
+              name: key,
+              parent: key
+            })
+          } else {
+            const cityObj = obj[key]
+            for (const cKey in cityObj) {
+              if (cityObj[cKey].length === 0) {
+                tagsArr.push({
+                  type: '市',
+                  name: cKey,
+                  parent: key
+                })
+              } else {
+                const districtArr = cityObj[cKey].map((item) => {
+                  return {
+                    name: item,
+                    type: '县',
+                    parent: cKey
+                  }
+                })
+                tagsArr.push(...districtArr)
+              }
+            }
+          }
+        }
+      }
+      this.tags = tagsArr
+    },
+    // 移除tag
+    onCloseTags (item) {
+      const index = this.tags.findIndex(v => v.parent === item.parent && v.name === item.name)
+      this.tags.splice(index, 1)
+      this.provinceList.forEach((province) => {
+        if (province.name === item.name) {
+          province.checked = false
+          province.children.forEach((p) => {
+            p.checked = false
+            p.children.forEach((c) => {
+              c.checked = false
+            })
+          })
+        } else {
+          province.children.forEach((city) => {
+            if (city.name === item.name) {
+              city.checked = false
+              const cityIndeterminate = province.children.filter(v => v.indeterminate && v.name !== '全部')
+              province.indeterminate = cityIndeterminate.length > 0
+              city.children.forEach((district) => {
+                district.checked = false
+              })
+            } else {
+              city.children.forEach((district) => {
+                // 区县存在重复 同时要比较父级城市名称
+                if (district.name === item.name && city.name === item.parent) {
+                  district.checked = false
+                  const districtIndeterminate = city.children.filter(v => v.checked && v.name !== '全部')
+                  city.indeterminate = districtIndeterminate.length > 0
+                  const cityIndeterminate = province.children.filter(v => v.indeterminate && v.name !== '全部')
+                  province.indeterminate = cityIndeterminate.length > 0
+                }
+              })
+            }
+          })
+        }
+      })
+      this.getAllCheckBoxSelectedStatus()
+      this.onSelectChange()
+    },
+    // 整理城市数据列表
+    initAreaMap () {
+      const provinceList = this.getCitiesFromJSONArray()
+      const specialRegion = ['全国', '香港', '澳门', '台湾']
+      // 如果需要包含全国选项
+      if (this.isHaveAll) {
+        provinceList.unshift({
+          name: '全国',
+          checked: false,
+          disabled: false,
+          indeterminate: false,
+          children: []
+        })
+      }
+      provinceList.forEach((province) => {
+        province.children.unshift({
+          name: '全部',
+          checked: false,
+          disabled: specialRegion.includes(province.name),
+          indeterminate: false,
+          children: []
+        })
+        province.children.forEach((city) => {
+          city.children.unshift({
+            name: '全部',
+            checked: false,
+            disabled: specialRegion.includes(province.name) || city.children.length === 0,
+            indeterminate: false
+          })
+        })
+      })
+      this.provinceList = provinceList
+      // 默认打开第一个城市、区县
+      this.citiesList = provinceList[0].children
+      this.countryList = provinceList[0].children[0].children
+    },
+    /**
+     *  整理chinaMapJSON,过滤掉海外、钓鱼岛、省份删除/省|市|自治区|特别行政区|壮族|回族|维吾尔字样
+     *  返回省市县区数组格式
+     */
+    getCitiesFromJSONArray () {
+      const filterData = chinaMapJSON.filter(function (item) {
+        item.name = item.name.replace(/\s*/g, '').replace(/省|市|自治区|特别行政区|壮族|回族|维吾尔/g, '')
+        return item.name !== '海外' && item.name !== '钓鱼岛'
+      })
+      // const municipality = ['北京', '上海', '天津', '重庆']
+      const specialRegion = ['香港', '澳门', '台湾']
+      // 按拼音排序
+      const sortFilterData = filterData.sort((prev, next) => {
+        return prev?.name.localeCompare(next?.name, 'zh')
+      })
+      // 处理成标准字段
+      const standardData = sortFilterData.map(p => {
+        if (specialRegion.includes(p.name)) {
+          // 港澳台
+          return {
+            name: p.name,
+            checked: false,
+            disabled: false,
+            children: []
+          }
+        } else {
+          return {
+            name: p.name,
+            checked: false,
+            disabled: false,
+            indeterminate: false,
+            children: p.city.map(c => {
+              return {
+                name: c.name,
+                checked: false,
+                disabled: false,
+                indeterminate: false,
+                children: c.area.map(a => {
+                  return {
+                    name: a,
+                    checked: false,
+                    disabled: false
+                  }
+                })
+              }
+            })
+          }
+        }
+      })
+      return standardData
+    },
+    /**
+     * 将过滤好的地区数据转换成map格式(根据场景需要转换)
+     * 例:{北京:{北京市:[]}, 河南:{ 郑州市:[新郑市, 登封市、金水区]}}
+     */
+    getCitiesToMap (filterData = this.getCitiesFromJSONArray()) {
+      const obj = {}
+      if (filterData.length > 0) {
+        filterData.forEach((province) => {
+          const cityMap = {}
+          province.children.forEach((city) => {
+            cityMap[city.name] = city.children.map(v => v.name)
+          })
+          obj[province.name] = cityMap
+        })
+      }
+      return obj
+    },
+    // 判断全国、全部全选半选状态
+    getAllCheckBoxSelectedStatus () {
+      if (!this.isHaveAll) return
+      const provinceList = this.provinceList
+      const allProvinceCount = provinceList.filter(v => v.name !== '全国').length
+      const allSelectedProvinceCount = provinceList.filter(v => v.name !== '全国' && v.checked).length
+      const allHalfSelectedProvinceCount = provinceList.filter(v => v.name !== '全国' && v.indeterminate).length
+      provinceList.forEach(province => {
+        if (allProvinceCount === allSelectedProvinceCount) {
+          provinceList[0].checked = true
+          provinceList[0].indeterminate = false
+        } else {
+          provinceList[0].indeterminate = allSelectedProvinceCount > 0 || allHalfSelectedProvinceCount > 0
+          const allCityCount = province.children.filter(v => v.name !== '全部').length
+          const allSelectedCityCount = province.children.filter(v => v.name !== '全部' && v.checked).length
+          const allHalfSelectedCityCount = province.children.filter(v => v.name !== '全部' && v.indeterminate).length
+          province.children.forEach(city => {
+            if (allCityCount === allSelectedCityCount && allSelectedCityCount > 0) {
+              province.children[0].checked = true
+              province.children[0].indeterminate = false
+            } else {
+              province.children[0].indeterminate = allSelectedCityCount > 0 || allHalfSelectedCityCount > 0
+              const allDistrictCount = city.children.filter(v => v.name !== '全部').length
+              const allSelectedDistrictCount = city.children.filter(v => v.name !== '全部' && v.checked).length
+              const allHalfSelectedDistrictCount = city.children.filter(v => v.name !== '全部' && v.indeterminate).length
+              city.children.forEach(district => {
+                if (allDistrictCount === allSelectedDistrictCount && allSelectedDistrictCount > 0) {
+                  city.children[0].checked = true
+                  city.children[0].indeterminate = false
+                } else {
+                  city.children[0].indeterminate = allSelectedDistrictCount > 0 || allHalfSelectedDistrictCount > 0
+                }
+              })
+            }
+          })
+        }
+      })
+    },
+    // 点击省展开城市
+    onOpenCity (province, pIndex) {
+      this.cActive = 0
+      this.pActive = pIndex
+      this.countryList = []
+      this.citiesList = province.children
+      this.countryList = province.children[0].children
+    },
+    // 点击城市展开区县
+    onOpenCountry (city, cIndex) {
+      this.cActive = cIndex
+      this.countryList = city.children
+    },
+    onProvinceMouseOut (e) {
+      clearTimeout(this.pTimer)
+      this.pTimer = null
+    },
+    onProvinceMouseOver (province, pIndex) {
+      if (!this.pTimer) {
+        this.pTimer = setTimeout(() => {
+          this.onOpenCity(province, pIndex)
+        }, 150)
+      }
+    },
+    onCityMouseOut (e) {
+      clearTimeout(this.cTimer)
+      this.cTimer = null
+    },
+    onCityMouseOver (city, cIndex) {
+      if (!this.cTimer) {
+        this.cTimer = setTimeout(() => {
+          this.onOpenCountry(city, cIndex)
+        }, 150)
+      }
+    },
+    // 设置全国全选、半选状态
+    setWholeCountryCheckedStatus () {
+      const selectedProvince = this.provinceList.filter((province) => {
+        return province.checked && province.name !== '全国'
+      })
+      const allProvince = this.provinceList.filter((province) => {
+        return province.name !== '全国'
+      })
+      const selectedIndeterminate = this.provinceList.filter((province) => {
+        return province.name !== '全国' && province.indeterminate
+      })
+      if (this.isHaveAll) {
+        // 如果有全国选项 则选中的省份数量(排除全国)等于全部省份数量 则绑定全国选中
+        if (selectedProvince.length === allProvince.length) {
+          this.provinceList[0].indeterminate = false
+          this.provinceList[0].checked = true
+        } else {
+          // 如果选中全国数量不等于全部省份数量 则全国半选  选中数等于0除外
+          this.provinceList[0].checked = false
+          this.provinceList[0].indeterminate = selectedIndeterminate.length > 0 || selectedProvince.length > 0
+        }
+      }
+      this.getRegionTags()
+    },
+    // 设置省全选、半选状态
+    setProvinceCheckedStatus () {
+      // 设置当前省份下城市的全选状态(全选、半选)
+      const currentProvince = this.provinceList[this.pActive]
+      const selectedCities = currentProvince.children.filter(v => v.checked && v.name !== '全部')
+      const allCities = currentProvince.children.filter(v => v.name !== '全部')
+      const cityIndeterminate = currentProvince.children.filter(v => v.name !== '全部' && v.indeterminate)
+      if (selectedCities.length === allCities.length) {
+        this.provinceList[this.pActive].checked = true
+        this.provinceList[this.pActive].indeterminate = false
+      } else {
+        this.provinceList[this.pActive].checked = false
+        this.provinceList[this.pActive].indeterminate = selectedCities.length !== 0 || cityIndeterminate.length > 0
+      }
+      currentProvince.children[0].indeterminate = (selectedCities.length !== allCities.length && selectedCities.length !== 0) || cityIndeterminate.length > 0
+      currentProvince.children[0].checked = selectedCities.length === allCities.length
+      this.setWholeCountryCheckedStatus()
+    },
+    setCitiesCheckedStatus () {
+      // 设置当前省份下城市的全选状态(全选、半选)
+      const currentCity = this.citiesList[this.cActive]
+      const selectedCountry = currentCity.children.filter(v => v.checked && v.name !== '全部')
+      const allCountry = currentCity.children.filter(v => v.name !== '全部')
+      if (selectedCountry.length === allCountry.length) {
+        currentCity.checked = true
+        currentCity.indeterminate = false
+      } else {
+        currentCity.checked = false
+        currentCity.indeterminate = selectedCountry.length !== 0
+      }
+      // if (this.isHaveAll) {
+      currentCity.children[0].indeterminate = selectedCountry.length !== allCountry.length && selectedCountry.length !== 0
+      currentCity.children[0].checked = selectedCountry.length === allCountry.length
+      // }
+      this.setProvinceCheckedStatus()
+    },
+    /**
+     * 提取省份checkbox选择状态方法
+     * checked: checkbox点击状态
+     * province: 当前点击的城市数据
+     */
+    setProvinceChangeCommon (checked, province) {
+      // 全国选中/取消选中
+      if (province.name === '全国' || province.name === '全部') {
+        this.provinceList.forEach((first) => {
+          first.checked = checked
+          first.indeterminate = false
+          first.children.forEach((second) => {
+            second.checked = checked
+            second.indeterminate = false
+            second.children.forEach((third) => {
+              third.checked = checked
+              third.indeterminate = false
+            })
+          })
+        })
+      } else {
+        // 省份选中/取消选中
+        province.indeterminate = false
+        province.children.forEach((second) => {
+          second.checked = checked
+          second.indeterminate = false
+          second.children.forEach((third) => {
+            third.checked = checked
+            third.indeterminate = false
+          })
+        })
+      }
+      this.setWholeCountryCheckedStatus()
+    },
+    // 省份选中事件
+    onProvinceChange (checked, province, index) {
+      this.onOpenCity(province, index)
+      // 选择超出可选区域冒泡事件
+      if (this.isExceed) {
+        province.checked = !checked
+        this.onSelectExceed()
+        return
+      }
+      province.checked = checked
+      this.setProvinceChangeCommon(checked, province)
+      this.onSelectChange()
+    },
+    /**
+     * 提取城市checkbox选择状态方法
+     * checked: checkbox点击状态
+     * city: 当前点击的城市数据
+     */
+    setCitiesChangeCommon (checked, city) {
+      if (city.name === '全部') {
+        // 全部城市选中/取消选中
+        this.provinceList.forEach((first, index) => {
+          if (this.pActive === index) {
+            first.checked = checked
+            first.indeterminate = false
+            first.children.forEach((second) => {
+              second.checked = checked
+              second.indeterminate = false
+              second.children.forEach((third) => {
+                third.checked = checked
+                third.indeterminate = false
+              })
+            })
+          }
+        })
+      } else {
+        // 当前城市选中/取消选中
+        city.indeterminate = false
+        city.children.forEach((third) => {
+          third.checked = checked
+          third.indeterminate = false
+        })
+      }
+      this.setProvinceCheckedStatus()
+    },
+    // 城市选中事件
+    onCitiesChange (checked, city, cIndex) {
+      this.onOpenCountry(city, cIndex)
+      // 没有权限 冒泡事件
+      if (!this.vip) {
+        city.checked = !checked
+        this.onNoPowerLimit(city)
+        return
+      }
+      // 选择超出可选区域冒泡事件
+      if (checked) {
+        city.checked = true
+        this.setCitiesChangeCommon(checked, city)
+        // 选择超出可选区域冒泡事件
+        if (this.isExceed) {
+          city.checked = !checked
+          this.setCitiesChangeCommon(!checked, city)
+          this.onSelectExceed()
+          return
+        }
+      } else {
+        // 取消选中
+        city.checked = false
+        this.setCitiesChangeCommon(checked, city)
+      }
+      this.onSelectChange()
+    },
+    setCountryChangeCommon (checked, country) {
+      if (country.name === '全部') {
+        this.countryList.forEach((third) => {
+          third.checked = checked
+          third.indeterminate = false
+        })
+        this.setCitiesCheckedStatus()
+      } else {
+        this.setCitiesCheckedStatus()
+      }
+    },
+    // 区县选中事件
+    onCountryChange (checked, country) {
+      // 没有权限 冒泡事件
+      if (!this.vip) {
+        country.checked = !checked
+        this.onNoPowerLimit(country)
+        return
+      }
+      // 选择超出可选区域冒泡事件
+      if (checked) {
+        country.checked = true
+        this.setCountryChangeCommon(checked, country)
+        // 选择超出可选区域冒泡事件
+        if (this.isExceed) {
+          country.checked = !checked
+          this.setCountryChangeCommon(!checked, country)
+          this.onSelectExceed()
+          return
+        }
+      } else {
+        country.checked = false
+        this.setCountryChangeCommon(checked, country)
+      }
+      this.onSelectChange()
+    },
+    // 下拉框出现/隐藏时触发
+    onVisibleChange (flag) {
+      if (!flag) {
+        this.$emit('hideSelect', this.getState())
+      } else {
+        this.$emit('showSelect', this.getState())
+      }
+    },
+    onNoPowerLimit(payload) {
+      this.$emit('limit', payload)
+    },
+    onSelectExceed() {
+      this.$emit('exceed')
+    },
+    onSelectChange() {
+      const state = this.getState()
+      this.$emit('change', state)
+    },
+    // 省市县区三级结构拆分省市(省:[市])和区县(地市:[区县])
+    formatProvinceAndCities (regionMap) {
+      let area = {}
+      const district = {}
+      if (Object.keys(regionMap).length === 0) {
+        area = {}
+      } else {
+        for (const key in regionMap) {
+          if (Object.keys(regionMap[key]).length === 0) {
+            area[key] = []
+          } else {
+            const cities = regionMap[key]
+            const cityArr = []
+            for (const city in cities) {
+              cityArr.push(city)
+              area[key] = cityArr
+              if (cities[city].length > 0) {
+                district[city] = cities[city]
+              }
+            }
+          }
+        }
+      }
+      return { area, district }
+    },
+    // 省市县区map转字符串 应用场景:平铺选择结果需要
+    formatRegionToString (regionMap, mark = '、') {
+      if (!regionMap || !Object.keys(regionMap).length) return '全国'
+      const tagsArr = []
+      for (const province in regionMap) {
+        if (Object.keys(regionMap[province]).length === 0) {
+          tagsArr.push(province)
+        } else {
+          const cityObj = regionMap[province]
+          for (const cKey in cityObj) {
+            if (cityObj[cKey].length === 0) {
+              tagsArr.push(cKey)
+            } else {
+              tagsArr.push(...cityObj[cKey])
+            }
+          }
+        }
+      }
+      return tagsArr.join(mark)
+    },
+    // 获取数据,并整理成前端标准格式
+    getState () {
+      let provinceList = JSON.parse(JSON.stringify(this.provinceList))
+      const allProvinceCount = provinceList.filter(v => v.name !== '全国').length
+      const allSelectedProvinceCount = provinceList.filter(v => v.name !== '全国' && v.checked).length
+      const allCountryChecked = provinceList.some((v) => v.name === '全国' && v.checked)
+      const noSelected = provinceList.every((v) => !v.checked && !v.indeterminate)
+      let regionMap = null
+      if (noSelected) {
+        // console.log('no region selected')
+      } else {
+        if (allProvinceCount === allSelectedProvinceCount && allCountryChecked) {
+          provinceList = []
+        } else {
+          provinceList.forEach(province => {
+            const allCityCount = province.children.filter(v => v.name !== '全部').length
+            const allSelectedCityCount = province.children.filter(v => v.name !== '全部' && v.checked).length
+            if (allCityCount === allSelectedCityCount) {
+              province.children = []
+            } else {
+              province.children.forEach(city => {
+                const allDistrictCount = city.children.filter(v => v.name !== '全部').length
+                const allSelectedDistrictCount = city.children.filter(v => v.name !== '全部' && v.checked).length
+                if (allDistrictCount === allSelectedDistrictCount) {
+                  city.children = []
+                }
+              })
+            }
+          })
+        }
+        const formatData = provinceList.filter(first => {
+          return (first.checked || first.indeterminate) && first.name !== '全国'
+        })
+        formatData.forEach((second) => {
+          const secondList = second.children.filter(v => {
+            return (v.checked || v.indeterminate) && v.name !== '全部'
+          })
+          secondList.forEach(item => {
+            item.children = item.children.filter(v => {
+              return v.checked && v.name !== '全部'
+            })
+          })
+          second.children = secondList
+        })
+        regionMap = this.getCitiesToMap(formatData)
+      }
+      return regionMap
+    },
+    setState (data = {}) {
+      this.resetState()
+      if (!data) return
+      if (Object.keys(data).length === 0) {
+        // 选择的全国
+        this.provinceList.forEach((province) => {
+          province.checked = true
+          province.indeterminate = false
+          province.children.forEach((city) => {
+            city.checked = true
+            city.indeterminate = false
+            city.children.forEach((country) => {
+              country.checked = true
+              country.indeterminate = false
+            })
+          })
+        })
+      } else {
+        // 选择的省
+        const provinceList = this.provinceList
+        provinceList[0].indeterminate = true
+        provinceList.forEach((province) => {
+          for (const key in data) {
+            if (province.name === key) {
+              if (Object.keys(data[key]).length === 0) {
+                // 选择的全省(省下的全部地市)
+                province.checked = true
+                province.children.forEach((city) => {
+                  city.checked = true
+                  city.children.forEach((district) => {
+                    district.checked = true
+                  })
+                })
+              } else {
+                // 选择省下的部分地市
+                province.indeterminate = true
+                province.children.forEach((city) => {
+                  for (const cKey in data[key]) {
+                    if (city.name === cKey) {
+                      if (Object.keys(data[key][cKey]).length === 0) {
+                        // 选择的市下的全部区县
+                        city.checked = true
+                        city.children.forEach((district) => {
+                          district.checked = true
+                        })
+                      } else {
+                        // 选择的市下的部分区县
+                        city.indeterminate = true
+                        city.children.forEach((district) => {
+                          if (data[key][cKey].length === 0) {
+                            district.checked = true
+                          } else {
+                            data[key][cKey].forEach((item) => {
+                              if (item === district.name) {
+                                district.checked = true
+                              }
+                            })
+                          }
+                        })
+                      }
+                    }
+                  }
+                })
+                this.getAllCheckBoxSelectedStatus()
+              }
+            }
+          }
+        })
+      }
+      this.getRegionTags()
+    },
+    resetState () {
+      this.provinceList.forEach((province) => {
+        province.checked = false
+        province.indeterminate = false
+        province.children.forEach((city) => {
+          city.checked = false
+          city.indeterminate = false
+          city.children.forEach((country) => {
+            country.checked = false
+            country.indeterminate = false
+          })
+        })
+      })
+      this.tags = []
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.region--wrap{
+  max-width: 580px;
+  background: #fff;
+  border-radius: 5px;
+  border: 1px solid $color_main;
+  overflow: hidden;
+  .select--count{
+    padding: 8px 8px 4px;
+    font-size: 14px;
+    line-height: 22px;
+    .count-label{
+      color: #686868;
+    }
+    .count-value{
+      color: #1d1d1d;
+    }
+  }
+  .select--result{
+    display: flex;
+    flex-wrap: wrap;
+    padding: 8px 0 0px;
+    max-height: 100px;
+    overflow-y: auto;
+    &::-webkit-scrollbar{
+      width: 4px;
+    }
+  }
+  .select--container{
+    display: flex;
+    align-items: center;
+    border-top: 1px solid #ececec;
+    height: 360px;
+    overflow: hidden;
+  }
+  .province-container{
+    min-width: 140px;
+    border-right: 1px solid #ececec;
+  }
+  .city-container,
+  .country-container{
+    min-width: 220px;
+    white-space: nowrap;
+  }
+  .city-container{
+    border-right: 1px solid #ececec;
+  }
+  .module-container{
+    height: 100%;
+    display: flex;
+    flex-direction: column;
+  }
+  .module-header{
+    padding: 12px 11px;
+    color:#999999;
+    font-size: 14px;
+    line-height: 22px;
+  }
+  .module-main{
+    flex: 1;
+    overflow-y: scroll;
+  }
+  .module-item{
+    position: relative;
+    display: flex;
+    align-items: center;
+    padding: 0 8px;
+    height: 30px;
+    font-size: 14px;
+    line-height: 22px;
+    color: #1d1d1d;
+    &.active,
+    &:hover{
+      background: #ececec;
+      cursor: pointer;
+    }
+    .item-name{
+      flex: 1;
+      margin-left: 4px;
+    }
+  }
+  .icon-have-vip {
+    position: relative;
+    &::after {
+      position: absolute;
+      content: '';
+      width: 38px;
+      height: 18px;
+      right: -42px;
+      top: 0;
+      background: url('~@/assets/images/icon/vip.png') no-repeat right center;
+      background-size: contain;
+    }
+  }
+  ::v-deep{
+    .el-tag{
+      width: auto;
+      max-width: unset!important;
+      margin: 0 0 8px 8px;
+      &.el-tag--info{
+        height: 24px;
+        line-height: 22px;
+        background:#F5F6F7;
+        color: #1d1d1d;
+        border: 1px solid#ececec;
+        font-size: 14px;
+      }
+      .el-tag__close.el-icon-close{
+        background: transparent;
+        color: #aaa;
+        font-size: 15px;
+        right: -4px;
+      }
+      &:hover {
+        color: $color-text--highlight;
+        border-color: $color-text--highlight;
+        background: #fff;
+        cursor: pointer;
+        .el-tag__close {
+          color: $color-text--highlight;
+        }
+      }
+    }
+  }
+  .module-main::-webkit-scrollbar{
+    width: 4px;
+  }
+}
+</style>

+ 160 - 0
apps/bigmember_pc/src/components/filter-items/SearchRangeDropdown.vue

@@ -0,0 +1,160 @@
+<template>
+  <Layout
+    :type="type"
+    :placeholder="placeholder"
+    :trigger="trigger"
+    :value="computedVal"
+  >
+    <div slot="empty" class="search-range-container">
+      <el-checkbox-group
+        v-model="selectVal"
+        @change="checkboxChange"
+        :min="min">
+        <el-checkbox
+          v-for="oItem in options"
+          :label="oItem.key"
+          :key="oItem.key">
+          {{oItem.label ? oItem.label.replace('搜索', '') : ''}}
+          <template v-for="tip in needTipList">
+             <el-tooltip  effect="dark"  placement="right" v-if="tip.name === oItem.label">
+              <div slot="content" style="width:300px; font-size: 14px;line-height: 24px;">
+                <span>{{tip.tip}}</span>
+              </div>
+              <i class="j-icon j-icon-base icon-help-img" ></i>
+            </el-tooltip>
+          </template>
+        </el-checkbox>
+      </el-checkbox-group>
+    </div>
+
+  </Layout>
+</template>
+
+<script>
+import { Tooltip } from 'element-ui'
+import Layout from '@/components/filter-items/Layout.vue'
+import CascadeContent from '@/components/filter-items/CascadeContent.vue'
+import { biddingSearchScope } from '@/assets/js/selector.js'
+export default {
+  name: 'InfoTypeDropdown',
+  components: {
+    [Tooltip.name]: Tooltip,
+    Layout,
+    CascadeContent
+  },
+  props: {
+    type: {
+      type: String,
+      default: 'dropdown'
+    },
+    trigger: {
+      type: String,
+      default: 'hover'
+    },
+    placeholder: {
+      type: String,
+      default: '搜索范围'
+    },
+    value: {
+      type: Array,
+      default: () => []
+    },
+    min: {
+      type: Number,
+      default: 1
+    }
+  },
+  model: {
+    prop: 'value',
+    event: 'change'
+  },
+  data () {
+    return {
+      needTipList: [
+        {
+          name: '采购单位',
+          tip: '“采购单位”采购单位名称中包含输入关键词的会展示出来'
+        },
+        {
+          name: '中标企业',
+          tip: '“中标企业”中标企业名称中包含输入关键词的会展示出来'
+        },
+        {
+          name: '招标代理机构',
+          tip: '“招标代理机构”招标代理机构中包含输入关键词的会展示出来'
+        }
+      ],
+      options: biddingSearchScope,
+      selectVal: []
+    }
+  },
+  watch: {
+    value: {
+      handler(val) {
+        this.selectVal = val
+      },
+      deep: true
+    }
+  },
+  computed: {
+    computedVal () {
+      return this.selectVal.length ? `${this.placeholder}${this.selectVal.length}个` : ''
+    }
+  },
+  created() {
+    this.selectVal = this.value
+  },
+  methods: {
+    checkboxChange () {
+      this.$emit('change', this.selectVal)
+    }
+  }
+}
+</script>
+<style lang="scss" scoped>
+.search-range-container {
+  min-width: 140px;
+  padding: 8px;
+  border: 1px solid #2cb7ca;
+  background: #fff;
+  border-radius: 5px;
+  margin-top: 2px;
+
+  .el-checkbox{
+    display: flex;
+    align-items: center;
+    margin-right: 20px;
+    color: #606266;
+    font-weight: 500;
+    font-size: 14px;
+    cursor: pointer;
+    user-select: none;
+    height: 30px;
+    line-height: 30px;
+  }
+
+}
+::v-deep {
+  .el-checkbox__label{
+    display: flex;
+    align-items: center;
+  }
+  .el-checkbox__input.is-disabled+span.el-checkbox__label{
+    color: #2CB7CA;
+  }
+  .el-checkbox__input.is-disabled.is-checked .el-checkbox__inner {
+    background-color: #2cb7ca;
+    border-color: #2cb7ca;
+    &::after{
+      border-color: #fff;
+    }
+  }
+}
+.icon-help-img {
+  width: 18px;
+  height: 18px;
+  margin-left: 6px;
+  cursor: pointer;
+}
+
+</style>

+ 130 - 0
apps/bigmember_pc/src/components/filter-items/SearchScopeSelector.vue

@@ -0,0 +1,130 @@
+<template>
+  <div class="search-scope-selector">
+    <CheckboxGroupSelector
+      :value="value"
+      :options="options"
+      :keepOne="keepOne"
+      @change="onChange"
+      :beforeChange="beforeChange"
+    >
+      <template #tips="{ prop }">
+        <div v-if="prop.value === 'winner' && !prop.power && isOld">
+          <span class="old-user-free">老用户免费专享</span>
+        </div>
+        <el-popover
+          v-if="prop.value === 'winner' || prop.value === 'buyer' || prop.value === 'agency'"
+          placement="right"
+          :append-to-body="false"
+          width="333"
+          trigger="hover"
+        >
+          <div class="popover-text">
+            <span v-if="prop.value === 'buyer'"
+              ><strong>"采购单位"</strong
+              >采购单位名称中包含输入关键词的会展示出来</span
+            >
+            <span v-if="prop.value === 'winner'"
+              ><strong>"中标企业"</strong
+              >中标企业名称中包含输入关键词的会展示出来</span
+            >
+            <span v-if="prop.value === 'agency'"
+              ><strong>"招标代理机构"</strong
+              >招标代理机构中包含输入关键词的会展示出来</span
+            >
+          </div>
+          <i class="iconfont icon-help" slot="reference" :class="{gold: prop.power}"></i>
+        </el-popover>
+      </template>
+    </CheckboxGroupSelector>
+  </div>
+</template>
+
+<script>
+import { Popover } from 'element-ui'
+import CheckboxGroupSelector from '@/components/filter-items/CheckboxGroupSelector.vue'
+import { searchScopeData } from '@/assets/js/selector.js'
+export default {
+  name: 'SearchScopeSelector',
+  props: {
+    beforeChange: Function,
+    value: {
+      type: Array,
+      default: () => []
+    },
+    options: {
+      type: Array,
+      default: () => searchScopeData
+    },
+    isOld: {
+      type: Boolean,
+      default: false
+    },
+    keepOne: {
+      type: Boolean,
+      default: false
+    }
+  },
+  components: {
+    [Popover.name]: Popover,
+    CheckboxGroupSelector
+  },
+  methods: {
+    onChange(value) {
+      this.$emit('change', value)
+      this.$emit('input', value)
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.search-scope-selector {
+  display: flex;
+  .s-header {
+    margin-right: 10px;
+    min-width: 120px;
+    font-size: 14px;
+    line-height: 22px;
+    color: #686868;
+    text-align: right;
+  }
+  ::v-deep {
+    .checkbox-item {
+      &:not(:last-of-type) {
+        margin-right: 20px;
+      }
+      //&:last-of-type {
+      //  margin-right: 0;
+      //}
+
+      .checkbox-item-icon.gold {
+        border:1px solid #C98F37;
+      }
+    }
+    .popover-text {
+      strong {
+        font-weight: bold;
+        color: #1d1d1d;
+      }
+    }
+    .old-user-free {
+      display: inline-block;
+      margin-left: 6px;
+      padding: 0 8px 2px 8px;
+      border-radius: 2px;
+      background: #eaf8fa;
+      font-size: 12px;
+      line-height: 18px;
+      color: #2abed1;
+    }
+    .icon-help{
+      margin-left:6px;
+      font-size:15px;
+      color: #2ABED1;
+      &.gold {
+        color:#C98F37;
+      }
+    }
+  }
+}
+</style>

+ 101 - 0
apps/bigmember_pc/src/components/filter-items/SelectorWithBasePower.vue

@@ -0,0 +1,101 @@
+<template>
+  <div class="selector-with-base-power">
+    <BasePowerLayout
+      @clickVipMask="clickVipMask"
+      @clickBaseMask="clickBaseMask"
+      :vipMaskShow="vipMaskShow"
+      :baseMaskShow="baseMaskShow"
+      :vipModuleShow="vipModuleShow"
+    >
+      <component
+        :is="component"
+        :value="value"
+        v-bind="{
+          ...commonConf,
+          ...freeConf,
+        }"
+        @input="onChange"
+      ></component>
+      <component
+        slot="vip"
+        :value="value"
+        :is="component"
+        v-bind="{
+          ...commonConf,
+          ...vipConf,
+        }"
+      ></component>
+    </BasePowerLayout>
+  </div>
+</template>
+
+<script>
+import BasePowerLayout from '@/components/filter-items/BasePowerLayout.vue'
+
+export default {
+  name: 'SelectorWithBasePower',
+  components: {
+    BasePowerLayout
+  },
+  props: {
+    component: {
+      required: true
+    },
+    baseMaskShow: {
+      type: Boolean,
+      default: false
+    },
+    value: {
+      default: undefined
+    },
+    vipMaskShow: {
+      type: Boolean,
+      default: false
+    },
+    vipModuleShow: {
+      type: Boolean,
+      default: false
+    },
+    commonConf: {
+      type: Object,
+      default: () => {}
+    },
+    freeConf: {
+      type: Object,
+      default: () => {}
+    },
+    vipConf: {
+      type: Object,
+      default: () => {}
+    },
+  },
+  model: {
+    prop: 'value',
+    event: 'change'
+  },
+  methods: {
+    clickVipMask() {
+      this.$emit('clickVipMask')
+    },
+    clickBaseMask() {
+      this.$emit('clickBaseMask')
+    },
+    onChange(value) {
+      this.$emit('change', value)
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.search-scope-selector {
+  display: flex;
+  ::v-deep {
+    .checkbox-group-selector {
+      .s-container {
+        min-width: unset;
+      }
+    }
+  }
+}
+</style>

+ 165 - 0
apps/bigmember_pc/src/components/search-input/SearchInput.vue

@@ -0,0 +1,165 @@
+<template>
+  <div class="input-container">
+    <slot name="prefix"></slot>
+    <el-input
+      :placeholder="placeholder"
+      :value="value"
+      :clearable="clearable"
+      @focus="preSearch.focus = true"
+      @blur="preSearch.focus = false"
+      @input="input"
+      @keyup.enter.native="onSearch"
+    >
+      <template slot="prefix">
+        <div class="input-prefix-icon">
+          <i class="iconfont icon-search"></i>
+        </div>
+      </template>
+      <template slot="append">
+        <div class="search-button no-select" @click="onSearch">搜 索</div>
+      </template>
+    </el-input>
+    <slot name="suffix"></slot>
+    <div
+      class="pre-search-list"
+      style="display: none"
+      v-show="preSearchListShow"
+      @mouseout="preSearch.hover = false"
+      @mouseover="preSearch.hover = true"
+    >
+      <slot name="preSearchContent"></slot>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'SearchInput',
+  props: {
+    placeholder: {
+      type: String,
+      default: ''
+    },
+    value: {
+      type: String,
+      default: ''
+    },
+    clearable: {
+      type: Boolean,
+      default: false
+    },
+    perSearchEnabled: {
+      type: Boolean,
+      default: false
+    }
+  },
+  model: {
+    prop: 'value',
+    event: 'input'
+  },
+  data() {
+    return {
+      preSearch: {
+        hover: false,
+        focus: false
+      }
+    }
+  },
+  computed: {
+    preSearchListShow: function () {
+      return (
+        this.value.trim().length >= 2 &&
+        this.perSearchEnabled &&
+        (this.preSearch.focus || this.preSearch.hover)
+      )
+    }
+  },
+  methods: {
+    input(e) {
+      this.$emit('input', e)
+    },
+    onSearch() {
+      this.$emit('onSearch', this.value)
+    }
+  }
+}
+</script>
+<style lang="scss" scoped>
+.search-button {
+  display: flex;
+  align-items: center;
+  padding: 0 20px;
+  // height: 36px;
+  cursor: pointer;
+  font-size: 16px;
+  line-height: 24px;
+}
+.input-prefix-icon {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  > img {
+    display: block;
+  }
+}
+
+.input-container {
+  position: relative;
+  display: flex;
+  align-items: center;
+  ::v-deep {
+    .el-input-group {
+      width: 528px;
+    }
+    .el-input__prefix {
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      left: 16px;
+    }
+    .el-input__inner,
+    .el-input-group__append {
+      border-width: 2px;
+      border-color: $color_main;
+    }
+    .el-input__inner {
+      padding-left: 44px;
+      border-radius: 8px 0 0 8px;
+    }
+    .el-input-group__append {
+      padding: 0;
+      font-size: 18px;
+      color: #fff;
+      background-color: #2cb7ca;
+      border-radius: 0 8px 8px 0;
+    }
+  }
+}
+
+.pre-search-list {
+  padding: 20px 0;
+  position: absolute;
+  z-index: 6;
+  top: 52px;
+  width: 640px;
+  background: #fff;
+  box-shadow: 0 0 20px rgb(0, 0, 0, 0.1);
+  border-radius: 8px;
+  overflow: hidden;
+  .pre-search-item {
+    padding: 12px 40px;
+    width: 100%;
+    font-size: 16px;
+    line-height: 24px;
+    color: #686868;
+    box-sizing: border-box;
+    transition: all 0.3s;
+    cursor: pointer;
+    &:hover {
+      padding: 12px 30px;
+      color: #1d1d1d;
+      background-color: #ececec;
+    }
+  }
+}
+</style>

+ 45 - 3
apps/bigmember_pc/src/components/selector/InfoTypeSelector.vue

@@ -5,7 +5,11 @@
     @onConfirm="onConfirm"
     @onCancel="onCancel"
   >
-    <div slot="header" :class="{ 's-header': selectorType === 'line' }">
+    <div
+      v-if="showLabel"
+      slot="header"
+      :class="{ 's-header': selectorType === 'line' }"
+    >
       <slot name="header">选择信息类型</slot>
     </div>
     <InfoTypeSelectorContent
@@ -14,6 +18,8 @@
       :initInfoType="initInfoType"
       :beforeChange="beforeChange"
       :oneLevelSelected="oneLevelSelected"
+      :showDataType="showDataType"
+      :options="options"
       @onChange="onChange"
     />
   </selector-card>
@@ -29,6 +35,10 @@ export default {
     InfoTypeSelectorContent
   },
   props: {
+    showLabel: {
+      type: Boolean,
+      default: true
+    },
     selectorType: {
       type: String,
       default: 'card' // card/line
@@ -44,6 +54,37 @@ export default {
     oneLevelSelected: {
       type: Boolean,
       default: false
+    },
+    value: {
+      type: Array,
+      default: () => []
+    },
+    // all 是所有数据, advance 仅仅超前项目数据,base 信息类型数据(除了超前项目外数据)
+    showDataType: {
+      type: String,
+      default: 'all'
+    },
+    options: {
+      type: Array,
+      default() {
+        return []
+      }
+    }
+  },
+  model: {
+    prop: 'value',
+    event: 'change'
+  },
+  watch: {
+    showDataType (newVal, oldVal) {
+      this.initList([], newVal)
+    },
+    value (val) {
+      if(!val || val?.length === 0) {
+        this.initList([], this.showDataType)
+      } else {
+        this.setInfoTypeState(val)
+      }
     }
   },
   data() {
@@ -51,8 +92,8 @@ export default {
   },
   created() {},
   methods: {
-    initList(data = []) {
-      return this.$refs.content.initInfoTypeFn(data)
+    initList(data = [], type) {
+      return this.$refs.content.initInfoTypeFn(data, type)
     },
     setInfoTypeState(data) {
       return this.$refs.content.setInfoTypeState(data)
@@ -69,6 +110,7 @@ export default {
     },
     onChange(selected) {
       this.$emit('onChange', selected)
+      this.$emit('change', selected)
     }
   }
 }

File diff suppressed because it is too large
+ 55 - 22
apps/bigmember_pc/src/components/selector/InfoTypeSelectorContent.vue


+ 229 - 0
apps/bigmember_pc/src/components/selector/SearchTimeScopeSelector.vue

@@ -0,0 +1,229 @@
+<template>
+  <div class="search-time-scope-selector">
+    <TimeSelectorContent
+      ref="content"
+      :options="options"
+      :beforeChange="beforeChange"
+      :selectorTime="type"
+      selectorType="line"
+      :defaultSelectedKey="value"
+      :exactCanHalf="exactCanHalf"
+      :showConfirmButton="showConfirmButton"
+      @onChange="onChange"
+    />
+  </div>
+</template>
+
+<script>
+import TimeSelectorContent from '@/components/selector/TimeSelectorContent.vue'
+export default {
+  name: 'search-time-scope-selector',
+  components: {
+    TimeSelectorContent
+  },
+  model: {
+    prop: 'value',
+    event: 'input'
+  },
+  props: {
+    beforeChange: Function,
+    options: {
+      type: Array,
+      default() {
+        return []
+      }
+    },
+    splitter: {
+      type: String,
+      default: '_'
+    },
+    value: {
+      type: String,
+      default: 'thisyear'
+    },
+    // 时间配置项类型
+    type: {
+      type: String,
+      default: 'bidSearch'
+    },
+    // 自定义选项是否可以选择一半值
+    exactCanHalf: {
+      type: Boolean,
+      default: false
+    },
+    // 是否展示自定义自动按钮
+    showConfirmButton: {
+      type: Boolean,
+      default: false
+    }
+  },
+  data() {
+    return {
+      selectTime: {
+        start: 0,
+        end: 0,
+        exact: ''
+      }
+    }
+  },
+  watch: {
+    value: {
+      handler(val) {
+        this.syncState(val)
+      },
+      deep: true
+    }
+  },
+  mounted() {
+    this.syncState(this.value)
+  },
+  methods: {
+    syncState(val) {
+      const splitter = this.splitter // 默认_
+      const time = this.transformTimeBefore(val)
+      if (time.indexOf(splitter) === -1) {
+        this.selectTime = {
+          start: 0,
+          end: 0,
+          exact: time
+        }
+      } else {
+        const times = time.split(splitter)
+        this.selectTime = {
+          start: times[0] * 1000,
+          end: times[1] * 1000,
+          exact: 'exact'
+        }
+      }
+      if (this.$refs.content) {
+        this.$refs.content.setState(this.selectTime)
+      }
+    },
+    setState(data) {
+      return this.$refs.content.setState(data)
+    },
+    getState() {
+      return this.$refs.content.getState()
+    },
+    transformTimeBefore(time) {
+      // 转换成组件需要的数据类型
+      const timeMap = {
+        'lately-7': 'lately7',
+        'lately-30': 'lately30',
+        thisyear: 'sinceLastYear',
+        threeyear: 'sinceLastThreeYear',
+        fiveyear: 'sinceLastFiveYear'
+      }
+      return timeMap[time] || time
+    },
+    transformTimeAfter(time) {
+      // 转换成接口需要的数据类型
+      const timeMap = {
+        lately7: 'lately-7',
+        lately30: 'lately-30',
+        sinceLastYear: 'thisyear',
+        sinceLastThreeYear: 'threeyear',
+        sinceLastFiveYear: 'fiveyear'
+      }
+      return timeMap[time] || time
+    },
+    onChange(state) {
+      const split = this.splitter
+      if (state.exact === 'exact') {
+        this.$emit('input', `${state.start / 1000}${split}${state.end / 1000}`)
+      } else {
+        this.$emit('input', this.transformTimeAfter(state.exact))
+      }
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.search-time-scope-selector {
+  &::v-deep {
+    .fw-bold {
+      font-weight: bold;
+    }
+    // 子组件按钮公共样式
+    .j-button-item {
+      display: flex;
+      align-items: center;
+      margin: 0 5px;
+      padding: 2px 6px;
+      line-height: 20px;
+      border-radius: 4px;
+      font-size: 14px;
+      text-align: center;
+      background-color: transparent;
+      cursor: pointer;
+      &:first-child {
+        margin-left: 0;
+      }
+      &.global {
+        // s-card中的全部按钮使用
+        padding: 6px 8px;
+        height: 24px;
+        line-height: 24px;
+        font-weight: 700;
+        color: inherit;
+        border-color: rgba(0, 0, 0, 0.05);
+      }
+      &.all {
+        // s-line中的全部按钮使用
+        font-weight: 700;
+        border-color: transparent;
+      }
+      &.hover:hover {
+        color: #2abed1;
+      }
+      // 选中状态
+      &.active {
+        // 默认蓝色边框蓝色字体
+        color: #2abed1;
+        border-color: #2abed1;
+        &.bgc {
+          // 默认蓝色背景白色字体
+          color: #fff;
+          background-color: $color-text--highlight;
+        }
+        &.bgc-opacity {
+          // 默认蓝色边框蓝色字体蓝色半透明背景
+          background-color: rgba(44, 183, 202, 0.1);
+        }
+      }
+    }
+
+    [class^='el-icon-'] {
+      transition: transform 0.2s ease;
+    }
+    .rotate180 {
+      transform: rotate(180deg);
+    }
+
+    .date-time-container .date-time-item {
+      width: 130px;
+      height: 24px;
+      line-height: 24px;
+    }
+    .date-time-container .date-time-item.left::after {
+      background-color: #e0e0e0;
+    }
+    .select-group-container {
+      margin-right: 10px;
+    }
+    .date-time-container {
+      padding: 0;
+      background-color: transparent !important;
+      &.active {
+        background-color: transparent !important;
+        input,
+        .date-time-item.left::after {
+          background-color: $color_main;
+          color: #fff;
+        }
+      }
+    }
+  }
+}
+</style>

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

@@ -93,8 +93,8 @@ export default {
       .j-button-item {
         display: flex;
         align-items: center;
-        margin: 6px 5px;
-        padding: 2px 6px;
+        margin: 2px 5px;
+        padding: 1px 6px;
         line-height: 20px;
         border-radius: 4px;
         font-size: 14px;
@@ -277,6 +277,7 @@ export default {
   }
   ::v-deep {
     .j-button-item {
+      margin-top: 8px;
       border-color: transparent;
     }
     .action-button.show-more {

+ 753 - 0
apps/bigmember_pc/src/components/selector/SelectorCascader.vue

@@ -0,0 +1,753 @@
+<template>
+  <div class="select-cascader">
+    <div id="selectArea_">
+      <div class="select_common_data">
+        <div class="select_box selectArea">
+          <div class="left">
+            <p class="title_" v-text="leftTit"></p>
+            <div
+              class="box_"
+              onmousewheel="var event = window.event || arguments.callee.caller.arguments[0];if(event.preventDefault) event.preventDefault();var delta = event.wheelDelta || event.originalEvent.wheelDelta || event.originalEvent.detail;var k = delta? delta:-delta*10;this.scrollTop = this.scrollTop - k;return false;"
+            >
+              <div
+                class="item_"
+                v-for="(item, index) in leftList"
+                :key="index"
+                @click.self="leftclick(item, index)"
+                :class="{ active_: active == index }"
+              >
+                <i class="el-icon-arrow-right"></i>
+                <el-checkbox
+                  v-model="item.selected"
+                  @change="handleCheckChange($event, item, index)"
+                  :indeterminate="item.indeterminate"
+                  :disabled="item.disabled"
+                ></el-checkbox>
+                <p
+                  class="name_"
+                  v-text="item.label"
+                  @click.self="leftclick(item, index)"
+                ></p>
+                <el-tooltip class="item" effect="dark" placement="right">
+                  <div slot="content">
+                    "采购意向"是指提供未发布招标公告前1-3个<br />月,政府单位的采购意向信息,包含采购内<br />容、预算金额、预计采购时间、采购联系人及<br />联系方式等相关信息。
+                  </div>
+                  <span
+                    v-if="item.label == '采购意向'"
+                    class="j-icon j-icon-base icon-help-img desc-icon tooltip-help-btn"
+                  ></span>
+                </el-tooltip>
+              </div>
+            </div>
+          </div>
+          <div class="right">
+            <div class="head_tit">
+              <p class="title_" v-text="rightTit"></p>
+              <span
+                v-if="type == 'area' && login && !vipState"
+                class="j-icon j-icon-base icon-vip-mark-img area-icon-vip"
+              ></span>
+            </div>
+            <div
+              class="box_"
+              onmousewheel="var event = window.event || arguments.callee.caller.arguments[0];if(event.preventDefault) event.preventDefault();var delta = event.wheelDelta || event.originalEvent.wheelDelta || event.originalEvent.detail;var k = delta? delta:-delta*10;this.scrollTop = this.scrollTop - k;return false;"
+            >
+              <div
+                class="item_"
+                v-for="(item1, index2) in rightList"
+                :key="index2"
+              >
+                <el-checkbox
+                  v-model="item1.selected"
+                  @change="handleCheckChange_right($event, item1, index2)"
+                  :indeterminate="item1.indeterminate"
+                  :disabled="item1.disabled"
+                ></el-checkbox>
+                <div>
+                  <p class="name_ ellipsis-2" v-text="item1.label"></p>
+                  <!-- 排除词 -->
+                  <p
+                    class="notkey ellipsis"
+                    v-if="
+                      item1.value !== '全部' &&
+                      item1.notkey &&
+                      item1.notkey !== ''
+                    "
+                  >
+                    排除词:{{ item1.notkey }}
+                  </p>
+                  <!-- 排除词 -->
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import {
+  Icon,
+  Select,
+  Checkbox,
+  Tooltip,
+  Dropdown,
+  DropdownMenu
+} from 'element-ui'
+export default {
+  name: 'select-cascader',
+  components: {
+    [Icon.name]: Icon,
+    [Select.name]: Select,
+    [Checkbox.name]: Checkbox,
+    [Tooltip.name]: Tooltip,
+    [Dropdown.name]: Dropdown,
+    [DropdownMenu.name]: DropdownMenu
+  },
+  props: {
+    // 地区接收数据格式
+    // [{"label":"全国","disabled":false,"children":[{"label":"全部","value":"全部","parent":"全国"}],"selected":false,"indeterminate":false,"value":"全国"},{"label":"安徽","disabled":false,"children":[{"label":"全部","value":"全部","parent":"安徽"},{"label":"合肥市","disabled":false,"selected":false,"indeterminate":false,"value":"合肥市","parent":"安徽"},{"label":"宿州市","disabled":false,"selected":false,"indeterminate":false,"value":"宿州市","parent":"安徽"}],"selected":false,"indeterminate":false,"value":"安徽"}]
+    // 行业接收数据格式
+    // [{"label":"全部","value":"全部","children":[{"label":"全部","value":"全部","parent":"全部","selected":false,"indeterminate":false,"disabled":false}],"selected":false,"indeterminate":false,"disabled":false},{"value":"建筑工程","label":"建筑工程","children":[{"label":"全部","value":"全部","parent":"建筑工程","selected":false,"indeterminate":false,"disabled":false}],"selected":false,"indeterminate":false,"disabled":false}]
+    listData: {
+      type: Array,
+      default: () => {
+        return []
+      }
+    },
+    valueLabel: {
+      type: String,
+      default: ''
+    },
+    leftTit: {
+      type: String,
+      default: ''
+    },
+    rightTit: {
+      type: String,
+      default: ''
+    },
+    viplimit: {
+      type: Number,
+      default: 0
+    },
+    vip: {
+      type: Number,
+      default: 0
+    },
+    login: {
+      type: Boolean,
+      default: false
+    },
+    type: {
+      type: String,
+      default: ''
+    }
+  },
+  data() {
+    return {
+      leftList: [],
+      rightList: [],
+      nowindex: 0,
+      chooseData: {},
+      active: 0,
+      num: '',
+      originalData: [],
+      selectArrow: 'bottom'
+    }
+  },
+  watch: {
+    listData: {
+      handler(val) {
+        if (val.length) {
+          let list = val
+          list.forEach((item, index) => {
+            if (item.children.length == 1) {
+              // 只有一个全部
+              item.children[0].disabled = true
+            }
+          })
+          this.originalData = JSON.parse(JSON.stringify(list))
+          this.leftList = list
+          this.rightList = this.leftList[0].children
+          this.getState()
+        }
+      },
+      immediate: true
+    }
+  },
+  methods: {
+    initData() {
+      //还原初始数据
+      let left = JSON.parse(JSON.stringify(this.originalData))
+      this.leftList = left
+      this.rightList = this.leftList[0].children
+    },
+    processingarea(area, city) {
+      // console.log(area, city, '地区组件调用回显获取的省市')
+      if (area || city) {
+        let a = area ? area.split(',') : []
+        let c = city ? city.split(',') : []
+        // 回显省份则其下地市全选
+        if (a && a.length > 0) {
+          a.forEach((item) => {
+            this.leftList.forEach((e) => {
+              if (item == e.value) {
+                e.selected = true
+                e.initialSelect = true //增加initialSelect记录初始勾选 用于恢复非VIP勾选恢复原始按钮状态
+                e.children.forEach((ele) => {
+                  ele.selected = true
+                  ele.initialSelect = true
+                })
+              }
+            })
+          })
+        }
+
+        // 有城市则说明所在省是半选
+        if (c && c.length > 0) {
+          c.forEach((item) => {
+            this.leftList.forEach((ele) => {
+              ele.children.forEach((items) => {
+                if (items.value == item) {
+                  // 回显市在多层级数据中的
+                  ele.selected = false // 省级半选
+                  ele.indeterminate = true
+                  ele.initialSelect = false
+                  ele.children[0].selected = false
+                  ele.children[0].indeterminate = true
+                  ele.children[0].initialSelect = false
+                  items.selected = true
+                  items.initialSelect = true
+                  items.indeterminate = false
+                }
+              })
+            })
+          })
+        }
+        this.allchoosestate()
+      } else {
+        this.num = '全国'
+        this.leftList.forEach((ele) => {
+          if (ele.value == '全国' || ele.value == '全部') {
+            ele.selected = true
+            ele.initialSelect = true
+            ele.indeterminate = false
+          } else {
+            ele.selected = false
+            ele.initialSelect = false
+            ele.indeterminate = false
+          }
+          ele.children.forEach((e) => {
+            if (e.parent == '全国' || e.parent == '全部') {
+              e.selected = true
+              e.initialSelect = true
+              e.indeterminate = false
+            } else {
+              e.selected = false
+              e.initialSelect = false
+              e.indeterminate = false
+            }
+          })
+        })
+      }
+      this.getState()
+    },
+    processingindustry(data) {
+      // 行业回显
+      if (data) {
+        let list
+        if (Array.isArray(data)) {
+          list = data
+        } else {
+          list = data.toString().split(',')
+        }
+        let arr = list
+        // /_(.+)/.exec(ele)[1]
+        this.checkecho(arr, 'id') // 回显选中信息
+      } else {
+        this.reactAll() // 置为初始
+      }
+      this.getState()
+    },
+    processingbuyerclass(data) {
+      // 采购单位回显
+      if (data) {
+        let list
+        if (Array.isArray(data)) {
+          list = data
+        } else {
+          list = data.toString().split(',')
+        }
+        let arr = list
+        // /_(.+)/.exec(ele)[1]
+        this.checkecho(arr, 'value') // 回显选中信息
+      } else {
+        this.reactAll() // 置为初始
+      }
+      this.getState()
+    },
+    reactAll() {
+      this.num = '全部'
+      this.leftList.forEach((ele) => {
+        if (ele.value == '全国' || ele.value == '全部') {
+          ele.selected = true
+          ele.initialSelect = true
+          ele.indeterminate = false
+        } else {
+          ele.selected = false
+          ele.initialSelect = false
+          ele.indeterminate = false
+        }
+        ele.children.forEach((e) => {
+          if (e.parent == '全国' || e.parent == '全部') {
+            e.selected = true
+            e.initialSelect = true
+            e.indeterminate = false
+          } else {
+            e.selected = false
+            e.initialSelect = false
+            e.indeterminate = false
+          }
+        })
+      })
+    },
+    checkecho(arr, key) {
+      arr.forEach((ele) => {
+        this.leftList.forEach((e) => {
+          e.children.forEach((item) => {
+            if (ele == item[key]) {
+              item.selected = true
+            }
+          })
+        })
+      })
+      this.leftList.forEach((ele, index) => {
+        let choosed = []
+        let all = []
+        ele.children.forEach((item) => {
+          if (item.label != '全部') {
+            all.push(item.value)
+          }
+          if (item.selected && item.label != '全部') {
+            choosed.push(item.value)
+          }
+        })
+        if (choosed.length == 0) {
+          // 未选
+          ele.selected = false
+          ele.indeterminate = false
+          ele.children[0].selected = false
+          ele.children[0].indeterminate = false
+        } else if (choosed.length >= all.length) {
+          // 全选
+
+          ele.selected = true
+          ele.indeterminate = false
+          ele.children[0].selected = true
+          ele.children[0].indeterminate = false
+        } else {
+          // 半选
+
+          ele.selected = false
+          ele.indeterminate = true
+          ele.children[0].selected = false
+          ele.children[0].indeterminate = true
+        }
+      })
+      this.allchoosestate()
+    },
+    change(val) {
+      // console.log(val, 'change')
+      this.selectArrow = val ? 'top' : 'bottom'
+      this.$emit('selectchange', val)
+    },
+    leftclick(item, index) {
+      this.active = index
+      this.rightList = item.children
+      this.nowindex = index
+      // console.log(item, index)
+    },
+    handleCheckChange(e, item, index) {
+      this.active = index
+      if (e) {
+        // 选中
+        if (item.value == '全国' || item.value == '全部') {
+          // 全部选中
+          this.leftList.forEach((ele) => {
+            ele.initialSelect = true // 记录不受双向绑定影响的选中状态
+            ele.selected = true
+            ele.indeterminate = false // 取消半选
+            ele.children.forEach((ele1) => {
+              ele1.initialSelect = true
+              ele1.selected = true
+              ele1.indeterminate = false // 取消半选
+            })
+          })
+        } else {
+          // 二级全选
+          item.indeterminate = false // 取消半选
+          item.children.forEach((ele) => {
+            ele.selected = true
+            ele.initialSelect = true
+            if (ele.label == '全部') {
+              ele.indeterminate = false
+            }
+          })
+        }
+      } else {
+        if (item.value == '全国' || item.value == '全部') {
+          // 全部取消
+          this.leftList.forEach((ele) => {
+            ele.selected = false
+            ele.initialSelect = false
+            ele.children.forEach((ele1) => {
+              ele1.selected = false
+              ele1.initialSelect = false
+            })
+          })
+        } else {
+          // 二级取消
+          item.children.forEach((ele) => {
+            ele.selected = false
+            ele.initialSelect = false
+          })
+        }
+      }
+      // 控制最外部全选状态
+      this.allchoosestate()
+      this.rightList = item.children
+      this.nowindex = index
+      this.getState()
+      this.$emit('datachange', this.chooseData)
+    },
+    handleCheckChange_right(e, item, index) {
+      if (this.viplimit && !this.vip) {
+        let initialSelect = item.initialSelect
+        item.selected = initialSelect
+        this.$emit('limit', item)
+        return
+      }
+      if (e) {
+        if (item.label == '全部') {
+          this.leftList[this.nowindex].children.forEach((ele) => {
+            this.$set(ele, 'selected', true)
+            this.$set(ele, 'indeterminate', false)
+          })
+        } else {
+          item.selected = true
+          item.indeterminate = false
+        }
+      } else {
+        if (item.label == '全部') {
+          this.leftList[this.nowindex].children.forEach((ele) => {
+            this.$set(ele, 'selected', false)
+            this.$set(ele, 'indeterminate', false)
+          })
+        } else {
+          item.selected = false
+          item.indeterminate = false
+        }
+      }
+
+      let arr = this.leftList[this.nowindex].children
+      if (arr.length == 1) {
+        if (e) {
+          this.leftList[this.nowindex].indeterminate = false
+          this.leftList[this.nowindex].selected = true
+          if (item.parent == '全国' || item.parent == '全部') {
+            this.num = '全部'
+            this.leftList.forEach((ele) => {
+              ele.selected = true
+              ele.initialSelect = true
+              ele.indeterminate = false
+              ele.children.forEach((items) => {
+                items.selected = true
+                items.initialSelect = true
+                items.indeterminate = false
+              })
+            })
+          }
+        } else {
+          this.leftList[this.nowindex].indeterminate = false
+          this.leftList[this.nowindex].selected = false
+          if (item.parent == '全国' || item.parent == '全部') {
+            this.num = '全部'
+            this.leftList.forEach((ele) => {
+              ele.selected = false
+              ele.initialSelect = false
+              ele.indeterminate = false
+              ele.children.forEach((items) => {
+                items.selected = false
+                items.initialSelect = false
+                items.indeterminate = false
+              })
+            })
+          }
+        }
+      } else {
+        let select = []
+        let realarr = []
+        arr.forEach((ele) => {
+          if (ele.selected && ele.label != '全部') {
+            select.push(ele)
+          }
+          if (ele.label != '全部') {
+            realarr.push(ele)
+          }
+        })
+
+        if (realarr.length > select.length && select.length != 0) {
+          // 未全选
+          // 控制上级部分
+          this.leftList[this.nowindex].indeterminate = true
+          this.leftList[this.nowindex].selected = false
+          // 二级的‘全部’按钮
+          this.leftList[this.nowindex].children.forEach((ele) => {
+            if (ele.label == '全部') {
+              ele.indeterminate = true
+              ele.selected = false
+            }
+          })
+        } else if (select.length >= realarr.length && select.length != 0) {
+          // 全选
+          // 控制上级部分
+          this.leftList[this.nowindex].indeterminate = false
+          this.leftList[this.nowindex].selected = true
+          // 二级的‘全部’按钮
+          this.leftList[this.nowindex].children.forEach((ele) => {
+            if (ele.label == '全部') {
+              ele.indeterminate = false
+              ele.selected = true
+            }
+          })
+        } else if (select.length == 0) {
+          //都未选
+          // 控制上级部分
+          this.leftList[this.nowindex].selected = false
+          this.leftList[this.nowindex].indeterminate = false
+          // 二级的‘全部’按钮
+          this.leftList[this.nowindex].children.forEach((ele) => {
+            if (ele.label == '全部') {
+              ele.indeterminate = false
+              ele.selected = false
+            }
+          })
+        }
+      }
+      // 控制最外部全选状态
+      this.allchoosestate()
+      this.getState()
+      this.$emit('datachange', this.chooseData)
+    },
+    getState() {
+      let one = [] // 所有选中的一级选项
+      let two = [] // 所有选中的二级选项
+      let area = [] // 省市规则选中省全部则只需要省市不用再传,选中省中的部分市则传选中的市,省不再传
+      let city = []
+      let oneAndtwo = []
+      let noSecondlevel = [] //没有二级选项 的一级选中 如直辖市
+      let alltwo = [] // 全部数据的集合
+      this.leftList.forEach((ele) => {
+        if (ele.selected) {
+          one.push(ele.value)
+          area.push(ele.value)
+          if (ele.children.length == 1 || !ele.children) {
+            noSecondlevel.push(ele.value)
+          }
+        } else {
+          // 一级半选
+          if (ele.children && ele.children.length != 0) {
+            ele.children.forEach((e) => {
+              if (e.selected && e.value != '全部') {
+                city.push(e.value)
+              }
+            })
+          }
+        }
+        if (ele.children && ele.children.length != 0) {
+          ele.children.forEach((e) => {
+            if (e.selected) {
+              two.push(e.value)
+              if (e.value != '全部') {
+                oneAndtwo.push(e.parent + '_' + e.value)
+              }
+            }
+            if (e.value != '全部') {
+              alltwo.push(e.parent + '_' + e.value)
+            }
+          })
+        }
+      })
+      let one_noall = this.removeElements(one, '全部') // 去掉一级全部选项的所有选中
+      let two_noall = this.removeElements(two, '全部') // 去掉二级全部选项的所有选中
+      let arr_ = noSecondlevel.concat(two_noall) // 没有二级选项的一级选项选中和二级选项所有选中合并得出总数
+      if (
+        (this.leftList[0].value == '全部' ||
+          this.leftList[0].value == '全国') &&
+        this.leftList[0].selected
+      ) {
+        this.num = this.leftList[0].value
+      } else {
+        this.num = arr_.length
+      }
+      this.chooseData = {
+        one,
+        two,
+        one_noall,
+        two_noall,
+        area,
+        city,
+        oneAndtwo,
+        noSecondlevel,
+        alltwo
+      }
+    },
+    allchoosestate() {
+      // 控制最外部全选状态
+      let all = []
+      let choosed = []
+      this.leftList.forEach((ele) => {
+        if (ele.label != '全部' && ele.label != '全国') {
+          all.push(ele.value)
+        }
+
+        if (ele.selected && ele.label != '全部' && ele.label != '全国') {
+          choosed.push(ele.value)
+        }
+        ele.children.forEach((item) => {
+          if (item.label != '全部') {
+            all.push(item.value)
+          }
+
+          if (item.selected && item.label != '全部') {
+            choosed.push(item.value)
+          }
+        })
+      })
+      if (choosed.length == 0) {
+        this.leftList[0].selected = false
+        this.leftList[0].indeterminate = false
+      } else if (choosed.length >= all.length) {
+        this.leftList[0].selected = true
+        this.leftList[0].indeterminate = false
+      } else {
+        this.leftList[0].selected = false
+        this.leftList[0].indeterminate = true
+      }
+      if (this.leftList[0].selected) {
+        this.leftList[0].children[0].selected = true
+        this.leftList[0].children[0].indeterminate = false
+      } else {
+        this.leftList[0].children[0].selected = false
+        this.leftList[0].children[0].indeterminate = false
+      }
+      if (this.leftList[0].indeterminate) {
+        this.leftList[0].children[0].indeterminate = true
+        this.leftList[0].children[0].selected = false
+      }
+    },
+    removeElements(arr, element) {
+      return arr.filter(function (value) {
+        return value !== element
+      })
+    },
+    checkArrays(arr1, arr2) {
+      let newArr = []
+
+      for (let i = 0; i < arr2.length; i++) {
+        if (!arr1.includes(arr2[i])) {
+          newArr.push(arr2[i])
+        }
+      }
+
+      return newArr
+    }
+  }
+}
+</script>
+<style>
+.select_common_data {
+  border: 1px solid #2ABED1;
+}
+</style>
+
+<style lang="scss" scoped>
+.select-cascader {
+  display: flex;
+  align-items: center;
+  background: #fff;
+  .valueBox {
+    &.active {
+      color: #2ABED1;
+    }
+  }
+}
+.select_box {
+  display: flex;
+  justify-content: space-between;
+  min-width: 462px;
+  height: 360px;
+  box-sizing: border-box;
+  .left,
+  .right {
+    flex: 1;
+    height: 100%;
+    box-sizing: border-box;
+    /* padding-left: 10px; */
+    padding-top: 12px;
+    padding-right: 2px;
+    position: relative;
+  }
+  .title_ {
+    font-size: 14px;
+    font-weight: 400;
+    line-height: 22px;
+    color: #999999;
+    margin-left: 12px;
+  }
+  .box_ {
+    margin-top: 12px;
+    height: 312px;
+    overflow-y: auto;
+    .item_ {
+      display: flex;
+      align-items: center;
+      min-height: 30px;
+      cursor: pointer;
+      /* margin-bottom: 6px; */
+      position: relative;
+      &.active_ {
+        background-color: #ececec;
+      }
+      .el-checkbox {
+        margin: 0 4px 0 8px;
+      }
+      .el-icon-arrow-right{
+        position: absolute;
+        right: 8px;
+        font-size: 16px;
+        color: #aaaaaa;
+      }
+    }
+  }
+  .right {
+    .item_ {
+      padding: 4px 0;
+      align-items: flex-start;
+      .name_ {
+        max-width: 190px;
+        line-height: 22px;
+      }
+      .notkey {
+        max-width: 190px;
+        font-size: 12px;
+        line-height: 18px;
+        color: #686868;
+      }
+    }
+  }
+}
+</style>

+ 11 - 7
apps/bigmember_pc/src/components/selector/TagSelector.vue

@@ -1,9 +1,9 @@
 <template>
   <div class="tag-selector-component">
-    <div slot="header" class="s-header">
+    <div slot="header" class="s-header" v-if="showHeader">
       <slot name="header">开标状态:</slot>
     </div>
-    <div class="tag-container">
+    <div class="selector-content tag-container">
       <button
         v-for="(item, index) in sourceList"
         :key="index"
@@ -27,18 +27,22 @@ export default {
         return []
       }
     },
-    value: [Number, String]
+    value: [Number, String],
+    showHeader: {
+      type: Boolean,
+      default: true
+    }
   },
   data() {
     return {}
   },
   model: {
     prop: 'value',
-    event: 'onChange'
+    event: 'change'
   },
   methods: {
     onChange(selected) {
-      this.$emit('onChange', selected.value)
+      this.$emit('change', selected.value)
     }
   }
 }
@@ -58,8 +62,8 @@ export default {
   .j-button-item {
     display: flex;
     align-items: center;
-    margin: 6px 5px;
-    padding: 2px 6px;
+    margin: 2px 5px;
+    padding: 1px 6px;
     line-height: 20px;
     border-radius: 4px;
     font-size: 14px;

+ 24 - 1
apps/bigmember_pc/src/components/selector/TimeSelector.vue

@@ -13,6 +13,7 @@
       :selectorTime="selectorTime"
       :selectorType="selectorType"
       :defaultSelectedKey="defaultSelectedKey"
+      :showExact="showExact"
       @onChange="onChange"
     />
   </selector-card>
@@ -36,12 +37,33 @@ export default {
       type: String,
       default: 'default'
     },
-    defaultSelectedKey: String
+    defaultSelectedKey: String,
+    showExact: {
+      type: Boolean,
+      default: true
+    },
+    value: {
+      type: [Object, String],
+      default: () => {
+        return {}
+      }
+    }
   },
   data() {
     return {}
   },
   created() {},
+  model: {
+    prop: 'value',
+    event: 'change'
+  },
+  watch: {
+    value (val) {
+      if (val) {
+        this.setState(val)
+      }
+    }
+  },
   methods: {
     setState(data) {
       return this.$refs.content.setState(data)
@@ -58,6 +80,7 @@ export default {
     },
     onChange(state) {
       this.$emit('onChange', state)
+      this.$emit('change', state)
     }
   }
 }

+ 255 - 25
apps/bigmember_pc/src/components/selector/TimeSelectorContent.vue

@@ -21,8 +21,9 @@
       </div>
     </div>
     <div
+      v-if="showExactConf"
       class="date-time-container"
-      :class="{ active: state.exact === 'exact' }"
+      :class="{ active: showActive}"
     >
       <el-date-picker
         v-model="dateTimePickerState.start"
@@ -36,6 +37,7 @@
         :picker-options="startPickerOptions"
         :append-to-body="false"
         @change="startDatePickerChange"
+        @focus="showConfirmHandle"
       >
       </el-date-picker>
       <el-date-picker
@@ -50,8 +52,10 @@
         :picker-options="endPickerOptions"
         :append-to-body="false"
         @change="endDatePickerChange"
+        @focus="showConfirmHandle"
       >
       </el-date-picker>
+      <el-button class="confirm-btn" v-if="showConfirmButton && showConBtn" @click="confirmHandle" >确定</el-button>
     </div>
   </div>
 </template>
@@ -61,6 +65,8 @@ import { DatePicker } from 'element-ui'
 import { dateFormatter } from '@/utils/'
 import moment from 'moment'
 import 'moment/locale/zh-cn'
+import { uniqWith } from 'lodash'
+
 moment.locale('zh-cn')
 const timeSelectMap = {
   default: [
@@ -150,31 +156,154 @@ const timeSelectMap = {
       value: 'lately90',
       selected: false
     }
+  ],
+  bidSearch: [
+    {
+      name: '最近7天',
+      value: 'lately7',
+      selected: false
+    },
+    {
+      name: '最近30天',
+      value: 'lately30',
+      selected: false
+    },
+    {
+      name: '最近1年',
+      value: 'sinceLastYear',
+      selected: true
+    },
+    {
+      name: '最近3年',
+      value: 'sinceLastThreeYear',
+      selected: false
+    },
+    {
+      name: '最近5年',
+      value: 'sinceLastFiveYear',
+      selected: false
+    }
+  ],
+  supplySearch: [
+     {
+      name: '全部',
+      value: 'all',
+      selected: true
+    },
+    {
+      name: '今天',
+      value: 'today',
+      selected: false
+    },
+    {
+      name: '最近7天',
+      value: 'lately7',
+      selected: false
+    },
+    {
+      name: '最近30天',
+      value: 'lately30',
+      selected: false
+    },
+    {
+      name: '近3个月',
+      value: 'lately90',
+      selected: false
+    }
+  ],
+  expire: [
+    {
+      name: '全部',
+      value: 'all',
+      selected: true
+    },
+    {
+      name: '本月到期',
+      value: '1',
+      selected: false
+    },
+    {
+      name: '1-3个月到期',
+      value: '1-3',
+      selected: false
+    },
+    {
+      name: '3-6个月到期',
+      value: '3-6',
+      selected: false
+    },
+    {
+      name: '6-12个月到期',
+      value: '6-12',
+      selected: false
+    },
+    {
+      name: '12个月后到期',
+      value: '12',
+      selected: false
+    }
   ]
 }
+
+function getTimeSelectList(map) {
+  let arr = []
+  for (const key in map) {
+    const item = map[key]
+    if (Array.isArray(item)) {
+      arr = arr.concat(item)
+    }
+  }
+  // 数组去重
+  const uniqed = uniqWith(arr, function (a, b) {
+    return a.value === b.value
+  })
+  return uniqed
+}
+
+// const timeSelectListAll = getTimeSelectList(timeSelectMap)
+
 export default {
   name: 'time-selector-content',
   components: {
     [DatePicker.name]: DatePicker
   },
   props: {
+    beforeChange: Function,
     selectorType: {
       type: String,
       default: 'card' // card/line
     },
     selectorTime: {
       type: String,
-      default: 'default' // default/sub/more
+      default: 'default' // default/sub/more/bidSearch
+    },
+    options: {
+      type: Array,
+      default: () => []
     },
     defaultSelectedKey: {
       type: String,
       default: 'all' // all/lately30/lately90...
+    },
+    showExact: {
+      type: Boolean,
+      default: true
+    },
+    // 自定义组件可以搬选返回
+    exactCanHalf: {
+      type: Boolean,
+      default: false
+    },
+    // 是否展示确认按钮
+    showConfirmButton: {
+      type: Boolean,
+      default: false
     }
   },
   data() {
     return {
       debug: false,
-      timeSelectList: timeSelectMap[this.selectorTime],
+      timeSelectList: [],
       dateTimePickerConf: {
         type: 'date',
         editable: false,
@@ -203,19 +332,55 @@ export default {
             return time.getTime() < +new Date(start)
           }
         }
-      }
+      },
+      showConBtn: false
     }
   },
   computed: {
+    showExactConf() {
+      // 如果传了options,则默认不展示exact
+      if (this.options.length) {
+        return this.options.includes('exact')
+      } else {
+        return this.showExact
+      }
+    },
     state() {
       return this.getState()
+    },
+    showActive () {
+      let result = false
+      if(this.showConfirmButton) {
+        result = this.state.exact === 'exact' && (this.dateTimePickerState.start || this.dateTimePickerState.end) && !this.showConBtn
+      } else {
+        let result = this.state.exact === 'exact'
+      }
+      return result
     }
   },
   created() {
+    this.timeSelectList = this.calcTimeSelectList()
     this.calcLastTime()
   },
   methods: {
     dateFormatter,
+    calcTimeSelectList() {
+      let timeSelectList = []
+      const propTimeSelectList = timeSelectMap[this.selectorTime]
+      if (this.options.length) {
+        timeSelectList = this.options
+          .map((item) => {
+            const conf = propTimeSelectList.find((t) => {
+              return t.value === item
+            })
+            return conf
+          })
+          .filter((v) => !!v)
+      } else {
+        timeSelectList = propTimeSelectList
+      }
+      return timeSelectList
+    },
     calcLastTime() {
       if (this.selectorTime === 'more') {
         const renameList = [
@@ -256,6 +421,8 @@ export default {
           case 'lately180':
           case 'thisYear':
           case 'sinceLastYear':
+          case 'sinceLastThreeYear':
+          case 'sinceLastFiveYear':
           case 'sinceYearBeforeLast':
           case 'lastYear': {
             this.setTimeSelectListState(data.exact)
@@ -263,12 +430,12 @@ export default {
             break
           }
           case 'exact': {
-            if (!data.start || !data.end) break
-            if (data.start < data.end) {
-              this.timeSelectList.forEach((v) => (v.selected = false))
-              this.dateTimePickerState.start = new Date(data.start)
-              this.dateTimePickerState.end = new Date(data.end)
+            if(!this.exactCanHalf && !this.showConfirmButton) {
+              if (!data.start || !data.end) break
             }
+            this.timeSelectList.forEach((v) => (v.selected = false))
+            this.dateTimePickerState.start = data.start ? new Date(data.start) : null
+            this.dateTimePickerState.end =data.end ?  new Date(data.end) : null
             break
           }
           default: {
@@ -305,6 +472,10 @@ export default {
           timeState.end =
             this.dateTimePickerState.end.getTime() + (durations.day1 - 1000)
         }
+        // 什么都未选择的情况
+        if (!timeState.start && !timeState.end) {
+          timeState.exact = ''
+        }
       }
       return timeState
     },
@@ -395,6 +566,20 @@ export default {
           t.start = +new Date(`${lastYear}`)
           break
         }
+        case 'sinceLastThreeYear': {
+          // 去年至今
+          const year = new Date(t.end).getFullYear()
+          const lastYear = year - 3
+          t.start = +new Date(`${lastYear}`)
+          break
+        }
+        case 'sinceLastFiveYear': {
+          // 去年至今
+          const year = new Date(t.end).getFullYear()
+          const lastYear = year - 5
+          t.start = +new Date(`${lastYear}`)
+          break
+        }
         case 'sinceYearBeforeLast': {
           // 前年至今
           const year = new Date(t.end).getFullYear()
@@ -410,6 +595,17 @@ export default {
       return t
     },
     clickTimeButton(item) {
+      const beforeChange = this.beforeChange
+      if (beforeChange) {
+        const pass = beforeChange(item)
+        if (pass) {
+          this.changeSelectItem(item)
+        }
+      } else {
+        this.changeSelectItem(item)
+      }
+    },
+    changeSelectItem(item) {
       if (item.selected) return
       this.timeSelectList.forEach((v) => (v.selected = false))
       item.selected = true
@@ -418,27 +614,48 @@ export default {
     },
     startDatePickerChange(start) {
       const { end } = this.dateTimePickerState
-      if (start && end) {
-        // start和end都有值
-        this.setTimeSelectListState()
-        this.onChange()
-      } else if (!start && !end) {
-        // start和end都没值
-        this.setTimeSelectListState(this.defaultSelectedKey)
-        this.onChange()
+      if(!this.exactCanHalf && !this.showConfirmButton) {
+        if (start && end) {
+          // start和end都有值
+          this.setTimeSelectListState()
+          this.onChange()
+        } else if (!start && !end) {
+          // start和end都没值
+          this.setTimeSelectListState(this.defaultSelectedKey)
+          this.onChange()
+        }
+      }
+      if(this.showConfirmButton) {
+        this.showConBtn = true
       }
     },
     endDatePickerChange(end) {
       const { start } = this.dateTimePickerState
-      if (start && end) {
-        // start和end都有值
-        this.setTimeSelectListState()
-        this.onChange()
-      } else if (!start && !end) {
-        // start和end都没值
-        this.setTimeSelectListState(this.defaultSelectedKey)
-        this.onChange()
+      if(!this.exactCanHalf && !this.showConfirmButton) {
+        if (start && end) {
+          // start和end都有值
+          this.setTimeSelectListState()
+          this.onChange()
+        } else if (!start && !end) {
+          // start和end都没值
+          this.setTimeSelectListState(this.defaultSelectedKey)
+          this.onChange()
+        }
+      }
+      if(this.showConfirmButton) {
+        this.showConBtn = true
       }
+    },
+    // 展示确定按钮
+    showConfirmHandle () {
+      this.showConBtn = true
+    },
+
+    // 确定操作
+    confirmHandle () {
+      this.setTimeSelectListState()
+      this.onChange()
+      this.showConBtn = false
     }
   }
 }
@@ -505,4 +722,17 @@ export default {
     }
   }
 }
+.confirm-btn{
+  width:48px;
+  height:24px;
+  border-radius: 2px;
+  font-size: 14px;
+  text-align: center;
+  color: #fff;
+  background: #2ABED1;
+  padding: 0;
+  line-height: 22px;
+  margin-left: 8px;
+  border: none;
+}
 </style>

+ 303 - 0
apps/bigmember_pc/src/components/selector/basicDropdown.vue

@@ -0,0 +1,303 @@
+<template>
+  <div class="select-container" :class="{ 'custom-select-auto-container': !isDefault }">
+    <el-select ref="selectSelector" v-model="activeLabel" :popper-append-to-body="false"
+      @visible-change="onVisibleChange" @mouseenter.native="onSelectMouseEnter" @mouseleave.native="onSelectMouseLeave"
+      :placeholder="placeholder" popper-class="select-custom">
+      <template v-if="!isDefault" slot="prefix">
+        <div class="select-prefix">
+          <span class="select-prefix-value highlight-text" v-if="activeLabel">{{ activeLabel }}</span>
+          <span class="select-prefix-value" v-else>{{ placeholder }}</span>
+          <i class="iconfont icon-xiala" :class="{ 'is-reverse': isFocus }"></i>
+        </div>
+      </template>
+      <template slot="empty">
+        <div class="select-container">
+          <div class="select-item" :class="{ 'active': item.value === activeValue }"
+            v-for="item in options" :key="item.label" :label="item.label" :value="item.value"
+            @click="handleChange(item)">
+            <span>{{ item.label }}</span>
+          </div>
+        </div>
+      </template>
+    </el-select>
+  </div>
+</template>
+<script>
+import { Select, Option, Popover, Button, Input } from 'element-ui'
+export default {
+  name: 'timeDropdown',
+  components: {
+    [Select.name]: Select,
+    [Option.name]: Option,
+    [Popover.name]: Popover,
+    [Button.name]: Button,
+    [Input.name]: Input
+  },
+  props: {
+    /**
+     * select 回显的输入框是否使用默认样式
+     * true:自带输入框
+     * false: 回显框为根据回显内容自适应宽度,下拉箭头为实心三角箭头
+    */
+    isDefault: {
+      type: Boolean,
+      default: false
+    },
+    /**
+     * 下拉框展开方式
+     * hover: 鼠标悬浮 click: 点击
+    */
+    trigger: {
+      type: String,
+      default: 'click'
+    },
+    placeholder: {
+      type: String,
+      default: '请选择'
+    },
+    /**
+     * 自定义popover展开方式
+     * hover: 鼠标悬浮 click: 点击
+    */
+    value: {
+      type: String,
+      default: ''
+    },
+    // 下拉数据
+    selectData: {
+      type: Array,
+      default: () => {
+        return [
+          {
+            label: '全部',
+            value: ''
+          }
+        ]
+      }
+    }
+  },
+  // events => change
+  data() {
+    return {
+      isFocus: false,
+      options: this.selectData,
+      activeValue: this.value,
+      isCustom: false, // 当前是否是自定义选项
+      time: {
+        start: '',
+        end: ''
+      },
+      showPopover: false,
+      timer: null
+    }
+  },
+  computed: {
+    activeLabel() {
+      const val = this.activeValue
+      if (val) {
+        if (this.options.find(v => v.value === val)) {
+          let label = this.options.find(v => v.value === val).label
+          return label || ''
+        } else {
+          return ''
+        }
+      } else {
+        return ''
+      }
+    }
+  },
+  watch: {
+    value(val) {
+      if (val) {
+        this.setState(val)
+      }
+    }
+  },
+  methods: {
+    onVisibleChange(flag) {
+      this.isFocus = flag
+      if (flag) {
+        this.setState(this.activeValue)
+      }
+    },
+    onSelectMouseEnter(e) {
+      if (this.trigger !== 'hover') return
+      if (!this.timer) {
+        this.timer = setTimeout(() => {
+          this.$refs.selectSelector.visible = true
+        }, 100)
+      }
+    },
+    onSelectMouseLeave(e) {
+      if (this.trigger !== 'hover') return
+      clearTimeout(this.timer)
+      this.timer = null
+      setTimeout(() => {
+        this.$refs.selectSelector.blur()
+      }, 300)
+    },
+    handleChange(item) {
+      this.activeValue = item.value
+      this.$emit('input', this.getState())
+      this.$emit('change', this.getState())
+      this.$refs.selectSelector.toggleMenu()
+    },
+    getState() {
+      return this.activeValue
+    },
+    getAllstate() {
+      return {
+        label: this.activeLabel,
+        value: this.activeValue
+      }
+    },
+    setState(data) {
+      this.activeValue = data
+      this.$emit('input', this.getState())
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.select-container {
+  ::v-deep {
+    .popper__arrow {
+      display: none;
+    }
+
+    .el-input__inner {
+      color: $color_main;
+    }
+
+    .el-select-dropdown {
+      border-radius: 5px;
+      margin-top: 0 !important;
+      border: 0;
+      background: transparent !important;
+    }
+  }
+
+  /* 需要自定义select输入框的样式 */
+  &.custom-select-auto-container {
+    .select-prefix {
+      display: flex;
+      align-items: center;
+      width: 100%;
+      padding: 0 0 0 10px;
+      height: 28px;
+      line-height: 28px;
+      background: #fff;
+      color: #1d1d1d;
+      text-overflow: ellipsis;
+      white-space: nowrap;
+      overflow: hidden;
+      text-align: left;
+      cursor: pointer;
+
+      .select-prefix-value {
+        display: inline-block;
+        margin-right: 2px;
+        flex: 1;
+        text-overflow: ellipsis;
+        overflow: hidden;
+        white-space: nowrap;
+      }
+
+      .icon-xiala {
+        display: inline-block;
+        font-size: 16px;
+        flex-shrink: 0;
+        transform: rotate(0deg);
+        transition: transform .5s;
+
+        &.is-reverse {
+          transform: rotate(180deg);
+        }
+      }
+    }
+
+    ::v-deep {
+      .el-select {
+        height: 24px;
+        text-align: start;
+        min-width: 50px;
+      }
+
+      .select-custom {
+        // top: 16px !important;
+        border-color: $color_main;
+      }
+
+      .el-input {
+        width: auto;
+      }
+
+      .el-input__prefix {
+        display: inline-block;
+        position: relative;
+        box-sizing: border-box;
+        color: #606266;
+        font-size: inherit;
+        height: 24px;
+        line-height: normal;
+        transition: border-color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
+        left: 0;
+      }
+
+      .el-input__inner {
+        position: absolute;
+        padding: 0;
+        height: 24px;
+        line-height: 24px;
+      }
+
+      .el-input__suffix {
+        display: none;
+      }
+    }
+  }
+
+  /* 下拉框自定义样式 */
+  .select-container {
+    min-width: 140px;
+    padding: 8px 0;
+    border: 1px solid $color_main;
+    background: #fff;
+    border-radius: 5px;
+    margin-top: 2px;
+
+    .select-item {
+      padding: 4px 16px;
+      font-size: 14px;
+      line-height: 22px;
+      color: #1d1d1d;
+      text-align: left;
+      cursor: pointer;
+
+      &:hover {
+        background: #ECECEC;
+      }
+
+      &.active {
+        color: $color_main;
+      }
+
+      span {
+        display: inline-block;
+        width: 100%;
+      }
+    }
+
+    .custom-label {
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+
+      .el-icon-arrow-right {
+        margin-right: -8px;
+      }
+    }
+  }
+}
+</style>

+ 504 - 0
apps/bigmember_pc/src/components/selector/timeDropdown.vue

@@ -0,0 +1,504 @@
+<template>
+  <div class="select-container" :class="{ 'custom-select-auto-container': !isDefault }">
+    <el-select ref="selectSelector" v-model="activeLabel" :popper-append-to-body="false"
+      @visible-change="onVisibleChange" @mouseenter.native="onSelectMouseEnter" @mouseleave.native="onSelectMouseLeave"
+      :placeholder="placeholder" popper-class="select-custom">
+      <template v-if="!isDefault" slot="prefix">
+        <div class="select-prefix">
+          <span class="select-prefix-value highlight-text" v-if="activeLabel">{{ activeLabel }}</span>
+          <span class="select-prefix-value" v-else>{{ placeholder }}</span>
+          <i class="iconfont icon-xiala" :class="{ 'is-reverse': isFocus }"></i>
+        </div>
+      </template>
+      <template slot="empty">
+        <div class="time-container">
+          <div class="time-item" :class="{ 'active': item.value === activeValue || (item.disabled && isCustom) }"
+            v-for="item in options" :key="item.label" :label="item.label" :value="item.value"
+            @click="handleChange(item)">
+            <el-popover v-if="item.disabled" class="custom-popover" :append-to-body="false" placement="right-end"
+              :trigger="popoverTrigger" :offset="12" v-model="showPopover" @show="popShow" @hide="popHide"
+              ref="customPopover">
+              <div class="custom-time">
+                <el-date-picker ref="timePick" class="time-pick" :append-to-body="false" v-model="p_time"
+                  type="daterange" range-separator="-" start-placeholder="开始日期" end-placeholder="结束日期"
+                  value-format="timestamp" align="center" @change="dateChange">
+                </el-date-picker>
+              </div>
+              <div slot="reference" class="custom-label">
+                <span>{{ item.label }}</span>
+                <i class="el-icon-arrow-right"></i>
+              </div>
+            </el-popover>
+            <span v-else>{{ item.label }}</span>
+          </div>
+        </div>
+      </template>
+    </el-select>
+  </div>
+</template>
+<script>
+import { Select, Option, Popover, Button, Input, DatePicker } from 'element-ui'
+import { dateFormatter } from '@/utils/'
+export default {
+  name: 'timeDropdown',
+  components: {
+    [Select.name]: Select,
+    [Option.name]: Option,
+    [Popover.name]: Popover,
+    [Button.name]: Button,
+    [Input.name]: Input,
+    [DatePicker.name]: DatePicker
+  },
+  props: {
+    /**
+     * select 回显的输入框是否使用默认样式
+     * true:自带输入框
+     * false: 回显框为根据回显内容自适应宽度,下拉箭头为实心三角箭头
+    */
+    isDefault: {
+      type: Boolean,
+      default: false
+    },
+    /**
+     * 下拉框展开方式
+     * hover: 鼠标悬浮 click: 点击
+    */
+    trigger: {
+      type: String,
+      default: 'click'
+    },
+    placeholder: {
+      type: String,
+      default: '成立时间'
+    },
+    /**
+     * 自定义popover展开方式
+     * hover: 鼠标悬浮 click: 点击
+    */
+    popoverTrigger: {
+      type: String,
+      default: 'hover'
+    },
+    value: {
+      type: String,
+      default: ''
+    },
+    // 下拉数据
+    selectData: {
+      type: Array,
+      default: () => {
+        return [
+          {
+            label: '全部',
+            value: ''
+          },
+          {
+            label: '近1年内',
+            value: '-1y'
+          },
+          {
+            label: '1-3年',
+            value: '1y-3y'
+          },
+          {
+            label: '3-5年',
+            value: '3y-5y'
+          },
+          {
+            label: '5-10年',
+            value: '5y-10y'
+          },
+          {
+            label: '10年以上',
+            value: '10y-'
+          },
+          {
+            value: '0',
+            label: '自定义',
+            disabled: true
+          }
+        ]
+      }
+    }
+  },
+  // events => change
+  // 自定义时间传入参数格式为 开始时间戳-结束时间戳(输出同理,时间戳为毫秒级)
+  data() {
+    return {
+      isFocus: false,
+      options: this.selectData,
+      activeValue: this.value,
+      isCustom: false, // 当前是否是自定义选项
+      p_time: '',
+      time: {
+        start: '',
+        end: ''
+      },
+      showPopover: false,
+      timer: null
+    }
+  },
+  computed: {
+    activeLabel() {
+      const time = this.activeValue
+      if (time) {
+        if (this.options.find(v => v.value === time)) {
+          let label = this.options.find(v => v.value === time).label
+          if (label === '自定义') {
+            let start = time.split('-')[0]
+            let end = time.split('-')[1]
+            return dateFormatter(Number(start), 'yyyy-MM-dd') + '-' + dateFormatter(Number(end), 'yyyy-MM-dd')
+          } else {
+            return label || ''
+          }
+        } else {
+          let start = time.split('-')[0]
+          let end = time.split('-')[1]
+          return dateFormatter(Number(start), 'yyyy-MM-dd') + '-' + dateFormatter(Number(end), 'yyyy-MM-dd')
+        }
+      } else {
+        return ''
+      }
+    }
+  },
+  watch: {
+    isFocus() {
+      this.$nextTick(() => {
+        if (this.showPopover) {
+          setTimeout(() => {
+            // popover在下拉框展示时需要重新计算位置,通过先将popover弹框透明度将为0等位置计算完成后再恢复
+            this.$refs.customPopover[0].updatePopper()
+            const $popover = this.$root.$el.querySelector('.custom-popover > .el-popover')
+            $popover.style.opacity = '1'
+            if (this.$refs.timePick) {
+              this.$refs.timePick[0].focus()
+            }
+          }, 300)
+        }
+      })
+    },
+    value(val) {
+      if (val) {
+        this.setState(val)
+      }
+    }
+  },
+  methods: {
+    popShow() {
+      if (this.$refs.timePick) {
+        this.$refs.timePick[0].focus()
+      }
+    },
+    popHide() {
+      // if (this.$refs.timePick) {
+      //   this.$refs.timePick[0].blur()
+      // }
+    },
+    onVisibleChange(flag) {
+      this.isFocus = flag
+      if (flag) {
+        this.setState(this.activeValue)
+      }
+    },
+    onSelectMouseEnter(e) {
+      if (this.trigger !== 'hover') return
+      if (!this.timer) {
+        this.timer = setTimeout(() => {
+          this.$refs.selectSelector.visible = true
+        }, 100)
+      }
+    },
+    onSelectMouseLeave(e) {
+      if (this.trigger !== 'hover') return
+      clearTimeout(this.timer)
+      this.timer = null
+      setTimeout(() => {
+        this.$refs.selectSelector.blur()
+      }, 300)
+    },
+    dateChange(val) {
+      if (!val || val.length === 0) return
+      let start = val[0]
+      let end = val[1]
+      this.activeValue = `${start}-${end}`
+      this.time.start = start
+      this.time.end = end
+      this.options.forEach(item => {
+        if (item.label === '自定义') {
+          item.value = `${start}-${end}`
+        }
+      })
+      this.isCustom = true
+      this.$refs.selectSelector.toggleMenu()
+      this.$refs.customPopover[0].doClose()
+      this.$emit('change', this.getState())
+    },
+    handleChange(item) {
+      if (item.label !== '自定义') {
+        this.activeValue = item.value
+        this.isCustom = false
+        this.p_time = ''
+        this.time.start = ''
+        this.time.end = ''
+        this.$refs.selectSelector.toggleMenu()
+        this.$refs.customPopover[0].doClose()
+        this.$emit('change', this.getState())
+      } else {
+        this.isCustom = true
+      }
+    },
+    getState() {
+      return this.activeValue
+    },
+    getAllstate() {
+      return {
+        label: this.activeLabel,
+        value: this.activeValue,
+        isCustom: this.isCustom
+      }
+    },
+    setState(data) {
+      this.isCustom = false
+      if (data) {
+        const valueArr = this.options.filter(v => !v.disabled).map(t => t.value)
+        if (valueArr.includes(data)) {
+          this.activeValue = data
+        } else {
+          const timeArr = data.split('-')
+          const start = Number(timeArr[0])
+          const end = Number(timeArr[1])
+          this.isCustom = true
+          this.time.start = start
+          this.time.end = end
+          this.activeValue = data
+          this.p_time = [start, end]
+          this.showPopover = true
+          this.$nextTick(() => {
+            const $popover = this.$root.$el.querySelector('.custom-popover > .el-popover')
+            $popover.style.opacity = '0'
+          })
+        }
+      } else {
+        this.activeValue = data
+      }
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.select-container {
+  .el-date-editor.time-pick {
+    color: transparent !important;
+    border-color: transparent !important;
+    background-color: transparent !important;
+  }
+
+  .time-pick {
+    ::v-deep {
+
+      .el-range-input,
+      .el-input__icon,
+      .el-range-separator {
+        display: none;
+      }
+
+      .el-picker-panel.el-date-range-picker.el-popper {
+        // position: relative !important;
+        border: 1px solid $color_main;
+        top: -48px !important;
+      }
+    }
+
+  }
+
+  ::v-deep {
+    .popper__arrow {
+      display: none;
+    }
+
+    .el-input__inner {
+      color: $color_main;
+    }
+
+    .el-select-dropdown {
+      border-radius: 5px;
+      margin-top: 0 !important;
+      border: 0;
+      background: transparent !important;
+    }
+  }
+
+  /* 需要自定义select输入框的样式 */
+  &.custom-select-auto-container {
+    .select-prefix {
+      display: flex;
+      align-items: center;
+      width: 100%;
+      padding: 0 0 0 10px;
+      height: 28px;
+      line-height: 28px;
+      background: #fff;
+      color: #1d1d1d;
+      text-overflow: ellipsis;
+      white-space: nowrap;
+      overflow: hidden;
+      text-align: left;
+      cursor: pointer;
+
+      .select-prefix-value {
+        display: inline-block;
+        margin-right: 2px;
+        flex: 1;
+        text-overflow: ellipsis;
+        overflow: hidden;
+        white-space: nowrap;
+      }
+
+      .icon-xiala {
+        display: inline-block;
+        font-size: 16px;
+        flex-shrink: 0;
+        transform: rotate(0deg);
+        transition: transform .5s;
+
+        &.is-reverse {
+          transform: rotate(180deg);
+        }
+      }
+    }
+
+    ::v-deep {
+      .el-select {
+        height: 24px;
+        text-align: start;
+        min-width: 50px;
+      }
+
+      .select-custom {
+        // top: 16px !important;
+        border-color: $color_main;
+      }
+
+      .el-input {
+        width: auto;
+      }
+
+      .el-input__prefix {
+        display: inline-block;
+        position: relative;
+        box-sizing: border-box;
+        color: #606266;
+        font-size: inherit;
+        height: 24px;
+        line-height: normal;
+        transition: border-color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
+        left: 0;
+      }
+
+      .el-input__inner {
+        position: absolute;
+        padding: 0;
+        height: 24px;
+        line-height: 24px;
+      }
+
+      .el-input__suffix {
+        display: none;
+      }
+    }
+  }
+
+  /* 下拉框自定义样式 */
+  .time-container {
+    min-width: 140px;
+    padding: 8px 0;
+    border: 1px solid $color_main;
+    background: #fff;
+    border-radius: 5px;
+    margin-top: 2px;
+
+    .time-item {
+      padding: 4px 16px;
+      font-size: 14px;
+      line-height: 22px;
+      color: #1d1d1d;
+      text-align: left;
+      cursor: pointer;
+
+      &:hover {
+        background: #ECECEC;
+      }
+
+      &.active {
+        color: $color_main;
+      }
+
+      span {
+        display: inline-block;
+        width: 100%;
+      }
+    }
+
+    .custom-label {
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+
+      .el-icon-arrow-right {
+        margin-right: -8px;
+      }
+    }
+
+    .custom-popover {
+      .custom-time {
+        padding: 20px;
+        margin-left: 4px;
+        // border: 1px solid $color_main;
+        // background: #fff;
+        border-radius: 4px;
+        position: relative;
+
+        &-item {
+          display: flex;
+          align-items: center;
+          margin-bottom: 12px;
+        }
+
+        &-button {
+          display: flex;
+          justify-content: flex-end;
+
+          .el-button {
+            width: 60px;
+            height: 28px;
+            padding: 0;
+          }
+        }
+      }
+
+      ::v-deep {
+        .el-popover {
+          margin-left: 16px;
+          border-color: $color_main;
+          padding: 0;
+          border: 0;
+          background: transparent;
+          box-shadow: 0px 0px 0px transparent;
+        }
+
+        .time-input {
+          width: 88px;
+          height: 24px;
+          margin: 0 4px;
+
+          .el-input__inner {
+            height: 100%;
+            padding: 0 8px;
+          }
+        }
+      }
+    }
+  }
+}
+</style>

+ 1 - 0
apps/bigmember_pc/src/components/subscribe-manager/KeyConfig.vue

@@ -199,6 +199,7 @@ export default {
       height: 18px;
       margin: 0 7px 0 8px;
       cursor: pointer;
+      background:none;
     }
     .add-classify {
       width: 110px;

+ 0 - 2
apps/bigmember_pc/src/components/subscribe-manager/powerPerson.vue

@@ -163,7 +163,6 @@ export default {
     this.$watch(
       'searchVal',
       this.debounce((newValue, oldValue) => {
-        console.log(newValue, oldValue)
         const searchList = []
         if (this.personSpareList.length !== 0) {
           this.personSpareList.forEach((v, i) => {
@@ -396,7 +395,6 @@ export default {
     delChooseUser(i) {
       this.selectedList.splice(i, 1)
       this.allChecked = false
-      console.log(this.selectedList.length)
       if (this.selectedList.length === 0) {
         this.quan = false
       }

+ 1 - 1
apps/bigmember_pc/src/components/toast/index.js

@@ -5,7 +5,7 @@ const ToastConstructor = vue.extend(toastComponent)
 let ToastHistory = {}
 
 // 定义弹出组件的函数 接收2个参数, 要显示的文本 和 显示时间
-function showToast(text, duration = 2000) {
+export function showToast(text, duration = 2000) {
   if (ToastHistory.el) {
     ToastHistory.destory()
   }

+ 2 - 0
apps/bigmember_pc/src/main.js

@@ -17,6 +17,8 @@ import ModalHelper from '@/utils/modelHlper'
 import Toast from './components/toast/index'
 import '@/utils/'
 import '@/utils/common'
+import '@/utils/directive'
+import '@/utils/prototype'
 import MetaInfo from 'vue-meta-info'
 import JyIcon from '@jianyu/icon' // 需要单独引入icon/index.css
 

+ 45 - 5
apps/bigmember_pc/src/router/modules/order.js

@@ -93,7 +93,22 @@ export default [
         components: commonOrder({
           info: () => import('@/views/order/components/big-member/info.vue'),
           desc: () => import('@/views/order/components/big-member/desc.vue')
-        })
+        }),
+        children: [
+          {
+            path: 'big-member',
+            alias: '/big/pc/page/buy_commit',
+            name: 'buy-big-member',
+            meta: {
+              title: '购买大会员',
+              productId: 104
+            },
+            components: {
+              'buy-tip': () =>
+                import('@/views/order/components/big-member/buy-tip.vue')
+            }
+          }
+        ]
       },
       {
         path: 'course',
@@ -158,7 +173,20 @@ export default [
           desc: () => import('@/views/vipsubscribe/components/Contrast.vue'),
           adsense: () =>
             import('@/views/order/components/vipsubscribe/adsense.vue')
-        })
+        }),
+        children: [
+          {
+            path: '',
+            meta: {
+              title: '超级订阅',
+              productId: 101
+            },
+            components: {
+              'buy-tip': () =>
+                import('@/views/order/components/vipsubscribe/buy-tip.vue')
+            }
+          }
+        ]
       },
       {
         path: 'data-pack',
@@ -192,9 +220,21 @@ export default [
         components: commonOrder({
           title: () => import('@/views/order/components/doc-member/title.vue'),
           info: () => import('@/views/order/components/doc-member/info.vue'),
-          adsense: () =>
-            import('@/views/order/components/doc-member/adsense.vue')
-        })
+          adsense: () => import('@/views/order/components/doc-member/adsense.vue')
+        }),
+        children: [
+          {
+            path: '',
+            meta: {
+              title: '剑鱼文库会员',
+              productId: 118
+            },
+            components: {
+              'buy-tip': () =>
+                import('@/views/order/components/doc-member/buy-tip.vue')
+            }
+          }
+        ]
       }
     ]
   }

+ 42 - 0
apps/bigmember_pc/src/router/modules/search.js

@@ -0,0 +1,42 @@
+// 搜索
+export default [
+  // 标讯搜索
+  {
+    path: '/search/bidding',
+    alias: ['/jylab/supsearch/index.html', '/jylab/bi/index.html', '/jylab/medical/index.html'],
+    name: 'bidding-search',
+    component: () => import('@/views/search/bidding/index.vue')
+  },
+  {
+    path: '/search/bidding-test',
+    alias: ['/jylab/supsearch/index.html'],
+    name: 'bidding-search-test',
+    component: () => import('@/views/search/bidding/test.vue')
+  },
+  //  企业搜索
+  {
+    path: '/search/ent',
+    alias: ['/jylab/entSearch/index.html'],
+    name: 'ent-search',
+    component: () => import('@/views/search/ent/index.vue')
+  },
+  // 采购单位搜索
+  {
+    path: '/search/purchase',
+    alias: ['/jylab/purSearch/index.html'],
+    name: 'purchase-search',
+    component: () => import('@/views/search/purchase/index.vue')
+  },
+  // 供应搜索
+  {
+    path: '/search/supply',
+    name: 'supply-search',
+    component: () => import('@/views/search/supply/index.vue')
+  },
+  // 拟在建搜索
+  {
+    path: '/search/nzj',
+    name: 'nzj-search',
+    component: () => import('@/views/search/nzj/index.vue')
+  }
+]

+ 7 - 1
apps/bigmember_pc/src/router/router-interceptors.js

@@ -6,6 +6,7 @@ import { getEntNicheAuth } from '@/api/modules'
 // 权限控制白名单-路由path
 const powerCheckPathWhiteRegList = [
   /free_*/,
+  /^\/search\//,
   /\/big\/page\/index/,
   /medical/,
   /set-/,
@@ -43,7 +44,12 @@ const powerCheckWhiteList = [
   'portrayal_loading',
   'article_detail',
   'recommen-list',
-  'business_detail'
+  'business_detail',
+  'bidding-search',
+  'ent-search',
+  'purchase-search',
+  'supply-search',
+  'nzj-search'
 ]
 
 const regListCheck = function (regList, path) {

+ 2 - 0
apps/bigmember_pc/src/router/router.js

@@ -7,6 +7,7 @@ import medicalFields from './medical-field'
 import myProperty from './my-property'
 import order from './modules/order'
 import analyse from './modules/analyse'
+import search from './modules/search'
 
 if (import.meta.env.NODE_ENV !== 'production') {
   Vue.use(VueRouter)
@@ -25,6 +26,7 @@ const router = new VueRouter({
     ...myProperty,
     ...order,
     ...analyse,
+    ...search,
     {
       path: '/404',
       name: '404',

+ 3 - 2
apps/bigmember_pc/src/router/routers.js

@@ -1,7 +1,8 @@
 export default [
   {
     path: '/',
-    redirect: '/workspace/dashboard'
+    // redirect: '/workspace/dashboard'
+    redirect: '/search/bidding'
   },
   {
     path: '/desktop',
@@ -34,7 +35,7 @@ export default [
   {
     path: '/big_subscribe',
     name: 'big_subscribe',
-    component: () => import('@/views/SubPush.vue')
+    component: () => import('@/views/subscribe/SubPush.vue')
   },
   // 潜在客户/对手
   {

+ 15 - 1
apps/bigmember_pc/src/store/user.js

@@ -22,6 +22,14 @@ const vtMap = {
   bigmember: 'm'
 }
 
+const UserVTypeMap = {
+  v: 'vType',
+  m: 'mType',
+  s: 'eType',
+  f: 'fType',
+  q: 'eType' // 企业订阅-也需调商机管理订阅信息接口,定义q为了区分企业订阅和个人订阅(商机管理)所传参数
+}
+
 export default {
   namespaced: true,
   state: () => ({
@@ -406,6 +414,10 @@ export default {
         return vtMap.free
       }
     },
+    // 获取后端所需的用户 type,用于接口
+    userType(state, getters) {
+      return UserVTypeMap[getters.vt] || 'fType'
+    },
     // 大会员子账号
     isSubCount: (state) => state.info.isSubCount,
     // 大会员权限
@@ -486,6 +498,8 @@ export default {
     isDocMember: (_, getters) => {
       const { docStatus } = getters.docMemberInfo
       return docStatus > 0
-    }
+    },
+    // 是否登录
+    loginFlag: (state) => state.loginFlag,
   }
 }

+ 23 - 0
apps/bigmember_pc/src/utils/brace/index.js

@@ -1,6 +1,29 @@
 import { tryCallHooks } from '@jianyu/easy-inject-qiankun'
 import Vue from 'vue'
 
+// 工作桌面内打开外部链接
+export function openOuterLink(link, openNewWindow = false) {
+  if (!openNewWindow) {
+    tryCallHooks(
+      {
+        fn: () => {
+          Vue.prototype.$BRACE.methods.open({
+            route: {
+              link
+            }
+          })
+        },
+        spareFn: () => {
+          location.href = link
+        }
+      }
+    )
+  } else {
+    window.open(link)
+  }
+}
+
+
 // TODO 临时处理内部跳转,部分页面需打开 in-web 剑鱼环境下,部分需要打开 in-app 工作台环境下
 export function openSelfLink(route, mode = false) {
   if (mode) {

+ 10 - 0
apps/bigmember_pc/src/utils/common.js

@@ -9,10 +9,20 @@ Vue.prototype.$checkLogin = function () {
       $('#bidLogin').modal('show')
     }
   } catch (error) {
+    location.href = '/notin/page?close_goBack=1'
     console.log(error)
   }
 }
 
+Vue.prototype.$showLoginDialog = function () {
+  try {
+    openLoginDig && openLoginDig(...arguments)
+  } catch (error) {
+    console.log(error)
+    location.href = '/notin/page?close_goBack=1'
+  }
+}
+
 Vue.prototype.contactCustomer = function (vm) {
   tryCallHooks({
     fn: () => {

+ 1 - 0
apps/bigmember_pc/src/utils/directive/index.js

@@ -0,0 +1 @@
+import './modules/visited'

+ 52 - 0
apps/bigmember_pc/src/utils/directive/modules/visited.js

@@ -0,0 +1,52 @@
+import Vue from 'vue'
+
+/**
+ * 检查是否已访问,为其新增 css class
+ * @param el
+ * @param vNode
+ */
+function checkVisited(el, vNode) {
+  if (Vue.prototype.$visited.check(vNode.data.visited)) {
+    vNode.context.$nextTick(() => {
+      el.classList.add(vNode.data.visited.css)
+    })
+  }
+}
+
+/**
+ * 合并参数到 vNode.data
+ * @param vNode
+ * @param params
+ */
+function mergeParams(vNode, params) {
+  const tranObj =
+    typeof params?.value === 'object' ? params?.value : { id: params?.value }
+  vNode.data.visited = Object.assign(
+    {
+      type: params?.arg,
+      css: 'visited',
+      stopClick: params?.modifiers?.stop || false
+    },
+    tranObj
+  )
+}
+
+Vue.directive('visited', {
+  update(el, binding, vNode) {
+    mergeParams(vNode, binding)
+    checkVisited(el, vNode)
+  },
+  inserted: function (el, binding, vNode) {
+    mergeParams(vNode, binding)
+    checkVisited(el, vNode)
+    // 是否阻止点击自动缓存
+    if (!vNode.data.visited.stopClick) {
+      function onClick(e) {
+        Vue.prototype.$visited.push(vNode.data.visited)
+        checkVisited(el, vNode)
+      }
+
+      el.addEventListener('click', onClick, false)
+    }
+  }
+})

+ 116 - 0
apps/bigmember_pc/src/utils/format/date.js

@@ -1,4 +1,6 @@
 // 计算倒计时
+import dayjs from 'dayjs'
+
 export function getCountDown(eTime, sTime) {
   let diffTime = eTime - sTime
   let day = Math.floor(diffTime / 86400000)
@@ -10,3 +12,117 @@ export function getCountDown(eTime, sTime) {
   sec = sec < 10 && sec > 0 ? '0' + sec : sec
   return (day > 0 ? day + '天' : '') + hour + '小时' + min + '分' + sec + '秒'
 }
+
+
+/**
+ * 根据 exact 返回格式化后的时间,支持单位 ms\s
+ * @param exact
+ * @param unit
+ * @returns {{start: number, end: number}}
+ */
+export function calcNotExactTime(exact = 'lately7', unit = 'ms') {
+  const t = {
+    start: 0,
+    end: dayjs().valueOf()
+  }
+  const durations = {
+    hour1: 60 * 60 * 1000,
+    day1: 60 * 60 * 1000 * 24 * 1,
+    day7: 60 * 60 * 1000 * 24 * 7,
+    day30: 60 * 60 * 1000 * 24 * 30
+  }
+  switch (exact) {
+    case 'today': {
+      t.start = dayjs().startOf('day').valueOf() // 当天0点时间戳
+      t.end = dayjs().endOf('day').valueOf() // 当天23:59:59时间戳
+      break
+    }
+    case 'yesterday': {
+      t.start = dayjs().startOf('day').valueOf() - durations.day1
+      t.end = dayjs().endOf('day').valueOf() - durations.day1
+      break
+    }
+    case 'lately3': {
+      // day操作减去3天,再取0点和23点
+      t.start = dayjs().subtract(3, 'day').startOf('day').valueOf()
+      t.end = dayjs().endOf('day').valueOf()
+      break
+    }
+    case 'lately-7':
+    case 'lately7': {
+      t.start = dayjs().subtract(7, 'day').startOf('day').valueOf()
+      t.end = dayjs().endOf('day').valueOf()
+      break
+    }
+    case 'lately-30':
+    case 'lately30': {
+      t.start = dayjs().subtract(30, 'day').startOf('day').valueOf()
+      t.end = dayjs().endOf('day').valueOf()
+      break
+    }
+    case 'lately90': {
+      // 近90天
+      t.start = dayjs().subtract(90, 'day').startOf('day').valueOf()
+      t.end = dayjs().endOf('day').valueOf()
+      break
+    }
+    case 'lately180': {
+      // 180天
+      t.start = dayjs().subtract(180, 'day').startOf('day').valueOf()
+      t.end = dayjs().endOf('day').valueOf()
+      break
+    }
+    case 'thisYear': {
+      // 今年全年
+      t.start = dayjs().startOf('year').valueOf()
+      t.end = dayjs().endOf('year').valueOf()
+      break
+    }
+    case 'lastYear': {
+      // 去年全年
+      t.start = dayjs().subtract(1, 'year').startOf('year').valueOf()
+      t.end = dayjs().subtract(1, 'year').endOf('year').valueOf()
+      break
+    }
+    case 'sinceLastYear': {
+      // 去年至今
+      t.start = dayjs().subtract(1, 'year').startOf('year').valueOf()
+      t.end = dayjs().valueOf()
+      break
+    }
+    case 'sinceYearBeforeLast': {
+      // 前年至今
+      t.start = dayjs().subtract(2, 'year').startOf('year').valueOf()
+      t.end = dayjs().valueOf()
+      break
+    }
+    // 最近1年
+    case 'thisyear': {
+      t.start = dayjs().subtract(1, 'year').valueOf()
+      t.end = dayjs().valueOf()
+      break
+    }
+    // 最近3年
+    case 'threeyear': {
+      t.start = dayjs().subtract(3, 'year').valueOf()
+      t.end = dayjs().valueOf()
+      break
+    }
+    // 最近5年
+    case 'fiveyear': {
+      t.start = dayjs().subtract(5, 'year').valueOf()
+      t.end = dayjs().valueOf()
+      break
+    }
+    default: {
+      t.start = 0
+      t.end = 0
+      break
+    }
+  }
+  if (unit === 's') {
+    t.start = dayjs(t.start).unix()
+    t.end = dayjs(t.end).unix()
+  }
+  return t
+}

+ 26 - 0
apps/bigmember_pc/src/utils/format/info-type-transform.js

@@ -131,4 +131,30 @@ export class InfoTypeTransform {
     }
     return map
   }
+  /**
+   * 输入参数
+   * {
+   *   招标公告: ['询价'],
+   *   招标预告: ['预告', '预审', '预审结果', '论证意见', '需求公示'],
+   * }
+   * 输出格式:一级分类为空数组则只显示一级分类,二级分类长度不同长度只显示二级分类
+   * ['询价', '招标预告']
+   */
+
+  static formatMoreMapToList (map) {
+    let resultArr = []
+    if(!map) return resultArr
+    if(typeof map === 'object') {
+      for (const key in infoTypeMapExp) {
+        if(map[key] && Array.isArray(map[key])) {
+          if(map[key].length === infoTypeMapExp[key].length) {
+            resultArr.push(key)
+          } else {
+            resultArr = resultArr.concat(map[key])
+          }
+        }
+      }
+    }
+    return resultArr
+  }
 }

+ 1058 - 0
apps/bigmember_pc/src/utils/format/search-bid-filter.js

@@ -0,0 +1,1058 @@
+import dayjs from 'dayjs'
+import { InfoTypeTransform } from './info-type-transform'
+import { calcNotExactTime } from './date'
+import {
+  biddingSearchScope,
+  biddingSearchConcat,
+  wordsModeList,
+  searchModeList,
+  biddingSearchTime,
+  buyerclassListMapExp,
+  industryListMapExp
+} from '@/assets/js/selector'
+import { dateFormatter } from '@jy/util'
+
+
+/**
+ * 接口中的数据转前端标准数据
+ */
+export class FilterHistoryAjaxModel2ViewModel {
+  static formatAll(map) {
+    // wordsMode
+    const { wordsMode, wordsModeText } = this.formatWordsMode(map.wordsMode)
+    // 精准匹配/模糊匹配
+    const { searchMode, searchModeText } = this.formatSearchMode(map.searchMode)
+    // 搜索范围整理
+    const { scope, scopeText } = this.formatScope(map.selectType)
+    // 行业整理
+    const { industry, industryText } = this.formatIndustry(map.industry)
+    // 地区整理
+    const { regionMap, regionMapText} = this.formatRegion(map.regionMap)
+    // 金额筛选整理
+    const {price, priceText} = this.formatPriceText(map.minprice, map.maxprice)
+    // 时间筛选整理
+    const { publishTime, publishTimeText } = this.formatTime(map.publishtime)
+    // 信息类型
+    const { infoType, infoTypeText } = this.formatInfoType(map.subtype)
+    // 采购单位
+    const { buyerClass, buyerClassText } = this.formatBuyerClass(map.buyerclass)
+    // 联系方式
+    const { basicData: buyerTel, basicDataText: buyerTelText } = this.formatContact(map.buyertel)
+    const { basicData: winnerTel, basicDataText: winnerTelText } = this.formatContact(map.winnertel)
+    // 附件
+    const fileExistsText = this.formatAttach(map.fileExists)
+
+    const formatted = {
+      keywords: map.searchvalue,
+      additionalWords: map.additionalWords,
+      wordsMode,
+      wordsModeText,
+      searchMode,
+      searchModeText,
+      searchGroup: map.searchGroup !== undefined ? String(map.searchGroup) : '0',
+      scope,
+      scopeText,
+      industry,
+      industryText,
+      regionMap,
+      regionMapText,
+      price,
+      priceText,
+      dateTime: publishTime, // 标讯搜索恢复数据可能要用到?
+      publishTime,
+      publishTimeText,
+      infoType,
+      infoTypeText,
+      buyerClass,
+      buyerClassText,
+      buyerTel,
+      buyerTelText,
+      winnerTel,
+      winnerTelText,
+      notkey: map.notkey ? map.notkey.replace(/,/g, ",") : '',
+      buyer: map.buyer ? map.buyer.replace(/,/g, ",") : '',
+      winner: map.winner ? map.winner.replace(/,/g, ",") : '',
+      agency: map.agency ? map.agency.replace(/,/g, ",") : '',
+      fileExists: map.fileExists,
+      fileExistsText: fileExistsText
+    }
+    // 删去undefined/null的项
+    for (const key in formatted) {
+      if (formatted[key] === undefined || formatted[key] === null) {
+        delete formatted[key]
+      }
+    }
+    return formatted
+  }
+
+  static mapToList(map) {
+    let list = []
+    if (!map) return list
+    for (const key in map) {
+      if (Array.isArray(map[key])) {
+        list = list.concat(map[key])
+      }
+    }
+    return list
+  }
+
+  /**
+   * 关键词匹配方式wordsModeList整理
+   * (输出包含文字描述和选择器使用的数据结构)
+   * @param String m '0'/'1'
+   * @returns Object
+   */
+  static formatWordsMode(m) {
+    const result = {
+      wordsMode: undefined,
+      wordsModeText: undefined
+    }
+    if (!m) {
+      m = '0'
+    }
+    m = String(m)
+    const target = wordsModeList.find((item) => m === item.key)
+    if (target) {
+      result.wordsMode = [target.key]
+      result.wordsModeText = target.label
+    }
+    return result
+  }
+
+  /**
+   * 搜索方式整理 精准匹配/模糊匹配
+   * @param String m '0'/'1'
+   * @returns Object
+   */
+  static formatSearchMode(m) {
+    const result = {
+      searchMode: undefined,
+      searchModeText: undefined
+    }
+    // m为空默认为0
+    if (!m) {
+      m = '0'
+    }
+    m = String(m)
+    const target = searchModeList.find((item) => m === item.key)
+    if (target) {
+      result.searchMode = [target.key]
+      result.searchModeText = target.label
+    }
+    return result
+  }
+
+  /**
+   * 搜索范围整理
+   * @param String val
+   * @returns Object
+   *
+   * 参数val示例:'content,title,ppa'
+   */
+  static formatScope(val) {
+    const result = {
+      scope: undefined,
+      scopeText: undefined
+    }
+    if (!val) return result
+    const map = {}
+    biddingSearchScope.forEach((item) => {
+      map[item.key] = item.label
+    })
+    // 将字符转换为中文
+    const selectKeyArr = val.split(',')
+    const selectKeyTextArr = selectKeyArr
+      .map((key) => {
+        return map[key]
+      })
+      .filter((key) => !!key)
+
+    result.scope = selectKeyArr
+    result.scopeText = selectKeyTextArr ? selectKeyTextArr.toString().replace(/,/g, ",") : ''
+    return result
+  }
+
+  /**
+   * 行业整理
+   * @param String val
+   * @returns Object
+   *
+   * 参数val示例:'机械设备_工程机械,机械设备_车辆,机械设备_其他机械设备,行政办公_通用办公设备,行政办公_办公家具'
+   */
+  static formatIndustry(val) {
+    const result = {
+      industry: undefined,
+      industryText: undefined
+    }
+    if (!val || val === '全部') return result
+
+    const industry = {}
+    let industryText = []
+
+    // 整理行业
+    val.split(',').forEach((v) => {
+      const vSplit = v.split('_')
+      const industryChildren = industry[vSplit[0]]
+      if (Array.isArray(industryChildren)) {
+        industryChildren.push(vSplit[1])
+      } else {
+        industry[vSplit[0]] = [vSplit[1]]
+      }
+    })
+    if (Object.keys(industry).length) {
+      result.industry = industry
+    }
+
+    // 整理行业text
+    // 统计完整行业数量
+    const calcChildrenCount = {}
+    for (const key in industryListMapExp) {
+      calcChildrenCount[key] = industryListMapExp[key].length
+    }
+    // 如果行业数据为全部,则只显示一级行业,否则需要显示二级行业
+    for (const key in industry) {
+      if (industry[key].length === calcChildrenCount[key]) {
+        industryText.push(key)
+      } else {
+        industryText = [...industryText, ...industry[key]]
+      }
+    }
+    if (industryText.length) {
+      result.industryText = industryText ? industryText.toString().replace(/,/g, ",") : ''
+    }
+    return result
+  }
+
+  /**
+   * 三级地区整理
+   * @params Object regionMap
+   * {
+   *    北京: {
+   *      朝阳区: [],
+   *      东城区: []
+   *    },
+   *    河南: {
+   *      南阳市: [],
+   *      郑州: ['金水区'],
+   *      洛阳市: ['栾川县']
+   *    },
+   *    澳门: {}
+   * }
+   * @returns Object
+   *
+   */
+  static formatRegion (region) {
+    if (!region || Object.keys(region).length === 0) return '全国'
+    const arr = []
+    for (let povince in region) {
+      if (Object.keys(region[povince]).length === 0) {
+        arr.push(povince)
+      } else {
+        for (let city in region[povince]) {
+          if (region[povince][city].length === 0) {
+            arr.push(city)
+          } else {
+            arr.push(...region[povince][city])
+          }
+        }
+      }
+    }
+    return {
+      regionMap: region,
+      regionMapText: arr.toString().replace(/,/g, ",")
+    }
+  }
+
+  /**
+   * 价格区间处理
+   * @param start
+   * @param end
+   * @param unit
+   * @returns {string}
+   */
+  static formatPriceText(start, end, unit = '万元') {
+    let priceText = ''
+    let price = ''
+    if (start && end) {
+      priceText = `${start}-${end}${unit}`
+      price = start + '-' + end
+    } else if (start && !end) {
+      price = start
+      priceText = `${start}${unit}以上`
+    } else if (!start && end) {
+      price = end
+      priceText = `${end}${unit}以下`
+    }
+    return {
+      price,
+      priceText,
+    }
+  }
+
+  /**
+   * 时间整理
+   * @param String val
+   * @returns Object
+   *
+   * 参数val示例:'1653321600_1654012800'
+   * 参数val示例:'fiveyear'
+   */
+  static formatTime(val, spl= '_') {
+    const result = {
+      publishTime: undefined,
+      publishTimeText: undefined
+    }
+    if (!val) return result
+    const map = {}
+    biddingSearchTime.forEach((item) => {
+      map[item.key] = item.label
+    })
+    const t = {
+      start: 0,
+      end: 0,
+      exact: 'exact'
+    }
+    // 如果是精确时间
+    if (val.indexOf(spl) > -1 && val.indexOf('lately') === -1) {
+      const split = val.split(spl)
+      const start = split[0].toString().length > 10 ? Number(split[0]) :  split[0] * 1000
+      const end = split[1].toString().length > 10 ? Number(split[1]) : split[1] * 1000
+      const textArr = []
+      if (start && !isNaN(start)) {
+        t.start = start
+        textArr[0] = dateFormatter(start, 'yyyy/MM/dd')
+      }
+      if (end && !isNaN(end)) {
+        t.end = end
+        textArr[1] = dateFormatter(end, 'yyyy/MM/dd')
+      }
+      let publishTime = ''
+      let publishTimeText = ''
+      if (textArr[0] && textArr[1]) {
+        publishTimeText = textArr.join('-')
+        publishTime = val
+      } else if (textArr[0] && !textArr[1]) {
+        publishTimeText = `${textArr[0]}以后`
+        publishTime = split[0] + '_' + '0'
+      } else if (!textArr[0] && textArr[1]) {
+        publishTimeText = `${textArr[1]}以前`
+        publishTime = '0' + '_' + split[1]
+      }
+      // 计算text
+      result.publishTimeText = publishTimeText
+      result.publishTime = publishTime
+    } else {
+      t.exact = val
+      result.publishTimeText = map[val] || ''
+      result.publishTime = val
+    }
+    return result
+  }
+
+  /**
+   * 信息类型整理
+   * @param String val
+   * @returns Object
+   *
+   * 参数val示例:'采购意向,中标,成交,废标,流标'
+   */
+  static formatInfoType(val) {
+    const result = {
+      infoType: undefined,
+      infoTypeText: undefined
+    }
+    if (!val || val === '全部') return result
+    const arr = val.split(',').map((v) => {
+      // 把<拟建>替换成<拟建项目>
+      if (v === '拟建') {
+        return '拟建项目'
+      } else {
+        return v
+      }
+    })
+    const obj = InfoTypeTransform.formatListToMap(arr)
+    result.infoType = InfoTypeTransform.mapToList(obj)
+
+    const map = InfoTypeTransform.listToMap(result.infoType)
+    result.infoTypeText = InfoTypeTransform.formatMapToList(map)
+    return {
+      infoType: result.infoType, // 恢复组件的数组内容
+      infoTypeText: result.infoTypeText ? result.infoTypeText.toString().replace(/,/g, ",") : ''// 文字
+    }
+  }
+  /**
+   * 采购单位类型整理
+   * @param String val
+   * @returns Object
+   *
+   * 参数val示例:'住建,传媒,建筑业,能源化工,批发零售,信息技术,运输物流,制造业,住宿餐饮'
+   */
+  static formatBuyerClass(val) {
+    const result = {
+      buyerClass: undefined,
+      buyerClassText: undefined
+    }
+    if (!val || val === '全部') return result
+    result.buyerClass = val.split(',')
+
+    // 逻辑和行业类似,如果二级全选,则展示一级分类文字,否则展示二级分类文字
+    // 1. 把选中的整理成原始数据结构
+    const keyList = []
+    const keyListMap = {}
+    for (const key in buyerclassListMapExp) {
+      keyList.push(key)
+      if (val.indexOf(key) !== -1) {
+        // 存在
+        if (Array.isArray(keyListMap[key])) {
+          keyListMap[key] = []
+        }
+      }
+      if (Array.isArray(buyerclassListMapExp[key])) {
+        // 循环二级子项
+        buyerclassListMapExp[key].forEach((item) => {
+          if (val.indexOf(item) !== -1) {
+            // 存在
+            if (Array.isArray(keyListMap[key])) {
+              keyListMap[key].push(item)
+            } else {
+              keyListMap[key] = [item]
+            }
+          }
+        })
+      }
+    }
+    // 判断是否某一项全选了
+    let buyerClassText = []
+    for (const key in keyListMap) {
+      if (keyListMap[key].length === buyerclassListMapExp[key].length) {
+        buyerClassText.push(key)
+      } else {
+        buyerClassText = [...buyerClassText, ...keyListMap[key]]
+      }
+    }
+    if (buyerClassText.length) {
+      result.buyerClassText = buyerClassText ? buyerClassText.toString().replace(/,/g, ",") : ''
+    }
+    return {
+      buyerClass: result.buyerClass,
+      buyerClassText: result.buyerClassText,
+      keyListMap: keyListMap
+    }
+  }
+
+  /**
+   * 处理联系方式
+   * @param val
+   * @returns {{basicDataText: undefined, basicData: undefined}}
+   */
+  static formatContact(val = '') {
+    const result = {
+      basicData: undefined,
+      basicDataText: undefined
+    }
+    const map = {}
+    biddingSearchConcat.forEach((item) => {
+      map[item.key] = item.label
+    })
+
+    if (val) {
+      result.basicData = val
+      result.basicDataText = map[val]
+    }
+    return result
+  }
+
+  /**
+   * 处理-附件
+   * @param val
+   * @returns {string}
+   */
+  static formatAttach (val) {
+    if (val == 1 || val == '1') {
+      return '有附件'
+    } else if (val == -1 || val == '-1') {
+      return '无附件'
+    } else {
+      return '全部'
+    }
+  }
+}
+
+
+/**
+ * 保存筛选条件
+ * 将组件初始化filter,格式成保存筛选条件接口需要数据格式
+ * 前端标准数据转接口中的数据
+ */
+export class FilterHistoryViewModel2AjaxModel {
+  static formatAll(map) {
+    // 搜索范围整理
+    const selectType = this.formatScope(map.selectType)
+    // 行业整理
+    const industry = this.formatIndustry(map.industry)
+    // 地区整理
+    // const { area, city } = this.formatAreaCity(map.area)
+    // 金额筛选整理
+    const { minPrice, maxPrice } = this.formatPrice(map.price)
+    // 时间筛选整理
+    const publishTime = map.publishTime?.includes('_') ? this.formatExactTime(map.publishTime, '_', '_') : this.formatTime(map.publishTime)
+    // 信息类型
+    let subtype = this.formatInfoType(map.subtype)
+    if (subtype) {
+      subtype = subtype.replace('拟建项目', '拟建')
+    }
+    // 采购单位类型
+    const buyerClass = this.formatBuyerClass(map.buyerclass)
+    // 包含关系、多个关键词
+    const { additionalWords, wordsMode } = this.formatSelectMoreKey(map.selectMoreKey, map.additionalWords, map.wordsMode)
+    // tab值
+    const searchGroup = this.formatSearchGroup(map.searchGroup)
+    const formatted = {
+      searchvalue: map.input,
+      selectType,
+      industry,
+      minprice: minPrice,
+      maxprice: maxPrice,
+      publishtime: publishTime, // 发布时间
+      subtype,   // 信息类型
+      buyerclass: buyerClass, // 采购单位类型
+      buyertel: map.buyertel,  // 采购单位联系方式
+      winnertel: map.winnertel,  // 中标企业联系方式
+      notkey: map.notkey ? map.notkey.join(',') : '', // 排除词
+      buyer: map.buyer ? map.buyer.join(',') : '',  // 采购单位
+      winner: map.winner ? map.winner.join(',') : '', // 中标企业
+      agency: map.agency ? map.agency.join(',') : '', // 招标代理机构
+      fileExists: map.fileExists, // 附件
+      regionMap: map.regionMap, // 地区
+      searchGroup: searchGroup, // 搜索分组:默认0:全部;1:招标采购公告;2:超前项目
+      searchMode: Number(map.searchMode),  // 搜索模式:0:精准搜索;1:模糊搜索
+      wordsMode: wordsMode, // 搜索关键词模式;默认0:包含所有,1:包含任意
+      additionalWords: additionalWords, //关键词:附加关键词(副:五组,每组最多15个字符)
+      dateTime:  map.publishTime, // 标讯搜索恢复数据可能要用到?
+    }
+
+    // 删去undefined/null的项
+    for (const key in formatted) {
+      if (formatted[key] === undefined || formatted[key] === null) {
+        delete formatted[key]
+      }
+    }
+
+    return formatted
+  }
+
+  /**
+   * 搜索范围整理
+   * @param String val
+   * @returns Object
+   *
+   * 参数val示例:[content, title ,ppa]
+   */
+  static formatScope(val = [], split = ',') {
+    if (!Array.isArray(val)) return ''
+    return val.join(split)
+  }
+
+  /**
+   * 行业整理
+   * @param String val
+   * @returns Object
+   *
+   * 参数val示例:
+   * {
+   *    机械设备: ['工程机械', '车辆', '其他机械设备', '办公家具']
+   * }
+   */
+  static formatIndustry(val = {}, split = ',') {
+    let industry = ''
+    if (!val || Object.keys(val).length === 0) return industry
+
+    const industryArr = []
+
+    for (const key in val) {
+      if (Array.isArray(val[key])) {
+        val[key].forEach((item) => {
+          industryArr.push(`${key}_${item}`)
+        })
+      }
+    }
+
+    industry = industryArr.join(split)
+
+    return industry
+  }
+
+  /**
+   * 三级地级市地区整理
+   * @param String area
+   * @param String city
+   * @returns Object
+   *
+   * 参数area示例
+   * {
+   *    北京: {
+   *      朝阳区: [],
+   *      东城区: []
+   *    },
+   *    河南: {
+   *      南阳市: [],
+   *      郑州: ['金水区'],
+   *      洛阳市: ['栾川县']
+   *    },
+   *    澳门: {}
+   * }
+   */
+  static formatAreaCity(p = {}, split = ',') {
+    return areaObjToSingle(p, split)
+  }
+  /**
+   * 金额整理
+   * @returns String
+   */
+  static formatPrice(price, split = '-') {
+    let priceArr = []
+    if(price) {
+      priceArr = price.split(split)
+    }
+    return {
+      minPrice: priceArr[0] || '',
+      maxPrice: priceArr[1] || '',
+    }
+  }
+
+  /**
+   * 时间整理
+   * @param String val
+   * @returns Object
+   *
+   * 参数time: 时间选择器选择结果
+   * {
+   *    start: '',
+   *    end: '',
+   *    exact: 'exact'
+   * }
+   *
+   * exact: 是否只输出精确结果
+   * split: 精确结果的分隔符
+   */
+  static formatTime(time, exact = false, split = '_') {
+    if(!time) return ''
+    const nTime = calcNotExactTime(time)
+    let sortedTime = ''
+    const { start, end } = nTime
+    if (exact) {
+      const startVal = start ? dayjs(start).unix() : ''
+      const endVal = end ? dayjs(end).unix() : ''
+      sortedTime = [startVal, endVal].join(split)
+      if (startVal || endVal) {
+        return [startVal, endVal].join(split)
+      } else {
+        return [].join(split)
+      }
+    } else {
+      sortedTime = time
+    }
+    return sortedTime
+  }
+
+  // 格式化自定义时间
+  static formatExactTime (time, join = '-', split='_') {
+    if(!time) return ''
+    let result = ''
+    let arr = []
+    if(time.indexOf(split) > -1) {
+      arr = time.split(split)
+    }
+    if(Number(arr[0]) === 0 && Number(arr[1]) > 0) {
+      result = `${join}${arr[1]}`
+    } else if(Number(arr[1]) === 0 && Number(arr[0]) > 0) {
+      result = `${arr[0]}${join}`
+    } else {
+      result = `${arr[0]}${join}${arr[1]}`
+    }
+    return result
+  }
+
+  /**
+   * 信息类型整理
+   * @param String val
+   * @returns Object
+   *
+   * 参数val示例:{}
+   */
+  static formatInfoType(infoType, split = ',') {
+    const typeMap = InfoTypeTransform.listToMap(infoType)
+    const resultType = InfoTypeTransform.formatMapToList(typeMap)
+    return resultType ? resultType.join(',') : ''
+  }
+
+
+  /**
+   * 采购单位类型整理
+   * @param String val
+   * @returns Object
+   *
+   * 参数val示例:{}
+   */
+  static formatBuyerClass(val, split = ',') {
+    let buyerClass = ''
+    if (!val || Object.keys(val).length === 0) return buyerClass
+
+    buyerClass = []
+
+    for (const key in val) {
+      if (Array.isArray(val[key])) {
+        val[key].forEach((item) => {
+          buyerClass.push(item)
+        })
+      }
+    }
+
+    buyerClass = buyerClass.join(split)
+
+    return buyerClass
+  }
+
+  /**
+   * 格式化包含关键词模式、包含关键词
+   */
+  static formatSelectMoreKey (selectMoreKey, additionalWords, wordsMode) {
+    let aWords = '' // 附加关键词
+    let wMode = 0 // 附加关键词筛选模式
+    if(selectMoreKey) {
+      aWords = additionalWords ? additionalWords.join(',') : ''
+      wMode = Number(wordsMode)
+    }
+    return {
+      additionalWords: aWords,
+      wordsMode: wMode
+    }
+  }
+
+  /**
+   * 整理当前搜索分组tab 搜索分组:默认全部0 招标采购公告1 超前项目2
+   * @param m
+   */
+  static formatSearchGroup(val) {
+    let result = 0
+    if (val !== undefined && val !== null) {
+      const nVal = Number(val)
+      switch (nVal) {
+        case 0:
+        case 4:
+        case 5:
+          result = 0
+          break
+        case 3:
+        case 1:
+          result = 3
+          break
+        case 2:
+          result = 2
+          break
+        default:
+          result = 0
+      }
+    }
+    return result
+  }
+}
+
+
+/**
+ * 三级地区对象转换成,单个area、city、district
+ */
+function areaObjToSingle(obj, split = ',') {
+  const map = {
+    area: '',
+    city: '',
+    district: ''
+  }
+  if (!obj) return map
+  const area = []
+  let city = []
+  let district = []
+  for (const key in obj) {
+    if (typeof obj[key] === 'object') {
+      if (Object.keys(obj[key]).length === 0) {
+        area.push(key)
+      } else {
+        // 城市项
+        const cityItem = obj[key]
+        for (const cKey in cityItem) {
+          // 区县项
+          const districtItem = cityItem[cKey]
+          if (Array.isArray(districtItem)) {
+            if (districtItem.length === 0) {
+              city.push(cKey)
+            } else {
+              const resetArr = districtItem.map((temp) => {
+                return cKey + '_' + temp
+              })
+              district = district.concat(resetArr)
+            }
+          }
+        }
+      }
+    }
+  }
+  return {
+    area: area.join(split),
+    city: city.join(split),
+    district: district.join(split)
+  }
+}
+
+/**
+ * 恢复筛选条件,已存的筛选条件转为原始组件所需筛选
+ */
+export class FilterHistoryAjaxModelRestore {
+  static formatAll (map) {
+    // wordsMode
+    const { additionalWords, wordsMode } = this.formatSelectMoreKey(map.additionalWords, map.wordsMode)
+    // 高亮关键词
+    const matchKeys = this.formatMatchKeys(map.keywords, additionalWords)
+    // 精准匹配/模糊匹配
+    const searchMode= this.formatSearchMode(map.searchMode)
+    // tab值
+    const searchGroup = this.formatSearchGroup(map.searchGroup)
+    // 采购单位
+    const { buyerClass } = this.formatBuyerClass(map.buyerClass)
+
+    const formatted = {
+      input: map.keywords,
+      additionalWords: additionalWords || [],
+      matchKeys,
+      wordsMode,
+      searchMode,
+      searchGroup,
+      selectType: map.scope || [],
+      industry: map.industry || {},
+      regionMap: map.regionMap || {},
+      price: map.price,
+      dateTime: map.publishTime, // 标讯搜索恢复数据可能要用到?
+      publishTime: map.publishTime,
+      subtype: map.infoType || [],
+      buyerclass: buyerClass|| {},
+      buyertel: map.buyerTel,
+      winnertel: map.winnerTel,
+      notkey: map.notkey ? map.notkey.split(',') : [],
+      buyer: map.buyer ?  map.buyer.split(',') : [],
+      winner: map.winner ? map.winner.split(',') : [],
+      agency: map.agency ? map.agency .split(',') : [],
+      fileExists: map.fileExists
+    }
+    return formatted
+  }
+
+
+  /**
+   * 搜索方式整理 精准匹配/模糊匹配
+   * @param String m '0'/'1'
+   * @returns Object
+   */
+  static formatSearchMode(m) {
+    let searchMode = '0'
+    if (Array.isArray(m) && m.length) {
+      searchMode =  m.join(',')
+    }
+    return searchMode
+  }
+
+  /**
+   * 处理关键词相关
+   * (输出包含文字描述和选择器使用的数据结构)
+   * @param String m '0'/'1'
+   * @returns Object
+   */
+  static formatSelectMoreKey(additionalWords, wordsMode) {
+    const moreKeywordsModeState = {
+      additionalWords: [],
+      wordsMode: undefined
+    }
+    if (additionalWords) {
+      moreKeywordsModeState.additionalWords = additionalWords.split(',')
+    }
+    if (Array.isArray(wordsMode) && wordsMode.length) {
+      moreKeywordsModeState.wordsMode = Number(wordsMode[0])
+    }
+    return moreKeywordsModeState
+  }
+
+  /**
+   * 处理页面需要匹配的高亮关键词
+   */
+  static formatMatchKeys (keywords, additionalWords) {
+    let arr = []
+    if(keywords) {
+      arr.push(keywords)
+    }
+    if(additionalWords?.length > 0) {
+     arr.concat(additionalWords)
+    }
+    return arr
+  }
+
+  /**
+   * 整理当前搜索分组tab 搜索分组:默认全部0 招标采购公告1 超前项目2
+   * @param m
+   */
+  static formatSearchGroup(val) {
+    let result = 0
+    if (val !== undefined && val !== null) {
+      const nVal = Number(val)
+      switch (nVal) {
+        case 0:
+        case 4:
+        case 5:
+          result = 0
+          break
+        case 3:
+        case 1:
+          result = 1
+          break
+        case 2:
+          result = 2
+          break
+        default:
+          result = 0
+      }
+    }
+    return result
+  }
+  /**
+   * 行业整理
+   * @param String val
+   * @returns Object
+   *
+   * 参数val示例:'机械设备_工程机械,机械设备_车辆,机械设备_其他机械设备,行政办公_通用办公设备,行政办公_办公家具'
+   */
+  static formatIndustry(val) {
+    const result = {
+      industry: undefined,
+    }
+    if (!val || val === '全部') return result
+
+    const industry = {}
+
+    // 整理行业
+    val.split(',').forEach((v) => {
+      const vSplit = v.split('_')
+      const industryChildren = industry[vSplit[0]]
+      if (Array.isArray(industryChildren)) {
+        industryChildren.push(vSplit[1])
+      } else {
+        industry[vSplit[0]] = [vSplit[1]]
+      }
+    })
+    if (Object.keys(industry).length) {
+      result.industry = industry
+    }
+
+    // 整理行业text
+    // 统计完整行业数量
+    const calcChildrenCount = {}
+    for (const key in industryListMapExp) {
+      calcChildrenCount[key] = industryListMapExp[key].length
+    }
+    return result
+  }
+
+  /**
+   * 采购单位类型整理
+   * @param String val
+   * @returns Object
+   *
+   * 参数val示例:'住建,传媒,建筑业,能源化工,批发零售,信息技术,运输物流,制造业,住宿餐饮'
+   */
+  static formatBuyerClass(val) {
+    const result = {
+      buyerClass: undefined,
+    }
+    if (!val || val === '全部') return result
+
+    // 逻辑和行业类似,如果二级全选,则展示一级分类文字,否则展示二级分类文字
+    // 1. 把选中的整理成原始数据结构
+    const keyListMap = {}
+    for (const key in buyerclassListMapExp) {
+      if (val.indexOf(key) !== -1) {
+        // 存在
+        if (Array.isArray(keyListMap[key])) {
+          keyListMap[key] = []
+        }
+      }
+      if (Array.isArray(buyerclassListMapExp[key])) {
+        // 循环二级子项
+        buyerclassListMapExp[key].forEach((item) => {
+          if (val.indexOf(item) !== -1) {
+            // 存在
+            if (Array.isArray(keyListMap[key])) {
+              keyListMap[key].push(item)
+            } else {
+              keyListMap[key] = [item]
+            }
+          }
+        })
+      }
+    }
+    return {
+      buyerClass: keyListMap,
+    }
+  }
+
+  /**
+   * 处理联系方式
+   * @param val
+   * @returns {{basicData: undefined}}
+   */
+  static formatContact(val = '') {
+    const result = {
+      basicData: undefined,
+    }
+    const map = {}
+    biddingSearchConcat.forEach((item) => {
+      map[item.key] = item.label
+    })
+
+    if (val) {
+      result.basicData = [val]
+    }
+    return result
+  }
+}
+
+/**
+ * 二级地区对象转换成,单个area、city、district
+ */
+export function areaObjTwoToSingle(obj, split = ',') {
+  const map = {
+    area: '',
+    city: ''
+  }
+  if (!obj) return map
+  const area = []
+  let city = []
+  for (const key in obj) {
+    if (Array.isArray(obj[key])) {
+      if (obj[key].length === 0) {
+        area.push(key)
+      } else {
+        city = city.concat(obj[key])
+      }
+    }
+  }
+  return {
+    area: area.join(split),
+    city: city.join(split)
+  }
+}
+
+// 信息类型 map
+/**
+ * 信息类型格式化,将map类型数据最终格式化成接口所需
+ * @param info
+ */
+export function infoTypeMapFormat (infoType = {}) {
+  const resultType = InfoTypeTransform.formatMoreMapToList(infoType)
+  return resultType ? resultType.join(',') : ''
+}

+ 39 - 1
apps/bigmember_pc/src/utils/globalFunctions.js

@@ -327,7 +327,7 @@ export function moneyUnit(m, type = 'string', lv = 0) {
 
       let result = num / Math.pow(10000, lv)
 
-      if (result > 10000 && lv < 2) {
+      if (result >= 10000 && lv < 2) {
         return this.test(num, type, lv + 1)
       } else {
         if (type === 'string') {
@@ -788,3 +788,41 @@ export function  extractKeywords(text, keywords, n = 10) {
   // 输出结果
   return result && result[1] ? result[1] : ''
 }
+
+/**
+ * 应用场景:1.翻页后页面滚动到列表第一条的位置
+ * @param targetEvent 页面要滚动到的目标元素
+ */
+export function scrollTargetView (targetEvent) {
+  if (!targetEvent) return
+  let scrollWrapper
+  let targetTop
+  const inWorkSpace = location.href.indexOf('work-bench') > -1
+  if (inWorkSpace) {
+    // 工作桌面内(qiankun、iframe)
+    const offsetHeight = document.querySelector('.el-header')?.offsetHeight
+    const inApp = window.__POWERED_BY_QIANKUN__
+    if (inApp) {
+      // qiankun
+      const appNode = document.querySelector('#app-container > div').shadowRoot
+      targetTop = appNode?.querySelector(targetEvent)?.offsetTop
+      scrollWrapper = document.querySelector('.el-main')
+      scrollWrapper.scrollTop = targetTop - offsetHeight
+    } else {
+      // iframe
+      const iframeNode = document.querySelector('#work-bench-container iframe')?.contentDocument
+      targetTop = iframeNode.querySelector(targetEvent)?.offsetTop
+      scrollWrapper = iframeNode.body
+    }
+    // console.log(`工作桌面内${inApp ? 'qiankun' : 'iframe'}---滚动元素:${scrollWrapper.className}, 滚动距离:${targetTop}`)
+    scrollWrapper.scrollTop = targetTop - offsetHeight
+  } else {
+    // 非工作桌面
+    targetTop = document.querySelector(targetEvent)?.offsetTop
+    // 因浏览器及模式差异,页面滚动元素存在不同(标准模式:document.documentElement;兼容模式:body)
+    // scrollingElement可获取页面滚动元素
+    scrollWrapper = document.scrollingElement
+    scrollWrapper.scrollTop = targetTop
+    // console.log(`非工作桌面---滚动元素:${scrollWrapper.className ? scrollWrapper.className : scrollWrapper.tagName}, 滚动距离:${targetTop}`)
+  }
+}

+ 2 - 0
apps/bigmember_pc/src/utils/index.js

@@ -7,3 +7,5 @@ export * from './globalFunctions'
 export * from './format/money'
 export * from './format/ad'
 export * from './format/date'
+export * from './format/search-bid-filter'
+export *  from './whichContainer'

+ 80 - 0
apps/bigmember_pc/src/utils/mixins/visited-setup.js

@@ -0,0 +1,80 @@
+import { useStore } from '@/store'
+import { computed } from 'vue'
+
+class VisitedPathItem {
+  constructor(path, search) {
+    this.path = path
+    this.search = search
+    this.timestamp = Date.now()
+  }
+}
+
+export function mixinVisited() {
+  const store = useStore()
+  const visitedList = computed(() => {
+    return store.state.user.visitedList
+  })
+
+  function createPathItem(path, search) {
+    return new VisitedPathItem(path, search)
+  }
+
+  // 从判断 path 是否访问过
+  // 参数path为 VisitedPathItem new 出来的实例
+  function pathVisitedIndex(path) {
+    const list = visitedList.value
+    let sameIndex = -1
+    if (list) {
+      for (let i = 0; i < list.length; i++) {
+        const same = comparePath(list[i], path)
+        if (same) {
+          sameIndex = i
+          break
+        }
+      }
+    }
+    return sameIndex
+  }
+
+  function comparePath(basePath, newPath) {
+    const pathSame = basePath.type === newPath.type
+    const searchSame = basePath.id === newPath.id
+    return pathSame && searchSame
+  }
+
+  function pathVisited(path) {
+    return pathVisitedIndex(path) !== -1
+  }
+
+  // 保存一条历史
+  // 参数path为 VisitedPathItem new出来的实例
+  function pathVisiting(path) {
+    if (!path) return
+    // 判断是否重复
+    const index = pathVisitedIndex(path)
+    const addVisitedList = visitedList.value ? JSON.parse(JSON.stringify(visitedList.value)) : []
+    if (index >= 0) {
+      // 已存在
+      const itemArr = addVisitedList.splice(index, 1)
+      const item = itemArr[0]
+      item.timestamp = Date.now()
+      addVisitedList.unshift(item)
+    } else {
+      addVisitedList.unshift(path)
+    }
+    // 全量替换
+    store.commit('user/addVisited', addVisitedList)
+  }
+
+  function refreshVisited() {
+    store.commit('user/refreshVisited')
+  }
+
+  return {
+    visitedList,
+    pathVisiting,
+    refreshVisited,
+    createPathItem,
+    pathVisited
+  }
+}

+ 1 - 0
apps/bigmember_pc/src/utils/prototype/index.js

@@ -0,0 +1 @@
+import './modules/visited'

+ 4 - 0
apps/bigmember_pc/src/utils/prototype/modules/visited.js

@@ -0,0 +1,4 @@
+import Vue from 'vue'
+import VisitedInfo from '@/utils/visited'
+
+Vue.prototype.$visited = new VisitedInfo()

+ 83 - 0
apps/bigmember_pc/src/utils/visited/index.js

@@ -0,0 +1,83 @@
+import { defaultLocalPageData } from '@/utils'
+import VisitedModelTransform from './transform'
+
+/**
+ * 已访问页面相关操作,提供查询,存储功能
+ */
+class VisitedInfo {
+  constructor(key = 'visited-path-list') {
+    this._CACHE_KEY = key
+    this.visitedList = []
+    this.refreshVisited()
+  }
+
+  refreshVisited() {
+    const expiresTime = 3 * 24 * 60 * 60 * 1000
+    const now = Date.now()
+    const TransformModel = new VisitedModelTransform(
+      defaultLocalPageData(this._CACHE_KEY, [])
+    )
+    this.visitedList = TransformModel.list.filter((item) => {
+      return now - item.timestamp <= expiresTime
+    })
+    this.visitedList = TransformModel.transform(this.visitedList)
+    localStorage.setItem('visited-path-list', JSON.stringify(this.visitedList))
+  }
+
+  comparePath(basePath, newPath) {
+    const pathSame = basePath.type === newPath.type
+    const searchSame = basePath.id === newPath.id
+
+    return pathSame && searchSame
+  }
+
+  pathVisitedIndex(item) {
+    const list = this.visitedList
+    let sameIndex = -1
+    if (list) {
+      for (let i = 0; i < list.length; i++) {
+        const same = this.comparePath(list[i], item)
+        if (same) {
+          sameIndex = i
+          break
+        }
+      }
+    }
+    return sameIndex
+  }
+
+  check(item) {
+    return this.pathVisitedIndex(item) !== -1
+  }
+
+  push({ type, id = null }) {
+    if (!id) {
+      return
+    }
+    // 判断是否重复
+    const item = this.createItem(type, id)
+    const index = this.pathVisitedIndex(item)
+    const visitedList = [].concat(this.visitedList)
+    // 已存在
+    if (index !== -1) {
+      visitedList.splice(index, 1)
+    }
+    visitedList.unshift(item)
+    this.visitedList = [].concat(visitedList)
+    this.save()
+  }
+
+  createItem(type, id) {
+    return {
+      type,
+      id,
+      timestamp: Date.now()
+    }
+  }
+
+  save() {
+    localStorage.setItem('visited-path-list', JSON.stringify(this.visitedList))
+  }
+}
+
+export default VisitedInfo

+ 53 - 0
apps/bigmember_pc/src/utils/visited/transform.js

@@ -0,0 +1,53 @@
+function defaultLocalPageData(key, defaultValues = {}) {
+  return JSON.parse(localStorage.getItem(key) || JSON.stringify(defaultValues))
+}
+
+/**
+ * 当前所有已查看页面匹配规则
+ * @type {{project: string, ent: string, issued: string, content: string, buyer: string}}
+ */
+const Rules = {
+  content: 'article/content/*',
+  issued: 'article/issued',
+  ent: 'ent(_ser)?_portrait',
+  buyer: 'client_portrayal',
+  project: 'client_follow_detail',
+  demand: 'demand/detail/*',
+  proposed: 'proposed/detail/*'
+}
+
+/**
+ * 旧数据模型转换处理,同时兼容新旧模型使用
+ */
+class VisitedModelTransform {
+  constructor(list = defaultLocalPageData('visited-path-list', [])) {
+    this.list = list
+    return this
+  }
+
+  checkPathRules(path) {
+    let result = path
+    for (const type in Rules) {
+      if (new RegExp(Rules[type]).test(path)) {
+        result = type
+        break
+      }
+    }
+    return result
+  }
+
+  transformItem(item) {
+    return {
+      id: item?.id || item?.search.replace(/(id|sid|entName)=/, ''),
+      type: item?.type || this.checkPathRules(item?.path),
+      path: item?.path,
+      timestamp: item?.timestamp
+    }
+  }
+
+  transform(list = this.list) {
+    return list.map(this.transformItem.bind(this))
+  }
+}
+
+export default VisitedModelTransform

+ 22 - 0
apps/bigmember_pc/src/utils/whichContainer.js

@@ -0,0 +1,22 @@
+
+/**
+ * 判断当前在哪个容器
+ * @returns {string}
+ * in-app 当前子应用在工作台中集成时
+ * in-web 当前子应用在剑鱼网页中集成时
+ */
+export function GetInWhichContainer() {
+  const InWhichContainer = window.parent !== window ? 'in-app' : 'in-web'
+  return InWhichContainer
+}
+
+const InWhichContainer = GetInWhichContainer()
+
+// in-iframe 当前页面在 iframe 中打开
+const isInIframe = window.frames.length > 0
+
+export const InContainer = {
+  inApp: InWhichContainer === 'in-app',
+  inWeb: InWhichContainer === 'in-web',
+  inIframe: isInIframe
+}

+ 4 - 2
apps/bigmember_pc/src/views/BidrenewalDialog/index.vue

@@ -80,8 +80,10 @@ export default {
     },
     refreshData() {
       try {
-        this.$refs.formBidrenewal.getOptionsInfo()
-        this.$refs.formBidrenewal.getInfo()
+        this.$nextTick(() => {
+          this.$refs.formBidrenewal.getOptionsInfo()
+          this.$refs.formBidrenewal.getInfo()
+        })
       } catch (e) {
         console.log(e)
       }

+ 24 - 2
apps/bigmember_pc/src/views/article-content/components/ContentBIActions.vue

@@ -98,11 +98,14 @@ export default {
       dialogVisible: false,
       IframeSrc: '',
       getEntData: {},
-      showPropertyDialog: false
+      showPropertyDialog: false,
+      haspowers: {}, // 判断客户类型
+      inBITopNet: '' // 判断是否是拓普
     }
   },
   created() {
     this.getEntInfo()
+    this.getHasPowers()
     this.getParams()
   },
   props: {
@@ -118,6 +121,10 @@ export default {
   },
   computed: {
     getList() {
+      // 拓普不显示任何按钮
+      if (this.inBITopNet) {
+        return []
+      }
       if (this.list[0].active === 0 && this.property !== 'BIProperty') {
         return this.list.slice(0, 1)
       } else if (this.property === 'BIProperty') {
@@ -127,7 +134,12 @@ export default {
         })
         return this.list
       } else {
-        return this.list
+        // 如果是营销通用版且无bi_yx_topnet,则只显示收录,有bi_yx_topnet,显示全部
+        if (this.haspowers.bi_yx_topnet) {
+          return this.list
+        } else {
+          return this.list.slice(0, 1)
+        }
       }
     }
   },
@@ -206,6 +218,7 @@ export default {
       const urlParams = new URLSearchParams(window.location.search)
       this.pageType = urlParams.get('resource')
       this.property = urlParams.get('property')
+      this.inBITopNet = urlParams.get('crm_type')
       this.fromJhfp = urlParams.get('from')
       if (this.pageType === 'BI' || this.property === 'BIProperty') {
         $('.com-tagsbar').hide()
@@ -314,6 +327,15 @@ export default {
           break
       }
     },
+    // 判断大客,大客还显示创建按钮, 营销通用版和非大客只显示收录按钮 P560版本调整
+    getHasPowers() {
+      const url = '/jyapi/resourceCenter/haspowers'
+      this.ajaxComponent(url).then((res) => {
+        if (res.error_code === 0) {
+          this.haspowers = res.data
+        }
+      })
+    },
     // 判断是否能创建
     isCanAdd(type) {
       const url = '/jyapi/crmApplication/info/canAdd'

+ 1 - 0
apps/bigmember_pc/src/views/article-content/components/ContentHeader.vue

@@ -321,6 +321,7 @@ const openTagLink = (event, url) => {
     font-weight: 400;
     line-height: 18px;
     text-decoration: none;
+    user-select: unset;
     &:not([href]),
     &[href*='javascript'] {
       cursor: unset;

+ 22 - 6
apps/bigmember_pc/src/views/article-content/components/Nps.vue

@@ -1,10 +1,5 @@
 <template>
-  <div
-    id="npsMain"
-    class="npsMain npsPc"
-    v-show="showModule"
-    @mousemove="getIsView"
-  >
+  <div id="npsMain" class="npsMain npsPc" v-show="showModule">
     <div class="gray-div"></div>
     <div class="nps-content">
       <div class="nps-head">
@@ -48,6 +43,7 @@
 
 <script>
 import { getNpsData, getSeeNps, collectionNps } from '@/api/modules/nps'
+import { isElementInScrollArea } from '@jy/util'
 
 export default {
   data() {
@@ -77,14 +73,34 @@ export default {
   created() {
     this.getNpsData()
   },
+  beforeDestroy() {
+    this.removeScrollEvent()
+  },
   methods: {
     getNpsData() {
       getNpsData().then((res) => {
         if (res && res.data) {
           this.showModule = res.data.isShowNps
+          this.bindScrollEvent()
         }
       })
     },
+    bindScrollEvent(){
+      window.addEventListener('scroll', this.onPageScroll)
+    },
+    removeScrollEvent() {
+      window.removeEventListener('scroll', this.onPageScroll)
+    },
+    checkNpsView() {
+      const target = this.$el
+      const visible = isElementInScrollArea(target)
+      if (visible) {
+        this.getIsView()
+      }
+    },
+    onPageScroll() {
+      this.checkNpsView()
+    },
     // emoji表情转为字符
     utf16toEntities(str) {
       const patt = /[\ud800-\udbff][\udc00-\udfff]/g // 检测utf16字符正则

+ 2 - 2
apps/bigmember_pc/src/views/article-content/pages/Article.vue

@@ -509,6 +509,8 @@ function doClickFreeView() {
                 </div>
               </div>
             </div>
+            <!--  nps评分  -->
+            <Nps></Nps>
             <!--  投标服务  -->
             <div
               class="content-card watch-tab-content"
@@ -554,8 +556,6 @@ function doClickFreeView() {
               />
             </div>
           </div>
-          <!--  评分  -->
-          <Nps></Nps>
           <!--  内容底部广告  -->
           <div class="article-content-footer-container">
             <adsense code="jy-pccontent-bottom"></adsense>

+ 20 - 2
apps/bigmember_pc/src/views/medical-field/Credentials.vue

@@ -26,7 +26,7 @@
               <p class="cred_desc">方式1:扫描二维码获得专属客户服务</p>
               <div class="qr_box">
                 <img
-                  src="@/assets/images/medical-field/qrcode-814.png"
+                  :src="kefuInfo.wxer"
                   alt=""
                   class="qr_img"
                 />
@@ -233,6 +233,8 @@ import {
   domainUsersave
 } from '@/api/modules/medicalField.js'
 import informationSuccess from '@/components/common/informationSuccess.vue'
+import { getCustomInfo } from '@/api/modules/'
+import qrCode from '@/assets/images/medical-field/qrcode-814.png'
 export default {
   name: 'Credentials',
   components: {
@@ -358,10 +360,14 @@ export default {
       jobData: [], // 职位数据
       branchData: [], // 部门数据
       source: 'Credentials',
-      list_choose: false
+      list_choose: false,
+      kefuInfo: {
+        wxer: qrCode
+      }
     }
   },
   created() {
+    this.getKefuInfo()
     this.getAllFunctions({ vm: this })
     this.jobData = jobJson.map((item) => {
       return {
@@ -593,6 +599,18 @@ export default {
     },
     otherFocus() {
       this.$refs.ruleForm.clearValidate(['job'])
+    },
+    // 获取客服二维码信息
+    getKefuInfo () {
+      const params = {
+        type: 'kf'
+      }
+      getCustomInfo(params).then(res => {
+        const { error_code: code, data } = res
+        if(code === 0 && data) {
+          this.kefuInfo = Object.assign(this.kefuInfo, data)
+        }
+      })
     }
   }
 }

+ 21 - 0
apps/bigmember_pc/src/views/order/components/big-member/buy-tip.vue

@@ -0,0 +1,21 @@
+<template>
+  <p class="buy-tip">
+    购买须知: <br />剑鱼平台产品与服务属于虚拟数字产品,鉴于服务的特殊性,一旦开通权益不支持退款,请确认无误后进行支付。
+  </p>
+</template>
+
+<script>
+export default {
+  name: 'buy-tip'
+}
+</script>
+
+<style scoped>
+.buy-tip {
+  margin-top: 28px;
+  font-size: 12px;
+  font-weight: 400;
+  color: #888888;
+  line-height: 16px;
+}
+</style>

+ 5 - 6
apps/bigmember_pc/src/views/order/components/data-export/buy-tip.vue

@@ -2,25 +2,24 @@
   <div class="buy-tip">
     <p>购买须知:</p>
     <p>
-      最低起售<span>{{ priceConf.orderMinPrice }}</span
-      >元
+      1.最低起售<span>{{ priceConf.orderMinPrice }}</span
+      >元
     </p>
     <p>
-      标准字段包: 原价<span>{{ priceConf.unitPrice_normal }}</span
+      2.“标准字段包”原价<span>{{ priceConf.unitPrice_normal }}</span
       >元/条, 限时<span>{{ priceConf.discount * 10 }}</span
       >折, 现价<span>{{
         priceConf.unitPrice_normal * priceConf.discount
       }}</span
       >元/条;
-    </p>
-    <p>
-      高级字段包: 原价<span>{{ priceConf.unitPrice_senior }}</span
+      “高级字段包” 原价<span>{{ priceConf.unitPrice_senior }}</span
       >元/条, 限时<span>{{ priceConf.discount * 10 }}</span
       >折, 现价<span>{{
         priceConf.unitPrice_senior * priceConf.discount
       }}</span
       >元/条。
     </p>
+    <p>3.剑鱼平台产品与服务属于虚拟数字产品,鉴于服务的特殊性,一旦数据交付不支持退款,请确认无误后进行支付。</p>
   </div>
 </template>
 

+ 22 - 0
apps/bigmember_pc/src/views/order/components/doc-member/buy-tip.vue

@@ -0,0 +1,22 @@
+<template>
+  <p class="buy-tip">
+    购买须知:
+    <br />剑鱼平台产品与服务属于虚拟数字产品,鉴于服务的特殊性,一旦开通权益不支持退款,请确认无误后进行支付。
+  </p>
+</template>
+
+<script>
+export default {
+  name: 'buy-tip'
+}
+</script>
+
+<style scoped>
+.buy-tip {
+  margin-top: 28px;
+  font-size: 12px;
+  font-weight: 400;
+  color: #888888;
+  line-height: 16px;
+}
+</style>

+ 3 - 3
apps/bigmember_pc/src/views/order/components/doc-member/info.vue

@@ -564,10 +564,10 @@ export default {
 <style lang="scss">
 .doc-member-adsense {
   .adsense{
-    padding: 0 0 24px 0;
+    padding: 0 0 24px 0!important;
     .content{
-      padding: 0;
-      border: 0;
+      padding: 0!important;
+      border: 0!important;
     }
   }
 }

+ 1 - 0
apps/bigmember_pc/src/views/order/components/resource-pack/buy-tip.vue

@@ -4,6 +4,7 @@
       $route.meta['buy-tip']
     }}个数时使用,可多次购买。
     <br />2.使用有效期仅限超级订阅服务周期内的当月,次月清零,不可转赠。
+    <br />3.剑鱼平台产品与服务属于虚拟数字产品,鉴于服务的特殊性,一旦开通权益不支持退款,请确认无误后进行支付。
   </p>
 </template>
 

+ 21 - 0
apps/bigmember_pc/src/views/order/components/vipsubscribe/buy-tip.vue

@@ -0,0 +1,21 @@
+<template>
+  <p class="buy-tip">
+    购买须知: <br />剑鱼平台产品与服务属于虚拟数字产品,鉴于服务的特殊性,一旦开通权益不支持退款,请确认无误后进行支付。
+  </p>
+</template>
+
+<script>
+export default {
+  name: 'buy-tip'
+}
+</script>
+
+<style scoped>
+.buy-tip {
+  margin-top: 28px;
+  font-size: 12px;
+  font-weight: 400;
+  color: #888888;
+  line-height: 16px;
+}
+</style>

+ 36 - 17
apps/bigmember_pc/src/views/portrayal/components/DataExportTip.vue

@@ -12,7 +12,7 @@
         进行定制化导出
       </p>
       <div class="export-dialog-wx">
-        <img :src="getWxCustom" alt="" />
+        <img :src="kefuInfo.wxer" alt="" />
       </div>
       <p class="wx-code-label">客服微信</p>
       <el-button @click="$emit('setExport')" class="export-continue-btn"
@@ -27,7 +27,8 @@
 
 <script>
 import { Button, Checkbox } from 'element-ui'
-import { mapState } from 'vuex'
+// import { mapState } from 'vuex'
+import { getCustomInfo } from '@/api/modules/'
 export default {
   name: 'export-dialog-container',
   components: {
@@ -36,30 +37,48 @@ export default {
   },
   data() {
     return {
-      agreement: false
+      agreement: false,
+      kefuInfo: {}
     }
   },
+  created () {
+    this.getKefuInfo()
+  },
   watch: {
     agreement(newval) {
       this.$emit('checkBoxChange', newval)
     }
   },
   computed: {
-    ...mapState({
-      info: (state) => state.user.info
-    }),
-    getWxCustom() {
-      const customers = this.info.customers
-      if (customers && customers.length > 0) {
-        const getWxCode = customers.filter((v) => v.name === '高静')
-        if (getWxCode && getWxCode.length > 0) {
-          return getWxCode[0].wxer
-        } else {
-          return ''
-        }
-      } else {
-        return ''
+    // ...mapState({
+    //   info: (state) => state.user.info
+    // }),
+    // getWxCustom() {
+    //   const customers = this.info.customers
+    //   if (customers && customers.length > 0) {
+    //     const getWxCode = customers.filter((v) => v.name === '高静')
+    //     if (getWxCode && getWxCode.length > 0) {
+    //       return getWxCode[0].wxer
+    //     } else {
+    //       return ''
+    //     }
+    //   } else {
+    //     return ''
+    //   }
+    // }
+  },
+  methods: {
+    // 获取客服二维码信息
+    getKefuInfo () {
+      const params = {
+        type: 'kf'
       }
+      getCustomInfo(params).then(res => {
+        const { error_code: code, data } = res
+        if(code === 0 && data) {
+          this.kefuInfo = data
+        }
+      })
     }
   }
 }

+ 2 - 2
apps/bigmember_pc/src/views/project/AttendBiddingList.vue

@@ -61,7 +61,7 @@
             <TagSelector
               :sourceList="bidEndDateList"
               v-model="endStateVal"
-              @onChange="onEndStateChange"
+              @change="onEndStateChange"
             >
               <div slot="header">投标截止状态:</div>
             </TagSelector>
@@ -70,7 +70,7 @@
             <TagSelector
               :sourceList="openStateList"
               v-model="openStateVal"
-              @onChange="onOpenStateChange"
+              @change="onOpenStateChange"
             >
               <div slot="header">开标状态:</div>
             </TagSelector>

+ 0 - 0
apps/bigmember_pc/src/views/search/Layout.vue


+ 218 - 0
apps/bigmember_pc/src/views/search/bidding/components/history-filter-dialog.vue

@@ -0,0 +1,218 @@
+<script setup>
+import { SearchBidModel } from '../model'
+
+const {
+  onSelectedFilter,
+  disposeFilterActionModel
+} = SearchBidModel
+
+const {
+  filterHistoryList: filterData,
+  onHasToggle, // 展开收起
+  onDeleteFilter, // 删除单条已存筛选条件
+} = disposeFilterActionModel
+
+ const props = defineProps({
+   visible: {
+     type: Boolean,
+     default: false
+    }
+ })
+
+// 关键词
+function formatToSpace (keywords, additionalWords, wordsModeText) {
+  let str = ''
+  if (additionalWords) {
+    if(keywords) {
+      str += ','
+    }
+    str += additionalWords.replace(/,/g, ",")  +'(' + wordsModeText + ')'
+  }
+  return str
+}
+
+const emit = defineEmits(['before-close'])
+function beforeClose () {
+  emit('before-close')
+}
+
+</script>
+
+<template>
+  <!-- 已存筛选弹框 -->
+  <el-dialog
+    custom-class="filter-dialog has-filter-dialog"
+    title="已存筛选条件"
+    :close-on-click-modal="true"
+    :close-on-press-escape="false"
+    width="750"
+    :center="true"
+    :visible.sync="visible"
+    :before-close='beforeClose'
+  >
+    <div class="filter-data-container">
+      <div v-for="item in filterData" class="filter-data-list">
+        <div class="f-l-title">
+          <div style="display: flex;align-items: center;flex:1;">
+            <i class="iconfont icon-xiala" :class="{ 'is-reverse': item.open}" @click="onHasToggle(item)"></i>
+            <span class="f-l-title-text" @click="onSelectedFilter(item)">
+              {{ item.keywords }}
+              <span v-if="item.additionalWords">{{ formatToSpace(item.keywords, item.additionalWords, item.wordsModeText) }}</span>
+            </span>
+          </div>
+          <i class="iconfont icon-delete" @click="onDeleteFilter(item)"></i>
+        </div>
+        <div class="has-search-model">搜索模式:{{ item.searchModeText}}</div>
+        <el-collapse-transition :collapse-transition="false">
+          <div v-show="item.open">
+            <div class="f-l-content" @click="onSelectedFilter(item)">
+              <p class="f-l-c-item">搜索范围:<em class="i-value">{{ item.scopeText }}</em></p>
+              <p class="f-l-c-item" v-if="item.industry">行业:<em class="i-value">{{ item.industryText }}</em></p>
+              <p class="f-l-c-item">
+                <span v-if="item.price">价格区间:<em class="i-value">{{ item.priceText }}</em></span>
+                <span v-if="item.publishTimeText">发布时间:<em class="i-value">{{ item.publishTimeText }}</em></span>
+                <span v-if="item.fileExistsText != '全部'">附件:<em class="i-value">{{ item.fileExistsText }}</em></span>
+              </p>
+              <p class="f-l-c-item" v-if="item.regionMapText">项目地区:<em class="i-value">{{ item.regionMapText }}</em></p>
+              <p class="f-l-c-item" v-if="item.infoTypeText">信息类型:<em class="i-value">{{ item.infoTypeText }}</em></p>
+              <p class="f-l-c-item" v-if="item.buyerClassText">采购单位类型:<em class="i-value">{{ item.buyerClassText }}</em></p>
+              <p class="f-l-c-item">
+                <span v-if="item.buyerTel === 'y'">采购单位联系方式:<em class="i-value">{{ item.buyerTelText }}</em></span>
+                <span v-if="item.winnerTel === 'y'">中标单位联系方式:<em class="i-value">{{ item.winnerTelText }}</em></span>
+                <span v-if="item.notkey">排除词:<em class="i-value">{{ item.notkey }}</em></span>
+              </p>
+              <p class="f-l-c-item" v-if="item.buyer">采购单位:<em class="i-value">{{ item.buyer }}</em></p>
+              <p class="f-l-c-item" v-if="item.winner">中标企业:<em class="i-value">{{ item.winner }}</em></p>
+              <p class="f-l-c-item" v-if="item.agency">招标代理机构:<em class="i-value">{{ item.agency }}</em></p>
+            </div>
+          </div>
+        </el-collapse-transition>
+      </div>
+    </div>
+  </el-dialog>
+</template>
+
+<style lang='scss' scoped>
+
+::v-deep {
+  .filter-dialog{
+    padding: 32px;
+    border-radius: 8px!important;
+    & ::-webkit-scrollbar {
+      /*滚动条整体样式*/
+      width: 2px!important;
+    }
+  }
+  .el-dialog{
+    width: 750px !important;
+  }
+  .el-dialog__body{
+    padding: 0;
+  }
+}
+.filter-dialog{
+  .el-dialog__header {
+    padding: 0;
+    .el-dialog__title{
+      color: #1D1D1D;
+    }
+  }
+  .el-dialog__body{
+    width: 686px;
+    overflow: hidden;
+  }
+  .filter-data-container {
+    max-height: 400px;
+    overflow-y: scroll;
+    //width: 694px;
+
+    .f-l-title{
+      padding: 8px 0 2px;
+    }
+    .f-l-content {
+      padding: 12px 0;
+      border-top: 1px dashed #ddd;
+      font-size: 12px;
+      color: #686868;
+      cursor: pointer;
+    }
+
+    .filter-data-list{
+      margin-top: 12px;
+      padding: 0 16px;
+      background: #F5F6F7;
+      border-radius: 4px;
+      .has-search-model{
+        padding:0 0 8px 22px;
+        font-size: 12px;
+        line-height: 18px;
+      }
+      .f-l-title{
+        display: flex;
+        align-items: center;
+        justify-content: space-between;
+        cursor: pointer;
+        font-size: 14px;
+        min-height: 22px;
+        color: #1D1D1D;
+        .f-l-title-text{
+          flex: 1;
+          text-align: justify;
+          font-size: 14px;
+          line-height: 22px;
+          max-width: 582px;
+        }
+      }
+      .f-l-title:hover {
+        .f-l-title-text{
+          color: #2cb7ca;
+        }
+        .icon-xiala{
+          color: #2cb7ca;
+        }
+        .icon-delete{
+          font-size: 18px;
+          color: #2cb7ca;
+        }
+      }
+      .f-l-c-item{
+        margin-top: 8px;
+        line-height: 18px;
+        span {
+          margin-right: 16px;
+        }
+        .i-value {
+          color: #1D1D1D;
+        }
+      }
+    }
+  }
+}
+.has-filter-dialog{
+  .el-dialog__headerbtn{
+    font-size: 18px;
+    top: 34px;
+    right: 32px;
+  }
+}
+
+.icon-xiala{
+  display: inline-block;
+  font-size: 18px;
+  flex-shrink: 0;
+  transform: rotate(0deg);
+  transition: transform .5s;
+  margin-right: 4px;
+  cursor: pointer;
+  &.is-reverse{
+    transform: rotate(180deg);
+  }
+}
+.icon-delete {
+  display: inline-block;
+  font-size:22px;
+  color:#aeaeae;
+  cursor: pointer;
+}
+
+</style>

+ 612 - 0
apps/bigmember_pc/src/views/search/bidding/components/recommend-card.vue

@@ -0,0 +1,612 @@
+<script setup>
+import { getCurrentInstance, watch} from 'vue'
+import MarketUserScatter from '@/views/analysisReport/components/MarketUserScatter'
+import BuyerScaleScatter from '@/views/analysisReport/components/BuyerScaleScatter'
+import { SearchBidModel } from '../model'
+const {
+  recommendCardCircleModel
+} = SearchBidModel
+
+const that = getCurrentInstance().proxy
+
+const {
+  toggleAdvancedContent,
+  advancedInfo,
+  onClickInterested,
+  goToContent,
+  getProjectTitle,
+  goToReport,
+  getShowChart,
+  getNotModuleDataStatus,
+  getNowInfo,
+  nowModuleName,
+  chartCustomData,
+  showModuleChart,
+  chart,
+  test
+} = recommendCardCircleModel
+</script>
+
+<template>
+  <!-- 超前项目推荐&&市场分析报告 -->
+  <div id="jyChartCom">
+    <div class="advanced-pro-rec" v-show="advancedInfo.show">
+      <div class="p-lr-32">
+        <div class="c-a-r-top">
+          <div class="c-a-r-title">
+            <div class="r-title-text">{{ getNowInfo.title }}</div>
+            <div class="r-title-tip">
+                  <span
+                    v-if="advancedInfo.showContent || getNotModuleDataStatus"
+                  >{{ getNowInfo.desc }}</span
+                  >
+              <span
+                v-else
+                class="total-item"
+                v-for="(item, index) in advancedInfo.briefList"
+                :key="index"
+              >
+                    {{ item.key }}:<span class="highlight-text"
+              ><em>{{ item.value }}</em
+              >条</span
+              >
+                  </span>
+            </div>
+          </div>
+          <div class="c-a-r-option">
+            <div
+              v-if="nowModuleName === '市场分析报告'"
+              class="c-view-report c-view-common"
+              @click="goToReport"
+            >
+              查看完整报告
+            </div>
+            <div
+              class="c-view-interest c-view-common"
+              @click="
+                    onClickInterested(
+                      nowModuleName === '市场分析报告' ? 'B' : 'A'
+                    )
+                  "
+            >
+              感兴趣点我
+            </div>
+            <div class="c-up-or-down" @click="toggleAdvancedContent()">
+              <el-button type="text">
+                {{ advancedInfo.showContent ? '收起' : '展开' }}
+                <i
+                  class="el-icon--right"
+                  :class="
+                        'el-icon-arrow-' +
+                        (advancedInfo.showContent ? 'up' : 'down')
+                      "
+                ></i>
+              </el-button>
+            </div>
+          </div>
+        </div>
+      </div>
+      <el-collapse-transition>
+
+        <div v-show="advancedInfo.showContent">
+          <!-- 超前项目 -->
+          <div
+            class="project-module"
+            v-if="nowModuleName === '超前项目推荐'"
+            :class="{ 'remove-bl': !getShowChart }"
+          >
+            <div class="project-item">
+              <div class="left-tag total-color">累计发布</div>
+              <div>
+                    <span
+                      class="total-item"
+                      v-for="(item, index) in advancedInfo.briefList"
+                      :key="index"
+                    >
+                      {{ item.key }}:<span class="highlight-text"
+                    ><em>{{ item.value }}</em
+                    >条</span
+                    >
+                    </span>
+              </div>
+            </div>
+            <div class="project-item">
+              <div class="left-tag new-color">最新项目</div>
+              <div class="new-group">
+                    <span
+                      class="ellipsis new-item"
+                      @click="goToContent(item)"
+                      v-for="(item, index) in advancedInfo.projectList"
+                      :key="index"
+                      v-html="getProjectTitle(item)"
+                    ></span>
+              </div>
+            </div>
+          </div>
+          <!-- 市场分析报告 -->
+          <div class="custom-report" v-if="getShowChart">
+            <div class="c-a-r-top" v-if="nowModuleName === '超前项目推荐'">
+              <div class="c-a-r-title">
+                <div class="r-title-text">市场分析报告</div>
+                <div class="r-title-tip">
+                  量身定制个性化报告,分析市场竞争格局,为企业找准市场机会!
+                </div>
+              </div>
+              <div class="c-a-r-option">
+                <div
+                  class="c-view-report c-view-common"
+                  @click="goToReport"
+                >
+                  查看完整报告
+                </div>
+                <div
+                  class="c-view-interest c-view-common"
+                  @click="onClickInterested('B')"
+                >
+                  感兴趣点我
+                </div>
+              </div>
+            </div>
+            <div class="c-a-r-chart">
+              <div
+                class="chart-common"
+                id="customerChart"
+                v-show="showModuleChart !== 'customer_scale'"
+              >
+                <div class="chart-title">客户分布:</div>
+                <div class="c-c-content">
+                  <!-- <div id="chartTreeMap"></div> -->
+                  <MarketUserScatter
+                    min-height="min-height: 211px"
+                    top="-8px"
+                    :key="chart.treeMapKey"
+                    ref="treeMap"
+                    :chartData="chart.treeMapData"
+                  />
+                </div>
+              </div>
+              <div
+                class="chart-common"
+                id="winnerChart"
+                v-show="showModuleChart !== 'winner_time_distribution'"
+              >
+                <div class="chart-title">中标规模分布:</div>
+                <div class="c-c-content chart-line">
+                  <!-- <div id="chartLineChart"></div> -->
+                  <BuyerScaleScatter
+                    :key="chart.winnerLineKey"
+                    height="211px"
+                    :chartData="chart.winnerLineData"
+                  />
+                </div>
+              </div>
+              <div
+                class="chart-common"
+                id="buyerChart"
+                v-show="showModuleChart !== 'buyer_time_distribution'"
+              >
+                <div class="chart-title">采购规模分布:</div>
+                <div class="c-c-content chart-line">
+                  <!-- <div id="chartLineChartBuyer"></div> -->
+                  <BuyerScaleScatter
+                    :key="chart.buyLineKey"
+                    height="211px"
+                    :chartData="chart.buyLineData"
+                  />
+                </div>
+              </div>
+            </div>
+          </div>
+          <!-- <custom-report :show-title="nowModuleName === '超前项目推荐'" @onReport="goToReport" @onInterest="onClickInterested('B')" v-if="getShowChart" :chartCustomData="chartCustomData"></custom-report> -->
+        </div>
+      </el-collapse-transition>
+    </div>
+    <el-dialog
+      custom-class="advanced-dialog"
+      :visible.sync="advancedInfo.showDialog"
+    >
+      <img
+        class="advanced-dialog--head"
+        src="@/assets/images/advanced/dialog-head.png"
+        alt="剑鱼标讯"
+      />
+      <img
+        class="advanced-dialog--qrcode"
+        src="@/assets/images/advanced/dialog-qrcode.png"
+        alt="扫码联系客服"
+      />
+      <div class="advanced-dialog--info">
+        <h4>扫码联系客服</h4>
+        <h4>{{ advancedInfo.dialogContent }}</h4>
+        <p>专业招投标大数据服务平台丨国家信息中心大数据战略合作商</p>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+#jyChartCom {
+  background: #fff;
+  border-radius: 8px;
+
+  .advanced-pro-rec {
+    margin-top: 16px;
+    margin-bottom: 16px;
+
+    .custom-report {
+      padding: 0 16px 0 32px;
+    }
+
+    // 超前项目模块
+    $total-color: #2abed1;
+    $new-color: #ff9f40;
+
+    .total-item {
+      line-height: 24px;
+
+      em {
+        font-weight: 700;
+        font-size: 20px;
+      }
+
+      & + .total-item {
+        margin-left: 72px;
+      }
+    }
+
+    .project-module {
+      font-weight: 400;
+      font-size: 14px;
+      line-height: 22px;
+      color: #1d1d1d;
+      padding: 24px 32px;
+      border-bottom: 1px solid #ececec;
+
+      &.remove-bl {
+        border-bottom-color: transparent;
+      }
+
+      .left-tag {
+        flex-shrink: 0;
+        display: inline-block;
+        font-size: 14px;
+        line-height: 24px;
+        color: #ffffff;
+        padding: 0 9px;
+        border-radius: 0 12px 12px 0;
+        margin-right: 24px;
+
+        &.total-color {
+          background: $total-color;
+        }
+
+        &.new-color {
+          background: $new-color;
+        }
+      }
+
+      .project-item {
+        width: 100%;
+        display: flex;
+        flex-direction: row;
+        align-items: center;
+
+        & + .project-item {
+          margin-top: 18px;
+        }
+      }
+
+      .new-group {
+        display: flex;
+        flex-direction: row;
+        width: 100%;
+      }
+
+      .new-item {
+        cursor: pointer;
+        font-size: 16px;
+        line-height: 24px;
+        max-width: calc(50% - 72px);
+
+        & + .new-item {
+          margin-left: 36px;
+        }
+
+        &::before {
+          content: '';
+          display: inline-block;
+          vertical-align: middle;
+          width: 7px;
+          height: 7px;
+          border-radius: 50%;
+          margin-right: 8px;
+          background: $new-color;
+        }
+      }
+    }
+
+    .p-lr-32 {
+      padding: 0 16px 0 32px;
+    }
+
+    .c-a-r-top {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      height: 60px;
+      border-bottom: 1px dashed #e0e0e0;
+
+      .c-a-r-title {
+        display: flex;
+        align-items: center;
+        height: 100%;
+        line-height: 21px;
+        font-family: 'Microsoft YaHei';
+        font-style: normal;
+        font-weight: 400;
+      }
+
+      .r-title-text {
+        display: flex;
+        align-items: center;
+        width: fit-content;
+        height: 95%;
+        margin-top: 5px;
+        color: #2cb7ca;
+        font-size: 16px;
+        border-bottom: 2px solid #2cb7ca;
+      }
+
+      .r-title-tip {
+        margin-top: 5px;
+        margin-left: 32px;
+        color: #686868;
+        font-size: 14px;
+      }
+    }
+
+    .c-a-r-option {
+      display: flex;
+      align-items: center;
+    }
+
+    .c-view-common {
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      padding: 0 17px;
+      height: 30px;
+      font-family: 'Microsoft YaHei';
+      font-style: normal;
+      font-weight: 400;
+      font-size: 14px;
+      line-height: 22px;
+      border-radius: 4px;
+      cursor: pointer;
+    }
+
+    .c-view-report {
+      border: 1px solid #2abed1;
+      color: #2abed1;
+    }
+
+    .chart-common {
+      /* width: 590px; */
+      height: 280px;
+    }
+
+    .c-c-content {
+      width: 383px;
+      height: 211px;
+    }
+
+    .c-c-content.chart-line {
+      width: 558px;
+    }
+
+    .c-up-or-down {
+      margin-left: 32px;
+
+      .el-icon--right {
+        margin-left: 2px;
+      }
+
+      .el-button--text {
+        padding: 0;
+        font-weight: 400;
+        font-size: 14px;
+        line-height: 19px;
+        color: #686868;
+      }
+    }
+
+    .c-view-interest {
+      margin-left: 36px;
+      background: #2abed1;
+      color: #fff;
+      border: 1px solid #2abdd1;
+    }
+
+    .c-a-r-chart {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      margin-top: 16px;
+    }
+
+    .chart-title {
+      padding: 16px 0 12px 0;
+      font-family: 'Microsoft YaHei';
+      font-style: normal;
+      font-weight: 400;
+      font-size: 16px;
+      line-height: 24px;
+      color: #1d1d1d;
+    }
+  }
+}
+.custom-report {
+  padding: 0 32px;
+
+  .c-a-r-top {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    height: 60px;
+    border-bottom: 1px dashed #e0e0e0;
+
+    .c-a-r-title {
+      display: flex;
+      align-items: center;
+      height: 100%;
+      line-height: 21px;
+      font-family: 'Microsoft YaHei';
+      font-style: normal;
+      font-weight: 400;
+    }
+
+    .r-title-text {
+      display: flex;
+      align-items: center;
+      width: fit-content;
+      height: 95%;
+      margin-top: 5px;
+      color: #2cb7ca;
+      font-size: 16px;
+      border-bottom: 2px solid #2cb7ca;
+    }
+
+    .r-title-tip {
+      margin-top: 5px;
+      margin-left: 32px;
+      color: #686868;
+      font-size: 14px;
+    }
+  }
+
+  .c-a-r-option {
+    display: flex;
+    align-items: center;
+  }
+
+  .c-view-common {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    padding: 0 17px;
+    height: 30px;
+    font-family: 'Microsoft YaHei';
+    font-style: normal;
+    font-weight: 400;
+    font-size: 14px;
+    line-height: 22px;
+    border-radius: 4px;
+    cursor: pointer;
+  }
+
+  .c-view-report {
+    border: 1px solid #2abed1;
+    color: #2abed1;
+  }
+
+  .chart-common {
+    /* width: 590px; */
+    height: 280px;
+  }
+
+  .c-c-content {
+    width: 383px;
+    height: 211px;
+  }
+
+  .c-c-content.chart-line {
+    width: 558px;
+  }
+
+  .c-view-interest {
+    margin-left: 36px;
+    background: #2abed1;
+    color: #fff;
+    border: 1px solid #2abdd1;
+  }
+
+  .c-a-r-chart {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-top: 16px;
+  }
+
+  .chart-title {
+    padding: 16px 0 12px 0;
+    font-family: 'Microsoft YaHei';
+    font-style: normal;
+    font-weight: 400;
+    font-size: 16px;
+    line-height: 24px;
+    color: #1d1d1d;
+  }
+}
+::v-deep {
+  .advanced-dialog {
+    width: 370px;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    border-radius: 8px;
+
+    .el-dialog__body {
+      display: flex;
+      flex-direction: column;
+      align-items: center;
+    }
+
+    .el-dialog__close {
+      font-size: 20px;
+      font-weight: bold;
+      color: #2abdd1;
+      cursor: pointer;
+    }
+
+    .el-dialog__header,
+    .el-dialog__body {
+      padding: 0;
+    }
+
+    &--head {
+      margin-top: -58px;
+      max-width: 100%;
+    }
+
+    &--qrcode {
+      margin-top: 26px;
+      margin-bottom: 22px;
+      width: 154px;
+      height: 154px;
+    }
+
+    &--info {
+      padding: 24px 20px;
+      padding-top: 0;
+
+      h4 {
+        font-weight: 400;
+        font-size: 16px;
+        line-height: 24px;
+        text-align: center;
+        color: #1d1d1d;
+      }
+
+      p {
+        margin-top: 16px;
+        font-weight: 400;
+        font-size: 12px;
+        line-height: 18px;
+        text-align: center;
+        color: #999999;
+      }
+    }
+  }
+}
+</style>
+

+ 181 - 0
apps/bigmember_pc/src/views/search/bidding/components/save-filter-dialog.vue

@@ -0,0 +1,181 @@
+<script setup>
+import { computed } from 'vue'
+import { SearchBidModel } from '../model'
+const { disposeFilterActionModel } = SearchBidModel
+const { viewFilterParams: currentFilter } = disposeFilterActionModel
+
+const props = defineProps({
+  visible: {
+    type: Boolean,
+    default: false
+  }
+})
+// 关键词
+const additionalWordsCon = computed (() => {
+  let str = ''
+  if (currentFilter.value.additionalWords) {
+    if(currentFilter.value.keywords) {
+      str += ','
+    }
+    str += currentFilter.value.additionalWords.replace(/,/g, ",")  +'(' + currentFilter.value.wordsModeText + ')'
+  }
+  return str
+})
+
+// 处理筛选数据-英文逗号转空格
+function formatToSpace (val) {
+  if (!val) return
+  return val.replace(/,/g, " ")
+}
+
+const emit = defineEmits(['cancel', 'confirm'])
+function cancelHandle () {
+  emit('cancel')
+}
+function confirmHandle () {
+  emit('confirm')
+}
+</script>
+
+<template>
+  <!-- 保存筛选弹框 -->
+  <el-dialog
+    custom-class="filter-dialog save-filter-dialg"
+    title="保存筛选条件"
+    :show-close="false"
+    :close-on-click-modal="false"
+    :close-on-press-escape="false"
+    width="750" :center="true"
+    :visible.sync="visible">
+    <div class="filter-save-item">
+      <div class="save-label">关键词:</div>
+      <div class="save-value">
+        {{ currentFilter.keywords }}
+        <span v-if="additionalWordsCon">{{ additionalWordsCon }}</span>
+        <div class="search_model">
+          搜索模式:{{ currentFilter.searchModeText }}
+        </div>
+      </div>
+    </div>
+    <div class="filter-save-item">
+      <div class="save-label">筛选条件:</div>
+      <div class="save-value">
+        <div class="save-value-bg"><span>搜索范围:</span>{{ currentFilter.scopeText }}</div>
+        <div class="save-value-bg" v-if="currentFilter.industry"><span>行业:</span>{{ currentFilter.industryText }}</div>
+        <div style="display: flex;">
+          <div class="save-value-bg" v-if="currentFilter.priceText"><span>价格区间:</span>{{ currentFilter.priceText }}</div>
+          <div class="save-value-bg" v-if="currentFilter.publishTime"><span>发布时间:</span>{{ currentFilter.publishTimeText }}</div>
+          <div class="save-value-bg" v-if="currentFilter.fileExistsText != '全部'"><span>附件:</span>{{ currentFilter.fileExistsText }}</div>
+        </div>
+        <div class="save-value-bg" v-if="currentFilter.regionMapText"><span>项目地区:</span>{{ currentFilter.regionMapText }}</div>
+        <div class="save-value-bg" v-if="currentFilter.infoType"><span>信息类型:</span>{{ currentFilter.infoTypeText }}</div>
+        <div class="save-value-bg" v-if="currentFilter.buyerClass"><span>采购单位类型:</span>{{ currentFilter.buyerClassText }}</div>
+        <div style="display: flex;">
+          <div class="save-value-bg" v-if="currentFilter.buyerTel"><span>采购单位联系方式:</span>{{ currentFilter.buyerTelText }}</div>
+          <div class="save-value-bg" v-if="currentFilter.winnerTel"><span>中标单位联系方式:</span>{{ currentFilter.winnerTelText }}</div>
+          <div class="save-value-bg" v-if="currentFilter.notkey"><span>排除词:</span>{{ formatToSpace(currentFilter.notkey) }}</div>
+        </div>
+        <div class="save-value-bg" v-if="currentFilter.buyer"><span>采购单位:</span>{{ formatToSpace(currentFilter.buyer) }}</div>
+        <div class="save-value-bg" v-if="currentFilter.winner"><span>中标企业:</span>{{ formatToSpace(currentFilter.winner) }}</div>
+        <div class="save-value-bg" v-if="currentFilter.agency"><span>招标代理机构:</span>{{ formatToSpace(currentFilter.agency) }}</div>
+      </div>
+    </div>
+    <span slot="footer" class="dialog-footer">
+        <el-button type="primary" class="btn-group confirm-btn" @click="confirmHandle">确 定</el-button>
+        <el-button class="btn-group cancel-btn" @click="cancelHandle">取 消</el-button>
+      </span>
+  </el-dialog>
+</template>
+
+
+<style lang="scss" scoped>
+
+::v-deep {
+  .filter-dialog{
+    padding: 32px;
+    border-radius: 8px!important;
+  }
+  .el-dialog{
+    width: 750px !important;
+  }
+}
+
+.filter-dialog{
+  width: 750px;
+  .el-dialog__header {
+    padding: 0;
+    .el-dialog__title{
+      color: #1D1D1D;
+    }
+  }
+  &.save-filter-dialog {
+    .el-dialog__body {
+      padding: 0 0 32px!important;
+    }
+  }
+  .btn-group {
+    width: 132px;
+    height: 36px;
+    padding: 0;
+    border-radius: 6px;
+    font-size: 16px;
+  }
+  .btn-group.confirm-btn {
+    background: #2cb7ca;
+    margin-right: 52px;
+    border: 0;
+    color: #fff;
+  }
+  .btn-group.confirm-btn:hover,
+  .btn-group.confirm-btn:focus {
+    color: #fff;
+  }
+  .btn-group.cancel-btn:hover{
+    background: unset;
+    border-color: #DCDFE6;
+    color: #606266;
+  }
+  .filter-save-item{
+    display: flex;
+    margin-top: 20px;
+    line-height: 18px;
+
+    .save-label{
+      min-width: 60px;
+      text-align: right;
+      color: #636467;
+      font-size: 12px;
+    }
+
+    .save-value {
+      margin-left: 8px;
+      flex: 1;
+      color: #1D1D1D;
+      text-align: left;
+      font-size: 12px;
+    }
+
+    .save-value-bg{
+      margin-bottom: 8px;
+      margin-right: 8px;
+      padding: 6px 8px;
+      background: #F5F6F7;
+      border-radius: 4px;
+      font-size: 12px;
+      color: #1D1D1D;
+      line-height: 18px;
+
+      & > span{
+        color: #636467;
+      }
+    }
+
+    .search_model{
+      margin-top: 4px;
+      font-size: 12px;
+      line-height: 18px;
+      color: #686868;
+    }
+  }
+}
+</style>

Some files were not shown because too many files changed in this diff