lianbingjie 1 سال پیش
والد
کامیت
f346efb934
100فایلهای تغییر یافته به همراه15338 افزوده شده و 129 حذف شده
  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. 18 0
      apps/bigmember_pc/src/api/modules/crmApplication.js
  8. 11 0
      apps/bigmember_pc/src/api/modules/dataExport.js
  9. 5 0
      apps/bigmember_pc/src/api/modules/index.js
  10. 12 0
      apps/bigmember_pc/src/api/modules/nzj.js
  11. 61 0
      apps/bigmember_pc/src/api/modules/search.js
  12. BIN
      apps/bigmember_pc/src/assets/images/empty/jy-loading.gif
  13. BIN
      apps/bigmember_pc/src/assets/images/icon/select-down-icon.png
  14. BIN
      apps/bigmember_pc/src/assets/images/icon/tip2.png
  15. 316 6
      apps/bigmember_pc/src/assets/js/selector.js
  16. 85 0
      apps/bigmember_pc/src/assets/js/selector/scope.js
  17. 74 1
      apps/bigmember_pc/src/assets/style/common.scss
  18. 48 4
      apps/bigmember_pc/src/assets/style/pic-icon.scss
  19. 134 34
      apps/bigmember_pc/src/components/article-item/ArticleItem.vue
  20. 2 1
      apps/bigmember_pc/src/components/collect-info/CollectInfo.vue
  21. 312 0
      apps/bigmember_pc/src/components/filter-items/AmountRangeSelector.vue
  22. 146 0
      apps/bigmember_pc/src/components/filter-items/AttachmentSelector.vue
  23. 89 0
      apps/bigmember_pc/src/components/filter-items/BasePowerLayout.vue
  24. 71 0
      apps/bigmember_pc/src/components/filter-items/BuyerTypeSelector.vue
  25. 416 0
      apps/bigmember_pc/src/components/filter-items/CascadeContent.vue
  26. 177 0
      apps/bigmember_pc/src/components/filter-items/ChangeHandsDropdown.vue
  27. 119 0
      apps/bigmember_pc/src/components/filter-items/CheckboxGroupSelector.vue
  28. 134 0
      apps/bigmember_pc/src/components/filter-items/CommonCheckboxSelector.vue
  29. 148 0
      apps/bigmember_pc/src/components/filter-items/CommonSingleChoice.vue
  30. 152 0
      apps/bigmember_pc/src/components/filter-items/ContactSelector.vue
  31. 311 0
      apps/bigmember_pc/src/components/filter-items/EntamountRangeData.vue
  32. 338 0
      apps/bigmember_pc/src/components/filter-items/EstablishTimeSelector.vue
  33. 71 0
      apps/bigmember_pc/src/components/filter-items/IndustrySelector.vue
  34. 103 0
      apps/bigmember_pc/src/components/filter-items/InfoTypeDropdown.vue
  35. 76 0
      apps/bigmember_pc/src/components/filter-items/KeywordSelector.vue
  36. 107 0
      apps/bigmember_pc/src/components/filter-items/KeywordTagsSelector.vue
  37. 189 0
      apps/bigmember_pc/src/components/filter-items/KeywordTagsSelectorContent.vue
  38. 176 0
      apps/bigmember_pc/src/components/filter-items/Layout.vue
  39. 290 0
      apps/bigmember_pc/src/components/filter-items/OnecascadeContent.vue
  40. 800 0
      apps/bigmember_pc/src/components/filter-items/RegionCollapseSelector.vue
  41. 995 0
      apps/bigmember_pc/src/components/filter-items/RegionSelector.vue
  42. 160 0
      apps/bigmember_pc/src/components/filter-items/SearchRangeDropdown.vue
  43. 130 0
      apps/bigmember_pc/src/components/filter-items/SearchScopeSelector.vue
  44. 101 0
      apps/bigmember_pc/src/components/filter-items/SelectorWithBasePower.vue
  45. 165 0
      apps/bigmember_pc/src/components/search-input/SearchInput.vue
  46. 45 3
      apps/bigmember_pc/src/components/selector/InfoTypeSelector.vue
  47. 55 22
      apps/bigmember_pc/src/components/selector/InfoTypeSelectorContent.vue
  48. 229 0
      apps/bigmember_pc/src/components/selector/SearchTimeScopeSelector.vue
  49. 2 2
      apps/bigmember_pc/src/components/selector/SelectorCard.vue
  50. 753 0
      apps/bigmember_pc/src/components/selector/SelectorCascader.vue
  51. 11 7
      apps/bigmember_pc/src/components/selector/TagSelector.vue
  52. 24 1
      apps/bigmember_pc/src/components/selector/TimeSelector.vue
  53. 255 25
      apps/bigmember_pc/src/components/selector/TimeSelectorContent.vue
  54. 303 0
      apps/bigmember_pc/src/components/selector/basicDropdown.vue
  55. 504 0
      apps/bigmember_pc/src/components/selector/timeDropdown.vue
  56. 0 2
      apps/bigmember_pc/src/components/subscribe-manager/powerPerson.vue
  57. 1 1
      apps/bigmember_pc/src/components/toast/index.js
  58. 2 0
      apps/bigmember_pc/src/main.js
  59. 30 2
      apps/bigmember_pc/src/router/modules/order.js
  60. 42 0
      apps/bigmember_pc/src/router/modules/search.js
  61. 7 1
      apps/bigmember_pc/src/router/router-interceptors.js
  62. 2 0
      apps/bigmember_pc/src/router/router.js
  63. 3 2
      apps/bigmember_pc/src/router/routers.js
  64. 17 0
      apps/bigmember_pc/src/store/user.js
  65. 23 0
      apps/bigmember_pc/src/utils/brace/index.js
  66. 10 0
      apps/bigmember_pc/src/utils/common.js
  67. 1 0
      apps/bigmember_pc/src/utils/directive/index.js
  68. 52 0
      apps/bigmember_pc/src/utils/directive/modules/visited.js
  69. 116 0
      apps/bigmember_pc/src/utils/format/date.js
  70. 26 0
      apps/bigmember_pc/src/utils/format/info-type-transform.js
  71. 1058 0
      apps/bigmember_pc/src/utils/format/search-bid-filter.js
  72. 39 1
      apps/bigmember_pc/src/utils/globalFunctions.js
  73. 2 0
      apps/bigmember_pc/src/utils/index.js
  74. 80 0
      apps/bigmember_pc/src/utils/mixins/visited-setup.js
  75. 1 0
      apps/bigmember_pc/src/utils/prototype/index.js
  76. 4 0
      apps/bigmember_pc/src/utils/prototype/modules/visited.js
  77. 83 0
      apps/bigmember_pc/src/utils/visited/index.js
  78. 53 0
      apps/bigmember_pc/src/utils/visited/transform.js
  79. 22 0
      apps/bigmember_pc/src/utils/whichContainer.js
  80. 4 2
      apps/bigmember_pc/src/views/BidrenewalDialog/index.vue
  81. 21 0
      apps/bigmember_pc/src/views/order/components/big-member/buy-tip.vue
  82. 5 6
      apps/bigmember_pc/src/views/order/components/data-export/buy-tip.vue
  83. 1 0
      apps/bigmember_pc/src/views/order/components/resource-pack/buy-tip.vue
  84. 21 0
      apps/bigmember_pc/src/views/order/components/vipsubscribe/buy-tip.vue
  85. 2 2
      apps/bigmember_pc/src/views/project/AttendBiddingList.vue
  86. 0 0
      apps/bigmember_pc/src/views/search/Layout.vue
  87. 218 0
      apps/bigmember_pc/src/views/search/bidding/components/history-filter-dialog.vue
  88. 612 0
      apps/bigmember_pc/src/views/search/bidding/components/recommend-card.vue
  89. 181 0
      apps/bigmember_pc/src/views/search/bidding/components/save-filter-dialog.vue
  90. 232 0
      apps/bigmember_pc/src/views/search/bidding/components/search-bid-filter.vue
  91. 162 0
      apps/bigmember_pc/src/views/search/bidding/components/search-bid-header.vue
  92. 163 0
      apps/bigmember_pc/src/views/search/bidding/components/search-filter-header.vue
  93. 261 0
      apps/bigmember_pc/src/views/search/bidding/components/search-list-table.vue
  94. 17 0
      apps/bigmember_pc/src/views/search/bidding/constant/index.js
  95. 263 0
      apps/bigmember_pc/src/views/search/bidding/constant/search-filters-bi.js
  96. 456 0
      apps/bigmember_pc/src/views/search/bidding/constant/search-filters.js
  97. 686 0
      apps/bigmember_pc/src/views/search/bidding/index.vue
  98. 1495 0
      apps/bigmember_pc/src/views/search/bidding/model/base.js
  99. 9 0
      apps/bigmember_pc/src/views/search/bidding/model/index.js
  100. 99 0
      apps/bigmember_pc/src/views/search/bidding/model/modules/before-search.js

+ 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'
+  })
+}

+ 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
+  })
+}

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

@@ -29,4 +29,9 @@ export * from './crmApplication'
 export * from './pay'
 export * from './message'
 export * from './business'
+<<<<<<< HEAD
 export * from './docs'
+=======
+export * from './search'
+export * from './nzj'
+>>>>>>> main

+ 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'
+  })
+}

BIN
apps/bigmember_pc/src/assets/images/empty/jy-loading.gif


BIN
apps/bigmember_pc/src/assets/images/icon/select-down-icon.png


BIN
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
+}

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

@@ -192,6 +192,79 @@ input[type='number'] {
   }
 }
 
+<<<<<<< HEAD
 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;
+  }
+}
+>>>>>>> main

+ 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()

+ 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)
     }
   }
 }

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 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>

+ 2 - 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;

+ 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>

+ 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
 

+ 30 - 2
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',

+ 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')
   },
   // 潜在客户/对手
   {

+ 17 - 0
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,
     // 大会员权限
@@ -480,6 +492,7 @@ export default {
     bigMemberPower: (_, getters) => {
       return getters.power
     },
+<<<<<<< HEAD
     // 剑鱼文库会员信息
     docMemberInfo: (state) => state.info.docsInfo,
     // 是否是剑鱼文库会员
@@ -487,5 +500,9 @@ export default {
       const { docStatus } = getters.docMemberInfo
       return docStatus > 0
     }
+=======
+    // 是否登录
+    loginFlag: (state) => state.loginFlag,
+>>>>>>> main
   }
 }

+ 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)
       }

+ 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>
 

+ 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>

+ 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>

+ 232 - 0
apps/bigmember_pc/src/views/search/bidding/components/search-bid-filter.vue

@@ -0,0 +1,232 @@
+<script setup>
+import { computed, watch } from 'vue'
+import { SearchBidModel } from '../model/index'
+import { getPropertyFilters } from '@/api/modules/bi'
+import { getCMCustomInfo } from '@/api/modules/'
+import SearchSchemaFilter from '@/views/search/components/search-schema-filter.vue'
+import SelectorWithBasePower from '@/components/filter-items/SelectorWithBasePower.vue'
+import $bus from '@/utils/bus'
+
+const {
+  isVip,
+  isLogin,
+  isInApp,
+  isInWeb,
+  inBIPropertyIframe,
+  inResourceBIIframe,
+  activeTab,
+  guideGoWorkSpace,
+  filterState,
+  updateFilterBase,
+  doQuery,
+  showFilter,
+  goLogin,
+  disposeFilterSchema,
+  onChangeTab,
+} = SearchBidModel
+
+const  {
+  SearchBidBaseSchema,
+  SearchBidMoreSchema,
+  searchBidMoreFreeSchema,
+  searchBidMoreVipSchema,
+  doUpdateData
+} =  disposeFilterSchema()
+
+watch(activeTab, (newVal) => {
+  doUpdateData()
+})
+
+// 物业专版获取筛选条件,更新视图
+async function getBiPropertyFilters () {
+  const { error_code: code, data} = await getPropertyFilters()
+  if(code === 0) {
+    let newData = {}
+    Object.keys(data).forEach(v => {
+      newData[v] = []
+      data[v].forEach(s => {
+        let sArr = s.split('#')
+        s = {
+          label: sArr[0],
+          value: sArr[1]
+        }
+        newData[v].push(s)
+      })
+    })
+    doUpdateData(newData)
+  }
+}
+if(inBIPropertyIframe) {
+  getBiPropertyFilters()
+}
+
+// 中国移动定制搜索条件-融创
+async function getCustomInfo () {
+  const { error_code:code, data = [] } = await getCMCustomInfo()
+  if(code === 0) {
+    if (data) {
+      const expandSearchParams = {}
+      data.forEach(item => {
+        if(item.key && item.defaultVal) {
+          const par = {
+            key: item.key,
+            value: item.defaultVal
+          }
+          expandSearchParams[item.key] = item.defaultVal
+          updateFilterBase(par)
+        }
+      })
+      doUpdateData(data, 'more')
+      doQuery(expandSearchParams, 'firstPage')
+    }
+  }
+}
+if(!inBIPropertyIframe) {
+  getCustomInfo()
+}
+
+const customMoreSchema = computed(() => {
+  return {
+    vipModuleShow: true,
+    commonConf: {
+      showLabel: false,
+      styleType: 'row'
+    },
+    freeConf: {
+      showRowLabel: true,
+      rowLabelText: '更多筛选:',
+      schema: searchBidMoreFreeSchema.value
+    },
+    vipConf: {
+      schema: searchBidMoreVipSchema.value
+    }
+  }
+})
+
+function noPower() {
+  $bus.$emit('search:filter:no-power')
+}
+
+function toLogin() {
+  $bus.$emit('bidding:goLogin')
+}
+
+function doChangeFilter() {
+  doQuery()
+}
+
+</script>
+
+<template>
+  <el-collapse-transition>
+    <div class="search-bid-filter" v-show="showFilter">
+      <div class="guide-go-workspace" v-if="isInWeb">
+        最近五年数据查询以及更多筛选条件请
+        <span class="highlight-text">"</span>
+        <span class="cursor-button highlight-text" @click="guideGoWorkSpace">{{ isLogin ? '' : '登录后' }}进入工作台</span>
+        <span class="highlight-text">"</span>
+        检索
+      </div>
+      <!--  标准筛选  -->
+      <search-schema-filter
+        v-model="filterState"
+        :schema="SearchBidBaseSchema"
+        @change="doChangeFilter"
+      ></search-schema-filter>
+
+      <!--  更多筛选  -->
+      <div
+        class="more-filters-container"
+        :class="{ 'wrap-line': !isVip }"
+      >
+        <search-schema-filter
+          v-if="isVip && isInApp"
+          v-model="filterState"
+          :schema="SearchBidMoreSchema"
+          :show-label="false"
+          showRowLabel
+          style-type="row"
+          @change="doChangeFilter"
+        >
+          <span slot="row-label-text">更多筛选:</span>
+        </search-schema-filter>
+        <template v-else>
+          <SelectorWithBasePower
+            :component="SearchSchemaFilter"
+            :commonConf="customMoreSchema.commonConf"
+            :freeConf="customMoreSchema.freeConf"
+            :vipConf="customMoreSchema.vipConf"
+            v-model="filterState"
+            vipMaskShow
+            :baseMaskShow="!isLogin"
+            :vipModuleShow="isInApp && !inBIPropertyIframe"
+            @clickVipMask="noPower"
+            @clickBaseMask="toLogin"
+            @change="doChangeFilter"
+          ></SelectorWithBasePower>
+        </template>
+      </div>
+    </div>
+  </el-collapse-transition>
+</template>
+
+<style lang="scss" scoped>
+
+@media (min-width: 1456px) {
+  .in-app{
+    .search-bid-filter {
+      .wrap-line {
+        ::v-deep {
+          .vip-module {
+            margin-top: 4px;
+            margin-left: 0;
+          }
+        }
+      }
+    }
+  }
+}
+.search-bid-filter {
+  position: relative;
+  padding: 16px 32px;
+  .wrap-line {
+    ::v-deep {
+      .filter-layout .select-prefix {
+        background: transparent;
+      }
+      .vip-module {
+        margin-top: 4px;
+        margin-left: 82px;
+      }
+    }
+  }
+  .guide-go-workspace{
+    left: 345px;
+    position: absolute;
+    border-radius: 4px;
+    background: linear-gradient(270deg, rgba(42, 190, 209, 0.24) 1.5%, rgba(42, 190, 209, 0.12) 97.45%);
+    padding: 2px 8px;
+    color: #1D1D1D;
+    font-size: 16px;
+    font-style: normal;
+    font-weight: 400;
+    line-height: 22px;
+
+    .highlight-text{
+      color: #2ABED1;
+      font-weight: 700;
+      font-family: -apple-system, system-ui, BlinkMacSystemFont, Helvetica Neue, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif;
+    }
+    .cursor-button{
+      text-decoration-line: underline;
+      background: transparent;
+      padding: 0;
+      cursor: pointer;
+    }
+  }
+  .more-filters-container{
+    margin-top:6px;
+    margin-right: 38px;
+  }
+}
+</style>

+ 162 - 0
apps/bigmember_pc/src/views/search/bidding/components/search-bid-header.vue

@@ -0,0 +1,162 @@
+<script setup>
+import { getCurrentInstance, computed } from 'vue'
+import SearchHeaderCard from '@/views/search/components/search-header-card.vue'
+import KeywordTagsPc from '@/views/search/components/keyword-tags.vue'
+import CommonSingleChoice from '@/components/filter-items/CommonSingleChoice.vue'
+import { SearchBidModel } from '../model/index'
+import $bus from '@/utils/bus'
+
+const {
+  isInApp,
+  isLogin,
+  isInBI,
+  inResourceBIIframe,
+  inBIPropertyIframe,
+  isBidField,
+  cooperateCode,
+  inputKeywordsState,
+  doQuery,
+  onChangeTab,
+  searchModelOptions,
+  SearchTabsModel,
+  goWorkSpace
+} = SearchBidModel
+
+const { searchTabs } = SearchTabsModel
+
+const that = getCurrentInstance().proxy
+
+function doSearch($event) {
+  const firstSearch = !$event ? 'firstSearch' : undefined
+  return doQuery({}, firstSearch)
+}
+// 跳转信息发布
+function goToPublish () {
+  if(!isLogin.value){
+    location.href = '/swordfish/frontPage/InformationDistribution/free/index'
+    return
+  }
+  // 工作台内
+  if (isInApp.value) {
+    that.$BRACE.methods.open({
+      route: {
+        link: '/swordfish/page_web_pc/issued/info',
+        appType: 'iframe'
+      }
+    })
+  } else {
+    window.open('/swordfish/page_web_pc/issued/info')
+  }
+}
+
+function checkPower ($event) {
+  if(!isLogin.value) {
+    $bus.$emit('bidding:goLogin')
+    return false
+  } else {
+    return true
+  }
+}
+// 未登录--多个关键词切换处理
+function onSelectMoreKey () {
+  if(!isLogin.value) {
+    inputKeywordsState.value.selectMoreKey = false
+    $bus.$emit('bidding:goLogin')
+  }
+}
+
+const showNewPublish = computed(() => {
+  return !isInBI.value && !isBidField
+})
+</script>
+
+<template>
+  <div class="search-bid-header">
+    <search-header-card
+      v-model="inputKeywordsState.input"
+      :tabs="searchTabs"
+      placeholder="请输入项目名称等关键词,例如:医疗设备"
+      @change-tab="onChangeTab"
+      @search="doSearch"
+      :show-wx-qr="!cooperateCode && !inBIPropertyIframe && !isBidField"
+      :show-workspace-button="isLogin && !isInApp"
+      :showTab="!inBIPropertyIframe"
+      @goWorkSpace="goWorkSpace"
+    >
+      <div class="flex flex-(row items-center)">
+        <div class="flex flex-(row items-center)">
+          <common-single-choice
+            class="m-l-6px"
+            v-model="inputKeywordsState.searchMode"
+            placeholder="精准搜索"
+            :options="searchModelOptions"
+            @change="doSearch"
+            :beforeChange="checkPower"
+          ></common-single-choice>
+
+          <el-tooltip
+            popper-class="tooltip-help-class"
+            effect="dark"
+            placement="bottom"
+            :offset="0"
+          >
+            <i
+              class="iconfont icon-help highlight-text cursor-pointer m-l-4px"
+            ></i>
+            <template slot="content">
+              <div class="tooltip-slot-content w-360px">
+                精准搜索: 搜索结果必须完全包含完整的关键词。如搜索"医疗设备"
+                ,搜索结果一定完整包含“医疗设备”才能被搜索到,而“医疗的设备”或“设备医疗”的项目不会被搜索到。
+                <br />
+                <br />
+                模糊搜索: 系统会先自动智能分词然后再进行搜索。如搜索"医疗设备"
+                ,系统会自动分成“医疗”“设备”然后进行搜索,只要项目中出现“医疗”和“设备”都会被搜索到,前提是两个词必须一同出现在一则公告内,不分先后顺序。
+              </div>
+            </template>
+          </el-tooltip>
+        </div>
+
+        <el-checkbox
+          class="m-l-16px"
+          v-model="inputKeywordsState.selectMoreKey"
+          @input="onSelectMoreKey"
+        >
+          多个关键词
+        </el-checkbox>
+        <div class="m-l-24px" v-if="showNewPublish">
+          <el-button class="use-badge" data-badge="限免" type="primary" @click='goToPublish'>
+            {{ isLogin ? '信息发布': '免费发布信息'}}
+          </el-button>
+        </div>
+      </div>
+    </search-header-card>
+    <keyword-tags-pc
+      class="p-l-32px p-b-16px"
+      v-if="inputKeywordsState.selectMoreKey"
+      :list="inputKeywordsState.additionalWords"
+      placeholder="请输入关键词"
+      @change="doSearch"
+    >
+      <div
+        class="m-l-16px"
+        slot="radio"
+        v-show="inputKeywordsState.additionalWords.length > 0"
+      >
+        <el-radio-group
+          class="keyword-radio-group"
+          v-model="inputKeywordsState.wordsMode"
+          @input="doSearch"
+        >
+          <el-radio :label="0">包含所有关键词</el-radio>
+          <el-radio :label="1">包含任意关键词</el-radio>
+        </el-radio-group>
+      </div>
+    </keyword-tags-pc>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.search-bid-header {
+  //
+}
+</style>

+ 163 - 0
apps/bigmember_pc/src/views/search/bidding/components/search-filter-header.vue

@@ -0,0 +1,163 @@
+<script setup>
+import { ref } from 'vue'
+import SaveFilterDialog from '../components/save-filter-dialog.vue'
+import HistoryFilterDialog from '../components/history-filter-dialog.vue'
+import { SearchBidModel } from '../model/index'
+const {
+  isLogin,
+  isInApp,
+  inBIPropertyIframe,
+  disposeFilterActionModel,
+  onSaveFilter,
+  onResetFilter,
+  toggleFilter,
+  showFilter,
+  fixedTop
+} = SearchBidModel
+
+const {
+  historyFilterCount, // 已存筛选条件个数
+  saveFilterDialogVisible, // 保存筛选条件弹窗
+  historyFilterDialogVisible, //已存筛选条件弹窗
+  saveFilterConfirm, // 确认保存筛选条件
+  saveFilterCancel, // 取消保存筛选条件
+  onHasFilter, // 已存筛选条件操作
+} = disposeFilterActionModel
+
+function closeHistoryFilterDialog () {
+  historyFilterDialogVisible.value = false
+}
+</script>
+
+<template>
+  <div class='searchTender'>
+    <div class="filter-header-container" :class="{'header-fixed': fixedTop}">
+      <div class="filter-header" :class="{'fixed-top': fixedTop}">
+        <div
+          class="f-h-label"
+          @click="toggleFilter"
+        >
+          <span>筛选条件</span>
+          <i class="iconfont icon-xiala highlight-text" :class="{ 'is-reverse': showFilter}"></i>
+        </div>
+        <div class="f-h-action" v-if="isLogin && isInApp && !inBIPropertyIframe">
+          <span class="action-item reset-item" @click="onResetFilter">重置筛选</span>
+          <span class="action-item has-item" @click="onHasFilter">已存筛选 {{ historyFilterCount || ''}}</span>
+          <span class="action-item save-item" @click="onSaveFilter">保存筛选</span>
+        </div>
+      </div>
+    </div>
+    <save-filter-dialog
+      v-if='saveFilterDialogVisible'
+      :visible="saveFilterDialogVisible"
+      @cancel='saveFilterCancel'
+      @confirm='saveFilterConfirm'
+    ></save-filter-dialog>
+    <history-filter-dialog
+      v-if='historyFilterDialogVisible'
+      :visible="historyFilterDialogVisible"
+      @before-close='closeHistoryFilterDialog'
+    >
+    </history-filter-dialog>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+
+  .in-app{
+    .filter-header-container.header-fixed {
+      position: fixed;
+      top: 0;
+      left: 0;
+      width: 100%;
+      padding: 0 24px;
+      box-shadow: 0px 4px 4px 0px rgba(0, 0, 0, 0.05);
+      background: #FFFFFF;
+      z-index: 99;
+    }
+    .filter-header-container.header-fixed .fixed-top {
+      position: relative;
+      width: auto;
+      top: unset;
+      border-bottom: unset;
+    }
+  }
+  .filter-header{
+    width:100%;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    padding: 0 16px 0 22px;
+    height: 50px;
+    line-height: 50px;
+    background: #fff;
+    border-bottom: 1px solid #ECECEC;
+    border-top-left-radius: 8px;
+    border-top-right-radius: 8px;
+
+    &.fixed-top{
+      position: fixed;
+      top: 52px;
+      width: 1200px;
+      z-index: 99;
+    }
+
+
+    .f-h-label{
+      width: 100px;
+      padding-left: 10px;
+      background-repeat: no-repeat;
+      background-position: right center;
+      background-size: 16px 16px;
+      cursor: pointer;
+      & > span{
+        display: inline-block;
+        height: 100%;
+        font-size: 16px;
+        color: #2cb7ca;
+        border-bottom: 2px solid #2cb7ca;
+      }
+    }
+    .f-h-action{
+      display: flex;
+      align-items: center;
+
+      .action-item{
+        width: 90px;
+        height: 30px;
+        line-height: 30px;
+        border-radius: 4px;
+        font-size: 14px;
+        text-align: center;
+        cursor: pointer;
+      }
+      .reset-item{
+        margin-right: 16px;
+        border: 1px solid #2cb7ca;
+        color: #2cb7ca;
+        background-color: #fff;
+      }
+      .has-item{
+        margin-right: 16px;
+        border: 1px solid #E0E0E0;
+        color: #1D1D1D;
+        background-color: #fff;
+      }
+      .save-item{
+        background: #2CB7CA;
+        border: 1px solid #2cb7ca;
+        color: #fff;
+      }
+    }
+    .icon-xiala{
+      display: inline-block;
+      font-size: 16px;
+      flex-shrink: 0;
+      transform: rotate(0deg);
+      transition: transform .5s;
+      &.is-reverse{
+        transform: rotate(180deg);
+      }
+    }
+  }
+</style>

+ 261 - 0
apps/bigmember_pc/src/views/search/bidding/components/search-list-table.vue

@@ -0,0 +1,261 @@
+<script setup>
+import { ref, computed } from 'vue'
+import { SearchBidModel } from '../model'
+const { onClickDataExport } = SearchBidModel
+import {
+  dateFromNow,
+  replaceKeyword,
+  moneyUnit,
+  openLinkInWorkspace
+} from '@/utils/'
+
+const props = defineProps({
+  list: {
+    type: Array,
+    default: () => []
+  },
+  listState: {
+    type: Object,
+    default: () => ({
+      finished: false, // 是否已经搜索过
+      loading: false,
+      pageNum: 1, // 当前页
+      pageSize: 50, // 每页多少条数据
+      total: 0 // 一共多少条数据
+    })
+  },
+  matchKeys: {
+    type: Array,
+    default: () => []
+  },
+  tableFixedTop: {
+    type: Boolean,
+    default: false
+  }
+})
+
+const  showTableMore = computed(() => {
+  return props.list.length >= 20 && props.listState.total > 20
+})
+
+function calcTitle (item)  {
+    if (!item.projectName) {
+      item.projectName = item.title
+    }
+    const { projectName, matchKeys } = item
+    const highKeys = getMatchKeys(matchKeys || [])
+    const hightLightedTitle = replaceKeyword(projectName, highKeys, [
+      '<span class="highlight-text">',
+      '</span>'
+    ])
+    return `${hightLightedTitle}`
+}
+
+function calcMoney(budget) {
+  if(budget && isNaN(budget)) {
+    return budget
+  } else if (budget && !isNaN(budget) && budget !== '0') {
+    return ( budget / 10000).toFixed(2).replace(
+      '.00',
+      ''
+    )
+  } else {
+    return ''
+  }
+}
+function  getMatchKeys(matchKeys) {
+  return props.matchKeys.concat(matchKeys)
+}
+
+const emit = defineEmits(['before-close'])
+function toDetail (item) {
+  emit('to-detail', item)
+}
+</script>
+
+<template>
+  <div
+      class="info-list search-table-list"
+      element-loading-background="rgba(255,255,255, .4)"
+      element-loading-custom-class="self-export-loading"
+    >
+    <div class='fixed-table' v-if='tableFixedTop'>
+      <table class="table" >
+        <thead class="thead fixed-head" >
+        <tr>
+          <td width="49.7">序号</td>
+          <td width="326.4" class="deep-border">项目名称</td>
+          <td width="87">公告类型</td>
+          <td width="75.6" class="deep-border">预算<br />(万元)</td>
+          <td width="187">招标单位</td>
+          <td width="106.7" class="deep-border">开标日期</td>
+          <td width="180.3">中标单位</td>
+          <td width="78.7" class="deep-border">中标金额<br />(万元)</td>
+          <td width="106.8">发布日期</td>
+        </tr>
+        </thead>
+      </table>
+    </div>
+      <table class="table" v-show="list.length">
+        <thead class="thead">
+          <tr>
+            <td width="48">序号</td>
+            <td width="315" class="deep-border">项目名称</td>
+            <td width="84">公告类型</td>
+            <td width="73" class="deep-border">预算<br />(万元)</td>
+            <td width="181">招标单位</td>
+            <td width="103" class="deep-border">开标日期</td>
+            <td width="174">中标单位</td>
+            <td width="76" class="deep-border">中标金额<br />(万元)</td>
+            <td width="103">发布日期</td>
+          </tr>
+        </thead>
+        <tbody>
+          <tr
+            v-for="(item, index) in list"
+            :class="{ visited: item.visited }"
+            :key="index + '_' + item.id"
+            @click="toDetail(item)"
+            v-visited:articleContent="item.id"
+          >
+            <td width="48">{{ index + 1 }}</td>
+            <td width="315" class="tt-l" v-html="calcTitle(item, index)"></td>
+            <td width="84">{{ item.subtype ? item.subtype + '公告' : ''}}</td>
+            <td width="73" class="tt-r">{{ calcMoney(item.budget) }}</td>
+            <td width="181" class="tt-l">{{ item.buyer }}</td>
+            <td width="103">
+              {{
+                dateFromNow(
+                  item.bidOpenTime ? item.bidOpenTime * 1000 : null,
+                  'yyyy-MM-dd HH:mm'
+                )
+              }}
+            </td>
+            <td width="174" class="tt-l">{{ item.winner }}</td>
+            <td width="76" class="tt-r">{{ calcMoney(item.bidAmount) }}</td>
+            <td width="103">{{ dateFromNow(item.publishTime * 1000) }}</td>
+          </tr>
+        </tbody>
+      </table>
+      <div class="shade_table" v-if="showTableMore">
+        <div class="more" data-need-bind-phone="" @click="onClickDataExport('table')">
+          查看更多&gt;
+        </div>
+      </div>
+      <div class="shade_table_blank" v-if="list.length"></div>
+    </div>
+</template>
+
+<style lang="scss" scoped>
+/* table */
+.in-app{
+  .search-table-list{
+    .fixed-table{
+      width:100%;
+      background: #fff;
+      top:48px;
+      left:0;
+      padding: 0 24px;
+      box-sizing: border-box;
+    }
+  }
+}
+
+
+.search-table-list {
+  /*全文搜索 表格*/
+  width:100%;
+  position: relative;
+  //border-bottom: 1px solid #e0e0e0;
+  table {
+
+    width: 100%;
+    border-collapse: collapse;
+    table-layout: fixed;
+    tr {
+      td {
+        border: 1px solid #e0e0e0;
+        vertical-align: middle;
+        text-align: center;
+        line-height: 26px;
+      }
+      td.tt-l {
+        text-align: left;
+      }
+      td.tt-c {
+        text-align: center;
+      }
+      td.tt-r {
+        text-align: right;
+      }
+    }
+  }
+  .thead {
+    tr {
+      font-size: 14px;
+      color: #888;
+      background-color: #f3fbff;
+      border-top: 2px solid #2cb7ca;
+
+      td {
+        color: #888888;
+        padding: 6px 0;
+        //border-top: 3px solid #2cb7ca;
+      }
+    }
+    .deep {
+      border-top: 3px solid #2c90cb;
+    }
+  }
+  tbody {
+    tr {
+      cursor: pointer;
+      &:hover {
+        background-color: #f5f6f7;
+        box-shadow: inset 0px -1px 0px rgb(0, 0, 0, 0.05);
+        cursor: pointer;
+      }
+      &:nth-of-type(2n) {
+        background-color: #f5f5fb;
+      }
+    }
+  }
+  .shade_table {
+    width: 100%;
+    position: absolute;
+    margin-top: -110px;
+    height: 150px;
+    background: linear-gradient(
+        to bottom,
+        rgba(255, 255, 255, 0),
+        rgba(255, 255, 255, 0.8),
+        rgba(255, 255, 255, 1)
+    );
+    .more {
+      position: absolute;
+      height: 35px;
+      width: 120px;
+      left: 50%;
+      margin-left: -60px;
+      bottom: 20px;
+      color: #2cb7ca;
+      border-radius: 5px;
+      border: 1px solid #2cb7ca;
+      line-height: 35px;
+      text-align: center;
+      cursor: pointer;
+    }
+  }
+  .shade_table_blank{
+    height:50px;
+  }
+  .fixed-table{
+    position: fixed;
+    top: 100px;
+    width:1200px;
+    table{
+      margin-bottom: 0;
+    }
+  }
+}
+</style>

+ 17 - 0
apps/bigmember_pc/src/views/search/bidding/constant/index.js

@@ -0,0 +1,17 @@
+import { createBiSearchBidBaseSchema, createBiSearchBidMoreSchema } from '@/views/search/bidding/constant/search-filters-bi.js'
+import { createSearchBidBaseSchema, createSearchBidMoreSchema  } from '@/views/search/bidding/constant/search-filters.js'
+
+export function getCreateSearchSchema(conf) {
+  const { bi } = conf
+  if(bi) {
+    return {
+      createSearchBidBaseSchema: createBiSearchBidBaseSchema,
+      createSearchBidMoreSchema: createBiSearchBidMoreSchema
+    }
+  } else {
+    return {
+      createSearchBidBaseSchema,
+      createSearchBidMoreSchema
+    }
+  }
+}

+ 263 - 0
apps/bigmember_pc/src/views/search/bidding/constant/search-filters-bi.js

@@ -0,0 +1,263 @@
+import RegionCollapseSelector from '@/components/filter-items/RegionCollapseSelector'
+import CommonCheckboxSelector from '@/components/filter-items/CommonCheckboxSelector'
+import PriceSelector from '@/components/selector/PriceSelector'
+// 信息类型
+import InfoTypeDropdown from '@/components/filter-items/InfoTypeDropdown.vue'
+// 搜索范围
+import SearchRangeDropdown from '@/components/filter-items/SearchRangeDropdown.vue'
+// 发布时间
+import TimeDropdown from '@/components/selector/timeDropdown'
+// 附件
+import AttachmentSelector from '@/components/filter-items/AttachmentSelector.vue'
+// 换手率
+import ChangeHandsDropdown from '@/components/filter-items/ChangeHandsDropdown.vue'
+import SearchTimeScopeSelector from '@/components/selector/SearchTimeScopeSelector.vue'
+import ContactSelector from '@/components/filter-items/ContactSelector.vue'
+import KeywordTagsSelector from '@/components/filter-items/KeywordTagsSelector'
+
+
+function createBiSearchBidBaseSchema(propertyListData = {}) {
+  const businessList = propertyListData['业务类型'] || []
+  const priceList =  propertyListData['价格区间'] || []
+  const periodList =  propertyListData['合同周期'] || []
+  const propertyList =  propertyListData['物业业态'] || []
+  const SearchBidBaseSchema = [
+    {
+      key: 'regionMap',
+      label: '地区:',
+      defaultVal: {},
+      _name: 'regionMap',
+      _type: 'component',
+      expand: {
+        component: RegionCollapseSelector,
+        props: {
+          showSelectedList: false
+        },
+        hooks: {}
+      }
+    },
+    {
+      key: 'expireTime',
+      label: '到期时间:',
+      defaultVal: 'all',
+      _name: 'expireTime',
+      _type: 'component',
+      expand: {
+        component: SearchTimeScopeSelector,
+        props: {
+          type: 'expire',
+          showConfirmButton: true,
+          exactCanHalf: true
+        },
+        hooks: {}
+      }
+    },
+    {
+      key: 'subInformation',
+      label: '业务类型:',
+      defaultVal: [],
+      _name: 'subInformation',
+      _type: 'component',
+      expand: {
+        component: CommonCheckboxSelector,
+        props: {
+          field: 'value',
+          options: {
+            value: businessList
+          }
+        },
+        hooks: {}
+      }
+    },
+    {
+      key: 'scale',
+      label: '价格区间:',
+      defaultVal: '',
+      _name: 'scale',
+      _type: 'component',
+      expand: {
+        component: CommonCheckboxSelector,
+        props: {
+          field: 'value',
+          options: {
+            value: priceList
+          }
+        },
+        hooks: {}
+      }
+    },
+    {
+      key: 'period',
+      label: '合同周期:',
+      defaultVal: '',
+      _name: 'period',
+      _type: 'component',
+      expand: {
+        component: CommonCheckboxSelector,
+        props: {
+          single: true,
+          field: 'value',
+          options: {
+            value: periodList
+          }
+        },
+        hooks: {}
+      }
+    },
+    {
+      key: 'propertyForm',
+      label: '物业业态:',
+      defaultVal: '',
+      _name: 'propertyForm',
+      _type: 'component',
+      expand: {
+        component: CommonCheckboxSelector,
+        props: {
+          field: 'value',
+          options: {
+            value: propertyList
+          }
+        },
+        hooks: {}
+      }
+    }
+  ]
+
+  return SearchBidBaseSchema
+}
+
+function createBiSearchBidMoreSchema() {
+  const SearchBidMoreSchema = [
+    {
+      key: 'selectType',
+      label: '搜索范围',
+      defaultVal: ['title', 'content'],
+      _name: 'selectType',
+      _type: 'component',
+      expand: {
+        component: SearchRangeDropdown,
+        hooks: {}
+      }
+    },
+    {
+      key: 'subtype',
+      label: '信息类型',
+      defaultVal: '',
+      _name: 'subtype',
+      _type: 'component',
+      expand: {
+        component: InfoTypeDropdown,
+        props: {
+          notNJ: true
+        },
+        hooks: {}
+      }
+    },
+    {
+      key: 'publishTime',
+      label: '发布时间',
+      defaultVal: 'fiveyear',
+      _name: 'publishTime',
+      _type: 'component',
+      expand: {
+        component: TimeDropdown,
+        props: {
+          placeholder: '发布时间',
+          selectData: [{
+            value: 'lately-7',
+            label: '最近7天'
+          }, {
+            value: 'lately-30',
+            label: '最近30天'
+          }, {
+            value: 'thisyear',
+            label: '最近1年'
+          }, {
+            value: 'threeyear',
+            label: '最近3年'
+          }, {
+            value: 'fiveyear',
+            label: '最近5年'
+          }, {
+            value: '0',
+            label: '自定义',
+            disabled: true
+          }],
+          selectorType: 'line',
+          singleChoice: true,
+
+        },
+        hooks: {}
+      }
+    },
+    {
+      key: 'changeHand',
+      label: '换手率',
+      defaultVal: '',
+      _name: 'changeHand',
+      _type: 'component',
+      expand: {
+        component: ChangeHandsDropdown,
+        hooks: {}
+      }
+    },
+    {
+      key: 'fileExists',
+      label: '附件',
+      defaultVal: '',
+      _name: 'fileExists',
+      _type: 'component',
+      expand: {
+        component: AttachmentSelector,
+        hooks: {}
+      }
+    },
+    {
+      key: 'buyerTel',
+      label: '采购单位联系方式',
+      defaultVal: '',
+      _name: 'buyerTel',
+      _type: 'component',
+      expand: {
+        component: ContactSelector,
+        props: {
+          source: 'buyer'
+        },
+        hooks: {}
+      }
+    },
+    {
+      key: 'winnerTel',
+      label: '中标企业联系方式',
+      defaultVal: '',
+      _name: 'winnerTel',
+      _type: 'component',
+      expand: {
+        component: ContactSelector,
+        props: {
+          source: 'winner'
+        },
+        hooks: {}
+      }
+    },
+    {
+      key: 'notKey',
+      label: '排除词',
+      defaultVal: [],
+      _name: 'notKeyComponent',
+      _type: 'component',
+      expand: {
+        component: KeywordTagsSelector,
+        props: {
+          placeholder: '排除词',
+          inputPlaceholder: '请输入不希望包含的关键词',
+          maxTip: '排除词个数已达上限'
+        },
+        hooks: {}
+      }
+    }
+  ]
+  return SearchBidMoreSchema
+}
+
+export { createBiSearchBidBaseSchema, createBiSearchBidMoreSchema }

+ 456 - 0
apps/bigmember_pc/src/views/search/bidding/constant/search-filters.js

@@ -0,0 +1,456 @@
+import InfoTypeSelector from '@/components/selector/InfoTypeSelector.vue'
+import SearchScopeSelector from '@/components/filter-items/SearchScopeSelector.vue'
+import SearchTimeScopeSelector from '@/components/selector/SearchTimeScopeSelector.vue'
+import BuyerTypeSelector from '@/components/filter-items/BuyerTypeSelector.vue'
+import ContactSelector from '@/components/filter-items/ContactSelector.vue'
+import AttachmentSelector from '@/components/filter-items/AttachmentSelector.vue'
+import AmountRangeSelector from '@/components/filter-items/AmountRangeSelector.vue'
+import IndustrySelector from '@/components/filter-items/IndustrySelector.vue'
+import KeywordTagsSelector from '@/components/filter-items/KeywordTagsSelector'
+import RegionSelector from '@/components/filter-items/RegionSelector'
+import SelectorWithBasePower from '@/components/filter-items/SelectorWithBasePower.vue'
+import OnecascadeContent from '@/components/filter-items/OnecascadeContent.vue'
+import { calcSearchScope } from '@/assets/js/selector/scope.js'
+import $bus from '@/utils/bus'
+import { findIndex } from 'lodash'
+
+function noPower() {
+  $bus.$emit('search:filter:no-power')
+}
+
+function beforeChangeHandle ($event, char, isLogin) {
+  if(isLogin) {
+    return true
+  }
+  // 发布时间
+  if(char === 'publishTime') {
+    if($event.value === 'sinceLastYear') {
+      return true
+    } else {
+      $bus.$emit('bidding:goLogin')
+      return false
+    }
+  } else if(char === 'selectType') {
+    if($event.value === 'file') {
+      $bus.$emit('bidding:goLogin')
+      return false
+    } else {
+      return true
+    }
+  } else if (char === 'subtype') {
+    if($event.value) {
+      $bus.$emit('bidding:goLogin')
+    } else {
+      return true
+    }
+  }
+}
+
+
+// 更多筛选中需要vip的筛选项
+const moreFiltersNeedVipKeyList = [
+  'buyerclass',
+  'buyertel',
+  'winnertel',
+  'notkey',
+  'buyer',
+  'winner',
+  'agency',
+]
+
+const BIInfoTypeOptions = [
+  {
+    name: '招标结果',
+    value: '招标结果',
+    level: 1,
+    children: [
+      {
+        name: '中标',
+        value: '中标',
+        level: 2
+      },
+      {
+        name: '成交',
+        value: '成交',
+        level: 2
+      },
+      {
+        name: '废标',
+        value: '废标',
+        level: 2
+      },
+      {
+        name: '流标',
+        value: '流标',
+        level: 2
+      }
+    ]
+  },
+  {
+    name: '招标信用信息',
+    value: '招标信用信息',
+    level: 1,
+    children: [
+      {
+        name: '合同',
+        value: '合同',
+        level: 2
+      },
+      {
+        name: '验收',
+        value: '验收',
+        level: 2
+      },
+      {
+        name: '违规',
+        value: '违规',
+        level: 2
+      }
+    ]
+  }
+]
+
+function createSearchBidBaseSchema(conf = {}) {
+  const isLogin = conf.isLogin || false
+  const vipUser = conf.vipUser || false
+  const oldUser = conf.oldUser || false
+  const showVip = conf.showVip || false
+  const infoType = conf.infoType
+  const inInjectBI = conf.inInjectBI || false
+
+  // 发布时间
+  const publishTimeExpandFree = {
+    component: SelectorWithBasePower,
+    props: {
+      component: SearchTimeScopeSelector,
+      vipMaskShow: true,
+      vipModuleShow: showVip,
+      freeConf: {
+        exactCanHalf: true,
+        showConfirmButton: true,
+        beforeChange ($event) {
+          return beforeChangeHandle($event, 'publishTime', isLogin)
+        },
+        options: ['lately7', 'lately30', 'sinceLastYear'],
+      },
+      vipConf: {
+        exactCanHalf: true,
+        showConfirmButton: true,
+        options: ['sinceLastThreeYear', 'sinceLastFiveYear', 'exact'],
+      }
+    },
+    hooks: {
+      clickVipMask: noPower
+    }
+  }
+  const publishTimeExpandVip = {
+    component: SearchTimeScopeSelector,
+    props: {
+      exactCanHalf: true,
+      showConfirmButton: true,
+    },
+    hooks: {}
+  }
+  // 搜索范围
+  const defaultScopeOptions = calcSearchScope({ vipUser, oldUser })
+  const freeOptions = defaultScopeOptions.filter((s) => !s.power)
+  const vipOptions = defaultScopeOptions.filter((s) => s.power)
+
+  const searchScopeExpandFree = {
+    component: SelectorWithBasePower,
+    props: {
+      vipModuleShow: showVip,
+      component: SearchScopeSelector,
+      options: defaultScopeOptions,
+      freeConf: {
+        isOld: oldUser && !vipUser,
+        beforeChange($event) {
+          return beforeChangeHandle($event, 'selectType', isLogin)
+        },
+        options: freeOptions,
+        keepOne: true
+      },
+      vipConf: {
+        options: vipOptions,
+        beforeChange() {
+          noPower()
+        },
+        keepOne: true
+      }
+    },
+    hooks: {
+      noPower
+    }
+  }
+  const searchScopeExpandVip = {
+    component: SearchScopeSelector,
+    props: {
+      isOld: oldUser && !vipUser,
+      keepOne: true
+    },
+    hooks: {}
+  }
+
+  const SearchBidBaseSchema = [
+    {
+      key: 'publishTime',
+      label: '发布时间:',
+      defaultVal: 'thisyear',
+      _name: 'time',
+      _type: 'component',
+      expand: vipUser ? publishTimeExpandVip : publishTimeExpandFree
+    },
+    {
+      key: 'selectType',
+      label: '搜索范围:',
+      defaultVal: ['content', 'title'],
+      _name: 'type',
+      _type: 'component',
+      expand: vipUser ? searchScopeExpandVip : searchScopeExpandFree
+    },
+    {
+      key: 'subtype',
+      label: '信息类型:',
+      defaultVal: '',
+      _name: 'subtype',
+      _type: 'component',
+      expand: {
+        component: InfoTypeSelector,
+        props: {
+          options: inInjectBI ? BIInfoTypeOptions : [],
+          showLabel: false,
+          selectorType: 'line',
+          showDataType: infoType,
+          beforeChange ($event) {
+            return beforeChangeHandle($event, 'subtype', isLogin)
+          },
+        },
+        hooks: {}
+      }
+    }
+  ]
+
+  return SearchBidBaseSchema
+}
+
+function createSearchBidMoreSchema(filterItems, conf) {
+  const isLogin = conf.isLogin || false
+  const isVip = conf.isVip || false
+  const isBidField = conf.isBidField || false
+
+  let SearchBidMoreSchema = [
+    {
+      key: 'regionMap',
+      label: '地区',
+      defaultVal: '',
+      _name: 'type',
+      _type: 'component',
+      expand: {
+        component: RegionSelector,
+        props: {
+          vip: isVip && isLogin,
+          showCount: false
+        },
+        hooks: {
+          limit: noPower
+        }
+      }
+    },
+    {
+      key: 'industry',
+      label: '行业',
+      defaultVal: '',
+      _name: 'type',
+      _type: 'component',
+      expand: {
+        component: IndustrySelector,
+        hooks: {}
+      }
+    },
+    {
+      key: 'fileExists',
+      label: '附件',
+      defaultVal: '',
+      _name: 'type',
+      _type: 'component',
+      expand: {
+        component: AttachmentSelector,
+        hooks: {}
+      }
+    },
+    {
+      key: 'price',
+      label: '金额区间',
+      defaultVal: '',
+      _name: 'type',
+      _type: 'component',
+      expand: {
+        component: AmountRangeSelector,
+        hooks: {}
+      }
+    },
+    {
+      key: 'buyerclass',
+      label: '采购单位类型',
+      defaultVal: '',
+      _name: 'type',
+      _type: 'component',
+      expand: {
+        component: BuyerTypeSelector,
+        hooks: {}
+      }
+    },
+    {
+      key: 'buyertel',
+      label: '采购单位联系方式',
+      defaultVal: '',
+      _name: 'type',
+      _type: 'component',
+      expand: {
+        component: ContactSelector,
+        props: {
+          source: 'buyer'
+        },
+        hooks: {}
+      }
+    },
+    {
+      key: 'winnertel',
+      label: '中标企业联系方式',
+      defaultVal: '',
+      _name: 'type',
+      _type: 'component',
+      expand: {
+        component: ContactSelector,
+        props: {
+          source: 'winner'
+        },
+        hooks: {}
+      }
+    },
+    {
+      key: 'notkey',
+      label: '排除词',
+      defaultVal: '',
+      _name: 'notKeyComponent',
+      _type: 'component',
+      expand: {
+        component: KeywordTagsSelector,
+        props: {
+          placeholder: '排除词',
+          inputPlaceholder: '请输入不希望包含的关键词',
+          maxTip: '排除词个数已达上限'
+        },
+        hooks: {}
+      }
+    },
+    {
+      key: 'buyer',
+      label: '采购单位',
+      defaultVal: '',
+      _name: 'buyerComponent',
+      _type: 'component',
+      expand: {
+        component: KeywordTagsSelector,
+        props: {
+          maxListLength: 15,
+          placeholder: '采购单位',
+          inputPlaceholder: '输入采购单位名称关键词,可找到目标单位的招标项目',
+          maxTip: '采购单位个数已达上限'
+        },
+        hooks: {}
+      }
+    },
+    {
+      key: 'winner',
+      label: '中标企业',
+      defaultVal: '',
+      _name: 'winnerComponent',
+      _type: 'component',
+      expand: {
+        component: KeywordTagsSelector,
+        props: {
+          maxListLength: 15,
+          placeholder: '中标企业',
+          inputPlaceholder: '输入中标企业名称关键词,可找到目标企业的中标项目',
+          maxTip: '中标企业个数已达上限'
+        },
+        hooks: {}
+      }
+    },
+    {
+      key: 'agency',
+      label: '招标代理机构',
+      defaultVal: '',
+      _name: 'agencyComponent',
+      _type: 'component',
+      expand: {
+        component: KeywordTagsSelector,
+        props: {
+          maxListLength: 15,
+          placeholder: '招标代理机构',
+          inputPlaceholder: '输入代理机构名称关键词,可找到目标企业的代理项目',
+          maxTip: '代理机构个数已达上限'
+        },
+        hooks: {}
+      }
+    }
+  ]
+
+  // 移动端融创,动态添加筛选条件(支持多个筛选条件插入)
+  if(filterItems && Array.isArray(filterItems)) {
+    const resultFilter = []
+    filterItems.forEach(fTemp => {
+      const { key, defaultVal, label, options, type } = fTemp
+      let obj  = {}
+      if(type === 'multiple') {
+       const formatOptions = options.map(o => {
+         return {
+           ...o,
+           label: o.label,
+           value: o.key,
+         }
+       })
+        obj = {
+          key: key,
+          label: label,
+          defaultVal: defaultVal,
+          _name: 'type',
+          _type: 'component',
+          expand: {
+          component: OnecascadeContent,
+            props: {
+            options: formatOptions,
+              placeholder: label
+            },
+            hooks: {}
+          }
+        }
+      }
+      resultFilter.push(obj)
+    })
+    const index = findIndex(SearchBidMoreSchema, function (o) {
+      return o.label === '采购单位类型'
+    })
+    SearchBidMoreSchema.splice(index, 0, ...resultFilter)
+  }
+  // 医械通--不展示行业以及采购单位类型
+  if(isBidField) {
+   const newArr = SearchBidMoreSchema.filter(item => {
+      return item.key !== 'industry' && item.key !== 'buyerclass'
+    })
+    SearchBidMoreSchema = newArr
+  }
+
+  // VIP处理
+  SearchBidMoreSchema.forEach((schema) => {
+    const key = schema.key
+    if (moreFiltersNeedVipKeyList.includes(key)) {
+      schema.vipMark = 1
+    }
+  })
+
+  return SearchBidMoreSchema
+}
+
+export { createSearchBidBaseSchema, createSearchBidMoreSchema }

+ 686 - 0
apps/bigmember_pc/src/views/search/bidding/index.vue

@@ -0,0 +1,686 @@
+<script setup>
+import { ref } from 'vue'
+import SearchBidHeader from '@/views/search/bidding/components/search-bid-header.vue'
+import SearchBidFilter from '@/views/search/bidding/components/search-bid-filter.vue'
+import searchFilterHeader from '@/views/search/bidding/components/search-filter-header.vue'
+import SearchListTable from '@/views/search/bidding/components/search-list-table.vue'
+import SearchList from '@/views/search/layout/search-list.vue'
+import ArticleItem from '@/components/article-item/ArticleItem.vue'
+import Adsense from '@/views/order/components/adsense/index.vue'
+import CollectInfo from '@/components/collect-info/CollectInfo.vue'
+import CustomDialog from '@/components/dialog/Dialog.vue'
+import ExportTip from '@/views/portrayal/components/DataExportTip.vue'
+import powerPerson from '@/components/subscribe-manager/powerPerson.vue'
+import BidrenewalDialog from '@/views/BidrenewalDialog/index.vue'
+import Empty from '@/components/common/Empty.vue'
+import recommendCard from '@/views/search/bidding/components/recommend-card.vue'
+// 导入业务模型
+import { useSearchBidModel, SearchBidModel } from './model/index'
+import { getMsgDistributor } from '@/api/modules/'
+
+// 初始化模型
+useSearchBidModel()
+
+// 初始化模型 解构业务所需 model \ fn
+const {
+  isLogin,
+  isInApp,
+  inBIPropertyIframe,
+  inResourceBIIframe,
+  inInjectBI,
+  isInBI,
+  isVip,
+  isFree,
+  vt,
+  filterState,
+  inputKeywordsState,
+  listState,
+  activeItemStyleType,
+  searchListProps,
+  doQuery,
+  doListHeaderAction,
+  doChangeAllSelect,
+  doChangeSelect,
+  doChangePageNum,
+  doChangePageSize,
+  toDetail,
+  list,
+  tableList,
+  tableFixedTop,
+  tagToDetail,
+  setExport,
+  exportDialogChange,
+  showDataExportDialog,
+  onClickSingleCollect,
+  usePowerRef,
+  doSubmitDistribute,
+  onJoinBid,
+  BidrenewalDialogRef,
+  showPropertyDialog,
+  propertyIframeSrc,
+  onSingleEmploy,
+  onAddInfoOfBI,
+  onSingleAddInfo,
+  vipDialogConf,
+  closeVipDialog,
+  timeSelectorText,
+  collectElementRef,
+  onFreeTaste,
+  interceptKeywords,
+  toggleBlurModeTip,
+  doToggleSearchBlurMode
+} = SearchBidModel
+
+const {
+  show: showVipDialog,
+  text: vipDialogText,
+  type: vipDialogType
+} = vipDialogConf
+
+
+// 开通超级订阅
+function toBuySvip () {
+  window.open('/swordfish/page_big_pc/free/svip/buy?type=buy')
+}
+
+// 列表-单条-配置
+const articleRef = ref({
+  bidding: true,
+  detail: true,
+  gray: true,
+  joinBid: !isInBI.value,
+  table: false,
+  collect: !isInBI.value,
+  push: false
+})
+</script>
+
+<template>
+  <div class="search-bidding-page">
+    <div class="search-bidding-header-container b-rd-8px">
+      <search-bid-header></search-bid-header>
+    </div>
+    <div class="search-bidding-keywords-tip intercept" v-if="interceptKeywords.interceptOtherWords">
+      <img src="@/assets/images/icon/tip2.png" alt="">
+      “<span>{{interceptKeywords.interceptOtherWords}}</span>“及其后面的字词均被忽略,因为剑鱼标讯的查询限制在<span class="interceptLimit">{{interceptKeywords.interceptLimit}}</span>个汉字以内。
+    </div>
+    <div class="search-bidding-filter-container b-rd-8px">
+      <search-filter-header></search-filter-header>
+      <search-bid-filter></search-bid-filter>
+    </div>
+    <div >
+      <recommend-card></recommend-card>
+    </div>
+    <div class='search-bidding-list-container'>
+      <search-list
+        class="b-rd-8px"
+        v-bind="searchListProps"
+        @doAction="doListHeaderAction"
+        @doChangeAllSelect="doChangeAllSelect"
+        @doChangeSelect="doChangeSelect"
+        :show-pagination="activeItemStyleType !== 'T'"
+        :is-table="activeItemStyleType === 'T'"
+        :table-fixed-top='tableFixedTop'
+      >
+
+        <template #other-action-item v-if="inInjectBI">
+          <div class="all-add bi-add-button" @click="onAddInfoOfBI()">添加</div>
+        </template>
+        <!--      <template #list-before v-if="listState.pageNum === 1">-->
+        <!--        <span>如对搜索结果满意,可直接订阅及时接收项目信息。</span>-->
+        <!--      </template>-->
+        <template #item-checkbox v-if="activeItemStyleType === 'T'">
+          <span></span>
+        </template>
+        <template v-slot:table='{ list }' v-if="activeItemStyleType === 'T'">
+          <search-list-table
+            :list="tableList"
+            :list-state='listState'
+            :match-keys="inputKeywordsState.matchKeys"
+            @to-detail='toDetail'
+            :table-fixed-top='tableFixedTop'
+          ></search-list-table>
+        </template>
+        <template v-slot:item="{ item, index }" v-if="activeItemStyleType !== 'T'">
+          <div>
+            <article-item
+              class="list-item"
+              :model="activeItemStyleType"
+              :class="{ visited: item.visited || item.ca_isvisit }"
+              :match-keys="inputKeywordsState.matchKeys"
+              :article="item"
+              :index="index"
+              :tag-click-list="['area', 'subtype']"
+              @onClick="toDetail(item)"
+              @tag-click="tagToDetail(item, $event)"
+              @onCollect="onClickSingleCollect"
+              @onJoinBid="onJoinBid"
+              :config="articleRef"
+            >
+              <template #bi-slot=" { item }">
+
+              </template>
+              <template #right-handle-container>
+                <div
+                  v-if="inBIPropertyIframe || inResourceBIIframe"
+                  class="bi-employ-bid"
+                  @click.prevent.stop="onSingleEmploy(item)"
+                >
+                  <i
+                    class="iconfont "
+                    :class="item.isEmploy ? 'icon-a-Property1yishoulu':  'icon-a-Property1shoulu'"
+                  ></i>
+                  <span>{{ item.isEmploy ? '已收录' : '收录' }}</span>
+                </div>
+                <div
+                  v-if="inInjectBI"
+                  class="bi-add-button"
+                  @click.prevent.stop="onSingleAddInfo(item)"
+                >
+                  <span>{{ item.isAdd ? '已添加' : '添加' }}</span>
+                </div>
+              </template>
+            </article-item>
+          </div>
+        </template>
+        <template #empty>
+          <empty :mtb60="false"  images="jy-back.png">
+            <div class="hasNoData_tiptext">
+              <p>对不起,没有找到 <span class="highlight-text">{{ timeSelectorText }}</span> 相关匹配的信息,</p>
+              <p>修改时间范围或换个搜索词试试吧</p>
+            </div>
+          </empty>
+        </template>
+        <template #list-after v-if="listState.finished && activeItemStyleType !== 'T' && isLogin">
+          <div class="p-16px text-right over-run-tips" v-if="isFree && listState.total >= 500">
+            为您展示前500条,
+            <span class="highlight-text" @click="onFreeTaste">点击免费查看更多信息</span>
+          </div>
+          <div class="text-right over-run-tips" v-if="isVip && listState.total >= 5000">为您展示前5000条,可细化筛选条件查看更多信息</div>
+        </template>
+        <template #pagination>
+          <el-pagination
+            background
+            popper-class="pagination-custom-select"
+            layout="prev, pager, next, sizes, jumper"
+            :current-page="listState.pageNum"
+            :page-size="listState.pageSize"
+            :page-sizes="[5, 10, 50, 100]"
+            :total="listState.pageTotal"
+            :show-confirm-btn="true"
+            @current-change="doChangePageNum($event)"
+            @size-change="doChangePageSize($event)"
+          >
+          </el-pagination>
+        </template>
+      </search-list>
+      <!-- 手动切换筛选模式提示 -->
+      <div class="tip-toggle-search-mode-container"  v-show="toggleBlurModeTip.show">
+        <div>
+          如需查看更多相关信息,建议您将搜索模式切换为
+          <div class="tip-action highlight-text">
+            “模糊搜索”
+            <el-tooltip popper-class="tooltip-help-class" effect="dark" placement="bottom">
+              <i class="iconfont icon-help" style="font-size:18px;"></i>
+              <template slot="content">
+                <div class="tooltip-slot-content w-360px">
+                  精准搜索: 搜索结果必须完全包含完整的关键词。如搜索"医疗设备" ,搜索结果一定完整包含“医疗设备”才能被搜索到,而“医疗的设备”或“设备医疗”的项目不会被搜索到。
+                  <br>
+                  <br>
+                  模糊搜索: 系统会先自动智能分词然后再进行搜索。如搜索"医疗设备" ,系统会自动分成“医疗”“设备”然后进行搜索,只要项目中出现“医疗”和“设备”都会被搜索到,前提是两个词必须一同出现在一则公告内,不分先后顺序。
+                </div>
+              </template>
+            </el-tooltip>
+          </div>
+          ,按照当前条件共匹配到{{ toggleBlurModeTip.count }}条公告。
+        </div>
+        <button @click="doToggleSearchBlurMode">立即切换查看</button>
+      </div>
+
+      <div class="sub-collection tags-box" style="display: none">
+        <div class="tags-inputs">
+          <div class="tag-input">
+            <div class="tag-labels"></div>
+            <input
+              type="text"
+              class="clear-input"
+              maxlength="10"
+              oninput="this.value=this.value.replace(/\s+/g,'')"
+            />
+            <div class="tag-placeholder">新增标签回车保存</div>
+          </div>
+          <div class="add-tag-button">添加并使用</div>
+        </div>
+        <div class="tags-list clearfix"></div>
+        <div class="tags-footer">
+          <div class="tags-button button-confirm">确认添加</div>
+          <div class="tags-button button-cancel">暂不添加</div>
+        </div>
+      </div>
+    </div>
+
+    <adsense class="footer-look-container" code="jy-pcsearch-bottom"></adsense>
+
+    <!--  留资弹窗  -->
+    <collect-info ref="collectElementRef"></collect-info>
+    <!--数据导出提示框-->
+    <custom-dialog
+      @close="showDataExportDialog = false"
+      customClass="export-class-dialog"
+      width="388px"
+      :show-footer="false"
+      :show-close="true"
+      :visible="showDataExportDialog"
+    >
+      <export-tip
+        @setExport="setExport"
+        @checkBoxChange="exportDialogChange"
+      ></export-tip>
+    </custom-dialog>
+    <CustomDialog
+      title="开通超级订阅"
+      customClass="open-vip-dialog"
+      width="380px"
+      top="30vh"
+      center
+      :visible.sync="showVipDialog"
+    >
+      {{ vipDialogText}}
+      <template #footer>
+        <button class="action-button confirm" @click="toBuySvip">去开通</button>
+        <button class="action-button cancel" @click="closeVipDialog">取消</button>
+      </template>
+    </CustomDialog>
+    <!-- 分发企业选择, 只有企业展示,默认vt为企业-->
+    <power-person
+      @manualDiatribution="doSubmitDistribute"
+      vt="q"
+      :list="list"
+      ref="usePowerRef"
+    ></power-person>
+    <!-- 参标更新状态弹窗 -->
+    <bidrenewal-dialog
+      ref="BidrenewalDialogRef">
+    </bidrenewal-dialog>
+
+<!--    物业专版收录-->
+    <el-dialog
+      custom-class="property-employ-dialog"
+      :visible.sync="showPropertyDialog"
+    >
+      <iframe
+        width="600"
+        height="650"
+        :src="propertyIframeSrc"
+        frameborder="0"
+      ></iframe>
+    </el-dialog>
+  </div>
+</template>
+
+<style lang='scss'>
+.open-vip-dialog {
+  width: 380px !important;
+
+  .el-dialog__header {
+    padding: 32px 32px 20px !important;
+  }
+
+  .el-dialog__body {
+    padding: 0 32px 0 !important;
+    text-align: center !important;
+  }
+
+  .el-dialog__footer {
+    padding: 32px !important;
+  }
+
+  .el-button {
+    width: 132px;
+    font-size: 16px;
+  }
+}
+.search-bidding-page .sub-collection.tags-box {
+  display: flex;
+  flex-direction: column;
+  min-height: 340px;
+  max-height: 360px;
+  position: absolute;
+  top: 0;
+  right: 0;
+  width: 332px;
+  padding: 20px 16px;
+  background: #ffffff;
+  border: 1px solid #ececec;
+  box-sizing: border-box;
+  border-radius: 8px;
+  box-shadow: 0px 0px 28px 0px rgba(0, 0, 0, 0.08);
+  z-index: 99;
+  .tags-list .tags-item {
+    float: left;
+    min-width: 44px;
+    padding: 0 8px;
+    margin: 10px 8px 0 0;
+    height: 24px;
+    line-height: 24px;
+    border-radius: 4px;
+    border: 1px solid #ececec;
+    box-sizing: border-box;
+    color: #1d1d1d;
+    text-align: center;
+    font-size: 14px;
+    background: #f5f6f7;
+    cursor: pointer;
+  }
+
+  .tags-list .tags-item {
+    float: left;
+    min-width: 44px;
+    padding: 0 8px;
+    margin: 10px 8px 0 0;
+    height: 24px;
+    line-height: 24px;
+    border-radius: 4px;
+    border: 1px solid #ececec;
+    box-sizing: border-box;
+    color: #1d1d1d;
+    text-align: center;
+    font-size: 14px;
+    background: #f5f6f7;
+    cursor: pointer;
+  }
+
+  .tags-item.tags-active {
+    padding: 0 8px 0 24px !important;
+    background: #2cb7ca
+    url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAABYlAAAWJQFJUiTwAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAADPSURBVHgB7ZNREcIwDIYjYRImYRLmZHPAHAwHlUAdIAEJSKiESgjpEY7Qg7uFdXnKd5eXdMmfpX8BHMdxDEDEgSJQ9GANiU4UGZ9cwBISXPFNGWIAKyrxZLr+suq/xdkwV4oFlFBNR3ET4veS0zaJosGoqOtZ8EVUi4tGWbM+rklC/Ax7KH++dY18ZbmZuGi8iKbhxzdTJT5DSyo/LNXZSZxljV80A3T4aayR86vIJTzyjX8xZTATF0NIU24y5xFDSFNGU3ExxNzsmTmO4zAPYEiZdz83IV0AAAAASUVORK5CYII=)
+    no-repeat 6px center !important;
+    color: #fff !important;
+    background-size: 16px !important;
+    border: 0 !important;
+  }
+
+  .tags-item.disabled {
+    color: #8e8e8e !important;
+  }
+  .tags-inputs .tag-input::-webkit-scrollbar {
+    width: 8px;
+  }
+
+  .tags-inputs .tag-input::-webkit-scrollbar-thumb {
+    border-radius: 4px;
+    background-color: #ececec;
+  }
+
+  .tags-list::-webkit-scrollbar {
+    width: 8px;
+  }
+
+  .tags-list::-webkit-scrollbar-thumb {
+    border-radius: 4px;
+    background-color: #ececec;
+  }
+  .tag-close {
+    display: inline-block;
+    width: 16px;
+    height: 16px;
+    margin-left: 8px;
+    background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAABYlAAAWJQFJUiTwAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAEwSURBVHgB7ZWxDYMwEEXPER1NRmEFJggUiJpJQiahRhSECViBUdzQIYgdgYQIPnwXmkT+DcbY/s9n3wHg5PTPKoriejRGYB+rqrpP0xT1fR9mWSaBaO77fiuEeCZJ8gAqwGyez68dBWIxV83gbSJEboK47HXWda1DF626Ar2gTUi35lo6iqbxuwBxHEvP80JF3lEg9sxhjp5pDnoHdCSGYWjVDj4W3B4HZo4dHQpgC8E1twI4gtANrrk1AAYxP1nmJAAEArjmZIADiI5TsC5AlJRS5zWcJVIEDLd9LXIUrCNgSjVqsWIBYHnOqZhrHR6BTZGhVEwSAKXCcSHEGebfQBjvAKe8cv6iRgC1i4ZijkHotUxz0TtQlmWuFrpxKtxyHOM4Nmma5sAVJaednH5SLxmyS6JIrGjkAAAAAElFTkSuQmCC);
+    background-position: center 2px;
+    background-repeat: no-repeat;
+    background-size: contain;
+  }
+  .tags-inputs {
+    position: relative;
+    width: 100%;
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+
+    .tag-input {
+      width: 100%;
+      padding: 0;
+      min-height: 34px;
+      max-height: 74px;
+      overflow-y: scroll;
+      display: inline-block;
+      border: 1px solid #ccc;
+      border-radius: 4px;
+      background-color: #fff;
+      cursor: text;
+      text-align: left;
+
+      .tag-labels {
+        display: inline;
+        vertical-align: middle;
+
+        .tag-label {
+          display: inline-block;
+          padding: 5px 12px;
+          font-size: 14px;
+          line-height: 1.2;
+          margin: 5px;
+          cursor: pointer;
+          border: 1px solid #ececec;
+          box-sizing: border-box;
+          border-radius: 4px;
+          background: #f5f6f7;
+          color: #1d1d1d;
+        }
+      }
+
+      .clear-input {
+        display: inline-block;
+        padding: 0 10px;
+        width: 160px;
+        height: 36px;
+        line-height: 1;
+        background: #fff;
+        border-radius: 2px;
+        vertical-align: middle;
+        border: none;
+        background-color: transparent;
+        box-shadow: none;
+        box-sizing: border-box;
+        font-size: 14px;
+        color: #1d1d1d;
+      }
+      }
+  }
+
+    .tags-list {
+      margin-top: 12px;
+      overflow-y: auto;
+      flex: 1;
+    }
+
+
+    .add-tag-button {
+      margin-left: 16px;
+      color: #2cb7ca;
+      font-size: 14px;
+      line-height: 22px;
+      white-space: nowrap;
+      cursor: pointer;
+    }
+
+    .tags-footer {
+      margin-top: 20px;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+    }
+
+    .tags-button {
+      padding: 3px 17px;
+      color: #1d1d1d;
+      font-size: 14px;
+      line-height: 22px;
+      border-radius: 4px;
+      border: 1px solid #e0e0e0;
+      text-shadow: 0px 0px 28px 0px rgba(0, 0, 0, 0.08);
+      cursor: pointer;
+    }
+
+    .button-confirm {
+      margin-right: 16px;
+      color: #fff;
+      background: #2cb7ca;
+      border-color: #2cb7ca;
+    }
+
+    .tag-placeholder {
+      position: absolute;
+      top: 12px;
+      left: 16px;
+      color: #bbb;
+      font-size: 14px;
+    }
+}
+</style>
+
+<style lang="scss" scoped>
+.in-app {
+  .search-bidding-page{
+    margin-top:0;
+    width: 100%;
+    padding:24px;
+  }
+}
+.in-web{
+  .search-bidding-page{
+    margin-top: 24px;
+  }
+}
+.search-bidding-page {
+  width: 1200px;
+  margin: 0 auto;
+
+  .search-bidding-header-container {
+    background-color: #fff;
+  }
+  .search-bidding-filter-container {
+    width: 100%;
+    background-color: #fff;
+    margin: 16px 0;
+  }
+
+  .search-bidding-list-container {
+    position: relative;
+  }
+  .footer-look-container.adsense {
+    padding: 0;
+    padding-bottom: 16px;
+    ::v-deep {
+      .content {
+        border: none;
+      }
+    }
+  }
+
+  .bi-employ-bid {
+    display: inline-block;
+    cursor: pointer;
+    font-size: 14px;
+    color: #1d1d1d;
+    .iconfont {
+      margin-right: 3px;
+      font-size: 20px !important;
+      vertical-align: top;
+    }
+    .icon-a-Property1shoulu{
+      color: #afafaf;
+    }
+    .icon-a-Property1yishoulu{
+      color: #2ABED1;
+    }
+  }
+  .bi-add-button {
+    &.all-add {
+      margin-right:8px;
+    }
+    display: inline-block;
+    margin-left: 16px;
+    border: 1px solid #2cb7ca;
+    color: #2cb7ca;
+    background-color: #fff;
+    padding: 0 6px;
+    height: 22px;
+    line-height: 22px;
+    border-radius: 4px;
+    font-size: 14px;
+    text-align: center;
+    cursor: pointer;
+    box-sizing: content-box;
+    min-width: 42px;
+  }
+  ::v-deep{
+    .a-i-detail{
+      padding-left: 0;
+    }
+    .keyword-tags-container{
+      margin-top: -6px;
+      .el-tag,
+      .add-keyword-actions,
+      .keyword-radio-group
+      {
+        margin-top: 6px;
+      }
+    }
+    //.filter-layout{
+    //  position: unset!important;
+    //}
+    .hasNoData_tiptext{
+      font-size: 14px;
+      font-family: Microsoft YaHei, Microsoft YaHei-Regular;
+      text-align: center;
+      color: #999999;
+      line-height: 22px;
+    }
+  }
+  .over-run-tips {
+    color: #686868;
+    text-align: right;
+    font-size: 14px;
+    line-height: 22px;
+    padding: 0 20px 16px 0;
+    cursor: pointer;
+  }
+}
+.search-bidding-keywords-tip {
+  margin-top: 25px;
+  font-size: 14px;
+  margin-bottom: -10px;
+  img {
+    vertical-align: middle;
+    width: 15px;
+    margin-right: 5px;
+    position: relative;
+    top: -2px;
+  }
+}
+.tip-toggle-search-mode-container {
+  border-top: 1px solid #0000000D;
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  padding: 16px 56px;
+  border-bottom-left-radius: 8px;
+  border-bottom-right-radius: 8px;
+  color: #5f5e64;
+  font-size: 13px;
+  line-height: 20px;
+  background: linear-gradient(90deg, #EDFDFF 0.51%, #FFFFFF 100%);
+  .tip-action {
+    display: inline-flex;
+    align-items: center;
+  }
+  button {
+    margin-left: 12px;
+    background: #2abed1;
+    padding: 5px 12px;
+    border-radius: 4px;
+    color: #f7f9fa;
+  }
+}
+</style>

+ 1495 - 0
apps/bigmember_pc/src/views/search/bidding/model/base.js

@@ -0,0 +1,1495 @@
+import { computed, reactive, ref, toRefs, onMounted,onBeforeMount, onBeforeUnmount, getCurrentInstance } from 'vue'
+import { without, throttle } from 'lodash'
+import { useStore } from '@/store'
+import { useRoute, useRouter } from 'vue-router/composables'
+import { MessageBox } from 'element-ui'
+import  {
+  FilterHistoryAjaxModelRestore,
+  FilterHistoryAjaxModel2ViewModel,
+  getParam,
+  openLinkInWorkspace,
+  InContainer
+} from '@/utils'
+import $bus from '@/utils/bus'
+// 筛选条件动态组件方法
+import { getCreateSearchSchema } from '@/views/search/bidding/constant/index'
+// API 业务模型
+import useQuickSearchModel from '@jy/data-models/modules/quick-search/model'
+// 扩展业务模型
+import { useSearchFilterModel } from './modules/filter'
+import { useSearchInputKeywordsModel } from './modules/filter-keywords'
+import { useSearchListHeaderActionsModel } from './modules/list-header-actions'
+import { useSearchTabsModel } from './modules/tabs'
+// 保存、重置、查看筛选条件业务
+import { saveFilterActionsModel } from './modules/save-filter-actions'
+// 数据导出业务
+import { dataExportActionsModel } from './modules/data-export-actions'
+// 标讯收藏业务
+import { dataCollectActionModel } from './modules/data-collect-actions'
+// 企业分发业务
+import { dataDistributeActionsModel } from './modules/data-distribute-actions'
+// 参标业务
+import { joinBidActionsModel } from './modules/join-bid-actions'
+// 收录业务
+import { dataEmployActionsModel } from './modules/data-employ-actions'
+// 添加业务
+import { dataAddActionsModel } from './modules/data-add-actions'
+// 进行搜索前业务判断
+import { beforeSearchModel } from './modules/before-search'
+// 潜在客户引流--市场分析报告&&超前项目推荐数据请求
+import { recommendCardModel } from './modules/recommend-card'
+
+export default function () {
+  const that = getCurrentInstance().proxy
+
+  const router = useRouter()
+  // 是否是免费用户
+  const isFree = computed(() => {
+    return useStore().getters['user/isFree']
+  })
+  // 企业管理员
+  const isEntAdmin = computed(() => {
+    return useStore().getters['user/isEntAdmin']
+  })
+  // 部门管理员
+  const isDepartmentAdmin = computed(() => {
+    return useStore().getters['user/isDepartmentAdmin']
+  })
+  // 是否登录
+  const isLogin = computed(() => {
+    return useStore().getters['user/loginFlag']
+  })
+  // 用户权限
+  const userPowerInfo = computed(() => {
+    return useStore().getters['user/getInfo']
+  })
+  // 是否是vip
+  const isVip = computed (() => {
+    const { entniche, memberStatus, vipStatus } = userPowerInfo.value
+    return entniche || memberStatus > 0 || vipStatus > 0
+  })
+  // 是否是老用户
+  const isOld = computed (() => {
+    const { isOld } = userPowerInfo.value
+    return isOld
+  })
+
+
+  // 是否在工作台内
+  // 本地调试,可改为工作台内isInApp = ref(true),  isInWeb = ref(false)   提交记得改回!
+  const isInApp = ref(InContainer.inApp)
+  const isInWeb = ref(InContainer.inWeb)
+
+  // 是否是渠道商
+  const cooperateCode = ref(false)
+  // 一切都好渠道商,是否是渠道商
+  const cookieInfo = document.cookie.split('; ')
+  cooperateCode.value = cookieInfo.some(item => item.indexOf('channelCode') > -1)
+  onMounted(() => {
+    window.addEventListener('scroll', watchScroll)
+  })
+
+  // 物业专版
+  const inBIPropertyIframe = useRoute().query.property === 'BIProperty'
+  // 营销BI嵌套
+  const inResourceBIIframe = useRoute().query.resource === 'BI'
+
+  // 是否山川应用嵌入环境 添加操作按钮 (个人年终报告嵌套)
+  const inInjectBI = useRoute().query.report === 'bi' || (location.href.indexOf('/jylab/bi/index') !== -1)
+
+  // 是否是BI嵌套页面
+  const isInBI = computed(() => {
+    return inResourceBIIframe || inResourceBIIframe || inInjectBI
+  })
+
+   // 是否是领域化页面(医械通)
+  const isBidField =  location.href.includes('/jylab/medical/index')
+
+  // 缓存存储配置
+  const storageConfig = {
+    listTab: {
+      key: 'pc_search_bidding_listTabActive',
+      _storage: localStorage
+    },
+    // 筛选条件上次搜索筛选项缓存key
+    filter: {
+      key: 'pc_search_bidding_lastFilters',
+      _storage: localStorage
+    },
+    // 页面tab(筛选项的searchGroup)
+    searchGroup: {
+      key: 'pc_search_bidding_lastSearchGroup',
+      _storage: localStorage
+    }
+  }
+  // 解构基础业务
+  const APIModel = useQuickSearchModel({
+    type: 'search-bid'
+  })
+  /**
+   * 列表相关model
+   */
+  const {
+    list,
+    total,
+    loading,
+    finished,
+    selectIds,
+    listIds,
+    searchResultCount,
+    isSelectSomeCheckbox,
+    selectCheckboxCount,
+    isSelectListAllCheckbox,
+    doToggleItemSelection,
+    doToggleListSelection,
+    doClearAllSelection,
+    doQuery: doRunQuery
+  } = APIModel
+  /**
+   * 关键词搜索相关
+   */
+  const {
+    inputKeywordsState,
+    searchModelOptions,
+    getFormatAPIParams: getFormatOfInputKeywords,
+    updateInputKeywordsState,
+  } = useSearchInputKeywordsModel()
+  /**
+   * 筛选v-model数据
+   */
+  const {
+    filterState,
+    filterBase,
+    getFormatAPIParams: getFormatOfFilter,
+    updateFilterBase
+  } = useSearchFilterModel({ inBIPropertyIframe, isFree, isInApp, isBidField })
+  /**
+   * 列表头操作
+   */
+  const {
+    limitActions,
+    headerActions,
+    listItemStyleType,
+    activeItemStyleType,
+    activeHeaderActions,
+    disabledHeaderActions
+  } = useSearchListHeaderActionsModel()
+
+  $bus.$on('bidding:updateInputKeywords', function (obj) {
+    updateInputKeywordsState(obj)
+  })
+
+  /**
+   * 搜索前校验
+   */
+  const { keywords: urlKeywords } = useRoute().query
+  const {
+    getWhiteList,
+    onTheWhiteList,
+    checkAndClearTextIncludesCommonWords
+  } = beforeSearchModel({ inputKeywordsState, urlKeywords })
+
+  onBeforeMount( async() => {
+    try{
+     await getWhiteList()
+    } catch (e){
+      console.warn('e')
+    }
+  })
+
+  /**
+   * 页面tab切换Model
+   */
+  const SearchTabsModel = useSearchTabsModel({ showTabs2: !isInApp.value, inInjectBI })
+  const { activeTab, doChangeTab } = SearchTabsModel
+  // tab切换处理
+  function onChangeTab (item) {
+    filterState.value = Object.assign(filterState.value, {
+      subtype: []
+    })
+    if(isInApp.value) {
+      doChangeTab(item)
+      doQuery({}, 'tab')
+    } else {
+      if(item.link) {
+        location.replace(item.link)
+      }
+    }
+  }
+
+  /**
+   * 筛选条件动态配置处理
+   */
+  const {
+    createSearchBidBaseSchema,
+    createSearchBidMoreSchema
+  } = getCreateSearchSchema({ bi: inBIPropertyIframe })
+  // 处理当前信息类型--展示数据类型
+  const  infoTypeDataType = computed(() =>  {
+    let result = 'all'
+    switch (activeTab.value) {
+      case 0:
+        result = 'all'
+        break
+      case 1:
+        result = 'base'
+        break
+      case 2:
+        result = 'advance'
+        break
+    }
+    return result
+  })
+
+
+  // 筛选条件动态配置处理
+  function disposeFilterSchema() {
+    const conf = computed(() => {
+      return {
+        isLogin: isLogin.value,
+        vipUser: isVip.value && isInApp.value,
+        oldUser: isOld.value && isInApp.value,
+        showVip: isLogin.value && isInApp.value,
+        infoType: infoTypeDataType.value,
+        isBidField: isBidField,
+        inInjectBI: inInjectBI,
+        isVip: isVip.value
+      }
+    })
+
+    // 标准筛选
+    const SearchBidBaseSchema = ref([])
+    const SearchBidMoreSchema = ref([])
+    // 更多筛选
+    const searchBidMoreFreeSchema = ref([])
+    const searchBidMoreVipSchema = ref([])
+
+    SearchBidBaseSchema.value = createSearchBidBaseSchema(conf.value)
+    SearchBidMoreSchema.value =  createSearchBidMoreSchema(null, conf.value)
+
+    searchBidMoreFreeSchema.value = SearchBidMoreSchema.value?.filter(
+      (s) => !s.vipMark
+    )
+    searchBidMoreVipSchema.value = SearchBidMoreSchema.value?.filter(
+      (s) => s.vipMark
+    )
+
+
+    function doUpdateData(schema, type) {
+      const payload = schema || conf.value
+      if(type === 'more') {
+        SearchBidMoreSchema.value = createSearchBidMoreSchema(schema, conf.value)
+      } else {
+        SearchBidBaseSchema.value = createSearchBidBaseSchema(payload)
+      }
+    }
+
+    return {
+      SearchBidBaseSchema,
+      SearchBidMoreSchema,
+      searchBidMoreFreeSchema,
+      searchBidMoreVipSchema,
+      doUpdateData
+    }
+  }
+
+  // 列表状态
+  const listState = reactive({
+    finished,
+    loading,
+    pageNum: 1,
+    pageSize: 50,
+    total,
+    pageTotal: 0
+  })
+  // 当前展示的列表
+  const activeList = computed(() => {
+    let arr = []
+    if(list.value && list.value.length > 0) {
+      arr = list.value.map((v) => {
+        v._id = v.id
+        v.checked = selectIds.value.includes(v.id)
+        // 中标单位联系方式处理
+        if(!v.winnerTel &&  v.winnerInfo && v.winnerInfo.length > 0) {
+          v.winnerTel = v.winnerInfo[0].winnerTel || ''
+          v.winnerPerson = v.winnerInfo[0].winnerPerson || ''
+        }
+        return v
+      })
+    }
+    return arr
+  })
+  // 表格展示的数据
+  const tableList = ref([])
+
+  // 搜索关键词限制
+  const interceptKeywords = reactive({
+    interceptOtherWords: '',
+    interceptLimit: 0
+  })
+
+  // 根据权限处理列表头部的操作按钮展示
+  const limitFilterHeaderActions =  computed(() => {
+    // 数据导出-非营销都展示
+    if(!inResourceBIIframe) {
+      limitActions.value['data-export'] = true
+    }
+
+    // BI营销以及个人分析报告不展示以下操作
+    if(!inResourceBIIframe && !inInjectBI) {
+      limitActions.value['refined-list'] = true
+      limitActions.value['table'] = true
+      limitActions.value['collect-bid'] = true
+      // 登录后才展示详细列表
+      if(isLogin.value) {
+        limitActions.value['detailed-list'] = true
+      }
+    }
+
+    // 展示收录
+    if(canBatchEmploy.value && inResourceBIIframe) {
+      limitActions.value['employ-bid'] = true
+    }
+    // 展示分发
+    if((isEntAdmin.value || isDepartmentAdmin.value) && !inBIPropertyIframe && !inResourceBIIframe) {
+      limitActions.value['distribute-bid'] = true
+    }
+    return headerActions.value
+  })
+
+  // search-list 组件所需参数
+  const searchListProps = computed(() => {
+    return {
+      isSelectAllCheckbox: isSelectListAllCheckbox.value,
+      isSelectSomeCheckbox: isSelectSomeCheckbox.value,
+      selectCheckboxCount: selectCheckboxCount.value,
+      searchResultCount: searchResultCount.value,
+      headerActions: limitFilterHeaderActions.value,
+      list: activeList.value,
+      listState: listState
+    }
+  })
+
+  /**
+   * 切换列表展示风格
+   * @param type - 可选风格 ['refined-list', 'detailed-list', 'table']
+   */
+  function doChangeItemStyleType(type) {
+    const styleTypes = ['refined-list', 'detailed-list', 'table']
+    if (!styleTypes.includes(type)) {
+      return console.warn('Not find style type!')
+    }
+    listItemStyleType.value = type
+    activeHeaderActions.value = without(
+      activeHeaderActions.value,
+      ...styleTypes
+    )
+    activeHeaderActions.value.push(type)
+    // 记录列表风格切换--存到缓存
+    if(type !== 'table') {
+      const { _storage, key } = storageConfig['listTab']
+      _storage.setItem(key, type)
+    }
+  }
+
+  /**
+   * 恢复以前选过的tab
+   */
+  function restoreListTabActive() {
+    const { _storage, key } = storageConfig['listTab']
+    const storage = _storage.getItem(key)
+    if (storage) {
+      if(storage === 'detailed-list' && isVip.value && isLogin.value) {
+        doChangeItemStyleType('detailed-list')
+      } else {
+        doChangeItemStyleType('refined-list')
+      }
+    } else {
+      _storage.setItem(key, 'refined-list')
+    }
+  }
+
+
+  /**
+   * 列表顶部按钮操作事件统一入口
+   * @param item - 按钮原型
+   * @param item.key - 按钮标识
+   */
+  function doListHeaderAction(item, $event) {
+    const { key } = item
+    switch (key) {
+      case 'refined-list': {
+        doChangeItemStyleType(key)
+        break
+      }
+      case 'detailed-list': {
+        detailListClick(key)
+        break
+      }
+      case 'table': {
+        doChangeItemStyleType(key)
+        break
+      }
+      case 'data-export': {
+        onClickDataExport()
+        break
+      }
+      case 'collect-bid' : {
+        onClickDataCollect($event)
+        break
+      }
+      case 'distribute-bid': {
+        onClickDataDistribute()
+        break
+      }
+      case 'employ-bid' : {
+        onClickDataEmploy()
+        break
+      }
+    }
+  }
+  function detailListClick (key) {
+    if(isFree.value) {
+      openListVipDialog()
+      return
+    }
+    doChangeItemStyleType(key)
+  }
+  // 全选复选框事件
+  function doChangeAllSelect(type) {
+    doToggleListSelection(type)
+  }
+
+  // 单个复选框事件
+  function doChangeSelect(item) {
+    doToggleItemSelection(item.id)
+  }
+
+  // 分页事件
+  function doChangePageNum(page) {
+    listState.pageNum = page
+    doQuery({}, 'pageNumChange', page)
+  }
+
+  // 分页大小事件
+  function doChangePageSize(size) {
+    listState.pageSize = size
+    listState.pageNum = 1
+    doQuery({}, 'pageNumChange', 1)
+  }
+  // 登录
+  $bus.$on('bidding:goLogin', goLogin)
+  function goLogin () {
+    that.$showLoginDialog()
+  }
+  // 跳转详情页
+  function toDetail(item) {
+    let aHref = ".html"
+    if (inputKeywordsState.value.input){
+      aHref+= '?kds=' + encodeURIComponent(inputKeywordsState.value.input)
+    }
+    if (getParam('resource') === 'BI') {
+      if(getParam('property') === 'BIProperty') {
+        aHref += aHref.indexOf('?') > -1 ? '&resource=BI&property=BIProperty' : '?resource=BI&property=BIProperty'
+      } else {
+        aHref += aHref.indexOf('?') > -1 ? '&resource=BI' : '?resource=BI'
+      }
+    }
+
+    const id = item.id
+    try {
+      that.$visited.push({
+        type: 'articleContent',
+        id:  id
+      })
+      item.visited = true
+    } catch(e) {
+      console.log(e)
+    }
+
+
+    if(isLogin.value) {
+      const prefix = isInApp.value ? '/article/content/' : '/nologin/content/'
+      const targetLink = `${prefix}${id}${aHref}`
+
+      // 在iframe里,往工作桌面跳转。不在iframe里,正常跳转
+      openLinkInWorkspace(isInApp.value, {
+        url: targetLink,
+        newTab: true,
+      })
+    } else{//没有登录跳转新的详情
+      // 一切都好渠道合作页,未登录跳转需要弹出登录,重置到已登录后的详情页
+      if(cooperateCode.value) {
+        window.location.href = '/notin/page'
+        // openLoginDig(null, "/article/content/"+thisId+aHref)
+      } else {
+        const targetLink = `/nologin/content/${id}${aHref}`
+        window.open(targetLink)
+      }
+    }
+  }
+  // 列表tag跳转处理
+  function tagToDetail (item, label) {
+    if(label === 'subtype' && (item['subtype'] === '拟建' || item['subtype'] === '采购意向')) {
+      return
+    }
+    let link = ''
+    if(label === 'subtype') {
+      link = item['subtypeUrl']
+    } else if(label === 'area'){
+      link = item['areaUrl']
+    }
+    if(link) {
+      window.open(link)
+    }
+  }
+
+  // 获取 store getters
+  const userType = computed(() => {
+    return useStore().getters['user/userType']
+  })
+
+  /**
+   * 变更搜索模式
+   * @type {boolean}
+   */
+  // P260需求精准搜索无结果时 自动切换到模糊搜索(查询) 模糊搜索也无结果时再切回精准(不查询)
+  // 条件:1.精准搜索模式 2.返回无数据 3.有主关键词或附加词
+  let autoSwitchModel = false
+  const { goback } = useRoute().query
+  function changeSearchMode () {
+    const total = listState.total
+    const { searchMode, additionalWords,  input } = inputKeywordsState.value
+    const jzModel = searchMode === '0' && (additionalWords || input)
+    const mhModel = searchMode === '1' && (additionalWords || input)
+    if(!autoSwitchModel && jzModel && total === 0) {
+      autoSwitchModel = true
+      inputKeywordsState.value.searchMode = '1'
+      doQuery()
+    }
+    if(mhModel && autoSwitchModel) {
+      if( total > 0) {
+        if(!goback) {
+          that.$toast('精准搜索无结果,已为您自动切换到模糊搜索')
+        }
+        autoSwitchModel = false
+      } else {
+        // 模糊搜索无结果时 再切回精准
+        inputKeywordsState.value.searchMode = '0'
+        autoSwitchModel = false
+      }
+    }
+  }
+
+  /**
+   * 格式化请求参数
+   * @param [params] - 可选值,部分情况会提供,默认会和该函数返回值进行合并
+   */
+
+  function getParams(params = {}) {
+    // 合并所有模型的搜索筛选项
+    const result = Object.assign(
+      {
+        searchGroup: activeTab.value,
+        reqType: 'lastNews', // cache:空搜索缓存数据;lastNews:最新数据
+        pageNum: listState.pageNum,
+        pageSize: listState.pageSize,
+        // 该接口与用户身份有关
+        _expand: {
+          type: userType.value
+        }
+      },
+      getFormatOfInputKeywords(),
+      getFormatOfFilter(),
+      params
+    )
+    return result
+  }
+
+  function beforeSearchCheck (searchType) {
+    // 第一次搜索或者tab切换不校验白名单规则
+    if (searchType) {
+      return true
+    }
+    // 如果在反爬白名单,则空搜索刷新搜索结果(即允许空搜索)
+    // 不在,则不允许空搜索(此处空搜索指的是主搜索框是否为空)
+    if (!onTheWhiteList.value) {
+      const searchKeywords = inputKeywordsState.value.input
+      const hasOneKey = (filterState.value.buyer?.length || filterState.value.winner?.length ||  filterState.value.agency?.length) > 0
+      // 切换三种筛选类型时候判断(切换tab时不弹窗)
+      // if (!searchKeywords && !hasOneKey) {
+      //   if (from.indexOf('tab-') !== -1) {
+      //     return false
+      //   }
+      // }
+      // 关键词去两边空格后为空
+      if (!searchKeywords && inputKeywordsState.value.additionalWords.length === 0) {
+        if (!hasOneKey) {
+          that.$toast('请先输入关键词')
+          return false
+        }
+      } else {
+        // 判断关键词中是否有通用词,并清空对应通用词的项
+        const hasCommonWords = checkAndClearTextIncludesCommonWords(searchKeywords)
+        if (hasCommonWords) {
+          that.$toast('请输入项目名称等关键词')
+          return false
+        }
+      }
+    }
+    return true
+  }
+  // 搜索前 处理一些事情
+  function beforeSearch (pageNum) {
+    // 列表清空
+    list.value = []
+    listState.total = 0
+    // 列表重新获取时
+    if(!pageNum) {
+      listState.pageNum = 1
+      // 清空已选中数据
+      doClearAllSelection()
+      // 清空表格数据
+      tableList.value = []
+    }
+    // 如果列表展示状态为table,将列表展现形式换成详细列表或者精简列表
+    if(activeItemStyleType.value === 'T') {
+      restoreListTabActive()
+    }
+    // 存储筛选条件
+    if(isInApp.value && isLogin.value && listState.pageNum === 1 && !inBIPropertyIframe) {
+      saveSearchGroupToLocal()
+
+      if(inputKeywordsState.value.input) {
+        saveFilterToLocal()
+      }
+    }
+
+    //P271需求--潜客圈进引流需求
+    if(!advancedInfo.show && !getShowChart.value && inputKeywordsState.value.input) {
+      getCustomReportData({ keywords:  inputKeywordsState.value.input })
+    }
+
+  }
+  /**
+   * 统一查询入口
+   * - 拦截 doQuery 进行一些返回值处理
+   * @param [params] - 可选值,默认会和 getParams(params) 返回值进行合并
+   */
+  function doQuery(params = {}, searchType, pageNum) {
+    const bSearch = beforeSearchCheck(searchType)
+    if(!bSearch) {
+      return
+    }
+    beforeSearch (pageNum)
+    // Ajax请求
+    return doRunQuery(getParams(params)).then((res) => {
+      afterQueryAjax(res)
+    })
+  }
+  // 处理查询请求后的数据以及其他业务
+  function afterQueryAjax (res) {
+    // 变更搜索模式
+    changeSearchMode()
+
+    const { origin } = res
+
+    listState.pageTotal = origin?.count || 0
+    // 限制关键词
+    interceptKeywords.interceptOtherWords = origin?.interceptOtherWords
+    interceptKeywords.interceptLimit = origin?.interceptLimit
+
+    let matchKeys = []
+    if(inputKeywordsState.value.input) {
+      matchKeys.push(inputKeywordsState.value.input)
+    }
+    if(inputKeywordsState.value.selectMoreKey) {
+      if(inputKeywordsState.value.additionalWords?.length) {
+        matchKeys = matchKeys.concat(inputKeywordsState.value.additionalWords)
+      }
+    }
+    // 用于搜索关键词高亮
+    inputKeywordsState.value.matchKeys = res.origin?.keyWords?.split(' ') || matchKeys
+
+    //表格列表数据--只取第一页的前20条展示
+    if(listState.pageNum === 1) {
+      if(listState.total > 0) {
+        tableList.value = list.value.slice(0, 20)
+      } else {
+        tableList.value = []
+      }
+    }
+
+    // 列表无数据,禁用数据导出按钮
+    if(listState.total === 0) {
+      disabledHeaderActions.value = ['data-export']
+    } else {
+      disabledHeaderActions.value = []
+    }
+
+    if(isLogin.value) {
+      // 获取参标的数据
+      getJoinBidInfo(listIds.value)
+      // BI 是否批量收录,获取收录数据
+      if(inBIPropertyIframe || inResourceBIIframe) {
+        getEmployData(listIds.value)
+      }
+      // 个人报告嵌套BI页面
+      if(inInjectBI) {
+        getBidAddInfos()
+      }
+
+    }
+    list.value = list.value.map(item => {
+      // 是否已读字段
+      const visited = that.$visited.check({
+        type: 'articleContent',
+        id: item.id
+      })
+      that.$set(item, 'visited', visited)
+
+      // 收藏字段显示
+      that.$set(item, 'collection', item.isCollected ? 1 : 0)
+
+      // 收录字段
+      that.$set(item, 'isEmploy', false)
+
+      return item
+    })
+
+    // 列表底部-是否展示需要切换模糊搜索的tip
+    checkToggleSearchMode({
+      pageNum: 1,
+      count: origin?.total || 0,
+      blurCount: origin?.bCount || 0,
+      searchMode: Number(inputKeywordsState.value.searchMode)
+    })
+  }
+
+  // 是否展示筛选条件
+  const showFilter = ref(true)
+  // 展开收起筛选条件
+  function toggleFilter() {
+    showFilter.value = !showFilter.value
+  }
+
+  // 组合好的组件格式的筛选条件
+  function packageFilter () {
+    const originParams = Object.assign(
+      {
+        searchGroup: activeTab.value,
+      },
+      filterState.value,
+      inputKeywordsState.value
+    )
+
+    return originParams
+  }
+
+// P271需求,潜客圈进,展示超前项目or分析报告引流
+  const recommendCardCircleModel = recommendCardModel({ that })
+  const {
+    getShowChart,
+    advancedInfo,
+    getCustomReportData,
+  } = recommendCardCircleModel
+
+  /******开通超级订阅弹窗start**********/
+
+  // 开通超级订阅弹窗配置
+  const vipDialogConfig = reactive({
+    type: 'filter',
+    show: false,
+    text: '立享更多搜索权限,寻找商机更精准'
+  })
+
+  // 筛选条件打开超级订阅弹窗
+  function openFilterVipDialog () {
+    vipDialogConfig.type = 'filter'
+    vipDialogConfig.show =  true
+    vipDialogConfig.text = '立享更多搜索权限,寻找商机更精准'
+  }
+  // 关闭超级订阅弹窗
+  function closeVipDialog () {
+    vipDialogConfig.show = false
+  }
+  // 数据列表--开通超级订阅弹窗
+  function openListVipDialog () {
+    vipDialogConfig.type = 'list'
+    vipDialogConfig.show = true
+    vipDialogConfig.text = '立享列表展示更多公告关键信息,例如:采购单位、中标单位、招标代理机构等,提高公告查看效率'
+  }
+
+  const vipDialogConf = toRefs(vipDialogConfig)
+
+  // 筛选条件无权限提示开通超级订阅弹窗
+  $bus.$on('search:filter:no-power', openFilterVipDialog)
+
+  /******开通超级订阅弹窗end**********/
+
+  /** 保存、重置筛选条件部分 start ****/
+
+  const disposeFilterActionModel = saveFilterActionsModel()
+  const {
+    historyFilterDialogVisible, //已存筛选条件弹窗
+    getFilterHistoryList, // 已存筛选条件列表
+    checkFilterPass, // 检测是否可以保存筛选条件
+  } = disposeFilterActionModel
+
+  /**
+   * 保存筛选条件
+   */
+  async function onSaveFilter() {
+    const originParams = packageFilter()
+    const config = { filter: originParams }
+    await checkFilterPass(config)
+  }
+
+  /**
+   * 恢复筛选条件
+   * @param item
+   */
+  function onSelectedFilter (item) {
+    const { isPay } = item
+    // 恢复选中状态
+    if (isFree?.value && isPay) {
+      MessageBox.confirm('已存筛选条件包含仅限会员用户可用的筛选条件,如需使用请开通超级订阅', '',{
+        confirmButtonText: '开通超级订阅',
+        cancelButtonText: '使用免费条件',
+        center: true,
+        showClose: false,
+        customClass: 'filter-delete-messagebox',
+        confirmButtonClass: 'btn-group confirm-btn',
+        cancelButtonClass: 'btn-group cancel-btn',
+      })
+        .then(() => {
+          router.push({
+            path: '/common/order/create/svip'
+          })
+        })
+        .catch(() => {
+          // 之前是付费用户,现在变成普通用户了,需要重置一些vip选项!!!!
+          const params = Object.assign(item,{
+            bidField: isBidField ? 'medical' : '',
+            publishTime: 'thisyear',
+            selectType: ['title'],
+            subtype: item.subtype,
+            regionMap: {},
+            industry: item.industry,
+            fileExists: item.fileExists,
+            price: item.price,
+            buyerclass: {},
+            buyertel: '',
+            winnertel: '',
+            notkey: '',
+            buyer: '',
+            winner: '',
+            agency: ''
+          })
+          restoreFilter(params, 'saveBack')
+          historyFilterDialogVisible.value = false
+        })
+    } else {
+      restoreFilter(item, 'saveBack')
+    }
+  }
+  function restoreFilter (viewFilter, type) {
+    let resultFilter = {}
+    if(type === 'saveBack') {
+      resultFilter = FilterHistoryAjaxModelRestore.formatAll(viewFilter)
+    } else {
+      resultFilter = viewFilter
+    }
+    doChangeTab({ key: resultFilter.searchGroup })
+    filterState.value = {
+      bidField: isBidField ? 'medical' : '',
+      publishTime: resultFilter.publishTime,
+      selectType: resultFilter.selectType,
+      subtype: resultFilter.subtype,
+      regionMap: resultFilter.regionMap,
+      industry: resultFilter.industry,
+      fileExists: resultFilter.fileExists,
+      price: resultFilter.price,
+      buyerclass: resultFilter.buyerclass,
+      buyertel: resultFilter.buyertel,
+      winnertel: resultFilter.winnertel,
+      notkey: resultFilter.notkey,
+      buyer: resultFilter.buyer,
+      winner: resultFilter.winner,
+      agency: resultFilter.agency
+    }
+    inputKeywordsState.value = {
+      input: resultFilter.input,
+      // 关键词筛选模式
+      searchMode: resultFilter.searchMode,
+      matchKeys: resultFilter.matchKeys,
+      // 附加关键词筛选模式
+      wordsMode: resultFilter.wordsMode,
+      // 附件关键词组
+      additionalWords: resultFilter.additionalWords,
+      selectMoreKey: resultFilter.additionalWords?.length > 0
+    }
+    if(type === 'saveBack') {
+      historyFilterDialogVisible.value = false
+    }
+    doQuery({}, 'not-filter')
+  }
+
+  // 重置筛选条件
+  function onResetFilter () {
+    updateFilterBase()
+    doQuery({}, 'not-filter')
+  }
+ /**保存、重置、查看筛选条件end******/
+
+ /*** 筛选条件头部、列表头部滚动start *****/
+
+  // 页面滚动方法 用于悬浮吸顶,将【筛选条件】置顶
+  const fixedTop = ref(false)
+  // 表格顶部操作方法滚动吸顶
+  const tableFixedTop = ref(false)
+
+  function watchScroll () {
+    const scrollTop = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop
+    const watchFilter = document.querySelector('.searchTender').offsetTop - 25// 25为margin-top值
+    const watchTable = document.querySelector('.search-bidding-list-container').offsetTop - 15 // 15为margin-top值
+    if (scrollTop >= watchFilter) {
+      fixedTop.value = true
+      tableFixedTop.value = false
+    } else {
+      fixedTop.value = false
+    }
+    if (scrollTop >= watchTable) {
+      tableFixedTop.value = true
+      fixedTop.value = false
+    } else {
+      tableFixedTop.value = false
+    }
+  }
+
+  /*** 筛选条件头部、列表头部滚动end *****/
+
+  /*******数据导出 start **********/
+  const {
+    dataExport,
+    setExport,
+    exportDialogChange,
+    showDataExportDialog,
+    toPayDataExport
+  } = dataExportActionsModel ()
+
+  // 数据导出操作
+  function onClickDataExport (table) {
+    if(!isLogin.value) {
+      goLogin()
+      return
+    }
+
+    const originParams = packageFilter()
+    const params = {
+      listState: listState,
+      selectCheckboxCount: selectCheckboxCount.value,
+      selectIds: selectIds.value,
+      filter: originParams
+    }
+    if(table) {
+      toPayDataExport(params)
+    } else {
+      dataExport(params)
+    }
+
+  }
+
+  /*******数据导出 end ***********/
+
+
+  /********* 标讯收藏部分start ********/
+  const {
+    onCollect
+  } = dataCollectActionModel({ that })
+
+  // 批量标讯收藏
+  function onClickDataCollect (e) {
+    const config = {
+      total: listState.total,
+      isSelectSomeCheckbox: isSelectSomeCheckbox.value,
+      event: e,
+      selectIds: selectIds.value,
+    }
+    onCollect(config, 'batch')
+  }
+
+  // 单个标讯收藏
+  function onClickSingleCollect (data) {
+    const config = {
+      item: data.item,
+      total: listState.total,
+      event: data.event
+    }
+    onCollect(config, 'single')
+  }
+
+  // 挂载搜索事件,供模块js回调
+  /**
+   * params: {
+   *   type:  C存储, R取消
+   *   ids: 要操作的id数组
+   * }
+   */
+  $bus.$on('bidding:updateListCollectStatus', function (obj) {
+    const { type, ids } = obj
+    list.value = list.value.map(temp => {
+      if (type === 'R' && ids.includes(temp.id)) {
+        that.$set(temp, 'collection', 0)
+      } else if(type === 'C' && ids.includes(temp.id)){
+        that.$set(temp, 'collection', 1)
+      }
+      return temp
+    })
+  })
+  /********* 标讯收藏部分end ********/
+
+  /********* 分发部分start ********/
+  const {
+    usePowerRef,
+    openDistribute,
+    doSubmitDistribute
+  } = dataDistributeActionsModel()
+
+  function onClickDataDistribute () {
+    openDistribute(selectIds.value)
+  }
+  /*********分发部分end ********/
+
+  /********参标start *********/
+  // 参标只有莱茵有权限
+  const {
+    BidrenewalDialogRef,
+    getJoinBidInfo,
+    onJoinBid
+  } = joinBidActionsModel ()
+
+  // 窗口切换刷新参标数据
+  document.addEventListener('visibilitychange', function () {
+    if (document.visibilityState === 'visible') {
+      that.$visited.refreshVisited()
+      if(isLogin.value) {
+        getJoinBidInfo(listIds.value)
+      }
+    }
+  })
+
+  // 变更列表数据的参标状态
+  $bus.$on('bidding:updateListJoinStatus', function (obj) {
+    const {item, joinData } = obj
+    if(joinData) {
+      for (const temp of joinData) {
+        list.value = list.value.map(v => {
+          if (temp.id === v.id) {
+            that.$set(v, 'joinBid', Boolean(temp.value))
+          }
+          return v
+        })
+      }
+    } else {
+      list.value = list.value.map((temp) => {
+        if (temp.id === item?.id) {
+          that.$set(temp, 'joinBid', true)
+        }
+        return temp
+      })
+    }
+  })
+  /*******参标end ***********/
+
+  /*******收录start *********/
+  const {
+    showPropertyDialog,
+    propertyIframeSrc,
+    canBatchEmploy,
+    getEmployData,
+    onSingleEmploy,
+    onBatchEmploy
+  } = dataEmployActionsModel({ inBIPropertyIframe })
+
+  // 变更列表收录状态
+  $bus.$on('bidding:updateDateEmployStatus', function (obj) {
+    const { type, employIds } = obj
+    list.value = list.value.map(function (item) {
+      // 添加收录
+      if(type === 'add' && employIds.includes(item.id)) {
+        item.isEmploy = true
+      }
+      // 取消收录
+      if(type === 'cancel' && employIds.includes(item.id)) {
+        item.isEmploy = false
+      }
+      return item
+    })
+  })
+  // 批量收录
+  function onClickDataEmploy() {
+    onBatchEmploy(selectIds.value)
+  }
+  /*******收录end ***********/
+
+  /*****BI添加操作start*********/
+  const {
+    getBidAddInfos,
+    doAddInfoOfBI
+  } = dataAddActionsModel()
+
+  $bus.$on('bidding:updateDataAddStatus', function(params) {
+    const { type, ids } = params
+    list.value = list.value.map(function (item) {
+      // 添加收录
+      if(type === 'add' && ids.includes(item.id)) {
+        item.isAdd = true
+      }
+      // 取消收录
+      if(type === 'cancel' && ids.includes(item.id)) {
+        item.isAdd = false
+      }
+      return item
+    })
+  })
+  // 批量添加
+  function onAddInfoOfBI () {
+    doAddInfoOfBI({ ids: selectIds.value })
+  }
+  // 单个添加
+  function onSingleAddInfo (item) {
+    doAddInfoOfBI({ item })
+  }
+  /*****BI添加操作end*********/
+
+  // 处理数据列表为空时,需要展示的提示文案包含时间
+  const timeSelectorText = computed(() => {
+    const publishTime = filterState.value.publishTime
+    const split = inBIPropertyIframe ? '-' : '_'
+    const { publishTimeText } = FilterHistoryAjaxModel2ViewModel.formatTime(publishTime, split)
+    let result = ''
+    if(publishTimeText) {
+      if(publishTimeText.includes('最')) {
+        result = publishTimeText.replace('最', '')
+      } else if(publishTimeText.includes('以后')){
+        result = publishTimeText.replace('以后', '')
+      }else if(publishTimeText.includes('以前')) {
+        result = publishTimeText.replace('以前', '')
+      } else {
+        result = publishTimeText
+      }
+    } else {
+      result = inBIPropertyIframe ? '近5年' : '近一年'
+    }
+
+    return result
+  })
+
+
+  /*********页面留资相关************/
+    // 打开留资弹窗
+  const collectElementRef = ref(null)
+  // 免费用户点免费体验留资
+  function onFreeTaste () {
+    if( collectElementRef.value) {
+      collectElementRef.value.isNeedSubmit('jylab_see500_plus', () => {
+      })
+    }
+  }
+
+  /*******工作台跳转start***********/
+  function guideGoWorkSpace () {
+    if (!isLogin.value) {
+      goLogin()
+      return
+    }
+    goWorkSpace()
+  }
+  // 跳转到工作台
+  function goWorkSpace () {
+    const { key, _storage } = storageConfig.filter
+    const goHref_ = location.origin + '/jylab/supsearch/index.html'
+    // 组件筛选条件
+    const originParams = packageFilter()
+    _storage.setItem(key, JSON.stringify(originParams))
+
+    // window.location.replace('/swordfish/page_big_pc/search/bidding?goback=true')
+    window.location.replace(`/page_workDesktop/work-bench/page?link=${encodeURIComponent(goHref_ + '?goback=true')}`)
+  }
+
+  // 监听路由事件
+  function backRouteParams () {
+    const { searchGroup, subtype } = useRoute().query
+    if(searchGroup) {
+      // 进入页面,默认回显tab
+      const key = Number(searchGroup)
+      doChangeTab({ key})
+    } else if(subtype && subtype ==='采购意向' && inBIPropertyIframe) {
+      // 物业专版,采购意向回显
+      filterState.value.subtype = {采购意向: ['采购意向']}
+    }
+  }
+  /*******工作台跳转end***********/
+
+  /***页面缓存回显相关*******/
+  /**
+   * searchGroup筛选项缓存
+   */
+  function saveSearchGroupToLocal() {
+    const { key, _storage } = storageConfig.searchGroup
+    const params = {
+      searchGroup: activeTab.value
+    }
+    _storage.setItem(key, JSON.stringify(params))
+  }
+
+  /**
+   * 从缓存恢复searchGroup筛选项
+   */
+  function restoreSearchGroupFromLocal() {
+    const { key, _storage } = storageConfig.searchGroup
+    const params =_storage.getItem(key)
+    const jParams = JSON.parse(params)
+    if (jParams) {
+      doChangeTab({ key: jParams.searchGroup })
+      filterState.value.searchGroup = jParams.searchGroup
+    }
+  }
+
+  /**
+   * 从缓存中恢复搜索范围
+   */
+  function restoreSelectTypeFromLocal () {
+  }
+
+  /**
+   *  恢复和保存筛选条件到本地业务说明:
+   *  工作台内 1、所有用户都存 2、只有付费用户回显上次筛选
+   *  工作台外:所有用户不存不会回显
+   *
+   *  补充业务:
+   *  付费用户回显筛选条件逻辑:
+   *  本地缓存有,则恢复本地缓存
+   *  本地缓存无,则恢复保存的筛选条件第一条
+   *  本地缓存无,已存筛选条件无,不处理回显
+   *
+   *  搜索范围缓存单独处理
+   *  工作台内外:本地有缓存有则回显,无则默认选中标题
+   */
+
+  /**
+   * 保存筛选条件到本地
+   * @param replace
+   */
+  function saveFilterToLocal(replace = {}) {
+    const { key, _storage } = storageConfig.filter
+    const originParams = packageFilter()
+    let params = originParams
+    if (replace && Object.keys(replace).length) {
+      params = Object.assign(params, replace)
+    }
+    _storage.setItem(key, JSON.stringify(params))
+  }
+
+  /**
+   * 从本地缓存恢复筛选条件
+   *
+   */
+  function restoreFilterFromLocal () {
+    const { key, _storage } = storageConfig.filter
+    const params =_storage.getItem(key)
+    if (params) {
+      restoreFilter(JSON.parse(params), 'localBack')
+    }
+  }
+
+  // 恢复筛选条件
+  // 恢复优先级、外边跳转进工作台的最高、其次是本地缓存、最后是已存筛选条件第一条
+  function getLastFilter () {
+    const { goback, searchGroup, subtype } = useRoute().query
+    if(isLogin.value) {
+      const { key, _storage } = storageConfig.filter
+      const params =_storage.getItem(key)
+      const jParams = params ? JSON.parse(params) : null
+      // 工作台内
+      if(isInApp.value) {
+        // 免费用户恢复tab,搜索范围默认标题和正文
+        if(isFree.value) {
+          restoreSearchGroupFromLocal()
+          const defaultSelectType = ['title', 'content']
+          filterState.value.selectType = defaultSelectType
+        }
+
+        // vip 或者 桌面跳转回显筛选条件
+        if(goback || (isVip.value && jParams)) {
+          restoreFilterFromLocal()
+        }
+
+        // 获取已存筛选条件的列表
+        getFilterHistoryList(null,function (arr) {
+          if(isVip.value) {
+            // 付费用户,优先恢复本地缓存,本地无缓存,则恢复已存筛选第一条数据
+            if(!jParams && Array.isArray(arr) && arr.length > 0){
+              restoreFilter(arr[0], 'saveBack')
+            }
+          }
+        })
+      }
+    }
+  }
+  /**
+   * 检查是否需要切换模糊搜索、是否展示提示
+   * 1. 精准搜索无数据 (自动切换模糊搜索)
+   * 2. 精准搜索有数据,< 50,提示手动切换搜索模式
+   */
+
+   const toggleSearchBlurData = reactive( {
+    show: false,
+    count: ''
+  })
+  function checkToggleSearchMode(params) {
+    const searchMode = params.searchMode
+    const pageNum = params.pageNum
+    const count = params.count || 0
+    const blurCount = params.blurCount || 0
+
+    if (pageNum === 1) {
+      // 重置变量
+      toggleSearchBlurData.show = false
+      toggleSearchBlurData.count = ''
+
+      if (searchMode === 0) {
+        const canShowToggleBlurModeTip = count >= 1 && count < 50 && blurCount > count
+
+        if (canShowToggleBlurModeTip) {
+          toggleSearchBlurData.show = true
+          toggleSearchBlurData.count = blurCount
+        }
+      }
+    }
+  }
+
+  // 切换到模糊搜索
+  function doToggleSearchBlurMode() {
+    inputKeywordsState.value.searchMode = '1'
+    setTimeout(function() {
+      scrollToTop(572)
+    }, 50)
+    doQuery()
+  }
+  // 滚动到某个位置
+  function scrollToTop(scrollTop) {
+    document.scrollingElement.scrollTop = scrollTop || 0
+  }
+  // 切换模糊搜索
+  const toggleBlurModeTip = computed( () => {
+    const isBlurMode = Number(inputKeywordsState.value.searchMode) === 1
+    let canShow = isBlurMode ? false : toggleSearchBlurData.show
+    if(listState.loading) {
+      canShow = false
+    }
+    const result = {
+      show: canShow,
+      count: toggleSearchBlurData.count
+    }
+    return result
+  })
+  /**********切换模糊搜索end*********/
+
+  // 页面初始化
+  function initPage () {
+    const { searchGroup } = useRoute().query
+    // 路由所带参数回显
+    backRouteParams()
+    // 恢复以前选过的tab
+    if(!searchGroup) {
+      restoreListTabActive()
+    }
+  }
+  initPage()
+
+  // // 恢复上次筛选条件
+  onMounted(() => {
+    if(!inBIPropertyIframe) {
+      getLastFilter()
+    }
+    //  P271潜客圈进--客户引流请求
+    if(inputKeywordsState.value.input) {
+      getCustomReportData({ keywords: inputKeywordsState.value.input})
+    }
+  })
+
+
+  return {
+    isLogin,
+    isInApp,
+    isInWeb,
+    inResourceBIIframe,
+    inBIPropertyIframe,
+    inInjectBI,
+    isInBI,
+    isFree,
+    isVip,
+    isBidField,
+    goLogin,
+    guideGoWorkSpace,
+    cooperateCode,
+    list,
+    tableList,
+    searchModelOptions,
+    searchListProps,
+    SearchTabsModel,
+    inputKeywordsState,
+    filterState,
+    updateFilterBase,
+    listState,
+    interceptKeywords,
+    activeItemStyleType,
+    disposeFilterSchema,
+    doQuery,
+    doListHeaderAction,
+    doChangeAllSelect,
+    doChangeSelect,
+    doChangePageNum,
+    doChangePageSize,
+    onChangeTab,
+    activeTab,
+    vipDialogConf,
+    closeVipDialog,
+    onSaveFilter, // 以下存筛选条件相关
+    disposeFilterActionModel,
+    historyFilterDialogVisible,
+    onSelectedFilter,
+    onResetFilter,
+    toggleFilter,
+    showFilter,
+    fixedTop,
+    tableFixedTop,
+    toDetail,
+    tagToDetail,
+    onClickDataExport, // 以下为导出相关
+    setExport,
+    exportDialogChange,
+    showDataExportDialog,
+    onClickSingleCollect, // 收藏
+    usePowerRef, // 以下分发相关
+    doSubmitDistribute,
+    onJoinBid,//以下 参标
+    BidrenewalDialogRef,
+    showPropertyDialog,// 以下收录
+    propertyIframeSrc,
+    onSingleEmploy,
+    onAddInfoOfBI,
+    onSingleAddInfo,
+    goWorkSpace,
+    timeSelectorText,
+    collectElementRef,
+    onFreeTaste,
+    getLastFilter,
+    toggleBlurModeTip,
+    doToggleSearchBlurMode,
+    recommendCardCircleModel,
+    getWhiteList
+  }
+}

+ 9 - 0
apps/bigmember_pc/src/views/search/bidding/model/index.js

@@ -0,0 +1,9 @@
+import useModel from './base'
+
+let SearchBidModel = {}
+function useSearchBidModel() {
+  SearchBidModel = useModel()
+  return SearchBidModel
+}
+
+export { useSearchBidModel, SearchBidModel }

+ 99 - 0
apps/bigmember_pc/src/views/search/bidding/model/modules/before-search.js

@@ -0,0 +1,99 @@
+// 获取是否允许空搜索白名单
+import { ref } from 'vue'
+import { trim } from 'lodash'
+// 获取反爬虫白名单,用于搜索前处理
+import { getInAntiSpiderWhiteList } from '@/api/modules/'
+import { showToast } from '@/components/toast'
+import $bus from '@/utils/bus'
+
+
+export function beforeSearchModel (paramsObj = { }) {
+
+  const { inputKeywordsState, urlKeywords } = paramsObj
+  // 是否是白名单用户
+  let onTheWhiteList = ref(false)
+  // 关键词搜索的校验规则
+  const commonSearchWordsRegExp = ref([])
+
+  async function getWhiteList () {
+    const keywords = urlKeywords || inputKeywordsState.value.input
+
+    const { error_code: code, data } = await getInAntiSpiderWhiteList()
+    if(code === 0 && data) {
+      onTheWhiteList.value = data.onTheWhitelist
+
+      if (Array.isArray(data.filterReg)) {
+        const newExp = []
+        data.filterReg.forEach(function(item) {
+          newExp.push(new RegExp(item))
+        })
+        commonSearchWordsRegExp.value = newExp
+      }
+      if (keywords && !onTheWhiteList.value) {
+        const timer = setTimeout(function() {
+          clearTimeout(timer)
+          const hasCommonWords = checkAndClearTextIncludesCommonWords(keywords)
+          if (hasCommonWords) {
+            showToast('请输入项目名称等关键词')
+          }
+        })
+      }
+    }
+  }
+
+  // 判断文字中是否有通用词(返回true表示text是通用词)
+  // (关键词不为空时,判断是否包含通用词)
+  function checkTextIncludesCommonWords (text) {
+    if (!text) {
+      return false
+    }
+    const passArr = []
+    text = trim(text)
+    if (Array.isArray(commonSearchWordsRegExp.value)) {
+      commonSearchWordsRegExp.value.forEach((reg) => {
+        const newText = text.replace(reg, '')
+        passArr.push(!!newText)
+      })
+    }
+
+    const hasNoPass = passArr.indexOf(false) !== -1
+    return hasNoPass
+  }
+   // text: 去空格后的主关键词
+  function checkAndClearTextIncludesCommonWords (text) {
+    let mainKeysHasCommonWords = false
+    let additionalWordsHasCommonWords = false
+    // 检查主关键词
+    if (checkTextIncludesCommonWords(text)) {
+      mainKeysHasCommonWords = true
+      $bus.$emit('bidding:updateInputKeywords', { input: ''})
+    }
+    // 检查附加词
+    const additionalWords = inputKeywordsState.value.additionalWords
+    const replacedAdditionalWords = []
+    if (Array.isArray(additionalWords)) {
+      for(let i = 0; i < additionalWords.length; i++) {
+        const item = additionalWords[i]
+        if (checkTextIncludesCommonWords(item)) {
+          // ...
+          additionalWordsHasCommonWords = true
+        } else {
+          replacedAdditionalWords.push(item)
+        }
+      }
+      if (additionalWordsHasCommonWords) {
+        $bus.$emit('bidding:updateInputKeywords', { additionalWords: replacedAdditionalWords})
+      }
+    }
+    return mainKeysHasCommonWords || additionalWordsHasCommonWords
+  }
+
+  return {
+    getWhiteList,
+    onTheWhiteList,
+    checkAndClearTextIncludesCommonWords
+  }
+
+}
+
+

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است